目录
前言
本文主要介绍C语言指针的一些基础知识,为后面深入理解指针打下基础,因此本文内容主要包括内存与地址的关系,指针的基本语法,指针运算,野指针,还有const修饰指针和assert断言的使用,最后还会讲到指针的传址调用,希望对大家有所帮助。
一、内存与地址的关系
指针作为C语言的核心知识,那么指针究竟是什么呢?
- 首先指针其实就是地址,而地址是内存中一个个内存单元的编号
- 我们知道计算机上CPU(中央处理器)在处理数据的时候,需要的数据是在内存中读取的,处理后的数据也会放回内存中,那我们买电脑的时候,电脑上内存有8GB/16GB/32GB等,这些内存就是程序运行时需要用到的内存
- 为了更高效的管理与使用这些内存,于是就将这些内存分为一个个内存单元,每个内存单元的大小取一个字节,也就是8个比特位,⼀个比特位可以存储一个2进制的位1或者0
- 每个内存单元也都有一个编号,有了这个内存单元的编号,CPU就可以快速找到一个内存空间,在计算机中我们把内存单元的编号也称为地址。C语言中给地址起了新的名字:指针
- 所以我们可以这样理解:内存单元的编号 == 地址 == 指针
如图:
二、指针变量
指针变量就是储存地址的变量
1. 取地址操作符 &
&操作符是一个单目操作符,用来取出一个变量的地址
比如,创建 int a,观察其地址
图中画红线的部分就为a变量在内存中的地址以及储存的数据,即0x004FF82C为地址,0a 00 00 00为以16进制储存的数据,其中 0a 表示的就是以16进制保存的十进制数字10,因为一个16进制数需要4个比特位表示,0a就是两个16进制数,占了8个比特位刚好为一个字节,而0a 00 00 00 四个这样的就刚好表示4个字节,至于为什么0a在前面,这是编译器自己的规则
2.指针变量创建
对于一般的指针变量的创建
(类型) * 变量名;
这样就将变量 a ,b 的地址存储在对应类型的指针变量里面,其余的如float、double等可以以此类推
注意:这里的*号表示其变量为指针变量,它是与变量名结合,前面的类型是指针变量的类型,它与指针访问地址时的权限大小有关(后面有讲)
3.解引用操作符 *
* 解引用操作符为一个单目操作符,它可以通过地址找到其对应的数据
因为指针变量存储的就是地址,所以指针变量搭配*就可以找到其对应的数据进行操作
如:
我们可以通过解引用操作符修改指针对应的变量数据
4.指针变量类型的意义
指针变量的大小和类型无关,只要是指针变量,在同一个平台下,大小都是一样的,比如在32位平台上指针类型的变量大小都是4个字节,64位平台上为8个字节(以下在32位上演示)
既然指针变量的大小和类型无关,那么指针变量的类型有什么意义呢?
其实,这个意义非常重要:指针的类型决定了,对指针解引用的时候有多大的权限(一次能操作几个字节)。比如: char* 的指针解引用就只能访问⼀个字节,而 int* 的指针的解引用就能访问四个字节,这个权限的大小就是指针变量类型的大小
如:我们再次创建一个变量a。(注:程序每次运行时分配的地址不一样)
除了a变量的地址不一样,其他和上面一样为 0a 00 00 00,它表示的是10,并且每两位表示一个字节,而一个字节表示一个内存单元,因此如上的0x0099FC8C其实表示的是储存0a的地址,我们可以一列一列的观察其内存
因为a为整形变量,占4个字节,因此其在内存中为4个连续的内存单元,如上标记的区域,此时如果我们创建一个整形指针变量接收a的地址,那么我们解引用该指针就可以操作这四个字节
如:
注:90 01 00 00 在读取时是以 00000190,也就是190,为16进制
十进制刚好为400
此时变量a可以被正常修改,而如果我们以字符类型的指针接收a的地址后,我们一次只能修改一个字节
如:
16进制28等于十进制40,如上我们貌似也能正常修改整形变量a的数组,但实际上只要我们修改的数值大于两位16进制能表达的最大数字,就不能正常修改a的数值
如:
我们只能改变一个字节,也就是char类型的指针一次只能修改一个字节
这就是指针类型的意义,当然不止如此,指针变量的类型还决定了指针加减整数的时候一次跳过多少字节,下面就会讲到
5.指针 + - 整数
先说结论:指针加减整数,会使指针前进或后退n个字节,而指针的类型决定了指针向前或者向后走一步有多大(距离)
也就是说,指针类型决定了指针加减1时的步长,比如char*指针,它一次只能跳过一个字节,它加减n也就是向前或向后跳过n*sizeof(char)个字节
比如:
(注:此处int *parr = arr不能写成&arr,下篇指针进阶文章我会讲到)
此处我们就利用了循环来使数组首元素地址依次跳过 i个int类型大小的字节,实现了循环打印数组元素
此处我们有几处需要注意的点:
- 数组的元素在内存中是连续存放的,并且地址由低到高,不了解的可以参考我主页的数组文章
- 此处我们发现,如果把 *(arr+i) 换成 arr[ i ] 也就是我们之前的写法达到的效果是一样的,这是因为 *(arr+i) 是完全等价于 arr[ i ] ,也就是说,当编译器遇到 arr[ i ] 时会把它解读为 *(arr+i),按这样理解,因为 arr+i 等于 i+arr ,也就是可以写成 *(i+arr),进而可以写成 i[arr] ,我们可以验证一下
- 答案是完全可以的,但是平时不建议这样写,因为 i[arr] 可读性不如 arr[ i ]。总结就是 [ ] 操作符其实也是解引用的效果,只不过多了加法的作用
6.指针 - 指针
对于指针 - 指针这个运算来说,只有两指针指向的是同一块连续的内存区域时才有意义
我们可以通过指针 - 指针来计算数组两元素地址之间有多少个元素
如:
那么为什么是9个而不是10个呢?
我们可以画图来理解:
画图我们就可以很直观的感受到,arr[9] 的元素没有被计算到,因为如果指针指向的数据大小大于一个字节,那么指针储存的该数据地址为该数据储存在内存中的首地址,参考上文中的 int a 变量的地址观察
三、野指针
1.概念
野指针就是指针指向的位置是不可知的(随机的、不正确的、没有明确限制的)。
野指针成因:
- 指针未初始化 :主要是创建在函数中的指针变量没有进行初始化,造成指针指向的地址是随机值,此时指针指向的地址随机,不能对其进行解引用。
- 指针越界访问 :这种主要出现在数组中,指针指向的地址超出了数组所在的内存区域,指向了一个不确定的地址
- 指针指向的空间被释放 :这种主要发生在指针变量指向的地址是已经被释放的内存空间地址,被释放的空间不属于该程序,虽然可能引用不会导致报错,但是不安全
2.如何规避野指针
野指针的危害有:访问违规、数据损坏、内存泄露、安全风险等。
野指针的危害众多、因此我们的代码中需要规避野指针,那么如何规避野指针呢?
- 指针变量初始化时如果没有需要赋值的地址就先赋值为NULL
- 指针变量不再使用时,及时置NULL,指针使用之前检查有效性:当指针变量指向一块区域的时候,我们可以通过指针访问该区域,后期不再使用这个指针访问空间的时候,我们可以把该指针置为NULL。因为约定俗成的⼀个规则就是:只要是NULL指针就不去访问, 同时使用指针之前可以判断指针是否为NULL。
- 避免返回局部变量的地址,比如避免函数返回局部变量地址
除了以上的方法,还有一个常用的方法:
assert 断言
assert.h 头文件定义了宏 assert() ,用于在运行时确保程序符合指定条件,如果不符合,就报错终止运行。这个宏常常被称为"断言"
比如:assert (p != NULL);
上面代码在程序运行到这一行语句时,验证变量 p 是否等于 NULL 。如果确实不等于NULL 继续运行,否则就会终止运行,并且给出报错信息提示。
程序 assert() 宏接受⼀个表达式作为参数。如果该表达式为真(返回值非零), assert()不会产生任何作用,程序继续运行。如果该表达式为假(返回值为零), assert() 就会报错,在标准错误流 stderr 中写入一条错误信息,显示没有通过的表达式,以及包含这个表达式的文件名和行号。
如:
assert() 的使用对程序员是非常友好的,使用 assert() 有几个好处:它不仅能自动标识文件和出问题的行号,还有一种无需更改代码就能开启或关闭 assert() 的机制。如果已经确认程序没有问 题,不需要再做断言,就在 #include <assert.h>语句的前面,定义一个宏 NDEBUG
如下assert()就会失去作用:
如果想再次启用assert()只需要注释掉第一行的宏就行
assert() 的缺点:因为引入了额外的检查,增加了程序的运行时间。 一般我们可以在 Debug 中使用,在 Release 版本中选择禁用 assert 就行,在 VS 这样的集成开发环境中,Release 版本中,assert()直接就是自动优化掉了。这样在debug版本写有利于程序员排查问题,在Release 版本不影响用户使用时程序的效率。
四、const
const的作用:被const修饰的变量不能被直接修改
如:
程序在还未运行时已经发出错误警告
虽然不能直接修改,但是还能通过指针变量间接修改:
但是如果const修饰的是指针变量,就分以下两种情况:
- const如果放在*的左边,修饰的是指针指向的内容,保证指针指向的内容不能通过指针来改变。 但是指针变量本身的内容可变。
- const如果放在*的右边,修饰的是指针变量本身,保证了指针变量的内容不能修改,但是指针指向的内容,可以通过指针改变。
如:
五、传值调用与传址调用
传值调用与传址调用,这个主要针对的是自定义函数的参数,也就是说:
- 函数参数为非指针类型,调用时传入非指针类型参数,即为传值调用
- 函数参数为指针类型,调用时传给函数地址,即为传址调用
那么这两个有什么区别呢?
其实主要是传值调用时,在函数内部修改形参不会影响实参,而在传址调用时,修改形参也同样会会修改实参
比如这个例题:编写一个函数,交换两个整形变量的内容
此前我们在主函数中只需要再创建一个变量,通过三者交换即可达成题目这样的效果,但如果我们在自定义函数里面,函数参数为两个整形变量,分别接收需要交换内容的两个实参,使用一样的方法是达不到一样的效果的,这时候我们只需要使用传址调用即可
如:
通过传给函数实参的地址,在函数中用指针变量的形参接收,就可以在函数中解引用该指针变量来修改对应的实参变量的内容
这就是传值调用与传址调用的不同
另外,如果函数的参数为数组类型,其实也是指针变量,给函数传参时,一般传入的就是数组名,因为数组名就是数组首元素的地址,至于详细原因我会在指针进阶中讲到
总结
以上就是本文的全部内容,希望对大家有所帮助,下一篇我会继续写指针的进阶篇,感谢大家的支持