C++内存四区初认识
栈区
:存放函数的局部变量、函数参数和返回值。栈区的内存由编译器自动管理,随着函数的调用和结束而动态分配和释放。堆区
:用于动态分配内存,存放程序运行时动态创建的对象和数据。堆区的内存需要手动分配和释放,可以通过new/delete
和malloc/free
关键字来管理。全局区/静态区
:存放全局变量,静态变量和常量。全局区/静态区的变量在整个程序运行期间都存在。代码区
:主要存放的是函数体被编译后的机器码。这部分内存通常是只读的,用于存储程序的指令。代码区中存放的就是CPU执行的机器指令,代码区是共享和只读的,共享是指对于频繁被执行的程序,只需要在内存中有一份代码就行。而只读则表示防止程序意外修改指令
代码区和全局区/静态区在程序运行之前就有了,它们存在于C++的可执行文件中
。总的来说,可以分为静态存储区(代码区,全局区)和动态区(栈区、堆区)。
java程序员困惑点来了
对于4大区中的堆区和栈区,相信大家没啥疑问,这一点和Java中的堆栈没啥区别。但是很多Java程序员会对全局区和代码区比较困惑,因为这一点和jvm运行时数据区的模型不太一样,jvm中倒是有个方法区可以存放静态变量,可是它也程序运行之后,需要类加载的时候才会有啊?而代码区更是在java和jvm中找不到类似的地方,因为java并没有设计专门的函数分区,只是在调用函数的时候,方法入栈出栈罢了。别着急,接下来,我先向大家证明它的存在,然后再解释c++为什么这样设计?
如何查看代码区和全局区
C++的编译流程,可以分为预处理、编译、汇编、链接
4个步骤,然后生成一个可执行文件。可执行文件中就包含我们的代码区和全局区。下面给出查看可执行文件的一种方式:
less
1.源文件生成目标文件
g++ -c main.cpp -o main.o
2.目标文件链接生成最终的二进制可执行文件(文件名是hh)
g++ main.o -o hh
3. file hh //查看文件格式
hh: Mach-O 64-bit executable x86_64
4.size hh //查看文件各分区大小
__TEXT __DATA __OBJC others dec hex
16384 0 0 4294983680 4295000064 100008000
为什么c++的内存模型要设计静态区?
上面说了,静态区指的是代码区和全局区/静态区,实际上对应的是可执行文件中的_text(文本)段
,_data(数据段)
。首先要理解下,Java是解释性语言,而C++是编译时语言,需要考虑它们的编译和执行过程。
Java是一种解释性语言,它的源代码首先被编译成字节码,然后由JVM在运行时解释执行。在JVM中,字节码被加载到方法区,它是JVM的一部分,用于存储类的结构信息、静态变量、常量池等。当Java程序执行时,方法区中的字节码被解释器或即时编译器逐行解释或编译成机器代码,并通过执行引擎执行。执行引擎通常使用栈来保存方法调用的上下文信息,包括局部变量、参数等。这与C++中的栈执行模型类似。 因此,在Java中,函数的执行过程涉及字节码的解释和执行引擎的栈操作,而不是直接在内存中的代码区执行机器指令。这也是Java和C++在函数执行上的重大区别。
C++是一种编译时语言,它的源代码被编译器直接翻译成机器码,可以直接在底层硬件上运行。编译器将C++代码转换成特定平台的机器码,生成一个可执行文件。这个可执行文件包含了所有的机器码指令,可以直接由操作系统加载和执行。在C++中,代码和全局变量在编译时就被处理并嵌入到可执行文件中。这意味着在程序执行之前,这些代码和变量已经存在于可执行文件中。C++的编译模型支持静态链接,即在编译时将所有代码和数据链接到一个可执行文件中。这样做的好处是,程序在运行时不需要依赖外部文件,可以直接加载并执行。
C++选择将代码和全局变量静态嵌入到可执行文件中,这是为了实现更高的性能和直接的系统级访问。而Java选择在运行时动态加载类和函数,这是为了提供更大的灵活性和可移植性。*
栈区
C++中的栈和jvm的栈是比较类似的。当一个函数被调用时,函数的参数和局部变量都会被分配到栈区。每个函数调用都会在栈上创建一个称为栈帧的数据结构,用于存储函数的局部变量、参数和返回地址等信息。当函数执行完毕后,对应的栈帧会被销毁,释放栈上的内存空间。
栈区的特点是自动分配和释放内存,这点很高效,但它的缺点是栈的大小是比较有限的(具体大小和编译器/系统有关),且无法动态调整。因此,在使用栈区存储变量时,需要注意变量的生命周期和内存使用情况,以免出现栈溢出(Stack Overflow)的问题。
错误案例1,栈溢出
如下代码,当调用func1
函数就会出现异常。注意,如下创建数组的方式是在栈内分配的内存,而栈内的内存是很有限的
cpp
void func1() {
int array[10000000];
for (int i = 0; i < 10000000; i++) {
array[i] = i;
}
}
错误案例2,返回一个局部变量的指针
cpp
int *func2() {
int a = 10;
return &a;
}
int main() {
int *p2 = func2();
cout << *p2 << endl; //第一次正常返回
cout << *p2 << endl; //第二次返回异常
return 0;
}
输出结果
10
32760
可以看到第一次返回值是正常的,第二次的值明显异常了。因为返回的是局部变量的地址,这个地址在函数执行完(出栈)时就会失效, 但是编译器还是会为你保留一次。尽管如此,还是不建议你在函数内返回局部变量的地址。
堆区
C++中的堆区是一块用于动态内存分配的区域。堆区的内存分配和释放和释放由我们手动释放。通常使用 new
运算符来分配一块指定大小的内存,并返回该内存的指针。分配的内存在不再需要时,需要使用 delete
运算符手动释放,以避免内存泄漏。
案例1,通过new运算符正确的返回局部变量的指针
上面说过,返回局部变量的指针是一种常见的错误做法。但有的场景确实需要在函数中返回指向局部变量的指针,可以用如下做法:
cpp
int *func3() {
int *array = new int[3];
return array;
}
这种是动态内存分配,用 new
运算符在堆上分配内存,并返回指向动态分配内存的指针。这样,即使函数执行完毕,内存仍然有效,可以在其他地方使用。但是,需要确保在不需要使用delete[]
释放内存,以避免内存泄漏。
此外解决这个问题还有一种方式, 参数传递:可以通过函数参数传递指向已存在的内存的指针,让调用方负责分配和释放内存。这样可以确保内存的有效性,并减少内存泄漏的风险。
cpp
void func4(int *array, int size) {
}
int main() {
int myArray[3];
func4(myArray, 10);
return 0;
}
创建类对象
cpp
int main() {
Student stu1("zs", 18); //方式1
Student *stu2 = new Student("ls", 20); //方式2
//一些对象操作
delete stu2;
return 0;
}
这里通过2种方式来创建类对象。注意c++创建类对象并不一定是在堆内存上。这是不是又颠覆了java程序员的三观?
在C++中,类对象的分配位置取决于它们是在哪里创建的?什么方式创建的?
栈内存:如果类对象是在函数内部、作为局部变量或者作为函数参数创建的,那么对象会被分配在栈内存上。
堆内存:如果使用
new
运算符在堆上创建类对象,那么对象将被分配在堆内存中。对于类的成员变量,它们的分配位置与所在的类对象的分配位置相同。如果类对象是在栈上分配的,那么成员变量也会在栈上,如果类对象是在堆上分配的,那么成员变量也会在堆上。注意:无论类对象是在栈上还是在堆上分配,成员变量的大小和布局都是相同的。唯一的区别是对象的生命周期和内存管理方式。
全局区
全局区中的变量类型
全局区在可执行文件中,它包含如下类型的变量:
- 全局变量
- 静态变量,在c++中静态变量可以是全局变量也可以定义成局部变量,函数内定义的局部static变量也是在全局区
- 字符串常量和其他常量,比如const,#define等
cpp
int globalVar1 = 10;
int globalVar2 = 11;
static int globalStaticVar1 = 20;
static int globalStaticVar2 = 21;
const int globalConst1 = 30;
const int globalConst2 = 31;
int main() {
cout << "&globalVar1: " << &globalVar1 << endl;
cout << "&globalVar2: " << &globalVar2 << endl;
cout << "&globalStaticVar1: " << &globalStaticVar1 << endl;
cout << "&globalStaticVar2: " << &globalStaticVar2 << endl;
static int staticVar3 = 22;
cout << "&staticVar3: " << &staticVar3 << endl;
cout << "&globalConst1: " << &globalConst1 << endl;
cout << "&globalConst2: " << &globalConst2 << endl;
return 0;
}
输出结果:
&globalVar1: 0x10ebcc140
&globalVar2: 0x10ebcc144
&globalStaticVar1: 0x10ebcc148
&globalStaticVar2: 0x10ebcc14c
&staticVar3: 0x10ebcc150
&globalConst1: 0x10ebc7cf0
&globalConst2: 0x10ebc7cf4
全局区的3段
全局区又分为3段,data 、bss、rodata 段。
- data段存放初始化了的全局变量和静态变量;
- bss段存放未初始化的全局变量和静态变量,BSS段中的变量在程序执行期间会自动初始化为零值或空值;
- rodata段(只读数据段),常量区,用于存放各类常量,如:const、#define等。
代码区
代码区是可执行文件的一部分,存放的是函数体的机器码。在程序执行之前,编译器将源代码转换为机器指令,并将这些指令存储在代码区。代码区通常是只读的,因为程序在运行时不应该修改自身的指令。
在C++中,当需要调用某个函数时,程序会通过函数的名称或函数指针来查找对应的函数地址。在编译过程中,编译器会生成函数的符号表(Symbol Table),其中包含了函数名称和对应的地址信息。在链接阶段,链接器会将函数的调用点与函数的地址进行关联。这样,在程序执行时,当遇到函数调用语句时,程序会根据函数的地址跳转到对应的函数的机器指令执行。