目录
一、动态内存分配
我们已经掌握的内存开辟方式有:
int val = 20;//在栈空间上开辟四个字节
char arr[10] = {0};//在栈空间上开辟10个字节的连续空间
但是上述的开辟空间的方式有两个特点:
1. 空间开辟大小是固定的。
2. 数组在申明的时候,必须指定数组的长度,它所需要的内存在编译时分配。
但是对于空间的需求,不仅仅是上述的情况。有时候我们需要的空间大小在程序运行的时候才能知道, 那数组的编译时开辟空间的方式就不能满足了。 这时候就只能试试动态存开辟了。
所谓动态内存分配(Dynamic Memory Allocation) 就是指在程序执行的过程中动态地分配或者回收存储空间的分配内存的方法。动态内存分配不象数组等静态内存分配方法那样需要预先分配存储空间,而是由系统根据程序的需要即时分配,且分配的大小就是程序要求的大小。
二、动态内存函数介绍
1、malloc
void* malloc (size_t size); //动态内存开辟
头文件:stdlib.h
介绍:malloc 是C语言提供的一个动态内存开辟的函数,该函数向内存申请一块连续可用的空 间,并返回指向这块空间的指针。具体情况如下:
① 如果开辟成功,则返回一个指向开辟好空间的指针。
② 如果开辟失败,则返回一个 NULL 指针。
③ 返回值的类型为 void* ,malloc 函数并不知道开辟空间的类型,由使用者自己决定。
④ 如果 size 为 0(开辟0个字节),malloc 的行为是标准未定义的,结果将取决于编译器。
2、free
void free( void *memblock ); //释放动态开辟的空间
头文件:stdlib.h
介绍:free 函数用来释放动态开辟的内存空间。具体情况如下:
① 如果参数 ptr 指向的空间不是动态开辟的,那么 free 函数的行为是未定义的。
② 如果参数 ptr 是 NULL 指针,那么 free 将不会执行任何动作。
注意事项:
① 使用完之后一定要记得使用 free 函数释放所开辟的内存空间。
② 使用指针指向动态开辟的内存,使用完并 free 之后一定要记得将其置为空指针。
代码演示:动态内存开辟空间,并且释放。
int main()
{
int* ptr = (int*)malloc(40);//开辟40个字节的空间
int* p = ptr;
//如果返回的为空指针,则开辟失败
if (p == NULL)
{
perror("malloc");//查询失败原因
return 1;
}
int i = 0;
for (i = 0; i < 10; i++)
{
//给开辟的空间赋值
*(p+i) = i;
printf("%d ", p[i]);
}
free(ptr);//将开辟的空间释放掉了,但指针还在,只不过这边空间不属于我们了
ptr = NULL;//所以ptr是野指针,需要手动置为空指针
return 0;
}
举一个开辟失败的例子:
int* ptr = (int*)malloc(INT_MAX);//开辟40个字节的空间
int* p = ptr;
//如果返回的为空指针,则开辟失败
if (p == NULL)
{
perror("malloc");//查询失败原因
return 1;
}
malloc: Not enough space
没有足够大的空间开辟
补充:
1) 为什么 malloc 前面要进行强制类型转换呢?
解析:
为了和 int* p 类型相呼应,所以要进行强制类型转换。你可以试着把强转删掉,其实也不会有什么问题。但是因为有些编译器要求强转,所以最好进行一下强转,避免不必要的麻烦。
2)为什么 free 之后,一定要把 p 置为空指针?
解析:
因为 free 之后那块开辟的内存空间已经不在了,它的功能只是把开辟的空间回收掉,但是 p 仍然还指向那块内存空间的起始位置,这合理吗?这不合理。所以我们需要使用 p = NULL 把他置成空指针。
0X01、
free函数用来释放动态开辟的内存。
如果参数 ptr 指向的空间不是动态开辟的,那free函数的行为是未定义的。
如果参数 ptr 是NULL指针,则函数什么事都不做。
0X02、
当我们不释放动态申请的内存时,如果程序结束,动态申请的内存由操作系统自动回收。
但是如果程序不结束,动态内存是不会自动回收的,就会形成内存泄漏的问题
3、calloc
void *calloc( size_t num, size_t size ); //开辟空间,将其元素初始化为0
头文件:stdlib.h
介绍:calloc 函数的功能实为 num 个大小为 size 的元素开辟一块空间,并把空间的每个字节初
始化为 0 ,返回一个指向它的指针。
对比:
① malloc 只有一个参数,而 calloc 有两个参数,分别为元素的个数和元素的大小。
② 与函数 malloc 的区别在于 calloc 会在返回地址前把申请的空间的每个字节初始
化 为 0。
代码演示:
int main()
{
// malloc
int* p = calloc(10,sizeof(int)); // 开辟10个整型空间,并且初始化为0
if (p == NULL)
{
return 1;
}
int i = 0;
for (i = 0; i < 10; i++)
{
printf("%d ", *(p + i));
}
free(p);
p = NULL;
return 0;
}
输出是 10个0,
如果用malloc的话,输出则是10个随机值,感兴趣的可以自己尝试一下。
4、realloc
void *realloc( void *memblock, size_t size ); //调整动态内存开辟的大小
头文件:stdlib.h
介绍:realloc 函数,让动态内存管理更加灵活。用于重新调整之前调用 malloc 或 calloc 所分配 的 ptr 所指向的内存块的大小,可以对动态开辟的内存进行大小的调整。
具体介绍如下:
① memblock 为指针要调整的内存地址(内存块)。
② size 为调整之后的新大小。
③ 返回值为调整之后的内存起始位置,请求失败则返回空指针。
④ realloc 函数调整原内存空间大小的基础上,还会将原来内存中的数据移动到新的空间。
realloc 函数在调整内存空间时存在的三种情况:
情况1:原有空间之后有足够大的空间。
情况2:原有空间之后没有足够大的空间。
情况3:realloc 有可能找不到合适的空间来调整大小。
情况1:当原有空间之后没有足够大的空间时,直接在原有内存之后直接追加空间,原来空间的数组不发生变化。
情况2:当原有空间之后没有足够大的空间时,会在堆空间上另找一个合适大小的连续的空间来使用。函数的返回值将是一个新的内存地址,并且释放掉旧的空间。
情况3:如果找不到合适的空间,就会返回一个空指针。
官方定义:
代码演示:
int main()
{
int* p = calloc(10,sizeof(int)); // 开辟10个整型空间,并且初始化为0
if (p == NULL)
{
return 1;
}
int i = 0;
for (i = 0; i < 10; i++)
{
printf("%d ", *(p + i));
}
//如果需要20给int的空间
//realloc扩容函数
p = (int*)realloc(p, 80);
free(p);
p = NULL;
return 0;
}
刚才提到的第三种情况,如果 realloc 找不到合适的空间,就会返回空指针。我们想让它增容,他却存在返回空指针的危险,那怎么办呢?
解决方法:可以先用临时指针判断一下是否为空。
int main()
{
int* p = calloc(10,sizeof(int)); // 开辟10个整型空间,并且初始化为0
if (p == NULL)
{
return 1;
}
int i = 0;
for (i = 0; i < 10; i++)
{
printf("%d ", *(p + i));
}
//如果需要20给int的空间
//realloc扩容函数
int*ptr = (int*)realloc(p, 80);
if (ptr!=NULL)
{
p = ptr;
}
free(p);
p = NULL;
return 0;
}
有趣的是,其实你可以把 realloc 当 malloc 用:
// 在要调整的内存地址部分,传入NULL:
int* p = (int*)realloc(NULL, 40); // 这里功能类似于malloc,就是直接在堆区开辟40个字节
三、常见的动态内存错误
1、对NULL指针的解引用操作
代码演示:
int main()
{
int* p = (int*)malloc(1000);
for (int i = 0; i < 1000; i++)
{
*(p + i) = i;//有可能是对空指针解引用,非法访问内容
}
free(p);
p = NULL;
return 0;
}
解决方案:对 malloc 函数的返回值做判空处理
int main()
{
int* p = (int*)malloc(1000);
if (p == NULL)
{
perror("malloc");
return 1;
}
for (int i = 0; i < 1000; i++)
{
*(p + i) = i;
}
free(p);
p = NULL;
return 0;
}
2、对动态开辟空间的越界访问
动态内存的开辟和数组是十分相似的,都是在连续的空间。
代码演示:
int main()
{
int* p = (int*)malloc(100);
if (p == NULL)
{
perror("malloc");
return 1;
}
for (int i = 0; i <= 25; i++)
{
*(p + i) = i;
}
free(p);
p = NULL;
return 0;
}
为了防止越界访问,使用空间时一定要注意开辟的空间大小。
3、对非动态开辟内存使用free释放
代码演示:
int main()
{
int a[] = { 1,2,3 };
int* p = &a;
free(p);
p = NULL;
return 0;
}
所以不要对非动态开辟的内存使用 free,否则会出现难以意料的错误。
4、使用free释放一块动态开辟内存的一部分
代码演示
int main()
{
int* p = (int*)malloc(100);
if (p == NULL)
{
perror("malloc");
return 1;
}
for (int i = 0; i < 10; i++)
{
*p = i;
p++;
}
free(p);
p = NULL;
return 0;
}
这么写代码会导致 p 只释放了后面的空间。没人记得这块空间的起始位置,再也没有人找得到它了,这是很件很可怕的事情,会存在内存泄露的风险。
释放内存空间的时候一定要从头开始释放
5、对同一块动态内存多次释放
代码演示:
int main()
{
int* p = (int*)malloc(100);
if (p == NULL)
{
perror("malloc");
return 1;
}
for (int i = 0; i < 10; i++)
{
*(p + i) = i;
}
free(p);
free(p);
p = NULL;
return 0;
}
解决方案:可以置为空指针之后再释放
free(p);
p = NULL;
free(p);
return 0;
6、动态开辟内存忘记释放(内存泄漏)
代码演示:
void test()
{
int* p = (int*)malloc(100);
if (p == NULL)
{
return;
}
}
int main()
{
test();
free(p); // 此时释放不了了,不知道这块空间的起始位置在哪了
p = NULL;
}
动态开辟的内存空间有两种回收方式:
1. 主动释放(free)
2. 程序结束
如果这块程序在服务器上 7x24 小时运行,如果你不主动释放或者你找不到这块空间了,最后就会导致内存泄漏问题。
内存泄漏(Memory Leak)是指程序中已动态分配的堆内存由于某种原因程序未释放或无法释放,造成系统内存的浪费,导致程序运行速度减慢甚至系统崩溃等严重后果。
malloc 这一系列函数 和 free 一定要成对使用,记得及时释放。
你自己申请的空间,用完之后不打算给别人用,就自己释放掉即可。如果你申请的空间,想传给别人使用,传给别人时一定要提醒别人用完之后记得释放。
四、经典笔试题
第一题
void GetMemory(char* p)
{
p = (char*)malloc(100);
}
void Test(void)
{
char* str = NULL;
GetMemory(str);
strcpy(str, "hello world");
printf(str);
}
int main()
{
Test();
return 0;
}
代码无任何输出
解析:str中存放的是NULL,所以这个时候 GetMemory中的参数p也会接收一个NULL,这个NULL是临时拷贝的,再把函数GetMemory中动态开辟的内存地址放到p中,随着GetMemory函数调用结束,p也随之销毁,但是malloc是在堆上开辟的内存,并不会销毁,因为p的销毁,就无法得到malloc开辟的内存空间地址,也就无法释放,导致内存泄漏。
因为是传值调用,并不会修改str,所以str依然是NULL,将"hello,world"拷贝到一个空指针中,就会造成非法访问,因为该指针并没有指向有效地址。所以没有任何输出。
修改后:
//修改后的
void GetMemory(char** p)//地址的地址,用二级指针
{
*p = (char*)malloc(100);
}
void Test(void)
{
char* str = NULL;
GetMemory(&str);//传递str的地址
strcpy(str, "hello world");
printf(str);
free(str);
str = NULL;
}
int main()
{
Test();
return 0;
}
hello world
第二题
char* GetMemory(void)
{
char p[] = "hello world";
return p;
}
void Test(void)
{
char* str = NULL;
str = GetMemory();
printf(str);
}
int main()
{
Test();
return 0;
}
烫烫烫烫烫烫烫烫\?=?
解析:
GetMemory 函数内部创建的数组是在栈区上创建的,出了函数 p 数组的空间就还给了操作系统,返回的地址是没有实际意义的,这个指针实际上是个野指针,如果通过返回的地址去访问内存,就会导致非法访问内存问题。
补充:栈空间内创建的的数据地址,不要随便返回,出栈之后数据会销毁。
第三题
以下代码出现什么问题,指出并修改
void GetMemory(char** p, int num)
{
*p = (char*)malloc(num);
}
void Test(void)
{
char* str = NULL;
GetMemory(&str, 100);
strcpy(str, "hello");
printf(str);
}
int main()
{
Test();
return 0;
}
没有释放内存,造成内存泄漏。
加上free即可
修改后:
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;
}
第四题
以下代码出现什么问题,指出并修改
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这时是一个野指针,free完后,应将str置为空指针。
修改后:
void Test(void)
{
char* str = (char*)malloc(100);
strcpy(str, "hello");
free(str);
str = NULL;
if (str != NULL)
{
strcpy(str, "world");
printf(str);
}
}
int main()
{
Test();
return 0;
}
五、C/C++程序的内存开辟
1. 栈区(stack):
在执行函数时,函数内局部变量的存储单元都可以在栈上创建,函数执行结 束时这些存储单元自动被释放。
栈内存分配运算内置于处理器的指令集中,效率很高,但是 分配的内存容量有限。 栈区主要存放运行函数而分配的局部变量、函数参数、返回数据、返 回地址等。
2.堆区(heap)
一般由程序员自主分配释放,若程序员不主动不释放,程序结束时可能由操作系统回收。其分配方式类似于链表。
3.数据段(data segment)
静态存储区,存放全局变量和静态数据,程序结束后由系统释放。
4. 代码段:(code segment)
存放函数体(类成员函数和全局函数)的二进制代码。
六、柔性数组
1、柔性数组介绍
定义:柔性数组(Flexible Array),又称可变长数组。一般数组的长度是在编译时确定,而柔性数组对象的长度在运行时确定。在定义结构体时允许你创建一个空数组(例如:arr [ 0 ] ),该数组的大小可在程序运行过程中按照你的需求变动。
出处:柔性数组(Flexible Array),是在C语言的 C99 标准中,引入的新特性。结构中的最后一个元素的大小允许是未知的数组,即为柔性数组。
例如:
typedef struct st_type
{
int i;
int a[0];//柔性数组成员
}type_a;
有些编译器会报错无法编译可以改成:
typedef struct st_type
{
int i;
int a[];//柔性数组成员
}type_a;
2、柔性数组的特点
1)结构中的柔性数组成员前面必须至少一个其他成员。
2)sizeof 返回的这种结构大小不包括柔性数组的内存。
3)包含柔性数组成员的结构用malloc ()函数进行内存的动态分配,并且分配的内存应该大于结构的大 小,以适应柔性数组的预期大小。
//code1
typedef struct st_type
{
int i;
int a[0];//柔性数组成员
}type_a;
printf("%d\n", sizeof(type_a));//输出的是4
3、柔性数组的使用
struct S
{
int num;
int arr[];
};
int main()
{
struct S* p = (struct S*)malloc(sizeof(struct S) + 40);//分配的内存应该大于结构的大小
if (p == NULL)
{
perror("malloc");
return 1;
}
p->num = 100;
for (int i = 0; i< 10; i++)
{
p->arr[i] = i;
printf("%d ", p->arr[i]);
}
//如果要继续使用的话,则需扩容,用realloc函数
printf("\n");
struct S* ptr = (struct S*)realloc(p, sizeof(struct S) + 80);
if (ptr == NULL)
{
perror("relloc");
return 1;
}
else
{
p = ptr;
}
for ( int i = 0; i < 20; i++)
{
p->arr[i] = i;
printf("%d ", p->arr[i]);
}
free(p);
p = 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 = 10;
ps->arr = (int*)malloc(10 * sizeof(int));
if (ps->arr == NULL)
{
return 1;
}
// 使用
int i = 0;
for (i = 0; i < 10; i++) {
ps->arr[i];
}
// 增容
int* ptr = (struct S*)realloc(ps->arr, 20 * sizeof(int));
if (ptr != NULL) {
ps->arr = ptr;
}
// 释放
free(ps->arr); // 先free第二块空间
ps->arr = NULL;
free(ps);
ps = NULL;
return 0;
}
4、柔性数组的优势
上述 代码1 和 代码2 可以完成同样的功能,但是 代码1 的实现有两个好处:
第一个好处是:方便内存释放
虽然 代码2 实现了相应的功能,但是和 代码1 比还是有很多不足之处的。代码2 使用指针完成,进行了两次 malloc ,而两次 malloc 对应了两次 free ,相比于 代码1 更容易出错如果我们的代码是在一个给别人用的函数中,你在里面做了两次内存分配,并把整个结构体返回给用户。
虽然用户调用 free 可以释放结构体,但是用户并不知道这个结构体内的成员也需要 free,所以你不能指望用户来发现这件事。所以,如果我们把结构体的内存以及其成员要的内存一次性分配好(而不是多次分配),并且返回给用户一个结构体指针,用户只需使用一次 free 就可以把所有的内存都给释放掉,可以间接地减少内存泄露的可能性。
第二个好处是:有利于访问速度
连续内存多多少少有益于提高访问速度,还能减少内存碎片。malloc 的次数越多,产生的内存碎片就越多,这些内存碎片不大不小,再次被利用的可能性很低。内存碎片越多,内存的利用率就会降低。频繁的开辟空间效率会变低,碎片也会增加。
因此,使用柔性数组,多多少少是有好处的。
由于作者水平有限,本文有错误和不准确之处在所难免,恳望读者读后发现,请批评指正!