The C Programming Language - annotation, 5

Pointer and Array

指针是一种保存变量地址的变量。在C语言中,指针的使用非常广泛,原因之一是,指针常常是表达某个计算的惟一途径,另一个原因是,同其他方法比较起来,使用指针通常可以生产更高效、更紧凑的代码。指针与数组之间的关系十分密切。

指针和goto语句一样,会导致程序难以理解。如果使用者粗心,指针很容易就指向了错误的地方。但是,如果谨慎地使用指针,便可以利用它写出简单、清晰的程序。

ANSI C的一个最重要的变化是,它明确地制定了操纵指针的规则。事实上,这些规则已经被很多优秀的程序设计人员和编译器所采纳。此外,ANSI C使用类型void *(指向void的指针)代替char *作为通用指针的类型。

1. 指针与地址

通常的机器都有一系列连续编号或编址的存储单元,这些存储单元可以单个进行操纵,也可以以连续成组的方式操纵。通常情况下,机器的一个字节可以存放一个char类型的数据,两个相邻的字节存储单元可存储一个short(短整型)类型的数据,而4个相邻的字节存储单元可存储一个long(长整型)类型的数据。指针是能够存放一个地址的一组存储单元(通常是两个或4个字节)。

一元运算符&可用于取一个对象的地址,…称p为“指向”c的指针。地址运算符&只能应用于内存中的对象,即变量与数组元素。它不能作用于表达式、常量或register类型的变量。

一元运算符*是间接寻址或间接引用运算符。当它作用于指针时,将访问指针所指向的对象。

…指针的声明,是为了便于记忆。这种声明变量的语法与声明该变量所在表达式的语法类似。同样的原因,对函数的声明也可以采用这种方式。

…指针只能指向某种特定类型的对象,也就是说,每个指针都必须指向某种特定的数据类型。(一个例外情况是指向void类型的指针可以存放指向任何类型的指针,但它不能间接引用其自身。)。

如果指针ip指向整形变量x,那么在x可以出现的任何上下文中都可以使用*ip。

一元运算符*&的优先级比算术运算符的优先级高。…从右至左的结合顺序。

由于指针也是变量,所以在程序中可以直接使用,而不必通过间接引用的方法使用。

2. 指针与函数参数

由于C语言是以传值的方式将参数传递给被调用函数,因此,被调用函数不能直接修改主调函数中变量的值。

…可以使主调函数将指向变量的指针传递给被调用函数。

指针参数使得被调用函数能够访问和修改主调函数中对象的值。

3. 指针与数组

在C语言中,指针和数组之间的关系十分密切。通过数组下标所能完成的任何操作都可以通过指针来实现。一般来说,用指针编写的程序比用数组下标编写的程序执行速度快,但另一方面,用指针实现的程序理解起来稍微困难一些。

声明

int a [10];

定义了一个长度为10的数组a。换句话说,它定义了一个由10个对象组成的集合,这10个对象存储在相邻的内存区域中,名字分别为a[0]、a[1]、…、a[9]。a[i]表示该数组的第i个元素。…指针pa指向数组a的第0个元素,pa的值为数组元素a[0]的地址。

如果pa指向数组中的某个特定元素,那么,根据指针运算的定义,pa+1将指向下一个元素,pa+i将指向pa所指向数组元素之后的第i个元素,而pa-i将指向pa所指向数组元素之前的第i个元素。因此,如果指针pa指向a[0],那么pa+i是数组元素a[i]的地址,*(pa+i)引用的是数组元素a[i]的内容。

无论数组a中元素的类型或数组长度是什么,上面的结论都成立。“指针加1”就意味着,pa+1指向pa所指向的对象的下一个对象。相应地,pa+i指向pa所指向的对象之后的第i个对象。

下标和指针运算之间具有密切的对应关系。根据定义,数组类型的变量或表达式的值是该数组第0个元素的地址。执行赋值语句

pa = &a[0];

后,pa和a具有相同的值。因为数组名所代表的就是该数组最开始的一个元素的地址,所以,赋值语句也可以写成下列形式:

pa = a;

…一个通过数组和下标实现的表达式可等价地通过指针和偏移量实现。

数组名和指针之间有一个不同之处。指针是一个变量…但数组名不是变量。

当把数组名传递给一个函数时,实际上传递的是该数组第一个元素的地址。在被调用函数中,该参数是一个局部变量,因此,数组名参数必须是一个指针,也就是一个存储地址值的变量。

在函数定义中,形式参数char s[];char *s;是等价的。我们通常更习惯于使用后一种形式,因为它比前者更直观地表明了该参数是一个指针。如果将数组名传递给函数,函数可以根据情况判定是按照数组处理还是按照指针处理,随后根据相应的方式操作该参数。为了直观且恰当地描述函数,在函数中甚至可以同时使用数组和指针这两种表示方法。

也可以将指向子数组起始位置的指针传递给函数,这样,就将数组的一部分传递给了函数。…对于函数来说,它并不关心所引用的是否只是一个更大数组的部分元素。

如果确信相应的元素存在,也可以通过下标访问数组第一个元素之前的元素。当然,引用数组边界之外的对象是非法的。

4. 地址算术运算

如果p是一个指向数组中某个元素的指针,那么p++将对p进行自增运算并指向下一个元素,而p+=i将对p进行加i的增量运算,使其指向指针p当前所指向的元素之后的第i个元素。这类运算是指针或地址算术运算中最简单的形式。

C语言中的地址算术运算方法是一致且有规律的,将指针、数组和地址的算术运算集成在一起是该语言的一大优点。

一般情况下,同其他类型的变量一样,指针也可以初始化。通常,对指针有意义的初始化值只能是0或者是表示地址的表达式,对后者来说,表达式所代表的地址必须是在此前已定义的具有适当类型的数据的地址。

…C语言保证,0永远不是有效的数据地址,因此,返回值0可用来表示发生了异常事件。

指针与整数之间不能相互转换,但0是惟一的例外:常量0可以赋值给指针,指针也可以和常量0进行比较。程序中经常用符号常量NULL代替常量0,这样便于更清晰地说明常量0是指针的一个特殊值。

指针算术运算有以下几个重要特点。首先,在某些情况下对指针可以进行比较运算。例如,如果指针p和q指向同一个数组的成员,那么它们之间就可以进行类似于==!=<>=的关系比较运算。如果p指向的数组元素的位置在q指向的数组元素位置之前,那么关系表达式p < q的值为真(true)。任何指针与0进行相等或不等的比较运算都有意义。但是,指向不同数组的元素的指针之间的算术或比较运算没有定义。(这里有一个特例:指针的算术运算中可使用数组最后一个元素的下一个元素的地址。)

指针可以和整数进行相加或相减运算。结构p + n表示指针p当前所指向的对象之后第n个对象的地址。…n将根据p指向的对象的长度按比例缩放,而p指向的对象的长度则取决于p的声明。

指针的减法运算也是有意义的:如果p和q指向相同数组中的元素,且p < q,那么q-p+1就是位于p和q指向的元素之间的元素的数目。

指针的算术运算具有一致性:所有的指针运算都会自动考虑它所指向的对象的长度。

有效的指针运算包括相同类型指针之间的赋值运算;指针同整数之间的加法或减法运算;指向相同数组中元素的两个指针间的减法或比较运算;将指针赋值为0或指针与0之间的比较运算。其他所有形式的指针运算都是非法的。

5. 字符指针与函数

字符串常量是一个字符数组,在字符串的内部表示中,字符数组以空字符\0结尾,所以,程序可以通过检查空字符找到字符数组的结尾。字符串常量占据的存储单元数也因此比双引号内的字符数大1。

字符串常量最常见的用法也许是作为函数参数,…实际上是通过字符指针访问该字符串的。字符串常量可通过一个指向其第一个元素的指针访问。

…该过程并没有进行字符串的复制,而只是涉及到指针的操作。C语言没有提供将整个字符串作为一个整体进行处理的运算符。

6. 指针数组以及指向指针的指针

由于指针本身也是变量,所以它们也可以像其他变量一样存储在数组中。

…消除了因移动文本行本身所带来的复杂的存储管理和巨大的开销这两个孪生问题。

…通常情况下,最好将程序划分成若干个与问题的自然划分相一致的函数,并通过主函数控制其他函数的执行。

7. 多维数组

C语言提供了类似于矩阵的多维数组,但实际上它们并不像指针数组使用得那样广泛。

在C语言中,二维数组实际上是一种特殊的一维数组,它的每个元素也是一个一维数组。除了表示方式的区别外,C语言中二维数组的使用方式和其他语言一样。数组元素按行存储,因此,当按存储顺序访问数组时,最右边的数组下标(即列)变化得最快。

数组可以用花括号括起来的初值表进行初始化,二维数组的每一行由相应的子列表进行初始化。

如果将二维数组作为参数传递给函数,那么在函数的参数声明中必须指明数组的列数。数组的行数没有太大关系,函数调用时传递的是一个指针,它指向由行向量构成的一维数组,其中每个行向量是具有13个整型元素的一维数组。在该例子中,传递给函数的是一个指向很多对象的指针,其中每个对象是由13个整型元素构成的一维数组。

…一般来说,除数组的第一维(下标)可以不指定大小外,其余各维都必须明确指定大小。

8. 指针数组的初始化

…由于上述声明中没有指明数组的长度,因此,编译器编译时将对初值个数进行统计,并将这一准确数字填入数组的长度。

9. 指针与多维数组

…指针数组的一个重要优点在于,数组的每一行长度可以不同。

…指针数组最频繁的用处是存放具有不同长度的字符串。

10. 命令行参数

在支持C语言的环境中,可以在程序开始执行时将命令行参数传递给程序。调用主函数main时,它带有两个参数。第一个参数(习惯上称为argc,用于参数计数)的值表示运行程序时命令行中参数的数目;第二个参数(称为argv,用于参数向量)是一个指向字符串数组的指针,其中每个字符串对应一个参数。我们通常用多级指针处理这些字符串。

按照C语言的约定,argv[0]的值是启动该程序的程序名,因此argc的值至少为1.如果argc的值为1,则说明程序名后面没有命令行参数。…第一个可选参数为argv[1],而最后一个可选参数为argv[argc-1]。另外,ANSI标准要求argv[argc]的值必须为一空指针。

UNIX系统中的C语言程序有一个公共的约定:以负号开头的参数表示一个可选标志或参数。

可选参数应该允许以任意次序出现,同时,程序的其余部分应该与命令行中参数的数目无关。此外,如果可选参数能够组合使用,将会给使用者带来更大的方便。

11. 指向函数的指针

在C语言中,函数本身不是变量,但可以定义指向函数的指针。这种类型的指针可以被赋值、存放在数组中、传递给函数以及作为函数的返回值等等。

…因为它们是函数,所以前面不需要加上取地址运算符&

12. 复杂声明

C语言常常因为声明的语法问题而受到人们的批评,特别是涉及到函数指针的语法。C语言的语法力图使声明和使用相一致。对于简单的情况,C语言的做法是很有效的,但是,如果情况比较复杂,则容易让人混淆,原因在于,C语言的声明不能从左至右阅读,而且使用了太多的圆括号。…*是一个前缀运算符,其优先级低于(),所以,声明中必须使用圆括号以保证正确的结合顺序。

尽管实际中很少用到过于复杂的声明,但是,懂得如何理解甚至如何使用这些复杂的声明是很重要的。…一种比较好的方法是,使用typedef通过简单的步骤合成。