目录
使用 “ =delete ” 来阻止成员的拷贝、赋值 操作 ( 449 )
“ =default ” 和 “ delete ” 有哪些不同呢?
*析构函数 最好不要使用 =delete (450 page)
使用 private 来阻止拷贝构造、赋值运算符完成拷贝 (451P)
使用 copy and swap 技术来避免 “ operator= ” 中的 “ 自我赋值 ” (459P)
使用标准库 move 函数 将左值转换为对应的右值引用类型(472P)
某类如果定义了移动操作,那么必须定义自定义的拷贝操作,否则,拷贝操作将是已删除的(477P)
当一个类既有移动和拷贝操作,那么使用普通的函数匹配规则来确定到底使用哪个函数 (477P)
如果某类有一个可以的拷贝操作而未定义移动操作,则对象是通过拷贝操作来 “ 移动 ” 的 (477P)
使用 Copy 和 Swap 技术 来避免在 移动操作中 “ 自我赋值 ” (478P)
更新 三/五法则 —— 通常,如果一个类定义了其中任何一个拷贝控制操作成员,它通常应该定义所有这些操作 (479P)
通常,拷贝资源需要一定的开销。定义移动构造函数和移动赋值运算符的类可以在并非需要拷贝的情况下避免这种开销(479P)
移动迭代器 的解引用运算符返回一个 右值引用( 480P )
使用 make_move_iterator 操作将普通迭代器 转换为 移动迭代器 ( 480P )
使用 引用限定符 & 或 && 来指出 this 的左值 / 右值 属性 (483P)
Preface
当我们定义一个类时, 我们可以显式 或 隐式地定义:拷贝构造函数,拷贝赋值运算符,移动构造函数,移动赋值运算符 ,析构函数。
- 拷贝和移动构造函数定义了 —— 当用同类型的另一个对象初始化本对象时做什么。
- 拷贝和移动赋值操作符定义了—— 当我们将一个类类型的对象赋值给另一个相同类类型的对象时会发生什么。
- 析构函数定义当此类型对象销毁时做什么。
我们将这些操作统称为 拷贝控制操作。
(合成)拷贝构造函数
- 拷贝构造函数在大多数中会被隐式地调用,所以拷贝构造函数不应该是 explicit 的。
- 拷贝构造函数的形参一般最好是 const的引用
合成拷贝构造函数 和 合成的默认构造函数 不同在于:
- 即使我们类中定义了其它的构造函数, 编译器也会为我们合成一个拷贝构造函数( 系统合成的拷贝构造函数是在需要的时候编译器才会去合成).
- 而对于合成的默认构造函数,只要类中定义任何一种构造函数,那么编译器就不会再去生成一个合成的默认构造函数。
那么合成的拷贝构造函数是如何工作的呢?
- 在需要的时候将实参对象中的成员逐个拷贝到正在创建的对象中。那么编译器从给定的对象中依次将每个非 static 的成员 拷贝到正在创建的对象中。—— static 成员是不会被拷贝的, 因为static 成员 被所有对象所共享, 而且它们的数据值都一样。
为什么 拷贝构造函数 的第一个参数必须是引用类型?
- 如果其参数不是引用类型, 则调用永远也不会成功,因为为了调用拷贝构造函数, 必须拷贝它的实参,但为了拷贝实参, 又需要调用拷贝构造函数, 如此无限循环。442P
一个类中的每个成员的类型决定了它如何拷贝:
- 对类类型的成员, 会使用其拷贝构造函数来拷贝;
- 内置类型的成员则直接拷贝。虽然我们不能直接拷贝一个数组, 但合成拷贝构造函数会逐元素地拷贝一个数组类型的成员。如果数组元素是类类型, 则使用元素的拷贝构造函数来进行拷贝。
拷贝初始化 和 值初始化的 区别 ( 441P)
string dots(10, '.'); // direct initialization
string s(dots); // direct initialization
string s2 = dots; // copy initialization
string null_book = "9-999-99999-9"; // copy initialization
string nines = string(100, '9'); // copy initialization
直接初始化和 拷贝初始化的区别在于:
当我们使用直接初始化时:
- 编译器首先将调用的实参与重载集合中每一个构造函数的形参进行比较,然后根据比较的结果来选择我们提供的实参最匹配的构造函数。
当我们使用拷贝初始化时:
- 编译器将 “ = ” 右侧对象拷贝到正在创建的对象中, 如果需要的话还要进行类型转换。
拷贝初始化通常来说是使用拷贝构造函数来完成的。 但也有例外,比如说: 如果一个类中有一个移动构造函数 ,则拷贝初始化有时候会使用移动构造函数而非拷贝构造函数来完成初始化。
整体来说,拷贝初始化时是依靠 拷贝构造函数 和 移动构造函数来完成的。
拷贝初始化何时会发生呢?( 441P)
- 在我们用 “ = ” 定义变量时会发生。
- 将一个对象作为实参传递给一个非引用类型的形参。 如果该形参是引用或者指针类型的话,就不会调用其拷贝构造函数
- 从一个返回类型为非引用类型的函数返回一个值。 例如:该值可以被用来初始化调用方的结果。442P
- 用花括号列表初始化一个数组中的元素或一个聚合类中的成员
- 某些类类型还对它们分配的对象使用拷贝初始化—— 比如说: 初始化标准库容器 或 调用其 insert 、push 成员时,容器会对元素进行拷贝初始化。用 emplace 成员创建的元素都进行直接初始化的方式。
拷贝初始化的限制 ( 442 P)
如果我们使用的初始值需要通过一个 explicit 构造函数来进行转换,那么使用 拷贝初始化 还是直接初始化 就很重要:
vector<int> v1(10); // ok: direct initialization
vector<int> v2 = 10; // 错误: 接受大小参数的构造函数是 explicit 的
void f(vector<int>)
{
}
int main()
{
f(10); // 错误: 不能用一个 explicit 的构造函数拷贝一个实参
f(vector < int>(10)); // 正确: 从一个 int 直接构造一个临时的 vector, 所谓的显式转换
system("pause");
return 0;
}
explicit 构造函数只能用于直接初始化。
练习题13.1:拷贝构造函数什么时候会被调用
拷贝构造函数是一个类中 的构造函数 ,并且其参数是 自身类类型的引用。并且所有其他参数(如果有的话)都有默认值,则此构造函数是拷贝构造函数
- 当我们使用一个 已有对象初始化给一个新对象时
- 将一个对象作为实参传递给一个非引用类型的形参
- 如果函数的返回值是类的对象, 函数执行完成返回调用者时
- 用花括号列表初始化一个数组中的元素或一个聚合类中的成员。
- 初始化标准库容器或调用其 insert / push操作时,容器会对其元素进行拷 贝初始化。
练习题13.2:
- 因为当调用该构造函数时必须传递一个 Sales_data 类型的对象,此时,会调用拷贝构造函数, 但调用永远也不会成功。因为其自身的参数也是非引用类型, 为了调用它 , 必须拷贝其实参,而为了拷贝实参,又需要调用拷贝构造函数,也就是其自身,从而造成死循环。
使用直接初始化可以跳过 调用拷贝构造函数 (442P)
(合成)拷贝赋值运算符 ( 443P )
- 为了与内置类型的赋值保持一致, 赋值运算符通常返回一个指 向其左侧运算对象的引用。另外值得注意的是, 标准库通常要求保存在容器中的元素类型要具有赋值运算符, 且其返回值是左侧运算对象的引用。
什么时候 合成的赋值运算符是可以用来禁止该类型的对象赋值?(450P)
- 如果类的某个成员的拷贝赋值运算符是删除的或不可以访问的,
- 或是 类有一个 const的 或引用成员,
- 则类的合成拷贝赋值运算符被定义为删除的。
合成的拷贝赋值运算符是如何工作的?
- 它会将右侧运算对象的每一个 非 static 成员 赋予左侧运算对象的对应成员
- 对于数组类型的成员,会逐个赋值数组元素。
练习题13.6:
- 拷贝赋值运算符函数是一个接受与类相同类型参数的函数,定义为类的成员函数, 左侧运算对象绑定到隐含的 this 参数, 而右侧运算对象是所属类类型的, 作为函数的参数, 函数返回指向其左侧运算对象的引用。
- 当用一个对象赋值给另一个对象时
- 它会将右侧运算对象的每一个 非 static 成员 赋予左侧运算对象的对应成员,对于数组类型的成员,会逐个赋值数组元素。
- 当一个类中,如果没有拷贝赋值运算符函数,在必要使用时,编译器自动生成。
(合成)析构函数 (444P)
析构函数不能被重载,所以一个类中只能有一个析构函数。 它没有参数 和返回值。
构造函数 和 析构函数 是如何完成工作的?
- 在一个构造函数中,成员的初始化是在函数体执行之前完成的, 且按照它们在类中出现的顺序进行初始化。
- 在一个析构函数中, 首先执行函数体, 然后销毁成员。成员按初始化顺序是按逆序销毁的。
- 注意: 合成的析构函数和未在“ { } 里”显式销毁一个内置指针成员,那么该内置指针成员是不会delete 所指向的对象。 与普通指针不同的是, 智能指针是类类型,所以具有析构函数。因此,与普通指针不同,智能指针成员在析构阶段会被自动销毁。
- 当指向一个对象的引用或指针离开作用域时, 析构函数不会执行。
- 析构函数的函数体自身并不直接销毁成员,成员是在析构函数体之后的隐式阶段中销毁的。那么该成员如何被销毁,依赖于该成员的类型。 比如销毁类类型成员,调用其自身的析构函数; 如果是内置类型,因为其没有析构函数,所以销毁内置类型的成员什么也不需要做。
练习题13.9:
- 析构函数是一个类的成员函数,名字由波浪号接类名构成,没有返回值,也不接受参数,也不可以被重载。
- 对于某些类来说,合成的析构函数可以用来阻止该类型的对象被销毁。否则,该类的成员会在函数体之后隐含的析构阶段中被销毁。
- 当一个对象不再使用时,该类又没有自定义析构函数,编译器会合成析构函数,来进行该对象在生存期分配的所有资源
练习题13.12:
会发生三次析构函数调用,分别是参数 accum ,对象item1,对象item2. 当函数结束时,它们都会调用析构函数
参数 trans,在函数结束时,其生命期也结束了。但是不会调用析构函数。 因为指向一个对象的引用或指针离开作用域时,不会调用析构函数
什么时候会调用析构函数 (445P)
三 / 五法则 ( 447 P)
需要析构函数的类也需要拷贝和赋值操作 ( 447P)
- 如果一个类需要自定义的析构函数,那么它几乎肯定它也需要一个自定义的拷贝构造函数和拷贝赋值操作符。
- 但是如果某类需要拷贝构造函数和拷贝赋值操作符。但是不一定需要 析构函数( 448P )
- 合成的析构函数不会delete 内置指针成员所指向的对象, 因此,我们需要定义一个析构函数来释放构造函数动态分配的内存。
如果某类定义了一个析构函数,但是使用的是合成版本的拷贝构造函数 和 拷贝赋值运算符时,考虑会发生什么?
- 这些函数会简单的拷贝指针成员,意味着该类的多个对象可能指向相同的内存,
需要拷贝操作的类也需要赋值操作,反之亦然 (448P)
- 如果一个类需要一个拷贝构造函数, 几乎可以肯定它也需要一个拷贝赋值运算符。
- 如果一个类需要一个拷贝赋值运算符, 几乎可以肯定它也需要一个拷贝构造函数。然而, 无论是需要拷贝构造函数还是需要拷贝赋值运算符都不必然意味着一定也需要析构函数。
对拷贝控制成员使用 =default (449P)
class Sales_data
{
public:
// 类内使用 = default 修饰成员的声明时, 生成相应合成的版本将隐式为内联的
Sales_data() = default;
Sales_data(const Sales_data&) = default;
Sales_data& operator=(const Sales_data &);
~Sales_data() = default;
};
// 如果我们不希望合成相应版本为内联的, 应该只对成员的类外定义使用= default
Sales_data& Sales_data::operator=(const Sales_data&) = default;
注意: 只能对默认构造函数 、拷贝构造函数、拷贝赋值运算符 、析构函数 这些成员函数 使用 =default, 来显式生成合成的相应版本。
使用 “ =delete ” 来阻止成员的拷贝、赋值 操作 ( 449 )
- 大多数类应该定义默认构造函数、拷贝构造函数 和 拷贝赋值运算符, 无论是隐式地还是显式地。
当某些类既不需要显式的拷贝控制成员,也不需要合成的版本时, 应该采取某种机制阻止生成合成的版本 。 此时应该怎么办?
- 我们可以通过将拷贝构造函数和拷贝赋值运算符定义为删除的函数(deleted function )来阻止拷贝。
- 删除的函数是这样一种函数: 我们虽然声明了它们, 但不能以任何方式使用它们。在函数的参数列表后面加上 =delete 来指出我们希望将它定义为删除的:
class NoCopy { public: NoCopy() = default; // 使用合成的默认构造函数 NoCopy(const NoCopy&) = delete; // 阻止拷贝 NoCopy &operator=(const NoCopy&) = delete; //阻止赋值 ~NoCopy() = default; //使用合成的析构函数 // other members }; int main() { NoCopy myNoCopy; NoCopy mmNoCopy; NoCopy ttNoCopy = myNoCopy; // 错误 ttNoCopy = myNoCopy; // 错误 system("pause"); return 0; }