【C++终极篇】C++11:编程新纪元的神秘力量揭秘_c++11的<>
杀马特主页:羑悻的小杀马特.-CSDN博客
------ ->欢迎阅读 欢迎阅读 欢迎阅读 欢迎阅读 <-------
目录
一·列表初始化的变化:
二·左右值即各自引用的概念:
2.1左右值:
2.2左右值引⽤:
2.3左值和右值的参数匹配 :
三·右值引用以及移动构造和移动赋值的作用:
3.1移动构造和移动赋值:
3.2:对于一些右值对象传参配合移动语义解决返回值问题分析:
移动构造有无分析:
①右值对象构造,只有拷⻉构造,没有移动构造的场景 :
②右值对象构造,有拷⻉构造,也有移动构造的场景:
移动赋值有无分析:
①右值对象赋值,只有拷⻉构造和拷⻉赋值,没有移动构造和移动赋值的场景:
②右值对象赋值,既有拷⻉构造和拷⻉赋值,也有移动构造和移动赋值的场景:
3.3右值引用对容器操作的提效:
四·类型分类:
五·引用折叠:
六·完美转发:
七·可变模版参数:
7.1介绍:
7.2包扩展:
7.3empalce系列接⼝:
八·类的一些新功能:
8.1默认的移动构造和移动赋值:
8.2声明时给缺省值:
8.3defult和delete:
①default:
②delete:
8.4final与override:
①final:
②override:
8.5STL中⼀些变化:
九·const限定符:
9.1顶层const和底层const:
9.2constexpr:
十·lambda:
10.1介绍:
10.2表达式用法及其部分介绍:
10.2.1组成介绍:
10.2.2捕捉列表:
10.2.3lambda原理:
十一·包装器:
11.1function:
11.2bind:
十二·智能指针:
12.1智能指针引入背景:
12.2智能指针设计前提:
12.3智能指针介绍:
12.3.2 weak_ptr:
12.3.3 unique_ptr:
12.4内存泄漏:
十三·处理类型:
13.1auto:
13.2decltype:
13.3typedef和using:
一·列表初始化的变化:
为我们所知在之前的c++98规定的⼀般数组和结构体可以⽤{}进⾏初始化。
但是到了c++11实现了可以用{}对容器进行一些初始化等,比如push/inset多参数构造的对象时,{}初始化会很⽅便,这是因为每个类型它都会有个initializer_list的一个构造,这样就方便了我们操作。
下面就拿我们实现的一个日期类来说:
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;}
展示一下上面叙述的{} 的一些操作:
Date d { 2024, 7, 25 };vector v; v.push_back(d1); v.push_back(Date(2025, 1, 1)); v.push_back({ 2025, 1, 1 })//{}支持的插入
下面我们简略简述一下这个“帮凶”: initializer_list:
它是std::initializer_list的类:这个类的本质是底层开⼀个数组,将数据拷⻉过来,内部有两个指针分别指向数组的开始和结束,然后通过迭代器等(自身也支持迭代器)完成相关容器初始化构造等。
下面我们来看一下容器增加的这个类的构造格式:
//STL中的容器都增加了⼀个initializer_list的构造 vector (initializer_list il, const allocator_type& alloc = allocator_type());list (initializer_list il, const allocator_type& alloc = allocator_type());map (initializer_list il,const key_compare& comp = key_compare(),const allocator_type& alloc = allocator_type());// ... template class vector { public: typedef T* iterator; vector(initializer_list l) { for (auto e : l) push_back(e) } private: iterator _start = nullptr; iterator _finish = nullptr; iterator _endofstorage = nullptr; };
从这里我们可以看出底层是两个指针(x86为四个字节);而地址非常接近变量i的,故同样是位于栈中的。
这里我们作为了解,其次会用{}的一些操作即可。
二·左右值即各自引用的概念:
2.1左右值:
C++11中新增了的右值引⽤语法特性,C++11之后我们之前学 习的引⽤就叫做左值引⽤。⽆论左值引⽤还是右值引⽤,都是给对象取别名。
这里我们都已了解了左值引用(就是我们之前常用的引用),所以这里就不过多介绍,那么我们区分左值还是右值的区别就是给它取地址,我们会得到结论:左值可以取地址无论是const修饰还是没有被修饰,而右值却无法取到它的地址。
左值是⼀个表⽰数据的表达式(如变量名或解引⽤的指针),⼀般是有持久状态,存储在内存中,我 们可以获取它的地址,左值可以出现赋值符号的左边,也可以出现在赋值符号右边。定义时const 修饰符后的左值,不能给他赋值,但是可以取它的地址。
右值也是⼀个表⽰数据的表达式,要么是字⾯值常量、要么是表达式求值过程中创建的临时对象 等,右值可以出现在赋值符号的右边,但是不能出现出现在赋值符号的左边,右值不能取地址。
这里我们总结一下:右值一般就是临时对象,匿名对象,常量,像这些不能取地址的等都是右值,其他都是左值,我们经常也以是否能取地址来判断。
2.2左右值引⽤:
先上一个简单的例子:
Type& r1 = x; Type&& rr1 = y; 第⼀个语句就是左值引⽤,左值引⽤就是给左值取别 名,第⼆个就是右值引⽤,同样的道理,右值引⽤就是给右值取别名。
其次就是:
①左值引⽤不能直接引⽤右值,但是const左值引⽤可以引⽤右值。
②右值引⽤不能直接引⽤左值,但是右值引⽤可以引⽤move(左值)(这里相当于给它转化成右值了,此时是一个将亡值)。
虽然这么说,但是还有会有一个误区,也就是左右值引用它们的属性是什么?
变量表达式都是左值属性,也就意味着⼀个右值被右值引⽤绑定后,右值引⽤变量变 量表达式的属性是左值。(底层都是指针)
下面看一下例子:
那么右值引用的作用是什么:右值引⽤可⽤于为临时对象延⻓⽣命周期,const的左值引⽤也能延⻓临时对象⽣存期,但这些对象⽆ 法被修改。
此时临时对象的生命周期就从一行往后延长了。
2.3左值和右值的参数匹配 :
这里就涉及到实参类型与形参类型匹配问题,也就是对于左值就是找左值引用,当然如果不存在也可以权限缩小即走const的,而对应右值那么走右值引用,如果右值引用不存在就会走const的左值引用。
三·右值引用以及移动构造和移动赋值的作用:
首先我们要先明白:左值引⽤主要使⽤场景是在函数中左值引⽤传参和左值引⽤传返回值时减少拷⻉,同时还可以修改实 参和修改返回对象的价值。
但是如果是对函数里面创建的临时对象做返回值,那么无论是最后左值引用返回还是右值引用返回,局部销毁的时候,空间也销毁了,因此这样也是无法解决的。
3.1移动构造和移动赋值:
①移动构造函数是⼀种构造函数,类似拷⻉构造函数,移动构造函数要求第⼀个参数是该类类型的引 ⽤,但是不同的是要求这个参数是右值引⽤,如果还有其他参数,额外的参数必须有缺省值。
② 移动赋值是⼀个赋值运算符的重载,他跟拷⻉赋值构成函数重载,类似拷⻉赋值函数,移动赋值函 数要求第⼀个参数是该类类型的引⽤,但是不同的是要求这个参数是右值引⽤。
那么移动的概念又是什么呢?这里可以简单理解为偷取,也就是我们实现一个指针把原指针的数据偷走了(swap),这样原指针就指向空了,而我们的新指针指向的就是那块区域。(当然是右值引用才会有的)。
对于像string/vector这样的深拷⻉的类或者包含深拷⻉的成员变量的类,移动构造和移动赋值才有 意义,因为移动构造和移动赋值的第⼀个参数都是右值引⽤的类型,他的本质是要“窃取”引⽤的 右值对象的资源,⽽不是像拷⻉构造和拷⻉赋值那样去拷⻉资源,从提⾼效率。下⾯的bit::string 样例实现了移动构造和移动赋值。
下面我们来看一下它们俩是如何实现的:
// 移动构造 string(string&& s) { cout << \"string(string&& s) -- 移动构造\" << endl; swap(s); }// 移动赋值 string& operator=(string&& s) { cout << \"string& operator=(string&& s) -- 移动赋值\" << endl; swap(s); return *this; }
3.2:对于一些右值对象传参配合移动语义解决返回值问题分析:
下面我们讲一下对于右值对象传参及返回时候,编译器做出来的一系列优化操作:
下面在自己实现的string类中测试一下:
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 _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; };}
移动构造有无分析:
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;}//移动构造有无测试:int main(){string ret = addStrings(\"11111\", \"2222\");cout << ret.c_str() << endl;return 0;}
①右值对象构造,只有拷⻉构造,没有移动构造的场景 :
而对应vs22这里就一步到位,直接让main函数中的ret所指向的区域让str也指向,有点抽象了。
②右值对象构造,有拷⻉构造,也有移动构造的场景:
移动赋值有无分析:
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;}//移动赋值有无测试:int main(){string ret;ret = addStrings(\"11111\", \"2222\");cout << ret.c_str() << endl;return 0;}
①右值对象赋值,只有拷⻉构造和拷⻉赋值,没有移动构造和移动赋值的场景:
②右值对象赋值,既有拷⻉构造和拷⻉赋值,也有移动构造和移动赋值的场景:
3.3右值引用对容器操作的提效:
①当实参是⼀个左值时,容器内部继续调⽤拷⻉构造进⾏拷⻉,将对象拷⻉到容器空间中的对象 (因为所传的参数是自定义类型都会对非引用的函数都要调用它的拷贝构造)。
②当实参是⼀个右值,容器内部则调⽤移动构造,右值对象的资源到容器空间的对象上。
因此一些容器的push_back和insert就支持了右值版本的参数类型,减少了资源的拷贝,再比如emplace系列,这里涉及可变模版参数,后续讲。
四·类型分类:
C++11以后,进⼀步对类型进⾏了划分,右值被划分纯右值(purevalue,简称prvalue)和将亡值 (expiringvalue,简称xvalue)。
①纯右值:就是匿名对象,临时对象,字面常量这类:1,a+b,string(“abc”)等
②将亡值:返回右值引⽤的函数的调⽤表达式和转换为右值引⽤的转换函数的调⽤表达,如 move(x)(强转为右值)、static_cast(x)(强转为右值引用,左值属性)。
③泛左值(generalizedvalue,简称glvalue),泛左值包含将亡值和左值。
下面一张图理清关系:
五·引用折叠:
简单来说就是以前不允许出现int& && r = i;这样的形式,c++11出现了支持这样操作:
右值引⽤的右值引⽤折叠成右值引⽤(int &&&&x->int &&x),所有其他组合均折叠成左值引⽤(int && &x,int & &&x->int &x)。
下面看一下例子:
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&&
下面是一个根据模版实例化不同引用的例子:
// 由于引⽤折叠限定,f1实例化以后总是⼀个左值引⽤ templatevoid f1(T& x){}// 由于引⽤折叠限定,f2实例化后可以是左值引⽤,也可以是右值引⽤ templatevoid f2(T&& x){}
f1(n); f1(0); //实例化出左值引用无法引用右值 f1(n); f1(0); // 实例化出左值引用无法引用右值 f1(n); f1(0);//const左值引用的可以引用右值 f1(n);//const右值引用的可以引用右值,权限缩小 f1(0); f2(n); //右值引用无法引用左值 f2(0); f2(n); f2(0); //左值引用无法引用右值 f2(n); // 右值引用无法引用左值 f2(0);
对于f2而言, 这就是一个模版,根据我们传递的模版类型不同,我们实例化右值引用它这个x就变成右值引用,实例左值引用,它这个模版就变成左值引用,
这里根据我们传的值不同自动推导T的类型,也就是我们传的实参是T&&类型,推导出T的类型,这里就运用了引用折叠的知识。
六·完美转发:
完美转发forward本质是⼀个函数模板,他主要还是通过引⽤折叠的⽅式实现,具体用法如下:
也可以把它当成强转来理解:即forward后面是右值引用类型如图;那么后面就把它强转为右值传给要传递的函数,左值引用就是传递左值就ok了
简单来说就是我们当传一个参数给一个函数,此时它里面的形参如果是右值引用,如果又拿这个形参去调用新的函数,由于这个形参是左值属性,它会自动匹配对应的左值,而我们需要它走右值的故可以在它要传之前给它,实现给它的左值属性变成右值,来完成我们想要的操作。
七·可变模版参数:
7.1介绍:
可变模版参数就是可以实例化出不同个数模版参数的模版(可以是不同参数的类模版也可以是函数模版)。
可变数⽬的参数被称 为参数包,存在两种参数包:模板参数包,表⽰零或多个模板参数;函数参数包:表⽰零或多个函 数参数。
如:
template void Func(Args... args) {}• template void Func(Args&... args) {}• template void Func(Args&&... args) {}
这里如果是模版参数包就是typename/class...+模版类型,函数参数包就是模版类型+...+形参;这样就形成了一个一个很多不同类型的参数长度不同的的模版函数了。
这⾥我们可以使⽤sizeof...运算符去计算参数包中参数的个数。如:sizeof...(args)。
这里区分一下:
函数模版:一个模版实例化出很多函数(参数个数相同)。
可变函数模版:一个模版实例化多个参数的模版函数。
7.2包扩展:
对于⼀个参数包,我们除了能计算他的参数个数,我们能做的唯⼀的事情就是扩展它。
说白了,我们简单模拟一下扩展它:把它取出来打印一下,这时可能会说直接遍历它不就行了,但是基于它的底层实现,无法做到,因此我们要递归每次调用自身就展开一个参数把它打印,直到最后无参数了让它走空的递归函数,这样简单模拟一下过程。
这里我们要知道它递归是个形象的说法不是运行时候递归而是编译的时候的递归调用解析。
void ShowList(){// 编译器时递归的终⽌条件,参数包是0个时,直接匹配这个函数 cout << endl;}template void ShowList(T x, Args... args){cout << x << \" \";// args是N个参数的参数包 // 调⽤ShowList,参数包的第⼀个传给x,剩下N-1传给第⼆个参数包 ShowList(args...);}// 编译时递归推导解析参数 template void Print(Args... args){ShowList(args...);}int main(){Print();Print(1);Print(1, string(\"xxxxx\"));Print(1, string(\"xxxxx\"), 2.2);return 0;}
当然也有另一种模拟的包扩展:
template const T& GetArg(const T& x){cout << x << \" \";return x;}template void Arguments(Args... args){}template void Print(Args... args){Arguments(GetArg(args)...);}int main(){Print(1, string(\"xxxxx\"), 2.2);return 0;}
这里就简单说一下它的思路:
这里我们的Arguments就充当一个空函数的作用,它所接受的一堆实参就是 GetArg函数的返回值,也就是说把参数包一个个都交给了 GetArg,这个函数完成了所有的打印任务,然后返回空给 Arguments,最后返回Print即可。
7.3empalce系列接⼝:
分为emplace和emplace_back:其实效率是高于push和push_back,因此以后建议使用前两者,
其次就是前两者不同于后两者的是就是可以不用像后两者一样用容器中的对象去初始化,而是可以直接用构造这个对象的参数去初始化,(这里涉及了它底层收到传递的这个参数该如何操作问题)
:其实就是由于它支持了可变模版参数,于是就把你给它的这个参数包一直往里传(这里注意右值引用是左值属性可能会走不是我们预期的函数,因此传递的时候保持它的右值属性给它完美转发一下),直到走到了构造处,完成构造成对象最后就完成了此操作。后面我们会展示一下这个过程。
template void emplace_back (Args&&... args);template iterator emplace (const_iterator position, Args&&... args);
这里可以看出emplace_back支持可以插入参数包,而push_back却不能。
下面就是这个emplace_back可以直接插入参数包的具体操作过程:
templatestruct ListNode{ListNode* _next;ListNode* _prev;T _data;ListNode(T&& data):_next(nullptr), _prev(nullptr), _data(move(data)){}template ListNode(Args&&... args) : _next(nullptr), _prev(nullptr), _data(std::forward(args)...){}};templatestruct ListIterator{typedef ListNode Node;typedef ListIterator Self;Node* _node;ListIterator(Node* node):_node(node){}// ++it;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;}};templateclass list{typedef ListNode Node;public:typedef ListIterator iterator; typedef ListIterator 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->_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->_next = newnode;newnode->_prev = prev;newnode->_next = cur;cur->_prev = newnode;return iterator(newnode);}template void emplace_back(Args&&... args){insert(end(), std::forward(args)...);}template iterator insert(iterator pos, Args&&... args){Node* cur = pos._node;Node* newnode = new Node(std::forward(args)...);Node* prev = cur->_prev;prev->_next = newnode;newnode->_prev = prev;newnode->_next = cur;cur->_prev = newnode;return iterator(newnode);}private:Node* _head;};
因为我们这里传的是参数包,因此接受它的函数都要包含这个参数模版,其次就是因为我们一开始的emplace_back是右值引用,因此其他接受的也应该是右值引用再之就是之间要注意完美转发。
八·类的一些新功能:
下面是c++11之后新更新的一些新的操作功能:
8.1默认的移动构造和移动赋值:
C++11新增了两个默认成员函数,移动构造函数和移动赋值运算符重载。
这些也就是如果我们没写(析构函数、拷⻉构造、拷⻉赋值重载,这三个函数是绑定的,因为无资源开毁的时候我们都不写让它浅拷贝就好,如果有的话那么就要深拷贝了,也就是要写了),此时编译器会自己生成移动构造,如果是内置类型就是浅拷,自定义类型就会调用它的移动构造,如果无,就浅拷;对于移动赋值也是如此,但是一旦自己提供一个,编译器都不会再自己生成。
因此我们做了一个规定如果无资源申请与销毁我们这几个都不写,让它走默认生成的浅拷贝,反之就都要写;话说得好,移动构造和赋值效率高,说白了只是对那些需要深拷贝的才会提高效率(转移资源)。
8.2声明时给缺省值:
这里就是以前类和对象讲述的,如果没传实参就会走缺省值,(重要的就是,声明和定义缺省值只能给一个)就不做多说明了。
8.3defult和delete:
①default:
它的作用就是告诉编译器要自己默认生成它:比如:我们再一个类内写了拷贝构造,但是没写构造,我们想要它的默认构造(此时假设声明给了缺省值),但是如果不写构造它会报错,但是写了也没有意义故可以函数声明后面=default。
②delete:
在C++11中,只需在该函数声明加上=delete即可,该语法指⽰编译器不⽣成对应函数的默认版本,称=delete修饰的函数为删除函数:比如我们像拷贝构造那样的三个函数都没写,想让编译器生成默认的,但是又不希望移动构造等生成,可以在其函数声明=delete。
8.4final与override:
①final:
我们不希望一个类被继承(或者不让它的子类出现像多态这样的行为)可以在类名后面加上final。
②override:
只允许加在子类的虚函数后面,检查子类的虚函数是否被重写,如果重写了父类的就不报错,否则直接报错。
8.5STL中⼀些变化:
①STL增加了许多新容器,后面这两个主要是常用的:unordered_map和unordered_set。
② STL中容器的新接⼝函数类型也增加了,最重要的就是右值引⽤和移动语义相关的push/insert/emplace系列 接⼝和移动构造和移动赋值,还有initializer_list版本的构造,以及范围for的遍历等,这些其实会使用就好。
九·const限定符:
9.1顶层const和底层const:
指针或者引用自身被const修饰就是顶层const,指向的对象被const修饰就是底层const。
我们可以这么记:*或者&左就是底层const,右边就是顶层const;无这两者就都是顶层const。
int main(){ int i = 0; int* const p1 = &i; // 顶层const const int ci = 42; // 顶层const const int* p2 = &ci; // 底层const const int& r = ci; // 底层const return 0;}
9.2constexpr:
①constexpr(constant expression):只能修饰常量表达式,且这个常量表达式只能用常量初始化不能用变量。
这里首先判断可不可以修饰:第一就是把constexpr舍弃后,句子语法成立,其次就是修饰的后面是不是常量表达式。
如:
constexpr int aa = 1;int b=2;constexpr const int x=b;//修饰的表达是是变量赋值。报错
那什么是常量表达式呢?
不会改变并且在编译过程中就能得到计算结果的表达式,字⾯值、常量表达式初 始化的const对象都是常量表达式,要注意变量初始化的const对象不是常量表达式。
const int a = 1; // a是常量表达式 const int b = a + 1; // b是常量表达式 int c = 1; // c不是常量表达式 const int d = c; // d不是常量表达式 const int e = size(); // e不是常量表达式
②当然constexpr可以修改变量,constexpr修饰的变量⼀定是常量表达式, 且必须⽤常量表达式初始化,否则会报错。如:
constexpr int aa = 1; constexpr int bb = aa+1; //constexpr int cc = c; 报错
③constexpr可以修饰指针,constexpr修饰的指针是顶层const,也就是指针本⾝。(也就是说constexpr只是起到修饰作用,被修饰的量如何进行还要看它本身的性质)如:
constexpr const int* p3 = &d; //constexpr修饰的是p3本⾝,const修饰*p3
④constexpr还可以修饰函数的返回值,要求函数体中,包含⼀条return返回语句,修饰的函数可以 有⼀些其他语句,但是这些语句运⾏时可以不执⾏任何操作就可以,如类型别名、空语句、using 声明等。并且要求参数和返回值都是字⾯值类型(整形、浮点型、指针、引⽤),并且返回值必须是 常量表达式。 如:
constexpr int func(int x){ return 10 + x;}constexpr int fxx(int x){ int i = x; i++; cout << i << endl; return 10 + x;}int main{constexpr int N2 = func(10);//constexpr int N3 = func(i); 返回值不是常量表达式constexpr int N5 = fxx(10); // 报错 ,返回值是常量表达式,但函数体内运行时还执行了其他操作}
⑤constexpr不能修饰⾃定义类型的对象,但是⽤constexpr修饰类的构造函数后可以就可以,但是初始化该 构造函数成员时必须使⽤常量表达式,并且函数体内部的语句运⾏时可以不执⾏任何操作就可以, 跟修饰函数类似。 如:
class Date{public: constexpr Date(int year, int month, int day) :_year(year) ,_month(month) ,_day(day) {} constexpr int GetYear() const { return _year; }private: int _year; int _month; int _day;};int main(){ constexpr Date d1(2024, 9, 8);//修饰自定义类型对象(参数必须字面值类型初始化) constexpr int y = d1.GetYear();//修饰成员函数的返回值(类似普通函数操作) return 0;}
⑥constexpr可以修饰模板函数,但是也要满足除了return其他运行时不执行的操作 ;其次就是由于模板中类型的不确定性,因此模板函数实例化后的函数是否 符合常量表达式函数的要求也是不确定的;因此c++11规定,如果onstexpr修饰的模板函数实例 化结果不满⾜常量表达式函数的要求自动忽略constexpr,也就和普通模版函数一样了。
templateconstexpr T Func(T t){ return t;}int main(){string ret1 = Func(\"111111\");//不是字面值常量,直接忽略constexpr存在constexpr int ret2 = Func(10);//是字面值常量 return 0; }
十·lambda:
10.1介绍:
lambda 表达式本质是⼀个匿名函数对象,跟普通函数不同的是他可以定义在函数内部;
但是它的定义虽不同于仿函数,但是用法相似都是对象调用+(参数);对于lambda它也是有返回类型的但是我们习惯用auto去自动推导它。
10.2表达式用法及其部分介绍:
在学习 lambda 表达式之前,我们的使⽤的可调⽤对象只有函数指针和仿函数对象,函数指针的 类型定义起来⽐较⿇烦,仿函数要定义⼀个类,相对会⽐较⿇烦。使⽤ lambda 去定义可调⽤对 象,既简单⼜⽅便。
lambda 在很多其他地⽅⽤起来也很好⽤。⽐如线程中定义线程的执⾏函数逻辑,智能指针中定制删除器等, lambda 的应⽤还是很⼴泛的,以后我们会不断接触到。
lambda表达式的格式: [capture-list] (parameters)-> return type { function boby }
10.2.1组成介绍:
①[capture-list] :捕捉列表,该列表总是出现在 lambda 函数的开始位置,编译器根据来判断接下来的代码是否为 lambda 函数,捕捉列表能够捕捉上下⽂中的变量供 lambda 函数使⽤,捕捉列表可以传值和传引⽤捕捉,也可以是它们俩的混合捕捉,捕捉列表为空也不能省略。
②(parameters) :参数列表,与普通函数的参数列表功能类似,如果不需要参数传递,则可以连 同()⼀起省略。
③->return type :返回值类型,⽤追踪返回类型形式声明函数的返回值类型,没有返回值时此 部分可省略。⼀般返回值类型明确情况下,也可省略,由编译器对返回类型进⾏推导。
④{function boby} :函数体,函数体内的实现跟普通函数完全类似,在该函数体内,除了可以 使⽤其参数外,还可以使⽤所有捕获到的变量,函数体为空也不能省略。
下面就是一个简单的lambda表达式:
auto add1 = [](int x, int y)->int {return x + y; }; cout << add1(1, 2) << endl;
10.2.2捕捉列表:
这里什么样的变量需要捕捉呢?对于参数列表内的肯定不用,全局和静态的也不用,只有我们用的
局外的才需要捕捉才能用。
这里又分为两种类型的捕捉(都是隐式捕捉,用谁捕捉谁):
①传值捕捉:x:如果我们是传值,那么里面默认是cosnt不能修改的;如果全部都传值捕捉可以直接=。
②传引用捕捉:&x:如果我们是传引用捕捉,是可以修改的;全部传引用捕捉可直接&。
当然两种也是可以混合使用的:[x, y,&z];[&,x,y];[=,&x]:后两个和我们理解的有区别:它不是除了给定的之外其他都传引用捕捉或者传值捕捉,而是用到哪个变量就去捕捉它。
一般来说传值捕捉是不可以修改的,但是如果我们在参数列表后面加上mutable可以取消其常量性 (如果要加mutable则()不能省略)。
10.2.3lambda原理:
lambda底层是仿函数对象,也就说我们写了⼀个 lambda 以后,编译器会⽣成⼀个对应的仿函数的类,仿函数的类名是编译按⼀定规则⽣成的,保证不同的 lambda ⽣成的类名不同。
这里我们拿lambda的组成和仿函数这个类对比一下:
lambda参数/返 回类型/函数体就是仿函数operator()的参数/返回类型/函数体, lambda 的捕捉列表本质是⽣成 的仿函数类的成员变量,也就是说捕捉列表的变量都是 lambda 类构造函数的实参,当然隐式捕捉,编译器要看使⽤哪些就传那些对象。
下面就是感兴趣就可以了解一下它的原理,当然我们会使用就可:
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\':::: (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\'::::operator() (0D824C0h)
十一·包装器:
分为function和bind,它们的头文件都是
11.1function:
首先它是一个可变参数模版(当然也是一个包装器):
template class Ret, class... Args> class functionRet(Args...)>;
function 的实例对象可以包装存 储其他的可以调⽤对象,包括函数指针、仿函数、 lambda 、 bind 表达式等。
下面演示一下它包装各种类型的对象如何使用:
模型:function 封装后的对象=被调用的对象。
封装后的对象(实参)
#includeint 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;};
普通函数或者lambda表达式:
如果它包装的是函数,普通函数取不取地址都一样(包装函数指针);但是如果是类的静态成员函数或者类的普通成员函数(这些函数默认有个this指针,故我们要从包装的function参数中加上,当然也可以是引用也可以是指针或者左右值对类的引用,目的就是可以进入类内去访问这个函数)一定要取地址的,因此这里我们只要是函数指针被包装都取地址。
//静态成员函数:function f4 = &Plus::plusi;cout << f4(1, 1) << endl;//传指针:function f5 = &Plus::plusd;Plus pd;cout << f5(&pd, 1.1, 1.1) << endl;//类自己:function f6 = &Plus::plusd;cout << f6(pd, 1.1, 1.1) << endl;cout << f6(pd, 1.1, 1.1) << endl;//类的右值引用function f7 = &Plus::plusd;cout << f7(move(pd), 1.1, 1.1) << endl;//属性改成右值cout << f7(Plus(), 1.1, 1.1) << endl;//匿名对象本身就是右值
函数指针、仿函数、 lambda 等可调⽤对象的类型各不相同, function 的优势就是统 ⼀类型,对他们都可以进⾏包装,这样在很多地⽅就⽅便声明可调⽤对象的类型,如(作为map的类型参数并结合lambda使用)下面是一道逆波兰表达式的题:
class Solution {public: int evalRPN(vector& tokens) { stack st; // function作为map的映射可调⽤对象的类型 map<string, function> 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(); }};
这样不就大大简化了我们繁琐的if 判断了,直接对应取出lambda调对象传参就可。
11.2bind:
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的⼀个命名空间中。
重点来了:
它可以调整参数的位置,增减传参个数,以及绑死参数的位置(我们常用最后一个)。
上面说的其实就是一些繁琐的概念,bind这个我们只要会用就好,下面简略的说一下用法:
在我们使用时候给它using一下:
using placeholders::_1;using placeholders::_2;using placeholders::_3;
auto 对象(这里类似上面function的对象一样类似仿函数形式)=bind(要绑定的函数名字,_1,_2,...)->(这里我们所调用的函数想给它传参就根据对应没被绑定的位置传参,如果要想给这个函数某个参数写死,调用时候这个形参是固定值,那么就可以在bind里对应函数位置给它写死,其次就是我们调用这个对象给它传参数,依次对应_1,_2...;然后去一样规则传给原函数,如果原函数有被写死的参数,就跳过,将当前的_跳过它继续往后传)->具体看下例子秒懂。
下面上例子就好了:
当然它还可以结合function 一起用,比如function包装类的普通成员函数,需要传类指针之类的,比较麻烦,我们可以给它绑定死。
这样每次就不用给f1传plus()了。
举个例子(关于绑死参数位置的实际应用):
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 func3_1_5 = bind(func1, 0.015, _1, 3); function func5_1_5 = bind(func1, 0.015, _1, 5); function func10_2_5 = bind(func1, 0.025, _1, 10); function func20_3_5 = bind(func1, 0.035, _1, 30);
计算增长函数的这个lambda中我们给它由固定年限把它的利率给绑定死了,这样不就符合我们实际的一些操作了嘛。
十二·智能指针:
12.1智能指针引入背景:
首先我们以一个例子来引出下面我们要讲解的智能指针:
double Divide(int a, int b){ // 当b == 0时抛出异常 if (b == 0) { throw \"Divide by zero condition!\"; } else { return (double)a / (double)b; }}void Func(){ int* array1 = new int[10]; int* array2 = new int[10]; try { int len, time; cin >> len >> time; cout << Divide(len, time) << endl; } catch (...) {//1.处代码 cout << \"delete []\" << array1 << endl; cout << \"delete []\" << array2 << endl; delete[] array1; delete[] array2; throw; } //2处代码 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;}
这里我们1处的代码,也就是这个catch处的代码是专门为Divide函数抛异常而写,而如果不抛异常就走了2处代码即可;但是如果是多个new在那里开辟空间呢?那我们就要写更多,但是最重要的是new也会抛异常:
这里我们假设 array1创建好了,然后array2抛了异常,因此我们就要在new后面对相应的new异常进行catch判断,但是如果是多个呢,不就麻烦了,因此智能指针就诞生了:(也就是说它是一个类的对象,我们在像这样的情况里面直接用智能指针这样,然后我们无需在里面涉及delete,因为它是一个对象,到结束自己会析构“它”所指向的空间)。
12.2智能指针设计前提:
首先引入概念介绍智能指针设计的思路:
首先就是RALL:
RAII是Resource AcquisitionIs Initialization的缩写,他是⼀种管理资源的类的设计思想,本质是 ⼀种利⽤对象⽣命周期来管理获取到的动态资源,避免资源泄漏,这⾥的资源可以是内存、⽂件指 针、⽹络连接、互斥锁等等。RAII在获取资源时把资源委托给⼀个对象,接着控制对资源的访问, 资源在对象的⽣命周期内始终保持有效,最后在对象析构的时候释放资源,这样保障了资源的正常 释放,避免资源泄漏问题。
这里为了适应RALL的特性,我们只能指针的这个类同样重载了operator*/operator->/operator[] 这些;但是并不是所有的智能指针都支持RALL,比如:我们后面要介绍的weak_ptr这款智能指针它就不支持(就是为了解决shared_ptr循环引用问题而诞生的)。
12.3智能指针介绍:
C++11,引⼊了unique_ptr和shared_ptr和weak_ptr。需要注意的是unique_ptr对应boost的 scoped_ptr。并且这些智能指针的实现原理是参考boost中的实现的。
C++标准库中的智能指针都在这个头⽂件下⾯,我们包含就可以是使⽤了, 智能指针有好⼏种,除了weak_ptr他们都符合RAII和像指针⼀样访问的⾏为,原理上⽽⾔主要是解 决智能指针拷⻉时的思路不同。
四种指针简介大框架:
①auto_ptr是C++98时设计出来的智能指针,他的特点是拷⻉时把被拷⻉对象的资源的管理权转移给 拷⻉对象,这是⼀个⾮常糟糕的设计,因为他会到被拷⻉对象悬空,访问报错的问题,C++11设计 出新的智能指针后,强烈建议不要使⽤auto_ptr。其他C++11出来之前很多公司也是明令禁⽌使⽤ 这个智能指针的。
② unique_ptr是C++11设计出来的智能指针,他的名字翻译出来是唯⼀指针,他的特点的不⽀持拷 ⻉,只⽀持移动。如果不需要拷⻉的场景就⾮常建议使⽤他。
③ shared_ptr是C++11设计出来的智能指针,他的名字翻译出来是共享指针,他的特点是⽀持拷⻉, 也⽀持移动。如果需要拷⻉的场景就需要使⽤他了。底层是⽤引⽤计数的⽅式实现的。
④weak_ptr是C++11设计出来的智能指针,他的名字翻译出来是弱指针,他完全不同于上⾯的智能指 针,他不⽀持RAII,也就意味着不能⽤它直接管理资源,weak_ptr的产⽣本质是要解决shared_ptr 的⼀个循环引⽤导致内存泄漏的问题。具体细节下⾯我们再细讲。
其次,我们还要知道,这里开辟好空间析构都是调用的delete;也就是说这些智能指针都不支持开辟连续空间的指向,只能维护单个对象;但是它后来特化支持了“删除器”(可调用的对象);这样就支持了“string[]”数组形式。比如: unique_ptr和shared_ptr都特化了⼀份[]的版本,使⽤时 unique_ptr up1(new Date[5]);shared_ptr sp1(new Date[5]); 就可以管理new[]的资源。
这里auto_ptr上面也说了存在风险问题,故这里我们就不做多说了,不要用它;因此后面讲解就不多说了。
12.3.1 shared_ptr:
对于它,其实只要记住可以拷贝(相当于资源共享),又可以移动;然后呢它底层有个计数器,专门记录多少个指针指向你这块资源,当这种指针析构到这个计数器为0,就delete这块空间(因此我们后面模拟简单实现它的时候这个计数器用整型指针模拟(因为这样每个对象拿到都是它的地址了,好操作,好维护))。
下面我们展示一下我们模拟实现的shared_ptr这个类(这里由于它可以传递这个delete(也就是这个删除器的仿函数或者lambda,我们多写了一个不同的,也就是两个参数的构造函数))
这里思路:个人认为重点在于这个引用计数的设计:
主要这⾥⼀份资源就需要⼀个 引⽤计数,所以引⽤计数才⽤静态成员的⽅式是⽆法实现的,要使⽤堆上动态开辟的⽅式,构造智能指针对象时来⼀份资源,就要new⼀个引⽤计数出来。多个shared_ptr指向资源时就++引⽤计 数,shared_ptr对象析构时就--引⽤计数,引⽤计数减到0时代表当前析构的shared_ptr是最后⼀ 个管理资源的对象,则析构资源。
形象介绍:
下面具体展示:
namespace ptr {class Fclose{public:void operator()(FILE* ptr){cout << \"fclose:\" << ptr << endl;fclose(ptr);}};templateclass shared_ptr{public:explicit shared_ptr(T*ptr):_ptr(ptr), _pcount(new int(1)) {}templateexplicitshared_ptr(T* ptr, D del): _ptr(ptr), _pcount(new int(1)), _del(del){} shared_ptr(const shared_ptr& sp) {_ptr = sp._ptr;_pcount = sp._pcount;(*_pcount)++; }shared_ptr& operator=(const shared_ptr& sp) {if (_ptr != sp._ptr) {if (--(*_pcount) == 0) {//delete _ptr;_del(_ptr);delete _pcount;}_ptr = sp._ptr;_pcount = sp._pcount;(*_pcount)++;}return *this; }~shared_ptr() {if (--(*_pcount) == 0) {//delete _ptr;_del(_ptr);delete _pcount;}}T& operator*(){return *_ptr;}T* operator->(){return _ptr;}int use_count(){return *_pcount;}private:T* _ptr;int* _pcount;function_del = [](T* ptr) {delete ptr; };//这里auto和decltype都不行,只能上包装器};}
这里值得一说的也就是这个赋值重载的设计了:
首先我们分为1.赋值给自身(直接不走返回这个*this)
2.赋值给其他存在的对象,这里我们就还要分情况:
①被赋值对象所指向的资源的count--后为1,那么此时直接释放掉这块空间,完成给被赋值对象拷贝并count++即可。
②如果--count后不为0,那么说明此刻这段资源还有其他智能指针指向着,不能释放,故这块资源已经--了,只需要重新完成给被赋值对象拷贝并count++即可。(代码同上)
1·其次就是我们可以细节观察到这里的构造函数都用explicit修饰了,也就是不允许普通指针隐式类型转换如:
ptr::shared_ptr sp1 = new string(\"a\");// 库里加了explicit不支持隐式转换ptr::shared_ptrsp1(new string(\"abc\"));//只支持
2·再之shared_ptr 除了⽀持⽤指向资源的指针构造,还⽀持 make_shared ⽤初始化资源对象的值 直接构造。(template shared_ptr =make_shared (Args&&... args); )(这里我们没有模拟实现,故就调用库里面的了。)
如:
shared_ptr sp1 = make_shared(\"aaaaaa\");
相对于我们之前学的make_pair就是多了个模版实例化。
3·当然它也支持了operator bool的类型转换(我们也就不实现了),如果智能指针对象是⼀个 空对象没有管理资源,则返回false,否则返回true,意味着我们可以直接把智能指针对象给if判断 是否为空。
如:
if (sp3)cout << \"no nullptr\" << endl;
4·我们来讲述一下这个两个参数的构造函数的应用(但是不是说可以仿函数或者lambda):
这里我们如果使用仿函数,于是我们加了一个封装仿函数的类:
class Fclose{public:void operator()(FILE* ptr){cout << \"fclose:\" << ptr << endl;fclose(ptr);}};
下面就是我们特殊的资源对它(这个删除器)的应用(当然库里也可以用函数指针,这里不常用我们就不用了(也就是说我们传递的这个对象能调用释放资源就好)):
//shared_ptr 可以lambda也可以仿函数;建议lambda//ptr::shared_ptr sp2(fopen(\"test.cpp\", \"r\"), [](FILE* ptr) { fclose(ptr); }); //ptr::shared_ptr sp2(fopen(\"test.cpp\", \"r\"), ptr::Fclose());//上面讲述到的那个特化[]版本的可调用对象: //ptr::shared_ptr sp1 ( new string [20] );
12.3.2 weak_ptr:
此刻,它一出场,也就是我们shared_ptr无法解决的循环引用问题了:
下面我们先上图:
也就是我们的
这个图也就是我们一个自定义类包含了这个智能指针(图上是shared_ptr)类型的prev和next:
此时如果我们析构右边的节点,就要先析构next,然后就是要析构n1,析构n1就要先析构n2的prev,然后就是先析构n2;这样不就绕回来了。具体一下比如我们在n1指向的对象里析构了next
那么引用计数变成了1,然后我们还要再析构一次n2的指向,达不到我们想要的目的(析构一次next就OK);因此这里把这个next和prev设计成了weak_ptr类型。
weak_ptr绑定到shared_ptr时不会增加它的 引⽤计数,next和prev不参与资源释放管理逻辑,就成功打破了循环引⽤,解决了这⾥的问题。
差不多就是这样:
我们到时候直接通过weak_ptr (next),它的值就是我们 这里n2值,也就是n2指向资源,直接销毁即可,也就是通过调用它的lock(后面我们会讲到)返回⼀个管理资源的shared_ptr,然后传删除器(lambda)直接完成删除(这样就完成了next和prev我们所期待的操作)。
weak_ptr具体介绍:
这里我们用这个weak_ptr只需要注意以下几点就可以:
①weak_ptr也没有重载operator*和operator->等,因为他不参与资源管理,那么如果他绑定的 shared_ptr已经释放了资源,那么他去访问资源就是很危险的。
②weak_ptr⽀持expired检查指向的 资源是否过期,use_count也可获取shared_ptr的引⽤计数,weak_ptr想访问资源时,可以调⽤ lock返回⼀个管理资源的shared_ptr,如果资源已经被释放,返回的shared_ptr是⼀个空对象,如 果资源没有释放,则通过返回的shared_ptr访问资源是安全的。
下面我们简单模拟一下weak_ptr实现(它是不参与count管控的,故它use_count后返回的是它绑定的资源的count):
template class weak_ptr { public: weak_ptr() {} weak_ptr(const shared_ptr& sp) :_ptr(sp.get()) {} weak_ptr& operator=(const shared_ptr& sp) { _ptr = sp.get(); return *this; } private: T* _ptr = nullptr; };}
然后我们测试一下它和shared_ptr联合使用的场景(请看图):
这里告诉们weak_ptr无地址,只是绑定资源,其次就是要注意lock转换成shared_ptr去访问绑定的资源,然后比如shared_ptr或者unique_ptr置nullptr相当于让它指向空,故原先资源的count--;它现在的count置为0。
auto sp2 = make_shared(\"333333\"); cout << wp.expired() << endl;//这里它所绑定的资源是否还在,如果到期就返回1;否则0
因此关于weak_ptr大概就到这里。
12.3.3 unique_ptr:
这里和上面讲的shared_ptr差不多,只不过从它的名字可以看出它是不能拷贝和赋值重载的(独特的,不支持资源共享),其他用法和shared_ptr差不太多了。
也就是说实现的时候把这两个函数直接delete掉了;我们就不实现了;有一个区别就是它的删除器操作和shared_ptr不同:
它给自己类型模版传递调用对象的类型而shared_ptr无需,故我们要说这里建议用仿函数;下面请看例子:
//unique_ptr 还是仿函数://unique_ptr up(fopen(\"test.cpp\", \"r\"), ptr::Fclose());auto u = [](FILE* ptr) { fclose(ptr); };unique_ptr up(fopen(\"test.cpp\", \"r\"), u);//先要auto给lambda形成对象再decltype
其他常见用法就和shared_ptr一样了,其他就不多说了。
12.4内存泄漏:
什么是内存泄漏?:内存泄漏指因为疏忽或错误造成程序未能释放已经不再使⽤的内存,⼀般是忘记释 放或者发⽣异常释放程序未能执⾏导致的。内存泄漏并不是指内存在物理上的消失,⽽是应⽤程序分 配某段内存后,因为设计错误,失去了对该段内存的控制,因⽽造成了内存的浪费。
内存泄漏的危害?:普通程序运⾏⼀会就结束了出现内存泄漏问题也不⼤,进程正常结束,⻚表的映射 关系解除,物理内存也可以释放。⻓期运⾏的程序出现内存泄漏,影响很⼤,如操作系统、后台服 务、⻓时间运⾏的客⼾端等等,不断出现内存泄漏会导致可⽤内存不断变少,各种功能响应越来越 慢,最终卡死。
因此,我们上面所讲的智能指针就充分的得到了证明,它存在的价值也是不可忽略的。
十三·处理类型:
13.1auto:
①auto就是可以帮我们推导参数类型(当参数类型长的时候常用);
②可以做返回值类型,但是不能做函数参数。
③不可以定义数组和其他遍历。
④在同一行auto的类型应该相同。
⑤在声明指针的时候auto和auto*都可以,但是引用必须auto&。
其次就是auto在推导的时候,会保留底层const而忽略掉顶层const;再者就是如果auto要去推导引用,我们需要去表明auto&,否则直接auto推导的是引用的对象的类型而非这个引用的类型。
下面请看例子:
int i = 0; const int ci = 42; // 顶层const int* const p1 = &i; // 顶层const const int* p2 = &ci; // 底层const const int& ri1 = ci; // 底层const const int& ri2 = i; // 底层const int& ri3 = i; auto r1 = ci; // r1类型为int,忽略掉顶层const r1++; auto r2 = p1; // r2类型为int*,忽略掉顶层const r2++; auto r3 = p2; // r3类型为const int*,保留底层const (*r3)++; // 报错 auto r4 = ri1; // r4类型为int,因为引⽤对象初始化auto类型时,推导为引⽤对象的类型,其次会忽略掉顶层const r4++; auto r5 = ri2; // r5类型为int,因为引⽤对象初始化auto类型时,推导为引⽤对象的类型 r5++; auto r6 = ri3; // r6类型为int,因为引⽤对象初始化auto类型时,推导为引⽤对象的类型 r6++; const auto r7 = ci; // r7类型为const int auto& r8 = ri1; // r8类型为const int&,因为const 因为为底层const 被保留 auto& r9 = ri2; // r9类型为const int&,因为const 因为为底层const 被保留 auto& r10 = ri3; // r8类型为int&
这写就把上面的要点都概括了。
13.2decltype:
decltype(declear type):它就是推导类型但是不会走这个()里的表达式子之类的。
int i = 0; const int ci = 0; const int& rci = ci;
decltype(i) m = 1; // m的类型是int decltype(ci) x = 1; //x是const int
①decltype(f()) x; 需要注意的是编译器并不会实际调⽤f函数,⽽是⽤f的返回类 型作为x的类型(如果我们不想让它走初始化可以用它),这里也就和auto不同了。
②其次就是和auto对处理const的方式也不同,比如它推导会保留顶层const不忽略底层const而auto是保留底层const并且忽略顶层;比如要推导引用而auto必须用auto&,而decltype可以直接用(如int &a=1;decltype(a)->int& )。
decltype(rci) y = x; // y的类型是const int& decltype(rci) z; //这里是const int&不能给它随机赋值,是常量故报错 int* p1 = &i;decltype(p1) p2 = nullptr; // p2的类型是int*
③这里有一些特殊操作,比如对*p它会识别成引用,(而auto对它的操作直接识别类型成引用对象类型了)而比如i变量 ;decltype((i))会被识别成引用;因为它把(i)当成可赋值的变量了。
// 特殊处理 decltype(*p1) r1 = i; // r1的类型是int&,解引⽤表达式推导出的内容是引⽤ decltype(i) r2; // r2的类型是int decltype((i)) r3 = i; // r3的类型是int&, (i)是⼀个表达式,变量是⼀种可以赋值特殊表达式,所以会推出引⽤类型
④还有些对类内的操作,不希望去初始化它(不能用auto),decltype就派上用场了:如我们的auto不是可以对类型的声明进行auto,但是如果我们这一个类内的private声明了一个变量,给它auto类型就会去初始化它,而此时不能进行它的初始化,因此我们就可以用decltype了:
#include using namespace std;template class A{public: void func(T& container) { _it = container.begin(); }private: // 这⾥不确定是iterator还是const_iterator,也不能使⽤auto typename T::iterator _it; // 使⽤decltype推导就可以很好的解决问题 //decltype(T().begin()) _it;};
这样我们decltype先不给它初始化等调用的时候就能推导出它的类型了。
因此可以总结一下:对于像类内特别长的类型可以用auto代替(不是变量),如果它是类的变量就只能用decltype了。
⑤decltype的尾置推导;如果⼀个函数模板的类型跟是不确定的,跟某 个参数对象有关,需要进⾏推导,因此这里如果直接decltype是不行的(前面推导找不到),也就是auto才能做返回值但是由于auto一些特性又要结合decltype一起使用,也就是declatype解决函数尾值返回问题。
如:
//template//auto Func(Iter it1, Iter it2)->decltype(*it1)//这里*it是引用类型(它的特殊转换)//{// auto& x = *it1;// ++it1;// while (it1 != it2)// {// x += *it1;// ++it1;// }// return x;//}
// 显⽰实例化能解决问题,但是调⽤就很⿇烦 /*auto ret1 = Func(v.begin(), v.end()); auto ret2 = Func(lt.begin(), lt.end());*/
这里只有这样才能调用到迭代器指针的引用来操作了。
13.3typedef和using:
首先我们的typedef和c语言一样,给前者起别名叫后者;而这里的using 就是让前者代替后者出现;如using a=b;那么a的出现在哪就代表b了,相当于a是b的别名等同于typedef b a;。