《高质量C++/C编程指南》第9章:类的构造函数、析构函数与赋值函数

构造函数、析构函数与赋值函数是每个类最基本的函数。它们太普通以致让人容易麻痹大意,其实这些貌似简单的函数就象没有顶盖的下水道那样危险。
       每个类只有一个析构函数和一个赋值函数,但可以有多个构造函数(包含一个拷贝构造函数,其它的称为普通构造函数)。对于任意一个类A,如果不想编写上述函数,C++编译器将自动为A产生四个缺省的函数,如
    A(void);                                     // 缺省的无参数构造函数
    A(const A &a);                         // 缺省的拷贝构造函数
    ~A(void);                                   // 缺省的析构函数
    A & operate =(const A &a);   // 缺省的赋值函数
 
这不禁让人疑惑,既然能自动生成函数,为什么还要程序员编写?
原因如下:
(1)如果使用“缺省的无参数构造函数”和“缺省的析构函数”,等于放弃了自主“初始化”和“清除”的机会, C++发明人Stroustrup的好心好意白费了。
(2)“缺省的拷贝构造函数”和“缺省的赋值函数”均采用“位拷贝”而非“值拷贝”的方式来实现,倘若类中含有指针变量,这两个函数注定将出错。
        对于那些没有吃够苦头的C++程序员,如果他说编写构造函数、析构函数与赋值函数很容易,可以不用动脑筋,表明他的认识还比较肤浅,水平有待于提高。
本章以类String的设计与实现为例,深入阐述被很多教科书忽视了的道理。String的结构如下:
    class String
    {
     public:
        String(const char *str = NULL);                   // 普通构造函数
        String(const String &other);                          // 拷贝构造函数
        ~ String(void);                                                  // 析构函数
        String & operate =(const String &other);    // 赋值函数
     private:
        char     *m_data;                                             // 用于保存字符串
    };
9.1 构造函数与析构函数的起源
       作为比C更先进的语言,C++提供了更好的机制来增强程序的安全性。C++编译器具有严格的类型安全检查功能,它几乎能找出程序中所有的语法问题,这的确帮了程序员的大忙。但是程序通过了编译检查并不表示错误已经不存在了,在“错误”的大家庭里,“语法错误”的地位只能算是小弟弟。级别高的错误通常隐藏得很深,就象狡猾的罪犯,想逮住他可不容易。
       根据经验,不少难以察觉的程序错误是由于变量没有被正确初始化或清除造成的,而初始化和清除工作很容易被人遗忘。 Stroustrup在设计C++语言时充分考虑了这个问题并很好地予以解决:把对象的初始化工作放在构造函数中,把清除工作放在析构函数中。当对象被创建时,构造函数被自动执行。当对象消亡时,析构函数被自动执行。这下就不用担心忘了对象的初始化和清除工作。
        构造函数与析构函数的名字不能随便起,必须让编译器认得出才可以被自动执行。Stroustrup的命名方法既简单又合理:让构造函数、析构函数与类同名,由于析构函数的目的与构造函数的相反,就加前缀‘ ~’以示区别。
        除了名字外,构造函数与析构函数的另一个特别之处是没有返回值类型,这与返回值类型为void的函数不同。构造函数与析构函数的使命非常明确,就象出生与死亡,光溜溜地来光溜溜地去。如果它们有返回值类型,那么编译器将不知所措。为了防止节外生枝,干脆规定没有返回值类型。(以上典故参考了文献[Eekel, p55-p56])
9.2 构造函数的初始化表
       构造函数有个特殊的初始化方式叫“初始化表达式表”(简称初始化表)。初始化表位于函数参数表之后,却在函数体 {} 之前。这说明该表里的初始化工作发生在函数体内的任何代码被执行之前。
       构造函数初始化表的使用规则:
       ①如果类存在继承关系,派生类必须在其初始化表里调用基类的构造函数。
例如
    class A
    {
        A(int x);     // A的构造函数
}; 
    class B : public A
    {
        B(int x, int y);// B的构造函数
    };
   B::B(int x, int y)
     : A(x)             // 在初始化表里调用A的构造函数
    {
     …
}   
      ② 类的const常量只能在初始化表里被初始化,因为它不能在函数体内用赋值的方式来初始化(参见 5.4节)。
      ③类的数据成员的初始化可以采用初始化表或函数体内赋值两种方式,这两种方式的效率不完全相同。
      非内部数据类型的成员对象应当采用第一种方式初始化,以获取更高的效率。例如
    class A
{…
    A(void);                                              // 无参数构造函数
    A(const A &other);                           // 拷贝构造函数
    A & operate =( const A &other);    // 赋值函数
};
 
    class B
    {
     public:
        B(const A &a);    // B的构造函数
     private:   
        A m_a;                 // 成员对象
};
 
示例 9-2(a)中,类B的构造函数在其初始化表里调用了类A的拷贝构造函数,从而将成员对象m_a初始化。
示例 9-2 (b)中,类B的构造函数在函数体内用赋值的方式将成员对象m_a初始化。我们看到的只是一条赋值语句,但实际上B的构造函数干了两件事:先暗地里创建m_a对象(调用了A的无参数构造函数),再调用类A的赋值函数,将参数a赋给m_a。
 
B::B(const A &a)
 : m_a(a)           
{
   …
}
B::B(const A &a)
{
m_a = a;
}
        示例9-2(a) 成员对象在初始化表中被初始化      示例9-2(b) 成员对象在函数体内被初始化
 
        对于内部数据类型的数据成员而言,两种初始化方式的效率几乎没有区别,但后者的程序版式似乎更清晰些。若类 F的声明如下:
class F
{
 public:
    F(int x, int y);     // 构造函数
 private:
    int m_x, m_y;
    int m_i, m_j;
}
示例 9-2(c)中F的构造函数采用了第一种初始化方式,示例9-2(d)中F的构造函数采用了第二种初始化方式。
 
F::F(int x, int y)
 : m_x(x), m_y(y)           
{
   m_i = 0;
   m_j = 0;
}
F::F(int x, int y)
{
   m_x = x;
   m_y = y;
   m_i = 0;
   m_j = 0;
}
  示例9-2(c) 数据成员在初始化表中被初始化     示例9-2(d) 数据成员在函数体内被初始化
9.3 构造和析构的次序
        构造从类层次的最根处开始,在每一层中,首先调用基类的构造函数,然后调用成员对象的构造函数。析构则严格按照与构造相反的次序执行,该次序是唯一的,否则编译器将无法自动执行析构过程。
一个有趣的现象是,成员对象初始化的次序完全不受它们在初始化表中次序的影响,只由成员对象在类中声明的次序决定。这是因为类的声明是唯一的,而类的构造函数可以有多个,因此会有多个不同次序的初始化表。如果成员对象按照初始化表的次序进行构造,这将导致析构函数无法得到唯一的逆序。[Eckel, p260-261]
9.4 示例:类String的构造函数与析构函数
       // String的普通构造函数
       String::String(const char *str)
{
    if(str==NULL)
    {
        m_data = new char[1];
        *m_data = ‘/0’;
    }  
    else
    {
        int length = strlen(str);
        m_data = new char[length+1];
        strcpy(m_data, str);
    }
}   
 
       // String的析构函数
       String::~String(void)
{
    delete [] m_data;
// 由于m_data是内部数据类型,也可以写成 delete m_data;
       }

 

9.5 不要轻视拷贝构造函数与赋值函数

        由于并非所有的对象都会使用拷贝构造函数和赋值函数,程序员可能对这两个函数有些轻视。请先记住以下的警告,在阅读正文时就会多心:

①本章开头讲过,如果不主动编写拷贝构造函数和赋值函数,编译器将以“位拷贝”的方式自动生成缺省的函数。倘若类中含有指针变量,那么这两个缺省的函数就隐含了错误。以类String的两个对象a,b为例,假设a.m_data的内容为“hello”,b.m_data的内容为“world”。

现将a赋给b,缺省赋值函数的“位拷贝”意味着执行b.m_data = a.m_data。这将造成三个错误:一是b.m_data原有的内存没被释放,造成内存泄露;二是b.m_data和a.m_data指向同一块内存,a或b任何一方变动都会影响另一方;三是在对象被析构时,m_data被释放了两次。

②拷贝构造函数和赋值函数非常容易混淆,常导致错写、错用。拷贝构造函数是在对象被创建时调用的,而赋值函数只能被已经存在了的对象调用。以下程序中,第三个语句和第四个语句很相似,你分得清楚哪个调用了拷贝构造函数,哪个调用了赋值函数吗?

String  a(“hello”);

String  b(“world”);

String  c = a;    // 调用了拷贝构造函数,最好写成 c(a);

c = b;                // 调用了赋值函数

本例中第三个语句的风格较差,宜改写成String c(a) 以区别于第四个语句.

 

9.6 示例:类String的拷贝构造函数与赋值函数

// 拷贝构造函数

String::String(const String &other)

{

// 允许操作other的私有成员m_data

int length = strlen(other.m_data);  

m_data = new char[length+1];

strcpy(m_data, other.m_data);

}

// 赋值函数

String & String:operate =(const String &other)

{

// (1) 检查自赋值

 if(this == &other)

return *this;

// (2) 释放原有的内存资源

delete [] m_data;

// (3)分配新的内存资源,并复制内容

int length = strlen(other.m_data);  

m_data = new char[length+1];

strcpy(m_data, other.m_data);

// (4)返回本对象的引用

return *this;

}

        类String拷贝构造函数与普通构造函数(参见9.4节)的区别是:在函数入口处无需与NULL进行比较,这是因为“引用”不可能是NULL,而“指针”可以为NULL。

类String的赋值函数比构造函数复杂得多,分四步实现:

(1)第一步,检查自赋值。你可能会认为多此一举,难道有人会愚蠢到写出 a = a 这样的自赋值语句!的确不会。但是间接的自赋值仍有可能出现,例如

// 内容自赋值Oo5
©达内科技论坛 -- 达内科技论坛  ;"-tI
b = a;DKUd(
©达内科技论坛 -- 达内科技论坛  :;W
©达内科技论坛 -- 达内科技论坛  6
©达内科技论坛 -- 达内科技论坛  (Sn*>
c = b;V3,ed
©达内科技论坛 -- 达内科技论坛  3H*"nq
©达内科技论坛 -- 达内科技论坛  "a/.I
©达内科技论坛 -- 达内科技论坛  xEJH=v
a = c; hXb"
©达内科技论坛 -- 达内科技论坛  I7
©达内科技论坛 -- 达内科技论坛  Hbz
// 地址自赋值U
©达内科技论坛 -- 达内科技论坛  $+j
b = &a;S
©达内科技论坛 -- 达内科技论坛  !
©达内科技论坛 -- 达内科技论坛  6a
©达内科技论坛 -- 达内科技论坛  b:
a = *b;x+
©达内科技论坛 -- 达内科技论坛  XB)
©达内科技论坛 -- 达内科技论坛  Cx
©达内科技论坛 -- 达内科技论坛  (
也许有人会说:“即使出现自赋值,我也可以不理睬,大不了化点时间让对象复制自己而已,反正不会出错!”?.
©达内科技论坛 -- 达内科技论坛  q&OU
他真的说错了。看看第二步的delete,自杀后还能复制自己吗?所以,如果发现自赋值,应该马上终止函数。注意不要将检查自赋值的if语句a|
©达内科技论坛 -- 达内科技论坛  dN"
if(this == &other)o{K
©达内科技论坛 -- 达内科技论坛  hmTkl
错写成为d|D(<
©达内科技论坛 -- 达内科技论坛  pdly>

if( *this == other)!:/
©达内科技论坛 -- 达内科技论坛  FWZ-
(2)第二步,用delete释放原有的内存资源。如果现在不释放,以后就没机会了,将造成内存泄露。Vh
©达内科技论坛 -- 达内科技论坛  l8]cz
(3)第三步,分配新的内存资源,并复制字符串。注意函数strlen返回的是有效字符串长度,不包含结束符‘/0’。函数strcpy则连‘/0’一起复制。t<
©达内科技论坛 -- 达内科技论坛  d<.r8u
(4)第四步,返回本对象的引用,目的是为了实现象 a = b = c 这样的链式表达。注意不要将 return *this 错写成 return this 。那么能否写成return other 呢?效果不是一样吗?,[;1E
©达内科技论坛 -- 达内科技论坛  A&5jUu
不可以!因为我们不知道参数other的生命期。有可能other是个临时对象,在赋值结束后它马上消失,那么return other返回的将是垃圾。T

wR
9.7 偷懒的办法处理拷贝构造函数与赋值函数x
©达内科技论坛 -- 达内科技论坛  .y)#Wb
      如果我们实在不想编写拷贝构造函数和赋值函数,又不允许别人使用编译器生成的缺省函数,怎么办?GQ
©达内科技论坛 -- 达内科技论坛  LW>G.a
      偷懒的办法是:只需将拷贝构造函数和赋值函数声明为私有函数,不用编写代码。HWlv#R
©达内科技论坛 -- 达内科技论坛  [!M
例如:`V
©达内科技论坛 -- 达内科技论坛  BYKu
   class Ae7a$M[
©达内科技论坛 -- 达内科技论坛  ]8bY
   { …q.1
©达内科技论坛 -- 达内科技论坛  I3{p%7
     private:;{(K
©达内科技论坛 -- 达内科技论坛  F
       A(const A &a);                // 私有的拷贝构造函数<|782
©达内科技论坛 -- 达内科技论坛  6n ?
       A & operate =(const A &a);    // 私有的赋值函数m
©达内科技论坛 -- 达内科技论坛  ZLv"G
   };sst
©达内科技论坛 -- 达内科技论坛  >>!UEp
©达内科技论坛 -- 达内科技论坛  m
©达内科技论坛 -- 达内科技论坛  +
如果有人试图编写如下程序:!9>:u
©达内科技论坛 -- 达内科技论坛  -Hk
   A  b(a);    // 调用了私有的拷贝构造函数~,u
©达内科技论坛 -- 达内科技论坛  O,
   b = a;      // 调用了私有的赋值函数}eCWm
©达内科技论坛 -- 达内科技论坛   %
编译器将指出错误,因为外界不可以操作A的私有函数。F


9.8 如何在派生类中实现类的基本函数~G@
©达内科技论坛 -- 达内科技论坛  BXA`N
      基类的构造函数、析构函数、赋值函数都不能被派生类继承。如果类之间存在继承关系,在编写上述基本函数时应注意以下事项:_8S
©达内科技论坛 -- 达内科技论坛  G&:;1

派生类的构造函数应在其初始化表里调用基类的构造函数。.8Z5b

基类与派生类的析构函数应该为虚(即加virtual关键字)。例如

         #include

class Base
{
 public:
    virtual ~Base() { cout<< "~Base" << endl ; }
};
 
class Derived : public Base
{
 public:
    virtual ~Derived() { cout<< "~Derived" << endl ; }
};
 
void main(void)
{
    Base * pB = new Derived; // upcast
    delete pB;
}
 
输出结果为:
       ~Derived
       ~Base
如果析构函数不为虚,那么输出结果为
       ~Base
 
在编写派生类的赋值函数时,注意不要忘记对基类的数据成员重新赋值。例如:
class Base
{
 public:
    Base & operate =(const Base &other);    // 类Base的赋值函数
 private:
    int m_i, m_j, m_k;
};
 
class Derived : public Base
{
 public:
    Derived & operate =(const Derived &other);    // 类Derived的赋值函数
 private:
    int m_x, m_y, m_z;
};
 
Derived & Derived::operate =(const Derived &other)
{
    //(1)检查自赋值
    if(this == &other)
        return *this;
 
    //(2)对基类的数据成员重新赋值
    Base::operate =(other);    // 因为不能直接操作私有数据成员
 
    //(3)对派生类的数据成员赋值
    m_x = other.m_x;
    m_y = other.m_y;
    m_z = other.m_z;
 
    //(4)返回本对象的引用
    return *this;
}
 
9.9 一些心得体会
有些C++程序设计书籍称构造函数、析构函数和赋值函数是类的“Big-Three”,它们的确是任何类最重要的函数,不容轻视。
也许你认为本章的内容已经够多了,学会了就能平安无事,我不能作这个保证。如果你希望吃透“Big-Three”,请好好阅读参考文献[Cline] [Meyers] [Murry]。


Trackback: http://tb.blog.csdn.net/TrackBack.aspx?PostId=1476436


目 录 前 言6 第1 文件结构 1.1 版权和版本的声明. 1.2 头文件的结构. 1.3 定义文件的结构. 1.4 头文件的作用. 1.5 目录结构. 第2 程序的版式 2.1 空行. 2.2 代码行. 2.3 代码行内的空格. 2.4 对齐. 2.5 长行拆分. 2.6 修饰符的位置. 2.7 注释. 2.8 的版式. 第3 命名规则 3.1 共性规则. 3.2 简单的WINDOWS 应用程序命名规则. 3.3 简单的UNIX 应用程序命名规则 第4 表达式和基本语句 4.1 运算符的优先级. 4.2 复合表达式. 4.3 IF 语句 4.4 循环语句的效率. 4.5 FOR 语句的循环控制变量. 4.6 SWITCH 语句. 4.7 GOTO 语句. 第5 常量 5.1 为什么需要常量. 5.2 CONST 与 #DEFINE 的比较. 5.3 常量定义规则. 5.4 中的常量. 第6 函数设计 高质量C++/C 编程指南,v 1.0 2001 Page 4 of 101 6.1 参数的规则. 6.2 返回值的规则. 6.3 函数内部实现的规则. 6.4 其它建议. 6.5 使用断言. 6.6 引用与指针的比较. 第7 内存管理 7.1 内存分配方式 7.2 常见的内存错误及其对策 7.3 指针与数组的对比 7.4 指针参数是如何传递内存的? 7.5 FREE 和DELETE 把指针怎么啦? 7.6 动态内存会被自动释放吗?. 7.7 杜绝“野指针”. 7.8 有了MALLOC/FREE 为什么还要NEW/DELETE ?. 7.9 内存耗尽怎么办?. 7.10 MALLOC/FREE 的使用要点 7.11 NEW/DELETE 的使用要点. 7.12 一些心得体会 第8 C++函数的高级特性 8.1 函数重载的概念. 8.2 成员函数的重载、覆盖与隐藏. 8.3 参数的缺省值. 8.4 运算符重载. 8.5 函数内联. 8.6 一些心得体会. 第9 构造函数析构函数赋值函数 9.1 构造函数析构函数的起源. 9.2 构造函数的初始化表. 9.3 构造和析构的次序. 9.4 示例:STRING构造函数析构函数 9.5 不要轻视拷贝构造函数赋值函数. 9.6 示例:STRING 的拷贝构造函数赋值函数 9.7 偷懒的办法处理拷贝构造函数赋值函数. 9.8 如何在派生中实现的基本函数. 9.9 一些心得体会. 第10 的继承与组合. 高质量C++/C 编程指南,v 1.0 2001 Page 5 of 101 10.1 继承 10.2 组合 第11 其它编程经验. 11.1 使用CONST 提高函数的健壮性 11.2 提高程序的效率 11.3 一些有益的建议 参考文献 附录A :C++/C 代码审查表. 附录B :C++/C 试题. 附录C :C++/C 试题的答案与评分标准.
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值