Skip to content

动态内存管理

动态内存分配的意义

在语言学习时,对于内存的划分为上述三者:栈区,堆区,静态区。栈区存放临时变量,静态区存放静态变量,堆区用来动态开辟。

动态内存开辟是在堆区上开辟空间,为了灵活的使用内存,以满足程序的需要。具体如何开辟请看下列函数。

动态内存函数的介绍

开辟释放函数 malloc & free

函数声明

c
void* malloc( size_t size );
void free( void* memblock );
  • malloc函数在堆区上申请size个字节的空间,并返回该空间的起始地址。
  • free函数释放指针指向的动态开辟的空间,但不会置空指针。

函数用法

  • malloc返回通用类型的指针,将其强制转换为所需类型,并用该类型的指针维护该内存空间。
  • 开辟成功返回空间起始地址,开辟失败则返回NULL
  • 使用结束free释放内存以防内存泄漏,将指针置空避免成为野指针。
c
//申请空间
int* p = (int*)malloc(40);
//检查
if (p == NULL) {
    printf("%s\n", strerror(errno));
    return -1;
}
//使用
for (int i = 0; i < 10; i++) {
    *(p + i) = i;
    printf("%d ", *(p + i));
}
//释放空间
free(p);
//置空
p = NULL;

内存开辟函数 calloc

函数声明

c
void* calloc( size_t num, size_t size );

calloc函数在堆区上申请numsize大小的空间,返回起始地址并将内容初始化为0。

函数用法

c
int* p = (int*)calloc(10, sizeof(int));
if (p == NULL) {
    perror("");
    return -1;
}
for (int i = 0; i < 10; i++) {
    *(p + i) = i;
    printf("%d ", p[i]);
}
free(p);
p = NULL;

内存调整函数 realloc

函数声明

c
void* realloc( void* memblock, size_t size );

realloc函数为已开辟的空间重新开辟大小。

  1. 当原空间后有足够大小时,就紧接原空间开辟剩余空间,并返回整个空间的起始地址。
  1. 当原空间后无足够大小时,就在堆区寻找新空间,再将原空间的内容移动到新空间,返回新空间的地址且释放原空间。
  1. 当剩余空间不够无法开辟时,增容失败,返回NULL

函数用法

c
//1.
p = (int*)realloc(p, 20 * sizeof(int));
//2.
int* ptr = (int*)realloc(p, 20 * sizeof(int));
if (ptr == NULL) {
    return -1;
}
p = ptr;

防止增容失败将原空间指针置空,故不可直接使用原指针接受返回值。判断非空后再赋给原指针。

常见的动态内存错误

1. 不检查空指针

c
void test() {
	int* p = (int*)malloc(INT_MAX / 4);
	*p = 20;
	free(p);
}

对指向动态开辟的空间的指针一定要做有效的判断。

2. 越界访问

c
void test() {
	int i = 0;
	int* p = (int*)malloc(10 * sizeof(int));
	if (NULL == p) {
		exit(EXIT_FAILURE);
	}
	for (int i = 0; i <= 10; i++) {
		*(p + i) = i;
	}
	free(p);
    p = NULL;
}

作为程序员必须有意识地检查所写的代码是否有越界访问的问题。

3. 释放非动态开辟内存

c
void test() {
	int a = 10;
	int* p = &a;
	free(p);
    p = NULL;
}

不可用free释放非动态开辟的空间。

4. 释放部分内存

c
int main()
{
	int* p = (int*)malloc(100);
	p++;
	free(p);
	return 0;
}

改变指向动态开辟内存的指针,内存将无法管理。释放不完全导致内存泄漏。

5. 重复释放内存

c
void test() {
	int* p = (int*)malloc(100);
	free(p);
	free(p);
}

使用free释放已释放的空间,即访问非法内存。建议释放内存和指针置空搭配使用。

6. 忘记释放内存

c
void test() {
    int *p = (int*)malloc(100);
    if(NULL != p) {
        *p = 20;
    }
}
int main() {
    test();
    while(1);
}

使用结束不释放内存造成内存泄漏。程序不停止,系统也不会自动回收。

笔试题

例1

c
void GetMemory(char* p) {
	p = (char*)malloc(100);
}
void test() {
	char* str = NULL;
	GetMemory(str);
	strcpy(str, "hello world");
	printf(str);
    free(str);
    str = NULL;
}

程序报错。

传值调用:并没有改变str的值仍为不予修改的空指针,可以使用二级指针接收str的地址。函数调用结束后指针销毁故无法释放空间以致内存泄漏。

例2

c
char* GetMemory() {
	char p[] = "hello world";
	return p;
}
void test() {
	char* str = NULL;
	str = GetMemory();
	printf(str);
    free(str);
    str = NULL;
}

程序打印随机值。

返回栈空间地址:数组p在函数内创建,出函数销毁,返回这部分空间的地址 ,属于访问非法空间。

例3

c
void GetMemory(char** p,int num) {
	*p = (char*)malloc(num);
}
void test() {
	char* str = NULL;
	GetMemory(&str, 100);
	strcpy(str, "hello");
	printf(str);
	free(str);
    str = NULL;
}

程序运行成功,打印"hello"

传址调用:本题是例一的正确写法。

例4

c
void test(void) {
	char* str = (char*)malloc(100);
	strcpy(str, "hello");
	free(str);
	if (str != NULL) {
		strcpy(str, "world");
		printf(str);
	}
}

程序报错。

野指针:动态开辟的内存释放后指针不置空,造成野指针访问非法内存。释放内存和指针置空应该搭配起来使用。

释放空间就是将空间归还给系统,即将此空间的使用权限归还操作系统。

虽不会改变空间内容以致打印出所谓的“正确结果”,但可能在之后被操作系统分配给其他程序时发生修改。但无论改变与否,一旦空间归还后再去访问就是访问非法内存。

C/C++内存划分

用例展示

c
int globalVar = 1;
static int staticGlobalVar = 1;
int main()
{
	static int staticVar = 1;

	int localVar = 1;
	int num1[10] = { 1,2,3,4 };
	char char2[] = "abcd";
	char* pChar3 = "abcd";
	int* ptr1 = (int*)malloc(4 * sizeof(int));
	int* ptr2 = (int*)calloc(4, sizeof(int));
	int* ptr3 = (int*)realloc(ptr2, 4 * sizeof(int));

	free(ptr1);
	free(ptr3);
	return 0;
}
  1. globalVal,staticGobalVar,staticVar分别是全局变量和静态变量,在数据段上创建。
  2. localVarnum,char2,pchar以及ptr本身都是局部变量,都是在栈区上创建的。
  3. malloc,calloc,realloc都是在堆区上开辟的内存块,由指针ptr指向而已。

内存划分

  1. 栈区(stack):执行函数时,函数的局部变量都会在栈区上创建。压栈:从栈顶向下开辟空间,弹栈:从栈底向上释放空间。
  2. 堆区(heap):一般由程序员分配和释放,从堆低向上开辟空间,堆顶向下释放空间。在程序结束后也被操作系统会自动回收。
  3. 数据段(静态区):存放全局变量,静态数据。变量本在栈上创建,被static修饰后放在常量区,程序结束后由系统释放。
  4. 代码段(常量区):存放可执行代码和只读常量。

值得注意的是,字符串数组char2的内容"abcd"也是存储在栈上的,是从常量区拷贝过来的。

柔性数组

柔性数组的定义

C99中,结构中最后一个元素允许是未知大小的数组,被称为柔性数组成员。例如:

c
//1.
struct st_type {
	int i;
	int a[0];//柔性数组成员
};
//2.
struct st_type {
	int i;
	int a[];//柔性数组成员
};

柔性数组的特点

  • 结构中柔性数组成员前必须至少有一个成员。
  • sizeof计算结构所占空间时不包含柔性数组的大小。
  • 包含柔性数组的结构用malloc进行动态内存分配,且分配的内存应大于结构大小,以满足柔性数组的预期。

使用含柔性数组的结构体,需配合以malloc等动态内存分配函数。分配空间减去其他成员的大小,即为为柔性数组开辟的空间。

柔性数组的使用

malloc开辟的大小写成如图所示的形式,增加代码的可阅读性。

结构体所分配空间减去其他成员的大小,所剩即为为柔性数组开辟的空间大小,若不够还可以用realloc调整大小,以满足柔性数组“柔性”的需求。

c
struct st_type {
	int i;
	int a[0];
};
int main() {
	printf("%d\n", sizeof(struct st_type));
	//1.
	struct st_type st;
	//2.
	struct st_type* pst = (struct st_type*)malloc(sizeof(struct st_type) + 10 * sizeof(int));
	if (pst == NULL) {
		perror("pst");
		return -1;
	}
	return 0;
}

含柔性数组结构体当然不可像第一种那样使用,这样结构体变量st仅有4个字节,不包含柔性数组。

举例

c
struct st_type {
	int i;
	int a[0];
};
int main() {
	struct st_type* pst = (struct st_type*)malloc(sizeof(struct st_type) + 10 * sizeof(int));
	if (pst == NULL) {
		perror("pst");
		return -1;
	}
	pst->i = 10;
	for (int i = 0; i < 10; i++) {
		printf("%d ", pst->a[i] = i);
	}
	//调整空间大小
	struct st_type* ptr = (struct st_type*)realloc(pst, sizeof(struct st_type) + 20 * sizeof(int));
	if (ptr == NULL) {
		perror("ptr");
		return -1;
	}
	pst = ptr;
	for (int i = 10; i < 20; i++) {
		printf("%d ", pst->a[i] = i);
	}
	//释放
	free(pst);
	pst = NULL;
	return 0;
}

柔性数组的优势

柔性数组成员利用动态内存可大可小,那同样将柔性数组成员替换成指向动态开辟内存的指针也可达到同样的效果。下文将对比二者都有何优劣。(为突出对比,已省略不必要的代码)

柔性数组版本

c
struct st_type {
	int i;
	int a[];
};
int main() {
	struct st_type* pst = (struct st_type*)malloc(sizeof(struct st_type) + 10 * sizeof(int));
	for (int i = 0; i < 10; i++) {
		printf("%d ", pst->a[i] = i);
	}
	//调整空间大小
	struct st_type* ptr = (struct st_type*)realloc(pst, sizeof(struct st_type) + 20 * sizeof(int));
	pst = ptr;
	for (int i = 10; i < 20; i++) {
		printf("%d ", pst->a[i] = i);
	}
	//释放
	free(pst);
	pst = NULL;
	return 0;
}

指针版本

c
struct st_type {
	int i;
	int* pa;
};
int main() {
	struct st_type* pst = (struct st_type*)malloc(sizeof(struct st_type));
    pst->pa = (int*)malloc(10 * sizeof(int));
    for (int i = 0; i < 10; i++) {
		printf("%d ", *(pst->pa + i) = i);
	}
	//调整空间大小
    int* ptr = (int*)realloc(pst->pa, 20 * sizeof(int));
	pst->pa = ptr;
	for (int i = 10; i < 20; i++) {
		printf("%d ", *(pst->pa + i) = i);
	}
    //释放
	free(pst);
	pst = NULL;
	free(pst->pa);
	pst->pa = NULL;
	return 0;
}
  1. 从用户的角度看,函数中不应作二次内存分配(不便一次释放完毕)。
  2. 多次开辟动态内存,内存碎片增多,也增加内存泄漏的风险。内存不连续,拖慢访问速度(局部性原理)。