04指针与数组
指针与地址
通常的机器都有一系列连续编号或编址的存储单元,这些存储单元可以单个进行操纵,也可以连续成组的方式操纵。通常情况下,机器的一个字节可以存放一个 char 类型的数据,两个相邻的字节存储单元可存储一个 short(短整型)类型的数据,而4个相邻的字节存储单元可存储一个 long(长整型)类型的数据。指针是能够存放一组存储单元(通常是两个或4个字节)。
一元运算符 & 可用于取一个对象的地址,因此,下列语句
p = &c;
将把 c 的地址赋值给变量 p,我们称 p 为「指向」c 的指针。地址运算符 & 只能应用于内存中的对象,即变量与数组元素。它不能作用于表达式、常量或 register 类型的变量。
一元运算符 * 是间接寻址或间接引用运算符。当它作用于指针时,将访问指针所指向的对象。
下面代码说明了如何在程序中声明指针以及如何使用运算符 & 和 *:
int x = 1, y = 2, z[10];
int *ip; //p是指向int类型的指针
int *iq;
ip = &x; //ip指向变量x的地址
y = *ip; //将ip指向变量x的值赋给y y = 1
*ip = 0; // x = 0
ip = &z[0]; //ip指向数组z的第一个元素的地址
y = *ip + 1;//把ip指向的对象的值取出并加1,然后将结果赋值给y
*ip += 1; //将ip指向的对象的值加1
iq = ip; //把ip中的值拷贝到iq中,这样,指针iq也将指向ip指向的对象
++*ip; //将ip指向的对象的值加1
(*ip)++ //将ip指向的对象的值加1
语句 (*ip)++
中的圆括号是必需的,否则,该表达式将对 ip (指向的地址)进行加 1 运算,而不是对 ip 指向的对象进行加 1 运算,这是因为,类似于 *
和 ++
这样的一元运算符遵循从右至左的结合顺序。
指针与函数参数
void swap(int *px, int *py) {
int temp;
temp = *px;
*px = *py;
*py = temp;
}
通过调用上述函数swap(&a,&b)
用来交换 a 和 b 的值,由于一元运算符 & 用来取变量的地址,这样 &a 就是一个指向变量 a 的指针。swap 函数的所有参数都声明为指针,并且通过这些指针来间接访问它们指向的操作数。
指针与数组
在 C 语言中,通过数组下标所能完成的任何操作都可以通过指针来实现。一般来说,用指针编写的程序比用数组下标编写的程序执行速度快。
声明
int a[10];
定义了一个长度为10的数组a。换句话说,它定义了一个由10个对象组成的集合,这10个对象存储在相邻的内存区域中,名字分别为a[0]、a[1]、···、a[9].
a[i] 表示该数组的第 i 个元素。如果 pa 的声明为:
int *pa;
则说明它是一个指向整型对象的指针,那么,赋值语句
pa = &a[0];
则可以将指针 pa 指向数组 a 的第 0 个元素,也就是说,pa 的值为数组元素a[0]的内存地址。
这样,赋值语句
x = *pa
将把数组元素 a[0]中的内容复制到变量 x 中。
如果 pa 指向数组中的某个特定元素,那么,根据指针运算的定义,pa + 1 将指向下一个元素,pa + i 将指向 pa 所指向数组元素之后的第 i 个元素,而 pa - i 将指向 pa 所指向数组元素的之前的第 i 个元素。因此,如果指针 pa 指向 a[0],那么*(pa + 1)
引用的是数组元素a[1]的内容,pa + i 是数组元素a[i]的地址,*(pa + i)
引用的是数组元素a[i]的内容.
根据定义,数组类型的变量或表达式的值是该数组第0个元素的地址。执行赋值语句
pa = &a[0];
pa 和 a 具有相同的值。因为数组名所代表的就是该数组最开始一个元素的地址,所以,赋值语句 pa = &a[0] 也可以写成下列形式:
pa = a;
对数组元素a[i]的引用也可以写成
*(a + i)
这种形式。实际上,在计算数组元素 a[i] 的值时, C 语言先将其转换为*(a + i)
的形式,然后在进行求值,因此在程序中这两种形式是等价的。如果对这两种的表示形式分别使用地址运算符 & ,便可以得出这样的结论:&a[i] 和 a + i 的含义也是相同的。a + i 是 a 之后第 i 个元素的地址。
数组传递
当把数组名传递给一个函数时,实际上传递的是该数组第一个元素的地址。在被调用函数中,该参数是一个局部变量,因此,数组名参数必须是一个指针,也就是一个存储地址值的变量。
//计算一个字符串的长度
int strlen(char *s) {
int n;
for(n = 0; *s != '\0',s++) {
n++;
}
return n;
}
因为 s 是一个指针,所以对其执行自增运算是合法的。执行 s++ 运算不会影响到 strlen函数的调用者中的字符串,它仅对该指针在strlen函数中的私有副本进行自增运算。下面的函数调用都是合法的:
strlen("hello, world");
strlen(array); //char array[100];
strlen(ptr); // char *ptr
在函数定义中,形式参数 char s[]
和 char *s
是等价的。如果将数组名传递给该函数,函数可以根据情况判定是按照数组处理还是指针处理,随后根据相应的方式操作该参数。也可以将指向子数组起始位置的指针传递给函数,这样,就将数组的一部分传递给了函数。例如,如果 a 是一个数组,那么下面两个函数调用
f(&a[2]);
与
f(a + 2);
都将把起始于 a[2] 的子数组的地址传递给函数f。在函数f中,参数的声明形式可以为
void f(int arr[]); //数组传递
void f(int *arr); //数组名传递
地址算术运算
如果 p 是一个指向数组中某个元素的指针,那么 p++ 将对 p 进行自增运算并指向下一个元素,而 P += i 将对 p 进行加 i 的增量运算,使其指向指针 p 当前所指向的元素之后的第 i 个元素。
如果指针 p 和 q 指向用一个数组的成员,那么它们之间就可以进行类似于 ==、!=、<、>=的关系比较运算。如果 p 指向的数组元素的位置在 q 指向的数组元素位置之前,那么关系表达式p < q
的值为真。
如果 p 和 q 指向相同数组的中的元素,且 p < q
,那么q - p + 1
就是位于 p 和 q 指向的元素之间的元素的数目。由此,我们可以编出函数 strelen 的另一个版本,如下所示:
int strlen(char *s) {
char *p = s;
while(*p != '\0')
p++;
return p -s;
}
在上述程序段的声明中,指针 p 被初始化为指向 s,即指向该字符串的第一个字符。while 循环语句将依次检查字符串中的每个字符,直到遇到标识字符数结尾的字符'\0'
为止。由于 p 是指向字符的指针,所以每执行一次 p++,p 就将指向下一个字符的地址,p - s 则表示已经检查过的字符数,即字符串的长度。(字符串中的字符数有可能超过 int 类型所能表示的最大范围。可以使用 size_t 作为函数 strlen 的返回值类型)。
字符指针与函数
字符串常量是一个字符数组,例如:I am a String
在字符串内部表示中,字符数组以空字符'\0'
结尾,所以,程序可以通过检查空字符找到字符数组的结尾。字符串常量占据的存储单元数也因此比双引号内的字符数大1。
假定指针pmessage的声明如下:
char *pmessage;
那么,语句
pmessage = "now is the time";
将把一个指向该字符数组的指针赋值给 pmessage。该过程并没有进行字符串的复制,而只是涉及到指针的操作。C 语言没有提供将整个字符串作为一个整体进行处理的运算符。
下面两个定义之间有很大的差别:
char amessage[] = "now is the time"; //定义一个数组
char *pmessage = "now is the time"; //定义一个指针
amessage 是一个仅仅足以存放初始化字符串以及空字符'\0'
的一维数组。数组中的单个字符可以进行修改,但 amessage 始终指向同一个存储位置。
pmessage 是一个指针,其初值指向一个字符串常量,之后它可以被修改以指向其他地址,但如果试图修改字符串的内容,结果是没有定义的。
下面给出两种实现字符串复制的方式:
//数组实现字符串的复制
void strcpy(char *s, char *t) {
int i;
i = 0;
while((s[i] = t[i]) != '\0')
i++;
}
//指针实现字符串的复制
void strcpy(char *s, char *t) {
while((*s = *t) != '\0')
s++;
t++;
}
void strcpy(char *s, char *t) {
while((*s++ = *t++) != '\0')
;
}
void strcpy(char *s, char *t) {
while(*s++ = *t++)
;
}
指针数组以及指向指针的指针
指针数组的声明:
char *lineptr[MAXLINES] = {“Hello, world!”,"Just do it!"};
它表示 lineptr 是一个具有 MAXLINES 个元素的一维数组,其中数组的每个元素是一个指向字符类型对象的指针。也就是说,lineptr[i]是一个字符指针,而 *lineptr[i] 是该指针指向的第 i 个文本行的首字符。
linepter本身是一个数组名,因此,可按照前面例子中相同的方法将其作为指针使用,可以写一个函数来打印 lineptr 数组。
void write_lines(char *linePtr[],int n) {
while (n > 0) {
printf("%s\n",*linePtr++);
n--;
}
}
循环开始时,*lineptr
指向第一行,每执行一次自增运算都使得 lineptr 指向下一行,同时对 n 进行自减运算。
多维数组
在 C 语言中,二维数组实际上是一种特殊的一组数组,它的每个元素也是一个一维数组。因此,数组下标应该写成
int daytab[i][j];
指针声明二维数组,如下
int *daytab[13];
上述声明了一个数组,该数组有 13 个元素,其中每个元素都是一个指向整型对象的指针。一般来说,除数组的第一维(下标)可以不指定大小外,其余各维都必须明确指定大小。
指针数组的初始化
指针数组的初始化语法和其它类型对象的初始化语法类似:
char *name[] = {"January","February","March","April","May","June"};
name 是一个一维数组,数组的元素是字符指针。name 数组的初始化通过一个字符串列表实现,列表中的每个字符串赋值给数组相应位置的元素。第 i 个字符串的所有字符存储在存储器中的某个位置,指向它的指针存储在 name[i] 中。由于上述声明中没有指明 name 的长度,因此,编译器编译时将对初值个数进行统计,并将这一准确数组填入数组的长度。
指针与多维数组
假如有下面两个定义:
int a[10][20];
int *b[10];
a 是一个真正的二维数组,它分配了 200 个 int 类型长度的存储空间,并且通过常规的矩阵下标计算公式 20 * row * col
计算得到 a[row][col] 的位置。
对 b 来说,该定义仅仅分配了 10 个指针,并且没有对它们初始化,它们的初始化必须以显式的方式进行,比如静态初始化或通过代码初始化。假定 b 的每个元素都指向一个具有 20 个元素的数组,那么编译器就要为它分配 200 个 int 类长度的存储空间以及 10 个指针的存储空间。指针数组的一个重要优点在于,数组的每一行长度可以不同,也就是说,b 的每个元素不必都指向一个具有 20 个元素的向量,某些元素可以指向具有 2 个元素的向量,某些元素可以指向具有 50 个元素的向量,而某些元素可以不指向任何向量。
命令行参数
在支持 C 语言的环境中,可以在程序开始执行时将命令行参数传递给程序。调用主函数 main 时,它带有两个参数。第一个参数的值(argc,用于参数计数)表示运行程序时命令行中参数的数目;第二个参数(argv,用于参数向量)是一个指向字符串数组的指针,其中每个字符串对应一个参数。
指向函数的指针
在 C 语言中,函数本身不是变量,但可以定义指向函数的指针。这种类型的指针可以被赋值、存放在数组中、传递给函数以及作为函数的返回值等。
int (*comp)(void *,void*)
它表明 comp 是一个指向函数的指针,该函数具有两个 void*
类型的参数,其返回值类型为 int。
int *comp(void *,void*)
它表明 comp 是一个函数,该函数返回一个指向 int 类型的指针。
#include <stdio.h>
int sum(int, int);
int (*FunPtr)(int, int);
int main() {
int a,b;
FunPtr = sum;
a = sum(3,7);
b = FunPtr(3,7);
printf("a = %d, b = %d\n",a,b);
return 0;
}
int sum(int x, int y) {
return x + y;
}