在C语言中,指针的的使用的非常广泛,原因之一是,指针常常是表达某个计算的唯一途径,另一个原因是,同其他方法比较起来,使用指针通常可以生成更高效、更紧凑的代码。
语句 | 含义 |
---|---|
type *ptr |
声明一个名为 ptr 的 type 类型指针; |
*ptr |
ptr指针所指向的位置的值; |
*(ptr + i) |
ptr 指针所指向的地址加 i 后所保存的值; |
&thing |
thing的地址 |
type *ptr = &thing |
声明一个类型为 type 的指针,并将 thing 的地址赋给它 |
ptr++ |
ptr 指针所指向的地址加一 |
一元运算符 &
可用于取一个对象的地址,因此,下列语句:
1 | p = &c; |
将把 c 的地址赋给变量 p,我们称 p 为“指向”c的指针。地址运算符 &
只能应用于内存中的对象,即变量与数组元素。它不能作用于表达式、常量或register类型的变量。
一元运算符 *
是间接寻址或间接引用运算符 。当它作用于指针时,将访问指针所指向的对象。我们在这里假定 x 与 y 是整数,而 ip 是指向 int 类型的指针 ,下面的代码段说明了如何在程序中声明指针以及如何使用运算符 &
和 *
:
1 | int x = 1, y = 2, z[10]; |
1 | int *ip; |
这样声明是为了便于记忆。该声明语句表明表达式 *ip 的结果是 int 类型。这种声明变量的语法与声明该变量所在表达式的语法类似。
如果指针ip指向整型变量,那么在 x 可以出现的任何上下文中都可以使用*ip,因此,语句
1 | *ip = *ip + 10; |
将把 *ip 的值增加10。
一元运算符 *
和 &
的优先级比算数运算符的优先级高,因此,赋值语句
1 | y = *ip + 1 |
将把 *ip 指向的对象的值取出并加1,然后再将结果赋值给y,而下列赋值语句:
1 | *ip += 1 |
则将 ip 指向的对象的值加1,它等同于
1 | ++*ip |
或
1 | (*ip)++ |
语句的执行结果。语句(**ip)++
中的圆括号是必需的,否则,该表达式将对ip进行加1运算,而不是对ip指向的对象进行加1运算,这是因为,类似于*和++这样的一元运算符遵循从右至左的结合顺序。
最后说明一点,由于指针也是变量,所以在程序中可以直接使用,而不必通过间接引用的方法使用。例如,如果 iq 是另一个指向整型的指针,那么语句:
1 | iq = ip |
将把 ip 中的值拷贝到 iq 中,这样,指针 iq 也将指向 ip 指向的对象。
由于 C 语言是以传值的方式将参数值传递给被调用函数。因此,被调用函数不能直接修改主调函数中变量的值。可以使主调程序将指向所要交换的变量的指针传递给被调用函数。由于一元运算符 &
用来取变量的地址,这样 &a
就是一个指向变量 a 的指针。
swap函数的所有参数都声明为指针,并且通过这些指针来间接访问它们指向的操作数。
1 | void swap(int *px, int *py) /* interchange *px and *py */ |
指针参数使得被调用函数能够访问和修改主调函数中对象的值。
指针可以保存数组元素的地址,例如:
1 | int a[10]; |
在取数组元素时用数组名和用指针的语法一样,但如果把数组名做左值使用,和指针就有区别了。例如pa++是合法的,但a++就不合法,pa = a + 1是合法的,但a = pa + 1就不合法。
数组名做右值时转换成指向首元素的指针,但做左值仍然表示整个数组的存储空间,而不是首元素的存储空间,数组名做左值还有一点特殊之处:不支持++、赋值这些运算符,但支持取地址运算符&,所以&a是合法的。
函数原型中的[ ]
表示指针而不表示数组,例如:
1 | void func(int a[]) |
等价于:
1 | void func(int *a) |
参数写成指针形式还是数组形式对编译器来说没区别,都表示这个参数是指针,之所以规定两种形式是为了给读代码的人提供有用的信息,如果这个参数指向一个元素,通常写成指针的形式,如果这个参数指向一串元素中的首元素,则经常写成数组的形式。
字符串字面值类似于数组名,做右值使用时自动转换成指向首元素的指针,这种指针应该是const char *型。我们知道printf函数原型的第一个参数是const char *型,可以把char *或const char *指针传给它,所以下面这些调用都是合法的:
1 | const char *p = "abcd"; |
数组中的每个元素可以是基本类型,也可以复合类型,因此也可以是指针类型。例如定义一个数组a由10个元素组成,每个元素都是int *指针:
1 | int *a[10]; |
这称为指针数组。它可以拆成两句:
1 | typedef int *t; |
我们知道main函数的标准原型应该是int main(int argc, char *argv[]);
。argc是命令行参数的个数。而argv是一个指向指针的指针,为什么不是指针数组呢?因为前面讲过,函数原型中的[ ]
表示指针而不表示数组,等价于char **argv
。那为什么要写成char *argv[]
而不写成char **argv
呢?这样写给读代码的人提供了有用信息,argv不是指向单个指针,而是指向一个指针数组的首元素。数组中每个元素都是char *指针,指向一个命令行参数字符串。
1 |
|
注意程序名也算一个命令行参数,所以执行./a.out a b c
这个命令时,argc是4,argv如下图所示:
由于argv[4]是NULL,我们也可以这样循环遍历argv:
1 | 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]来区分不同的命令。
以下定义一个指向数组的指针,该数组有10个int元素:
1 | int (*a)[10]; /* a points to an array of 10 ints */ |
用 typedef 类型定义可使指向多维数组元素的指针更容易读、写和理解。上面的句子可以拆成两句:
1 | typedef int int_array[10]; |
t代表由10个int组成的数组类型,a则是指向这种类型的指针。
下面代码分别演示了上述的两种定义方法。
1 |
|
1 |
|
下列代码演示了使用 pa
或 ppa
访问数组 a 中的 r
元素。注意理解 pa 和 ppa 所保存的内容的不同:
1 |
|
const限定符和指针结合起来常见的情况有以下几种。
1 | const int *a; |
这两种写法是一样的,a是一个指向const int型的指针,a所指向的内存单元不可改写,所以(*a)++是不允许的,但a可以改写,所以a++是允许的。
1 | int * const a; |
a是一个指向int型的const指针,*a是可以改写的,但a不允许改写。
1 | int const * const a; |
a是一个指向const int型的const指针,因此*a和a都不允许改写。
指向非const变量的指针或者非const变量的地址可以传给指向const变量的指针,编译器可以做隐式类型转换,例如:
1 | char c = 'a'; |
但是,指向const变量的指针或者const变量的地址不可以传给指向非const变量的指针,以免透过后者意外改写了前者所指向的内存单元,例如对下面的代码编译器会报警告:
1 | const char c = 'a'; |
首先定义一个结构体类型,然后定义这种类型的变量和指针:
1 | struct unit { |
要通过指针p访问结构体成员可以写成(*p).c和(*p).num
,为了书写方便,C语言提供了->
运算符,也可以写成p->c
和p->num
。
指针可以指向基本类型,也可以指向复合类型,因此也可以指向另外一个指针变量,称为指向指针的指针。
1 | int i; |
这样定义之后,表达式*ppi
取pi的值,表达式**ppi
取i的值。
很自然地,也可以定义指向“指向指针的指针”的指针,但是很少用到:
1 | int ***p; |
在C语言中,函数也是一种类型,可以定义指向函数的指针。函数指针存放的就是函数的入口地址(位于.text段)。
函数指针的声明格式如下:
1 | int (*pointer_name)(int a, int b) = func_name; |
其中 func_name 是该指针指向的函数的名字。
如果要声明一个返回指针类型变量的函数指针,则可以这么写:
1 | char *(*pointer_name)(int awesome_levels) = func_name; |
1 |
|
函数指针的常见用途是回调函数。
要声明一个返回指针的函数,只需在函数名前加上 *
,例如:
1 | void *max(void *data[], int num, cmp_t cmp); |
实现:
1 | void *max(void *data[], int num, cmp_t cmp) |