不定参数的应用

本文深入探讨了C语言中不定参数的功能与使用方法,包括原理、访问方式及潜在问题等关键内容。

不定参数当年做为C/C++语言一个特长被很多人推崇,但是实际上这种技术并没有应用很多。除了格式化输出之外,我实在没看到多少应用。主要原因是这种技术比较麻烦,副作用也比较多,而一般情况下重载函数也足以替换它。尽管如此,既然大家对它比较感兴趣,我就简单总结一下它的使用和需要注意的常见问题。

原理

刚学C语言的时候,一般人都会首先接触printf函数。通过这个函数,你可以打印不定个数的变量到屏幕,如:

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

 上述代码看似简单,实际上却需要我们解决许多问题。在我们设计printf的时候,我们是不知道到底会传入几个参数的。在这种未知的情况下,我们需要解决下面几个问题:

  1. 怎么告诉printf我们会传入几个参数
  2. printf怎么去访问这些参数
  3. 函数调用完成后,系统怎么把参数从传递用的堆栈中释放

为了解决这些问题,我们首先要解释cdecl调用约定(参见论调用约定),所有使用不定参数的函数必须是使用cdecl(全局函数)或者this call(类成员函数)调用约定。该约定对于参数传递规定如下:

  1. 参数从右向左入栈(也就是如果你调用f(a,b,c),则c先入栈,然后是b,最后是a入栈)
  2. 调用者负责清理堆栈

其中第二点直接解决了前面三个问题中的第三个问题。我们来详细说说其他两个问题。

确定参数的个数

在一个函数中,一般有如下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得到第一个参数的地址,对于不定参数列表之前的类型固定的参数,我们也可以根据类型信息得到其实际的位置(例如,第一个参数的位置偏移第一个参数的大小,就是第二个参数的地址)。

注意不定参数函数有个限制,就是不定参数的列表必须在整个函数的参数列表的最后。我们不可以定义如下的函数:

void func(int a, ..., int c)

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

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

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

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

int sumi(int c, ...)
{
    va_list ap;
    va_start(ap,c);
    
int i;
    
int sum = c;
    c 
= va_arg(ap,int);
    
while(0!=c)
    
{
        sum 
= sum+c;
        c 
= va_arg(ap,int);
    }

    
return sum;
}

使用这个函数的代码为:

int main(int argc, char* argv[])
{
    
int i=sumi(1,2,3,4,5,6,7,8,9,0);
    
    
return 0;
}

访问各个参数

其实前文已经告诉我们怎么去访问不定参数。va_start和va_arg函数可以被结合起来用于依次访问每个函数,他们实际上都是宏函数。

在vc6,va_start函数定义为:

#define _INTSIZEOF(n)   ( (sizeof(n) + sizeof(int) - 1) & ~(sizeof(int) - 1) )
#define va_start(ap,v)  ( ap = (va_list)&v + _INTSIZEOF(v) )

其中_INTSIZEOF(n)计算比n大的sizeof(int)的最小倍数,如果n=101,则_INTSIZEOF(n)为104。

va_start执行完毕后,ap指向变量v后第一个4字节对齐的地址。例如,v的地址为0x123456, v的大小为13,则v后面的下一个与字边界对齐的地址为0x123456+0x0D=0x123463再调整为与4字节对齐的下一个地址,也就是0x123464.

va_arg函数定义为:

#define va_arg(ap,t)    ( *(t *)((ap += _INTSIZEOF(t)) - _INTSIZEOF(t)) )

分析与va_start一样,它的结果是使ap指向当前变量的下一个变量。

这样,我们只要在开始时使用va_start把不定参数列表赋值给ap,然后依次用va_arg获得不同参数即可。

潜在问题

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

问题1的理解相对简单:我们在重载一个函数的时候,不能依赖不定参数列表部分对函数进行区分。

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

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(12.230)

注意第二个参数我们传入了一个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完全不同,因此我们得不到需要的整数。感兴趣的朋友可以用下面简单的代码做测试:

double a;
a
=1.1;
int b = *(int*& a;

因此,当我们调用有不定参数列表的函数时,不要期望系统做隐式类型转换,系统不会做这种检查或者转换,你给的参数类型必须严格和你希望的值一样。

<think>首先,用户要求使用中文回答问题。问题是用中文写的:“不定参数c++详解”,意思是“在C++中不定参数的详细解释”。 所以,我需要用中文详细解释C++中的不定参数。在C++中,不定参数通常指的是可变参数函数,比如使用va_list、va_start等的函数。 关键点: - C++中的不定参数允许函数接受任意数量的参数。 - 这可以通过C风格的va_list实现,或者在C++11及以后使用变参模板(variadic templates)。 - 我应该覆盖基本概念、用法、示例代码。 结构我的回答: 1. 简要介绍什么是C++中的不定参数。 2. 解释C风格的实现(使用stdarg.h)。 3. 解释C++风格的实现(使用变参模板)。 4. 提供一个简单的代码示例。 5. 讨论优缺点或注意事项。 用户指定了在回答完问题后,必须提出3-5个相关问题。相关问题必须以"§§相关问题§§"开头,并在同一行输出,不能换行分散。相关问题应该基于用户的提问和我的回答。 从示例格式中: - 回答结束后,直接输出"§§相关问题§§",然后列出1. 2. 3. 等问题。 - 相关问题应该是有意义的后续问题。 规划相关问题: 1. 关于C风格的不定参数的问题。 2. 关于C++变参模板的问题。 3. 实际应用或最佳实践问题。 4. 安全问题或常见错误。 5. 与可变参数宏的对比。 确保所有内容符合规则:回答用中文,相关问题用中文列出。 最终回答结构: - 先详细解释"不定参数c++详解"。 - 然后输出"§§相关问题§§",后跟3-5个相关问题。 编写回答: - 开头:解释C++不定参数。 - 中间:提供代码示例(用代码块,如Python示例所示)。 - 结尾:相关问题。 代码示例应该用C++,用户问题是关于C++的。示例格式中用了Python,但这里应该是C++代码。 用户提供的示例是Python代码,但我需要根据问题用C++代码。 现在,撰写中文解释。 详细解释: - 定义:不定参数指的是函数可以接受不同数量和类型的参数。 - C风格:使用头文件<cstdarg>,va_list, va_start, va_arg, va_end。 - C++风格:C++11引入的变参模板,使用模板参数包。 代码示例: - 一个简单的C风格示例:计算整数和。 - 一个简单的变参模板示例:打印任意多个参数。 相关问题: 1. 在C++中,如何使用va_list处理不同类型的参数? 2. 变参模板在编译时类型安全方面有什么优势? 3. C风格不定参数有什么局限性? 4. 如何在实际项目中安全地使用不定参数? 5. C++17和C++20对可变参数有哪些改进? 确保输出格式: - 回答结束后,直接写"§§相关问题§§"(不带换行),然后列表相关问题。 - 相关问题编号用1. 2. 3. 等。 完整回答: </think>### C++ 不定参数详解 在C++中,"不定参数"(也称为可变参数)指的是函数可以接受任意数量和类型的参数。这主要用于处理未知输入的场景。C++支持两种实现方式:C风格的`va_list`宏机制(继承自C语言)和现代C++的变参模板(variadic templates)。下面将详细解释并提供代码示例。 #### 1. **C风格不定参数(使用`va_list`)** - **原理**:通过宏定义(如`va_start`, `va_arg`, `va_end`)实现,参数存储在栈上。使用时需要包含头文件`<cstdarg>`。 - **限制**:类型安全差,运行时检查参数类型;只能处理基本类型(如int、double),不能直接处理类对象或字符串(需通过指针)。 - **示例代码**:计算整数参数的平均值。 ```cpp #include <cstdarg> #include <iostream> double average(int count, ...) { va_list args; va_start(args, count); // 初始化参数列表 double sum = 0; for (int i = 0; i < count; ++i) { sum += va_arg(args, int); // 依次获取每个int参数 } va_end(args); // 清理参数列表 return sum / count; } int main() { std::cout << "平均值: " << average(3, 10, 20, 30) << std::endl; // 输出: 20 return 0; } ``` #### 2. **C++风格变参模板(C++11及以后)** - **原理**:使用模板参数包(template parameter pack),在编译期处理参数,提供类型安全和灵活性。支持任意类型(包括类对象)。 - **优势**:编译时类型检查、可递归展开参数包、适用于泛型编程。 - **示例代码**:递归打印任意类型参数。 ```cpp #include <iostream> // 基函数:处理无参数情况 void print() { std::cout << std::endl; } // 变参模板函数:递归打印参数 template<typename T, typename... Args> void print(T first, Args... args) { std::cout << first << " "; // 处理第一个参数 print(args...); // 递归处理剩余参数 } int main() { print(1, "hello", 3.14); // 输出: 1 hello 3.14 return 0; } ``` #### 关键注意事项 - **类型安全**:C风格易出错(如类型不匹配),C++模板更安全。 - **性能**:C风格在运行时开销小,但模板在编译期优化好。 - **应用场景**:C风格兼容旧代码;C++模板推荐用于现代开发(如格式化输出、元组实现)。 - **C++标准演进**:C++17引入折叠表达式(fold expressions)简化递归,例如`(std::cout << ... << args)`可替代递归打印。 在实际项目中,优先使用变参模板以避免潜在错误。如果有旧代码依赖,则谨慎使用`va_list`。
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值