1. 重载操作符
赋值操作符的返回类型应该与内置类型赋值运算返回的类型相同。内置类型的赋值运算返回对右操作数的引用,因此,赋值操作符也返回对同一类类型的引用。例如,Sales_item的赋值操作符可以声明为:
class Sales_item {
public:
// other members as before
// equivalent to the synthesized assignment operator
Sales_item& operator=(const Sales_item &);
};
2. 合成赋值操作符
合成赋值操作符与合成复制构造函数的操作类似。它会执行逐个成员赋值:右操作数对象的每个成员赋值给左操作数对象的对应成员。
除数组之外,每个成员用所属类型的常规方式进行赋值。对于数组,给每个数组元素赋值。
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
//撤销一个容器(不管是标准库容器还是内置数组)时,也会运行容器中的类类型元素的析构函数:
{
Sales_item *p = new Sales_item[10]; // dynamically allocated
vector<Sales_item> vec(p, p + 10); //local object
// ...
delete [] p; // arrayis freed; destructor run on each element
}
4. 何时编写显式析构函数
许多类不需要显式析构函数,尤其是具有构造函数的类不一定需要定义自己的析构函数。仅在有些工作需要析构函数完成时,才需要析构函数。
析构函数通常用于释放在构造函数或在对象生命期内获取的资源。
如果类需要析构函数,则它也需要赋值操作符和复制构造函数,这是一个有用的经验法则。这个规则常称为三法则,指的是如果需要析构函数,则需要所有这三个复制控制成员。
5. 如何编写析构函数
在类名字之前加上一个代字号(~),它没有返回值,没有形参。析构函数与复制构造函数或赋值操作符之间的一个重要区别是,
即使我们编写了自己的析构函数,合成析构函数仍然运行。例如,可以为Sales_item: 类编写如下的空析构函数:
class Sales_item {
public:
// empty; no work todo other than destroying the members,
// which happensautomatically
~Sales_item() { }
// other members asbefore
};
撤销Sales_item 类型的对象时,将运行这个什么也不做的析构函数,它执行完毕后,将运行合成析构函数以撤销类的成员。合成析构函数调用string 析构函数来撤销string 成员,string析构函数释放了保存isbn 的内存。units_sold 和 revenue 成员是内置类型,所以合成析构函数撤销它们不需要做什么。
6. 赋值构造函数
直接初始化和复制初始化。复制初始化使用 = 符号,而直接初始化将初始化式放在圆括号中。 当用于类类型对象时,初始化的复制形式和直接形式有所不同:直接初始化直接调用与实参匹配的构造函数,复制初始化总是调用复制构造函数。复制初始化首先使用指定构造函数创建一个临时对象,然后用复制构造函数将那个临时对象复制到正在创建的对象:
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
对于类类型对象,只有指定单个实参或显式创建一个临时对象用于复制时,才使用复制初始化。
7. 管理指针
如果对左操作数进行解引用,则修改的是指针所指对象的值;如果没有使用解引用操作,则修改的是指针本身的值
在类的实现中。包含指针的类需要特别注意复制控制,原因是复制指针时只复制指针中的地址,而不会复制指针指向的对象。
指针可能出错:
设计具有指针成员的类时,类设计者必须首先需要决定的是该指针应提供什么行为。将一个指针复制到另一个指针时,两个指针指向同一对象。当两个指针指向同一对象时,可能使用任一指针改变基础对象。类似地,很可能一个指针删除了一对象时,另一指针的用户还认为基础对象仍然存在。
指针成员默认具有与指针对象同样的行为。然而,通过不同的复制控制策略,可以为指针成员实现不同的行为。大多数C++ 类采用以下三种方法之一管理指针成员:
1. 指针成员采取常规指针型行为。这样的类具有指针的所有缺陷但无需特殊的复制控制。
2. 类可以实现所谓的“智能指针”行为。指针所指向的对象是共享的,但类能够防止悬垂指针。
3. 类采取值型行为。指针所指向的对象是唯一的,由每个类对象独立管理
8. 如何管理指针
A.定义智能指针类
a.引入使用计数
每次创建类的新对象时,初始化指针并将使用计数置为1。当对象作为另一对象的副本而创建时,复制构造函数复制指针并增加与之相应的使用计数的值。对一个对象进行赋值时,赋值操作符减少左操作数所指对象的使用计数的值(如果使用计数减至0,则删除对象),并增加右操作数所指对象的使用计数的值。最后,调用析构函数时,析构函数减少使用计数的值,如果计数减至0,则删除基础对象。
// 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;}
};
/* smart pointerclass: takes ownership of the dynamically allocated
* object to which it isbound
* User code must dynamically allocate an object to initialize a HasPtr
* and must not deletethat object; the HasPtr class will delete it
*/
class HasPtr {
public:
// HasPtr owns the pointer; p must have been dynamically allocated
HasPtr(int *p, inti): ptr(new U_Ptr(p)), val(i) { }
// copy members andincrement the use count
HasPtr(const HasPtr&orig):
ptr(orig.ptr),val(orig.val) { ++ptr->use; }
HasPtr&operator=(const HasPtr&);
// if use count goesto zero, delete the U_Ptr object
~HasPtr() { if(--ptr->use == 0) delete ptr; }
private:
U_Ptr *ptr; // pointsto use-counted U_Ptr class
int val;
};
b. 赋值与使用计数.赋值操作符比复制构造函数复杂一点:
HasPtr&HasPtr::operator=(const HasPtr &rhs)
{
++rhs.ptr->use; //increment use count on rhs first
if (--ptr->use ==0)
delete ptr; // if usecount goes to 0 on this object,delete it
ptr = rhs.ptr; //copy the U_Ptr object
val = rhs.val; //copy the int member
return *this;
}
c. 改变其他成员.现在需要改变访问int* 的其他成员,以便通过U_Ptr 指针间接获取int:
class HasPtr {
public:
// copy control andconstructors as before
// accessors mustchange to fetch value from U_Ptr object
int *get_ptr() const{ return ptr->ip; }
int get_int() const {return val; }
// change theappropriate data member
void set_ptr(int *p){ ptr->ip = p; }
void set_int(int i) {val = i; }
// return or changethe value pointed to, so ok for const objects
// Note: *ptr->ipis equivalent to *(ptr->ip) 628
int get_ptr_val()const { return *ptr->ip; }
void set_ptr_val(inti) { *ptr->ip = i; }
private:
U_Ptr *ptr; // pointsto use-counted U_Ptr class
int val;
};
B. 定义值型类
处理指针成员的另一个完全不同的方法,是给指针成员提供值语义。具有值语义的类所定义的对象,其行为很像算术类型的对象:复制值型对象时,会得到一个不同的新副本。对副本所做的改变不会反映在原有对象上,反之亦然。string类是值型类的一个例子。
要使指针成员表现得像一个值,复制HasPtr 对象时必须复制指针所指向的对象:
/*
* Valuelike behavioreven though HasPtr has a pointer member:
* Each time we copy aHasPtr object, we make a new copy of the
* underlying intobject to which ptr points.
*/
class HasPtr {
public:
// no point topassing a pointer if we're going to copy it anyway
// store pointer to acopy of the object we're given 630
HasPtr(const int&p, int i): ptr(new int(p)), val(i) {}
// copy members andincrement the use count
HasPtr(const HasPtr&orig):
ptr(new int(*orig.ptr)), val(orig.val) { }
HasPtr&operator=(const HasPtr&);
~HasPtr() { deleteptr; }
// accessors mustchange to fetch value from Ptr object
int get_ptr_val()const { return *ptr; }
int get_int() const {return val; }
// change theappropriate data member
void set_ptr(int *p){ ptr = p; }
void set_int(int i) {val = i; }
// return or changethe value pointed to, so ok for const objects
int *get_ptr() const{ return ptr; }
void set_ptr_val(intp) const { *ptr = p; }
private:
int *ptr; // pointsto an int
int val;
};
复制构造函数不再复制指针,它将分配一个新的int 对象,并初始化该对象以保存与被复制对象相同的值。每个对象都保存属于自己的int 值的不同副本。因为每个对象保存自己的副本,所以析构函数将无条件删除指针。 赋值操作符不需要分配新对象,它只是必须记得给其指针所指向的对象赋新值,而不是给指针本身赋值:
HasPtr&HasPtr::operator=(const HasPtr &rhs)
{
// Note: Every HasPtris guaranteed to point at an actual int;
// We know that ptrcannot be a zero pointer
*ptr = *rhs.ptr; //copy the value pointed to
val = rhs.val; //copy the int
return *this;
}<span style="font-family: Arial, Helvetica, sans-serif; background-color: rgb(255, 255, 255);"> </span>
换句话说,改变的是指针所指向的值,而不是指针。
9. 小结
类除了定义该类型对象上的操作,还需要定义复制、赋值或撤销该类型对象的含义。特殊成员函数(复制构造函数、赋值操作符和析构函数)可用于定义这些操作。这些操作统称为“复制控制”函数。
如果类没有定义这些操作中的一个或多个,编译器将自动定义它们。合成操作执行逐个成员初始化、赋值或撤销:合成操作依次取得每个成员,根据成员类型进行成员的复制、赋值或撤销。如果成员为类类型的,合成操作调用该类的相应操作(即,复制构造函数调用成员的复制构造函数,析构函数调用成员的析构函数,等等)。如果成员为内置类型或指针,则直接复制或赋值,析构函数对撤销内置类型或指针类型的成员没有影响。如果成员为数组,则根据元素类型以适当方式复制、赋值或撤销数组中的元素。
与复制构造函数和赋值操作符不同,无论类是否定义了自己的析构函数,都会创建和运行合成析构函数。如果类定义了析构函数,则在类定义的析构函数结束之后运行合成析构函数。
定义复制控制函数最为困难的部分通常在于认识到它们的必要性。
分配内存或其他资源的类几乎总是需要定义复制控制成员来管理所分配的资源。如果一个类需要析构函数,则它几乎也总是需要定义复制构造函数和赋值操作符。