3.10.1 理解指针

3.10.1 理解指针

指针是 C 语言的一个核心特色。它们以一种统一方式,对不同数据结构中的元素产生引用。对于编程新手来说,指针总是会带来很多的困惑,但是基本概念其实非常简单。在此,我们重点介绍一些指针和它们映射到机器代码的关键原则。

  • 每个指针都对应一个类型。这个类型表明该指针指向的是哪一类对象。以下面的指针声明为例:

    int *ip;
    char **cpp;

    变量 ip 是一个指向 int 类型对象的指针,而 cpp 指针指向的对象自身就是一个指向 char 类型对象的指针。通常,如果对象类型为 T,那么指针的类型为 T*。特殊的 void * 类型代表通用指针。比如说,malloc 函数返回一个通用指针,然后通过显式强制类型转换或者赋值操作那样的隐式强制类型转换,将它转换成一个有类型的指针。指针类型不是机器代码中的一部分;它们是 C 语言提供的一种抽象,帮助程序员避免寻址错误。

  • 每个指针都有一个值。这个值是某个指定类型的对象的地址。特殊的 NULL(0) 值表示该指针没有指向任何地方。

  • 指针用 & 运算符创建。这个运算符可以应用到任何 lvalue 类的 C 表达式上,lvalue 意指可以出现在赋值语句左边的表达式。这样的例子包括变量以及结构、联合和数组的元素。我们已经看到,因为 leaq 指令是设计用来计算内存引用的地址的,& 运算符的机器代码实现常常用这条指令来计算表达式的值。

  • * 操作符用于间接引用指针。其结果是一个值,它的类型与该指针的类型一致。间接引用是用内存引用来实现的,要么是存储到一个指定的地址,要么是从指定的地址读取。

  • 数组与指针紧密联系。一个数组的名字可以像一个指针变量一样引用(但是不能修改)。数组引用(例如 a[3])与指针运算和间接引用(例如 *(a + 3))有一样的效果。数组引用和指针运算都需要用对象大小对偏移量进行伸缩。当我们写表达式 p + i,这里指针 p 的值为 p,得到的地址计算为 p + L · i,这里 L 是与 p 相关联的数据类型的大小。

  • 将指针从一种类型强制转换成另一种类型,只改变它的类型,而不改变它的值。强制类型转换的一个效果是改变指针运算的伸缩。例如,如果 p 是一个 char * 类型的指针,它的值为 p,那么表达式 (int *) p + 7 计算为 p + 28,而 (int *) (p + 7) 计算为 p + 7。(回想一下,强制类型转换的优先级高于加法。)

  • 指针也可以指向函数。这提供了一个很强大的存储和向代码传递引用的功能,这些引用可以被程序的某个其他部分调用。例如,如果我们有一个函数,用下面这个原型定义:

    int fun(int x, int *p);

    然后,我们可以声明一个指针 fp,将它赋值为这个函数,代码如下:

    int (*fp)(int, int *);
    fp = fun;

    然后用这个指针来调用这个函数:

    int y = 1;
    int result = fp(3, &y);

    函数指针的值是该函数机器代码表示中第一条指令的地址。

给 C 语言初学者:函数指针

函数指针声明的语法对程序员新手来说特别难以理解。对于以下声明:

int (*f)(int*);

要从里(从“f”开始)往外读。因此,我们看到像“(*f)”表明的那样,f 是一个指针;而“(*f)(int*)”表明 f 是一个指向函数的指针,这个函数以一个 int * 作为参数。最后,我们看到,它是指向以 int * 为参数并返回 int 的函数的指针。

*f 两边的括号是必需的,否则声明变成

int *f(int*);

它会被解读成

(int *) f(int*);

也就是说,它会被解释成一个函数原型,声明了一个函数 f,它以一个 int * 作为参数并返回一个 int *

Kernighan 和 Ritchie [61, 5.12 节] 提供了一个有关阅读 C 声明的很有帮助的教程。