一、可变参数列表
1.1 什么是可变参数
在C语言中printf函数原型如下:
int printf(const char* format, ...);
上面的三个点就是可变参数,那就是可变的参数。
1.2 可变参数四个宏
typedef char* va_list;
#define _INTSIZEOF(n) ( (sizeof(n) + sizeof(int) - 1 ) & ~(sizeof(int) - 1) )
#define va_start(ap, v) ( ap = (va_list)&v + _INTSIZEOF(n) )
#define va_arg(ap, t) ( *(t *) ((ap += _INTSIZEOF(n)) - _INTSIZEOF(n)) )
#define va_end(ap) ( ap = (va_list)0 )
1.2.1 _INTSIZEOF(n)
_INTSIZEOF(n)是用于内存对齐的,先说句题外的
----------------------------------------------------------------------------------------------------------
对于整数向上向下取整的问题,
向下取整
int(num / den);
向上取整
int( (num + den - 1) / den)
// 不能 int( num / den ) + 1,4/4这种整除的会出错
对于分数保留小数点后几位的问题
// 保留小数点后3位
double( int( ((double)num / den) * 10000 + 5 ) / 10 ) / 1000;
-----------------------------------------------------------------------------------------------------------------------
然后回到 _INTSIZEOF(n) 就是对4字节取整,按照向上取证的方法,应该
(sizeof(type) + sizeof(int) - 1) / sizeof(int) * sizeof(int);
但是sizeof(int)是4字节,标准的二进制100,所以可以除完再乘可以直接转换成移位,变成
((sizeof(type) + sizeof(int) - 1) >> 2) << 2
然而,你会发现做了这么多,其实就是把(sizeof(type) + sizeof(int) - 1)低两位置00,就有了
( (sizeof(n) + sizeof(int) - 1 ) & ~(sizeof(int) - 1) )
所以,_INTSIZEOF(n)就是找到大于n的最小的4的倍数。不过记得内存分布是可调的,有效对齐不一定是4,也可能不对齐,也可能对齐别的数值,确实在stdarg.h文件中没找到这个定义,不过网上找打的资料说是这个,就先这个了。
1.2.2 va_start(ap, v)
先看看一个例子看看四个宏怎么用的(例子来源于可变参数函数原理)
#include <stdarg.h>
int FindMax(int num, ...)
{
va_list arg;
va_start(arg, num);
int max = va_arg(arg, int);
for (int i = 0; i < num - 1; i++)
{
int x = va_arg(arg, int);
if (max < x)
{
max = x;
}
}
va_end(arg);
return max;
}
int main()
{
int max = FindMax(5, 1, 2, 3, 9, 7);
printf("max = %d\n", max);
system("pause");
return 0;
}
va_start将arg指针指向第一个参数的起始位置(第一个参数不是int num, 而是可变参数的第一个参数,就是5 1 2 3 9 7的1位置。)
va_start原理也很简单,就是取num地址,然后加上num类型对齐后占的内存,然后此时arg指向第一个可变参数。
1.2.3 va_arg(ap, t)
原理就是先把ap加上t类型内存占用的内存大小,然后转换成t类型返回结果。
1.2.4 va_end(ap)
ap置空变成空指针。
1.3 模板中的可变参数
本节内容几乎全部来自:
【C++ 泛型编程 进阶篇】C++ 可变参数模板的妙用:解决参数不足问题
可变参数是不是很简单,然而到了c++11中,模板编程中的可变参数就不是用四个宏,而是一种折叠表达式用法+递归(折叠表达式详见:折叠表达式(C++17 起))。
1.3.1 可变参数的基本概念
变参数模板,顾名思义就是参数个数和类型都可能发生变化的模板,要实现这一点,那就必须要使用模板形参包。模板形参包主要有三种,即:非类型模板形参包、类型模板形参包、模板模板形参包,函数参数包。
- 非类型模板参数包:类型选择一个固定的类型,args其实是一个可修改的参数名。例如:
template<typename... args>
// 例如 template<int... data> xxx;
这里,args 是一个模板参数包,可以接受任意数量的类型参数。但是typename仅限为整型、指针和引用
- 类型模板参数包:可以接受任意数量的类型参数。例如:
template<typename... Args> xxx;
这里,Args 是一个函数参数包,可以接受任意数量类型的参数。
-
模板模板形参包:形参包本身它也是一个模板。例如:
template < 形参列表 > class ... Args(可选)
说真的,这个没明白,以后再说。
- 函数参数包:函数参数包也可以包含零个或多个函数参数。例如:
template<typename... Args>
void my_function(Args... args) {
// ...
}
1.3.2 sizeof...()获得参数数量
sizeof… 是一个编译时操作符,可以用于获取参数包中元素的数量。
template<typename... Args>
size_t count_args(Args... args) {
return sizeof...(args);
}
int main() {
std::cout << count_args(1, 2.0, "Hello", 3.14) << std::endl; // 输出:4
return 0;
}
1.3.3 递归展开参数包
#include <iostream>
// 基本情况:这是必须的,处理无参数的情况,否则编译器会找不到匹配函数
void print() {}
// 递归情况:处理一个参数,并递归调用剩余参数
template<typename T, typename... Args>
void print(T first, Args... rest) {
std::cout << first << " ";
print(rest...); // 递归调用
}
int main() {
print(1, 2.0, "Hello", 3.14);
return 0;
}
1.3.4 折叠表达式(C++17)
C++17引入,可以简化一些递归的表达式(简单来说就是将看不懂的递归变成看不懂的运算符解包)
折叠表达式将分为四种解包规则:
- 一元右折叠:(E op ...) = ( E1 op ( ... op (EN-1 op En) ) )
- 一元左折叠:(... op E) = ( ( (E1 op E2) op ... ) op EN)
- 二元右折叠:(E op ... op I) = (E1 op (... op (EN−1 op (EN op I) ) ) )
- 二元左折叠:(I op ... op E) = ( ( ( (I op E1) op E2) op ...) op EN)
关于op有哪些操作符详见https://www.apiref.com/cpp-zh/cpp/language/fold.html
然后这个例子
#include <iostream>
using namespace std;
template<class type>
void printAmt(int &iSumAge){
return;
}
template<class type, int age0, int ... age>
void printAmt(int &iSumAge){
iSumAge += age0;
if ( (sizeof ... (age)) > 0 ){
printAmt<type, age...>(iSumAge);
}
}
int main(){
int sumAge = 0;
printAmt<int,1,2,3,4,5,7,6,8>(sumAge);
cout << "the sum of age is " << sumAge << endl;
return 0;
}
就可以这样写
#include <iostream>
using namespace std;
template<class type, int ... age>
void printAmt(int &iSumAge){
iSumAge = (age + ...);
}
int main(){
int sumAge = 0;
printAmt<int,1,2,3,4,5,7,6,8>(sumAge);
cout << "the sum of age is " << sumAge << endl;
return 0;
}
非常的简洁(反正咋都看不懂....)
然后看看C++17 折叠表达式中一个很坑的例子。
c++ 如何实现 x in (1,2,3)
这样的运算?c++17之前 不难写出一个递归的模板函数
template <class Target>
bool In(Target&& /**/) {
return false;
}
template <class Target, class First, class... Args>
bool In(Target&& t, First&& f, Args&&... args) {
return t == f || In(std::forward<Target>(t), std::forward<Args>(args)...);
}
这种场景就非常适合 折叠表达式优化
template <class Target, class... Args>
bool In(Target&& t, Args&&... args) {
return ((t == args) || ...);
}
但是这么简单个例子里面其实也藏着很多 细节(坑)
||
运算符在参数包为空时返回值为false
,(&&
为true
)t == args
要用 括号包裹,因为==
优先级高于||
,E
或者I
中有任何优先级高于op
的操作符都要用括号。当然建议不要记,统统加括号准没错(当然你会发现不加括号根本编译不通过,总之加括号就对了)。
最后,为什么模板中用右值引用,见引用折叠、万能引用、完美转发
二、define的使用
define的用法就相对简单点,可以分为:无参宏、有参宏和变参宏。而宏定义中还存在一些特殊的符号比如#、##和#@。
2.1 无参宏
无参宏就很简单,就定义
#define PIE 3.14
2.2 有参宏
就是包含参数的宏
#define puls(x) x*x
上面这个例子实际上是有问题的,有参宏需要注意加括号,不加括号可能会有问题
plus(1+2)
// 希望计算 3*3
// 但是实际上会变成 1+2*1+2
2.3 变参宏
__VA_ARGS__ 是一个可变参数宏,与省略号 ... 配合使用,用来替换省略号所代表的字符串。
#define P(...) printf(__VA_ARGS__)
// 例如:P("1%d", 2);
// 也可以这样
#define P(fmt, ...) printf(__VA_ARGS__)
宏前面加上##的作用在于,当可变参数的个数为0时,这里的##起到把前面多余的”,”去掉的作用,否则会编译会报错(GUN中必须要## __VA_ARGS__否则编译会报错,但是visual studio不会报错且可以正常运行)。
2.4 #符号
将输入变成字符串,例如
#define TOSTRING(x) #x
cout << TOSTRING(123) << endl; // 输出123
cout << sizeof(TOSTRING(123)) << endl; // 4
2.5 ##符号
字符拼接,例如
#define CONCAT(a, b) a##b
int x1 = 1;
cout << CONCAT(x, 1); // 输出1
cout << CONCAT("hello ", "world"); // 输出hello world
2.6 #@符号
转化成字符。例如
#define TOCHAR(x) #@x
cout << TOCHAR(1) << endl; // 1
cout << (int)TOCHAR(1) << endl; // 49