指针是C语言重要而且独特的一个概念。指针很灵活,学习指针必备的知识是要了解C语言中的数据存储方式。因篇幅有限,本节课先介绍指针的一些基本概念,以及用指针的方式去引用我们之前学过的基本数据类型、数组、函数等。
注意,请认真学习完《C程序设计(第五版)》第八章后再阅读本文会有更大的收获。
指针
指针即内存地址。变量在内存中初始化时被分配一个地址,之后再用到这个变量都是根据其地址找到对应的存储单元,再结合其数据类型进行取值。
直接访问
根据变量名来访问其在内存中的值。对于基本数据类型如整型、浮点型、字符型等一般都采用此种方式。
间接访问
把变量A的存储地址存在另一个变量B里,通过访问变量B获取变量A的地址,进而根据地址去访问A。
指针变量
定义
用一个变量来存储某个数据的内存地址,那么这个变量就是指针变量。
为了区别于普通变量,定义指针变量的时候加上一个星号“*”,但指针变量的变量名不包含星号。
引用
定义一个指针变量后,我们可以给它赋值,赋的值自然要是一个指针,通常指针通过取址符“&”获取。指针变量的引用形式有两种:
- 不带星号,引用指针变量自身的值,即存储的指针可以做指针相关的运算,或者重新赋值一个同类型的指针
- 带星号,引用指针变量存储的指针对应的变量,即上述的“间接访问”变量,等同于对变量进行运算,或者重新赋值
星号的位置
目前两种主流的写法:
- int* a; 星号紧跟数据类型之后
- int *a; 星号紧挨着变量名
这两种写法在编译时都能通过而且没有区别,主要是看个人习惯以及对指针变量定义的理解。
在Visual Studio里默认星号紧跟数据类型,想修改的话打开“工具”->“选项”进行调整如下:
作为函数参数
指针变量作为函数参数的时候,实参传递给形参的是指针。改变形参的值不会改变实参,而通过“*p += 10”这样的运算则会改变其指针对应的变量的值,例如:
在上面的代码里,指针变量p1和p2传入到fun1()函数中去做运算之后,并没有发生变化,而b的值却发生了变化,有疑惑的自己敲代码运行一下看输出和心理预期有没有出入。
指针变量的指针
变量的指针存储在指针变量里,指针变量和普通变量一样,也有它自己的指针,叫作指针变量的指针。
上面这句话有点像绕口令,其实不难理解,我们打个比方:一个柜子有很多个颜色各不相同的格子,每个格子上贴有不同的字母标记,有一个金币放在标记为A的红色格子中;另外有一个标记为B的蓝色格子,里面放一张纸条并写有字母A,得出以下类比:
- 所有格子都是变量,里面存的东西就是变量的值
- 字母A表示红色格子的指针,即变量的指针
- 蓝色格子存放字母A的纸条,即存放红色格子的指针,所以蓝色格子是一个指针变量
- 字母B表示蓝色格子的指针,即指针变量的指针
借助断点理解指针
在Visual Studio编辑器中利用断点可以在指针变量赋值的过程中看到其值的变化,例如:
以上代码里定义了两个指针变量p和p2:
- p是正确的定义方式,把变量a的内存地址赋值给p
- p2初始化定义没有问题,但是直接把整数11赋给p2是不对的;p2会把11转成16进制0x000000000000000b,然后根据这个地址找存储单元取值,结果自然是找不到的
提示:初学者不太容易接受和理解指针这个新的概念,当遇到“指针”这个词的时候,心里自动把它转成“地址”来过渡。
数组与指针
通过上面的学习,初学者可能有点疑惑,基础变量的指针看起来好像就是引用的时候做一次转换,感觉不到特别有用的地方。接下来,通过指针引用数组就能体现出指针的便利之处了。
数组的指针
之前学习数组我们了解到:数组名就表示数组首个元素的存储地址,即首个元素的指针,也是数组的指针。
存放数组指针的指针变量,定义的时候和基础变量的指针一样,前面声明数组的数据类型,如下几种定义数组指针的方式都是等价的:
int a[10]; int *p1 = &a[0]; int *p2 = &a; int *p3 = a;
数组指针的运算
通过数组指针的运算来引用数组里的元素是我们学习的关键知识点。由于数组的在初始化的时候,被系统分配连续的内存来存储各个元素,所以知道了首个元素的指针p,通过加法运算得到第i个元素的指针p+i(i从0开始计数)。
对于初学者来说,为了熟悉数组指针的用法,要多练习写一些不同条件的for循环和while循环去通过指针引用数组的元素,并实现数组的排序、找最大值、最小值等算法。
数组作为函数参数
之前已经学习过数组作为函数参数是,形参数组的变化会影响实参数组。函数的形参不管是数组,还是数组指针,它们都是等价的,同样实参传递数组和数组指针也是等价的。
二维数组与指针
以上是一维数组的指针相关特性,那二维数组的指针如何呢?和一维数组一样,二维数组的名字表示首个元素的指针。二维数组元素的指针引用比一维数组稍微复杂,但是只要记住一点,在指针运算中,一般先计算出行(第一维的数组指针)指针,再根据行指针里存储的第二维数组的首元素指针,例如二维数组a的i行j列元素为:
a[i][j] = *(*(a + i) + j);
对于二维数组a[m][n],我们定义一个指针变量p,p里指针指向一个一维数组且有n个元素,使得p指向a[0],p+1指向a[1],p+i指向a[i]
- ,p的指针指向a[0][0],是一个整型元素,则p+1指向的是a[0][0]的下一个元素a[0][1],这时a[i][j] = *(p + n*i +j); 相当于指针从a[0][0]开始按行逐个移动到a[i][j]
- 如果定义 int (*p)[n] = a,p的指针指向a[0],是一个包含n个元素的数组,则p+1指向a[0]的下一个元素a[1],这时a[i][j] = *(*(p + i) + j); 相当于指针从a[0]先移动到第i行a[i],在移动到第j列a[i][j]
这里注意的是,之前我们用的指针变量指向一个基本数据类型的变量,或者是一维数组内的(基本数据类型)元素,而这里定义一个指针变量指向的是一个一维数组,采用了新的定义方法:int (*p)[n]; ,虽然p也指向了一个数组的首元素,但p+i并不表示指向第i个元素,而是指向第i行。
字符串与指针
字符串赋值
C语言可以直接用字符串赋值给一个指针变量,但是不能给一个指针变量直接赋值一个数组:
- char *s = "hello world"; 是可以的
- int *p = { 1, 2, 3 }; 是错误的
这个与C语言对字符串的特殊对待方式有关,C语言对字符串按照常量字符串处理,并为其开辟字符数组来存储,所以有了存储地址就等于有了指针了,而一个数组就没有这个待遇了~
字符指针的引用
和数组指针类似,自身也可以做指针运算,引用字符串中某一个字符,或者改变字符串中某一个字符,但是注意直接赋值给指针变量的字符串是不能改变其中的字符的,如下代码:
因为str指针是直接字符串赋值,而C语言这种处理方式是把字符串作为常量存储在内存中的,故不能改变常量的值,只能引用。
字符指针作为函数参数
字符数组也是数组,因此形参改变也会影响实参。
函数与指针
函数指针变量的定义
函数指针变量定义形式:数据类型 (* 指针变量名)(函数参数列表); 例如
void (*p)();
这里定义指针变量时也用括号把星号和变量名包含起来,再结合之前我们定义一个一维数组指针也是用括号包含星号和变量名,即可得知复杂类型数据的指针变量定义都要注意这点,其主要原因是星号的优先级普遍偏低。
注意的是函数指针变量指向函数体存储的入口地址,对函数指针变量做运算没有意义。
函数指针变量作为参数
有点类似于“函数式编程”的意思,比如JavaScript的回调函数,在一些高级语言中都有类似的“回调”语法规则。
函数指针的使用
在使用函数指针的时候,无论是直接调用还是当做参数传递到其他函数中调用,我们来看下面几种写法:
上面代码中的5种调用都能执行,而且结果一致,是不是有点疑惑呢?答案就在打印输出的地址中——指向函数的指针和函数里的指针是一致的。
类比上面格子的比喻,可以这样理解:标记为A的红色格子里面存放的纸条也写着A,所以无论你是从颜色(红色)找格子,还是从格子的标记(A)找格子,还是从格子里的纸条标记(A)再去找格子,最终找到的都是标记为A的红色格子。
初学者可能一时半会理解不透彻,可以去网上搜索“函数指针星号调用”查阅更多的资料。
函数返回指针
上一课学习函数,我们了解到函数的返回值要么为空,要么是一种基本的数据类型。如果返回一种基本数据类型的指针是否可行呢?答案是肯定的。
为了区别于普通自定义函数,我们在返回的指针函数定义时,在函数名前加一个星号:返回数据类型 *函数名(参数列表);
在使用的过程中,因为要接收函数的返回值,所以要定义一个同等类型的指针变量来作为函数返回值的赋值对象。下面是一个相当于字符串截取的参考示例,返回一个字符指针:
指针数组和指向指针的指针
指针数组
指针数组指的是一个数组里的元素都是指针类型的数据,定义形式为:数据类型 *数组名[数组长度];
注意:区分和指向一维数组指针变量的定义区别。
指针数组最常用的使用场景是处理字符串数组,如果按照普通的二维数组去定义一个字符串数组是非常麻烦的,因为二维数组要制定列数,但是每个字符串(字符数组)的长度都不相同,有长有短,这样会造成存储空间的浪费,如果把字符串(字符数组)用字符指针代替存储到数组里,就能很好的解决这个问题了。
指向指针的指针
如果一个指针变量里存放的指针指向的不是一个具体的值,而是另外一个指针,那么这个指针就是“指向指针的指针”。继续拿格子举例:红色格子里存放纸条B,纸条B对应蓝色格子,而蓝色格子里又存放了纸条C,纸条C对应绿色格子,绿色格子里存放着金币,那么可以这么说:
- 蓝色格子里存放金币所在格子的指针C
- 红色格子里存放指向“指向存放金币格子的指针C”的格子的指针B
- 指针B就是一个指向指针C的指针
main()函数的默认参数
在执行编译好的exe文件可以加上参数,main()默认两个参数:
- int argc,参数的总数量,至少为1个参数,就是执行的当前文件的路径
- char *argv[],参数组成的指针数组,即运行命令后跟的参数,以空格来隔开
可以在Visual Studio的项目配置里添加调试参数,点击“调试”->“你的项目名+属性调试”(最下面一个菜单),如下图所示:
总结
本节的知识点比较多,几乎是用指针把前面所学的东西又重新表达了一遍。使用指针要多练习,特别是二维数组指针的使用,很容易把定义一个指向一个一维数组整体的指针和一维数组首元素的指针搞混淆。
使用指针有时候能巧妙地解决一些复杂的问题,不要为了使用指针而使用,要保证程序的可读性和代码的可靠性,不必要故意写那些让人费解、故弄玄虚的代码。