C语言自学--动态内存管理
目录
1、为什么要有动态内存分配
2、 malloc和free
2.1、malloc
2.2、free C语言提供了另外一个函数free,专门是用来做动态内存的释放和回收的,函数原型如下:
防止野指针
3、calloc和realloc
3.1、calloc
3.1.1、举例说明malloc和calloc的区别:
3.2、realloc
4、常见的动态内存的错误
4.1 、对NULL指针的解引用操作
4.2 对动态开辟空间的越界访问
4.3 对非动态开辟内存使用free释放
4.4 使用free释放⼀块动态开辟内存的⼀部分
4.5 对同⼀块动态内存多次释放
4.6 动态开辟内存忘记释放(内存泄漏)
4.7、补充(realloc等价于malloc)
5、动态内存经典笔试题分析
5.1、题目1
5.2、题目2
5.3、题目3
5.4、题目4
6、柔性数组
6.1、柔性数组的特点:
6.2、柔性数组的使用
7、总结C/C++中程序内存区域划分
7.1、栈区(stack)
7.2、堆区(heap)
7.3、数据段(静态区,static)
7.4、代码段
1、为什么要有动态内存分配
我们已经掌握的内存开辟方式有:
int val = 20;//在栈空间上开辟四个字节char arr[10] = {0};//在栈空间上开辟10个字节的连续空间
上述空间分配方式存在两个局限性:
- 分配的空间大小固定不变
- 数组声明时必须指定长度,且一旦确定就无法修改
然而实际编程中,我们经常需要在程序运行时才能确定所需空间大小。这种需求使得编译时静态分配的方式显得力不从心。为此,C语言引入了动态内存管理机制,允许程序员在运行时自主申请和释放内存,大大提高了内存管理的灵活性。
2、 malloc和free
-
2.1、malloc
C语言提供了一个动态内存开辟的函数:
void* malloc (size_t size);
该函数用于在内存中申请一块连续的可用空间,并返回指向该空间的指针。
特性说明:
- 成功申请时:返回指向已分配空间的指针
- 申请失败时:返回NULL指针
- 返回值类型为void*,因此使用前需要进行类型转换
- 当size参数为0时,malloc的行为标准是未定义的,函数行为取决于具体编译器实现
#include <stdlib.h>
int main()
{//申请10个整型的空间int* p = (int *)malloc(10*sizeof(int));if (p == NULL){//空间开辟失败perror("malloc");return 1;}//空间开辟成功,可以使用这40个字节int i = 0;for (i=0;i<10;i++){*(p + i) = i + 1;}return 0;
}
-
2.2、free C语言提供了另外一个函数free,专门是用来做动态内存的释放和回收的,函数原型如下:
void free(void *ptr);
free
函数用于释放之前通过malloc
、calloc
或realloc
动态分配的内存空间。释放后的内存会被系统回收,可供后续分配使用。
- 若参数
ptr
指向的空间并非动态分配,则free
函数的行为是未定义的。 - 若参数
ptr
为NULL
指针,该函数不会执行任何操作。
注意:malloc
和free
函数均声明在stdlib.h
头文件中。
不妨思考:为何要执行 p = NULL 这一操作
#include <stdlib.h>
int main()
{//申请10个整型的空间int* p = (int*)malloc(10 * sizeof(int));if (p == NULL){//空间开辟失败perror("malloc");return 1;}//空间开辟成功,可以使用这40个字节int i = 0;for (i = 0; i < 10; i++){*(p + i) = i + 1;}//释放free(p);p = NULL;return 0;
}
防止野指针
在动态内存分配中,free(p)
释放了 p
指向的内存空间,但 p
本身仍保留原来的地址值。如果不将 p
设为 NULL
,p
会成为一个野指针(Dangling Pointer),指向已释放的内存区域。通俗来说,就是p指向的空间不属于当前程序,但还是找得到这个空间,这就是成为野指针了。
3、calloc和realloc
-
3.1、calloc
C语言还提供了一个名为calloc
的函数,用于动态内存分配。其函数原型如下:
void* calloc (size_t num, size_t size);
- 分配
num
个大小为size
字节的内存空间,并且把空间的每个字节初始化为0。 - 与函数 malloc 的区别只在于 calloc 会在返回地址之前把申请的空间的每个字节初始化为全0。
参数说明:
num
:需要分配的元素数量(size_t
类型)size
:每个元素占用的字节数(size_t
类型)
返回值:
- 成功时返回指向分配内存起始地址的
void*
指针。 - 失败时返回
NULL
(如内存不足)。
3.1.1、举例说明malloc和calloc的区别:
-
#include <stdio.h> int main() {//申请10个整型的空间int* p = (int*)malloc(10 * sizeof(int));if (p == NULL){//空间开辟失败perror("malloc");return 1;}//空间开辟成功,可以使用这40个字节int i = 0;for (i = 0; i < 10; i++){printf("%d ",p[i]);//*(p+i)}//释放free(p);p = NULL;return 0; }
可以看出,malloc对申请的地址没有初始化,都是随机值,我们再来看看calloc:
-
//calloc #include <stdlib.h> #include <stdio.h> int main() {//申请10个整型的空间int* p = (int*)calloc(10 , sizeof(int));if (p == NULL){//空间开辟失败perror("malloc");return 1;}//空间开辟成功,可以使用这40个字节int i = 0;for (i = 0; i < 10; i++){printf("%d ",p[i]);//*(p+i)}//释放free(p);p = NULL;return 0; }
不难看出,calloc 会在返回地址之前把申请的空间的每个字节初始化为全0
-
3.2、realloc
- realloc函数的出现让动态内存管理更加灵活。
- 有时会我们发现过去申请的空间太小了,有时候我们又会觉得申请的空间过大了,那为了合理的时 候内存,我们一定会对内存的大小做灵活的调整。那realloc 函数就可以做到对动态开辟内存大小的调整。 函数原型如下:
void* realloc (void* ptr, size_t size);
ptr
表示待调整的内存地址(一定要是动态调整的地址才行,不能随便传个地址!!)
size
指定调整后的新内存大小
函数返回值为调整后的内存起始地址
该函数不仅会调整原有内存空间大小,还会将原内存中的数据迁移至新空间
realloc
在调整内存空间时存在两种情形:
- 原内存空间之后存在足够大的连续空间
- 原内存空间之后没有足够的连续空间
在内存扩展时存在两种不同情况:
- 情况1:若原有内存块后有足够空间,则直接追加扩展,原数据保持不变。
- 情况2:若后续空间不足,则需在堆中另寻足够大的连续空间,此时函数将返回新的内存地址。
鉴于这两种机制,使用realloc
函数时需特别注意。
#include <stdlib.h>
#include <stdio.h>
int main()
{//申请10个整型的空间int* p = (int*)calloc(10, sizeof(int));if (p == NULL){//空间开辟失败perror("malloc");return 1;}//使用空间int i = 0;for (i = 0; i < 10; i++){printf("%d ", p[i]);//*(p+i)}//扩容,希望调整为20个字节的整型空间int *ptr = (int *)realloc(p,20*sizeof(int));if (ptr != NULL){p = ptr;//如果修改成功,那么指针起始地址就和原来一样,//反之,修改失败上面返回的就是空指针给ptr,// 然后单独复制一份到堆区域,并释放掉原来申请的空间,所以起始地址就和原来不一样}//使用// ...//释放free(p);p = NULL;return 0;
}
4、常见的动态内存的错误
-
4.1 、对NULL指针的解引用操作
-
#include <stdio.h>int main() {int*p = (int *)malloc(10 * sizeof(int));//if (p ==NULL)//{//perror("malloc");//return 1;//}int i = 0;for (i = 0; i < 10; i++){p[i] = i;//*(p+i)//如果p的值是NULL,就会有问题}free(p);p = NULL;return 0; }
-
4.2 对动态开辟空间的越界访问
-
void test() {int i = 0;int* p = (int*)malloc(10 * sizeof(int));if (NULL == p){exit(EXIT_FAILURE);}for (i = 0; i <= 10; i++){*(p + i) = i;//当i是10的时候越界访问}free(p); }
-
4.3 对非动态开辟内存使用free释放
-
void test() {int a = 10;int* p = &a;free(p);//ok? }
free()
函数只能用于释放通过malloc
、calloc
或realloc
等动态内存分配函数分配的堆内存。而变量a
是局部变量,其内存是在栈上自动分配的,不属于堆内存范畴。
-
-
4.4 使用free释放⼀块动态开辟内存的⼀部分
-
void test() {int* p = (int*)malloc(100);p++;free(p);//p不再指向动态内存的起始位置 }
-
4.5 对同⼀块动态内存多次释放
-
void test() {int *p = (int *)malloc(100);free(p);free(p);//重复释放 }
-
4.6 动态开辟内存忘记释放(内存泄漏)
-
void test() {int *p = (int *)malloc(100);if(NULL != p) {*p = 20; } } int main() {test();while(1); }
- 忘记释放不再使⽤的动态开辟的空间会造成内存泄漏。 切记:动态开辟的空间⼀定要释放,并且正确释放。
4.7、补充(realloc等价于malloc)
当 realloc
的第一个参数为 NULL
时,其行为与 malloc
完全一致
-
int main() {//正常用法//int* p = (int*)malloc(20);//realloc(p, 40);//malloc==reallocint* p = (int*)realloc(NULL, 40);//==malloc(40)if (p == NULL){//....return 1;}free(p);p = NULL;return 0; }
5、动态内存经典笔试题分析
-
5.1、题目1
-
void GetMemory(char *p){p = (char *)malloc(100);}void Test(void){char *str = NULL;GetMemory(str);strcpy(str, "hello world");printf(str);}
运行Test
函数会有什么样的结果?
-
问题分析
运行 Test
函数会导致程序崩溃或未定义行为。以下是具体原因和过程:
-
原因1:指针传递问题
GetMemory
函数的参数 p
是一个指针,但传递方式是值传递。当 str
作为参数传入时,p
是 str
的副本,而非 str
本身。因此,malloc
分配的内存地址赋值给 p
后,str
仍然为 NULL
。
-
原因2:解引用空指针
strcpy(str, "hello world")
尝试向 str
(仍为 NULL
)写入数据,导致解引用空指针,触发段错误(Segmentation Fault)或访问违规。
-
修正方法
-
方法1:传递指针的指针
void GetMemory(char **p) {*p = (char *)malloc(100);
}
void Test() {char *str = NULL;GetMemory(&str); // 传递str的地址strcpy(str, "hello world");printf("%s", str);free(str); // 释放内存
}
-
方法2:返回动态分配的内存
char* GetMemory() {return (char *)malloc(100);
}
void Test() {char *str = GetMemory();strcpy(str, "hello world");printf("%s", str);//printf(ptr);//打印字符串相当于传字符串的首地址==printf("hehe\n");free(str);
}
注意事项
- 动态分配的内存需手动释放,避免内存泄漏。
- 直接操作未初始化的指针或空指针是危险行为,必须确保指针有效后再使用。
-
5.2、题目2
char *GetMemory(void){char p[] = "hello world";return p;}
void Test(void){char *str = NULL;str = GetMemory();printf(str);}
GetMemory()
函数中定义的数组p
是局部变量,存储在栈内存中。当函数返回时,栈帧被销毁,该数组占用的内存被系统回收。虽然返回了数组首地址p
,但该地址指向的内存内容已不再有效。
Test()
函数中将返回的地址赋值给str
并尝试打印,此时访问的是已被释放的栈内存。这种操作属于未定义行为(Undefined Behavior),可能导致以下几种结果:
- 程序崩溃(最常见情况)
- 输出乱码或错误内容
- 看似正常输出"hello world"(残留数据未覆盖)
- 其他不可预测行为
- 使用静态存储
char* GetMemory(void) {
//void 表示该函数不接受任何参数。这是一种明确的写法,表明调用该函数时不需要传入任何参数。static char p[] = "hello world";return p;
}
-
动态内存分配
char* GetMemory(void) {char* p = malloc(12);strcpy(p, "hello world");return p;
}
// 需记得在调用后free
- 传入缓冲区
void GetMemory(char* buf, size_t size) {strncpy(buf, "hello world", size);
}
-
5.3、题目3
-
void GetMemory(char **p, int num) {*p = (char *)malloc(num); } void Test(void) {char *str = NULL;GetMemory(&str, 100);strcpy(str, "hello");printf(str); }
原代码能正确运行并输出"hello",但存在内存泄漏和未检查分配失败的风险。分配的内存未被释放,导致内存泄漏。虽然程序能正确运行输出,但每次调用Test都会泄露100字节内存。
-
-
void Test(void) {char* str = NULL;GetMemory(&str, 100);strcpy(str, "hello");printf(str);free(str);str = NULL; }
-
5.4、题目4
-
#include <stdio.h> #include <string.h> void Test(void) {char* str = (char*)malloc(100);strcpy(str, "hello");free(str);if (str != NULL){strcpy(str, "world");printf(str);} }int main() {Test();return 0; }
Test函数中存在以下几个关键问题:
- 内存释放后未置空指针:
free(str)
后,str
指针仍然指向原来的内存地址,但该内存已被释放,属于悬空指针(dangling pointer)。 - 释放后访问内存:
if (str != NULL)
的条件判断无效,因为free
不会自动将指针置为NULL
,此时str
仍为非NULL
的悬空指针。 - 释放后写入内存:
strcpy(str, "world")
会尝试向已释放的内存写入数据,属于未定义行为(Undefined Behavior, UB)。 -
free(str); str = NULL; // 避免悬空指针 if (str != NULL) // 此时条件为假,不会进入分支
6、柔性数组
柔性数组(Flexible Array Member)是C99标准引入的特性,允许结构体的最后一个成员是未指定大小的数组。这种数组不占用结构体本身的内存空间,仅为动态分配内存提供便利的访问方式。
struct S
{int n;char c;double d;int arr[];//未知大小的数组,arr就是柔性数组的成员
};//有些编译器会报错⽆法编译可以改成:
struct S
{int n;char c;double d;int arr[0];//未知大小的数组,arr就是柔性数组的成员
};
-
6.1、柔性数组的特点:
- 结构中的柔性数组成员前⾯必须至少⼀个其他成员。
- sizeof返回的这种结构大小不包括柔性数组的内存。
- 包含柔性数组成员的结构用malloc()函数进行内存的动态分配,并且分配的内存应该大于结构的大小,以适应柔性数组的预期大小。
-
#include <stdio.h> struct S {int n;//4int arr[]; };int main() {printf("%d",sizeof(struct S));return 0; }
-
#include <stdio.h> struct S {int n;//4int arr[]; };int main() {//malloc(sizeof(struct S) + 20*sizeof(int));// 4字节 80字节,为了适应柔性数组//该怎么接收malloc的返回值呢,用结构体指针struct S *ps = (struct S *)malloc(sizeof(struct S) + 20 * sizeof(int));return 0; }
-
6.2、柔性数组的使用
-
#include <stdio.h> #include <stdlib.h> struct S {int n;//4int arr[]; };int main() {//malloc(sizeof(struct S) + 20*sizeof(int));// 4字节 80字节,为了适应柔性数组//该怎么接收malloc的返回值呢,用结构体指针struct S *ps = (struct S *)malloc(sizeof(struct S) + 20 * sizeof(int));if (ps == NULL){perror("malloc");return 1;}//使用这些空间ps->n = 100;int i = 0;for (i = 0; i < 20; i++){ps->arr[i] = i + 1;}//调整ps指向空间的大小struct S* ptr = (struct S*)realloc(ps, sizeof(struct S) + 40 * sizeof(int));if(ptr !=NULL){ps = ptr;//ps指向新追加的空间}else{return 1;//调整失败,结束下来}for (i = 0; i < 20; i++){ps->arr[i] = i + 1;}//使用for (i = 0; i < 40; i++){printf("%d\n", ps->arr[i]);//前20个是1-20,后20个是随机值}//释放空间free(ps);ps = NULL;return 0; }
柔性数组的优势
- 内存连续:结构体和数组成员在内存中连续分布,减少内存碎片,提升访问效率。
- 简化释放:只需一次
free
即可释放全部内存,避免指针嵌套导致的多次释放。 - 节省空间:相比指针成员+动态数组的方式,省去了指针本身的存储开销。
7、总结C/C++中程序内存区域划分
-
7.1、栈区(stack)
在函数执行时,局部变量、函数参数、返回数据、返回地址等存储在栈区。栈区的内存分配和释放由编译器自动完成,效率高但容量有限。函数执行结束时,栈上的存储单元自动释放。
-
7.2、堆区(heap)
由程序员手动分配和释放,如使用malloc
、calloc
、new
等动态分配内存。若未释放,程序结束时可能由操作系统回收。堆区分配方式类似链表,容量较大但管理复杂。
-
7.3、数据段(静态区,static)
存放全局变量和静态变量(如static
修饰的局部变量或全局变量)。程序结束后由系统自动释放,生命周期贯穿整个程序运行期间。
-
7.4、代码段
存储函数体(包括类成员函数和全局函数)的二进制代码,通常是只读区域,防止程序意外修改指令。