1、数组指针
- 数组(Array)是一系列具有相同类型的数据集合,每一份数据叫做一个数组元素(Element)。
- 数组中的所有元素在内存中是连续排列的,整个数组占用一块内存。
- 以
int arr[] = { 99, 15, 100, 888, 252 };
为例,该数组在内存中的分布:
- 定义数组时,要给出数组名和数组长度,数组名可以认为是一个指向数组的第 0 个元素的指针。
- C 语言中将第 0 个元素的地址称为数组的首地址。
- 以上面的数组为例,下图是 arr 的指向:
-
- 数组名的本意是表示整个数组(多份数据的集合),但在使用过程中经常会转换为指向数组第 0 个元素的指针,所以上面使用了“认为”一词,表示数组名和数组首地址并不总是等价。
-
-
- 初学者可以暂时忽略这个细节,把数组名当做指向第 0 个元素的指针使用即可(参考第 3、4 章节)。
#include <stdio.h>
int main(){
int arr[] = {99, 15, 100, 888, 252};
// 求数组长度
int len = sizeof(arr) / sizeof(int);
int i;
for(i=0; i<len; i++){
// *(arr+i) 等价于 arr[i]
printf("%d ", *(arr+i) );
}
printf("\n");
return 0;
}
/*
99 15 100 888 252
*/
sizeof(arr)
会获得整个数组所占用的字节数,sizeof(int)
会获得一个数组元素所占用的字节数,它们相除的结果就是数组包含的元素个数,即数组长度。*(arr+i)
:arr
是数组名,指向数组的第 0 个元素,表示数组首地址, arr+i
指向数组的第 i 个元素,*(arr+i)
表示取第 i 个元素的数据,等价于arr[i]
。
-
arr
是int*
类型的指针,每次加 1 时它自身的值会增加sizeof(int)
,加i
时自身的值会增加sizeof(int) * i
。
int arr[] = {99, 15, 100, 888, 252};
int *p = arr;
arr
本身就是一个指针(数组第 0 个元素的地址),可直接赋值给指针变量 p
。int *p = arr;
也可以写作int *p = &arr[0];
。arr
、p
、&arr[0]
这三种写法都是等价的,都指向数组第 0 个元素(指向数组的开头)。
-
- 注意:“arr 本身就是一个指针”这种表述并不准确,严格来说应该是“arr 被转换成了一个指针”(参考第 3、4 章节)。
- 如果一个指针指向了数组,就称它为数组指针(Array Pointer)。
- 数组指针指向的是数组中的一个具体元素,而不是整个数组,所以数组指针的类型和数组元素的类型有关。
- 上面的例子中,
p
指向的数组元素是int
类型,所以p
的类型必须也是int *
。
-
- p 并不知道它指向的是一个数组,p 只知道它指向的是一个整数,究竟如何使用 p 取决于程序员的编码。
#include <stdio.h>
int main(){
int arr[] = {99, 15, 100, 888, 252};
int i, *p = arr, len = sizeof(arr) / sizeof(int);
for(i=0; i<len; i++){
printf("%d ", *(p+i) );
}
printf("\n");
return 0;
}
/*
99 15 100 888 252
*/
- 数组在内存中只是数组元素的简单排列,没有开始和结束标志,在求数组的长度时不能使用
sizeof(p) / sizeof(int)
,因为 p 只是一个指向 int 类型的指针,编译器并不知道它指向的到底是一个整数还是一系列整数(数组),所以sizeof(p)
求得的是p
这个指针变量本身所占用的字节数,而不是整个数组占用的字节数。
-
- 根据数组指针不能逆推出整个数组元素的个数,以及数组从哪里开始、到哪里结束等信息。
- 不像字符串,数组本身没有特定的结束标志,如果不知道数组的长度,就无法遍历整个数组。
- 对指针变量进行加法和减法运算时,是根据数据类型的长度来计算的。
-
- 如果一个指针变量
p
指向了数组的开头,那么p+i
就指向数组的第i
个元素 - 如果
p
指向了数组的第n
个元素,那么p+i
就是指向第n+i
个元素 - 不管
p
指向了数组的第几个元素,p+1
总是指向下一个元素,p-1
也总是指向上一个元素
#include <stdio.h>
int main(){
int arr[] = { 99, 15, 100, 888, 252 };
// 也可以写作: int *p = arr + 2;
int *p = &arr[2];
printf("%d, %d, %d, %d, %d\n", *(p-2), *(p-1), *p, *(p+1), *(p+2) );
return 0;
}
/*
99, 15, 100, 888, 252
*/
(1)访问数组元素
- 引入数组指针后,有两种方案来访问数组元素,一种是使用下标,另外一种是使用指针。
-
- 使用下标:采用
arr[i]
的形式访问数组元素。如果p
是指向数组arr
的指针,则可以使用p[i]
来访问数组元素(等价于arr[i]
) - 使用指针:使用
*(p+i)
的形式访问数组元素。另外数组名本身被转换成了一个指针,也可以使用*(arr+i)
来访问数组元素(等价于*(p+i)
)
- 不管是数组名还是数组指针,都可以使用上面的两种方式来访问数组元素。不同的是:
-
- 数组名是常量,它的值不能改变(数组名只能指向数组的开头)
- 数组指针是变量(除非特别指明它是常量),它的值可以任意改变(数组指针可以先指向数组开头,再指向其他元素)
#include <stdio.h>
int main(){
int arr[] = { 99, 15, 100, 888, 252 };
int i, *p = arr, len = sizeof(arr) / sizeof(int);
for(i=0; i<len; i++){
printf("%d ", *p++ );
}
printf("\n");
return 0;
}
/*
99 15 100 888 252
*/
- 第 7 行代码中的
*p++
应理解为*(p++)
,每次循环都会改变 p 的值(p++
使得 p 自身的值增加),以使 p 指向下一个数组元素。该语句不能写为*arr++
,因为 arr 是常量,而arr++
会改变它的值,这显然是错误的。
(2)关于数组指针的谜题
- 假设 p 是指向数组 arr 中第 n 个元素的指针,那么
*p++
、*++p
、(*p)++
分别是什么?
-
*p++
等价于*(p++)
,表示先取得第 n 个元素的值,再将 p 指向下一个元素。*++p
等价于*(++p)
,会先进行++p
运算,使得 p 的值增加,指向下一个元素,整体上相当于*(p+1)
,所以会获得第n+1
个数组元素的值。(*p)++
会先取得第n
个元素的值,再对该元素的值加 1。假设p
指向第 0 个元素,并且第 0 个元素的值为 99,执行完该语句后,第 0 个元素的值就会变为 100。
2、数组灵活多变的访问形式
- C 语言中的指针使得代码的编写非常灵活,如果指针能够和数组结合,那将会有更多的“花招”:
#include <stdio.h>
int main(){
char str[20] = "Hello World!";
char *s1 = str;
char *s2 = str+2;
char c1 = str[4];
char c2 = *str;
char c3 = *(str+4);
char c4 = *str+2;
char c5 = (str+1)[5];
int num1 = *str+2;
long num2 = (long)str;
long num3 = (long)(str+2);
printf(" s1 = %s\n", s1); // s1 = Hello World!
printf(" s2 = %s\n", s2); // s2 = llo World!
printf(" c1 = %c\n", c1); // c1 = o
printf(" c2 = %c\n", c2); // c2 = H
printf(" c3 = %c\n", c3); // c3 = o
printf(" c4 = %c\n", c4); // c4 = J
printf(" c5 = %c\n", c5); // c5 = W
printf("num1 = %d\n", num1); // num1 = 74
printf("num2 = %ld\n", num2); // num2 = 140726459680720
printf("num3 = %ld\n", num3); // num3 = 140726459680722
return 0;
}
str
既是数组名称,也是一个指向字符串的指针;指针可以参加运算,加 1 相当于数组下标加 1。
-
printf()
输出字符串时,要求给出一个起始地址,并从这个地址开始输出,直到遇见字符串结束标志\0
。s1
为字符串str
第 0 个字符的地址s2
为第 2 个字符的地址
- 指针可以参加运算,
str+4
表示第 4 个字符的地址,c3 = *(str+4)
表示第 4 个字符。 - 数组元素的访问形式可以看做
address[offset]
,address
为起始地址,offset
为偏移量。
-
c1 = str[4]
表示以地址str
为起点,向后偏移 4 个字符c5 = (str+1)[5]
表示以地址str+1
为起点,向后偏移 5 个字符,等价于str[6]
- 字符与整数运算时,先转换为整数(字符对应的 ASCII 码)。
num1
与c4
右边的表达式相同:
-
- 对于
num1
,*str+2 == 'H'+2 == 72+2 == 74
,即num1
的值为 74 - 对于
c4
,74 对应的字符为 J
,所以c4
的输出值为J
num2
和num3
分别为字符串 str 的首地址和第 2 个元素的地址。- 示例(加深理解):
#include <stdio.h>
#include <stdlib.h>
int main(){
// 将字符数组中的所有元素都初始化为 \0, 这样在循环结束时就无需添加字符串结束标志
char str[20] = {0};
int i;
for(i=0; i<10; i++){
// 97 为字符 a 的 ASCII 码值
*(str+i) = 97+i;
}
printf("%s\n", str);
printf("%s\n", str+2);
printf("%c\n", str[2]);
// 以地址 str+2 为起点, 向后偏移 2 个字符,等价于str[4]
printf("%c\n", (str+2)[2]);
return 0;
}
/*
abcdefghij
cdefghij
c
e
*/
3、数组和指针绝不等价,数组是另外一种类型
- 数组和指针不等价的一个典型案例:求数组的长度只能使用数组名,不能使用数组指针
#include <stdio.h>
int main(){
int a[6] = {0, 1, 2, 3, 4, 5};
int *p = a;
int len_a = sizeof(a) / sizeof(int);
int len_p = sizeof(p) / sizeof(int);
printf("sizeof(a) = %d\n", sizeof(a));
printf("sizeof(p) = %d\n", sizeof(p));
printf("sizeof(int) = %d\n", sizeof(int));
printf("len_a = %d, len_p = %d\n", len_a, len_p);
return 0;
}
/*
sizeof(a) = 24
sizeof(p) = 8
sizeof(int) = 4
len_a = 6, len_p = 2
*/
- 数组是一系列数据的集合,没有开始和结束标志,
p
仅仅是一个指向int
类型的指针,编译器不知道它指向的是一个整数还是一堆整数,对p
使用sizeof
求得的是指针变量本身的长度。
-
- 编译器并没有把
p
和数组关联起来,p
仅仅是一个指针变量,不管它指向哪里,sizeof
求得的永远是它本身所占用的字节数。
- 站在编译器的角度讲,变量名、数组名都是一种符号,它们最终都要和数据绑定起来。变量名用来指代一份数据,数组名用来指代一组数据(数据集合),它们都是有类型的,以便推断出所指代的数据的长度。
- 数组也是一种类型。我们可以将 int、float、char 等理解为基本类型,将数组理解为由基本类型派生得到的稍微复杂一些的类型。sizeof 就是根据符号的类型来计算长度的。
- 数组 a 的类型是
int [6]
,表示一个拥有 6 个 int 数据的集合,1 个 int 的长度为 4,6 个 int 的长度为 4×6 = 24,sizeof
很容易求得。 - 指针变量 p 的类型是
int *
,在 32 位环境下长度为 4,在 64 位环境下长度为 8。 - 归根结底,a 和 p 这两个符号的类型不同,指代的数据也不同,它们不是一码事,sizeof 是根据符号类型来求长度的,a 和 p 的类型不同,求得的长度也不一样。
- 二维数组也是类似的道理,如
int a[3][3]={1, 2, 3, 4, 5, 6, 7, 8, 9};
的类型是int [3][3]
,长度是4×3×3 = 36
。
- 编程语言的目的是为了将计算机指令(机器语言)抽象成人类能够理解的自然语言,让程序员能够更加容易地管理和操作各种计算机资源,这些计算机资源最终表现为编程语言中的各种符号和语法规则。
- 整数、小数、数组、指针等不同类型的数据都是对内存的抽象,它们的名字用来指代不同的内存块,程序员在编码过程中不需要直接面对内存,使用这些名字将更加方便。
- 编译器在编译过程中会创建一张专门的表格用来保存名字以及名字对应的数据类型、地址、作用域等信息,
sizeof
是一个操作符,不是函数,使用sizeof
时可以从这张表格中查询到符号的长度。 - 与普通变量名相比,数组名既有一般性也有特殊性:
-
- 一般性表现在数组名也用来指代特定的内存块,也有类型和长度
- 特殊性表现在数组名有时会转换为一个指针,而不是它所指代的数据本身的值
4、数组在什么时候会转换为指针
- 数组名的本意是表示一组数据的集合,它和普通变量一样,都用来指代一块内存;但在使用过程中,数组名有时候会转换为指向数据集合的指针(地址),而不是表示数据集合本身。
- 数据集合包含了多份数据,直接使用一个集合没有明确的含义,将数组名转换为指向数组的指针后,可以很容易地访问其中的任何一份数据,使用时的语义更加明确。
- C 语言标准规定,当数组名作为数组定义的标识符(即定义或声明数组时)、
sizeof
或&
的操作数时,它才表示整个数组本身,在其他的表达式中,数组名会被转换为指向第 0 个元素的指针(地址)。 - 数组和指针的关系颇像诗和词的关系,它们都是一种文学形式,有不少共同之处,但在实际的表现手法上又各有特色。
(1)再谈数组下标 [ ]
- C 语言标准还规定,数组下标与指针的偏移量相同。对数组下标的引用总是可以写成“一个指向数组的起始地址的指针加上偏移量”。假设现在有一个数组 a 和指针变量 p:
-
int a = {1, 2, 3, 4, 5}, *p, i = 2;
-
p = a; p[i];
p = a; *(p + i);
p = a + i; *p;
- 对数组的引用
a[i]
在编译时总是被编译器改写成*(a+i)
的形式,C 语言标准也要求编译器必须具备这种行为。 - 取下标操作符
[ ]
建立在指针的基础上,它的作用是使一个指针和一个整数相加,产生出一个新的指针,然后从这个新指针(新地址)上取得数据;假设指针的类型为T *
,所产生的结果的类型就是T
。 - 取下标操作符的两个操作数是可交换的(先后顺序不重要),就像加法中的 3+5 和 5+3。以上面的数组 a 为例,如果希望访问第 3 个元素,可以写作
a[3]
,也可以写作3[a]
,这两种形式都是正确的,只不过后面的形式不曾使用,它除了可以把初学者搞晕之外,实在没有什么实际的意义。
-
a[3]
等价于*(a + 3)
,3[a]
等价于*(3 + a)
,仅仅是把加法的两个操作数调换了位置。
- 使用下标时,编译器会自动把下标的步长调整到数组元素的大小。数组 a 中每个元素都是 int 类型,长度为 4 个字节,那么
a[i+1]
和a[i]
在内存中的距离是 4(而不是 1)。
(2)数组作函数参数
- C 语言标准规定,作为“类型的数组”的形参应该调整为“类型的指针”。
- 在函数形参定义这个特殊情况下,编译器必须把数组形式改写成指向数组第 0 个元素的指针形式。编译器只向函数传递数组的地址,而不是整个数组的拷贝。
- 这种隐式转换意味着下面三种形式的函数定义完全等价:
void func(int *parr){ ...... }
void func(int arr[]){ ...... }
void func(int arr[5]){ ...... }
- 在函数内部,arr 会被转换成一个指针变量,编译器为 arr 分配 4 个字节的内存,用
sizeof(arr)
求得的是指针变量的长度,而不是数组长度。 - 要想在函数内部获得数组长度必须额外增加一个参数,在调用函数之前求得数组长度。
- 参数传递是一次赋值的过程,赋值也是一个表达式,函数调用时不管传递的是数组名还是数组指针,效果都一样,相当于给一个指针变量赋值。
- 把作为形参的数组和指针等同起来是出于效率方面的考虑。数组是若干类型相同的数据的集合,数据的数目没有限制,可能只有几个,也可能成千上万,如果要传递整个数组,无论在时间还是内存空间上的开销都可能非常大。而且绝大部分情况下并不需要整个数组的拷贝。
(3)关于数组和指针可交换性的总结
- 用
a[i]
这样的形式对数组进行访问总是会被编译器改写(解释)为像*(a+i)
这样的指针形式。 - 指针始终是指针,不可以改写成数组。可以用下标形式访问数组,一般都是指针作为函数参数时,而且实际传递给函数的是一个数组。
- 在特定的环境中(数组作为函数形参),一个数组可以看做一个指针。作为函数形参的数组始终会被编译器修改成指向数组第一个元素的指针。
- 当希望向函数传递数组时,可以把函数参数定义为数组形式(可以指定长度或不指定长度),也可以定义为指针。不管哪种形式,在函数内部都要作为指针变量对待。