这个特定的C函数是如何工作的?

9

我正在尝试学习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如何接受任意数量、任意类型的参数,并正确处理它们呢?
我尝试通过下载glibc源代码包来查找printf()的工作方式,但却找不到,虽然我会继续寻找。
这里有人能解释一下C语言是如何完成上述任务的吗?

如果您想了解更多信息,请阅读有关调用约定和堆栈的内容。 - Seth Carnegie
6
您的问题涉及C语言,但是您说“我正在尝试学习C ++”。如果您想学习C++,最好先避开该语言中的C部分(并保持在C++提供的更高抽象层次),一旦您对此感到舒适,再深入学习语言的其他部分。 - James McNellis
如果您不想担心这个问题,那么请将“-Wformat”作为参数传递给g++。其他编译器应该会有类似的警告,我认为/希望/期望。这将导致编译器检查类型是否匹配。 - Aaron McDaid
3个回答

13

C支持一种名为“可变参数”的函数类型,它意味着“可变(数量的)参数”。这样的函数必须至少有一个必需参数。在printf的情况下,格式字符串是必需参数。

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

已定义了C宏,允许您检索可变的参数。

关键点是:

  • 可变参数没有类型安全性。在printf()的情况下,如果格式字符串不正确,则代码将从内存中读取无效结果,可能会崩溃。
  • 可变参数是通过一个指针读取的,该指针通过包含这些参数的内存递增。
  • 必须使用va_start初始化参数指针,使用va_arg递增参数指针,并使用va_end释放参数指针。

我在相关问题上发布了大量您可能会发现有趣的代码:

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

这是一个只格式化整数(“%d”)的printf()的框架:

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

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

充实它需要相当多的工作--处理字段宽度、精度、更多的转换等等。然而,这至少能给你一个口味如何在函数内检索不同类型的不同参数。


@HeathHunnicutt:也许你能解释一下你认为有什么问题? - Jerry Coffin
你直接跳到了已经创建了va_list的部分。这不是自动的,但你的回答让它看起来像是自动的。也许printf()是一个包装器,围绕着你给出的my_vprintf()的例子。但重要的知识是如何使用va_start和最终的非变量参数创建va_list。跳过这部分就像“错误”的非答案一样。 - Heath Hunnicutt
@HeathHunnicutt:我并没有“跳过”任何内容。如果你往下看代码,你会发现my_printfmy_fprintf,它们调用了va_startva_end - Jerry Coffin
@HeathHunnicutt:我刚刚进行了足够的编辑,你应该可以取消投票。 - Jerry Coffin

2
不要忘记,如果你使用gcc(以及g ++?),你可以在编译器选项中传递-Wformat来让编译器检查参数类型是否与格式匹配。希望其他编译器也有类似的选项。
有人能解释一下C如何执行上述任务吗?
盲目自信。它假设您已确保参数的类型与格式字符串中对应的字母完全匹配。当调用printf时,所有参数都以二进制形式表示,不加仪式地串联在一起,并作为一个单独的大型参数传递给printf。如果它们不匹配,你会遇到麻烦。当printf遍历格式字符串时,每次看到%d时,它将从参数中取出4个字节(假设是32位的,在64位int中,它将占用8个字节),并将它们解释为一个整数。
现在可能实际上传递了一个double(通常占用两倍于int的内存),这种情况下printf将只取32位作为一个整数。然后,下一个格式字段(可能是一个%d)将获取双精度浮点数的其余部分。
因此,基本上,如果类型不完全匹配,则会得到严重混乱的数据。如果你运气不好,你将遇到未定义的行为。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接