你所看到的惊艳,都曾被平庸历练
简介
相信学过c/c++的佬们都知道,如果一个函数将自己的参数确定好后,要想使用该函数就必须传递合适的数量以及类型的参数,然而这样的函数非常具有局限性。例如有一个求和函数,我们并不知道我们要求几个数的和,这就导致在确定函数参数时无从下手,而这时候不定参数就派上了用场。
不定参数是指在函数定义时,允许接收可变数量的参数。这种机制允许函数在调用时接受不同数量的实际参数,使得函数更加灵活,能够适应不同的使用场景。例如C语言中的printf
函数,scanf
函数等。在C和C++中,不定参数的实现方式主要依赖于标准库中的stdarg.h
头文件。
在使用不定参数后,sum
函数可以接受不同数量的参数,并计算它们的总和。这种灵活性使得函数能够适应不同的求和需求,而不需要为每种情况都定义一个单独的函数。这样的设计使代码更简洁,更容易扩展,同时提供了更大的通用性。
不定参数的优势:
优势 | 说明 |
---|---|
灵活性 | 不定参数函数允许在调用函数时传递不同数量的参数,提高了函数的灵活性和通用性。 |
适应变化 | 在处理不确定数量的参数或者未来可能会新增参数的情况下,不定参数函数能够更好地适应变化。 |
简化代码 | 使用不定参数可以简化函数定义,避免定义多个函数处理不同数量的参数情况。 |
可读性 | 在某些情况下,使用不定参数可以使函数调用更加清晰和可读,特别是当参数的数量可能变化时。 |
C语言不定参数
在C语言中,不定参数是通过stdarg.h
头文件提供的一组宏来实现的。这些宏主要包括va_list
、va_start
、va_arg
和va_end
。
-
va_list
:va_list
是一个类型,用于声明一个变量,该变量将用于存储不定参数的信息。
-
va_start
:va_start
宏用于初始化va_list
,使其指向不定参数的起始位置。它接受两个参数,第一个是va_list
对象,第二个是最后一个确定的参数,用于确定不定参数的起始位置。
void function_with_varargs(int fixed_arg, ...) { va_list args; va_start(args, fixed_arg); // 现在args指向第一个可变参数 // ... va_end(args); }
-
va_arg
:va_arg
宏用于获取当前位置的值,并且在每次调用后,会将指针移动到下一个可变参数的位置。它接受两个参数,第一个是va_list
对象,第二个是要获取的参数的类型。
int value = va_arg(args, int);
-
va_end
:va_end
宏用于清理va_list
对象,确保在使用完不定参数后正确释放资源。
va_end(args);
如下是一个在C语言中使用不定参数的求和函数:
#include <stdio.h>
#include <stdarg.h>
int sum(int n, ...) {
va_list args; // 定义用于存储不定参数信息的变量
va_start(args, n); // 初始化 va_list,使其指向不定参数列表的起始位置
int ret = 0;
for (int i = 0; i < n; i++) {
int tmp = va_arg(args, int); // 获取当前位置的参数值
ret += tmp;
}
va_end(args); // 清理 va_list,确保资源正确释放
return ret;
}
int main() {
int ret = sum(5, 10, 24, 10, 34, 0); // 调用 sum 函数,传递5个整数
printf("%d\n", ret); // 打印计算结果
return 0;
}
在 sum
函数中,va_start
初始化了 va_list
,并使其指向传递给函数的不定参数的起始位置。然后,通过使用 va_arg
获取每个参数的值,并将它们累加到 ret
变量中。最后,通过 va_end
清理 va_list
,确保资源得到正确释放。
在 main
函数中,调用了 sum
函数,并将 5 个整数作为参数传递给它,然后打印计算结果。
不定参宏函数
除此之外,还可以在C语言中使用宏来创建带有不定参数的宏函数。不定参数的宏函数通常使用__VA_ARGS__
来表示可变数量的参数。
#include <stdio.h>
#include <stdarg.h>
// 定义 LOG 宏,使用 __VA_ARGS__ 来表示可变参数
#define LOG(fmt, ...) printf("[%s : %d] " fmt "\n", __FILE__, __LINE__, ##__VA_ARGS__)
int main() {
// 使用 LOG 宏输出日志信息,自动包含文件名和行号
LOG("登陆失败");
return 0;
}
在这个例子中,LOG
宏使用了 __VA_ARGS__
来表示可变参数。在调用 LOG
时,传递的字符串会被输出,并自动包含文件名和行号。这样,通过这个宏,您可以轻松地在代码中输出带有文件名和行号的日志信息。
需要注意的是,在
##__VA_ARGS__
中的##
用于处理不带参数时的情况,以防止在宏展开时产生语法错误。在可变参数为空的情况下,##__VA_ARGS__
将会被移除,避免了末尾逗号的问题。
其中 filename.c
是当前文件名,7
是当前行号,登陆失败
是通过 LOG
宏传递的字符串。不定参数宏函数的使用可以帮助简化代码,但也需要小心使用,确保在宏展开时没有意外的副作用。
C++不定参数
C++中的不定参函数通常使用可变参数模板来实现,这使得函数能够接受不定数量和类型的参数。使用可变参数模板的不定参函数是 C++11 引入的特性。如果你使用的是更早版本的 C++,可能需要使用 <cstdarg>
头文件中的宏来处理不定参数。
基本模板:
template<typename ...Args>
void print(Args... args) {}
又由于无法直接获取参数包args中的每个参数,所以只能通过展开参数包的方式来获取参数包中的每个参数,这是使用可变模版参数的一个主要特点,也是最大的难点,即如何展开可变模版参数。由于语法不支持使用args[i]
这样方式获取可变参数,所以只能想一些特殊的办法来获取参数包的值。
方法一:递归展开
递归展开可变参数包是使用可变参数模板的一种常见技术,通常用于在编译时展开不定数量的参数。下面是一个递归展开可变参数包的示例:
#include <iostream>
void print() {
// 终止条件,处理空参数包
}
template <typename T, typename... Args>
void print(T first, Args... args) {
std::cout << "Cur : " << first << " args size : " << sizeof...(args) << std::endl;
print(args...); // 递归调用,展开参数包
}
int main() {
print("你干嘛", "哎呦", 100, 345);
return 0;
}
这个程序定义了一个函数模板 print
,该函数接受任意数量和类型的参数,并递归展开这些参数。
-
print
函数有两个版本:一个是终止条件版本(空参数包时调用),另一个是递归版本。 -
在递归版本中,首先打印第一个参数
first
和参数包的大小(使用sizeof...(args)
),然后通过print(args...)
递归地调用自己,展开参数包。 -
在
main
函数中,你调用了print
函数,传递了不同类型的参数。
这样,当你运行程序时,print
函数将递归展开参数包,打印每个参数的值和参数包的大小。
这样,通过递归调用,编译器会在编译时展开参数包,使得函数能够处理不定数量和类型的参数。这是一种在编译时进行模板展开的方式,相比运行时的展开更加高效。
方式二:折叠表达式
除了递归展开参数包的方式,C++17 引入了折叠表达式(fold expressions)的特性,这是一种更简洁的方式来展开参数包。折叠表达式在处理可变参数包时可以提供更紧凑的语法。如下代码:
#include <iostream>
// 使用折叠表达式展开参数包
template <typename... Args>
void print(Args... args) {
(std::cout << ... << args) << std::endl; // 折叠表达式展开参数包
}
int main() {
print("你干嘛", "哎呦", 100, 345);
return 0;
}
在这个例子中,(std::cout << ... << args)
是一个折叠表达式,用于展开参数包 args
。这个表达式实际上将所有参数连接到 std::cout
流中,然后通过 << std::endl
添加换行符。
当需要使用折叠表达式求和时,也可以如下代码:
#include <iostream>
template<class ...Args>
int sum(Args... args) {
return (args + ...);
}
int main() {
std::cout << sum(25, 34, 59) << std::endl;
return 0;
}
代码中使用了 C++17 中引入的折叠表达式 (args + ...)
来计算参数包中所有参数的和。在这个例子中,折叠表达式展开了 args
中的参数,并使用 +
运算符对它们求和。
使用折叠表达式的好处是语法更为紧凑,而且可以在一个表达式中完成参数包的展开。这是在 C++17 及更高版本中的一种更现代的方式。
总体而言,递归展开和折叠表达式是两种常见的方式,具体选择取决于个人或团队的偏好以及编译器的支持版本。在 C++17 及更高版本中,折叠表达式通常被认为是更好的选择。
方法三:逗号表达式
除了递归展开和折叠表达式,还有一种使用逗号表达式的方式展开参数包。这种方式通常结合逗号运算符 ,
和初始化列表来实现。
以下是一个示例:
#include <iostream>
// 使用逗号表达式展开参数包
template <typename... Args>
void print(Args... args) {
int dummy[] = {(std::cout << args << " ", 0)...}; // 逗号表达式展开参数包
(void)dummy; // 防止编译器警告未使用的变量
std::cout << std::endl;
}
int main() {
print("你干嘛", "哎呦", 100, 345);
return 0;
}
在这个例子中,逗号表达式 (std::cout << args << " ", 0)
被用于展开参数包 args
。逗号表达式的返回值是初始化列表中的最后一个表达式的值(在这里是 0
),而初始化列表的目的是为了创建一个临时数组 dummy
。
这种方式相对较独特,通常在一些特殊情况下使用。一般而言,递归展开和折叠表达式更为常见和直观,但了解这种逗号表达式的方法也是有益的。选择展开参数包的方法通常取决于个人或团队的偏好、代码可读性和编译器支持。
总结
在C语言和C++中,可变参数是一种强大的特性,它允许函数接受可变数量和类型的参数。在C语言中,我们通常使用<stdarg.h>
头文件提供的宏,如va_start
、va_arg
、和va_end
,来处理可变参数函数。而在C++中,我们有更灵活和类型安全的可变参数模板。总结起来,可变参数的使用有以下关键点:
-
C语言中的可变参数:
- 使用
<stdarg.h>
头文件提供的宏,例如va_start
、va_arg
、和va_end
。 - 缺点是类型不安全,需要手动管理参数的类型和数量。
- 使用
-
C++中的可变参数模板:
- 引入了可变参数模板,允许在编译时处理可变数量和类型的参数。
- 提高了类型安全性,无需手动管理参数的类型和数量。
- 可以使用递归展开、折叠表达式、逗号表达式等多种方式展开可变参数包。
-
递归展开:
- 使用递归函数模板,逐一处理参数包中的每个参数。
- 通过终止条件和递归调用来展开参数包。
-
折叠表达式:
- 在C++17及以后版本引入,提供了更紧凑的语法。
- 使用
(expression op ...)
的形式,其中op
可以是二元运算符。
-
逗号表达式:
- 使用逗号表达式展开参数包时,返回值不能直接用于数组初始化,通常使用0作为占位符。
- 可以通过逗号表达式在一行中执行多个操作。
在选择可变参数的处理方式时,应根据具体情况和编程需求来选择最合适的方法。C++中的可变参数模板提供了更强大、类型安全和灵活的处理方式,但对于特定情况,C语言中的可变参数也可能是一种合适的选择。最终,可变参数的使用应该以代码的清晰性、可读性和维护性为重要考虑因素。
最后,码文不易,如果文章对你有所帮助的话,不妨点上一个👍,感谢大佬的支持!