目录

1. 为什么要有动态内存分配

2. malloc和free

2.1 malloc - 分配未初始化的内存

2.2 free - 释放动态内存

3. calloc和realloc

3.1 calloc - 分配并初始化为零的内存

3.2 realloc - 调整已分配内存的大小

4. 常见的动态内存的错误

5. 题目练习

6.柔性数组

7.总结C/C++中程序内存区域划分


1. 为什么要有动态内存分配

在 C/C++ 编程中,程序的内存主要分为以下几个区域:

  • 栈区(stack):存放局部变量、函数参数等,由编译器自动分配和释放。

  • 静态区(static):存放全局变量、静态变量,程序结束时释放。

  • 堆区(heap):由程序员手动分配和释放,也就是动态内存分配的核心区域。

静态分配和自动分配的局限性:

  1. 大小固定,缺乏灵活性
    数组长度必须在编译时确定(C99 之前),或者即便支持变长数组(VLA),其大小也受限于栈空间,无法在运行时随意调整。

  2. 生命周期受限于作用域
    局部变量在函数返回后即销毁,全局变量则全程占用内存,不能按需创建和释放,造成内存浪费。

  3. 栈空间有限
    栈空间通常只有几 MB,分配大块内存(如大数组、图像数据)极易导致栈溢出。

  4. 程序真正需要的数据量往往运行时才能确定
    例如读取用户输入、网络数据流、文件内容、动态数据结构(链表、树、图)等,大小事先未知。

动态内存分配正是为了解决上述问题:

  • 上申请任意大小的内存块,

  • 生命周期由程序员控制,按需分配,及时释放

  • 可随时调整大小(通过 realloc),

  • 适合构建灵活的数据结构。


C语言标准库提供四个动态内存管理函数,声明在<stdlib.h>

2. malloc和free

2.1 malloc - 分配未初始化的内存

void* malloc(size_t size);

malloc

功能

向内存的堆区申请一块连续可用的空间,并指向这块空间的起始地址
参数 size:要分配的内存块的字节数
返回值

开辟成功返回这块空间的起始地址

开辟失败返回NULL指针所以malloc的返回值一定要检查

注意事项

1.内存中的内容是未初始化的(随机值)

2.若size为0,malloc的行为是标准未定义的,取决于编译器

int* p = (int* )malloc(20);//在堆区上申请20个字节的空间
if (p == NULL)
{
	perror("use malloc:");
	return 1;
}

2.2 free - 释放动态内存

void free(void* ptr);

free
功能 释放之前通过动态内存分配函数(如malloc,calloc,realloc)开辟的空间
参数

ptr:指向要释放的内存块的指针

  • 如果ptr是NULL则不做任何事
  • 如果ptr指向的空间不是动态开辟的,则free函数的行为是未定义的
注意事项 释放空间后应立即将指针置为NULL
int main()
{
	int* p = (int* )malloc(20);//在堆区上申请20个字节的空间
	if (p == NULL)
	{
		perror("use malloc:");
		return 1;
	}
	
	free(p);//释放内存
	p = NULL;//置为空

	return 0;
}

3. calloc和realloc

3.1 calloc - 分配并初始化为零的内存

void* calloc(size_t num,size_t size);

calloc

功能

分配 num*size 字节的内存,并把每一位都初始化为0
参数

size:每个元素的大小

num:元素个数

返回值

开辟成功返回这块空间的起始地址

开辟失败返回NULL指针

calloc初始化为0:

malloc初始化为随机值:

calloc使用举例:


int main()
{
    //使用calloc开辟空间
	int* p = (int*)calloc(5, sizeof(int));
	//检查
    if (p == NULL)
	{
		perror("use calloc");
		return 1;
	}
	//使用
	int i = 0;
	for (i = 0; i < 5; i++)
	{
		p[i] = i + 1;
	}
    //释放
	free(p);
	p = NULL;
}

3.2 realloc - 调整已分配内存的大小

void* realloc(voidptrsize_t new_size);

realloc
功能 重新调整之前分配的内存块的大小,它可以在不丢失原有数据的情况下,扩大或缩小动态分配的内存块
参数

ptr:是要调整的内存空间的起始地址,如果ptr是NULL指针,realloc功能相当于malloc函数

new_size:调整之后新的大小,单位是字节

返回值

成功返回一个指向重新分配的内存块的void*类型指针。这个指针可能与原来的指针不同

失败:如果内存重新分配失败,返回NULL,并且原来的内存块保持不变

注意

realloc开辟空间示有两种情况:

1.要拓展的内存在原来内存后直接追加空间返回的地址还是旧地址

2.原来空间不够大,会在内存的堆区寻找新的满足要求的空间返回新的起始地址,此时会:

          ▪ 寻找新的满足要求的空间

          ▪ 将旧空间的数据拷贝到新空间,保证数据不会丢失

          ▪ 释放旧空间,返回新空间的起始地址

//用realloc开辟新空间
int* p2 = (int*)realloc(p1, 10 * sizeof(int));//p1是原来空间的地址
if (p2 == NULL)
{
	perror("realloc");
	return 1;
}
else
{
	p1 = p2;//p1的地址变为新空间地址,旧的空间会自动释放
	p2 = NULL;
}

注意以下事项:


4. 常见的动态内存的错误

1 - 对NULL指针的解引用操作

void test()
{
	int* p = (int*)malloc(100*INT_MAX);
	*p = 20;//p的值是NULL,就会有问题
	free(p);
	p = NULL;
}

2 - 对动态开辟空间的越界访问

void test()
{
	int i = 0;
	int* p = (int*)malloc(10 * sizeof(int));
	if (p == NULL)
	{
		return 1;
	}
	for (i = 0;i <= 10;i++)
	{
		p[i] = i + 1; //当i是10的时候越界访问
	}
	free(p);
	p = NULL;
}

3 - 对非动态开辟内存使用free释放

 
int main()
{
	int a = 10;
	int* p = &a;

	free(p);//err,free只能释放由 malloc,calloc,realloc 动态分配的内存
	p = NULL;

	return 0;
}

4 - 使用free释放一块动态开辟内存的一部分

void test()
{
	int* p = (int*)malloc(100);
	p++;//p本身发生了变化
	free(p);//p不再指向动态内存的起始地址
}

5 - 对同一块动态内存多次释放

void test()
{
	int* p = (int*)malloc(100);
	free(p);
	free(p);//重复释放
}

6 - 动态开辟内存忘记释放(内存泄漏)

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

    if (NULL != p)
    {
        *p = 20;
    }
}

int main()
{
    test();
    //...   没有释放内存
    while (1);
    return 0;
}

注:动态申请的内存空间,应该用free释放,若果没用free释放,程序结束时,也会被操作系统回收


5. 题目练习

1 - 下面代码有什么问题

void GetMemory(char* p)
{
    p = (char*)malloc(100);
}

void Test(void)
{
    char* str = NULL;
    GetMemory(str);
    strcpy(str, "hello world");
    printf(str);//str仍然是NULL
}

int main()
{
    Test();
    return 0;
}

解析:

1.GetMemory(str);无法改变str的值(p是形参

2.p开辟空间后没释放


修改上述代码:

1.使用二级指针(传递指针的地址)

void GetMemory(char** p)
{
    *p = (char*)malloc(100);
}

void Test(void)
{
    char* str = NULL;
    GetMemory(&str);
    strcpy(str, "hello world");
    printf(str);

    free(str);
    str = NULL;
}

2.通过返回值传递

char* GetMemory(char* p)
{
    p = (char*)malloc(100);
    return p;
}

void Test(void)
{
    char* str = NULL;
    str = GetMemory(str);
    strcpy(str, "hello world");
    printf(str);

    free(str);
    str = NULL;
}

总结:C语言函数传参永远传的是值的副本。想要修改函数外部的变量,必须传入该变量的地址(即一级指针修改变量,二级指针修改一级指针)。


2 - 下面代码有什么问题

char* GetMemory(void)
{
    char p[] = "hello world";//返回了局部变量的地址,导致野指针
    return p;
}

void Test(void)
{
    char* str = NULL;
    str = GetMemory();//str是野指针
    printf(str);
}

int main()
{
    Test();
    return 0;
}

解析:

这段代码的核心问题是返回了局部变量的地址(栈内存被收回)

char p[] = "hello world";

栈内存是随函数生命周期自动管理的。当 GetMemory 函数执行完毕并返回时,这块栈内存会被立即释放(销毁),系统会将这块区域标记为“可用”,随时可能被其他函数调用覆盖掉。


修改方法:

1.static修饰

char* GetMemory(void)
{
    static char p[] = "hello world";
    return p;
}

2.直接返回字符串字面量的指针

char* GetMemory(void)
{
    char *p = "hello world"; // 常量字符串,指向静态区,而不是栈上的数组
    return p; // 安全
}

3.动态内存开辟

char* GetMemory(void)
{
    char* p = (char*)malloc(100);
    if (p) 
        strcpy(p, "hello world");
    return p; // 堆内存不会随函数销毁,但需要手动 free
}

void Test(void)
{
    char* str = GetMemory();
    printf(str);
    free(str);
}

int main()
{
    Test();
    return 0;
}

总结:不要返回指向“局部栈上数组”的指针。


3 - 下面代码有什么问题

void Test(void)
{
    char* str = (char*)malloc(100);
    strcpy(str, "hello");
    free(str);//释放了内存
   

    if (str != NULL)//但是str本身的值没有改变,它仍然存储着那块已释放内存的地址,换言之,str变成了野指针
    {
        strcpy(str, "world");//对野指针的访问,非法访问内存
        printf(str);
    }
}

int main()
{
    Test();
    return 0;
}

解析:

1.free(str) 只是把堆内存归还给系统,但指针变量 str 本身的值(那块内存的地址)没有被改变。此时 str 变成了野指针(悬空指针)。

修改:

free(str) 后立即加上str = NULL;

总结:释放 ≠ 清空数据


6.柔性数组

C99中,结构体中有多个成员的情况下,最后一个元素允许是未知大小的数组,这就叫“柔性数组”成员,如:

struct st_type
{
        int i;
        int a[0];//柔性数组成员
        //也可以写成:
        int a[];
};

特点:

  • 结构体中不能只有一个成员
  • sizeof返回的这种结构大小不包括柔性数组的内存
  • 包含柔性数组成员的结构用malloc()函数进行内存的动态分配,并且分配的内存应该大于结构的大小,以适应柔性数组的预期大小
struct st_type
{
	int i;
	int a[];
};

int main()
{
	printf("%d ", sizeof(struct st_type));//4
	return 0;
}

柔性数组的使用:

typedef struct st_type
{
	int i;
	int arr[];//柔性数组
}type_a;

int main() 
{
	type_a* pa = (type_a*)malloc(sizeof(type_a) + 5 * sizeof(int));
	if (pa == NULL)
	{
		perror("malloc");
		return 1;
	}
	
	pa->i = 100;
	for (int i = 0;i < 5;i++)
	{
		pa->arr[i] = i + 1;
	}

	free(pa);
	pa = NULL;
	return 0;
}

柔性数组的优势:

如果不用柔性数组,代码是这样写的:

typedef struct st_type
{
	int i;
	int* arr;
}type_b;

int main()
{
	type_b* pb = (type_b*)malloc(sizeof(type_b));
	if (pb == NULL)
	{
		perror("malloc");
		return 1;
	}

	pb->i = 100;
	pb->arr = (int*)malloc(20);
	if (pb->arr == NULL) 
	{
		perror("malloc arr");
		free(pb);   // 内部失败也要释放结构体
		return 1;
	}
	for (int i = 0;i < 5;i++)
	{
		pb->arr[i] = i + 1;
	}

	free(pb->arr);//释放时要释放两次
	free(pb);
	pb = NULL;
	return 0;
}

总结:柔性数组(int arr[]):必须作为结构体最后一个成员,且前面必须有其他成员,开辟空间时只用一次。

           普通指针成员(int *arr):内存分成两块(结构体一块,数据一块),缓存性能差,且释放时必须手动先后释放两次


7.总结C/C++中程序内存区域划分

Logo

DAMO开发者矩阵,由阿里巴巴达摩院和中国互联网协会联合发起,致力于探讨最前沿的技术趋势与应用成果,搭建高质量的交流与分享平台,推动技术创新与产业应用链接,围绕“人工智能与新型计算”构建开放共享的开发者生态。

更多推荐