当定义一个新类型时,需要显式或隐式地指定复制、赋值和撤销该类型的对象时会发生什么– 复制构造函数、赋值操作符和析构函数的作用!
复制构造函数:具有单个形参,该形参(常用const修饰)是对该类类型的引用。当定义一个新对象并用一个同类型的对象对它进行初始化时,将显式的使用复制构造函数;当将该类型的对象传递给函数或者从函数返回该类型的对象时,将隐式使用复制构造函数。
析构函数:作为构造函数的互补,当对象超出作用域或动态分配的对象被删除时,将自动应用析构函数。
赋值操作符:与构造函数一样,复制操作符可以通过指定不同类型的右操作数而重载。右操作数为类类型的版本比较特殊:如果我们没有编写这种版本,则编译器将为我们合成一个。
一、复制构造函数
只有单个形参,而且该形参是对本类类型对象的引用(常用const修饰),这样的构造函数称为复制构造函数。与默认构造函数一样,复制构造函数可由编译器隐式调用。复制构造函数可用于:
1)根据另一个同类型的对象显式或隐式初始化一个对象。
2)复制一个对象,将它作为实参传给一个函数。
3)从函数返回时复制一个对象。
4)初始化顺序容器中的元素。
5)根据元素初始化式列表初始化数组元素。
对于类类型,初始化的复制形式和直接形式有所不同:直接初始化直接调用与实参匹配的构造函数,复制初始化式总是调用复制构造函数。[复制初始化首先使用指定构造函数创建一个临时对象,然后复制构造函数将那个临时对象复制到正在创建的对象!]
- //理解下列语句的区别
- string null_book = "9-99999-999-9"; //<赋值初始化
- string dots(10,'.'); //<直接初始化
- string empty_copy = string(); //<赋值初始化
- string empty_direct; //<直接初始化
对于类类型对象,只有指定单个实参或显式创建一个临时对象用于复制时,才使用复制初始化!
- ifstream file1("filename"); //OK
- ifstream file2 = "filename";//Error,由于IO类型的对象不能复制
2、形参与返回值
当形参为非引用类型的时候,将复制实参的值,类似的,以非引用类型作返回值时,将返回return语句中值的副本。
- //当形参/返回值为类类型时,由复制构造函数进行复制。
- //然而该函数的形参是const的引用,因此不会复制
- string make_plural(size_t,const string &,const string &);
3、初始化容器元素
- //首先使用string默认构造函数创建一个临时值来初始化svec
- //然后使用复制构造函数将临时值复制到svec的每个元素
- vector<string> svec(5);
合成的复制构造函数
合成复制构造函数的行为是:执行逐个(非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) {}
对许多类而言,合成复制构造函数只完成必要的工作。只包含类类型成员或内置类型(但不是指针类型)成员的类,无须显式地定义复制构造函数,也可以复制。
然而,有些类必须对复制对象时发生的事情加以控制。这样的类经常有一个数据成员是指针或者有成员表示在构造函数中分配的其他资源,而另一些类在创建新对象时必须做一些特定工作。这两种情况下,都必须定义复制构造函数!
复制构造函数的定义与其他构造函数一样:它与类同名,没有返回值,可以(而且应该)使用构造函数初始化列表初始化新创建对象的成员,可以在函数体中做任何其他必要工作。
//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;
- }
撤销类对象时会自动调用析构函数:
- Sales_item *p = new Sales_item;
- {
- Sales_item item(*p); //调用复制构造函数
- delete p; //调用指针p的析构函数
- } //调用对象item的析构函数
动态分配的对象只有在指向该对象的指针被删除时才撤销,如果没有删除指向动态对象的指针,则不会运行该对象的析构函数,对象就会一直存在,从而导致内存泄漏,而且,对象内部使用的任何资源也不会释放!
当对象的引用或指针超出作用域时,不会运行析构函数。只有删除指向动态分配对象的指针或实际对象(而不是对象的引用)超出作用域时,才会运行析构函数!
撤销一个容器(不管是标准库容器还是内置数组)时,也会运行容器中的类类型元素的析构函数:
- {
- Sales_item *p = new Sales_item[10];
- vector<Sales_item> vec(p,p + sizeof(p)/sizeof(*p));
- delete []p; //调用指针p的析构函数
- } //调用vector的析构函数,执行每个元素的撤销
容器中的元素总是按照逆序撤销:首先撤销下标为size()-1的元素
如果类需要析构函数,则他也需要赋值操作符和复制构造函数,这是一个有用的经验法则“三法则”,指的是如果需要析构函数,则需要所有这三个复制控制成员!
析构函数并不仅限于用来释放资源,一般而言,析构函数还可以执行任意操作,该操作是类设计者希望在该类对象的使用完毕之后执行的!
与复制构造函数或赋值操作符不同编译器总会为我们合成一个析构函数,当然也与构造函数不同
即使我们已经自己编写了析构函数。合成析构函数按对象创建时的逆序撤销每个非static成员,因此,它按成员在类中声明次序的逆序撤销成员!对于类类型的每个成员,合成析构函数调用该类成员的析构函数来撤销对象。
撤销内置类型成员或复合类型的成员没什么影响。尤其是,合成析构函数并不删除指针成员所指向的对象。(如果有指针成员变量,那么要自己写析构函数,防止出现内存泄露的情况)
虽然可以为一个类定义多个构造函数,但只能提供一个析构函数,应用于类的所有对象。
析构函数与复制构造函数或赋值操作符之间的一个重要区别是,即使我们编写了自己的析构函数,合成析构函数仍然运行。
- class Sales_item
- {
- public:
- ~Sales_item()
- {
- }
- };
撤销Sales_item类型的对象时,将会运行这个什么也不做的析构函数,它执行完毕之后,将运行合成析构函数以撤销类的成员。
- class NoName
- {
- public:
- NoName():pstring(new std::string),i(0),d(0){}
- NoName(const NoName &rhs)
- {
- pstring = new std::string;
- *pstring = *(rhs.pstring);
- i = rhs.i;
- d = rhs.d;
- }
- ~NoName()
- {
- delete pstring; //<只需要单独将这个写出来,其余的元素会通过合成析构函数撤销掉
- }
- private:
- std::string *pstring;
- int i;
- double d;
- };