一.动态内存函数

1.1 malloc

作用:开辟动态内存空间

・如果开辟成功,则返回一个指向开辟好空间的指针。

・如果开辟失败,则返回一个NULL指针,因此malloc的返回值一定要做检查。

头文件:#include<stdlib.h>

格式:void*malloc(size_t size)

#include <errno.h>
#include <string.h>
#include <stdlib.h>
#include <stdio.h>

int main()
{
    int arr[10] = {0};//两者都申请了40个字节
    int*p = (int*)malloc(40);
    if (p == NULL)
    {
        printf("%s\n", strerror(errno));//当malloc开辟失败时报错
    }
int i;
for(i=0;i<10;i++)
{
*(p+i)=i;
}
for(i=0;i<10;i++)
{
printf("%d",*(p+i));
}
    return 0;
}
//开辟失败的可能原因:开辟内存过大,没有足够的内存

不用assert()函数的原因:

assert调试断言,作用:

  • 表达式为假 → 直接终止程序
  • 只在 Debug 模式生效
  • Release 模式会被直接删掉

它的目的是:开发阶段快速发现 bug,不是处理运行时错误

malloc的好处:堆区可以调整申请的空间,而栈区的空间是不能调整的

拓展:变长数组(内存大小还是不会改变)x

int n = 5;
scanf("%d", &n);
int arr[n];   // ✅ 变长数组,C99 支持

而普通数组

int n = 5;
int arr[10];  
int arr[n];   // 老式C语言不行

1.2 free

作用:释放动态分配的内存空间,必须配合 malloc/calloc/realloc 使用。

使用free()函数的原因:

  1. 归还内存给系统你用 malloc 申请的堆内存,不会自动释放,必须手动调用 free 归还。
  2. 防止内存泄漏不调用 free → 内存一直被占用 → 程序运行越久,占用内存越多 → 最后崩溃。
  3. 让指针可以重复使用释放后,这块内存就不再属于你的程序了。

使用实例:

free(p);
p=NULL;

后面要加上p=NULL的原因:free(p)后将40个字节的还给操作空间,但p的地址还没发生变化,p还能找到这个内存区域,可是这个内存区域已经还给操作系统,这样访问p的话,p就成了野指针,不安全,所以还要加上p=NULL。

上图代码中的free函数什么事都不干。

1.3 calloc

头文件:#include<stdlib.h>

格式:void*calloc(size_t num,size_t size)

例:

int main()
{
int*p=(int*)calloc(10,sizeof(int));
if(p==NULL)
{
printf("%s",strerror(errno));
return 1;
}
int i;
for(i=0;i<10;i++)
{
printf("%d",*(p+i)));
}
return 0;
}

该函数的返回值就是开辟空间的起始地址,并且在返回之前将这个空间初始化成0

1.4 malloc和calloc的选择

  • 当我们想初始化时,用calloc
  • 不想初始化时,用malloc

可理解为:calloc=malloc+memset

1.5 realloc

作用:可对动态开辟内存大小进行调整

格式:void*realloc(void*ptr,size_t size)

ptr是要调整空间的起始地址

size是调整之后的新大小

realloc函数作用原理:

当我们想扩容时会碰见两种情况

  • 情况一:开辟的动态内存如果再往后开辟40个字节会覆盖后面原有的数据

这么做是不可取的,所以realloc函数会在堆区找一个空的内存开辟80个字节,然后将原有的40个字节内存拷贝过来,释放旧的空间,然后返回新的地址

  • 情况二:开辟的动态内存后面还有多余的空间能容纳新增的内存

则realloc还会返回旧的地址

使用实例:

int main()
{
    int* p = (int*)malloc(40);
    if (NULL == p)
    {
        printf("%s\n", strerror(errno));
        return 1;
    }
    
    int i = 0;
    for (i = 0; i < 10; i++)
    {
        *(p + i) = i + 1;
    }
    
   int*prt=(int*)realloc(p,80);
(这里不直接用p=realloc(p,80)是因为防止设置的内存太大,找不到空区域开辟新的内存,导致p变为空指针了)
if(prt!=NULL)
{
p=prt;
}

for(i=0;i<10;i++)
{
printf("%d ",*(p+i));
}
free(p);
p=NULL;
    return 0;
}

realloc(NULL,40)和malloc(40)是等效的

二.常见的动态内存错误

2.1 对NULL指针的解引用操作

错误示例:

int*p=(int*)malloc(40);
*p=20;
free(p);

若p为NULL,则此时就会导致问题,所以我们在给*p赋值之前,应加以判断,即:

if(p==NULL)
{
return 1;
}

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

错误示例:


    int* p = (int*)malloc(40);
    if (p == NULL) 
    {
        printf("%s\n", strerror(errno));
        return 1;
    }   
    int i = 0;
    for (i = 0; i <= 10; i++)
    {
        p[i] = i; //此处进行了越界访问
    }
    free(p);
    p = NULL;  

只开辟了40个字节的动态内存,却使用了44个字节的空间,这属于越界访问

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

错误示例:

int a=10;
int *p=&a;
free(p);

free只能释放动态开辟的内存,是无法释放栈区内存的

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

错误示例:

  int* p = (int*)malloc(40);
    if (p == NULL)
    {
        return 1;
    }   
    int i = 0;
    for (i = 0; i < 5; i++)
    {
        *p = i;
        p++;
    }
    free(p);
    p = NULL;

这里的p++导致p的起始位置发生变化,free(p)就会释放从p的最终地址到开辟内存结尾的空间,但这种情况是不允许的,会导致程序崩溃,所以应该改为如下代码:

 for (i = 0; i < 5; i++)
    {
        *(p+i) = i;
    }

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

错误示例:

int*p=(int*)malloc(40)
...
free(p);
...
free(p);

对同一块空间的多次释放会导致程序崩溃,并且为了防止p为野指针,在free释放后,应该将p设置为NULL,即:

free(p);
p=NULL;
free(p);

这样,即使再次释放p也不会发生任何事情

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

错误示例一:

   void test()
{
    int* p = (int*)malloc(100);
    // ...
    int flag = 0;
    scanf("%d", &flag);
    if (flag == 5)
        return; 

    free(p);
    p = NULL;
}

int main()
{
    test();
    // ...(main函数其他逻辑)
    return 0;
}

在执行free函数之前就已经已经返回,导致开辟的动态内存还没来得及释放,那么给变量p的100个字节大小的空间就再也无法找到了,造成了内存泄漏

错误示例二:

int* test()
{
    int* p = (int*)malloc(100);
    if (p == NULL)
    {
        return p;
    }
    return p;
}

int main()
{
    // 调用 test(),接收其返回的动态内存指针,存入 ret
    int* ret = test();
    // 忘记释放了
    // 此处没有执行 free(ret); 释放内存
    return 0;
}

这也会造成内存泄漏

三.常见的错误代码

3.1题目一

错误代码:

void GetMemory(char* p)
{
    p = (char*)malloc(100);
}
void Test(void)
{
    char* str = NULL;
    GetMemory(str);//p出了函数后就被销毁了,发生了内存泄漏
    strcpy(str, "hello world");从strcpy的内部逻辑可知,当str是NULL时,解引用时会崩溃
    printf(str);
}

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

补充:上述代码将NULL传给p后,p=(char*)malloc(100)会使p发生改变,为开辟空间的起始地址

而开辟内存后,用p是否为NULL来判断开辟成功与否,是因为若开辟失败(int*)malloc(100)会返回NULL,并不是p为NULL就无法开辟内存

改正后的代码:

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

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

    // 打印 str 指向的字符串
    printf(str);
 
    free(str);
    str = NULL;
}

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

对printf()函数进行解释:

printf 函数原型:

int printf(const char *format, ...);

printf 的第一个参数,本身就要求是一个「字符串地址」

str 本身就是一个 char* 类型,完全符合 printf 第一个参数的要求。

但平时不推荐这样写,因为不安全、不规范

3.2题目二

错误代码:

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

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

问题:

  • GetMemory 函数执行结束后,栈帧会被销毁,p 指向的内存就被释放了。
  • 此时 Test 函数里的 str 得到的是一个野指针,指向了已经失效的栈内存。
  • 后续 printf(str) 会导致未定义行为(可能乱码、崩溃,或者偶尔输出 “hello world”)。

注:函数返回值不是绝对不能与局部变量有关,而是有严格限制

  1. 完全安全、正确的用法:                                                                                                         
    int add(int a, int b)
    {
        int sum = a + b;  // sum 是局部变量
        return sum;       // ✅ 返回值,没问题
    }
      原理:返回时会拷贝一份值给调用者,局部变量销毁也不影响。                                    
  2. 绝对禁止:返回局部变量的地址(指针)
char *GetMemory(void)
{
    char p[] = "hello";  // 栈上局部数组
    return p;            // ❌ 返回 p 的地址(指针)
}

原理:

  • 局部变量(数组、变量)存在栈内存
  • 函数一结束,栈内存立刻被系统回收
  • 你返回的指针指向的是已经被回收的无效内存(野指针)
  • 后续使用这个指针 = 程序崩溃 / 乱码 / 未知行

3.3题目三

错误代码:

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;
}

当free(str)后,str所存放的hello的地址已经还给操作系统了,而且str不为NULL,就进入了if语句,

此时将"world"强行放进str中就形成了非法访问,str为野指针。

3.4拓展:内存区域的分布

四.柔性数组

定义:结构体中的最后一个元素允许是未知大小的数组,这就叫做【柔性数组】成员。

例:

struct st_type
{
   int i;
   int a[];//柔性数组成员
}

也可写成int a[0],这里的0就表示不知道数组里有几个成员

4.1柔性数组的特点

• 结构中的柔性数组成员前面必须至少一个其他成员

即上面代码中的  int  i

• sizeof 返回的这种结构大小不包括柔性数组的内存。

struct s
{ int n;
  int arr[];
};
int main()
{ int sz=sizeof(struct s);
  printf("%d",sz);//4
  return 0;
}

sizeof只计算了int n的内存,没有计算int arr[]的内存

• 包含柔性数组成员的结构用 malloc() 函数进行内存的动态分配,并且分配的内存应该大于结构的大小,以适应柔性数组的预期大小。

错误用法:

struct s
{ int n;
  int arr[];
};
int main()
{ struct s m;
  return 0;
}

当结构体存在柔性数组时,不能这样创建变量

正确用法一:

struct s
{ int n;
  int arr[];
};
int main()
{ 
struct s* ps=(struct s*)malloc(sizeof(struct s)+40);
if(ps==NULL)
{ return 1;}
ps->n=100;
int i=0;
for(i=0;i<10;i++)
{
ps->arr[i]=i;
}
struct s* ptr=(struct s*)realloc(ps,sizeof(struct s)+80);
if(ptr!=NULL)
{
 ps=ptr;
 ptr=NULL;//这里不释放ptr是因为ps和ptr指向的是同一块空间,如果释放了ptr,那么ps也会被释放。
}
free(ps);
ps=NULL;
return 0;
}

正确用法二:

struct s
{ int n;
  int *arr;
};
int main()
{ 
struct s* ps=(struct s*)malloc(sizeof(struct s));//这里是为了将结构体的成员都放在堆上
if(ps==NULL)
{ 
return 1;
}
ps->n=100;
ps->arr=(int*)malloc(40);//可理解为一个数组,arr为数组名
if(ps->arr==NULL)
{ 
return 1;
}
int i=0;
for(i=0;i<10;i++)
{
ps->arr[i]=i;//
}
struct s* ptr=(struct s*)realloc(ps,sizeof(struct s));
if(ptr!=NULL)
{
 ps=ptr;
 ptr=NULL;//这里不释放ptr是因为ps和ptr指向的是同一块空间,如果释放了ptr,那么ps也会被释放。
}
free(ps);
ps=NULL;
return 0;
}

法一和法二ps->arr的区别:

                                          法二                                                                  法一

法二是通过int*arr来达到与int arr[]有相同的作用

对方法二中ps->arr[i]=i;进行解析:

在C语言中有一个关键的等价关系:a[i] 等价于 *(a + i)//这里的*是解引用符号

ps->arr指的就是结构体中的int*arr

  • ps->arr[i]值,arrint* 类型时,arr[i] 是一个 int 类型的值!
  • ps->arr + i地址

Logo

openEuler 是由开放原子开源基金会孵化的全场景开源操作系统项目,面向数字基础设施四大核心场景(服务器、云计算、边缘计算、嵌入式),全面支持 ARM、x86、RISC-V、loongArch、PowerPC、SW-64 等多样性计算架构

更多推荐