条款11: 为需要动态分配内存的类声明一个拷贝构造函数和一个赋值操作符
理由:
使用缺省的拷贝和复制会产生一系列不良结果--例如在进行字符串的拷贝时,被拷贝指针曾指向的内存永远不会被删除而产生内存泄露;或者两个指针中任何一个调用析构函数都将导致另一指针指向的那块内存被删除等。
当用一个已经初始化过了的自定义类类型对象去初始化另一个新构造的对象的时候,拷贝构造函数就会被自动调用:
1.当对象直接作为参数传给函数时,函数将建立对象的临时拷贝,这个拷贝过程也将调同拷贝构造函数。
例如:
void donothing(string localstring) {}
string s = "the truth is out there";
donothing(s);
一切好象都很正常。但因为被传递的localstring是一个值,它必须从s通过(缺省)拷贝构造函数进行初始化。于是localstring拥有了一个s内的指针的拷贝。当donothing结束运行时,localstring离开了其生存空间,调用析构函数。其结果也将是:s包含一个指向localstring早已删除的内存的指针。
2.当函数中的局部对象被被返回给函数调者时,也将建立此局部对象的一个临时拷贝,拷贝构造函数也将被调用
CTest func()
{
CTest theTest;
return theTest
}
注意:
1、用delete去删除一个已经被删除的指针,其结果是不可预测的,只要类里有指针时,就要写自己版本的拷贝构造函数和赋值操作符函数;
2、在不需要实现拷贝构造函数和赋值操作符的时候,可以把它们声明为private成员而不去定义它们,这就防止了会有人去调用它们。
条款12: 尽量使用初始化而不要在构造函数里赋值
a.一些情况如const和引用数据成员只能用初始化(列表),不能被赋值;
b.成员初始化列表还是比在构造函数里赋值效率要高。
注意:
1、当在构造函数里对成员变量执行赋值时,会对成员变量调用operator=函数。这样总共有两次对string的成员函数的调用:一次是缺省构造函数,另一次是赋值。如果用一个成员初始化列表来指定来初始化成员变量,那么就会通过拷贝构造函数以仅一个函数调用的代价被初始化;
2、当有大量的固定类型的数据成员要在每个构造函数里以相同的方式初始化的时候对类的数据成员用赋值比用初始化更合理;
3、静态成员在程序运行的过程中只被初始化一次,所以每当类的对象创建时都去"初始化"它们没有任何意义。
条款13: 初始化列表中成员列出的顺序和它们在类中声明的顺序相同
理由:类成员是按照它们在类里被声明的顺序进行初始化的,和它们在成员初始化列表中列出的顺序没一点关系。对一个对象的所有成员来说,它们的析构函数被调用的顺序总是和它们在构造函数里被创建的顺序相反。如果是按初始化列表的顺序来初始化,编译器就要为每一个对象跟踪其成员初始化的顺序,以保证它们的析构函数以正确的顺序被调用。这会带来昂贵的开销。
注意:
初始化列表中成员列出的顺序和成员在类内声明的顺序保持一致。这样才不会进行错误的赋值。比如:
array::array(int lowbound, int highbound)
: size(highbound - lowbound + 1),
lbound(lowbound), hbound(highbound),
data(size)
{}
这样得到的size不可确定,因为为size赋值的lowbound和highbond还不可确定。
条款14: 确定基类有虚析构函数
理由:
当通过基类的指针去删除派生类的对象,而基类又没有虚析构函数时,结果将是不可确定的。实际运行时经常发生的是,派生类的析构函数永远不会被调用。
注意:
1、使基类有虚构函数virtual,让派生类去定制自己的行为;
2、当一个类不准备作为基类使用时,使析构函数为虚一般是个坏主意,因为包含虚函数将使对象的体积翻番,而且降低代码的可移植性下降;
3、在定义抽象类的时候,定义纯虚构函数(eg: awov::~awov() {});
条款15: 让operator=返回*this的引用
理由:
采用缺省形式定义的赋值运算符里,对象返回值有两个很明显的候选者:赋值语句左边的对象(被this指针指向的对象)和赋值语句右边的对象(参数表中被命名的对象),在自定义的operator=中返回右边对象的版本往往不能通过
注意:
1、不要让operator=返回void--它妨碍了连续(链式)赋值操作;
2、不要让operator=返回const对象的引用;
const widget& operator=(const widget& rhs); 这样做通常是为了防止程序中做象下面这样愚蠢的操作:
widget w1, w2, w3;
(w1 = w2) = w3; // w2赋给w1, 然后w3赋给其结果
//(给operator= 一个const返回值
// 就使这个语句不能通过编译)
这可能是很愚蠢,但固定类型这么做并不愚蠢:
int i1, i2, i3;
(i1 = i2) = i3; // 合法! i2赋给i1
// 然后i3赋给i1!
这样的做法实际中很少看到,但它对int来说是可以的,对我和我的类来说也可以。那它对你和你的类也应该可以。为什么要无缘无故地和固定类型的常规做法不兼容呢?
3、当定义自己的赋值运算符时,必须返回赋值运算符左边参数的引用。
条款16: 在operator=中对所有数据成员赋值
理由:
只要想对赋值过程的某一个部分进行控制,就必须负责做赋值过程中所有的事。
class base {
public:
base(int initialvalue = 0): x(initialvalue) {}
private:
int x;
};
class derived: public base {
public:
derived(int initialvalue)
: base(initialvalue), y(initialvalue) {}
derived& operator=(const derived& rhs);
private:
int y;
};
逻辑上说,derived的赋值运算符应该象这样:
// erroneous assignment operator
derived& derived::operator=(const derived& rhs)
{
if (this == &rhs) return *this; // 见条款17
y = rhs.y; // 给derived仅有的
// 数据成员赋值
return *this; // 见条款15
}
不幸的是,它是错误的,因为derived对象的base部分的数据成员x在赋值运算符中未受影响。例如,考虑下面的代码段:
void assignmenttester()
{
derived d1(0); // d1.x = 0, d1.y = 0
derived d2(1); // d2.x = 1, d2.y = 1
d1 = d2; // d1.x = 0, d1.y = 1
}
应该改为:
// 正确的赋值运算符
derived& derived::operator=(const derived& rhs)
{
if (this == &rhs) return *this;
base::operator=(rhs); // 调用this->base::operator=
//某些编译要这么写static_cast<base&>(*this) = rhs;
y = rhs.y;
return *this;
}
拷贝构造函数类似:
class derived: public base {
public:
derived(const derived& rhs): base(rhs), y(rhs.y) {}
注意:
1、当类里增加新的数据成员时,也要记住更新赋值运算符函数;
2、派生类的赋值运算符也必须处理它的基类成员的赋值。
条款17: 在operator=中检查给自己赋值的情况
理由:
1.效率。如果可以在赋值运算符函数体的首部检测到是给自己赋值,就可以立即返回,从而可以节省大量的工作,否则必须去实现整个赋值操作。
2.一个赋值运算符必须首先释放掉一个对象的资源(去掉旧值),然后根据新值分配新的资源。在自己给自己赋值的情况下,释放旧的资源将是灾难性的,因为在分配新的资源时会需要旧的资源。
注意:
1、可能发生的自己给自己赋值的情况先进行检查,如果该情况发生就立即返回;
2、一个方法是对相同内容的对象认为是同一对象(用operator==实现);
3、另一个确定对象身份是否相同的方法是用内存地址。