C++高级特性
对象的优化
对象的应用优化
- c++对对象构造的优化:临时对象生命周期只存在当前语句,用临时对象生成新对象的时候,临时对象不会产生,直接构造新对象
Test t4 = Test(20);
这里的Test(20)就是一个临时对象,实际上这个临时对象不会产生- 但
t4 = Test(30);
这里的临时对象是会生成的,因为这里是为了给t4赋值,而不是构造,同义的还有t4 = (Test)30;
显式生成临时对象;t4 = 30;
隐式生成临时对象
- 用指针指向临时对象是不安全的,比如
Test* p1 = &Test(30);
因为出了这条语句,临时对象就析构了 - 用引用指向临时对象是可以的,因为引用相当于别名,使用引用后临时对象的生存周期变成了引用的生存周期,比如
const Test &ref = Test(30);
函数的应用优化
- 函数调用把实参给形参,是初始化(因为此时形参并没有产生),调用拷贝构造函数
- 当函数结束时,先是析构掉形参变量,再是析构掉函数里的临时对象
- 在函数中return tmp,由于tmp出了函数之后就没了,所以为了在main函数中用t2接收它,必须在main函数的栈帧中构造一个临时对象,把tmp给到这个临时对象
当这个对象成员有一个指针,该指针指向一片其他内存时,以上函数就会有两个耗费性能的地方
- 函数中的对象tmp在main函数栈帧上拷贝构造一个临时对象,在这个过程中临时对象会将tmp指向的内存一个一个拷贝到自己的内存,然后析构掉tmp:为啥不直接让临时对象指向tmp指向的内存?
- main函数中的赋值操作会先产生一个临时对象,然后让t2一个一个把临时对象的内存拷贝到自己指向的那片内存,然后把临时对象析构掉:为啥不直接让t2的指针指向临时对象的内存?
解决方法:增加右值引用的拷贝构造和赋值函数
右值:没名字、没内存; 左值:有名字、有内存
一个临时对象就是右值,所以用带右值引用的拷贝构造和赋值函数就能接收它,然后执行不同于普通拷贝构造和赋值函数的操作,比如:
c++CMsytring(CMystring &&str){ //直接让自己的指针指向临时对象所指向的资源 mptr = str.mptr; //然后将临时对象的指针置null str.mptr = nullptr; }
新的问题:如果使用到了空间配置器,由于右值引用本身是个左值,所以匹配到的依旧是空间配置器里左值引用的版本,必须使用move语义将左值类型转换为右值,比如:
scss
void construct(T *p, T &&val){
new(p) T(std::move(val));
}
void push_back(T &&val){
if(full()) expand();
_allocator.construct(_last, std::move(val));
_last++;
}
有没有什么更简单的优化版本方法?:
-
函数模板的类型推演 + 引用折叠(也就是&&会抵消,即& + && = &;&& + && = &&)+ forward类型的完美转发
c++template<typename Ty> //这样一个函数就能实现既接收左值又接收右值 void push_back(Ty &&val){ if(full()) expand(); //而move(左值):是移动语义,只能得到右值类型 //能够识别左值或者右值类型,类型的完美转发 _allocator.construct(_last, std::forward<Ty>(val)); _last++; } template<typename Ty> void construct(T *p, Ty &&val){ new(p) T(std::forward<Ty>(val)); }
三条对象优化的规则
- 函数参数传递过程中,优先按引用传递
- 在函数返回对象时直接返回临时对象,而不要返回一个定义过的对象,这样就是用临时对象构造一个新对象,编译器可以优化,比如
return Test(val);
- 接收返回值是对象的函数调用时,优先按初始化的方式接收,而不要按赋值的方式接收
智能指针
智能指针体现在把裸指针进行了一次面向对象的封装,在构造函数中初始化资源地址,在析构函数中自动释放资源;
所以智能指针是栈上的,利用栈上的对象出作用域自动析构的特点保证资源不泄露(不可能是堆上的!因为堆上的需要自己手动delete)
还需要解决智能指针的浅拷贝问题:如果CSmartPtr<int> ptr1(new int); CSmartPtr<int> ptr2(ptr1);
,由于默认的拷贝构造函数是浅拷贝,所以这两个ptr指向的资源是同一份,可能会导致资源多次释放,delete野指针导致崩溃
在之前的场景里面深拷贝是实现自己的拷贝构造函数,把底层资源复制一份出来;但在指针场景下,CSmartPtr<int> ptr1(new int); CSmartPtr<int> ptr2(ptr1);
之后,用户会认为ptr1和ptr2管理的是同一份资源,按照之前的思路不能满足这个需求
-
解决浅拷贝问题:不带引用计数的智能指针
-
auto_ptr:当ptr2拷贝构造ptr1的时候,将ptr1指向底部资源的指针置null,永远只让最后一个ptr指向资源,前面的所有ptr都是null,不推荐使用这个auto_ptr
-
scoped_ptr:直接把拷贝构造和赋值函数给delete掉,直接不能拷贝构造和赋值,也不太推荐
-
unique_ptr:首先,它也像scoped_ptr把拷贝构造和赋值函数也delete了;但是它支持
unique_ptr<int> p2(std::move(ptr1));
,因为它提供了带右值引用的拷贝构造和赋值运算符,所以它可以支持在函数中的传参和返回,比如:arduinotemplate<typename T> unique_ptr<T> getSmartPtr(){ unique_ptr<T> ptr(new T()); return ptr; } unique_ptr<int> ptr1 = getSmartPtr<int>();
如果是scoped_ptr是不行的,因为返回值是先在main函数栈帧上通过拷贝构造生成一个临时对象后再给ptr1的,而拷贝构造函数已经被删了
在这里用户的用意是很明显的,
unique_ptr<int> p2(std::move(ptr1));
很明显就是要把ptr1的内容转移给ptr2,这是它跟auto_ptr的区别
-
-
解决浅拷贝问题:带引用计数的智能指针,多个智能指针可以管理同一个资源,给每个对象资源匹配一个引用计数,直到引用计数为0的时候才析构
-
shared_ptr:强智能指针,可以改变资源的引用计数
-
weak_ptr:弱智能指针,不会改变资源的引用计数,没有提供*和->,只是一个观察者,不能改变资源
通过弱智能指针操作强智能指针,再操作资源
那它有什么用?需要使用提升方法把它转成shared_ptr,才能用*和->,适用于线程安全的地方:当使用指针访问资源的时候,如果资源已经释放会提升失败,资源还在才提升成功,解决多线程访问共享对象的线程安全问题
C++shared_ptr<A> ps = _ptra.lock(); if(ps != nullptr) ps->testA();
-
为什么要有强弱两种指针------强智能指针循环引用问题
- 问题场景:A和B两个智能指针分别指向classA和classB,而classA里又有一个智能指针指向B,classB里有一个智能指针指向A,导致new出来的资源无法释放,资源泄露
- 解决:定义对象的地方用强智能指针,引用对象的地方用弱智能指针
-
解决智能指针自动释放所有类型的资源:自定义删除器deletor,因为不是所有类型的资源都是用delete的
-
方式一:不推荐,因为这个class可能只用一次,但却要特地定义出来
c++template<typename T> class Mydeletor{ public: void opeartor()(T* ptr)const { fclose(ptr); } } unique_ptr<FILE, mydeletor<FILE>> ptr2(fopen("data.txt","w"));
-
方式二:lambda表达式,推荐
phpunique_ptr<FILE, function<void(FILE*)>> ptr(fopen("data.txt","w"), [](FILE *p)->void { fclose(p); } );
function和绑定器
function
-
作用:留下绑定器、函数对象、lambda表达式的类型,以便在多条语句中使用
-
function<void()> func1 = hello; func1();
:使用函数类型实例化function,注意返回值+参数列表是函数类型,而void(*)()
是函数指针类型 -
function<int(int,int)> func2 = [](int a,int b)->int {return a + b;}
-
function<void(Test*, string)> func3 = &Test::hello;
-
应用场景:
-
当需要根据choice的值使用switch调用不同的函数时:使用switch-case,不好,因为不满足开闭原则,当有需求更改的时候这块switch-case代码也要更改
-
解决:使用function+map的方法
scssmap<int,void()> actionmp; mp.insert({1, func1}); mp.insert({2, func2}); //然后调用的时候 auto it = actionmp.find(1); it->second();
-
-
实现原理
把传入的函数对象,保存到了成员变量里,即保存了传入的函数对象类型
同时模板处使用
typename... A
的意思是可变参的类型参数,说明那里是一组参数,可以应对传入不同形参个数的函数对象
函数对象
类似于c语言中的函数指针,区别如下
- 通过函数对象调用operator(),可以省略函数的调用开销,比通过函数指针调用函数(不能够inline内联使用,因为在编译阶段看到那个函数指针根本不知道是调用哪个函数,只有在运行时去取地址才知道)效率更高
- 因为函数对象使用类生成的,可以添加相关的成员变量,用来记录函数对象使用时的更多的信息
在前文的set和map都可以通过传入不同的函数对象,来实现从大到小排序还是从小到大排序,比如set<int, greater<int>> set1
:设定从大到小,(默认是从小到大)
绑定器
bind1st,bind2nd,将operator()的第一个或第二个形参变量绑定成一个确定的值
auto it = find_if(vec.begin(), vec.end(), bind1st(greater<int>(), 65));
底层原理,绑定器本质上还是函数对象的一个应用,只不过是传入了一个值给二元函数对象,实质上还是那个二元函数对象在起作用,但bind1st和bind2nd都只能用于二元函数对象,所以c++11推出了更强大的bind
C++11的bind绑定器:返回的结果还是一个函数对象
- 可以使用它的小括号重载函数,比如:
bind(hello, "hello")();
- 参数占位符,
bind(hello, placeholders::_1)("hello");
参数占位符,告诉编译器参数被绑定了但还没传,需要在调用的时候由用户进行传递
可以综合使用function和bind进行绑定器的复用
function<void(string)> func1 = bind(hello, placeholders::_1);
lambda表达式
捕获外部变量\] (形参列表) -\>返回值{操作代码};
` auto func1 = []()->void{cout<<"hello world";}`
其中的\[捕获外部变量\]就是代表等价的函数对象构造函数里需要传入的参数
* \[\],说明不传入任何变量给函数对象
* \[=\],以传值的方式捕获外部的所有变量
> 类似于的函数对象
>
> ```arduino
> template