C++ - C++11拓展
文章目录
- 列表初始化
- C++98传统的{ }
- C++11中的 { }
- C++11中的std::initializer_list
- 右值引用和移动语义
- 左值和右值
- 左值引用和右值引用
- 引用延长生命周期
- 左值和右值的参数匹配
- 右值引用和移动语言的使用场景
- 左值引用的使用场景
- 移动构造和移动赋值
- 右值引用和移动语义解决传值返回问题
- 场景一:直接初始化
- 场景二:先声明后赋值
- 类型分类
- 引用折叠
- 完美转发
- 可变参数模板
- 基本语法和原理
- 包扩展
- empalce系列接口
- 新的类功能
- 默认的移动构造和移动赋值
- 成员变量声明缺省值
- defult 和 delete
- lambda
- lambda表达式语法
- 捕捉列表
- lambda的应用
- 包装器
- funciton
- 基本用法(函数,Lambda、函数对象)
- 带参数和带返回值的函数
- 作为回调函数(事件处理)
- bind
- 智能指针
- 智能指针的使用场景分析
- RALL和智能指针的设计思路
- C++标准库智能指针的使用
- 部分智能指针简单的模拟实现
- shared_ptr和weak_ptr
- shraed_ptr循环引用问题
- weak_ptr
列表初始化
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;
}
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};// ⾃定义类型⽀持// 这⾥本质是⽤{ 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;
}
C++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内部有两个指针分别指向数组的开始和结束。 - 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之后我们之前学习的引⽤就叫做左值引⽤。⽆论左值引⽤还是右值引⽤,都是给对象取别名。
左值和右值
- 左值是⼀个表⽰数据的表达式(如变量名或解引⽤的指针),⼀般是有持久状态,存储在内存中,我们可以获取它的地址,左值可以出现赋值符号的左边,也可以出现在赋值符号右边。定义时const修饰符后的左值,不能给他赋值,但是可以取它的地址。
- 右值也是⼀个表⽰数据的表达式,要么是字⾯值常量、要么是表达式求值过程中创建的临时对象等,右值可以出现在赋值符号的右边,但是不能出现出现在赋值符号的左边,右值不能取地址。
- 左值和右值的核⼼区别就是能否取地址。
#include <iostream>
#include <cmath>
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;
}

左值引用和右值引用
Type& r1 = x; Type&& rr1 = y;第⼀个语句就是左值引⽤,左值引⽤就是给左值取别名,第⼆个就是右值引⽤,同样的道理,右值引⽤就是给右值取别名。- 左值引⽤不能直接引⽤右值,但是const左值引⽤可以引⽤右值
- 右值引⽤不能直接引⽤左值,但是右值引⽤可以引⽤
move(左值),move会将左值转换为右值,move是库⾥⾯的⼀个函数模板,本质内部是进⾏强制类型转换 - 变量表达式都是左值属性,也就意味着⼀个右值被右值引⽤绑定后,右值引⽤变量变量表达式的属性是左值
#include <iostream>#include <iostream>
#include <utility> //move
#include <cmath>
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;
}

引用延长生命周期
- 右值引用可用于为临时对象延长生命周期,如果该对象本身是可修改的,那么该右值引用对象可以被修改。
const的左值引用也能延长临时对象生存期但对象无法被修改。
#include <iostream>
#include <string>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;
}

左值和右值的参数匹配
- C++98中,我们实现⼀个const左值引⽤作为参数的函数,那么实参传递左值和右值都可以匹配
- C++11以后,分别重载
左值引⽤、const左值引⽤、右值引⽤作为形参的f函数,那么实参是左值会匹配f(左值引⽤),实参是const左值会匹配f(const 左值引⽤),实参是右值会匹配f(右值引⽤)。
#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;
}

右值引用和移动语言的使用场景
左值引用的使用场景
左值引⽤主要使⽤场景是在函数中左值引⽤传参和左值引⽤传返回值时减少拷⻉,同时还可以修改实参和修改返回对象的价值。左值引⽤已经解决⼤多数场景的拷⻉效率问题,但是有些场景不能使⽤传左值引⽤返回,如addStrings和generate函数,C++98中的解决⽅案只能是被迫使⽤输出型参数解决。那么C++11以后这⾥可以使⽤右值引⽤做返回值解决吗?显然是不可能的,因为这⾥的本质是返回对象是⼀个局部对象,函数结束这个对象就析构销毁了,右值引⽤返回也⽆法改变对象已经析构销毁的事实。
#include <iostream>
#include <vector>
#include <string>
#include <algorithm>
using namespace std;
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;}
};
移动构造和移动赋值
- 移动构造函数是⼀种构造函数,类似拷⻉构造函数,移动构造函数要求第⼀个参数是该类类型的引⽤,但是不同的是要求这个参数是右值引⽤,如果还有其他参数,额外的参数必须有缺省值。
- 移动赋值是⼀个赋值运算符的重载,他跟拷⻉赋值构成函数重载,类似拷⻉赋值函数,移动赋值函数要求第⼀个参数是该类类型的引⽤,但是不同的是要求这个参数是右值引⽤。
- 对于像string/vector这样的深拷⻉的类或者包含深拷⻉的成员变量的类,移动构造和移动赋值才有意义,因为移动构造和移动赋值的第⼀个参数都是右值引⽤的类型,他的本质是要“窃取”引⽤的右值对象的资源,⽽不是像拷⻉构造和拷⻉赋值那样去拷⻉资源,从提⾼效率。下⾯的my::string样例实现了移动构造和移动赋值,我们需要结合场景理解
#include <iostream>
#include <assert.h>
#include <string.h>
#include <algorithm>
using namespace std;
namespace my
{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()
{my::string s1("xxxxx");// 拷⻉构造my::string s2 = s1;// 构造+移动构造,优化后直接构造my::string s3 = my::string("yyyyy");// 移动构造my::string s4 = move(s1);cout << "******************************" << endl;return 0;
}

右值引用和移动语义解决传值返回问题
#include <iostream>
#include <algorithm>
#include <cassert>
#include <cstring>using namespace std;
namespace my
{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;};my::string addStrings(my::string num1, my::string num2){my::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()
{my::string ret = my::addStrings("11111", "2222");cout << ret.c_str() << endl;return 0;
}

执行流程:
addStrings函数参数构造两个临时stringaddStrings内部创建局部string对象str- 返回时可能触发RVO(返回值优化),直接将
str构造到ret中- 如果RVO生效:无额外拷贝/移动
- 如果RVO不生效:调用移动构造
- 输出结果
场景二:先声明后赋值
// 场景2:先声明后赋值
int main()
{my::string ret;ret = my::addStrings("11111", "2222");cout << ret.c_str() << endl;return 0;
}

执行流程:
- 先默认构造
ret(空字符串) addStrings函数参数构造两个临时stringaddStrings内部创建局部string对象str- 返回时必定调用移动赋值运算符(因为
ret已存在) - 输出结果
类型分类
- 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),泛左值包含将亡值和左值。

引用折叠
- C++中不能直接定义引⽤的引⽤如
int& && r = i;,这样写会报错,通过模板或typedef中的类型操作可以构成引用的引用 - 通过模板或 typedef 中的类型操作可以构成引⽤的引⽤时,这时C++11给出了⼀个引⽤折叠的规则:右值引⽤的右值引⽤折叠成右值引⽤,所有其他组合均折叠成左值引⽤。
// 由于引⽤折叠限定,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;
}
像f2这样的函数模板中,T&& x参数看起来是右值引⽤参数,但是由于引⽤折叠的规则,他传递左值时就是左值引⽤,传递右值时就是右值引⽤,有些地⽅也把这种函数模板的参数叫做万能引⽤
#include <iostream>
#include <utility> // std::move
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);// std::move(b)右值,推导出T为const int,模板实例化为void Function(const int&&t)// 所以Function内部会编译报错,x不能++Function(std::move(b)); // const 右值return 0;
}
Function(T&& t)函数模板程序中,假设实参是int右值,模板参数T的推导int,实参是int左值,模板参数T的推导int&,再结合引⽤折叠规则,就实现了实参是左值,实例化出左值引⽤版本形参的Function,实参是右值,实例化出右值引⽤版本形参的Function。
完美转发
- Function(T&& t)函数模板程序中,传左值实例化以后是左值引⽤的Function函数,传右值实例化以后是右值引⽤的Function函数。
- 但是变量表达式都是左值属性,也就意味着⼀个右值被右值引⽤绑定后,右值引⽤变量表达式的属性是左值,也就是说Function函数中t的属性是左值,那么我们把t传递给下⼀层函数Fun,那么匹配的都是左值引⽤版本的Fun函数。这⾥我们想要保持t对象的属性,就需要使⽤完美转发实现。
- 完美转发forward本质是⼀个函数模板,他主要还是通过引⽤折叠的⽅式实现
#include <iostream>
#include <utility> // std::move
using namespace std;template <class _Ty>
_Ty &&forward(remove_reference_t<_Ty> &_Arg) noexcept
{ // forward an lvalue as either an lvalue or an rvaluereturn 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;
}

可变参数模板
基本语法和原理
-
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…运算符去计算参数包中参数的个数。
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);
// ...

包扩展
- 对于⼀个参数包,我们除了能计算他的参数个数,我们能做的唯⼀的事情就是扩展它,当扩展⼀个包时,我们还要提供⽤于每个扩展元素的模式,扩展⼀个包就是将它分解为构成的元素,对每个元素应⽤模式,获得扩展后的列表。我们通过在模式的右边放⼀个省略号(…)来触发扩展操作。
- C++还⽀持更复杂的包扩展,直接将参数包依次展开依次作为实参给⼀个函数去处理。
#include <iostream>
#include <string>
using namespace std;
// 可变模板参数
// 参数类型可变
// 参数个数可变
// 打印参数包内容
// 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;
//}
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;
}

#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必须返回或者到的对象,这样才能组成参数包给ArgumentsArguments(GetArg(args)...);
}
// 本质可以理解为编译器编译时,包的扩展模式
// 将上⾯的函数模板扩展实例化为下⾯的函数
// void Print(int x, string y, double z)
//{
// Arguments(GetArg(x), GetArg(y), GetArg(z));
//}int main()
{Print(1, string("xxxxx"), 2.2);cout << endl;return 0;
}

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对象的参数
- emplace_back总体⽽⾔是更⾼效,推荐以后使⽤emplace系列替代insert和push系列
- 传递参数包过程中,如果是
Args&&... args的参数包,要⽤完美转发参数包,⽅式如下std::forward<Args>(args)... ,否则编译时包扩展后右值引⽤变量表达式就变成了左值。
#include<iostream>
#include<string>
#include<utility>
#include<list>
using namespace std;
// emplace_back总体⽽⾔是更⾼效,推荐以后使⽤emplace系列替代insert和push系列
int main()
{list<std::string> lt;// 传左值,跟push_back⼀样,⾛拷⻉构造std::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<std::string, int>> lt1;// 跟push_back⼀样// 构造pair + 拷⻉/移动构造pair到list的节点中data上pair<std::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;
}
新的类功能
默认的移动构造和移动赋值
- 原来C++类中,有6个默认成员函数:构造函数/析构函数/拷⻉构造函数/拷⻉赋值重载/取地址重载/const 取地址重载。C++11 新增了两个默认成员函数,移动构造函数和移动赋值运算符重载
- 如果没有⾃⼰实现移动构造函数,且没有实现析构函数 、拷⻉构造、拷⻉赋值重载中的任意⼀个。那么编译器会⾃动⽣成⼀个默认移动构造。默认⽣成的移动构造函数,对于内置类型成员会执⾏逐成员按字节拷⻉,⾃定义类型成员,则需要看这个成员是否实现移动构造,如果实现了就调⽤移动构造,没有实现就调⽤拷⻉构造
- 如果没有⾃⼰实现移动赋值重载函数,且没有实现析构函数 、拷⻉构造、拷⻉赋值重载中的任意⼀个,那么编译器会⾃动⽣成⼀个默认移动赋值。默认⽣成的移动构造函数,对于内置类型成员会执⾏逐成员按字节拷⻉,⾃定义类型成员,则需要看这个成员是否实现移动赋值,如果实现了就调⽤移动赋值,没有实现就调⽤拷⻉赋值。(默认移动赋值跟上⾯移动构造完全类似)
- 如果实现了移动构造或者移动赋值,编译器不会⾃动提供拷⻉构造和拷⻉赋值。
成员变量声明缺省值
成员变量声明时给缺省值是给初始化列表⽤的,如果没有显⽰在初始化列表初始化,就会在初始化列表⽤这个缺省值初始化。
defult 和 delete
- 假设要使⽤某个默认的函数,但是因为⼀些原因这个函数没有默认⽣成。⽐如:我们提供了拷⻉构造,就不会⽣成移动构造了,那么可以使⽤default关键字显⽰指定移动构造⽣成。
- 如果想要限制某些默认函数的⽣成,在C++98中,是该函数设置成private,并且只声明不定义,这样只要其他⼈想要调⽤就会报错。在 C++11中更简单,只需在该函数声明加上=delete即可,该语法指⽰编译器不⽣成对应函数的默认版本,称=delete修饰的函数为删除函数。
class Person
{public:Person(const char* name = "", int age = 0):_name(name), _age(age){}Person(const Person& p):_name(p._name)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:bit::string _name;int _age;};int main(){Person s1;Person s2 = s1;Person s3 = std::move(s1);return 0;}
lambda
lambda表达式语法
-
lambda 表达式本质是⼀个匿名函数对象,跟普通函数不同的是他可以定义在函数内部lambda 表达式语法使⽤层⽽⾔没有类型,所以⼀般是⽤auto或者模板参数定义的对象去接收 lambda 对象。
-
lambda表达式的格式:
[capture-list] (parameters)-> return type {function boby } -
[capture-list]: 捕捉列表,该列表总是出现在 lambda 函数的开始位置,编译器根据[ ]来判断接下来的代码是否为 lambda 函数,捕捉列表能够捕捉上下⽂中的变量供 lambda 函数使⽤,捕捉列表可以传值和传引⽤捕捉。捕捉列表为空也不能省略。 -
(parameters):参数列表,与普通函数的参数列表功能类似,如果不需要参数传递,则可以连同()⼀起省略 -
->return type:返回值类型,⽤追踪返回类型形式声明函数的返回值类型,没有返回值时此部分可省略。⼀般返回值类型明确情况下,也可省略,由编译器对返回类型进⾏推导 -
{function boby}:函数体,函数体内的实现跟普通函数完全类似,在该函数体内,除了可以使⽤其参数外,还可以使⽤所有捕获到的变量,函数体为空也不能省略
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;
}

捕捉列表
- 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;
// 捕捉列表必须为空,因为全局变量不⽤捕捉就可以⽤,没有可被捕捉的变量
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;// 混合捕捉1auto func4 = [&, a, b]{// a++;// b++;c++;d++;return a + b + c + d;};func4();cout << a << " " << b << " " << c << " " << d << endl;// 混合捕捉1auto 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;
}

lambda的应用
- 在学习 lambda 表达式之前,我们的使⽤的可调⽤对象只有函数指针和仿函数对象,函数指针的类型定义起来⽐较⿇烦,仿函数要定义⼀个类,相对会⽐较⿇烦。使⽤ lambda 去定义可调⽤对象,既简单⼜⽅便。
- lambda 在很多其他地⽅⽤起来也很好⽤。⽐如线程中定义线程的执⾏函数逻辑,智能指针中定制删除器等。
#include <iostream>
#include <vector>
#include <string>
#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;
}
包装器
funciton
template <class T>
class function; // undefinedtemplate <class Ret, class... Args>
class function<Ret(Args...)>;
std:function是一个类模板,也是一个包装器。std::function的实例对象可以包装存储其他的可以调⽤对象,包括函数指针、仿函数、lambda 、 bind 表达式等,存储的可调⽤对象被称为std::function的目标。若std::function不含⽬标,则称它为空。调⽤空std::function的目标导致抛出std::bad_function_call异常- 函数指针、仿函数、 lambda 等可调⽤对象的类型各不相同,std::function 的优势就是统⼀类型,对他们都可以进⾏包装,这样在很多地⽅就⽅便声明可调⽤对象的类型。
基本用法(函数,Lambda、函数对象)
#include <iostream>
#include <functional> // std::function// 普通函数
void print_hello() { std::cout << "Hello\n"; }// 函数对象
struct PrintGoodbye {void operator()() { std::cout << "Goodbye\n"; }
};int main() {// 包装器统一存储不同类型的可调用对象std::function<void()> func_wrapper;// 绑定普通函数func_wrapper = print_hello;func_wrapper(); // 输出: Hello// 绑定 Lambdafunc_wrapper = []() { std::cout << "Lambda\n"; };func_wrapper(); // 输出: Lambda// 绑定函数对象func_wrapper = PrintGoodbye();func_wrapper(); // 输出: Goodbyereturn 0;
}

带参数和带返回值的函数
#include <iostream>
#include <functional>// 普通函数
int add(int a, int b) { return a + b; }int main() {// 包装器:接收两个 int,返回 intstd::function<int(int, int)> func;// 绑定普通函数func = add;std::cout << "10 + 20 = " << func(10, 20) << "\n"; // 输出: 30// 绑定 Lambdafunc = [](int x, int y) { return x * y; };std::cout << "3 * 4 = " << func(3, 4) << "\n"; // 输出: 12return 0;
}

作为回调函数(事件处理)
#include <iostream>
#include <functional>
#include <vector>// 事件处理器类
class EventHandler {
public:// 注册回调函数void setCallback(std::function<void(int)> cb) {callback = cb;}// 触发事件void triggerEvent(int value) {if (callback) {callback(value);}}private:std::function<void(int)> callback;
};int main() {EventHandler handler;// 注册 Lambda 作为回调handler.setCallback([](int val) {std::cout << "Event triggered with value: " << val << "\n";});// 触发事件handler.triggerEvent(42); // 输出: Event triggered with value: 42return 0;
}

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 也在
<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的一个命名空间中。
#include <iostream>
using namespace std;#include <functional>
using placeholders::_1;
using placeholders::_2;
using placeholders::_3;
int Sub(int a, int b)
{return (a - b) * 10;
}
int SubX(int a, int b, int c)
{return (a - b - c) * 10;
}
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;// 计算复利的lambdaauto 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;
}

智能指针
智能指针的使用场景分析
#include <iostream>
#include <exception>
using namespace std;
double Divide(int a, int b)
{// 当b == 0时抛出异常if (b == 0){throw "Divide by zero condition!";}else{return (double)a / (double)b;}
}
void Func()
{// 这⾥可以看到如果发⽣除0错误抛出异常,另外下⾯的array和array2没有得到释放。// 所以这⾥捕获异常后并不处理异常,异常还是交给外⾯处理,这⾥捕获了再重新抛出去。// 但是如果array2new的时候抛异常呢,就还需要套⼀层捕获释放逻辑int *array1 = new int[10];int *array2 = new int[10]; // 抛异常try{int len, time;cin >> len >> time;cout << Divide(len, time) << endl;}catch (...){cout << "delete []" << array1 << endl;cout << "delete []" << array2 << endl;delete[] array1;delete[] array2;throw; // 异常重新抛出,捕获到什么抛出什么}// ...cout << "delete []" << array1 << endl;delete[] array1;cout << "delete []" << array2 << endl;delete[] array2;
}
int main()
{try{Func();}catch (const char *errmsg){cout << errmsg << endl;}catch (const exception &e){cout << e.what() << endl;}catch (...){cout << "未知异常" << endl;}return 0;
}

可以看到,new了以后,也delete了,但是因为抛异常导致后⾯的delete没有得到执⾏,所以就内存泄漏了,所以需要new以后捕获异常,捕获到异常后delete内存,再把异常抛出,但是因为new本⾝也可能抛异常,连续的两个new和下⾯的Divide都可能会抛异常,处理起来很⿇烦。智能指针放到这样的场景⾥⾯就让问题简单多了。
RALL和智能指针的设计思路
- RAII是Resource Acquisition Is Initialization的缩写,他是⼀种管理资源的类的设计思想,本质是⼀种利⽤对象⽣命周期来管理获取到的动态资源,避免资源泄漏,这⾥的资源可以是内存、⽂件指针、⽹络连接、互斥锁等等。RAII在获取资源时把资源委托给⼀个对象,接着控制对资源的访问,资源在对象的⽣命周期内始终保持有效,最后在对象析构的时候释放资源,这样保障了资源的正常释放,避免资源泄漏问题。
- 智能指针类除了满⾜RAII的设计思路,还要⽅便资源的访问,所以智能指针类还会想迭代器类⼀样,重载 operator*/operator->/operator[] 等运算符,⽅便访问资源。
#include <iostream>
#include <exception>
using namespace std;
// 简单的智能指针类,管理new出来的数组资源
template <class T>
class SmartPtr
{
public:// RAIISmartPtr(T *ptr): _ptr(ptr){}~SmartPtr(){cout << "delete[] " << _ptr << endl;delete[] _ptr;}// 重载运算符,模拟指针的⾏为,⽅便访问资源T &operator*(){return *_ptr;}T *operator->(){return _ptr;}T &operator[](size_t i){return _ptr[i];}private:T *_ptr;
};
double Divide(int a, int b)
{// 当b == 0时抛出异常if (b == 0){throw "Divide by zero condition!";}else{return (double)a / (double)b;}
}
void Func()
{// 这⾥使⽤RAII的智能指针类管理new出来的数组以后,程序简单多了SmartPtr<int> sp1 = new int[10];SmartPtr<int> sp2 = new int[10];for (size_t i = 0; i < 10; i++){sp1[i] = sp2[i] = i;}int len, time;cin >> len >> time;cout << Divide(len, time) << endl;
}
int main()
{try{Func();}catch (const char *errmsg){cout << errmsg << endl;}catch (const exception &e){cout << e.what() << endl;}catch (...){cout << "未知异常" << endl;}return 0;
}

C++标准库智能指针的使用
-
C++标准库中的智能指针都在
<memory>这个头⽂件下⾯,我们包含<memory>就可以是使⽤了,智能指针有好⼏种,除了weak_ptr他们都符合RAII和像指针⼀样访问的⾏为,原理上⽽⾔主要是解决智能指针拷⻉时的思路不同。 -
auto_ptr是C++98时设计出来的智能指针,他的特点是拷⻉时把被拷⻉对象的资源的管理权转移给拷⻉对象,这是⼀个⾮常糟糕的设计,因为他会导致被拷⻉对象悬空,访问报错的问题,所以建议不要使⽤auto_ptr。
-
unique_ptr是C++11设计出来的智能指针,名字翻译出来是唯⼀指针,特点是不⽀持拷⻉,只⽀持移动。
-
shared_ptr是C++11设计出来的智能指针,他的名字翻译出来是共享指针,他的特点是⽀持拷⻉,也⽀持移动。
-
weak_ptr是C++11设计出来的智能指针,名字翻译出来是弱指针,完全不同于上⾯的智能指针,他不⽀持RAII,也就意味着不能⽤它直接管理资源,weak_ptr的产⽣本质是要解决shared_ptr的⼀个循环引⽤导致内存泄漏的问题。
-
智能指针析构时默认是进⾏delete释放资源,这也就意味着如果不是new出来的资源,交给智能指针管理,析构时就会崩溃。智能指针⽀持在构造时给⼀个删除器,所谓删除器本质就是⼀个可调⽤对象,这个可调⽤对象中实现你想要的释放资源的⽅式,当构造智能指针时,给了定制的删除器,在智能指针析构时就会调⽤删除器去释放资源。
//unique_ptr和shared_ptr都特化了⼀份[]的版本 unique_ptr<Date[]> up1(newDate[5]); shared_ptr<Date[]> sp1(new Date[5]); //这样就可以管理new[]的资源 -
shared_ptr 除了⽀持⽤指向资源的指针构造,还⽀持 make_shared ⽤初始化资源对象的值直接构造。
-
shared_ptr 和 unique_ptr 都⽀持了
operator bool的类型转换,如果智能指针对象是⼀个空对象没有管理资源,则返回false,否则返回true,意味着我们可以直接把智能指针对象给if判断是否为空。 -
shared_ptr 和 unique_ptr 都得构造函数都使⽤
explicit修饰,防⽌普通指针隐式类型转换成智能指针对象。
#include <iostream>
#include <memory>
using namespace std;struct Date
{int _year;int _month;int _day;Date(int year = 1, int month = 1, int day = 1):_year(year),_month(month),_day(day){}~Date(){cout << "~Date()" << endl;}
};
int main()
{auto_ptr<Date> ap1(new Date);// 拷⻉时,管理权限转移,被拷⻉对象ap1悬空auto_ptr<Date> ap2(ap1);// 空指针访问,ap1对象已经悬空//ap1->_year++;unique_ptr<Date> up1(new Date);// 不⽀持拷⻉//unique_ptr<Date> up2(up1);// ⽀持移动,但是移动后up1也悬空,所以使⽤移动要谨慎unique_ptr<Date> up3(move(up1));shared_ptr<Date> sp1(new Date);// ⽀持拷⻉shared_ptr<Date> sp2(sp1);shared_ptr<Date> sp3(sp2);cout << sp1.use_count() << endl;sp1->_year++;cout << sp1->_year << endl;cout << sp2->_year << endl;cout << sp3->_year << endl;// ⽀持移动,但是移动后sp1也悬空,所以使⽤移动要谨慎shared_ptr<Date> sp4(move(sp1));return 0;
}

#include <iostream>
#include <memory>
#include <cstdio>
using namespace std;struct Date
{int _year;int _month;int _day;Date(int year = 1, int month = 1, int day = 1):_year(year),_month(month),_day(day){}~Date(){cout << "~Date()" << endl;}
};template<class T>void DeleteArrayFunc(T* ptr)
{delete[] ptr;
}
template<class T>class DeleteArray{public:void operator()(T* ptr){delete[] ptr;}};
class Fclose
{public:void operator()(FILE* ptr){cout << "fclose:" << ptr << endl;if (ptr){fclose(ptr);}}
};
int main()
{// 这样实现程序会崩溃// unique_ptr<Date> up1(new Date[10]);// shared_ptr<Date> sp1(new Date[10]);// 解决⽅案1// 因为new[]经常使⽤,所以unique_ptr和shared_ptr// 实现了⼀个特化版本,这个特化版本析构时⽤的delete[]unique_ptr<Date[]> up1(new Date[5]);shared_ptr<Date[]> sp1(new Date[5]);// 解决⽅案2// 仿函数对象做删除器//unique_ptr<Date, DeleteArray<Date>> up2(new Date[5], DeleteArray<Date>());// unique_ptr和shared_ptr⽀持删除器的⽅式有所不同// unique_ptr是在类模板参数⽀持的,shared_ptr是构造函数参数⽀持的// 这⾥没有使⽤相同的⽅式还是挺坑的// 使⽤仿函数unique_ptr可以不在构造函数传递,因为仿函数类型构造的对象直接就可以调⽤// 但是下⾯的函数指针和lambda的类型不可以unique_ptr<Date, DeleteArray<Date>> up2(new Date[5]);shared_ptr<Date> sp2(new Date[5], DeleteArray<Date>());// 函数指针做删除器unique_ptr<Date, void(*)(Date*)> up3(new Date[5], DeleteArrayFunc<Date>);shared_ptr<Date> sp3(new Date[5], DeleteArrayFunc<Date>);// lambda表达式做删除器auto delArrOBJ = [](Date* ptr) {delete[] ptr; };unique_ptr<Date, decltype(delArrOBJ)> up4(new Date[5], delArrOBJ);shared_ptr<Date> sp4(new Date[5], delArrOBJ);// 实现其他资源管理的删除器shared_ptr<FILE> sp5(fopen("Test.cpp", "r"), Fclose());shared_ptr<FILE> sp6(fopen("Test.cpp", "r"), [](FILE* ptr) {cout << "fclose:" << ptr << endl;if (ptr){fclose(ptr);}});return 0;
}

int main()
{shared_ptr<Date> sp1(new Date(2024, 9, 11));shared_ptr<Date> sp2 = make_shared<Date>(2024, 9, 11);auto sp3 = make_shared<Date>(2024, 9, 11);shared_ptr<Date> sp4;// if (sp1.operator bool())if (sp1)cout << "sp1 is not nullptr" << endl;if (!sp4)cout << "sp1 is nullptr" << endl;// 报错shared_ptr<Date> sp5 = new Date(2024, 9, 11);unique_ptr<Date> sp6 = new Date(2024, 9, 11);return 0;
}

部分智能指针简单的模拟实现
#include <iostream>
#include <functional>
using namespace std;namespace my
{template <class T>class auto_ptr{public:auto_ptr(T *ptr): _ptr(ptr){}auto_ptr(auto_ptr<T> &sp): _ptr(sp._ptr){// 管理权转移sp._ptr = nullptr;}auto_ptr<T> &operator=(auto_ptr<T> &ap){// 检测是否为自己赋值if (this != &ap){// 释放当前对象中的资源if (_ptr){delete _ptr;}// 转移ap中资源达到当前对象中_ptr = ap._ptr;ap._ptr = nullptr;}}~auto_ptr(){if (_ptr){cout << "delete:" << _ptr << endl;delete _ptr;}}T &operator*(){return *_ptr;}T *operator->(){return _ptr;}private:T *_ptr;};template <class T>class unique_ptr{public:explicit unique_ptr(T *ptr): _ptr(ptr){}~unique_ptr(){if (_ptr){cout << "delete:" << _ptr << endl;delete _ptr;}}T &operator*(){return *_ptr;}T *operator->(){return _ptr;}unique_ptr(const unique_ptr<T> &sp) = delete;unique_ptr<T> &operator=(const unique_ptr<T> &sp) = delete;unique_ptr(unique_ptr<T> &&sp): _ptr(sp._ptr){sp._ptr = nullptr;}unique_ptr<T> &operator=(unique_ptr<T> &&sp){delete _ptr;_ptr = sp._ptr;sp._ptr = nullptr;}private:T *_ptr;};template <class T>class shared_ptr{public:// explicit禁止隐式类型转换explicit shared_ptr(T *ptr = nullptr): _ptr(ptr), _pcount(new int(1)){}// 删除器模板template <class D>shared_ptr(T *ptr, D del): _ptr(ptr), _pcount(new int(1)), _del(del){}// 拷贝构造函数shared_ptr(const shared_ptr<T> &sp): _ptr(sp._ptr), _pcount(sp._pcount), _del(sp._del){++(*_pcount); // 引用计数加1}void release(){// 先减减引用计数,如果引用计数变为0if (--(*_pcount) == 0){// 最后⼀个管理的对象,释放资源_del(_ptr);delete _pcount;_ptr = nullptr;_pcount = nullptr;}}shared_ptr<T> &operator=(const shared_ptr<T> &sp){if (_ptr != sp._ptr){release();_ptr = sp._ptr;_pcount = sp._pcount;++(*_pcount);_del = sp._del;}return *this;}~shared_ptr(){release();}T *get() const{return _ptr;}int use_count() const{return *_pcount;}T &operator*(){return *_ptr;}T *operator->(){return _ptr;}private:T *_ptr;int *_pcount; // 引用计数std::function<void(T *)> _del = [](T *ptr){ delete ptr; }; // 删除器};template <class T>class weak_ptr{public:weak_ptr(){}weak_ptr(const shared_ptr<T> &sp): _ptr(sp.get())//弱绑定{}//弱绑定weak_ptr<T> &operator=(const shared_ptr<T> &sp){_ptr = sp.get();return *this;}private:T *_ptr = nullptr;};}struct Date
{int _year;int _month;int _day;Date(int year = 1, int month = 1, int day = 1): _year(year), _month(month), _day(day){}~Date(){cout << "~Date()" << endl;}
};int main()
{my::auto_ptr<Date> ap1(new Date);// 拷⻉时,管理权限转移,被拷⻉对象ap1悬空my::auto_ptr<Date> ap2(ap1);// 空指针访问,ap1对象已经悬空// ap1->_year++;my::unique_ptr<Date> up1(new Date);// 不⽀持拷⻉// unique_ptr<Date> up2(up1);// ⽀持移动,但是移动后up1也悬空,所以使⽤移动要谨慎my::unique_ptr<Date> up3(move(up1));my::shared_ptr<Date> sp1(new Date);// ⽀持拷⻉my::shared_ptr<Date> sp2(sp1);my::shared_ptr<Date> sp3(sp2);cout << sp1.use_count() << endl;sp1->_year++;cout << sp1->_year << endl;cout << sp2->_year << endl;cout << sp3->_year << endl;return 0;
}

重点是看shared_ptr,尤其是引⽤计数的设计,主要这⾥⼀份资源就需要⼀个引⽤计数,所以引⽤计数才⽤静态成员的⽅式是⽆法实现的,要使⽤堆上动态开辟的⽅式,构造智能指针对象时来⼀份资源,就要new⼀个引⽤计数出来。多个shared_ptr指向资源时就++引⽤计数,shared_ptr对象析构时就–引⽤计数,引⽤计数减到0时代表当前析构的shared_ptr是最后⼀个管理资源的对象,则析构资源。
shared_ptr和weak_ptr
shraed_ptr循环引用问题
shared_ptr⼤多数情况下管理资源⾮常合适,⽀持RAII,也⽀持拷⻉。但是在循环引⽤的场景下会导致资源没得到释放内存泄漏。
如下图,n1和n2析构后,管理两个节点的引⽤计数为1

将两个节点链接

将n1和n2析构后

由于引用计数没有为降为零,所以申请的空间没有被析构,造成内存泄漏
weak_ptr
- weak_ptr不⽀持RAII,也不⽀持访问资源只⽀持绑定到shared_ptr,绑定到shared_ptr时,不增加shared_ptr的引⽤计数,那么就可以解决上述的循环引⽤问题。
- weak_ptr也没有重载operator*和operator->等,因为weak_ptr不参与资源管理,那么如果weak_ptr绑定的shared_ptr已经释放了资源,那么weak_ptr去访问资源就是很危险的。weak_ptr⽀持expired检查指向的资源是否过期,use_count也可获取shared_ptr的引⽤计数,weak_ptr想访问资源时,可以调⽤lock返回⼀个管理资源的shared_ptr,如果资源已经被释放,返回的shared_ptr是⼀个空对象,如果资源没有释放,则通过返回的shared_ptr访问资源是安全的。
int main()
{std::shared_ptr<string> sp1(new string("111111"));std::shared_ptr<string> sp2(sp1);std::weak_ptr<string> wp = sp1;cout << wp.expired() << endl;// 检查 wp 是否过期cout << wp.use_count() << endl;// 输出引用计数// sp1和sp2都指向了其他资源,则weak_ptr就过期了sp1 = make_shared<string>("222222");cout << wp.expired() << endl;cout << wp.use_count() << endl;sp2 = make_shared<string>("333333");cout << wp.expired() << endl;cout << wp.use_count() << endl;wp = sp1;//std::shared_ptr<string> sp3 = wp.lock();auto sp3 = wp.lock();// 从 wp 获取 shared_ptrcout << wp.expired() << endl;cout << wp.use_count() << endl;*sp3 += "###";cout << *sp1 << endl;return 0;
}

