编程语言应用

首页 » 常识 » 诊断 » 成为高手必须弄懂的问题,C语言中类在
TUhjnbcbe - 2023/5/29 21:55:00

C/C++语言特别适合用于开发压榨机器性能的程序,在资源有限的情况下,C/C++程序员不能放过机器的每一点计算力以及每一字节内存。当然了,要做到这一点,编程语言本身要提供精准的控制能力,C/C++语言作为一种强类型的编程语言,“精准控制”自然不在话下。这样看来,将机器性能发挥到极限的责任最后还是落在程序员身上了,要做到这一点,我们至少需要对各种数据类型使用的内存情况了然于胸。

使用的内存情况了然于胸

类的内存模型

一般来说,我们在刚开始接触C/C++语言时,就会知道不同的数据类型占用内存空间通常不同的概念,比如char类型占用1个字节内存空间,int类型常常占用4字节内存空间,double类型常常占用8字节内存空间,有经验的程序员还会明白指针以及结构体占用内存空间的情况,等等。

请注意“常常”一词,C语言标准暂时还没有明确定义int等数据类型占用内存空间的情况。

事实上,我之前也在讨论C语言特性的时候专门写文章分析过数据类型与内存使用之间的关系,虽说也涉及到稍稍复杂一些的内存对齐概念,但是对于C++语言中的类,倒是完全没有提及,而类是C++语言中非常重要的概念,要是弄不清楚它的内存分布,“精准控制”就完全是吹牛了。

查阅了不少资料,没有找到直接讨论C++语言中类的内存分布的,我倒也不想再花时间在搜索引擎上,倒不如通过几条线索自己分析,直观的理解C++语言中类的内存分布了。

空类

请看下面这段C++语言代码:

classA{};coutsizeof(A)endl;//输出1

看来即使是空类,编译器在处理它时,也会隐含的添加1个字节。

类的成员变量

现在为类A增加几个成员变量,并且分别将此时类A的size,实例化对象的地址,以及成员变量的地址打印出来:

类的成员变量

这段C++语言代码的输出如下,请看:

sizeofA:8

aaddr:0x7fff

A::pub_i1addr:0x7fff

A::pub_i2addr:0x7fff

可以看出,此时类A的的size恰好等于两个成员变量占用的内存之和(我的机器上sizeof(int)等于4),并且对象a的地址和第一个成员变量pub_i1的地址相等,第二个成员变量pub_i2的地址则紧跟在pub_i1之后,由此可以推测类A的内存分布如下图:

类A的内存分布

此时的类A倒有些类似于C语言中的结构体:

structA{

intpub_i1;

intpub_i2;

};

的确如此,事实上,类A中成员变量的在内存中的存储方式也会涉及到内存对齐,这一点也和C语言中的结构体类似,不过这不是本文的重点,感到陌生的读者可以再回头看看我之前的文章。

类的成员函数

为类A添加两个成员函数,并且将其地址打印出来:

添加两个成员函数

这段C++语言代码的输出如下:

sizeofA:8

aaddr:0x7ffe2dbc

A::pub_i1addr:0x7ffe2dbc

A::pub_i2addr:0x7ffe2dbcc

A::pub_foo1()addr:0xb28

A::pub_foo2()addr:0xbc2

从输出可以看出,成员函数的加入并未增加类A的size,而且两个成员函数pub_foo1()和pub_foo2()在地址上离类A的成员变量非常远,有理由推测此时类A在内存中的分布如下,请看:

类A的成员函数独立分布

即,类A的(非虚)成员函数其实在内存中是独立分布的,并且彼此之间不毗邻,它们都远离类A对象的地址,并不占据类A的size。

类的私有成员

前面讨论的都是类A的公有成员,现在为其增加几个私有成员,并通过公有函数pub_foo1()将它们的地址打印出来,相关的C++语言代码如下,请看:

类的私有成员

修改后的C++语言编译执行后输出如下,请看:

sizeofA:16

aaddr:0x7ffdbbfe

A::pub_i1addr:0x7ffdbbfe

A::pub_i2addr:0x7ffdbbfe

A::pub_foo1()addr:0xace

A::pub_foo2()addr:0xbb0

A::prv_i1addr:0x7ffdbbfe

A::prv_i2addr:0x7ffdbbfec

A::prv_foo1()addr:0xbba

A::prv_foo2()addr:0xbc4

此时sizeof(A)变为16了,这恰好等于4个int型成员变量占用的内存之和,并且仔细观察还能发现这4个int型成员在内存中是连续分布的,并且顺序是它们在类中被定义的顺序(这一点读者可自行验证)。private成员并无特别之处,私有函数也是独立于类A对象a独立存储的,因此推测此时类A的内存模型如下,请看:

private成员并无特别之处

虚函数

我们已经知道C++语言中类的常规非虚成员函数并不占用类的size了,那么作为目前唯一已知动态绑定的虚函数是否也如此呢?我们在类A中添加两个虚函数:

虚函数

编译并执行这段C++语言代码,可以得到如下输出,请看:

sizeofA:24

aaddr:0x7fffb26a22a0

A::pub_i1addr:0x7fffb26a22a8

A::pub_i2addr:0x7fffb26a22ac

A::pub_foo1()addr:0xb28

A::pub_foo2()addr:0xbc2

A::pub_vfoo1()addr:0xbcc

A::pub_vfoo2()addr:0xbd6

A::prv_i1addr:0x7fffb26a22b0

A::prv_i2addr:0x7fffb26a22b4

A::prv_foo1()addr:0xbe0

A::prv_foo2()addr:0xbea

此时类A的size为24,增加了8个字节,看来虚函数的确很特别(常规函数并不增加类的size)。还不止于此,在我的机器上,一个函数指针占用的内存空间为8字节,这里我们添加了2个虚函数,却只增加1个函数指针的大小,为什么呢?

还记再前面一节中我们曾提到C++语言编译器会为含有虚函数的类添加虚表存放虚函数指针吗?这里多出一个指针正是虚表指针__vptr:只需要一个指针就能够找到虚表,进而在虚表中找到所有的虚函数。请注意,含有虚函数的类A的对象a的地址已经不等于第一个成员变量的地址,而是多出了8个字节的内存偏移,这个偏移量恰好能够存放一个指针,因此我们推测含有虚函数的类A在内存中的分布如下图,请看:

含有虚函数的类A在内存中的分布

这种推测对不对呢?我们可以再做实验确认下,编写下面的C++语言代码:

...

a.pub_foo1();

void**__vptr=(void**)a;

void**virtual_table=(void**)(*__vptr);

printf(virtualtable[0]:%p\n,virtual_table[0]);

printf(virtualtable[1]:%p\n,virtual_table[1]);

对上述C++语言代码稍作解释:我们已经分析虚表指针__vptr恰好等于类A对象a的地址,因此可以根据a获得实际的虚表指针地址。因为__vptr指向的正是虚表(virtual_table),所以将得到的地址中的值取出就得到了virtual_table的地址,依次将virtual_table中的前2个(类A只有2个虚函数)元素的值打印出来,应该分别等于类A的两个虚函数地址,对不对呢?实际编译并执行这段C++语言代码,得到的输出如下,请看:

sizeofA:24

...

A::pub_vfoo1()addr:0xc16

A::pub_vfoo2()addr:0xc20

...

virtualtable[0]:0xc16

virtualtable[1]:0xc20

我省去了一些信息,这样便可以清晰的看出输出其实与我们的推测一致。

小结

敬请
1
查看完整版本: 成为高手必须弄懂的问题,C语言中类在