
粉丝问:类的函数是不是不占内存?
这是一个既非常有趣又很有深度的问题。

问题引入与代码示例
事情是这样的,假设我有一个这样的C++类,里边有一个构造函和一个析构函数,一个成员函数还有一个INT型的成员变量。
#include <iostream>
class TestClass {
public:
TestClass(int n):memberN(n) {
}
~TestClass() {
}
void Bark() {
std::cout << "Wang Wang" << std::endl;
}
private:
int memberN;
};
int main()
{
int n = sizeof(Derived);
std::cout << "sizeof TestClass = " << n << std::endl;
return 1;
}
现在我们用SIZEOF计算它的大小,运行程序后发现这个类的大小只有4个字节,也就是只是这个INT型成员变量的大小。
那成员函数去哪里了?
程序内存布局详解
要弄清楚这个问题,你首先得知道程序的内存布局,嗯,又是内存知识,似乎咱们很多底层原理知识都与内存紧密相关哈。
程序在内存中的布局可以分为几个主要的区域,从低地址到高地址。
首先是代码段,然后是数据段,接着是BSS段,再就是堆内存空间,堆内存空间由低地址向高地址增长。
然后是栈内存空间,栈内存空间由高地址向低地址增长。
编译与成员函数存储
当编译器编译我们写的这份C++代码时,编译器会将源代码转化为汇编代码。
对于类的成员函数,编译器会为它们生成相应的机器指令代码,而所有代码会存储在代码段中。
已初始化的 全局变量 和 静态变量会存储在数据段中。
未初始化的 全局变量 和 静态变量则会存储在BSS段中。
堆空间用于程序运行时动态分配内存,栈空间则用于存储函数调用的 局部变量、函数参数 和 返回地址。
编译器生成的汇编代码会通过链接器(linker)进一步处理。
链接器会将所有的代码段和数据段,bss合并,从而生成最终的可执行文件。
因此,类的成员函数存储在程序的 代码段,这部分内存是整个程序共享的,不是每个对象单独拥有的。
无论你创建了多少个 TestClass 类型的对象,成员函数的代码都只有一份。
当你使用sizeof 计算类的大小时,它计算的实际是对象的实际内存占用,而不是类本身。
简单的讲,类的成员函数是类的行为定义而不是状态,它们不会占用每个类对象的内存。
空类的特殊处理
需要注意的是,如果你的类实现中只有函数,并没有成员变量,编译器为了确保类对象的地址是唯一的,通常会为其分配 1 字节内存,从而确保它不是空对象。这是因为 C++ 标准规定。
例如我们这个EmptyClass,它没有成员变量,我们用sizeof计算它的大小时,就是1个字节。
#include <iostream>
class EmptyClass {
public:
EmptyClass() {
}
~EmptyClass() {
}
void Bark() {
std::cout << "Wang Wang" << std::endl;
}
};
int main()
{
int n = sizeof(EmptyClass);
std::cout << "sizeof EmptyClass = " << n << std::endl;
return 1;
}
继承场景下的内存优化
但是如果一个没有成员变量的类被继承之后,所继承的类有一个INT型的成员变量,那它这个继承类的大小就是4个字节,也就是编译器不会再为它空的父类生成那额外的1个字节内存了。
class EmptyClass {
public:
EmptyClass() {
}
~EmptyClass() {
}
void Bark() {
std::cout << "Wang Wang" << std::endl;
}
};
class Derived : public EmptyClass {
int x;
};
int main()
{
int n = sizeof(Derived);
std::cout << "sizeof Derived = " << n << std::endl;
return 1;
}
虚函数的影响及其他注意事项
此外,如果类中存在虚函数,虚函数表还会占用额外的内存大小,原理一样就不再细说了。
C++类的本质思考
其实像 C++ 这样的高级语言,类只是一种为了让人类更好的理解和掌握编程语言的封装数据和行为的抽象概念。
当这种高级语言代码被编译后,类的概念本身会消失,因为低级语言只关注机器指令和硬件层面的操作,并没有类似 C++ 中的 类、继承、多态 等高级抽象概念。
关于C++类,现在你有更深入的认识了吗?


