深度探索C++对象模型-第三章

Data语意学

一 成员变量的绑定

如果在类内部和外部出现了同名成员变量,而类的内部在成员变量声明前有内联成员函数的话,会有以下误解:

extern float x;
class A{
public:
    int func(){ return X; }
private:
    int x;
};

这里的x不会出现绑定错误的原因,因为C++内部将内联函数放到整个类的后面进行处理,所以当内联函数被处理的时候,类内部的成员变量已经覆盖了外部extern进来的x。

但是如果是typedef的话,还是会出现问题:

typedef float length;
class A{
public:
    length func(){ return X; }
private:
    typedef int length;
    int x;
};

这里,函数的返回值是float,而不是我们想要的int,所以在类内部进行typedef的话,最好是放到类的最前面。

二 成员变量的内部布局

同一个访问部分(也就是public,protected,private区段),成员的排列按照声明顺序在类的内部由低地址到高地址排列(不是写在一起的同一个访问部分也是按照声明顺序排列)。不同访问部分的成员变量顺序没有准确定义。

三 成员变量的存取

1 静态成员

类内静态变量只有一份,放在程序数据段,存取类内静态变量通过类名而不是对象名。
对类内静态成员取地址,得到的是指向其数据类型的指针,而不是指向类成员的指针。

2 非静态成员

非静态成员在每一个类对象中,通过显式/隐式类对象进行存取。并且在类对象中对成员变量的存取是通过隐式的this指针完成的(每一个成员函数都包含一个隐藏的this指针)。

类对象中每一个非静态成员变量的偏移位置在编译期就可以被知道,即使是继承而来的成员变量也是,所以在类内部进行数据存取和存取C结构体内部数据是一样的。但是虚基类的成员存取操作必须延迟到执行期。

四 “继承”和数据成员

1 只有继承没有多态(虚函数)
基类的子对象(子类中基类的数据成员部分)内存上处于派生类数据成员之前,内存的排列和非派生(将基类的数据移入派生类,取消继承)是一致的,唯一不同的是对齐的差别。

扫描二维码关注公众号,回复: 1494324 查看本文章
class T{ int a; char b; char c; };

这个类会产生对齐操作,sizeof(T)返回值是8。(补2个char)

class A{ int a; };
class B : public A{ char b; };
class C : public B{ char c; };

在继承中也会产生内存对齐,但是内存对齐将会发生多次,在A,B,C类中都会发生,sizeof(A)为4,sizeof(B)为8,sizeof(C)为12。

2 有多态

有多态情况比较复杂,C++标准没有规定虚函数表的指针必须放在类的头部还是尾部,所以不同编译器有不同的实现。但是毫无疑问的是,在多态的情况下,由于虚函数表指针的引入,指针的偏移将会需要特殊的计算,对于数据成员的存取将会造成额外的负担。

虚表指针放入类头部的话,很明显所有的数据成员都需要进行额外的偏移。虚表指针放入尾部的话,基类的虚表指针会导致派生类数据的偏移,仍然会产生数据的额外偏移。

多态的额外负担如下:

  1. 类多出一个虚函数表,用以存放虚函数地址,再加上一两个slots(支持RTTI)。
  2. 类中多出一个虚表指针,提供执行器链接,使每一个对象找到相应的虚表。
  3. 加强ctor的功能,使其可以为虚表指针设定初值。指向类对应的虚表。在派生类和基类的ctor中,虚表指针的值可能都是要重新赋值的。
  4. 加强dtor的功能,使其可以正确删除虚表指针。

3 多重继承

多重继承下,第一个继承的类的子对象(派生类中基类的数据部分)在类的最前方(这里以虚表指针在类尾部为例),指针无需额外偏移,但是第二个继承的类,将需要额外偏移sizeof(A)的大小:

class A{
    int a;
    char aa;
    virtual int funcA(){ return a; } 
};
class B{
    int b;
    char bb;
    virtual int funcB(){ return b; } 
};
class C{
    int c;
    char cc;
    int funcA(){ return c; };
    int funcB(){ return cc; };
};

具体的内部实现是看编译器的,这只是理论上的一种排列方法。

4 虚拟继承

虚拟继承要解决两个问题:

  1. 对象腰围其虚基类产生一个额外的指针,但是我们希望类对象的大小是固定的,不希望类对象会因为虚基类的增多而变化大小。
  2. 虚继承串链的增长会导致间接存取层次的增加,我们不希望这种情况发生。

解决1的方法可以是产生虚基类表格,类对象只维护虚基类表格的指针;解决2的方法是将虚基类的偏移整合到虚函数表中去,正常调用虚函数采用正向偏移虚函数表指针的方式,而寻找虚基类则采用负偏移虚函数表指针的方式。

五 对象成员的效率

数据的存取效率不会因为继承而降低(优化后(-O)),但是会因为虚拟继承而降低,并且会因为虚拟继承层数的增加而降低的更明显。

六 指向成员数据的指针

对一个类的非静态成员取地址,得到的是这个成员在类中的偏移,但是通过对象取地址会得到成员的真正地址,对一个类的静态成员取地址,得到的也是真正的地址,因为静态成员不存储在类中,存储在成组的数据段中。

例子如下:

#include <iostream>
#include <cstdio>
using namespace std;
class A
{
public:
    static int as;
    int a;
    char aa;
    float aaa;
    void func(){ cout << "A" << endl; }
};
int A::as = 1;
int main()
{
    A a;
    printf("%p\n", &A::a);  //输出结果:00000000(十六进制)
    printf("%p\n", &A::as); //输出结果:00C01024(十六进制)
    printf("%p\n", &a.a);   //输出结果:0030FB88(十六进制)

    return 0;
}

很明显第一个输出了偏移,后两个输出了地址。

猜你喜欢

转载自blog.csdn.net/u012630961/article/details/80434402