C++(进阶) 第9章 C++ 11
前言
C++11 是 C++ 的第⼆个主要版本,并且是从 C++98 起的最重要更新。它引⼊了⼤量更改,标准化了既
有实践,并改进了对 C++ 程序员可⽤的抽象
一、C++11的发展历史
C++11 是 C++ 的第⼆个主要版本,并且是从 C++98 起的最重要更新。它引⼊了⼤量更改,标准化了既
有实践,并改进了对 C++ 程序员可⽤的抽象。在它最终由 ISO 在 2011 年 8 ⽉ 12 ⽇采纳前,⼈们曾使
⽤名称“C++0x”,因为它曾被期待在 2010 年之前发布。C++03 与 C++11 期间花了 8 年时间,故⽽这
是迄今为⽌最⻓的版本间隔。从那时起,C++ 有规律地每 3 年更新⼀次。、
二、 列表初始化
2.1 C++98传统的{}
C++98中⼀般数组和结构体可以⽤{}进⾏初始化
struct Point
{
int _x;
int _y;
};
int main()
{
int array1[] = { 1, 2, 3, 4, 5 };
int array2[5] = { 0 };
Point p = { 1, 2 };
return 0;
}
2.2 C++11中的{}
• C++11以后想统⼀初始化⽅式,试图实现⼀切对象皆可⽤{}初始化,{}初始化也叫做列表初始化。
• 内置类型⽀持,⾃定义类型也⽀持,⾃定义类型本质是类型转换,中间会产⽣临时对象,最后优化了以后变成直接构造。
• {}初始化的过程中,可以省略掉=
• C++11列表初始化的本意是想实现⼀个⼤统⼀的初始化⽅式,其次他在有些场景下带来的不少便利,如容器push/inset多参数构造的对象时,{}初始化会很⽅便
#include <iostream>
#include <vector>
using namespace std;
struct Point
{
int _x;
int _y;
};
class Date
{
public:
Date(int year = 1, int month = 1, int day = 1)
: _year(year),
_month(month),
_day(day)
{
cout << "Date(int year, int month, int day)" << endl;
}
Date(const Date& d)
: _year(d._year),
_month(d._month),
_day(d._day)
{
cout << "Date(const Date& d)" << endl;
}
private:
int _year;
int _month;
int _day;
};
// ⼀切皆可⽤列表初始化,且可以不加 =
int main()
{
// -------------------- C++98 初始化方式 --------------------
int a1[] = {1, 2, 3, 4, 5};
int a2[5] = {0};
Point p = {1, 2};
// -------------------- C++11 列表初始化 --------------------
// 内置类型支持
int x1 = {2};
int x2{2}; // 省略 = 的形式
// 自定义类型支持(编译器优化拷贝构造)
Date d1 = {2025, 1, 1}; // 列表初始化(带=)
Date d6{2024, 7, 25}; // 列表初始化(不带=)
const Date& d2 = {2024, 7, 25}; // 引用临时对象
const Date& d7{2024, 7, 25}; // 引用临时对象(不带=)
// 单参数简化写法
Date d3 = {2025}; // 列表初始化
Date d4 = 2025; // C++98 风格类型转换
// 结构体初始化
Point p1{1, 2}; // 省略 = 的形式
// 容器初始化
vector<Date> v;
v.push_back(d1); // 有名对象
v.push_back(Date(2025, 1, 1)); // 匿名对象
v.push_back({2025, 1, 1}); // 直接列表初始化(推荐方式)
// Date d8 2025; // 错误:必须使用构造函数语法或列表初始化
return 0;
}
2.3 C++11中的std::initializer_list
上⾯的初始化已经很⽅便,但是对象容器初始化还是不太⽅便,⽐如⼀个vector对象,我想⽤N个
值去构造初始化,那么我们得实现很多个构造函数才能⽀持, vector v1 ={1,2,3};vector v2 = {1,2,3,4,5};
• C++11库中提出了⼀个std::initializer_list的类, auto il = { 10, 20, 30 }; // thetype of il is an initializer_list ,这个类的本质是底层开⼀个数组,将数据拷⻉过来,std::initializer_list内部有两个指针分别指向数组的开始和结束。
• 这是他的⽂档:initializer_list,std::initializer_list⽀持迭代器遍历。
• 容器⽀持⼀个std::initializer_list的构造函数,也就⽀持任意多个值构成的 {x1,x2,x3…} 进⾏初始化。STL中的容器⽀持任意多个值构成的 {x1,x2,x3…} 进⾏初始化,就是通过std::initializer_list的构造函数⽀持的。
// STL中的容器都增加了⼀个initializer_list的构造
vector(initializer_list<value_type> il,
const allocator_type& alloc = allocator_type());
list(initializer_list<value_type> il,
const allocator_type& alloc = allocator_type());
map(initializer_list<value_type> il,
const key_compare& comp = key_compare(),
const allocator_type& alloc = allocator_type());
// ...
template<class T>
class vector {
public:
typedef T* iterator;
vector(initializer_list<T> l)
{
for (auto e : l)
push_back(e)
}
private:
iterator _start = nullptr;
iterator _finish = nullptr;
iterator _endofstorage = nullptr;
};
// 另外,容器的赋值也⽀持initializer_list的版本
vector& operator=(initializer_list<value_type> il);
map& operator=(initializer_list<value_type> il);
#include <iostream>
#include <vector>
#include <string>
#include <map>
using namespace std;
int main()
{
std::initializer_list<int> mylist;
mylist = {10, 20, 30};
cout << sizeof(mylist) << endl;
// 这⾥begin和end返回的值initializer_list对象中存的两个指针
// 这两个指针的值跟i的地址跟接近,说明数组存在栈上
int i = 0;
cout << mylist.begin() << endl;
cout << mylist.end() << endl;
cout << &i << endl;
// {}列表中可以有任意多个值
// 这两个写法语义上还是有差别的,第⼀个v1是直接构造,
// 第⼆个v2是构造临时对象+临时对象拷⻉v2+优化为直接构造
vector<int> v1({1, 2, 3, 4, 5});
vector<int> v2 = {1, 2, 3, 4, 5};
const vector<int>& v3 = {1, 2, 3, 4, 5};
// 这⾥是pair对象的{}初始化和map的initializer_list构造结合到⼀起⽤了
map<string, string> dict = {{"sort", "排序"}, {"string", "字符串"}};
// initializer_list版本的赋值⽀持
v1 = {10, 20, 30, 40, 50};
return 0;
}
三、右值引⽤和移动语义
C++98的C++语法中就有引⽤的语法,⽽C++11中新增了的右值引⽤语法特性,C++11之后我们之前学习的引⽤就叫做左值引⽤。⽆论左值引⽤还是右值引⽤,都是给对象取别名。
3.1 左值和右值
•左值是⼀个表⽰数据的表达式(如变量名或解引⽤的指针),⼀般是有持久状态,存储在内存中,我们可以获取它的地址,左值可以出现赋值符号的左边,也可以出现在赋值符号右边。定义时const修饰符后的左值,不能给他赋值,但是可以取它的地址。
• 右值也是⼀个表⽰数据的表达式,要么是字⾯值常量、要么是表达式求值过程中创建的临时对象等,右值可以出现在赋值符号的右边,但是不能出现出现在赋值符号的左边,右值不能取地址。
• 值得⼀提的是,左值的英⽂简写为lvalue,右值的英⽂简写为rvalue。传统认为它们分别是leftvalue、right value 的缩写。现代C++中,lvalue 被解释为loactor value的缩写,可意为存储在内存中、有明确存储地址可以取地址的对象,⽽ rvalue 被解释为 read value,指的是那些可以提供数据值,但是不可以寻址,例如:临时变量,字⾯量常量,存储于寄存器中的变量等,也就是说左值和右值的核⼼区别就是能否取地址。
#include <iostream>
using namespace std;
int main()
{
// -------------------- 左值示例 --------------------
// 左值:可以取地址
int* p = new int(0);
int b = 1;
const int c = b;
*p = 10;
string s("111111");
s[0] = 'x';
// 验证左值地址
cout << &c << endl;
cout << (void*)&s[0] << endl;
// -------------------- 右值示例 --------------------
double x = 1.1, y = 2.2;
// 右值:不能取地址
10; // 字面常量
x + y; // 表达式结果
fmin(x, y); // 函数返回值
string("11111"); // 匿名对象
// 以下尝试取右值地址会编译失败(已注释)
// cout << &10 << endl;
// cout << &(x + y) << endl;
// cout << &(fmin(x, y)) << endl;
// cout << &string("11111") << endl;
return 0;
}
3.2 左值引⽤和右值引⽤
• Type& r1 = x; Type&& rr1 = y; 第⼀个语句就是左值引⽤,左值引⽤就是给左值取别名,第⼆个就是右值引⽤,同样的道理,右值引⽤就是给右值取别名。
• 左值引⽤不能直接引⽤右值,但是const左值引⽤可以引⽤右值
• 右值引⽤不能直接引⽤左值,但是右值引⽤可以引⽤move(左值)
• template typename remove_reference::type&& move (T&&
arg);
• move是库⾥⾯的⼀个函数模板,本质内部是进⾏强制类型转换,当然他还涉及⼀些引⽤折叠的知识,这个我们后⾯会细讲。
• 需要注意的是变量表达式都是左值属性,也就意味着⼀个右值被右值引⽤绑定后,右值引⽤变量变量表达式的属性是左值
• 语法层⾯看,左值引⽤和右值引⽤都是取别名,不开空间。从汇编底层的⻆度看下⾯代码中r1和rr1汇编层实现,底层都是⽤指针实现的,没什么区别。底层汇编等实现和上层语法表达的意义有时是背离的,所以不要然到⼀起去理解,互相佐证,这样反⽽是陷⼊迷途
#include <iostream>
using namespace std;
template <class _Ty>
remove_reference_t<_Ty>&& move(_Ty&& _Arg)
{
return static_cast<remove_reference_t<_Ty>&&>(_Arg);
}
int main()
{
// ==================== 左值示例 ====================
int* p = new int(0);
int b = 1;
const int c = b;
*p = 10;
string s("111111");
s[0] = 'x';
double x = 1.1, y = 2.2;
// 左值引用(绑定左值)
int& r1 = b;
int*& r2 = p;
int& r3 = *p;
string& r4 = s;
char& r5 = s[0];
// ==================== 右值引用示例 ====================
// 右值引用(直接绑定右值)
int&& rr1 = 10;
double&& rr2 = x + y;
double&& rr3 = fmin(x, y);
string&& rr4 = string("11111");
// const左值引用绑定右值
const int& rx1 = 10;
const double& rx2 = x + y;
const double& rx3 = fmin(x, y);
const string& rx4 = string("11111");
// ==================== move语义示例 ====================
// 右值引用绑定move后的左值
int&& rrx1 = move(b);
int*&& rrx2 = move(p);
int&& rrx3 = move(*p);
string&& rrx4 = move(s);
string&& rrx5 = (string&&)s;
// ==================== 左值验证 ====================
cout << &b << endl; // 左值地址
cout << &r1 << endl; // 左值引用地址
cout << &rr1 << endl; // 右值引用变量本身是左值
// ==================== 右值引用特性验证 ====================
int& r6 = r1; // 允许:左值引用绑定左值
// int&& rrx6 = rr1; // 错误:右值引用不能绑定左值
int&& rrx6 = move(rr1); // 正确:强制转换
return 0;
}
3.3 引⽤延⻓⽣命周期
右值引⽤可⽤于为临时对象延⻓⽣命周期,const 的左值引⽤也能延⻓临时对象⽣存期,但这些对象⽆法被修改。
#include <iostream>
#include <string>
int main()
{
std::string s1 = "Test";
// 错误示例:右值引用不能绑定左值
// std::string&& r1 = s1; // 编译错误
// const左值引用可以延长临时对象生命周期
const std::string& r2 = s1 + s1;
// r2 += "Test"; // 编译错误:const引用不可修改
// 右值引用绑定临时对象,并延长其生命周期
std::string&& r3 = s1 + s1;
r3 += "Test"; // 合法:通过非const右值引用修改
std::cout << r3 << '\n'; // 输出合并后的字符串
return 0;
}
3.4 左值和右值的参数匹配
• C++98中,我们实现⼀个const左值引⽤作为参数的函数,那么实参传递左值和右值都可以匹配。
• C++11以后,分别重载左值引⽤、const左值引⽤、右值引⽤作为形参的f函数,那么实参是左值会匹配f(左值引⽤),实参是const左值会匹配f(const 左值引⽤),实参是右值会匹配f(右值引⽤)。
• 右值引⽤变量在⽤于表达式时属性是左值,这个设计这⾥会感觉跟怪,下⼀⼩节我们讲右值引⽤的使⽤场景时,就能体会这样设计的价值了
#include <iostream>
using namespace std;
void f(int& x)
{
cout << "左值引用重载 f(" << x << ")\n";
}
void f(const int& x)
{
cout << "到 const 的左值引用重载 f(" << x << ")\n";
}
void f(int&& x)
{
cout << "右值引用重载 f(" << x << ")\n";
}
int main()
{
int i = 1;
const int ci = 2;
// 基础调用测试
f(i); // 调用 f(int&)
f(ci); // 调用 f(const int&)
f(3); // 调用 f(int&&)
f(move(i)); // 调用 f(int&&)
// 右值引用变量特性验证
int&& x = 1;
f(x); // 调用 f(int&) → 右值引用变量是左值
f(move(x)); // 调用 f(int&&)
return 0;
}
3.5 右值引⽤和移动语义的使⽤场景
3.5.1 左值引⽤主要使⽤场景回顾
左值引⽤主要使⽤场景是在函数中左值引⽤传参和左值引⽤传返回值时减少拷⻉,同时还可以修改实参和修改返回对象的价值。左值引⽤已经解决⼤多数场景的拷⻉效率问题,但是有些场景不能使⽤传左值引⽤返回,如addStrings和generate函数,C++98中的解决⽅案只能是被迫使⽤输出型参数解决。那么C++11以后这⾥可以使⽤右值引⽤做返回值解决吗?显然是不可能的,因为这⾥的本质是返回对象是⼀个局部对象,函数结束这个对象就析构销毁了,右值引⽤返回也⽆法概念对象已经析构销毁
的事实。
#include <vector>
#include <algorithm>
using namespace std;
class StringSolution {
public:
// 使用移动语义优化返回值
string addStrings(string num1, string num2) {
string str;
int end1 = num1.size()-1, end2 = num2.size()-1;
int next = 0;
while(end1 >= 0 || end2 >= 0) {
int val1 = end1 >= 0 ? num1[end1--]-'0' : 0;
int val2 = end2 >= 0 ? num2[end2--]-'0' : 0;
int ret = val1 + val2 + next;
next = ret / 10;
ret %= 10;
str.push_back(static_cast<char>('0' + ret)); // 显式类型转换
}
if(next == 1) {
str.push_back('1');
}
reverse(str.begin(), str.end());
return move(str); // 显式移动语义
}
};
class VectorSolution {
public:
// 优化大对象返回效率
vector<vector<int>> generate(int numRows) {
vector<vector<int>> vv(numRows);
// 预分配内存
for(int i = 0; i < numRows; ++i) {
vv[i].reserve(i+1); // 使用 reserve 替代 resize 提升性能
vv[i].resize(i+1, 1); // 保持初始化逻辑
}
// 计算杨辉三角
for(int i = 2; i < numRows; ++i) {
for(int j = 1; j < i; ++j) {
vv[i][j] = vv[i-1][j] + vv[i-1][j-1];
}
}
return move(vv); // 显式移动语义
}
};
3.5.2 移动构造和移动赋值
• 移动构造函数是⼀种构造函数,类似拷⻉构造函数,移动构造函数要求第⼀个参数是该类类型的引⽤,但是不同的是要求这个参数是右值引⽤,如果还有其他参数,额外的参数必须有缺省值。
• 移动赋值是⼀个赋值运算符的重载,他跟拷⻉赋值构成函数重载,类似拷⻉赋值函数,移动赋值函数要求第⼀个参数是该类类型的引⽤,但是不同的是要求这个参数是右值引⽤。
• 对于像string/vector这样的深拷⻉的类或者包含深拷⻉的成员变量的类,移动构造和移动赋值才有意义,因为移动构造和移动赋值的第⼀个参数都是右值引⽤的类型,他的本质是要“窃取”引⽤的右值对象的资源,⽽不是像拷⻉构造和拷⻉赋值那样去拷⻉资源,从提⾼效率。下⾯的bit::string样例实现了移动构造和移动赋值,我们需要结合场景理解。
#define _CRT_SECURE_NO_WARNINGS 1
#include <iostream>
#include <assert.h>
#include <string.h>
#include <algorithm>
using namespace std;
namespace bit {
class string {
public:
// 迭代器类型定义
typedef char* iterator;
typedef const char* const_iterator;
// 迭代器接口
iterator begin() { return _str; }
iterator end() { return _str + _size; }
const_iterator begin() const { return _str; }
const_iterator end() const { 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);
}
// 交换函数
void swap(string& s) {
::swap(_str, s._str);
::swap(_size, s._size);
::swap(_capacity, s._capacity);
}
// 拷贝构造函数
string(const string& s) : _str(nullptr)
{
cout << "string(const string& s) -- 拷贝构造" << endl;
reserve(s._capacity);
for (auto ch : s) {
push_back(ch);
}
}
// 移动构造函数 (关键点1)
string(string&& s)
{
cout << "string(string&& s) -- 移动构造" << endl;
swap(s); // 交换资源所有权
}
// 拷贝赋值运算符
string& operator=(const string& s)
{
cout << "string& operator=(const string& s) -- 拷贝赋值" << endl;
if (this != &s) {
_str[0] = '\0'; // 清空当前内容
_size = 0;
reserve(s._capacity);
for (auto ch : s) {
push_back(ch);
}
}
return *this;
}
// 移动赋值运算符 (关键点2)
string& operator=(string&& s)
{
cout << "string& operator=(string&& s) -- 移动赋值" << endl;
swap(s); // 交换资源所有权
return *this;
}
~string()
{
cout << "~string() -- 析构" << endl;
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];
if (_str)
{
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; }
size_t size() const { return _size; }
private:
char* _str = nullptr;
size_t _size = 0;
size_t _capacity = 0;
};
}
int main()
{
// 基础构造
bit::string s1("xxxxx"); // 普通构造
// 拷贝语义测试
bit::string s2 = s1; // 拷贝构造
// 移动语义测试 (关键点3)
bit::string s3 = bit::string("yyyyy"); // 构造+移动构造
3.5.3 右值引⽤和移动语义解决传值返回问题
namespace bit
{
string addStrings(string num1, string num2)
{
string str;
int end1 = num1.size() - 1, end2 = num2.size() - 1;
int next = 0;
while (end1 >= 0 || end2 >= 0)
{
int val1 = end1 >= 0 ? num1[end1--] - '0' : 0;
int val2 = end2 >= 0 ? num2[end2--] - '0' : 0;
int ret = val1 + val2 + next;
next = ret / 10;
ret = ret % 10;
str += ('0' + ret);
}
if (next == 1)
str += '1';
reverse(str.begin(), str.end());
cout << "******************************" << endl;
return str;
}
}
// 场景1
int main()
{
bit::string ret = bit::addStrings("11111", "2222");
cout << ret.c_str() << endl;
return 0;
}
// 场景2
int main()
{
bit::string ret;
ret = bit::addStrings("11111", "2222");
cout << ret.c_str() << endl;
return 0;
}
右值对象构造,只有拷⻉构造,没有移动构造的场景
• 图1展⽰了vs2019 debug环境下编译器对拷⻉的优化,左边为不优化的情况下,两次拷⻉构造,右边为编译器优化的场景下连续步骤中的拷⻉合⼆为⼀变为⼀次拷⻉构造。
• 需要注意的是在vs2019的release和vs2022的debug和release,下⾯代码优化为⾮常恐怖,会直接将str对象的构造,str拷⻉构造临时对象,临时对象拷⻉构造ret对象,合三为⼀,变为直接构造。变为直接构造。要理解这个优化要结合局部对象⽣命周期和栈帧的⻆度理解,如图3所⽰。
• linux下可以将下⾯代码拷⻉到test.cpp⽂件,编译时⽤ g++ test.cpp -fno-elideconstructors 的⽅式关闭构造优化,运⾏结果可以看到图1左边没有优化的两次拷⻉。
右值对象构造,有拷⻉构造,也有移动构造的场景
• 图2展⽰了vs2019 debug环境下编译器对拷⻉的优化,左边为不优化的情况下,两次移动构造,右边为编译器优化的场景下连续步骤中的拷⻉合⼆为⼀变为⼀次移动构造。
• 需要注意的是在vs2019的release和vs2022的debug和release,下⾯代码优化为⾮常恐怖,会直接将str对象的构造,str拷⻉构造临时对象,临时对象拷⻉构造ret对象,合三为⼀,变为直接构造。要理解这个优化要结合局部对象⽣命周期和栈帧的⻆度理解,如图3所⽰。
• linux下可以将下⾯代码拷⻉到test.cpp⽂件,编译时⽤ g++ test.cpp -fno-elideconstructors 的⽅式关闭构造优化,运⾏结果可以看到图1左边没有优化的两次移动。
右值对象赋值,只有拷⻉构造和拷⻉赋值,没有移动构造和移动赋值的场景
• 图4左边展⽰了vs2019 debug和 g++ test.cpp -fno-elide-constructors 关闭优化环境下编译器的处理,⼀次拷⻉构造,⼀次拷⻉赋值。
• 需要注意的是在vs2019的release和vs2022的debug和release,下⾯代码会进⼀步优化,直接构造要返回的临时对象,str本质是临时对象的引⽤,底层⻆度⽤指针实现。运⾏结果的⻆度,我们可以看到str的析构是在赋值以后,说明str就是临时对象的别名。
右值对象赋值,既有拷⻉构造和拷⻉赋值,也有移动构造和移动赋值的场景
• 图5左边展⽰了vs2019 debug和 g++ test.cpp -fno-elide-constructors 关闭优化环境下编译器的处理,⼀次移动构造,⼀次移动赋值。
• 需要注意的是在vs2019的release和vs2022的debug和release,下⾯代码会进⼀步优化,直接构造要返回的临时对象,str本质是临时对象的引⽤,底层⻆度⽤指针实现。运⾏结果的⻆度,我们可以看到str的析构是在赋值以后,说明str就是临时对象的别名。
3.5.4 右值引⽤和移动语义在传参中的提效
• 查看STL⽂档我们发现C++11以后容器的push和insert系列的接⼝否增加的右值引⽤版本
• 当实参是⼀个左值时,容器内部继续调⽤拷⻉构造进⾏拷⻉,将对象拷⻉到容器空间中的对象
• 当实参是⼀个右值,容器内部则调⽤移动构造,右值对象的资源到容器空间的对象上
• 把我们之前模拟实现的bit::list拷⻉过来,⽀持右值引⽤参数版本的push_back和insert
• 其实这⾥还有⼀个emplace系列的接⼝,但是这个涉及可变参数模板,我们需要把可变参数模板讲
解以后再讲解emplace系列的接⼝。
// void push_back (const value_type& val);
// void push_back (value_type&& val);
// iterator insert (const_iterator position, value_type&& val);
// iterator insert (const_iterator position, const value_type& val);
int main()
{
std::list<bit::string> lt;
bit::string s1("111111111111111111111");
lt.push_back(s1);
cout << "*************************" << endl;
lt.push_back(bit::string("22222222222222222222222222222"));
cout << "*************************" << endl;
lt.push_back("3333333333333333333333333333");
cout << "*************************" << endl;
lt.push_back(move(s1));
cout << "*************************" << endl;
return 0;
}
运⾏结果:
string(char* str)
string(const string& s) -- 拷⻉构造
*************************
string(char* str)
string(string&& s) -- 移动构造
~string() -- 析构
*************************
string(char* str)
string(string&& s) -- 移动构造
~string() -- 析构
*************************
string(string&& s) -- 移动构造
*************************
~string() -- 析构
~string() -- 析构
~string() -- 析构
~string() -- 析构
~string() -- 析构
// List.h
// 以下代码为了控制课件篇幅,把跟这⾥无关的接口都删除了
namespace bit
{
template<class T>
struct ListNode
{
ListNode<T>* _next;
ListNode<T>* _prev;
T _data;
ListNode(const T& data = T())
: _next(nullptr)
, _prev(nullptr)
, _data(data)
{}
ListNode(T&& data)
: _next(nullptr)
, _prev(nullptr)
, _data(move(data))
{}
};
template<class T, class Ref, class Ptr>
struct ListIterator
{
typedef ListNode<T> Node;
typedef ListIterator<T, Ref, Ptr> Self;
Node* _node;
ListIterator(Node* node)
: _node(node)
{}
Self& operator++()
{
_node = _node->_next;
return *this;
}
Ref operator*()
{
return _node->_data;
}
bool operator!=(const Self& it)
{
return _node != it._node;
}
};
template<class T>
class list
{
typedef ListNode<T> Node;
public:
typedef ListIterator<T, T&, T*> iterator;
typedef ListIterator<T, const T&, const T*> const_iterator;
iterator begin()
{
return iterator(_head->_next);
}
iterator end()
{
return iterator(_head);
}
void empty_init()
{
_head = new Node();
_head->_next = _head;
_head->_prev = _head;
}
list()
{
empty_init();
}
void push_back(const T& x)
{
insert(end(), x);
}
void push_back(T&& x)
{
insert(end(), move(x));
}
iterator insert(iterator pos, const T& x)
{
Node* cur = pos._node;
Node* newnode = new Node(x);
Node* prev = cur->_prev;
// prev newnode cur
prev->_next = newnode;
newnode->_prev = prev;
newnode->_next = cur;
cur->_prev = newnode;
return iterator(newnode);
}
iterator insert(iterator pos, T&& x)
{
Node* cur = pos._node;
Node* newnode = new Node(move(x));
Node* prev = cur->_prev;
// prev newnode cur
prev->_next = newnode;
newnode->_prev = prev;
newnode->_next = cur;
cur->_prev = newnode;
return iterator(newnode);
}
private:
Node* _head;
};
}
// Test.cpp
#include "List.h"
#include <iostream>
using namespace std;
int main()
{
bit::list<bit::string> lt;
cout << "*************************" << endl;
bit::string s1("111111111111111111111");
lt.push_back(s1);
cout << "*************************" << endl;
lt.push_back(bit::string("22222222222222222222222222222"));
cout << "*************************" << endl;
lt.push_back("3333333333333333333333333333");
cout << "*************************" << endl;
lt.push_back(move(s1));
cout << "*************************" << endl;
return 0;
}
3.6 类型分类
• C++11以后,进⼀步对类型进⾏了划分,右值被划分纯右值(pure value,简称prvalue)和将亡值(expiring value,简称xvalue)。
• 纯右值是指那些字⾯值常量或求值结果相当于字⾯值或是⼀个不具名的临时对象。如: 42、true、nullptr 或者类似 str.substr(1, 2)、str1 + str2 传值返回函数调⽤,或者整形 a、b,a++,a+b 等。纯右值和将亡值C++11中提出的,C++11中的纯右值概念划分等价于C++98中的右值。
• 将亡值是指返回右值引⽤的函数的调⽤表达式和转换为右值引⽤的转换函数的调⽤表达,如move(x)、static_cast<X&&>(x)
• 泛左值(generalized value,简称glvalue),泛左值包含将亡值和左值。
• 值类别 - cppreference.com 和 Value categories这两个关于值类型的中⽂和英⽂的官⽅⽂档,有兴
趣可以了解细节。
3.7 引⽤折叠
• C++中不能直接定义引⽤的引⽤如 int& && r = i; ,这样写会直接报错,通过模板或 typedef中的类型操作可以构成引⽤的引⽤。
• 通过模板或 typedef 中的类型操作可以构成引⽤的引⽤时,这时C++11给出了⼀个引⽤折叠的规则:右值引⽤的右值引⽤折叠成右值引⽤,所有其他组合均折叠成左值引⽤。
• 下⾯的程序中很好的展⽰了模板和typedef时构成引⽤的引⽤时的引⽤折叠规则,⼤家需要⼀个⼀
个仔细理解⼀下。
• 像f2这样的函数模板中,T&& x参数看起来是右值引⽤参数,但是由于引⽤折叠的规则,他传递左值时就是左值引⽤,传递右值时就是右值引⽤,有些地⽅也把这种函数模板的参数叫做万能引⽤。
• Function(T&& t)函数模板程序中,假设实参是int右值,模板参数T的推导int,实参是int左值,模板参数T的推导int&,再结合引⽤折叠规则,就实现了实参是左值,实例化出左值引⽤版本形参的Function,实参是右值,实例化出右值引⽤版本形参的Function。
#include <iostream>
#include <utility> // for std::move
using namespace std;
// 由于引用折叠限定,f1实例化以后总是一个左值引用
template<class T>
void f1(T& x) {}
// 由于引用折叠限定,f2实例化后可以是左值引用,也可以是右值引用
template<class T>
void f2(T&& x) {}
int main() {
typedef int& lref;
typedef int&& rref;
int n = 0;
lref& r1 = n; // r1 的类型是 int&
lref&& r2 = n; // r2 的类型是 int&
rref& r3 = n; // r3 的类型是 int&
rref&& r4 = 1; // r4 的类型是 int&&
// 没有折叠 -> 实例化为 void f1(int& x)
f1<int>(n);
f1<int>(0); // 报错
// 折叠 -> 实例化为 void f1(int& x)
f1<int&>(n);
f1<int&>(0); // 报错
// 折叠 -> 实例化为 void f1(int& x)
f1<int&&>(n);
f1<int&&>(0); // 报错
// 折叠 -> 实例化为 void f1(const int& x)
f1<const int&>(n);
f1<const int&>(0);
// 折叠 -> 实例化为 void f1(const int& x)
f1<const int&&>(n);
f1<const int&&>(0);
// 没有折叠 -> 实例化为 void f2(int&& x)
f2<int>(n); // 报错
f2<int>(0);
// 折叠 -> 实例化为 void f2(int& x)
f2<int&>(n);
f2<int&>(0); // 报错
// 折叠 -> 实例化为 void f2(int&& x)
f2<int&&>(n); // 报错
f2<int&&>(0);
return 0;
}
template<class T>
void Function(T&& t) {
int a = 0;
T x = a;
// x++;
cout << &a << endl;
cout << &x << endl << endl;
}
int main() {
// 10是右值,推导出T为int,模板实例化为void Function(int&& t)
Function(10); // 右值
int a;
// a是左值,推导出T为int&,引用折叠,模板实例化为void Function(int& t)
Function(a); // 左值
// std::move(a)是右值,推导出T为int,模板实例化为void Function(int&& t)
Function(std::move(a)); // 右值
const int b = 8;
// b是左值,推导出T为const int&,引用折叠,模板实例化为void Function(const int& t)
// 所以Function内部会编译报错,x不能++
Function(b); // const 左值
// std::move(b)右值,推导出T为const int,模板实例化为void Function(const int&& t)
// 所以Function内部会编译报错,x不能++
Function(std::move(b)); // const 右值
return 0;
}
3.8 完美转发
• Function(T&& t)函数模板程序中,传左值实例化以后是左值引⽤的Function函数,传右值实例化以后是右值引⽤的Function函数。
• 但是结合我们在5.2章节的讲解,变量表达式都是左值属性,也就意味着⼀个右值被右值引⽤绑定后,右值引⽤变量表达式的属性是左值,也就是说Function函数中t的属性是左值,那么我们把t传递给下⼀层函数Fun,那么匹配的都是左值引⽤版本的Fun函数。这⾥我们想要保持t对象的属性,就需要使⽤完美转发实现。
• template T&& forward (typename remove_reference::type&
arg);
• template T&& forward (typenameremove_reference::type&& arg);
• 完美转发forward本质是⼀个函数模板,他主要还是通过引⽤折叠的⽅式实现,下⾯⽰例中传递给Function的实参是右值,T被推导为int,没有折叠,forward内部t被强转为右值引⽤返回;传递给Function的实参是左值,T被推导为int&,引⽤折叠为左值引⽤,forward内部t被强转为左值引⽤返回。
#include <iostream>
#include <utility> // for std::move
#include <type_traits> // for std::remove_reference
using namespace std;
// 模板实现:转发一个lvalue或rvalue
template <class _Ty>
_Ty&& forward(remove_reference_t<_Ty>& _Arg) noexcept {
return static_cast<_Ty&&>(_Arg);
}
// 函数重载:不同类型参数的处理
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<class T>
void Function(T&& t) {
Fun(t);
// Fun(forward<T>(t)); // 可选:使用转发
}
int main() {
// 10是右值,推导出T为int,模板实例化为void Function(int&& t)
Function(10); // 右值
int a;
// a是左值,推导出T为int&,引用折叠,模板实例化为void Function(int& t)
Function(a); // 左值
// std::move(a)是右值,推导出T为int,模板实例化为void Function(int&& t)
Function(std::move(a)); // 右值
const int b = 8;
// b是左值,推导出T为const int&,引用折叠,模板实例化为void Function(const int& t)
Function(b); // const 左值
// std::move(b)是右值,推导出T为const int,模板实例化为void Function(const int&& t)
Function(std::move(b)); // const 右值
return 0;
}
四、 可变参数模板
4.1 基本语法及原理
• C++11⽀持可变参数模板,也就是说⽀持可变数量参数的函数模板和类模板,可数⽬的参数被称为参数包,存在两种参数包:模板参数包,表⽰零或多个模板参数;函数参数包:表⽰零或多个函数参数。
• template <class …Args> void Func(Args… args) {}
• template <class …Args> void Func(Args&… args) {}
• template <class …Args> void Func(Args&&… args) {}
• 我们⽤省略号来指出⼀个模板参数或函数参数的表⽰⼀个包,在模板参数列表中,class…或typename…指出接下来的参数表⽰零或多个类型列表;在函数参数列表中,类型名后⾯跟…指出接下来表⽰零或多个形参对象列表;函数参数包可以⽤左值引⽤或右值引⽤表⽰,跟前⾯普通模板
⼀样,每个参数实例化时遵循引⽤折叠规则。
• 可变参数模板的原理跟模板类似,本质还是去实例化对应类型和个数的多个函数。
• 这⾥我们可以使⽤sizeof…运算符去计算参数包中参数的个数。
#include <iostream>
#include <string>
#include <utility> // for std::forward
using namespace std;
// 可变参数模板 Print,接受任意数量的参数
template <class ...Args>
void Print(Args&&... args) {
cout << sizeof...(args) << endl; // 输出参数数量
}
int main() {
double x = 2.2;
Print(); // 包里有0个参数
Print(1); // 包里有1个参数
Print(1, string("xxxxx")); // 包里有2个参数
Print(1.1, string("xxxxx"), x); // 包里有3个参数
return 0;
}
// 原理1:编译本质上将结合引用折叠规则实例化出以下四个函数
/*
void Print();
void Print(int&& arg1);
void Print(int&& arg1, string&& arg2);
void Print(double&& arg1, string&& arg2, double& arg3);
*/
// 原理2:更本质去看没有可变参数模板,我们实际上要实现多个函数模板才能支持这些功能。
// 有了可变参数模板,我们进一步被解放,它是类型泛化基础上叠加数量变化,让我们泛型编程更灵活。
/*
void Print();
template <class T1>
void Print(T1&& arg1);
template <class T1, class T2>
void Print(T1&& arg1, T2&& arg2);
template <class T1, class T2, class T3>
void Print(T1&& arg1, T2&& arg2, T3&& arg3);
*/
4.2 包扩展
• 对于⼀个参数包,我们除了能计算他的参数个数,我们能做的唯⼀的事情就是扩展它,当扩展⼀个包时,我们还要提供⽤于每个扩展元素的模式,扩展⼀个包就是将它分解为构成的元素,对每个元素应⽤模式,获得扩展后的列表。我们通过在模式的右边放⼀个省略号(…)来触发扩展操作。底层的实现细节如图1所⽰。
• C++还⽀持更复杂的包扩展,直接将参数包依次展开依次作为实参给⼀个函数去处理。
#include <iostream>
#include <string>
using namespace std;
// 递归终止函数:无参数时调用
void ShowList() {
cout << endl; // 打印换行符
}
// 递归函数:打印参数包的内容
template <class T, class ...Args>
void ShowList(T x, Args... args) {
cout << x << " "; // 打印当前参数
// 递归调用 ShowList,传入剩余的参数
ShowList(args...);
}
// 编译时递归推导解析参数
template <class ...Args>
void Print(Args... args) {
ShowList(args...); // 调用 ShowList 打印参数
}
int main() {
Print(); // 参数包为空
Print(1); // 包含1个参数
Print(1, string("xxxxx")); // 包含2个参数
Print(1, string("xxxxx"), 2.2); // 包含3个参数
return 0;
}
// 下面是注释部分,用于阐释原理
/*
template <class T, class ...Args>
void ShowList(T x, Args... args) {
cout << x << " ";
Print(args...);
}
// 当调用 Print(1, string("xxxxx"), 2.2) 时,编译器会推导出以下重载函数
void ShowList(double x) {
cout << x << " ";
ShowList();
}
void ShowList(string x, double z) {
cout << x << " ";
ShowList(z);
}
void ShowList(int x, string y, double z) {
cout << x << " ";
ShowList(y, z);
}
void Print(int x, string y, double z) {
ShowList(x, y, z);
}
*/
#include <iostream>
#include <string>
using namespace std;
// 取得参数并打印
template <class T>
const T& GetArg(const T& x) {
cout << x << " ";
return x;
}
// 空参数包函数
template <class ...Args>
void Arguments(Args... args) {
// 该函数并不做任何操作,仅用于接受参数
}
// 打印函数,接受任意数量和类型的参数
template <class ...Args>
void Print(Args... args) {
// 注意GetArg必须返回引用,这样才能组成参数包传给Arguments
Arguments(GetArg(args)...);
}
// 主函数
int main() {
Print(1, string("xxxxx"), 2.2); // 调用Print,传递不同参数
return 0;
}
4.3 empalce系列接口
• template <class… Args> void emplace_back (Args&&… args);
• template <class… Args> iterator emplace (const_iterator position,Args&&… args);
• C++11以后STL容器新增了empalce系列的接⼝,empalce系列的接⼝均为模板可变参数,功能上兼容push和insert系列,但是empalce还⽀持新玩法,假设容器为container,empalce还⽀持直接插⼊构造T对象的参数,这样有些场景会更⾼效⼀些,可以直接在容器空间上构造T对象。
• emplace_back总体⽽⾔是更⾼效,推荐以后使⽤emplace系列替代insert和push系列
• 第⼆个程序中我们模拟实现了list的emplace和emplace_back接⼝,这⾥把参数包不段往下传递,最终在结点的构造中直接去匹配容器存储的数据类型T的构造,所以达到了前⾯说的empalce⽀持直接插⼊构造T对象的参数,这样有些场景会更⾼效⼀些,可以直接在容器空间上构造T对象。
• 传递参数包过程中,如果是 Args&&… args 的参数包,要⽤完美转发参数包,⽅式如下std::forward(args)… ,否则编译时包扩展后右值引⽤变量表达式就变成了左值。
#include <iostream>
#include <list>
#include <string>
#include <utility> // for std::move
using namespace std;
int main()
{
// 创建一个字符串类型的自定义链表
list<string> lt;
// 传左值,跟 push_back 一样,走拷贝构造
string s1("111111111111");
lt.emplace_back(s1);
cout << "*********************************" << endl;
// 传右值,跟 push_back 一样,走移动构造
lt.emplace_back(move(s1));
cout << "*********************************" << endl;
// 直接将构造字符串参数包往下传,构造字符串
// 这种调用效果是 push_back 做不到的,因为它不能直接接受构造的参数
lt.emplace_back("111111111111");
cout << "*********************************" << endl;
// 创建一个包含 pair 的自定义链表
list<pair<string, int>> lt1;
// 跟 push_back 一样,构造 pair + 拷贝/移动构造 pair 到链表节点中
pair<string, int> kv("苹果", 1);
lt1.emplace_back(kv);
cout << "*********************************" << endl;
// 跟 push_back 一样,移动构造 pair
lt1.emplace_back(move(kv));
cout << "*********************************" << endl;
// 直接将构造 pair 参数包往下传,直接用 pair 参数包构造 pair
// 这种调用效果是 push_back 做不到的
lt1.emplace_back("苹果", 1);
cout << "*********************************" << endl;
return 0;
}
namespace bit {
template<class T>
struct ListNode {
ListNode<T>* _next; // 指向下一个节点
ListNode<T>* _prev; // 指向前一个节点
T _data; // 节点存储的数据
// 移动构造函数
ListNode(T&& data)
: _next(nullptr),
_prev(nullptr),
_data(move(data)) {}
// 可变参数模板构造函数
template <class... Args>
ListNode(Args&&... args)
: _next(nullptr),
_prev(nullptr),
_data(std::forward<Args>(args)...)
{}
};
// 迭代器实现
template<class T, class Ref, class Ptr>
struct ListIterator {
typedef ListNode<T> Node;
typedef ListIterator<T, Ref, Ptr> Self;
Node* _node;
ListIterator(Node* node) : _node(node) {}
// 前置递增
Self& operator++() {
_node = _node->_next;
return *this;
}
// 前置递减
Self& operator--() {
_node = _node->_prev;
return *this;
}
// 解引用操作符
Ref operator*() {
return _node->_data;
}
// 不等操作符
bool operator!=(const Self& it) {
return _node != it._node;
}
};
// 自定义链表类
template<class T>
class list {
typedef ListNode<T> Node;
public:
typedef ListIterator<T, T&, T*> iterator;
typedef ListIterator<T, const T&, const T*> const_iterator;
iterator begin() {
return iterator(_head->_next);
}
iterator end() {
return iterator(_head);
}
// 初始化链表为空
void empty_init() {
_head = new Node();
_head->_next = _head;
_head->_prev = _head;
}
list() {
empty_init();
}
// 添加元素到链表末尾
void push_back(const T& x) {
insert(end(), x);
}
void push_back(T&& x) {
insert(end(), move(x));
}
// 插入节点的实现
iterator insert(iterator pos, const T& x) {
Node* cur = pos._node;
Node* newnode = new Node(x);
Node* prev = cur->_prev;
// prev newnode cur
prev->_next = newnode;
newnode->_prev = prev;
newnode->_next = cur;
cur->_prev = newnode;
return iterator(newnode);
}
iterator insert(iterator pos, T&& x) {
Node* cur = pos._node;
Node* newnode = new Node(move(x));
Node* prev = cur->_prev;
// prev newnode cur
prev->_next = newnode;
newnode->_prev = prev;
newnode->_next = cur;
cur->_prev = newnode;
return iterator(newnode);
}
// 可以任意参数类型的 emplace_back 方法
template <class... Args>
void emplace_back(Args&&... args) {
insert(end(), std::forward<Args>(args)...);
}
// 原理:编译器根据可变参数模板生成对应参数的插入函数
template <class... Args>
iterator insert(iterator pos, Args&&... args) {
Node* cur = pos._node;
Node* newnode = new Node(std::forward<Args>(args)...);
Node* prev = cur->_prev;
// prev newnode cur
prev->_next = newnode;
newnode->_prev = prev;
newnode->_next = cur;
cur->_prev = newnode;
return iterator(newnode);
}
private:
Node* _head; // 表示链表的头节点
};
}
五、 新的类功能
5.1 默认的移动构造和移动赋值
• 原来C++类中,有6个默认成员函数:构造函数/析构函数/拷⻉构造函数/拷⻉赋值重载/取地址重载/const 取地址重载,最后重要的是前4个,后两个⽤处不⼤,默认成员函数就是我们不写编译器会⽣成⼀个默认的。C++11 新增了两个默认成员函数,移动构造函数和移动赋值运算符重载。
• 如果你没有⾃⼰实现移动构造函数,且没有实现析构函数 、拷⻉构造、拷⻉赋值重载中的任意⼀个。那么编译器会⾃动⽣成⼀个默认移动构造。默认⽣成的移动构造函数,对于内置类型成员会执⾏逐成员按字节拷⻉,⾃定义类型成员,则需要看这个成员是否实现移动构造,如果实现了就调⽤移动构造,没有实现就调⽤拷⻉构造。
• 如果你没有⾃⼰实现移动赋值重载函数,且没有实现析构函数 、拷⻉构造、拷⻉赋值重载中的任意⼀个,那么编译器会⾃动⽣成⼀个默认移动赋值。默认⽣成的移动构造函数,对于内置类型成员会执⾏逐成员按字节拷⻉,⾃定义类型成员,则需要看这个成员是否实现移动赋值,如果实现了就调⽤移动赋值,没有实现就调⽤拷⻉赋值。(默认移动赋值跟上⾯移动构造完全类似)
• 如果你提供了移动构造或者移动赋值,编译器不会⾃动提供拷⻉构造和拷⻉赋值。
#include <iostream>
#include <string>
#include <utility> // for std::move
namespace bit {
class string {
// 这里可以是自定义字符串类的实现
};
}
// 定义 Person 类
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:
bit::string _name; // 姓名
int _age; // 年龄
};
int main() {
Person s1; // 默认构造
Person s2 = s1; // 拷贝构造 (如果未注释)
Person s3 = std::move(s1); // 移动构造 (如果未注释)
Person s4; // 默认构造
s4 = std::move(s2); // 移动赋值 (如果未注释)
return 0;
}
5.2 成员变量声明时给缺省值
成员变量声明时给缺省值是给初始化列表⽤的,如果没有显⽰在初始化列表初始化,就会在初始化列表⽤这个却绳⼦初始化,这个我们在类和对象部分讲过了,忘了就去复习吧。
5.3 defult和delete
• C++11可以让你更好的控制要使⽤的默认函数。假设你要使⽤某个默认的函数,但是因为⼀些原因这个函数没有默认⽣成。⽐如:我们提供了拷⻉构造,就不会⽣成移动构造了,那么我们可以使⽤default关键字显⽰指定移动构造⽣成。
• 如果能想要限制某些默认函数的⽣成,在C++98中,是该函数设置成private,并且只声明补丁已,这样只要其他⼈想要调⽤就会报错。在C++11中更简单,只需在该函数声明加上=delete即可,该语法指⽰编译器不⽣成对应函数的默认版本,称=delete修饰的函数为删除函数。
#include <iostream>
#include <utility> // for std::move
namespace bit {
class string {
// 这里可以是自定义字符串类的实现。
};
}
// 定义 Person 类
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(Person&& p) = default;
// 禁用拷贝赋值运算符
Person& operator=(const Person& p) = delete;
private:
bit::string _name; // 姓名
int _age; // 年龄
};
int main() {
Person s1; // 默认构造
Person s2 = s1; // 调用拷贝构造
Person s3 = std::move(s1); // 调用移动构造
return 0;
}
5.4 final与override
这个我们在继承和多态章节已经进⾏了详细讲过了,忘了就去复习吧。
六、 STL中⼀些变化
• 下图1圈起来的就是STL中的新容器,但是实际最有⽤的是unordered_map和unordered_set。这
两个我们前⾯已经进⾏了⾮常详细的讲解,其他的⼤家了解⼀下即可。
• STL中容器的新接⼝也不少,最重要的就是右值引⽤和移动语义相关的push/insert/emplace系列
接⼝和移动构造和移动赋值,还有initializer_list版本的构造等,这些前⾯都讲过了,还有⼀些⽆关
痛痒的如cbegin/cend等需要时查查⽂档即可。
• 容器的范围for遍历,这个在容器部分也讲过了
七、 lambda
7.1 lambda表达式语法
• lambda 表达式本质是⼀个匿名函数对象,跟普通函数不同的是他可以定义在函数内部。
lambda 表达式语法使⽤层⽽⾔没有类型,所以我们⼀般是⽤auto或者模板参数定义的对象去接
收 lambda 对象。
• lambda表达式的格式: [capture-list] (parameters)-> return type {
function boby }
• [capture-list] : 捕捉列表,该列表总是出现在 lambda 函数的开始位置,编译器根据[]来
判断接下来的代码是否为 lambda 函数,捕捉列表能够捕捉上下⽂中的变量供 lambda 函数使
⽤,捕捉列表可以传值和传引⽤捕捉,具体细节7.2中我们再细讲。捕捉列表为空也不能省略。
• (parameters) :参数列表,与普通函数的参数列表功能类似,如果不需要参数传递,则可以连
同()⼀起省略
• ->return type :返回值类型,⽤追踪返回类型形式声明函数的返回值类型,没有返回值时此
部分可省略。⼀般返回值类型明确情况下,也可省略,由编译器对返回类型进⾏推导。
• {function boby} :函数体,函数体内的实现跟普通函数完全类似,在该函数体内,除了可以
使⽤其参数外,还可以使⽤所有捕获到的变量,函数体为空也不能省略。
#include <iostream>
using namespace std;
int main()
{
// 创建一个简单的 lambda 表达式用于相加
auto add1 = [](int x, int y) -> int {
return x + y;
};
cout << "Result of add1(1, 2): " << add1(1, 2) << endl;
// 1. 捕捉为空,不能省略
// 2. 参数为空时可以省略
// 3. 返回值可以省略,自动推导
// 4. 函数体不能省略
// 创建 lambda 表达式,打印 "hello bit"
auto func1 = [] {
cout << "hello bit" << endl;
return 0; // 返回值可省略
};
func1(); // 调用 func1
int a = 0, b = 1; // 定义两个变量
// 创建 lambda 表达式用于交换两个整数的值
auto swap1 = [](int& x, int& y) {
int tmp = x;
x = y;
y = tmp;
};
// 交换 a 和 b 的值
swap1(a, b);
cout << "After swap: a = " << a << ", b = " << b << endl;
return 0;
}
7.2 捕捉列表
• lambda 表达式中默认只能⽤ lambda 函数体和参数中的变量,如果想⽤外层作⽤域中的变量就需要进⾏捕捉
• 第⼀种捕捉⽅式是在捕捉列表中显⽰的传值捕捉和传引⽤捕捉,捕捉的多个变量⽤逗号分割。[x,y, &z] 表⽰x和y值捕捉,z引⽤捕捉。
• 第⼆种捕捉⽅式是在捕捉列表中隐式捕捉,我们在捕捉列表写⼀个=表⽰隐式值捕捉,在捕捉列表写⼀个&表⽰隐式引⽤捕捉,这样我们 lambda 表达式中⽤了那些变量,编译器就会⾃动捕捉那些变量。
• 第三种捕捉⽅式是在捕捉列表中混合使⽤隐式捕捉和显⽰捕捉。[=, &x]表⽰其他变量隐式值捕捉,x引⽤捕捉;[&, x, y]表⽰其他变量引⽤捕捉,x和y值捕捉。当使⽤混合捕捉时,第⼀个元素必须是&或=,并且&混合捕捉时,后⾯的捕捉变量必须是值捕捉,同理=混合捕捉时,后⾯的捕捉变量必须是引⽤捕捉。
• lambda 表达式如果在函数局部域中,他可以捕捉 lambda 位置之前定义的变量,不能捕捉静态局部变量和全局变量,静态局部变量和全局变量也不需要捕捉, lambda 表达式中可以直接使
⽤。这也意味着 lambda 表达式如果定义在全局位置,捕捉列表必须为空。
• 默认情况下, lambda 捕捉列表是被const修饰的,也就是说传值捕捉的过来的对象不能修改,mutable加在参数列表的后⾯可以取消其常量性,也就说使⽤该修饰符后,传值捕捉的对象就可以
修改了,但是修改还是形参对象,不会影响实参。使⽤该修饰符后,参数列表不可省略(即使参数为
空)。
#include <iostream>
using namespace std;
int x = 0; // 全局变量
int main() {
// lambda 表达式示例 1
// 捕捉列表必须为空,因为全局变量不需要捕捉
auto func1 = []() {
x++; // 修改全局变量 x
};
// 只能用当前 lambda 局部域和捕捉的对象与全局对象
int a = 0, b = 1, c = 2, d = 3;
// lambda 表达式示例 2
// 捕获 a 的值和 b 的引用
auto func2 = [a, &b]() {
// 值捕捉的变量不能修改,引⽤捕捉的变量可以修改
// a++; // 错误:不能修改值捕捉
b++; // 修改 b
return a + b; // 返回 a + b
};
cout << "func2 result: " << func2() << endl;
// 隐式值捕捉:捕捉所用到的变量
auto func3 = [=]() {
return a + b + c; // 使用 a, b, c
};
cout << "func3 result: " << func3() << endl;
// 隐式引用捕捉:捕捉所用到的变量
auto func4 = [&]() {
a++; // 修改 a
c++; // 修改 c
d++; // 修改 d
};
func4(); // 调用
cout << "After func4: a = " << a << ", b = " << b << ", c = " << c << ", d = " << d << endl;
// 混合捕捉示例 1
auto func5 = [&, a, b]() {
// a++; // 错误:不能修改值捕捉
// b++; // 错误:不能修改值捕捉
c++; // 只能修改引用捕捉的变量
d++;
return a + b + c + d; // 返回 a + b + c + d
};
func5();
cout << "After func5: a = " << a << ", b = " << b << ", c = " << c << ", d = " << d << endl;
// 混合捕捉示例 2
auto func6 = [=, &a, &b]() {
a++; // 修改 a
b++; // 修改 b
// c++; // 错误:不可以修改隐式捕捉的值
// d++; // 错误:不可以修改隐式捕捉的值
return a + b + c + d; // 返回 a + b + c + d
};
func6();
cout << "After func6: a = " << a << ", b = " << b << ", c = " << c << ", d = " << d << endl;
// 局部的静态和全局变量不能捕捉,也不需要捕捉
static int m = 0; // 静态局部变量
auto func7 = []() {
int ret = x + m; // 使用全局变量和静态变量
return ret;
};
cout << "func7 result: " << func7() << endl;
// 传值捕捉本质是拷贝,并且被 const 修饰
// mutable 相当于去掉 const 属性,可以修改变量
auto func8 = [=]() mutable {
a++; // 这里修改的是 a 的拷贝
b++;
c++;
d++; // 这里将直接影响 d
return a + b + c + d; // 返回 a + b + c + d
};
cout << "func8 result: " << func8() << endl;
cout << "Final values: a = " << a << ", b = " << b << ", c = " << c << ", d = " << d << endl;
return 0;
}
7.3 lambda的应⽤
• 在学习 lambda 表达式之前,我们的使⽤的可调⽤对象只有函数指针和仿函数对象,函数指针的类型定义起来⽐较⿇烦,仿函数要定义⼀个类,相对会⽐较⿇烦。使⽤ lambda 去定义可调⽤对象,既简单⼜⽅便。
• lambda 在很多其他地⽅⽤起来也很好⽤。⽐如线程中定义线程的执⾏函数逻辑,智能指针中定制删除器等, lambda 的应⽤还是很⼴泛的,以后我们会不断接触到。
#include <iostream>
#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) {}
};
// 比较器,按价格升序排序
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.0, 4},
{"橙子", 2.2, 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;
});
// 输出排序后的商品列表
for (const auto& item : v) {
cout << "商品: " << item._name << ", 价格: " << item._price
<< ", 评价: " << item._evaluate << endl;
}
return 0;
}
7.4 lambda的原理
• lambda 的原理和范围for很像,编译后从汇编指令层的⻆度看,压根就没有 lambda 和范围for这样的东西。范围for底层是迭代器,⽽lambda底层是仿函数对象,也就说我们写了⼀个lambda 以后,编译器会⽣成⼀个对应的仿函数的类。
• 仿函数的类名是编译按⼀定规则⽣成的,保证不同的 lambda ⽣成的类名不同,lambda参数/返回类型/函数体就是仿函数operator()的参数/返回类型/函数体, lambda 的捕捉列表本质是⽣成的仿函数类的成员变量,也就是说捕捉列表的变量都是 lambda 类构造函数的实参,当然隐式捕捉,编译器要看使⽤哪些就传那些对象。
• 上⾯的原理,我们可以透过汇编层了解⼀下,下⾯第⼆段汇编层代码印证了上⾯的原理。
#include <iostream>
using namespace std;
// 定义一个 Rate 类,用于计算利息
class Rate {
public:
// 构造函数,初始化利率
Rate(double rate) : _rate(rate) {}
// 重载 operator() 以便该类的对象可以像函数一样被调用
double operator()(double money, int year) {
return money * _rate * year; // 计算利息
}
private:
double _rate; // 利率
};
int main() {
double rate = 0.49; // 定义利率
// 使用 lambda 表达式定义一个函数
auto r2 = [rate](doublemoney,intyear) {
return money * rate * year; // 计算利息
};
// 使用函数对象
Rate r1(rate); // 创建函数对象
double interest1 = r1(10000, 2); // 使用函数对象计算利息
double interest2 = r2(10000, 2); // 使用 lambda 表达式计算利息
// 输出结果
cout << "Interest calculated using Rate object: " << interest1 << endl;
cout << "Interest calculated using lambda: " << interest2 << endl;
// 另一个简单的 lambda 表达式,输出信息
auto func1 = [] {
cout << "hello world" << endl;
};
func1(); // 调用 lambda 表达式
return 0;
}
// lambda
auto r2 = [rate](double money, int year) {
return money * rate * year;
};
// 捕捉列表的rate,可以看到作为lambda_1类构造函数的参数传递了,这样要拿去初始化成员变量
// 下⾯operator()中才能使⽤
00D8295C lea eax,[rate]
00D8295F push eax
00D82960 lea ecx,[r2]
00D82963 call `main'::`2'::<lambda_1>::<lambda_1> (0D81F80h)
// 函数对象
Rate r1(rate);
00D82968 sub esp,8
00D8296B movsd xmm0,mmword ptr [rate]
00D82970 movsd mmword ptr [esp],xmm0
00D82975 lea ecx,[r1]
00D82978 call Rate::Rate (0D81438h)
r1(10000, 2);
00D8297D push 2
00D8297F sub esp,8
00D82982 movsd xmm0,mmword ptr [__real@40c3880000000000 (0D89B50h)]
00D8298A movsd mmword ptr [esp],xmm0
00D8298F lea ecx,[r1]
00D82992 call Rate::operator() (0D81212h)
// 汇编层可以看到r2 lambda对象调⽤本质还是调⽤operator(),类型是lambda_1,这个类型名
// 的规则是编译器⾃⼰定制的,保证不同的lambda不冲突
r2(10000, 2);
00D82999 push 2
00D8299B sub esp,8
00D8299E movsd xmm0,mmword ptr [__real@40c3880000000000 (0D89B50h)]
00D829A6 movsd mmword ptr [esp],xmm0
00D829AB lea ecx,[r2]
00D829AE call `main'::`2'::<lambda_1>::operator() (0D824C0h)
八、 包装器
8.1 function
template <class T>
class function; // undefined
template <class Ret, class... Args>
class function<Ret(Args...)>;
• std::function 是⼀个类模板,也是⼀个包装器。 std::function 的实例对象可以包装存储其他的可以调⽤对象,包括函数指针、仿函数、 lambda 、 bind 表达式等存储的可调⽤对象被称为 std::function 的⽬标。若 std::function 不含⽬标,则称它为空。调⽤空std::function 的⽬标导致抛出 std::bad_function_call 异常。
• 以上是 function 的原型,他被定义头⽂件中。std::function - cppreference.com是function的官⽅⽂件链接。
• 函数指针、仿函数、 lambda 等可调⽤对象的类型各不相同, std::function 的优势就是统⼀类型,对他们都可以进⾏包装,这样在很多地⽅就⽅便声明可调⽤对象的类型,下⾯的第⼆个代码样例展⽰了 std::function 作为map的参数,实现字符串和可调⽤对象的映射表功能。
#include <iostream>
#include <functional>
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:
Plus(int n = 10) : _n(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; }; // 包装 lambda 表达式
// 输出结果
cout << "f1(1, 1): " << f1(1, 1) << endl; // 调用普通函数
cout << "f2(1, 1): " << f2(1, 1) << endl; // 调用函数对象
cout << "f3(1, 1): " << f3(1, 1) << endl; // 调用 lambda 表达式
// 包装静态成员函数
function<int(int, int)> f4 = &Plus::plusi; // 指向静态成员函数
cout << "f4(1, 1): " << f4(1, 1) << endl; // 调用静态成员函数
// 包装普通成员函数
function<double(Plus*, double, double)> f5 = &Plus::plusd; // 指向普通成员函数
Plus pd; // 创建 Plus 对象
cout << "f5(&pd, 1.1, 1.1): " << f5(&pd, 1.1, 1.1) << endl; // 使用指针调用成员函数
// 通过对象直接调用普通成员函数
function<double(Plus, double, double)> f6 = &Plus::plusd; // 直接传对象
cout << "f6(pd, 1.1, 1.1): " << f6(pd, 1.1, 1.1) << endl; // 调用并输出
// 使用右值对象调用成员函数
function<double(Plus&&, double, double)> f7 = &Plus::plusd; // 指向普通成员函数
cout << "f7(move(pd), 1.1, 1.1): " << f7(move(pd), 1.1, 1.1) << endl; // 使用移动语义
cout << "f7(Plus(), 1.1, 1.1): " << f7(Plus(), 1.1, 1.1) << endl; // 使用临时对象
return 0;
}
#include <iostream>
#include <vector>
#include <stack>
#include <map>
#include <functional>
#include <string>
using namespace std;
// 传统方式的实现
class Solution1 {
public:
int evalRPN(vector<string>& tokens) {
stack<int> st; // 使用栈来存储操作数
for (auto& str : tokens) {
if (str == "+" || str == "-" || str == "*" || str == "/") {
// 操作符
int right = st.top(); // 取出栈顶元素,右操作数
st.pop();
int left = st.top(); // 取出下一个元素,左操作数
st.pop();
switch (str[0]) {
case '+':
st.push(left + right); // 加法
break;
case '-':
st.push(left - right); // 减法
break;
case '*':
st.push(left * right); // 乘法
break;
case '/':
st.push(left / right); // 除法
break;
}
} else {
// 操作数
st.push(stoi(str)); // 将字符串转换为整数并推入栈中
}
}
return st.top(); // 返回栈顶元素,即计算结果
}
};
// 使用 map 映射 string 和 function 的方式实现
class Solution2 {
public:
int evalRPN(vector<string>& tokens) {
stack<int> st; // 使用栈来存储操作数
// map 映射操作符到对应的 lambda 表达式
map<string, function<int(int, int)>> opFuncMap = {
{"+", [](int x, int y) { return x + y; }},
{"-", [](int x, int y) { return x - y; }},
{"*", [](int x, int y) { return x * y; }},
{"/", [](int x, int y) { return x / y; }}
};
for (auto& str : tokens) {
if (opFuncMap.count(str)) { // 操作符
int right = st.top(); // 取出右操作数
st.pop();
int left = st.top(); // 取出左操作数
st.pop();
int ret = opFuncMap[str](left,right); // 调用对应的运算函数
st.push(ret); // 将结果推入栈
} else {
// 操作数
st.push(stoi(str)); // 将字符串转换为整数并推入栈中
}
}
return st.top(); // 返回栈顶元素,即计算结果
}
};
// 示例使用
int main() {
Solution1 sol1;
Solution2 sol2;
vector<string> tokens = {"2", "1", "+", "3", "*"}; // 示例输入
cout << "Solution1 result: " << sol1.evalRPN(tokens) << endl; // 结果应为 9
cout << "Solution2 result: " << sol2.evalRPN(tokens) << endl; // 结果应为 9
return 0;
}
8.2 bind
simple(1)
template <class Fn, class... Args>
/* unspecified */ bind (Fn&& fn, Args&&... args);
with return type (2)
template <class Ret, class Fn, class... Args>
/* unspecified */ bind (Fn&& fn, Args&&... args);
• bind 是⼀个函数模板,它也是⼀个可调⽤对象的包装器,可以把他看做⼀个函数适配器,对接收
的fn可调⽤对象进⾏处理后返回⼀个可调⽤对象。 bind 可以⽤来调整参数个数和参数顺序。
bind 也在这个头⽂件中。
• 调⽤bind的⼀般形式: auto newCallable = bind(callable,arg_list); 其中
newCallable本⾝是⼀个可调⽤对象,arg_list是⼀个逗号分隔的参数列表,对应给定的callable的
参数。当我们调⽤newCallable时,newCallable会调⽤callable,并传给它arg_list中的参数。
• arg_list中的参数可能包含形如_n的名字,其中n是⼀个整数,这些参数是占位符,表⽰
newCallable的参数,它们占据了传递给newCallable的参数的位置。数值n表⽰⽣成的可调⽤对象
中参数的位置:_1为newCallable的第⼀个参数,_2为第⼆个参数,以此类推。_1/_2/_3…这些占
位符放到placeholders的⼀个命名空间中。
#include <iostream>
#include <functional>
using namespace std;
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;
}
// 加法类
class Plus {
public:
// 静态成员函数
static int plusi(int a, int b) {
return a + b;
}
// 普通成员函数
double plusd(double a, double b) {
return a + b;
}
};
int main() {
// 绑定 Sub 函数,创建绑定的函数对象
auto sub1 = bind(Sub, _1, _2);
cout << "sub1(10, 5): " << sub1(10, 5) << endl;
// bind 本质返回的仿函数对象
// 调整参数顺序(不常用)
// _1代表第一个实参
// _2代表第二个实参
auto sub2 = bind(Sub, _2, _1);
cout << "sub2(10, 5): " << sub2(10, 5) << endl;
// 调整参数个数(常用)
auto sub3 = bind(Sub, 100, _1);
cout << "sub3(5): " << sub3(5) << endl;
auto sub4 = bind(Sub, _1, 100);
cout << "sub4(5): " << sub4(5) << endl;
// 分别绑死第1、2、3个参数
auto sub5 = bind(SubX, 100, _1, _2);
cout << "sub5(5, 1): " << sub5(5, 1) << endl;
auto sub6 = bind(SubX, _1, 100, _2);
cout << "sub6(5, 1): " << sub6(5, 1) << endl;
auto sub7 = bind(SubX, _1, _2, 100);
cout << "sub7(5, 1): " << sub7(5, 1) << endl;
// 成员函数对象进行绑死,就不需要每次都传递了
function<double(Plus&&, double, double)> f6 = &Plus::plusd;
Plus pd;
cout << "f6(move(pd), 1.1, 1.1): " << f6(move(pd), 1.1, 1.1) << endl;
cout << "f6(Plus(), 1.1, 1.1): " << f6(Plus(), 1.1, 1.1) << endl;
// bind一般用于,绑死一些固定参数
function<double(double, double)> f7 = bind(&Plus::plusd, Plus(), _1, _2);
cout << "f7(1.1, 1.1): " << f7(1.1, 1.1) << endl;
// 计算复利的lambda
auto func1 = [](double rate, double money, int year) -> double {
double ret = money;
for (int i = 0; i < year; i++) {
ret += ret * rate; // 按年计算复利
}
return ret - money; // 返回利息
};
// 绑死一些参数,实现出支持不同年华利率、不同金额和不同年份计算出复利的结算利息
function<double(double)> func3_1_5 = bind(func1, 0.015, _1, 3);
function<double(double)> func5_1_5 = bind(func1, 0.015, _1, 5);
function<double(double)> func10_2_5 = bind(func1, 0.025, _1, 10);
function<double(double)> func20_3_5 = bind(func1, 0.035, _1, 30);
cout << func3_1_5(1000000) << endl;
cout << func5_1_5(1000000) << endl;
cout << func10_2_5(1000000) << endl;
cout << func20_3_5(1000000) << endl;
return 0;
}
九、智能指针
后面一章会详细介绍
# 总结
提示:这里对文章进行总结:
例如:以上就是今天要讲的内容,本文仅仅简单介绍了pandas的使用,而pandas提供了大量能使我们快速便捷地处理数据的函数和方法。