动态内存

  • C语言的一切操作都是基于内存的
  • 变量和数组都是内存的别名

​ 内存分配由编译器在编译期间决定
​ 定义数组的时候必须指定数组长度
​ 数组长度是在编译期就必须确定的

image-20220221142109655

  • malloc所分配的是一块连续的内存

  • malloc以字节为单位,并且不带任何的类型信息

  • free用于将动态内存归还系统

    void* malloc (size_t size);
    void free(void* pointer);

malloc和free是库函数,而不是系统调用
malloc实际分配的内存可能会比请求的多
不能依赖于不同平台下的malloc行为(可能被改动)
当请求的动态内存无法满足是,malloc返回NULL
当free参数为NULL时,函数直接返回

一道面试题:malloc(0)的返回?

1
2
3
4
5
6
7
8
9
10
11
#include <stdio.h>
#include <malloc.h>

int main()
{
int* p = (int *)malloc(0);

printf("p = %p, sizeof(p) = %d\n", p,(int)sizeof(p));

return 0;
}
1
2
fengyun@ubuntu:~/share$ ./test
p = 0x55920ddff2a0, sizeof(p) = 8

p指针是保存了地址值的,malloc(0)是合法的!
我们向系统申请了一段长度为0的内存空间,系统返回了一个地址值。
这个程序我们也应该加上free函数。

程序里一直malloc(0)申请内存但是不释放会造成内存泄漏吗?

需要!因为当代操作系统malloc得到的空间往往比实际内存更大,比如内存对齐,malloc(0)得到4个字节。

内存泄漏检测模块

非线程安全,主要讲一下原理

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
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
#include "mleak.h"

#define SIZE 256

/* 动态内存申请参数结构体 */
typedef struct
{
void* pointer;
int size;
const char* file;
int line;
} MItem;

static MItem g_record[SIZE]; /* 记录动态内存申请的操作 */

void* mallocEx(size_t n, const char* file, const line)
{
void* ret = malloc(n); /* 动态内存申请 */

if( ret != NULL )
{
int i = 0;

/* 遍历全局数组,记录此次操作 */
for(i=0; i<SIZE; i++)
{
/* 查找位置 */
if( g_record[i].pointer == NULL )
{
g_record[i].pointer = ret;
g_record[i].size = n;
g_record[i].file = file;
g_record[i].line = line;
break;
}
}
}

return ret;
}

void freeEx(void* p)
{
if( p != NULL )
{
int i = 0;

/* 遍历全局数组,释放内存空间,并清除操作记录 */
for(i=0; i<SIZE; i++)
{
if( g_record[i].pointer == p )
{
g_record[i].pointer = NULL;
g_record[i].size = 0;
g_record[i].file = NULL;
g_record[i].line = 0;

free(p);

break;
}
}
}
}

void PRINT_LEAK_INFO()
{
int i = 0;

printf("Potential Memory Leak Info:\n");

/* 遍历全局数组,打印未释放的空间记录 */
for(i=0; i<SIZE; i++)
{
if( g_record[i].pointer != NULL )
{
printf("Address: %p, size:%d, Location: %s:%d\n", g_record[i].pointer, g_record[i].size, g_record[i].file, g_record[i].line);
}
}
}

calloc和readlloc

calloc() 在内存中动态地分配 num 个长度为 size 的连续空间,并将每一个字节都初始化为 0。所以它的结果是分配了 num*size 个字节长度的内存空间,并且每个字节的值都是0。

calloc() 与 malloc() 的一个重要区别是:calloc() 在动态分配完内存后,自动初始化该内存空间为零,而 malloc() 不初始化,里边数据是未知的垃圾数据。

realloc() 对 ptr 指向的内存重新分配 size 大小的空间,size 可比原来的大或者小,还可以不变(如果你无聊的话)。当 malloc()、calloc()分配的内存空间不够用时,就可以用 realloc() 来调整已分配的内存。扩大的部分仍然是随机值

指针 ptr 必须是在动态内存空间分配成功的指针,形如如下的指针是不可以的:int *i; int a[2];会导致运行时错误,可以简单的这样记忆:用 malloc()、calloc()、realloc() 分配成功的指针才能被 realloc() 函数接受。

成功分配内存后 ptr 将被系统回收,一定不可再对 ptr 指针做任何操作,包括 free();相反的,可以对 realloc() 函数的返回值进行正常操作。

如果是扩大内存操作会把 ptr 指向的内存中的数据复制到新地址(新地址也可能会和原地址相同,但依旧不能对原指针进行任何操作);如果是缩小内存操作,原始据会被复制并截取新长度。

程序中的栈

栈在程序中主要维护函数调用上下文

函数的参数和局部变量存储在栈上

image-20220221143335013

程序中栈可以理解为是一种行为–后进先出。

image-20220221143533591

每一次函数调用都对应着一个栈上的活动记录

调用函数的活动记录位于栈的中部
被调函数的活动记录位于栈的顶部

image-20220221143733990

image-20220221150629888

函数调用时,对应的栈空间在函数返回前是专用的,函数调用结束后,栈空间被释放,数据将不再有效

image-20220221150902736

如图,f函数临时变量数组b获取g返回的指针pointer指向的数组,尽管g函数返回的数组a内部存储值没有改变,但也是没有意义的。

image-20220221151510857

注销掉for循环,直接打印g函数返回值,可以看到值被覆盖了。原因是调用了printf函数,原先记录被printf的一些参数信息给覆盖了。

image-20220221151747241

程序中的堆

堆是程序中一块预留的内存空间,可由程序自由使用
堆中被程序申请使用的内存在被主动释放前将一直有效

为什么有了栈还需要堆?
栈上的数据在函数返回后就会被释放掉,无法传递到函数外部,如:局部数组

静态存储区

  • 静态存储区随着程序的运行而分配空间
  • 静态存储区的生命周期直到程序运行结束
  • 程序的编译期静态存储区的大小就已经确定了
  • 静态存储区主要用于保存全局变量和静态局部变量
  • 静态存储区的信息最终会保存到可执行程序中
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
#include <stdio.h>

int g_v = 1;

static int g_vs = 2;

void f()
{
static int g_vl = 3;

printf("%p\n", &g_vl);
}

int main()
{
printf("%p\n", &g_v);

printf("%p\n", &g_vs);

f();

return 0;
}

在编译期静态存储区就分配好了,全局变量和静态变量都放在了静态存储区的并且静态存储区的大小位置在程序的编译期就已经确定了

1
2
3
4
5
fengyun@ubuntu:~/share$ gcc test.c -o test
fengyun@ubuntu:~/share$ ./test
0x55d3af22e010
0x55d3af22e014
0x55d3af22e018

程序的内存布局

初始化好了的全局变量和静态局部变量存到了.data段

未初始化好了的全局变量和静态局部变量存到了.bss段

这张图是可执行程序,是没有栈的

image-20220221152144052

这就必须说一下进程了

程序和进程不同

  • 程序是静态的概念,是硬盘上的一个文件,表现形式为一个可执行文件
  • 进程是动态的概念,程序由操作系统加载运行后得到进程
  • 每个程序可以对应多个进程
  • 每个进程只能对应一个程序

一道面试题:
包含脚本代码的文本文件时一种类型的可执行程序吗?如果是,对应什么样的进程?

image-20220221153249955

程序文件布局在内存映射

image-20220221160616714

各个段的作用:

  • 堆栈段在程序运行后才正式存在,是程序运行的基础
  • .bss段存放的是未初始化的全局变量和静态变量
  • .text段存放的是程序的可执行代码
  • .data段保存的是已经初始化了的全局变量和静态变量
  • .rodata段存放程序中的常量值,如字符串常量

程序术语的对应关系

  • 静态存储区通常指程序中的.bss和.data段
  • 只读存储区通常指程序中的.rodata段
  • 局部变量所占空间为栈上的空间
  • 动态空间为堆中的空间
  • 程序可执行代码存放于.text段

一道面试题:同是全局变量和静态变量,为什么初始化的和未初始化的保存在不同段.bss和.data中?(开放)
一个全局变量或静态变量没有在代码中初始化时,在内存加载时操作系统会设置为0,
而对于已经在代码中初始化的全局变量或静态变量必然在程序里一定要保存它的初始值;并且初始值一定要对应好,在最终的可执行文件那么就应该要有映射的对应关系。
如果不加区分,加载的时候效率低,映射关系复杂。

内存操作经典问题

野指针

  • 指针变量中的值是非法的内存地址,进而形成野指针
  • 野指针不是NULL指针,是指向不可用内存地址的指针
  • NULL指针并无危害,很好判断,也很好调试
  • C语言中无法判断一个指针所保存的地址是否合法

野指针的由来

  • 局部指针变量没有被初始化
  • 指针所指向的变量在指针之前被销毁
  • 使用已经释放过的指针
  • 进行了错误的指针运算
  • 进行了错误的强制类型转换
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
#include <stdio.h>
#include <malloc.h>

int main()
{
int* p1 = (int*)malloc(40);//合法
int* p2 = (int*)1234567; //错误地强制类型转换
int i = 0;

printf("%p\n", p1);

for(i=0; i<40; i++)
{
*(p1 + i) = 40 - i; //p1+1是移动4个字节,第11次开始使用了野指针
}

free(p1); //p1的值仍然不变,但是内存被释放了变成了野指针,释放后应该立即设为nullptr

printf("%p\n", p1);

for(i=0; i<40; i++)
{
p1[i] = p2[i]; //使用了已经释放的内存空间,程序崩溃
}

return 0;
}

这种类似情况千万不能出现,十分地危险!!!

1
2
3
4
fengyun@ubuntu:~/share$ ./test
0x55e37ca7f2a0
0x55e37ca7f2a0
段错误 (核心已转储)

基本原则

  1. 绝不返回局部变量和局部数组的地址
  2. 任何变量在定义后必须0初始化
  3. 字符数组必须确认0结束符后才能成为字符串
  4. 任何使用于内存操作相关函数必须指定长度信息

杜绝野指针,非常的重要!!!

常见内存错误

  • 结构体成员指针未初始化
  • 结构体成员指针未分配足够的内存
  • 内存分配成功,但并未初始化
  • 内存操作越界

内存操作的交通规则

  • 动态内存申请之后,应该立即检查指针,值是否为NULL,防止使用NULL指针
  • free指针之后必须立即赋值为NULL
  • 任何与内存操作相关的函数都必须带长度信息
  • malloc操作与free操作必须匹配,防止内存泄漏和多次释放(尽量不要跨函数释放)

关于string存储位置

以下数据均在ubuntu 64位系统,g++ c++11情况下测试

1. 数据<16字节,在当前栈区

局部变量b应该是存储在栈区的,地址与&b相近,位于栈区

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
#include <iostream>
#include <stdio.h>
#include <stdlib.h>
using namespace std;
int main()
{
string temp = "123456789012345"; //注意长度
int* a = (int*)malloc(sizeof(int));
int b = 0;
for (auto& c : temp)
{
printf("字符%c的位置%p\n",c, &c);
}
printf("\n");
printf("a指向堆的内存位置%p\n", a);
printf("局部变量b的位置%p\n", &b);

printf("\n");
const char* s1 = "1234567890";
const char* s2 = "1234567890";
printf("常量字符串s1的位置%p\n", s1);
printf("常量字符串s2的位置%p\n", s2);
return 0;

}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
fengyun@ubuntu:~/share$ ./test
字符1的位置0x7ffe6800d390
字符2的位置0x7ffe6800d391
字符3的位置0x7ffe6800d392
字符4的位置0x7ffe6800d393
字符5的位置0x7ffe6800d394
字符6的位置0x7ffe6800d395
字符7的位置0x7ffe6800d396
字符8的位置0x7ffe6800d397
字符9的位置0x7ffe6800d398
字符0的位置0x7ffe6800d399
字符1的位置0x7ffe6800d39a
字符2的位置0x7ffe6800d39b
字符3的位置0x7ffe6800d39c
字符4的位置0x7ffe6800d39d
字符5的位置0x7ffe6800d39e

a指向堆的内存位置0x562d39878eb0
局部变量b的位置0x7ffe6800d344

常量字符串s1的位置0x562d38c3e061
常量字符串s2的位置0x562d38c3e061

2. 数据>=16字节,在堆区

修改string temp = "1234567890123456"16字节

观察到string的各个字节的地址与a在堆的内存的地址接近,因此位于堆空间中。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
fengyun@ubuntu:~/share$ ./test
字符1的位置0x55b80d645eb0
字符2的位置0x55b80d645eb1
字符3的位置0x55b80d645eb2
字符4的位置0x55b80d645eb3
字符5的位置0x55b80d645eb4
字符6的位置0x55b80d645eb5
字符7的位置0x55b80d645eb6
字符8的位置0x55b80d645eb7
字符9的位置0x55b80d645eb8
字符0的位置0x55b80d645eb9
字符1的位置0x55b80d645eba
字符2的位置0x55b80d645ebb
字符3的位置0x55b80d645ebc
字符4的位置0x55b80d645ebd
字符5的位置0x55b80d645ebe
字符6的位置0x55b80d645ebf
字符7的位置0x55b80d645ec0

a指向堆的内存位置0x55b80d645ed0
局部变量b的位置0x7fff498adcf4

常量字符串s1的位置0x55b80d242063
常量字符串s2的位置0x55b80d242063

const char*

const char* = “1234567890”这是个常量字符串,位于内存的常量区。