Bootstrap

C++ 函数中的不定参数

.定义
所谓不定参数,指的就是在函数的定义中,函数的参数列表不确定的情况,即函数并不清楚自己将要接收多少个参数,最常见的例子就是我们常用的printf()函数,如:

printf("%d", 3);
printf("%d,%d",3,4);

诸如多种类似的情况,此时仅凭重载函数是十分低效的。
因此,我们可以将函数定义成能够接受任意数量的实参。通过将省略号(3个句点…)写在函数定义中形参列表的最后,即可表示调用该函数时可以提供数量可变的实参。例如:

void test(int a , ...)
{
	
}

关于含有不定参数的几个注意事项:

1、	函数定义中必须至少有一个普通形参,但也可以有多个。
	因此不能出现void test(...)的情况	
	
2、	省略号必须总是放在形参列表的最后。
	因此不能出现void test(int a , ...int c)的情况

.参数的读取
下面参考:
https://www.cnblogs.com/liaocheng/p/4262125.html

在一个函数中,一般有如下prolog代码:

00401020 push ebp
00401021 mov ebp,esp
00401023 sub esp,48h
执行上述代码之后,func(a,b,c)函数所处的堆栈上下文就变成如下布局:
在这里插入图片描述

其中,ebp指向保存旧的ebp的堆栈内存的下一个字的地址,ebp+8指向eip地址,ebp+12则指向函数调用的第一个参数,而ebp和esp之间是用于临时变量(也就是堆栈变量)的空间。

注意,由于上述prolog代码的存在,我们很容易通过ebp得到第一个参数的地址,对于不定参数列表之前的类型固定的参数,我们也可以根据类型信息得到其实际的位置(例如,第一个参数的位置偏移第一个参数的大小,就是第二个参数的地址)。

所有类型固定的参数都必须出现在参数列表的开始。这样根据前面的论述,我们就可以得到所有类型固定的参数。

而获取参数的方法则是通过va_list指针,它是在 C 语言中引入解决变参问题的一组宏(有兴趣可以搜索下),使用时需引入<stdarg.h>。
具体用法为:
1、在函数体内定义va_list的变量,用于指向参数的指针;
2、调用va_start(),初始化变量,与变参表绑定,让其指向参数表中的第一个参数
3、利用va_arg()获取返回的参数,其中,VA_ARG的第二个参数是你要返回的参数的类型(如果函数有多个可变参数的,依次调用VA_ARG获取各个参数)
4、用va_end结束参数的获取。

void test(int count,...)
{
    va_list v;              //定义va_list
    int value;              //用于获取值

    va_start(v,count);      //使v跟可变参数表绑定
    value = va_arg(v, int); //获取可变参数表的参数

    va_end(v);              //结束参数的获取
}

在设计具有不定参数列表的函数的时候,我们有两种方法来确定到底多少参数会被传递进来。

方法一:
在类型固定的参数中指明后面有多少个参数以及他们的类型。printf()就是采用的这种方法,它的format参数指明后面每个参数的类型。

例子:

//count用于表明后续需要读入多少个参数
void sum(int count , ...)
{
    va_list v;
    va_start(v, count);

    int ans = 0;
    
    cout << "参数个数为:" << count << endl;

    for(int i=0;i<count;i++)
    {
        int value = va_arg(v, int);
        cout << value << " ";
        ans += value;
    }
    cout << endl;

    va_end(v);
    cout << "和为:" << ans << endl;
}

int main()
{
    sum(5, 1, 2, 3, 4, 5);
}
————————————————————————————
输出结果为:
参数个数为:5
1 2 3 4 5
和为:15

方法2是指定一个结束参数。这种情况一般是不定参数拥有同样的类型,我们可以指定一个特定的值来表示参数列表结束。

例子:

void sum_1(int no_need,...)
{
    va_list v;
    va_start(v, no_need);

    int ans = 0;
    cout << "读入为0时停止" << endl;

    int value = va_arg(v, int);
    while (value!=0)
    {
        cout << value << " ";
        ans += value;
        value = va_arg(v, int);
    }
    va_end(v);

    cout << endl;
    cout << "现在末尾为:" << value << endl;
}

int main()
{
    sum_1(5, 1, 2, 3, 4, 5, 0);
}
————————————————————————————
输出结果为:
读入为0时停止
1 2 3 4 5
现在末尾为:0

.注意事项
使用不定参数列表,有两个问题特别需要注意。

一、在重载一个函数的时候,不能依赖不定参数列表部分对函数进行区分。

假定我们定义两个重载函数如下:

int func(int a, int b, ...)

int func(int a, int b, float c);

则上述函数会导致编译器不知道怎么去解释func(1,2, 3.3),因为当第三个参数为浮点数时,两个实现都可以满足匹配要求。一般情况,个人建议对于不定参数函数不要去做重载。

二、不能期望不定参数表会做隐性转换。
绝大多数情况下,C和C++的变量都是强类型的,而不定参数列表属于一个特例。

当我们调用va_arg的时候,我们指明下一个参数的类型,而在执行的时候,va_arg正是根据这个信息在堆栈上来找到对应的参数的。如果我们需要的类型和真实传递进来的参数完全一致时自然没有问题,但是假如类型不一样,则会有大麻烦。

假如上面的的sumi函数,我们用下面方法调用:

int sum = sumi(1, 2.2, 3, 0)
注意第二个参数我们传入了一个double类型的2.2,我们希望sumi在做加法时可以做隐式类型转换,转换为int进行计算。但是实际情况时,当我们分析到这个参数时,调用的是:

c=va_arg(ap,int)
根据前文va_arg的定义,这个宏被翻译成:

#define va_arg(ap,t) ( *(int *)((ap += _INTSIZEOF(int)) - _INTSIZEOF(int)) )
如果后面的+=计算出正确的地址,最后就变成

*(int*)addr
如果希望能得到正确的整数值,必须要求addr所在的地址是一个真实的int类型。但是当我们传入double时,实际上其内存布局和int完全不同,因此我们得不到需要的整数。

.拓展:如何读取含有不同类型的不定参数表
上文讲到,我们无法通过不定参数表做隐式类型转换,但有时候我们又不得不需要这样,如printf()函数需要输出含有不同数据类型的数据。

方法一
实际上,printf()就是一个绝佳的解决方法,它根据读入的format来判断接下来读入的类型,即我们可以通过不定参数列表的第一个参数说明接下来读入的参数的类型,属于参数读入方法一的变种。

例子:

//严格来说下面的例子并不严格,这里仅做抽象化举例
void test(string s, ...)
{
    va_list v;
    va_start(v, s);

    //用s的长度表示不定参数的个数
    int count = s.length();

    for (int i = 0;i < count;i++)
    {
        //通过s的字符判定对应参数类型
        char ch = s[i];
        int value_int;
        double value_double;
        string value_string;

        switch (ch)
        {
        case 'a':
            value_int = va_arg(v, int);
            cout << "第" << i + 1 << "个参数类型是:int!值为:" << value_int << endl;
            break;
        case 'b':
            value_double = va_arg(v, double);
            cout << "第" << i + 1 << "个参数类型是:double!值为:" << value_double << endl;
            break;
        case 'c':
            value_string = va_arg(v, string);
            cout << "第" << i + 1 << "个参数类型是:string!值为:" << value_string << endl;
            break;
        default:
            break;
        }
    }

}

int main()
{
    test(string("abcabc"), 10, 15.51, string("hello!"), 6, 85.0, string("world!"));
    //85.0不能写成85,因为这里的实参是85是整型会出错的。
}
——————————
输出结果:
第1个参数类型是:int!值为:102个参数类型是:double!值为:15.513个参数类型是:string!值为:hello!4个参数类型是:int!值为:65个参数类型是:double!值为:856个参数类型是:string!值为:world!

方法二
尽管不定参数列表无法得知全部参数的类型,但是,唯独第一个参数是可以明确的(注意事项一),我们可以在这里做文章,比如陆续抽取不定参数列表的第一个参数,当列表仅剩一个时,等同于一个只含一个参数的同名函数,执行完也就自然停止了。(可归为方法二的变种)

可以看出,这个方法将递归调用自身,函数要读入不同类型的第一个参数,自然需要函数模板,不过不建议这么用。

例子:

//当函数参数仅剩一个时
template<typename T>
void show(T t)
{
	if(typeid(t)==typeid(string))
	{
        cout << t << " 不是个可计算类型!" << endl;
        return;
	}

    cout << t << " 是个可计算类型!" << endl;
}

//当函数参数为复数时
template<typename T,typename ... Others>
void show(T t,Others ... args)
{
    show(t);
    show(args ...);
}

int main()
{
    show(1, 20.25, string("this is a char*!"), 25, string("hello!"));;
}
——————————
输出结果:
1 是个可计算类型!
20.25 是个可计算类型!
this is a char*! 不是个可计算类型!
25 是个可计算类型!
hello! 不是个可计算类型!
;