一.C++11的发展历史
2003 年 C++ 标准委员会发布了一份技术勘误表 TC1,于是 C++03 就诞生了,用来替代原先的 C++98,成为 C++11 之前最新的标准。不过 C++03 本质只是修补 C++98 里的各类漏洞,语言核心语法几乎没有改动,所以大家习惯把二者合在一起称作 C++98/03 标准。
C++11 可以说是十年磨一剑,是真正意义上的又一版重大标准。对比 C++98/03,C++11 新增了大概 140 项新特性,同时修复了 C++03 里约 600 处缺陷,改动幅度非常大,几乎相当于一门新语言。不管是系统开发还是库开发,C++11 都表现更好,语法更通用简洁,代码也更稳定安全,功能更强,还能提高开发效率。现在企业项目里 C++11 使用十分普遍,也是我们学习的重点,下面主要介绍工作里常用的实用语法。
C++ 标准委员会 1998 年成立,原本计划每 5 年更新一次标准。在做完 C++03 之后,下一版标准原本打算 2007 年推出,因此暂定名叫 C++07。到 2006 年,委员会发现 2007 年肯定完成不了,就连 2008 年也很难落地,索性改名为 C++0x。这里的 x 代表未知数,意思是不确定会是 07、08 还是 09 年正式发布。可项目一直延期,2010 年依旧没能定稿,直到 2011 年新标准才正式落地,于是最终命名为 C++11。

二.统一的列表初始化
1.C++98传统的{}
C++98中⼀般数组和结构体可以⽤花括号{}进⾏进行统一的列表初始值设定。
cpp
struct Hjq
{
int _x;
int _y;
};
int main()
{
int array[] = { 1,2,3,4,5,6 };
int array1[] = { 0,1,2 };
Hjq h = { 1,2 };
return 0;
}
C++11 拓展了大括号初始化列表的适用场景,内置类型和自定义类型都支持用花括号完成初始化。写代码的时候,前面可以带上等号=,也可以直接省略不用。
cpp
class Date
{
public:
Date(int year, int month, int day)
:_year(year)
,_month(month)
,_day(day)
{
cout << "Date(int year, int month, int day)" << endl;
}
private:
int _year;
int _month;
int _day;
};
int main()
{
Date d1(2026, 9, 2); // old style
// C++11支持的列表初始化,这里会调用构造函数初始化
Date d2{ 2026, 9, 2 };
Date d3 = { 2026, 9, 3 };
return 0;
}
2.std::initializer_list
https://cplusplus.com/reference/initializer_list/initializer_list/

std::initializer_list 是 C++11 提供的库模板类型 。当我们写
{...}初始化列表时,编译器会自动生成该类型对象 。它相当于对一组常量元素的轻量视图,不拷贝数据,只持有指针,内部元素只读,很多 STL 容器依靠它实现花括号批量初始化。
cpp
int main()
{
// the type of il is an initializer_list
auto il = { 10, 20, 30 };
cout << typeid(il).name() << endl;
return 0;
}

- auto il = {10,20,30}推导类型为std::initializer_list<int>。
- auto il{10,20,30}(无等号)C++17 之后会编译报错,不能推导出 initializer_list。
- initializer_list仅作为视图,本身不存数据**,编译器自动生成临时数组保存元素。**
- typeid(il).name()的输出各个编译器不一样,gcc 打印重整后的名字St16initializer_listIiE,VS 会输出可读的完整类型。
- typeid() 是 C++ 运算符,在运行时获取对象的类型信息 ;
.name()是它返回对象的成员函数,用来拿到类型对应的字符串。
std::initializer_list使用场景
std::initializer_list 最常见就是用作构造函数参数。C++11 为大量 STL 容器增加了该参数的构造函数,实现花括号直接初始化容器。 它同样可以作为operator=的参数,支持花括号直接赋值。我们自定义类也可以利用它实现批量初始化,注意列表内元素只读,不允许修改里面的数据
cpp
// vector initializer_list 构造
cplusplus.com/reference/vector/vector/vector/
// vector initializer_list 赋值 operator=
cplusplus.com/reference/vector/vector/operator=/
// list initializer_list 构造
cplusplus.com/reference/list/list/list/
// list initializer_list 赋值 operator=
cplusplus.com/reference/list/list/operator=/
// map initializer_list 构造
cplusplus.com/reference/map/map/map/
// map initializer_list 赋值 operator=
cplusplus.com/reference/map/map/operator=/
// std::initializer_list 本身
cplusplus.com/reference/initializer_list/initializer_list/
https://cplusplus.com/reference/map/map/map/

https://cplusplus.com/reference/vector/vector/vector/
https://cplusplus.com/reference/list/list/list/
https://cplusplus.com/reference/vector/vector/operator=/

cpp
#include<iostream>
using namespace std;
#include<vector>
#include<list>
#include<map>
int main()
{
vector<int> v = { 1,2,3,4 };
list<int> lt = { 1,2 };
// 这里{"sort", "排序"}会先初始化构造一个pair对象
map<string, string> dict = { {"sort", "排序"}, {"insert", "插入"} };
// 使用大括号对容器赋值
v = {10, 20, 30};
return 0;
}
- vector、list、map 这些 STL 容器,C++11 都增加了接收 std::initializer_list 的构造函数和赋值函数,所以既可以花括号初始化,也可以花括号赋值。
- list<int> lt = {1,2,3};:{1,2,3} 编译器隐式转为 std::initializer_list<int>,传给 list 的构造函数。
- 注意坑:map里面放的是pair<string,string>,所以初始化要嵌套一层大括号 {"key","value"}。
接下来看一下,让模拟实现的 vector 也支持 {} 初始化和赋值
cpp
namespace hjq
{
template<class T>
class vector {
public:
typedef T* iterator;
// 接收initializer_list的构造函数
vector(initializer_list<T> l)
{
// 根据initializer_list的元素个数开辟空间
_start = new T[l.size()];
_finish = _start + l.size();
_endofstorage = _start + l.size();
iterator vit = _start;
// 获取initializer_list的迭代器遍历
typename initializer_list<T>::iterator lit = l.begin();
while (lit != l.end())
{
*vit++ = *lit++;
}
// 范围for也可以简化上面遍历
//for (auto e : l)
// *vit++ = e;
}
// 支持花括号赋值 operator=
vector<T>& operator=(initializer_list<T> l) {
// 利用构造函数创建临时对象tmp
vector<T> tmp(l);
// 交换资源,tmp出作用域自动释放旧空间
std::swap(_start, tmp._start);
std::swap(_finish, tmp._finish);
std::swap(_endofstorage, tmp._endofstorage);
return *this;
}
private:
iterator _start; // 数据起始位置
iterator _finish; // 有效数据末尾
iterator _endofstorage; // 容量末尾
};
}
三.声明
在c++11当中提供了多种简化声明的方式,尤其是在使用模板时候。
1.auto
C++98 里的
auto是存储类型说明符,用来标记局部变量为自动存储。但局部作用域定义的变量默认就是自动存储,所以这个关键字在 C++98 里几乎没有实际用处。**C++11 废弃了
auto旧的语义,把它改成自动类型推断关键字。**使用 auto 定义变量时,必须显式给出初始化值,编译器会根据初始化表达式,自动推导变量的实际类型。
代码使用如下:
cpp
#define _CRT_SECURE_NO_WARNINGS
#include<iostream>
using namespace std;
#include<vector>
#include<list>
#include<map>
int main()
{
int i = 10;
auto p = &i;
auto pf = strcpy;
cout << typeid(p).name() << endl;
cout << typeid(pf).name() << endl;
map<string, string> dict = { {"sort", "排序"}, {"insert", "插入"} };
//map<string, string>::iterator it = dict.begin();
auto it = dict.begin();
return 0;
}

解析:
- int * __ptr64auto p = &i;,p推导出来类型是int*,整型指针。
- char * __ptr64 (__cdecl*)(char * __ptr64,char const * __ptr64)auto pf = strcpy;,pf推导成strcpy 的函数指针类型。
strcpy 函数原型:char* strcpy(char* dest, const char* src);,pf就是这个函数类型的指针。
- auto it = dict.begin(); 不用写很长的 map<string,string>::iterator,auto 自动推导为 map 迭代器类型。
2.decltype
decltype可以获取表达式的类型,用它来定义变量时,变量的类型就等于该表达式的类型。 和auto不一样:decltype是拿表达式本身推导类型,不需要变量做初始化赋值。
cpp
#include<iostream>
using namespace std;
// decltype的一些使用使用场景
template<class T1, class T2>
void F(T1 t1, T2 t2)
{
decltype(t1 * t2) ret;
cout << typeid(ret).name() << endl;
}
int main()
{
const int x = 1;
double y = 2.2;
decltype(x * y) ret; // ret的类型是double
decltype(&x) p; // p的类型是int*
cout << typeid(ret).name() << endl;
cout << typeid(p).name() << endl;
F(1, 'a');
return 0;
}

3.nullptr
C++ 里传统的NULL本质就是字面量 0,它既可以当作整型常量,也可以当作空指针使用。一个 0 值同时具备两种含义,函数重载的时候就容易出现调用歧义,引发意料之外的 bug。
为了解决这个问题,C++11 新增nullptr,专门用来表示空指针,它拥有独立的空指针类型,不会和整数 0 混淆。
cpp
#ifndef NULL
#ifdef __cplusplus
#define NULL 0
#else
#define NULL ((void *)0)
#endif
#endif

四.范围 for 循环(C++11)
普通 for 循环需要程序员手动控制循环起始、结束边界,手写边界很容易出错。C++11 引入范围 for 循环,用来简化容器、数组的遍历。
cpp
void TestFor()
{
int array[] = {1, 2, 3, 4, 5};
// 传统下标遍历
for (int i = 0; i < sizeof(array) / sizeof(array[0]); ++i)
{
array[i] *= 2;
}
// 范围for:依次把集合里每个元素赋值给e,自动判断起止
for(auto e : array)
{
cout << e << " ";
}
return;
}
和普通 for 循环一样,范围 for 同样支持continue结束本次循环,break跳出整个循环。
范围 for 的使用条件
范围 for 本质是编译器在底层把它转换成普通迭代代码,和传统遍历逻辑等价,但有使用限制:
- 迭代范围必须可以确定
原生数组:编译器要能拿到数组首元素与末尾的范围。数组传参到函数时会退化成指针,丢失数组大小信息,不能使用范围 for。
cpp
//错误写法,array已经退化为指针,无法确定范围
void TestFor(int array[])
{
for(auto& e : array)
{
cout << e << endl;
}
}
自定义类型:类必须提供begin()和end()成员方法,返回迭代器,以此确定循环的起止区间。
- 迭代器必须支持
++自增、!=不等比较运算,才能完成遍历。
补充小细节:
如果要修改容器内元素,要用auto& e : 集合引用接收;
只做读取,推荐const auto& e : 集合,避免不必要拷贝。
五.STL 中的一些变化
1.新容器
C++11 新增了一批 STL 容器,其中日常写代码最实用的是 unordered_map、unordered_set,哈希实现,查找效率很高。剩下几个简单了解概念就行,工作学习里用得不多。
- unordered_set:哈希集合,元素去重,无序,底层哈希表,查找平均 O (1)。
- unordered_map:哈希键值对,key 不重复,无序,底层哈希表,查找平均 O (1)。
对比老容器:set/map底层是红黑树,元素自动排序,查找是 O (logN);不需要有序优先选 unordered 系列,速度更快。
其余 C++11 新增容器了解即可:
- forward_list:单向链表,只支持向后遍历,内存开销比 list 更小,不能反向迭代。
- array:封装的静态数组,大小编译期固定,比原生数组多了迭代器、size 等接口,不能扩容。

2.容器中的一些新方法
翻看 STL 容器接口可以看到,C++11 给大部分容器都新增了不少成员方法,不过很多日常开发用到的机会不多,属于锦上添花的特性。比如新增的cbegin()、cend(),专门返回const迭代器,只能读元素,不能修改。 但其实容器本身的begin()、end(),在 const 对象上调用时,本来就会自动返回 const 迭代器,所以cbegin/cend实际使用场景比较有限。
真正写代码里会高频用到的几个 C++11 容器新接口:
- emplace / emplace_back
push_back是先构造对象,再拷贝 / 移动存入容器;emplace_back直接在容器内存空间原地构造对象,省去拷贝,效率更高。emplace对应任意位置原地构造。
cpp
vector<pair<string,int>> v;
v.emplace_back("aaa",1); //直接原地构造pair,不用手动传pair对象
- 移动语义相关:
move赋值、移动构造 容器支持移动构造、移动赋值,对象资源直接转移,不用大批量拷贝数据。 - shrink_to_fit() vector、deque 可用,把多余容量释放掉,让容量缩小到和有效元素个数一致,回收空闲内存
实际上 C++11 更新后,容器中增加的新方法最后用的插入接口函数的右值引用版本:
https://cplusplus.com/reference/vector/vector/emplace_back/
https://cplusplus.com/reference/vector/vector/push_back/
https://cplusplus.com/reference/map/map/insert/
https://cplusplus.com/reference/map/map/emplace/
这些接口是如何提高效率的?
那咱们请看下面的右值引用 和移动语义 的讲解。同时emplace 还会涉及模板的可变参数,也需要我们再继续深入学习后面章节的知识。
六.右值引用和移动语义
1.左值引用和右值引用
什么是左值?什么是左值引用?
左值,简单说就是有地址、能取址的表达式。比如变量名、解引用的指针,这些都可以用 & 取到地址,也能放在赋值号左边。不过要注意,用const 修饰的左值虽然不能赋值,但地址依然能取,所以它还是左值。
我们之前学的 int &a = b; 这种引用,就是左值引用,本质上是给左值起个别名。
C++11 引入的右值引用(int &&a = 10;)是另一个独立的概念,用来绑定临时对象、支持移动语义,和左值引用不是一回事。在之前C++学的引用,现在确实统称为"左值引用"。右值引用是新增的工具,功能上更侧重资源转移,而不是简单的别名。
cpp
int main()
{
// 以下的a、b、c、*p都是左值
int* a = new int(0);
int b = 1;
const int c = 2;
// 以下几个是对上面左值的左值引用
int*& rp = a;
int& rb = b;
const int& rc = c;
int& pvalue = *p;
return 0;
}
什么是右值,什么是右值引用?
右值 这个东西,你就当它是**"用完就扔"** 的临时货。比如你写的 10、a + b 这种表达式的结果、还有函数返回的临时值(前提是函数不是返回左值引用),这些都是右值。
它和左值最大的区别就两点:
- 不能取地址,你写 &10 编译器直接报错
- 不能放等号左边,10 = a;
右值引用就是给这种临时货起个名儿,写法是 int &&r = 10;。以前你碰到 10 这种字面量,想引用都引用不了(int &r = 10; 是错的),现在有了右值引用,就能绑定了。它的主要用途不是让你给常量起名玩,而是用来偷临时对象的资源,省掉不必要的拷贝,这也就是所谓的**"移动语义"**。
所以你以前学的 & 是左值引用,只能绑左值;现在新出的 && 是右值引用,专门绑右值。一个盯"长住的",一个盯"临时的",各管各的。
cpp
int main()
{
double x = 1.1, y = 2.2;
//以下是常见的右值
10;
x + y;
fmin(x + y);
//以下是对右值的右值引用
int&& rr1 = 10;
double rr2 = x + y;
double&& rr3 = fmin(x + y);
// 这里编译会报错:error C2106: "=": 左操作数必须为左值
10 = 1;
x + y = 1;
fmin(x, y) = 1;
}
2.左值引用与右值引用比较
左值引用默认只能绑左值,比如 int &r = a; 没问题,但 int &r = 10; 直接报错。
不过有个例外:加了 const 的左值引用就比较通融,左值右值都能绑 。比如 const int &r = 10; 这样写是合法的,编译器会给你临时生成一个变量来保住这个右值的命。
所以你可以这么记:普通的左值引用挺挑食,只吃左值;const 左值引用不挑食,左右通吃。
cpp
int main()
{
// 右值引用只能右值,不能引用左值。
int&& r1 = 10;
// error C2440: "初始化": 无法从"int"转换为"int &&"
//所以 : 无法将左值绑定到右值引用
int a = 10;
int&& r2 = a;
// 右值引用可以引用move以后的左值
int&& r3 = std::move(a);
return 0;
}
我们看到这里有个std::move 到底干了啥?
它不是真的字面意思"移动"什么东西,就是做个类型转换 ,把左值 a 强行转成右值类型,骗过编译器让它觉得 a 是个右值,这样右值引用 r3 就能绑上去了。至于数据有没有真被移走,那是后面代码的事,move 就好像"伪装"这个变量a一样。
注意哈:就是 move 的全名别忘了加 std::,还得包含 <utility> 头文件。
3.右值引用使用场景和意义
前面讲了,const 左值引用左右通吃,那问题来了:C++11 为啥还要多此一举搞个右值引用?是不是吃饱了撑的?
其实不是。const 左值引用虽然能绑右值,但它有个硬伤:只读。你绑上右值之后只能看不能改,这就限制了很多操作场景。
cpp
namespace hjq
{
class string
{
public:
typedef char* iterator;
iterator begin()
{
return _str;
}
iterator end()
{
return _str + _size;
}
string(const char* str = "")
:_size(strlen(str))
, _capacity(_size)
{
//cout << "string(char* str)" << endl;
_str = new char[_capacity + 1];
strcpy(_str, str);
}
// s1.swap(s2)
void swap(string& s)
{
::swap(_str, s._str);
::swap(_size, s._size);
::swap(_capacity, s._capacity);
}
//拷贝构造(左值引用版本)
// 参数是 const 左值引用,能绑左值也能绑右值
// 但不管绑什么,都是老老实实做深拷贝,开销大
// 场景:用已有对象拷贝一个新对象
string(const string& s)
:_str(nullptr)
{
cout << "string(const string& s) -- 深拷贝" << endl;
string tmp(s._str); // 用 s 的字符串构造一个临时对象
swap(tmp); // 把临时对象的资源换过来,tmp 带旧资源析构
}
// 拷贝赋值(左值引用版本)
// 参数传值,实参传进来时要么拷贝构造要么移动构造
// 然后和当前对象交换资源
string& operator=(const string& s)
{
cout << "string& operator=(string s) -- 深拷贝" << endl;
string tmp(s); // 拷贝构造一个临时对象
swap(tmp); // 交换资源,tmp 带旧资源析构
return *this;
}
//移动构造(右值引用版本)
// 参数是右值引用,专门绑临时对象或者 std::move 过来的对象
// 不深拷贝,直接跟临时对象交换资源,把它的资源"偷"过来
// 场景:函数返回临时对象、用 std::move 强制转移资源
string(string&& s) // s 是右值引用,绑定的就是快死的临时对象
:_str(nullptr) // 先把自己置空,方便 swap
,_size(0)
,_capacity(0)
{
cout << "string(string&& s) -- 移动语义" << endl;
swap(s); // 和临时对象交换资源:this 拿到临时对象的数据,临时对象拿到 this 的空指针
// 函数结束 s(临时对象)析构,但它现在只拿着空指针,啥也没释放,资源已经被 this 偷走了
}
// 移动赋值(右值引用版本)
// 和移动构造同理,只不过是用在赋值场景
// 场景:把临时对象或 std::move 的对象赋值给一个已有对象
string& operator=(string&& s)
{
cout << "string& operator=(string&& s) -- 移动语义" << endl;
swap(s); // 直接交换资源,this 拿到 s 的资源,s 拿到 this 的旧资源
return *this; // s 是右值引用,马上要销毁,它的旧资源(也就是 this 原来的资源)跟着 s 一起析构释放
}
~string()
{
delete[] _str;
_str = nullptr;
}
char& operator[](size_t pos)
{
assert(pos < _size);
return _str[pos];
}
void reserve(size_t n)
{
if (n > _capacity)
{
char* tmp = new char[n + 1];
strcpy(tmp, _str);
delete[] _str;
_str = tmp;
_capacity = n;
}
}
void push_back(char ch)
{
if (_size >= _capacity)
{
size_t newcapacity = _capacity == 0 ? 4 : _capacity * 2;
reserve(newcapacity);
}
_str[_size] = ch;
++_size;
_str[_size] = '\0';
}
string& operator+=(char ch)
{
push_back(ch);
return *this;
}
const char* c_str() const
{
return _str;
}
private:
char* _str;
size_t _size;
size_t _capacity; // 不包含最后做标识的\0
};
}
左值引用的使用场景:
做参数和做返回值都可以提高效率。
cpp
void func1(hjq::string s) // 传值:实参拷进来,有拷贝开销
{}
void func2(const hjq::string& s) // 传引用:不拷贝,直接绑
{}
int main()
{
hjq::string s1("hello world");
// func1传值,s1得拷一份给s,有深拷贝
func1(s1);
// func2传const左值引用,s直接绑s1,零拷贝
func2(s1);
// string operator+=(char ch) 传值返回存在深拷贝
// string& operator+=(char ch) 传左值引用没有拷贝提高了效率
// 之前 += 要是写成传值返回,每次返回都得拷一份临时对象
// 现在写成传引用返回,返回的就是s1本身,没有拷贝
s1 += '!';
return 0;
}
函数参数用左值引用(尤其是
const引用),能省掉传值时的一次拷贝。函数返回值用左值引用,能省掉返回时临时对象的拷贝。
所以左值引用在这两种场景下确实能提效。不过右值引用提效的场景不一样------它是在临时对象或者转移资源的时候发挥作用,比如移动构造和移动赋值。
左值引用的短板:
当函数要返回一个局部对象时,这个对象出了函数作用域就被销毁了,所以没法用左值引用返回它------引用绑定的对象都死了,用不得。这时候只能老老实实传值返回。
比如 hjq::string to_string(int value) 这种函数,内部构造的字符串对象是局部变量,只能传值返回。而传值返回就避不开拷贝构造,至少一次,旧一点的编译器甚至可能拷两次,性能损失在所难免。这就是左值引用搞不定的场景,也是右值引用要出场的地方。右值引用能偷走临时对象的资源,把这次拷贝省掉,补上左值引用这个短板
来看代码:
cpp
hjq::string to_string(int value)
{
hjq::string str;
// 这里把 value 转成字符串存到 str 里,具体代码省略
return str;
}
int main()
{
hjq::string ret = to_string(123);
return 0;
}
这个函数返回了一个局部对象
str。局部变量嘛,函数一结束它就析构了,所以没法用左值引用返回它------引用绑一个将死之物,那是悬垂引用,写了就是 bug。
那咋整?只能传值返回。
传值返回就意味着要拷一份出去。按最原始的理论,这里至少要发生一次拷贝构造(严格来说是两次,但现在的编译器一般会做返回值优化,优化成一次)。
这倒不算啥大问题,毕竟编译器帮我们兜了底。但问题是,这次拷贝能不能也省掉?
答案是能,用右值引用。
cpp
namespace hjq
{
// 拷贝构造 ------ 左值引用版本,走深拷贝
string(const string& s)
: _str(nullptr)
{
cout << "string(const string& s) -- 深拷贝" << endl;
string tmp(s._str);
swap(tmp);
}
// 整数转字符串
hjq::string to_string(int value)
{
bool flag = true;
if (value < 0)
{
flag = false;
value = 0 - value;
}
hjq::string str; // 局部对象
while (value > 0)
{
int x = value % 10;
value /= 10;
str += ('0' + x);
}
if (flag == false)
{
str += '-';
}
std::reverse(str.begin(), str.end());
return str; // 传值返回局部对象
}
}
int main()
{
hjq::string ret1 = hjq::to_string(1234);
hjq::string ret2 = hjq::to_string(-1234);
return 0;
}
这段代码想说明什么?
to_string 函数里定义了一个局部对象 str,它出了函数就销毁,所以没法用左值引用返回它,只能传值返回。
return str; 返回的是一个临时对象 ,也就是右值 。ret1 和 ret2 用这个右值来构造,如果没有移动构造,编译器会去匹配拷贝构造(因为 const 左值引用能绑右值 ),于是就走到了上面那个拷贝构造,发生一次深拷贝。
短板在哪儿?
str 马上就要销毁了,它手里的堆空间反正也没人要,按理说直接"偷"过来给 ret1 用就行了,省一次深拷贝。但左值引用干不了这事,它只能老老实实拷贝。这就是左值引用处理不了"将亡对象资源转移"的短板。
右值引用和移动语义解决上述问题
上面那个 to_string 返回局部对象的问题,用右值引用就能解决。
具体做法:在 hjq::string 里增加一个移动构造。移动构造说白了就是偷------把右值临时对象的资源直接"抢"过来占为己有,省掉深拷贝的开销。所以叫"移动构造",本质就是窃取别人的资源。
cpp
// 移动构造
string(string&& s)
: _str(nullptr)
, _size(0)
, _capacity(0)
{
cout << "string(string&& s) -- 移动语义" << endl;
swap(s); // 把自己的空指针和临时对象的资源交换,资源就到手了
}
加上移动构造之后,再跑一遍 hjq::to_string 的调用:
cpp
int main()
{
hjq::string ret2 = hjq::to_string(-1234);
return 0;
}
这回就不走深拷贝的拷贝构造了,而是匹配到移动构造。移动构造里没开新空间、没拷贝数据,只是交换了几个指针,效率一下就上去了。
在 hjq::string 类中增加移动赋值函数,再去调用 xyl::to_string(1234),不过这次是将 hjq::to_string(1234) 返回的右值对象赋值给 ret1 对象,这时调用的是移动构造。
cpp
// 移动赋值 ------ 右值引用版本
string& operator=(string&& s)
{
cout << "string& operator=(string&& s) -- 移动语义" << endl;
swap(s); // 直接偷资源,不拷贝
return *this;
}
int main()
{
hjq::string ret1; // 先构造一个空对象
ret1 = hjq::to_string(1234); // 把临时对象赋值给 ret1
return 0;
}
// 运行结果:
// string(string&& s) -- 移动语义
// string& operator=(string&& s) -- 移动语义
为什么调了两次移动?
因为 ret1 是已经存在的对象,编译器没办法做返回值优化(RVO)。所以过程是:
to_string 里的局部对象 str 先被编译器识别成右值,触发移动构造,生成一个临时对象。
这个临时对象赋值给 ret1,触发移动赋值。两步都是移动语义,没有发生深拷贝。
STL 中的容器都是增加了移动构造和移动赋值(比如下面的): .
https://cplusplus.com/reference/string/string/string/

https://cplusplus.com/reference/vector/vector/vector/

4.右值引用引用左值及其一些更深入的使用场景分析
按照语法规则,右值引用只能绑定右值,那是不是右值引用就绝对不能关联左值?
有些场景我们希望对左值对象触发移动语义,这时就可以使用std::move,把左值强制转换成右值,从而可以被右值引用绑定。
C++11 中std::move()定义在<utility>头文件。这个函数名字很容易让人误解:它本身不会移动、搬运任何数据,仅仅做类型转换,将左值强制转为右值引用类型,以此触发移动构造或者移动赋值,实现移动语义。
cpp
// std::move 的底层实现(简化版)
template<class _Ty>
inline typename remove_reference<_Ty>::type&& move(_Ty&& _Arg) _NOEXCEPT
{
// 功能:把传入的参数强转成右值引用类型
// 注意:它啥也没搬,只是做了个类型转换
return ((typename remove_reference<_Ty>::type&&)_Arg);
}
看不懂上面的模板语法没关系,记住 std::move 的本质就行:它就是给左值披上右值的外衣,让编译器以为它是右值,从而去匹配移动构造或移动赋值
cpp
int main()
{
hjq::string s1("hello world");
// s1 是左值,匹配拷贝构造 → 深拷贝,s2 独立拥有一份数据
hjq::string s2(s1);
// std::move(s1) 把 s1 强转成右值,匹配移动构造 → 资源被偷走
// s3 拿到了 s1 的堆空间,s1 被置空
hjq::string s3(std::move(s1));
// 注意:走到这里 s1 已经空了,不能再拿来用
// 除非重新给 s1 赋值,比如 s1 = "new string";
return 0;
}
s1 本身是左值,正常情况下只能匹配拷贝构造 。但std::move(s1) 把它"伪装"成右值,于是匹配到了移动构造,s1 的资源被 s3 偷走。
不过要小心: 这种写法一般不建议随便用,因为 std::move(s1) 之后,s1 的资源已经被转移了,它变成了空壳,再访问它就可能出问题。除非你明确知道自己在干啥,或者之后重新给 s1 赋值。
STL容器插入接口函数也增加了右值引用版本:
https://cplusplus.com/reference/list/list/push_back/

https://cplusplus.com/reference/vector/vector/push_back/

cpp
#include <utility> // std::move
#include <list> // std::list
#include <iostream> // std::cout
using namespace std;
namespace hjq
{
class string
{
public:
// 构造函数
string(const char* str = "")
: _size(strlen(str))
, _capacity(_size)
{
_str = new char[_capacity + 1];
strcpy(_str, str);
}
// 拷贝构造 ------ 打印日志
string(const string& s)
: _str(nullptr)
{
cout << "string(const string& s) -- 深拷贝" << endl;
string tmp(s._str);
swap(tmp);
}
// 移动构造 ------ 打印日志
string(string&& s)
: _str(nullptr)
, _size(0)
, _capacity(0)
{
cout << "string(string&& s) -- 移动语义" << endl;
swap(s);
}
void swap(string& s)
{
::swap(_str, s._str);
::swap(_size, s._size);
::swap(_capacity, s._capacity);
}
~string()
{
delete[] _str;
_str = nullptr;
}
private:
char* _str;
size_t _size;
size_t _capacity;
};
}
int main()
{
std::list<hjq::string> v; // 存放 string 的链表
hjq::string s1("1111"); // s1 是左值
v.push_back(s1); // 传左值 --> 拷贝构造
v.push_back("2222"); // 传右值(字面量) --> 移动构造
v.push_back(std::move(s1)); // 传右值(move后的左值) --> 移动构造
return 0;
}
运行结果:

- v.push_back(s1) s1 是左值 push_back(const value_type& val) 拷贝构造 string(const string& s) -- 深拷贝
- v.push_back("2222") 字面量是右值 push_back(value_type&& val) 移动构造 string(string&& s) -- 移动语义
- v.push_back(std::move(s1)) move后是右值 push_back(value_type&& val) 移动构造 string(string&& s) -- 移动语义
简单小结一下就是:push_back 有两个重载版本------左值引用版本走拷贝构造,右值引用版本走移动构造。传左值就老老实实拷贝一份,传右值就直接把资源偷过来,省掉深拷贝的开销。
5.完美转发
【模板中的 && ------ 万能引用】
模板里出现 T&&,别一看是 && 就以为是右值引用。它有个专门的名字叫万能引用(也叫转发引用),这东西左右通吃------既能绑左值,也能绑右值。
cpp
template<typename T>
void func(T&& arg); // arg 是万能引用,左值右值都能接
不过有个坑:万能引用 虽然接收的时候能区分左右值 ,但一旦进了函数体,arg 本身就成了一个左值(因为它有名字了,能取地址)。这就导致如果你想把它继续传给下一个函数,它的左右值属性就丢了。比如你写了个转发函数,想把参数原封不动地传给另一个函数,结果传过去的一律成了左值,移动语义全白瞎了。
怎么办?
不用怕:C++11 提供了 std::forward(完美转发),它能保证参数在传递过程中保持原来的左右值属性------左值还是左值,右值还是右值。
cpp
void Fun(int& x) { cout << "左值引用" << endl; }
void Fun(const int& x) { cout << "const 左值引用" << endl; }
void Fun(int&& x) { cout << "右值引用" << endl; }
void Fun(const int&& x) { cout << "const 右值引用" << endl; }
template<typename T>
void PerfectForward(T&& t)
{
Fun(t);
}
int main()
{
PerfectForward(10); // 左值引用
int a;
PerfectForward(a); // 左值引用
PerfectForward(std::move(a)); // 左值引用
const int b = 8;
PerfectForward(b); // const 左值引用
PerfectForward(std::move(b)); // const 左值引用
return 0;
}

std::forward 完美转发在传参的过程中保留对象原生类型属性。
cpp
void Fun(int& x) { cout << "左值引用" << endl; }
void Fun(const int& x) { cout << "const 左值引用" << endl; }
void Fun(int&& x) { cout << "右值引用" << endl; }
void Fun(const int&& x) { cout << "const 右值引用" << endl; }
// std::forward<T>(t)在传参的过程中保持了t的原生类型属性。
template<typename T>
void PerfectForward(T&& t)
{
Fun(std::forward<T>(t));
}
int main()
{
PerfectForward(10); // 右值引用
int a;
PerfectForward(a); // 左值引用
PerfectForward(std::move(a)); // 右值引用
const int b = 8;
PerfectForward(b); // const 左值引用
PerfectForward(std::move(b)); // const 右值引用
return 0;
}

我们看完美转发在实际中的使用场景(实现双向循环链表):
cpp
#include <list>
#include <iostream>
using namespace std;
#include <iostream>
#include <string>
#include <utility>
using namespace std;
template<class T>
struct ListNode
{
ListNode* _next = nullptr;
ListNode* _prev = nullptr;
T _data;
};
template<class T>
class List
{
typedef ListNode<T> Node;
public:
List()
{
_head = new Node;
_head->_next = _head;
_head->_prev = _head;
}
template<typename U>
void PushBack(U&& x)
{
Insert(_head, std::forward<U>(x));
}
template<typename U>
void PushFront(U&& x)
{
Insert(_head->_next, std::forward<U>(x));
}
template<typename U>
void Insert(Node* pos, U&& x)
{
Node* prev = pos->_prev;
Node* newnode = new Node;
newnode->_data = std::forward<U>(x);
prev->_next = newnode;
newnode->_prev = prev;
newnode->_next = pos;
pos->_prev = newnode;
}
void Insert(Node* pos, const T& x)
{
Node* prev = pos->_prev;
Node* newnode = new Node;
newnode->_data = x;
prev->_next = newnode;
newnode->_prev = prev;
newnode->_next = pos;
pos->_prev = newnode;
}
private:
Node* _head;
};
int main()
{
List<string> l;
string s1("hello");
l.PushBack(s1); // 左值 --> 拷贝
l.PushBack("1111"); // 右值 --> 移动
l.PushFront("2222"); // 右值 --> 移动
return 0;
}
七.新的类功能
默认成员函数
C++98 的类中一共有 6 个默认成员函数,不手动写的话,编译器会自动生成:
- 构造函数
- 析构函数
- 拷贝构造函数
- 拷贝赋值重载
- 取地址重载
- const 取地址重载
实际开发里重点关注前 4 个,后两个几乎很少用到。
C++11 又新增两个默认成员函数:移动构造函数 、移动赋值运算符重载 。
关于移动构造与移动赋值,有几条关键规则:
- 如果我们没有手写移动构造,同时也没有手写析构、拷贝构造、拷贝赋值当中的任意一个,编译器才会自动生成默认移动构造。 默认移动构造做逐成员处理:内置类型成员直接拷贝;自定义类型成员,如果该类实现了移动构造就调用移动构造,没有实现就退回去调用拷贝构造。
- 如果我们没有手写移动赋值重载,同时也没有手写析构、拷贝构造、拷贝赋值当中的任意一个,编译器才会自动生成默认移动赋值。 默认移动赋值逻辑和默认移动构造大体一致:内置类型逐成员拷贝;自定义类型成员优先调用自身移动赋值,没有移动赋值就调用拷贝赋值。
- 只要我们自己实现了移动构造或者移动赋值其中任意一个,编译器就不会再自动生成拷贝构造和拷贝赋值。
来看代码:
cpp
#include <utility>
#include <iostream>
#include <string>
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:
std::string _name;
int _age;
};
int main()
{
Person s1;
Person s2 = s1; // s1是左值,调用拷贝构造
Person s3 = std::move(s1); // move转右值,触发编译器默认生成的移动构造
Person s4;
s4 = std::move(s2); // move转右值,触发编译器默认生成的移动赋值
return 0;
}
- Person 类,没有手写析构、拷贝构造、拷贝赋值,编译器自动生成移动构造和移动赋值。 std::move把左值转为右值,触发默认移动函数;
- 一旦我们手动实现析构、拷贝构造、拷贝赋值任意一个,编译器就不再生成移动构造和移动赋值,移动场景会降级走拷贝。
- VS2013 编译器对该特性支持不全,需要 VS2019 及以上运行观察效果。
类成员变量初始化
C++11 支持在类定义内部直接给成员变量设置默认初始值,编译器自动生成的默认构造函数,会使用这些缺省值完成成员初始化。这部分在类和对象章节已经讲过,这里不展开细说。
强制生成默认函数关键字 default
C++11 提供default关键字,可以手动控制编译器生成默认成员函数。 有些情况下编译器不会自动生成默认函数,比如:手写了拷贝构造之后,编译器就不再生成移动构造。这时如果仍然想要编译器生成默认版本的移动构造,就可以用default显式指定,强制编译器生成。
来看一个实际例子:
cpp
#include <utility>
#include <string>
class Person
{
public:
Person(const char* name = "", int age = 0)
:_name(name)
, _age(age)
{}
// 手写拷贝构造,按照规则编译器原本不会自动生成移动构造
Person(const Person& p)
:_name(p._name)
,_age(p._age)
{}
// 显式default,强制编译器生成默认版本的移动构造
Person(Person&& p) = default;
private:
std::string _name;
int _age;
};
int main()
{
Person s1;
Person s2 = s1; // s1是左值,调用手写的拷贝构造
Person s3 = std::move(s1); // std::move把s1转为右值,调用default生成的默认移动构造
return 0;
}
禁止生成默认函数的关键字 delete
如果想要限制某些默认函数的生成,C++98 的做法是:把该函数声明为private,并且只声明不实现。外部一旦尝试调用,就会编译报错。
到 C++11 写法更加简洁,直接在函数声明后面加上=delete;即可。告诉编译器不要生成该函数的默认版本,被=delete修饰的函数叫做删除函数。
cpp
#include <utility>
#include <string>
class Person
{
public:
Person(const char* name = "", int age = 0)
:_name(name)
, _age(age)
{}
// =delete:删除拷贝构造函数
Person(const Person& p) = delete;
private:
std::string _name;
int _age;
};
int main()
{
Person s1;
// Person s2 = s1; // 编译报错!拷贝构造已经被delete,不能拷贝对象
// Person s3 = std::move(s1); // 编译报错!
return 0;
}
接下来就到了继承和多态中的 final 与 override 关键字
这个我们在继承和多态章节已经进行了详细讲解这里就不再细讲了,如果需要的话去复习继承和多态章节,那里有你有你想要的答案哈。
八.可变参数模板
C++11 新增可变参数模板,支持编写可以接收任意数量模板参数的函数模板和类模板 。 在 C++98/03 时代,函数模板、类模板的参数个数必须写死,只能接收固定数量参数;可变模板参数算是一个很大的升级。但可变参数模板语法比较抽象,写代码有固定套路,理解起来挺困难的。日常学习工作,掌握基础用法即可,有余力再深入学习。
cpp
// Args是模板参数包,args是函数参数包,可以接收任意个任意类型参数
template<class ...Args>
void Show(Args&&... args)
{
}
- class ...Args:模板参数包,代表 0 个或多个不同类型的模板参数。
- Args&&... args:函数参数包,对应模板参数包,接收 0 个或多个函数实参。
- ... 省略号不是随便写,跟在参数包后面,代表参数包的展开。
注意: 可变参数包不能直接遍历,C++11 没有直接获取参数包长度、按下标取参数的语法。一般靠递归拆解参数包,逐个取出参数。
1.递归函数方式展开参数包
cpp
// 递归终止函数
template <class T>
void ShowList(const T& t)
{
cout << t << endl;
}
// 展开函数
template <class T, class ...Args>
void ShowList(T value, Args... args)
{
cout << value <<" ";
ShowList(args...);
}
int main()
{
ShowList(1);
ShowList(1, 'A');
ShowList(1, 'A', std::string("sort"));
return 0;
}
2.逗号表达式展开参数包
还有一种参数包展开思路,不用写递归终止函数,直接在expand函数内部就地展开。这里的printarg并不是递归终止函数,它就负责单独处理每一个参数,整个技巧的核心靠逗号表达式实现。
逗号表达式的执行规则是先跑逗号左边的代码,再返回逗号右边的值。 拿(printarg(args), 0)来说,会先调用printarg(args)处理参数,整个表达式最终得到结果 0。
代码里还搭配了 C++11 的初始化列表,用来创建一个临时 int 数组: {(printarg(args), 0)...},参数包展开之后就变成: ((printarg(arg1),0), (printarg(arg2),0), (printarg(arg3),0) ...)
最后得到一个全部元素都是 0 的数组,数组长度由sizeof...(Args)拿到的参数个数决定。 在数组初始化构造的时候,逗号表达式前面的printarg(args)就会挨个执行,把参数包里所有参数处理完。这个 int 数组本身没有业务用处,纯粹是借数组初始化这个语法特性,把参数包给展开。
cpp
#include <iostream>
#include <string>
using namespace std;
template <class T>
void PrintArg(T t)
{
cout << t << " ";
}
//展开函数
template <class ...Args>
void ShowList(Args... args)
{
// 逗号表达式+初始化列表展开参数包
int arr[] = { (PrintArg(args), 0)... };
cout << endl;
}
int main()
{
ShowList(1);
ShowList(1, 'A');
ShowList(1, 'A', std::string("sort"));
return 0;
}
STL 容器中的 empalce 相关接口函数:
https://cplusplus.com/reference/vector/vector/emplace_back/
cpp
template <class... Args>
void emplace_back (Args&&... args);
emplace 系列接口
emplace系列接口支持模板可变参数以及万能引用。那对比insert、push_back,emplace/emplace_back优势到底在哪?
cpp
#include <iostream>
#include <list>
#include <utility>
#include <string>
using namespace std;
int main()
{
list<pair<int, char>> mylist;
// emplace_back:接收构造对象所需参数,直接在容器内存原地构造pair对象
mylist.emplace_back(10, 'a');
mylist.emplace_back(20, 'b');
// 也可以传已经构造好的pair对象,这时用法和push_back差别不大
mylist.emplace_back(make_pair(30, 'c'));
// push_back:需要传入一个完整已经构造好的pair对象
mylist.push_back(make_pair(40, 'd'));
mylist.push_back({ 50, 'e' });
for (auto e : mylist)
{
cout << e.first << ":" << e.second << endl;
}
return 0;
}
cpp
int main()
{
// 使用带有拷贝、移动构造的string来观察区别
list<pair<int, string>> mylist;
// emplace_back:传入构造参数,容器内原地直接构造pair,省去拷贝/移动
mylist.emplace_back(10, "sort");
// 传入现成pair对象,就会走移动/拷贝,优势体现不出来
mylist.emplace_back(make_pair(20, "sort"));
// push_back:必须先外部构造好pair临时对象,再移动进容器
mylist.push_back(make_pair(30, "sort"));
mylist.push_back({ 40, "sort" });
return 0;
}
emplace_back最核心的好处就是原地构造。 直接把构造对象要用的参数丢给它,容器就在自己的内存空间里把对象直接造出来。不用先在外面生成临时对象,再拷贝或者移动到容器内部,省下了拷贝、移动的开销。
但如果你传给emplace_back的本身就是一个已经造好的对象,比如make_pair返回的结果,这时候它的表现和push_back基本没区别,照样会触发移动或者拷贝,体现不出性能优势。
反观push_back,只能接收完整对象。得先把对象在外部构造完毕,再通过移动或者拷贝把对象放进容器里。
九.lambda 表达式
1.举C++98 中的一个例子
C++98 里,如果要对一组数据排序,可以使用std::sort。
cpp
#include <algorithm>
#include <functional>
int main()
{
int array[] = {5,2,9,1,7,3,0,8,4,6};
// 默认使用小于比较,排序结果为升序
std::sort(array, array + sizeof(array)/sizeof(array[0]));
// 如果想要降序,就要传入自定义比较规则
std::sort(array, array + sizeof(array)/sizeof(array[0]), greater<int>());
return 0;
}
std::sort前两个参数传指针,排序区间是左闭右开。sizeof(array)/sizeof(array0)用来算出数组元素个数。不给第三个参数,默认用<比较,排升序。 要降序就传仿函数greater<int>(),记得包含<functional>头文件。
C++98 想改比较规则,要么用现成仿函数,要么自己写全局函数。
问题在于:哪怕逻辑很简单,也要额外定义函数,写起来麻烦,lambda 就是为了解决这个麻烦。
如果待排序元素为自定义类型,需要用户定义排序时的比较规则:
cpp
#include <vector>
#include <algorithm>
#include <string>
using namespace std;
struct Goods
{
string _name; // 商品名称
double _price; // 商品价格
int _score; // 评分
Goods(const char* str, double price, int score)
:_name(str)
, _price(price)
, _score(score)
{}
};
// 仿函数:按价格升序
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 = { {"珍珠奶茶",12.5,4}, {"果茶",15.2,5}, {"可乐",4.5,3}, {"咖啡",18.0,4} };
sort(v.begin(), v.end(), ComparePriceLess()); // 按价格从小到大排序
sort(v.begin(), v.end(), ComparePriceGreater()); // 按价格从大到小排序
return 0;
}
这里拿奶茶商品做示例,要给商品容器排序。C++98 如果想改排序规则,就得单独写仿函数结构体,重载operator()。
ComparePriceLess仿函数,用<对比价格,实现价格升序。 ComparePriceGreater仿函数,用>对比价格,实现价格降序。
创建仿函数对象传给 sort 的第三个参数,sort 内部就会调用仿函数的operator()做元素对比。
缺点也很实在:明明就很短的比较逻辑,还得额外定义两个结构体,代码分得很散,写着麻烦。C++11 的 lambda 就解决了这个问题,可以直接在 sort 调用的地方就地写比较代码,不用再单独写仿函数。
2.lambda 表达式
随着 C++ 语法的发展,人们开始觉得上面的写法太复杂了,每次为了实现一个 algorithm 算法, 都要重新去写一个类,如果每次比较的逻辑不一样,还要去实现多个类,特别是相同类的命名, 这些都给编程者带来了极大的不便。因此,在 C++11 语法中出现了 Lambda 表达式。
cpp
#include <vector>
#include <algorithm>
#include <string>
using namespace std;
struct Goods
{
string _name; // 商品名
double _price; // 价格
int _evaluate; // 评价分数
Goods(const char* str, double price, int evaluate)
:_name(str)
, _price(price)
, _evaluate(evaluate)
{}
};
int main()
{
vector<Goods> v = { { "苹果", 2.1, 5 }, { "香蕉", 3, 4 }, { "橙子", 2.2, 3 }, { "菠萝", 1.5, 4 } };
// 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;
}
上述代码就是使用 C++11 中的 lambda 表达式来解决,可以看出 lambda 表达式实际是一个匿名函数。
3.lambda 表达式语法
lambda 完整书写格式:capture‑list (parameters) mutable -> return‑type { statement }
a.各部分说明
- capture‑list 捕捉列表 写在 lambda 最开头,编译器靠\[\]识别这是 lambda。作用是把上下文的变量捕获进来,供 lambda 内部使用。
- (parameters) 参数列表 和普通函数参数用法一样。如果不需要参数,可以把括号直接省掉。
- mutable lambda默认自带 const 属性,不能修改捕获到的值。加mutable可以取消这个 const 限制。只要用了 mutable,参数括号就不能省略,就算没有参数也要写()。
- -> return‑type 返回值类型 追踪返回类型写法。没有返回值可以不写;返回值类型很明确的时候,也可以省略,交给编译器自动推导。{statement} 函数体 里面写业务逻辑。除了传入的参数,也可以使用捕获过来的外部变量。
注意 参数列表、返回值类型是可选;捕捉列表和函数体不能丢。
所以 C++11 最简单合法的 lambda 就是:\[\]{};,不过这个 lambda 什么都干不了。
来看代码:
cpp
#include <iostream>
using namespace std;
int main()
{
// 最简单的lambda,捕获、函数体都为空,没有实际作用
[]{};
int a = 3, b = 4;
// [=] 值捕获:把外部变量按值拷贝进来,返回值int交给编译器推导
[=]{return a + 3; };
// [&] 引用捕获,省略返回值,无返回;用auto接收lambda对象
auto fun1 = [&](int c){b = a + c; };
fun1(10);
cout<<a<<" "<<b<<endl;
// 混合捕获:a值捕获,b引用捕获,显式指定返回值int
auto fun2 = [=, &b](int c)->int{return b += a+ c; };
cout<<fun2(10)<<endl;
// 值捕获x,mutable取消const,允许修改拷贝过来的x副本
int x = 10;
auto add_x = [x](int a) mutable { x *= 2; return a + x; };
cout << add_x(10) << endl;
return 0;
}
通过上面代码可以看出,lambda 表达式实际上可以理解为无名函数,该函数无法直接调用,如果想要直接调用,可借助 auto 将其赋值给一个变量。
b.捕获列表说明
捕捉列表
捕捉列表决定 lambda 可以拿到哪些外部变量,是拷贝过来用,还是直接拿原变量引用。
- var:值捕获变量 var
- =:把父作用域所有变量都值捕获,也包含 this 指针
- \&var:引用捕获变量 var
- \&:把父作用域所有变量都引用捕获,也包含 this 指针
- this:值捕获当前对象的 this 指针
注意
- 父作用域,就是包裹这个 lambda 的代码块。
- 捕捉列表支持多个捕获项,逗号隔开。 举例子:=, \&a, \&b:a、b 用引用捕获,剩下其他变量全部值捕获。\&, a, this:a 和 this 值捕获,其余变量全部引用捕获。
- 同一个变量不能重复捕获,不然直接编译报错。 像=, a,=已经把全部变量值捕获了,再写 a 就重复。
- 如果 lambda 写在代码块外面(全局位置),捕获列表必须是空\[\]。
- 写在代码块内部的 lambda,只能抓父作用域的局部变量。去抓别的作用域、非局部变量,编译报错。
- lambda 之间不能互相赋值,就算看着类型一样也不行。
cpp
#include <iostream>
using namespace std;
// 函数指针类型
void (*PF)();
int main()
{
auto f1 = []{cout << "hello world" << endl; };
auto f2 = []{cout << "hello world" << endl; };
// f1 = f2; // 编译报错,lambda不支持赋值重载operator=()
// 可以用lambda拷贝构造出新副本
auto f3(f2);
f3();
// 无捕获的lambda可以赋值给对应类型的函数指针
PF = f2;
PF();
return 0;
}

大致解析:
f1 和 f2 两个 lambda,哪怕代码写得一模一样,也是完全不同的类型。所以
f1=f2会编译报错,lambda 本身没有赋值运算符。但拷贝构造是可以的,auto f3(f2)直接复制 f2 生成 f3 副本,正常调用
f3()没问题。重点:只有捕获为空的 lambda,才可以赋值给普通函数指针。只要 lambda 有捕获,就不能往函数指针 PF 上赋值。
**小提示:**编译器会给每一个 lambda 单独生成专属仿函数类,这就是写法相同但类型不一样的原因,学完 lambda 底层原理就好懂了。
4.函数对象与 lambda 表达式
函数对象也叫仿函数,就是类里面重载了operator(),让类的对象可以像函数一样调用。
cpp
#include <iostream>
using namespace std;
class Rate
{
public:
Rate(double rate): _rate(rate)
{}
double operator()(double money, int year)
{
return money * _rate * year;
}
private:
double _rate;
};
int main()
{
// 使用仿函数
double rate = 0.49;
Rate r1(rate);
r1(10000, 2);
// lambda实现同样功能
auto r2 = [=](double money, int year)->double{return money*rate*year; };
r2(10000, 2);
return 0;
}
大致解析:
仿函数对象 r1 创建出来之后,直接r1(参数)就能调用,底层实际执行的就是类里的operator()。
lambda 的 r2 完成完全一样的计算。编译器背后会把 lambda 转成仿函数类,所以 lambda 和仿函数本质是一回事。
=把 rate 变量按值捕获进来,接收本金和年限,算出利息。
我们仔细对比来看:仿函数得我们自己手写类;lambda 不用写类,写代码更省事。

实际在底层编译器对于 lambda 表达式的处理方式,完全就是按照函数对象的方式处理的,即:如果定义了一个 lambda 表达式,编译器会自动生成一个类,在该类中重载了operator()。
十.包装器
1.function 包装器
function 包装器也叫适配器,C++ 里function本质是一个类模板,用来包装各类可调用对象。
为什么要用 function?
ret = func(x); 这里的func可以是普通函数、函数指针、仿函数、lambda 对象,这些全都属于可调用类型。 如果直接拿它们去传模板参数,每一种不同类型都会实例化出一份全新的模板,会造成模板代码膨胀
cpp
#include <iostream>
using namespace std;
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 f(double i)
{
return i / 2;
}
// 仿函数
struct Functor
{
double operator()(double d)
{
return d / 3;
}
};
int main()
{
// 传普通函数名
cout << useF(f, 11.11) << endl;
// 传仿函数对象
cout << useF(Functor(), 11.11) << endl;
// 传lambda表达式
cout << useF([](double d)->double{ return d/4; }, 11.11) << endl;
return 0;
}
模板参数F会根据传入可调用对象的类型进行实例化。 这里传了三种不一样的可调用类型:普通函数、仿函数、lambda。 所以模板useF会生成三份不同实例。 函数内部的静态变量count,每一份实例都有属于自己的静态变量,看&count打印出来的地址就能看出来地址不一样。
问题就在这里:功能差不多,因为可调用对象类型不同,就生成多份模板代码,带来代码膨胀。 std::function就是用来解决这个问题,把不同种类可调用对象包装成同一种function类型,模板就只会实例化一份。
通过上面的程序验证,我们会发现 useF 函数模板实例化了三份。包装器function就可以很好的解决上面的问题
cpp
// std::function在头文件<functional>
// 类模板原型如下
template <class T> function; // undefined
template <class Ret, class... Args>
class function<Ret(Args...)>;
// 模板参数说明:
// Ret: 被调用函数的返回类型
// Args...:被调用函数的形参
cpp
// 使用方法如下:
#include <functional>
#include <iostream>
using namespace std;
int f(int a, int b)
{
return a + b;
}
struct Functor
{
public:
int operator() (int a, int b)
{
return a + b;
}
};
class Plus
{
public:
static int plusi(int a, int b)
{
return a + b;
}
double plusd(double a, double b)
{
return a + b;
}
};
int main()
{
// 函数名(函数指针)
std::function<int(int, int)> func1 = f;
cout << func1(1, 2) << endl;
// 函数对象
std::function<int(int, int)> func2 = Functor();
cout << func2(1, 2) << endl;
// lambda表达式
std::function<int(int, int)> func3 = [](const int a, const int b)
{
return a + b;
};
cout << func3(1, 2) << endl;
// 类的静态成员函数
std::function<int(int, int)> func4 = &Plus::plusi;
cout << func4(1, 2) << endl;
// 类的普通成员函数,需要传入对象实例
std::function<double(Plus, double, double)> func5 = &Plus::plusd;
cout << func5(Plus(), 1.1, 2.2) << endl;
return 0;
}
std::function<返回值(参数列表)>就是包装器的写法,各种可调用对象都能包成同一种 function 类型。
- func1:包装普通函数或者函数指针
- func2:包装仿函数对象
- func3:包装 lambda 表达式
- func4:包装类静态成员函数,直接拿函数地址就行
- func5:包装普通成员函数。普通成员函数离不开类对象,调用的时候得传类实例,这里直接传临时对象Plus()
包装完之后,不管原本是函数、仿函数还是 lambda,全都直接func(实参)调用。 记得要包含头文件<functional>
有了包装器,如何解决模板的效率低下,实例化多份的问题呢?
来看代码:
cpp
#include <iostream>
#include <functional>
using namespace std;
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 f(double i)
{
return i / 2;
}
struct Functor
{
double operator()(double d)
{
return d / 3;
}
};
int main()
{
// 普通函数包装进function
std::function<double(double)> func1 = f;
cout << useF(func1, 11.11) << endl;
// 仿函数包装进function
std::function<double(double)> func2 = Functor();
cout << useF(func2, 11.11) << endl;
// lambda包装进function
std::function<double(double)> func3 = [](double d)->double{ return d / 4; };
cout << useF(func3, 11.11) << endl;
return 0;
}
现在用std::function,把普通函数、仿函数、lambda 全都包装成function<double(double)>这同一个类型。
接着把 func1、func2、func3 传给模板 useF,模板参数 F 就变成同一种 function 类型,模板只会生成一份实例。 里面的静态变量 count 也就只有一份,打印&count,地址全部一样。
这样就解决了前面的问题:底层调用对象虽然不一样,经过 function 包装之后类型统一,不会生成多份模板代码,避免代码膨胀。
2.bind
std::bind定义在<functional>头文件,属于函数模板,也算是函数适配器。接收一个可调用对象,生成出新的可调用对象,用来适配原来函数的参数列表。
简单说:原本函数接收 N 个参数,可以用 bind 绑定一部分实参,得到一个接收 M 个参数的新函数;也可以用来调换参数顺序。
bind 原型:
cpp
template <class Fn, class... Args>
/* unspecified */ bind (Fn&& fn, Args&&... args);
template <class Ret, class Fn, class... Args>
/* unspecified */ bind (Fn&& fn, Args&&... args);
可以把 bind 当成通用的函数适配器。接收一个可调用对象,产出一个新的可调用对象,用来调整原函数的参数。
bind 常用写法:auto newCallable = bind(callable, arg_list);
- newCallable 就是 bind 生成的新可调用对象。
- arg_list 是一串参数,逗号隔开,用来对应原 callable 的参数。调用 newCallable 的时候,内部就会跑原来的 callable,把 arg_list 里面的参数传过去。
arg_list 里可以写_1、_2这种占位符。 _1代表新函数的第一个参数,_2代表第二个,以此类推。占位符用来占住参数位置,也可以调换参数的先后顺序。
来看代码:
cpp
// 使用举例
#include <iostream>
#include <functional>
using namespace std;
int Plus(int a, int b)
{
return a + b;
}
class Sub
{
public:
int sub(int a, int b)
{
return a - b;
}
};
int main()
{
//表示绑定函数plus 参数分别由调用 func1 的第一,二个参数指定
std::function<int(int, int)> func1 = std::bind(Plus, placeholders::_1, placeholders::_2);
//auto func1 = std::bind(Plus, placeholders::_1, placeholders::_2);
//func2的类型为 function<void(int, int, int)> 与func1类型一样
//表示绑定函数 plus 的第一,二为: 1, 2
auto func2 = std::bind(Plus, 1, 2);
cout << func1(1, 2) << endl;
cout << func2() << endl;
Sub s;
// 绑定成员函数
std::function<int(int, int)> func3 = std::bind(&Sub::sub, s,
placeholders::_1, placeholders::_2);
// 参数调换顺序
std::function<int(int, int)> func4 = std::bind(&Sub::sub, s,
placeholders::_2, placeholders::_1);
cout << func3(1, 2) << endl;
cout << func4(1, 2) << endl;
return 0;
}
- func1:绑定普通函数 Plus。_1、_2是占位符,调用新函数时传进来的两个参数,直接交给原来的 Plus。
- func2:直接把 Plus 的两个参数固定成 1 和 2,调用func2()不用给参数,内部就跑Plus(1,2)。 代码注释里写的function<void(int, int, int)>是写错了,并不是真实类型。
- func3:绑定类普通成员函数 sub。普通成员函数得依赖对象,这里传入对象 s;_1、_2接收外面传的参数,等价于s.sub(1,2)。
- func4:靠占位符调换参数顺序。把_2放前面,_1放后面。调用func4(1,2)实际执行s.sub(2,1),算出结果2‑1=1。
**注意:**placeholders::_1、_2属于std::placeholders命名空间,代码记住要包含头文件<functional>。