C++必知系列(一)——构造/析构/赋值

一. 编译器何时为类生成合适的特殊默认函数

      当声明如下一个空类时:

                        class CA {};

一般认为C++编译会在背后默默帮你生成5个函数默认构造函数,拷贝构造函数,析构函数,赋值运算符重载函数,取地址运算符重载函数,结果类被扩展为如下形式:

                       class CA()

                       {

                            public:

                                  CA() {...}                                                                   

                                  CA(const CA& other) {...}                                          

                                  ~CA() {...}                                                                 

                                 CA& operator=(const CA& other) {...}                     

                                 CA* operator&() {...}                                               

                       }

但实际情况并非如此,编译器只有认为需要的时候才生成相应函数,这体现了C++的效率至上理念,对于如上空类根本就不需要生成任何函数,因为这些默认函数没有任何有意义的事可做,当你在类里显式声明了任何类型的构造函数时(包括拷贝构造函数),编译器便不会为你生成默认构造函数,同样其他四个函数当在类里有显式声明时,编译器都不会产生默认的,这里要特别强调一下operator&()函数,可能很多人不知道它的存在,如果你不去显式申明,确实没有存在的必要,就是获取对象的地址,但你在类里显式定义后,也许会改变它的默认意义,可能会造成使用者的困惑, 例如:

                        class CA { CA* operator&() {...}};

                        CA a;

                        CA* p = &a;   //这里&a等价与a.operator&()调用,其意义完全取决于设计者的实现,而未必就是取对象a的地址。

那么除去operator&(),在没有显式申明其他函数的前提下,编译器何时会为你生成这些特殊函数呢,以下列举几种常见情况(更多信息可参考《深度探索C++对象模型》):

1. 类里声明了虚函数

2. 基类体系某一个里申明了虚函数

3. 基类体系里某一个里显式定义了相应函数          

4. 含有类成员,类成员出现了以上情况之一                          

二. 拷贝构造/赋值操作注意事项

    当一个类里申明了一个引用成员或const成员,必须显式定义operator=(...)才能处理对象赋值问题,否则编译无法通过,如下所示:

                     class CB 

                     {

                       public:

                          CB() : a(1) , b(a) {}

                          int a;

                          int& b;

                     };

                      CB b , a;

                     a = b; //无法通过编译

当一个派生类显式定义了拷贝构造函数或operator=(...),他们并不会默认的去调基类的相应函数,这可能与你的预期不一致,导致对象拷贝赋值的不完全,如下所示:

                   class Base

                   {

                    public:

                        Base() {...}

                        Base(const Base& other) : a(other.a) {}

                        Base& operator=(const Base& other) { a = other.a; }

                        int a;

                   };

                   class Derive : public Base

                   {

                     public:

                          Derive() {...}

                          Derive(const Derive& other) : b(other.b) {...}                         //不是调用Base的拷贝构造函数,而是调用无参构造函数Base(),导致Base部分没有拷贝

                          Derive& operator=(const Derive& other) { b = other.b; }      //没有调用基类operator=(...)函数

                          int b;

                   }

要想实现完全拷贝,需要自己显式申明:

                   class Derive : public Base

                   {

                     public:

                          Derive() {...}

                          Derive(const Derive& other) : Base(other), b(other.b) {...}                     

                          Derive& operator=((const Derive& other) { Base::operator=(other); b = other.b; }            

                          int b;

                   }

                   

三. 虚拟析构函数

     在C++里,构造函数是没办法被显式调用的,只能由编译器调用,但析构函数是可以被显式调用的。一个类是否都应该有一个显式的虚拟析构函数呢?不一定,只有当该类需要当作基类,而且需要delete一个该基类指针,而该基类指针实际指向其派生类时,才需要,目的是防止内存泄漏,当不存在如此情况时,若不需要在析构函数里做什么工作,可完全不声明,由编译器去决定是否需要生成一个默认的析构函数。析构函数有时会声明为纯虚的,这一般出现在定义一个不能实例化的类,但该类除了析构函数没有其他的虚函数,此时可将析构函数声明为纯虚的。

四. 构造/析构函数里原则上不应该有的操作

     在构造/析构函数里一般不要调用虚函数,其行为一般非你所预期,这依赖于实现,目前的实现来讲,不会发生多态行为,而是像调用一个普通成员函数一样直接产生编译期绑定,如下例:       

           class Base
          {
           public:
               Base() { test(); }
               virtual void test() {cout << "Base::test" << endl; }
          };
          class Derive : public Base
         {
          public:
            Derive() { test(); }
             virtual void test() { cout << "Derive::test" << endl; }
         };

  Derive d;

 输出:Base::test

           Derive::test

另不要在构造/析构完成过复杂容易引起异常的操作。 

五. 限制对象创建与复制

     当想让一个对象只能在栈上分配,而禁止在堆上分配时,可以通过将opertor new(size_t size)申明为类的私有函数方式实现,反过来可通过申明类的构造函数为私有的,同时定义一个静态工厂函数,实现里通过new的方式返回对象指针即可,这种方式也能控制对象创建的数量,当然还有其他的方法,对对象创建的控制无论是创建方式还是数量都基本可通过显式定义operator new(...)/operator delete(...), 控制构造/析构函数的访问权限,另外可能辅助一些静态工厂函数来实现。当想禁止一个对象的复制时,可通过将拷贝构造函数及operator=(...)显式申明为私有的。

六. 成员初始化列表机制

     一个类里成员的初始化有两种方式,常见的是在构造函数里初始化,但这并非真正的初始化,因为成员在进入构造函数体之前已完成了默认的初始化工作,在构造函数内都只能算赋值动作,如果想真正显式执行特定的类成员初始化动作,可采用第二种初始化列表机制,如下类:

class CA

{

 public:

    CA() {...}

    CA(int a) {...}

};

class CB

{

public:

   CB(int a) : ca(a) {...}   //通过初始化列表机制显式指定相应ca的构造方法  

   CA ca;

 需要注意的是初始化是按照成员在类里申明顺序初始化的,这有时可能会导致一些隐晦的错误,例如如下定义:

             class CA

             {

              public:

                  CA(): j(1),i(j) {}

             private:

                 int i;

                 int j;

             };

这里虽然j的初始化放在i之前,但由于声明时i在j之前,所以i会先初始化,而此时j处于不确定状态。

 另外需要注意的是有些类成员只能通过成员初始化列表机制初始化,有如下几种常见情况:

1. 类成员没有默认构造函数或可不带参数调用的构造函数(注:这里没有说无参构造函数是考虑到默认参数的存在);

2. 类成员为引用类型;

3. 类成员为const修饰型;

七. 类对象的构造过程

     为了下面叙述方便,先提出一个扩展构造函数的概念,由编译器在你编写的实际构造函数前插入必要代码构成,当你定义一个类对象时,实际上初始化是通过调用扩展构造函数完成,在进入你所编写的构造函数之前,这段必要代码会完成基类扩展构造函数,类类型成员扩展构造函数调用,虚表指针初始化等关键工作,那么一个最具普遍意义的类对象的构造过程如下:

1. 调用类的扩展构造函数,当有继承存在时进行步骤2,否则到步骤3;

2. 当不存在虚拟继承时,调用基类的扩展构造函数,当存在多个基类时,按照申明顺序依次调用,基类的构造行为同样,这样就保证了按照继承树从跟往下构造的顺序;

   存在虚拟继承的情况比较复杂一点,平时很少用到,这里不细说了,具体细节可参看《深度探索C++对象模型》,这里指出一点技巧,可以利用虚拟继承的相关特性设计一个不能被继承使用的类。

3. 当存在自定义类型成员时,按照其在类里的申明顺序依次调用相应的扩展构造函数;

4. 进入你编写的实际构造函数完成整个构造过程;

下面举例说明:

class Base1

{

public:

   Base1() { cout << "Base1 Constructor..." << endl; }

};

class Base2

{

public:

  Base2() { cout << "Base2 Constructor..." << endl; }

};

class Member

{

public:

  Member() { cout << "Member Constructor..." << endl; }

};

class Derive : public Base1 , pubic Base2

{

public:

    Derive() { cout << "Derive Construcotor..." << endl; }

    Member member;

};

Derive d;

输出:

Base1 Constructor...

Base2 Constructor...

Member Constructor...

Derive Constructor...

 

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值