初学C语言day07--指针与堆内存
什么是指针:
指针是一种特殊的数据类型,使用它可以定义指针变量,指针变量中存储的是整形数据,该整型数据代表了内存的编号(地址),可以通过这个编号访问对应的内存
为什么要使用指针:
1、函数之间是相互独立的,但是有时候需要共享变量
传参是单向值传递
全局变量可以共享,但是容易命名冲突
使用数组可以共享,但是要额外传递长度、不方便
虽然函数之间命名空间是独立的,但是地址空间是同一个,所以通过指针可以解决函数之间共享变量的问题
2、由于函数之间普通变量的传参是单向值传递(拷贝),对于字节数多的参数,值传递的效率很低,如何传递变量的地址,只需要传递首地址即可,字节数4或8个字节,使用指针可以提高传参效率
3、堆内存无法取名字,它不像data\bss\stack内存段可以让变量名与内存之间建立联系,只能使用指针变量记录存储数据的堆内存的地址
如何使用指针:
定义:
类型名* 变量名_p;
1、指针变量与普通变量的用法上有很大区别,建议在取名时以p结尾加以区分
2、指针的类型表示该内存存储的是什么类型的变量的数据,通过该类型决定了该指针变量访问的字节数
3、一个*只能定义一个指针变量
4、指针变量与普通变量一样,默认值是随机的,一般初始化为NULL
int *p1,p2,p3; // p1是指针,p2p3是int
int *p1,*p2,*p3; // p1p2p3都是指针
赋值:
变量名_p = 地址; // 必须是有权限且有意义的地址
指向栈内存:
变量名_p = &普通变量;
指向堆内存:
变量名_p = malloc(4);
解引用:
通过指针变量中记录的地址编号,访问对应的内存
该过程运行时可能会产生段错误,原因是该指针变量中存储的内存编号是非法的
注意:解引用时访问的字节数由定义时指针的类型决定
*变量名_p;
使用指针需要注意哪些问题:
空指针:
指针变量的值为NULL,都称为空指针
对空指针解引用一定会产生段错误
一般用空指针对指针变量初始化
当函数的返回值为指针类型时,如果函数执行出错时,返回值可以返回NULL作为错误标志
if(NULL == p) if(!p)
注意:绝大多数的系统中NULL是0,个别系统中是1
如何避免空指针带来的段错误的:
使用来历不明的指针前先判断:
1、当调用的函数的返回值是指针类型时,有可能会返回一个空指针
2、当函数的参数是指针类型时,别人传的参数可能是一个空指针
野指针:
指向不确定的内存空间的指针,称为野指针
对野指针解引用的后果:
1、一切正常
2、段错误
3、脏数据
野指针比空指针危害更大,因为野指针无法通过判断语句分辨出来,而且可能是隐藏性的问题,短时间内不暴露而已
野指针都是人为制造,如何避免产生野指针:
1、定义指针变量时一定要初始化
2、函数不要返回栈内存的地址
3、当指针指向的内存被释放后,指针要及时置空NULL
指针的运算:
指针变量中存储的是整型,理论上整型可以使用的运算,指针变量也可以使用,但是绝大多数没有意义以及不被允许使用
只有以下:
指针 + n <=> 指针编号+n * 指针类型字节数 得到依然是一个临时指针
相当于前进了n个元素
指针 - n <=> 指针编号-n * 指针类型字节数 得到依然是一个临时指针
相当于后退了n个元素
指针A - 指针B <=> (指针A编号-指针B编号) / 指针类型字节数
相当于计算 A 和 B 之间间隔的元素个数
注意:A B 必须类型相同才能相减
指针与const(就近原则)
常量指针
const int * p; //保护指针所指向的内存不被修改
int const * p; //同上
指针常量
int * const p; //保护指针的指向不能修改
const int* const p; //既保护指向的内存,也保护指针的指向不被修改
int const * const p; // 同上
当为了提高传参效率而使用指针时,虽然传参效率提高了,但是变量有被修改的风险,因此可以配合const保护指针所指向的内存
指针数组和数组指针:
指针数组:
由相同类型的指针变量组成的数组,成员都是相同类型的指针变量
int* arr[10];
数组指针:
专门指向数组的指针
int p;
int (p)[10]; //p是指向长度为10,类型为int的数组的数组指针
数组名与指针的关系:
数组名是一种特殊的"指针",是常量,不能修改它的值,它代表的地址与数组的首地址之间存在映射关系
指针与内存之间是指向关系,是变量,可以修改指针变量的值
数组名可以当做指针使用,同时,指向数组首地址的指针也可以当做数组使用
int arr[10];
int* p = &arr[0];
*(arr+i) == *(p+i);
arr[i] == p[i];
二级指针
二级指针就是指向指针的指针,里面存储的是指针变量的地址
定义:
类型名** 变量名_pp;
赋值:
变量名_pp = &指针变量;
解引用:
*变量名_pp == 指针变量 **变量名_pp == *指针变量 == 普通变量
注意:当函数之间需要共享一级指针时,参数需要传递二级指针
函数指针
函数名就是个地址(整数),代表了该函数在代码段中的位置
函数指针就是专门指向函数的指针,里面存储的是函数的在代码段中的首地址
例如:
void (*funcp)(int,int);
funcp是一个指向参数为int\int,返回值为void的函数的函数指针
int scanf(const char *format, ...);
回调模式: 当函数中需要调用调用者提供的函数时,参数中就需要使用函数指针,这种调用方式称为回调模式
void qsort(void *base, size_t nmemb, size_t size, int (*compar)(const void *, const void *));
万能指针 void* 在C语言中任意类型的指针都可以自动转换成void* 并且void*也可以自动转换成任意类型
什么是堆内存
是进程中的一个内存段(text\data\bss\stack\heap),由程序员手动管理
优点:足够大
缺点:使用麻烦且具备一定危险性
为什么要使用堆内存
1.随着程序的复杂,数据量增多,其他内存段已经不够用了
2.其它内存段的申请和释放不受控制,但是堆内存的申请和释放是受控制
如何使用堆内存:
注意:C语言中没有任何管理堆内存的语句,只能通过标准库的函数进行管理堆内存
void * malloc(size_t size);
功能:
从堆内存中申请size个字节的连续内存,内存中的数据值不确定
返回值:
成功时返回该内存的首地址,失败时返回NULL
int* p = malloc(4);
void free(void *ptr);
功能:
释放一块连续的堆内存
注意:free仅仅是释放使用权,但是内存中的数据不会全部清理
注意:不能重复释放,必须释放有效内存
注意:可以free(NULL),但无意义 free(p);
void *calloc(size_t nmemb, size_t size);
功能:从堆内存中申请nmemb块,每块size个字节的内存,得到的依然是一块连续的内存
注意:通过calloc申请到的内存会被初始化为0
void *realloc(void *ptr, size_t size);
功能:改变已有堆内存块的大小
ptr:旧堆内存首地址
size:调整后的堆内存大小
返回值:成功返回调整后的堆内存首地址
注意:一定要重新接收,因为有可能不是在原地址上调整
如果无法在原基础上调整大小:
1、申请一块符合新要求的连续内存
2、拷贝旧内存中数据到新内存中
3、释放旧内存,并返回新内存首地址
malloc的内存管理机制:
1、当首次通过malloc申请堆内存时,malloc会向操作系统申请内存,操作系统会直接分配33页(1页=4096字节)交给malloc管理,这样可以减少操作系统的运转次数,但是这样不意味着可以随意越界访问,因为malloc可能会继续分配给其他人,如果越界就会产生脏数据
2、连续通过malloc申请内存时,每个内存块之间会有一些空隙(4~12字节),这些空隙一部分是为了访问内存时对齐,可以加快内存的访问速度,其中有4个字节的空隙是为了记录malloc的维护信息,如果维护信息被破坏,会导致下一次free时出现内存崩溃
堆内存越界的后果:
1、一切正常
2、段错误
3、脏数据
4、如果破坏了malloc的维护信息,会影响下一次free
使用堆内存需要注意的问题:
1、内存泄漏
无法释放,又无法使用的内存,而想要再次使用内存时,又重新申请,然后继续重复以上过程,长期以往会导致系统中可使用的内存越来越少,这种情况称为内存泄漏
注意:程序一旦结束,属于它的所有资源都会被操作系统回收
如何尽量避免产生内存泄漏:
谁申请的,谁释放
谁知道该释放,谁释放
你有没有定位内存泄漏的方式:(上网搜)
1)查看进程的内存使用情况
windows:任务管理器
Linux: ps -aux
2)代码分析工具 mtrace
3)重新封装malloc、free函数
void* zz_malloc(size_t size)
{
void* p = malloc(size);
// 记录此函数调用时间、行数、所处函数等等信息到日志中
}
2、内存碎片
已经释放了但是又无法使用的内存叫做内存碎片,是由于申请和释放的时间不协调导致的,只能尽量减少不能避免
如何减少内存碎片的产生:
1)尽量申请大块内存自己管理
2)不要频繁地申请释放内存
3)使用栈内存不会产生内存碎片
内存清理函数
void bzero(void *s, size_t n);
头文件:#include <strings.h>
功能:把一段内存清理为0
s:待清理内存的首地址
n:待清理内存的字节数
void *memset(void *s, int c, size_t n);
头文件:#include <string.h>
功能:把一段内存按字节设置为c
s:待设置内存的首地址
c:想要设置的ASCII码值
n:待设置内存的字节数
返回值:设置成功后的内存首地址,为了链式调用(一个函数的返回值作为另一个函数的参数)
memset(memset(p,0,40),1,40);
memcpy memmove memcmp
在堆内存定义二维数组
指针数组:
类型名* arr[n];
for(int i=0; i<n; i++)
{
arr[i] = malloc(m*sizeof(类型));
// 申请n行m列的二维数组
注意:每一行的列数可以不相同,从而得到不规则的二维数组
}
缺点:容易产生内存碎片
数组指针:
类型名 (arrp)[m] = malloc(sizeof(类型)m*n);
得到n行m列的二维数组,而且在内存中全部连续
优点:不容易产生内存碎片
缺点:相对而言对内存的要求高
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 震惊!C++程序真的从main开始吗?99%的程序员都答错了
· 【硬核科普】Trae如何「偷看」你的代码?零基础破解AI编程运行原理
· 单元测试从入门到精通
· 上周热点回顾(3.3-3.9)
· winform 绘制太阳,地球,月球 运作规律