C++ Primer 学习笔记_54_类与数据抽象 --复制构造函数、赋值操作符
复制控制
--复制构造函数、赋值操作符
引言:
当定义一个新类型时,需要显式或隐式地指定复制、赋值和撤销该类型的对象时会发生什么– 复制构造函数、赋值操作符和析构函数的作用!
复制构造函数:具有单个形参,该形参(常用const修饰)是对该类类型的引用。当定义一个新对象并用一个同类型的对象对它进行初始化时,将显式的使用复制构造函数;当将该类型的对象传递给函数或者从函数返回该类型的对象时,将隐式使用复制构造函数。
析构函数:作为构造函数的互补,当对象超出作用域或动态分配的对象被删除时,将自动应用析构函数。
赋值操作符:与构造函数一样,复制操作符可以通过指定不同类型的右操作数而重载。右操作数为类类型的版本比较特殊:如果我们没有编写这种版本,则编译器将为我们合成一个。
【小心地雷】
通常,编译器为我们合成的复制构造函数函数是非常精炼的---它们只做必须的工作,但对于类而言,依赖于默认定义有时会导致灾难!
一、复制构造函数
只有单个形参,而且该形参是对本类类型对象的引用(常用const修饰),这样的构造函数称为复制构造函数。与默认构造函数一样,复制构造函数可由编译器隐式调用。复制构造函数可用于:
1)根据另一个同类型的对象显式或隐式初始化一个对象。
2)复制一个对象,将它作为实参传给一个函数。
3)从函数返回时复制一个对象。
4)初始化顺序容器中的元素。
5)根据元素初始化式列表初始化数组元素。
1、对象的定义形式
对于类类型,初始化的复制形式和直接形式有所不同:直接初始化直接调用与实参匹配的构造函数,复制初始化式总是调用复制构造函数。[复制初始化首先使用指定构造函数创建一个临时对象,然后复制构造函数将那个临时对象复制到正在创建的对象!]
//理解下列语句的区别 string null_book = "9-99999-999-9"; string dots(10,‘.‘); string empty_copy = string(); string empty_direct;
对于类类型对象,只有指定单个实参或显式创建一个临时对象用于复制时,才使用复制初始化!
支持初始化的复制形式主要是为了与C的用法兼容,当情况许可时,可以允许编译器跳过复制构造函数函数直接创建对象,但是编译器没有义务这样做!
通常直接初始化和复制初始化仅在低级别上存在差异。然而,对于不支持复制的类型,或者使用非explicit构造函数的时候,它们有本质区别:
ifstream file1("filename"); //OK ifstream file2 = "filename";//Error,由于IO类型的对象不能复制 Sales_item item = string("9-99999-999-9");
2、形参与返回值
当形参为非引用类型的时候,将复制实参的值,类似的,以非引用类型作返回值时,将返回return语句中值的副本。
//当形参/返回值为类类型时,由复制构造函数进行复制。 //然而该函数的形参是const的引用,因此不会复制 string make_plural(size_t,const string &,const string &);
3、初始化容器元素
//首先使用string默认构造函数创建一个临时值来初始化svec //然后使用复制构造函数将临时值复制到svec的每个元素 vector<string> svec(5);
【推荐】
作为一般规则,除非你想使用容器元素的默认初始值,更有效的办法是,分配一个空容器并将已知元素的值加入容器。
4、构造函数与数组元素
如果没有为类类型数组提供元素初始化式,则将用默认构造函数初始化每个元素。然而,如果使用常规的花括号括住的数组初始化列表来提供显式元素初始化式,则使用复制初始化来初始化每个元素。根据指定值创建适当类型的元素,然后用复制构造函数将该值复制到相应元素:
Sales_item arrItem[] = {string("0-201-16487-6"), string("0-201-54848-6"), string("0-201-82470-6"), Sales_item() };
合成的复制构造函数
合成复制构造函数的行为是:执行逐个(非static)成员初始化,将新对象初始化为原对象的副本!
合成复制构造函数直接复制内置类型成员的值,类类型成员使用该类的复制构造函数进行复制。数组成员的复制是个例外。虽然一般不能复制数组,但如果一个类具有数组成员,则合成复制构造函数将复制数组。复制数组时合成复制构造函数将复制数组的每一个元素。
逐个成员初始化最简单的概念模型是,将合成复制构造函数看作这样一个构造函数:其中每个数据成员在构造函数初始化列表中进行初始化。
class Sales_item { public: Sales_item(const Sales_item &); private: std::string isbn; int units_sold; double revenue; }; Sales_item::Sales_item(const Sales_item &orig): isbn(orig.isbn),units_sold(orig.units_sold),revenue(orig.revenue) {}
定义自己的复制构造函数
class Foo { public: Foo(); Foo(const Foo &); //复制构造函数 };
复制构造函数的形参通常是一个const引用;因为由于向函数传递对象和从函数返回对象,该构造函数一般不应设置为explicit!
对许多类而言,合成复制构造函数只完成必要的工作。只包含类类型成员或内置类型(但不是指针类型)成员的类,无须显式地定义复制构造函数,也可以复制。
然而,有些类必须对复制对象时发生的事情加以控制。这样的类经常有一个数据成员是指针或者有成员表示在构造函数中分配的其他资源,而另一些类在创建新对象时必须做一些特定工作。这两种情况下,都必须定义复制复制构造函数!
通常,定义复制构造函数最困难的部分在于认识到需要复制构造函数O(∩_∩)O~。只要能认识到需要复制构造函数,定义构造函数一般非常简单。复制构造函数的定义与其他构造函数一样:它与类同名,没有返回值,可以(而且应该)使用构造函数初始化列表初始化新创建对象的成员,可以在函数体中做任何其他必要工作。
//P410 习题13.4 class NoName { public: NoName():pstring(new std::string),i(0),d(0){} NoName(const NoName &temp):i(temp.i),d(temp.d) { pstring = new std::string; *pstring = *(temp.pstring); } private: std::string *pstring; int i; double d; };
禁止复制
有些类需要完全禁止复制。例如,iostream类就不允许复制。如果想要禁止复制,似乎可以省略复制构造函数,然而,如果不定义复制构造函数,编译器将合成一个。
通过声明但不定义private复制构造函数可以禁止任何复制类类型对象的尝试:用于代码中的复制尝试将在编译时标记为错误,而成员函数和友元中的复制尝试将在链接时导致错误!
大多数类应定义复制构造函数和默认构造函数
不定义复制构造函数和/或默认构造函数,会严重局限类的使用:不允许复制的类对象只能作为引用传递给函数或从函数返回,它们也不能用作容器的元素。
一般来说,最好显式或隐式定义默认构造函数和复制构造函数。只有不存在其他构造函数时才合成默认构造函数。如果定义了复制构造函数,也必须定义默认构造函数。
二、赋值操作符
与复制构造函数一样,如果类没有定义自己的赋值操作符,则编译器会合成一个!
1、介绍重载赋值
重载操作符是一些函数,其名字为operator后跟着所定义的操作符的符号。因此,通过定义名为operator=的函数,我们可以对赋值进行定义。像任何其他函数一样,操作符函数有一个返回值和一个形参表。形参表必须具有与该操作符数目相同的形参(如果操作符是一个类成员,则包括隐式this形参)。赋值是二元运算,所以该操作符函数有两个形参:第一个形参对应着左操作数,第二个形参对应右操作数。
大多数操作符可以定义为成员函数或非成员函数。当操作符为成员函数时,它的第一个操作数隐式绑定到this指针。有些操作符(包括赋值操作符)必须是定义自己的类的成员。因为赋值必须是类的成员,所以this绑定到指向左操作数的指针。因此,赋值操作符接受单个形参,且该形参是同一类类型的对象。右操作数一般作为const引用传递。
赋值操作符也返回对同一类类型的引用。
class Sales_item { public: Sales_item &operator=(const Sales_item &); };
2、合成赋值操作符
合成赋值操作符会执行逐个成员赋值:右操作数对象的每个成员赋值给左操作数对象的对应成员。除数组之外,每个成员用所属类型的常规方式进行赋值。对于数组,给每个数组元素赋值。如:
Sales_item &Sales_item::operator=(const Sales_item &rhs) { isbn = rhs.isbn; units_sold = rhs.units_sold; revenue = rhs.revenue; return *this; }
3、复制和赋值常一起使用
实际上,应该将复制构造函数和赋值操作符看做一个单元,如果需要其中一个,我们几乎也可以肯定需要另一个!
//P412 习题13.9 NoName &NoName::operator=(const NoName &rhs) { if (pstring) { delete pstring; } pstring = new string(); *pstring = *(rhs.pstring); i = rhs.i; d = rhs.d; return *this; }
//习题13.10 class Employee { public: typedef unsigned int num_type; Employee(const std::string Name = "NoName"):name(Name),mark(count) { set(); } Employee(const Employee &rhs):name(rhs.name),mark(count) { set(); } ~Employee() { -- count; } Employee &operator=(const Employee &rhs) { name = rhs.name; return *this; } ostream &output(ostream &os) { os << "Name: " << name << "\t\tMark: " << mark << endl; return os; } private: std::string name; num_type mark; static num_type count; void set() { ++ count; } }; Employee::num_type Employee::count = 0;