目录
1、内存
1.1 内存布局
在开始学习之前,先来了解内存的布局。也就是 C 语言中内存是如何布局的呢?
在 C 语言中,一般把内存分为 5 个区域,分别为全局区(静态区)、代码区、常量区、堆、栈。( 注意,这里所说的堆、栈是两个不同的概念,要与数据结构中的堆栈分开。不过也有人把二者合称为堆栈,因此,有人称内存分为 4 个区域。)
下面对这五个区域进行简单介绍:
(1)全局区:存放在这个区域的数据,在整个程序的运行期间都是有效的,也就是生存期贯穿整个程序运行期。全局区是用来存放全局变量、静态变量。全局区的分配、释放均由编译器自动完成。全局区也称为静态区。
(2)代码区:显而易见,就是存放程序的区域。
(3)常量区:编程的时候,经常会用到大量常量,这些常量在程序运行期间不会改变,这类数据就存放在常量区。常量区的分配、释放也是由编译器自动完成。
(4)堆区:程序运行期间,可能临时产生大量有用的数据,但这些数据只是临时的,并不需要程序持续保留,这个时候就需要临时分配一些内存空间来保存。当这些数据不再使用时,我们就没有再保留它们的必要了,弃之即可。而内存大小是有限的,不能随意丢弃不管。所以,不再保留这些数据时,还得收回分配的内存空间。这就是内存的动态分配和释放的原因。整个这些操作就是在所谓的堆区进行的。显然,编译器是不知道什么时候有临时数据,而需要分配内存的。所以堆区进行的内存分配释放是编程人员控制的。由于有分配,而且可释放,所以称为动态内存分配。
(5)栈区:在没有操作系统的 C 语言编程中,除了 main 函数,都是可以被调用的函数。在被调函数中由函数本身、且只供函数本身使用的变量,称为局部变量。这些变量在调用函数时分配空间,调用函数结束后,自动释放空间。这类数据就是存放在栈区。类似的数据还有调用函数时的形参等。
如下图:
1.2 为什么存在动态内存的分配
为什么存在动态内存分配呢? 前面学到开辟空间的方式有:
int value = 10;//在栈空间上开辟了四个字节
int array[10] = { 0 };//在栈空间上开辟 40 个字节的连续空间
但是上述的开辟空间的方式有两个特点:
(1)空间开辟大小是固定的。
(2)数组在申请的时候,必须指定数组长度,它所需要的内存在编译时分配。
但是对于空间的需求,不仅仅是上述的情况。有时候我们需要的空间大小在程序运行的时候才知道,那么数组在编译时开辟空间的方式就不能满足了。这就是为什么存在动态内存的分配了。
2、动态开辟内存函数的介绍和使用
需要存放的临时数据无限,内存有限。编程人员,需要随着程序运行,随时分配内存空间,并且随着临时数据的失效,及时地回收,正所谓动态分配内存。动态分配内存,分配的是堆区的内存空间。 分配内存的函数有多个不同的原型,这些都集成在库函数 stdlib.h 中。
2.1 malloc
函数原型如下: 该函数向内存申请一块连续可用的空间,并返回指向这块空间的指针。
void* malloc (size_t size);
函数说明:size_t 是什么呢?size_t是标准C库中定义的,在64位系统中为long long unsigned int,非64位系统中为long unsigned int。①调用该函数,需要用户指定分配内存空间的大小。分配成功后,系统会为用户分配一块内存空间,空间大小为 size 字节,其值是随机值。该函数的返回值是 void 类型的指针,该指针指向分配的这块内存。②由于是 void 类型的指针,所以在使用时,需要把该指针强制转换成需要的类型。③内存空间有限,如果指定的大小超过了可分配内存空间的大小,则有可能分配失败。如果分配失败,返回的则是NULL。所以,在使用动态分配的内存空间之前,首先要判断是否分配成功。④由于 malloc 函数只是分配内存,并不进行初始化,所以分配成功后,这块内存区域为随机值。
总结:
(1)如果开辟成功,则返回一个指向开辟好空间的指针。
(2)返回值的类型是 void* ,所以 malloc 函数并不知道开辟空间的类型,需要使用者把该指针强制转换成需要的类型。
(3)如果开辟失败,则返回一个NULL指针,因此 malloc 的返回值一定要做检查。
(4)新分配的内存块的内容未初始化,保留不确定的值。
(5)如果参数 size 为 0,malloc的行为是标准是未定义的,取决于编译器。(size_t size :开辟的空间大小,单位字节)
使用 malloc 函数开辟空间简单的例子:
#include<stdio.h>
#include<stdlib.h>
#include<errno.h>
//malloc 函数的简单例子
int main()
{
int* ptr = (int*)malloc(10 * sizeof(int));//开辟了一块 10*4=40 字节大小的空间,并且放回一个指向开辟好空间的指针
if (ptr == NULL)//对 malloc 函数的返回值进行检测,防止开辟空间失败
{
printf("%s\n", strerror(errno));//打印错误原因的一个方式
}
else//检测没有问题就可正常使用开辟好的这块内存空间了
{
//正常使用
int i = 0;
for (i = 0; i < 10; i++)//对这块空间赋值
{
*(ptr + i) = i;
}
for (i = 0; i < 10; i++)//打印该空间中的数据
{
printf("%d ", *(ptr + i));
}
}
//当动态申请的空间不再使用的时候
//就应该还给操作系统
free(ptr);//释放开辟好的空间,还给操作系统,下面将会学到该函数,这里为了程序的正确性。
ptr = NULL;//并且把该指针置为 NULL(空)。防止该指针还指向已经被释放(被回收)的内存空间,导致非法访问。
return 0;
}
2.2 free
内存有限,分配出去的内存空间在不用时要及时回收。分配、回收配对使用,无疑是个很好的办法。内存的回收,也称内存的释放。与动态分配一样,同样需要编程人员来完成。
函数原型如下: 专门是用来做动态内存的释放和回收的。
void free (void* ptr);
动态分配的内存使用结束后,要及时释放。调用该函数,指定需要释放的内存空间地址,即可完成释放。需要注意的是,内存释放与指针的关系。内存释放,只是把这块内存的数据变成无效数据,而指针 ptr 依然指向这块内存,虽然这块内存的数据无效,此时 ptr 已经成为野指针了,如果对该指针进行操作的话会导致非法访问内存,造成不可避免的损失。
为了防止该指针在这种情况下被继续使用,释放内存后,要及时把指针指向 NULL,这样的话下次使用该指针时,通过判错功能 if (ptr==NULL) 就可阻止使用无效指针。也就是说,放内存的同时,也要释放指针。同理,如果单纯的把指针指向 NULL,也是不行的,释放指针并不等于释放内存。
总结:(1)ptr 是指向一块内存空间的地址。(2)free 函数用来释放动态开辟的内存。(3)free 函数释放动态开辟的内存后,要把指向动态开辟内存的指针置NULL。(4)不能单纯只把指针指向NULL,不对内存进行释放,会导致这块开辟的内存没有没有相应的指针进行管理。(5)如果参数 ptr 指向的空间不是动态开辟的,那 free 函数的行为是未定义的。(6)如果参数 ptr 是NULL指针,则函数什么事都不做。(7)free 函数要与开辟内存空间的函数成双成对的存在,防止开辟好的内存没有回收,导致内存泄漏。 (free 函数使用相应的例子如上)
2.3 calloc
函数原型如下:calloc 函数也是用来动态内存分配的。
void* calloc (size_t num, size_t size);
调用该函数,同样需要用户指定相应的参数,参数包括元素的数量和每个元素的字节数,这一点不同于 malloc 函数。calloc 分配的内存空间大小,由 num、size 两个参数决定。如果分配成功分配所得的内存空间大小为 num* size 字节,并且内存空间被初始化为 0 或 NULL。该函数返回的同样是指向这块内存的指针。如果分配失败,返回的则是 NULL。所以,该指针使用前同样建议进行是否为NULL的判断。
总结:
(1)函数的功能是为 num 个大小为 size 的元素开辟一块空间,并且把空间的每个字节初始化为 0。
(2)与函数 malloc 的区别只在于 calloc 会在返回地址之前把申请的空间的每个字节初始化为全 0。
使用 calloc 函数开辟空间简单的例子:
#include<stdio.h>
#include<stdlib.h>
#include<errno.h>
//calloc
int main()
{
//int* ptr = (int*)malloc(sizeof(int))//效率高但不初始化
int* ptr = (int*)calloc(10, sizeof(int));//效率低一些,但是全部初始化为0(开辟好空间了,初始化后,再返回指向该空间的地址)
if (ptr == NULL)//对 calloc 函数的返回值进行检测,防止开辟空间失败
{
printf("%s\n", strerror(errno));//打印错误原因的一个方式
}
else//打印出该空间的数据
{
int i = 0;
for (i = 0; i < 10; i++)
{
printf("%d ", *(ptr + i));
}
}
free(ptr);//释放开辟好的空间,还给操作系统。
ptr = NULL;//并且把该指针置为 NULL(空)。防止该指针还指向已经被释放(被回收)的内存空间,导致非法访问。
return 0;
}
2.4 realloc
realloc 函数的出现让动态内存管理更加灵活。有时候我们发现过去申请的空间太小了,有时候我们又会觉得申请的空间过大了,那为了合理的使用内存,我们一定会对内存的大小做灵活的调整。那 realloc 函数就可以做到对动态开辟内存大小的调整。
函数原型如下: realloc 函数用于修改一个原先已经分配的内存块的大小。也可以说是对已有的内存空间进行重新分配。
void* realloc (void* ptr, size_t size);
void 类型的指针 ptr 指向已有的内存空间,size 用来指定重新分配之后分配所得的整个空间大小。如果分配成功,返回指向新分配空间的指针;如果分配失败,同样返回 NULL。
使用这个函数,你可以使一块内存扩大或缩小。如果它用于扩大一个内存块,那么这块内存原先的内容依然保留,新增加的内存添加到原先内存块的后面,新内存并未以任何方法进行初始化。如果它用于缩小一个内存块,该内存块尾部的部分内存便被拿掉,剩余部分内存的原先内容依然保留。
如果原先的内存块无法改变大小,realloc 将分配另一块正确大小的内存,并把原先那块内存的内容复制到新的块上。因此,在使用 realloc 之后,你就不能再使用指向旧内存的指针,而是应该改用 realloc 所返回的新指针。
最后,如果 realloc 函数的第1个参数是NULL,那么它的行为就和 malloc 一模一样。
总结:
(1) ptr 是要调整的内存地址 ,size 是调整之后新大小。
(2)返回值为调整之后的内存起始位置。
(3)这个函数在调整原内存空间大小的基础上,还会将原来内存中的数据移动到新的空间。
realloc在调整内存空间的是存在两种情况:
(1)原有空间之后有足够大的空间。要扩展内存就直接原有内存之后直接追加空间,原来空间的数据不发生变化。
(2)原有空间之后没有足够大的空间。原有空间之后没有足够多的空间时,扩展的方法是:在堆空间上另找一个合适大小 的连续空间来使用。这样函数返回的是一个新的内存地址。
使用 realloc 函数修改内存空间简单的例子:
#include<stdio.h>
#include<stdlib.h>
#include<errno.h>
/* realloc 调整动态内存空间的大小 */
int main()
{
int* p = (int*)malloc(20);//开辟20个字节的空间
if (p == NULL)
{
printf("%s\n", strerror(errno));
}
int i = 0;
for (i = 0; i < 5; i++)
{
*(p + i) = i;
}
int* ptr = (int*)realloc(p, 40);//用一个新的指针变量来接收 realloc 函数的返回值
if (ptr != NULL)//判断是否调整成功
{
p = ptr;
for (i = 5; i < 10; i++)
{
*(p + i) = i;
}
for (i = 0; i < 10; i++)
{
printf("%d ", *(p + i));
}
}
//释放内存
free(p);
p = NULL;
return 0;
}
分析:这里使用 malloc 开辟的20个空间,假设在这里,20个字节不能满足我们的使用,我们希望能够有40个字节的空间,这里就可以使用 realloc 来调整动态开辟的内存空间了。
realloc 函数使用的注意事项:
(1)如果 p 指向的空间之后有足够的内存空间可以追加,则直接追加,然后返回 p。
(2)如果 p 指向的空间之后没有足够的内存空间可以追加,则 realloc 函数会重新找一块新的内存区域来开辟一块满足需求的空间,并且把原来内存中的数据拷贝过来,释放旧的内存空间,最后返回新开辟的内存空间地址。
(3)得用一个新的指针变量来接收 realloc 函数的返回值。
3、常见的动态内存错误
3.1 对NULL指针的解引用操作
#include<stdio.h>
#include<stdlib.h>
void test()
{
int* p = (int*)malloc(INT_MAX / 4);
*p = 20;//如果 malloc 开辟空间失败,那么 p 的值是NULL,解引用,就会有问题。对一个没有指向任何内存空间的指针进行解引用
free(p);
}
int main()
{
test();
return 0;
}
3.2 对动态开辟空间的越界访问
#include<stdio.h>
#include<stdlib.h>
void test()
{
int i = 0;
int* p = (int*)malloc(10 * sizeof(int));//开辟了一块40个字节大小的空间
if (NULL == p)//判断开辟的内存空间
{
exit(EXIT_FAILURE);//结束程序
}
for (i = 0; i <= 10; i++)//0~10 11*4=44个字节,访问的内存空间超过了开辟的内存空间,越界访问
{
*(p + i) = i;//当i是10的时候越界访问
}
free(p);
}
int main()
{
test();
return 0;
}
3.3 对非动态开辟内存使用 free 释放
#include<stdio.h>
#include<stdlib.h>
void test()
{
int a = 10;
int* p = &a;
free(p);// a 的空间栈区开辟出来的,不是在堆区开辟出来的,而 free 函数只能释放在堆区上开辟出来的动态内存空间
}
int main()
{
test();
return 0;
}
3.4 使用 free 释放一块动态开辟内存的一部分
#include<stdio.h>
#include<stdlib.h>
void test()
{
int a = 10;
int* p = &a;
free(p);// a 的空间栈区开辟出来的,不是在堆区开辟出来的,而 free 函数只能释放在堆区上开辟出来的动态内存空间
}
int main()
{
test();
return 0;
}
3.5 对同一块动态内存多次释放
#include<stdio.h>
#include<stdlib.h>
void test()
{
int* p = (int*)malloc(100);
p++;
free(p);//p不再指向动态内存的起始位置,不再是完整的动态内存开辟的空间
//free 只能从开辟好的内存空间起始位置开始释放,不能只释放一部分。
}
int main()
{
test();
return 0;
}
3.6 动态开辟内存忘记释放(内存泄漏)
#include<stdio.h>
#include<stdlib.h>
void test()
{
int *p = (int *)malloc(100);
if(NULL != p)
{
*p = 20;
}
}
int main()
{
test();
while(1);
}
忘记释放不再使用的动态开辟的空间会造成内存泄漏。切记: 动态开辟的空间一定要释放,并且正确释放 。
4、动态内存经典笔试题
题目 1:
#include<stdio.h>
#include<stdlib.h>
#include<string.h>
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;
}
存在问题:
(1)运行代码程序会出现崩溃的现象。
(2)程序存在内存泄漏的问题。
str 以值传递的形式给 p,p 是 GetMemory 函数的形参,只能函数内部有效等 GetMemory 函数返回之后,动态开辟内存尚未释放并且无法找到,所以会造成内存泄漏。如下图:
正确的形式如下:
①
#include<stdio.h>
#include<stdlib.h>
#include<string.h>
/* 正确的写法 */
void GetMemory(char** p)//二级指针接收一级指针的地址
{
*p = (char*)malloc(100);//解引用拿到str,将开辟的内存空间的地址赋值给str
}
void Test(void)
{
char* str = NULL;
GetMemory(&str);//传递地址,形参的改变能够影响到实参,目的是使str能够指向开辟的内存空间
strcpy(str, "hello world");
printf(str);
free(str);//释放开辟的动态内存
str = NULL;//同时将指向改地址的指针置NULL,防止野指针的存在,导致非法访问内存
}
int main()
{
Test();
return 0;
}
②
#include<stdio.h>
#include<stdlib.h>
#include<string.h>
/* 正确的写法 */
char* GetMemory(char* p)
{
p = (char*)malloc(100);
return p;
}
void Test(void)
{
char* str = NULL;
str=GetMemory(str);
strcpy(str, "hello world");
printf(str);
free(str);//释放开辟的动态内存
str = NULL;//同时将指向改地址的指针置NULL,防止野指针的存在,导致非法访问内存
}
int main()
{
Test();
return 0;
}
题目 2
#include<stdio.h>
#include<stdlib.h>
#include<string.h>
//返回栈空间的地址的问题
char* GetMemory(void)
{
char p[] = "hello world";//在栈上开辟了一个内存空间,存储着字符串 hello world
return p;//将开辟的地址返回出去
}//该函数结束时,会对在栈上开辟的空间进行回收,也就是把开辟的数组内存空间进行回收
void Test(void)
{
char* str = NULL;
str = GetMemory();//当str拿到函数返回回来的地址,
printf(str);//拿着地址去访问已经被回收的空间,导致非法访问内存,输出随机值。
}
int main()
{
Test();
return 0;
}
正确的改法:
#include<stdio.h>
#include<stdlib.h>
#include<string.h>
char* GetMemory(void)
{
static char p[] = "hello world";//此时存储该字符串的内存空间在静态区,就算该函数的生命周期结束了,也不会对该空间进行回收
return p;
}
void Test(void)
{
char* str = NULL;
str = GetMemory();
printf(str);
}
int main()
{
Test();
return 0;
}
题目 3
#include<stdio.h>
#include<stdlib.h>
#include<string.h>
void GetMemory(char** p, int num)//二级指针接收一级指针的地址
{
*p = (char*)malloc(num);//对二级指针解引用,拿到一级指针,将 malloc 开辟的内存的地址放到 srt 中
}
void Test(void)
{
char* str = NULL;
GetMemory(&str, 100);
strcpy(str, "hello");
printf(str);//内存泄漏,没有释放动态开辟的内存空间。有开辟没有释放,内存泄漏
//改进
//free(str);
//str=NULL;
}
int main()
{
Test();
return 0;
}
题目 4
#include<stdio.h>
#include<stdlib.h>
#include<string.h>
/*释放后的空间再次被使用*/
void Test(void)
{
char* str = (char*)malloc(100);
strcpy(str, "hello");
free(str);//释放内存了,没有把指向的指针置NULL,str成为野指针
if (str != NULL)
{
strcpy(str, "world");//非法访问内存,访问不属于自己的空间
printf(str);
}
}
int main()
{
Test();
return 0;
}
篡改动态内存区的内容,后果难以预料,非常危险。因为 free(str);之后,str 成为野指针,if(str != NULL) 语句不起作用。
正确的改法:
#include<stdio.h>
#include<stdlib.h>
#include<string.h>
/*释放后的空间再次被使用*/
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;
}