文章

C 内存管理

AUTOGEN e082a75e863d49dbbc155f11e49ea778

C 内存管理

1. 内存四区

内存四区是指计算机内存在运行程序时划分的四个主要区域,用于存储不同类型的数据。这些区域包括:

  1. 代码区(Code Segment):也称为文本区,用于存储程序的执行代码。在程序运行之前,代码区的内容就已经被确定并且不能被修改。
  2. 数据区(Data Segment):数据区分为两个部分:

    1. 静态数据区(Static Data Segment):用于存储全局变量和静态变量。这些变量在程序的整个执行过程中都存在,并且其内存空间在编译时就已经分配好。
    2. 堆区(Heap Segment):用于存储动态分配的内存,例如通过new或malloc函数在运行时申请的内存。堆区的内存分配和释放由程序员手动控制。
  3. 栈区(Stack Segment):用于存储程序的局部变量和函数调用时的临时数据。栈区的内存分配和释放是自动进行的,由编译器根据函数的调用和返回自动管理。
  4. 常量区(Constant Segment):用于存储常量数据,例如字符串常量。常量区的内容在程序运行期间是不可修改的。

image-20240804223805027

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
# include <stdio.h>
# include <stdlib.h>


int G_var_1 = 1;
int G_var_2 = 2;
int G_var_3 = 3;

static int S_var_1 = 11;
static int S_var_2 = 12;
static int S_var_3 = 13;

const int C_var_1 = 101;
const int C_var_2 = 102;

int* create_a_heap(){
    return (int*)malloc(sizeof(int));
}

void printmem(unsigned char *start, int len)
{
    printf("a prefix at %p is %.2x\n",--start);
    ++start;
    while(len--)
        printf("%p -> %.2x \n",start,*(start++));
    
}

int main(){

    printf("global variable 1 at %p is %d\n",&G_var_1,G_var_1);
    printf("global variable 2 at %p is %d\n",&G_var_2,G_var_2);
    printf("global variable 3 at %p is %d\n",&G_var_3,G_var_3);
    printf("--------------\n");
    printmem((unsigned char *)&G_var_1,3*4);
    printf("static variable 1 at %p is %d\n",&S_var_1,S_var_1);
    printf("static variable 2 at %p is %d\n",&S_var_2,S_var_2);
    printf("static variable 3 at %p is %d\n",&S_var_3,S_var_3);
    printf("--------------\n");
    printmem((unsigned char *)&S_var_1,3*4);
    printf("constant variable 1 at %p is %d\n",&C_var_1,C_var_1);
    printf("constant variable 2 at %p is %d\n",&C_var_2,C_var_2);
    printmem((unsigned char *)&C_var_1,2*4);
    int * h = create_a_heap();
    *h = 123;
    printf("heap variable h at %p is %d\n",h,*h);
    printmem((unsigned char *)h,8);
}
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
53
54
55
56
global variable 1 at 00402000 is 1
global variable 2 at 00402004 is 2
global variable 3 at 00402008 is 3
--------------
a prefix at 00401FFF is 60ffcc
00402001 -> 01 
00402002 -> 00 
00402003 -> 00 
00402004 -> 00 
00402005 -> 02 
00402006 -> 00 
00402007 -> 00 
00402008 -> 00 
00402009 -> 03 
0040200A -> 00 
0040200B -> 00 
0040200C -> 00 
static variable 1 at 0040200C is 11
static variable 2 at 00402010 is 12
static variable 3 at 00402014 is 13
--------------
a prefix at 0040200B is 60ffcc
0040200D -> 0b 
0040200E -> 00 
0040200F -> 00 
00402010 -> 00 
00402011 -> 0c 
00402012 -> 00 
00402013 -> 00 
00402014 -> 00 
00402015 -> 0d 
00402016 -> 00 
00402017 -> 00 
00402018 -> 00 
constant variable 1 at 00403000 is 101
constant variable 2 at 00403004 is 102
a prefix at 00402FFF is 60ffcc
00403001 -> 65 
00403002 -> 00 
00403003 -> 00 
00403004 -> 00 
00403005 -> 66 
00403006 -> 00 
00403007 -> 00 
00403008 -> 00 
heap variable h at 00E01518 is 123
a prefix at 00E01517 is 60ffcc
00E01519 -> 7b 
00E0151A -> 00 
00E0151B -> 00 
00E0151C -> 00 
00E0151D -> c0 
00E0151E -> 00 
00E0151F -> e0 
00E01520 -> 00 

栈区

对于栈区来讲,它是先进后出的。

程序使用前,编译器会根据代码来分配合适的栈空间,程序中的局部变量,返回值,参数等等都会放在栈空间中,编译器也会在程序结束时进行回收,如果栈空间的内存不足,就会导致内存溢出 overflow。

但是栈空间是完全由编译器管理的。

image-20240804223834175

堆区

堆区是程序员自己申请分配和管理的一块内存区域,要分配就要释放。例如一个两个节点的动态链表:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
#include <iostream>
#include <cstdio>
#include <malloc.h>
using namespace std;
struct LinkedNode {
	int data;
	LinkedNode* next;
};
typedef LinkedNode Node;
int main() {
	Node* p1 = NULL, * p2 = NULL;
	p1 = (Node*)malloc(sizeof(Node));
	p2 = (Node*)malloc(sizeof(Node));
	//Consider p1 and p2 allocate success
	p1->next = p2;
	p2->next = NULL;
	printf("%p\n%p", p1, p2);
	return 0;
}

image-20240804223900652

代码区

代码区涉及汇编,暂不讨论

数据区

数据区中存放着静态变量,全局变量等。

例如下面这段代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
#include <iostream>
#include <cstdio>
using namespace std;
int g_var = 20;
int main() {
	int a = 1;
	static int b = 2;
	const int c = 10;
	printf("int a = %d,          \tadd = %p\n", a,&a);
	printf("const int c = %d,    \tadd = %p\n", c, &c);
	printf("static int b = %d,   \tadd = %p\n", b,&b);
	printf("globe int g_var = %d,\tadd = %p\n", g_var,&g_var);
	return 0;
}
1
2
3
4
int a = 1,              add = 00F8F82C
const int c = 10,       add = 00F8F820
static int b = 2,       add = 0076C008
globe int g_var = 20,   add = 0076C004

我们发现变量a和变量c是连续的

但是静态变量和局部变量和前者并不连续,因为他们储存在数据区

而数据区分为

Data Segment (数据区)

存放已初始化的全局和静态变量, 常量数据(如字符串常量)。

BSS(Block started by symbol)

存放未初始化的全局和静态变量。(默认设为0)

2. 内存对齐

内存占位

首先以整型和字符型举例

我们通过如下代码知晓了两个数据类型的大小:

1
2
3
4
5
6
7
int main() {
	int _intVar;
	char _charVar;
	printf("int takes \t%d\n", sizeof(_intVar));
	printf("char takes \t%d\n", sizeof(_charVar));
	return 0;
}
1
2
int takes       4
char takes      1

内存对齐

1
2
3
4
5
6
7
8
9
10
11
12
struct MemoryAligned {
	int _intVar01;
	char _charVar01;
	char _charVar02;
	char _charVar03;
	char _charVar04;
};
struct MemoryNotAligned {
	char _charVar01;
	int _intVar01;
	char _charVar02;
};

在第一个结构体中,定义了5个变量,第二个结构体中只定义了3个,那么按照内存分配来说,第一个占据的内存空间应该比第二个大。

1
2
memory aligned takes 8
memory not aligned takes 12

但结果却是第二个内存占据了比第一个内存还小的空间。

之所以出现这种现象是因为内存没有对齐

下面我们看这个对齐的示意图:

image-20240804224049996

3. 内存操作

内存开辟

通过new或者其他内存开辟函数和方法来开辟内存,内存大小可以指定。

例如这个函数可以指定特定的类型和空间大小。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
/*被注释掉的部分是C语言风格的,但是C语言并不支持模板和decltype表达式,所以不建议使用*/
#include <iostream>
#include <malloc.h>
using std::cout;
using std::endl;
template <typename T>
T* AllocateBlock(int number){
	/*
	T *p = NULL;
	p = (T*)malloc(sizeof(T)*number);
	return p;
	*/
	T *pArray = new T(number);
	return pArray;
}
int main(){
	int test_type;
	decltype(test_type) *p = NULL;
	p = AllocateBlock<decltype(test_type)>(10);//开辟10个test_type类型的内存空间
	//free(p);
	delete p;
	return 0;
}

内存的精确写入

如果要在一个块中写入特定的值,并将其通用化。可以使用malloc函数,这是一个底层函数。

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
/**
	2021/2/8
	[email protected]
*/
#include <iostream>
#include <typeinfo>
#include <malloc.h>
using std::cout;
using std::endl;
//提供了在特定内存块写入特定值的函数
template<typename T>
T *TestBlock(T data){
	T *p = (T*)malloc(sizeof(T));
	cout << "size of " << typeid(T).name() << " is " << sizeof(T) << endl;
	*p = data;
	cout << "block storge " << (int)*p << " at " << &p << endl;
	return p;
}
int main(){
	char test_type;
	decltype(test_type)*p =NULL;//指定类型
	p = TestBlock<decltype(test_type)>(0b01000100);//写值
	free(p);
	return 0;
}

以上函数在调用时的参数是二进制,在内存块中申请了一个字节,这个字节是八个位,八个位写的值非常精确。

内存排布

1
2
3
4
5
6
7
8
9
10
11
12
13
14
void showMemoryBytes(void *startADD,int len) {
	const char *header = "|Address |0x|0b       |\n|--------|--|---------|\n";
	char HexList[16][5] = {
	"0000",	"0001",	"0010",	"0011",	"0100",	"0101", "0110", "0111",
	"1000", "1001", "1010", "1011", "1100", "1101", "1110", "1111"
	};
	unsigned char *pStart = (unsigned char *)startADD;
	printf(header);
	for (int i = 0; i < len; i++)
	{
		printf("|%p|%.2x|%s %s|\n", pStart, *pStart,HexList[*pStart/16], HexList[*pStart%16]);
		pStart++;
	}
}

调用上面的函数可以查看内存的排布情况

本文由作者按照 CC BY 4.0 进行授权