第13章 复制控制
复制构造函数、赋值操作符和析构函数总称为复制控制。
编译器自动实现这些操作,但类也可定义自己的版本。有一种特别常见的情况需要类定义自己的复制控制成员的:类具有指针成员。
13.1 复制构造函数
只有单个形参,而且该形参是对本类类型对象的引用(常用 const 修饰),这样的构造函数称为复制构造函数。
与默认构造函数一样,复制构造函数可由编译器隐式调用。复制构造函数可用于:
A.根据另一个同类型的对象显式或隐式初始化一个对象。
B.复制一个对象,将它作为实参传给一个函数。
C.从函数返回时复制一个对象。
D.初始化顺序容器中的元素。
E.根据元素初始化式列表初始化数组元素。
myclass obj1;
myclass obj2 = obj1; // 根据另一个同类型的对象显式或隐式初始化一个对象
myclass fun(myclass par)
{
// ...
return par; // 从函数返回时复制一个对象
}
fun(obj1); // 复制一个对象,将它作为实参传给一个函数
vector<string> svec(5);; // 编译器首先使用 string 默认构造函数创建一个临时值来初始化 svec,然后使用复制构造函数将临时值复制到 svec 的每个元素
myclass ls[]{obj1,obj1,obj1,obj1};// 根据对象初始化数组
myclass ls[]{myclass(),myclass(),myclass()}; // 按照书上说是会调用复制构造函数但实际不会调用,据说是做了优化
1.当用于类类型对象时,初始化的复制形式和直接形式有所不同:直接初始化直接调用与实参匹配的构造函数,复制初始化总是调用复制构造函数。复制初始化首先使用指定构造函数创建一个临时对象,然后用复制构造函数将那个临时对象复制到正在创建的对象:
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
对于类类型对象,只有指定单个实参或显式创建一个临时对象用于复制时,才使用复制初始化。
由于不能复制IO类型的对象,所以不能对那些类型的对象使用复制初始化。
ifstream file1("filename");
ifstream file2="filename";//error:copy construction is private
对于Sales_item类而言,如果构造函数是显式的,初始化失败。
Sales_item item=string("9-999-99999-9");//构造函数不是显式菜初始化成功。
2.形参与返回值:当形参为非引用类型的时候,将复制实参的值。类似地,以非引用类型作返回值时,将返回 return 语句 中的值的副本.
3.初始化容器的有效办法是分配一个空容器并将已知元素的值加入容器。
13.1.1 合成的复制构造函数
合成复制构造函数的行为是,执行逐个成员初始化,将新对象初始化为原对象的副本。
假设有一个类,它有三个数据成员:
class Sales_item{
//other members constructors as before
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){ }
13.1.2 定义自己的复制构造函数
制构造函数就是接受单个类类型引用形参(通常用 const 修饰)的构造函数:
class Foo {
public:
Foo(); // default constructor
Foo(const Foo&); // copy constructor
// ...
};
有些类必须对复制对象时发生的事情加以控制。这样的类经常有一个数据成员是指针,或者有成员表示在构造函数中分配的其他资源。而另一些类在创建新对象时必须做一些特定工作。这两种情况下,都必须定义复制构造函数。
通常,定义复制构造函数最困难的部分在于认识到需要复制构造函数。只要能认识到需要复制构造函数,定义构造函数一般非常简单。复制构造函数的定义与其他构造函数一样:它与类同名,没有返回值,可以(而且应该)使用构造函数初始化列表初始化新创建对象的成员,可以在函数体中做任何其他必要工作。
13.1.3 禁止复制
为了防止复制,类必须显式声明其复制构造函数为 private。
如果想要连友元和成员中的复制也禁止,就可以声明一个(private)复制构造函数但不对其定义。
大多数类应定义复制构造函数和默认构造函数:不定义复制构造函数和/或默认构造函数,会严重局限类的使用。不允许复制的类对象只能作为引用传递给函数或从函数返回,它们也不能用作容器的元素。
一般来说,最好显式或隐式定义默认构造函数和复制构造函数。只有不存在其他构造函数时才合成默认构造函数。如果定义了复制构造函数,也必须定义默认构造函数。
13.2 赋值操作符
类的赋值操作符实际上是操作符重载(operator=),返回对同一类类型的引用。
myclass& operator=(const myclass &obj)
{
// obj是源对象,用它来为操作符左面对象赋值
age = obj.age;
name = obj.name;
return *this;
}
一般而言,如果类需要复制构造函数,它也会需要赋值操作符。
13.3 析构函数
1.何时调用析构函数:
撤销类对象时会自动调用析构函数:
// 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
撤销一个容器(不管是标准库容器还是内置数组)时,也会运行容器中的类类型元素的析构函数:
{
Sales_item *p = new Sales_item[10]; // dynamically allocated
vector<Sales_item> vec(p, p + 10); // local object
// ...
delete [] p; // array is freed; destructor run on each element
} // vec goes out of scope; destructor run on each element
2.何时编写显式析构函数:
仅在有些工作需要析构函数完成时,才需要析构函数。析构函数通常用于释放在构造函数或在对象生命期内获取的资源。
如果类需要析构函数,则它也需要赋值操作符和复制构造函数,这是一个有用的经验法则。这个规则常称为三法则,指的是如果需要析构函数,则需要所有这三个复制控制成员。
3.合成析构函数:与复制构造函数或赋值操作符不同,编译器总是会为我们合成一个析构函数。合成析构函数按对象创建时的逆序撤销每个非 static 成员,因此,它按成员在类中声明次序的逆序撤销成员。
4.如何编写析构函数:
它的名字是在类名字之前加上一个代字号(~),它没有返回值,没有形参。因为不能指定任何形参,所以不能重载析构函数。虽然可以为一个类定义多个构造函数,但只能提供一个析构函数,应用于类的所有对象。
析构函数与复制构造函数或赋值操作符之间的一个重要区别是,即使我们编写了自己的析构函数,合成析构函数仍然运行。如果类定义了析构函数,则在类定义的析构函数结束之后运行合成析构函数。
例如,可以为 Sales_item: 类编写如下的空析构函数:
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 的内存。units_sold 和 revenue 成员是内置类型,所以合成析构函数撤销它们不需要做什么。
13.5 管理指针成员
管理指针成员的三种方法:
指针成员采取常规指针型行为。 类可以实现所谓的“智能指针”行为。 类采取值型行为。
1.智能指针引入计数类
class U_Ptr {
friend class HasPtr; //定义成友元
size_t use;
int *ip;
U_Ptr(int *p):ip(p), use(1) {}
~U_Ptr() { delete ip; }
};
使用计数类
class HasPtr{
public:
HasPtr(int *p, int i):uptr(new U_Ptr(p)), val(i) {} //p是指向int型数组的指针
HasPtr(const HasPtr& orig):uptr(orig.uptr),val(orig.val) { ++uptr->use; //复制完成将使用计数加1 }
HasPtr& operator = (const HasPtr& rhs);
~HasPtr() {if(--uptr->use == 0) //检查假如只有一个对象在共享该指针,则删除 delete uptr; }
int *getPtr() { return uptr->ip;}
int getValue() { return val; }
void setPtr(int *p) { uptr->ip = p;}
void setValue(int i) { val = i;}
int getPtrValue() const {return *uptr->ip;}
void setPtrValue(int i) { *uptr->ip = i; }
private:
U_Ptr *uptr;
int val;
};
赋值操作符
HasPtr& HasPtr::operator =(const HasPtr &rhs)
{
++ rhs.uptr->use;
if (--uptr->use == 0)
delete uptr;
uptr = rhs.uptr;
val = rhs.val;
return *this;
}
2.定义值型类
class HasPtr{
public:
HasPtr(int *p, int i):ptr(new int(p)), val(i) {}
HasPtr(const HasPtr& orig):ptr(new int (*orig.uptr)),val(orig.val) { }
HasPtr& operator = (const HasPtr& );
~HasPtr() { delete ptr; }
int *getPtr() { return ptr;}
int getValue() { return val; }
void setPtr(int *p) { ptr = p;}
void setValue(int i) { val = i;}
int getPtrValue() const {return *ptr;}
void setPtrValue(int i) { *ptr = i; }
private:
int *ptr;
int val;
};
HasPtr& HasPtr::operator =(const HasPtr &rhs)
{
*ptr = rhs.ptr;
val = rhs.val;
return *this;
}