# 对象的内存模型 类是创建对象的模板,不占用内存空间,不存在于编译后的可执行文件中;而对象是实实在在的数据,需要内存来存储。对象被创建时会在栈区或者堆区分配内存。 直观的认识是,如果创建了 10 个对象,就要分别为这 10 个对象的成员变量和成员函数分配内存。不同对象的成员变量的值可能不同,需要单独分配内存来存储。但是不同对象的成员函数的代码是一样的,上面的内存模型保存了 10 份相同的代码片段,浪费了不少空间,可以将这些代码片段压缩成一份。 事实上编译器也是这样做的,编译器会将成员变量和成员函数分开存储:分别为每个对象的成员变量分配内存,但是所有对象都共享同一段函数代码。成员变量在堆区或栈区分配内存,成员函数在代码区分配内存。 【示例】使用 sizeof 获取对象所占内存的大小: ```c++ #include using namespace std; class Student { private: //私有的 const char *m_name; int m_age; float m_score; public: //共有的 void setname(const char *name); void setage(int age); void setscore(float score); void show(); }; //成员函数的定义 void Student::setname(const char *name) { m_name = name; } void Student::setage(int age) { m_age = age; } void Student::setscore(float score) { m_score = score; } void Student::show() { cout << m_name << "的年龄是" << m_age << ",成绩是" << m_score << endl; } int main() { //在栈上创建对象 Student stu1; cout << sizeof(stu1) << endl; //在堆上创建对象 Student *pstu2 = new Student; cout << sizeof(*pstu2) << endl; //类的大小 cout << sizeof(Student) << endl; return 0; } ``` Student 类包含三个成员变量,它们的类型分别是 char *、int、float,都占用 4 个字节的内存,加起来共占用 12 个字节的内存。通过 sizeof 求得的结果等于 12,恰好说明对象所占用的内存仅仅包含了成员变量。 类可以看做是一种复杂的数据类型,也可以使用 sizeof 求得该类型的大小。从运行结果可以看出,在计算类这种类型的大小时,只计算了成员变量的大小,并没有把成员函数也包含在内。 对象的大小只受成员变量的影响,和成员函数没有关系。 假设 stu1 的起始地址为 0X1000,那么该对象的内存分布如下图所示: ![image_202207051.drawio](D:\Data\img\image_202207051.drawio.png) m_name、m_age、m_score 按照声明的顺序依次排列,和结构体非常类似,也会有内存对齐的问题。 ## 函数编译原理和成员函数的实现 从上面的分析中可以看出,对象的内存中只保留了成员变量,除此之外没有任何其他信息,程序运行时不知道 stu 的类型为 Student,也不知道它还有四个成员函数 setname()、setage()、setscore()、show(),C++ 究竟是如何通过对象调用成员函数的呢? ### 函数的编译 C++和C语言的编译方式不同。C语言中的函数在编译时名字不变,或者只是简单的加一个下划线`_`(不同的编译器有不同的实现),例如,func() 编译后为 func() 或 _func()。 而C++中的函数在编译时会根据它所在的命名空间、它所属的类、以及它的参数列表(也叫参数签名)等信息进行重新命名,形成一个新的函数名。这个新的函数名只有编译器知道,对用户是不可见的。对函数重命名的过程叫做名字编码(Name Mangling),是通过一种特殊的算法来实现的。 Name Mangling 的算法是可逆的,既可以通过现有函数名计算出新函数名,也可以通过新函数名逆向推演出原有函数名。Name Mangling 可以确保新函数名的唯一性,只要函数所在的命名空间、所属的类、包含的参数列表等有一个不同,最后产生的新函数名也不同。 如果你希望看到经 Name Mangling 产生的新函数名,可以只声明而不定义函数,这样调用函数时就会产生链接错误,从报错信息中就可以看到新函数名。请看下面的代码: ```c++ #include using namespace std; void display(); void display(int); namespace ns { void display(); } class Demo { public: void display(); }; int main() { display(); display(1); ns::display(); Demo obj; obj.display(); return 0; } ``` 上面的代码编译是可以的哦,但是在函数链接过程中会产生链接错误。 ```bash 1>demo78.obj : error LNK2019: 无法解析的外部符号 "void __cdecl display(void)" (?display@@YAXXZ),该符号在函数 _main 中被引用 1>demo78.obj : error LNK2019: 无法解析的外部符号 "void __cdecl display(int)" (?display@@YAXH@Z),该符号在函数 _main 中被引用 1>demo78.obj : error LNK2019: 无法解析的外部符号 "void __cdecl ns::display(void)" (?display@ns@@YAXXZ),该符号在函数 _main 中被引用 1>demo78.obj : error LNK2019: 无法解析的外部符号 "public: void __thiscall Demo::display(void)" (?display@Demo@@QAEXXZ),该符号在函数 _main 中被引用 ``` 小括号中就是经 Name Mangling 产生的新函数名,它们都以`?`开始,以区别C语言中的`_`。上图是 VS2017 产生的错误信息,不同的编译器有不同的 Name Mangling 算法,产生的函数名也不一样。 > `__thiscall`、`__cdecl` 是函数调用惯例 ### 成员函数的调用 从上图可以看出,成员函数最终被编译成与对象无关的全局函数,如果函数体中没有成员变量,那问题就很简单,不用对函数做任何处理,直接调用即可。 如果成员函数中使用到了成员变量该怎么办呢?成员变量的作用域不是全局,不经任何处理就无法在函数内部访问。 C++规定,编译成员函数时要额外添加一个参数,把当前对象的指针传递进去,通过指针来访问成员变量。 假设 Demo 类有两个 int 型的成员变量,分别是 a 和 b,并且在成员函数 display() 中使用到了,如下所示: ```c++ void Demo::display(){ cout << a << endl; cout << b << endl; } ``` 那么编译后的代码类似于: ```c++ void new_function_name(Demo *const p){ //通过指针p来访问a、b cout << p->a << endl; cout << p->b << endl; } ``` 使用`obj.display()`调用函数时,也会被编译成类似下面的形式: ```c++ new_function_name(&obj); ``` 这样通过传递对象指针就完成了成员函数和成员变量的关联。这与我们从表明上看到的刚好相反,通过对象调用成员函数时,不是通过对象找函数,而是通过函数找对象。 这一切都是隐式完成的,对程序员来说完全透明,就好像这个额外的参数不存在一样。 最后需要提醒的是,`Demo * const p`中的 const 表示指针不能被修改,p 只能指向当前对象,不能指向其他对象。