关于构造函数,析构函数和赋值操作符

1、只要类里有指针时,就要写自己版本的拷贝构造函数和赋值操作符函数。在这些函数里,你可以拷贝那些被指向的数据结构,从而使每个对象都有自己的拷贝; 或者你可以采用某种引用计数机制去跟踪当前有多少个对象指向某个数据结构。引用计数的方法更复杂,而且它要求构造函数和析构函数内部做更多的工作,但在某 些(虽然不是所有)程序里,它会大量节省内存并切实提高速度。

对于有些类,当实现拷贝构造函数和赋值操作符非常麻烦的时候,特别是可以确信程序中不会做拷贝和赋值操作的时候,去实现它们就会相对来说有点得不偿失。这 时可以只声明这些函数(声明为private成员)而不去定义(实现)它们。这就防止了会有人去调用它们,也防止了编译器去生成它们。


2、在写构造函数时,必须将参数值传给相应的数据成员。有两种方法来实现。第一种方法是使用成员初始化列表,第二种方法是在构造函数体内赋值。两种方法有重大的不同。

从纯实际应用的角度来看,有些情况下必须用初始化。特别是const和引用数据成员只能用初始化,不能被赋值。当两者都可以时,用成员初始化列表还是比在 构造函数里赋值要好。这次的原因在于效率。当使用成员初始化列表时,只有一个成员函数(拷贝构造函数)被调用。而在构造函数里赋值时,将有两个被调用(缺 省构造函数、赋值函数)。

通过成员初始化列表来进行初始化总是合法的,效率也决不低于在构造函数体内赋值,它只会更高效。另外,它简化了对类的维护,因为如果一个数据成员以后被修改成了必须使用成员初始化列表的某种数据类型,那么,什么也不用变。

但有一种情况下,对类的数据成员用赋值比用初始化更合理。这就是当有大量的固定类型的数据成员要在每个构造函数里以相同的方式初始化的时候。然而你可以利 用固定数据类型的(非const, 非引用)对象其初始化和赋值没有操作上的不同的特点,安全地将成员初始化列表用一个对普通的初始化函数的调用来代替。

请注意static类成员永远也不会在类的构造函数初始化。静态成员在程序运行的过程中只被初始化一次,所以每当类的对象创建时都去“初始化”它们没有任何意义。至少这会影响效率。


3、成员按它们在初始化列表上出现的顺序被初始化这一点只是非静态数据成员的初始化遵守以上规则。静态数据成员的行为有点象全局和名字空间对象,所以只会 被初始化一次。另外,基类数据成员总是在派生类数据成员之前被初始化,所以使用继承时,要把基类的初始化列在成员初始化列表的最前面。(如果使用多继承, 基类被初始化的顺序和它们被派生类继承的顺序一致,它们在成员初始化列表中的顺序会被忽略。请确信你的初始化列表中成员列出的顺序和成员在类内声明的顺序 一致。


4、确定基类有虚析构函数,因为当通过基类的指针去删除派生类的对象,而基类又没有虚析构函数时,结果将是不可确定的。如果某个类不包含虚函数,那一般是 表示它将不作为一个基类来使用。当一个类不准备作为基类使用时,使析构函数为虚一般是个坏主意。实现虚函数需要对象附带一些额外信息,以使对象在运行时可 以确定该调用哪个虚函数。对大多数编译器来说,这个额外信息的具体形式是一个称为vptr(虚函数表指针)的指针。vptr指向的是一个称为 vtbl(虚函数表)的函数指针数组。每个有虚函数的类都附带有一个vtbl。当对一个对象的某个虚函数进行请求调用时,实际被调用的函数是根据指向 vtbl的vptr在vtbl里找到相应的函数指针来确定的。

如果类包含一个虚函数,它的对象的体积将不知不觉地翻番,因为加上了一个32位的vptr。所以基本的一条是,无故的声明虚析构函数和永远不去声明一样是错误的。实际上,很多人这样总结:当且仅当类里包含至少一个虚函数的时候才去声明虚析构函数。

这是一个很好的准则,大多数情况都适用。但不幸的是,当类里没有虚函数的时候,也会带来非虚析构函数问题。

在某些类里声明纯虚析构函数很方便。纯虚函数将产生抽象类——不能实例化的类(即不能创建此类型的对象)。有些时候,你想使一个类成为抽象类,但刚好又没 有任何纯虚函数,方法很简单:在想要成为抽象类的类里声明一个纯虚析构函数。但必须提供纯虚析构函数的定义,因为虚析构函数工作的方式是:最底层的派生类 的析构函数最先被调用,然后各个基类的析构函数被调用。这就是说,即使是抽象类,编译器也要产生对纯虚函数的调用,所以要保证为它提供函数体。

如果声明虚析构函数为inline,将会避免调用它们时产生的开销,但编译器还是必然会在什么地方产生一个此函数的拷贝。


5、一般情况下几乎总要遵循operator=输入和返回的都是类对象的引用的原则,然而有时候需要重载operator=使它能够接受不同类型的参数(例如,标准string类型提供了两个不同版本的赋值运算符)。

请注意,即使在重载时,返回类型也是类的对象的引用。如果是让operator=返回void,这好象没什么不合理的,但它妨碍了连续(链式)赋值操作, 所以不要这样做。如果是让operator=返回一个const对象的引用,就会无缘无故地和固定类型的常规做法不兼容了。

当定义自己的赋值运算符时,必须返回赋值运算符左边参数的引用,*this。如果不这样做,就会导致不能连续赋值,或导致调用时的隐式类型转换不能进行,或两种情况同时发生。


6、在写赋值运算符时,必须对对象的每一个数据成员赋值。而且当类里增加新的数据成员时,也要记住更新赋值运算符函数。


7、在operator=中检查给自己赋值的情况:在赋值运算符中要特别注意可能出现别名的情况,其理由基于两点。其中之一是效率。如果可以在赋值运算符 函数体的首部检测到是给自己赋值,就可以立即返回,从而可以节省大量的工作,否则必须去实现整个赋值操作。另一个更重要的原因是保证正确性。一个赋值运算 符必须首先释放掉一个对象的资源(去掉旧值),然后根据新值分配新的资源。在自己给自己赋值的情况下,释放旧的资源将是灾难性的,因为在分配新的资源时会 需要旧的资源。

解决问题的方案是对可能发生的自己给自己赋值的情况先进行检查,如果有这种情况就立即返回。一个方法是,如果两个对象具有相同的值,就说它们是相同的(具 有相同的身份);另一个确定对象身份是否相同的方法是用内存地址。采用这个定义,两个对象当且仅当它们具有相同的地址时才是相同的。这个定义在c ++程序中运用更广泛,可能是因为它很容易实现而且计算很快,而采用值相等的定义则不一定总具有这两个优点。如果需要一个更复杂的机制来确定两个对象是否 相同,这就要靠程序员自己来实现。最普通的方法是实现一个返回某种对象标识符的成员函数。
class c {
public:
objectid identity() const;
...
};

对于两个对象指针a和b,当且仅当 a->identity() == b->identity()的时候,它们所指的对象是完全相同的。当然,必须自己来实现objectids的operator==。别名和 object identity的问题不仅仅局限在operator=里。在任何一个用到的函数里都可能会遇到。在用到引用和指针的场合,任何两个兼容类型的对象名称都 可能指的是同一个对象。

别名可以以各种形式出现,所以决不要忘记它或期望自己永远不会碰到
它。也许你不会碰到,但我们大多数会碰到。而很明显的一条是,处理它会达到事半功倍的效果。所以任何时候写一个函数,只要别名有可能出现,就必须在写代码时进行处理。

你可能感兴趣的:(关于构造函数,析构函数和赋值操作符)