右值引用
左值:具有可寻址的存储单元,并且能由用户改变其值的量
右值:即将被销毁的临时变量,如字面值常量“hello”(将亡值),返回非引用类型的表达式int func()(纯右值)等;
为什么要有右值引用?
避免不必要的拷贝,如果没有右值引用,每一次的拷贝都需要开辟一片新的空间存储被拷贝的值,这样就使拷贝不会影响原来的值,这样当然没有错,但是有些情况下被拷贝的值再完成拷贝之后就被销毁了(如各种临时变量),那么也就没有必要再考虑会不会影响原来的值了,可以直接把原来的值 偷 过来,也就是直接将原来的存储空间拿给拷贝后的变量,不需要开辟新空间;
因此,右值引用就被提出了,对于右值引用,我们可以使用 移动构造函数 而不是 拷贝构造函数 从而避免开辟新空间,大大提高效率;
右值引用的用法
- 对于临时变量,编译器会自动将其看做一个右值引用;
- 对于左值,可以使用 std::move(lvalue) 移动语义将其变为一个右值引用;
1 2 3
| class M; M c1(c); M c2(std::move(c1));
|
因此,对于自定义的带有指针的类,需要定义移动构造函数;在标准库中,大多数函数也都有针对右值引用的版本;
拷贝构造和移动构造的区别:
在写移动构造或者移动赋值时要注意,只要让原来的指针断开(指向NULL)即可,不要销毁,销毁是析构函数要做的事;
std::forward
C++11还实现了 完美转发,完美转发的含义是在参数传递的过程中保持其左值或右值的特性;
没有完美转发的案例如:
1 2 3 4 5 6 7 8 9 10 11 12 13
| void print(int& t){ cout<<"左值"<<endl; } void print(int&& t){ cout<<"右值"<<endl; } void testForward(int&& t){ print(t); } int main(){ testForward(1); return 0; }
|
使用 std::forward 可以实现完美转发:
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| void print(int& t){ cout<<"左值"<<endl; } void print(int&& t){ cout<<"右值"<<endl; } void testForward(int&& t){ print(t); print(std::forward<int>(t)); } int main(){ testForward(1); return 0; }
|
Lambdas
lambda表达式可以理解为一个匿名的内联函数;可以被当作一个参数或者局部对象;常用于标准库函数中的函数对象参数,如定义比较大小的函数对象参数。
lambda表达式的完全形式如上,**[]中表示传入的外界变量,()中表示函数参数**,后三个分别表示 **mutable(可改变传入值),throwSpec(允许报错),retType(返回类型)**;
1 2 3 4 5 6 7 8 9 10 11 12 13
| []{ std::cout<<"Hello Lambda"<<std::endl; }
[]{ std::cout<<"Hello Lambda"<<std::endl; }()
auto L=[]{ std::cout<<"Hello Lambda"<<std::endl; };
L();
|
可以看出,编译器会把Lambda表达式看成一个**Functor(仿函数/函数对象)**;
lambda表达式是一个匿名的内联函数,在编译阶段就会被展开填补在调用处,因此如果是 传值 如左上图中案例;如果传引用则不会有这种问题,如中上图案例;如果没写mutable却要更改传入值,则会编译报错;
如下图所示,是lambda表达式的一种常见用法,先定义出lambda表达式,再用 decltype 得到其类型作为模板参数:
1 2 3 4 5 6
| auto cmp = [](const pair<int, int>& lhs, const pair<int, int>& rhs){ return lhs.second>rhs.second; };
priority_queue<pair<int, int>, vector<pair<int, int>>, deeltype(cmp)> pri_que;
|
还有一种用法是直接在参数处定义一个lambda表达式作为局部函数对象,不需要定义一个完整的仿函数:
1 2 3 4
| vector<int> vi{5,28,50,83,70,590,245}; int x=30; int y=100; vi.erase(remove_if(vi.begin(), vi.end(), [x,y](int n){return x<n&&n<y}))
|
智能指针
C++ 智能指针最佳实践&源码分析
unique_ptr
unique_ptr的核心特点就如它的名字一样,它拥有对持有对象的唯一所有权。即两个unique_ptr不能同时指向同一个对象。
- unique_ptr 不能被复制到另一个unique_ptr
- unique_ptr 只能通过转移语义将所有权转移到另一个unique_ptr
1 2 3 4
| std::unique_ptr<A> a1(new A()); std::unique_ptr<A> a2(a1); std::unique_ptr<A> a2 = a1; std::unique_ptr<A> a3 = std::move(a1);
|
实现
删除拷贝构造函数和赋值构造函数,在析构函数中delete raw_ptr
shared_ptr
与unique_ptr的唯一所有权所不同的是,shared_ptr强调的是共享所有权。也就是说多个shared_ptr可以拥有同一个原生指针的所有权。
shared_ptr 是通过引用计数的方式管理指针,当引用计数为 0 时会销毁拥有的原生对象。
1 2 3 4
| std::shared_ptr<A> a1(new A()); std::shared_ptr<A> a2(a1); std::shared_ptr<A> a2 = a1; std::shared_ptr<A> a3 = std::move(a1);
|
实现
- 构造函数:指针指向该对象,引用计数置为一
- 拷贝构造函数:指针指向该对象,引用计数加一
- 赋值构造函数:=左边引用计数减一,右边引用计数加一,如果左边引用计数降为0,要销毁指针指向的对象
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69
| template<typename T> class sharedPtr { private: T* _ptr; int* _count; public: sharedPtr(T* p):_ptr(p) { if (p != nullptr) { _count = new int(1); } else { _count = new int(0); } } sharedPtr(const sharedPtr& other) { if (&other != this) { _ptr = other._ptr; _count = other._count; (*_count)++; } else { _ptr = nullptr; _count = new int(0); } } sharedPtr& operator=(const sharedPtr& rhs) { if (rhs == this) { return *this; } if (_ptr) { (*_count)--; if ((*_count) == 0) { delete _ptr; delete _count; } } (* rhs._count)++; _ptr = rhs._ptr; _count = rhs._count; return *this; } T* operator->() { if (_ptr) { return _ptr; } } T& operator*() { if (_ptr) { return *_ptr; } } ~sharedPtr() { (* _count)--; if ((*_count) == 0) { delete _ptr; delete _count; } } void getRefCount() { cout << "Reference Count:" << (*_count) << endl; } };
|
shared_ptr指向的对象不是线程安全的;
weak_ptr
weak_ptr 比较特殊,它主要是为了配合shared_ptr而存在的。就像它的名字一样,它本身是一个弱指针,因为它本身是不能直接调用原生指针的方法的。如果想要使用原生指针的方法,需要将其先转换为一个shared_ptr。那weak_ptr存在的意义到底是什么呢?
解决shared_ptr的循环引用问题,weak_ptr不增加引用计数;
如图,栈上创建的共享指针person指向一个新建的Person对象,引用计数为1,car指向一个新建的Car对象,引用计数为1;
之后让person的成员变量m_car指向car表示这个人的汽车,car对象引用计数为2,让car的成员变量m_person指向person表示这辆车的主人,person对象引用计数为2。
此时如果程序结束,栈上的person被销毁,堆中的person对象引用计数减1,变为1,栈上的car被销毁,推中的car对象引用计数减1,变为1;都没有变成0,都不会被销毁,造成内存泄漏。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| std::shared_ptr<A> a1(new A()); std::weak_ptr<A> weak_a1 = a1; if(weak_a1.expired()) { }
long a1_use_count = weak_a1.use_count();
if(std::shared_ptr<A> shared_a = weak_a1.lock()) { }
weak_a1.reset();
|
另外,一切应该不具有对象所有权,又想安全访问对象的情况。
如:一个公司类可以拥有员工,那么这些员工就使用std::shared_ptr维护。另外有时候我们希望员工也能找到他的公司,所以也是用std::shared_ptr维护,这个时候问题就出来了。但是实际情况是,员工并不拥有公司,所以应该用std::weak_ptr来维护对公司的指针。