C语言的那些事儿.doc_第1页
C语言的那些事儿.doc_第2页
C语言的那些事儿.doc_第3页
C语言的那些事儿.doc_第4页
C语言的那些事儿.doc_第5页
已阅读5页,还剩42页未读 继续免费阅读

下载本文档

版权说明:本文档由用户提供并上传,收益归属内容提供方,若内容存在侵权,请进行举报或认领

文档简介

C语言的那些事儿多维数组那回事儿2011年2月10日 由 edsionte 留言 前面几篇“那回事儿”的文章更强调一维组和指针之间的关系,本文关注的是多维数组,即“数组的数组”。多维数组我们可以将多维数组抽象的看作是具有某种类型的一维数组。当“某种类型”为基本的数据类型时,多维数组就退化成普通的一维数组;当“某种类型”仍然为数组时,那么就形成了多维数组。也就是说任何一个多维数组都可以分解成几个一维数组。下面通过示例程序来深入了解多维数组ma23的构成。view sourceprint?01#include 0203intmain()0405intma23;06int(*r)23;07int(*p)3;08int*t;0910/*代码段1*/11p = ma;12printf(sizeof(ma0)=%dn,sizeof(ma0);13printf(ma =%ptp =%pn,ma,p);14printf(p+1 =%pn,p+1);15/*代码段2*/16r = &ma;17printf(sizeof(ma)=%dn,sizeof(ma);18printf(&ma =%ptr =%pn,&ma,r);19printf(&ma+1 =%ptr+1=%pn,&ma+1,r+1);20/*代码段3*/21t = ma0;22printf(sizeof(ma00)=%dn,sizeof(ma00);23printf(ma0 =%ptt =%pn,ma0,t);24printf(ma0+1 =%ptt+1 =%pn,ma0+1,t+1);25return0;26由多维数组ma最左维的长度2可知,ma数组包含两个元素ma0和ma1。数组名ma在表达式中是数组ma首元素的首地址。在代码段1中将ma赋值给数组指针p,则p指向多维数组ma的首元素ma0,则p+1指向第二个元素ma1。其中p是一个数组指针,它指向一个长度为3的数组,则指针p每次移动的偏移量为12。可参考下图:在代码2中对ma取地址并将其赋值给指针r。r现在指向一个“第一维的大小为2,第二维的大小为3的数组”,则r+1将指向下一个这样的数组(尽管这样的数组并不存在)。由此也可得知r每次的偏移量为24。ma0和ma1都是一个长度为3的整型数组,现在以ma0为例进行说明。ma0中包含三个元素ma00,ma01和ma02。在代码段3中将ma0赋值给t,则t指向数组ma0的第一个元素a00,则t+1和t+2则依次指向第二个元素和第三个元素。对多维数组ma的结构有了一定了解后,现在再看上述程序的运行结果:view sourceprint?01edsionteedsionte-laptop:/code/expertC$ gcc array.c -o array02edsionteedsionte-laptop:/code/expertC$ ./array03sizeof(ma0)=1204ma =0xbfdfaa6c p=0xbfdfaa6c05p+1 =0xbfdfaa7806sizeof(ma)=2407&ma =0xbfdfaa6c r=0xbfdfaa6c08r+1 =0xbfdfaa8409sizeof(ma00)=410ma0=0xbfdfaa6c t=0xbfdfaa6c11t+1 =0xbfdfaa70注意在结果中,p,r和t的值均相同,但是所指向的数据却不同。更具体的说,这三个指针每次移动时的偏移量不同。多维数组的初始化数组的初始化只能在对数组进行声明(具体为定义型声明)时进行。一维数组的初始化很简单,只要将所有初始值放在一个大括号中即可。如果声明数组时未指定数组的长度,则编译器会根据初始值的个数来确定数组的长度。view sourceprint?01#include 0203intmain()0405intm = 1,2,3;06intn = 1,2,3,;0708printf(length(m)=%dn,sizeof(m)/sizeof(m0);09printf(length(n)=%dn,sizeof(n)/sizeof(n0);10return0;111213/* 编译并运行 */14edsionteedsionte-laptop:/code/expertC$ gcc init_array.c -o init_array15edsionteedsionte-laptop:/code/expertC$ ./init_array16length(m)=317length(n)=3注意,在最后一个初始值后面可以继续加一个逗号也可以省略,这并不影响数组的长度。对于多维数组而言,通常使用嵌套的大括号进行多维数组的初始化。由于多维的数组其实是有若干个一维数组构成的,则每个大括号都代表一个一维数组。对于多维数组而言只能省略最左边 下标的长度。view sourceprint?01#include 0203intmain()0405intb3 = 1,2,1,1;06intc3 = 1,2,1,1,2,3,;0708printf(length(b)=%dn,sizeof(b)/sizeof(b0);09printf(length(c)=%dn,sizeof(c)/sizeof(c0);10return0;111213/* 编译并运行 */14edsionteedsionte-laptop:/code/expertC$ gcc init_array.c -o init_array15edsionteedsionte-laptop:/code/expertC$ ./init_array16length(b)=217length(c)=2可以看到,不使用大括号也可以对多维数组进行初始化,只不过代码可读性较差。它总是迷惑你!一旦涉及到多维数组,总有些让你迷惑的地方。比如:view sourceprint?1charma232=21,2,2,3,3,4,33,5,4,5,3,34;56sizeof(ma0,1,1)=?对于上面的代码,我们最后的迷惑点都可能落在ma0,1,1上。难道多维数组可以这样使用吗?如果ma0,1,1和ma011等价,那么sizeof(ma0,1,1)的值就是1。很可惜这样的猜测是不正确的,正确答案为6。再比如下面的代码:view sourceprint?1charma32 = 2(1,2),(3,4),(5,3)3;45ma00=?上述代码是为数组ma进行初始化,那么ma00的值是多少?恐怕很多人都会认为是1。不过正确答案是2。这两个问题都涉及到了逗号表达式。如果你对逗号表达式有基本的了解,那么也就没有上述那种莫名其妙的迷惑了。根据逗号表达式的运算,对于举例1中的ma0,1,1实际上等价于ma1;对于举例2中的初始化其实等价为char ma32 = 2,4,3。参考:C专家编程 人民邮电出版社;(美)林登(LinDen.P.V.D) 著,徐波 译;指针和数组的可交换性2011年2月7日指针和数组是不相同的,但“很多时候”我们总认为指针和数组等价的。不可否认,这两者在某种情况下是可以相互替换的,但并不能就因此而认为在所有情况下都适合。指针和数组不是一回事儿系列文章将逐步深入分析指针和数组的不同之处,并解释什么时候指数组等价于指针。本文属于指针和数组不是一回事儿系列文章之三。虽然前面两篇文章已经说明了数组和指针的不同,但不可否认的是,指针和数组某些可相互交换的用法仍然令人混淆。本文将给出指针和数组可交换的情景,并且分析可交换的原因。“指针和数组可以交换!”说出这句话并不是毫无根据的,因为在下面的两个举例中使用数组形式和指针形式都可以达到相同的结果。举例1:view sourceprint?01#include 0203intmain()0405char*p =edsionte;06charstr =edsionte;0708printf(p1=%c *(p+1)=%cn,p1,*(p+1);09printf(str1=%c *(str+1)=%cn,str1,*(str+1);1011return0;121314/* 编译并运行程序 */15edsionteedsionte-laptop:/code/expertC$ gcc tmp.c -o tmp16edsionteedsionte-laptop:/code/expertC$ ./tmp17p1=d *(p+1)=d18str1=d *(str+1)=d在举例1中,指针p指向一个匿名的字符串“edsionte”,这个匿名字符串的占用的内存空间为9个字节;与p指向一个匿名字符串不同,数组str内存储着字符串“edsionte”,占用了9个字节的空间。现在分别要访问d,则方法如下。对于指针p,分别可以通过指针形式*(p+1)和数组形式p1来访问其所指的数据;对于数组str,分别可以通过指针形式*(str+1)和数组形式str1来访问数组内的元素。我们已经知道指针和数组在内存构造和访问方式上都不同,但为什么它们都分别可以通过指针的方式和数组的方式进行访问?举例2:view sourceprint?01#include 0203voidgetStr_pointer(char*str)0405printf(%sn,str);06printf(getStr_pointer(): sizeof(str)=%dn,sizeof(str);070809voidgetStr_array(charstr100)1011printf(%sn,str);12printf(getStr_array(): sizeof(str)=%dn,sizeof(str);131415intmain()1617charstr =I am edsionte!;1819getStr_pointer(str);20getStr_array(str);21printf(main(): sizeof(str)=%dn,sizeof(str);222324/* 编译并运行程序 */25edsionteedsionte-laptop:/code/expertC$ gcc tmp2.c -o tmp226edsionteedsionte-laptop:/code/expertC$ ./tmp227I am edsionte!28getStr_pointer():sizeof(str)=429I am edsionte!30getStr_array():sizeof(str)=431main():sizeof(str)=15在举例2中,getStr_pointer函数和getStr_array函数的功能都是显示一条字符串。但不同的是,前者传入的参数是一个指针,后者传入的参数是一个数组。在主函数中分别调用这两个函数,传入的参数都是数组str。既然数组和指针不同,但为什么作为函数的形参,char str 和char *str相同?上述举例所引出的这两个问题正是本文讨论的重点,它们分别对应着“指针和数组是相同”的两种情况。下面将分别进行讨论。1.表达式中的数组名就是指针表达式中的数组名其实就是数组首元素的首地址。对于编译器而言,ai其实就是*(a+i)的形式,因此以数组形式访问数组元素总是可以写成“数组首元素首地址加上偏移量”的形式。取下标符号 其实可以看成一种运算规则,即指向T类型的指针和一个整数相加,最终产生的结果类型为T。这里的指针就为数组首元素首地址,而整数即为数组的偏移量。这里必须说明一下偏移量,它是指针每次移动的步长。对于数组而言,偏移量即数组元素的大小;对于指针而言,它的偏移量即为指针所指类型的大小。在对指针进行移动时,编译器负责计算每次指针移动的步长。因此,stri和*(str+i)两种形式其实是等价的。因为编译器总是将数组形式的访问自动转换成指针形式的访问。上面的分析都是针对数组而言,其实对指针以数组和指针形式访问的原理也是如此。只不过此时的访问是对指针所指向数据的访问。结合数组和指针访问方式的不同,下面对举例1的代码做详细分析:1.1.以指针的形式和以数组的形式访问数组从符号表中得到符号str的地址即为数组首元素的首地址。以数组的形式:str1。从符号表中得到str符号的地址,即数组首元素的首地址;编译器将数组形式转化为*(str+1),在首元素首地址上加一个偏移量得到新地址;从这个新地址中读取数据,即为d;以指针的形式:*(str+1)。从符号表中得到str的地址,即数组首元素的首地址;在此地址上加一个偏移量得到新地址;从这个新地址中读取数据,即为d;1.2.以指针的形式和以数组的形式访问指针不管以何种方式访问,我们应该清楚p始终是一个指针。从编译器符号表中得到符号p的地址为指针p的地址。以指针的形式:*(p+1)。首先从符号表中得到p的地址;从该地址中得到指针p;对指针p加上1个偏移量得到新地址;从这个新地址中读取数据,即为d;以数组的形式:p1。首先从符号表中得到p的地址;从该地址中得到指针p;编译器将数组形式转化成*(p+1),对p加一个偏移量得到新地址;从这个新地址中读取新数据,即为d;分析至此,你应该了解到以数组形式和以指针形式访问只是写法上的不同而已,其本质对内存的访问过程是一样的。2.作为函数参数的数组名等同于指针当作为函数形参时,编译器会将数组改成指向数组首元素的指针。此时的数组就等价于指针。之所以将传递给函数的数组形参转化为指针是处于效率的考虑。在C语言中,所有非数组的实参数据都是以传值形式传递给函数的,即将实参的一份拷贝传递给调用函数中的形参,调用函数对这份拷贝(也就是形参)的修改不影响实参本身的值。如果按照这样的道理,传递数组时就必须拷贝整个数组空间,这样必然会产生很大的开销。并且,大部分时候并不会访问到数组中所有的元素而只是其中的几个。考虑到上述的原因,数组作为实参传递给调用函数时,只需将数组名传递给函数即可;而形参会被编译器该成指针的形式。因此,作为形参的数组既可以写成数组也可以写成指针。现在再回到举例2中的代码,对于形参中的char str和char *str也就感到不再奇怪了。事实上,即便将形参写成char str或char str100,编译器仍然会将它们改成char *str的形式。既然任何数组作为形参时候都等价于一个指针,那么在函数内对“数组”的一切操作都等价于对指针的操作。验证这一点的很好例证就是举例2中对数组str求长度。在主函数中,sizeof(str)的值为15,这个结果毫无争议,它就是数组str的长度。而在getStr_pointer()和getStr_array()中,sizeof(str)的值都为4,也就验证了作为形参的数组str在调用函数中就是一个指针!在上述情况1中,虽然表达式中数组名也被认为是指针,但是数组仍然是数组(main函数中sizeof的结果就是很好的验证),而此部分数组就是指针。这也是数组等价于指针的唯一情况。换句话说,虽然在将数组作为形参的函数中,你可以继续以数组的形式使用这个参数,但实际上你跟不可能找到数组的踪影!总结关于指针和数组之间的异同需要反复的思考和总结,才能搞清关系。下面对指针和数组之间的可交换性再作义简单的总结。1.在表达式中以ai这样的形式对数组进行访问时,编译器总将其解释为*(a+i)的形式;2.在数组作为函数的形参时,编译器将数组改写成指针,这个指针即为数组首元素的首地址。这也是数组等价指针的唯一情形;3.由于2的原因,一个数组作为函数的形参时,既可以将数组定义成数组,也可以将数组定义成指针;4.指针和数组永远是两码事,因此在不同文件中的声明和定义必须匹配,但却始终都能写成指针的形式和数组的形式(这完全是写法的不同)。参考:C专家编程 人民邮电出版社;(美)林登(LinDen.P.V.D) 著,徐波 译;C语言深度解剖北京航空航天大学出版社;陈正冲 著;没有评论 发表在C语言的那些事儿Tags: C编程 C语言的那些事儿 指针 数组指针和数组的访问方式2011年2月5日指针和数组是不相同的,但“很多时候”我们总认为指针和数组等价的。不可否认,这两者在某种情况下是可以相互替换的,但并不能就因此而认为在所有情况下都适合。指针和数组不是一回事儿系列文章将逐步深入分析指针和数组的不同之处,并解释什么时候指数组等价于指针。本文属于指针和数组不是一回事儿系列文章之二。前文从内存结构的角度说明了指针和数组的不同,本文将以访问方式的角度再次说明指针和数组的不同。先看下面的代码:view sourceprint?1charstr =edsionte;2char*p =edsionte;当编译完程序后,程序中的标示符都有一个地址,所有标示符的地址形成一个符号表。数组的访问方式以数组str为例,如果要访问str1,即数组str的第二个元素,则它的访问步骤如下:1.从编译器的符号表中得到str的地址,比如0x0000FE00。这个地址即为数组str首元素的首地址;2.在这个地址上加一个偏移量得到新的地址0x0000FE01;3.从这个新地址中读取数据;通过下图可以加深对数组访问方式的理解:指针的访问方式以上述代码中的指针p为例,如果要访问*(p+1),即指针p所指向的匿名字符串的第二个字符,则它的访问步骤如下:1.从编译器符号表中得到指针p的地址,比如0x0000EE20。这个地址即为&p,也就是指向指针p的指针;2.从地址0x0000EE20中读取它内容即为指针p,比如0x00F0A000;3.在0x00F0A000的基础上加一个偏移量,得到新地址0x00F0A001;4.读取0x00F0A001中的内容,即为指针p所指的数据;通过下图可以近一步理解指针的访问方式:通过分析得知,在符号表中得到的是指针的地址而不是我们所要访问的指针;而在符号表中可以直接得到数组首元素的首地址。因此,访问指针时必须先通过符号表中指针的地址得到所要访问的指针,再接着进行指针所指内容的访问;而数组则直接可以通过符号表中的地址进行元素访问。也就是说指针的访问比数组的访问多了一次对内存地址的读取。一不小心就引发的错误现在看下面的两段代码:view sourceprint?01/* 代码段1 */02file1:03charstr =edsionte;04file2:05externchar*str;06/* 代码段2 */07file1:08char*p =edsionte;09file2:10externcharstr;对于代码段1,在文件1中定义了数组str,而在文件2中将str声明为指针;对于代码段2,在文件1中定义了指针p,而在文件2中将p声明为数组。这里的声明指的是外部引用型声明,定义指的是定义型声明。不管是上述那种情形,编译的时候都会出现错误。从上述对指针和数组访问方式的分析中可以得知,一个标示符被声明成指针还是数组对其访问方式影响巨大。下面我们对这两种错误作详细分析。定义为数组,声明为指针在文件2中,既然str被声明成指针,那么就应当按照指针的方式进行访问。首先从符号表中得到指针str的地址;从该地址中读取4个字节的数据即为指针str;接下来根据指针str访问其所指向的数据。这个过程好像很顺利,不过对于文件1中的数组str,其访问过程又是怎样的?文件1和文件2的访问结果是否一致?下图可帮助你理解。从上图可以看到,str在文件2中被声明成指针,那么就符号表中str的地址0x0000FE00会被当作指针str的地址。根据指针的访问方式,必须从这个地址中取出指针p。虽然以0x0000FE00为首的四个字节中存储的是“edsi”,但是它们一律会被当成地址,按十六进制表示即为065647379。即便可以访问到这个地址,但是从这个地址中按照char型取出的数据并不是我们想要的。由于编译器对每个文件进行单独编译,文件2并不知道str在文件1中被定义成什么类型。str在文件1中被定义成数组,那么就应该按照数组的方式访问数据;str在文件2中被声明成指针,那么就应该按照数组的方式访问数据。因此,在两个不同的文件中分别将str定义成数组而声明成指针会出现对str访问不一致的现象,所以编译器会产生错误。定义为指针,声明为数组此时,对于这种情况的理解也就简单多了。由于在文件而中p被声明成数组,因此就应该按照数组的方式对其进行访问。编译器会将原本指针str的地址当作str数组首元素的首地址,再对其加相应偏移量进行访问。这显然也是不合理的,因此编译器产生错误。具体可参见下图:对上述的错误进行分析后,我们应该清楚将一个标示符声明成数组,编译器就会按照数组的访问方式去访问它;指针也是如此。因此,应该在多个文件中保持声明和定义相匹配。指针和数组的其他区别指针和数组除了在内存构造和访问方式上不同外,还有一些其他的区别。1.指针通常用于指向一个动态的数据结构,而数组则用于存储固定大小和数据类型相同的数据;2.指针所指向的数据通过malloc()分配,并且需要free()释放;而数组本身的内存空间则是隐士分配和释放,也就是在定义数组的时候进行;3.指针所指向的数据通常是匿名的,而数组名则是数组所占内存空间的名字;在本系列的最后一篇文章中,我们将分析指针和数组易被混淆的根源也可将其称为指针和数组的可交换性。参考:C专家编程 人民邮电出版社;(美)林登(LinDen.P.V.D) 著,徐波 译;C语言深度解剖北京航空航天大学出版社;陈正冲 著;没有评论 发表在C语言的那些事儿Tags: C编程 C语言的那些事儿 指针 数组指针和数组的内存构造2011年2月1日指针和数组是不相同的,但“很多时候”我们总认为指针和数组等价的。不可否认,这两者在某种情况下是可以相互替换的,但并不能就因此而认为在所有情况下都适合。指针和数组不是一回事儿系列文章将逐步深入分析指针和数组的不同之处,并解释什么时候指数组等价于指针。本文属于指针和数组不是一回事儿系列文章之一。指针和数组的本质是什么,这是本文讨论的重点。从内存结构的角度来说,两者是截然不同的两个概念。数组的声明与定义关于C语言中的声明,声明那回事儿一文中已详细叙述。这里再具体针对数组的定义和声明做以分析。以下面的声明代码为例:view sourceprint?1charstr10;2externcharstr;第一条声明语句定义了一个char型的数组,并为其分配了100字节大小的内存空间。而第二条声明语句则是为了告诉编译器这个数组的类型以及大小。由于在外部引用型声明中并不会数组分配内存空间,因此这种声明并不需要指定数组的大小。对于多维数组也并不需要指定第一维的大小。指针的内存布局指针本质上就是一个内存地址,为了方便使用这个内存地址将它和一个标示符绑定在一起。比如:view sourceprint?1inti = 10;2int*p = &i;/* 假设变量i的内存地址为0x000F3E00 */上述语句将地址0x000F3E00和p绑定在一起,并且一旦绑定就不能再修改,p此时也被称为指针变量,简称指针。“指针变量“中的“变量”并不是说明p可以再和其他地址绑定,而是强调与p绑定的这个地址中的内容可变,即i的值可以变化。既然指针p是一个内存地址,那么在32位的系统中指针p所占的内存大小就始终为4字节。虽然整型变量i也占4字节,但是这两个同大小的内存空间却有着本质区别。指针p只能存放内存地址,并且这个地址只能是整型数据的首地址。即使在p内存放了其他数据,也会一律被当作内存地址来处理。通过下图可以近一步了解指针和其所指数据的关系:从图中可得知,不管指针所指数据占多大的内存空间,指针本身只占用4字节的大小。由于指针p本身占用4字节的内存空间,因此这部分内存空间也必然会有首地址。通过&p操作就可以得到指针p的首地址,也就是存储指针p的内存空间的首地址。从上图中可以看到指针p中即为整型变量i的首地址,因此我们也称p是一个指向整型变量i的指针。数组的内存布局数组是一块连续的内存空间,这块内存空间的名称即为数组名。比如:view sourceprint?1inta100;当定义了一个具体的数组a时,编译器就根据数据类型和大小为其分配100*sizeof(int)大小的内存空间,并将这块连续的内存空间命名为a。虽然我们可以通过ai这种方式来访问元素i,但这并不代表ai就是这个元素的名称。因此每个数组元素实际上是没有名字的,编译器只为这块内存提供了唯一的名称a。同时数组名a也代表数组首元素的首地址。数组的内存结构如下:通过对指针和数组内存布局的分析,我们可以得知这两者完全是不相同的。指针不管指向什么数据,它本身的大小就是4个字节(32位系统);而数组则是一块连续的内存空间。在下文中,将会从访问方式的角度分析指针和数组的不同。参考:C专家编程 人民邮电出版社;(美)林登(LinDen.P.V.D) 著,徐波 译;C语言深度解剖北京航空航天大学出版社;陈正冲 著;typedef那回事儿2011年1月30日typedef是一种特殊的声明方式,不过它与普通声明(详见这里)的含义取大不相同。普通声明的主角是“变量”,它或是创建一个新变量或是对外文件变量使用前的声明;而typedef声明的主角则是“类型”,通过这个声明对一种数据类型引入新的名字。从引入新名字这个角度来说,typedef声明又和宏定义有些相似:用新名字代替已有的名字。不过,在本文接下来的叙述中你会看到这两者之间的区别。typedef是特殊的正如本文一开始所说的那样,typedef是特殊的声明。我们最常见以及常用的方式如下:view sourceprint?1/* 代码段1 */2structstuinfo34charid20;5charname20;6intage;7;8typedefstructstuinfo stu;/* 语句1 */通过typedef声明为stuinfo结构体引入了一个新的名字stu。现在stuinfo结构和stu属于同一种数据类型,只不过两者在声明一个变量时使用的名字不同:view sourceprint?1/* 代码段2 */2stu mystu1;3structstuinfo mystu2;/* 语句2 */通过上述两个代码段,可以再一次的理解typedef声明和普通声明的区别。代码段1通过typedef声明为stuinfo引入了一个新的名字stu;而代码段2则通过同一种数据类型的不同名称分别声明了两个同类型的变量。注意到语句1和语句2,除了语句1在声明前多了typedef关键字外,两者在形式上几乎一样,因此都可以通过上文所述的声明规则进行阅读。正是由于typedef这个关键字,使得这两种声明的含义有着巨大差异。其实,像上面的举例那样通过typedef声明而省去一个struct并没有多大的意义。使用typedef声明的最大优点是可以简洁的表达一个指针。比如ANSI C中的signal(),它的定义如下:view sourceprint?1void( *signal(intsignum,void(*handler)(int) ) (int);考验你的时刻到了!你是否能快速说出这个声明的含义?这个复杂的语句声明了signal函数,这个函数有两个参数signum和handler;signum参数是一个整型变量。handler是一个函数指针,指向一个拥有整型参数并且返回空值的函数;signal函数的返回值是一个函数指针,该指针同样指向一个拥有整型参数并且返回空值的函数。对于这个复杂声明的解读的确很令人费劲。但是经过typedef的改进,它的阅读过程就简化了很多:view sourceprint?1typedefvoid(*sighandler_t)(int);2sighandler_tsignal(intsignum, sighandler_t handler);通过typedef的声明,使得sighandler_t是这样一种类型:它是一个函数指针,该函数拥有一个整型参数并且返回空值。第二条语句则声明了函数signal,它拥有两个参数signum和handler。并且这个函数的返回值和参数handler都是sighandler_t类型的。虽然这样的声明在形式上简洁许多,不过和普通声明一样,此时阅读声明时仍然要记住声明符号的优先级规则。这种困扰在C语言中是难以避免的。typedef int x和#define x int是不一样的typedef和宏定义看似都是文本替换,但其实质不同。typedef表面上是对已有数据类型引入新名称,实则是对数据类型的严格封装。这种封装体现在下述两个方面。首先,经过宏定义后的类型名可以进行再次扩展,但是经过typedef引入的类型名则不能进行扩充。比如:view sourceprint?1#define myint1 int2unsigned myint1 x;/* 正确 */34typedefintmyint2;5unsigned myint2 x;/* 语法错误! */由于typedef是一种严格的数据封装,它只引入了myint2类型而没有引入unsigned myint2类型。也就是说,通过typedef的声明,编译器只能识别myint2类型。而unsigned myint2既不是基本类型也不是经过typedef声明过的类型,编译器就无法识别。其次,在连续的几个变量声明中,使用typedef定义的类型能够保证所有变量均为相同类型,而用宏定义的变量则无法保证统一性。比如:view sourceprint?1#define myint1 int *2myint1 x,y;/* 经过宏替换后为: int *x,y; */34typedefint* myint2;5myint2 x,y;由于宏定义只是直接的文本替换,因此只能保证x是整型的指针变量而y为整型变量。而typedef定义过的类型myint2则是对int *的完全封装,所以x和y均为整型的指针变量。C语言中的名字空间在说明名字空间之前,请先阅读下面的代码:view sourceprint?01/*02*Author: edsionte03*Email: 04*Time: 2011/02/0305*/0607#include 08#include 0910structid1112intid;13id;1415typedefstructname1617charname20;18name;1920structname name1;21name name2;2223intmain()2425id.id = 1;26strcpy(,hello,);27strcpy(,edsionte!);28printf(id.id = %d, %s%sn,id.id,,);29return0;3031/* 运行结果 */32edsionteedsionte-laptop:/code/expertC$ gcc tpdef.c -o tpdef33edsionteedsionte-laptop:/code/expertC$ ./tpdef34id.id = 1, hello,edsionte!你可能已经发现在上述代码中出现了多个id和name,并且这样的代码可以成功的编译。这些相同的名字标签为何可以同时出现?每个标签代表什么含义?这些问题将是下面分析的重点。以上述代码中10到13行的代码为例,这条语句中包含三个id标签。它们分别对应C语言中三种常见的名字空间:结构标签:这种标签用于结构体、联合体和枚举类型;struct后的id即为此类型的名字空间;成员名:每个结构体或联合体内部都与属于自己的名字空间;struct内部的成员id即为此类型;标签名:声明中的标示符;比如最后一个id,他是struct id类型的变量;由于这三种标签所处的名字空间不同,因此它们可以同时存在。但是在同一个名字空间中不能出现多个同名的标签。常见的例子就是一个结构体内不可能出现同名的变量。根据上面对名字空间的划分,15到18行的代码的解释为:struct后的name属于结构体标签;结构体内部的name属于成员名;而最后一个name属于声明的标示符;整条语句的含义是通过typedef声明将name结构体重命名为name。通过上面对typedef的分析,你应该对于struct name和name均可以声明一个变量不再陌生。此处我们用名字空间的来理解他们的区别,20句中的name属于结构标签,21句中的name属于一种类型的名称。上述同名的情况在日常的代码中实属罕见,这里只是为了说明名字空间而特别的举例。一般为了提高代码的可阅读性,最好对容易产生混淆的标签加上特别标记。比如VFS中inode和dentry结构体,两者内部均有flag一字段。尽管不同的结构体内有各自的名字空间,但是实际命名时仍然采用i_flag和d_flag。有了上面的基础,本部分一开始所举例的代码也就可以轻松阅读了。参考:C专家编程 人民邮电出版社;(美)林登(LinDen.P.V.D) 著,徐波 译;没有评论 发表在C语言的那些事儿Tags: C编程 C语言的那些事儿 typedef声明那回事儿2011年1月29日C语言中的变量声明是让程序员比较苦恼的一件事,因为过多的优先级规则使得阅读声明并不能像自然方式那样从左至右的阅读。比如下面这个声明:int (*(*fun)()();对于这个声明,你能准确说出它的含义吗?这个声明涉及到本文的两大主题:什么是声明和声明的阅读规则。本文的最后将给出这个声明的准确含义。声明和定义在C语言中,提到声明就不得不提到定义。这里说的声明既包含变量的声明又包含函数的声明。对于函数的定义和声明易于理解,比如:view sourceprint?1intmyfun2();23intmyfun2()45printf(I am myfun2n);6return0;7所谓函数的定义就是对该函数进行具体的功能实现,而函数声明则是对该函数返回值和参数类型的说明,使得其他函数感知到这个函数的存在,以便在需要时直接调用该函数。变量的声明具体分为两种情况:定义型声明和外部引用型声明。定义型声明其实就等价于定义。C语言的定义是指为变量分配内存空间,并在需要时为其赋一个初值。定义型声明用于创建一个新的变量,它在定义这个变量的同时也声明了这个变量。比如下述代码就声明(定义)了一个变量num,并将0作为其初值。view sourceprint?1intnum = 0;使用外部引用型声明是由于要在当前程序中使用定义在其他文件中的变量。也就是说这个变量是已存在的,因此这种声明并不包含变量的定义。比如:view sourceprint?1file1:2inta = 100;3file2:4externinta;C语言中的变量只能有一个定义,但是它却可以有多个extern的声明。因为extern声明并不分配内存空间,只是告诉引用这个变量的函数:这个变量已经定义过了,你可以直接的使用。声明的组成声明确定了变量的基本类型和相应的初值(如果需要的话)。一个完整的声明包括三部分:一个类型说明符,一个或多个声明器(declarator)和一个分号。类型说明符用于描述所要声明变量的类型;分号说明了声明的结束;声明器是标示符以及和它组合在一起的指针符、函数括号和数组下标等,有时候也将初始化内容放在声明器中。多个声明器用逗号隔开。关于声明和声明器的关系可参考下图:该图所示的语句声明了四个变量,其基本类型都是整型。由于四个变量分别对应着不同的声明器,则最终的变量类型就有所不同。整型变量a的声明器即为标示符a;第二个声明器为*b=NULL,它声明b是一个指针变量,其初值为NULL。由于类型说明符为int,则说明了b是一个指向整型的指针;第三个声明器为(*c)20,它声明c是一个数组指针,该指针指向拥有20个元素的数组。由类型说明符得知该数组的元素都是整型的;最后一个声明器为*j20,其说明j是一个指针数组,该数组有20个元素,每个元素都是指向整型变量的指针。优先级规则了解了声明的组成后,到了该给出声明优先级规则的时候了。C语言中声明的优先级规则如下:1.声明从最左的标示符开始,然后按照下面的优先级规则依次读取;2.具体的优先级为:2.1 被括号括起来的那部分;2.2 后缀部分;如果后缀为( ),表明这是一个函数;如果后缀为 ,表明这是一个数组;2.3 前缀部分;*表示指向的指针;3.如果const或volatile关键字后紧邻基本类型说明符,则它作用于该类型的变量;否则,const和volatile作用于仅靠它左边的星号,即作用于指针变量;上述规则需要参考实际的声明来慢慢理解。下面通过两个简单的声明举例来说明上述优先级规则:view sourceprint?1constint(*p)();2constint*p();声明1:首先找到标示符p,由于声明1中的p和*被包括起来,根据规则2.1得知p是一个指针。该指针指向什么类型是接下来读声明的关注对象;标示符的后缀部分是( ),根据规则2.2得知该指针指向一个函数。这个函数返回值是什么类型是接下来读声明的关注对象;读完上述声明符号后,剩下了const int,根据规则3得知这个函数的返回值是一个只读型的整数;综合上面的几部分可得知该声明的含义:p是一个指针,它指向一个函数。这个函数没有参数,返回一个只读型的整数。声明2:首先找到标示符p,它与*没有被包括在一起,因此可以排除p是一个指针;由于标示符的后缀部分是( ),根据规则2.2得知p是一个函数。该函数返回什么类型是接下来读声明的关注对象;由于p标示符前有*,则说明该函数的返回值是一个指针。至于该指针指向什么样的数据是接下来对声明的关注对象;通过队则3可得知该指针指向一个只读型的整型变量;通过上述的分步分析可得知该声明的含义:p是一个函数,它没有参数,返回值是一个指针。该指针指向一个只读型的整型变量。通过上述的优先级规则,可以轻松的阅读任何一个声明。从上述两个举例中也可总结出阅读声明的大致方法,首先判别该声明是声明一个函数还是一个变量;再根据具体的声明类型切换接下来读声明的关注对象;最后未读的基本类型就是最后一次关注对象的类型。上述的大致方法可详细总结如下:1.找到最左边的标示符,表示已被阅读;2.若已阅读符号右方紧邻,则从左至右阅读到与

温馨提示

  • 1. 本站所有资源如无特殊说明,都需要本地电脑安装OFFICE2007和PDF阅读器。图纸软件为CAD,CAXA,PROE,UG,SolidWorks等.压缩文件请下载最新的WinRAR软件解压。
  • 2. 本站的文档不包含任何第三方提供的附件图纸等,如果需要附件,请联系上传者。文件的所有权益归上传用户所有。
  • 3. 本站RAR压缩包中若带图纸,网页内容里面会有图纸预览,若没有图纸预览就没有图纸。
  • 4. 未经权益所有人同意不得将文件中的内容挪作商业或盈利用途。
  • 5. 人人文库网仅提供信息存储空间,仅对用户上传内容的表现方式做保护处理,对用户上传分享的文档内容本身不做任何修改或编辑,并不能对任何下载内容负责。
  • 6. 下载文件中如有侵权或不适当内容,请与我们联系,我们立即纠正。
  • 7. 本站不保证下载资源的准确性、安全性和完整性, 同时也不承担用户因使用这些下载资源对自己和他人造成任何形式的伤害或损失。

评论

0/150

提交评论