特定的C功能如何工作?

时间:2021-02-18 16:46:26

I am trying to learn C and am very confused already.

我正在努力学习C并且我已经非常困惑了。

In the OOP languages i have used there exists the ability to perform method overloading, where the same function could have different parameter types and call whichever was the most appropriate.

在我使用的OOP语言中,存在执行方法重载的能力,其中相同的函数可以具有不同的参数类型并且调用哪个是最合适的。

Now in C i know that this is not the case so i cant figure out the following problem, How printf() works.

现在在C中我知道情况并非如此,所以我无法弄清楚以下问题,printf()如何工作。

For example:

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);

Now as C does'nt support function overloading, How does printf manage to take any number of arguments, of any type, and then work correctly with them?

现在因为C不支持函数重载,printf如何设法获取任何类型的任意数量的参数,然后正确地使用它们?

I have tried to find the printf() working by downloading the glibc source package but can quite seem to find it, though i'll keep looking.

我试图通过下载glibc源程序包找到printf()工作,但似乎很容易找到它,虽然我会继续寻找。

Could anyone here explain how C performs the above task?

这里的任何人都可以解释C如何执行上述任务吗?

3 个解决方案

#1


12  

C supports a type of function signature called "varargs" meaning "variable (number of) arguments". Such a function must have at least one required argument. In the case of printf, the format string is a required argument.

C支持一种称为“varargs”的函数签名,意思是“变量(数量)参数”。这样的函数必须至少有一个必需的参数。对于printf,格式字符串是必需参数。

Generally, on a stack-based machine, when you call any C function, the arguments are pushed onto the stack from right-to-left. In this way, the first argument to the function is that found on the "top" of the stack, just after the return address.

通常,在基于堆栈的机器上,当您调用任何C函数时,参数将从右向左推入堆栈。这样,函数的第一个参数是在堆栈的“顶部”找到的,就在返回地址之后。

There are C macros defined which allow you to retrieve the variable arguments.

定义了C宏,允许您检索变量参数。

The key points are:

关键点是:

  • There is no type-safety for the variable arguments. In the case of printf(), if the format string is wrong, the code will read invalid results from memory, possibly crashing.
  • 变量参数没有类型安全性。对于printf(),如果格式字符串错误,代码将从内存中读取无效结果,可能会崩溃。

  • The variable arguments are read through a pointer which is incremented through the memory containing those arguments.
  • 通过指针读取变量参数,该指针通过包含这些参数的内存递增。

  • The argument pointer must be initialized with va_start, incremented with va_arg, and released with va_end.
  • 必须使用va_start初始化参数指针,使用va_arg递增,并使用va_end释放。

I have posted a ton of code you may find interesting on the related question:

我发布了大量您可能会对相关问题感兴趣的代码:

Best Way to Store a va_list for Later Use in C/C++

存储va_list以便以后在C / C ++中使用的最佳方法

Here's a skeleton of a printf() which only formats integers ("%d"):

这是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 );
}

#2


5  

Internally, printf will (at least usually) use some macros from stdarg.h. The general idea is (a greatly expanded version of) something like this:

在内部,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

Fleshing it out does involve quite a bit of work -- dealing with field width, precision, more conversions, etc. This is enough, however, to at least give a flavor of how you retrieve varying arguments of varying types inside your function.

充实它确实涉及相当多的工作 - 处理字段宽度,精度,更多转换等。然而,这足以说明如何在函数内检索不同类型的不同参数。

#3


2  

(Don't forget that, if you're using gcc (and g++?), you can pass -Wformat in the compiler options to get the compiler to check that the types of the arguments match the formatting. I hope other compilers have similar options.)

(不要忘记,如果你正在使用gcc(和g ++?),你可以在编译器选项中传递-Wformat,让编译器检查参数的类型是否与格式匹配。我希望其他编译器有类似的选项)。

Could anyone here explain how C performs the above task?

这里的任何人都可以解释C如何执行上述任务吗?

Blind faith. It assumes that you have ensured that the types of the arguments match perfectly with the corresponding letters in your format string. When printf is called, all the arguments are represented in binary, unceremoniously concatenated together, and passed effectively as a single big argument to printf. If they don't match, you'll have problems. As printf iterates through the format string, every time it see %d it will take 4 bytes from the arguments (assuming 32-bit, it would be 8 bytes for 64-bit ints of course) and it will interpret them as an integer.

迷信。它假定您已确保参数类型与格式字符串中的相应字母完全匹配。当调用printf时,所有参数都以二进制表示,毫不客气地连接在一起,并作为printf的一个大参数有效地传递。如果它们不匹配,你就会遇到问题。当printf遍历格式字符串时,每次看到%d时,它将从参数中获取4个字节(假设32位,当然,对于64位整数,它将是8个字节)并且它将它们解释为整数。

Now maybe you actually passed a double (typically taking up twice as much memory as an int), in which case printf will just take 32 of those bits and represented them as an integer. Then the next format field (maybe a %d) will take the rest of the double.

现在也许你实际上传递了一个double(通常占用的内存是int的两倍),在这种情况下,printf将只取32个这些位并将它们表示为整数。然后下一个格式字段(可能是%d)将取其余的双倍。

So basically, if the types don't match perfectly you'll get badly garbled data. And if you're unlucky you will have undefined behaviour.

所以基本上,如果类型不完全匹配,你会得到严重乱码的数据。如果你运气不好,你会有不确定的行为。

#1


12  

C supports a type of function signature called "varargs" meaning "variable (number of) arguments". Such a function must have at least one required argument. In the case of printf, the format string is a required argument.

C支持一种称为“varargs”的函数签名,意思是“变量(数量)参数”。这样的函数必须至少有一个必需的参数。对于printf,格式字符串是必需参数。

Generally, on a stack-based machine, when you call any C function, the arguments are pushed onto the stack from right-to-left. In this way, the first argument to the function is that found on the "top" of the stack, just after the return address.

通常,在基于堆栈的机器上,当您调用任何C函数时,参数将从右向左推入堆栈。这样,函数的第一个参数是在堆栈的“顶部”找到的,就在返回地址之后。

There are C macros defined which allow you to retrieve the variable arguments.

定义了C宏,允许您检索变量参数。

The key points are:

关键点是:

  • There is no type-safety for the variable arguments. In the case of printf(), if the format string is wrong, the code will read invalid results from memory, possibly crashing.
  • 变量参数没有类型安全性。对于printf(),如果格式字符串错误,代码将从内存中读取无效结果,可能会崩溃。

  • The variable arguments are read through a pointer which is incremented through the memory containing those arguments.
  • 通过指针读取变量参数,该指针通过包含这些参数的内存递增。

  • The argument pointer must be initialized with va_start, incremented with va_arg, and released with va_end.
  • 必须使用va_start初始化参数指针,使用va_arg递增,并使用va_end释放。

I have posted a ton of code you may find interesting on the related question:

我发布了大量您可能会对相关问题感兴趣的代码:

Best Way to Store a va_list for Later Use in C/C++

存储va_list以便以后在C / C ++中使用的最佳方法

Here's a skeleton of a printf() which only formats integers ("%d"):

这是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 );
}

#2


5  

Internally, printf will (at least usually) use some macros from stdarg.h. The general idea is (a greatly expanded version of) something like this:

在内部,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

Fleshing it out does involve quite a bit of work -- dealing with field width, precision, more conversions, etc. This is enough, however, to at least give a flavor of how you retrieve varying arguments of varying types inside your function.

充实它确实涉及相当多的工作 - 处理字段宽度,精度,更多转换等。然而,这足以说明如何在函数内检索不同类型的不同参数。

#3


2  

(Don't forget that, if you're using gcc (and g++?), you can pass -Wformat in the compiler options to get the compiler to check that the types of the arguments match the formatting. I hope other compilers have similar options.)

(不要忘记,如果你正在使用gcc(和g ++?),你可以在编译器选项中传递-Wformat,让编译器检查参数的类型是否与格式匹配。我希望其他编译器有类似的选项)。

Could anyone here explain how C performs the above task?

这里的任何人都可以解释C如何执行上述任务吗?

Blind faith. It assumes that you have ensured that the types of the arguments match perfectly with the corresponding letters in your format string. When printf is called, all the arguments are represented in binary, unceremoniously concatenated together, and passed effectively as a single big argument to printf. If they don't match, you'll have problems. As printf iterates through the format string, every time it see %d it will take 4 bytes from the arguments (assuming 32-bit, it would be 8 bytes for 64-bit ints of course) and it will interpret them as an integer.

迷信。它假定您已确保参数类型与格式字符串中的相应字母完全匹配。当调用printf时,所有参数都以二进制表示,毫不客气地连接在一起,并作为printf的一个大参数有效地传递。如果它们不匹配,你就会遇到问题。当printf遍历格式字符串时,每次看到%d时,它将从参数中获取4个字节(假设32位,当然,对于64位整数,它将是8个字节)并且它将它们解释为整数。

Now maybe you actually passed a double (typically taking up twice as much memory as an int), in which case printf will just take 32 of those bits and represented them as an integer. Then the next format field (maybe a %d) will take the rest of the double.

现在也许你实际上传递了一个double(通常占用的内存是int的两倍),在这种情况下,printf将只取32个这些位并将它们表示为整数。然后下一个格式字段(可能是%d)将取其余的双倍。

So basically, if the types don't match perfectly you'll get badly garbled data. And if you're unlucky you will have undefined behaviour.

所以基本上,如果类型不完全匹配,你会得到严重乱码的数据。如果你运气不好,你会有不确定的行为。