Функции с переменным количеством параметров

Захотелось мне изучить функции с переменным количеством аргументов. В качестве практики решил сделать свой вариант функции printf. Код:

#include <iostream>
#include <cstring>

using namespace std;

void _printf ( const char *format, ... )
{
    register int i;
    /*ptr - указатель, который будет хранить в себе адрес последнего байта
    аргумента, и с помощью него, можно будет получать доступ к следующим аргументам.
    Берём последний байт строки format*/
    char *ptr = const_cast<char*>( &format[strlen(format)] );
    for ( i = 0; format[i]; i++ )
    {
        if ( format[i] == '%' )
        {
            i++;
            switch ( format[i] )
            {
                case 'i':
                    {
                        /*Создаём указатель на тип int,
                        инкремируем указатель ptr, тем самым получаем
                        адресс первого байта следующего аргумента
                        этот адресс присваеваем созданному укзателю...*/
                        int *p = (int*)(++ptr);
                        /*в результате, если использовать операцию разыменовования
                        то, на экран выведется число.*/
                        cout << *p;
                        /*Присваеваем указателю ptr адресс последнего байта, только что
                        выведенного на экран числа.*/
                        ptr = (char*)p;
                        ptr += sizeof(int)-1;
                    };break;
                case 'd':
                    {
                        //теже действия, что и с int, только для double
                        double *p = (double*)(++ptr);
                        cout << *p;
                        ptr = (char*)p;
                        ptr += sizeof(double) - 1;
                    };break;
                case 'c':
                    {
                        //тут всё просто, так как char занимает 1 байт
                        ptr++;
                        cout << *ptr;
                    };break;
                case 'p':
                    {
                        //тут я вообще не знаю что делать :(
                    };break;
                default:
                    cout << format[i];
                    break;
            }
        }
        else
            cout << format[i];
    }
}

int main()
{
    int i;
    _printf( "int: %i \ndouble: %d \nchar: %c \npointer: %p", 5, 123.45, 'r', &i );
    return 0;
}

Думаю алгоритм понятен. Но, естественно, если я пишу сюда, то код не работает. В чём проблема?
P.S.: stdarg не предлагать, зарежу :)

Во-первых, ты зря отказываешься от stdarg. Функции с переменным числом параметров — штука очень зависимая от архитектуры процессора и компилятора.

Во-вторых, ты не там ищешь необязательные аргументы. Искать надо в стеке, а не за концом форматной строки. Обычно (зависит от компилятора!) перед вызовом функции фактические параметры заносятся в стек, а потом вызывается функция. Например, вот такая программа:

#include <iostream>

using namespace std;

void _printf ( const char *format, ... )
{
    long long *ptr = (long long *)( &format );
    for (int i = 0; i < 10; i++) {
        cout << *ptr++ << endl;
    }
}

int main()
{
    int i;
    _printf( "int: %i \ndouble: %d \nchar: %c \npointer: %p", 5, 6, 100, 200 );
    return 0;
}

у меня выдаёт:

4710408
5
6
100
200
1
6848416
4199349
0
46

Здесь первое число — адрес форматной строки. Далее 4 числа — необязательные аргументы функции (см. вызов функции в main()). Далее — мусор (для нас).

Компилятор: TDM-GCC 4.8.1 64-bit Debug
Win7

Вот как у меня выводится ваша программа:

альтернативный текст

Компилятор gcc, 32bit, linux ubuntu

Искать надо в стеке, а не за концом форматной строки

А разве форматная строка также не передаётся через стек?

А, понял. Передаётся же не вся строка, а только адрес. Но ведь механизм передачи фактических параметров везде одинаков, а значит, беря адрес format, мы получаем адрес аргументов в стеке, тогда почему у меня такой вывод?

Если пользоваться stdarg( видимо придётся резать себя:) ) и вот таким кодом:

void _printf ( const char *format, ... )
{
    va_list arglist;
    va_start( arglist, format );
    register int i;
    for ( i = 0; format[i]; i++ )
    {
        if ( format[i] == '%' )
        {
            i++;
            switch ( format[i] )
            {
                case 'i':
                    {
                        int i = va_arg( arglist, int );
                        cout << i;
                    };break;
                case 'd':
                    {
                        double d = va_arg( arglist, double );
                        cout << d;
                    };break;
                case 'c':
                    {
                        char c = va_arg( arglist, int );
                        cout << c;
                    };break;
                case 'p':
                    {
                        void *p = va_arg( arglist, void* );
                        cout << p;
                    };break;
                default:
                    cout << format[i];
                    break;
            }
        }
        else
            cout << format[i];
    }
}

вывод верен.
Вот содержимое файла stdarg.h, я не могу понять, как он вообще что-то делает :) :

/* Copyright (C) 1989, 1997, 1998, 1999, 2000, 2009 Free Software Foundation, Inc.

This file is part of GCC.

GCC is free software; you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation; either version 3, or (at your option)
any later version.

GCC is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
GNU General Public License for more details.

Under Section 7 of GPL version 3, you are granted additional
permissions described in the GCC Runtime Library Exception, version
3.1, as published by the Free Software Foundation.

You should have received a copy of the GNU General Public License and
a copy of the GCC Runtime Library Exception along with this program;
see the files COPYING3 and COPYING.RUNTIME respectively.  If not, see
<http://www.gnu.org/licenses/>.  */

/*
 * ISO C Standard:  7.15  Variable arguments  <stdarg.h>
 */

#ifndef _STDARG_H
#ifndef _ANSI_STDARG_H_
#ifndef __need___va_list
#define _STDARG_H
#define _ANSI_STDARG_H_
#endif /* not __need___va_list */
#undef __need___va_list

/* Define __gnuc_va_list.  */

#ifndef __GNUC_VA_LIST
#define __GNUC_VA_LIST
typedef __builtin_va_list __gnuc_va_list;
#endif

/* Define the standard macros for the user,
   if this invocation was from the user program.  */
#ifdef _STDARG_H

#define va_start(v,l)   __builtin_va_start(v,l)
#define va_end(v)   __builtin_va_end(v)
#define va_arg(v,l) __builtin_va_arg(v,l)
#if !defined(__STRICT_ANSI__) || __STDC_VERSION__ + 0 >= 199900L || defined(__GXX_EXPERIMENTAL_CXX0X__)
#define va_copy(d,s)    __builtin_va_copy(d,s)
#endif
#define __va_copy(d,s)  __builtin_va_copy(d,s)

/* Define va_list, if desired, from __gnuc_va_list. */
/* We deliberately do not define va_list when called from
   stdio.h, because ANSI C says that stdio.h is not supposed to define
   va_list.  stdio.h needs to have access to that data type, 
   but must not use that name.  It should use the name __gnuc_va_list,
   which is safe because it is reserved for the implementation.  */

#ifdef _HIDDEN_VA_LIST  /* On OSF1, this means varargs.h is "half-loaded".  */
#undef _VA_LIST
#endif

#ifdef _BSD_VA_LIST
#undef _BSD_VA_LIST
#endif

#if defined(__svr4__) || (defined(_SCO_DS) && !defined(__VA_LIST))
/* SVR4.2 uses _VA_LIST for an internal alias for va_list,
   so we must avoid testing it and setting it here.
   SVR4 uses _VA_LIST as a flag in stdarg.h, but we should
   have no conflict with that.  */
#ifndef _VA_LIST_
#define _VA_LIST_
#ifdef __i860__
#ifndef _VA_LIST
#define _VA_LIST va_list
#endif
#endif /* __i860__ */
typedef __gnuc_va_list va_list;
#ifdef _SCO_DS
#define __VA_LIST
#endif
#endif /* _VA_LIST_ */
#else /* not __svr4__ || _SCO_DS */

/* The macro _VA_LIST_ is the same thing used by this file in Ultrix.
   But on BSD NET2 we must not test or define or undef it.
   (Note that the comments in NET 2's ansi.h
   are incorrect for _VA_LIST_--see stdio.h!)  */
#if !defined (_VA_LIST_) || defined (__BSD_NET2__) || defined (____386BSD____) || defined (__bsdi__) || defined (__sequent__) || defined (__FreeBSD__) || defined(WINNT)
/* The macro _VA_LIST_DEFINED is used in Windows NT 3.5  */
#ifndef _VA_LIST_DEFINED
/* The macro _VA_LIST is used in SCO Unix 3.2.  */
#ifndef _VA_LIST
/* The macro _VA_LIST_T_H is used in the Bull dpx2  */
#ifndef _VA_LIST_T_H
/* The macro __va_list__ is used by BeOS.  */
#ifndef __va_list__
typedef __gnuc_va_list va_list;
#endif /* not __va_list__ */
#endif /* not _VA_LIST_T_H */
#endif /* not _VA_LIST */
#endif /* not _VA_LIST_DEFINED */
#if !(defined (__BSD_NET2__) || defined (____386BSD____) || defined (__bsdi__) || defined (__sequent__) || defined (__FreeBSD__))
#define _VA_LIST_
#endif
#ifndef _VA_LIST
#define _VA_LIST
#endif
#ifndef _VA_LIST_DEFINED
#define _VA_LIST_DEFINED
#endif
#ifndef _VA_LIST_T_H
#define _VA_LIST_T_H
#endif
#ifndef __va_list__
#define __va_list__
#endif

#endif /* not _VA_LIST_, except on certain systems */

#endif /* not __svr4__ */

#endif /* _STDARG_H */

#endif /* not _ANSI_STDARG_H_ */
#endif /* not _STDARG_H */

Я не зря написал, что функции с переменным количеством параметров — штука сильно системозависимая, и специально указал свой компилятор и ось. Возможно для твоей системы тип long long надо заменить на long или даже на int. У меня параметры в стеке получаются выровнены по границе 8 байт. У тебя, судя по выводу, — по границе 4 байт.

Но ведь механизм передачи фактических параметров везде одинаков

Не факт. На 100% на это нельзя закладываться. Зависит от компилятора и архитектуры компьютера.

Если пользоваться stdarg( видимо придётся резать себя:) ) и вот таким кодом:

Там в конце вроде ещё должно быть va_end()?

stdarg.h, я не могу понять, как он вообще что-то делает :) :

Собственно говоря, он объявляет тип va_list и определяет макросы va_...():

#define va_start(v,l)   __builtin_va_start(v,l)
#define va_end(v)   __builtin_va_end(v)
#define va_arg(v,l) __builtin_va_arg(v,l)

А вот что делают функции __builtin_va_...(), сказать сложно — у меня нет исходников стандартных библиотек.

заменить на long или даже на int

да, это помогло

У меня параметры в стеке получаются выровнены по границе 8 байт. У тебя, судя по выводу, — по границе 4 байт.

Я так понял, чтобы сделать программу без использования va_...()достаточно вместо long long указать тип, равный размеру машинного слова( только для систем windows и linux ).
Но как тогда получать объекты, размер которых больше размера машинного слова?

А вот что делают функции __builtin_va_...()

А откуда берутся функции __builtin_va_...() их ведь нет ни в этом файле, ни в другом?

Я так понял, чтобы сделать программу без использования va_...()достаточно вместо long long указать тип, равный размеру машинного слова

Но как тогда получать объекты, размер которых больше размера машинного слова?

Всё очень зависит от компилятора. См. документацию на конкретный компилятор.

Конечно, иметь понятие о всей этой внутренней кухне необходимо, но применять подобные трюки на практике сугубо не рекомендуется, поскольку они порождают непереносимый код.

Функции __builtin_va_...() берутся из Run-Time Library (RTL).

Спасибо, Череп, всё как всегда доступно и качественно.

void _pitntf(string message)
{
    cout << message.c_str();
}

superUSer, зачем такая функция нужна? cout и так, вроде работает с типом string.

Ради интереса попытался прогнать неопределённое количество параметров в attiny2313, не используя stdarg( если он тут вообще есть :) ) Результат положительный, устройство работает как надо.
Думаю после этого можно утверждать, что параметры везде передаются одинаково, по крайней мере языком C.

Внимание! Это довольно старый топик, посты в него не попадут в новые, и их никто не увидит. Пишите пост, если хотите просто дополнить топик, а чтобы задать новый вопрос — начните новый.

Ответить

Вы можете использовать разметку markdown для оформления комментариев и постов. Используйте функцию предпросмотра для проверки корректности разметки.

Пожалуйста, оформляйте исходный код в соответствии с правилами разметки. Для того, чтобы вставить код в комментарий, скопируйте его в текстовое поле ниже, после чего выделите то, что скопировали и нажмите кнопку «код» в панели инструментов. Иначе ваш код может принять нечитаемый вид.

Либо производите оформление кода вручную, следующим образом:

``` #include <iostream> using namespace std; int main() { // ... } ```

Предпросмотр сообщения

Ваше сообщение пусто.