文章目录
一、为什么存在内存分配
1、数组的缺点:
开辟空间的大小被固定
数组开辟多少空间不缺点,有时候要程序运行时才知道
如果开辟太大空间就会造成空间浪费,并且不能通过手动释放空间
在函数内创建的数组,在函数运行结束后才会把空间释放掉
数组的长度不能在函数程序运行的过程中进行扩充或缩小
例如:
int main ()
{
int a =0;//4byte
int arr[10];//40byte
return 0;
}
小结:
1.空间被固定
2.数组的申明的时候,必须指定数组的长度,它所需要的内存在编译时分配
2.动态内存分配
a.计算机系统中几个内存区域
1)栈区:在栈里存放我们定义的局部变量,函数,形参,
2)堆区:堆注意是通过动态分配的存储空间。
3)静态区(全局区):在全局区里存储一下全局变量和静态变量
二、动态内存函数的
1.malloc
malloc用来开辟动态内存空间
void *malloc(size_t size)
这个函数向内存申请一块连续可用的空间,并返回指向这块空间的指针。
如果开辟成功,则返回一个指向开辟好空间的指针。
如果开辟失败,则返回一个NULL指针,因此malloc的返回值一定要做检查。
返回值的类型是 void* ,所以malloc函数并不知道开辟空间的类型,具体在使用的时候使用者自己来决定。
如果参数 size 为0,malloc的行为是标准是未定义的,取决于编译器。
2.free
c语言提供了free函数
free函数专门用来回收和释放动态内存空间
具体如下:
void free(void*ptr);
如果参数 ptr 指向的空间不是动态开辟的(不是malloc,calloc,realloc和动态内存的),
那free函数的行为是未定义的
如果参数 ptr 是NULL指针,则函数什么事都不做
通过上面的学习我们来用一下malloc和free函数把!
#include<stdio.h>
#include<stdlib.h>
int main()
{
int num = 0;
int* ptr = NULL;
scanf("%d", &num);
ptr = malloc(num * sizeof(int));
//初始化
//判断一下malloc是否开辟失败
if (ptr != NULL)
{
//开辟成功
for (int i = 0; i < num; i++)
{
*(ptr + i) = i;
}
}
free(ptr);//如果ptr是空指针那么,free函数什么也不干
// 如果ptr不等于空指针就释放malloc开辟的空间
//free函数不会改变ptr的地址
ptr = NULL;//内存已经释放了,为了不让ptr不是野指针,所以给ptr赋值空指针;
return 0;
}
3.问题
1.问题来了free怎么知道要释放多少空间?
答:free找到malloc存放的开辟空间大小数值。至于通过上面方法,要学到c++才知道呵呵哈哈哈。
2.怎样才会开辟失败。
答:例如开辟40亿个字节,开辟很大就会开辟失败。
举个哈哈的例子(帮助大家理解)
你呢,跟你girlfriend前期 相处非常好,你把女友电话记得牢牢的,有助于交流。
但是,有一天 你女朋友看不上你了,就free§【跟你分手了]
但是你§还死皮赖脸的记住你前女友的号码(动态开辟空间的地址),随时可能打电话骚扰她,并且找到她,影响她的生活,说明你很危险(访问该空间内容,并修改,该指针很危险)
这时候,你前女友为了你能断开念想(其实保护自己==维护程序安全)
你前女友 给你当头一棒(爆头),让你失忆了。 (p = NULL)
4.calloc
void * calloc(size_t num, size_t size);
- c语言还提供一个与malloc类似的函数, 在malloc基础上,给空间初始为0;
- 参数的意思就是 开辟num个大小为size的元素。
- 开辟成功后返回空间首地址
- 开辟失败就返回NULL;
#include<stdio.h>
#include<stdlib.h>
int main()
{
int* p =(int*) calloc(10, sizeof(int));
if (NULL != p)
{
//开辟成功
//使用空间
}
free(p);
p = NULL;
return 0;
}
#include<stdio.h>
#include<stdlib.h>
#include<string.h>
int main()
{
int* p = (int*)calloc(10, sizeof(int));
if (p == NULL)
{
printf("%s\n", strerror(errno));//如果开辟失败则打印错误
}
int i = 0;
for (i = 0; i < 10; i++)
{
printf("%d", *(p + i));//0000000000
}
free(p);
p = NULL;
return 0;
}
5. 问题
1.以后要用malloc和calloc函数?
答:你要初始化就calloc,不初始就malloc。
6.realloc
realloc函数的出现让动态内存管理更加灵活。
有时会我们发现过去申请的空间太小了,有时候我们又会觉得申请的空间过大了,那为了合理的时候内存,
我们一定会对内存的大小做灵活的调整。那 realloc 函数就可以做到对动态开辟内存大小的调整。 函数原型
如下:
void * realloc(void *memblock ,size_t size);
- ptr 是要调整的内存地址
- size 调整之后新大小
- 返回值为调整之后的内存起始位置。
7.realloc 调整内存空间有两种情况
- 原有空间之后有足够大的空间
- 原有空间之后没有足够大的空间
- 在上面的基础上,没有找到一块够大的空间,开辟失败
#include<stdio.h>
#include<stdlib.h>
#include<string.h>
int main()
{
int* p = (int*)calloc(10, sizeof(int));
if (p == NULL)
{
printf("%s\n", strerror(errno));//如果开辟失败则打印错误
}
int i = 0;
for (i = 0; i < 10; i++)
{
*(p + i) = i;
}
int* pp = (int*)realloc(p, 20 * sizeof(int));//防止增容失败,返回NULL,用临时指针
if (NULL != pp)
{
//增容成功、判断pp
p = pp;
}
else
{
return -1;
}
// 初始化增容后的空间
for (i = 10; i < 20; i++)
{
*(p + i) = i;
}
for (i = 0; i < 20; i++)
{
printf("%d ", *(p + i));//0 1 2 3 4 5 ~ 19
}
free(p);
p = NULL;
return 0;
}
8.常见的内存错误
- 对空指针解应用
int main()
{
int* p = (int*)malloc(40);
//如果开辟失败,就是对空指针解引用
*p = 0;
free(p);
p=NULL;
return 0;
}
int main()
{
int* p = (int*)malloc(40);
//如果开辟失败,就是对空指针解引用
// 所以要进行判断一下
if (p == NULL)
{
return -1;
}
*p = 0;
free(p);
p=NULL;
return 0;
}
- 对动态开辟的空间进行越界的访问
int main()
{
int* p = (int*)malloc(200);
//如果开辟失败,就是对空指针解引用
// 所以要进行判断一下
if (p == NULL)
{
return -1;
}
for(int i = 0 ;i<80;i++)
{
*(p+i)=i;
}
free(p);
p=NULL;
return 0;
}
- 对非动态内存空间使用free函数
#include<stdio.h>
#include<stdlib.h>
#include<string.h>
int main()
{
int a = 0;
int* p = &a;
free(p); // 直接奔溃
return 0;
}
- 使用free释放一块动态开辟内存的一部分
int main()
{
int* p = (int*)malloc(40);
p++;
free(p);// 程序崩溃
p == NULL;
return 0;
}
- 多次释放
int main()
{
int* p = (int*)malloc(40);
free(p);
free(p);
p == NULL;
return 0;
}
解决方案:
int main()
{
int* p = (int*)malloc(40);
free(p);
p==NULL;
free(p);//p是空指针,所以free函数什么也不干;
p == NULL;
return 0;
}
- 动态内存忘记释放(内存泄漏)
动态内存有两种释放的方式
1.主动free
2.程序退出的时候,申请的空间也会被没收
int main()
{
int* p = (int*)malloc(40);
if (p == NULL)
{
return -1;
}
// 使用后
// 忘记释放
getchar();//如果这个程序一直挂着,那么40个字节就一直存在
//
return 0;
}
如果是循环啥的,有可能内存就被一点点用光
几道金典面试题
题目1、
#include<stdio.h>
#include<string.H>
void GetMemory(char* p)
{
p = (char*)malloc(100);
}
void test(void)
{
char* str = NULL;
GetMemory(str);
strcpy(str, "hello");
printf(str);
}
int main()
{
test();
return 0;
}
这组代码在调用strcpy函数时,发生断言;
首先进入到test函数里,先将str置空指针,
GetMemory (char*p),p来接收str,一级指针传递给一级指针,是值传递,所以str是p的一份临时拷贝,形参的改变不会影响实参,
调用完malloc后返回开辟空间的首地址给p,p的改变不会改变str
所以此时str还是空指针
进入strcpy后,因为str是NULL,strcpy会调用assert函数,所以遇到空指针就会报错(断言);
小结:
导致程序崩溃,并且内存泄漏
内存泄漏,已经没有地址记录malloc开辟的空间,这块动态内存
不能够释放,除非程序结束时;
题目2、
char* GetMemory(void)
{
char p[] = "hello";
return p;
}
void test(void)
{
char* str = NULL;
str = GetMemory();
printf(str);
}
这道题主要的问题是:返回栈空间地址问题
由于p指向的这块数组是局部的,所以当返回p地址时,GetMeomery空间销毁,这时候p就是野指针,那么把p赋值给str,str也是野指针,str指向的值不确定,就好像接下来,要调用printf函数
需要在栈帧上压栈,给printf开辟一块空间,也就是说,之前给GetMemory的那块空间,已经被printf 覆盖了,所以,str指向的值就是一个随机的。
小结:
str是一个野指针,指向的内容不确定,
所以printf 打印出来的内容也不确定,随机值,有可能是hello!!!
题目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);
}
题目3与题目1,差不多,一个传地址,一个传值,这道题注意就是
内存泄漏,动态内存用完后,没用及时释放空间
解决
在printf后加代码free函数;
题目4、
void Test(void)
{
char *str = (char *) malloc(100);
strcpy(str, "hello");
free(str);
if(str != NULL)
{
strcpy(str, "world");
printf(str);
}
}
空间被释放
str非法访问
虽然打印出来是hello,但是
它本质就是错的,!!!!!!!!!!!!!!
九、 c/c++程序的内存开辟
- 栈区(stack):在执行函数时,函数内局部变量的存储单元都可以在栈上创建,函数执行结束时这些
存储单元自动被释放。栈内存分配运算内置于处理器的指令集中,效率很高,但是分配的内存容量有
限。 栈区主要存放运行函数而分配的局部变量、函数参数、返回数据、返回地址等。 - 堆区(heap):一般由程序员分配释放, 若程序员不释放,程序结束时可能由OS回收 。分配方式类似
于链表。 - 数据段(静态区)(static)存放全局变量、静态数据。程序结束后由系统释放。
- 代码段:存放函数体(类成员函数和全局函数)的二进制代码。
被static修饰的局部变量,我的理解是,把局部变量放到了静态区,然后,改变了生命周期,但是作用域;
十、柔细数组
也许你从来没有听说过柔性数组(flexible array)这个概念,但是它确实是存在的。 C99 中,结构中的最
后一个元素允许是未知大小的数组,这就叫做『柔性数组』成员。
typedef struct st_type
{
int i;
int a[0];
//int a[];或者这样
}; type_a;
柔性数组的特点
结构中的柔性数组成员前面必须至少一个其他成员。 sizeof 返回的这种结构大小不包括柔性数组的内存。 包含柔性数组成员的结构用malloc
()函数进行内存的动态分配,并且分配的内存应该大于结构的大小,以适应 柔性数组的预期大小。
例如:
typedef struct st_type
{
int i;
int a[0];
//int a[];或者这样
}; type_a;
printf("%d\n", sizeof(type_a));//输出4
这是一块包含柔性数组的结构体声明,并创建了一个结构体变量,这结构也存在内存对齐,并且在sizeof计算的时候,结构体大小不包含柔性数组的大小。
代码一
#include<stdio.h>
#include<errno.h>
#include<stdlib.h>
struct type
{
int i;
int a[];//假如有10个元素
};
int main()
{
struct type* p = (struct type*)malloc(sizeof(struct type) + sizeof(int) * 10);
if (NULL == p)
{
printf("%s\n", strerror(errno));
return -1 ;
}
//开辟成功
else
{
p->i = 100;
for (int i = 0; i < 10; i++)
{
p->a[i] = i;
}for (int i = 0; i < 10; i++)
{
printf("%d ",p->a[i]);
}
}
// 增容一下
struct type* ps = (struct type*)realloc(p, sizeof(struct type) + sizeof(int) * 20);
if (NULL == ps)
{
printf("增容失败\n");
return -1;
}
else
{
p = ps;
}
//使用
// 、、
//释放
free(p);
p = NULL;
return 0;
}
代码二
#include<stdio.h>
#include<errno.h>
#include<stdlib.h>
struct type
{
int i;
int* a;
};
int main()
{
struct type * ps =(struct type*)malloc(sizeof(struct type));
ps->i = 100;
ps->a = (int*)malloc(10 * sizeof(int));//维护动态内存地址
for (int i = 0; i < 10; i++)
{
ps->a[i] = i;
}
//维护动态内存地址
for (int i = 0; i < 10; i++)
{
printf("%d ",ps->a[i]);
}
//a空间不够需要调整了
int* ptr = (int*)realloc(ps->a, sizeof(int) * 20);
if (ptr == NULL)
{
printf("增容失败\n");
}
else
{
ps->a = ptr;
}
//使用
//、、
//释放
free(ps->a);
ps->a = NULL;
free(ps);
ps == NULL;
return 0;
}
简单的画两张图方便理解
第一个好处是:方便内存释放
如果我们的代码是在一个给别人用的函数中,你在里面做了二次内存分配,并把整个结构体返回给用户。用
户调用free可以释放结构体,但是用户并不知道这个结构体内的成员也需要free,所以你不能指望用户来发
现这个事。所以,如果我们把结构体的内存以及其成员要的内存一次性分配好了,并返回给用户一个结构体
指针,用户做一次free就可以把所有的内存也给释放掉。
第二个好处是:这样有利于访问速度 连续的内存有益于提高访问速度,也有益于减少内存碎片。(其实,我个人觉得也没多高了,反正你跑不了
要用做偏移量的加法来寻址)
十一、为什么连续的内存有易于提高访问速点?
局部性原理