C++11 详解版本1.0
目录
🌟1. C++11的大优势
🌟2、列表初始化
🌟3、变量类型推导
一、为什么需要类型推导
二、decltype类型推导(了解)
🌟4、final 与 override
🌟5. 左值和右值
🌟6. 左值引用和右值引用
🌟7. 左值引用和右值引用的真正用途(移动构造)
🌟8. 移动赋值
🌟9. 完美转发
一、没有完美转发的问题
二、完美转发的必要性
🌟10. 新的类中成员函数
🌟11. lambda表达式
1. 核心概念
2. 基础语法
一、捕获列表详解
1.1捕获的细节点
二、参数详解
三、mutable使用
四、返回类型
🌟12. lambda实际应用
🌟13. delete与default
🌟14. 可变参数模板
14.1 递归函数方式展开参数包(配运行截图)
🌟15. 包装器
🌟16. 完结
声明: 本周(2025.3.18——3.23)内此文章将持续进行优化,但是内容整体不变
(时间太紧实在太忙 T . T)
🌟1. C++11的大优势
相比于C++98/03,C++11则带来了数量可观的变化,其中包含了约140个新特性,以及对C++03标准中 约600个缺陷的修正,这使得C++11更像是从C++98/03中孕育出的一种新语言。相比较而言,C++11能更好地用于系统开发和库开发、语法更加泛华和简单化、更加稳定和安全,不仅功能更强大,而且能提升程序员的开发效率,公司实际项目开发中也用得比较多,所以我们要作为一个 重点去学习。
🌟2、列表初始化
在C++11中,支持了万物(自定义类型/内置类型)都可以用{ } 进行初始化,并且可以省略 = 的书写
1、内置类型的列表初始化(写不写 = 都可以)
// 内置类型变量 int x1 = {10}; int x2{10};//建议使用原来的 int x3 = 1+2; int x4 = {1+2}; int x5{1+2}; // 数组 int arr1[5] {1,2,3,4,5}; int arr2[]{1,2,3,4,5}; // 动态数组,在C++98中不支持 int* arr3 = new int[5]{1,2,3,4,5}; // 标准容器 vector<int> v{1,2,3,4,5};//这种初始化就很友好,不用push_back一个一个插入 map<int, int> m{{1,1}, {2,2,},{3,3},{4,4}};
2、自定义类型的列表初始化
- 标准库支持单个对象的列表初始化
class Point { public: Point(int x = 0, int y = 0): _x(x), _y(y) {} private: int _x; int _y; }; int main() { Point p = { 1, 2 }; Point p{ 1, 2 };//不建议,但可以,看着混乱 return 0; }
2.多个对象的列表初始化
多个对象想要支持列表初始化,需给该类(模板类)添加一个带有initializer_list类型参数的构造函数即可。
注意:initializer_list是系统自定义的类模板,该类模板中主要有三个方法:begin()、end()迭代器以及获取区间中元素个数的方法size()class Date { public: Date(int year = 0, int month = 1, int day = 1) :_year(year) , _month(month) , _day(day) { cout << "这是日期类" << endl; } private: int _year; int _month; int _day; }; int main() { //C++11容器都实现了带有initializer_list类型参数的构造函数 vector<Date> vd = { { 2022, 1, 17 }, Date{ 2022, 1, 17 }, { 2022, 1, 17 } }; return 0; }
其中,v1[0],vd[1]都是类型为Date的对象
🌟3、变量类型推导
一、为什么需要类型推导
在定义变量时,必须先给出变量的实际类型,编译器才允许定义,但有些情况下可能不知道需要实际类型怎么给,或者类型写起来特别复杂
C++11中,可以使用auto来根据变量初始化表达式类型推导变量的实际类型,可以给程序的书写提供许多方便。将程序中c与it的类型换成auto,程序可以通过编译,而且更加简洁。
例如我们在使用顺序表的begin( )函数时
// 使用迭代器遍历容器, 迭代器类型太繁琐 可以使用auto //std::map<std::string, std::string>::iterator it = m.begin(); auto it = m.begin();
二、decltype类型推导(了解)
为什么需要decltype?
auto使用的前提是:必须要对auto声明的类型进行初始化,否则编译器无法推导出auto的实际类型。但有时候可能需要根据表达式运行完成之后结果的类型进行推导,因为编译期间,代码不会运行,此时auto也就无能为力。
decltype
decltype是根据表达式的实际类型推演出定义变量时所用的类型,如下->:
1、推演表达式类型作为变量的定义类型
int a = 10, b = 20; decltype(a + b)c; cout << typeid(c).name() << endl;
效果图如下->:
🌟4、final 与 override
final
1、final修饰类的时候,表示该类不能被继承
class A final { private: int _year; }; class B : public A //无法继承 { };
2、final修饰虚函数时,这个虚函数不能被重写
class A { public: virtual void fun() final//修饰虚函数 { cout << "this is A" << endl; } private: int _year; }; class B : public A { public: virtual void fun()//父类虚函数用final修饰,无法重写 { cout << "this is B" << endl; } };
![]()
override
检查派生类虚函数是否重写了基类某个虚函数,如果没有重写编译报错(写在派生类函数)
class A { public: virtual void fun() { cout << "this is A" << endl; } private: int _year; }; class B : public A { public: virtual void fun() override { cout << "this is B" << endl; } };
这里先展示重写了的->:
再展示没有重写的->:
override发现派生类fun1不是重写,报错
🌟5. 左值和右值
我们知道,在C++98中,是有引用的使用的,在C++11中新增了右值引用的语法,因此,我们之前所学的引用都叫做左值引用。无论是左值引用还是右值引用,都是给变量取别名
1.什么是左值?
左值是一个表示数据的表达式(如变量名和解引用的指针),我们可以获取它的地址,也可以对它赋值,左值可以出现在赋值符号的左边,右值不可以出现在左边。左引用加const修饰,不能对其赋值,但可取地址,是一种特殊情况。左值引用就是给左值取别名。
但是这一看都啥啊乱码七糟的,别急,我们解释一下->:
//以下都是左值 int* p = new int[10]; int a = 10; const int b = 20; //对左值的引用 int*& pp = p; int& pa = a; const int& rb = b;
总结一下->:
左值:
1、可以取地址
2、一般情况下可以修改(const修饰时不能修改)
2、什么是右值?
右值也是一个表示数据的表达式,如:字面常量、表达式返回值、传值返回函数的返回值(不能是左值引用返回)等,右值可以出现在赋值符号的右边,但是不能出现在左边。右值引用就是给右值取别名。
这一看又是啥啊,一堆乱码七糟的,别急,我们接着解释->:
double x = 1.1, y = 2.2; //常见右值 10; x + y; add(1, 2); //右值引用 int&& rr1 = 10; double&& rr2 = x + y; double && rr3 = add(1, 2); //右值引用一般情况不能引用左值,可使用move将一个左值强制转化为右值引用 int &&rr4 = move(x); //右值不能出现在左边,错误 10 = 1; x + y = 1.0; add(1, 2) = 1;
到此为止,我们先总结一下->:
右值都是一些生命周期极短的临时变量,如以下->:10;//字面常量(为临时变量->:不可修改,不可取地址) x+y;(运算都是返回的临时变量->:不可修改,不可取地址) func(x,y);(函数返回的都是临时变量->:不可修改,不可取地址) string("1111");(匿名对象,生命周期极短,临时变量->:不可修改,不可取地址)
左值:除了右值的就是左值
我们可以如上那么总结,但还是要记住,左值和右值的真正区别在于能否取地址
🌟6. 左值引用和右值引用
1.左值引用与右值引用的写法
我们上面提到过,无论左值引用还是右值引用,都是给变量取别名,所以不会开辟新的空间
从汇编层看,二者都是用指针实现,没什么区别
左值引用给左值起别名,右值引用给右值起别名
书写如下->:
int main() { int a = 10; int& pa = a;//左值引用,给左值起别名 int&& b = 10;//右值引用,给右值起别名 cout << b;//打印10 }
区别在于&的个数。我们知道,临时变量的生命周期基本都只有一行,那么常量10在这一行过去后,就彻底灰飞烟灭了,但是可以用右值引用延续它的数据,这也叫做"延长生命周期",这里先做了解,后面也会讲
2.左值引用,右值引用的细节点
首先,左值引用不可以引用右值,同理,右值引用也不可以引用左值,如下->:
那该怎么做?难道没有一种办法让左值引用可以引用右值,让右值引用可以引用左值吗?
当然有啦!!!!
左值引用 引用 右值
想要让左值引用引用右值,我们可以根据原理层出发,我们知道,右值都是临时变量,不可修改,那么我们让左值引用加上一个const修饰,左值引用就可以引用右值了,如下->:
右值引用引用左值
想要让右值引用引用左值,我们需要使用一个关键字move,它的作用是将左值强制转换为右值,但是move并不会生成临时变量如下->:
怕各位忘记,这里再次强调,move不会生成临时变量
延长生命周期
我们在上面提了一嘴延长生命周期,但具体是什么意思呢?
我们用代码举个例子->:
s1+s1返回的是一个临时变量,如果不用左值引用或者右值引用,那么在s1+s1那一行执行完后,它就彻底灰飞烟灭。但是因为运用了引用,数据就得以保存下来,也就会因此,延长了生命周期
小问题->:我们看下面的代码->:int main() { string s1 = "Test"; const string& s2 = s1 + s1;//左值引用延长生命周期 string&& s3 = s1+s1; //右值引用延长生命周期 s3 += "Test"; }
诶?为什么s3可以修改啊,s3是右值引用,右值引用引用的是临时变量,临时变量不可以修改啊 ! ,这里怎么修改了,这100%会报错,但真的会报错吗 ?
我们可以发现,它并没有报错,这是为什么?
能修改是因为,普通的临时变量不可以修改是因为它的生命周期只有一行,当我们去修改时,会导致我们在修改一个未定义的东西
但是这里我们延长了生命周期,所以可以修改
左值和右值的参数匹配
这里没什么重点,但我们还是展示一下->:
就是一个参数匹配问题,没什么要点
🌟7. 左值引用和右值引用的真正用途(移动构造)
上述只是一些左值引用和右值引用的基础,接下来要开始爆表的干货了
我们在之前学习过拷贝构造函数,拷贝构造函数的形参就是运用的左值引用,但是有一个问题,如果数据极大,运用拷贝构造岂不是程序废了,等运行出来都要等半个小时,那还怎么编写代码。
所以,C++11才提出了右值引用,运用右值引用,开辟出了一种新的拷贝构造函数,也叫做"移动构造"
概念介绍->:
移动构造是一种构造函数,类似拷贝构造函数,移动构造函数要求第一个参数是该类类型的引用,但是不同的是这个参数要是右值引用,如果还有其他参数,额外的参数必须有缺省值
多说无意,我们展示一下->:
class A { friend ostream& operator<<(ostream& cout, const A& a); public: //构造函数 A(int a = 1,int b = 2,int c = 3) { year = a; month = b; day = c; } //拷贝构造函数 A(const A& a) { cout << "调用拷贝构造" << endl; year = a.year; month = a.month; day = a.day; } //移动构造 A(A&& a) { cout << "调用移动构造" << endl; swap(a); } void swap(A& a) { std::swap(year,a.year); ::swap(month, a.month); ::swap(day, a.day); } private: int year; int month; int day; }; ostream& operator<<(ostream& cout,const A& a) { cout << a.year <<" " << a.month <<" " << a.day; return cout; } int main() { A a(2025, 3, 15); A b = move(a); cout << b; }
我们可以发现,调用的是移动构造。
更细致的观察,我们发现->:
1. 移动构造的实现是通过swap函数直接进行数据的交换,这大大节省了拷贝的时间
2. 移动构造的参数并没有加const修饰,这是因为我们需要实现数据的交换,所以不能加const,如果加了const,数据无法实现交换,就无法进行移动拷贝构造
我们再打印a试一下会输出什么->:
可以发现,a已经变成随机数了
这就是移动构造,霸道的构造,不过通常我们传的都是右值,并不会像图中传递左值,
通常都会用匿名对象,如下->:
int main() { A a(2025, 3, 15); A b = A(2025,3,3);//传递右值(匿名对象) cout << b<<endl; }
总结移动构造->:
既然你们都要死,还不如死在我的手里,为我的数据增长资质
🌟8. 移动赋值
如果你真的掌握了移动构造,那么移动赋值也是很好理解的,因为它的实现方法与移动构造相同,都是进行swap交换数据,如下->:
class A { friend ostream& operator<<(ostream& cout, const A& a); public: //构造函数 A(int a = 1,int b = 2,int c = 3) { year = a; month = b; day = c; } //我们之前学习的赋值运算符重载a.operator(b); A& operator=(const A& a) { year = a.year; month = a.month; day = a.day; return *this; } //移动赋值 A& operator=(A&& a) { swap(a); return *this; } void swap(A& a) { std::swap(year,a.year); ::swap(month, a.month); ::swap(day, a.day); } private: int year; int month; int day; }; ostream& operator<<(ostream& cout,const A& a) { cout << a.year <<" " << a.month <<" " << a.day; return cout; } int main() { A a(2025, 3, 15); A b; b = move(a); b = A(1999, 9, 9); cout << b<<endl; }
效果如图,a因为交换,变为了 1 2 3
🌟9. 完美转发
所谓完美 : 函数模板在向其他函数传递自身形参时,如果相应实参是左值,它就应该被转发为左值;如果相应实参是右值,它就应该被转发为右值。这样做是为了保留在其他函数针对转发而来的参数的左右值属性进行不同处理
那有人问了?这不就是模板做的事吗,有什么可讲的?这件事,普通的模板还真做不了,因此,完美转发出现了
我们先来了解万能引用
1、模板中的&&不代表右值引用,而是万能引用,其既能接收左值又能接收右值。
2、模板的万能引用只是提供了能够接收同时接收左值引用和右值引用的能力
3、但是引用类型的唯一作用就是限制了接收的类型,后续使用中都退化成了左值
4、我们希望能够在传递过程中保持它的左值或者右值的属性,就需要用我们下面学习的完美转发
我们先看这样一段代码->:
一、没有完美转发的问题
新知识点 : 左值的类型为左值,右值的类型为左值
我们之前写模板函数会像这样进行书写
void func(int& x) { cout << "左值" << endl; } void func(int&& x) { cout << "右值" << endl; } template<typename T> void wrapper(T arg) { // 直接传值,丢失左/右值属性 func(arg); // 无论arg是左值还是右值,arg本身是左值,总调用左值版本 } int main() { int x = 10; wrapper(x); // 预期调用左值版本,实际正确 wrapper(20); // 预期调用右值版本,但实际调用左值版本! return 0; }
但是在出现左值引用和右值引用后,这种写法并不完全可靠,我们在使用wrapper(20)时,20为右值,那么模版T推导出来是int&&,没有错,但是当模板推导出来后进入到函数中,T就退化成为了int&,因此调用的还是左值
二、完美转发的必要性
使用完美转发可以保留参数的属性,正如我们上面提到的"模板中的&&不代表右值引用,而是万能引用,其既能接收左值又能接收右值。"
用代码为例->:
template<typename T> void wrapper(T&& arg) { // Universal Reference func(std::forward<T>(arg)); // 完美转发,保留原始属性 } wrapper(20); // 正确调用右值版本
使用T&&作为参数,可以保证参数的属性不会降级到int&,这就是完美转发
🌟10. 新的类中成员函数
我们之前在类与对象是学到过四种成员函数,它们分别是构造函数,拷贝构造函数,析构函数,和拷贝赋值运算符,这四种即使我们不写也会自动生成,这些都是老顾客了,没什么可讲的
但是,在C++11中,新增了两位,分别是移动构造和移动赋值运算符
它们跟之前四大先贤并肩,成为类中的六大成员函数,都是即使不写也会自动生成
这里我们需要记住几个知识点,如下->:
1. 如果你没有自己实现移动构造函数,且没有实现析构函数、拷贝构造、拷贝赋值重载中的任意一个。那么编译器会自动生成一个默认移动构造。默认生成的移动构造函数,对于内置类型成员会执行逐成员按字节拷贝,自定义类型成员,则需要看这个成员是否实现移动构造,如果实现了就调用移动构造,没有实现就调用拷贝构造。
2. 如果你没有自己实现移动赋值重载函数,目没有实现析构函数,接贝构造、拷贝赋值重载中的任意一个,那么编译器会自动生成一个默认移动赋值。默认生成的移动构造函数,对于内置类型成员会执行逐成员按字节拷贝,自定义类型成员,则需要看这个成员是否实现移动赋值,如果实现了就调用移动赋值,没有实现就调用拷贝赋值。(默认移动赋值跟上面移动构造完全类似)
3. 如果你提供了移动构造或者移动赋值,编译器不会自动提供拷贝构造和拷贝赋值。
什么意思?请看代码->:
class A { friend ostream& operator<< (ostream& cout, const A& tep); public: A(int year, int month) { this->year = year; this->month = month; } private: int year; int month; }; ostream& operator<< (ostream& cout, const A& tep) { cout << tep.year << " " << tep.month << " "<<endl; return cout; } int main() { A a = A(2025, 1);//传递右值,调用移动构造 A b(1999, 9); A c(1800,1); c = move(b); cout << a;// 2025 1 cout << b;//1999 9 cout << c;//1999 9 }
这张代码就覆盖了1和2的两点,我们并没有实现析构函数、拷贝构造、拷贝赋值重载的任意一个,那么默认生成的移动构造或者移动赋值,都等同于默认生成的拷贝构造/拷贝赋值
所以这里的 b 并没有改变,依旧是1999 9
如果我们显示写了其中一个呢?
class A { friend ostream& operator<< (ostream& cout, const A& tep); public: ~A() { } A(int year, int month) { this->year = year; this->month = month; } private: int year; int month; }; ostream& operator<< (ostream& cout, const A& tep) { cout << tep.year << " " << tep.month << " "<<endl; return cout; } int main() { A a = A(2025, 1);//传递右值,调用移动构造 A b(1999, 9); A c(1800,1); c = move(b); cout << a;// 2025 1 cout << b;//1999 9 cout << c;//1999 9 }
结果还是不变,虽然我们写了析构函数,抑制了移动赋值的默认生成,但是别忘了!拷贝赋值还存在呢,所以这里调用的是拷贝赋值
🌟11. lambda表达式
1. 核心概念
Lambda表达式是C++11引入的匿名函数对象,具有以下特点:
- 就地定义,无需单独命名
- 可捕获上下文变量
- 自动推导返回类型(多数情况)
- 可作为函数参数传递
2. 基础语法
[capture](parameters) mutable -> return_type { // 函数体 } / 以下为汉化版本 [捕获列表(不能省略)](参数(可以省略)) mutable -> 返回值(可以省略,会自己推导) { // 函数体 } 1、捕捉可以为空,但是不能省略 2、参数可以为空,可以省略 3、返回值可以省略,并且可以通过返回对象⾃动推导 4、函数题不能省略 总结:只有捕获列表不能省略,其余都可以省略
含义:
1. [capture]:捕获列表,用于指定Lambda如何访问外部变量。
2. (parameters):参数列表,和普通函数的参数类似,但需要注意Lambda参数C++14起才支持auto类型。且不允许有默认参数。
mutable:这个关键字的作用是允许修改按值捕获的变量,或者调用非const的成员函数。默认情况下Lambda的operator()是const的,所以不加mutable的话,无法修改按值捕获的变量。
return_type:返回类型,通常可以自动推导,但在某些情况下需要显式指定,比如函数体内有多个return语句且返回类型不一致时。
函数体:Lambda的具体实现代码,和普通函数类似,但可以访问捕获的变量。
具体是什么意思,我们逐一讲解->:
一、捕获列表详解
我们先展示三种捕获,因为返回值可以自己推导,所以这里先不用写
int main() { auto func1 = [] //空捕获 { cout << 10 << endl;//自动推测返回值为void }; func1();//打印10 int a = 99; auto func2 = [a] //值捕获需要从外部捕获a,否则无法在函数体内使用 //相当于拷贝了一份临时变量 { //a++;//值捕获的不可以修改,因为是临时变量 cout << a << endl; return a;//自动推测返回值为int //可以不写return,那么返回值就为void }; func2();//打印99 auto func3 = [&a] //引用捕获,捕获的为a的引用 { a += 100; cout << a << endl;//打印199 //返回值就为void }; cout << a << endl;//打印199,a被改变了 func3(); }
接下来展示另外三种捕获
int main() { int a = 10, b = 20; auto func1 = [=] //隐式值捕获,用了哪些变量就捕获哪些变量 { cout << a + b << endl; }; func1();//打印30 auto func2 = [&] //隐式引用捕获,用了哪些变量就捕获哪些变量的引用 { a = 999; b = 888; }; func2(); cout << a <<" " << b << endl;//打印999 888 int c = 10; auto func3 = [&, a, b] //混合捕获 //& 是隐式引用捕获,意味着除了 a 和 b,其他外部变量都会以引用的形式被捕获。 //捕获c的引用。a,b的值拷贝 { //a++;报错 //b++;报错 c += 10; cout << a << " " << b << " " << c << endl; }; func3();//打印999,888,20 }
掌握这六种捕获就够了,混合捕获并不仅限于我的例子的写法,你想怎么写怎么写
但是还是有细节点的,请往下看
1.1捕获的细节点
1. 掌握以上六种写法,我们主要讲解一下混合捕获,先看这两段代码->:
int main() { int a = 999, b = 888; int c = 10; auto func3 = [&, a, b] //混合捕获 //捕获c的引用。a,b的值拷贝 { //a++;报错 //b++;报错 c += 10; cout << a << " " << b << " " << c << endl; }; func3();//打印999,888,20 } int main() { int a = 999, b = 888; int c = 10; auto func3 = [a, b, &] //混合捕获 //捕获c的引用。a,b的值拷贝 { //a++;报错 //b++;报错 c += 10; cout << a << " " << b << " " << c << endl;//报错 }; func3(); }
如果你像下面这样写是会报错的,这其实并不是因为捕获的顺序关系,而是因为编译器识别不出来,所以并不是语法的使用出现错误,如果你明确的指出&c,就可以解决
2. 局部的静态和全局变量不能捕捉,也不需要捕捉对于局部的静态变量和全局变量我们不能去捕捉。如果捕捉,那么就会编译报错我们在函数体中可以直接使用静态变量和全局变量,所以不需要捕获int a = 10; int main() { static int b = 20; auto func1 = [] { cout << a << endl;//打印10 cout << b << endl;//打印20 }; func1(); }
二、参数详解
lambda的参数就是我们正常使用函数时候的参数,也就是形参。
在调用函数时候进行传参,跟普通的函数一样,支持缺省值的存在,但不支持重载
代码展示如下->:
int main() { int a = 10, b = 20; auto func1 = [](int a,int b) { cout << a << endl;//打印10 cout << b << endl;//打印20 }; func1(a,b);//打印10,20 auto func3 = [](int c = 100, int d = 200) //支持缺省函数,可以不传参 { cout << c << endl; cout << d << endl; }; func3();//打印100,200 //不支持重载,编译报错 //func3 = [](double c = 100.8, double d = 200.9) // { // cout << c << endl; // cout << d << endl; // cout << "double" << "double" << endl; // }; //func3(); }
三、mutable使用
我们知道,在值捕获的时候,实际上函数体内操作的是临时变量,而临时变量具有常性,就相当于被const修饰了,从而不能修改
这时候使用mutable就可以解决这个问题
mutable的作用就相当于把const去掉,代码如下->:
int main() { int a = 10, b = 20; auto func1 = [a, b] { //值拷贝,不能修改 //a++; //b++; }; auto func2 = [a, b]()mutable //使用mutable时,前面必须存在参数(),是不是空无所谓 { //可以修改 a++; b++; }; cout << a << " " << b << endl;//并不会修改a,b的值 }
四、返回类型
这里的返回值需要我们写的是返回值的类型,代码如下->:
int main() { int a = 10, b = 20; auto func1 = [a, b]() ->int //使用返回值类型也同样必须前面要有() { return a + b; }; func1(); }
这里没什么可讲的,就是我们函数的返回类型
🌟12. lambda实际应用
我们在之前写类的对象比较时候,需要用到仿函数来进行比较
但是,学习了lambda后,我们可以不再依靠仿函数,运用 sort 函数的第三个参数,加上我们的lambda,来实现对象的比较
代码如下->:
struct Goods { string _name; // 名称 double _price; // 价格 int _evaluate; // 评价 Goods(const char* str, double price, int evaluate) : _name(str), _price(price), _evaluate(evaluate) {} }; struct ComparePriceLess { bool operator()(const Goods& gl, const Goods& gr) { return gl._price < gr._price; } }; struct ComparePriceGreater { bool operator()(const Goods& gl, const Goods& gr) { return gl._price > gr._price; } }; int main() { vector<Goods> v = { {"苹果", 2.1, 5}, {"香蕉", 3, 4}, {"橘子", 2.5, 3}, {"菠萝", 1.5, 4} }; // 使用仿函数排序 sort(v.begin(), v.end(), ComparePriceLess()); sort(v.begin(), v.end(), ComparePriceGreater()); // 使用 lambda 按价格升序排序 sort(v.begin(), v.end(), [](const Goods& g1, const Goods& g2) { return g1._price < g2._price; }); // 使用 lambda 按价格降序排序 sort(v.begin(), v.end(), [](const Goods& g1, const Goods& g2) { return g1._price > g2._price; }); // 使用 lambda 按评价升序排序 sort(v.begin(), v.end(), [](const Goods& g1, const Goods& g2) { return g1._evaluate < g2._evaluate; }); // 使用 lambda 按评价降序排序 sort(v.begin(), v.end(), [](const Goods& g1, const Goods& g2) { return g1._evaluate > g2._evaluate; }); return 0; }
Lambda 的核心优势在于代码简洁性和局部逻辑表达能力,而仿函数在复用性和复杂场景中更具优势。根据具体需求选择,两者在 C++ 中是互补关系。
🌟13. delete与default
我们之前展示过default关键字的功能,这里再次讲解一下->:
default关键字: 强制函数生成
delete关键字: 强制函数不生成
具体是什么意思,请看接下来的代码->:
delete关键字->:
class A { public: A() = delete; }; int main() { A a;//报错 //编译器尝试使用构造函数初始化a,但由于默认构造函数已经被删除,所以就会产生编译错误。 }
接下来是运行截图->:
delete关键字的作用是禁止生成默认函数,如图,我们使用了delete,那么本该自动生成的默认构造函数就无法生成
default关键字->:
class A { public: A() = default;//强制生成默认构造函数 A(int a) { oppo = a; } int oppo; }; int main() { A a(5);//两个构造函数只要构成重载就不会报错 cout << a.oppo;//打印5 A b;//调用默认构造函数 }
这里不展示结果了。我们显示写了构造函数后,默认生成的构造函数本应不会出现,但是我们使用了default后,强制生成了默认构造函数。
可能有些人看起来觉得很鸡肋,没什么用。
但是仔细想一想,编译器自动生成的默认构造函数会对自定义类型调用它的默认构造函数,就省去我们手动写了
🌟14. 可变参数模板
C++11的新特性可变参数模板能够让您创建可以接受可变参数的函数模板和类模板。
下面是一个基本的可变参数的函数模板:
// Args是一个模板参数包,args是一个函数形参参数包 // 声明一个参数包Args...args,这个参数包中可以包含0到任意个模板参数。 template <class ...Args> void ShowList(Args... args) {}
上面的参数args前面有省略号,所以它就是一个可变模版参数
我们把带省略号的参数称为“参数 包”,它里面包含了0到N(N>=0)个模版参数。
我们无法直接获取参数包args中的每个参数的,只能通过展开参数包的方式来获取参数包中的每个参数,这是使用可变模版参数的一个主要特点,也是最大的难点,即如何展开可变模版参数。
由于语法不支持使用args[i]这样方式获取可变参数,所以我们的用一些奇招来一一获取参数包的值
首先我们先展示一下可变参数模板的使用
请看代码->:
template<class ...Args> void func(Args... args) { cout << sizeof...(args) << endl; //注意是sizeof... } int main() { func(1, 'a');//输出2 func(1, 2);//输出2 func(1, 2, 3);//输出3 func(1, 'a', 'd');//输出3 func(1, 2.5, 'a');//输出3 }
Args 是模板参数包
args
是函数参数包
sizeof...(args)
用于获取函数参数包args
中参数的数量
我们上面提到过,参数包里面的参数不能直接取出,也就是不可以直接使用
那么接下来,最关键的就是怎么取出参数包中的每一个参数
14.1 递归函数方式展开参数包(配运行截图)
//结尾函数 //当函数包args里面没有参数,也就是传的是0,那么就会调用这里 void func() { cout << "结束" << endl; cout << endl; } template<class T,class ...Args> void func(T,Args... args) { cout <<"当前函数包的类型个数为->:" << sizeof...(args) << endl; cout <<"当前函数中的类型->:" << typeid(T).name() << endl; cout << endl; func(args...); } int main() { func(1, 2.5, 'a'); }
运行截图如下->:
接下来进行讲解,我们的func传递了3个类型的形参
那么1这个int类型传给了T,剩余两个类型传给了参数包,同样是函数包
所以第一次运行时,函数包的类型有2个,当前函数中的类型以T为主
可以理解为每次从函数包中取一个类型,取没为止
以此类推
当函数包为0时,也就是什么都不传,那么就会调用func( ),这里是形参类型匹配问题
因此结束掉整个程序
🌟15. 包装器
函数包装器器其实就是函数指针,用了包装器之后,函数模板只会实例化一次,这里我们了解其用法即可。
可调用对象的类型:函数指针、仿函数(函数对象)、lambda
所需头文件->:
#include<functional>
我们先看这样一段代码->:
// 函数模板会被实例化多次 template<class F, class T> T useF(F f, T x) { static int count = 0; cout << "count:" << ++count << endl; cout << "count:" << &count << endl; return f(x); } double func(double i) { return i / 2; } struct Functor { double operator()(double d) { return d / 3; } }; int main() { // 函数名 cout << useF(func, 11.11) << endl; // 函数对象 cout << useF(Functor(), 11.11) << endl; // lamber表达式 cout << useF([](double d)->double{ return d / 4; }, 11.11) << endl; return 0; }
效果图如下->:
可以发现,每一次实例化出静态成员,每次的地址都不一样
插叙->:如果有不懂的可以停下看看,如果懂的可以直接跳过
1、 为什么count的地址不相同? count不是静态成员吗,怎么会每次都不同?
解答->:
count是一个静态成员,这让很多人误以为只会创建出一个count,实则不然
- 每次模板被不同类型实例化时,编译器会生成一个独立的函数实体。
- 每个实体中的
static int count
是该函数私有的静态变量,作用域仅限于该函数实例。
2、 static不是存放在静态区吗? 那么它的作用域和生命周期怎么算?
解答->:
存储位置与作用域是独立的概念
存储位置:
- 静态变量(包括函数模板中的静态变量)存放在静态存储区(全局 / 静态区),生命周期贯穿整个程序。
作用域:
- 函数模板中的静态变量:
- 作用域仅限于该函数模板的特定实例。
- 例如:
useF<int>
和useF<double>
的count
是两个不同的变量,彼此不可见。
3、 我记得类中的static不是都是一个吗?怎么这里变成三个了?
解答->:
类中的static成员是一个没错,那是因为类中的static成员属于类本身,而不属于对象
4.静态区那么多的count,不会重复命名吗?
不会的,编译器在内部会进行特殊处理
- 当实例化为
useF(func, 11.11)
(F=double(*)(double)
,T=double
)时,编译器会生成类似_Z4useFIPFddEIdE5countE
的符号名。- 当实例化为
useF(Functor(), 11.11)
(F=Functor
,T=double
)时,符号名类似_Z4useFIF7FunctorIdEIdE5countE
。- 不同的符号名确保静态区的
count
变量在链接时不会冲突。
我们可以通过包装器只让函数模板实例化一次
int main() { // 函数名 std::function<double(double)> f1 = func; cout << useF(f1, 11.11) << endl; // 函数对象 std::function<double(double)> f2 = Functor(); cout << useF(f2, 11.11) << endl; // lamber表达式 std::function<double(double)> f3 = [](double d)->double{ return d / 4; }; cout << useF(f3, 11.11) << endl; return 0; }
这里我们讲解下什么意思->:
std::function<double(double)> f1 = func; <func的函数参数(func函数的返回类型)> 相当于我们把func函数包装成为了f1,那么我们使用f1时,就是在使用func f1的类型为std::function<double(double)> cout << useF(f1, 11.11) << endl;
以此类推,每一个都是如此
如果感觉不好记,我们可以理解为一个函数指针,这样就很好写出来,比如->:
f1是一个函数指针,这个指针指向的函数参数类型是double,函数的返回类型是double,那么f1就是func的函数指针,这样就能写出来
因此,我们可以写出很多,各位具体的代码就如下展示了,不做讲解了,记住即可->:
(配效果图)#include<functional> int f(int a, int b) { return a + b; } struct Functor { public: int operator() (int a, int b) { return a + b; } }; class Plus { public: Plus(int n = 10) :_n(n) {} static int plusi(int a, int b) { return a + b; } double plusd(double a, double b) { return (a + b) * _n; } private: int _n; }; int main() { // 包装各种可调⽤对象 function<int(int, int)> f1 = f; function<int(int, int)> f2 = Functor(); function<int(int, int)> f3 = [](int a, int b) {return a + b; }; cout << f1(1, 1) << endl; cout << f2(1, 1) << endl; cout << f3(1, 1) << endl; // 包装静态成员函数 // 成员函数要指定类域并且前⾯加&才能获取地址 function<int(int, int)> f4 = &Plus::plusi; cout << f4(1, 1) << endl; // 包装普通成员函数 // 普通成员函数还有⼀个隐含的this指针参数,所以绑定时传对象或者对象的指针过去都可以 function<double(Plus*, double, double)> f5 = &Plus::plusd; Plus pd; cout << f5(&pd, 1.1, 1.1) << endl; function<double(Plus, double, double)> f6 = &Plus::plusd; cout << f6(pd, 1.1, 1.1) << endl; cout << f6(pd, 1.1, 1.1) << endl; function<double(Plus&&, double, double)> f7 = &Plus::plusd; cout << f7(move(pd), 1.1, 1.1) << endl; cout << f7(Plus(), 1.1, 1.1) << endl; return 0; }
效果图如下->: