一. 类的新功能:更精细的默认函数与初始化控制
C++11 扩展了类的核心能力,允许开发者更灵活地控制默认成员函数、初始化方式,解决了传统 C++ 中类设计的诸多痛点。
1.1 新增默认成员函数:移动构造与移动赋值
C++98 有 6 个默认成员函数,C++11 新增移动构造函数和移动赋值运算符重载,专门用于 “窃取” 右值对象的资源,提升效率。
核心规则:
若未手动实现移动构造 / 移动赋值,且未实现析构、拷贝构造、拷贝赋值中的任意一个,编译器会自动生成默认移动构造 / 移动赋值;默认移动构造 / 赋值:对内置类型成员逐字节拷贝(浅拷贝),对自定义类型成员调用其移动构造 / 赋值(无则调用拷贝构造/拷贝赋值);若手动提供移动构造 / 赋值,编译器不再自动生成拷贝构造 / 赋值。
实际示例:
namespace Scy { class string { public: // ………… // 移动构造:窃取右值资源 string(string&& s) { cout << "string(string&& s) -- 移动构造" << endl; swap(s); // 交换当前对象与右值对象的资源 } // 移动赋值:窃取右值资源 string& operator=(string&& s) { cout << "string& operator=(string&& s) -- 移动赋值" << endl; swap(s); return *this; } private: char* _str = nullptr; size_t _size = 0; size_t _capacity = 0; }; } class Person { public: Person(const char* name = "", int age = 0) :_name(name) , _age(age) { } /*Person(const Person& p) :_name(p._name) ,_age(p._age) {}*/ /*Person& operator=(const Person& p) { if(this != &p) { _name = p._name; _age = p._age; } return *this; }*/ /*~Person() {}*/ private: Scy::string _name; int _age; }; int main() { Person s1; Person s2 = s1; Person s3 = std::move(s1); Person s4; s4 = std::move(s2); return 0; }1.2 成员变量声明时给缺省值 && final与override
成员变量给缺省值:
直接在类中声明成员变量时指定缺省值,初始化列表未显式初始化时,会使用该缺省值:
class Person { private: Scy::string _name = "张三"; // 声明时给缺省值 int _age = 18; };final和override:
好的,这是一个关于 C++final和override关键字的简单使用场景对比表格。
| 关键字 | 作用对象 | 主要目的 | 简单使用场景举例 |
final | 类 | 禁止该类被继承。 | class Base final { };class Derived : public Base { };//错误:无法继承 final 类 |
final | 虚函数 | 禁止该虚函数在派生类中被重写。 | virtual void func() final { }void func() override { }//错误:无法重写 final 函数 |
override | 虚函数 | 显式声明意图重写基类的虚函数,让编译器检查签名是否正确。 | virtual void baseFunc(int);void baseFunc(int) override;//正确void baseFunc(float) override;//错误:函数签名不匹配 |
核心思想总结:
final:用于“禁止”进一步扩展(继承或重写)。override:用于“明确”并“验证”重写行为,防止因笔误导致的错误,提高代码安全性。
1.3 default 与 delete:控制默认函数生成
default:强制编译器生成默认函数(如手动实现拷贝构造后,仍想保留默认移动构造);delete:禁止编译器生成默认函数(如禁止拷贝构造,避免对象拷贝)。
class Person { public: Person(const char* name = "", int age = 0) : _name(name), _age(age) {} // 强制生成默认移动构造 Person(Person&& p) = default; // 禁止拷贝构造(类似ostream的设计) Person(const Person& p) = delete; private: Scy::string _name; int _age; };class Person { public: Person(const char* name = "张三yyyyyyyyyyyy",int age = 18) :_name(_name) ,_age(age) {} // C++11 // delete之后就不会自动生成默认拷贝构造函数了,库里面的ostream就用到了 // Person(const Person& p) = delete; // default之后让编译器强制生成移动构造函数 /*Person(const Person& p) = default; Person(Person&& p) = default;*/ ~Person() {} private: // C++98--将该函数设置为private,并且只声明不定义,这样也能实现类似于C++11中delete的作用 /*Person(const Person& p);*/ string _name; int _age; }; int main() { Person s1; Person s2 = s1; Person s3 = std::move(s1); //Person s4("************************", 1); //s4 = std::move(s2); return 0; }1.4 其他类新功能(补充:委托构造 && 继承构造)
- 委托构造:一个构造函数调用同类的另一个构造函数,减少代码冗余。其中被委托的构造函数必须初始化所有成员变量,因为委托构造函数后(也是因为所有成员都会走初始化列表)不能再重复初始化了。
- 继承构造:通过using Base::Base继承基类所有的普通构造函数(特殊的拷贝构造函数/移动构造函数不继承),简化派生类设计;继承构造函数中派生类自己的成员变量如果有缺省值会使用缺省值初始化,如果没有缺省值那么跟之前类似,内置类型成员不确定,自定义类型成员使用默认构造初始化,但是派生类有自己的成员需要初始化时,一般不适合用继承构造。
// 委托构造 class Example { public: Example(int a, int b) :_x(a) , _y(b) { cout << "目标构造函数\n"; } Example(int a) :Example(a, 0) { cout << "委托构造函数\n"; } int _x; int _y; }; class Time { public: Time(int h,int m) :_hour(h) ,_minute(m) {} //error C3511: “Time”: 对委托构造函数的调用应仅为成员初始值设定项,被委托函数必须已经实现了全部成员变量的初始化 // error C2437 : “_second”: 已初始化 Time(int h, int m, int s) :Time(h, m) // , _second(s) {} private: int _hour; int _minute; int _second = 0; }; int main() { Example(1, 2); Example(1); return 0; }// 继承构造 class Base { public: Base(int x,double d) :_x(x) ,_d(d) {} Base(int x) :_x(x) {} Base(double d) : _x(d) {} protected: int _x = 0; double _d = 0; }; // 传统的派生类实现构造,很麻烦复杂 //class Derived : public Base { //public: // Derived(int x) : Base(x) {} // Derived(double d) : Base(d) {} // Derived(int x, double d) : Base(x, d) {} //}; // C++11继承基类的所有构造函数 class Derived : public Base { public: using Base::Base; // 这里需要注意的是这样的继承构造仅仅适合子类没有变量需要构造的,或者通过缺省值就可以了的。 /*protected: int _i = 0; string _s;*/ }; int main() { Derived d1(1); Derived d2(1.1); Derived d3(2, 2.2); return 0; }二. STL中的一些变化
- 下图中圈起来的就是STL中新的容器,但是实际最有用的是 unordered_map 和 unordered_set。这两个我们前面已经进行了非常详细的讲解,其他的大家了解一下即可。
- STL中容器的新接口也不少,最重要的就是右值引用和移动语义相关的push/insert/emplace系列接口和移动构造与移动赋值,还有initializer_list版本的构造等,这些前面都有讲过,还有一些无关痛痒的如cbegin/cend等需要时查查文档即可。
- 容器的范围 for 遍历,这个在容器部分也讲过了。
三. lambda 表达式:简洁的匿名函数对象
lambda表达式本质是“匿名仿函数”,可在函数内部定义,无需单独声明类,极大简化可调用对象的定义。
3.1 lambda 核心语法
lambda表达式语言使用层而言没有类型,所以我们一般是用auto或者模版参数定义的对象去接受lambda对象
格式:
[capture-list] (parameters) -> return-type { function-body }- [capture-list]:捕捉列表(不可省),捕捉外层作用域变量供函数体使用;捕捉列表总是出现在 lambda 函数的开始位置,编译器根据 [] 来判断接下来的代码是否为 lambda 函数,捕捉列表能够捕捉上下文的变量供函数使用,捕捉列表可以传值或传引用捕捉,具体的后面还会再讲。捕捉列表为空也不能省略。
(parameters):参数列表(可省),与普通函数的参数列表功能类似,无参数时可省略;-> return-type:返回值类型(可省),用追踪返回类型形式声明函数的返回值类型,没有返回值的时候这部分可以直接省略。一般返回值类型明确的情况下,也可以省略,编译器可自动推导(但还是建议写一下的);另外这种形式普通函数也可以用,但是用的比较少。{function-body}:函数体(不可省),函数体内的实现跟普通函数完全类似,在该函数体内,除了可以使用其参数外,还可以使用所以捕获到的变量,函数体就算为空也不可以省略。
简单示例演示:
// 无捕捉、无参数、返回值自动推导 auto func1 = [] { cout << "hello Lotso" << endl; }; func1(); // 输出:hello Lotso // 有参数、有返回值 auto add = [](int x, int y) -> int { return x + y; }; cout << add(1, 2) << endl; // 输出:3// 关于返回值后置的 // std::map<std::string, std::pair<std::string, std::string>>::iterator func(); // auto func()->std::map<std::string, std::pair<std::string, std::string>>::iterator; int main() { // 一个简单的lamba表达式 // 这里可以只靠auto推导返回值类型,但是还是建议写出来的 /*auto add1 = [](int x, int y){return x + y; };*/ auto add1 = [](int x, int y)->int {return x + y; }; cout << add1(1, 2) << endl; // 关于那些东西可以省略,哪些不可以 // 1. 就算捕捉为空也是不可以省略的 // 2. 参数为空可以直接省,()都不用了 // 3. 返回值可以省略,可以通过返回对象自动推导 // 4. 函数体不可以省略 auto func1 = [] { cout << "hello Lotso" << endl; return 0; };//分号不要掉哈 func1(); int a = 0, b = 1; auto swap1 = [](int& x, int& y) { int temp = x; x = y; y = temp; }; swap1(a, b); cout << a << ":" << b << endl; return 0; }3.2 捕捉列表:灵活复用外层变量
捕捉列表控制外层变量的访问方式,支持值捕捉、引用捕捉、混合捕捉,核心规则如下:
| 捕捉方式 | 含义 | 示例 |
| [var] | 值捕捉var(拷贝,默认 const,不可修改) | [a] { return a * 2; } |
| [&var] | 引用捕捉var(可修改外层变量) | [&a] { a++; } |
| [=] | 隐式值捕捉所有使用的外层变量 | [=] { return a + b; } |
| [&] | 隐式引用捕捉所有使用的外层变量 | [&] { a++; b++; } |
| [=, &var] | 隐式值捕捉 + 显式引用捕捉var | [=, &a] { a++; return b; } |
| [&, var] | 隐式引用捕捉 + 显式值捕捉var | [&, a] { b++; return a; } |
| [this] | 捕捉当前类的 this 指针(类成员函数中使用) | [this] { _a1++; } |
关键注意:
- 值捕捉的变量默认被
const修饰,需修改时加mutable(仅修改拷贝,不影响外层变量); - 全局变量、静态变量无需捕捉,可直接使用;
- 捕捉列表不能为空(无变量需捕捉时写[])。
- 如果是在类里面就算是值捕捉,成员变量也是可以修改的。
实际示例:
int x = 0; // 这里捕捉列表必须为空,因为全局变量不用捕捉就可以用,没有可以被捕捉的变量(静态成员变量也是同样的道理) auto func1 = []() { x++; }; int main() { // 只能用当前 lambda 局部域捕捉到的对象和全局对象 // 捕获列表的意义,本质是更方便的使用当前局部域的对象 int a = 0, b = 1, c = 2, d = 3; // 加了这个mutable之后值捕捉也可以修改a了,这里()就不可以省了就算没参数 // 传值捕捉本质是⼀种拷⻉,并且被const修饰了 // mutable相当于去掉const属性,可以修改了 // 但是修改了不会影响外⾯被捕捉的值,因为是⼀种拷⻉ /*auto func1 = [a,&b]() mutable*/ auto func1 = [a, &b] { // 值捕捉的变量不可以修改,引用捕捉的可以修改 //a++; b++; int ret = a + b; x++;//全局变量 return ret; }; cout << func1() << endl; // 隐式值捕捉 // 用了哪些变量就捕捉哪些变量 auto func2 = [=] { int ret = a + b; return ret; }; cout << func2() << endl; // 隐式引用捕捉 // 用了哪些变量就捕捉哪些变量 auto func3 = [&] { a++; c++; d++; }; func3(); cout << a << " " << b << " " << c << " " << d << endl; cout << "*********************" << endl; // 混合捕捉的=或&一定是最前面的,而且如果前面是=后面必须是引用捕捉,前面是&后面必须是值捕捉 // 混合捕捉1 auto func4 = [&, a, b] { //a++; //b++; c++; d++; int ret = a + b + c + d; return ret; }; cout << func4() << endl; cout << a << " " << b << " " << c << " " << d << endl; // 混合捕捉2 auto func5 = [=, &a, &b] { a++; b++; /*c++; d++;*/ return a + b + c + d; }; func5(); cout << a << " " << b << " " << c << " " << d << endl; // 局部的静态和全局变量不能捕捉,也不需要捕捉 static int m = 0; auto func6 = [] { int ret = x + m; return ret; }; // 传值捕捉本质是⼀种拷⻉,并且被const修饰了 // mutable相当于去掉const属性,可以修改了 // 但是修改了不会影响外⾯被捕捉的值,因为是⼀种拷⻉ auto func7 = [=]()mutable { a++; b++; c++; d++; return a + b + c + d; }; cout << func7() << endl; cout << a << " " << b << " " << c << " " << d << endl; }在类里面的使用:
class A { public: void func() { int x = 0, y = 1; auto f1 = [=] { // 为什么是值捕捉,但是成员变量还可以++呢,因为这里实际上捕捉的是他的this指针 _a1++; return x + y + _a1 + _a2; }; cout << f1() << endl; auto f2 = [&] { x++; _a1++; return x + y + _a1 + _a2; }; cout << f2() << endl; // 捕捉this指针的本质就是为了可以访问成员变量 auto f3 = [x,this] { _a1++; return x + _a1 + _a2; }; cout << f3() << endl; } private: int _a1 = 0; int _a2 = 1; }; int main() { A a; a.func(); return 0; }3.3 lambda 的应用场景:替代仿函数与函数指针
传统排序需要定义仿函数,lambda则简洁高效:
- 在学习 lambda 表达式之前,我们使用的可调用对象只有函数指针和仿函数对象,函数指针的类型定义起来比较麻烦,仿函数则是要定义一个类,相对会比较麻烦。使用 lambda 去定义可调用对象,即简单又方便。
- lambda 在很多其他地方用起来也是很好用的。比如线程中定义线程的执行函数逻辑,智能指针中定制删除器等,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; // } //}; // //struct CompareEvaluateLess //{ // bool operator()(const Goods& gl, const Goods& gr) // { // return gl._evaluate < gr._evaluate; // } //}; // //struct CompareEvaluateGreater //{ // bool operator()(const Goods& gl, const Goods& gr) // { // return gl._evaluate > gr._evaluate; // } //}; int main() { vector<Goods> v = { { "苹果", 2.1, 5 }, { "香蕉", 3, 4 }, { "橙子", 2.2, 3}, { "菠萝", 1.5, 4 } }; // 类似这样的场景,我们实现仿函数对象或者函数指针支持商品中 // 不同项的比较,相对还是比较麻烦的,那么这里lambda就很好用了 //sort(v.begin(), v.end(), ComparePriceLess()); //sort(v.begin(), v.end(), ComparePriceGreater()); //sort(v.begin(), v.end(), CompareEvaluateLess()); //sort(v.begin(), v.end(), CompareEvaluateGreater()); //auto priceLess = [](const Goods& gl, const Goods& gr) //{ // return gl._price < gr._price; //}; //sort(v.begin(), v.end(), priceLess); // 这样写更简单,但是调试查看的时候需要将断点打到下一个sort或下一个语句上 // 因为如果打到当前sort,他即算打到sort又算打到lambda sort(v.begin(), v.end(), [](const Goods& gl, const Goods& gr){ return gl._price < gr._price; }); sort(v.begin(), v.end(), [](const Goods& gl, const Goods& gr) { return gl._price > gr._price; }); sort(v.begin(), v.end(), [](const Goods& gl, const Goods& gr) { return gl._evaluate < gr._evaluate; }); sort(v.begin(), v.end(), [](const Goods& gl, const Goods& gr) { return gl._evaluate > gr._evaluate; }); return 0; }注意:上面的程序最后那些sort需要调试的话可以按注释的方式来做
3.4 lambda 的原理:底层是仿函数
编译器会将lambda表达式编译为一个匿名仿函数类:
捕捉列表的变量成为该类的成员变量;operator()的参数、返回值、函数体与 lambda 一致;不同 lambda 对应不同的仿函数类(类名由编译器自动生成)。
补充说明:
lambda 的原理和范围 for 很像,编译后从汇编指令层的角度看,压根就没有 lambda 和范围 for 这样的东西。范围 for 底层是迭代器,而 lambda 底层是仿函数对象,也就说我们写了一个 lambda 以后,编译器会生成一个对应的仿函数的类。
仿函数的类名是编译器按一定规则生成的,保证不同的 lambda 生成的类名不同,lambda 参数 / 返回类型 / 函数体就是仿函数 operator () 的参数 / 返回类型 / 函数体,lambda 的捕捉列表本质是生成的仿函数类的成员变量,也就是说捕捉列表的变量都是 lambda 类构造函数的实参,当然隐式捕捉,编译器要看使用哪些就传那些对象。
上面的原理,我们可以透过汇编层了解一下,下面第二段汇编层代码印证了上面的原理。
实际示例:
int x = 0; // lambda参数/返回类型 / 函数体就是仿函数operator()的参数 / 返回类型 / 函数体 // lambda 的捕捉列表本质是生成的仿函数类的成员变量,也就是说捕捉列表的变量都是 lambda 类构造函数的实参 int main() { int a = 0, b = 1, c = 2, d = 3; //class lambada5(const int& a_, int& b_) //也行,但是int没必要 class lambada5 { public: lambada5(int a_, int& b_) :a(a_) ,b(b_) {} int operator()(int x) { //a++; ++b; return a + b + x; } private: const int a;//对应值捕捉 int& b;// 对应引用捕捉 }; // 可以看看汇编层 auto func = [a, &b](int x) { //a++; ++b; return a + b + x; }; cout << func(1) << endl; // 等价于 /*lambada5 func(a, b);*/ return 0; }四. 包装器:统一可调用对象的类型
C++ 中的可调用对象包括函数指针、仿函数、lambda、成员函数,但它们类型各异,难以统一管理。std::function和std::bind包装器解决了这一问题。
4.1 std::function:可调用对象的 “容器”
std::function是类模板,可包装任意符合 “返回值 (参数类型)” 签名的可调用对象,统一类型接口。
参考文档:function - C++ Reference
补充说明:
std::function是一个类模板,也是一个包装器。std::function的实例对象可以包装存储其他的可以调用对象,包括函数指针、仿函数、lambda、bind 表达式等,存储的可调用对象被称为std::function的目标。若std::function不含目标,则称它为空。调用 空std::function的目标导致抛出std::bad_function_call异常。以上是
function的原型,他被定义<functional>头文件中。std::function- cppreference.com 是function的官方文件链接。函数指针、仿函数、lambda 等可调用对象的类型各不相同,
std::function的优势就是统一类型,对他们都可以进行包装,这样在很多地方就方便声明可调用对象的类型,下面的第二个代码样例展示了std::function作为 map 的参数,实现字符串和可调用对象的映射表功能。
核心用法:
#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; vector<function<int(int, int)>> v; v.push_back(f); v.push_back(Functor()); v.push_back([](int a, int b) {return a + b; }); for (auto& f : v) { cout << f(1, 1) << endl; } // 静态成员函数,下面两种写法都可以,用第二种可以统一规范 // function<int(int, int)> f4 = Plus::plusi; function<int(int, int)> f4 = &Plus::plusi; cout << f4(1, 1) << endl; // 成员函数,必须带&,并且不要忘了this指针的存在,所以下面实际是三个参数,但是写法很多 function<double(Plus*, double, double)> f5 = &Plus::plusd; Plus ps; cout << f5(&ps, 1.1, 1.1) << endl; function<double(Plus, double, double)> f6 = &Plus::plusd; // Plus ps; cout << f6(ps, 1.1, 1.1) << endl; function<double(Plus, double, double)> f7 = &Plus::plusd; cout << f7(Plus(), 1.1, 1.1) << endl; function<double(Plus&&, double, double)> f8 = &Plus::plusd; cout << f8(Plus(), 1.1, 1.1) << endl; auto pf1 = &Plus::plusd; Plus* ptr = &ps; cout << (ps.*pf1)(1.1, 1.1) << endl; cout << (ptr->*pf1)(1.1, 1.1) << endl; return 0; }- 关于类型擦除大家可以自己下去再了解一下,这里就不过多介绍了。
4.2 std::bind:可调用对象的 “适配器”
std::bind是函数模板,可调整可调用对象的参数个数、顺序,绑定固定参数,返回一个新的可调用对象。
参考文档:bind - C++ Reference
补充说明:
核心用法:(有的地方需要用到上面的代码,这里就不重复写了)
#include<functional> using placeholders::_1; using placeholders::_2; using placeholders::_3; int Sub(int a, int b) { return (a - b) * 10; } int SubX(int a, int b, int c) { return (a - b - c) * 10; } int main() { // bind 本质返回一个仿函数对象 // 调整参数顺序(不常用) // _1 代表第一个实参 // _2 代表第二个实参 // ………… auto f1 = bind(Sub, _1, _2); auto f2 = bind(Sub, _2, _1); // _1 代表第一个实参 // _2 代表第二个实参 cout << f1(10, 5) << endl; cout << f2(10, 5) << endl; // 调整参数个数 auto f3 = bind(SubX, 10, _1, _2); cout << f3(15, 5) << endl; // _1 代表第一个实参 // _2 代表第二个实参 // 底层operator(),调整SubX,第一个参数10,15,5 auto f4 = bind(SubX, _1, 10, _2); cout << f4(15, 5) << endl; // 底层operator(),调整SubX,第一个参数15,10,5 auto f5 = bind(SubX, _1, _2, 10); cout << f5(15, 5) << endl; // 底层operator(),调用SubX,第一个参数15,5,10 // 利用bind改进 function<double(Plus, double, double)>f7 = &Plus::plusd; cout << f7(Plus(), 1.1, 1.1) << endl; cout << f7(Plus(), 2.2, 1.1) << endl; cout << f7(Plus(), 3.3, 1.1) << endl; // 绑定成员函数(需传入this指针或对象) function<double(double, double)>f8 = bind(&Plus::plusd, Plus(), _1, _2); cout << f8(1.1, 1.1) << endl; cout << f8(2.2, 1.1) << endl; cout << f8(3.3, 1.1) << endl; return 0; }声明:C++11中还有智能指针,在后面会单独写一篇博客的
核心总结与避坑指南:
1. 类新功能避坑
- 手动实现拷贝构造 / 赋值后,默认移动构造 / 赋值不再生成,需手动添加或用
default; delete可禁止拷贝(如单例模式),default可强制生成默认函数(如移动构造)。
2. lambda 避坑
- 值捕捉的变量默认
const,修改需加mutable(仅影响拷贝); - 引用捕捉需确保外层变量生命周期长于
lambda,避免悬垂引用。
3. 包装器避坑
function包装成员函数时,需传入this指针或对象(隐含第一个参数);bind的占位符_n对应新可调用对象的第 n 个参数,顺序不可混淆。