C/C++ 内存四区总结

0.简单概述

C/C++程序在执行时,将内存大方向划分为4个区域(内存四区)来存放所有数据。

程序运行前产生

  • 代码区:存放函数体的二进制代码,由操作系统进行管理的

  • 全局区:存放全局变量、静态变量以及常量

程序运行后产生

  • 栈区由编译器自动分配释放, 存放函数的参数值、局部变量等

  • 堆区由程序员分配和释放, 若程序员不释放,程序结束时由操作系统回收

内存四区意义:不同区域存放的数据,赋予不同的生命周期, 给我们更大的灵活编程。

按是否在全局区来划分变量和常量:

image-20230805161358288

虚拟地址空间(操作系统的视角)

虚拟地址空间被分配给进程,用于存储程序代码、数据和堆栈等。

image-20230707120709782

小总结:

(1).全局区存放哪些数据?

全局变量、静态变量、常量(字符串常量,const修饰的全局常量或变量)

(2).不在全局区中的有哪些?

局部变量(栈区),const修饰的局部常量或变量

(3).区分const修饰的局部常量或变量:

const 修饰的局部常量或变量在声明时必须初始化,并且一旦初始化后,就不能再改变它们的值。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
修饰常量:
const int MAX_VALUE = 100; // 这里用const声明并初始化了一个常量,其值不能被修改

修饰指针变量:
const int* ptr = &x; // ptr是一个指向常量的指针,指针内容不可修改
int* const ptr2 = &y; // ptr2是一个常量指针,指针本身地址不可修改
const int* const ptr3 = &z; // ptr3是一个指向常量的常量指针,指针本身及内容都不可修改

int main()
{
int x = 5;
const int* ptr = &x; // ptr 是一个指向常量的指针,指向的内容不能通过指针修改
// *ptr = 10; error

int y = 15;
int* const ptr2 = &y; // ptr2 是一个常量指针,指针本身不能被修改
*ptr2 = 20; // 指向的内容可以修改,可以修改y的值
// ptr2 = &x; error 常量指针本身地址不能被修改

const int z = 25;
const int* const ptr3 = &z; // ptr3 是一个指向常量的常量指针,既不能修改指针本身,也不能通过指针修改指向的内容
// *ptr3 = 30; error
// ptr3 = &x; error
}

1.程序运行前

在C++程序编译后,Windows环境下生成了.exe可执行程序,未执行该程序前分为两个区域:代码区和全局区。

程序运行前总结:

  • C++中在程序运行前分为全局区和代码区
  • 代码区特点是共享和只读
  • 全局区中存放全局变量、静态变量、常量
  • 常量区中存放 const修饰的全局常量 和 字符串常量

01.代码区

特点:只读、共享

存放 CPU 执行的机器指令(即二进制0101)

代码区是只读的,使其只读的原因是防止程序意外地修改了它的指令;

代码区是共享的,共享的目的是对于频繁被执行的程序,只需要在内存中有一份代码可。


02.全局区

全局变量和静态变量,还包含常量区, 字符串常量和其他常量也存放在此。

该区域的数据在程序结束后由操作系统释放。

代码示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
#include<iostream>
using namespace std;

// 全局区(存放的变量): 全局变量、静态变量、常量(字符串常量,const修饰的全局常量或变量)
// 数据在程序结束后,由操作系统释放
int g_a = 10;
int g_b = 20;

// const修饰的全局常量
const int c_g_a = 10;
const int c_g_b = 20;

int main()
{
cout << "在全局区中" << endl;
// 全局变量
cout << "全局变量g_a的地址为:" << (int) &g_a << endl;
cout << "全局变量g_b的地址为:" << (int) &g_b << endl;

// 静态变量
static int s_a = 10;
static int s_b = 10;
cout << "静态变量s_a的地址:" << (int) &s_a << endl;
cout << "静态变量s_b的地址:" << (int) &s_b << endl;

// 常量
// 字符串常量(双引号引起来的) 即"xxxxxxx"
cout << "字符串常量的地址:" << (int)&"hhhhh" << endl;

// const修饰的全局(global)常量
cout << "const修饰的全局常量c_g_a的地址为:" << (int) &c_g_a << endl;
cout << "const修饰的全局常量c_g_b的地址为:" << (int) &c_g_b << endl;


cout << "------------------------------------------------" << endl;

cout << "不在全局区中" << endl;

// 创建普通局部变量(写在函数体内),局部变量存放在栈区
int a = 10;
int b = 20;
cout << "局部变量a的地址为:" << (int)&a << endl;
cout << "局部变量b的地址为:" << (int)&b << endl;

// const修饰的局部(local)常量
const int c_l_a = 10;
const int c_l_b = 110;
cout << "const修饰的局部常量c_l_a的地址为:" << (int)&c_l_a << endl;
cout << "const修饰的局部常量c_l_b的地址为:" << (int)&c_l_b << endl;

return 0;
}

运行结果:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
在全局区中
全局变量g_a的地址为:653516800
全局变量g_b的地址为:653516804
静态变量s_a的地址:653516808
静态变量s_b的地址:653516812
字符串常量的地址:653504136
const修饰的全局常量c_g_a的地址为:653503408
const修饰的全局常量c_g_b的地址为:653503412
------------------------------------------------
不在全局区中
局部变量a的地址为:1060109108
局部变量b的地址为:1060109140
const修饰的局部常量c_l_a的地址为:1060109172
const修饰的局部常量c_l_b的地址为:1060109204

2.程序运行后

程序运行后分为栈区和堆区。

01.栈区

由编译器管理开辟和释放, 存放局部变量、函数的形参等。

注意事项:不要返回局部变量的地址或引用

(1).返回局部变量的地址,warning: address of local variable ‘a’ returned

当一个函数返回指向栈内存的地址时,栈上的局部变量会被释放,这意味着返回的地址将指向一个已经不再有效的内存区域。

在后续使用该地址时,相当于访问已经释放的内存,可能会导致程序崩溃或产生不可预测的行为。

(2).返回局部变量的引用,warning: reference to local variable ‘a’ returned

当函数执行完毕后,局部变量 a 将被销毁,这意味着返回的引用将成为悬挂引用 (dangling reference)。这个操作比较危险,会导致数据不可控。

悬挂引用是指指向已经被销毁的内存的引用。

当调用者试图使用这个引用时,会访问一个无效的内存位置,导致未定义的行为,很可能会导致程序崩溃或产生不可预测的结果。

(3).解决方案:

如果函数需要返回一个局部变量的值,可以通过值传递的方式返回,

或者返回一个动态分配的内存对象,可以使用智能指针(如 std::unique_ptrstd::shared_ptr)来管理动态内存的释放。

代码示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
#include <iostream>
using namespace std;

// 栈区:局部变量,形参
// 栈区注意事项,不要返回局部变量的地址
// 栈区的数据由编译器管理开辟和释放

int* func(int b) { // 写法一
b = 20;
int a = 10; // 栈上,局部变量存放在栈区,函数执行完后自动释放
return &a; // 返回了局部变量的地址(不建议)
}

int& func1() { // 写法二
int a = 10;
return a; // 返回了局部变量的引用 (不建议)
} // int &temp = a;

int main()
{
int* p = func(1);
cout << *p << endl; // 第一次,编译器做了保留,可以打印正确的值(visual studio里可以正常打印)
cout << *p << endl; // 第二次,这个数据就不会被保留了,相当于是访问已经释放的内存,这是非法操作。
// error 程序运行发生 Segmentation fault,段错误
cout << "-------------" << endl;

int& q = func1();
cout << q << endl; // error 也发生了 Segmentation fault,段错误

return 0;
}

修改后的函数如下,

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// 使用值传递
int func(int b) {
b = 20;
int a = 10;
return a; // 返回局部变量的值
}

// 返回动态分配的内存对象
#include <memory>
std::unique_ptr<int> func(int b) {
b = 20;
std::unique_ptr<int> ptr = std::make_unique<int>(10);
return ptr; // 返回 std::unique_ptr,它会负责释放内存
}

02.堆区

在C++中主要利用new关键字,在堆区开辟内存;

堆区开辟的数据,由程序员手动开辟,手动释放,**释放利用操作符delete**;若程序员不释放, 程序结束时则由操作系统回收。

语法: new 数据类型

利用new创建的数据,会返回该数据对应的类型的指针。

小总结:

堆区数据由程序员管理开辟和释放

堆区数据利用new关键字进行开辟内存

指针是放在栈区(局部变量),指针存放的数据在堆区。

image-20230805181400665

代码示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
#include <iostream>
using namespace std;

// 堆区,由程序员来管理
int* func() {
// 利用new关键字,可以将数据开辟到堆区
// 指针是放在栈区(局部变量),指针存放的数据在堆区
// new返回的是该数据类型的指针
int* p = new int(100);
return p;
}

void test1() {
int* x = func();
cout << *x << endl; // 100
cout << *x << endl; // 100
delete x; // 释放堆区数据
// cout << *x << endl; // error 释放的空间不可访问,非法访问
}

void test2() {
// 在堆区利用new开辟数组
int* arr = new int[10]; // 创建一个有10个元素的地址
// 返回对应类型的指针
for (int i = 0; i < 10; i++) {
arr[i] = i + 100;
cout << arr[i] << endl; // 100 ~109
}
// 释放数组
delete[] arr;
}

int main()
{
//test1();
test2();
return 0;
}

参考资料

【视频 黑马程序员 C++教程 p84开始】