effective-c++系列:对象模型杂谈(2)

在构造函数和析构函数中能调用虚函数吗?

  • 严禁在构造函数中调用虚函数

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    class A{
    int data;
    public:
    virtual void dosome()const{};
    A(){
    dosome();
    }
    };
    class B : public A{
    public:
    virtual void dosome(){};
    };
    B b;//往往会出现意想不到的错误
    • 如果在父类的构造函数中调用虚函数,当使用子类对象的时候,会首先调用父类的构造函数。
    • 在父类构造函数执行过程中,this指针为base class对象,因此在dosome()函数中的操作是在父类对象中完成的。
    • 由于虚函数dosome()在父类对象中完成,往往达不到多态(延迟绑定)的目的。
  • 严禁在析构函数中调用虚函数

    • 在子类对象析构时顺序为先将自身的local data释放,然后调用父类的析构函数,如果父类析构存在虚函数,那么编译器会有两种选择:调用虚函数的基类版本或者调用虚函数的派生类版本。
    • 如果调用派生类版本的函数,此时子类部分已经释放,会导致严重错误。
    • 实际情况是编译器会调用基类版本的虚函数,那么和构造函数一样,不会发生多态。
  • 解决方式

    • 将在构造或析构函数中调用的函数设置为non-virtual,要求子类对象在构造时传递必要的信息。
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    class A{
    int data;
    public:
    void dosome(const string& info)const{};
    A(const string& info){
    dosome(info);
    }
    };
    class B : public A{
    private:
    static string createParamter();
    public:
    virtual void dosome(){};
    B():A(createParamter()){};
    };

如果new、delete、delete[]没有成对使用会发生什么?

单个对象和对象数组的内存布局
  • 使用delete释放对象数组

    1
    2
    3
    string* s = new string[100];
    //do something
    delete s;
    • 上述情况下,只会调用第一个s的函数,其余99个对象不执行任何操作,后面程序也不会再拿到其句柄,从而会造成内存泄露。
    • 实际上,对于一组对象,delete[]会寻找数组前的一个count计数来获取数组长度。
  • 使用delete[]释放单个对象

    1
    2
    3
    string* s = new string;
    //do something
    delete[] s;// Exception has occurred.Segmentation fault
    • delete[]在内存中读取s指针前4字节内存作为数组长度,这样做显然是非法的。

设计一个class需要注意什么?

  • 对象创建和销毁(构造、析构函数、内存分配)
  • 对象初始化和赋值
  • 对象如果pass by value应该怎么做?(如果对象内存在指针,是否需要深拷贝)
  • 定义class约束(数据类型检查)
  • 继承或者被继承
  • 类型转换(隐式类型转换)
  • 操作符重载
  • 标准函数是否适用(拷贝赋值,拷贝构造)