更多精彩内容.....
🎉❤️播主の主页✨😘
Stark、-CSDN博客
本文所在专栏:
C系列语法知识_Stark、的博客-CSDN博客
其它专栏:
数据结构与算法_Stark、的博客-CSDN博客
C系列项目实战_Stark、的博客-CSDN博客
座右铭:梦想是一盏明灯,照亮我们前行的路,无论风雨多大,我们都要坚持不懈。
泛型的意思就是广泛的类型。泛型编程是C++很强大的一个特性。它主要的一个目的是增加代码复用性,增加程序的可扩展性。C++的泛型编程主要靠模板来实现,模板又被分为两类:函数模板和类模板。在学习模板前,我先提出一个问题:请写出一个相加函数。
你可能下意识地就写出来了:
int add(int a,int b){return a+b;
}
现在我来实验一下:
int main(){cout<<add(3.0,4.5)<<endl;//预期7.5//实际输出7return 0;
}
你觉得我这是在挑刺,但是事实就是这样,客户就需要你写出来一个能做任何类型都能相加的一个函数。你就无奈的去写去改去增加。 为了解决反复更改增加这一问题,我们应该使用C++为我们提供的模板技术来应用到编程上。这时候我就可以写一个函数:
//template<class T>
template<typename T>//用哪个关键字都一样
T& add(const T& a,const T& b)
{return a+b;
}
在使用时我们就可以指定类型了:
int main()
{cout<<add<int>(1,3)<<endl;cout<<add<double>(3.14,6.28)<<endl;cout<<add<string>("123","321")<<endl;return 0;
}
我们只需要写一段函数代码,就可以实现之前需要定义多个函数需要干的事,是不是很方便。
另外,我们前面实现vector时就通篇使用了模板的泛型编程思想。包括我们使用的std::vector都离不开模板的支持。
类属:类型参数化,又称参数模板。使得程序(算法)可以从逻辑功能上抽象,把被处理的对象(数据)类型作为参数传递。
模板把函数或类要处理的数据类型参数化,表现为参数的多态性,称为类属
模板用于表达逻辑结构相同,但具体数据元素类型不同的数据对象的通用行为。
一、函数模板
C++提供了函数模板(function template)。所谓函数模板,实际上是建立一个通用函数,其函数类型和形参类型不具体指定,用一个虚拟的类型来代表。这个通用函数就称为函数模板。凡是函数体相同的函数都可以用这个模板来代替,不必定义多个函数,只需在模板中定义一次即可。在调用函数时系统会根据实参的类型来取代模板中的虚拟类型,从而实现了不同函数的功能。
1.函数模板的定义
template<typename _Tx,typename _Ty, ...... ,typename _Tn>
返回值类型 函数名(参数列表){//函数体//return;
}
Tips:
①template关键字告诉C++编译器:我要开始泛型了,你不要随便报错
②typename _Tx,_Ty,....._Tn 为模板的参数列表,使用尖括号<>包围,列表不能为空
③typename只可以被class代替
④_Tx,_Ty,......, _Tn等表示类型,可以像函数的缺省参数一样给出缺省类型
template<typename Tx,typename Ty> //true
template<class Tx,class Ty> //true
template<class Tx,typename Ty> //truetemplate<class Tx,Ty,Tz> //false
template<typename Tx,Ty> //falsetemplate<class Tx,class Ty=int> //true
以上语法就相当于一个修饰符,作为函数的修饰,使得函数认识修饰中提到的类型。
函数模板定义由模板说明和函数定义组成。模板说明的类属参数必须在函数定义中至少出现一次。函数参数列表中可以使用类属类型参数,也可以使用一般类型参数。
2.函数模板的使用(实例化)
通过为函数模板的参数列表赋予具体类型变成模板函数的过程叫做实例化。模板函数包括显式调用与隐式调用。
自动数据类型推导(隐式调用)
int main(){int ia=1,ib=2;double da=10.3,db=20.5;//true,函数模板自动推算出T=intadd(ia,ib);//true,函数模板自动推算出T=doubleadd(da,db);//false,函数模板没有理解你的T到底是什么意思add(ia,db);//true,函数模板自动推算出T=intadd(ia,(int)da);return 0;
}
显式类型调用
//函数模板 是一个模板,等待被实例化
template<typename T>
T& add(const T& a,const T& b)
{return a+b;
}int main(){int a=3;double b=10.5;//true,模板已经确定了T为int,就会尝试将所有参数类型转化为intadd<int>(a,b);//模板函数是一个函数,由函数模板实例化得到return 0;
}
3.模板函数遇上函数重载
函数模板和普通函数的区别:函数模板不允许自动类型转化,普通函数能够进行自动类型转换
模板函数和普通函数在一起时的调用规则:
1.函数模板可以像普通函数一样被重载
2.C++编译器优先考虑普通函数
3.如果函数模板可以产生更好的匹配,那么选择模板
4.可以通过空模板实参列表的语法限定编译器只能通过模板匹配
#include <iostream>
using namespace std;template <typename T>
void myswap(T &a, T &b)
{T t;t = a;a = b;b = t;cout<<"myswap 模板函数do"<<endl;
}void myswap(char &a, int &b)
{int t;t = a;a = b;b = t;cout<<"myswap 普通函数do"<<endl;
}void main()
{char cData = 'a';int iData = 2;//myswap<int>(cData, iData); //结论 函数模板不提供隐式的数据类型转换 必须是严格的匹配myswap(cData, iData); //myswap(iData, cData);cout<<"hello..."<<endl;system("pause");return ;
}
#include "iostream"
using namespace std;
int Max(int a, int b)
{cout<<"int Max(int a, int b)"<<endl;return a > b ? a : b;
}template<typename T>
T Max(T a, T b)
{cout<<"T Max(T a, T b)"<<endl;return a > b ? a : b;
}template<typename T>
T Max(T a, T b, T c)
{cout<<"T Max(T a, T b, T c)"<<endl;return Max(Max(a, b), c);
}void main()
{int a = 1;int b = 2;cout<<Max(a, b)<<endl; //当函数模板和普通函数都符合调用时,优先选择普通函数cout<<Max<>(a, b)<<endl; //若显示使用函数模板,则使用<> 类型列表cout<<Max(3.0, 4.0)<<endl; //如果 函数模板产生更好的匹配 使用函数模板cout<<Max(5.0, 6.0, 7.0)<<endl; //重载cout<<Max('a', 100)<<endl; //调用普通函数 可以隐式类型转换 system("pause");return ;
}
C++编译器模板机制剖析
思考:为什么函数模板可以和函数重载放在一起。C++编译器是如何提供函数模板机制的?
编译器编译原理
①什么是gcc
gcc(GNU C Compiler)编译器的作者是Richard Stallman,也是GNU项目的奠基者。 |
什么是gcc:gcc是GNU Compiler Collection的缩写。最初是作为C语言的编译器(GNU C Compiler),现在已经支持多种语言了,如C、C++、Java、Pascal、Ada、COBOL语言等。 |
gcc支持多种硬件平台,甚至对Don Knuth 设计的 MMIX 这类不常见的计算机都提供了完善的 |
②gcc主要特征
1)gcc是一个可移植的编译器,支持多种硬件平台
2)gcc不仅仅是个本地编译器,它还能跨平台交叉编译。
3)gcc有多种语言前端,用于解析不同的语言。
4)gcc是按模块化设计的,可以加入新语言和新CPU架构的支持
5)gcc是自由软件
③gcc编译过程
预处理(Pre-Processing)
编译(Compiling)
汇编(Assembling)
链接(Linking)
Gcc *.c –o 1exe (总的编译步骤)
Gcc –E 1.c –o 1.i //宏定义 宏展开
Gcc –S 1.i –o 1.s
Gcc –c 1.s –o 1.o
Gcc 1.o –o 1exe
结论:gcc编译工具是一个工具链。。。。
hello程序是一个高级C语言程序,这种形式容易被人读懂。为了在系统上运行hello.c程序,每条C语句都必须转化为低级机器指令。然后将这些指令打包成可执行目标文件格式,并以二进制形式存储器于磁盘中。
④gcc常用编译选项
选项 | 作用 |
-o | 产生目标(.i、.s、.o、可执行文件等) |
-c | 通知gcc取消链接步骤,即编译源码并在最后生成目标文件 |
-E | 只运行C预编译器 |
-S | 告诉编译器产生汇编语言文件后停止编译,产生的汇编语言文件扩展名为.s |
-Wall | 使gcc对源文件的代码有问题的地方发出警告 |
-Idir | 将dir目录加入搜索头文件的目录路径 |
-Ldir | 将dir目录加入搜索库的目录路径 |
-llib | 链接lib库 |
-g | 在目标文件中嵌入调试信息,以便gdb之类的调试程序调试 |
练习
gcc -E hello.c -o hello.i(预处理) gcc -S hello.i -o hello.s(编译) gcc -c hello.s -o hello.o(汇编) gcc hello.o -o hello(链接) 以上四个步骤,可合成一个步骤 gcc hello.c -o hello(直接编译链接成可执行目标文件) gcc -c hello.c或gcc -c hello.c -o hello.o(编译生成可重定位目标文件) |
建议初学都加这个选项。下面这个例子如果不加-Wall选项编译器不报任何错误,但是得到的结果却不是预期的。 #include <stdio.h> int main(void) { printf("2+1 is %f", 3); return 0; } |
Gcc编译多个.c |
hello_1.h hello_1.c main.c 一次性编译 gcc hello_1.c main.c –o newhello 独立编译 gcc -Wall -c main.c -o main.o gcc -Wall -c hello_1.c -o hello_fn.o gcc -Wall main.o hello_1.o -o newhello |
模板函数反汇编观察
命令:g++ -S 7.cpp -o 7.s
.file "7.cpp"
.text
.def __ZL6printfPKcz; .scl 3; .type 32; .endef
__ZL6printfPKcz:
LFB264:
.cfi_startproc
pushl %ebp
.cfi_def_cfa_offset 8
.cfi_offset 5, -8
movl %esp, %ebp
.cfi_def_cfa_register 5
pushl %ebx
subl $36, %esp
.cfi_offset 3, -12
leal 12(%ebp), %eax
movl %eax, -12(%ebp)
movl -12(%ebp), %eax
movl %eax, 4(%esp)
movl 8(%ebp), %eax
movl %eax, (%esp)
call ___mingw_vprintf
movl %eax, %ebx
movl %ebx, %eax
addl $36, %esp
popl %ebx
.cfi_restore 3
popl %ebp
.cfi_restore 5
.cfi_def_cfa 4, 4
ret
.cfi_endproc
LFE264:
.lcomm __ZStL8__ioinit,1,1
.def ___main; .scl 2; .type 32; .endef
.section .rdata,"dr"
LC0:
.ascii "a:%d b:%d \12\0"
LC1:
.ascii "c1:%c c2:%c \12\0"
LC2:
.ascii "pause\0"
.text
.globl _main
.def _main; .scl 2; .type 32; .endef
_main:
LFB1023:
.cfi_startproc
.cfi_personality 0,___gxx_personality_v0
.cfi_lsda 0,LLSDA1023
pushl %ebp
.cfi_def_cfa_offset 8
.cfi_offset 5, -8
movl %esp, %ebp
.cfi_def_cfa_register 5
andl $-16, %esp
subl $32, %esp
call ___main
movl $0, 28(%esp)
movl $10, 24(%esp)
movb $97, 23(%esp)
movb $98, 22(%esp)
leal 24(%esp), %eax
movl %eax, 4(%esp)
leal 28(%esp), %eax
movl %eax, (%esp)
call __Z6myswapIiEvRT_S1_ //66 ===>126
movl 24(%esp), %edx
movl 28(%esp), %eax
movl %edx, 8(%esp)
movl %eax, 4(%esp)
movl $LC0, (%esp)
call __ZL6printfPKcz
leal 22(%esp), %eax
movl %eax, 4(%esp)
leal 23(%esp), %eax
movl %eax, (%esp)
call __Z6myswapIcEvRT_S1_ //77 ===>155
movzbl 22(%esp), %eax
movsbl %al, %edx
movzbl 23(%esp), %eax
movsbl %al, %eax
movl %edx, 8(%esp)
movl %eax, 4(%esp)
movl $LC1, (%esp)
call __ZL6printfPKcz
movl $LC2, (%esp)
LEHB0:
call _system
LEHE0:
movl $0, %eax
jmp L7
L6:
movl %eax, (%esp)
LEHB1:
call __Unwind_Resume
LEHE1:
L7:
leave
.cfi_restore 5
.cfi_def_cfa 4, 4
ret
.cfi_endproc
LFE1023:
.def ___gxx_personality_v0; .scl 2; .type 32; .endef
.section .gcc_except_table,"w"
LLSDA1023:
.byte 0xff
.byte 0xff
.byte 0x1
.uleb128 LLSDACSE1023-LLSDACSB1023
LLSDACSB1023:
.uleb128 LEHB0-LFB1023
.uleb128 LEHE0-LEHB0
.uleb128 L6-LFB1023
.uleb128 0
.uleb128 LEHB1-LFB1023
.uleb128 LEHE1-LEHB1
.uleb128 0
.uleb128 0
LLSDACSE1023:
.text
.section .text$_Z6myswapIiEvRT_S1_,"x"
.linkonce discard
.globl __Z6myswapIiEvRT_S1_
.def __Z6myswapIiEvRT_S1_; .scl 2; .type 32; .endef
__Z6myswapIiEvRT_S1_: //126
LFB1024:
.cfi_startproc
pushl %ebp
.cfi_def_cfa_offset 8
.cfi_offset 5, -8
movl %esp, %ebp
.cfi_def_cfa_register 5
subl $16, %esp
movl 8(%ebp), %eax
movl (%eax), %eax
movl %eax, -4(%ebp)
movl 12(%ebp), %eax
movl (%eax), %edx
movl 8(%ebp), %eax
movl %edx, (%eax)
movl 12(%ebp), %eax
movl -4(%ebp), %edx
movl %edx, (%eax)
leave
.cfi_restore 5
.cfi_def_cfa 4, 4
ret
.cfi_endproc
LFE1024:
.section .text$_Z6myswapIcEvRT_S1_,"x"
.linkonce discard
.globl __Z6myswapIcEvRT_S1_
.def __Z6myswapIcEvRT_S1_; .scl 2; .type 32; .endef
__Z6myswapIcEvRT_S1_: //155
LFB1025:
.cfi_startproc
pushl %eb
.cfi_def_cfa_offset 8
.cfi_offset 5, -8
movl %esp, %ebp
.cfi_def_cfa_register 5
subl $16, %esp
movl 8(%ebp), %eax
movzbl (%eax), %eax
movb %al, -1(%ebp)
movl 12(%ebp), %eax
movzbl (%eax), %edx
movl 8(%ebp), %eax
movb %dl, (%eax)
movl 12(%ebp), %eax
movzbl -1(%ebp), %edx
movb %dl, (%eax)
leave
.cfi_restore 5
.cfi_def_cfa 4, 4
ret
.cfi_endproc
LFE1025:
.text
.def ___tcf_0; .scl 3; .type 32; .endef
___tcf_0:
LFB1027:
.cfi_startproc
pushl %ebp
.cfi_def_cfa_offset 8
.cfi_offset 5, -8
movl %esp, %ebp
.cfi_def_cfa_register 5
subl $8, %esp
movl $__ZStL8__ioinit, %ecx
call __ZNSt8ios_base4InitD1Ev
leave
.cfi_restore 5
.cfi_def_cfa 4, 4
ret
.cfi_endproc
LFE1027:
.def __Z41__static_initialization_and_destruction_0ii; .scl 3; .type 32; .endef
__Z41__static_initialization_and_destruction_0ii:
LFB1026:
.cfi_startproc
pushl %ebp
.cfi_def_cfa_offset 8
.cfi_offset 5, -8
movl %esp, %ebp
.cfi_def_cfa_register 5
subl $24, %esp
cmpl $1, 8(%ebp)
jne L11
cmpl $65535, 12(%ebp)
jne L11
movl $__ZStL8__ioinit, %ecx
call __ZNSt8ios_base4InitC1Ev
movl $___tcf_0, (%esp)
call _atexit
L11:
leave
.cfi_restore 5
.cfi_def_cfa 4, 4
ret
.cfi_endproc
LFE1026:
.def __GLOBAL__sub_I_main; .scl 3; .type 32; .endef
__GLOBAL__sub_I_main:
LFB1028:
.cfi_startproc
pushl %ebp
.cfi_def_cfa_offset 8
.cfi_offset 5, -8
movl %esp, %ebp
.cfi_def_cfa_register 5
subl $24, %esp
movl $65535, 4(%esp)
movl $1, (%esp)
call __Z41__static_initialization_and_destruction_0ii
leave
.cfi_restore 5
.cfi_def_cfa 4, 4
ret
.cfi_endproc
LFE1028:
.section .ctors,"w"
.align 4
.long __GLOBAL__sub_I_main
.ident "GCC: (rev2, Built by MinGW-builds project) 4.8.0"
.def ___mingw_vprintf; .scl 2; .type 32; .endef
.def _system; .scl 2; .type 32; .endef
.def __Unwind_Resume; .scl 2; .type 32; .endef
.def __ZNSt8ios_base4InitD1Ev; .scl 2; .type 32; .endef
.def __ZNSt8ios_base4InitC1Ev; .scl 2; .type 32; .endef
.def _atexit; .scl 2; .type 32; .endef
函数模板机制结论
1.编译器并不是把函数模板处理成能够处理任意类的函数
2.编译器从函数模板通过具体类型产生不同的函数
3.编译器会对函数模板进行两次编译
4.在声明的地方对模板代码本身进行编译;在调用的地方对参数替换后的代码进行编译。
二、类模板
类模板用于实现类所需数据的类型参数化;类模板在表示如数组、表、图等数据结构显得特比重要。这些数据结构不受所包含的元素类型的影响。最成功的案例也就是我们使用的STL的容器。
1.单个类模板语法
//类的类型参数化 抽象的类
//单个类模板
template<typename T>
class A
{
public:A(T t){this->t = t;}T& getT(){return t;}
protected:
public:T t;
};
void main()
{//模板了中如果使用了构造函数,则遵守以前的类的构造函数的调用规则A<int> a(100); a.getT();printAA(a);return ;
}
2.继承中的类模板语法
结论: 子类从模板类继承的时候,需要让编译器知道 父类的数据类型具体是什么(数据类型的本质:固定大小内存块的别名)A<int>
class B : public A<int>
{
public:B(int i) : A<int>(i){}void printB(){cout<<"A:"<<t<<endl;}
protected:
private:
};//模板与上继承
//怎么样从基类继承
//若基类只有一个带参数的构造函数,子类是如何启动父类的构造函数
void pintBB(B &b)
{b.printB();
}
void printAA(A<int> &a) //类模板做函数参数
{//a.getT();
}void main()
{A<int> a(100); //模板了中如果使用了构造函数,则遵守以前的类的构造函数的调用规则 a.getT();printAA(a);B b(10);b.printB();cout<<"hello..."<<endl;system("pause");return ;
}
3.类模板语法知识体系梳理
①类模板函数写在类的内部:正常写
②类模板函数写在类的外部(在一个.cpp中)
//构造函数 没有问题
//普通函数 没有问题
//友元函数:用友元函数重载 << >>
//friend ostream& operator<< <T> (ostream &out, Complex<T> &c3) ;
//友元函数:友元函数不是实现函数重载(非 << >>)
//1)需要在类前增加 类的前置声明 函数的前置声明
template<typename T>
class Complex; template<typename T>
Complex<T> mySub(Complex<T> &c1, Complex<T> &c2);//2)类的内部声明 必须写成:
friend Complex<T> mySub <T> (Complex<T> &c1, Complex<T> &c2);
//3)友元函数实现 必须写成:
template<typename T>
Complex<T> mySub(Complex<T> &c1, Complex<T> &c2)
{Complex<T> tmp(c1.a - c2.a, c1.b-c2.b);return tmp;
}
//4)友元函数调用 必须写成
Complex<int> c4 = mySub<int>(c1, c2);
cout<<c4;
结论:友元函数只用来进行 左移 友移操作符重载。
③类模板函数卸载类的外部(在不同的.h和.cpp中)
也就是类模板函数说明和类模板实现分开写。比如在头文件写模板类的声明定义,成员函数只声明未实现,在源程序文件中进行函数的实现。
此时如果我们像往常一样包含头文件,在编译时的链接阶段会报错。解决方法有两种:
1.将.cpp文件一同包含
2.将两个文件写到一个.hpp文件中(.hpp只是一个约定俗成的模板类头文件后缀名)
4.类模板中的static关键字
- 从类模板实例化的每个模板类有自己的类模板数据成员,该模板类的所有对象共享一个static数据成员
- 和非模板类的static数据成员一样,模板类的static数据成员也应该在文件范围定义和初始化
- 每个模板类有自己的类模板的static数据成员副本
#include<bits/stdc++.h>
using namespace std;const double pi = 3.14159;
template<class T>
class Circle {T radius;static int total;//类模板的静态数据成员
public:Circle(T r = 0):radius(r){tatal++;}void Set_Radius(T r) { radius = r; }double Get_Radius() { return radius; }double Get_Girth() { return 2 * pi * radius; }double Get_Area() { return pi * radius * radius; }static int ShowTotal();//类模板的静态成员函数
};
template<class T>
int Circle<T>::total = 0;
template<class T>
int Circle<T>::ShowTotal() { return total; }
void test241004_01() {Circle<int> A, B;A.Set_Radius(16);B.Set_Radius(105);cout << "who\tRadius\tGirth\tArea" << endl;cout << "A\t" << A.Get_Radius() << "\t" << A.Get_Girth() << "\t" << A.Get_Area() << endl;cout << "B\t" << B.Get_Radius() << "\t" << B.Get_Girth() << "\t" << B.Get_Area() << endl;cout << "int Total=" << Circle<int>::ShowTotal() << endl;//cout << "Total=" << B.ShowTotal() << endl;//cout << "Total=" << A.ShowTotal() << endl;
}
void test241004_02() {Circle<double> X(6.23), Y(10.5), Z(25.6);cout << "who\tRadius\tGirth\tArea" << endl;cout << "X\t" << X.Get_Radius() << "\t" << X.Get_Girth() << "\t" << X.Get_Area() << endl;cout << "Y\t" << Y.Get_Radius() << "\t" << Y.Get_Girth() << "\t" << Y.Get_Area() << endl;cout << "Z\t" << Z.Get_Radius() << "\t" << Z.Get_Girth() << "\t" << Z.Get_Area() << endl;cout << "int Total=" << Circle<int>::ShowTotal() << endl;cout << "double Total=" << Circle<double>::ShowTotal() << endl;
}
我们明显看到Circle<int>和Circle<double>属于两个类,他们都有属于自己的static成员total,互不干扰。
5.类模板在项目开发中的应用
- 模板是C++类型参数化的多态工具。C++提供函数模板和类模板。
- 模板定义以模板说明开始。类属参数必须在模板定义中至少出现一次。
- 同一个类属参数可以用于多个模板。
- 类属参数可用于函数的参数类型、返回类型和声明函数中的变量。
- 模板由编译器根据实际数据类型实例化,生成可执行代码。实例化的函数。
模板称为模板函数;实例化的类模板称为模板类。
- 函数模板可以用多种方式重载。
- 类模板可以在类层次中使用 。
*.模板特化与可变参数模板
模板特化
模板特化分为全特化与偏特化。偏特化又分为:部分特化与限制特化
template<class _Tx,class _Ty>
class Data{_Tx m_dx;_Ty m_dy;
public:Data(){cout<<"Data<_Tx , _Ty>"<<endl;}
};//全特化:将类属全部确定
template<>
class Data<int,char>
{int m_dx;char m_dy;
public:Data(){cout<<"Data<int,char>"<<endl;}
};//偏特化:确定类属中部分类型
//部分特化
template<class T>
class Data<T,int>
{T m_dx;int m_dy;
public:Data(){cout<<"Data<T,int>"<<endl;}
};
//限制特化
template<class M,class N>
class Data<M* , N*>
{/*略*/};
template<class M,class N>
class Data<M& ,N&>
{/*略*/};
可变参数模板
C++可变参数模板(Variadic Templates)是C++11引入的一种功能,允许你定义接受可变数量的参数的模板。这种特性非常强大,可以用来编写更灵活和通用的代码,尤其是在处理函数、类等时。
template<typename... Args>
void func(Args... args) { // 函数体
}
这里,Args
是一个类型参数包,可以接受任意数量的类型。args
是一个参数包,可以接收任意数量的参数。
递归展开参数包:
#include <iostream>
#include <string> template<typename T>
void print(const T& value) { std::cout << value << std::endl;
} template<typename T, typename... Args>
void print(const T& first, const Args&... rest) { std::cout << first << std::endl; print(rest...); // 递归调用
} int main() { print(1, 2.5, "Hello", std::string("World")); // 可以输入多种类型 return 0;
}
在上面的例子中,print
函数可以接受任意数量和类型的参数,并且能够逐个打印它们的值。在 C++11 中,由于没有逗号表达式的折叠表达式的特性,我们只能通过递归方式来处理参数包。
逗号表达式展开参数包 :
#include <iostream> // 打印单个值的函数模板
template<typename T>
void print(const T& value) { std::cout << value << std::endl;
} // 使用逗号表达式展开参数包的函数模板
template<typename... Args>
void printAll(const Args&... args) { // 使用逗号表达式展开,以执行打印 (print(args), ...); // 展开参数包并使用逗号表达式
} int main() { printAll(1, 2.5, "Hello", std::string("World")); return 0;
}
在上述代码中,printAll
函数接受一个可变数量的参数,并使用(print(args), ...)
的形式展开这些参数。这是一个**折叠表达式(Fold Expression)**的例子,它是 C++17 中引入的新特性。逗号运算符在这里允许我们对 print(args)
的每个调用依次执行,从而打印出所有参数的内容。
重要特性
递归解包:通过递归模板函数,你可以将参数包逐个提取和处理。
类型推断:编译器可以自动推导传入参数的类型。
使用标准库功能:可以与标准库中的功能(如
std::tuple
,std::index_sequence
等)结合使用,以实现更复杂的功能。
感谢大家!