C++对象模型知识点摘录

409 阅读9分钟

本编博文是对《深度探索C++对象模型》的诸多网络读书笔记博客的摘录,因摘录博文较多,故未一列举博文地址,望请见谅!

1.对象模型类型(对象在内存中的存在形式)

  • 简单对象模型(A Simple Object Model)
    在Object中不存储任何直接的data member 或 function member, object中存储的都是指针,这个指针指向members。 这就是简单对象模型,对于此模型,可以避免诸如“不同的数据类型,需要不同的的存储空间”这一类的问题。

  • 表格驱动对象模型(A Table-driven Object Model)
    在object中,只存在两个指针,一个指向member data table, 另一个指向member Function table. 在member data talbe中存在着实际的数据,在member function talbe中存在的只是成员函数的指针。

2.封装在空间上的代价:

  • 如果仅仅是普通继承则并没有什么代价,因为每个数据成员直接内涵在每一个class对象中,和c中的struct一样,而member function如果不是inline的则只会产生一个实体,而inline会在每一个使用的模块产生一个函数实体。

  • C++中凡是处于一个access section中的数据,在内存中必定保证按照声明顺序在内存中进行布局。而对于多个access section中的各笔数据,内存布局则不敢保证。也就说即使你把protect放在前面,而把public放在后面,也不敢保证protect的数据在内存中排列在public前面。

  • 一个指针,不管它指向哪一种数据类型,它本身的大小时固定的。不同类型的指针之间的差异,不在于指针表示法不同,也不在其内容不同,而在于它所指向的object的类型不同。也就是说,不同类型的指针会告诉编译器对它所指向的地址及内容的不同的解释方式。类型不同,则对地址和内容的解释方式不同。

c++在空间和存取效率的代价主要由virtual带来:

3.运行时多态必须通过public继承实现

这个设计是符合逻辑的. 可以设想, 如果使用其他继承方式, 那么从逻辑上说, 在类外不应该能访问父类成 员. 但是要实现运行时多态, 正常做法是将子类指针/引用赋值给一个父类类型的指针/引用(设为bp), 一旦复制成功, 我们就可以通过bp访问父类的public成员, 这显然与前面的逻辑要求矛盾. 所以, 在C++中, 前面说得"赋值"是违法的. 而没有这个"赋值"操作, 也就无法实现运行时多态, 因此必须通过public继承实现运行时多态.

4.编译器为类生成有效构造函数的情形

首先指出两个误解:
1)任何类如果没有定义默认构造函数,就会被合成出来一个。
2)编译器合成出来的默认构造函数会显式设定“类内每一个数据成员的默认值”。
上述两种说法都是错误的!

以下情形编译器默认构造函数是无效的(trival),编译器并不会生成它

以下情形编译器生成的默认构造函数才是有效的(nontrivial)

  • 类中的成员对象含有默认构造函数 如果该类没有构造函数,但它含有的对象拥有默认构造函数,那么编译器会生成implicit inline notrivial的默认构造函数,如果函数太复杂,会合成explicit static实例。该默认构造函数会按顺序调 用相应成员的默认构造函数。
    如果该类有构造函数,编译器会在每个构造函数中首部安插代码来调用类成员相应的默认构造函数。 总的意思就是,类成员有默认构造函数,说明它需要被初始化,所以编译器需要做工作去初始化他们。
  • 该类派生于带有默认构造函数的基类 基类有默认构造函数,说明基类需要被正确初始化,此时会通过生成默认构造函数或安插代码先调用基类的默认构造函数初始化基类部分,而后处理相应的类成员(如果有的话)。
  • 存在虚函数的类
    原理同上,编译器生成相关代码初始化vptr和vprtable。
  • 带有虚基类
    多重继承中的虚基类在子类对象中只保留一个,需要构造函数设置相应的虚基类对象指针。

拷贝构造函数也分为trivial和nontrivial,并且nontrivial才会被编译器真正合成。

拷贝构造也分为trivial和nontrivial,并且nontrivial才会被编译器真正合成。

调用拷贝构造函数的情况:

class X{};
X x1 = X(); *// 默认构造*
X x2 = x1; *// 拷贝构造*
void foo(X x);
X f()
{
X tmp;
foo(tmp); *// 拷贝构造*
return tmp; *// 拷贝构造函数返回值*
}
按位逐次拷贝

像C中的POD类型的数据一个bit一个bit的拷贝。如果拷贝构造函数是trivial那么就会按位逐次拷贝,并不会生成它

拷贝构造函数合成时机(nontrivial)
  • 一个类包含的某个成员设置了默认的拷贝构造函数。(调用成员的默认拷贝构造函数)
  • 一个类继承自一个含有默认拷贝构造函数的基类。(调用积累中的默认拷贝构造函数)
  • 一个类含有虚函数。(正确拷贝vptr)
  • 一个类继承自虚基类体系。(正确设置虚基对象指针)
//第3种情况:
class ZooAnimal
{
public:
ZooAnimal();
virtual ~ZooAnimal();
virtual void animate();
virtual void draw();
*// ...*
};
class Bear: public ZooAnimal
{
public:
Bear();
void animate(); *// virtual*
void draw(); *// virtual*
virtual void dance();
*// ...*
};
Bear yogi;
*// 存在虚函数*
*// 编译器生成默认拷贝构造,正确设置w**innie**对象中的vptr*
Bear winnie = yogi;

5.成员初始化列表

使用成员初始化列表的情况:

  • 初始化一个引用成员
  • 初始化一个const成员
  • 调用一个含参的基类的构造函数
  • 调用一个成员的含参的构造函数 类成员的初始化顺序和初始化列表的顺序无关,==而是与类成员的声明顺序一致==,在编译阶段会将初始化列表的初始化操作转化为代码安插在构造函数的显式代码之前。
1.最好不要在初始化列表用一个成员初始化另一个成员,容易在顺序上发生罕见的错误。
2.在初始化列表可以调用成员函数来初始化成员,但要求该函数不依赖于这个类的数据成员,因为有可能在调用该函数时有某些成员还未初始化。
3.最好不要用成员函数来初始化基类成员。

6.成员函数指针使用方法

成员函数指针只能指向类的非静态成员函数, 使用方法如下:

struct C
{
    void f(int i) {}
};
void (C::* p)(int) = &C::f; // pointer to member function
C c, *cp = &c;
(c.*p)(1); // 通过对象调用函数f
(cp->*p)(2); // 通过对象指针调用函数f

父类成员函数指针可以直接赋值给子类成员函数指针, 如下面的例子:

struct B
{
    virtual void f() {}
};

struct D : B
{
    virtual void f() {}
};

void (B::* bf)() = &B::f;
void (D::* df)() = bf;

B bp = new D;
(bp.*bf)(); // 调用D::f()
(bp.*df)(); // 调用D::f()

而子类的成员函数指针可以通过static_cast或C风格的类型转换将其转换为父类的成员函数指针。

void (D::* df)() = &D::f;
void (B::* bf1)() = static_cast<void (B::*)()>(df);
void (B::* bf2)() = (void (B::*)())df;

从上面的例子中可以看到, 成员函数指针仍然支持虚函数机制. 下面看看编译器是如何支持各种虚拟机制的。

7.对象析构

书中提到一个值得注意的问题, 并不是定义了构造函数就需要定义析构函数, 这种"对称"是无意义的. 只有当需要一个析构函数时, 我们才应该显式定义之. 那么什么时候需要呢? 首先要搞清楚析构函数的作用, 她是对象的生命周期的终结, 而函数体内执行的主要是是对对象持有的资源的释放, 例如在构造函数中动态申请的空间. 析构函数的操作与构造函数类似, 但是顺序相反.

Trivial destructor 类T的析构函数如果满足下面的条件, 就是trivial(无效的,只会按位析构)的:

  • 析构函数不是用户定义的.(隐式声明或声明为default)
  • 析构函数非虚.(这就要求父类的虚函数也非虚)
  • 直接父类的析构函数是trivial的.
  • 非静态数据成员(数组的数据成员)的析构函数是trivial的.

==trivial析构函数不进行任何操作, 析构时只需要释放对象的空间即可. 所有与C语言兼容的数据类型都是trivial destructible的.==

8.多重继承和虚函数表

详细请查看:代码分析和验证

  • 基类有虚函数表,则继承和修改他们的表。多重继承时第一个表为默认表,有新的虚函数则在此表中添加;各个基类在派生类中的内存布局保持不变;
  • 基类没有虚函数表,派生类含虚函数,则生成一个虚函数表并在对象头部插入一个虚函数表指针;

总结:
与单继承相同的是所有的虚函数都包含在虚函数表中,所不同的多重继承有多个虚函数表,当子类对父类的虚函数有重写时,子类的函数覆盖父类的函数在对应的虚函数位置,当子类有新的虚函数时,这些虚函数被加在第一个虚函数表的后面。