指针是 C 语言的一个重要特色。它们提供一种统一方式,能够远程访问数据结构。
指针基本的概念其实非常简单,下面的代码说明了许多这样的概念:
struct str { /* Example Structure */
int t;
char v;
};
union uni { /* Example Union */
int t;
char v;
} u;
int g = 15;
void fun(int *xp)
{
void (*f)(int*) = fun; /* f is a function pointer */
/* Allocate structure on stack */
struct str s= {1, 'a'}; /* Initialize structure */
/* Allocate union from heap */
union uni *up = (union uni *) malloc(sizeof(union uni));
/* Locally declared array */
int *ip[2] = {xp, &g};
up->v = s.v + 1;
printf("ip = %p, *ip = %p, **ip = %d\n",
ip, *ip, **ip);
printf("ip+1 = %p, ip[1] = %p, *ip[1] = %d\n",
ip+1, ip[1], *ip[1]);
printf("&s.v = %p, s.v = '%c'\n", &s.v, s.v);
prinf("&up->v = %p, up->v = '%c'\n", &up->v, up->v);
printf("f = %p\n", f);
if (--(*xp) > 0)
f(xp); /* Recursive call of fun */
}
int test()
{
int x = 2;
fun(&x);
return x;
}
-
每个指针都有一个类型。这个类型表明指针指向的对象是哪一类的。示例代码中,看到如下的一些指针类型:
注意,该表中,既指明了指针本身的类型,也指出了它所指向的对象的类型。通常,如果对象类型为 T T T,则指针的类型为 T ∗ T* T∗。特殊的void *
类型代表通用指针。例如,malloc
函数返回一个通用指针,然后它再被强制类型转换成一个有类型的指针(第21行)。 -
每个指针都有一个值。这个值是某个指定类型的对象的地址。特殊的 NULL(0) 值表示该指针没有指向任何地方。
-
指针是用
&
运算符创建的。这个运算符可应用到任何lvalue
类的 C 表达式上,即可以出现在赋值语句左边的表达式,这样的例子包括变量以及结构、联合和数组的元素。示例代码中,看到这个操作符应用到全局变量g
上(第24行),应用到结构元素s.v
上(第32行),应用到联合元素up->v
上(第33行),以及应用到局部变量x
上(第42行)。 -
*
操作符用于指针的间接引用。其结果是一个值,它的类型与该指针的类型相关。看到间接引用应用到ip
和*ip
上(第 29 行),应用到ip[1]
上(第31行),以及应用到xp
上(第 35 行)。此外,表达式up->v
(第 33 行)既间接引用了指针up
,同时还选取了域v
。 -
数组与指针是紧密联系的。可以引用一个数组的名字(但是不能修改),就好像它是一个指针变量一样。数组引用(如
a[3]
)与指针运算和间接引用(如*(a+3)
) 有一样的效果。可以在 29 行看到这一点,打印出数组ip
的指针值,并用*ip
引用它的第一项(元素0)。 -
指针也可以指向函数。这提供了一个很强大的存储(storing)和传递代码引用的功能,这些代码可以被程序的某个其他部分调用。看看变量
f
(第 15 行),它被声明为一个指向函数的变量,该函数以一个int *
作为参数,并返回void
。赋值语句使f
指向fun
。在后面使用f
(第 36 行)时,是在进行递归调用。
函数指针
函数指针声明的语法对于程序员新手是难以理解的。对于这样一个声明:void (*f)(int *);
要从里(从 “
f
” 开始)往外读。因此,看到像 “(*f)
” 表明的那样,f
是一个指针。像 “(*f)(int *)
” 表明的那样,它是一个指针,指向一个以一个int *
作为参数的函数。最后,它是一个指向一个以int *
作为参数并返回void
的函数的指针。
*f
两边的括号是必须的,因为否则声明void *f(int *);
就要读成
(void *) f(int *);
也就是,它会被解释成一个函数原型,声明了一个函数
f
,它以一个int *
作为参数并返回一个void *
。
代码中包含很多对 prinf
的调用,打印出一些指针(用指令 %p)和值。在执行时,产生下面这样的输出:
可以看到,这个函数执行了两次——第一次是从 test
中直接调用(第 42 行),而第二次是间接的递归调用(第 36 行)。可以看出,打印出来的指针值都对应于地址。那些从 0xbfffef 开始的指针指向栈中的位置,而其他的是全局存储的一部分(0x804965c),或是可执行代码的一部分(0x8048414) ,或是堆中的位置(0x8049760 和 0x8049770)。
数组ip
被初始化了两次——每次调用 fun
都初始化一次。第二次的值(0xbfffef68)小于第一次的值(0xbfffefa8),这是因为栈是向下增长的。不过,数组的内容两次都是一样的。数组元素 0(*ip)
是一个指向 test 栈帧中变量
x
x
x 的指针,元素 1 是一个指向全局变量
g
g
g 的指针。
可以看到,结构
s
s
s 也被初始化了两次,两次都是在栈中,而变量 up
指向的联合是在堆中分配的。
最后,变量
f
f
f 是一个指向函数 fun
的指针。在反汇编代码中,看到如下 fun
的初始化代码:
打印出来的指针
f
f
f 的值 0x8048414 就是 fun
的代码中第一条指令的地址。
向函数传递参数
其他语言(如Pascal)提供两种方式来向过程传递参数——传值(by value)和引用(by reference)。
传值是指调用者提供实际的参数值,而引用是指调用者提供一个指向该值的指针。
在 C 中,所有的参数都是传值的,但是可以通过显式地产生一个指向一个值的指针,并把该指针传递给过程,从而实现了引用参数的效果。函数fun(&x)
中的参数xp
就是这样的。第一次调用fuc(&x)
时(第 42 行),给了一个函数对test
中局部变量 x x x 的引用。每次调用fun
时,这个变量都会减小,从而在两次调用之后,递归会停止。