C++Primer学习笔记(九)复制控制

复制构造函数是一种特殊构造函数,具有单个形参,该形参(常用 const 修饰)是对该类类型的引用。当定义一个新对象并用一个同类型的对象对它进行初始化时,将显式使用复制构造函数。 当将该类型的对象传递给函数或函数返回该类型的对象时,将隐式使用复制构造函数。

析构函数是构造函数的互补:当对象超出作用域或动态分配的对象被删除时,将自动应用析构函数。析构函数可用于释放对象时构造或在对象的生命期中所获取的资源。

复制构造函数、赋值操作符和析构函数总称为复制控制。
有一种特别常见的情况需要类定义自己的复制控制成员的:类具有指针成员。

13.1. 复制构造函数

只有单个形参,而且该形参是对本类类型对象的引用(常用 const 修饰),这样的构造函数称为复制构造函数。
复制构造函数可用于:
• 根据另一个同类型的对象显式或隐式初始化一个对象。
• 复制一个对象,将它作为实参传给一个函数。
• 从函数返回时复制一个对象。
• 初始化顺序容器中的元素。
• 根据元素初始化式列表初始化数组元素。

对象的定义形式

C++ 支持两种初始化形式:直接初始化和复制初始化。复制初始化使用 = 符号,而直接初始化将初始化式放在圆括号中。

直接初始化直接调用与实参匹配的构造函数,复制初始化总是调用复制构造函数。

对于类类型对象,只有指定单个实参或显式创建一个临时对象用于复制时,才使用复制初始化。

支持初始化的复制形式主要是为了与 C 的用法兼容。                                                              

形参与返回值

当形参为非引用类型的时候,将复制实参的值。类似地,以非引用类型作返回值时,将返回 return 语句 中的值的副本。

当形参或返回值为类类型时,由复制构造函数进行复制。

初始化容器元素

复制构造函数可用于初始化顺序容器中的元素。

构造函数与数组元素

如果没有为类类型数组提供元素初始化式,则将用默认构造函数初始化每个元素。

如果使用常规的花括号括住的数组初始化列表来提供显式元素初始化式,则使用复制初始化来初始化每个元素。

合成的复制构造函数

如果我们没有定义复制构造函数,编译器就会为我们合成一个

合成复制构造函数的行为是,执行逐个成员初始化,将新对象初始化为原对象的副本。

合成复制构造函数直接复制内置类型成员的值,类类型成员使用该类的复制构造函数进行复制。

定义自己的复制构造函数

有些类必须对复制对象时发生的事情加以控制。

而另一些类在创建新对象时必须做一些特定工作。

这两种情况下,都必须定义复制构造函数。                                                           

禁止复制

为了防止复制,类必须显式声明其复制构造函数为 private。

大多数类应定义复制构造函数和默认构造函数

不定义复制构造函数和/或默认构造函数,会严重局限类的使用。不允许复制的类对象只能作为引用传递给函数或从函数返回,它们也不能用作容器的元素。

如果定义了复制构造函数,也必须定义默认构造函数。

13.2. 赋值操作符

与复制构造函数一样,如果类没有定义自己的赋值操作符,则编译器会合成一个

介绍重载赋值

重载操作符是一些函数,其名字为 operator 后跟着所定义的操作符的符号。因此,通过定义名为 operator= 的函数,我们可以对赋值进行定义。

像任何其他函数一样,操作符函数有一个返回值和一个形参表。形参表必须具有与该操作符数目相同的形参(如果操作符是一个类成员,则包括隐式 this 形参)。赋值是二元运算,所以该操作符函数有两个形参:第一个形参对应着左操作数,第二个形参对应右操作数。

大多数操作符可以定义为成员函数或非成员函数。当操作符为成员函数时,它的第一个操作数隐式绑定到 this 指针。

赋值操作符的返回类型应该与内置类型赋值运算返回的类型相同。内置类型的赋值运算返回对右操作数的引用,因此,赋值操作符也返回对同一类类型的引用。

合成赋值操作符

合成赋值操作符与合成复制构造函数的操作类似。它会执行逐个成员赋值:右操作数对象的每个成员赋值给左操作数对象的对应成员。

除数组之外,每个成员用所属类型的常规方式进行赋值。

复制和赋值常一起使用

实际上,就将这两个操作符看作一个单元。如果需要其中一个,我们几乎也肯定需要另一个。

13.3. 析构函数

构造函数的一个用途是自动获取资源。例如,构造函数可以分配一个缓冲区或打开一个文件,在构造函数中分配了资源之后,需要一个对应操作自动回收或释放资源。

何时调用析构函数

撤销类对象时会自动调用析构函数

动态分配的对象只有在指向该对象的指针被删除时才撤销。如果没有删除指向动态对象的指针,则不会运行该对象的析构函数,对象就一直存在,从而导致内存泄漏,而且,对象内部使用的任何资源也不会释放。

当对象的引用或指针超出作用域时,不会运行析构函数。只有删除指向动态分配对象的指针或实际对象(而不是对象的引用)超出作用域时,才会运行析构函数。

合成析构函数

与复制构造函数或赋值操作符不同,编译器总是会为我们合成一个析构函数。合成析构函数按对象创建时的逆序撤销每个非 static 成员,因此,它按成员在类中声明次序的逆序撤销成员。

如何编写析构函数

析构函数是个成员函数,它的名字是在类名字之前加上一个代字号(~),它没有返回值,没有形参。

13.5. 管理指针成员

本书始终提倡使用标准库。这样做的一个原因是,使用标准库能够大大减少现代 C++ 程序中对指针的需要。

包含指针的类需要特别注意复制控制,原因是复制指针时只复制指针中的地址,而不会复制指针指向的对象。

类设计者必须首先需要决定的是该指针应提供什么行为。将一个指针复制到另一个指针时,两个指针指向同一对象。当两个指针指向同一对象时,可能使用任一指针改变基础对象。类似地,很可能一个指针删除了一对象时,另一指针的用户还认为基础对象仍然存在。

大多数 C++ 类采用以下三种方法之一管理指针成员:
1. 指针成员采取常规指针型行为。这样的类具有指针的所有缺陷但无需特殊的复制控制。
2. 类可以实现所谓的“智能指针”行为。指针所指向的对象是共享的,但类能够防止悬垂指针。
3. 类采取值型行为。指针所指向的对象是唯一的,由每个类对象独立管理。

定义智能指针类

用户仍然可以通过普通指针访问对象,但绝不能删除指针。HasPtr 类将保证在撤销指向对象的最后一个 HasPtr 对象时删除对象。

为了编写析构函数,需要知道这个 HasPtr对象是否为指向给定对象的最后一个。

引入使用计数
定义智能指针的通用技术是采用一个使用计数。智能指针类将一个计数器与类指向的对象相关联。使用计数为 0 时,删除对象。

唯一的创新在于决定将使用计数放在哪里。计数器不能直接放在 HasPtr 对象中

// private class for use by HasPtr only
class U_Ptr {
  friend class HasPtr;
  int *ip;
  size_t use;
  U_Ptr(int *p): ip(p), use(1) { }
  ~U_Ptr() { delete ip; }
};

这个类的所有成员均为 private。我们不希望用户使用 U_Ptr 类,所以它没有任何 public 成员。将 HasPtr 类设置为友元,使其成员可以访问 U_Ptr 的成员。

U_Ptr 类保存指针和使用计数,每个 HasPtr 对象将指向一个 U_Ptr 对象,使用计数将跟踪指向每个U_Ptr 对象的 HasPtr 对象的数目 

赋值与使用计数

首先将右操作数中的使用计数加 1,然后将左操作数对象的使用计数减 1 并检查这个使用计数。像析构函数中那样,如果这是指向 U_Ptr 对象的最后一个对象,就删除该对象,这会依次撤销 int 基础对象。将左操作数中的当前值减 1(可能撤销该对象)之后,再将指针从 rhs 复制到这个对象。赋值照常返回对这个对象的引用。

这个赋值操作符在减少左操作数的使用计数之前使 rhs 的使用计数加 1,从而防止自身赋值。

定义值型类
处理指针成员的另一个完全不同的方法,是给指针成员提供值语义。

posted @ 2020-08-06 19:23  thsj  阅读(59)  评论(0)    收藏  举报