在建立一个类的过程中,首先应该考虑的是,有哪些成员变量,即对象拥有的属性,第二步抽象出类的接口,即需要哪些成员函数,并确定访问权限。第三步,当然是考虑如何为类建立构造函数和析构函数了。对于成员变量中有指针的情况,需要提供拷贝构造函数和赋值函数。
1 构造函数:这个函数里面有许多需要注意的问题,第一,成员变量的赋值是放入初始化成员列表中,还是放在构造函数内部,两者的区别是什么?第二,哪些成员变量必须放在初始化成员列表中?第三,初始化顺序是根据什么规则来的?
首先解答第一个问题,借用《Effective C++》条款12来说,尽量使用初始化列表,这样做主要是从效率来考虑的,避免多次函数调用总是高效的。如果一个类中有一个成员变量是某个类的变量,那么当这个类实例化之前,成员变量的对象初始化已经结束,这肯定是调用了一次缺省构造函数,如果在类的构造函数次赋值,那么就调用了2次函数,一次是缺省构造函数,一次可能是赋值函数,这样明显效率就不高。如果使用成员列表,那只需要调用一次。为了更好的说明这个问题,举一个例子。
template<class t>
class namedptr {
public:
namedptr(const string& initname, t *initptr);
...
private:
const string name;
t * const ptr;
};
对namedptr类来说,这意味着string对象name的构造函数总是在程序执行到namedptr的构造函数体之前就已经被调用了。问题只在于:string的哪个构造函数会被调用?
这取决于namedptr类的成员初始化列表。如果没有为name指定初始化参数,string的缺省构造函数会被调用。当在namedptr的构造函数里对name执行赋值时,会对name调用operator=函数。这样总共有两次对string的成员函数的调用:一次是缺省构造函数,另一次是赋值。
相反,如果用一个成员初始化列表来指定name必须用initname来初始化,name就会通过拷贝构造函数以仅一个函数调用的代价被初始化。
解答第二个问题,哪些成员变量必须放在初始化成员列表中?对于const成员变量必须放在初始化列表中来赋值,因为const变量只能初始化不能赋值。对于引用成员变量必须放在成员初始化列表中,因为一个引用变量,引用的同时必须初始化。
解答第三个问题,第三,初始化顺序是根据什么规则来的?程序总是先执行初始化列表,再执行构造函数体。然而,初始化列表的顺序又是如何呢?初始化顺序跟类中成员变量的声明顺序是一样的,而和初始化列表中的顺序无关。这是编译器设置的,如果不这样做会产生很多问题。
2 拷贝构造函数:objectA(objectB),这样的对象初始化调用的是拷贝构造函数,如何编写该函数?以下是一个很简单的string类。
// 一个很简单的string类
class string {
public:
string(const char *value);
~string();
private:
char *data;
};
string::string(const char *value)
{
if (value) {
data = new char[strlen(value) + 1];
strcpy(data, value);
}
else {
data = new char[1];
*data = '/0';
}
}
从上面的函数来说,拷贝构造函数总是要动态分配一个内存,然后根据参数为成员变量赋值。
3 赋值函数:主要是为对象初始化支持=操作符。
A 当定义自己的赋值运算符时,必须返回赋值运算符左边参数的引用,*this。如果不这样做,就会导致不能连续赋值,或导致调用时的隐式类型转换不能进行,或两种情况同时发生。因此,该函数必须返回*this.
B 在operator=函数中为所有的数据成员赋值,包括从基类里继承下来的数据成员,因此必须在子类的operator=中调用基类的operator=函数。如下代码:
// 正确的赋值运算符
derived& derived::operator=(const derived& rhs)
{
if (this == &rhs) return *this;
base::operator=(rhs); // 调用this->base::operator=
y = rhs.y;
return *this;
}
C 在operator=中检查给自己赋值的情况,即每一个operator=函数中加入一行代码if (this == &rhs) return *this,这样做的原因如下:
做类似下面的事时,就会发生自己给自己赋值的情况:
class x { ... };
x a;
a = a; // a赋值给自己
这种事做起来好象很无聊,但它完全是合法的,所以看到程序员这样做不要感到丝
毫的怀疑。更重要的是,给自己赋值的情况还可以以下面这种看起来更隐蔽的形式
出现:
a = b;
如果b是a的另一个名字(例如,已被初始化为a的引用),那这也是对自己赋值,
虽然表面上看起来不象。这是别名的一个例子:同一个对象有两个以上的名字。
4 析构函数:用着基类的析构函数必须为虚函数,析构函数执行时先调用派生类的析构函数,其次才调用基类的析构函数。如果析构函数不是虚函数,而程序执行时又要通过基类的指针去销毁派生类的动态对象,那么用delete销毁对象时,只调用了基类的析构函数,未调用派生类的析构函数。这样会造成销毁对象不完全。