C++细节之四----再论四大函数

本文探讨了C++中类的构造过程,包括构造函数、拷贝构造函数、赋值函数及析构函数的设计原则。重点讲解了成员变量初始化、拷贝构造函数实现、赋值函数编写以及析构函数注意事项。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

     在建立一个类的过程中,首先应该考虑的是,有哪些成员变量,即对象拥有的属性,第二步抽象出类的接口,即需要哪些成员函数,并确定访问权限。第三步,当然是考虑如何为类建立构造函数和析构函数了。对于成员变量中有指针的情况,需要提供拷贝构造函数和赋值函数。

 

     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销毁对象时,只调用了基类的析构函数,未调用派生类的析构函数。这样会造成销毁对象不完全。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值