本文介绍数组与指针的特关系,以及数组指针,指针数组的区别,多维数组的知识。
一维数组
数组名
int a;
int b[10];
我们把变量a称为标量,因为它是个单一的值,这个变量的类型是一个整数。我 们把变量b称为数组,因为它是一些值的集合。
下标和数组名一起使用,用于标识该 集合中某个特定的值,数组名的值是一个指针常量,也就是数组第1个元素的地址。它的类型取决于 数组元素的类型:
如果它们是int类型,那么数组名的类型就是“指向int的常量指 针”;如果它们是其他类型,那么数组名的类型就是“指向其他类型的常量指 针”。
但是请不要根据这个事实得出数组和指针是相同的结论。数组具有一些和指针完全 不同的特征。
例如,数组具有确定数量的元素,而指针只是一个标量值。编译器用 数组名来记住这些属性。只有当数组名在表达式中使用时,编译器才会为它产生一 个指针常量。 注意这个值是指针常量,而不是指针变量。你不能修改常量的值。你只要稍微 回想一下,就会认为这个限制是合理的:指针常量所指向的是内存中数组的起始位 置,如果修改这个指针常量,唯一可行的操作就是把整个数组移动到内存的其他位 置。但是,在程序完成链接之后,内存中数组的位置是固定的,所以当程序运行 时,再想移动数组就为时已晚了。因此,数组名的值是一个指针常量。
只有在两种场合下,数组名并不用指针常量来表示——就是当数组名作为 sizeof操作符或单目操作符&的操作数时。
sizeof返回整个数组的长度,而不是指 向数组的指针的长度。
取一个数组名的地址所产生的是一个指向数组的指针(而不是一个指向某个指针常量值的指 针。
总结就是:数组名表示指向数组第一个元素的指针,但他是个常量。&数组名,例如:int a[10]
&a 表示指向一个指向数组的指针,对他进行加一操作 :&a + 1 移动的是一个数组的长度。
数组下标与指针
int a[10];
除了优先级之外,下标引用和间接访问完 全相同。
即:a[3] = = *(a+3)
对任意数组有:
array[subscript]
*( array + ( subscript ) )
以上两个表达式都相等
例如:
int array[10];
int *ap = array + 2;
在下面各个涉及ap的表达式中,看看你能不能写出使用array的对等表达式。
*ap 这个也很容易,间接访问跟随指针访问它所指向的位置,也就是 array[2]。你也可以这样写:*(array+2)。
ap[0] “你不能这样做,ap不是一个数组!”如果你是这样想的,你就陷 入了“其他语言不能这样做”这个惯性思维中了。记住,C的下标引用和间接访问表 达式是一样的。在现在这种情况下,对等的表达式是*(ap+(0)),除去0和括号,其 结果与前一个表达式相等。因此,它的答案和上一题相同:array[2]。
ap+6 如果ap指向array[2],这个加法运算产生的指针所指向的元素是 array[2]向后移动6个整数位置的元素。与它对等的表达式是array+8或 &array[8]。 *ap+6 小心!这里有两个操作符,哪一个先执行呢?是间接访问。间接访问 的结果再与6相加,所以这个表达式相当于表达式array[2]+6。 *(ap+6) 括号迫使加法运算首先执行,所以我们这次得到的值是 array[8]。注意这里的间接访问操作和下标引用操作的形式是完全一样的。 ap[6] 把这个下标表达式转换为与其对应的间接访问表达式形式,你会发现 它就是我们刚刚完成的那个表达式,所以它们的答案相同。
&ap 这个表达式是完全合法的,但此时并没有对等的涉及array的表达式, 因为你无法预测编译器会把ap放在相对于array的什么位置。 ap[-1] 怎么又是它?负值的下标!下标引用就是间接访问表达式,你只要 把它转换为那种形式并对它进行求值。ap指向第3个元素(就是那个下标值为2的元 素),所以使用偏移量-1使我们得到它的前一个元素,也就是array[1]。
ap[9] 这个表达式看上去很正常,但实际上却存在问题。它对等的表达式是 array[11],但问题是这个数组只有10个元素。这个下标表达式的结果是一个指针 表达式,但它所指向的位置越过了数组的右边界。根据标准,这个表达式是非法 的。
作为函数参数的数组名
当一个数组名作为参数传递给一个函数时会发生什么情况呢?你现在已经知道 数组名的值就是一个指向数组第1个元素的指针,所以很容易明白此时传递给函数的 是一份该指针的拷贝。函数如果执行了下标引用,实际上是对这个指针执行间接访 问操作,并且通过这种间接访问,函数可以访问和修改调用程序的数组元素。
如果你想把一个数组名参数传递给函数,正确的函数 形参应该是怎样的?它是应该声明为一个指针还是一个数组? 正如你所看到的那样,调用函数时实际传递的是一个指针,所以函数的形参实 际上是个指针。但为了使程序员新手更容易上手一些,编译器也接受数组形式的函 数形参。因此,下面两个函数原型是相等的:
int strlen( char *string );
int strlen( char string[] );
你可以使用任何一种声明,但哪个“更加准确”呢?答案是指针。因为实参实 际上是个指针,而不是数组
你会发现:函数原型中的一维数组形参无需写明它的元素数目,因 为函数并不为数组参数分配内存空间。形参只是一个指针,它指向的是已经在其他 地方分配好内存的空间。
这个事实解释了为什么数组形参可以与任何长度的数组匹 配——它实际传递的只是指向数组第1个元素的指针。
另一方面,这种实现方法使函 数无法知道数组的长度。如果函数需要知道数组的长度,它必须作为一个显式的参 数传递给函数。
int strlen( char string[] );
int strlen( char string[8] );
函数参数中的数组声明 char string[]
和 char string[8]
是等价的。当数组作为函数参数时,它被视为指向数组首元素的指针。因此,无论您在数组参数中指定的大小是多少,或者是否省略了大小,该参数都会被视为指向 char
类型的指针。数组大小在函数参数中更多的是为了提供给阅读代码的人一个提示,而不是对函数本身的行为有任何影响。
一个误区
char message1[] = "Hello";
char *message2 = "Hello";
这两个初始化看上去很像,但它们具有不同的含义。前者初始化一个字符数组 的元素,而后者则是一个真正的字符串常量。这个指针变量被初始化为指向这个字 符串常量的存储位置,如下图所示:
多维数组
int a;
int b[10];
int c[6][10];
int d[3][6][10];
a是个简单的整数。
接下来的那个声明增加了一个维数,所以b就是一个向量, 它包含10个整型元素。
c只是在b的基础上再增加一维,所以我们可以把c看作是一个包含6个元素的向 量,只不过它的每个元素本身是一个包含10个整型元素的向量。换句话说,c是个一 维数组的一维数组。
d也是如此:它是一个包含3个元素的数组,每个元素都是包含6 个元素的数组,而这6个元素中的每一个又都是包含10个整型元素的数组。简洁地 说,d是一个3排6行10列的整型三维数组。 理解这个视点是非常重要的,因为它正是C实现多维数组的基础。
int matrix[3][10];
这段代码创建了matrix,它可以看作是一个一维数组,包含3个元素,只是每个元素恰 好是包含10个整型元素的数组。一维数组名的值是一个指针常量,它的类型是“指向元素类型的指针”,它指 向数组的第1个元素
matrix这个名字的值是一个指向它第1个元素的指针,所以matrix是一个指向 一个包含10个整型元素的数组的指针。
matrix 它的类型是“指向包含10个整型元素的数组的指针”, 它指向包含10个整型元素的第1个子数组。
matrix + 1 也是一个“指向包含10个整型元素的数组的指针”,但它指向matrix的另一 行
*(matrix + 1) 事实上标识了一个包含10个整型元素的子数组。数组名的值是个常量指针,它 指向数组的第1个元素,在这个表达式中也是如此。它的类型是“指向整型的指 针”
*( matrix + 1 ) + 5 前一个表达式是个指向整型值的指针,所以5这个值根据整型的长度进行调整。 整个表达式的结果是一个指针,它指向的位置比原先那个表达式所指向的位置向后 移动了5个整型元素。
对其执行间接访问操作: *( *( matrix + 1 ) + 5 ) 就是指针指向的那个数组元素:matrix[1][5]
我们可以把子表 达式*(matrix + 1)改写为matrix[1]。把这个下标表达式代入原先的表达式,我 们将得到: *( matrix[1] + 5 ) 这个表达式是完全合法的。matrix[1]选定一个子数组,所以它的类型是一个 指向整型的指针。我们对这个指针加上5,然后执行间接访问操作。
指向数组的指针
下面这些声明合法吗?
int vector[10], *vp = vector;
int matrix[3][10], *mp = matrix;
第1个声明是合法的。它为一个整型数组分配内存,并把vp声明为一个指向整型 的指针,并把它初始化为指向vector数组的第1个元素。vector和vp具有相同的类 型:指向整型的指针。
但是,第2个声明是非法的。它正确地创建了matrix数组, 并把mp声明为一个指向整型的指针。但是,mp的初始化是不正确的,因为matrix并 不是一个指向整型的指针,而是一个指向整型数组的指针。
正确的做法是:
int (*p)[10] = matrix;
它使p指向matrix的第1行。
p是一个指向拥有10个整型元素的数组的指针。当你把p与一个整数相加时,该 整数值首先根据10个整型值的长度进行调整,然后再执行加法。所以我们可以使用 这个指针一行一行地在matrix中移动。
作为函数参数的多维数组
int matrix[3][10];
...
func2( matrix );
这里,参数matrix的类型是指向包含10个整型元素的数组的指针。func2的原 型应该是怎样的呢?你可以使用下面两种形式中的任何一种:
void func2( int (*mat)[10] );
void func2( int mat[][10] );
把func2写成下面这样的原型是不正确的:
void func2( int **mat );
这个例子把mat声明为一个指向整型指针的指针,它和指向整型数组的指针并不 是一回事。
指针数组
指针数组简单解释就是数组元素都是指针变量的数组,他本质还是个数组,而数组指针,它的本质是个指针,指向了一整个数组。
指针变量和其他变量很相似。正如你可以创建整型数组一样, 你也可以声明指针数组。
int *api[10];
它的元素类型是指向整型的指针。
指针数组作为函数形参,应该如何声明呢?
可以这样声明函数:
void myFunction(char *str_array[]) {
// 函数体
}
或者,也可以使用以下等效的声明方式,它明确指出 str_array
是一个指向指针的指针:
void myFunction(char **str_array) {
// 函数体
}
在这两种情况下,str_array
都可以接受一个指针数组作为参数。当您调用这个函数时,您可以传递一个指针数组
例子:
char const *keyword[] = {
"do",
"for",
"if",
"register",
"return",
"switch",
"while"
};
这个声明创建了一个指针数组,每个指针元素都初始化为指向各个不同的字符 串常量,如下所示
总结
在绝大多数表达式中,数组名的值是指向数组第1个元素的指针。这个规则只有 两个例外。sizeof返回整个数组所占用的字节而不是一个指针所占用的字节。单目 操作符&返回一个指向数组的指针,而不是一个指向数组第1个元素的指针的指针。
除了优先级不同以外,下标表达式array[value]和间接访问表达式*(array+ (value))是一样的。因此,下标不仅可以用于数组名,也可以用于指针表达式中。
指针和数组并不相等。数组的属性和指针的属性大相径庭。当我们声明一个数 组时,它同时也分配了一些内存空间,用于容纳数组元素。但是,当我们声明一个 指针时,它只分配了用于容纳指针本身的空间。
当数组名作为函数参数传递时,实际传递给函数的是一个指向数组第1个元素的 指针。函数所接收到的参数实际上是原参数的一份拷贝,所以函数可以对其进行操 纵而不会影响实际的参数。但是,对指针参数执行间接访问操作允许函数修改原先 的数组元素。数组形参既可以声明为数组,也可以声明为指针。这两种声明形式只 有当它们作为函数的形参时才是相等的
多维数组实际上是一维数组的一种特型,就是它的每个元素本身也是一个数 组。多维数组中的元素根据行主序进行存储,也就是最右边的下标率先变化。多维 数组名的值是一个指向它第1个元素的指针,也就是一个指向数组的指针。对该指针 进行运算将根据它所指向数组的长度对操作数进行调整。多维数组的下标引用也是 指针表达式。当一个多维数组名作为参数传递给一个函数时,它所对应的函数形参 的声明中必须显式指明第2维(和接下去所有维)的长度。
练习:
1
int array[4][2];
请写出下面每个表达式的值。假定数组的起始位置为1000,整型值在内存中占 据4个字节的空间。 表达式
array
array + 2
array[3]
array[2] - 1
&array[1][2]
&array[2][0]
数组名表示数组的起始地址。给定数组 int array[4][2]
的起始地址为1000,且每个 int
类型占据4个字节空间,我们可以计算出每个表达式的值。以下是每个表达式的值:
-
array
:这是数组的起始地址,所以值为1000。 -
array + 2
:这表示数组起始地址后移两个二维数组的长度。由于每个二维数组有2个int
,每个int
占4字节,所以后移的总长度是 (2 \times 2 \times 4 = 16) 字节。因此,值为 (1000 + 16 = 1016)。 -
array[3]
:这表示第四个二维数组的起始地址。每个二维数组占 (2 \times 4 = 8) 字节,所以第四个二维数组的起始地址是 (1000 + 3 \times 8 = 1024)。 -
array[2] - 1
:array[2]表示指向数组的指针,对他进行减一操作实际移动了一个数组的大小 答案为: 1012
-
&array[1][2]
:这是数组第二个二维数组的第三个元素的地址。但由于每个二维数组只有2个元素,所以这个表达式实际上是指向下一个二维数组的第一个元素,即array[2][0]
。因此,值为 (1016)。 -
&array[2][0]
:这是第三个二维数组的第一个元素的地址。因此,值为 (1000 + (2 \times 2 \times 4) = 1016)。
2
给定下列声明
int array[4][5][3];
把下列各个指针表达式转换为下标表达式。
表达式
*array
*( array + 2 )
*( array + 1) + 4 *( *( array + 1) + 4 )
*( *( *( array + 3 ) + 1 ) + 2 )
*( *(*array + 1) + 2 ) *( **array + 2 )
**(*array + 1 )
***array
解答:
*array
表达式可以转换为下标表达式来访问三维数组 array[4][5][3]
的元素。array
表示指向第一个二维数组(array[0]
)的指针,而 *array
相当于访问这个二维数组的第一个元素,即 array[0][0]
。
因此,*array
的下标表达式为:array[0][0][0]
。
*(array + 2)
: 这个表达式表示访问 array
指向的元素向后偏移两个二维数组的位置。在下标表达式中,它对应于第三个二维数组的第一个元素,即 array[2][0][0]
。
*(array + 1) + 4
: 这个表达式首先访问 array
指向的元素向后偏移一个二维数组的位置,然后再向后偏移4个 int
类型的大小。在下标表达式中,它对应于第二个二维数组的第五个元素,即 array[1][4][0]
。
*(*(array + 1) + 4)
: 这个表达式首先访问 array
指向的元素向后偏移一个二维数组的位置,然后取得该位置的首地址,再向后偏移4个 int
类型的大小。在下标表达式中,它对应于第二个二维数组的第五行的第一个元素,即 array[1][4][0]
。
*( *( *( array + 3 ) + 1 ) + 2 )
: 这个表达式表示访问 array
指向的元素向后偏移三个二维数组的位置,然后在该二维数组中向后偏移一行,再在该行中向后偏移两个元素。在下标表达式中,它对应于 array[3][1][2]
。
*( *(*array + 1) + 2 )
: 这个表达式首先访问 array
指向的第一个二维数组,然后在该二维数组中向后偏移一行,再在该行中向后偏移两个元素。在下标表达式中,它对应于 array[0][1][2]
。
*( **array + 2 )
: 这个表达式首先访问 array
指向的第一个二维数组的第一个元素,然后在该元素所在的行中向后偏移两个元素。在下标表达式中,它对应于 array[0][0][2]
。
**(*array + 1 )
: 这个表达式首先访问 array
指向的第一个二维数组,然后在该二维数组中向后偏移一行,最后访问该行的第一个元素。在下标表达式中,它对应于 array[0][1][0]
。
***array
: 这个表达式表示访问 array
指向的第一个二维数组的第一个元素的第一个元素。在下标表达式中,它对应于 array[0][0][0]
。
3
下面的函数原型可以改写为什么形式,但保持结果不变? void function( int array[3][2][5] );
解答:
当数组作为函数参数时,除了第一维之外,其他维度的大小是必须指定的。因此,您提供的函数原型可以改写为以下形式,同时保持结果不变:
void function(int array[][2][5]);
在这种情况下,第一维的大小可以省略,因为它在函数内部不会被使用来确定数组的大小。编译器只需要知道后两维的大小来正确地计算元素的位置。
4
解释void function( int const a, int const b[] )
两种const关键字用法的显著区别所在。
解答
const
关键字用于声明一个变量为常量,这意味着一旦初始化后,它的值就不能被修改。
void function(int const a, int const b[])
const
关键字的两种用法有以下显著区别:
-
int const a
:这里const
修饰a
,表示a
是一个常量整数。在函数体内,不能修改a
的值,因为它是只读的。 -
int const b[]
:在这种情况下,const
修饰b
数组的内容,意味着不能修改数组b
中的元素。b
是一个指向常量整数的指针,可以改变指针b
的指向,但不能改变指针指向的数组元素的值。
总结来说,第一种用法保护了变量 a
的值不被改变,而第二种用法保护了数组 b
中的元素值不被改变。