水滴石穿C语言之C语言的底层操作_第1页
水滴石穿C语言之C语言的底层操作_第2页
水滴石穿C语言之C语言的底层操作_第3页
水滴石穿C语言之C语言的底层操作_第4页
水滴石穿C语言之C语言的底层操作_第5页
已阅读5页,还剩111页未读 继续免费阅读

下载本文档

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

文档简介

1、水滴石穿C语言之C语言的底层操作概述 HYPERLINK /key/1574/1574.html t _blank C语言的内存模型基本上对应了现在von Neumann(冯诺伊曼)计算机的实际存储模型,很好的达到了对机器的 HYPERLINK /key/4883/14883.html t _blank 映射,这是C/C+适合做底层开发的主要原因,另外,C语言适合做底层开发还有另外一个原因,那就是C语言对底层操作做了很多的的支持,提供了很多比较底层的功能。下面结合问题分别进行阐述。问题:移位操作在运用移位操作符时,有两个问题必须要清楚:(1)、在右移操作中,腾空位是填 0 还是符号位;(2)、

2、什么数可以作移位的位数。答案与分析:和移位的位数 左移: 变量名移位的位数 经过移位后, 一端的位被挤掉,而另一端空出的位以0 填补,在C语言中的移位不是循环移动的。(1) 第一个问题的答案很简单,但要根据不同的情况而定。如果被移位的是无符号数,则填 0 。如果是有符号数,那么可能填 0 或符号位。如果你想解决右移操作中腾空位的填充问题,就把变量声明为无符号型,这样腾空位会被置 0。(2) 第二个问题的答案也很简单:如果移动 n 位,那么移位的位数要不小于 0 ,并且一定要小于 n 。这样就不会在一次操作中把所有数据都移走。比如,如果整型数据占 32 位,n 是一整型数据,则 n 31 和 n

3、 0 都合法,而 n 32 和 n 1 不可能为 0 。 问题:位段结构struct RPR_ATD_TLV_HEADERULONG res1:6;ULONG HYPERLINK /key/4018/19018.html t _blank type:10;ULONG res1:6;ULONG length:10; ;位段结构是一种特殊的结构, 在需按位访问一个字节或字的多个位时, 位结构比按位运算符更加方便。 位结构定义的一般形式为: struct位结构名 数据类型 变量名: 整型常数; 数据类型 变量名: 整型常数; 位结构变量;其中: 整型常数必须是非负的整数, 范围是015, 表示二进制

4、位的个数, 即表示有多少位。变量名是选择项, 可以不命名, 这样规定是为了排列需要。 例如: 下面定义了一个位结构。struct unsigned incon: 8; /*incon占用低字节的07共8位*/ unsigned txcolor: 4;/*txcolor占用高字节的03位共4位*/ unsigned bgcolor: 3;/*bgcolor占用高字节的46位共3位*/ unsigned blink: 1; /*blink占用高字节的第7位*/ ch;位结构成员的访问与结构成员的访问相同。 例如: 访问上例位结构中的bgcolor成员可写成: ch.bgcolor位结构成员可以与其

5、它结构成员一起使用。 按位访问与设置,方便&节省例如:struct info char name8; int age; struct addr address; float pay; unsigned state: 1; unsigned pay: 1; workers;上例的结构定义了关于一个工从的信息。其中有两个位结构成员, 每个位结构成员只有一位, 因此只占一个字节但保存了两个信息, 该字节中第一位表示工人的状态, 第二位表示工资是否已发放。由此可见使用位结构可以节省存贮空间。 注意不要超过值限制问题:字节对齐让偶们先来看下面这个结构体:struct stu1 int a; char b

6、; 来看看sizeof(stu)的结果为多少? 怎么是8啊?你先别急,再来看下一个例子:struct stu2 char b; int a; 这个sizeof(stu2)是多少?怎么还是8啊?现在创建一个结构体变量stu2 s2 a , 0 x12345678h; stu1 s1 0 x12345678, a 运行DEGUG,怎么样发现了什么?在第一个结构体中char b的后面内存有三个字节是添了数据的.也就是这样 78 56 34 12 61 cc cc cc 而在第二个结构体中CHAR B的后面内存中也添加了数据.61 cc cc cc 78 56 34 12这又是怎么回事呢?需要字节对齐

7、当然有设计者的考虑了,原来这样有助于加快计算机的存取速度,否则就得多花指令周期了。所以,编译器通常都会对结构体进行处理,让宽度为2的基本数据类型(short等)都位于能被2整除的地址上,让宽度为4的基本数据类型(int等)都位于能被4整除的地址上。正是因为如此两个数中间就可能需要加入填充字节,所以结构体占的内存空间就增长了。其实字节对齐的细节和具体编译器实现相关,但一般而言,满足三个准则:结构体变量的首地址能够被其最宽基本类型成员的大小所整除;2) 结构体每个成员相对于结构体首地址的偏移量都是成员大小的整数倍,如有需要编译器会在成员之间加上填充字节;例如上面第二个结构体变量的地址空间。 3)

8、结构体的总大小为结构体最宽基本类型成员大小的整数倍,如有需要编译器会在最末一个成员之后加上填充字节。例如上面第一个结构体变量。(哎呀!知道!真多嘴!)现在就可以解释上面的问题了,第一个结构体变量中成员变量最宽为4(SIZEOF(INT) = 4),所以S1变量首地址必须能被整除。(不信你试试!)S1的大小也应该为4的整数倍。但是现在s1中有 4 + 1 的空间,所以为了满足第三个条件就在char b的后面在加上三个字节的空间以凑够8个字节空间。第二个结构体变量S2中 成员变量最大宽度为4,而且按照以前的理解int a 的地址和s2的地址相差5个字节,但是为了满足第而个条件(相差的距离-偏移地址

9、必须是4的整数倍)所以在char b的后面添加了三个字节的空间以保证int a的偏移地址是4的整数倍即为4。至于涉及到结构体嵌套的问题,你也可以用上述方法总结的,只不过你把被嵌套的结构体在原地展开就行了,不过在计算偏移地址的时候被嵌套的结构体是不能原地展开的必须当作整体。嘿嘿!偶申明一点,上述三条建议不是偶说的,是做编译器的工程师总结出来的,偶只是借用而已。我在使用VC编程的过程中,有一次调用DLL中定义的结构时,发觉结构都乱掉了,完全不能读取正确的值,后来发现这是因为DLL和调用程序使用的字节对齐选项不同,那么我想问一下,字节对齐究竟是怎么一回事?答案与分析:为了能使CPU对变量进行高效快速

10、的访问,变量的起始地址应该具有某些特性,即所谓的“对齐”。例如对于4字节的int类型变量,其起始地址应位于4字节边界上,即起始地址能够被4整除。关于字节对齐:1、 当不同的结构使用不同的字节对齐定义时,可能导致它们之间交互变得很困难。2、 在跨CPU进行通信时,可以使用字节对齐来保证唯一性,诸如通讯协议、写驱动程序时候寄存器的结构等。三种对齐方式:1、 自然对齐方式( HYPERLINK /key/4870/19870.html t _blank Natural Alignment):与该数据类型的大小相等。2、 指定对齐方式 :#pragma pack(8) /指定Align为 8;#pra

11、gma pack() /恢复到原先值3、 实际对齐方式:Actual Align = min ( Order Align, Natual Align )对于复杂数据类型(比如结构等):实际对齐方式是其成员最大的实际对齐方式:Actual Align = max( Actual align1,2,3,)编译器的填充规律:1、 成员为成员Actual Align的整数倍,在前面加Padding。成员Actual Align = min( 结构Actual Align,设定对齐方式)2、 结构为结构Actual Align的整数倍,在后面加Padding.例子分析:#pragma pack(8) /

12、指定Align为 8struct STest1char ch1; long lo1;char ch2; test1;#pragma pack()现在Align of STest1 = 4 , sizeof STest1 = 12 ( 4 * 3 )test1在内存中的排列如下( FF 为 padding ):00 - - - 04 - - - 08 - - - 12 - - -01 FF FF FF 01 01 01 01 01 FF FF FF ch1 - lo1 - ch2#pragma pack(2) /指定Align为 2struct STest2char ch3;STest1 tes

13、t; test2;#pragma pack()现在 Align of STest1 = 2, Align of STest2 = 2 , sizeof STest2 = 14 ( 7 * 2 )test2在内存中的排列如下:00 - - - 04 - - - 08 - - - 12 - - -02 FF 01 FF FF FF 01 01 01 01 01 FF FF FF ch3 ch1 - lo1 - ch2注意事项:1、 这样一来,编译器无法为特定平台做优化,如果效率非常重要,就尽量不要使用#pragma pack,如果必须使用,也最好仅在需要的地方进行设置。2、 需要加pack的地方一

14、定要在定义结构的头文件中加,不要依赖命令行选项,因为如果很多人使用该头文件,并不是每个人都知道应该pack。这特别表现在为别人开发库文件时,如果一个库函数使用了struct作为其参数,当调用者与库文件开发者使用不同的pack时,就会造成错误,而且该类错误很不好查。3、 在VC及BC提供的头文件中,除了能正好对齐在四字节上的结构外,都加了pack,否则我们编的Windows程序哪一个也不会正常运行。4、 在 #pragma pack(n) 后一定不要include其他头文件,若包含的头文件中改变了align值,将产生非预期结果。5、 不要多人同时定义一个 HYPERLINK /key/837/8

15、37.html t _blank 数据结构。这样可以保证一致的pack值。问题:按位运算符 C语言和其它高级语言不同的是它完全支持按位运算符。这与汇编语言的位操作有些相似。 C中按位运算符列出如下: 操作符 作用 & 位逻辑与 | 位逻辑或 位逻辑异或 - 位逻辑反 右移 左移 注意:1、按位运算是对字节或字中的实际位进行检测、设置或移位, 它只适用于字符型和整数型变量以及它们的变体, 对其它数据类型不适用。 2、关系运算和逻辑运算表达式的结果只能是1或0。 而按位运算的结果可以取0或1以外的值。 要注意区别按位运算符和逻辑运算符的不同, 例如, 若x=7, 则x&8 的值为真(两个非零值相与

16、仍为非零), 而x&8的值为0。 3、 | 与 |,&与&,与! 的关系&、| 和 操作符把它们的操作数当作一个为序列,按位单独进行操作。比如:10 & 12 = 8,这是因为&操作符把 10 和 12 当作二进制描述 1010 和 1100 ,所以只有当两个操作数的相同位同时为 1 时,产生的结果中相应位才为 1 。同理,10 | 12 = 14 ( 1110 ),通过补码运算,10 = -11 ( 11.110101 )。 &、| 和!操作符把它们的操作数当作真或假,并且用 0 代表假,任何非 0 值被认为是真。它们返回 1 代表真,0 代表假,对于&和|操作符,如果左侧的操作数的值就可以

17、决定表达式的值,它们根本就不去计算右侧的操作数。所以,!10 是 0 ,因为 10 非 0 ;10 & 12 是 1 ,因为 10 和 12 均非 0 ;10 | 12也是 1 ,因为 10 非 0 。并且,在最后一个表达式中,12 根本就没被计算,在表达式 10 | f( ) 中也是如此。水滴石穿C语言之extern声明辨析1 基本解释 HYPERLINK /key/263/495263.html t _blank extern可以置于变量或者 HYPERLINK /key/1456/6456.html t _blank 函数前,以标示变量或者函数的定义在别的文件中,提示编译器遇到此变量和函

18、数时在其他模块中寻找其定义。另外,extern也可用来进行链接指定。2 问题:extern 变量在一个源文件里定义了一个数组:char a6;在另外一个文件里用下列语句进行了声明:extern char *a;请问,这样可以吗? 答案与分析:1)、不可以,程序运行时会告诉你非法访问。原因在于,指向类型T的指针并不等价于类型T的数组。extern char *a声明的是一个指针变量而不是字符数组,因此与实际的定义不同,从而造成运行时非法访问。应该将声明改为extern char a 。2)、例子分析如下,如果a = abcd,则外部变量a=0 x61626364 (abcd的ASCII码值),*

19、a显然没有意义,如下图:显然a指向的空间(0 x61626364)没有意义,易出现非法内存访问。3)、这提示我们,在使用extern时候要严格对应声明时的格式,在实际编程中,这样的错误屡见不鲜。4)、extern用在变量声明中常常有这样一个作用,你在*.c文件中声明了一个全局的变量,这个全局的变量如果要被引用,就放在*.h中并用extern来声明。3 问题:extern 函数1常常见extern放在函数的前面成为函数声明的一部分,那么, HYPERLINK /key/1574/1574.html t _blank C语言的关键字extern在函数的声明中起什么作用?答案与分析:如果函数的声明中

20、带有关键字extern,仅仅是暗示这个函数可能在别的源文件里定义,没有其它作用。即下述两个函数声明没有明显的区别:extern int f(); 和int f();当然,这样的用处还是有的,就是在程序中取代include “*.h”来声明函数,在一些复杂的项目中,我比较习惯在所有的函数声明前添加extern修饰。4 问题:extern 函数2当函数提供方单方面修改函数原型时,如果使用方不知情继续沿用原来的extern申明,这样编译时编译器不会报错。但是在运行过程中,因为少了或者多了输入参数,往往会照成系统错误,这种情况应该 HYPERLINK /key/4203/549203.html t _

21、blank 如何解决?答案与分析:目前业界针对这种情况的处理没有一个很完美的方案,通常的做法是提供方在自己的xxx_pub.h中提供对外部接口的声明,然后调用方include该头文件,从而省去extern这一步。以避免这种错误。宝剑有双锋,对extern的应用,不同的场合应该选择不同的做法。5 问题:extern “C”在C+环境下使用C函数的时候,常常会出现编译器无法找到obj模块中的C函数定义,从而导致链接失败的情况,应该如何解决这种情况呢?答案与分析: HYPERLINK /key/4186/504186.html t _blank C+语言在编译的时候为了解决函数的多态问题,会将函数名

22、和参数联合起来生成一个中间的函数名称,而C语言则不会,因此会造成链接时找不到对应函数的情况,此时C函数就需要用extern “C”进行链接指定,这告诉编译器,请保持我的名称,不要给我生成用于链接的中间函数名。下面是一个标准的写法:/在.h文件的头上#ifdef _cplusplus#if _cplusplusextern C#endif#endif /* _cplusplus */ /.h文件结束的地方#ifdef _cplusplus#if _cplusplus#endif#endif /* _cplusplus */水滴石穿C语言之static辨析1、概述static 声明的变量在C语言中

23、有两方面的特征:1)、变量会被放在程序的全局存储区中,这样可以在下一次调用的时候还可以保持原来的赋值。这一点是它与堆栈变量和堆变量的区别。2)、变量用static告知编译器,自己仅仅在变量的作用范围内可见。这一点是它与全局变量的区别。2、问题:Static的理解关于static变量,请选择下面所有说法正确的内容:A、若全局变量仅在单个C文件中访问,则可以将这个变量 HYPERLINK /key/4425/49425.html t _blank 修改为静态全局变量,以降低 HYPERLINK /key/4819/54819.html t _blank 模块间的耦合度;B、若全局变量仅由单个函数访

24、问,则可以将这个变量改为该函数的静态局部变量,以降低模块间的耦合度;C、设计和使用访问动态全局变量、静态全局变量、静态局部变量的函数时,需要考虑重入问题;D、静态全局变量过大,可那会导致堆栈溢出。 答案与分析:对于A,B:根据本篇概述部分的说明b),我们知道,A,B都是正确的。对于C:根据本篇概述部分的说明a),我们知道,C是正确的(所谓的函数重入问题,下面会详细阐述)。对于D:静态变量放在程序的全局数据区,而不是在堆栈中分配,所以不可能导致堆栈溢出,D是错误的。因此,答案是A、B、C。3、问题:不可重入函数曾经设计过如下一个函数,在代码检视的时候被提醒有bug,因为这个函数是不可重入的,为什

25、么?unsigned int sum_int( unsigned int base )unsigned int index;static unsigned int sum = 0; / 注意,是static类型的。 for (index = 1; index 1:int *(*a5)(int, char*);2:void (*b10) (void (*)();3. doube(*)() (*pa)9;答案与分析:对复杂变量建立一个类型别名的方法很简单,你只要在传统的变量声明表达式里用类型名替代变量名,然后把关键字typedef加在该语句的开头就行了。 (注:如果你对有些变量的声明语法感到难以理

26、解,请参阅本系列第十篇的相关内容)。1:int *(*a5)(int, char*);/pFun是我们建的一个类型别名typedef int *(*pFun)(int, char*); /使用定义的新类型来声明对象,等价于int* (*a5)(int, char*);pFun a5; 2:void (*b10) (void (*)();/首先为上面表达式蓝色部分声明一个新类型typedef void (*pFunParam)();/整体声明一个新类型typedef void (*pFun)(pFunParam);/使用定义的新类型来声明对象,等价于void (*b10) (void (*)()

27、;pFun b10;3. doube(*)() (*pa)9; /首先为上面表达式蓝色部分声明一个新类型typedef double(*pFun)();/整体声明一个新类型typedef pFun (*pFunParam)9;/使用定义的新类型来声明对象,等价于doube(*)() (*pa)9;pFunParam pa;水滴石穿C语言之编译器引出的问题本节主要探讨C编译器下面两方面的特点所引发的一系列常见的编程问题。 对C文件进行分别编译:C程序通常由几个 HYPERLINK /key/173/15173.html t _blank 小程序(.c文件)组成,编译器将这几个小程序分别编译,然后

28、通过链接程序将它们组合在一起形成一个目标代码。由于编译器每次只能编译一个文件,因此它不能立即检查需要几个源文件配合才能发现的错误。 对 HYPERLINK /key/1456/6456.html t _blank 函数的参数和返回值建立临时变量C编译器会对函数的参数建立临时参数,也可能会对函数的返回值隐含传递一个指针。因为这些临时变量的隐含性存在,使得在某些情况下,特别是有指针存在的时候,会引发一系列的问题。 C文件中所包含的头文件会和 HYPERLINK /key/1574/1574.html t _blank C语言一同编译C语言中被包含的头文件是和.c文件一起编译的,头文件中的问题会反映

29、到.c文件的编译中。 问题:C文件的分别编译我有一个数组a定义在f1.c中,但是我想在f2.c中计算它的元素个数,用sizeof可以达到这个目的吗? 答案与分析:答案是否定的,你没有办法达到目的,本质原因是sizeof操作符只是在“编译时(compile time)”起作用,而C语言的编译单位是每次单个.c文件进行编译(其它语言也都如此)。因此,sizeof可以确定同一个源文件中某个数组的大小,但是对于定义在另一个源文件中的数组它无能为力了,因为那已经是“运行时(run time)”才能确定的事情了。一件事情要想做,总会有办法的,下面提供有三种可选的办法来解决这个问题:1)、定义一个全局变量,

30、让它记住数组的大小,在另外一个.c文件中我们通过访问这个全局变量来得到数组的大小信息(好像有点小题大做得不偿失_)。2)、在某个.h文件中用宏定义数组的大小,例如#define ARRAY_SIZE 50,然后在两个源文件中都包含这个.h文件,通过直接访问ARRAY_SIZE来得到定义在不同.c文件中的数组的大小。3)、设置数组的最后一个元素为特殊值,例如0,-1,NULL等,然后我们通过遍历数组来寻找这个特殊的结尾元素,从而判断数组的长度(这个办法效率低,也是笨笨的)。 问题:函数返回值隐含传递指针下面的代码可以正常工作,但是在程序结束时会有一个致命错误产生。究竟是什么原因呢?struct

31、listchar *item;struct list *next;main (argc, argv).答案与分析:原因很简单,稍微注意一点不难发现,在定义结构list的右花括弧后面加一个分号就可以解决这个问题:struct list char *item;struct list *next;;/缺了这个分号可不行!好了,问题是解决了,但,你知道这个错误究竟导致了什么致命问题吗?问题不是表面上那么简单的,OK,让我们来看看事情背后的真相。首先看一看下面这段代码:VOID Func ( struct HYPERLINK /key/3948/3948.html t _blank my_struct

32、stX).struct my_struct stY = .;Func (stY);当调用函数Func的时候,是把结构变量stY的值拷贝一份到调用栈中,从而作为参数传递给函数FUNC的,这个叫做C语言的参数值传递。我相信这个你一定很清楚,那么,你应该知道:如果函数的返回值是结构变量的话,函数应该如何将值返回给调用者呢?且看下面这段代码:struct my_structFunc (VOID).struct my_struct stY = Func();此时函数Func的返回值是一个结构类型的值,这个返回值被放在内存中一个阴暗恐怖的地方,同时安排了一个指针指向这个地方(暂时称为“神秘指针”),而这个

33、指针会由C语言的编译器作为一个隐藏参数传递给函数Func。当函数Func返回时,编译器生成的代码将这个由隐藏指针指向的内存区的值拷贝到返回结构stY中,从而完成将结构变量值返回给调用者。你明白了上述所讲的东东,那么今天问题的真正原因也就呼之欲出了:因为struct list .的定义后面没有加分号,导致主函数main (argc, argv)被编译器理解为是一个返回值为结构变量的函数,从而期望得到除了argc和argv以外的第三个参数,也就是我们上面提到的那个隐含传入的“神秘指针”。可是,大家知道,这里函数是main函数,main函数的参数是由程序中的启动代码(startup code)提供的

34、。而启动代码当然认为main()天生就应该只得到两个参数,要“神秘指针”,当然没有,如此一来, main()在返回时自作主张地去调用栈中访问它的那个并不存在的第三个参数(即神秘指针),这样导致非法访问,产生致命问题。这才是这个问题的真正根源。建议: 1)、尽量将结构变量的指针而不是结构本身作为函数参数,否则 HYPERLINK /key/696/450696.html t _blank 函数调用时内存拷贝的开销可不小,尤其是对那些调用频繁、结构体大的情况。 2)、结构定义的后面一定要加分号,经过上面我的大段讲述,我相信你不会犯相同的错误问题:编译器会给函数的参数隐含制造临时副本 请问运行下面的

35、Test函数会有什么样的结果?void GetMemory2(char *p, int num)*p = (char *)malloc(num);void Test(void)char *str = NULL;GetMemory(&str, 100);strcpy(str, hello);printf(str);答案与分析:这是林锐的C/C+高质量编程指南上面的例子,拿来用一下。这样调用会产生如下两个后果:1)、能够输出hello2)、内存泄漏 另一个相关问题:请问运行Test函数会有什么样的结果?void GetMemory(char *p)p = (char *)malloc(100);v

36、oid Test(void) char *str = NULL;GetMemory(str);strcpy(str, hello world);printf(str);答案与分析:后果严重,运行的结果是程序崩溃,通过运行调试我们可以看到,经过GetMemory后,Test函数中的 str仍旧是NULL。可想而知,一调用strcpy(str, hello world);程序必然崩溃了事。原因分析: C编译器总是会为函数的每个参数制作临时副本,指针参数p的副本是 _p,编译器使 _p = p。如果函数体内的程序修改了_p的内容,就导致参数p的内容作相应的修改。这就是指针可以用作输出参数的原因。在本

37、例中,_p申请了新的内存,只是把_p所指的内存地址改变了,但是p丝毫未变。所以函数GetMemory并不能输出任何东西,如果想要输出动态内存,请使用指向指针的指针,或者,使用指向引用的指针。问题:头文件和包含它的.c文件一同编译问 下面的代码非常短小,看起来毫无问题,但编译器会报告一个错误,请问问题可能出现在什么地方?#include someheader.hint myint = 0;答案与分析:不用盯着int myint = 0;看,这一句赋值应该是C语言中最简单的语句,问题肯定不会出在它身上,那么问题只可能出现在someheader.h中,最常见的就是该头文件的最后一行的声明(函数也好,

38、变量也好)没有用分号;结尾,那么编译器会将它和myint变量结合起来考虑,自然就会出错了。这个问题主要是提醒你,在定位问题时思路要拓宽一点,可能要考虑一下所包含的头文件是否有问题。结论:被包含的头文件是和.c文件一起编译的,头文件中的问题会反映到.c文件编译中去的,切记。水滴石穿C语言之可变参数问题 HYPERLINK /key/4647/354647.html t _blank 概述C语言中有一种长度不确定的参数,形如:,它主要用在参数个数不确定的函数中,我们最容易想到的例子是printf函数。原型:int printf( const HYPERLINK /key/3784/213784.h

39、tml t _blank char *format , argument. );使用例:printf(Enjoy yourself everyday!n);printf(The value is %d!n, value);这种可变参数可以说是C语言一个比较难理解的部分,这里会由几个问题引发一些对它的分析。注意:在C+中有 HYPERLINK /key/2073/202073.html t _blank 函数重载(overload)可以用来区别不同函数参数的调用,但它还是不能表示任意数量的函数参数。问题:printf的实现请问,如何自己实现printf函数,如何处理其中的可变参数问题? 答案与分

40、析:在标准C语言中定义了一个头文件专门用来对付可变参数列表,它包含了一组宏,和一个va_list的typedef声明。一个典型实现如下:typedef char* va_list;#define va_start(list) list = (char*)&va_alist#define va_end(list)#define va_arg(list, mode)(mode*) (list += sizeof(mode)-1 自己实现printf:#include int printf(char* format, )va_list ap;va_start(ap, format);int n =

41、vprintf(format, ap);va_end(ap);return n;问题:运行时才确定的参数有没有办法写一个函数,这个函数参数的具体形式可以在运行时才确定?答案与分析:目前没有正规的解决办法,不过独门偏方倒是有一个,因为有一个函数已经给我们做出了这方面的榜样,那就是main(),它的原型是:int main(int argc,char *argv);函数的参数是argc和argv。深入想一下,只能在运行时确定参数形式,也就是说你没办法从声明中看到所接受的参数,也即是参数根本就没有固定的形式。常用的办法是你可以通过定义一个void *类型的参数,用它来指向实际的参数区,然后在函数中根

42、据根据需要任意解释它们的含义。这就是main函数中argv的含义,而argc,则用来表明实际的参数个数,这为我们使用提供了进一步的方便,当然,这个参数不是必需的。虽然参数没有固定形式,但我们必然要在函数中解析参数的意义,因此,理所当然会有一个要求,就是调用者和被调者之间要对参数区内容的格式,大小, HYPERLINK /key/1912/271912.html t _blank 有效性等所有方面达成一致,否则南辕北辙各说各话就惨了。问题:可变长参数的传递有时候,需要编写一个函数,将它的可变长参数直接传递给另外的函数,请问,这个要求能否实现?答案与分析:目前,你尚无办法直接做到这一点,但是我们可

43、以迂回前进,首先,我们定义被调用函数的参数为va_list类型,同时在调用函数中将可变长参数列表转换为va_list,这样就可以进行变长参数的传递了。看如下所示:void subfunc (char *fmt, va_list argp).arg = va_arg (fmt, argp); /* 从argp中逐一取出所要的参数 */.void mainfunc (char *fmt, .)va_list argp;va_start (argp, fmt); /* 将可变长参数转换为va_list */subfunc (fmt, argp); /* 将va_list传递给子函数 */va_end

44、 (argp);.问题:可变长参数中类型为函数指针我想使用va_arg来提取出可变长参数中类型为函数指针的参数,结果却总是不正确,为什么?答案与分析:这个与va_arg的实现有关。一个简单的、演示版的va_arg实现如下:#define va_arg(argp, type) (*(type *)(argp) += sizeof(type) - sizeof(type)其中,argp的类型是char *。如果你想用va_arg从可变参数列表中提取出函数指针类型的参数,例如int (*)(),则va_arg(argp, int (*)()被扩展为:(*(int (*)() *)(argp) +=

45、sizeof (int (*)() -sizeof (int (*)()显然,(int (*)() *)是无意义的。解决这个问题的办法是将函数指针用typedef定义成一个独立的数据类型,例如:typedef int (*funcptr)();这时候再调用va_arg(argp, funcptr)将被扩展为:(* (funcptr *)(argp) += sizeof (funcptr) - sizeof (funcptr)这样就可以通过编译检查了。问题:可变长参数的获取有这样一个具有可变长参数的函数,其中有下列代码用来获取类型为float的实参:va_arg (argp, float);这样

46、做可以吗?答案与分析:不可以。在可变长参数中,应用的是加宽原则。也就是float类型被扩展成 HYPERLINK /key/3008/213008.html t _blank double;char, short被扩展成int。因此,如果你要去可变长参数列表中原来为float类型的参数,需要用va_arg(argp, double)。对char和short类型的则用va_arg(argp, int)。问题:定义可变长参数的一个限制为什么我的编译器不允许我定义如下的函数,也就是可变长参数,但是没有任何的固定参数?int f (.).答案与分析:不可以。这是ANSI C 所要求的,你至少得定义一个

47、固定参数。这个参数将被传递给va_start(),然后用va_arg()和va_end()来确定所有实际调用时可变长参数的类型和值。水滴石穿C语言之内存使用问题:内存使用有人写了一个将整数转换为字符串的函数: HYPERLINK /key/3784/213784.html t _blank char *itoa (int n)char retbuf20;sprintf(retbuf, %d, n);return retbuf;如果我调用这个函数:char *str5 = itoa(5),str5会是什么结果呢?答案分析:答案是不确定,可以确定的是肯定不是我们想要的 “5”。 retbuf定义在

48、函数体中,是一个局部变量,它的内存空间位于栈(stack)中的某个位置,其作用范围也仅限于在itoa()这个函数中。当itoa()函数退出时,retbuf在调用栈中的内容将被收回,这时,这块内存地址可能存放别的内容。因此将retbuf这个局部变量返回给调用者是达不到预期的目的的。那么如何解决这个问题呢,不用担心,方法不但有,而且还不止一个,下面就来阐述三种能解决这个问题的办法:1)、在itoa()函数内部定义一个static char retbuf20,根据 HYPERLINK /key/385/185385.html t _blank 静态变量的特性,我们知道,这可以保证函数返回后retbu

49、f的空间不会被收回,原因是函数内的静态变量并不是放在栈中,而是放在程序中一个叫“.bss”段的地方,这个地方的内容是不会因为函数退出而被收回的。这种办法确实能解决问题,但是这种办法同时也导致了itoa()函数变成了一个不可重入的函数(即不能保证相同的输入肯定有相同的输出),另外, retbuf 中的内容会被下一次的调用结果所替代,这种办法不值得推荐。2)、在itoa()函数内部用malloc() 为retbuf申请内存,并将结果存放其中,然后将retbuf返回给调用者。由于此时retbuf位于堆(heap)中,也不会随着函数返回而 HYPERLINK /key/1028/186028.html

50、 t _blank 释放,因此可以达到我们的目的。但是有这样一种情况需要注意:itoa()函数的调用者在不需要retbuf的时候必须把它释放,否则就造成内存泄漏了,如果此函数和调用函数都是同一个人所写,问题不大,但如果不是,则比较容易会疏漏此释放内存的操作。3)、将函数定义为char *itoa(int n, char *retbuf),且retbuf的空间由调用者申请和释放,itoa()只是将转换结果存放到retbuf而已。这种办法明显比第一、二种方法要好,既避免了方法1对函数的影响,也避免了方法2对内存分配释放的影响,是目前一种比较通行的做法。扩展分析:其实就这个问题本身而言,我想大家都可

51、以立刻想到答案,关键在于对内存这种敏感资源的正确和合理地利用,下面对内存做一个简单的分析:1)、程序中有不同的内存段,包括: HYPERLINK /key/879/215879.html t _blank .data - 已 HYPERLINK /key/1942/201942.html t _blank 初始化全局/静态变量,在整个软件执行过程中有效;.bss - 未初始化全局/静态变量,在整个软件执行过程中有效;.stack - 函数调用栈,其中的内容在函数执行期间有效,并由编译器负责分配和收回;.heap - 堆,由程序显式分配和收回,如果不收回就是内存泄漏。2)、自己使用的内存最好还是

52、自己申请和释放。这可以说是一个内存分配和释放的原则,比如说上面解决办法的第二种,由itoa()分配的内存,最后由调用者释放,就不是一个很好的办法,还不如用第三种,由调用者自己申请和释放。另外这个原则还有一层意思是说:如果你要使用一个指针,最好先确信它已经指向合法内存区了,如果没有就得自己分配,要不就是非法指针访问。很多程序的致命错误都是访问一个没有指向合法内存区的指针,这也包括空指针。问题:内存分配 & sizeof 我使用sizeof来计算一个指针变量,我希望得到这个指针变量所分配的内存块的大小,可以吗?Char *p = NULL;int nMemSize = 0;p = malloc(1

53、024);nMemSize = sizeof(p);答案与分析: 答案是达不到你的要求,sizeof只能告诉你指针本身占用的内存大小。指针所指向的内存,如果是malloc分配的,sizeof 是没有办法知道的。换句话说,malloc分配的内存是没有办法向内存管理模块进行事后查询的,当然你可以自己编写代码来维护。 问题:栈内存使用 下面程序运行有什么问题?char *GetString(void)char p = hello world;return p;/ 编译器将提出警告void Test4(void)char *str = NULL;str = GetString();/ str 的内容是

54、垃圾cout str = ulAllocedLines)/ * 当前竖向空间已经不够了,通过realloc对其进行扩展。ulAllocedLines += 50; / 每次扩展50行。 ppText = realloc (ppText, ulAllocedLines * (char *);if (NULL = ppText)return; / 内存分配失败,返回 ppTextulCurrLines+ = p; / 横向“扩展”,指向不定长字符串 问题:指针数组与数组指针与指向指针的指针指针和数组分别有如下的特征:指针:动态分配,初始空间小数组:索引方便,初始空间大下面使用高维数组来说明指针数组

55、、数组指针、指向指针的指针各自的适合场合。 多维静态数组:各维均确定,适用于整体空间需求不大的场合,此结构可方便索引,例a1040。 数组指针:低维确定,高维需要动态生成的场合,例ax40。int (*pA)5. pA+. 指针数组:高维确定,低维需要动态生成的场合,例a10y。int *pAi. pA0 = &b0 指向指针的指针:高、低维均需要动态生成的场合,例axy。问题:数组名相关问题假设有一个整数数组a,a和&a的区别是什么?答案与分析:a = &a = &a0,数组名a不占用存储空间。需要引用数组(非字符串)首地址的地方,我一般使用&a0,使用a容易和指针混淆,使用&a容易和非指针

56、变量混淆。区别在于二者的类型。对数组a的直接引用将产生一个指向数组第一个元素的指针,而&a的结果则产生一个指向全部数组的指针。例如:int a2 = 1, 2;int *p = 0;p = a; /* p指向a0所在的地方 */x = *p; /* x = a0 = 1*/p = &a; /* 编译器会提示你错误,*/*显示整数指针与整数数组指针不一样 */问题:函数指针与指针函数请问:如下定义是什么意思:int *pF1();int (*pF2)();答案与分析:首先清楚它们的定义: 指针函数,返回一个指针的函数。 函数指针,指向一个函数的指针。可知: pF1是一个指针函数,它返回一个指向i

57、nt型数据的指针。 pF2是一个函数指针,它指向一个参数为空的函数,这个函数返回一个整数。水滴石穿C语言之指针步进辨析基本解释通过上一篇的分析,我们已经很清楚地知道:指针不是一个简单的类型,它是一个本身和所指向物相复合的类型。指针的算术运算(如步进)与指针所指向物的类型密切相关。 问题:指针步进 & HYPERLINK /key/3883/183883.html t _blank amp; 步进单位下面的代码中打印出的结果是几?int arContext5 =0,1,2,3,4, i, *pAr;pAr = arContext;printf (%dn, *(pAr + 3 * HYPERLIN

58、K /key/2851/182851.html t _blank sizeof (int);答案与分析:这段代码没有正确答案,因为这段代码是错的,printf将打出无法预测的内存区的值,其中的原因如下:在 HYPERLINK /key/4077/169077.html t _blank C语言中,指针总是按照它所指向的对象的大小步进。在上面的例子中,pAr是指向整数类型变量的指针,一个整数是4个字节(默认CPU字长是32位),pAr + 1就指向下一个整数,也就是指针后移4个字节,而不是说将地址只移动一个字节。因为C语言编译器知道每个指针的类型,因此对指针的运算是会自动把所指类型的Size考虑

59、进去的。pAr + 3 * sizeof (int) = pAr + 3 * 4 = pAr + 12 ,因此pAr指向了数组的第 HYPERLINK /key/738/215738.html t _blank 13个整数元素。而数组本身才5个元素,pAr早已经超出了界限,所指向的地方当然就是无人可知道的东西了,具体指向什么东西,各种不同的编译器互不相同。总之,肯定不能打印出我们想要的值就是了。指针不是一个简单的类型,它是一个和指针所指物的类型相复合的类型。因此,它的算术运算与指针所指物的类型密切相关, HYPERLINK /key/193/200193.html t _blank 在C+语言

60、中也是同样。再比如下面的例子:int a8;int* p = a;int* q = p + 3;p+;指针的加减并不是指针本身的二进制表示加减,要记住,指针是一个元素的地址,它每加一次,就指向下一个元素。所以:int* q = p + 3;q指向从p开始的第三个整数。p+; p指向下一个整数。问题:指针步进 & 步进单位转换我有一个char *类型的指针,恰好指向了一个int类型的值,我想让这个指针跳过int指向下一个char,下面的代码可以达到这个目的吗?(int *)p)+;答案与分析:可以。首先我们要清楚语言中左值和右值的概念,C语言中左值是指可以放在“=”左侧,即可以被赋值,右值是可以

温馨提示

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

评论

0/150

提交评论