1.基本概念:
首先定义一个结构体类型,然后定义这种类型的变量和指针:
struct unit { char c; int num; }; struct unit u; struct unit *p = &u;
要通过指针p
访问结构体成员可以写成(*p).c
和(*p).num
,为了书写方便,C语言提供了->
运算符,也可以写成p->c
和p->num
。
2.指向指针的指针与指针数组
指针可以指向基本类型,也可以指向复合类型,因此也可以指向另外一个指针变量,称为指向指针的指针。
int i; int *pi = &i; int **ppi = π
这样定义之后,表达式*ppi
取pi
的值,表达式**ppi
取i
的值。请读者自己画图理解i
、pi
、ppi
这三个变量之间的关系。
很自然地,也可以定义指向“指向指针的指针”的指针,但是很少用到:
int ***p;
数组中的每个元素可以是基本类型,也可以复合类型,因此也可以是指针类型。例如定义一个数组a
由10个元素组成,每个元素都是int *
指针:
int *a[10];
这称为指针数组。int *a[10];
和int **pa;
之间的关系类似于int a[10];
和int *pa;
之间的关系:a
是由一种元素组成的数组,pa
则是指向这种元素的指针。所以,如果pa
指向a
的首元素:
int *a[10]; int **pa = &a[0];
则pa[0]
和a[0]
取的是同一个元素,唯一比原来复杂的地方在于这个元素是一个int *
指针,而不是基本类型。
我们知道main函数的标准原型应该是int main(int argc, char *argv[]);
。argc
是命令行参数的个数。而argv
是一个指向指针的指针,为什么不是指针数组呢?因为前面讲过,函数原型中的[]
表示指针而不表示数组,等价于char **argv
。那为什么要写成char *argv[]
而不写成char **argv
呢?这样写给读代码的人提供了有用信息,argv
不是指向单个指针,而是指向一个指针数组的首元素。数组中每个元素都是char *
指针,指向一个命令行参数字符串。
例 23.2. 打印命令行参数
#include <stdio.h> int main(int argc, char *argv[]) { int i; for(i = 0; i < argc; i++) printf("argv[%d]=%s/n", i, argv[i]); return 0; }
编译执行:
$ gcc main.c $ ./a.out a b c argv[0]=./a.out argv[1]=a argv[2]=b argv[3]=c $ ln -s a.out printargv $ ./printargv d e argv[0]=./printargv argv[1]=d argv[2]=e
注意程序名也算一个命令行参数,所以执行./a.out a b c
这个命令时,argc
是4,argv
如下图所示:
由于argv[4]
是NULL
,我们也可以这样循环遍历argv
:
for(i=0; argv[i] != NULL; i++)
NULL
标识着argv
的结尾,这个循环碰到NULL
就结束,因而不会访问越界,这种用法很形象地称为Sentinel,NULL
就像一个哨兵守卫着数组的边界。
在这个例子中我们还看到,如果给程序建立符号链接,然后通过符号链接运行这个程序,就可以得到不同的argv[0]
。通常,程序会根据不同的命令行参数做不同的事情,例如ls -l
和ls -R
打印不同的文件列表,而有些程序会根据不同的argv[0]
做不同的事情,例如专门针对嵌入式系统的开源项目Busybox,将各种Linux命令裁剪后集于一身,编译成一个可执行文件busybox
,安装时将busybox
程序拷到嵌入式系统的/bin
目录下,同时在/bin
、/sbin
、/usr/bin
、/usr/sbin
等目录下创建很多指向/bin/busybox
的符号链接,命名为cp
、ls
、mv
、ifconfig
等等,不管执行哪个命令其实最终都是在执行/bin/busybox
,它会根据argv[0]
来区分不同的命令。
3.指向数组的指针与多维数组
指针可以指向复合类型,上一节讲了指向指针的指针,这一节学习指向数组的指针。以下定义一个指向数组的指针,该数组有10个int
元素:
int (*a)[10];
和上一节指针数组的定义int *a[10];
相比,仅仅多了一个()
括号。如何记住和区分这两种定义呢?我们可以认为[]
比*
有更高的优先级,如果a
先和*
结合则表示a
是一个指针,如果a
先和[]
结合则表示a
是一个数组。int *a[10];
这个定义可以拆成两句:
typedef int *t; t a[10];
t
代表int *
类型,a
则是由这种类型的元素组成的数组。int (*a)[10];
这个定义也可以拆成两句:
typedef int t[10]; t *a;
t
代表由10个int
组成的数组类型,a
则是指向这种类型的指针。
现在看指向数组的指针如何使用:
int a[10]; int (*pa)[10] = &a;
a
是一个数组,在&a
这个表达式中,数组名做左值,取整个数组的首地址赋给指针pa
。注意,&a[0]
表示数组a
的首元素的首地址,而&a
表示数组a
的首地址,显然这两个地址的数值相同,但这两个表达式的类型是两种不同的指针类型,前者的类型是int *
,而后者的类型是int (*)[10]
。*pa
就表示pa
所指向的数组a
,所以取数组的a[0]
元素可以用表达式(*pa)[0]
。注意到*pa
可以写成pa[0]
,所以(*pa)[0]
这个表达式也可以改写成pa[0][0]
,pa
就像一个二维数组的名字,它表示什么含义呢?下面把pa
和二维数组放在一起做个分析。
int a[5][10];
和int (*pa)[10];
之间的关系同样类似于int a[10];
和int *pa;
之间的关系:a
是由一种元素组成的数组,pa
则是指向这种元素的指针。所以,如果pa
指向a
的首元素:
int a[5][10]; int (*pa)[10] = &a[0];
则pa[0]
和a[0]
取的是同一个元素,唯一比原来复杂的地方在于这个元素是由10个int
组成的数组,而不是基本类型。这样,我们可以把pa
当成二维数组名来使用,pa[1][2]
和a[1][2]
取的也是同一个元素,而且pa
比a
用起来更灵活,数组名不支持赋值、自增等运算,而指针可以支持,pa++
使pa
跳过二维数组的一行(40个字节),指向a[1]
的首地址。
4.函数类型和函数指针类型
在C语言中,函数也是一种类型,可以定义指向函数的指针。我们知道,指针变量的内存单元存放一个地址值,而函数指针存放的就是函数的入口地址(位于.text
段)。下面看一个简单的例子:
例 23.3. 函数指针
#include <stdio.h> void say_hello(const char *str) { printf("Hello %s/n", str); } int main(void) { void (*f)(const char *) = say_hello; f("Guys"); return 0; }
分析一下变量f
的类型声明void (*f)(const char *)
,f
首先跟*
号结合在一起,因此是一个指针。(*f)
外面是一个函数原型的格式,参数是const char *
,返回值是void
,所以f
是指向这种函数的指针。而say_hello
的参数是const char *
,返回值是void
,正好是这种函数,因此f
可以指向say_hello
。注意,say_hello
是一种函数类型,而函数类型和数组类型类似,做右值使用时自动转换成函数指针类型,所以可以直接赋给f
,当然也可以写成void (*f)(const char *) = &say_hello;
,把函数say_hello
先取地址再赋给f
,就不需要自动类型转换了。
可以直接通过函数指针调用函数,如上面的f("Guys")
,也可以先用*f
取出它所指的函数类型,再调用函数,即(*f)("Guys")
。可以这么理解:函数调用运算符()
要求操作数是函数指针,所以f("Guys")
是最直接的写法,而say_hello("Guys")
或(*f)("Guys")
则是把函数类型自动转换成函数指针然后做函数调用。
下面再举几个例子区分函数类型和函数指针类型。首先定义函数类型F:
typedef int F(void);
这种类型的函数不带参数,返回值是int
。那么可以这样声明f
和g
:
F f, g;
相当于声明:
int f(void); int g(void);
下面这个函数声明是错误的:
F h(void);
因为函数可以返回void
类型、标量类型、结构体、联合体,但不能返回函数类型,也不能返回数组类型。而下面这个函数声明是正确的:
F *e(void);
函数e
返回一个F *
类型的函数指针。如果给e
多套几层括号仍然表示同样的意思:
F *((e))(void);
但如果把*
号也套在括号里就不一样了:
int (*fp)(void);
这样声明了一个函数指针,而不是声明一个函数。fp
也可以这样声明:
F *fp;
通过函数指针调用函数和直接调用函数相比有什么好处呢?我们研究一个例子。回顾第 3 节 “数据类型标志”的习题1,由于结构体中多了一个类型字段,需要重新实现real_part
、img_part
、magnitude
、angle
这些函数,你当时是怎么实现的?大概是这样吧:
double real_part(struct complex_struct z) { if (z.t == RECTANGULAR) return z.a; else return z.a * cos(z.b); }
现在类型字段有两种取值,RECTANGULAR
和POLAR
,每个函数都要if ... else ...
,如果类型字段有三种取值呢?每个函数都要if ... else if ... else
,或者switch ... case ...
。这样维护代码是不够理想的,现在我用函数指针给出一种实现:
double rect_real_part(struct complex_struct z) { return z.a; } double rect_img_part(struct complex_struct z) { return z.b; } double rect_magnitude(struct complex_struct z) { return sqrt(z.a * z.a + z.b * z.b); } double rect_angle(struct complex_struct z) { double PI = acos(-1.0); if (z.a > 0) return atan(z.b / z.a); else return atan(z.b / z.a) + PI; } double pol_real_part(struct complex_struct z) { return z.a * cos(z.b); } double pol_img_part(struct complex_struct z) { return z.a * sin(z.b); } double pol_magnitude(struct complex_struct z) { return z.a; } double pol_angle(struct complex_struct z) { return z.b; } double (*real_part_tbl[])(struct complex_struct) = { rect_real_part, pol_real_part }; double (*img_part_tbl[])(struct complex_struct) = { rect_img_part, pol_img_part }; double (*magnitude_tbl[])(struct complex_struct) = { rect_magnitude, pol_magnitude }; double (*angle_tbl[])(struct complex_struct) = { rect_angle, pol_angle }; #define real_part(z) real_part_tbl[z.t](z) #define img_part(z) img_part_tbl[z.t](z) #define magnitude(z) magnitude_tbl[z.t](z) #define angle(z) angle_tbl[z.t](z)
当调用real_part(z)
时,用类型字段z.t
做索引,从指针数组real_part_tbl
中取出相应的函数指针来调用,也可以达到if ... else ...
的效果,但相比之下这种实现更好,每个函数都只做一件事情,而不必用if ... else ...
兼顾好几件事情,比如rect_real_part
和pol_real_part
各做各的,互相独立,而不必把它们的代码都耦合到一个函数中。“低耦合,高内聚”(Low Coupling, High Cohesion)是程序设计的一条基本原则,这样可以更好地复用现有代码,使代码更容易维护。如果类型字段z.t
又多了一种取值,只需要添加一组新的函数,修改函数指针数组,原有的函数仍然可以不加改动地复用。