深度搜索C++对象模型: 构造,拷贝,析构语义学
//*********************************************************************************************//
---> "构造函数语义学":
class A
{
public:
virtual ~A() = 0;
virtual int Func() { return m_num; };
protected:
int m_num;
};
class B : public A
{
};
观察两个类 我们提出一些问题:
-> 第一 class A 并没有提供构造函数, 当我们需要一个 class B object 时, 如果要对其对象"初始化",
那么责任就落到了 class B 身上, 需要在 class B 里访问 base class 的 protected 对象, 并设定初值,
那么如果当有一天 class B 不在被需要了, 那么 class A 的对象初始化工作也就没有人做了 这并不是一个好的习惯,
所以我们建议自己的 data member, 自己初始化, 任何其他操作会破坏封装;
-> 第二 class A 中有一个纯虚函数, 那么 class A 就不能被实例化, 但是任然可以在子类成员函数中"静态调用"纯虚函数
B::Func2() { A::Func(); };
但是把析构函数设置为了纯虚函数, 那么就一定要定义他, 因为对于派生类来说 他会在"析构时静态调用其基类的析构函数",
如果不定义就会链接失败;
-> 第三 class A 中有一个 virtual int Func(); 但是观察其定义内容, 他完全不和类的类型有关 ( 必须是派生类调用or基类调用 )
他几乎不可能会被后继的派生类改写, 所以此时必须由编译器判断其是否需要改写 vtbl, 但是这么做效率上的报应可谓不轻,
//-----------------------------------------------------------------------------------------------------------------------//
--> "无继承情况下的对象构造":
class A
{
};
A global_a;
A Func()
{
A local_a;
A *heap_a = new A;
*heap_a = local_a;
delete heap_a;
return local_a;
}
以上程序表现出三种不同的对象产生方式: "global内存配置", "local内存配置", "heap内存配置";
一个 object 的声明, 是该 object 的一个"执行期属性";
--> "global object" 的生命和整个程序生命相同;
当编译器遇上 global object 的定义时, "观念上的 trivial constructor 和 destructor" ( 实际上并不会产生 观念上的 ), 都会被产生和调用;
constructor 在程序的起始处被调用, destructor 在程序的 exit() 处被调用, ( 事实上 这只是一个观念上的东西, 因为他压根就不会产生构造和析构 );
而在C++中, 全局对象被认为是完全定义的, 他会阻止第二个或者更多定义,
-> "local object" 的生命从定义开始, 到作用域结束 ( 遇上 } );
事实上 local object 同样是没有被构造也没有被析构, 如果程序在之后就要使用他的初值 那么没有构造就是一个败笔;
-> "heap object" 的声明从他被 new 运算符配置出来开始, 直到他被 delete 运算符摧毁为止;
而对 heap object 的初始化操作, 会被转化为对 new 运算符的调用( 由 library 提供 ): A *heap_a = __new( sizeof( A ) );
此时 并没有默认构造函数施加于 __new 传回来的 object 身上,
而对 heap object 的delete操作, 会被转化为对 delete 运算符的调用( 由 library 提供 ): __delete( heap_a );
之后, *heap_a = local_a; 这只是一个赋值操作, 如果 local_a 里是正确的数据, 那么一切都没有问题;
总结: "观念上 这些操作都会触发我们所谓的 构造函数, 复制构造函数, 析构函数, 但事实上并不是";
我们认为会触发构造函数的地方, 都不会触发, 因为并没有需要构造函数来初始化的 object;
我们认为会触发复制构造函数的地方, 都不会触发, 因为这些操作都是由"位拷贝操作"完成, 完全不需要复制构造函数;
我们认为会触发析构函数的地方, 都不会触发, 理由同构造函数, 完全没有需要析构函数来删除的object;
class A
{
public:
A( int num = 0 ) : m_num( num ) { };
private:
int m_num;
};
A global_a;
A Func()
{
A local_a;
A *heap_a = new A;
*heap_a = local_a;
delete heap_a;
return local_a;
}
分析这个程序和上面程序的区别, 唯一就是类中多了一个 "自己定义的构造函数";
( 题外话: 如果要对一个值初始化, 那么给出一个初始化成员列表 就像这个程序, 会比 相同语义的构造函数效率高 )
现在 在 构造出的三个对象里, 他们"都会调用到我们所定义的构造函数", 剩余的都和上面一样;
注意, 这些操作"仍然不会导致析构函数或者复制构造函数的调用", 因为我们根本没有定义他们;
//-----------------------------------------------------------------------------------------------------------------------//
--> "继承情况下的对象构造":
class A
{
public:
A() { std::cout << "Constructor A" << Size() << std::endl; }
virtual int Size() { return sizeof( A ); };
int m_num_a;
};
class B : public A
{
public:
B() { std::cout << "Constructor B" << Size() << std::endl; }
virtual int Size() { return sizeof( B ); };
int m_num_b;
};
class C : public B
{
public:
C() { std::cout << "Constructor C" << Size() << std::endl; }
virtual int Size() { return sizeof( C ); };
int m_num_c;
};
如果我们 C c;会怎么样 ?
先讨论一下构造函数, 当我们构造 c 的时候; 编译器会在 C::C() 中内插代码 来初始化其 subobject,
class C 扩张: C::C() { B::B(); } class B 扩张: B::B() { A::A(); }
根据这个扩张我们发现 其实他们是一个递归调用, 而且内插的代码在"执行代码之前";
所以结果应该是先输出最内层的代码, 在逐次往外, 这就解释了为什么 输出结果的一部分是 A B C;
在讨论 内含的 virtual 函数, 我们构造 C c; 的时候 内含的三个 Size() 函数会被怎么决议?
毕竟我们在构造的是 C c; 那么他会被决议为 C::Size() ? 或者是我们正在构建的 this::Size() ?
在C++语言规则中有一条: "在一个 class 中, 经由构造中的对象来调用一个 virtual function, 其函数实例应该是此 class 中有作用的那个";
通俗点说: 在构造过程中调用虚函数, 那么构造到哪一步 调用到哪一步; 所以结论是: 8 12 16 ( vptr + data member );
总结: "通常 constructor 的执行算法":
-> 在 derived class constructor中, 所有 virtual base class 以及上一层 base class 的 constructor 会被调用;
( 解释一下: 如果是虚拟继承 会直接由构建的 derived class object 来构造其虚拟继承的 base class, 而且只构造一次 通过一个标志,条件确定是否构造过,
然后调用一次 上一层的 base class constructor )
-> 上述完成之后, 对象的 vptr 被初始化, 指向合适的 vtbl;
-> 如果有 member init list的话 将在 constructor 中扩展开, 这必须在 vptr 初始化之后;
-> 最后 执行程序员提供的代码;
所以一个构造函数一般被构建为如下形态: ( A为 base class, B虚拟继承A, C继承B )
C::C()
{
if( __most__derived != false ) // 条件判断虚基类, 初始化之后把标志位设置为false;
this->A::A();
this->B::B(); // 无条件调用上一层 base class 的构造函数
this->vptr = &vtbl; // vptr 的初始化
//code
}
//*********************************************************************************************//
---> "对象复制语义学( operator=() )":
当我们设计一个 class, 并以一个 class object 指定给另一个 class object 时, 我们有三种选择:
-> 什么都不作 实施默认行为;
-> 提供一个i额显示的 operaotr=();
-> 显示的拒绝这个行为;
如果要实现第三点, 把 operator= 声明为 private 就可以了;
现在的问题是:我们什么时候需要一个 显示的 operator=() ?
如果我们要支持的只是一个简单的拷贝操作, 而且默认行为已经足够 我们没有理由去提供他, 只有当默认行为导致不安全或不正确时 我们才应该去设计他;
事实上 只有我们需要拷贝的是一个 subobject 或者 需要拷贝一个 pointer 指向的堆我们需要 复制构造函数和对象复制 其他时候 我们并不需要;
那么什么时候用复制构造函数, 什么时候用对象复制?
A a;
A b = a; // 对于这种情况 表面上一个对象复制 实际上因为此时要新建一个对象b, 所以调用的是拷贝构造函数;
a = b; // 对于已经新建好的对象, 赋值之间调用的就是 对象复制 operator=();
//*********************************************************************************************//
---> "析构函数语义学":
"析构函数被编译器合成的条件和构造函数不同":
--> 在 class 内含 member object, 而且这个 member object 有析构函数;
--> 这个类是上述类的派生类;
"就算有 virtual function 机制, 仍然不一定合成析构函数"
那么什么时候会"有一个析构函数"? ( 不管是合成了还是声明了 )
--> 当我们在构造函数里分配了某些资源的时候 我们自然要在析构函数里收回资源
那么什么时候我们"需要一个虚析构函数"?
--> 当我们希望使用一根父类的指针来对子类对象执行delete操作的时候,
如果不是虚析构函数, 那么他即将调用的是父类的析构函数, 这不合适, 所以需要虚析构函数让他来自动根据对象来调用子类的析构函数;
那么析构函数都干了什么?
--> 首先 destructor 本身首先被执行;
--> 如果 class 拥有 member class object, 而且这个 member class object 拥有 destructor, 那么会以声明顺序反向调用;
--> 如果 object 内含 vptr, 此时调整 vptr, 指向合适的 vtbl;
--> 如果上一层 base class 拥有 destructor, 按照声明顺序反向调用其 destructor;
--> 如果 virtual base class 拥有 destructor, 按照声明顺序反向调用其 destructor;
所以一个析构函数很可能被扩张为如下形式:( A 为 base class, B 继承 A 且内含X, C 继承 B 且内含 X, 他们都有析构函数 和 vptr )
C::~C()
{
// code;
X::~X();
this->vptr = vtbl;
B::~B();
}
B::~B()
{
// code;
X::~X();
this->ptr = vtbl;
A::~A();
}
//*********************************************************************************************//