条款 12: 尽量使用初始化而不要在构造函数里赋值

来源:互联网 发布:java线程笔试题 编辑:程序博客网 时间:2024/04/28 11:15


          看这样一个模板,它生成的类使得一个名字和一个 T 类型的对象的指针关联起来。

template<class T>class NamedPtr {public:NamedPtr(const string& initName, T *initPtr);...private:string name;T *ptr;};



因为有指针成员的对象在进行拷贝和赋值操作时可能会引起指针混乱(见条款 11),NamedPtr 也必须实现这些函数(见条款 2))

条款11:为需要动态分配内存的类声明一个拷贝构造函数和赋值操作符。即只要类含有指针成员就需要定义拷贝构造函数和赋值操作符。

在写 NamedPtr 构造函数时,必须将参数值传给相应的数据成员。有两种方法来实现。第一种方法是使用成员初始化列表:

template<class T>NamedPtr<T>::NamedPtr(const string& initName, T *initPtr ): name(initName), ptr(initPtr){}

第二种方法是在构造函数体内赋值:

template<class T>
NamedPtr<T>::NamedPtr(const string& initName, T *initPtr)
{
name = initName;
ptr = initPtr;
}

两种方法,用引用的效率最好。

从纯实际应用的角度来看,有些情况下必须用初始化。

特别是 const 和引用数据成员只能用初始化,不能被赋值。所以,如果想让 NamedPtr<T>对象不能改变它的名字或指针成员,就必须遵循条款 21 的建议声明成员为 const

为什么不能对const 和引用数据成员赋值,只能用初始化。我的解释如下:

因为const数据类型是如法赋值的,引用数据类型必须要初始化,所以,对于引用类型和const数据类型只能初始化。


赋值与初始化的区别:

赋值操作是在两个已经存在的对象间进行的,而初始化是要创建一个新的对象,并且其初值来源于另一个已存在的对象。编译器会区别这两种情况,赋值的时候调用重载的赋值运算符初始化的时候调用拷贝构造函数。如果类中没有拷贝构造函数,则编译器会提供一个默认的。这个默认的拷贝构造函数只是简单地复制类中的每个成员。


另外一个原因:

用成员初始化列表还是比在构造函数里赋值要好。这次的原因在于效率。当使用成员初始化列表时,只有一个 string 成员函数被调用。而在构造函数里赋值时,将有两个被调用。为了理解为什么,请看在声明 NamedPtr<T>对象时都发生了些什么。


对象的创建分两步:
1.  数据成员初始化。 (参见条款 13)
2.  执行被调用构造函数体内的动作。

(对有基类的对象来说,基类的成员初始化和构造函数体的执行发生在派生类的成员初始化和构造函数体的执行之前)

对 NamedPtr 类来说,这意味着 string 对象 name 的构造函数总是在程序执行到 NamedPtr 的构造函数体之前就已经被调用了。问题只在于:string 的哪个构造函数会被调用?

这取决于 NamedPtr 类的成员初始化列表。如果没有为 name 指定初始化参数,string 的缺省构造函数会被调用。当在 NamedPtr 的构造函数里对 name执行赋值时,会对 name 调用 operator=函数。这样总共有两次对 string 的成员函数的调用:一次是缺省构造函数,另一次是赋值。

相反, 如果用一个成员初始化列表来指定 name 必须用 initName 来初始化,name 就会通过拷贝构造函数以仅一个函数调用的代价被初始化。


养成尽可能使用成员初始化列表的习惯,不但可以满足 const 和引用成员初始化的要求,还可以大大减少低效地初始化数据成员的机会。换句话说,通过成员初始化列表来进行初始化总是合法的,效率也决不低于在构造函数体内赋值,它只会更高效。




0 0