游戏开发论坛

 找回密码
 立即注册
搜索
查看: 14788|回复: 10

C++基本功和 Design Pattern系列(3) constructor & destructor

[复制链接]

27

主题

179

帖子

259

积分

中级会员

Rank: 3Rank: 3

积分
259
发表于 2006-11-8 00:25:00 | 显示全部楼层 |阅读模式
======================================================
大家请把我的文章当参考,详细内容  还请参照 权威书籍
<c++ programming language>如果文中有错误和遗漏,
请指出,Aear会尽力更正, 谢谢!
======================================================
最近实在是太忙了,无工夫写呀。只能慢慢来了。呵呵,今天Aear讲的是class.ctor 也就是constructor, 和 class.dtor, destructor. 相信大家都知道constructor 和 destructor是做什么用的,基本功能我就不废话了。下面先说效率的问题,让我们看个简单的例子:

class SomeClass;   // forward declaration

class AnotherClass {
private:
    SomeClass SomeClassInstance;
public:
    AnotherClass(const SomeClass & Para) { SomeClassInstance = Para; };
    ~AnotherClass();
};

也许这是很多初学者经常写出来的代码,Aear以前也写过。让我们来看看这段代码有什么问题。

首先需要说明的是,在一个class实例化之前,所有的member都会被初始化,如果member是个class,那么那个class的constructor就会被调用。也就是说,在运行AnotherClass的constructor之前,SomeClass的constructor就已经运行了。接下来的代码里,SomeClassInstance又被重新执行了次 = 操作。也就是说,我们在给SomeClassInstance附初值的时候,调用了2次SomeClass的method. 这个浪费也太大了,比较标准的方式是使用初始化列表,如下:

    AnotherClass (const SomeClass & Para): SomeClassInstance(Para) {};
如果有多个类成员,可以用","来分割,如:

    AnotherClass (const SomeClass & Para1, UINT32 Para2):
                   SomeClassInstance(Para1),
                   SecondAttr(Para2),
                   ThirdAttr(Para3) {};
值得注意的是, 类成员的初始化顺序和在类中的声明顺序应该一致。这个是有compiler来控制的,并不根据你在AnotherClass的constructor中提供的初始化顺序来进行。所以,如果你想先初始化ThirdAttr,然后把ThirdAttr传到SecondAttr作为初始化参数,是会失败的。只有改变声明顺序才会成功。

同理,在声明类变量被附初值的时候,使用拷贝构造函数,效率更高:

=====错误=====
class x1;
x1 = x2;

=====正确=====
class x1(x2);

===================分割线===================

从上面的例子可以看到,几乎所有的class,都需要提供拷贝构造函数,也就是 className(const className &)。同时值得注意的是,如果提供了拷贝构造函数,一般也就需要提供 "="操作,也就是 className & operator = (const className &),说到 operator =, 也有必要强调下implicit type conversion的问题,这将会在以后的章节张有详细描述。至于为什么要提供 operator =,举个简单的例子:

class1 {
public:
    class1() { p = new int[100]; };
    ~class1() { delete[] p; };
private:
    char* p;
} x1, x2;

如果class1不提供operator =, 那么运行 x1 = x2的时候,C++会运行最基本的拷贝操作,也就是 x1.p = x2.p,那么在x1被释放的时候,delete p;被执行。这时候 x2再要访问p,p已经变成非法指针了。 也许有人会说,我才不会用x1 = x2这么危险的操作,那让我们看看更加隐性的操作吧,例子如下:

void func(class1 Para) {...};

func(x1);

这时候,c++会调用class1的拷贝构造函数,来把参数从x1里拷贝到Para,如果class1没有提供copy constructor,那么c++就执行简单拷贝工作,也就是 Para.p = x1。当func返回的时候,Para被释放,调用 Para.~class1(),并且delete p;那么x1.p就变成非法指针了。

这样大家就知道为什么要同时提供copy constructor和 operator =了吧。特别是在class里有指针的情况下,必须提供以上2个method。如果不想提供,可以把他们设为private,代码如下:

class1 {
...
private:
    class1 (const class1 &);
    class1 & operator = (const class1 &);
}
这样别人在执行 = 和 func()的时候就会报错了。

还有,在声明构造函数的时候,单参数的构造函数,最好都用explicit来声明,例如:

class1 {
public:
    class1(int Para) {...}
    ...
};

其中class1(int Para)是个单参数的构造函数,如果执行下列操作,如:

class1 x1 = 2;

的时候,因为2不是class1,所以c++会用隐性的类型转换,也就是把2转换成class1,因此会调用class1(2),然后用operator = 符值给 x1. 这种操作经常会产生很多问题。比如如果我们提供了 operator == ,那么 在 if(x1 == 2)的时候,c++也会进行类似的操作,可能会产生我们不需要的结果。所以,对于这种单参数的constructor 最好做如下声明:

explicit class1 (int Para) {...}

这样做再执行 class1 x1 = 2;的时候就会报错了,explicit的意思就是C++ 的compiler不能做隐性类型转换,必须由程序员做type cast,比如:

class1 x1 = static_cast<class1>(2) 才会成功。

===================分割线===================
在运行constructor的时候,值得注意的一点就是,如果在constructor里,要初始化会throw exception的代码,一定要在constructor里catch。比如:

class1 {
    class1()
    {
       pInt = new int[100];
       try {
           pClass2 = new pClass2;
       }catch(...)
       { delete pInt; throw; };
     }
}

大家看的明白了吧,如果不catch pClass2的exception,pInt分配的内存就不会释放,因为constructor如果失败,c++是不会调用destructor的。

===================分割线===================
最后关于destructor,需要注意的是,如果是被继承的base class,destructor一定要是virtual。比如:

BaseClass ()
{
public:
    BaseClass();
    virtual ~BaseClass();
}

DerivedClass : public BaseClass()
{
public:
    DerivedClass();
    ~DerivedClass();
}

BaseClass * pBase = static_cast<BaseClass *>(new DerivedClass());
delete pBase;

如果BaseClass的destructor是virtual,那么正确的ctor dtor调用顺序是:

BaseClass();
DerivedClass();
~DerivedClass();
~BaseClass();

如果不是Virtual,调用顺序是:

BaseClass();
DerivedClass();
~BaseClass();

也就是说,DerivedClass的派生类不能被正确调用,这主要是因为在delete的时候c++并不知道你delete的是 DerivedClass, 因此需要把BaseClass的 dtor 设置成 virtual, 这样可以使用 vptr在 vtbl中查找 destructor,从而能够正确的调用destructor。

===================分割线===================
从上面的例子大家也看出来了,如果是派生类,那么就要调用基类的constructor,在多层次的派生类创建过程中,所以基类的constructor都要被调用。 destructor同理。因此要想提高效率,可以在关键代码短使用非派生类。

也许有人会说,所有的constructor和destructor都被compiler inline了,但是即使是inline并且base class的constructor中不进行任何操作,c++也要为每个类设置vptr,也是有不需要的overhead。当然,我们得到效率的同时,失去的是可扩展性,良好的程序层次结构等等,大家要根据具体情况来权衡。


Aear的Blog,大家有空去坐坐 http://blog.sina.com.cn/u/1261532101 下次见。

8

主题

716

帖子

716

积分

高级会员

Rank: 4

积分
716
发表于 2006-11-8 13:15:00 | 显示全部楼层

Re:C++基本功和 Design Pattern系列(3) constructor & destructor

感谢aear分享
看过后,有几点补充如下

1. 推荐使用initialization list来initialize member values,其初始化顺序以member value的声明顺序为主

2. 对于初始化class
当T(int)不是explicit时,T x=3; 其实和 T x(3)一样是调用的class T的T(int)的这个ctor,两者不存在效率差异

3. 对于初始化class
T x1;
T x2 = x1; 其实和 T x2(x1); 一样是调用的class T的copy ctor,两者不存在效率差异

4. 当new的是个[],在delete时也要加上[],否则可能会导致内存释放不完全。
在早期的c++ compiler,出现这种情况时,需要程序员手动填入具体数目,如char * p = new char [100],则delete [100]p; 而现在的compiler,在new的时候会多分配一小块固定大小的memory,称作cookie,里面记录了相关信息,当delete时,就可以知道其具体大小数目了。
视compiler实现,如果new和new []使用相同大小cookie,像是VC6,即使漏写[]也没问题;但是当两者cookie不同,这时加[]和不加则行为未定义,大家可以想象其原因

5. 感觉aear太过于喜欢使用static_cast,BaseClass * pBase = static_cast<BaseClass *>(new DerivedClass());,这里是不需要static_cast,直接BaseClass *pBase = new DerivedClass;即可

248

主题

2674

帖子

2702

积分

金牌会员

Rank: 6Rank: 6

积分
2702
QQ
发表于 2006-11-8 13:35:00 | 显示全部楼层

Re:C++基本功和 Design Pattern系列(3) constructor & destructor


那个构造函数的调用好像是:
初始化的时候调用构造函数,而不在于他们的形式。
如 T x(o); T x = 0; 都是一样的,不过其实也只能适合单参数的情况,多参数也只能用第一种形式了。
拷贝初始化的情况,就是用同类型对象来初始化的情况。

除了这些,实际T x(o ), 和 T x =o; 有个微小的区别,不过我忘了哈哈~好像是关于类型转换的。


27

主题

179

帖子

259

积分

中级会员

Rank: 3Rank: 3

积分
259
 楼主| 发表于 2006-11-8 13:53:00 | 显示全部楼层

Re:C++基本功和 Design Pattern系列(3) constructor & destructor

谢楼上以及楼上的楼上,有几个小错误,已改

25

主题

304

帖子

311

积分

中级会员

Rank: 3Rank: 3

积分
311
发表于 2006-11-8 14:10:00 | 显示全部楼层

Re:C++基本功和 Design Pattern系列(3) constructor & destructor

我也来补充一下
c++也要为每个类设置vptr,这个是不一定的
如果基类没有任何虚函数存在,基类会没有VPTR,派生类调用的VPTR就会和基类的第一个成员变量冲突,因为派生类从类的内存开头寻找VPTR

8

主题

716

帖子

716

积分

高级会员

Rank: 4

积分
716
发表于 2006-11-8 14:33:00 | 显示全部楼层

Re:C++基本功和 Design Pattern系列(3) constructor & destructor

当ctor有多参数时,如果有默认参数存在,T x = 3同样也是可以的. 例如 T T(int x, int y=1, int z=2);

出家人不打诳语
- 不妄语,<<佛家五戒>>

没有调查,就没有发言权
- 毛泽东,<<反对本本主义>>

248

主题

2674

帖子

2702

积分

金牌会员

Rank: 6Rank: 6

积分
2702
QQ
发表于 2006-11-8 17:27:00 | 显示全部楼层

Re:C++基本功和 Design Pattern系列(3) constructor & destructor


c++的细节蛮隐秘的,有时从某处看过,不久就忘掉~
如果有人整理成一篇“c++X档案”,那就是功德无量了~

1

主题

18

帖子

18

积分

新手上路

Rank: 1

积分
18
发表于 2006-11-8 17:48:00 | 显示全部楼层

Re:C++基本功和 Design Pattern系列(3) constructor & destructor

在 T x=3,
到底是调用的拷贝构造 还是 默认转换+赋值运算??

1

主题

18

帖子

18

积分

新手上路

Rank: 1

积分
18
发表于 2006-11-8 17:52:00 | 显示全部楼层

Re:C++基本功和 Design Pattern系列(3) constructor & destructor

有一定收获

8

主题

716

帖子

716

积分

高级会员

Rank: 4

积分
716
发表于 2006-11-8 18:12:00 | 显示全部楼层

Re:C++基本功和 Design Pattern系列(3) constructor & destructor

其实很多时候,写一点点code跟一下,就会明白了.
您需要登录后才可以回帖 登录 | 立即注册

本版积分规则

作品发布|文章投稿|广告合作|关于本站|游戏开发论坛 ( 闽ICP备17032699号-3 )

GMT+8, 2026-1-25 19:26

Powered by Discuz! X3.4

Copyright © 2001-2021, Tencent Cloud.

快速回复 返回顶部 返回列表