C++幕后故事(五)--数据你在哪里?

读者若是以为我文章还不错的,但愿能够多多支持下我,文章能够转发,可是必须保留原出处和原做者署名。更多内容请关注个人微信公众号:cpp手艺人linux

这个章节咱们主要学习如下几个知识点:ios

1.数据成员绑定时机。git

2.多种模型下数据成员布局。windows

3.数据成员如何读取的。微信

4.进程内存布局函数

1.数据成员绑定时机

你们一看标题可能有点懵了,什么叫数据成员的绑定时机。请随我看段代码,这段代码节选自《深刻探索C++对象模型》工具

extern float x;
class Point3D {
public:
    Point3D(float, float, float);
    float X() { return x; }
    void X(float new_x) { x = new_x; }
private:
    float x, y, z;
};
复制代码

若是我调用了Point3D的X()返回的这个x是Point3D的成员变量值,仍是外部定义的x。如今看来的话,很显然返回的Point3D的成员变量值。可是在C++诞生没多久的时代,编译器返回的是外部定义的x。这样的结果对于如今的你来讲是否是有点惊讶。因此你如今看到以下的代码风格,请不要惊讶也不要彷徨。注意下面的高亮部分,将成员变量的定义提早,这样就防护了以前的成员绑定问题。布局

extern float x;
class Point3D {
private:
    float x, y, z;  
public:
Point3D(float, float, float);
float X() { return x; }
    void X(float new_x) { x = new_x; }
};
复制代码

能够看出来现代C++编译器,在绑定成员变量时,是延迟到整个类解析完以后才会进行成员变量的绑定。 可是你们请注意,这里有一个小坑。就是成员函数的参数列表绑定时机。 看以下的代码:学习

#include <string>
using std::string;

typedef string mytype;

class TestDataAnalyse {
public:
    int Fun();
    // 对于成员函数参数的解析,编译器是第一次遇到这个类型的mytype类型的时候被决定的
    // mytype第一次遇到的时候就看到了type string mytype
    void CopyValue(mytype value);
private:
int tempvalue;
typedef int mytype;
};

void TestDataAnalyse::CopyValue(mytype value)
{
    tempvalue = value;
}

void test_data_analyse() {
    TestDataAnalyse data;
    data.Fun();
}
复制代码

编译器报错的错误: error C2511: “void TestDataAnalyse::CopyValue(TestDataAnalyse::mytype)”:“TestDataAnalyse”中没有找到重载的成员函数看到这样的错误,你可能找半天都找不到头绪。mytype我已经定义为int类型的,可是为何说没有找到重载的成员函数,这和重载函数有什么关系。测试

缘由就是:成员函数参数的绑定是利用就近原理,当解析到它的时候,它的定义就是最近的定义这个类型的类型。什么意思呢,就是mytype这个类型最近定义这个类型的为string类型(从上往下解析)。CopyValue中将string类型复制给int类型,固然是编译失败。这个时候咱们把类中嵌套的定义typedef int mytype,这句话放在类的开始处,就能够避免这个问题,这个时候mytype的类型就为int。

在这里能够看出成员变量和成员函数参数的绑定时机是不一样的。

2.数据成员布局

2.1未继承任何父类

class Child {
public:
    int m_a;
    int m_b;
    int m_c;
    int m_d;
    int m_e;
};
void test_member_layout() {
    Child child;
    printf("m_a = 0x%p\n", &child.m_a);
    printf("m_b = 0x%p\n", &child.m_b);
    printf("m_c = 0x%p\n", &child.m_c);
    printf("m_d = 0x%p\n", &child.m_d);
    printf("m_e = 0x%p\n", &child.m_e);

    // m_a = 0x010FFD14
    // m_b = 0x010FFD18
    // m_c = 0x010FFD1C
    // m_d = 0x010FFD20
    // m_e = 0x010FFD24
}
复制代码

未继承任何的父类的类,类的成员变量都是按照声明的顺序排列的。根据打印出的地址,以下图所示:

在这里插入图片描述

2.2单继承无虚函数父类

class Base {
public:
    int m_base_a;
    int m_base_b;
};

class Child : public Base
{
public:
    int m_a;
    int m_b;
    int m_c;
    int m_d;
    int m_e;
};

void test_member_layout() {
    Child child;
    printf("m_base_a = 0x%p\n", &child.m_base_a);
    printf("m_basse_b = 0x%p\n", &child.m_base_b);
    printf("m_a = 0x%p\n", &child.m_a);
    printf("m_b = 0x%p\n", &child.m_b);
    printf("m_c = 0x%p\n", &child.m_c);
    printf("m_d = 0x%p\n", &child.m_d);
    printf("m_e = 0x%p\n", &child.m_e);
    // m_base_a = 0x00B3FCDC
    // m_basse_b = 0x00B3FCE0
    // m_a = 0x00B3FCE4
    // m_b = 0x00B3FCE8
    // m_c = 0x00B3FCEC
    // m_d = 0x00B3FCF0
    // m_e = 0x00B3FCF4
}
复制代码

单继承无虚函数,先是父类声明的前后顺序,再按照子类的声明的前后顺序,根据打印出的地址,以下图所示:

在这里插入图片描述

2.3 多重继承父类

class Base3 {
public:
    int m_base3_a;
    int m_base3_b;
};

class Child : public Base, public Base3
{
public:
    int m_a;
    int m_b;
    int m_c;
    int m_d;
    int m_e;
};

void test_member_layout() {
    Child child;
    printf("m_base_a = 0x%p\n", &child.m_base_a);
    printf("m_basse_b = 0x%p\n", &child.m_base_b);
    printf("m_base3_a = 0x%p\n", &child.m_base3_a);
    printf("m_basse3_b = 0x%p\n", &child.m_base3_b);
    printf("m_a = 0x%p\n", &child.m_a);
    printf("m_b = 0x%p\n", &child.m_b);
    printf("m_c = 0x%p\n", &child.m_c);
    printf("m_d = 0x%p\n", &child.m_d);
    printf("m_e = 0x%p\n", &child.m_e);

    // m_base_a = 0x010FF6B0
    // m_basse_b = 0x010FF6B4
    // m_base3_a = 0x010FF6B8
    // m_basse3_b = 0x010FF6BC
    // m_a = 0x010FF6C0
    // m_b = 0x010FF6C4
    // m_c = 0x010FF6C8
    // m_d = 0x010FF6CC
    // m_e = 0x010FF6D0
}
复制代码

当类中出现多个继承父类,成员变量的排列顺序,按照继承的前后顺序排列。若下图所示:

在这里插入图片描述

2.4 多重继承父类+(单/双)虚函数

class Base {
public:
    int m_base_a;
    int m_base_b;
    virtual ~Base() {}
};

class Base3 {
public:
    int m_base3_a;
int m_base3_b;
// virtual ~Base3() {}
};

class Child : public Base, public Base3
{
public:
    int m_a;
    int m_b;
    int m_c;
    int m_d;
    int m_e;
};

int main() {
    return 0;
}
复制代码

将上面的代码保存为member_layout.cpp,咱们利用在第四节文章里面的用到的工具,在windows菜单找到vs2013 开发人员命令提示工具,双击进入命令行的界面。

运行cl /d1 reportSingleClassLayoutChild member_layout.cpp,在不改变继承的顺序下,手动的添加和删除虚析构函数,导出的结果以下:

在这里插入图片描述

从表对比能够看出,继承的父类有没有虚函数是会影响子类的成员的布局。

1.子类继承了多个父类,其中有多个父类有虚函数,会优先排列有虚函数的父类而且按照继承的前后顺序排列,其次再排序无虚函数的父类。

2.若是继承的父类都是有虚函数或者是都没有虚函数,那么都按照继承的前后顺序排列。

在这里插入图片描述
在这里插入图片描述

2.5 虚基类继承无虚函数

将以下代码保存为member_layout.cpp,注意在这里为了探究内存布局,我删除了父类的全部虚函数,这样的设计不合理的,你们请注意。

class Grand {
public:
    int G1;
    int G11;
};
class Parent1 : virtual public Grand
{
public:
    int P1;
};
class Parent2: virtual public Grand
{
public:
    int P2;
};
class Child3: public Parent1, public Parent2
{
public:
    int C3;
};
int main() {
    return 0;
}
复制代码

使用vs2013开发人员命令提示工具,定位你本身的member_layout.cpp目录,好比个人:J:\code\code_git\ \polymorphism_virtual\source,输入命令:cl /d1 reportSingleClassLayoutChild3 member_layout.cpp,Child3表示你要导出的类布局。

我截取重要的内容贴出来:

在这里插入图片描述
在这里插入图片描述
从表中咱们能够清晰的看出来,Child3虚继承以后的布局。从Child3角度每一个父类都会带一个vbptr(虚基类表指针),它指向一个虚基类表。咱们先画出内存结构图的

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述
在这里插入图片描述
虚基类表中包含两项,第一项咱们先跳过不解释,第二项是什么意思呢? 咱们来写段测试代码看看:

void test_virtual_base_table() {
Child3 c3;
c3.G1 = 0;
    c3.G11 = 11;
    c3.P1 = 1;
    c3.P2 = 2;
    c3.C3 = 3;
}
复制代码

咱们反汇编看下:

00C1DF48  push 1  
00C1DF4A  lea ecx,[c3]  ; 虚基类表指针复制给ecx
00C1DF4D  call data_semantics::Child3::Child3 (0C11758h)  
    c3.G1 = 0;
00C1DF52  mov eax,dword ptr [c3]  ; 经过基类表指针-->找到虚基类表首地址
    c3.G1 = 0;
00C1DF55  mov ecx,dword ptr [eax+4]  ; 跳过虚基类表的前4个字节,找到第二项
00C1DF58  mov dword ptr c3[ecx],0  ; c3+[ecx]偏移,复制给G1所在内存地址为0
    c3.G11 = 11;
00C1DF60  mov eax,dword ptr [c3]  ; 经过基类表指针-->找到虚基类表首地址
00C1DF63  mov ecx,dword ptr [eax+4]  ; 跳过虚基类表的前4个字节,找到第二项
00C1DF66  mov dword ptr [ebp+ecx-20h],0Bh  ; 复制给G1所在内存地址为0BH,ebp-20h其实为P1的地址
    c3.P1 = 1;
00C1DF6E  mov dword ptr [ebp-20h],1  
    c3.P2 = 2;
00C1DF75  mov dword ptr [ebp-18h],2  
    c3.C3 = 3;
00C1DF7C  mov dword ptr [ebp-14h],3
复制代码

若是你阅读过《第四章 虚函数的原理》,你会发现这个对虚函数的寻址机制是彻底相同的。 都是根据指针->虚表->虚表中的内容,看下图的成员变量的偏移量值。

在这里插入图片描述
咱们看下Child3中G1是怎么寻址的,看下图所示。

在这里插入图片描述
因此能够得出结论,子类虚基类表中的第二项内容其实存储是个偏移量。虚基类中的内容是放到子类的最后面,而后子类根据虚基类表中的偏移找到虚基类成员的位置。

一样的道理G11也是按照这种原理寻址的。须要注意下mov dword ptr [ebp+ecx-20h],ebp-20h其实P1的首地址,因此从P1的地址开始(跳过前面的vbptr 4个字节)+20正好就是G11的地址。

Child3中的Parent1,Parent2中都包含一个vbptr。这和vptr是十分相似的原理,懂了虚函数的原理,那么你搞懂虚基类表也是很是容易的。

2.6 虚继承+虚函数继承

在2.4中为了探究虚继承的内存布局,我把Grand类中的虚析构函数删除。这样的设计是不合理的,这里咱们给Grand加上虚析构函数。关于为何父类中必定要虚析构函数能够参考《第四章 虚函数原理》。

class Grand {
public:
    int G1;
    int G11;
    virtual ~Grand() {}
};
复制代码

咱们再使用工具导出类布局

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

能够看出Grand中增长一个虚函数,在子类中Child3中的布局中增长了一个vfptr指针,这个指针指向一个表,表中就存储了虚函数的地址,其实就是咱们说的虚函数表,vfptr就是至关于咱们以前讨论的vptr。

还剩下的几种状况虚继承+虚函数的情形,都是相同的分析机制。这里我就再也不分析了。

3.数据成员的读取

前面咱们详细的分析了各类模式下,类中内存中的布局。既然咱们知道了数据在哪里,那么接下来讨论数据是怎么读取的。

其实,咱们已经知道数据在内存的位置,将会很是有利于咱们理解对于成员变量的读取,甚至能够说咱们已经理解了一半。

3.1 数据成员

3.1.1 static成员

static成员是属于整个类的,并不单独属于某一个实例化的对象。咱们接着借用上面的代码Child3类,在里面增长几行代码。

class Child3: public Parent1, public Parent2
{
public:
    int C3;
	static int s_Age;
};
int Child3::s_Age = -1;

void test_call_member() {
	Child3::s_Age = 0;

	Child3 c3;
	c3.s_Age = 1;
	
	Child3 *pc3 = new Child3();
	pc3->s_Age = 2;
}
复制代码

接下来咱们看下test_call_member反汇编的调用方式有什么不一样。

Child3::s_Age = 0;
0092ADC0  mov         dword ptr ds:[93B000h],0  
    Child3 c3;
0092ADCA  push        1  
0092ADCC  lea         ecx,[c3]  
0092ADCF  call        data_semantics::Child3::Child3 (09217A8h)  
0092ADD4  mov         dword ptr [ebp-4],0  
    c3.s_Age = 1;
0092ADDB  mov         dword ptr ds:[93B000h],1  
Child3 *pc3 = new Child3(); 
0092AE26  mov         dword ptr ds:[93B000h],2  
复制代码

从上述代码中咱们看出,若是是静态成员,无论你的调用方式如何。其实他们最后的汇编代码都是同样的。从侧面能够看出静态数据是放在全局的数据区,和对象是没有关系的。

3.1.2 no static成员

前面咱们学习了static成员的是怎么找到,可是和实例对象相关的成员咱们该怎么定位呢。

咱们看下面的代码:

class Child4 {
public:
    int m1;
    int m2;
    int m3;
    int m4;
};

void test_member_initialize() {
    Child4 *c5 = new Child4();
    c5->m1 = 0;ss
    c5->m2 = 1;
    c5->m3 = 2;
    c5->m4 = 3;
    
    Child4 c4;
    c4.m1 = 0;
    c4.m2 = 1;
    c4.m3 = 2;
    c4.m4 = 3;
}
复制代码

接下来咱们经过反汇编代码看下是如何定位成员变量。

; 第一段
    c5->m1 = 0;
011AC94F  mov         eax,dword ptr [c5]  
011AC952  mov         dword ptr [eax],0  
    c5->m2 = 1;
011AC958  mov         eax,dword ptr [c5]  
011AC95B  mov         dword ptr [eax+4],1  
    c5->m3 = 2;
011AC962  mov         eax,dword ptr [c5]  
011AC965  mov         dword ptr [eax+8],2  
    c5->m4 = 3;
011AC96C  mov         eax,dword ptr [c5]  
011AC96F  mov         dword ptr [eax+0Ch],3  
; 第二段
    Child4 c4;
    c4.m1 = 0;
011AC976  mov         dword ptr [c4],0  
    c4.m2 = 1;
011AC97D  mov         dword ptr [ebp-1Ch],1  
    c4.m3 = 2;
011AC984  mov         dword ptr [ebp-18h],2  
    c4.m4 = 3;
011AC98B  mov         dword ptr [ebp-14h],3  
复制代码

咱们先看第一段汇编代码,首先[c5]中保存对象的首地址给eax,对eax解引用而后赋值0。再对(eax+4)解引用赋值1,再对(eax+8)解引用赋值2,再对(eax+12)解引用赋值3。我就能够看出这里根据对象的首地址+成员变量的偏移,就能找到对应的对象而后进行读取操做。

这时咱们再看第二段的汇编代码,你们可能就奇怪了,为何第二种的定位方式第一种不一样呢。

其实这里定位的原理是一致的。第一种方式,c5对象是new出来,也就是说它的内存是堆中的,堆区是个很是灵活的区域申请和释放都是本身能够控制的,同时它的增加的方向是从低地址->高地址。而第二种方式呢,c4是在栈中申请的内存,相比较堆呢,它的可控性就弱了不少,并且它的增加方向是从高地址->低地址,这一点和堆是彻底相反的,记住ebp表示的基址指针寄存器,该指针指向系统栈最上面一个栈帧的底部。

咱们作个表格对比下两种:

在这里插入图片描述

采用的原理都是同样的,都是利用成员变量的偏移量,只不过它们的增加的方式不同。堆往上增加,栈是往下增加。

3.2读取的效率

class Base6 {
public:
    int m_base6_a;
    virtual ~Base6() {}
    static int s_base6_b;
};
int Base6::s_base6_b = 0;

class Base4 : virtual public Base6
{
public:
    int m_base4_a;
    int m_base4_b;
    static int s_base4_c;
    virtual ~Base4() {}
};
int Base4::s_base4_c = 0;

class Base5 : virtual public Base6
{
public:
    int m_base5_a;
    int m_base5_b;
    static int s_base5_c;
    virtual ~Base5() {}
};
int Base5::s_base5_c = 0;

class Child5 : public Base4, public Base5
{
public:
    int m_a;
    int m_b;
    int m_c;
    int m_d;
    int m_e;
};

void test_member_effective() {
Child5::s_base6_b = 10;
    Child5::s_base4_c = 1;
    Child5::s_base5_c = 2;

    Child5 c5;
    c5.m_a = 3;
    c5.m_base4_a = 4;
    c5.m_base5_a = 5;
    c5.m_base6_a = 6;
}
复制代码

咱们从汇编的角度(debug 模式下)看下他们的效率。

在这里插入图片描述

3.3 Data Member指针

指向类的成员变量指针,其实不是指针,实际的内容实际上是在整个类中的偏移值。举个例子:

class Class7 {
public:
    virtual void VirFunc() {}

    int m_a;
    int m_b;
    int m_c;
    int m_d;
};

void test_member_point() {
    printf("&Class7::m_a:%x\n", &Class7::m_a);
    printf("&Class7::m_b:%x\n", &Class7::m_b);
    printf("&Class7::m_c:%x\n", &Class7::m_c);
    printf("&Class7::m_d:%x\n", &Class7::m_d);
    // &Class7::m_a:4
    // &Class7::m_b:8
    // &Class7::m_c:c
// &Class7::m_d:10
    // 用法
    int Class7::*cpoint = &Class7::m_a;
    Class7 c7;
    c7.*cpoint = 1; 
    // *(&c7+cpoint) = 1;
}
复制代码

从代码中能够看出来,打印的是成员变量的偏移值。值的注意下就是这里打印的偏移值是从4开始的,由于Class7含有虚函数,存在一个虚表指针。

咱们再看下成员指针的初始化和使用,int Class7::*cpoint = &Class7::m_a; Class7 c7; c7.*cpoint = 1;用法其实很简单,可是困扰个人是这个成员变量指针到底有啥用。我尝试google下也没有发现有价值的资料,从《深刻探索C++对象模型》里面提到了这个能够帮咱们看出成员变量的偏移位置,可是我以为这个意义不大的。这个让我很困惑。

4.进程内存布局

变量在内存是如何分布的,咱们借助linux上nm(names)命令看下,代码以下

#include <stdio.h>
#include <iostream>

using std::cout;
using std::endl;

int g1;
int g2;

int g3 = 3;
int g4 = 4;

int g5;
int g6 = 1;  

static int gs_7;
static int gs_8 = 0;
static int gs_9 = 1; 

void g_func() {
    return;
}

class MyClass {
public:
    int m_i;
    static int m_si;
    int m_j;
    // 声明
    static int m_sj;
    int m_k;
    static int m_sk;
};
// 定义
int MyClass::m_sj = 1;

int main(int argc ,char *argv[]) {
    int temp_i = 0;
    printf("tempi address = %p\n", &temp_i);
    printf("g1 address = %p\n", &g1);
    printf("g2 address = %p\n", &g2);
    printf("g3 address = %p\n", &g3);
    printf("g4 address = %p\n", &g4);
    printf("g5 address = %p\n", &g5);
    printf("g6 address = %p\n", &g6);
    printf("gs_7 address = %p\n", &gs_7);
    printf("gs_8 address = %p\n", &gs_8);
    printf("gs_9 address = %p\n", &gs_9);
    printf("MyClass::m_sj address = %p\n", &(MyClass::m_sj));
    printf("g_func() address = %p\n", g_func);
    printf("main() address = %p\n", main);
    cout << "g_test " << (void *)g_test << endl;
    return 0;
}
复制代码

1.使用g++ process_member_layout -o process_memeber_layout

2.在使用nm process_memeber_layout导出的数据以下所示。

在这里插入图片描述

这里解释下nm中间字段的含义

在这里插入图片描述

咱们再对比下nm导出的数据和上述代码的打印的地址

在这里插入图片描述

我从网上找了张内存映射图

在这里插入图片描述

5.总结

在这里插入图片描述
相关文章
相关标签/搜索