初学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列的二维数组,而且在内存中全部连续

优点:不容易产生内存碎片

缺点:相对而言对内存的要求高

posted @   BigBig飞  阅读(24)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· 震惊!C++程序真的从main开始吗?99%的程序员都答错了
· 【硬核科普】Trae如何「偷看」你的代码?零基础破解AI编程运行原理
· 单元测试从入门到精通
· 上周热点回顾(3.3-3.9)
· winform 绘制太阳,地球,月球 运作规律
点击右上角即可分享
微信分享提示