首页 > 学院 > 开发设计 > 正文

C语言指针导学(3)——指针与数组的“爱恨情仇”

2019-11-06 06:03:51
字体:
来源:转载
供稿:网友
 

C语言指针导学(3)——指针与数组的“爱恨情仇”

    三.指针与数组的“爱恨情仇”

本将中指针的算术运算本应放在第二讲中,但考虑到它与数组关系密切故将其纳入本讲。

1.指针的算术运算

在上一讲指针初始化的第4种方式中提到了可以将一个T类型数组的名字赋给一个相同类型的指针,这说明指针可以和数组发生联系,在后面我们会看到这种联系是十分密切的。当有语句char ary[100] = {'a', 'b', 'c', 'd','e', 'f'}; char *cp = ary; 后,cp就指向了数组array中的第一个元素。我们可以通过指针来访问数组的元素:PRintf("%d", *cp); 此语句的作用是打印出cp所指向的元素的值,也就是数组的第一个元素。现在通过cp = &array[3];使cp指向数组中的第4个元素,然后我们就可以对它进行各种操作了。

实际中经常会用指针来访问数组元素,当两个指针指向同一个数组时,会用到指针的算术运算:

<1>.指针+整数 或 指针-整数

指针与一个整数相加的结果是一个另一个指针。例如将上面的cp加1,运算后产生的指针将指向数组中的下一个字符。事实上当指针和一个整数相加减时,所做的就是指针加上或减去步长乘以那个整数的积。所谓步长就是指针所指向的类型的大小(即指针移动一个位置时要跳过几个字节)。下面的例子会让大家更加明了:

intia[100] = {0, 1, 2, 3, 4, 5};           double da[100] = {0.0, 1.0, 2.0, 3.0, 4.0, 5.0};

int *ip =ia;                                     double *dp = da;

ip += 3;                                           dp += 3;

ip加上3实际进行的操作是ip + 4 * 3,因为ip指向的元素的类型为int;而dp加3实际进行的操作是dp + 8 * 3,因为dp指向的元素的类型为double;这正是指针需要定义基类型的原因,因为编译器要知道一个指针移动时的步长。

要注意的是指针的算术运算只有在原始指针和计算出来的新指针都指向同一个数组的元素或指向数组范围的下一位置时才是合法的;另外,这种形式也适用于使用malloc动态分配获得的内存。

下面这段代码在大多数编译器上都是可以运行的,但它却是不安全的,因为b元素后面的内存区域所存储的内容是不确定的,有可能是受系统保护的,如果又编写了对p解引用的语句,那么很可能会造成运行时错误:

   int b;

          int *p = &b;

   p += 2;

          printf("%p/n", p);

<2>。指针间的减法

当两个指针指向同一数组或有一个指针指向该数组末端的下一位置时,两个指针还可以做减法运算。

          intia[100] = {0, 1, 2, 3, 4, 5};

          int *ip =ia;

          int *ig =ia +3;

          ptrdiff_t n = ig – ip;

n应该为3,表示这两个指针所指向的元素的距离为3,ptrdiff_t是一个标准库类型,它是一个无符号整数,可以为负数。注意指针进行减法得到的结果指示出两指针所指向元素间的距离,即它们之间相隔几个数组元素,与步长的概念无关。

另外,一个指针可以加减0,指针保持不变;如果一个指针具有0值(空指针),则在该指针上加0也是合法的,结果得到另一个值为0的指针;对两个空指针做减法运算,得到的结果也是0。注意:ANSI C标准没有定义两个指针相加的运算,如果两个指针相加,绝大多数编译器会在编译期报错。

2.指针与数组的爱恨情仇

数组和指针有着千丝万缕的联系,它们之间的问题困惑着不少朋友,有的朋友对它们的概念不是很清楚,所以可能会导致误用,从而出错。下面就对数组名是什么,数组什么时候和指针相同等相关问题做出解释。

<1>.数组名

声明中:当我们声明一个数组时,编译器将根据声明所指定的元素数量及类型为数组保留内存空间,然后再创建数组名,编译器会产生一个符号表,用来记录数组名和它的相关信息,这些信息中包含一个与数组名相关联的值,这个值是刚刚分配的数组的第一个元素的首地址(一个元素可能会占据几个地址,如整型占4个,此处是取起始地址)。现在声明一个数组:int ia[100]; 编译器此时为它分配空间,假设第一个数组元素的地址为0x22ff00;那么编译器会进行类似#define ia 0x22ff00的操作,这里只是模拟,真实情况并非完全一样,我们在编程时无需关注编译器所做的事情,但要知道此时(声明时)数组名只是一个符号,它与数组第一个元素的首地址相关联。注意:数组的属性和指针的属性不相同,在声明数组时,同时分配了用于容纳数组元素的空间;而声明一个指针时,只分配了用于容纳指针本身的空间。

表达式中:当我们在表达式中使用数组名,如:ia[10] = 25;时,这个名字会被编译器转换为指向数组第一个元素的常量指针(指针本身的值不可变),它的值还是数组的第一个元素的首地址(一个指针常量),编译器的动作类似于int *const  ia = (void *)0x22ff00; 这里我们应重点关注的是:数组名是一个常量指针(常指针),即指针自身的值不能被改变。如果有类似ia++或ia+=3这类的语句是绝对不对的,会产生编译错误。注意:当数组名作为sizeof操作符的操作数时,返回的是整个数组的长度,也就是数组元素的个数乘以数组元素类型的大小;另外,在对数组名实施&操作时,返回的是一个指向数组的指针,而非具有某个指针常量值的指针(这个问题在后面会详细论述)。

通过数组名引用数组元素时:在前面讲过的指针算术运算中指针加上一个整型数,结果仍然是指针,并且可以对这个指针直接解引用,不用先把它赋给一个新指针。如int last = *(ia + 99);此时的ia已经是一个常指针了,这个表达式计算出ia所指向元素后面的第99个元素的地址,然后对它解引用得到相应的值。这个表达式等价于int last = ia[99];事实上每当我们采用[ ]的方式引用数组元素时,如:ia[99],在编译器中都会转换成指针形式,也就是*(ia + 99) (这里ia + 99和&ia[99]的值都为数组最后一个元素的首地址,所以*(ia + 99)和*&ia[99]得到的结果是一样的,较难理解的是*&ia[99],按照优先级和结合性规则,先对ia[99]取地址再解引用,有些编译器见到这种表达式会直接优化成ia[99]。)现在可以看出来在表达式中,指针和数组名的使用可以互换,但唯一要注意的就是:数组名是常指针,不能对它的值进行修改。ia + 99是可以的,但ia++是不行的,它的意思是ia = ia +1;修改了ia的值。

作为函数参数:先来了解一下函数的实参与形参。实参(argument)是在实际调用时传递给函数的值;形参(parameter)是一个变量,在函数定义或者原型中声明。C语言标准规定作为形参的数组声明转换为指针。在声明函数形参的特定情况下,编译器会把数组形式改写成指向数组第一个元素的指针。所以不管下面哪种声明方式,都会被转换成指针:

       void array_to_pointer(int *ia){……} //无需转换

       void array_to_pointer(int ia[ ]){……} //被转换成*ia

       void array_to_pointer(int ia[100 ]){……} //被转换成*ia

那么如果有下面的操作

       void array_test(int ia[100])

{

        doubleda[10];

  printf("%d", sizeof( ia ));

  ia++;

  //da++;   //编译错误,数组名是常指针

       }

输出的结果为4,此时的ia是作为函数形参而声明的数组,已经被转换为了一个不折不扣的指针(不再是常指针了),因此ia++;是合法的,不会引发编译错误。为什么C语言要把数组形参当作指针呢?因为C语言中所有非数组形式的数据实参(包括指针)均以值传递形式调用(所谓值传递就是拷贝出一个实参的副本并把这个副本赋值给形参,从此实参与形参是各不相干的,形参值的变化不会影响实参)。如果要拷贝整个数组,在时间和空间上的开销都很大,所以把作为形参的数组和指针等同起来是出于效率原因的考虑。我们可以把形参声明为数组(我们打算传递给函数的东西)或者指针(函数实际接收到的东西),但在函数内部,编译器始终把它当作一个指向数组第一个元素(数组长度未知)的指针。在函数内部,对数组参数的任何引用都将产生一个对指针的引用。我们没有办法传递一个数组本身,因为它总是被自动转换为指向数组首元素的指针,而在函数内部使用指针时,能对数组进行的操作几乎和传递数组没有区别,唯一不同的是:使用sizeof(形参数组名)来获得数组的长度时,得到的只是一个指针的大小,正如上面所述的ia。但要注意:以上讨论的都是数组名作为函数形参的特殊情况,当我们在函数体内声明一个数组时,它就是一个普通的数组,它的数组名仍是一个常指针,所以上面的da++;仍会引起编译错误,请大家不要混淆。

    还有一点,既然是值传递,那么理所当然地,在用数组名作为实参调用函数时,实参数组名同样会被转换为指向数组第一个元素的指针。

<2>.指向数组的指针

好了,关于数组名的讨论可以告一段落了,现在来看指针与数组的另一种联系。在前面说过,当对一个一维数组的数组名进行 &操作时,返回的是一个指向数组的指针。现在我们就来看看什么是指向数组的指针。在C语言中,所谓的多维数组实际上只是数组的数组,也就是说一个数组中的每个元素还是数组,由于二维数组较为常用,所以本文着重讨论二维数组,更多维数组的原理与二维数组相同。所谓二维数组(数组的数组),就是每个元素都是一个一维数组的一维数组。另外,请大家先有一个感性的认识:指向数组的指针主要用来对二维数组进行操作,大家不理解没有关系,我会在后面详细说明。

通常我们声明一个指向一维数组中的元素的指针是这样做的:int ia[100], *ip = ia; ip指向这个数组的第一个元素,通过指针的算术运算,可以让ip指向数组中的任一元素。对于二维数组,我们的目的同样是让一个指针指向它的每一个元素,只不过这次的元素类型是一个数组,所以在声明这个指针时稍有不同,假设有二维数组int matrix[50][100], C语言采用如下的方式来声明一个指向数组的指针。int (*p) [100];比普通声明稍复杂一些,但并不难理解。由于括号的优先级是最高的,所以首先执行解引用,表明了p是一个指针,接下来是数组下标的引用,说明p指向的是某种类型的数组,前面的int表明p指向的这个数组的每个元素都是整数。对于这个声明还可以换一个角度来理解:现在要声明的是一个指针,因此在标识符p前面加上*。如果从内向外读p的声明,可以理解为*p是int[100]类型,即p是一个指向含有100个元素的数组的指针。

有些朋友可能对于一个用来操纵二维数组的指针只使用一个下标表示困惑,为什么声明不是int (*p) [50][100]呢?现在来回顾一下操纵一维数组的指针声明int *ip = ia;它表示ip指向了一个数组的第一个元素,通过对指针的算术运算可以使它指向数组中的任何一个元素,编译器不需要知道指针ip指向的是一个多长的数组。对于二维数组道理相同,int (*p) [100] = matrix; matrix可以看成是一个长度为50的一维数组,每个元素都是一个int[100]型的数组,p同样指向了matrix数组的第一个元素(第一个int[100]型的数组),通过对p的算术运算也可以使它指向matrix数组中的任意一个元素而不需要知道matrix是一个多长的数组,但一定需要知道matrix中每个数组元素的长度,所以就有了int (*p) [100]这种形式的声明。由此可知,如果进行p + n (n为整数)这样的运算,每次的步长就是n * 100 * sizof (int),相当于跳过了矩阵中的n行,因为每行都有100个元素并且元素为整型,所以跳过了n * 100 * sizof (int)个字节,指向这些字节之后的位置。现在,对指向数组指针的声明方式的疑惑我认为已经讲清楚了。下面来看一个关于数组长度的问题。

在C语言中没有一种内建的机制去检查一个数组的边界范围,完全是由程序员自己去控制,这是C语言设计的一种哲学或者说一种理念:给程序员最大的自由度,程序员应该知道自己在做什么。凡事有利有弊,自由度大了,出错的几率就高了。很有朋友(包括我自己)在初用数组时应该会或多或少地遇到过数组越界的问题。在前面的论述中提到了通过对指针的算术运算可以使它指向数组中的任何一个元素包括超出数组范围的第一个元素,这个超出范围的第一个元素实际上是不存在的,这个“元素”的地址在数组所占的内存之后,它是数组的第一个出界点,这个地址可以赋给指向数组元素的指针,但ANSI C仅允许它进行赋值或比较运算,不能对保存这个地址的指针进行解引用或下标运算。

<3>.再回首——数组名

现在又要开始数组名的讨论了,之所以再回首而没有一气呵成,是因为在一维数组名和二维数组名之间需要一个过渡知识,就是指向数组的指针。在表达式中一维数组名会转换为指向数组第一个元素的指针,二维数组也是一样的,请大家牢记在C语言中二维数组就是数组的数组,所以也会被转换为指向第一个元素的指针,它的第一个元素是一个数组,所以最终的结果就是二维数组名被转换成指向数组的指针。

来看int(*p) [100] = matrix;此时的matrix被转换为一个指向数组的指针,对于matrix[n],是matrix数组的第n+1个元素的名字,也就是matrix数组中50个有着100个整型元素的数组之一,所以可以有p = &matrix[n]; 即p指向了一个数组元素,也就是矩阵中的某一行,matrix[n]本身是一个一维数组的数组名,它会被转换为指向数组第一个元素的指针,因此可以有int *column_p = matrix[n];这个表达式是最常见的也最容易理解。如果对matrix[n]进行sizeof操作结果是100*sizeof(int);而sizeof(matrix)结果是50*100*sizeof (int)。

总结一下,p和matrix都是指向矩阵的一行(一个整型数组),p+ m 或者matrix + m都将使指针跳跃m行(m个整型数组),column_p和matrix[n]都指向某行(一个整型数组)的第一个元素,column_p + m和matrix[n] + m都将使指针跳跃m个整型元素。假若要访问二维数组matrix中第1行第1列(注意数组下标从0开始)的元素可以有以下的几种方式(i为int型变量):

通过数组名引用         通过指针p的引用         通过指针column_p的引用

i = matrix [0][0];          i = *(*(p+0)+0);           column_p = matrix[0];

i = *(matrix [0]+0);        i = *(p[0] + 0);             i = *(column_p+0);

i = *(*(matrix+0)+0);      i = (*(p + 0))[0];            i = column_p[0];

上面的各种表达式中的“+0”均可以省略掉,但如果数字不是0就不能省略了,由此在引用第1行第1列的元素时会产生一些简化的表达式,如下:

通过数组名引用         通过指针p的引用         通过指针column_p的引用

i = matrix [0][0];          i = **p;                  column_p= matrix[0];

i = *matrix [0];           i =*p[0];                 i= *column_p;

i = **matrix;             i = (*p )[0];               i = column_p[0];

现在来看下面语句的输出,它们可能会让你感到困惑:

printf("%p/n", &matrix);     对应的指针操作:无

printf("%p/n", matrix);       对应的指针操作:printf("%p/n", p);

printf("%p/n",&matrix[0]);   对应的指针操作:printf("%p/n", p);

printf("%p/n",matrix[0]);     对应的指针操作:printf("%p/n", column_p);

printf("%p/n",&matrix[0][0]); 对应的指针操作:printf("%p/n", column_p);

在我机器上的输出是:

0022B140

0022B140

0022B140

0022B140

0022B140

输出的值虽然一样,但这些参数的类型却不完全相同。下面一一做出解释:

&matrix: 对二维数组名取地址,返回一个指向二维数组的指针;

matrix:  二维数组名会被转换为指向第一行(第一个数组)的指针,与&matrix[0]等价;

&matrix[0]:对第一个一维数组的数组名取地址,返回一个指向一维数组的指针;

matrix[0]:  二维数组中第一个一维数组的数组名,与&matrix[0][0]是等价的;

&matrix[0][0]:对第一行第一列元素取地址,返回一个指向整型元素的指针。

在ANSIC标准中没有说明对一个数组名进行&操作是否合法,但现在的编译器大都认为是合法的,并且会返回一个指向数组的指针。简单地说就是:对一个n维数组的数组名取地址得到的是一个指向n维数组的指针。

另外,上例中相对应的指针表示方式我也写了出来。对于&matrix没有相对应的指针表示方式,因为我们没有定义那种类型的指针,用p是表示不出来的,如果对p进行&的话,得到的是p这个指针的地址,而不是matrix的地址,两者完全不同,值也不会相同的。

再次提醒大家:无论是matrix还是matrix[0],它们都是数组名,都会被转化为一个常指针,不能修改它们自身的值。对于&matrix、&matrix[0]、&matrix[0][0],它们得到的都是常量(指针常量),表示的是物理内存的地址,同样不能修改它们的值。本质上讲就是你不能也不可能修改一个物理内存的地址。

<4>.指针数组

在声明一个指向数组的指针时千万不要丢到那个括号,如:int (*p) [100];如果丢掉了括号那就完全改变了意图,从而意外地声明了一个指针数组。指针数组要比指向数组的指针好理解,而且前面已经有了一些铺垫,这个概念相信大家可以很轻松地搞定。

所谓指针数组就是一个数组它的所有元素都是指针,这与普通的数组没什么区别,不过元素是指针罢了。下面来声明一个指针数组char *cars[10];这种方式可能不太利于理解,如果写成char* cars[10];的形式,可读性就很强了,它明确表示了cars是一个具有10个元素的数组,每个元素的类型都是char*。下面举一个完整的例子并用它来结束这段指针与数组的爱恨情仇。

#include<stdio.h>

 

voiddisplay_car_brands(const char *brand_table[], int size)  // 有关参数和局部变量中的const解析请参见第6章Section 3.

{

       const char **cbp;

       for(cbp = brand_table;  cbp < brand_table+ size;  cbp++) {

              printf("%s/n", *cbp);

              printf("%c/n",**cbp);

       }

}

 

int main( )

{

       const char *cars[] = {     "ASTONMARTIN",

                                  "AUDI",

                                  "BENZ",

                                  "BENTLEY",

                                  "BMW",

                                  "BUGATTI",

                                  "FERRARI",

                                  "JAGUAR",

                                 "LAMBORGHINI",

                                 "MASERATI",

                                  "MAYBACH",

                                  "ROLLSROYCE"

                            };

       int array_size = sizeof(cars)/sizeof(cars[0]);

       display_car_brands(cars,array_size);

       return 0;

}

首先我们定义了一个指针数组,每个元素都是一个指向char类型的指针,并将它初始化。初始化后的数组有12个指针元素,分别指向以上的各个字符串(即保存着每个字符串首字符的地址)。在display_car_brands()中定义了一个二级指针cbp,指向指针数组的第一个元素,通过自增cbp,遍历每一个数组元素,正如上图所示(这种数组就是所谓的锯齿型数组,也叫交错数组,即jagged array)。

对cbp解引用得到每个数组元素的值(即每一个字符串首字符的地址),然后通过printf("%s/n", *cbp);语句来输出每个字符串,注意*cbp得到的是字符串首字符的地址,通过%s格式项接收一个地址以输出整个字符串。接下来的printf("%c/n", **cbp);输出每个串的第一个字母,**cbp首先得到每个字符串首字符的地址,再对该地址解引用得到相应的字符。因此程序的输出为:

ASTON MARTIN

A

AUDI

A

BENZ

B

BENTLEY

B

BMW

B

BUGATTI

B

FERRARI

F

JAGUAR

J

LAMBORGHINI

L

MASERATI

M

MAYBACH

M

ROLLS ROYCE

R


发表评论 共有条评论
用户名: 密码:
验证码: 匿名发表