C语言中的常见指针类型

    学过C语言的朋友都知道,指针在C语言中的重要地位,它为我们对于C语言中的地址理解提供了不可或缺的帮助。下面我来分享几个C语言中常见的指针。

    首先我们来解释一下指针的概念,相信大家都听过内存这一概念,将内存划分为一个个内存单元,每个内存单元大小占一个字节(内存单元就可以类比学生宿舍),每个单元都有着其特地的地址编号(类比宿舍门牌号),CPU通过这些编号便可快速的访问相应的内存。C语言中为这些地址起了一个名字:指针。

e3a92fa77d644094b18310d0082a73f4.png

 

内存单元的编号==地址==指针

    C语言中变量创建的本质其实是在内存中申请空间,创建的内存空间的大小是由指向该变量的类型决定的。特别的,指针变量的大小与类型无关。(32位平台下地址是32个bit位(即4字节),64位平台下地址是64个bit位(即8字节))。

计算机中的编址,并不是把每个字节的地址记录下来,⽽是通过硬件设计完成的。钢琴、吉他上⾯没有写上“剁、来、咪、发、唆、拉、西”这样的信息,但演奏者照样能够准确找到每⼀个琴弦的每⼀个位置,这是为何?因为制造商已经在乐器硬件层⾯上设计好了,并且所有的演奏者都知道。本质是⼀种约定出来的共识!39941471518142d99aab6e5074b56235.png

 

指针的类型决定了对指针解引用时有多大的权限(一次能操作几个字节)。例如(char*)的指针解引用只能访问一个字节,而(int*)的指针解引用就能访问四个字节。

如图所示:

032734fec2c64384be1e7c07fa8ec1a1.png

 我们将其打印出来对比结果

f31a41ab387f488b8f44080789f041c3.png

得出结论:指针的类型决定了指针向前或者向后走一步有多大距离。

void*指针

在指针类型中有⼀种特殊的类型是 (void *) 类型的,可以理解为⽆具体类型的指针(或者叫泛型指针),这种类型的指针可以⽤来接受任意类型地址。但是也有局限性,( void* )类型的指针不能直接进 ⾏指针的+-整数和解引⽤的运算。

200013a65d7b468d9137ddcdf86f09fc.png

668b4aa1a02649249a6b0e994a98a58c.png

 

 在上⾯的代码中,将⼀个int类型的变量的地址赋值给⼀个char*类型的指针变量。编译器给出了⼀个警告(如下图),是因为类型不兼容。⽽使⽤void*类型就不会有这样的问题。

使用void指针接收地址时:

bc79edc59330486a906c9417ef605a30.png

 66c814f18d21442e873756f9dc60b78a.png

 void指针用在接收不确定类型的地址,使用该地址时要注意将其强制性转化为相应的指针类型后进行使用。如下图的qsort函数使用实例:

33b2f3f255fa4c36956a19ea30009508.png

8888c3aa00794ef68fc96798ef5cbf90.png 

 const 修饰指针

一般来说const修饰指针变量,可以放在*的左边,也可以放在*的右边,意义不同,下面我通过两个简单的例子来解释。

84a09d58f9f8412dbdaf132d306af724.png

 10d94e0a795e429db5b0d1b696997aa5.png

2ad07801aeee4f8788f92d0d6ce681de.png结论: 

 const修饰指针变量时

const如果放在*的左边,修饰的是指针指向的内容,保证指针指向的内容不能通过指针来改变。但是指针变量本⾝的内容可变。

const如果放在*的右边,修饰的是指针变量本⾝,保证了指针变量的内容不能修改,但是指针指向的内容,可以通过指针改变。

 

指针运算

1.指针 +- 整数

2.指针 - 指针

3.指针的关系运算

 

指针+-整数:

因为数组在内存中是连续存放的,只要知道第⼀个元素的地址,顺藤摸⽠就能找到后⾯的所有元素。

e1f8e20c06fa49779c893b8fc83df41b.png

f80be28d7d694871aa4102d77fb4c7bb.png 

86b4eb8866cc4cb7b0335f05b6a5a155.png 

 指针-指针:

通过指针相减运算,可以得出strlen函数的模拟实现(后期会出一期讲相关库函数的模拟实现)

5608f2c8b5ae4ba0b80a3a5a8ce42476.png

 野指针

概念:野指针就是指针指向的位置是不可知的(随机的、不正确的、没有明确限制的) 

野指针成因:

1.指针未初始化

bdfb90a643c344df9a2da36866b68f0f.png

 2.指针越界访问

6f2fbb8d92254af2aba0dfd0e5f10281.png

 3.指针指向的空间释放

b6fbe18926e14c12a7a44a0a95db341d.png

9d0c27b65db3493c83db210cc134a9ef.png 

 那么我们该如何规避野指针呢?

1.指针初始化

如果明确知道指针指向哪⾥就直接赋值地址,如果不知道指针应该指向哪⾥,可以给指针赋值NULL. NULL 是C语⾔中定义的⼀个标识符常量,值是0,0也是地址,这个地址是⽆法使⽤的,读写该地址会报错。 (初始化如下)

3304dd15556d446497bc634f0fb70528.png

 2.小心指针越界

⼀个程序向内存申请了哪些空间,通过指针也就只能访问哪些空间,不能超出范围访问,超出了就是越界访问。

3.指针变量不再使用时,及时置NULL,指针使用前检查有效性

当指针变量指向⼀块区域的时候,我们可以通过指针访问该区域,后期不再使⽤这个指针访问空间的时候,我们可以把该指针置NULL。因为约定俗成的⼀个规则就是:只要是NULL指针就不去访问,同时使⽤指针之前可以判断指针是否为NULL。

我们可以把野指针想象成野狗,野狗放任不管是⾮常危险的,所以我们可以找⼀棵树把野狗拴起来,就相对安全了,给指针变量及时赋值为NULL,其实就类似把野狗栓起来,就是把野指针暂时管理起来。

不过野狗即使拴起来我们也要绕着⾛,不能去挑逗野狗,有点危险;对于指针也是,在使⽤之前,我们也要判断是否为NULL,看看是不是被拴起来起来的野狗,如果是不能直接使⽤,如果不是我们再去使⽤。 

b0a827b1cc1a49d39219018f490e15bb.png

 assert断言

    assert.h 头⽂件定义了宏 assert() ,⽤于在运⾏时确保程序符合指定条件,如果不符合,就报错终⽌运⾏。这个宏常常被称为“断⾔”。 

b41dfc2f906c4b0ca9857f577c9d4ac2.png

    上⾯代码在程序运⾏到这⼀⾏语句时,验证变量 p 是否等于 NULL 。如果确实不等于 NULL ,程序继续运⾏,否则就会终⽌运⾏,并且给出报错信息提⽰。

    assert() 宏接受⼀个表达式作为参数。如果该表达式为真(返回值⾮零), assert() 不会产⽣ 任何作⽤,程序继续运⾏。如果该表达式为假(返回值为零), assert() 就会报错,在标准错误流 stderr 中写⼊⼀条错误信息,显⽰没有通过的表达式,以及包含这个表达式的⽂件名和⾏号。

    assert() 的使⽤对程序员是⾮常友好的,使⽤ assert() 有⼏个好处:它不仅能⾃动标识⽂件和出问题的⾏号,还有⼀种⽆需更改代码就能开启或关闭 assert() 的机制。如果已经确认程序没有问题,不需要再做断⾔,就在 #include 语句的前⾯,定义⼀个宏 NDEBUG

 34d0988648b745f3aab99331145fb182.png

    然后,重新编译程序,编译器就会禁⽤⽂件中所有的 assert() 语句。如果程序⼜出现问题,可以移除这条 #define NDEBUG 指令(或者把它注释掉),再次编译,这样就重新启⽤了 assert() 语句。

    assert() 的缺点是,因为引⼊了额外的检查,增加了程序的运⾏时间。

    ⼀般我们可以在 Debug 中使⽤,在 Release 版本中选择禁⽤ assert 就⾏,在 VS 这样的集成开发环境中,在 Release 版本中,直接就是优化掉了。这样在debug版本写有利于程序员排查问题,在 Release 版本不影响⽤⼾使⽤时程序的效率。 

指针的使用和传址调用

下面先来看一串代码(传值调用)

263c832c49144e9c84ff7bb4565beb0a.png

 我们发现在main函数内部,创建了a和b,a的地址是0x00cffdd0,b的地址是0x00cffdc4,在调⽤ Swap1函数时,将a和b传递给了Swap1函数,在Swap1函数内部创建了形参x和y接收a和b的值,但是 x的地址是0x00cffcec,y的地址是0x00cffcf0,x和y确实接收到了a和b的值,不过x的地址和a的地址不 ⼀样,y的地址和b的地址不⼀样,相当于x和y是独⽴的空间,那么在Swap1函数内部交换x和y的值, ⾃然不会影响a和b,当Swap1函数调⽤结束后回到main函数,a和b的没法交换。Swap1函数在使⽤的时候,是把变量本⾝直接传递给了函数,这种调⽤函数的⽅式我们之前在函数的时候就知道了,这种叫传值调⽤。

结论:实参传递给形参的时候,形参会单独创建⼀份临时空间来接收实参,对形参的修改不影响实参。所以Swap1是失败的了。

传址调用

 

c0d67d039aab461e85462f5fde651735.png

e97d75406ad84e7e8b42c711e1114cdb.png 

我们可以看到实现成Swap2的⽅式,顺利完成了任务,这⾥调⽤Swap2函数的时候是将变量的地址传递给了函数,这种函数调⽤⽅式叫:传址调⽤。传址调⽤,可以让函数和主调函数之间建⽴真正的联系,在函数内部可以修改主调函数中的变量;所以未来函数中只是需要主调函数中的变量值来实现计算,就可以采⽤传值调⽤。如果函数内部要修改主调函数中的变量的值,就需要传址调⽤。 

 

至此,指针的部分概念告一段落,后期我会按小模块不定期更新指针的相关知识。再次感谢您的观看💪,让我们共同进步。

 

  • 37
    点赞
  • 21
    收藏
    觉得还不错? 一键收藏
  • 3
    评论
评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值