为什么存在动态内存分配 

关于C语言我们大部分人都掌握的内存开辟方式有:

//1.创建变量
int a=20;//在栈空间上开辟四个字节 局部变量
int g_a=100//在静态区开辟4个字节 全局变量
char arr[10]= {0}//在栈空间上开辟10个字节的连续空间

内存类型分布如下:

C语言-动态内存管理(一)_malloc

但是上述开辟空间的方式有两个特点:

1.空间开辟大小是固定的。

2.数组在声明的时候,必须指定数组的长度,它所需要的内存在编译时分配。

但是对于空间的需求,不仅仅是上述的情况。有时候我们需要的空间大小在程序运行的时候才能知道,那数组的编译时开辟空间的方式就不能满足了,这时候就只能试试动态内存开辟了。

假设我们需要一个数组 来存储班级的学生信息

struct S{
    char name[20];
    int age;
};
int main(){
    struct  S arr[50];
    return 0;
    
}

那么这样的话如果有的班级有60人有的班级有30人这样是不满足我们日常生活中需求的。所以就需要一个动态可变大小的数组,根据你的所需分配大小,这样是最合理的。而动态内存分配就是做这个工作的。

当然有没有人会想这样操作:

struct S{
    char name[20];
    int age;
};
int main(){
    int n;
    scanf("%d",&n);
    struct  S arr[n];
    return 0;
    
}

这样可以吗?,这种写法在某些编译器是可以的,C99中增加了这种写法。gcc是支持C99的也就是用linux开发得时候用gcc编译器你是可以这样写。但是用的很少。称之为变长数组

但是有些编译器是不支持的,所以没有跨平台可言。



动态内存函数的介绍

C语言给我们提供了一些有关动态内存的函数,在stdlib.h中


malloc

 C语言提供了一个动态内存开辟的函数:

void* malloc (size_t size);

在cplusplus.com中的描述如下:

C语言-动态内存管理(一)_动态内存_02

分配内存块, 分配一个size字节的内存块,返回指向块开头的指针。并且新分配的内存块的内容没有初始化,留有不确定的值。如果size为零,返回值取决于特定的库实现(它可能是也可能不是空指针),但返回的指针不应被解引用。

int main(){
    //向内存申请10个整形的空间
   int *p= (int*) malloc(10*sizeof (int ));
	return 0;
}

动态内存开辟,方式其实很简单。就上面这样开辟。

但是malloc 是有可能开辟空间失败的,如果没有足够的内存可用,就会返回一个NULL指针

int main(){
    //向内存申请10个整形的空间
    int *p= (int*) malloc(10*sizeof (int ));
    if(p==NULL){
        printf("%s\n",strerror(errno));
    }else{
        //成功申请
        int i=0;
        for (i = 0; i < 10; ++i) {
            *(p+i)=i;
        }
        for (i = 0; i < 10; ++i) {
            printf("%d",*(p+i));
        }
    }
    return 0;
}

C语言-动态内存管理(一)_动态内存_03

这样我们把申请到的空间,进行初始化,然后打印输出了

然后我们试一下没够足够空间可以用的情况

int main(){
        int *p= (int*) malloc(10*INT_MAX);
    if(p==NULL){
        printf("%s\n",strerror(errno));
    }else{
        //成功申请
        int i=0;
        for (i = 0; i < 10; ++i) {
            *(p+i)=i;
        }
        for (i = 0; i < 10; ++i) {
            printf("%d",*(p+i));
        }
    }
    return 0;

}

INT_MAX的值为:2147483647

#define INT_MAX         2147483647    
/* max value for an int */

定义在limits.h中。

但即使是这样有些还是会申请到,所以我们让其在*10试一下。

输出结果为:

C语言-动态内存管理(一)_realloc_04

但是我们申请了,用完之后不用了,还要还回去,不能一直申请不还,所以还给操作系统就是用下面的free函数。

malloc指针申请完一定要做检查,判断指针是否为NULL。

free

C语言提供了一个动态内存回收的函数:

void free (void* ptr);

在cplusplus.com中的描述如下:

C语言-动态内存管理(一)_malloc_05

回收内存块

以前通过调用malloccallocrealloc分配的内存块被释放,使其再次可用于进一步分配。如果ptr不指向与上述函数分配的内存块,它会导致未定义的行为。如果ptr是空指针,该函数什么都不做。请注意,此函数不会改变ptr本身的值,因此它仍然指向相同的(现在无效)位置。

我们在上面的代码基础上加入内存释放:

int main(){
    int *p= (int*) malloc(10*sizeof (int));
    if(p==NULL){
        printf("%s\n",strerror(errno));
    }else{
        //成功申请
        int i=0;
        for (i = 0; i < 10; ++i) {
            *(p+i)=i;
        }
        for (i = 0; i < 10; ++i) {
            printf("%d",*(p+i));
        }
    }

    free(p);//这里加上free
        p=NULL;
    return 0;

}

输出结果为:

C语言-动态内存管理(一)_free_06

好像从输出结果上看并没有什么不同,但是这里区别可就大了去了

首先关于C语言运行一个程序,在执行结束,这个程序结束的时候会自动把你申请的空间释放掉,自动还给操作系统。但是这里是程序结束,假设这里之后程序还没有结束,如果没有释放。那么这块空间用完之后,一直没有还给操作系统,那么后续这块空间就不能被别人使用。这空间就被白白浪费了

同时最后在free之后要把指针p给赋值为NULL。因为即使这片空间还给操作系统了但是指针p依然具有访问这片空间的权限,所以释放完毕之后要将指针指向NULL。

所以 malloc 和free 一般都是成对使用

calloc

calloc也是一个动态内存分配的函数。

void* calloc (size_t num, size_t size);

在cplusplus.com中的描述如下:

C语言-动态内存管理(一)_free_07

开辟一块空间,并把元素改为0

为num个元素的数组分配一个内存块,每个元素size字节长,并将其所有位初始化为零。有效结果是分配一个(num*size)字节的零初始化内存块。如果size为零,返回值取决于特定的库实现(它可能是也可能不是空指针),但返回的指针不应被解引用。

num:要分配的元素数量。

size:每个元素的字节数目

举个例子使用一下这个函数

int main(){

    int* p=(int *) calloc(10,sizeof(int));
    if (p==NULL){
        printf("%s\n", strerror(errno));
    }else{
        int i=0;
        for ( i = 0; i < 10; ++i) {
            printf("%d",*(p+i));
        }
    }
    //释放
    free(p);
    p=NULL;
    return 0;

}

输出结果为:

C语言-动态内存管理(一)_动态内存_08

发现输出结果均为0

那么malloc 和calloc 都是开辟一段内存,然后malloc是不初始化,而calloc需要初始化所以他的效率要比malloc 低一点,在具体使用中如果需要初始化为0就是用calloc,如果不需要就是用malloc,malloc使用偏多一点。

 

realloc

realloc函数的出现,让动态内存管理更加灵活

有时候我们发现过去申请的空间太小了,有时候我们又觉得申请的空间过大了,那为了合理的内存分配,我们一定会对内存的大小做灵活的调整,那么realloc函数就可以做到怼冬天开辟内存大小的调整,函数原型如下:


void* realloc (void* ptr, size_t size);

在cplusplus.com中的描述如下:

C语言-动态内存管理(一)_malloc_09

更改ptr指向的内存块的大小。该函数可能会将内存块移动到新位置(其地址由该函数返回)。即使将块移动到新位置,内存块的内容也会保留到新旧size中的较小。如果新size较大,则新分配部分的值不确定。如果ptr是一个空指针,该函数的行为类似于malloc,分配一个新的大小字节块,并返回指向其开头的指针。

如果函数未能分配请求的内存块,则返回一个空指针,并且参数ptr指向的内存块不会被取消分配(它仍然有效,其内容不变)。

ptr:指向以前用malloc、calloc或realloc分配的内存块的指针。或者,这可以是一个空指针,在这种情况下,会分配一个新的块(就像调用malloc一样)。

size:内存块的新大小,以字节为单位。Size_t是一个无符号integral类型。

举个例子:


int main(){
    int *p=(int*) malloc(20);
    if (p==NULL){
        printf("%s\n", strerror(errno));
    } else{
        for (int i = 0; i < 5; ++i) {
            *(p+i)=i;
        }
    }
    //假设malloc 开辟的20个字节空间不够用
    //我们期望有40个字节空间
    //就可以使用realloc来调整动态开辟的内存。

    int* p2= realloc(p,40);
    int i=0;
    for (int j= 0; j < 10; ++j) {
        printf("%d ",*(p2+j));

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


}


输出结果为:

C语言-动态内存管理(一)_动态内存_10

前面5个数字我们初始化了,这是前20个字节的空间,后续我们使用realloc 又开辟了20个字节空间,后面没有初始化就是随机值,这里我的环境下编译器是0,换个平台可能是其他随机值。

另外注意:我在使用realloc的时候我用的是新的指针p2来存放realloc开辟的空间,并没有使用p继续去存储。如果使用p来存储realloc重新分配的空间是有很大风险的。

再说这个风险之前我们先分析一下realloc分配空间可能出现的情况:

realloc 再重新分配空间的时候如果你要用它新增空间,那么一定会出现两种情况。

情况1 

C语言-动态内存管理(一)_动态内存_11

重新分配所增加的空间 不影响后面其他程序开辟的空间

那么直接在这个空间后面追加这么大的空间即可,返回的地址还是p。


情况2

重新分配所增加的空间比较大,会影响到后续其他程序开辟的空间。那么此时realloc会在另外一个地方(能够满足所申请空间的大小的地方)去重新开辟一段空间,并且把原有的内容拷贝过来在返回一个新的地址。然后释放旧的也就是p指向的空间。此时空间的地址已经发生变化了。

C语言-动态内存管理(一)_calloc_12

风险

int* p3= realloc(p2,10*INT_MAX);

假设你需要使用realloc重新开辟的空间分配失败了(如上述代码,没有那么大的空间可以分配),那么就会返回一个空指针,你如果还用p去接收那么此时p就变成了NULL指针了。原本p还指向了20个字节,现在就找不到了这个已有的20字节的空间。

所以最好的办法要么是重新声明一个指针来存储realloc开辟的新的空间

或者判断一下:

int* p2= realloc(p,40);
    int* p3= realloc(p2,10*INT_MAX);
    if(p3!=NULL){
    p2=p3;
    p3=NULL;
    }

如果没有开辟失败,再使用p2指向这个地址。

那么以上就是动态内存分配的含义以及一些函数的定义及使用。

后续还有

常见的动态内存错误、以及相关经典的笔试题、还有柔性数组

会在之后更新后面的内容。