这些小活动你都参加了吗?快来围观一下吧!>>
电子产品世界 » 论坛首页 » 嵌入式开发 » MCU » 再再论指针

共13条 1/2 1 2 跳转至

再再论指针

院士
2011-01-27 13:11:42     打赏

篇首语
    指针是C语言规范里面一项核心内容,指针具有与生俱来的优势,利用指针可以写出许多短小精悍、效率极高的代码,它是C语言一把无可替代的利器,凭着这把利器,C语言与其它高级语言相比至少在效率方面高人一筹。但是,由于指针的原理与使用方式跟人们通常的思维习惯有较大的差别,造成了指针比C语言其它概念难理解得多,这使得对指针认识不足成为了一种在C程序员中普遍存在的现象,这种不足必然导致程序员在指针的使用过程中不断遭受挫折,挫折多了,指针俨然变成一道无法逾越的难关,恐惧感也就油然而生了。在恐惧感面前,某些程序员甚至产生了要避免使用指针的念头,这是非常不可取的。指针是如此犀利,正是它才使得C语言威猛无比,如果就这样把它放弃了,那么C语言就算是白学了。我们应当让指针成为你手中那把砍掉索伦手指上魔戒的举世无双的纳西尔圣剑,而不是成为你心中永远的魔戒。
 
    本文的目的,是希望通过跟各位朋友一起讨论关于指针的几个关键概念及常见问题,以加深对指针的理解。因此,本文并不是讲述形如int *p、struct {int i;float j;} *p等这些东西是什么的文章,阅读本文的朋友最好对指针已经具有一定的使用经验,正因如此,笔者才给文章起名叫《再再论指针》。笔者不敢奢望能够完全解开你心中的魔结,但如果通过阅读本文,能够让你在日后的指针使用过程中减少失误,那笔者就心满意足了。本文将讨论如下十个主题,读者最好按主题的顺序一个一个地阅读,当然,如果你只对其中某个或某几个主题感兴趣,只看那几个也未尝不可。
 
    当你阅读本文后:
 
    如果你有不同的意见,欢迎你在评论里留下自己的见解,笔者很乐意跟你一起讨论,共同进步。
 
    如果你觉得我说的全都是废话,那么恭喜你,你的指针已经毕业了。
 
    如果你有太多不明白的地方,那么我介绍你先找一些关于数组与指针的读物看看,笔者推荐你阅读一本叫《C与指针》的书,看完后再回来继续思考你的问题。
 




关键词: 再论     指针    

院士
2011-01-27 13:12:29     打赏
2楼
第一章 什么是数组名?--一个让你吃惊的事实! 数组是指针的基础,多数人就是从数组的学习开始指针的旅程的。下面我节选一些在各种论坛和文章里经常见到的关于数组的文字: “一维数组是一级指针” “二维数组是二级指针” “数组名可以作为指针使用” “数组名就是..........的常量指针” “数组名就是..........的指针常量” .................................. 这些文字看起来非常熟悉吧?类似的文字还有许多,或许你就是经常说这些话的人呢。不过非常遗憾,这些文字都是错误的,实际上数组名永远都不会是指针!这个结论也许会让你震惊,但它的确是事实。数组名、指针、地址这几个概念虽然是基础中的基础,但它们恰恰是被混淆和滥用得最多的概念,把数组名说成指针,是一个概念性的错误,实质是混淆了指针与地址两个概念的本质。俗话说得好:浅水淹死人。因此,在讨论数组之前,有必要先回过头来澄清一下什么是指针,什么是地址,什么是数组名。       指针是C语言具有低级语言特征的最直接的证据。在汇编语言里面,指针的概念随处可见。比如SP,SP寄存器又叫堆栈指针,它的值是地址,由于SP保存的是地址,并且SP的值是不断变化的,因此可以看作一个变量,而且是一个地址变量。地址也是C语言指针的值,C语言的指针跟SP这样的寄存器虽然不完全一样,但原理却是相通的。C语言的指针也是一种地址变量,C89明确规定,指针是一个保存对象地址的变量。这里要注意的是,指针跟地址概念的不同,指针是一种地址变量,通常也叫指针变量,统称指针。而地址则是地址变量的值。       看到这里,也许你会觉得,这么简单的东西还用你来说吗?的确,对于p与&p来说,99%的人都能在0.1秒内脱口而出谁是指针,谁是地址,但是,又有多少人在使用指针的过程中能够始终如一毫不动摇地遵循这两个概念呢?不少人使用指针的时候就会自觉或不自觉地把指针和地址两个概念混淆得一塌糊涂了,数组名的滥用就是一个活生生的例子。这一点甚至连一些经典著作也没能避免。       不过也不能全怪你自己,笔者认为某些国内教材应该承担最大的责任。这些教材一开始就没有给读者好好地分清指针与地址的区别,相反还在讲述的过程中有意无意地混用这两个概念。更有甚者,甚至在书中明言指针就是地址!说这话的家伙最应该在C语言这个地图上抹掉,呵呵。两个月前我在购书中心随手翻开了某个作者主编的一本被冠以国家“十五”规划重点研究项目的书,书里就是这么写的。当时笔者就感慨:不知道又要有多少人的思想被这家伙“强奸”了。   实际上,地址这个东西,本来就是一种基本数据类型,本应该在介绍整数、浮点、字符等基本类型的时候把地址显式地放在一起讨论,这样在后面介绍指针与数组的时候就能避免许多误解。可惜不少教材或者根本没有谈及,或者就算提起这个类型也用了指针类型这个字眼。这就错了,指针不是类型,真正的类型是地址,指针只是存储地址这种数据类型的变量!打个比方,对于   int i=10;   10是整数,而i是存储整数的变量,指针就好比这个i,地址就好比那个10。指针能够进行加减法,原因并不是因为它是指针,加减法则不是属于指针这种变量的,而是地址这种数据类型的本能,正是因为地址具有加减的能力,所以才使指针作为存放地址的变量能够进行加减运算。这跟整数变量因为整数能够进行加减乘除因而它也能进行加减乘除一个道理。       那么数组名又应该如何理解呢?用来存放数组的区域是一块在栈中静态分配的内存(非static),而数组名是这块内存的代表,它被定义为这块内存的首地址。这就说明了数组名是一个地址,而且,还是一个不可修改的常量,完整地说,就是一个地址常量。数组名跟枚举常量类似,都属于符号常量。数组名这个符号,就代表了那块内存的首地址。注意了!不是数组名这个符号的值是那块内存的首地址,而是数组名这个符号本身就代表了首地址这个地址值,它就是这个地址,这就是数组名属于符号常量的意义所在。由于数组名是一种符号常量,因此它是一个右值,而指针,作为变量,却是一个左值,一个右值永远都不会是左值,那么,数组名永远都不会是指针!不管什么话,只要说数组名是一个指针的,都是错误的!就象把刚才int i=10例子中的10说成是整数变量一样,在最基本的立足点上就已经完错了。       总之要牢牢记住,数组名是一个地址,一个符号地址常量,不是一个变量,更不是一个作为变量的指针!       在数组名并非指针这个问题上,通常会产生两种疑问: 1。作为形参的数组,不是会被转换为指针吗? 2。如果形参是一个指针,数组名可以作为实参传递给那个指针,难道不是说明了数组名是一个指针吗?       首先,C语言之所以把作为形参的数组看作指针,并非因为数组名可以转换为指针,而是因为当初ANSI委员会制定标准的时候,从C程序的执行效率出发,不主张参数传递时复制整个数组,而是传递数组的首地址,由被调函数根据这个首地址处理数组中的内容。那么谁能承担这种“转换”呢?这个主体必须具有地址数据类型,同时应该是一个变量,满足这两个条件的,非指针莫属了。要注意的是,这种“转换”只是一种逻辑看法上的转换,实际当中并没有发生这个过程,没有任何数组实体被转换为指针实体。另一方面,大家不要被“转换”这个字眼给蒙蔽了,转换并不意味着相同,实际上,正是因为不相同才会有转换,相同的话还转来干吗?这好比现在社会上有不少人“变性”,一个男人可以“转换”为一个女人,那是不是应该认为男人跟女人是相同的?这不是笑话么。       第二,函数参数传递的过程,本质上是一种赋值过程。C89对函数调用是这样规定的:函数调用由一个后缀表达式(称为函数标志符,function designator)后跟由圆括号括起来的赋值表达式列表组成,在调用函数之前,函数的每个实际参数将被复制,所有的实际参数严格地按值传递。因此,形参实际上所期望得到的东西,并不是实参本身,而是实参的值或者实参所代表的值!举个例来说,对于一个函数声明:   void fun(int i);   我们可以用一个整数变量int n作实参来调用fun,就是fun(n);当然,也正如大家所熟悉的那样,可以用一个整数常量例如10来做实参,就是fun(10);那么,按照第二个疑问的看法,由于形参是一个整数变量,而10可以作为实参传递给i,岂不就说明10是一个整数变量吗?这显然是谬误。实际上,对于形参i来说,用来声明i的类型说明符int,所起的作用是用来说明需要传递给i一个整数,并非要求实参也是一个整数变量,i真正所期望的,只是一个整数,仅此而已,至于实参是什么,跟i没有任何关系,它才不管呢,只要能正确给i传递一个整数就OK了。当形参是指针的时候,所发生的事情跟这个是相同的。指针形参并没有要求实参也是一个指针,它需要的是一个地址,谁能给予它一个地址?显然指针、地址常量和符号地址常量都能满足这个要求,而数组名作为符号地址常量正是指针形参所需要的地址,这个过程就跟把一个整数赋值给一个整数变量一样简单!       在后面的章节中,笔者将严格地使用地址这一概念,该是地址时就用地址,该是指针时就用指针,以免象其它教材那样给读者一个错误的暗示。  

院士
2011-01-27 13:13:41     打赏
3楼
第二章 再一次吃惊--数组的数组与多维数组的区别     看见这个题目,也许有些人就会嘀咕了:难道两者不是一样的吗?C语言的多维数组不就是数组的数组吗?不!两者是有区别的,而且还不小呢。首先看看两者的共同点:   1。内存映象一样。   2。数组引用方式一样,都是“数组名[下标][下标]........”。   3。数组名都是数组的首地址,都是一个符号地址常量、一个右值。         由于两者的共同点主要反映在外部表现形式上,因此,从外部看来,数组的数组跟多维数组似乎是一样的,这造成了C程序员对两者的区别长期以来模糊不清。但实际上,c语言限于本身的语言特性,实现的并非真正的多维数组,而是数组的数组。   数组的数组与多维数组的主要区别,就在于数组的数组各维之间的内在关系是一种鲜明的层级关系。上一维把下一维看作下一级数组,也就是数组嵌套。数组引用时需要层层解析,直到最后一维。举个例,对于数组:   int a[7][8][9];   如果要访问元素a[4][5][6],首先就要计算第一维元素4的地址,也就是a+4,由于是数组的数组,元素4的值代表了一个数组,因此元素4的值就是它所代表的那个数组的首地址,我们用一个符号address1代表它,也就是address1=*(a+4),接着计算第二维,显然元素5的地址是address1+5,其值也是一个数组的首地址,用address2表示它,就是address2=*(address1+5),最后一维,由于已经到达了具体的元素,因此这个元素的地址是address2+6,其值*(address2+6)是一个整数,把address1和address2分别代入相应表达式,就成了:   *(*(*(a+4)+5)+6);   这就是我们熟知的[]运算符的等价表达式。   而真正的多维数组并没有这么多“束缚”,相比之下简单得多,由于各维之间不是这种复杂的层级关系,元素a[4][5][6]的偏移量可以这样直接获得:(4*8*9+5*9+6)*sizeof(int),再加上数组的首地址a就是元素a[4][5][6]的地址了。但是,c语言的数组能够这样用首地址加上(4*8*9+5*9+6)*sizeof(int)的形式来访问元素吗?显然是不行的。归根到底就在于C语言的地址数据类型不但有类型,还具有级别。就是这种层级关系造成了C语言只能用数组的数组当作多维数组。如果C语言非得要实现真正的多维数组,那么地址与指针的概念就得重新改写了。  

院士
2011-01-27 13:14:07     打赏
4楼
第三章 数组的解剖学     这一章我们来讨论一下数组的内涵,对数组的内部构造进行一次解剖,看看里面究竟隐藏了什么秘密。 有了前面两章对数组名和C语言数组本质的澄清,再来理解这一章的内容,就容易多了。        在下面的叙述中,笔者会用到一个运算符sizeof,由于在不同的编译器和编译模式下,对一个地址进行sizeof运算的结果有可能是不同的,为了方便讨论,我都假设地址长度为4个字节。       多数教材在讲述数组的时候,都是把重点放在外部表现形式上,很少涉及数组的内部,只告诉你如何做,却忽视了为什么要这样做。在解释的过程中,还会列出各种各样的表达式,例如:a、a+1、a[0]、a[0][0]、&a[0]、&a[0][0]、*(a+1)等等,让人眼花缭乱。但实际上真正能够用来描述数组内部构造的表达式只有其中的几个。       上一章讲到,C语言的数组实现并非真正的多维数组,而是数组嵌套,访问某个元素的时候,需要逐层向下解析。仍然以上一章的例子数组int a[7][8][9]来说,第一维元素0的值a[0]是a[0]所代表的那个数组的首地址,这个表达式在C语言的数组里面具有特殊的意义,之所以特殊,不仅仅在于它所代表的东西与一般的地址不同,而且类型也并非一般的地址类型,它的类型叫做数组类型,数组类型这个名称在绝大多数教材中是从来没有出现过的,在C89标准中,也仅仅出现在介绍数组定义的那一段。具有数组类型的地址跟一般类型地址的主要区别,在于长度不一样,对一个一般类型的地址进行sizeof运算,结果是4个字节,而a[0]由于代表了一个数组,sizeof(a[0])的结果是整个数组的长度8*9*sizeof(int),并非4个字节。具有数组类型的地址跟数组名一样都是一个符号地址常量,因此它必定是一个右值。数组类型在数组的定义与引用中具有非常重要的作用,它可以用来识别一个标识符或表达式是否真正的数组,一个真正数组的数组名,是一个具有数组类型的符号地址常量,它的长度,是整个数组的长度,并非一般地址的长度,如果一个标识符不具备数组类型,那它就不是一个真正的数组。在后面的章节里,还会再次使用这个概念。       与a[0]类似的数组类型地址还有a[0][0],a[0][0]是a[0]的下一层数组,因此sizeof(a[0][0])的结果是9*sizeof(int)。类似地,对于一个三维数组:   a[i][j][k]   a、a[x]、a[x][y](其中x、y大于等于0而小于i、j)都是具有数组类型的地址常量,而且都是一个右值。这一点要牢牢记住。正是由这些特殊类型的地址构成了整个数组。   以上结论对于n维数组同样适用。   接下来跟各位一起讨论一下跟数组有关的各种表达式的意义及其类型:   &a[0][0][0]:   &a[0][0][0]仅仅是一个地址,它的意义,仅仅表示元素a[0][0][0]的地址,sizeof(&a[0][0][0])的结果是4。不少人把它说成是数组a的首地址,这是错误的,这是对数组首地址概念的滥用。真正能代表数组a的数组首地址只有a本身,a与&a[0][0][0]的意义根本就是两回事,真正的数组首地址是具有数组类型的地址,sizeof(a)结果是i*j*k*sizeof(int),而不是4,只不过由于a[0][0][0]位置特殊,恰好是数组a的第一个元素,所以它们的地址值才相同。而对于a[0]和a[0][0],它们是在数组a内部a[0]和a[0][0]所代表的那个数组的首地址,它们的地址值也是由于位置“特殊”,因此才跟a和&a[0][0][0]一样。这一点一定要区分清楚了。   a+i:   可能有些人会对a+i感到迷惑,数组的首地址加上一个整数是什么呢?它是第一维元素i的地址,sizeof(a+i)为4。   a+j:   跟上面的类似,a+j是a所代表的那个数组的元素j的地址,sizeof(a+j)的结果也为4。   &a:   对数组名取地址在C标准里面是未定义的。这个表达式曾经引起过争论,焦点在于对一个右值取地址的合法性。C89规定&运算符的操作数必须具有具体的内存空间,换言之就是一个左值,但数组名却是一个右值,按照&运算符的要求,这是非法行为。因此,早期的编译器通常规定&a是非法的。但不知道什么原因,现在的编译器都把&a人为地定义成一个比a高一级而地址值跟a一样的地址,但作为比a高一级的地址,有一个行为却非常怪诞,sizeof(&a)的结果跟sizeof(a)相同,这也是人为的痕迹。笔者倾向于把&a定义为非法,应该维护&运算符的权威性,而不是在规定对某个右值取地址为非法的同时,又允许对另一个右值取地址,这是互相矛盾的。   &a和&a[j]:   跟&a一样,也是未定义的,同样不符合&运算符的规则。由于a是a[j]的上一层数组,有些人可能会想当然地以为:a=&a[j],错也,实际上,由于a[j]=*(a+j),因此&a[j]=&*(a+j),结果是a+j。对于sizeof(&a)和sizeof(&a[j]),由于是未定义的,因此有些编译器规定其值跟sizeof(a)和sizeof(a[j])相同,有些编译器却规定为4,就是一个地址的长度。

院士
2011-01-27 13:14:38     打赏
5楼
第四章 [ ]运算符的本质     数组是存在于人们头脑中的一个逻辑概念,而编译器其实并不知道有数组这个东西,它所知道的,只是[]运算符,当遇到[]运算符的时候,编译器只是简单地把它转换为类似*(*(a+i)+j)这样的等价表达式,之所以是这种表达式,如前几章所述,是因为C语言的数组实现本质上是数组的嵌套。       由于这种等价关系的存在,会产生一些古零精怪的表达式,例如:   10[a]   这个表达式初看上去让人摸不着头脑,它是什么呢?如上所述,编译器会把它转换为*(10+a),把a和10调换一下,就是*(a+10)了,这个就是a[10]。   []运算符之前还可以是一个表达式,例如:(10+20)[a]。   严格来讲,以上两个表达式是非法的,因为C89对于数组的引用(注意不是数组定义)规定:带下标的数组引用后缀表达式由一个后缀表达式后跟一个括在方括号中的表达式组成。方括号前的后缀表达式的类型必须为“指向T类型的指针”,其中T为某种类型;方括号中表达式的类型必须为整型。这个规定说明,进行数组引用的时候,[]运算符的左边并非必须为数组名,而可以是一个表达式,但这个表达式的类型必须为“指向某类型的指针”。显然10跟(10+20)连地址都不是,因此实际上他们是非法的,编译器在这里并没有严格遵守标准的规定。但如果是:   int a[10], *p = a;   (p+1)[2]这样就是合法的,因为p+1的结果仍然是一个指针。   要注意的是,虽然后缀表达式是一个“指向某类型的指针”,但不要被这里所说的指针一词搞混了,上面的规定不能反过来使用。还是以上面的例子为例,我们可以p[i]这样使用p,这是符合上述规定的,但并不能因为指针p能够以p[i]这种形式使用就认为p是一个数组,这就错误了,不能反过来应用上述规则。       最后说一下编译器对&*的优化,对于数组int a[10],如果对其中一个元素取地址,例如&a[1],这条表达式等价于&*(a+1),编译器并不会先计算*再运算&,而是对&*两个运算符进行优化,把它们同时去掉,因为两者的作用是相反的,最后得到计算的是a+1表达式。  

院士
2011-01-27 13:15:12     打赏
6楼
第五章 指向数组的指针     讲到第五章了,数组两个字还离不开我们的左右,数组的内容也真多,另一方面也因为数组与指针的关系的确非常密切。       通常,对于int a[8][9]这个二维数组,我们可以这样定义一个指向它的指针:   int (*p)[9];   这个声明的形式跟人们所熟悉的int *p的形式大相庭径,初学者通常会感到迷惑,不理解的地方大致有四个:   1。为什么会以这种形式声明?   2。(*p)应该如何理解?   3。为什么必须把第二维显式地声明?   4。为什么忽略第一维?   下面我们就一起逐个讨论这四个问题:   1。这种形式是C标准的声明语法规定的,由于本章不是对标准的解释,只是对标准的应用,因此笔者尽量以简洁的方式解释这个声明,详细的讨论将在第七章进行。C标准的声明包含了两部分:   声明:   声明说明符 初始化声明符表opt      (opt的意思是可选)   在声明说明符里面有一项类型说明符,int就是这种类型说明符。而初始化声明符表里面的其中一种形式,就是:   直接声明符 [常量表达式opt]   (*p)[9]就是这种直接声明符加[]的形式。   2。p左边的*在这里不是取值运算符,而是一个声明符,它指出p是一个指针。而()括号是不能去掉的,如果去掉了,由于[]运算符优先级比*高,p就会先跟[]结合,这样p就变成了一个指针数组,而不是指向数组的指针。   题外话:   *p还有一种用法,就是当*是取值运算符的时候,*p是一个左值,表示一个变量,为什么*p是一个变量呢?也许有人会说,因为int i, *p=&i嘛,其实这是结果不是原因。严格来说,i只是一个变量名,不是变量,在编译器的符号表里面,变量名是一个符号地址,它所代表的地址值是它指向的那段内存单元的地址,真正叫变量的是那段内存单元,懂汇编的朋友能很容易地区分出来,在汇编里面,可以这样定义一个变量名:   VARW DW 10,20   VARW就是一个变量名,它在汇编里面是一个地址,代表了10所在的内存单元这个变量。由于p被初始化为&i,*p指向i所代表的那段内存单元,因此说*p是一个变量。把i称为变量是一种习惯上的统称。   3。定义一个指针的时候,首先必须定出指针的类型,由于这是一个指向数组的指针,如果数组的元素的类型定下来了,那么这个指针的类型也就定下来了。前面说过,C语言的多维数组实质上是数组的嵌套,那么所指向数组的元素必定具有数组类型,也就是说,这个数组的元素是一个具有9个int元素的数组,因此,p定义的时候,必须指定第二维的上界,这样才能把p的类型定下来。   4。有这种疑问的人已经犯了一个错误,没有分清楚什么是指针,什么是数组,以数组的思维模式来看待这个指针p。定义一个数组(非static)的时候,需要在栈中静态分配一块内存,那么就需要知道这块内存的大小,因此定义数组时需要确定各维的上界。而这里只是定义一个指针而已,对于一个指针的定义,需要知道的是它所指向对象的类型,并不需要知道对象的大小,这是多余的。因此,所有指向数组的指针的第一维被忽略。       以上介绍了如何声明一个指向二维数组的指针,类似地,对一个指向n维数组的指针也可以用同样的方法来声明,如下:   int (*p)[x2][x3]......[xn];   同样可以忽略第一维,而其它维必须指定上界。       最后再讨论一种很常见的对多维数组的错误理解,有些人常常会以为,二维数组就是二级指针,这种错误的根源,来自于可以把一个二级指针int **p以p[j]这种形式使用。首先把数组称为指针就是错误的,第一章笔者已经说明了数组名是地址,不能理解为指针。第二,并非能以p[j]这种形式使用,那么p就是一个二维数组了,C标准对数组引用的规定,并没有指定数组引用时[]运算符的左边必须是数组名,而可以是一个表达式。第三,这是一种“巧合”,归根到底是由于C语言的数组实现是数组的嵌套同时C标准把[]运算符转换为类似*(*(a+i)+j)这样的等价表达式造成的,那两个取值运算符“恰好”可以用于一个二级指针。第四,p与p[j]并不具有数组类型,sizeof(p)和sizeof(p[j])的结果只是一个指针的大小4字节。而对于一个真正的数组,p与p[j]都是具有数组类型的地址。   实际上,int **p只是一个指向一维指针数组的指针,而不是指向二维数组的指针。同样地,对于n级指针,都可以看作一个指向一维指针数组的指针,这个指针数组的元素都是n-1级指针。

院士
2011-01-27 13:15:37     打赏
7楼
第六章 “另类”数组     动态数组与字符串常量可算是两种“另类”数组。       VLA可变长数组并不为C89所支持,C99才开始支持VLA。但如果想在只支持C89的编译环境中使用VLA的话,怎么办呢?我们可以用动态数组来“模拟”,动态数组在矩阵的运算中很常见,常用来向函数传递一个大小可变的矩阵。动态数组的原理,是利用一块或多块动态分配的内存存储各维的首地址,这样就可以p[i][j]的形式访问数组的数据了。但是,动态数组并非真正的数组,它只是对数组的一种模拟。由于具有数组类型的数组名是系统行为,在用户这一级没法做到,因此只能以指针的形式存放首地址,sizeof(p)和sizeof(p[i])结果都是4字节。虽然动态数组是依靠动态分配内存来建立的,但动态的意义并非来自这里,而是指大小可变。笔者觉得用“动态数组”这个名称来命名非常适合,既不失大小可变的特征,又可以跟VLA可变长数组区分开来。       下面是建立动态数组的示例:   #include <stdio.h>
#include <stdlib.h> void computedata(int *, int, int); int main(void)
{
        int iData[100], x, y;
        do
        {
                printf("The product obtained by multiplying x and y must be less than 100!");
                printf("x=");
                scanf("%d", &x);
                printf("y=");
                scanf("%d", &y);
        }
        while(x*y > 100);
        computedata(iData, x, y);
        return 0;
} void computedata(int *ipSource, int iRow, int iColumn)
{
        int **ipTemp, i, j;
        ipTemp = (int **)malloc(iRow*sizeof(int*));
        for(i=0; i<iRow; ++i) ipTemp[i] = ipSource+i*iColumn;
        for(i=0; i<iRow; ++i) for(j=0; j<iColumn; ++j) ipTemp[i][j] += 1;
        free(ipTemp);
        return;
} 以上示例把动态数组ipTemp的元素都加了1,由于只是示例,笔者省略了检测数据合法性的代码。iRow是第一维上界,iColumn是第二维上界,iData是源数据缓冲区,iRow*iColumn的积不能超过iData缓冲区的大小,否则就会越界了,但可以比它小。示例中iData被定义为一维数组,当然根据自己的需要也可以用其它类型的缓冲区代替,例如动态分配的一块内存,或者多维数组,如果是多维数组,例如三维数组int iData[10][10][10],调用computedata函数时,实参iData得做一些转换: computedata((int *)iData, x, y);或者computedata(&iData[0][0][0], x, y);都可以。 ipSource指针用来传递缓冲区的首地址,这个指针由于要用来计算各维的地址,因此最好定义为一级指针,这样比较方便。ipTemp是一个二级指针,这是因为它指向的那块内存存放的是指针,这些指针指向各维的首地址,对ipTemp的元素来说,ipTemp就是二级的。最后记得free(ipTemp);     以上是定义一个二维动态数组的例子,多维动态数组的创建方法跟这个类似,下面给出三维动态数组的代码: void computedata(int *ipSource, int iHigh, int iRow, int iColumn)
{
        int ***ipTemp, i, j, k;
        ipTemp = (int ***)malloc(iHigh*sizeof(int**));
        for(i=0; i<iHigh; ++i) ipTemp[i] = (int **)malloc(iRow*sizeof(int*));
        for(i=0; i<iHigh; ++i) for(j=0; j<iRow; ++j) ipTemp[i][j] = ipSource+i*iRow*iColumn+j*iColumn;
        for(i=0; i<iHigh; ++i) for(j=0; j<iRow; ++j) for(k=0; k<iColumn; ++k) ipTemp[i][j][k] += 1;
        for(i=0; i<iHigh; ++i) free(ipTemp[i]);
        free(ipTemp);
        return;
}     下面来讨论字符串常量。
    众所周知,C语言是没有字符串变量的,因而,C89规定,字符串常量就是一个字符数组。因此,尽管字符串常量的外部表现形式跟数组完全不同,但它的确是一个真正的数组,实际上,字符串常量本身就是这个数组的首地址,并且具有数组类型,对一个字符串常量进行sizeof运算,例如sizeof("abcdefghi"),结果是10,而不是4。字符串常量与一般数组的主要区别,是字符串常量存放在静态存储区,而一般数组(非static)则是在栈中静态分配的。由于字符串常量是数组首地址,因此可以数组引用的形式使用它,例如: printf("%s", &"abcdefghi"[4]); 这将打印出字符串efghi。还可以这样: printf("%s", "abcdefghi"+4); 同样打印出字符串efghi。实际上,&"abcdefghi"[4]等价于&*("abcdefghi"+4),去掉&*后,就是"abcdefghi"+4了。 我们可以利用字符串常量这些特性写出一些有趣的程序来,例如:
#include <stdio.h> int iLine=1; int main(void)
{
 printf("%*s\n", 7-(iLine>4?iLine-4:4-iLine), "*******"+2*(iLine>4?iLine-4:4-iLine));
 if(++iLine != 8) main();
 return 0;
} 这个程序不使用任何数组形式的引用,不使用循环,就可以打印出用*号组合出来的菱形。当然,笔者并非鼓励大家编写这样的代码,但通过这样的例子加深对字符串常量的认识,仍然是非常重要的。   这个程序不使用任何数组形式的引用,不使用循环,就可以打印出用*号组合出来的菱形。当然,笔者并非鼓励大家编写这样的代码,但通过这样的例子加深对字符串常量的认识,仍然是非常重要的。

院士
2011-01-27 13:16:04     打赏
8楼
第七章 C语言声明详解     人们常说,C语言的声明太复杂了,的确,这也是C语言饱受批评的地方之一。不过,笔者认为,真正要受到批评的不是语言本身,而是那些传播者。传播者们通常都有一个共识:讲述要由浅入深。作为原则,笔者并非要反对它,毕竟笔者对C语言的学习,也经历了相同的过程。但是,由浅入深并不意味着一切从简,以偏盖全。计算机语言不同于数学理论(虽然它的确根植于数学,与数学密不可分),数学理论是一种循序渐进的过程,后面的理论以前面的理论为基础。但C语言归根说底,就是一堆语言规则而已,应该让学习者一开始就全面且详细地了解它,而不是象现在某些教材所做的那样,只说一部分,不说另一部分,以为这就是由浅入深了,实际上这是以偏盖全。       语言如此,声明作为C语言的一部分更是如此。我们最常见到的对声明的描述是这样的:   存储类别 类型限定词 类型 标识符   这种说明会给人们一种暗示:C语言的声明是静止的、死板的,什么声明都能够以这个为基础,往上一套就OK了。事实真的如此吗?说句心里话,笔者也祈祷事实真的如此,这样世界就简单多了、清静多了。但别忘了,这个世界总是让人事与愿违的。实际上,C的声明的组织形式是以嵌套为基础的,是用嵌套声明组织起来的,并非象上面所述那么死板,存储类说明符一定得放在限定词前面吗?类型说明符一定要紧贴标识符吗?不!C标准从来没有这样说过!下面来看一看C89对声明的形式是如何规定的:   声明:   声明说明符   初始化声明符表opt [opt的意思是option,可选]   其中声明说明符由以下三项构成:   声明说明符:   存储类说明符 声明说明符opt 类型说明符    声明说明符opt 类型限定符    声明说明符opt   在这里,一个声明说明符可以包含另一个声明说明符,这就是声明的嵌套,这种嵌套贯穿于整个声明之中,今天我们看来一个非常简单的声明,其实就是由多个声明嵌套组成的,例如:   static const int i=10, j=20, k=30;   变量i前面就是声明说明符部分,有三个声明说明符:static const int,static是一个存储类说明符,它属于这种形式:   static 声明说明符   static后面的声明说明符就是const int,const是一个类型限定符,这也是个嵌套,它是由   const 声明说明符   组成,最后的int是一个类型说明符,到这里已经没有嵌套了,int就是最底的一层。对于存储类说明符、类型说明符和类型限定符的排列顺序,C标准并没有规定其顺序,谁嵌套谁都可以。换言之,上面的声明可以写成:   int static const i=10, j=20, k=30;或者const int static i=10, j=20, k=30;   这无所谓,跟原声明是一样的。再举一个有趣的例子:   const int *p;与int const *p;   有些人会对后面一种形式感到困惑,因为他一直以来学习的都是那种死板的形式,因此他无法理解为什么那个const可以放在int的后面。实际上对于标准来说,这是再正常不过的行为了。   上面举的例子是变量的声明,函数的声明也同样道理,例如:   static const int func(void); ......   int main(void) {         int static const (*p)(void);         p=func;         .........         return 0; }   const int static func(void) {         .......         return 0; }   func的函数原型声明、函数定义跟main内的函数指针p的声明是一样的。但是,笔者并非鼓励大家把声明说明符写得乱七八糟,作为一个良好的风格,应该按照已经习惯约定的方式排列说明符,但懂得其中的原理非常重要。   声明static const int i=10, j=20, k=30;的int后面的部分就是初始化声明符表,这比较容易理解,这个符表实际上也是嵌套的:   初始化声明符表:   初始化声明符 初始化声明符表, 初始化声明符     初始化声明符:   声明符 声明符=初值         声明符是初始化声明符的主体,现在来讨论一下声明符是如何规定的:   声明符:   指针opt 直接声明符   这里写的指针opt指的是那个指针声明符*,要注意的是,*属于声明符,而不是声明说明符的一部分。   指针opt又包含:   指针:   * 类型限定符表opt * 类型限定符表opt 指针   在这里有一个常见的问题,就是const int *p;与int * const p的区别,第一个声明的const属于声明说明符,它跟int一起,是用来说明*p这个声明符的,因此const修饰的是p所指向的那个对象,这个对象是const的。而第二个声明的const是声明符的一部分,它修饰的对象是p本身,因此p是const的。       上面规定的第二条值得注意,这条规定产生了一种指针与const的复杂形式,例如:   const int * const *** const ** const p;(是不是有种想冲向厕所的冲动?)这是一种复杂的声明嵌套,如何解读这种声明?其实只要掌握了它的规律,无论它有多少个const、多少个*都不难解读的,这个内容我将在第九章进行解释。       剩下的就是直接声明符和类型限定词表的内容:   直接声明符:   标识符 (声明符) 直接声明符[常量表达式opt] 直接声明符(形式参数类型表) 直接声明符(标识符表opt)     类型限定符表:   类型限定符 类型限定符表 类型限定符         这一章的最后一个内容,是讨论一下typedef,typedef用来声明一个别名,typedef后面的语法,是一个声明。本来笔者以为这里不会产生什么误解的,但结果却出乎意料,产生误解的人不在少数。罪魁祸首又是那些害人的教材。在这些教材中介绍typedef的时候通常会写出如下形式:   typedef int PARA;   这种形式跟#define int PARA几乎一样,如前面几章所述,这些教材的宗旨是由浅入深,但实际做出来的行为却是以偏盖全。的确,这种形式在所有形式中是最简单的,但却没有对typedef进一步解释,使得不少人用#define的思维来看待typedef,把int与PARA分开来看,int是一部分,PARA是另一部分,但实际上根本就不是这么一回事。int与PARA是一个整体!就象int i:声明一样是一个整体声明,只不过int i定义了一个变量,而typedef定义了一个别名。这些人由于持有这种错误的观念,就会无法理解如下一些声明:   typedef int a[10]; typedef void (*p)(void);   他们会以为a[10]是int的别名,(*p)(void)是void的别名,但这样的别名看起来又似乎不是合法的名字,于是陷入困惑之中。实际上,上面的语句把a声明为具有10个int元素的数组的类型别名,p是一种函数指针的类型别名。   虽然在功能上,typedef可以看作一个跟int PARA分离的动作,但语法上typedef属于存储类声明说明符,因此严格来说,typedef int PARA整个是一个完整的声明。

院士
2011-01-27 13:16:29     打赏
9楼
第八章 右左法则----复杂指针解析     上一章费那么多唇舌讨论C语言的声明,其实目的都是为了这一章,期望读者通过对C语言声明形式的详细了解,树立声明嵌套的观念,因为C语言所有复杂的指针声明,都是由各种声明嵌套构成的。如何解读复杂指针声明呢?右左法则是一个既著名又常用的方法。不过,右左法则其实并不是C标准里面的内容,它是从C标准的声明规定中归纳出来的方法。C标准的声明规则,是用来解决如何创建声明的,而右左法则是用来解决如何辩识一个声明的,两者可以说是相反的。右左法则的英文原文是这样说的:   The right-left rule: Start reading the declaration from the innermost parentheses, go right, and then go left. When you encounter parentheses, the direction should be reversed. Once everything in the parentheses has been parsed, jump out of it. Continue till the whole declaration has been parsed.     这段英文的翻译如下:   右左法则:首先从最里面的圆括号看起,然后往右看,再往左看。每当遇到圆括号时,就应该掉转阅读方向。一旦解析完圆括号里面所有的东西,就跳出圆括号。重复这个过程直到整个声明解析完毕。       笔者要对这个法则进行一个小小的修正,应该是从未定义的标识符开始阅读,而不是从括号读起,之所以是未定义的标识符,是因为一个声明里面可能有多个标识符,但未定义的标识符只会有一个。       现在通过一些例子来讨论右左法则的应用,先从最简单的开始,逐步加深:   int (*func)(int *p);   首先找到那个未定义的标识符,就是func,它的外面有一对圆括号,而且左边是一个*号,这说明func是一个指针,然后跳出这个圆括号,先看右边,也是一个圆括号,这说明(*func)是一个函数,而func是一个指向这类函数的指针,就是一个函数指针,这类函数具有int*类型的形参,返回值类型是int。   int (*func)(int *p, int (*f)(int*));   func被一对括号包含,且左边有一个*号,说明func是一个指针,跳出括号,右边也有个括号,那么func是一个指向函数的指针,这类函数具有int *和int (*)(int*)这样的形参,返回值为int类型。再来看一看func的形参int (*f)(int*),类似前面的解释,f也是一个函数指针,指向的函数具有int*类型的形参,返回值为int。   int (*func[5])(int *p);   func右边是一个[]运算符,说明func是一个具有5个元素的数组,func的左边有一个*,说明func的元素是指针,要注意这里的*不是修饰func的,而是修饰func[5]的,原因是[]运算符优先级比*高,func先跟[]结合,因此*修饰的是func[5]。跳出这个括号,看右边,也是一对圆括号,说明func数组的元素是函数类型的指针,它所指向的函数具有int*类型的形参,返回值类型为int。   int (*(*func)[5])(int *p);   func被一个圆括号包含,左边又有一个*,那么func是一个指针,跳出括号,右边是一个[]运算符号,说明func是一个指向数组的指针,现在往左看,左边有一个*号,说明这个数组的元素是指针,再跳出括号,右边又有一个括号,说明这个数组的元素是指向函数的指针。总结一下,就是:func是一个指向数组的指针,这个数组的元素是函数指针,这些指针指向具有int*形参,返回值为int类型的函数。   int (*(*func)(int *p))[5];   func是一个函数指针,这类函数具有int*类型的形参,返回值是指向数组的指针,所指向的数组的元素是具有5个int元素的数组。   要注意有些复杂指针声明是非法的,例如:   int func(void) [5];   func是一个返回值为具有5个int元素的数组的函数。但C语言的函数返回值不能为数组,这是因为如果允许函数返回值为数组,那么接收这个数组的内容的东西,也必须是一个数组,但C语言的数组名是一个右值,它不能作为左值来接收另一个数组,因此函数返回值不能为数组。   int func[5](void);   func是一个具有5个元素的数组,这个数组的元素都是函数。这也是非法的,因为数组的元素除了类型必须一样外,每个元素所占用的内存空间也必须相同,显然函数是无法达到这个要求的,即使函数的类型一样,但函数所占用的空间通常是不相同的。       作为练习,下面列几个复杂指针声明给读者自己来解析,答案放在第十章里。   int (*(*func)[5][6])[7][8];   int (*(*(*func)(int *))[5])(int *);   int (*(*func[7][8][9])(int*))[5];       实际当中,需要声明一个复杂指针时,如果把整个声明写成上面所示的形式,对程序可读性是一大损害。应该用typedef来对声明逐层分解,增强可读性,例如对于声明:   int (*(*func)(int *p))[5];   可以这样分解:   typedef int (*PARA)[5]; typedef PARA (*func)(int *);   这样就容易看得多了。

院士
2011-01-27 13:17:33     打赏
10楼
第九章 指针与const
    const一词是英文constant的缩写,设立这个关键字的本意,是希望让它所修饰的对象成为一个常量。记得在国家间的外交中,有一个经常用到的术语:“从事与身份不符的活动”,这个const恰恰也正从事着这样的活动,呵呵。C语言可以有三种方法定义一个常量:#define、const和枚举,但只有枚举才是真正的常量,什么是真正的常量?真正的常量是没有存储空间的,是一个右值,这意味着通过任何合法的手段也不会被修改,但被const修饰的对象依然是一个左值,尽管这个对象被const限定,笔者仍然至少可以找到三种合法的手段去修改它,而#define所做的只不过是编译期替换而已,只有枚举常量才能真正做到这一点。const实在不应该被命名为const,这会让人们产生误解,它应该命名为readonly或类似的字眼,意即不能通过被const修饰的对象修改它所指向的对象或者它所代表的对象。但在C的世界里把const称为常量早已是普遍的现象,那我们就只好随大流咯,也称之为常量吧,只要知道它实际上不是真正的常量就行了。
 
    第七章曾经讨论过const int *p;与int * const p的区别,这两个声明的中文名称常常搞得混乱不堪。第一个声明的const是声明说明符,它修饰p所指向的对象,但p仍然是可变的,这意味着p是一个指向常量的指针,简称常量指针。第二个声明的const是声明符的一部分,它修饰的对象是p,这意味着p是一个常量,而且是一个指针类型的常量,简称指针常量。指针常量又常常被人称为“常指针”或“常指针变量”,常指针变量这个名称有点蹩脚,又常又变的,容易让人摸不着头脑,最好还是不要这样称呼。这里还得再强调一次指针常量与地址常量是不同的,不能把数组名称为指针常量,也不能把指针常量称为地址常量,因为指针常量依然是一个左值,而数组名是一个右值,这里肯定有人会问:“什么?指针常量是一个左值?我没听错吧?”你的确没有听错,C89对于左值是这样定义的:
 
对象是一个命名的存储区域,左值(lvalue)是引用某个对象的表达式。
 
换言之,如果一个表达式引用的是一个具有具体存储空间的对象,它就是一个左值!那么既然指针常量是一个左值,为什么却不能给它赋值呢?是因为它受限于赋值表达式的一条规则:赋值表达式的左值不能含有限定词!
 
    为了防止指针指向的常量被修改,C标准对于指针间赋值有一个规定,就是左值必须包含右值的所有限定词。 这就限定了一个指向const对象的指针不能赋值给指向非const对象的指针,但反过来就允许。这个规定初看上去非常合理,但其效用其实只限于一级指针,二级指针间的赋值即使满足规定也不再安全,下面举个例子:
 
const int i=10;
const int **p1;
int *p2;
p1 = &p2;
*p1 = &i;
*p2 = 20;
 
现在你会发现,作为常量的i的值被修改了。i的值被修改的关键原因在*p1=&i;这一句,&i是一个指向常量的一级地址,如果没有二级指针p1,受限于上述规定,作为左值接受这个一级地址的指针就必须也是一个指向常量的一级指针,于是就不能进行下一步赋值20的操作。因此,正由于指向const对象的二级指针p1的出现,使得*p1也是一个指向const的指针,于是*p1=&i能够合法地运行,常量i的值被修改也就成了一个预想中的结果了。有鉴于此,某些编译器也会限定非const二级指针之间的赋值,规定上面的p1=&p2也是非法的。
 
    第七章介绍声明符的指针部分有一种形式:
 
* 类型限定符表opt 指针
 
这种形式产生了一种比较复杂的带const的指针,例如:
 
const int * const *** const ** const p;
 
这是一个会让人头晕目眩的表达式,声明符部分嵌套了九次,如何辨认谁是const,谁不是const呢?一旦明白了其中的原则,其实是非常简单的。第一和最后一个const大家都已经很熟悉的了。对于藏在一堆*号中的const,有一个非常简单的原则:const与左边最后一个声明说明符之间有多少个*号,那么就是多少级指针是const的。例如从右数起第二个const,它与int之间有4个*号,那么p的四级部分就是const的,下面的赋值表达式是非法的:
 
**p = (int *const***)10;
 
但下面的赋值是允许的:
 
***p=(int*const**)10;
 
从左边数起第二个const,它与int之间有1个*,那么p的一级部分是const的,也就是*****p = (int*const***const*)10;是非法的。

共13条 1/2 1 2 跳转至

回复

匿名不能发帖!请先 [ 登陆 注册 ]