左右值、&&、移动语义、智能指针
#
有两个重要概念需要区分:
类型 (Type) #
表示一段内存究竟是怎么样的性质。对于一段内存,它内部的结构应该是怎么样被解析的,使用什么样的运算符。
其实就是一开始的类型系统,包括基本类型的 int、double,也可以是标准库中的 std::string,用户在 class 定义类型。
值类别 (Value Category) #
在移动语义后完善的概念,重点关注此表达式本身的性质,具有什么身份,是否可以被移动。
在 C++98,只有左值和右值。
但是在 C++11,我们引入了移动语义,出现了一个将亡值。
将亡值有身份可以取地址,但是马上被移动也就消失了,所以我们扩大了左值的定义,认为他是广义左值;
同时,因为他即将消失,所以认为是右值,那么就把以前的右值定义为纯右值,这样五种值类别就出来了。
&&/& 作为函数形参
#
考虑下面这个函数:
int main(){
int&& ref{ 5 };
fun(ref);
}
给出两个 fun 的函数声明:
void fun(int&&);
void fun(const int&);
事实上,只有第二个能够匹配传入的参数,而不是第一个。
在没有引用的时候,我们的函数形参其实是匹配对象的类型的,因为只有确定了类型,函数内部才知道这个对象使用哪一种操作符,如何解释这一段内存。
引入引用之后,带 & 的参数会解释这个对象如何绑定到实参:
- 左值引用:只能绑定到左值。
- 常量左值引用:能绑定到左值和右值,如果直接绑定到右值,会延长该对象的生命周期。
- 右值引用:绑定到右值时,在函数内部转换成一个左值;绑定到左值时,是一个左值引用。
但是需要注意一点,虽然 ref 是一个 int&& 类型的对象,但是他的值类别其实是一个左值,所以形参可以是一个 (常量) 左值引用。
&& 在模板参数的使用
#
仅在模板以及泛型中,&& 才有引用折叠作用,常用于完美转发。
最常见的在于这样一个泛型函数:
template <typename T> wrapper(T&& arg){
target(std::forward<T>(arg))
}
传入左值的时候,会被折叠成 T&,而传入右值的时候,保留 T&& 的类型,&&非常智能,自动判断左右值。
实现移动 #
std::move() 返回的值是一个将亡值 (xvalue),至于如何把这个值移动,是类的移动构造函数/移动赋值运算符实现的。
所以如果手动编写的移动构造函数/移动赋值运算符,需要注意:
- 加上
noexcept关键字,避免抛出异常(要么成功,要么终止,避免移动一半),避免回退到复制。 - 手动把地址给到目标,然后把原来的指针指向空指针。
- 对于运算符,可以加上判断一下不是自己给自己,如果是就返回
*this。
至于源对象如何释放,是取决它本身的生命周期的。此时我们已经完成了所有权的转移。
智能指针 #
unique_ptr
#
这个指针指向的堆内存必须是它独占的,如果使用移动语义来赋值的话,会自动把上一个指针置空。
不要用一个裸指针来初始化一个智能指针,虽然语法上成立,但是语义上会破坏 unique 的独占性,仍然会发生双重释放的问题。
使用 make_unique 将一个纯右值的对象变成独占的指针,不用手动 new 故比较推荐。
make_unique 其实是一个函数,为一个纯右值返回一个独占指针,所以返回值一定会有一个指针接收,一般用 auto 配合效果很好:
auto ptr {std::make_unique<Fraction>(Fraction{3, 5})};
如果要求传入一个类型的裸指针,但是手上只有智能指针,可以使用 .get() 来得到一个底层中使用的裸指针的副本。
shared_ptr 和 weak_ptr
#
共享指针允许多个指针同时指向一块内存,同时其内部会保存指向这段内存的计数。当最后一个共享指针销毁的时候,内存将会被自动释放。
不过 shared_ptr 会导致两个共享指针相互依赖的问题,这样在释放的时候认为是互相依赖使得最终内存无法释放导致内存泄漏。
最典型的例子就是,在一个类中有 shared_ptr 的成员变量,然后两个实例化的对象的共享指针指向对方,同时具有相同的生命周期,这样释放的时候互相认为对方存在,从而都不释放内存。
因此,weak_ptr 出现了。弱指针不增加引用计数,因此可以很好的解决上面的问题。
弱指针其实有点像 string_view,他们都只是一个视图(观察者)而已。
使用弱指针:
.expired()可以检测弱指针是否指向无效的内存。.lock()锁定指向的内容,把引用计数 +1,同时返回一个shared_ptr;如果已过期,那么会返回nullptr(这样甚至不需要std::optional)。
关系 #
unique_ptr 可以转化为 shared_ptr,反之不能转化。
unique_ptr 只能移动不能拷贝,shared_ptr 和 weak_ptr 可以拷贝。
shared_ptr 可以被 weak_ptr 创建一个视图。
weak_ptr 的成员函数 .lock() 有效时会返回一个 shared_ptr,否则会返回一个 nullptr。
组合、聚合和关联 #
| 属性 (Property) | 组合 (Composition) | 聚合 (Aggregation) | 关联 (Association) |
|---|---|---|---|
| 关系类型 | 整体/部分 | 整体/部分 | 其他不相关 |
| 部分可属于多个整体 | 否 | 是 | 是 |
| 部分的存在由整体管理 | 是 | 否 | 否 |
| 方向性 | 单向 | 单向 | 单向或双向 |
| 关系动词 | 是…的一部分 (Part-of) | 拥有 (Has-a) | 使用 (Uses-a) |
| **方式 | 类成员 | 引用 (数组) | 引用 (数组) |
初始化列表 initializer_list
#
如果想要让自己的类使用一个初始化列表来初始化,需要手动添加一个重载的构造函数:
template<typename T>
class MyClass{
private:
T m_size{};
T* m_data{};
public:
MyClass(int size) : m_size{size}, m_data{new T[static_cast<size_t>(size)]}{};
MyClass(std::initializer_list<T> list) : MyClass(static_cast<T>(list.size())){
// 用委托构造函数来创建一个
std::copy(list.begin(), list.end(), m_data);
}
// 3/5/0 法则的其他函数
}