目录
Ⅰ、为什么存在动态内存分配
我们已经掌握的内存开辟方式有:
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 (){// 代码 1int num = 0 ;scanf ( "%d" , & num );int arr [ num ] = { 0 };// 代码 2int* 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 );}//扩展容量//代码 1ptr = ( int* ) realloc ( ptr , 1000 ); // 这样可以吗? ( 如果申请失败会如何? ) 失败返回NULL
这样的代码就不够好,直接赋值给原空间会出现一个问题。如果开辟空间失败,返回NULL给ptr,这样ptr原本的数据也就没有了,我们可以制造一个中间变量,把调整的空间赋值给中间变量,开辟成功了后才赋值给原变量,如下:
//代码 2int* 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 () 函数进行内存的动态分配,并且分配的内存应该大于结构的大小,以适应柔性数组的预期大小。
例如:
//code1typedef struct st_type{int i ;int a [ 0 ]; // 柔性数组成员} type_a ;printf ( "%d\n" , sizeof ( type_a )); // 输出的是 4
注意:
柔性数组虽然不参与sizeof结构体计算的大小。但是他的类型还是会参与最大成员对齐数的判定,会影响sizeof返回的大小,当他有内存的时候也会参与结构体内存对齐;
2 .柔性数组的使用
// 代码 1int 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
结构也可以设计为:
// 代码 2typedef 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就可以把所有的内存也给释放掉。
第二个好处是:
这样有利于访问速度
.
连续的内存有益于提高访问速度,也有益于减少内存碎片。(其实,个人觉得也没多高了,反正都要用做偏移量的加法来寻址)