13
拷贝构造函数
-
拷贝构造函数第一个参数必须是引用类型 通常是一个const引用
-
拷贝构造函数在很多情况下会被隐式使用 因此拷贝构造函数通常不应该是explicit的
-
不管有没有自定义其他构造函数 编译器都会合成一个拷贝构造函数
-
合成的拷贝构造函数将其参数的非static成员逐个的拷贝到正在创建的对象中
-
每个成员的类型决定了其如何拷贝 类类型用拷贝构造函数拷贝内置类型直接拷贝
-
数组虽然不能直接拷贝 但合成拷贝构造函数会逐元素的拷贝一个数组类型的成员
-
-
合成拷贝赋值运算符和拷贝构造函数同理
struct A
{
int arr[2];
};
A a;
a.arr[0] = 1;
a.arr[1] = 2;
cout << a.arr[0] << a.arr[1] << endl;
A b = a;
cout << b.arr[0] << b.arr[1] << endl;
// 数组不允许直接拷贝和赋值 下面的赋值会编译错误
// int arr1[2];
// int arr2[2] = arr1;
析构函数
-
在析构函数中
首先执行函数体 其次按照初始化顺序的逆序销毁成员
这里需要特别注意 析构函数本身并不直接销毁成员 成员是在函数体执行之后的第二阶段隐式自动的销毁 -
通常在析构中处理生存期分配的资源的释放
-
销毁类类型时执行成员自己的析构函数 内置类型成员的销毁什么都不需要处理
-
隐式的销毁一个内置指针类型成员不会delete其所指向的对象 但是智能指针可以 因为智能治指针时类类型
-
析构时机
-
变量离开作用域
-
对象被销毁时 成员会被销毁
-
容器(标准库或者数组)被销毁时 其元素也会被销毁
-
动态分类的对象delete时被销毁
-
临时对象 当创建他的完整表达式结束时被销毁
-
-
合成拷贝构造函数也可以使用
=default
的方式 -
定义删除的函数 将拷贝构造函数和拷贝赋值运算符定义为=delete可以阻止拷贝
-
=delete必须出现在函数第一次声明时
因为=default直到编译器生成代码时才需要 但是=delete编译器需要直到其是delete的以便阻止试图使用它的操作 -
析构函数不能是删除的成员 因为是删除的就无法销毁了
如果定义了一个删除析构函数的类型 那么编译器将不允许定义该类型的变量或者创建该类的临时对象
虽然可以new出来一个动态分配的对象但是却不能delete -
合成的拷贝控制成员可能是删除的情况有很多
-
类的某个成员的
析构函数
是删除的或者不可访问 则类的合成析构函数
定义为删除 -
类的某个成员的
拷贝构造函数
是删除的或者不可访问或者
类的某个成员的析构函数
是删除的或者不可访问
则类的合成拷贝构造函数
定义为删除 -
类的某个成员的
拷贝赋值运算符
是删除的或者不可访问或者
类有一个const成员或者
类有一个引用成员
则类的合成拷贝赋值运算符
定义为删除 -
类的某个成员的
析构函数
是删除的或者不可访问
或者
类有一个引用成员且没有类内初始值
或者
类有一个const成员且没有类内初始值且其类型未显示定义默认构造函数
则类的默认构造函数
定义为删除 -
还有更多情况(比如 移动构造部分 继承部分 union部分)导致定义为删除函数
-
-
交换操作swap
-
对于那些与重排元素顺序的算法一起使用的类 定义swap非常重要
在这类算法中交换两个元素需要用到swap -
如果一个类定义了自己的swap 那么算法将使用类自定义版本 否则算法将使用标准库定义的swap
-
应该尽量使用swap而不是std::swap 后者直接点名是std中的swap 而不会使用我们的自定义swap
-
定义swap的操作经常使用swap来定义赋值运算符 这些运算符使用一种
拷贝并交换
的技术
这种技术将左侧运算对象与右侧运算对象的一个副本进行交换
书上说是这种技术自动处理了自身赋值的情况并且也是异常安全的P459
其实就是将赋值运算符的参数改为非引用 然后参数的传递就会进行拷贝 然后函数体调用swap 大概就这意思
-
-
拷贝赋值运算符通常需要执行拷贝构造函数和析构函数的工作 通常公共的工作应该放在private的工具函数中
对象移动与右值引用
-
这里就比较复杂了这里就比较复杂了
-
右值引用
-
&&符号
-
右值引用只能绑定到一个即将销毁的对象 因此可以自由的将一个右值引用的资源
移动
到另一个对象 -
常规引用即左值引用(非const的)只能绑定到左值不能绑定到字面值常量或者返回右值的表达式等等
但是右值引用可以绑定到这些东西 但是不能讲右值引用绑定到一个左值 -
变量表达式是左值 不能讲右值引用直接绑定到变量上 就算这个变量是右值引用也不行
-
-
虽然不能将右值引用直接绑定到左值 但是可以显示的将左值转换为对应的右值引用类型
通过标准库的move函数获得绑定到左值的右值引用 -
move在头文件utility中
-
调用std::move(x)就意味着除了对x进行赋值或者销毁 我们不在使用x 调用move之后不能对x的值做任何假设 我们不能使用他
-
尽量使用std::move 避免名字冲突
-
移动构造函数 移动赋值运算符
-
类似拷贝构造函数移动构造函数第一个参数也是该类型的一个引用 只不过是一个右值引用
除了完成资源移动还必须保证移后源对象处于一个这样的状态-----销毁他是无害的
一旦完成资源的移动必须保证源对象不再指向被移动的资源 这些资源已经被新创建的对象接管 -
移动操作不应该抛出任何异常 要使用
noexcept
noexcept
需要出现在参数列表之后 初始化冒号之前 且必须在声明和定义都同时指定noexcept
-
移动赋值运算符同理也需要
noexcept
-
只有当一个类没有定义任何的拷贝控制成员 且每个非static数据成员都可以移动
编译器才会合成移动构造函数或者移动赋值运算符 -
移动操作永远不会隐式定义为删除的函数
-
一个类如果定义了移动构造函数或者移动赋值运算符 那么该类的
合成
拷贝构造函数和拷贝赋值运算符定义是删除的
所以如果定义了移动构造函数 那么基本必须定义拷贝构造函数 不然默认是删除的 gg -
对没有定义移动构造函数的类执行std::move其实调用的就是拷贝构造函数啦啦
-
移动迭代器
-
移动迭代器解引用生成右值
-
标准库的 make_move_iterator 可以将普通迭代器转换为移动迭代器
-
-
-
引用限定符
可以是左值引用限定符 也可以是右值引用限定符 可以指定this是左值属性还是右值属性-
成员函数可以同时用const和引用限定符限定 但是引用限定符号在cosnt之后
-
也可以根据引用限定符号来进行重载版本区分
-
定义两个或者两个以上具有相同名字和参数的成员函数 要么全部都加引用限定 要么都不加
-
引用限定需要在声明处和定义处都出现
-
定义行为像值的类 代码的注释很重要 要看要看要看
class HasPtr
{
public:
HasPtr(const std::string &s = std::string()) : ps(new std::string(s)), i(0)
{
}
HasPtr(const HasPtr &p) : ps(new std::string(*p.ps)), i(p.i) {
}
HasPtr &operator=(const HasPtr &rhs);
~HasPtr() {
delete ps; }
private:
std::string *ps;
int i;
};
// 正确示范
// 拷贝赋值运算符 通常需要保证两点
// 1. 把一个对象赋予自身必须正确工作
// 2. 通常拷贝赋值运算符需要组合析构函数和拷贝构造函数的工作
HasPtr &HasPtr::operator=(const HasPtr &rhs)
{
auto newp = new string(*rhs.ps);
delete ps;