
🔥承渊政道: 个人主页
❄️个人专栏: 《C语言基础语法知识》 《数据结构与算法》
✨逆境不吐心中苦,顺境不忘来时路! 🎬 博主简介:

引言:前篇文章,小编已经介绍了关于⽤哈希表封装myunordered_map和myunordered_set以及位图和布隆过滤器介绍!相信大家应该有所收获!接下来我将带领大家继续深入学习C++的相关内容!本篇文章着重介绍关于深入回溯C++11的发展历程!回溯C++11的发展历程,从来不是对特性的机械罗列,而是探寻一门编程语言在危机中破局的底层逻辑:从早期提案的零星萌芽,到TR1的技术试水,再到委员会的激烈辩论、特性的权衡取舍,每一步演进都镌刻着兼容历史与拥抱未来的博弈,追求性能与简化开发的平衡.它不仅解决了C++十余年的积弊,更定义了现代编程语言的设计哲学,为C++14/17/20的后续发展筑牢了根基.那么这里面到底有哪些知识需要我们去学习的呢?废话不多说,带着这些疑问,下面跟着小编的节奏🎵一起去疯狂的学习吧!
目录
1.C++11的发展历史
C++11是C++的第⼆个主要版本,并且是从 C++98 起的最重要更新.它引⼊了⼤量更改,标准化了既有实践,并改进了对C++程序员可⽤的抽象.在它最终由ISO在2011年8⽉12⽇采纳前,⼈们曾使⽤名称"C++0x",因为它曾被期待在 2010 年之前发布.C++03与C++11期间花了8年时间.故⽽这是迄今为⽌最⻓的版本间隔.从那时起,C++有规律地每3年更新⼀次.
2.列表初始化
2.1C++98传统的{}
cpp
//C++98中⼀般数组和结构体可以⽤{}进⾏初始化
#include<iostream>
using namespace std;
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.2C++11中的{}
①C++11以后想统⼀初始化⽅式,试图实现⼀切对象皆可⽤{}初始化,
{}初始化也叫做列表初始化.
②内置类型⽀持,⾃定义类型也⽀持,⾃定义类型本质是类型转换,中间会产⽣临时对象,最后优化了以后变成直接构造.
③{}初始化的过程中,可以省略掉=
④C++11列表初始化的本意是想实现⼀个⼤统⼀的初始化⽅式,其次它在有些场景下带来的不少便利,如容器push/inset多参数构造的对象时,{}初始化会很⽅便.
cpp
#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 };
// ⾃定义类型⽀持
// 这⾥本质是⽤{ 2025, 1, 1}构造⼀个Date临时对象
// 临时对象再去拷⻉构造d1,编译器优化后合⼆为⼀变成{ 2025, 1, 1}直接构造初始化d1
// 运⾏⼀下,我们可以验证上⾯的理论,发现是没调⽤拷⻉构造的
Date d1 = { 2025, 1, 1};
// 这⾥d2引⽤的是{ 2024, 7, 25 }构造的临时对象
const Date& d2 = { 2024, 7, 25 };
// 需要注意的是C++98⽀持单参数时类型转换,也可以不⽤{}
Date d3 = { 2025};
Date d4 = 2025;
// 可以省略掉=
Point p1 { 1, 2 };
int x2 { 2 };
Date d6 { 2024, 7, 25 };
const Date& d7 { 2024, 7, 25 };
// 不⽀持,只有{}初始化,才能省略=
// Date d8 2025;
vector<Date> v;
v.push_back(d1);
v.push_back(Date(2025, 1, 1));
// ⽐起有名对象和匿名对象传参,这⾥{}更有性价⽐
v.push_back({ 2025, 1, 1 });
return 0;
}
2.3C++11中的std::initializer_list
①上⾯的初始化已经很⽅便,但是对象容器初始化还是不太⽅便,⽐如⼀个vector对象,我想⽤N个值去构造初始化,那么我们得实现很多个构造函数才能⽀持,
vector<int> v1 ={1,2,3};vector<int> v2 = {1,2,3,4,5};
②C++11库中提出了⼀个std::initializer_list的类,auto il = { 10, 20, 30 }; //the type 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的构造函数⽀持的.
cpp
//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);
cpp
#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;
}
3.左右值引⽤和移动语义
C++98的C++语法中就有引⽤的语法,⽽C++11中新增了的右值引⽤语法特性,C++11之后我们之前学习的引⽤就叫做左值引⽤.⽆论左值引⽤还是右值引⽤,都是给对象取别名.
3.1左值和右值
①左值是⼀个表示数据的表达式(如变量名或解引⽤的指针),⼀般是有持久状态,存储在内存中,我们可以获取它的地址,左值可以出现赋值符号的左边,也可以出现在赋值符号右边.定义时const修饰符后的左值,不能给它赋值,但是可以取它的地址.
②右值也是⼀个表示数据的表达式,要么是字⾯值常量、要么是表达式求值过程中创建的临时对象等,右值可以出现在赋值符号的右边,但是不能出现出现在赋值符号的左边,右值不能取地址.
③值得⼀提的是,左值的英⽂简写为lvalue,右值的英⽂简写为rvalue.传统认为它们分别是leftvalue、right value的缩写.现代C++中,lvalue被解释为loactor value的缩写,可意为存储在内存中、有明确存储地址可以取地址的对象,⽽rvalue被解释为read value,指的是那些可以提供数据值,但是不可以寻址,例如:临时变量,字⾯量常量,存储于寄存器中的变量等,也就是说左值和右值的核⼼区别就是能否取地址.
cpp
#include<iostream>
using namespace std;
int main()
{
// 左值:可以取地址
// 以下的p、b、c、*p、s、s[0]就是常⻅的左值
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")都是常⻅的右值
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 <class T>typename remove_reference<T>::type&& move (T&&arg);
⑤move是库⾥⾯的⼀个函数模板,本质内部是进⾏强制类型转换,当然他还涉及⼀些引⽤折叠的知识,这个后⾯会介绍.
⑥需要注意的是变量表达式都是左值属性,也就意味着⼀个右值被右值引⽤绑定后,右值引⽤变量变
量表达式的属性是左值.
⑦语法层⾯看,左值引⽤和右值引⽤都是取别名,不开空间.从汇编底层的⻆度看下⾯代码中r1和rr1汇编层实现,底层都是⽤指针实现的,没什么区别.底层汇编等实现和上层语法表达的意义有时是背离的,所以不要混到⼀起去理解,互相佐证,这样反⽽是陷⼊迷途.
cpp
#include<iostream>
using namespace std;
//template <class _Ty>
//remove_reference_t<_Ty>&& move(_Ty&& _Arg)
//{ forward _Arg as movable
//return static_cast<remove_reference_t<_Ty>&&>(_Arg);
//}
int main()
{
// 左值:可以取地址
// 以下的p、b、c、*p、s、s[0]就是常⻅的左值
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(左值)
int&& rrx1 = move(b);
int*&& rrx2 = move(p);
int&& rrx3 = move(*p);
string&& rrx4 = move(s);
string&& rrx5 = (string&&)s;
// b、r1、rr1都是变量表达式,都是左值
cout << &b << endl;
cout << &r1 << endl;
cout << &rr1 << endl;
//这⾥要注意的是,rr1的属性是左值,所以不能再被右值引⽤绑定,除⾮move⼀下
int& r6 = r1;
//int&& rrx6 = rr1;
int&& rrx6 = move(rr1);
return 0;
}
3.3引⽤延⻓⽣命周期
右值引⽤可⽤于临时对象延⻓⽣命周期,const 的左值引⽤也能延⻓临时对象⽣存期,但这些对象⽆法被修改.
cpp
#include<iostream>
using namespace std;
int main()
{
std::string s1 = "Test";
// std::string&& r1 = s1; // 错误:不能绑定到左值
const std::string& r2 = s1 + s1; // OK:到 const 的左值引⽤延⻓⽣存期
// r2 += "Test"; // 错误:不能通过到 const 的引⽤修改
std::string&& r3 = s1 + s1; // OK:右值引⽤延⻓⽣存期
r3 += "Test"; // OK:能通过到⾮ const 的引⽤修改
std::cout << r3 << '\n';
return 0;
}
3.4左值和右值的参数匹配
①C++98中,我们实现⼀个const左值引⽤作为参数的函数,那么实参传递左值和右值都可以匹配.
②C++11以后,分别重载左值引⽤、const左值引⽤、右值引⽤作为形参的f函数,那么实参是左值会匹配f(左值引⽤),实参是const左值会匹配f(const 左值引⽤),实参是右值会匹配f(右值引⽤).
③右值引⽤变量在⽤于表达式时属性是左值,这个设计这⾥会感觉跟怪,后面介绍右值引⽤的使⽤场景时,就能体会这样设计的价值了.</font
cpp
#include<iostream>
using namespace std;
void f(int& x)
{
std::cout << "左值引⽤重载 f(" << x << ")\n";
}
void f(const int& x)
{
std::cout << "到 const 的左值引⽤重载 f(" << x << ")\n";
}
void f(int&& x)
{
std::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(int&&) 重载则会调⽤ f(const int&)
f(std::move(i)); // 调⽤ f(int&&)
// 右值引⽤变量在⽤于表达式时是左值
int&& x = 1;
f(x); // 调⽤ f(int& x)
f(std::move(x)); // 调⽤ f(int&& x)
return 0;
}
3.5右值引⽤和移动语义的使⽤场景
3.5.1左值引⽤主要使⽤场景回顾
左值引⽤主要使⽤场景是在函数中左值引⽤传参和左值引⽤传返回值时减少拷⻉,同时还可以修改实参和修改返回对象的价值.左值引⽤已经解决⼤多数场景的拷⻉效率问题,但是有些场景不能使⽤传左值引⽤返回,如addStrings和generate函数,C++98中的解决⽅案只能是被迫使⽤输出型参数解决.那么C++11以后这⾥可以使⽤右值引⽤做返回值解决吗?显然是不可能的,因为这⾥的本质是返回对象是⼀个局部对象,函数结束这个对象就析构销毁了,右值引⽤返回也⽆法概念对象已经析构销毁的事实.
cpp
class Solution {
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 = ret % 10;
str += ('0'+ret);
}
if(next == 1)
str += '1';
reverse(str.begin(), str.end());
return str;
}
};
class Solution {
public:
// 这⾥的传值返回拷⻉代价就太⼤了
vector<vector<int>> generate(int numRows) {
vector<vector<int>> vv(numRows);
for(int i = 0; i < numRows; ++i)
{
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 vv;
}
};
3.5.2移动构造和移动赋值
①移动构造函数是⼀种构造函数,类似拷⻉构造函数,移动构造函数要求第⼀个参数是该类类型的引⽤,但是不同的是要求这个参数是右值引⽤,如果还有其他参数,额外的参数必须有缺省值.
②移动赋值是⼀个赋值运算符的重载,它跟拷⻉赋值构成函数重载,类似拷⻉赋值函数,移动赋值函数要求第⼀个参数是该类类型的引⽤,但是不同的是要求这个参数是右值引⽤.
③对于像string/vector这样深拷⻉的类或者包含深拷⻉的成员变量的类,移动构造和移动赋值才有意义,因为移动构造和移动赋值的第⼀个参数都是右值引⽤的类型,它本质是要"窃取"引⽤的右值对象的资源,⽽不是像拷⻉构造和拷⻉赋值那样去拷⻉资源,从提⾼效率.下⾯的样例实现了移动构造和移动赋值,我们需要结合场景理解.
cpp
#define _CRT_SECURE_NO_WARNINGS 1
#include<iostream>
#include<assert.h>
#include<string.h>
#include<algorithm>
using namespace std;
namespace lcz
{
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);
}
}
//移动构造
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;
}
//移动赋值
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()
{
lcz::string s1("xxxxx");
// 拷⻉构造
lcz::string s2 = s1;
// 构造+移动构造,优化后直接构造
lcz::string s3 = lcz::string("yyyyy");
// 移动构造
lcz::string s4 = move(s1);
cout << "******************************" << endl;
return 0;
}
3.5.3右值引⽤和移动语义解决传值返回问题
cpp
#include<iostream>
#include<string.h>
using namespace std;
namespace lcz
{
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()
{
std::string ret = lcz::addStrings("11111", "2222");
cout << ret.c_str() << endl;
return 0;
}
// 场景2
int main()
{
std::string ret;
ret = lcz::addStrings("11111", "2222");
cout << ret.c_str() << endl;
return 0;
}
右值对象构造,只有拷⻉构造,没有移动构造的场景
①下面展示了vs2019 debug环境下编译器对拷⻉的优化,左边为不优化的情况下,两次拷⻉构造,右边为编译器优化的场景下连续步骤中的拷⻉合⼆为⼀变为⼀次拷⻉构造.
②需要注意的是在vs2019的release和vs2022的debug和release,下⾯代码优化为⾮常恐怖,会直接将str对象的构造,str拷⻉构造临时对象,临时对象拷⻉构造ret对象,合三为⼀,变为直接构造.变为直接构造.要理解这个优化要结合局部对象⽣命周期和栈帧的⻆度理解
③linux下可以将下⾯代码拷⻉到test.cpp⽂件,编译时⽤ g++ test.cpp -fno-elideconstructors的⽅式关闭构造优化,运⾏结果可以看到左边没有优化的两次拷⻉.
右值对象构造,有拷⻉构造,也有移动构造的场景.
①下图展示了vs2019 debug环境下编译器对拷⻉的优化,左边为不优化的情况下,两次移动构造,右边为编译器优化的场景下连续步骤中的拷⻉合⼆为⼀变为⼀次移动构造.
②需要注意的是在vs2019的release和vs2022的debug和release,下⾯代码优化为⾮常恐怖,会直接将str对象的构造,str拷⻉构造临时对象,临时对象拷⻉构造ret对象,合三为⼀,变为直接构造.要理解这个优化要结合局部对象⽣命周期和栈帧的⻆度理解.
③linux下可以将下⾯代码拷⻉到test.cpp⽂件,编译时⽤ g++ test.cpp -fno-elideconstructors 的⽅式关闭构造优化,运⾏结果可以看到左边没有优化的两次移动.
右值对象赋值,只有拷⻉构造和拷⻉赋值,没有移动构造和移动赋值的场景.
①下图展示了vs2019debug和g++ test.cpp -fno-elide-constructors 关闭优化环境下编译器的处理,⼀次拷⻉构造,⼀次拷⻉赋值.
②需要注意的是在vs2019的release和vs2022的debug和release,下⾯代码会进⼀步优化,直接构造要返回的临时对象,str本质是临时对象的引⽤,底层⻆度⽤指针实现.运⾏结果的⻆度,我们可以看到str的析构是在赋值以后,说明str就是临时对象的别名.
右值对象赋值,既有拷⻉构造和拷⻉赋值,也有移动构造和移动赋值的场景.
①下图展示了vs2019debug和g++ test.cpp -fno-elide-constructors 关闭优化环境下编译器的处理,⼀次移动构造,⼀次移动赋值.
②需要注意的是在vs2019的release和vs2022的debug和release,下⾯代码会进⼀步优化,直接构造要返回的临时对象,str本质是临时对象的引⽤,底层⻆度⽤指针实现.运⾏结果的⻆度,我们可以看到str的析构是在赋值以后,说明str就是临时对象的别名.
3.5.4右值引⽤和移动语义在传参中的提效
①查看STL⽂档我们发现C++11以后容器的push和insert系列的接⼝否增加的右值引⽤版本.
②当实参是⼀个左值时,容器内部继续调⽤拷⻉构造进⾏拷⻉,将对象拷⻉到容器空间中的对象.
③当实参是⼀个右值,容器内部则调⽤移动构造,右值对象的资源到容器空间的对象上.
④如果把我们之前模拟实现的lcz::list拷⻉过来,⽀持右值引⽤参数版本的push_back和insert显然是可以的,但为了不那么麻烦,我们就模拟实现一个string类去调用list的接口即可.
⑤其实这⾥还有⼀个emplace系列的接⼝,但是这个涉及可变参数模板,我们需要把可变参数模板介绍以后再介绍emplace系列的接⼝.
cpp
#define _CRT_SECURE_NO_WARNINGS
#include <iostream>
#include <list> // std::list 容器头文件
using namespace std;
namespace lcz
{
//模拟实现string类,支持 拷贝/移动 构造/赋值
class string
{
public:
//普通构造函数(const char* 字符串构造)
string(const char* str = "")
{
_str = new char[strlen(str) + 1];
strcpy(_str, str);
cout << "string构造函数" << endl;
}
//拷贝构造函数(深拷贝)
//适配:lt.push_back(左值对象) 调用
string(const string& s)
{
_str = new char[strlen(s._str) + 1];
strcpy(_str, s._str);
cout << "string拷贝构造(深拷贝)" << endl;
}
//移动构造函数(核心:转移资源所有权,高效)
//适配:lt.push_back(右值) / lt.push_back(move(左值)) 调用
string(string&& s) noexcept
{
_str = s._str; //直接浅拷贝指针
s._str = nullptr; //将源对象置空,避免析构释放资源
cout << "string移动构造(转移资源)" << endl;
}
//析构函数
~string()
{
if (_str)
{
delete[] _str;
_str = nullptr;
}
}
private:
char* _str;
};
}
int main()
{
std::list<lcz::string> lt;
//push_back(左值对象) → 调用 拷贝构造
lcz::string s1("111111111111111111111");
lt.push_back(s1);
cout << "*************************" << endl;
//push_back(匿名对象/右值) → 调用 移动构造
lt.push_back(lcz::string("22222222222222222222222222222"));
cout << "*************************" << endl;
//push_back(常量字符串) → 隐式构造临时对象 + 移动构造
lt.push_back("3333333333333333333333333333");
cout << "*************************" << endl;
//push_back(move(左值)) → 将左值转为右值 → 移动构造
lt.push_back(move(s1));
cout << "*************************" << endl;
return 0;
}
3.6类型分类
①C++11以后,进⼀步对类型进⾏了划分,右值被划分纯右值(pure value)和将亡值(expiring value).
②纯右值是指那些字⾯值常量或求值结果相当于字⾯值或是⼀个不具名的临时对象.如: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),泛左值包含将亡值和左值.
⑤值类别和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.
cpp
#include <iostream>
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;
}
cpp
#include <iostream>
using namespace std;
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;
// a是左值,推导出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函数.
②但是结合之前的介绍,变量表达式都是左值属性,也就意味着⼀个右值被右值引⽤绑定后,右值引⽤变量表达式的属性是左值,也就是说Function函数中t的属性是左值,那么我们把t传递给下⼀层函数Fun,那么匹配的都是左值引⽤版本的Fun函数.这⾥我们想要保持t对象的属性,就需要使⽤完美转发实现.
③template <class T> T&& forward (typename remove_reference<T>::type&arg);
④template <class T> T&& forward (typenameremove_reference<T>::type&& arg);
⑤完美转发forward本质是⼀个函数模板,它主要还是通过引⽤折叠的⽅式实现,下⾯示例中传递给Function的实参是右值,T被推导为int,没有折叠,forward内部t被强转为右值引⽤返回;传递给Function的实参是左值,T被推导为int&,引⽤折叠为左值引⽤,forward内部t被强转为左值引⽤返回.
cpp
#include <iostream>
using namespace std;
template <class _Ty>
_Ty&& forward(remove_reference_t<_Ty>& _Arg) noexcept
{ // forward an lvalue as either an lvalue or an rvalue
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;
// a是左值,推导出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.可变参数模板
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...运算符去计算参数包中参数的个数.
cpp
#include <iostream>
using namespace std;
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包扩展
①对于⼀个参数包,我们除了能计算它的参数个数,我们能做的唯⼀的事情就是扩展它,当扩展⼀个包时,我们还要提供⽤于每个扩展元素的模式,扩展⼀个包就是将它分解为构成的元素,对每个元素应⽤模式,获得扩展后的列表.我们通过在模式的右边放⼀个省略号(...)来触发扩展操作.底层的实现细节如图所示.
②C++还⽀持更复杂的包扩展,直接将参数包依次展开依次作为实参给⼀个函数去处理.
cpp
// 可变模板参数
// 参数类型可变
// 参数个数可变
// 打印参数包内容
//template <class ...Args>
//void Print(Args... args)
//{
//可变参数模板编译时解析
//下⾯是运⾏获取和解析,所以不⽀持这样⽤
//cout << sizeof...(args) << endl;
//for (size_t i = 0; i < sizeof...(args); i++)
//{
//cout << args[i] << " ";
//}
// cout << endl;
//}
#include <iostream>
using namespace std;
void ShowList()
{
// 编译器时递归的终⽌条件,参数包是0个时,直接匹配这个函数
cout << endl;
}
template <class T, class ...Args>
void ShowList(T x, Args... args)
{
cout << x << " ";
//args是N个参数的参数包
//调⽤ShowList,参数包的第⼀个传给x,剩下N-1传给第⼆个参数包
ShowList(args...);
}
//编译时递归推导解析参数
template <class ...Args>
void Print(Args... args)
{
ShowList(args...);
}
int main()
{
Print();
Print(1);
Print(1, string("xxxxx"));
Print(1, string("xxxxx"), 2.2);
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);
//}
cpp
#include <iostream>
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)...);
}
//本质可以理解为编译器编译时,包的扩展模式
//将上⾯的函数模板扩展实例化为下⾯的函数
//是不是很抽象,C++11以后,只能说委员会的⼤佬设计语法思维跳跃得太厉害
//void Print(int x, string y, double z)
//{
// Arguments(GetArg(x), GetArg(y), GetArg(z));
//}
int main()
{
Print(1, string("xxxxx"), 2.2);
return 0;
}
4.3empalce系列接⼝
①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<T>,empalce还⽀持直接插⼊构造T对象的参数,这样有些场景会更⾼效⼀些,可以直接在容器空间上构造T对象.
③emplace_back总体⽽⾔是更⾼效,推荐以后使⽤emplace系列替代insert和push系列.
④第⼆个程序中我们模拟实现了list的emplace和emplace_back接⼝,这⾥把参数包不段往下传递,最终在结点的构造中直接去匹配容器存储的数据类型T的构造,所以达到了前⾯说的empalce⽀持直接插⼊构造T对象的参数,这样有些场景会更⾼效⼀些,可以直接在容器空间上构造T对象.
⑤传递参数包过程中,如果是 Args&&... args 的参数包,要⽤完美转发参数包,⽅式如下std::forward<Args>(args)...,否则编译时包扩展后右值引⽤变量表达式就变成了左值.
cpp
#define _CRT_SECURE_NO_WARNINGS 1
#include <iostream>
using namespace std;
#include<list>
namespace lcz
{
class string
{
public:
// 普通构造
string(const char* str = "")
{
_str = new char[strlen(str) + 1];
strcpy(_str, str);
cout << "string构造函数" << endl;
}
// 拷贝构造(左值插入调用)
string(const string& s)
{
_str = new char[strlen(s._str) + 1];
strcpy(_str, s._str);
cout << "string拷贝构造(深拷贝)" << endl;
}
// 移动构造(右值插入调用)
string(string&& s) noexcept
{
_str = s._str;
s._str = nullptr;
cout << "string移动构造(转移资源)" << endl;
}
// 析构函数
~string()
{
if (_str)
{
delete[] _str;
_str = nullptr;
}
}
private:
char* _str;
};
}
// emplace_back总体⽽⾔是更⾼效,推荐以后使⽤emplace系列替代insert和push系列
int main()
{
list<lcz::string> lt;
// 传左值,跟push_back⼀样,⾛拷⻉构造
lcz::string s1("111111111111");
lt.emplace_back(s1);
cout << "*********************************" << endl;
// 右值,跟push_back⼀样,⾛移动构造
lt.emplace_back(move(s1));
cout << "*********************************" << endl;
// 直接把构造string参数包往下传,直接⽤string参数包构造string
// 这⾥达到的效果是push_back做不到的
lt.emplace_back("111111111111");
cout << "*********************************" << endl;
list<pair<lcz::string, int>> lt1;
//跟push_back⼀样
//构造pair + 拷⻉/移动构造pair到list的节点中data上
pair<lcz::string, int> kv("苹果", 1);
lt1.emplace_back(kv);
cout << "*********************************" << endl;
//跟push_back⼀样
lt1.emplace_back(move(kv));
cout << "*********************************" << endl;
// 直接把构造pair参数包往下传,直接⽤pair参数包构造pair
// 这⾥达到的效果是push_back做不到的
lt1.emplace_back("苹果", 1);
cout << "*********************************" << endl;
return 0;
}
5.新的类功能
5.1默认的移动构造和移动赋值
①原来C++类中,有6个默认成员函数:构造函数/析构函数/拷⻉构造函数/拷⻉赋值重载/取地址重载/const 取地址重载,最后重要的是前4个,后两个⽤处不⼤,默认成员函数就是我们不写编译器会⽣成⼀个默认的.C++11 新增了两个默认成员函数,移动构造函数和移动赋值运算符重载.
②如果你没有⾃⼰实现移动构造函数,且没有实现析构函数 、拷⻉构造、拷⻉赋值重载中的任意⼀个.那么编译器会⾃动⽣成⼀个默认移动构造.默认⽣成的移动构造函数,对于内置类型成员会执⾏逐成员按字节拷⻉,⾃定义类型成员,则需要看这个成员是否实现移动构造,如果实现了就调⽤移动构造,没有实现就调⽤拷⻉构造.
③如果你没有⾃⼰实现移动赋值重载函数,且没有实现析构函数 、拷⻉构造、拷⻉赋值重载中的任意⼀个,那么编译器会⾃动⽣成⼀个默认移动赋值.默认⽣成的移动构造函数,对于内置类型成员会执⾏逐成员按字节拷⻉,⾃定义类型成员,则需要看这个成员是否实现移动赋值,如果实现了就调⽤移动赋值,没有实现就调⽤拷⻉赋值.(默认移动赋值跟上⾯移动构造完全类似)
④如果你提供了移动构造或者移动赋值,编译器不会⾃动提供拷⻉构造和拷⻉赋值.
cpp
#define _CRT_SECURE_NO_WARNINGS 1
#include <iostream>
using namespace std;
namespace lcz
{
class string
{
public:
// 普通构造
string(const char* str = "")
{
_str = new char[strlen(str) + 1];
strcpy(_str, str);
cout << "string构造函数" << endl;
}
// 拷贝构造(左值插入调用)
string(const string& s)
{
_str = new char[strlen(s._str) + 1];
strcpy(_str, s._str);
cout << "string拷贝构造(深拷贝)" << endl;
}
// 移动构造(右值插入调用)
string(string&& s) noexcept
{
_str = s._str;
s._str = nullptr;
cout << "string移动构造(转移资源)" << endl;
}
// 析构函数
~string()
{
if (_str)
{
delete[] _str;
_str = nullptr;
}
}
private:
char* _str;
};
}
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:
lcz::string _name;
int _age;
};
int main()
{
Person s1;
Person s2 = s1;
Person s3 = std::move(s1);
Person s4;
// s5 = std::move(s2);
return 0;
}
5.2委托构造函数
①C++中的委托构造函数(Delegating Constructor)是C++11引入的特性,允许一个构造函数调用同类中的其他构造函数,从而减少代码重复并提高可维护性.
②被委托的构造函数必须初始化所有成员变量,因为委托构造函数后不能再重复初始化.
cpp
//在初始化列表中调用同类的其他构造函数:类名(参数列表)
//委托后,不能再初始化其他成员变量
//执行顺序:先执行被委托的构造函数 → 再执行自身函数体
class Person
{
public:
// 1. 核心构造函数(被委托,完成所有初始化)
Person(string name, int age, int id)
: _name(name)
, _age(age)
, _id(id)
{
cout << "全参构造执行" << endl;
}
// 2. 委托全参构造
Person()
: Person("未知", 0, 0) // 委托调用
{
cout << "无参构造执行" << endl;
}
// 3. 委托全参构造
Person(string name)
: Person(name, 0, 0) // 委托调用
{
cout << "单参构造执行" << endl;
}
private:
string _name;
int _age;
int _id;
};
5.3继承构造函数
①继承构造函数是C++11引入的一项特性,它允许派生类直接继承基类的构造函数,而不需要手动重新定义它们.这一特性显著简化了派生类的编写,特别是在基类有多个构造函数的情况下.
②派生类继承基类的普通构造函数,特殊的拷贝构造函数/移动构造函数不继承.
③继承构造函数中派生类自己的成员变量如果有缺省值会使用缺省值初始化,如果没有缺省值那么跟之前类似,内置类型成员不确定,自定义类型成员使用默认构造初始化.
cpp
class Base
{
public:
Base() { cout << "Base()" << endl; }
Base(int a) : _a(a) { cout << "Base(int)" << endl; }
Base(int a, double b) : _a(a), _b(b) { cout << "Base(int,double)" << endl; }
private:
int _a = 0;
double _b = 0.0;
};
// 派生类:继承构造函数
class Derive : public Base
{
public:
//C++11 核心:一行继承基类所有构造函数
using Base::Base;
private:
// 派生类新增成员,推荐给默认值
int _c = 10;
};
5.4成员变量声明时给缺省值
成员变量声明时给缺省值是给初始化列表⽤的,如果没有显示在初始化列表初始化,就会在初始化列表⽤这个却绳⼦初始化,这个我在类和对象部分的文章介绍过了,这里就不细讲了,有兴趣的可以去看看.
5.5defult和delete
①C++11可以让你更好的控制要使⽤的默认函数.假设你要使⽤某个默认的函数,但是因为⼀些原因这个函数没有默认⽣成.⽐如:我们提供了拷⻉构造,就不会⽣成移动构造了,那么我们可以使⽤default关键字显示指定移动构造⽣成.
②如果能想要限制某些默认函数的⽣成,在C++98中,是该函数设置成private,并且只声明补丁已,这样只要其他⼈想要调⽤就会报错.在C++11中更简单,只需在该函数声明加上=delete即可,该语法指示编译器不⽣成对应函数的默认版本,称=delete修饰的函数为删除函数.
cpp
#define _CRT_SECURE_NO_WARNINGS 1
#include <iostream>
using namespace std;
namespace lcz
{
class string
{
public:
// 普通构造
string(const char* str = "")
{
_str = new char[strlen(str) + 1];
strcpy(_str, str);
cout << "string构造函数" << endl;
}
// 拷贝构造(左值插入调用)
string(const string& s)
{
_str = new char[strlen(s._str) + 1];
strcpy(_str, s._str);
cout << "string拷贝构造(深拷贝)" << endl;
}
// 移动构造(右值插入调用)
string(string&& s) noexcept
{
_str = s._str;
s._str = nullptr;
cout << "string移动构造(转移资源)" << endl;
}
// 析构函数
~string()
{
if (_str)
{
delete[] _str;
_str = nullptr;
}
}
private:
char* _str;
};
}
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(const Person& p) = delete;
private:
lcz::string _name;
int _age;
};
int main()
{
Person s1;
Person s2 = s1;
Person s3 = std::move(s1);
return 0;
}
5.6final与override
这个我在继承和多态文章已经进⾏了详细讲过了,感兴趣的可以去看看.
6.STL中⼀些变化
①下面圈起来的就是STL中的新容器,但是实际最有⽤的是unordered_map和unordered_set.这两个我前⾯已经进⾏了⾮常详细的讲解,其他的⼤家了解⼀下即可.
②STL中容器的新接⼝也不少,最重要的就是右值引⽤和移动语义相关的push/insert/emplace系列接⼝和移动构造和移动赋值,还有initializer_list版本的构造等,这些前⾯都讲过了,还有⼀些⽆关痛痒的如cbegin/cend等需要时查查⽂档即可.
③容器的范围for遍历,这个在容器部分也介绍了.
7.lambda
7.1lambda表达式语法
①lambda表达式本质是⼀个匿名函数对象,跟普通函数不同的是他可以定义在函数内部.lambda 表达式语法使⽤层⽽⾔没有类型,所以我们⼀般是⽤auto或者模板参数定义的对象去接收lambda对象.
②lambda表达式的格式:[capture-list] (parameters)-> return type {function boby }
③[capture-list]:捕捉列表,该列表总是出现在lambda函数的开始位置,编译器根据[]来判断接下来的代码是否为lambda函数,捕捉列表能够捕捉上下⽂中的变量供 lambda 函数使⽤,捕捉列表可以传值和传引⽤捕捉,捕捉列表为空也不能省略.
④(parameters):参数列表,与普通函数的参数列表功能类似,如果不需要参数传递,则可以连同()⼀起省略.
⑤->return type:返回值类型,⽤追踪返回类型形式声明函数的返回值类型,没有返回值时此部分可省略.⼀般返回值类型明确情况下,也可省略,由编译器对返回类型进⾏推导.
⑥{function boby}:函数体,函数体内的实现跟普通函数完全类似,在该函数体内,除了可以使⽤其参数外,还可以使⽤所有捕获到的变量,函数体为空也不能省略.
cpp
#include <iostream>
using namespace std;
int main()
{
// ⼀个简单的lambda表达式
auto add1 = [](int x, int y)->int {return x + y; };
cout << add1(1, 2) << endl;
// 1、捕捉为空也不能省略
// 2、参数为空可以省略
// 3、返回值可以省略,可以通过返回对象⾃动推导
// 4、函数题不能省略
auto func1 = []
{
cout << "hello world" << endl;
return 0;
};
func1();
int a = 0, b = 1;
auto swap1 = [](int& x, int& y)
{
int tmp = x;
x = y;
y = tmp;
};
swap1(a, b);
cout << a << ":" << 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加在参数列表的后⾯可以取消其常量性,也就说使⽤该修饰符后,传值捕捉的对象就可以修改了,但是修改还是形参对象,不会影响实参.使⽤该修饰符后,参数列表不可省略(即使参数为空).
cpp
#include <iostream>
using namespace std;
int x = 0;
//捕捉列表必须为空,因为全局变量不⽤捕捉就可以⽤,没有可被捕捉的变量
auto func1 = []()
{
x++;
};
int main()
{
//只能⽤当前lambda局部域和捕捉的对象和全局对象
int a = 0, b = 1, c = 2, d = 3;
auto func1 = [a, &b]
{
//值捕捉的变量不能修改,引⽤捕捉的变量可以修改
//a++;
b++;
int ret = a + b;
return ret;
};
cout << func1() << endl;
//隐式值捕捉
//⽤了哪些变量就捕捉哪些变量
auto func2 = [=]
{
int ret = a + b + c;
return ret;
};
cout << func2() << endl;
//隐式引⽤捕捉
//⽤了哪些变量就捕捉哪些变量
auto func3 = [&]
{
a++;
c++;
d++;
};
func3();
cout << a <<" "<< b <<" "<< c <<" "<< d <<endl;
//混合捕捉1
auto func4 = [&, a, b]
{
//a++;
//b++;
c++;
d++;
return a + b + c + d;
};
func4();
cout << a << " " << b << " " << c << " " << d << endl;
//混合捕捉1
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;
return 0;
}
7.3lambda的应⽤
①在学习lambda表达式之前,我们的使⽤的可调⽤对象只有函数指针和仿函数对象,函数指针的类型定义起来⽐较⿇烦,仿函数要定义⼀个类,相对会⽐较⿇烦.使⽤lambda去定义可调⽤对象,既简单⼜⽅便.
②lambda 在很多其他地⽅⽤起来也很好⽤.⽐如线程中定义线程的执⾏函数逻辑,智能指针中定制删除器等,lambda的应⽤还是很⼴泛的,以后我们会不断接触到.
cpp
#include <iostream>
#include<vector>
#include<algorithm>
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, 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(), [](const Goods& g1, const Goods& g2) {return g1._price < g2._price;
});
sort(v.begin(), v.end(), [](const Goods& g1, const Goods& g2) {return g1._price > g2._price;
});
sort(v.begin(), v.end(), [](const Goods& g1, const Goods& g2) {return g1._evaluate < g2._evaluate;
});
sort(v.begin(), v.end(), [](const Goods& g1, const Goods& g2) {return g1._evaluate > g2._evaluate;
});
return 0;
}
7.4lambda的原理
①lambda的原理和范围for很像,编译后从汇编指令层的⻆度看,压根就没有lambda和范围for这样的东西.范围for底层是迭代器,⽽lambda底层是仿函数对象,也就说我们写了⼀个lambda以后,编译器会⽣成⼀个对应的仿函数的类.
②仿函数的类名是编译按⼀定规则⽣成的,保证不同的lambda⽣成的类名不同,lambda参数/返回类型/函数体就是仿函数operator()的参数/返回类型/函数体,lambda的捕捉列表本质是⽣成的仿函数类的成员变量,也就是说捕捉列表的变量都是lambda类构造函数的实参,当然隐式捕捉,编译器要看使⽤哪些就传那些对象.
③上⾯的原理,我们可以透过汇编层了解⼀下,下⾯第⼆段汇编层代码印证了上⾯的原理.
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;
//lambda
auto r2 = [rate](double money, int year) {
return money * rate * year;
};
//函数对象
Rate r1(rate);
r1(10000, 2);
r2(10000, 2);
auto func1 = [] {
cout << "hello world" << endl;
};
func1();
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.包装器
包装器(function wrapper)是C++11引入的标准库类模板std::function,它是一个通用的可调用对象封装工具,也是现代C++中最常用的类型适配器之一.我们可以把它理解为:给所有能被调用的东西套上一个统一的外壳.无论你是普通函数、仿函数、lambda 表达式、类成员函数,经过std::function包装后,都会变成同一种通用类型,可以统一存储、传递、调用.
C++中有很多可调用对象,但它们的类型完全不同:
普通函数
函数指针
仿函数(重载operator() 的类)
Lambda 表达式
类的静态/非静态成员函数
这些对象无法统一存放、无法统一传参,代码冗余且难以维护.
包装器的使命:抹平所有可调用对象的类型差异,提供统一的调用接口.
8.1function
①std::function 是⼀个类模板,也是⼀个包装器.std::function的实例对象可以包装存储其他的可以调⽤对象,包括函数指针、仿函数、 lambda 、 bind 表达式等,存储的可调⽤对象被称为std::function的⽬标.若std::function不含⽬标,则称它为空.调⽤空std::function的⽬标导致抛出 std::bad_function_call 异常.
②以上是function 的原型,它被定义<functional>头⽂件中.std::function - cppreference是function的官⽅⽂件链接.
③函数指针、仿函数、 lambda 等可调⽤对象的类型各不相同,std::function 的优势就是统⼀类型,对它们都可以进⾏包装,这样在很多地⽅就⽅便声明可调⽤对象的类型,下⾯的代码样例展示了std::function作为map的参数,实现字符串和可调⽤对象的映射表功能.
cpp
#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)
{}
static int plusi(int a, int b)
{
return a + b;
}
double plusd(double a, double b)
{
return (a + b) * _n;
}
private:
int _n;
};
int main()
{
// 包装各种可调⽤对象
function<int(int, int)> f1 = f;
function<int(int, int)> f2 = Functor();
function<int(int, int)> f3 = [](int a, int b) {return a + b; };
cout << f1(1, 1) << endl;
cout << f2(1, 1) << endl;
cout << f3(1, 1) << endl;
//包装静态成员函数
//成员函数要指定类域并且前⾯加&才能获取地址
function<int(int, int)> f4 = &Plus::plusi;
cout << f4(1, 1) << endl;
//包装普通成员函数
//普通成员函数还有⼀个隐含的this指针参数,所以绑定时传对象或者对象的指针过去都可以
function<double(Plus*, double, double)> f5 = &Plus::plusd;
Plus pd;
cout << f5(&pd, 1.1, 1.1) << endl;
function<double(Plus, double, double)> f6 = &Plus::plusd;
cout << f6(pd, 1.1, 1.1) << endl;
cout << f6(pd, 1.1, 1.1) << endl;
function<double(Plus&&, double, double)> f7 = &Plus::plusd;
cout << f7(move(pd), 1.1, 1.1) << endl;
cout << f7(Plus(), 1.1, 1.1) << endl;
return 0;
}
cpp
#include <iostream>
#include <functional>
using namespace std;
//普通函数
int add(int a, int b) { return a + b; }
int main() {
// 包装器:统一包装所有返回int、参数两个int的可调用对象
function<int(int, int)> func;
// 包装普通函数
func = add;
cout << func(1,2) << endl; //调用:3
// 包装Lambda表达式
func = [](int a, int b){return a*b;};
cout << func(3,4) << endl; //调用:12
return 0;
}
8.2用function来解决逆波兰表达式
cpp
//之前传统⽅式的实现
class Solution {
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;
default:
break;
}
}
else
{
// 运算数入栈
st.push(stoi(str));
}
}
return st.top();
}
};
//使⽤map映射string和function的⽅式实现
//这种⽅式的最⼤优势之⼀是⽅便扩展,假设还有其他运算,我们增加map中的映射即可
class Solution {
public:
int evalRPN(vector<string>& tokens) {
stack<int> st;
// function作为map的映射可调⽤对象的类型
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();
}
};
8.3bind
①bind 是⼀个函数模板,它也是⼀个可调⽤对象的包装器,可以把他看做⼀个函数适配器,对接收的fn可调⽤对象进⾏处理后返回⼀个可调⽤对象.bind可以⽤来调整参数个数和参数顺序.bind也在<functional>这个头⽂件中.
②调⽤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的⼀个命名空间中.
cpp
#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()
{
auto sub1 = bind(Sub, _1, _2);
cout << sub1(10, 5) << endl;
//bind本质返回的⼀个仿函数对象
//调整参数顺序(不常⽤)
//_1代表第⼀个实参
//_2代表第⼆个实参
//...
auto sub2 = bind(Sub, _2, _1);
cout << sub2(10, 5) << endl;
//调整参数个数(常⽤)
auto sub3 = bind(Sub, 100, _1);
cout << sub3(5) << endl;
auto sub4 = bind(Sub, _1, 100);
cout << sub4(5) << endl;
//分别绑死第123个参数
auto sub5 = bind(SubX, 100, _1, _2);
cout << sub5(5, 1) << endl;
auto sub6 = bind(SubX, _1, 100, _2);
cout << sub6(5, 1) << endl;
auto sub7 = bind(SubX, _1, _2, 100);
cout << sub7(5, 1) << endl;
//成员函数对象进⾏绑死,就不需要每次都传递了
function<double(Plus&&, double, double)> f6 = &Plus::plusd;
Plus pd;
cout << f6(move(pd), 1.1, 1.1) << endl;
cout << 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) << 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;
}

敬请期待下一篇文章内容:C++异常相关内容以及类型转换介绍!
每日心灵鸡汤:沉淀自己是最好的升华!
三毛曾说过:"给自己时间,不要焦急,一步一步来,一日一日过,请相信生命的韧性是惊人的,跟自己的心去合作,不要放弃对自己的爱护."当你的能力还驾驭不了你的目标时,你就应该沉下心来历练,祛除杂念,不好高骛远,也不轻易放弃.信手拈来的从容都是厚积薄发的沉淀,找到一个准确的定位,认真打磨自己,慢慢就能变得波澜不惊,在喧闹中宁静致远.但愿你心安,向内探索不停,做有心的积蓄者,沉淀自己,升华自己!












