C专家编程10

  • 格式:doc
  • 大小:766.50 KB
  • 文档页数:23

再论指针立早千万不要忘了,当你把一个手指指向别人的时候,你手上的另外还有3个手指指向了你自己……10.I多维数组的内招布局多维数组在系统编程中并不常用。

所以,毫不奇怪的是,C语言并未像其他语言所要求的那样定义了详细的运行时程序来支持这个特性。

对于某些结构如动态数组,程序员必须使用指针显式地分配和操纵内存,而不是由编译器自动完成。

另外还有一些结构(作为参数的多维数组),在C语言中并没有一般的形式来表达。

本章将讲述这些主题。

现在,每个人都已经熟悉了多维数组在内存中的布局。

如果我们具有以下声明:char pea[4】[6】;有些人把二维数组看作是排列在一张表格中的一行行的一维数组,如图l0.1所示。

pea[1][2】图10.1假想中的二维数组内存布局9 一事实上系统绝不允许程序按照这种方式存储数据。

单个元素的存储和引用实际上是以线性形式排列在内存中的,如图10.2所示。

图10-2实际上的二维数组内存布局数组下标的规则告诉我们如何计算左值pea[i][j],首先找到pea[i]的位置,然后根据偏移量D]取得字符。

因此,pea[i][j]将被编译器解析为+(+(pea+i)+j)但是(这正是关键所在!),“pea[i]”的意思将随pea定义的不同而变化。

我很快将解释这个表达式,但首先让我们看一下C语言中最常见最重要的数据结构:指向字符串一维指针数细。

10.2指针数维就是Iliffe向量可以通过声明一个一维指针数组,其中每个指针指向一个字符串l来取得类似二维字符数组的效果。

这种形式的声明如下:软件信条注意声明的语法注意char木mmip[23]把“turnip”声明为一个具有23个元素的数组,每个元素的类型是一个指向字符的指针(或者一个字符串——单纯从声明中无法区分两者)。

可以假想它两边加上了括号——(char幸)tunip[23]。

这跟从左至右读时看上去的样子(一个指向“具有23个字1 这里我们略微进行了简化——指针实际上是声明为指向单个字符的。

但是如果定义为指向字符的指针,就存在一种可能性,就是其他字符可能紧邻着它存储,隐式地形成了一个字符串。

像下面这样的声明char(+rhubarb[4】)[7];才是真正声明了一个指向字符串的指针数组。

在实际代码中从未曾使用过这种形式,因为它不必要地限制了所指向的数组的长度(只能恰好为7)。

220第l0章再论指针符类型元素的数组”的指针)不一样。

这是因为下标方括号的优先级比指针的星号高。

关于声明语法的分析,第3章已经作了详细介绍。

用于实现多维数组的指针数组有多种名字,如“Iliffe向量”、“display”或“dope 向量”。

display在英国也用来表示一个指针向量,用于激活一个在词法上封闭的过程的活动记录(作为“一个静态结点后面跟一个链表”的替代方案)。

这种形式的指针数组是一种强大的编程技巧,在C语言之外取得了广泛的应用。

图20.3显示了这样的结构。

pea[1][2图10.3指向字符串的指针数组这种数组必须用指向为字符串而分配的内存的指针进行初始化,可以在编译时用一个常量初始值,也可以在运行时用下面这样的代码进行初始化:for(j=0;J<=4;J++) ’pea[J]=malloc(6);另一种方法是一次性地用malloc分配整个X×y个数据的数组,malloc(row_size‘colu mn—size+sizeof(char));然后,使用一个循环,用指针指向这块内存的各个区域。

整个数组保证能够存储在连续的内存中,即按C用于分配静态数组的次序。

它减少了调用malloc的维护性开销,但缺点是当处理完一个字符串时无法单独将其释放。

软件信条当你看见squash[i][j]这样的形式时,你不知道它是怎样被声明的!两个下标的二维数组和一维指针数组所存在的一个问题是:当你看到squash[i][j]这.样的引用形式时,你并不知道squash是声明为:221曩曩睇蠢Bl匿EF群EEt垦E酞昆E匿瞽|E瞄彭量E匿EI或是或是int squash[23】[12n /·int类型的二维数组·//+2 3个int类型指针的Iliffe向量+//*int类型的指针的指针+/或甚至是int(*squash)[12】;/·类型为int数组(长度为12)的指针·/这有点类似在函数内部无法分辨传递给函数的实参究竟是一个数组还是一个指针。

当然,基于同样的理由:作为左值的数组名被编译器当作是指针。

在上面几种定义中,都可以使用如squash[i]U]这样的形式,尽管在不同的情况中访问的实际类型。

垄至塑曼L——————————————一M㈣Ⅻ№㈣w㈣Ⅻ㈣㈣∞㈨∞㈣㈣ⅢM㈣㈣Ⅷm“m㈣∞Ⅻ㈣∞∞㈨㈣ⅫM㈣_㈣㈣㈣㈣㈣㈣*㈣㈣w㈣㈣_㈣㈣㈣㈨m㈣wMⅢ———m㈣*㈣_———————————一一与数组的数组一样,一个Iliffe向量中的单个字符也是使用两个下标来引用数组中的元素(如pea[i】D])。

指针下标引用的规则告诉我们pea[i][j]被编译器解释为:.+(+(pea+i)+J)是不是觉得很熟悉?应该是这样。

它和一个多维数组引用的分解形式完全一样,在许多C语言书中就是这样解释的。

然而,这里存在一个很大的问题。

尽管这两种下标形式在源代码里看上去是一样,而且被编译器解释为同一种指针表达式,但它们在各自的情况下所引用的实际类型并不相同。

表10.1和表l0一2显示了这种区别。

表1 0—1 一个数组的数组char a【4】【61char a[4]【6]——一个数组的数组 t在编译器符号表中,a的地址为9980运行时步骤1:取i的值,把它的长度调整为一行的宽度(这里是6),然后加到9980上。

运行时步骤2:取j的值,把它的长度调整为一个元素的宽度(这里是1),然后加到前面所得出的结果上。

运行时步骤3:从地址(9980+i'scale.factorl+j+scale-factor2)中取出内容。

a[il[j]a【0】 a【1】 a【2】 a[3】char a【4】[6]的定义表示a是一个包含4个元素的数组,每个元素是一个char类型的数组(长度为6)。

所以查找到第4个数组的第i个元素(前进i+6个字节),然后找到数组中的第j个元素。

——————————————————————————————————————————————————————————————————————————————一第10章再论指针表10.2 字符串指针数组中的char’p[4】char+p【4】——一个字符串指针数组在编译器的符号表中,P的地址为4624运行时步骤1:取i的值,乘以指针的宽度(4个字节),并把结果加到4624上。

运行时步骤2:从地址(4624+4"i)取出内容,为“5081”。

运行时步骤3:取J的值,乘以元素的宽度(这里是1个字节),并把结果加到5081上。

运行时步骤4:从地址(5081+j*1)取出内容。

p[i】啪p【i】D】44624char*p[41的定义表示P是一个包含4个元素的数组,每个元素为一个指向char的指针。

所以除非指针已经指向字符(或字符数组),否则查找过程无法完成。

假定每个指针都给定了一个值,那么查寻过程先找到数组的第i个元素(每个元素均为指针),取出指针的值,加上编移量J,以此为地址,取出地址的内容。

这个过程之所以可行是因为第9章的规则2:一个下标始终相当于指针的偏移量。

因此,turnip[i]选择一个元素,也就是一个指针,然后使用下标D】引用指针,产生+(指针+j),它所指向的是一个单字符。

这仅仅是a【2】和p[2】的一种扩展,它们的结果都是一个字符,正如我们在前一章所见到的那样。

10,3在锯齿状数缎上使屠指针Iliffe向量是一种旧式的编译器编写技巧,最初用于Algol一60。

它们原先用于提高数组访问的速度,当时的机器内存有限,通常在内存中只存储数组的部分数据,这个技巧也有助于简化管理任务。

在现代的系统中,这两个用途都已毫无必要,但Iliffe向量在另外两个方面仍然具有价值:存储各行长度不一的表以及在一个函数调用中传递~个字符串数组。

如果需要存储50个字符串,每个字符串的最大长度可以达到255个字符,可以声明下面的二维数组:char carrot[50】[256】jC专家编程——————————————————————————————————————————————————它声明了50个字符串,其中每一个都保留256字节的空间,即使有些字符串的实际长度只到一两个字节。

如果经常这样做,内存的浪费很大。

一种替代方法就是使用字符串指针数组,注意它的所有第二级数组并不需要长度都相同,如图10.4所示。

turnip[0】【1】【2】[3】[4】【5】图10.4锯齿状字符串数组如果声明一个字符串指针数组,并根据需要为这些字符串分配内存,将会大大节省系统资源。

有些人把它称作“锯齿状数组’’是因为它右端的长度不一。

可以通过用字符串指针填充Iliffe向量来创建一个这种类型的数组,字符串指针可以直接使用现有的,也可以通过分配内存创建一份现有字符串的新鲜拷贝。

图l0—5显示了这两种方法。

图10-5创建一个锯齿状数组只要有可能,尽量不要选择拷贝整个字符串的方法。

如果需要从两个不同的数据结构访问它,拷贝一个指针比拷贝整个数组快得多,空间也节省很多。

另一个可能影响性能的因素是Iliffe向量可能会使字符串分配于内存中不同的页面中。

这就违反了局部引用的规则(一次读写的数据位于同一页面上),并导致更加频繁的页面交换,具体如何取决于怎样访问数据以及访问的频度。

224第10章再论指针数组和指针参数是如何被编译器修改的“数组名被改写成一个指针参数”规则并不是递归定义的。

数组的数组会被改写为“数组的指针”,而不是“指针的指针”。

实数组的数组指针数组数组指针(行指针)指针的指针所匹配的形式参数char(*)[10];数组指针char★★c;指针的指针char(*c)[64]j 不改变char★★c;不改变你之所以能在main()函数中看到char木*argv这样的参数,是因为argv是个指针数组(即char木argV口)。

这个表达式被编译器改写为指向数组第一个元素的指针,也就是一个指向指针的指针。

如果argv参数事实上被声明为一个数组的数组(也就是char argv[10][15]),它将被编译器改写为char(木argv)[15](也就是一个字符数组指针),而不是char术术argv。

只适用于高级学生的材料让我们花点时间,回顾一下图9—8“多维数组的存储”。

看看图左边标为“兼容类型”的变量是如何与对应的被声明为函数参数的数组(如上表所示)正确匹配的。