11.为需要动态分配内存的类声明一个拷贝构造函数和一个赋值操作符。
显然,由于动态内存分配,绝对会有深浅拷贝的问题,要重写拷贝构造函数,使其为深拷贝,才能实现真正意义上的拷贝。这是我理解的关于要声明拷贝构造函数的原因。
而对于赋值操作符,类似的道理。
~~~
A b = a;
b = a;
~~~
对于上述两种形式,上面调用的是复制构造函数,而下面才是 赋值操作符=。赋值与复制很相似,缺省的操作都是将类的全部成员进行复制。
深拷贝主要的操作很简单,对于指针,动态申请一块内存来存放指针指向的数据,每个指针都指向自己的一块内存,而不是其他人的。
12.尽量使用初始化而不要在构造函数里赋值。
即尽量使用成员初始化列表,而不是使用赋值的方法。
首先对于const成员和引用,只能使用初始化列表来初始化。其次初始化列表效率更高,因为对象的创建分为两步:数据成员的初始化和执行被调用构造函数体内的动作。即使用赋值之前,先进行了数据成员的初始化,然后才是赋值。所以使用初始化列表效率更高。
但当有大量的固定类型的数据成员要在每个构造函数中以相同的方式初始化的时候,使用赋值会更加合理一点。
13。初始化列表中成员列出的顺序和它们在类中声明的顺序相同。
这是因为初始化列表的顺序并不影响初始化的顺序,初始化的顺序是有成员在类中声明的顺序决定的,而让其顺序相同是使程序看起来是按照初始化列表的顺序初始化。
而c++不使用初始化列表的顺序的原因是:对象的析构函数是按照 与成员在构造函数中创建的相反的顺序 创建的。则如果对象不是按照一种固定的顺序来初始化,编译器就要记录下每一个对象成员的初始化顺序,这将带来较大的开销。
14、确定基类有虚析构函数。
通过基类的指针去删除派生类的对象时,基类一定要有虚析构函数,不然 会有不可预测的后果。不使用虚析构函数,只调用基类的析构函数去删除派生类对象,这是无法做到,也是无法确定后果的。
构造函数调用是先基类后派生类,而析构函数的顺序是先派生类后基类。
当一个类不作为基类使用时,使用虚析构函数是一个坏主意。因为虚函数的对象会有一个虚指针指向虚表,会浪费空间来储存这个没有意义的指针。
纯虚函数在虚函数后加 =0 即可。
15.让operator = 返回 *this 的引用。
= 号可以连接起来,因为其返回值的原因,声明operator=的形式如下:
~~~
C& C:: operator= (const C&);
~~~
其输入和返回都是类对象的引用。以实现连续的赋值操作。返回值是 = 左边值的引用 即 *this的引用,因为右边即参数是const类型的。
函数的参数是 const类型的原因,是 = 右边的值经过计算会获得一个新的结果,而这个对象要用一个临时对象来储存,这个临时对象是const类型的,因为其作为函数的参数,且不能被函数修改。
16.在operator = 中对所有数据成员赋值。
对于深拷贝要自己写一个更加正确的 = 操作。
在涉及继承时,派生类的赋值运算必须处理基类的赋值。如果重写派生类的赋值运算,就必须同时显示的对基类部分进行赋值。
~~~
class A{
public:
int a;
A(int x):a(x){}
// A& operator=(const A& x){ a = x.a; return *this;}
};
class B:public A{
public:
int b;
B(int x):A(x),b(x){}
B& operator=(const B&);
};
B& B::operator=(const B& x){
if(this == &x)return *this;
// A::operator=(x);//调用基类的赋值函数要如此写
static_cast<A&>(*this) = x;//也可以强制转换为A类型然后在调用基类的默认赋值函数
b = x.b;
return *this;
}
~~~
拷贝构造函数也是如此,也要对基类的成员进行复制,只要在成员初始化列表中添加基类即可。
17.在operator=中检查给自己赋值的情况。
这是基于效率考虑的,在赋值的首部检测是给自己赋值,就立即返回,如16中函数所写的那样。这里除了类中自己成员的赋值,如果有基类,还要调用基类的赋值函数,会增加开销。
另一个原因是保证正确性。一个赋值运算符必须首先释放掉一个对象的资源,如有些指针指向了动态申请的空间,则赋值前一般要释放这些资源,然后在指向新的资源(如果在赋值开始,用些临时的指针来记录之前的所有指针指向的内存,然后在赋值后再将临时指针指向的内存全部释放。这还是不行,因为对这些指针如p,必须有 p = new p[]...来指向新申请的一块空间,而如果是同一个对象的话,这里就将原来的指针指向一个新的地址,且两者相同了,所以又需要一个新的临时指针来指向赋值对象的指针的值),也就是说为了使其给自己赋值,对与每个指针必须新建两个指针,一个储存左边的对象的原指针,一个储存右边的对象的原指针,这样的开销时极大极浪费的,也是没有必要的,为了一些不应该进行的为自己赋值要提前准备大量内存来储存数据,这也是不科学的。
所以最好的解决办法是检测是否为自己赋值,一般采用检测对象地址是否相等,c++中一般采取这种方法。对于java中,不好的做法是检测对象是否相等,即其全部值是否完全相等,较好的方法是根据对象的id来判断对象是否为同一个对象。