为什么这种C语言中的函数重载方法有效?

问题描述 投票:1回答:2

我已经查看了一些用C语言做的方法,但我只找到了C99。

但是我从Lock Less那里得到了下面的解决方案。

问题是,我不太明白它是如何工作的,并且想知道那里发生的事情的基本原理,以便能够更清楚地理解它。

我已经上网了一段时间,发现this关于__VA_ARGS__,但遗憾的是,仅凭这一点还不够。

我真的很感激有关此问题的解释或指导,任何形式的参考都会有所帮助。

我用GCC-5.4.1和-ansi标志编译了这段代码。

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

#define COUNT_PARMS2(_1, _2, _3, _4, _5, _6, _7, _8, _9, _10, _, ...) _
#define COUNT_PARMS(...)\
    COUNT_PARMS2(__VA_ARGS__, 10, 9, 8, 7, 6, 5, 4, 3, 2, 1)

void count_overload1(int p1)
{
    printf("One param: %d\n", p1);
}

void count_overload2(double *p1, const char *p2)
{
    printf("Two params: %p (%f) %s\n", p1, *p1, p2);
}

void count_overload3(int p1, int p2, int p3)
{
    printf("Three params: %c %d %d\n", p1, p2, p3);
}

void count_overload_aux(int count, ...)
{
    va_list v;
    va_start(v, count);

    switch(count)
    {
        case 1:
        {
            int p1 = va_arg(v, int);
            count_overload1(p1);
            break;
        }

        case 2:
        {
            double *p1 = va_arg(v, double *);
            const char *p2 = va_arg(v, const char *);
            count_overload2(p1, p2);
            break;
        }

        case 3:
        {
            int p1 = va_arg(v, int);
            int p2 = va_arg(v, int);
            int p3 = va_arg(v, int);
            count_overload3(p1, p2, p3);
            break;
        }

        default:
        {
            va_end(v);

            printf("Invalid arguments to function 'count_overload()'");
            exit(1);
        }
    }

    va_end(v);
}
#define count_overload(...)\
    count_overload_aux(COUNT_PARMS(__VA_ARGS__), __VA_ARGS__)


int main(int argc, char const *argv[])
{
    double d = 3.14;
    count_overload(1);
    count_overload(&d, "test");
    count_overload('a',2,3);
    return 0;
}

输出是:

One param: 1
Two params: 0x7ffc0fbcdd30 (3.140000) test
Three params: a 2 3
c overloading
2个回答
5
投票

让我们分解COUNT_PARMSCOUNT_PARMS2宏。第一个COUNT_PARMS

#define COUNT_PARMS(...)\
    COUNT_PARMS2(__VA_ARGS__, 10, 9, 8, 7, 6, 5, 4, 3, 2, 1)

由于宏不包含任何命名参数,因此传递给它的任何参数都将替换为__VA_ARGS__

以下是来电:

COUNT_PARMS(arg1)
COUNT_PARMS(arg1, arg2)
COUNT_PARMS(arg1, arg2, ,arg3)

将扩展为:

COUNT_PARMS2(arg1,   10,    9,  8, 7, 6, 5, 4, 3, 2, 1)
COUNT_PARMS2(arg1, arg2,   10,  9, 8, 7, 6, 5, 4, 3, 2, 1)
COUNT_PARMS2(arg1, arg2, arg3, 10, 9, 8, 7, 6, 5, 4, 3, 2, 1)
                                                  // x

我将参数间隔开,以便您可以看到哪些参数彼此对应。特别注意标记为x的列。这是传递给COUNT_PARMS的参数数量,它是每种情况下的第11个参数。

现在让我们来看看COUNT_PARMS2

#define COUNT_PARMS2(_1, _2, _3, _4, _5, _6, _7, _8, _9, _10, _, ...) _

有11个名称参数,加上...来解释任何其他参数。宏的整个主体是_,这是第11个参数的名称。所以这个宏的目的是获取11个或更多的参数,并用第11个参数替换它们。

再看一下COUNT_PARAMS的定义,它以这样的方式扩展,即它调用COUNT_PARMS2,第11个参数是传递给COUNT_PARAMS的参数数量。这就是神奇的发生。

现在看看main中的函数调用:

count_overload(1);
count_overload(&d, "test");
count_overload('a',2,3);

这些扩展到:

count_overload_aux(COUNT_PARMS(1), 1);
count_overload_aux(COUNT_PARMS(&d, "test"), &d, "test");
count_overload_aux(COUNT_PARMS('a',2,3), 'a',2,3);

然后这个:

count_overload_aux(COUNT_PARMS2(1, 10, 9, 8, 7, 6, 5, 4, 3, 2, 1), 1);
count_overload_aux(COUNT_PARMS2(&d, "test", 10, 9, 8, 7, 6, 5, 4, 3, 2, 1), &d, "test");
count_overload_aux(COUNT_PARMS2('a',2,3, 10, 9, 8, 7, 6, 5, 4, 3, 2, 1), 'a',2,3);

然后这个:

count_overload_aux(1, 1);
count_overload_aux(2, &d, "test");
count_overload_aux(3, 'a',2,3);

最终结果是你可以调用一个带有可变数量参数的函数,而不必明确说明有多少参数。


2
投票

dbush's great answer解释了宏正在做什么。我想扩展一下,并谈谈这里使用的省略号...。你说关于可变参数宏和__VA_ARGS__的阅读并没有帮助,所以我认为你也可能不太了解C省略号。

在C中,声明一个采用可变数量参数的函数的方法是使用省略号...。这种函数的一个主要例子是printf,它至少可以使用一个参数,但它可以接受更多参数。

printf的原型是:

int printf(const char *format, ...);

...用于声明省略号。请注意,...只能出现在命名参数的末尾,它不应该是寄存器变量,函数或数组类型,因此:

void foo(...)
{
}

无效,编译器会显示如下错误:

c.c:6:10: error: ISO C requires a named argument before ‘...’
 void foo(...)
          ^~~

那么,怎么用呢?你使用va_list中定义的stdarg.h

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

int sum(int num_of_values, ...)
{
    va_list ap;

    // use the last named argument
    va_start(ap, num_of_values);

    int s = 0;
    for(int i = 0; i < num_of_values; ++i)
    {
        int v = va_arg(ap, int);
        s += v;
    }

    va_end(ap);

    return s;
}

int main(void)
{
    printf("The sum is: %d\n", sum(5, 1, 2, 3, 4, 5));
}

这将输出The sum is: 15

因此,当您的函数具有省略号时,必须首先声明va_list类型的变量,并将该变量作为第一个参数调用va_start,将最后一个命名参数作为第二个参数调用。

然后你可以使用va_arg(ap, <type>)获取值,其中<type>是值的类型,在上面的例子中,它将是int。像printf这样的函数解析格式并使用转换说明符来获取正确的类型。当printf发现%d,它会做va_arg(ap, int),如果%f被发现它会做va_arg(ap, float)并且如果发现%s,它将做va_arg(ap, char*)等等。这就是为什么printf在格式和参数不匹配时具有未定义的行为,因为在va_arg调用中会使用错误的类型,这会随后调用va_arg。最后必须召唤va_end

对于我在大学期间必须编写的微内核,我必须实现这些va_*-macros。我使用了编译器的行为,它将所有参数放在堆栈框架中,因此我的va_start计算了最后一个命名参数之后的下一个值的堆栈中的地址。 va_arg基于va_start的计算加上由类型确定的偏移量移动堆栈,同时还使用最后消耗的参数更新ap变量。让它工作很棘手,但最后它在该系统上工作,但是在x86_64上的相同实现只产生垃圾。

例如,在GCC编译器中我是如何实现的,我不知道,但我怀疑GCC做了类似的事情。我检查了源代码gcc/builtins.c:4855,但像往常一样,我发现GCC代码非常复杂。

© www.soinside.com 2019 - 2024. All rights reserved.