动态内存管理(变量和数组都是固定了用多少个字节的内存来储存,但是有没有可变内存大小来开辟空间的呢当然有啦,动态内存的管理,将带你打开新世界的大门。可变的动态内存,任君开辟)

前言:

malloc,calloc,realloc,free

这四个函数将是今天的主角

它们都有的个性有如下几点:

1.前面三个函数都得和free搭配使用,前三个函数用于开辟空间,free用于释放空间

2.都有可能开辟空间失败,所以在开辟空间之后一定要判断返回的是否是空指针后,再使用

3.返回值都是void*,即所开辟空间的首地址.需要用的时候,强制类型转化为里面存的什么元素的对应指针即可

4.使用是要引文件   #include <stdlib.h>

5.动态内存所开辟的空间都是在堆区上开辟的(后面也会详细讲一下内存分区)

(变量和数组都是固定了用多少个字节的内存来储存,但是有没有可变内存大小来开辟空间的呢当然有啦,动态内存的管理,将带你打开新世界的大门。可变的动态内存,任君开辟)

int main()

{

int a = 10;//变量

int arr[10];//数组

return 0;

}

一、malloc,calloc,realloc,free

1.malloc函数

首先我们来认识一下  malloc函数

void* malloc(size_t);

1.返回值为 void*,即开辟空间的首元素地址。可以强制类型转化为所需要的各种类型

  但要注意的是,如果malloc函数开辟空间失败,会返回NULL,空指针,若是没判断就对其进行了解引用操作,程序会崩

2.参数为size_t,及开辟多少个字节的空间,size_t是unsigned int 的缩写,即无符号整型

3.malloc函数只开辟空间,不会初始化里面的元素,这也是malloc和colloc最大的差别

malloc的使用

int main()

{

int* p = (int*)malloc(40);//开辟40个字节的空间,用来存放10个int型数据

if (p == NULL)

{

perror("malloc");//打印错误的函数,用于提示我们哪里错了

return 1;

}

//开辟成功

for (int i = 0; i < 10; i++)

{

printf("%d\n", *(p + i));//malloc没有初始化空间,所以打印出来必然是随机值

/*

*(p+i),  p[i]并不会改变p的指向,但是 p++会改变,但是malloc函数开辟了空间需要用free来找到所开辟空间的首地址,才好释放

*/

}

//用动态内存开辟了空间只有两种方法释放,1.free  2.程序退出

//动态内存是放开了申请空间的权限,但是维护起来就需要细心了

free(p);

p = NULL;//释放了该空间后,p还记着那个地址呢,所以一定要记住置它为空,否则容易导致非法访问



return 0;

}

2.calloc函数

然后我们来认识一下  calloc函数

 void*  calloc( size_t num,size_t size )

 1.两个参数        size_t num和size_t size,num是开辟多少个字节,size是指每个元素占多大内存

 2.返回值为 void*

 3.calloc开辟的空间里的元素会初始化为0

calloc的使用

int main()

{

int* p = (int*)calloc(INT_MAX, sizeof(int));//INT_MAX是整型能表示的最大值2147483647,用于测试开辟失败

if (p == NULL)

{

perror("calloc");

return 1;

}

//打印数据

for (int i = 0; i < 10; i++)

{

printf("%d\n", p[i]);

}

//释放

free(p);

p = NULL;

return 0;

}

3.reallo函数

前面的两个函数都只是能开辟空间,只是在为realloc做准备,realloc才能真正调整所开辟的空间大小

接下来我们来学习一下realloc函数

void* realloc (void* ptr, size_t size);

1.两个参数ptr 是要调整的内存地址,size 调整之后新大小

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

3.这个函数调整原内存空间大小的基础上,还会将原来内存中的数据移动到新的空间。

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

情况1:原有空间之后有足够大的空间

当是情况1 的时候,要扩展内存就直接原有内存之后直接追加空间,原来空间的数据不发生变化

情况2:原有空间之后没有足够大的空间

原有空间之后没有足够多的空间时,扩展的方法是:在堆空间上另找一个合适大小

的连续空间来使用。这样函数返回的是一个新的内存地址。

由于上述的两种情况,realloc函数的使用就要注意一些。

int main()

{

int* p = (int*)malloc(40);

if (p == NULL)

{

perror("malloc");

return 1;

}



//开辟成功

//赋初始值

int i = 0;

for (i = 0; i < 10; i++)

{

p[i] = i;

}



//调整(如果小于原空间,则是缩小)

int* ptr = (int*)realloc(p, 60);//扩容到60个字节,也就是增加了20个字节,多放了5个整型

if (ptr == NULL)

{

perror("realloc");

return 1;

}

for (i = 0; i < 15; i++)

{

printf("%d\n", ptr[i]);

}

//释放

free(ptr);

ptr= NULL;



return 0;

}

常见的动态内存错误

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

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

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

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

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

6. 动态开辟内存忘记释放(内存泄漏)(忘记释放不再使用的动态开辟的空间会造成内存泄漏。)

下面针对每个常见内存错误写出相应的 示例代码

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

}

//请问运行Test 函数会有什么样的结果?



//解析

void GetMemory(char* p)//这里的形参会开辟一块临时的空间,用于存放指针变量p, p里面存的是NULL.但是p与str没有半毛钱关系

{

//错1

p = (char*)malloc(100);//这里给p在堆区上开辟了一份动态空间,

   //但p在出了这个函数作用域后,被销毁了,这份空间还在,也就没法子销毁了

}

void Test(void)

{

char* str = NULL;

GetMemory(str);//这里传参,传的是指针变量,而非地址,相当于把str指向的NULL字符串传给了形参p



//错2

strcpy(str, "hello world");//strcpy函数里必然会对str指针进行解引用操作,但是str仍然是NULL,解引用空指针本身就是错的

printf(str);

}





//题目2:

char* GetMemory(void)

{

char p[] = "hello world";

return p;

}

void Test(void)

{

char* str = NULL;

str = GetMemory();

printf(str);

}

//请问运行Test 函数会有什么样的结果?



//解析

char* GetMemory(void)

{

char p[] = "hello world";

return p;//p在除了这个函数后销毁了,但是却返回了地址

}

void Test(void)

{

char* str = NULL;

str = GetMemory();//str记住了函数返回的地址,但是这个地址已经还给系统了

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

}

//请问运行Test 函数会有什么样的结果?



//解析

void GetMemory(char** p, int num)

{

*p = (char*)malloc(num);//无法释放空动态内存所开辟的空间

}

void Test(void)

{

char* str = NULL;

GetMemory(&str, 100);

strcpy(str, "hello");

printf(str);

}





//题目4:

void Test(void)

{

char* str = (char*)malloc(100);

strcpy(str, "hello");

free(str);

if (str != NULL)

{

strcpy(str, "world");

printf(str);

}

}

//请问运行Test 函数会有什么样的结果?



void Test(void)

{

char* str = (char*)malloc(100);

strcpy(str, "hello");

free(str);//释放了没有置空该指针,该指针仍然存有地址,

if (str != NULL)

{

strcpy(str, "world");//会导致这两句代码语句非法访问

printf(str);

}

}

二、内存分配

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

 1. 栈区(stack):在执行函数时,函数内局部变量的存储单元都可以在栈上创建,函数执行结 束时这些存储单元自动被释放。栈内存分配运算内置于处理器的指令集中,效率很高,但是 分配的内存容量有限。 栈区主要存放运行函数而分配的局部变量、函数参数、返回数据、返 回地址等。

 2. 堆区(heap):一般由程序员分配释放, 若程序员不释放,程序结束时可能由OS回收 。分 配方式类似于链表。

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

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

 有了这幅图,我们就可以更好的理解在《C语言初识》中讲的static关键字修饰局部变量的例子了。

实际上普通的局部变量是在栈区分配空间的,栈区的特点是在上面创建的变量出了作用域就销毁。 但是被static修饰的变量存放在数据段(静态区),数据段的特点是在上面创建的变量,直到程序 结束才销毁 所以生命周期变长

三、柔性数组

1.结构中的柔性数组成员前面必须至少一个其他成员。

2.sizeof 返回的这种结构大小不包括柔性数组的内存。

3.包含柔性数组成员的结构用malloc ()函数进行内存的动态分配,并且分配的内存应该大于结构的大 小,

以适应柔性数组的预期大小。

柔性数组的使用

typedef struct st_type

{

 int i;

int a[0];//柔性数组成员

 }type_a;

 printf("%d\n", sizeof(type_a));//输出的是4

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

 

 

上述代码1和 代码2可以完成同样的功能,但是 第一个好处是:方便内存释放 方法1的实现有两个好处: 如果我们的代码是在一个给别人用的函数中,你在里面做了二次内存分配,并把整个结构体返回给 用户。用户调用free可以释放结构体,但是用户并不知道这个结构体内的成员也需要free,所以你 不能指望用户来发现这个事。所以,如果我们把结构体的内存以及其成员要的内存一次性分配好 了,并返回给用户一个结构体指针,用户做一次free就可以把所有的内存也给释放掉。 第二个好处是:这样有利于访问速度. 连续的内存有益于提高访问速度,也有益于减少内存碎片。(其实,我个人觉得也没多高了,反正 你跑不了要用做偏移量的加法来寻址)

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值