主页 > 教育

C 语言中的指针与数组

时间:2019-09-17 来源:袁园是先生

本文主要总结了《The C Programming Language》和谭浩强主编的《C 程序设计》教材中指针和数组相关章节的内容。

在 C 语言中,指针与数组之间有着非常密切的关系,一般来说,通过数组下标能完成的任何操作都可以通过指针来实现。本文将介绍指针与数组的概念和关系,以及一些相关的问题。目录如下:

  • 数组

  • 指针

  • 指针与数组的关系

  • 字符串与数组

  • 字符串与指针

  • 指针常量与常量指针

  • 指针函数与函数指针

  • 指针数组与指向指针的指针

  • 空指针与野指针

数组

在 C 语言中,数组用于表示相同类型的有序数据的集合,定义方式如下:

类型名 数组名[常量表达式];

例如:

int a[10];

它表示定义了一个整型数组,数组名为 a,该数组中有 10 个元素。换句话说,它定义了一个由 10 个元素组成的集合,这 10 个元素存储在相邻的内存区域中,名字分别 a[0]a[1]、…、a[9],如下图所示:

其中,a[i] 表示该数组中的第 i 个元素(i 从 0 开始计数)。

此外,给数组元素进行初始化有如下几条规则:

  • 在定义数组时对数组元素赋予初值,例如:

int a[10] = {0123456789};

  • 也可以只给一部分元素赋值,例如:

int a[10] = {01234};

表示定义 a 数组有 10 个元素,并给前 5 个元素赋初值,后 5 个元素值默认为 0。但是我们无法跳着给某些元素赋值,例如 int a[5] = {,,3,4,5}; 是错误的写法。

  • 给数组中的元素全部赋予相同的初值 0,例如:

int a[10] = {0000000000};
int a[10] = {0}; // 与上面等价

注意,上述是一个特例,两种写法之所以等价是因为 int a[10] 存储在内存栈上,它所有的元素默认为 0,第二种写法只是初始化了第一个元素为 0 而已,如果把 0 改为 2,两者是不等价的。

  • 给数组赋初值时,如果数据的个数已经确定,则可以不指定数组的长度:

int a[5] = {12345}; // 可以写成如下形式
int a[] = {12345};  // 声明时省略了数组的长度

上述第二种写法,花括号中有 5 个数,编译器在编译时会根据此自动定义 a 数组的长度为 5。但如果要定义的数组的长度与提供的初值的个数不同,则数组长度不能忽略。例如,想定义数组长度为 10,就不能省略数组长度的定义,而必须写成:int a[10] = {1, 2, 3, 4, 5};,表示初始化前五个元素,而后五个元素为 0。

指针

指针是一种保存变量地址的变量,定义方式如下:

类型名 * 指针变量名;

在程序中定义了一个变量,在编译时,系统会给这个变量分配内存单元。编译系统根据程序中定义的变量类型,分配一定长度的内存空间(不同类型的长度不同,一般字符类型为 1 个字节,整型为 2 个或 4 个字节等)。在内存区的每一个存储单元都有一个编号,这就是“地址”的概念,而指针变量就是用于存放“变量地址”的变量。

如下图所示,如果变量 c 的类型为 char,在内存中存放的位置为图中的位置,我们可以定义一个指针变量 p,指向 c 的存储位置。

char * p = &c;

指针主要有两个运算符:

  • &:取址运算符

  • *:指针运算符(或称为“间接访问”运算符),取其指向的内容。

例如上述例子中,&c 表示变量 c 的地址,*p 表示指针变量 p 所指向的存储单元的内容(即 p 所指向的变量 c 的值)。

此外,在定义指针时声明的类型,表示该指针所指向的地址存放的内容的数据类型。而所有指针变量自身的类型都为整型,其所占的大小(字节数)在不同位数的操作系统中不一样。

思考一个问题,既然指针变量是用来存放地址的(且它自身的类型都为整型),那么好像只需要指定其为“指针型变量”即可,为什么定义指针时还要声明其指向内容的类型呢?

如前面所述,不同类型的数据在内存中所占的字节数是不同的,而对于指针的“移动”或“加减运算”,例如“使指针移动 1 个位置”或者“使指针值加 1”,这里的 “1” 代表什么呢?

“指针加 1”,表示与指针所指向的数据相邻的下一个数据的地址。举个例子,如果指针是指向一个整型变量(假设为 2 个字节),那么“使指针移动 1 个位置”意味着移动 2 个字节,“使指针值加 1” 意味着使地址值加 2 个字节。而如果指针是指向一个浮点型变量(假设为 4 个字节),则增加的不是 2 而是 4 个字节了。因此必须指定指针变量所指向的变量的类型,即“基类型”,这样才能准确地对指针进行相关位移操作。

最后需要注意的是,一个指针变量只能指向同一个类型的变量,即不能把声明为指向字符型的指针指向整型等其它类型的变量。

指针与数组的关系

在第一小节中,声明了一个数组 int a[10];,假设这里我们又定义了一个指针变量 pa 如下:

int *pa;

则说明 pa 是一个指向整型数据的指针,那么赋值语句:

pa = &a[0];

表示将指针 pa 指向数组 a 的第 0 个元素,也就是说,pa 的值为数组元素 a[0] 的地址,如下图:

那么赋值语句 int x = *pa; 表示将把数组元素 a[0] 中的值复制到变量 x中,与 int x = a[0]; 是等价的。

如果 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] 的内容,如下图所示:

无论数组 a 中的元素类型或者数组长度是什么,上面结论都成立。“指针加 1”就意味着,pa+1 表示 pa 所指向的元素的下一个元素。

数组下标和指针运算之间具有密切的对应关系。C 语言规定,数组名代表数组中首个元素(即序号为 0 的元素)的地址。也就是说,数组名可以理解为是一个指针常量。

所以,执行赋值语句 pa = &a[0]; 后,pa  a 具有相同的值。因为数组名所代表的就是该数组最开始的一个元素的地址,因此, pa = &a[0]; 也可以写成 pa = a;

对数组元素 a[i] 的引用也可以写成 *(a+i) 的形式。实际上,在编译过程中,编译器也是先把 a[i] 转换成 *(a+i) 这种指针表示形式然后再进行求值,所以这两种形式是等价的

当我们对 a[i]  *(a+i) 分别进行取址运算,可以知道 &a[i]  & *(a+i)(简化为 a+i)也是相同的,a+i 表示 a 之后第 i 个元素的地址。

相应的,如果 pa 是一个指针,那么,在表达式中也可以在它的后面添加下标。pa[i]  *(pa+i) 是等价的。简而言之