C专家编程10Word格式.docx
- 文档编号:21772206
- 上传时间:2023-02-01
- 格式:DOCX
- 页数:31
- 大小:126.27KB
C专家编程10Word格式.docx
《C专家编程10Word格式.docx》由会员分享,可在线阅读,更多相关《C专家编程10Word格式.docx(31页珍藏版)》请在冰豆网上搜索。
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‘column—size+sizeof(char));
然后,使用一个循环,用指针指向这块内存的各个区域。
整个数组保证能够存储在连续的内
存中,即按C用于分配静态数组的次序。
它减少了调用malloc的维护性开销,但缺点是当处
理完一个字符串时无法单独将其释放。
当你看见squash[i][j]这样的形式时,你不知道它是怎样被声明的!
两个下标的二维数组和一维指针数组所存在的一个问题是:
当你看到squash[i][j]这.样的
引用形式时,你并不知道squash是声明为:
221
曩曩睇蠢Bl匿EF群EEt垦E酞昆E匿瞽|E瞄彭量E匿EI
或是
intsquash[23】[12n/·
int类型的二维数组·
/
/+23个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显示了这种区别。
表10—1一个数组的数组chara【4】【61
chara[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】
chara【4】[6]的定义表示a是一个包含4个元素的数组,每个元素是一个char类型的数组(长度为6)。
所以
查找到第4个数组的第i个元素(前进i+6个字节),然后找到数组中的第j个元素。
——————————————————————————————————————————————————————————————————————————————一
第10章再论指针
表10.2字符串指针数组中的char’p[4】
char+p【4】——一个字符串指针数组
在编译器的符号表中,P的地址为4624
取i的值,乘以指针的宽度(4个字节),并把结果加到4624上。
从地址(4624+4"
i)取出内容,为“5081”。
取J的值,乘以元素的宽度(这里是1个字节),并把结果加到5081上。
运行时步骤4:
从地址(5081+j*1)取出内容。
p[i】啪
p【i】D】
44624
char*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个字符,可以声明下面的
二维数组:
charcarrot[50】[256】j
C专家编程
——————————————————————————————————————————————————
它声明了50个字符串,其中每一个都保留256字节的空间,即使有些字符串的实际长度
只到一两个字节。
如果经常这样做,内存的浪费很大。
一种替代方法就是使用字符串指针数
组,注意它的所有第二级数组并不需要长度都相同,如图10.4所示。
turnip[0】
【1】
【2】
[3】
[4】
【5】
图10.4锯齿状字符串数组
如果声明一个字符串指针数组,并根据需要为这些字符串分配内存,将会大大节省系统
资源。
有些人把它称作“锯齿状数组’’是因为它右端的长度不一。
可以通过用字符串指针填
充Iliffe向量来创建一个这种类型的数组,字符串指针可以直接使用现有的,也可以通过分配
内存创建一份现有字符串的新鲜拷贝。
图l0—5显示了这两种方法。
图10-5创建一个锯齿状数组
只要有可能,尽量不要选择拷贝整个字符串的方法。
如果需要从两个不同的数据结构访
问它,拷贝一个指针比拷贝整个数组快得多,空间也节省很多。
另一个可能影响性能的因素
是Iliffe向量可能会使字符串分配于内存中不同的页面中。
这就违反了局部引用的规则(一次
读写的数据位于同一页面上),并导致更加频繁的页面交换,具体如何取决于怎样访问数据以
及访问的频度。
224
数组和指针参数是如何被编译器修改的
“数组名被改写成一个指针参数”规则并不是递归定义的。
数组的数组会被改写为“数
组的指针”,而不是“指针的指针”。
实
数组的数组
指针数组
数组指针(行指针)
指针的指针
所匹配的形式参数
char(*)[10];
数组指针
char★★c;
指针的指针
char(*c)[64]j不改变
不改变
你之所以能在main()函数中看到char木*argv这样的参数,是因为argv是个指针数组(即
char木argV口)。
这个表达式被编译器改写为指向数组第一个元素的指针,也就是一个指向指
针的指针。
如果argv参数事实上被声明为一个数组的数组(也就是charargv[10][15]),它
将被编译器改写为char(木argv)[15](也就是一个字符数组指针),而不是char术术argv。
只适用于高级学生的材料
让我们花点时间,回顾一下图9—8“多维数组的存储”。
看看图左边标为“兼容类型”的
变量是如何与对应的被声明为函数参数的数组(如上表所示)正确匹配的。
这并不令人吃惊。
图9—8显示了表达式中的数组名是如何变成指针的;
上面的表格显示
了作为函数参数的数组名是如何变成指针的。
这两种情况都受一个相似规则的支配,就是在
特定的上下文环境中,数组名被改写为指针。
图10.6显示了所有有效代码的组合。
我们可以发现:
·
3个函数都接受同样类型的参数,就是一个【2】[3][5]int型三维数组或是一个指向[3]【5]
int型二维数组的指针。
3个变量:
apricot,P,木q都匹配所有3个函数的参数声明。
錾凰ll
编程挑战
检验一下
键入图10.6中的C代码,亲手运行一下。
0;
4
图10.6所有有效代码的组合
10。
4向函数传递—个—维数缎
在C语言中,任何一维数组均可以作为函数的实参。
形参被改写为指向数组第一个元素
的指针,所以需要一个约定来提示数组的长度。
一般有两个基本方法:
‘增加一个额外的参数,表示元素的数目(ar90就是起这个作用)。
‘赋予数组最后一个元素一个特殊的值,提示它是数组的尾部(字符串结尾的。
\0,字
符就是起这个作用)。
这个特殊值必须不会作为正常的元素值在数组中出现。
二维数组的情况要复杂一些,数组被改写为指向数组第一行的指针。
现在需要两个约定,
其中一个用于提示每行的结束,另一个用于提示所有行的结束。
提示单行结束可以使用一维
数组所用的两种方法,提示所有行结束也可以这样。
我们所接收的是一个指向数组第一个元
素的指针。
每次当对指针执行自增操作时,指针就指向数组中下一行的起始位置,但怎么知道
指针到达了数组的最后一行呢?
我们可以增加一个额外的行,行内所有元素的值都是不可能在
数组正常元素中出现的,能够提示数组超出了范围。
当对指针进行自增操作时,要对它进行检
查,看看它是否到达了那一行。
另一种方法是,定义~个额外的参数,提示数组的行数。
S使用指针自函数传递—个多维数缎
使用上一节所描述的笨拙方法,可以解决标记数组范围这个难题。
但是还存在一个问题,
就是如何在函数内部声明一个二维数组参数,这才是真正的麻烦所在。
C语言没有办法表达
“这个数组的边界在不同的调用中可以变化”这个概念。
C编译器必须要知道数组的边界,以
便为下标引用产生正确的代码。
从技术上说,也可以在运行时处理才知道数组的边界,而且
很多其他语言就是这样做的,但这种做法违背了C语言的设计理念。
我们能够采取的最好方法就是放弃传递二维数组,把a玎ay【x]【y】这样的形式改写为一个
一维数组mTay[x+1],它的元素类型是指向array【y】的指针。
这样就改变了问题的性质,而改
变后的问题是我们已经解决了的。
在数组最后的那个元素mTay[x+1]里存储一个NULL指针,
提示数组的结束。
在C语言中,没有办法向函数传递一个普通的多维数组
这是因为我们需要知道每一维的长度,以便为地址运算提供正确的单位长度。
在c语言
中,我们没有办法在实参和形参之间交流这种数据(它在每次调用时会改变)。
因此,你必
须提供除了最左边一维以外的所有维的长度。
这样就把实参限制为除最左边一维外所有维都
必须与形参匹配的数组。
●
invert—in_place(inta[][3][5】);
用下面两种方法调用都可以:
intb[10][3][5ninvert.一in—place(b);
intb[999][3][5¨
invert—in—place(b);
但像下面这样任意的三维数组:
intfailsl[10][5][5]jinvertinplace(failsl)j/·
无法通过编译+/
intfails2[999][3】[6];
invertinplace(fails2);
/}无法通过编译·
却是无法通过编译器这一关的。
7●
C专家编程.————————————————————————————————————————————————————————————————————————一
——————_—————_——————_——————_●————————————————————————————-———————_——_————————————————一
二维或更多维的数组无法在C语言中用作一般形式的参数。
你无法向函数传递一个普通
的多维数组。
可以向函数传递预先确定长度的特殊数组,但这个方法并不能满足一般情况。
最显而易见的方法是声明一个像下面这样的原型:
10-5.1方法l
my—function(intmy—array[i0][20]);
尽管这是最简单的方法,但同时也是作用最小的。
因为它追使函数只处理l0行20列的
int型数组。
我们想要的是一个确定更为普通的多维数组形参的方法,使函数能够操作任意长
度的数组。
注意,多维数组最主要的一维的长度(最左边一维)不必显式写明。
所有的函数
都必须知道数组其他维的确切长度和数组的基地址。
有了这些信息,它就可以一次“跳过”
一个完整的行,到达下一行。
,’
10.5.2方法2
我们可以合法地省略第一维的长度,像下面这样声明多维数组:
my—function(fntmy—array[][20]);
但这样做法仍不够充分,因为每一行都必须正好是20个整数的长度。
函数也可以类似地
声明为:
my—function(int(★my—array)[20]);
参数列表中(木my_array)周围的括号是绝对需要的,这样可以确保它被翻译为一个指向20
个元素的int数组的指针,而不是一个20个int指针元素的数组。
同样,我们对最右边一维
的长度必须为20感觉不快。
一致性数组
按照最初的设计,Pascal也具有和C语言同种的功能缺陷——没有办法向N--+m数#
递长度不同的数组。
事实上Pascal的情况更糟,因为它甚至不能支持一维数组的情况,而c
语言倒可以实现。
数组边界是函数原型的一部分,如果实参数组的长度不能与形参完全匹配,
就会产生一个类型不匹配错误。
像下面这样的Pascal代码是非法的:
varapple:
array[1..l0]ofinteger;
precedureinvert(a:
array[1..l5]ofinteger;
,-^黪瓣黼
。
。
0豢蹶黼燃鳓
≤蔡㈣簟
_i蕊嚣篱■
invert(apple);
(无法通过编译)1
为了弥补这个缺陷,Pascal标准化语言协会构思了一个概念,称为一致性数组
(conformationarrays、卜_—域许取名为“confuse’emarrays(混淆他们的数组)”更为合适。
它
是一种协议,用于实参和形参之间数组长度的通信。
对于一般的程序员而言,这个方法的工
作原理并非一眼可见,而且它也不存在于其他的主流语言中。
你必须像下面这样编写代码:
数据名I0和hi(当然也可以取其他的名字)所对应的数组边界在每次调用时根据实际参数
进行填充。
经验显示,许多程序员认为这种形式只会把事情搞得更乱。
在解决了普通情况的
数组参数传递问题后,语言的设计者把最简单的字符长度固定的数组这种情况搞成了非法代
码:
1procedurea(fname:
array【1../0J0fchar);
E--—-—-—-—-—-—-—-—-—-—-—-—-—--·
·
-—-—‘—_—_—-—-—-—·
—-“·
—-—。
Expectedidehtifier
这种语言定义的方式很显然与许多程序员预期的行为背道而驰。
时至今日,我们已经接
到无数的技术支持电话请求帮助。
在Sun的编译器小组里,每隔数月“Pascal编译器Bu9”
的报告便上升一个数量级。
Pascal的一致性数组另外还存在一个问题,例如,一个一致性字
符数组并不具有字符串类型(因为它的类型无法用任何数组类型来表示),所以即使它是一
个字符数组,它也不能作为字符串参数传递!
一致性数组形参会给Pascal程序员带来更多的
烦恼,也许只有交互式I/0比它更麻烦。
更糟糕的是,有些人正在讨论要不要在C语言中增
加一致性数组。
10.5.3方法3
我们可以采取的第三种方法是放弃二维数组,把它的结构改为一个Iliffe向量。
也就是说,
创建一个一维数组,数组中的元素是指向其他东西的指针。
回想一下main()i垂i数的两个参数,
我们已经习惯了看到char,.cargv[];
的形式,有时也能看到char木木argv;
这样的形式,它能提醒
我们怎样分析这个声明。
可以简单地传递一个指向数组参数的第一个元素的指针,如下所示
(用于二维数组):
my—function(char”my—array);
注意:
只有把二维数组改为一个指向向量的指针数组的前提下才可以这样做!
Iliffe向量这种数据结构的美感在于:
它允许任意的字符串指针数组传递给函数,但必须
是指针数组,而且必须是指向字符串的指针数组。
这是因为字符串和指针都有一个显式的越
界值(分别为NUL和NULL),可以作为结束标记。
至于其他类型,并没有一种类似的通用
花括号是Pascal的注释形式。
——译者注
且可靠的值,所以并没有一种内置的方法知道何时到达数组某一维的结束位置。
即使是指向
字符串的指针数组,通常也需要一个计数参数argc,记录字符串的数量。
10.5.4方法4
我们可以采取的最后一种方法也是放弃多维数组的形式,提供自己的下标方式。
当
GrouchoMarx评论“如果你把酸果蔓煮成苹果酱那样,它们尝起来会比大黄更像李子”时,
他脑子里想的肯定就是这种错综复杂的迂回方法。
char—array[row_size+i+J]=…
这很容易误入歧途,而且会让你困惑,如果可以手工做这些事情,为什么还需要使用编
译器呢?
总之,如果多维数组各维的长度都是一个完全相同的固定值,那么把它传递给一个函数
毫无问题。
如果情况更普通一些,也更常见一些,就是作为函数的参数的数组的长度是任意
的,我们用下面的方法进行进一步的分析:
一维数组——没有问题,但需要包括一个计数值或者是一个能够标识越界位置的结束
符。
被调用的函数无法检测数组参数的边界。
正因为如此,9ets()函数存在安全漏洞,从而导
致了Internet蠕虫的产生。
二维数组——不能直接传递给函数,但可以把矩阵改写为一个一维的Iliffe向量,并
使用相同的下标表示方法。
对于字符串来说,这样做是可以的,对于其他类型,需要增加一
个记数值或者能够标识越界位置的结束符。
同样,它依赖于调用函数和被调用函数之间的约
定。
三维或更多维的数组——都无法使用。
必须把它分解为几个维数更少的数组。
对多维数组作为参数传递的支持缺乏是C语言存在的一个内在限制。
这使得用C语言编
写某些特定类型的程序非常困难(如数值分析算法)。
’
10.6使用指针从函数返回一个数缎
前面一节,我们分析了怎样把数组作为参数传递给函数。
本节换个方向讨论数据的转换:
从函数返回一个数组。
严格地说,无法直接从函数返回一个数组。
但是,可以让函数返回一个指向任何数据结
构的指针,当然也可以是一个指向数组的指针。
记住,声明必须在使用之前。
一个声明的例
子是:
int(+pal())[20】j
这里,paf是一个函数,它返回一个指向包含20个int元素的数组的指针。
它的定义可
能如下:
int(+pal())[20】{
230
int(*pear)[20】;
声明一个指向包含20个int元素的数组的指针·
pear=calloc(20,sizeof(int));
if(!
pear)longjmp(error,1)’j
returnpear;
)
你用下面这样的方法来调用函数:
int(*result)(20】;
/*声明一个指向包含20个int元素的数组的指针*/
result=paf();
/+调用函数+/
(*resu
- 配套讲稿:
如PPT文件的首页显示word图标,表示该PPT已包含配套word讲稿。双击word图标可打开word文档。
- 特殊限制:
部分文档作品中含有的国旗、国徽等图片,仅作为作品整体效果示例展示,禁止商用。设计者仅对作品中独创性部分享有著作权。
- 关 键 词:
- 专家 编程 10