Как работает конкретная функция C? - PullRequest
10 голосов
/ 16 января 2012

Я пытаюсь изучить C и уже очень запутался.

В используемых мной языках ООП существует возможность выполнять перегрузку методов, когда одна и та же функция может иметь разные типы параметров и вызывать в зависимости от того, что былонаиболее подходящий.

Теперь в C я знаю, что это не так, поэтому я не могу понять следующую проблему, Как работает printf ().

Например:

char chVar = 'A';
int intVar = 123;
float flVar = 99.999;

printf("%c - %i - %f \n",chVar, intVar, flVar);
printf("%i - %f - %c \n",intVar, flVar, chVar);
printf("%f - %c - %i \n",flVar, chVar, intVar);

Теперь, поскольку C не поддерживает перегрузку функций, как printf удается получить любое количество аргументов любого типа и затем корректно работать с ними?

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

Может ли кто-нибудь здесь объяснить, как C выполняет вышеуказанную задачу?

Ответы [ 3 ]

14 голосов
/ 16 января 2012

C поддерживает тип сигнатуры функции, которая называется «varargs», что означает «переменная (количество) аргументов».У такой функции должен быть хотя бы один обязательный аргумент.В случае printf строка формата является обязательным аргументом.

Как правило, на компьютере со стеком, когда вы вызываете любую функцию C, аргументы помещаются в стек справа налево.оставил.Таким образом, первый аргумент функции находится в «вершине» стека сразу после адреса возврата.

Определены макросы C, позволяющие получить аргументы переменной.

Ключевыми моментами являются:

  • Безопасность переменных для аргументов переменных отсутствует.В случае printf(), если строка формата неверна, код будет считывать недопустимые результаты из памяти, что может привести к сбою.
  • Аргументы переменной считываются через указатель, который увеличивается в памяти, содержащей эти аргументы.
  • Указатель аргумента должен быть инициализирован с помощью va_start, увеличен с помощью va_arg и освобожден с помощью va_end.

Я разместил тонну кода, который вы можете найтиинтересно по смежному вопросу:

Лучший способ сохранить va_list для дальнейшего использования в C / C ++

Вот скелет из printf(), который форматирует только целые числа ("% d"):

int printf( const char * fmt, ... )
{
    int d;  /* Used to store any int arguments. */
    va_list args;  /* Used as a pointer to the next variable argument. */

    va_start( args, fmt );  /* Initialize the pointer to arguments. */

    while (*fmt)
    {
        if ('%' == *fmt)
        {
            fmt ++;

            switch (*fmt)
            {
                 case 'd':  /* Format string says 'd'. */
                            /* ASSUME there is an integer at the args pointer. */

                     d = va_arg( args, int);
                     /* Print the integer stored in d... */
                     break;
             }
        }
        else 
           /* Not a format character, copy it to output. */
        fmt++;
    }

    va_end( args );
}
5 голосов
/ 16 января 2012

Внутренне, printf будет (по крайней мере обычно) использовать некоторые макросы из stdarg.h. Общая идея (сильно расширенная версия) примерно такая:

#include <stdarg.h>
#include <stdio.h>
#include <string.h>

int my_vfprintf(FILE *file, char const *fmt, va_list arg) {

    int int_temp;
    char char_temp;
    char *string_temp;
    char ch;
    int length = 0;

    char buffer[512];

    while ( ch = *fmt++) {
        if ( '%' == ch ) {
            switch (ch = *fmt++) {
                /* %% - print out a single %    */
                case '%':
                    fputc('%', file);
                    length++;
                    break;

                /* %c: print out a character    */
                case 'c':
                    char_temp = va_arg(arg, int);
                    fputc(char_temp, file);
                    length++;
                    break;

                /* %s: print out a string       */
                case 's':
                    string_temp = va_arg(arg, char *);
                    fputs(string_temp, file);
                    length += strlen(string_temp);
                    break;

                /* %d: print out an int         */
                case 'd':
                    int_temp = va_arg(arg, int);
                    itoa(int_temp, buffer, 10);
                    fputs(buffer, file);
                    length += strlen(buffer);
                    break;

                /* %x: print out an int in hex  */
                case 'x':
                    int_temp = va_arg(arg, int);
                    itoa(int_temp, buffer, 16);
                    fputs(buffer, file);
                    length += strlen(buffer);
                    break;
            }
        }
        else {
            putc(ch, file);
            length++;
        }
    }
    return length;
}

int my_printf(char const *fmt, ...) {
    va_list arg;
    int length;

    va_start(arg, fmt);
    length = my_vfprintf(stdout, fmt, arg);
    va_end(arg);
    return length;
}

int my_fprintf(FILE *file, char const *fmt, ...) {
    va_list arg;
    int length;

    va_start(arg, fmt);
    length = my_vfprintf(file, fmt, arg);
    va_end(arg);
    return length;
}


#ifdef TEST 

int main() {
    my_printf("%s", "Some string");
    return 0;
}

#endif

Реализация этого требует довольно большой работы - работа с шириной поля, точностью, большим количеством преобразований и т. Д. Однако этого достаточно, чтобы хотя бы дать представление о том, как вы извлекаете различные аргументы различных типов внутри вашего функция.

2 голосов
/ 16 января 2012

(Не забывайте, что если вы используете gcc (и g ++?), Вы можете передать -Wformat в параметрах компилятора, чтобы заставить компилятор проверить, что типы аргументов соответствуют форматированию. Надеюсь другие компиляторы имеют аналогичные параметры.)

Может ли кто-нибудь здесь объяснить, как C выполняет вышеуказанную задачу?

Слепая вера. Предполагается, что вы убедились, что типы аргументов полностью совпадают с соответствующими буквами в строке формата. Когда вызывается printf, все аргументы представляются в двоичном виде, бесцеремонно соединяются вместе и эффективно передаются как один большой аргумент printf. Если они не совпадают, у вас будут проблемы. Поскольку printf выполняет итерацию по строке формата, каждый раз, когда он видит %d, он будет брать 4 байта из аргументов (при условии, что 32-битные, это будет 8 байтов для 64-битных битов, конечно), и он будет интерпретировать их как целое число.

Теперь, возможно, вы на самом деле передали double (обычно занимающий вдвое больше памяти, чем int), и в этом случае printf просто возьмет 32 из этих битов и представит их как целое число. Тогда следующее поле формата (возможно, %d) займет остаток от двойного.

В общем, если типы не совпадают идеально, вы получите искаженные данные. И если вам не повезет, у вас будет неопределенное поведение.

...