【C语言】动态内存管理

目录

Ⅰ、为什么存在动态内存分配

Ⅱ、动态内存函数的介绍

         1 .malloc和free

         2 .calloc

         3 .realloc

Ⅲ、常见的动态内存错误

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

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

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

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

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

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

Ⅳ、几个经典的笔试题

Ⅴ、C/C++程序的内存开辟

Ⅵ、柔性数组

         1 .柔性数组的特点

         2 .柔性数组的使用

         3 .柔性数组的优势


Ⅰ、为什么存在动态内存分配

我们已经掌握的内存开辟方式有:
int val = 20 ; // 在栈空间上开辟四个字节
char arr [ 10 ] = { 0 }; // 在栈空间上开辟 10 个字节的连续空间
但是上述的开辟空间的方式有两个特点:
1. 空间开辟大小是固定的。
2. 数组在申明的时候,必须指定数组的长度,它所需要的内存在编译时分配。
但是对于空间的需求,不仅仅是上述的情况。有时候我们需要的空间大小在程序运行的时候才能知道,那数组的编译时开辟空间的方式就不能满足了。
这时候就只能试试动态存开辟了。

Ⅱ、动态内存函数的介绍

         1 .malloc和free

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

void*  malloc (size_t size);

这个函数向内存申请一块 连续可用 的空间,并返回指向这块空间的指针。
1.如果开辟成功,则返回一个指向开辟好空间的指针。
2.如果开辟失败,则返回一个 NULL 指针,因此 malloc 的返回值一定要做检查。
3.返回值的类型是 void* ,所以 malloc 函数并不知道开辟空间的类型,具体在使用的时候使用者自己 来决定。
4.如果参数 size 0 malloc 的行为是标准是未定义的,取决于编译器。
C 语言提供了另外一个函数 free ,专门是用来做动态内存的释放和回收的,函数原型如下:
void free ( void* ptr );
free 函数用来释放动态开辟的内存。
1. 如果参数 ptr 指向的空间不是动态开辟的,那 free 函数的行为是未定义的。
2. 如果参数 ptr NULL 指针,则函数什么事都不做。
malloc free 都声明在 stdlib.h 头文件中。
举个例子:
#include <stdio.h>
#include <stdlib.h>
int main ()
{
// 代码 1
        int num = 0 ;
        scanf ( "%d" , & num );
        int arr [ num ] = { 0 };
// 代码 2
        int* ptr = NULL ;
        ptr = ( int* ) malloc ( num * sizeof ( int ));
if ( NULL != ptr ) // 判断 ptr 指针是否为空
{
        int i = 0 ;
        for ( i = 0 ; i < num ; i ++ )
        {
                * ( ptr + i ) = 0
        }
}
free ( ptr ); // 释放 ptr 所指向的动态内存
ptr = NULL ; // 是否有必要?//有必要
return 0 ;
}
这段代码释放空间后,有必要置空,以防使用野指针

         2 .calloc

C 语言还提供了一个函数叫 calloc calloc 函数也用来动态内存分配。原型如下:
void* calloc ( size_t num , size_t size );
函数的功能是为 num 个大小为 size 的元素开辟一块空间,并且把空间的每个字节初始化为 0
与函数 malloc 的区别只在于 calloc 会在返回地址之前把申请的空间的每个字节初始化为全 0

例如:

#include <stdio.h>
#include <stdlib.h>
int main ()
{
        int * p = ( int* ) calloc ( 10 , sizeof ( int ));
        if ( NULL != p )
{
// 使用空间
}
        free ( p );
        p = NULL ;
        return 0 ;
}

可以清楚看见,这里calloc申请了10个int的空间,并把空间全部初始化为了0;

         3 .realloc

1.realloc 函数的出现让动态内存管理更加灵活。
2. 有时会我们发现过去申请的空间太小了,有时候我们又会觉得申请的空间过大了,那为了合理的时候内存,我们一定会对内存的大小做灵活的调整。那 realloc 函数就可以做到对动态开辟内存大小的调整。
函数原型如下:
void* realloc ( void* ptr , size_t size );
开辟成功:老空间释放
开辟失败:原空间不变

注意:

1.ptr 是要调整的内存地址
2.size 调整之后新大小
3.返回值为调整之后的内存起始位置。
4.这个函数调整原内存空间大小的基础上,还会将原来内存中的数据移动到 的空间。
5.ealloc 在调整内存空间的是存在两种情况:
        情况1 :原有空间之后有足够大的空间
        情况2 :原有空间之后没有足够大的空间

情况 1
当是情况 1 的时候,要扩展内存就直接原有内存之后直接追加空间,原来空间的数据不发生变化。
情况 2
当是情况 2 的时候,原有空间之后没有足够多的空间时,扩展的方法是:在堆空间上另找一个合适大小的连续空间来使用。这样函数返回的是一个新的内存地址。
由于上述的两种情况, realloc 函数的使用就要注意一些。
举个例子:
#include <stdio.h>
int main ()
{
        int * ptr = ( int* ) malloc ( 100 );
        if ( ptr != NULL )
        {
                    // 业务处理
        }
        else
        {
                    exit ( EXIT_FAILURE );    
        }
        //扩展容量
        //代码 1
        ptr = ( int* ) realloc ( ptr , 1000 ); // 这样可以吗? ( 如果申请失败会如何? )  失败返回NULL
        
这样的代码就不够好,直接赋值给原空间会出现一个问题。如果开辟空间失败,返回NULL给ptr,这样ptr原本的数据也就没有了,我们可以制造一个中间变量,把调整的空间赋值给中间变量,开辟成功了后才赋值给原变量,如下:
        //代码 2
        int* p = NULL ;
        p = realloc ( ptr , 1000 );
        if ( p != NULL )
        {
                        ptr = p ;
        }
        //业务处理
        free ( ptr );
        return 0 ;
}

Ⅲ、常见的动态内存错误

         1 .NULL指针的解引用操作

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

         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 );
}
这里的越界访问注意:越界读可能不会报错,越界写要报错。

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

void test ()
{
        int a = 10 ;
        int * p = & a ;
        free ( p ); //ok?
}
释放只能只针对于动态开辟的空间

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

void test()

{
        int * p = ( int * ) malloc ( 100 );
        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 );
}
忘记释放不再使用的动态开辟的空间会造成内存泄漏。虽然程序结束时系统会进行释放,但是对于不停机工作的系统长期内存泄漏会造成很多问题。
切记:
动态开辟的空间一定要释放,并且正确释放

Ⅳ、经典的笔试题

题目 1
void GetMemory ( char * p )
{
        p = ( char * ) malloc ( 100 );
}
void Test ( void )
{
        char * str = NULL ;
        GetMemory ( str );
        strcpy ( str , "hello world" );
        printf ( str );
}

这个题经典错误:一级指针变量要进行修改要用二级指针变量,这里的代码有两个问题:

1. 对NULL的解引用

2. 内存泄漏 (GetMemory函数开辟空间后未释放,且函数执行完之后管理开辟的空间的变量已经释放)

题目 2
char * GetMemory ( void )
{
        char p [] = "hello world" ;
        return p ;
}
void Test ( void )
{
        char * str = NULL ;
        str = GetMemory ();
        printf ( str );
}

错误:返回了局部变量的地址,会访问已经归还给系统的空间

题目 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 ,内存泄漏

题目 4
void Test ( void )
{
        char * str = ( char * ) malloc ( 100 );
        strcpy ( str , "hello" );
        free ( str );
        if ( str != NULL )
        {
                strcpy ( str , "world" );
                printf ( str );
        }
}

问题:释放空间后未置空,执行 str!=NULL 时为真  执行代码时可能会造成野指针问题。

Ⅴ、C/C++程序的内存开辟

C/C++ 程序内存分配的几个区域:
1. 栈区 stack ):在执行函数时,函数内局部变量的存储单元都可以在栈上创建,函数执行结束时这些存储单元 自动被释放 。栈内存分配运算内置于处理器的 指令集中,效率很高 ,但是分配的 内存容量有限 。 栈区主要存放运行函数而分配的 局部变量、函数参数、返回数据、返回地址 等。
2. 堆区 heap ):一般由 程序员分配释放 , 若程序员不释放,程序结束时 可能由OS回收 。分配方式类似于链表。
3. 数据段(静态区) static )存放 全局变量、静态数据 。程序结束后由 系统释放
4. 代码段 :存放 函数体 (类成员函数和全局函数)的二进制代码。
理解 static 关键字修饰局部变量
实际上普通的局部变量是在 栈区 分配空间的,栈区的特点是在上面创建的变量 出了作用域就销毁 。但是被static 修饰的变量存放在 数据段(静态区) ,数据段的特点是在上面创建的变量,直到 程序结束才销毁
所以生命周期变长。

Ⅵ、柔性数组

也许你从来没有听说过 柔性数组( flexible array 这个概念,但是它确实是存在的。
C99 中, 结构中 的最后一个元素允许是未知大小的数组,这就叫做『柔性数组』成员。
例如:
typedef struct st_type
{
        int i ;
        int a [ 0 ]; // 柔性数组成员
} type_a ;
有些编译器会报错无法编译可以改成:
typedef struct st_type
{
        int i ;
        int a []; // 柔性数组成员
} type_a ;

         1 .柔性数组的特点

1. 结构中的柔性数组成员 前面 必须至 少一个其他成员
2. sizeof 返回的这种结构大小 不包括柔性数组的内存
3. 包含柔性数组成员的结构用 malloc () 函数进行内存的动态分配,并且分配的内存应该大于结构的大小,以适应柔性数组的预期大小。
例如:
//code1
typedef struct st_type
{
        int i ;
        int a [ 0 ]; // 柔性数组成员
} type_a ;
printf ( "%d\n" , sizeof ( type_a )); // 输出的是 4
注意:
柔性数组虽然不参与sizeof结构体计算的大小。但是他的类型还是会参与最大成员对齐数的判定,会影响sizeof返回的大小,当他有内存的时候也会参与结构体内存对齐;

         2 .柔性数组的使用

// 代码 1
int i = 0 ;
type_a * p = ( type_a * ) malloc ( sizeof ( type_a ) + 100 * sizeof ( int ));
// 业务处理
p -> i = 100 ;
for ( i = 0 ; i < 100 ; i ++ )
{
        p -> a [ i ] = i ;
}
free ( p );

         3 .柔性数组的优势

上述的 type_a 结构也可以设计为:
// 代码 2
typedef struct st_type
{
        int i ;
        int * p_a ;
} type_a ;
type_a * p = ( type_a * ) malloc ( sizeof ( type_a ));
p -> i = 100 ;
p -> p_a = ( int * ) malloc ( p -> i * sizeof ( int ));
// 业务处理
for ( i = 0 ; i < 100 ; i ++ )
{
        p -> p_a [ i ] = i ;
}
// 释放空间
free ( p -> p_a );
p -> p_a = NULL ;
free ( p );
p = NULL ;
上述 代码 1 代码 2 可以完成同样的功能,但是 方法 1 的实现有两个好处:
第一个好处是: 方便内存释放
如果我们的代码是在一个给别人用的函数中,你在里面做了二次内存分配,并把整个结构体返回给用户。用户调用free 可以释放结构体,但是用户并不知道这个结构体内的成员也需要 free ,所以你不能指望用户来发现这个事。所以,如果我们把结构体的内存以及其成员要的内存一次性分配好了,并返回给用户一个结构体指针,用户做一次free就可以把所有的内存也给释放掉。
第二个好处是: 这样有利于访问速度 .
连续的内存有益于提高访问速度,也有益于减少内存碎片。
(其实,个人觉得也没多高了,反正都要用做偏移量的加法来寻址)
  • 27
    点赞
  • 14
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

何陈陈

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值