C语言中的指针

本文详细探讨了C语言中指针的概念、使用技巧、运算规则,包括空指针与野指针的处理,以及堆内存的原理、优点与避免内存泄漏的方法。涵盖了指针数组、数组指针、数组名与指针的关系,以及二级指针、函数指针和内存分配与管理的实践案例。
摘要由CSDN通过智能技术生成

一、指针

什么是指针:

指针是一种特殊数据类型,使用它可以定义指针变量,指针变量存储的是整型数据,代表了内存的编号,通过这个编号可以直接访问对应的内存

为什么要用指针:

1、函数之间是相互独立的,但是有时候需要共享变量
    传参是单向值传递
    全局变量容易命名冲突
    使用数组麻烦、还需要额外传递长度
    虽然函数之间命名空间是独立的,但是地址空间是同一个编号,指针可以解决共享变量的问题
2、由于函数之间传参是值传递(内存拷贝),对于字节数较多的变量,值传递的效率较低,如果传递变量的地址只需要传递4 | 8 个字节
3、堆内存无法取名字,它不像data、bss、stack让变量名与内存建立联系,只能使用指针记录堆内存的地址来访问对应的内存

如何使用指针:

定义:   类型名* 变量名_p;
         int* num_p;
         int *num_p;
1、指针变量与普通变量的用法有很大的区别,建议在取名以p结尾以示区分
2、指针的类型表示存储的是什么类型变量的地址,它决定了通过这个指针变量可以访问的字节数
3、一个*只能定义一个指针变量
        int *a,b,c;         //p1是指针  p2p3是int
        int *p1,*p2,*p3     // p1p2p3都是指针
4、指针变量与普通变量一样默认值是随机的,一般初始化为NULL

赋值: 变量名_p = 地址;  //必须是有意义且有权限的地址
    指向栈内存:
        int num;
        int* p = #
    指向堆内存:
        int* p = malloc(4);

解引用: *变量名_p
    通过指针变量中记录的内存的编号去访问对应的内存,该过程可能会产生段错误,原因是里面存储的内存编号是非法的
    注意:访问的字节数由指针定义时类型决定,后面都不会改变

使用指针时需要注意的问题:

空指针:

值为NULL的指针变量叫做空指针

如果对空指针解引用一定会产生段错误

NULL一般作为一种错误标志,当一个函数的返回值是指针类型时,可以使用NULL作为函数执行出错的返回结果
如何避免空指针带来的段错误:
使用来历不明的指针前先做判断
        if(NULL == p)   if(!p)
        1、当函数的参数是指针,别人传给你的指针可能是空指针
        2、从函数中获取的返回值是指针类型时,可能会返回空指针
注意:NULL在绝大多数系统中是0,个别是1

野指针:

指向不确定的内存空间的指针叫做野指针
对野指针解引用的后果:
1、一切正常
2、段错误
3、脏数据

野指针比空指针的危害更严重,因为它无法判断出来,而且可能是隐藏性的错误,短时间不暴露
所有的野指针都是程序员自己制造出来的

如何避免产生野指针
1、定义指针变量时一定要初始化
    int* p = NULL;
2、函数不要返回栈内存(函数内局部变量)的地址
3、指针指向的内存被释放后,指针变量要及时置空NULL

二、指针的运算

指针变量中存储的是整数,理论上整数可以使用的运算符它都可以使用,但是绝大多数运算符无意义的

指针 + n    指针+指针类型宽度*n     <-->  前进n共和元素
指针 - n    指针-指针类型宽度*n     <-->  后退n共和元素
指针 - 指针 (指针-指针)/指针类型宽度    计算两个指针之间间隔了多少个指针元素

三、指针与const

当我们为了提高传参效率而使用指针作为函数参数时,传参效率提高了,但是变量被共享存在被修改的风险,可以使用const保护指针所指向内存
const int* p; 保护指针所指向的内存不被修改
int const *p; 同上
int* const p; 保护指针变量不被修改
const int* const p; 指针变量和指针所指向内存都不能修改
int const* const p; 同上

四、指针数组和数组指针

指针数组:

由指针变量组成的数组,它的成员都是类型相同的指针变量

    类型* arrp[长度];
    int* arrp[10];

数组指针:

是专门指向数组的指针

    类型 (*arrp)[长度];
    int (*arrp)[10];        

五、数组名与指针:

数组名就是一种特殊的指针
数组名是常量,不能修改它的值,数组名没有自己的存储空间,它与数组首地址之间是映射关系
    数组名 == &数组名
指针变量是拥有自己的存储空间,它与所指向的内存是指向关系
注意:当指针变量指向数组首地址时,指针可以当作数组名使用,数组名也可以当作指针使用
        数组名[i] == *(数组名+i)
        *(p+i) == p[i]
注意:数组作为函数的参数时蜕变成了指针,所以长度丢失

六、二级指针

二级指针就是指向指针的指针,里面存储的是指针变量的地址

定义:   类型名** 变量名_pp;
赋值:   变量名_pp = &指针变量;
解引用:
    *变量名_pp <==> 指针变量
    **变量名_pp <==> *指针变量 <==> 数据
注意:当需要函数之间共享指针变量,传递指针的地址(二级指针)

七、函数指针

函数名就是一个地址,函数名代表了函数在代码段中所处的入口位置

函数指针就是指向函数的指针,它里面存储的是函数在代码段中所处的入口位置地址

返回值类型 (*p)(类型1,类型2,...);
int func(int num1,double num2);
int (*funcp)(int,double);   //funcp专门指向func类型的函数指针

可以通过函数指针,把函数当作参数传递给另一个函数,这种方式称之为函数回调模式

void qsort(void *base, size_t nmemb, size_t size,int (*compar)(const void *, const void *));

八、堆内存

什么是堆内存

是进程的一个内存段(text\data\bss\heap\stack),由程序员手动管理

优点:

足够大

缺点:

使用麻烦

为什么要使用堆内存

1、随着程序的复杂,数据量变多
2、其他内存段的申请释放不受控制吗,堆内存的申请释放受控制,可以适时地节约内存

如何使用堆内存

注意:C语言中没有控制堆内存的语句,只能使用C标准库中的函数来
#include <stdlib.h>

void *malloc(size_t size);
功能:从堆内存中申请size个字节的内存,申请到的内存中数据的值不确定
返回值:成功时返回申请到的连续内存的首地址,失败返回NULL

void free(void *ptr);
功能:释放一块堆内存
ptr:要释放的堆内存的首地址
注意:free释放只是使用权限,数据不会全部清理
注意:不能连续释放,但是可以释放NULL

void *calloc(size_t nmemb, size_t size);
功能:从堆内存申请nmemb块size字节大小的内存
返回值:成功返回申请到的连续内存的首地址,失败返回NULL
注意:calloc申请到的内存会被初始化为0,速度比malloc慢
short* p = calloc(20,2)    == malloc(40)

void *realloc(void *ptr, size_t size);
功能:改变已有的堆内存的大小,size表示调整后的大小,在原有的基础上调大调小
返回值:调整后内存块的新首地址,一定要重新接收返回值,可能不是在原位置进行调整
    如果无法在原位置调整:
    1、申请一块新的符合大小的内存
    2、拷贝原内存中的数据
    3、释放原内存,返回新内存首地址

malloc的内存管理机制

1、当首次向malloc申请内存,malloc会向操作系统申请内存,操作系统会直接分配33页(1页=4096字节)堆内存交给malloc管理,但是并不意味着可以越界访问,因为malloc可能把其他的内存分配给"其他人",这样就会产生脏数据

2、每个内存块之间会有空隙(4~12字节),一部分空隙是为了内存的对齐,其中一定有4字节记录了malloc的维护信息,这些维护信息决定了下一次malloc分配内存的位置,如果破坏了维护信息,会影响下一次malloc或者free的过程

使用堆内存需要注意的问题

内存泄漏:

内存无法再使用,也无法被释放,而需要再次使用时只能重新申请内存,然后继续重复以上过程,日积月累后可用的内存越来越少
注意:一旦进程结束属于该进程所有的资源都会被操作系统回收

如何尽量地避免内存泄露:

谁申请谁释放,谁知道该释放谁释放

如何判断、定位内存泄漏的:
1、查看内存使用情况
      win     任务管理器
     Linux ps -aux命令
2、借助代码分析工具 mtrace,检查malloc和free是否成对出现
3、封装malloc、free,记录申请、释放的信息到日志文件中

内存碎片:

已经释放了但无法继续使用的内存叫做内存碎片,由于申请和释放的时间不协调导致的,无法完全避免只能尽量减少

如何减少内存碎片:
1、尽量使用栈内存
2、不要频繁地申请、释放内存
3、尽量申请大块的内存自己管理

内存清理函数

#include <strings.h>
void bzero(void *s, size_t n);
功能:把一块内存清理为0
s:内存块的首地址
n:内存块的字节数

#include <string.h>
void *memset(void *s, int c, size_t n);
功能:把内存块按字节设置为c
s:内存块的首地址
c:想要设置的ASCII值
n:内存块的字节数
返回值:成功设置后的内存首地址

堆内存定义二维数组:

指针数组

定义n行,m列二维数组
    类型* arr[n];
    for(int i=0; i<n; i++)
    {
        arr[i] = malloc(m*sizeof(类型))
    }
注意:每一行的m值可以不同,这种方式可以定义不规则的二维数组
缺点:申请麻烦、容易产生内存碎片
优点:可以不规则、对内存要求较低

数组指针

类型 (*arrp)[n] = malloc(sizeof(类型)*n*m);
    申请m行n列的二维数组
缺点:对连续内存要求高,可能申请失败
优点:申请简单

注意:所谓的多维数组其实都是用一维数组模拟

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值