欢迎光临散文网 会员登陆 & 注册

第 15 讲:指针

2022-01-05 16:23 作者:SunnieShine  | 我要投稿

这是最为头疼的 C 语言专题,比起之前的内容来说,指针确实较难理解一些。更别说使用了。不过,看一下讲解,应该会比较清楚一些它的基本用途。


指针的定义

指针(Pointer),就像是一个桌面上的“快捷方式”。你打开程序为了不去每一个盘找到对应的应用程序然后打开,就建立了一个桌面快捷方式,这样统一管理这些东西,会比较轻松一些。

我们可以将指针理解为一个变量的快捷方式。为了统一管理数据,所以建立起了这些指针,来“间接”操控数据的数值。

我们使用这样的符号来声明一个指针:

它们的名字和普通的变量名字都是一样的规范:数字、字母、下划线(且首字符不可是数字)。当然了,因为指针英文名叫 pointer,所以取名为这三个写法,是指针最常见的取名。我们可以用星号 * 表示它是一个指针变量。注意哦,这个星号要写在变量名字的左边才奏效;写在变量的右侧则可能会被处理成乘法符号而报错。

另外,在 C 语言之中,星号是挨着变量名字的,当然你也可以写作

全部都可以,它们都和第一句 int *p 是一样的意思,不过有些时候为了方便理解代码呢,你可能需要借助后面两种思维来理解指针。现在来看看如何理解指针。


指针的赋值和使用

我们使用这两个符号来使用指针:星号 * 和 and 符号 &。注意了哈,这里又出现了星号,而这个星号和定义是指针变量的含义不同。

  • 星号 *(取值符):取变量对应的数值;

  • and 符号 &(取地址符):取变量的地址。

来看下这三行代码。第一行是定义变量 a,并且 a 的存储空间上放了个数值是 25;第二行是说,定义一个指针变量,赋的值是 a 的地址(因为这里 a 前有一个取地址的符号,表示是取出了地址值)。

说成大白话,就是有一个快捷方式,取名叫 p,这个 p 它总归是一个变量,但怎么和 a 产生关联呢?就让 p 这个变量存的东西是 a 的地址,就 OK 了。换句话说,p 这个变量的存储空间上,它的数值并不是 a 的这个值 25,而是 a 变量的“位置”。

电脑的存储数据是用一系列十六进制数表示的,当然它也可以转为整数,不过因为系统为了和数字本身作区分,所以用十六进制表达,这样看起来不那么像是一个整数数值,来达到区分地址数值和普通数值的目的。每一个变量的存储位置,都是有一个对应的值的,比如这块内存区域下的第一号位、第二号位等等。只是它用十六进制表示了。然后每一个变量在声明时,都会生成一个空闲的内存区域,用来操作数据。这个时候,由于会产生空闲区域的关系,这个空闲区域同时也会对应一个“编号”,那么它就是地址了。上面的指针就是取出了地址值,然后 p 变量处存放的不是别的,就是这个地址的数值。

第三句话则是输出 p 变量存的值(a 的地址)和 p 上地址所对应的那个变量的值(也就是 a 的值),所以会输出一个十六进制数(具体是多少我也不知道,定义变量的时候,电脑随机分配的内存区域,所以根本不清楚到底是哪里,每一台电脑的数据有差异)和 25。那个 %x 就是输出一个十六进制数(详见之前写的“格式化字符串”)。

这样说,你可能就明白了指针了。它是一个快捷方式,是一个变量的快捷方式,它也是一个变量,不过变量存的值并不是数字,而是和它形成对应的变量的地址(可以理解成快捷方式本身其实也只是使用文件位置(路径)封装成文件的程序,它本身是一个空壳,本身没有用途,每次双击它,都是找到路径,然后打开对应的程序)。

所以,一定要记清楚使用指针的符号:取地址符号 &、取值符号 * 和声明指针变量(还是用 * 符号)的三大用途。

那么,下面的写法都是对的(只要顺序执行语句的话),用两次星号表示是“二重指针”,即指针指向一个指针变量,指针变量再指向数值变量的存在。

输入语句 scanf 用得非常少,在参数列表里,我们从字符串后开始写上字符串里所有格式化字符串的对应变量的序列,但需要赋值的是这些变量的所有地址,这是因为,scanf 要求函数给定一个地址数值,来找到变量获取元素信息,如果只有一个数字的话,就无法取得地址和数值的关联。

虽说这一点也可以被设计成只需要传入数值,然后直接放上去这样的模式,但 scanf 函数在最初的设计就用的是指针,所以需要取地址符号来操作。不过,如果本身变量就已经是一个指针变量了,这样就不需要加入地址符号,否则就会变为画蛇添足的效果。&a 表示获取 a 变量的地址,那么 &p 表示获取 p 指针变量它自己的地址,这是没有意义的。而针对于二级指针的话,我们肯定需要获取的是它指向的变量 p 存储的值(a 的地址数值),来找到 a 的位置,所以需要写上的是 *q,来取指向的变量 p 的存的内容。

这一点有点绕,请你反复理解这段内容。


指针的用途

那么,这样复杂和难过的东西有什么用呢?


引用参数(ref 参数)

还记得之前说到的函数的销毁吗?

当我们交换变量时,因为在调用了变量交换,虽然值确实被交换了,但函数会被销毁的缘故,最终的变量依然没有发生任何变化:

这个时候我们使用指针,来改变地址,就改变了数值。

如果写作指针,那么函数此时传入的就不是简单的数值了,而是一个地址数值。

当我们尝试把 ij 变量传递到 swap 里的时候,它此时并不是传递进去了 1 和 5 两个数,而是这两个变量自己的地址数值。

然后进入 swap 函数开始交换。首先我们把 a 这个指针变量的指向变量数值取出,因为 a 传入进来的是 i 变量的地址,所以此时 a&i 可以说是等价的,所以取出 i 变量地址,然后取出这个地址上的数据,所以就得到了 main 函数里那个 i 变量的数值 1 了;然后把这个 1 赋值到 temp 里。

然后把 b 变量指向的变量数值取出,然后赋值到 a 变量指向的内存区域上。左值 *a 可能现在还不好理解。你可以认为,把取内容符号 * 放在等号左侧的变量上,指代这个变量指向的那块内存(变量),然后尝试把右侧的数值放入到这个内存(变量)里去。

然后最后一步就是把 temp 取出来,放入 b 指向的那块内存。由于过程里 ab 分别指向的是 main 里的 ij,所以 swap 函数整体执行完毕后,销毁的也是 ab 这两个指针变量,即它们的指向是被销毁了,但指向销毁肯定是不影响到原本数据 ij 的,所以交换就起效了。

我们借用这个示例为大家解释了如何使用指针传参的模式来同步在两个函数之间操作的过程,这种操作可以影响到另外一个函数里的变量数值,我们称此时 swap 里的指针变量(ab)为引用参数(有时候也叫 ref 参数,ref 是 reference 的缩写),这种参数跨 swapmain 两个函数在使用相同的变量。

引用是高级编程语言里的一种说法,它指代的是两个指针变量同时指向相同的内存区域,所以可以认为两个指针调用同一个变量的写法格式除了变量名不同外,其它都一样,所以称为引用一致。比如

例如上述写法里,pq 都指向了 a 变量。

使用的是完全一样的书写格式(变量 = *指针),而且指向同一块内存区域(同一个变量),所以我们认为 pq 具有一致的引用。

而在上面 ref 参数里,我们使用的正是引用不变这一个点,来从 swap 里修改到 main 里的信息,因为此时 swap 的参数(指针变量)的引用是和 main 函数调用 swap 函数时传入的参数是一样的:&i&j 表示两个地址数值,而在 swap 的参数里,本应该获取的是两个指针变量,但这里传入的地址数值可以用来表示一个指针变量的信息。这就叫引用。

不过,C 语言没有引用一说,是因为引用并不是像上面说得这么简单,由于原理的实现,它更类似于为变量取别名,显然 pi 不是别名关系,它们差了一个地址符号。你不用理解这么详细,你大概知道,引用是啥样的就可以了。


输出参数(out 参数)

再来考虑一个问题。我们如果要让返回值返回多个数值信息怎么办?函数是只可能返回一个数值的,这是数学规定的,因为一个函数(不考虑多值函数这个广义理解下)是不允许有多个返回值的。C 语言沿用了这一点,所以从函数返回值角度出发,受到语义制约,C 语言是不允许返回多个返回值的。

那么,如果我们从参数返回呢?这是个好办法,可是从参数返回,在函数销毁的时候,变量不就跟着没有了么?是的,所以我们需要指针来搞定。

考虑如下操作。我们尝试对一个学生计算数学、英语、C 语言三个科目的成绩的平均数值,并要求同时返回是否平均成绩及格(一个布尔型 bool 数值或用一个 int 表示)和三个科目的平均分。

这个题目难点在于需要同时返回是不是及格和平均分两个数据。我们可以这么书写代码:


我们在 IsPassOfAverage 函数里的唯一一个指针变量 avg 赋上了平均结果,然后返回了 avg 是否超过 60 分这个比较结果,而由于是一个指针的方式,在执行完整个 IsPassOfAverage 函数后,avg 会销毁,但它是指针变量的关系,它指向被销毁,但指向的那个变量并不会被销毁。然后,执行完毕前,计算结果就已经得到了。所以输出结果信息都是全部成立的。

这样我们就模拟出了返回多个结果的要求。我们称这里 avg 变量仅用于输出用,所以称为输出参数(有时候也称为 out 参数)。但别忘了,在计算平均数的时候要为 avg(赋值语句的左值)添加取内容符号。


还有其它的用法吗?

实际上,指针并不只有这些用法,还有很多用法。不过今天作为入门指针的讲解,我不打算放在这里说,下一部分内容我们将会详细分析和研究指针的运算符,以及它和数组的关系。


参数修饰符

在函数传递参数的过程中,我们可以使用很多修饰符(一部分关键字)来告诉编译器很多信息,丰富我们的使用。


const 修饰符

最难用的要数这个关键字了。这个关键字放置到参数上,可以让每一个参数的信息只用来读取,而执行期间不允许修改这个变量的数据。


数值变量的修饰

考虑如下操作。如果我们尝试把上面计算平均成绩的代码稍加改动。可以看到三个成绩信息是没有必要去改动的,我们为了语法约定,可以增加 const 修饰符来保证这些数值可以通过不被修改的方式完成执行逻辑。

这样我们就无法在代码里修改这些变量了。

指针变量的修饰

当然,这样的操作也可以修饰指针变量。如果一个传入的指针的数值(即地址)不用修改,可以尝试为变量修饰 const

这样就可以保证,我们传入的指针变量的地址信息不被修改。


指针变量的全只读

const 是很灵活的关键字。可以看到,指针是暗含地址信息和指向变量的信息两个内容的。如果我们修饰的是变量本身,那么这个变量本身的内容就不可以被修改。针对于数值变量而言就是它自己的数值不能修改,而针对于指针变量就是它存入的地址不可以被修改(即指向不能变)。但此时,指向的变量的内容是可能变化的。

如果我们甚至想要保证指向的变量的内容也不可以改变,那么还需要在指针符号 * 和变量名之间插入 const 关键字来保证。当然,这一点不好举例,这里简单考虑如下写法。

这样就可以保证指针指向不变,且指向的内容也不可以更改,进而保证了安全性。


const 的位置

可以看到,这个修饰符放在很多地方都可以,所以考虑如下四种写法:

  • const int *p

  • int const *p

  • int *const p

  • const int *const p

这四种写法都是合理和允许的。不过,前面两种是等价的,因为它们都没有放在定义符号后,所以只能保证指向不变(自身存储的数值,即地址信息不变)。而第三种只能保证内容不可以被修改,但指向是可以修改的(这一点不好理解,但被允许,请尽量少这么用它)。第四种则是都不可以变。


restrict 修饰符

在前文我哦们提到了引用参数,这种参数利用了“引用一致”的说法,来达到影响原数据的效果。

这样书写代码完全没有错误,但会引入一个非常隐蔽的 bug。如果我们操作了 a,篡改了数据后,pq 虽然指向不变,但最终 a 的数值发生变化后,pq 指向的变量数值就肯定是同步变化了。

由于执行到 *b = 6 的时候,它们操作的可能是同一个内存(同一个指向的变量),所以返回值还真不一定是预期的 11(可能因为 ab 指向同一个变量而导致结果是 12)。

但,如果我们预期就可以认为和保证,这两个变量不应是同一个指向的话,我们就为指针和变量名中间添加 restrict 修饰符,来保证它们肯定不能指向同一个变量:

而在编译器层面,在执行程序之前,它就可以通过指向不同这一个隐藏的信息来优化代码。所以执行期间就会发现,结果一定就是 11 了,而且是编译器优化后直接得到的结果,而不会出现问题。

总之,restrict 关键字修饰在变量名和指针定义符号 * 之间,可以表示这些指针指向的变量一定不同,进而可以把一些执行语句按照规范调整执行顺序,来达到减少执行步骤,以优化的目的。

这个关键字在 C99 标准后才可以用。


还有其它的吗?

其实,还有 static 等等修饰符,但这里不作阐述,因为它们都超纲了。


NULL 常量

在 C 语言里甚至为我们规定了指针的默认值。之前学到的很多数据类型都具有自己的默认数值,比如 int 的默认数值是 0,而 bool 类型的默认数值是 false,字符的默认数值是 \0(讲解字符和指针一节的时候会说明这一点)。指针也具有默认数值。不过,因为指针都是一个地址信息,所以不论指针的指向元素类型,它们统一都有一个默认数值是 NULL。注意,这个默认数值是全部字母都大写的,而且它的意义是指向 0 号内存的地址数值。好比是把数值进行“数值变指针的强制转换”。

而具体如何使用它,我们将在内存分配里才会讲到。


第 15 讲:指针的评论 (共 条)

分享到微博请遵守国家法律