C语言---动态内存管理

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

指针+结构体+动态内存管理

是学习数据结构的非常重要的知识

int main()
{
    int n = 0;//向内存申请一块空间---一个整型4个字节
    int arr[10] = { 0 };//向内存中申请一块连续的空间--10个整型--40个字节
    return 0;
}

这两种

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

• 空间开辟⼤⼩是固定的。

• 数组在申明的时候,必须指定数组的⻓度,数组空间⼀旦确定了⼤⼩不能调整,但是对于空间的需求,不仅仅是上述的情况。有时候我们需要的空间⼤⼩在程序运⾏的时候才能知道,那数组的编译时开辟空间的⽅式就不能满⾜了。

C语⾔引⼊了动态内存开辟,让程序员⾃⼰可以申请和释放空间,就⽐较灵活了。

2.malloc和free

malloc函数

C语言提供了一个动态内存开辟的函数:malloc --头文件stdlib.h

void * malloc (size_t size);

就是你利用malloc指定开辟一块多大的内存空间,然后这个函数就将这块地址的起始地址返回给你

//int main()
//{
//    int n = 0;//向内存申请一块空间---一个整型4个字节
//    int arr[10] = { 0 };//向内存中申请一块连续的空间--10个整型--40个字节
//    return 0;
//}






//void * malloc (size_t size);//开辟内存块,这里的size就是我们要开辟的内存块是多大,单位是字节
//这个函数返回的是一个void *的指针

//就是你指定开辟一块多大的内存空间,然后这个函数就将这块地址的起始地址返回给你

//如果开辟空间过大的话,就开辟失败了,返回的是一个空指针

/*
我们要注意几个点:
1.参数的单位是字节
2.申请空间成功的话,返回的是开辟空间的起始地址
3.申请失败的话,返回NULL

*/

int main()
{
    //申请10个整型的空间
    //
    int* p = (int*)malloc(10 * sizeof(int));//void *返回值放到整型指针里面我们就需要进行整型指针强制类型转换的操作了

    //因为malloc返回值是void*类型的指针,所以我们就强制类型转换我们期望类型的指针就行了

    //这个时候p就指向了这40个字节
    //我们需要进行判断,判断是否开辟成功
    if (p == NULL)
    {
        //如果空间开辟失败
        perror("malloc");//返回开辟失败的错误信息
        return 1;
    }
    //开辟成功就是可以使用这40个字节

    //我们已经知道了这40个字节空间的起始地址了,
    /*那么如何存放10个整数呢?*/

    for (int i = 0; i < 10; i++)
    {
        *(p + i)=i+1;//*(p + i)是可以找到下标为i的元素的,i+1操作为其赋值
    }
    for (int i = 0; i < 10; i++)
    {
        printf("%d ", *p);
        p++;
    }

    return 0;
}
/*
malloc申请的空间和数组的空间有什么区别呢?

1.动态内存的大小是可以调节的
2.开辟空间的位置不一样

*/

这个函数向内存申请⼀块连续可⽤的空间,并返回指向这块空间的指针。

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

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

• 返回值的类型是 void* ,所以malloc函数并不知道开辟空间的类型,具体在使⽤的时候使⽤者⾃

⼰来决定。

• 如果参数 size 为0,malloc的⾏为是标准是未定义的,取决于编译器

我们在用malloc是要检查返回值是不是空指针

free函数

C语⾔提供了另外⼀个函数free,专⻔是⽤来做动态内存的释放和回收的,函数原型如下:void free (void* ptr);

free函数⽤来释放动态开辟的内存。

• 如果参数 ptr 指向的空间不是动态开辟的,那free函数的⾏为是未定义的。

• 如果参数 ptr 是NULL指针,则函数什么事都不做。

malloc和free都声明在 stdlib.h 头⽂件中。

我们给free传地址,什么类型的地址都行,将地址传给free,free函数就能将这个地址所对应的空间删掉

我们在每次释放完对应地址的空间后,为了防止这个地址变成野指针对程序造成影响,我们应该将这个指针进行一个空指针的赋值

int main()
{
    //申请10个整型的空间
    //
    int* p = (int*)malloc(10 * sizeof(int));//void *返回值放到整型指针里面我们就需要进行整型指针强制类型转换的操作了

    //因为malloc返回值是void*类型的指针,所以我们就强制类型转换我们期望类型的指针就行了

    //这个时候p就指向了这40个字节
    //我们需要进行判断,判断是否开辟成功
    if (p == NULL)
    {
        //如果空间开辟失败
        perror("malloc");//返回开辟失败的错误信息
        return 1;
    }
    //开辟成功就是可以使用这40个字节

    //我们已经知道了这40个字节空间的起始地址了,
    /*那么如何存放10个整数呢?*/

    for (int i = 0; i < 10; i++)
    {
        *(p + i)=i+1;//*(p + i)是可以找到下标为i的元素的,i+1操作为其赋值
    }
    for (int i = 0; i < 10; i++)
    {
        printf("%d ", *p);
        p++;
    }
    //释放空间
    free(p);//p指向的空间不属于当前程序了,但是还能找到这个空间,这个p已经是野指针了
    p = NULL;//我们直接将p赋值为空指针,这样p就用不了了
    return 0;
}
/*
malloc申请的空间和数组的空间有什么区别呢?

1.动态内存的大小是可以调节的
2.开辟空间的位置不一样

*/
//我们申请完空间之后,不想用了,我们就进行释放就行了

//那我们如何将这块空间释放呢?

//这个时候就涉及到free函数
//那么我们就加上free(p)

//可是在释放完这块空间后,p就成野指针了,怎么办呢?
//我们直接将p赋值为空指针,这样p就用不了了


/*
int main
{
    int arr[10]

}
对于这种在内存中创造空间存放元素

这里的数组是个局部变量,出了这个程序这个局部变量所占的内存就自动还给空间了
进入大括号就创建,出了大括号就销毁

而malloc手动申请的空间需要我们进行手动释放空间


*/
//int main()
//{
//    int arr[10] = { 0 };
//    int* p = arr;
//    //…………
//
//    free(p);//error,这种写法是错的,因为p指向的这片空间不是动态开辟的
//    p = NULL;//
//    return 0;
//}


int main()
{
    ///....
    int* p = NULL;
    ///....
    free(p);//因为这个p是空指针,所以free什么都不干了
    return 0;
}

如果p是空指针的话,那么free(p)就什么都不干

malloc和free最好成对使用,有申请有释放

3.calloc和realloc

calloc函数的使用

C语⾔还提供了⼀个函数叫 calloc , calloc 函数也⽤来动态内存分配。原型如下:

void* calloc (sizet num, sizet size);

• 函数的功能是为 num 个⼤⼩为 size 的元素开辟⼀块空间,并且把空间的每个字节初始化为0。

• 与函数 malloc 的区别只在于 calloc 会在返回地址之前把申请的空间的每个字节初始化为全

0。

返回的void*是开辟地址的空间地址

int main()
{
    //申请10个整型的空间
    int*p=(int*)calloc(10, sizeof(int));//开辟10个大小为4个字节的空间
    if (p == NULL)
    {
        perror("calloc");//打印错误信息
        return 1;//直接结束
    }
    //使用空间
    for (int i = 0; i < 10; i++)
    {
        printf("%d ", p[i]);

    }
    free(p);
    p = NULL;
    return 0;
}
//malloc函数申请的空间不初始化,但是calloc函数申请的空间会初始化为0
//calloc函数有两个参数,malloc函数只有1个参数

realloc函数的使用

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

• 有时会我们发现过去申请的空间太⼩了,有时候我们⼜会觉得申请的空间过⼤了,那为了合理的使⽤内存,我们⼀定会对内存的⼤⼩做灵活的调整。那 realloc 函数就可以做到对动态开辟内存⼤⼩的调整。

函数原型如下:void* realloc (void* ptr, size_t size);

• ptr 是要调整的内存地址

• size 调整之后新⼤⼩

• 返回值为调整之后的内存起始位置。

• 这个函数调整原内存空间⼤⼩的基础上,还会将原来内存中的数据移动到 新 的空间。

• realloc在调整内存空间的是存在两种情况:

◦ 情况1:原有空间之后有⾜够⼤的空间

◦ 情况2:原有空间之后没有⾜够⼤的空间,直接创建一个新地址,返回的是一个新地址,在返回地址后,我们对新地址进行判断,如果返回的地址不是空指针,就是说开辟成功,那么我们直接将新地址赋值给原地址,那么原地址p就指向了新开辟的空间,并且realloc会将原空间数据拷贝到新空间里面

int main()
{
    //申请10个整型的空间
    int*p=(int*)calloc(10, sizeof(int));//开辟10个大小为4个字节的空间
    if (p == NULL)
    {
        perror("calloc");//打印错误信息
        return 1;//直接结束
    }
    //使用空间
    for (int i = 0; i < 10; i++)
    {
        printf("%d ", p[i]);

    }
    //如果觉得空间不够了,我们想调整空间--希望变成20个整型空间
    int*ptr=(int*)realloc(p, 20 * sizeof(int));
//第一个参数是我们调整的是哪一块空间,第二个参数就是我们想调整的大小
    //直接将原来的空间变成20个字节
    if (ptr != NULL)//说明realloc函数开辟额外的空间成功了
    {
        p = ptr;//那么我们就将ptr赋值给p
        //这样的话p就拿到了新的20个字节空间的地址了
    }

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

/*
realloc在追加空间的时候,有两种情况
情况一:原先是10个字节,我们扩展成20个字节,我们在原有空间后面追加10个字节的内存空间。这10个内存空间之前是没人使用的

情况二:我们在后面追加10个字节,但是后面的内存空间已经被别人占了,后面的空间是不够10个字节的


//对于情况一的话:后面空间足够,直接把空间给你,返回的是这块空间起始地址

对于情况二的话:1.后续空间不够的话,realloc函数直接在内存堆区开辟一个新的20个字节满足大小的空间

2.将旧的数据拷贝到新的空间

3.realloc函数会讲旧的空间释放,旧的空间就用我们释放了,realloc帮我们释放


总之: realloc函数返回的地址可能是一个旧的地址,也可能是一个新的地址,也可能是一个空指针

那么我们应该拿什么东西进行指针接收呢?
原来的空间地址是p,假如说realloc函数开辟空间失败的话,原本我们的p还是有10个字节的空间的,
但是现在开辟失败,返回一个空指针,直接将这个空指针赋值给p了,p就是空指针了
原本地址里面的数据就不见了 

就算realloc函数开辟失败了,原先的空间也不能搞丢啊
所以我们最好不拿原先的地址p接收

我们可以再专门创建一个新地地址ptr进行接收

并且我们进行判断

if (ptr != NULL)//说明realloc函数开辟额外的空间成功了
    {
        p = ptr;//那么我们就将ptr赋值给p
        //这样的话p就拿到了新的20个字节空间的地址了
    }
如果开辟字节空间成功,那么直接将新地址赋值给原地址,那么p就指向了新地址了



如果realloc函数在缩小空间的时候就是直接返回的是原空间的地址
realloc函数直接在原空间的基础上将多余空间舍弃



realloc函数是很好的,有了realloc函数,使我们的动态内存管理更加灵活
*/

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

1.

int main()
{
    int *p=(int*)malloc(10 * sizeof(int));
    if (p == NULL)
    {
        perror("malloc");//打印错误信息
        return 1;//为空指针的话我们直接返回
    }

    for (int i = 0; i < 10; i++)
    {
        printf("%d ", p[i]);//--*(p+i)//如果地址开辟失败了,返回了一个空指针,那么这里就是对空指针进行解引用操作
        //如果p是空指针的话,下面的代码就全错了,所以我们在之前要进行判断

    }
    free(p);
    p = NULL;
    return 0;
}
//我们在写代码的时候要写的规范点,对malloc返回值要进行判断

2.

int main()
{
    int *p=(int*)malloc(10 * sizeof(int));
    if (p == NULL)
    {
        perror("malloc");//打印错误信息
        return 1;//为空指针的话我们直接返回
    }

    for (int i = 0; i < 40; i++)//我们只生成了10个整型的空间,但是我们循环40次,以为这里是40个字节,这种情况就是越界问题了
    {
        printf("%d ", p[i]);
    }
    free(p);
    p = NULL;
    return 0;
}

//访问空间在我们创造的空间内就行了,尽量不越界访问

3.

int main()
{
    int a=10;
    int* p = &a;//并非动态开辟的

    free(p);
    p = NULL;
    return 0;
}
//这种错误就是用free去释放并非动态内存开辟的空间,最后运行时会报错的

4.释放内存的时候没有从开头释放内存

int main()
{
    int *p=(int*)malloc(10 * sizeof(int));
    if (p == NULL)
    {
        perror("malloc");//打印错误信息
        return 1;//为空指针的话我们直接返回
    }

    for (int i = 0; i < 5; i++)//
    {
        *p = i;
        p++;
    }//5次循环之后,p就指向了第6个元素了
    free(p);//这里的p就是第6个元素的地址了,那么我们从这里开始释放内存是不行的
   //我们要释放空间必须从头开始释放
    p = NULL;
    return 0;
}

5.

//int main()
//{
//    int *p=(int*)malloc(10 * sizeof(int));
//    if (p == NULL)
//    {
//        perror("malloc");
//        return 1;
//    }
//    free(p);
//    p = NULL;
//    //…………
//    //再次free
//    free(p);//在这里的p已经是空指针了,因为已经内存释放过一次了,这里就没啥问题
//    p = NULL;
//    return 0;
//}


//但是如果是下面的这种,第一次释放的时候没有将p赋值为NULL,第二次接着对p进行释放
int main()
{
    int* p = (int*)malloc(10 * sizeof(int));
    if (p == NULL)
    {
        perror("malloc");
        return 1;
    }
    free(p);

    //…………
    //再次free
    free(p);
    p = NULL;
    return 0;
}
//如果第一次在内存释放的时候没有将p赋值为空指针,第二次接着用p释放内存,这样就会报错的,
//这种就是对一块动态内存多次释放 
//相当与对野指针进行释放

//多以在用完p释放完内存后,将p赋值为空指针很有必要的

6.在动态内存内开辟空间之后,一定要进行内存释放,不然会出现内存泄漏的问题

//多以在用完p释放完内存后,将p赋值为空指针很有必要的
void test()
{
    int flag = 1;
    int* p = (int*)malloc(100);//开辟100个字节的空间
    if (p == NULL)
    {
        ///
        return;
    }
    //不为空指针--正常使用
    if (flag)//条件让我们直接返回了,那么后面的内存释放就没有进行了,没机会释放了
    {
        return;
    }

    free(p);
    p = NULL;
}
int main()
{
    test();
    return 0;
}

//不进行内存释放了,除非程序运行完,否则这块空间永远找不到了
//这就叫内存泄露

/*
如果像解决我们就应该提前将这块空间地址返回去,让别人帮你释放这块空间

*/

动态内存是一把双刃剑

1.提供灵活的内存管理的方式

2.带来风险

malloc calloc函数都能开辟空间

realloc函数不仅能调整空间,而且还能申请空间

int main()
{
    /*int* p = (int*)malloc(20);
    realloc(p, 40);使用realloc拓展开辟额外的20个字节的空间*/

    realloc(NULL, 40);//==malloc(40)
    //如果realloc函数的第一个参数传的是空指针的话,那么这个函数的作用和malloc函数作用是一样的了
    return 0;
}

如果realloc函数的第一个参数传的是空指针的话,那么这个函数的作用和malloc函数作用是一样的了

5.动态内存经典笔试题分析

1.传值不如传址

//传过来的仅仅只是指针变量,并不是地址,所以这里的p起始是一份拷贝品
//改变p改变不了str
void GetMemory(char* p)//这里的形参是实参的一份临时拷贝的
//相当与将str内的NULL传给p,那么p里面得到就是空指针
{
    p = (char*)malloc(100);//申请100个字节的空间,将这个空间的地址赋值给p
    //那么p就有能力找到这个空间了
    //这个函数结束了
}
void Test(void)
{
    char* str = NULL;
    //指针变量str,里面放的是空指针

    GetMemory(str); //将str传过去,这里的str是指针变量,里面存着NULL的地址
    strcpy(str, "hello world");//这里的str依然为空指针,因为str并没有因为p的改变而改变
    //如果这里的str是空指针,那么这里就对空指针就进行了非法访问了

    //一但对NULL进行解引用操作,程序就会崩溃的

    //上面函数内的malloc函数申请的空间并没有进行及时的释放内存,这样会造成内存泄漏的问题
    printf(str);//printf接受了首元素地址就能进行打印了
}
int main()
{
    Test();
    return 0;
}


//第一种解决方法:
void GetMemory(char** p)//那么我们用二级指针变量进行接收
{
    *p = (char*)malloc(100);
    //对二级指针进行解引用得到的就是一级指针,这里就得到了str
} 

void Test(void)
{
    char* str = NULL;
    GetMemory(&str);//将地址传过去,因为str是一级指针变量
    //经过这个函数,str里面放的就是100个字节的地址了
    strcpy(str, "hello world");//将hello world拷贝过去
    printf(str);
    free(str);
    str = NULL;
}
int main()
{
    Test();
    return 0;
}

//第二种方法
//char *GetMemory(char* p)//返回值p的类型是char*类型的
//{
//    p = (char*)malloc(100);
//    return p;//直接将p返回
//}
//
//void Test(void)
//{
//    char* str = NULL;
//    str=GetMemory(str);//用str来接收返回值
//    strcpy(str, "hello world");
//    printf(str);
//    free(str);
//    str = NULL;
//}
//int main()
//{
//    Test();
//    return 0;
//}


//我们甚至可以有第三种写法,在第二种写法的基础上
//我们这两个函数都可以不用传参
//开辟完空间直接将空间地址返回就行了
//完后让str进行一个接受就行了
char* GetMemory()//返回值p的类型是char*类型的
{
    char*p = (char*)malloc(100);
    return p;//直接将p返回
}

void Test(void)
{
    char* str = NULL;
    str = GetMemory();//用str来接收返回值
    strcpy(str, "hello world");
    printf(str);
    free(str);
    str = NULL;
}
int main()
{
    Test();
    return 0;
}

2.函数内返回的变量地址就是野指针,因为变量在函数调用完之后就被销毁了

不如将变量返回,不能返回地址

典型的返回栈空间的问题

//char* GetMemory(void)
//{
//    char p[] = "hello world";//局部数组
//    return p;//p是这个字符串的起始元素的地址
//}
//void Test(void)
//{
//    char* str = NULL;//str里面放的是空指针
//    str = GetMemory();
//    printf(str);
//}
//int main()
//{
//    Test();
//    return 0; 
//}
/*
进入这个函数char* GetMemory(void)
我们向内存申请一块空间来放"hello world"
我们一但出了这个函数,这块空间就不属于我们了
这个函数返回之后,这块空间不属于我们了,
我们将返回地址放到str内,注定了str是个野指针


出了函数那块空间就没了

如果我们用malloc去开辟这块空间的话,如果我们不释放的话,那么p就一直指向了这块空间


这个问题的原因就是因为这个数组是临时的,进入使用,出去就被销毁了
这种局部变量就放在栈区上面的


典型的返回栈空间地址的问题,栈空间就是临时的空间
返回栈空间上地址的问题
我们可以将变量返回去
但是不能返回这个变量的地址


函数调用结束的话,原本的那块空间就销毁了
*/
int test()
{
    int a = 10;
    return a;//将a里面的10通过return 返回,那么n里面就接收到了10
}//a出了这个函数就销毁了
int main()
{
    int n=test();
    printf("%d ", n);
    return 0;
}
//如果返回的是地址的话,因为a已经出函数销毁了,那么我们就无法通过这个
//寻找到a,所以这个地址就成了野指针了

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

    //下面的内存释放步骤很重要
    free(str);
    str = NULL;
}
int main()
{
    Test();
    return 0;
}
//这里的malloc没有进行内存释放
//造成内存泄漏问题
//忘记free

4.在free后,没有将str赋值为空指针,就会造成非法访问

//void Test(void)
//{
//    char* str = (char*)malloc(100);
//    strcpy(str, "hello");
//    free(str);//将str指向的空间还给操作系统,无法继续使用了
//    //但是str指向的地址还是那块空间的地址
//
//    //free完,str其实就是野指针了
//    if (str != NULL)//真
//    {
//        strcpy(str, "world");
//        //我们要将这个字符串放到一个不属于我们的空间,这就形成非法访问了
//        printf(str);
//    }
//}
//
//int main()
//{
//    Test();
//    return 0;
//}


//正确编写
void Test(void)
{
    char* str = (char*)malloc(100);
    strcpy(str, "hello");
    free(str);
    str = NULL;//及时将str赋值为空指针 ,是非法访问失效

    if (str != NULL)//真
    {
        strcpy(str, "world");

        printf(str);
    }
}

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

6.柔性数组

C99 中,结构中的最后⼀个元素允许是未知⼤⼩的数组,这就叫做『柔性数组』成员。

1.在结构体中

2.最后一个成员

3.位置大小的数组

struct S
{

    int n;
    char c;
    double d;

    //未知大小的数组---arr就是柔性数组的成员
    int arr[];
};
struct S2
{

    int n;
    char c;
    double d;

    //未知大小的数组---arr就是柔性数组的成员
    int arr[0];//数组大小写成0就是表示数组大小是未知的

};

柔性数组的概念

• 结构中的柔性数组成员前⾯必须⾄少⼀个其他成员。

• sizeof 返回的这种结构⼤⼩不包括柔性数组的内存。只计算柔性数组前面的成员的大小

• 包含柔性数组成员的结构⽤malloc ()函数进⾏内存的动态分配,并且分配的内存应该⼤于结构的⼤

⼩,以适应柔性数组的预期⼤⼩。

//struct s
//{
//    int n;
//    int arr[];
//};
//int main()
//{
//    printf("%zd", sizeof(struct s));//4
//    //所以在sizeof的计算中,我们是没有对这个柔性数组进行计算的
//    return 0;
//}
/*
因为我们在计算这个结构体大小的时候不包含柔性数组的大小,所以我们要包含一些其他成员


所以我们在使用柔性数组的时候,前面一定要有其他成员
*/
struct S
{
    int n;
    int arr[];
};
int main()
{

    /*struct S s;*///我们这么创建的话,s的大小仅仅4个字节
    //只给n开辟了空间,没有给arr开辟空间,所以这是错误的柔性数组开辟方法
    struct S*ps=(struct S*)malloc(sizeof(struct S) + 20 * sizeof(int));
    //为什么我们这样开辟呢?
    // sizeof(struct S)这个大小就是给n开辟的
    //后面的80个字节就是多余开辟出来给arr的,20个整型的大小


    //struct S*ps这个就是结构体类型的指针
    if (ps == NULL)
    {
        perror("malloc()");
        return 1;
    }
    //成功开辟,使用这些空间
    ps->n = 100;
    for (int i = 0; i < 20; i++)
    {
        ps->arr[i] = i + 1;
    }
    /*for (int i = 0; i < 20; i++)
    {
        printf("%d ", ps->arr[i]);
    }*/

    //调整ps指向空间的大小
    struct S* ptr=(struct S*)realloc(ps, sizeof(struct S) + 40 * sizeof(int));
    //为了防止开辟失败,返回的是一个空指针,我们创建一个新的结构体指针进行接收
    if (ptr != NULL)
    {
        ps = ptr;
        ptr = NULL;
    }
    else
    {
        perror("realloc()");
        return 1;
    }
    //使用空间
    for (int i = 0; i < 40; i++)
    {
        printf("%d ", ps->arr[i]);
    }



    //释放空间
    free(ps);
    ps = NULL;
    return 0;
}

柔性数组的优点

柔性数组

struct S
{
    int n;
    int arr[];
};
int main()
{

    /*struct S s;*///我们这么创建的话,s的大小仅仅4个字节
    //只给n开辟了空间,没有给arr开辟空间,所以这是错误的柔性数组开辟方法
    struct S*ps=(struct S*)malloc(sizeof(struct S) + 20 * sizeof(int));
    //为什么我们这样开辟呢?
    // sizeof(struct S)这个大小就是给n开辟的
    //后面的80个字节就是多余开辟出来给arr的,20个整型的大小


    //struct S*ps这个就是结构体类型的指针
    if (ps == NULL)
    {
        perror("malloc()");
        return 1;
    }
    //成功开辟,使用这些空间
    ps->n = 100;
    for (int i = 0; i < 20; i++)
    {
        ps->arr[i] = i + 1;
    }
    /*for (int i = 0; i < 20; i++)
    {
        printf("%d ", ps->arr[i]);
    }*/

    //调整ps指向空间的大小
    struct S* ptr=(struct S*)realloc(ps, sizeof(struct S) + 40 * sizeof(int));
    //为了防止开辟失败,返回的是一个空指针,我们创建一个新的结构体指针进行接收
    if (ptr != NULL)
    {
        ps = ptr;
        ptr = NULL;
    }
    else
    {
        perror("realloc()");
        return 1;
    }
    //使用空间
    for (int i = 0; i < 40; i++)
    {
        printf("%d ", ps->arr[i]);
    }



    //释放空间
    free(ps);
    ps = NULL;
    return 0;
}

非柔性数组

struct S
{
    int n;
    int* arr;
};
int main()
{
    //给结构体ps开辟空间
    struct S *ps=(struct S*)malloc(sizeof(struct S));
    if (ps == NULL)
    {
        perror("malloc");
        return 1;
    }
    //给arr单独申请20个整型的空间
    int*tmp=(int *)malloc(20 * sizeof(int));
    if (tmp != NULL)
    {
        ps->arr = tmp;//这块空间指向了ps里面的arr
    }
    else
    {
        perror("malloc");
        return 1;
    }
    ps->n = 100;
    //给arr中的20个元素赋值为1-20
    for (int i = 0; i < 20; i++)
    {
        ps->arr[i] = i + 1;

    }
    //调整空间
    //这个arr就是首元素的地址,就是那块空间的起始地址
    tmp = (int*)realloc(ps->arr, 40 * sizeof(int));
    if (tmp != NULL)
    {
        ps->arr = tmp;
    }
    else
    {
        perror("realloc");
        return 1;
    }
    for (int i = 0; i < 40; i++)
    {
        printf("%d ", ps->arr[i]);
    }
    //释放
    free(ps->arr);//先释放arr申请的空间 
    ps->arr = NULL;
    free(ps);//再将ps释放了
    ps = NULL;
    return 0;
}

相较于这两种代码,我们跟能发现柔性数组更加方便,仅仅使用一次malloc

在第一个代码中,我们直接将柔性代码的大小额外用malloc开辟出来

在第二个代码中,我们先要给ps开辟空间,再单独给arr开辟空间

总结:柔性数组好,用起来省事

第⼀个好处是:⽅便内存释放

如果我们的代码是在⼀个给别⼈⽤的函数中,你在⾥⾯做了⼆次内存分配,并把整个结构体返回给⽤⼾。⽤⼾调⽤free可以释放结构体,但是⽤⼾并不知道这个结构体内的成员也需要free,所以你不能指望⽤⼾来发现这个事。所以,如果我们把结构体的内存以及其成员要的内存⼀次性分配好了,并返回给⽤⼾⼀个结构体指针,⽤⼾做⼀次free就可以把所有的内存也给释放掉。

第⼆个好处是:这样有利于访问速度.连续的内存有益于提⾼访问速度,也有益于减少内存碎⽚。(其实,我个⼈觉得也没多⾼了,反正你跑不了要⽤做偏移量的加法来寻址)

malloc用多了,内存碎片也多了

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

局部变量放在栈区

函数的参数也放在栈区

堆区是动态申请的内存都在堆区

数据段内存放全局变量和别static修饰的静态变量

内核空间--用户代码不能读写

代码段--可执行代码/只读常量,不能修改的

C/C++程序内存分配的⼏个区域:

  1. 栈区(stack):在执⾏函数时,函数内局部变量的存储单元都可以在栈上创建,函数执⾏结束时

这些存储单元⾃动被释放。栈内存分配运算内置于处理器的指令集中,效率很⾼,但是分配的内

存容量有限。 栈区主要存放运⾏函数⽽分配的局部变量、函数参数、返回数据、返回地址等。

《函数栈帧的创建和销毁》

  1. 堆区(heap):⼀般由程序员分配释放, 若程序员不释放,程序结束时可能由OS(操作系统)回收 。分配⽅式类似于链表。

  2. 数据段(静态区):(static)存放全局变量、静态数据。程序结束后由系统释放。

  3. 代码段:存放函数体(类成员函数和全局函数)的⼆进制代码。

  • 95
    点赞
  • 45
    收藏
    觉得还不错? 一键收藏
  • 30
    评论
C语言-学生信息管理系统是一个基于链表数据结构的学生信息管理系统。链表是一种数据结构,可以存储和管理一系列具有相同类型的数据。在学生信息管理系统中,链表被用来存储和操作学生的基本信息。 该系统主要有以下功能: 1. 添加学生信息:可以添加学生的姓名、学号、性别、年龄等信息,并将该学生的信息节点插入到链表中。 2. 删除学生信息:根据学号或其他关键词查找到对应的学生信息节点,并从链表中删除该节点。 3. 修改学生信息:根据学号或其他关键词查找到对应的学生信息节点,并根据需求修改学生的信息。 4. 查询学生信息:可以根据学号或其他关键词查找到对应的学生信息节点,并显示该学生的详细信息。 5. 遍历学生信息:可以遍历整个链表,显示所有学生的基本信息。 链表的优势在于插入和删除节点的操作比较高效,因为只需要改变节点的指针指向即可,不需要移动其他节点。而数组在插入和删除操作时需要移动其他元素,效率较低。 在实现学生信息管理系统时,可以使用指针来操作链表,通过指针的指向找到链表的某个节点,并进行相应的操作。同时,需要注意对内存的管理,确保动态分配和释放内存的正确性,避免内存泄漏和访问错误。 总之,C语言-学生信息管理系统是一个基于链表数据结构的系统,可以实现学生信息的增删改查等功能。通过灵活运用链表的特点,可以高效地管理学生的基本信息。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 30
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值