无法触及的构造函数?

Posted by zhangxiaojian on August 11, 2014

Pre:

在More Effective C++条款8中,介绍new operator的时候,说new operator是由两部分组成的,一部分是operator new,用来根据对象大小申请空间;另一部分是对象的构造函数,用来初始化对象。代码描述如下:

//申请空间,用来存放string对象
void *memory = operator new(sizeof(string));
call string::string("Memory Management") on memory;
string *ps = static_cast<string*>(memory);

意思简单明了,接着它又这么描述:

注意上述第二个步骤“调用一个construct”,身为程序员的你没有权利这么做。然而你的编译器百无禁忌,可以为所欲为。这就是为什么你想做出一个head-based object,一定得使用new operator的原因:你无法直接调用“对象初始化所必需的的construct”(尤其是它可能得为重要成分vtbl设定初值)

仔细想想,调用构造函数的时候都给了它一个构造好放置对象的“标记”,比如:

string s("Hello Luffy!");

编译器在栈空间构造的string对象由局部变量s标记,是编译器把构造函数构造好的对象和局部变量联系在了一起。如果在堆上分配一个string对象,那就像上面所说的,需要使用new operator来把构造函数构造好的对象和申请的空间联系在一起。如果我们直接调用构造函数呢?

string::string("Hello Luffy!");

因为构造函数是没有任何返回值的,所以仅仅是构造了一个对象,并没有办法使用。就好像构造函数对程序员来说是无法触及的,它永远隐藏在编译器之下。

Mid:

对于c++语言,如果要我想一个广告语,那一定是 never say never,来看看这个程序:

class Data{
    public:
        Data(int a,Data* &p):data(a)
    { 
        p = this; 
    }
        ~Data(){ std::cout<<"call deconstruct"<<std::endl; }
        void printData()
        { 
            std::cout<<"data: "<<data<<std::endl; 
        }
    private:
        int data;
};

int main()
{
    Data* rawMemory;

    Data::Data(2,rawMemory);

    rawMemory->printData();

    return 0;
}

目的是在构造函数中,将this指针指向外部的指针,这样就将构造的对象和外部的“标记”联系在了一起,我们可以使用了,来看看结果是否如愿:

>> call deconstruct
>> data: 2

看来结果不错,正确的输出了值,但是在输出值之前调用了析构函数。语义很正确,构造函数构造的临时对象没有找到可以联系在一起的外部变量,匆匆构造然后析构了事。那我们怎么还可以输出正确的值呢?

因为这里的析构函数其实是一个空壳,Data类即没有拥有析构函数的父类,又没有拥有析构函数的数据成员。因此它不会合成一个析构函数,如果用户自己定义了,就像Data类一样,那么也只是像一个普通函数一样去执行,编译器不会对它进行任何的扩充处理。

如果数据成员不是int,而是拥有析构函数的对象,比如string,将上述程序的构造函数和main中调用的参数略作修改,再执行发现结果成了这样:

>> call deconstruct
>> data:

没有数据了,因为在析构函数中调用了string的析构函数,它把数据析构掉了!不过这里能不能输出正确的值,完全取决于成员变量析构函数的行为,如果没有在析构函数中把值改变,那么就能够得到我们预期的行为。

High:

Pre部分灰色斜体括号中强调尤其是有vtbl的时候。那么我们就试试有vtbl的情况,定义一个有虚函数的基类:

class Original{
    public:
        Original():orig(10){}
        virtual void printData(){ std::cout<<"data from original : "<<orig<<std::endl; }
    public:
        int orig;
};

同时修改Data的定义让它继承Original:

class Data:public Original

其余部分不变。来看看结果:

>> call deconstruct
>> data: 2

看来有vtbl也没有影响到程序的行为,这又是析构函数的特性帮的忙,因为class Data这时虽然有vtbl指针,但是父类没有定义析构函数,编译器不会为Data的析构函数做多余的任何事,即使在class Original中将data member改为有析构函数的string类型,Original有了一个编译器为它合成的析构函数,情况依然没有任何改变!因为对于class Data来说,它还是看不到父类的析构函数。

那么就为Original定义一个析构函数:

~Original(){ 
    std::cout<<"~Original.."<<std::endl; 
}

没什么特别的,但是再看看结果:

    >> call deconstruct
    >> ~Original..
    >> data from original : 10

不一样了!这时子类检测到了父类的析构函数,编译器在调用Data析构函数的时候,默默的将虚函数指针改变,指向父类的vtbl,调用到的是父类的虚函数。当然,能正确访问到父类数据成员,也得益于数据成员本身并没有被自己的析构函数析构掉,这点和前面所述相同。

End:

其实构造函数看似无法触及,但是只要我们“别有用心”的绕过析构函数这一关,就可以直接用它,而不用借助编译器默默的帮助。即使是在堆上分配,也只需要多做一些工作:

    Data *dataMemory = static_cast<Data*>(operator new(sizeof(Data)));
    memcpy(dataMemory,rawMemory,sizeof(Data));

这种用法能够达到我们想要的效果,但是也受到析构函数的限制,结论是,编译器做的更好。乖乖的使用 new operaor吧。