《深度探索C++对象模型》学习笔记

1 关于对象

1.1 C++对象模式

C++对象模型

非静态数据成员放置于每一个类对象中,静态数据成员则被存放在类对象之外。静态与非静态成员函数也被放在类对象之外。虚函数则以两个步骤支持:

  1. 每一个类产生出一堆指向虚函数的指针,放在表格之中,此表称为虚表 (virtual table, vtbl)。
  2. 在每一个类对象中安插一个指针,指向相关的虚表。通常这个指针被称为 vptr 。它的设定与重置由每一个类的构造函数、析构函数、拷贝赋值运算符自动完成。每一个类所关联的 type_info 对象(用以支持runtime type identification, RTTI)也经由虚表指出,通常放在表格的第一个slot(位置)。

3 Data语义学

3.0 综述

  • 一个虚基类子对象只会在派生类中存在一份实例,不管它在继承体系中出现了多少次。
  • C++标准并不强制规定如“基类子对象的排列顺序“或”不同存取层级的数据成员的排列顺序“这种琐碎细节。它也不规定虚函数或虚基类的实现细节。C++标准说:那些细节由各家厂商自定。
  • C++对象模型把非静态数据成员直接存放在每一个类对象中,对于继承来的非静态数据成员(无论是虚继承还是非虚继承)也是如此。不过并没有强制定义其间的排列顺序。至于静态数据成员则被放置在程序的一个全局数据段(global data segment)中,不影响类对象的大小。

3.1 Data Member 的绑定

  1. 有关数据成员的绑定问题,现在的C++已经解决了。

  2. 若一个 inline 函数在 class 声明之后立刻被定义,则还是对其评估求值。即对成员函数本体的分析会直到整个class的声明都出现了开始。因此在一个inline成员函数体之内的一个数据成员绑定操作,会在整个class声明完成之后才发生。(member scope resolution rules)

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    extern int x;

    class Point3d {
    public:
    // 对于函数本体的分析将延迟直到class声明的右大括号出现才开始
    float X() const { return x; }
    private:
    float x;
    };

    // 分析在这里进行
  3. 然而这对于成员函数的参数列表并不为真。参数列表中的名称还是会在它们第一次遇到时被适当地决议(resolved)完成。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    typedef int length; // global typedef

    class Point3d {
    public:
    void mumble(length val); // length: int
    length mumble() { return _val; } // length: int
    private:
    typedef float length; // nested typedef
    length _val; // length: float
    }

    对于这种情况,请总是把“nested type声明”放在class的起始处。

3.2 Data Member 的布局

3.6 指向 Data Members 的指针

取一个非静态数据成员的地址将会得到它在类中的偏移量。

下面代码中数据成员指针的类型为:float Point3d::*。有些编译器返回的偏移量总是多1,因为考虑到不指向任何成员的指针应为0。如果不加1,有可能导致第一个数据成员成员的指针和不指向任何成员的指针相等,都为0。(此时说它是偏移量就有些不合适)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
// G++ 8.1.0, 64-bit
#include <iostream>
using namespace std;

class Point3d {
public:
virtual ~Point3d() {}
static Point3d origin;
float x, y, z;
};

struct Base1 {
int val1;
};
struct Base2 {
int val2;
};
struct Derived : Base1, Base2 {
int val3;
};

Point3d Point3d::origin;

#define show(ptrToDataMember) printf(#ptrToDataMember " = %d\n", ptrToDataMember)

int main() {
// 注意不能用 cout << &Point3d::x,会匹配到 operator<<(bool)
// 为了简便,这里定义宏(与书上不同)
show(&Point3d::x); // 8
show(&Point3d::y); // 12
show(&Point3d::z); // 16

show(&Base1::val1); // 0
show(&Base2::val2); // 0
show(&Derived::val1); // 0
show(&Derived::val2); // 0(比较奇怪)
show(&Derived::val3); // 8
return 0;
}

个人感觉,成员指针并不能拿来输出。所以输出什么值也只能作为参考,便于理解这个概念。事实上,我觉得cout给出了正确的行为。它将一个成员指针视为bool,以表示其是否真正有效。即

1
2
3
4
5
6
7
8
9
10
11
12
struct Base1 {
int val1;
};

int main() {
int Base1::* p = 0;
cout << boolalpha << p << endl; // false
p = &Base1::val1;
printf("%d\n", p); // 0
cout << boolalpha << p << endl; // true
return 0;
}
Your browser is out-of-date!

Update your browser to view this website correctly. Update my browser now

×