C语言提高篇(四)

本文介绍数组与指针的特关系,以及数组指针,指针数组的区别,多维数组的知识。

一维数组

数组名

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个字节空间,我们可以计算出每个表达式的值。以下是每个表达式的值:

  1. array:这是数组的起始地址,所以值为1000。

  2. array + 2:这表示数组起始地址后移两个二维数组的长度。由于每个二维数组有2个 int,每个 int 占4字节,所以后移的总长度是 (2 \times 2 \times 4 = 16) 字节。因此,值为 (1000 + 16 = 1016)。

  3. array[3]:这表示第四个二维数组的起始地址。每个二维数组占 (2 \times 4 = 8) 字节,所以第四个二维数组的起始地址是 (1000 + 3 \times 8 = 1024)。

  4. array[2] - 1array[2]表示指向数组的指针,对他进行减一操作实际移动了一个数组的大小 答案为: 1012

  5. &array[1][2]:这是数组第二个二维数组的第三个元素的地址。但由于每个二维数组只有2个元素,所以这个表达式实际上是指向下一个二维数组的第一个元素,即 array[2][0]。因此,值为 (1016)。

  6. &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 关键字的两种用法有以下显著区别:

  1. int const a:这里 const 修饰 a表示 a 是一个常量整数。在函数体内,不能修改 a 的值,因为它是只读的。

  2. int const b[]:在这种情况下,const 修饰 b 数组的内容,意味着不能修改数组 b 中的元素。b 是一个指向常量整数的指针,可以改变指针 b 的指向,但不能改变指针指向的数组元素的值。

总结来说,第一种用法保护了变量 a 的值不被改变,而第二种用法保护了数组 b 中的元素值不被改变。

  • 20
    点赞
  • 22
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值