C++primer第十三章 复制控制

  每种类型还定义了创建该类型的对象时会发生什么——构造函数定义了该类类型对象的初始化。类型还能控制复制、赋值或撤销该类型的对象时会发生什么——类通过特殊的成员函数:复制构造函数、赋值操作符和析构函数来控制这些行为。

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

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

  复制构造函数、赋值操作符和析构函数总称为复制控制。

  有一种特别常见的情况需要类定义自己的复制控制成员的:类具有指针成员。

13.1. 复制构造函数

  只有单个形参,而且该形参是对本类类型对象的引用(常用 const 修饰),这样的构造函数称为复制构造函数。

  • 根据另一个同类型的对象显式或隐式初始化一个对象。
  • 复制一个对象,将它作为实参传给一个函数。
  • 从函数返回时复制一个对象。
  • 初始化顺序容器中的元素。
  • 根据元素初始化式列表初始化数组元素。

对象的定义形式

  回忆一下,C++ 支持两种初始化形式(第 2.3.3 节):直接初始化和复制初始化。复制初始化使用 = 符号,而直接初始化将初始化式放在圆括号中。

  当用于类类型对象时,初始化的复制形式和直接形式有所不同:直接初始化直接调用与实参匹配的构造函数,复制初始化总是调用复制构造函数。复制初始化首先使用指定构造函数创建一个临时对象(第 7.3.2 节),然后用复制构造函数将那个临时对象复制到正在创建的对象:

string null_book = "9-999-99999-9"; // copy-initialization
string dots(10, ‘.‘); // direct-initialization
string empty_copy = string(); // copy-initialization
string empty_direct; // direct-initialization

  通常直接初始化和复制初始化仅在低级别上存在差异。然而,对于不支持复制的类型,或者使用非 explicit 构造函数(第 12.4.4 节)的进修,它们有本质区
别:

ifstream file1("filename"); // ok: direct initialization
ifstream file2 = "filename"; // error: copy constructor is private
// This initialization is okay only if
// the Sales_item(const string&) constructor is not explicit
Sales_item item = string("9-999-99999-9");

形参与返回值

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

初始化容器元素

  复制构造函数可用于初始化顺序容器中的元素。例如,可以用表示容量的单个形参来初始化容器(第 3.3.1 节)。容器的这种构造方式使用默认构造函数和复制构造函数:

// default string constructor and five string copy constructors invoked
vector<string> svec(5);

  编译器首先使用 string 默认构造函数创建一个临时值来初始化 svec,然后使用复制构造函数将临时值复制到 svec 的每个元素。

构造函数与数组元素

  如果没有为类类型数组提供元素初始化式,则将用默认构造函数初始化每个元素。然而,如果使用常规的花括号括住的数组初始化列表(第 4.1.1 节)来提供显式元素初始化式,则使用复制初始化来初始化每个元素。根据指定值创建适当类型的元素,然后用复制构造函数将该值复制到相应元素:

Sales_item primer_eds[] = {    string("0-201-16487-6"),
    string("0-201-54848-8"),
    string("0-201-82470-1"),
    Sales_item()
};

13.1.1. 合成的复制构造函数

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

  逐个成员初始化最简单的概念模型是,将合成复制构造函数看作这样一个构造函数:其中每个数据成员在构造函数初始化列表中进行初始化。

class Sales_item {
// other members and constructors as before
private:
    std::string isbn;
    int units_sold;
    double revenue;
};

  合成复制构造函数如下所示:

Sales_item::Sales_item(const Sales_item &orig):
    isbn(orig.isbn), // uses string copy constructor
    units_sold(orig.units_sold), // copies orig.units_sold
    revenue(orig.revenue) // copy orig.revenue
{     } // empty body

13.1.2. 定义自己的复制构造函数

  复制构造函数就是接受单个类类型引用形参(通常用 const 修饰)的构造函数:

class Foo {
public:
    Foo(); // default constructor
    Foo(const Foo&); // copy constructor
    // ...
};

  对许多类而言,合成复制构造函数只完成必要的工作。只包含类类型成员或内置类型(但不是指针类型)成员的类,无须显式地定义复制构造函数,也可以复制。

  然而,有些类必须对复制对象时发生的事情加以控制。这样的类经常有一个数据成员是指针,或者有成员表示在构造函数中分配的其他资源。而另一些类在创建新对象时必须做一些特定工作。这两种情况下,都必须定义复制构造函数。

  通常,定义复制构造函数最困难的部分在于认识到需要复制构造函数。只要能认识到需要复制构造函数,定义构造函数一般非常简单。复制构造函数的定义与其他构造函数一样:它与类同名,没有返回值,可以(而且应该)使用构造函数初始化列表初始化新创建对象的成员,可以在函数体中做任何其他必要工作。

13.1.3. 禁止复制

  有些类需要完全禁止复制。例如,iostream 类就不允许复制(第 8.1 节)。如果想要禁止复制,似乎可以省略复制构造函数,然而,如果不定义复制构造函数,编译器将合成一个。

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

  如果复制构造函数是私有的,将不允许用户代码复制该类类型的对象,编译器将拒绝任何进行复制的尝试。

  然而,类的友元和成员仍可以进行复制。如果想要连友元和成员中的复制也禁止,就可以声明一个(private)复制构造函数但不对其定义。

  声明而不定义成员函数是合法的,但是,使用未定义成员的任何尝试将导致链接失败。通过声明(但不定义)private 复制构造函数,可以禁止任何复制类类型对象的尝试:用户代码中复制尝试将在编译时标记为错误,而成员函数和友元中的复制尝试将在链接时导致错误。

13.2. 赋值操作符

  与类要控制初始化对象的方式一样,类也定义了该类型对象赋值时会发生什么:

Sales_item trans, accum;
trans = accum;

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

介绍重载赋值

  在介绍合成赋值操作符之前,需要简单了解一下重载操作符

  重载操作符是一些函数,其名字为 operator 后跟着所定义的操作符的符号。因此,通过定义名为 operator= 的函数,我们可以对赋值进行定义。像任何其他函数一样,操作符函数有一个返回值和一个形参表。形参表必须具有与该操作符数目相同的形参(如果操作符是一个类成员,则包括隐式 this 形参)。赋值是二元运算,所以该操作符函数有两个形参:第一个形参对应着左操作数,第二个形参对应右操作数。

  当操作符为成员函数时,它的第一个操作数隐式绑定到 this 指针。有些操作符(包括赋值操作符)必须是定义自己的类的成员。因为赋值必须是类的成员,所以 this 绑定到指向左操作数的指针。因此,赋值操作符接受单个形参,且该形参是同一类类型的对象。右操作数一般作为 const 引用传递。

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

合成赋值操作符

  合成赋值操作符与合成复制构造函数的操作类似。它会执行逐个成员赋值:右操作数对象的每个成员赋值给左操作数对象的对应成员。除数组之外,每个成员用所属类型的常规方式进行赋值。对于数组,给每个数组元素赋值。

// equivalent to the synthesized assignment operator
Sales_item& Sales_item::operator=(const Sales_item &rhs)
{
    isbn = rhs.isbn; // calls string::operator=
    units_sold = rhs.units_sold; // uses built-in int assignment
    revenue = rhs.revenue; // uses built-in double assignment
    return *this;
}

复制和赋值常一起使用

  一般而言,如果类需要复制构造函数,它也会需要赋值操作符。
  实际上,就将这两个操作符看作一个单元。如果需要其中一个,我们几乎也肯定需要另一个。

13.3. 析构函数

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

何时调用析构函数

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

// p points to default constructed object
Sales_item *p = new Sales_item;
{
    // new scope
    Sales_item item(*p); // copy constructor copies *p into item
    delete p; // destructor called on object pointed to by p
} // exit local scope; destructor called on item

何时编写显式析构函数

  许多类不需要显式析构函数,尤其是具有构造函数的类不一定需要定义自己的析构函数。仅在有些工作需要析构函数完成时,才需要析构函数。析构函数通常用于释放在构造函数或在对象生命期内获取的资源。

  如果类需要析构函数,则它也需要赋值操作符和复制构造函数,这是一个有用的经验法则。这个规则常称为三法则,指的是如果需要析构函数,则需要所有这三个复制控制成员。

合成析构函数

  合成析构函数按对象创建时的逆序撤销每个非 static 成员,因此,它按成员在类中声明次序的逆序撤销成员。对于类类型的每个成员,合成析构函数调用该成员的析构函数来撤销对象。

如何编写析构函数

  析构函数是个成员函数,它的名字是在类名字之前加上一个代字号(~),它没有返回值,没有形参。因为不能指定任何形参,所以不能重载析构函数。虽然可以为一个类定义多个构造函数,但只能提供一个析构函数,应用于类的所有对象。

  析构函数与复制构造函数或赋值操作符之间的一个重要区别是,即使我们编写了自己的析构函数,合成析构函数仍然运行。

class Sales_item {
public:
// empty; no work to do other than destroying the members,
// which happens automatically
    ~Sales_item() { }
// other members as before
};

  撤销 Sales_item 类型的对象时,将运行这个什么也不做的析构函数,它执行完毕后,将运行合成析构函数以撤销类的成员。合成析构函数调用 string 析构函数来撤销 string 成员,string 析构函数释放了保存 isbn 的内存。

时间: 2024-10-06 22:32:24

C++primer第十三章 复制控制的相关文章

c++primer(第五版) 第十三章 拷贝控制习题答案

纯原创    转载请注明出处:http://blog.csdn.net/axuan_k 13.2    13.3   13.4    13.5 #include<iostream> using namespace std; class Point{ int a; }; Point global; //13.4 Point foo_bar(Point arg) //1 { Point local = arg, *heap = new Point(global); //2,3 *heap = lo

【C++ Primer 第十三章】4. 拷贝控制示例

拷贝控制示例 1 #include<iostream> 2 #include<string> 3 #include<set> 4 using namespace std; 5 6 class Folder; 7 8 class Message { 9 friend void swap(Message&, Message&); 10 friend class Folder; 11 public: 12 explicit Message(const stri

C++Primer 第十三章

//1.当定义一个类时,我们显示地或隐式地指出在此类型的对象(注意这里是此类型的对象,而不包括此类型的指针)拷贝,移动,赋值,销毁时做什么.一个类通过定义五种特殊的成员函数来控制这些操作:拷贝构造函数,拷贝赋值运算符,移动构造函数,移动赋值运算符,析构函数. //当定义了五种特殊成员函数的其中一个的时候,一般也需要定义其他几个操作. //拷贝构造函数的第一个参数必须是一个引用类型,若第一个参数不是引用类型则会造成逻辑错误:传值调用会产生一个临时对象,而此对象必须通过类的拷贝构造函数生成,所以会陷

【C++ Primer】复制控制

十三.复制控制 1. 复制构造函数 类中的成员函数都默认为inline类型,所以即使在类定义体内的函数声明显示定义为inline类型,在进行函数定义时也能够将inline进行省略. // 复制构造函数应该为常量引用类型,如果允许传值参数会造成无限循环调用从而导致内存溢出. CopyConstruct(const CopyConstruct& a){value = a.value;} 复制构造函数可用于初始化顺序容器中的元素,如vector<string> svec(5); 这种方式使用

《C++ Primer》读书笔记—第十三章 控制拷贝

声明: 文中内容收集整理自<C++ Primer 中文版 (第5版)>,版权归原书所有. 学习一门程序设计语言最好的方法就是练习编程 第III部分,类设计者的工具 1.类是C++的核心概念.每个类都定义了一个新类型和在此类型对象上可执行的操作. 2.当定义一个类时,我们显式或隐式地指定在此类型的对象的拷贝.移动.赋值和销毁时做什么.一个类通过定义五种特殊的成员函数来控制这些操作,包括:拷贝构造函数(copy construcor),拷贝赋值运算符(copy-assignment operato

C++ Primer 学习笔记_67_面向对象编程 --转换与继承、复制控制与继承

面向对象编程 --转换与继承.复制控制与继承 I.转换与继承 引言: 由于每一个派生类对象都包括一个基类部分,因此能够像使用基类对象一样在派生类对象上执行操作. 对于指针/引用,能够将派生类对象的指针/引用转换为基类子对象的指针/引用. 基类类型对象既能够作为独立对象存在,也能够作为派生类对象的一部分而存在,因此,一个基类对象可能是也可能不是一个派生类对象的部分,因此,没有从基类引用(或基类指针)到派生类引用(或派生类指针)的(自己主动)转换. 关于对象类型,尽管一般能够使用派生类型的对象对基类

C++ Primer 学习笔记_68_面向对象编程 --构造函数和复制控制[续]

面向对象编程 --构造函数和复制控制[续] 三.复制控制和继承 合成操作对对象的基类部分连同派生类部分的成员一起进行复制.赋值或撤销,使用基类的复制构造函数.赋值操作符或析构函数对基类部分进行复制.赋值或撤销. 类是否需要定义复制控制成员完全取决于类自身的直接成员.基类可以定义自己的复制控制而派生类使用合成版本,反之,基类使用合成版本,而派生类使用自己定义的复制控制也可以. 只包含类类型或内置类型的数据成员.不包含指针的类一般可以使用合成操作,复制.赋值或撤销这样的成员不需要使用特殊控制.但是:

c++ primer plus(第6版)中文版 第十三章编程练习答案

第十三章编程练习答案 13.1根据Cd基类,完成派生出一个Classic类,并测试 //13.1根据Cd基类,完成派生出一个Classic类,并测试 #include <iostream> #include <cstring> using namespace std; // base class class Cd { char performers[50]; char label[20]; int selections; // number of selections double

C Primer Plus 第十三章 学习总结……2015.5.8

第十三章:文件的输入/输出 这一章学习起来,很迷惑,和上面几章并不太连贯,介绍了好多 用于文件输入输出的函数,就像高中学习数学一样,一下子出现好多 公式,虽然每个公式都知道是干什么的.怎么用,但就不一定把习题 做出来了.把本章看完后,只是大致了解了文件的各种输出输入,打 开等方式.所以还需要进一步的了解,实践应用. <重定向运算符,  echo_eof<words 该运算符把words文件与 stdin流关联起来,将该文件words内容引导至echo_eof程序中. echo_eof将键盘输入