水滴石穿C语言之C语言的底层操作-Read

上传人:灯火****19 文档编号:169065921 上传时间:2022-11-14 格式:DOCX 页数:103 大小:169.57KB
收藏 版权申诉 举报 下载
水滴石穿C语言之C语言的底层操作-Read_第1页
第1页 / 共103页
水滴石穿C语言之C语言的底层操作-Read_第2页
第2页 / 共103页
水滴石穿C语言之C语言的底层操作-Read_第3页
第3页 / 共103页
资源描述:

《水滴石穿C语言之C语言的底层操作-Read》由会员分享,可在线阅读,更多相关《水滴石穿C语言之C语言的底层操作-Read(103页珍藏版)》请在装配图网上搜索。

1、水滴石穿C语言之C语言的底层操作概述 鸣宣的内存模型基本上对应了现在von Neumann (冯诺伊曼)计算机的实际存 储模型,很好的达到了对机器的映射,这是C/C+适合做底层开发的主要原因,另外,C语 言适合做底层开发还有另外一个原因,那就是C语言对底层操作做了很多的的支持,提供了 很多比较底层的功能。下面结合问题分别进行阐述。问题:移位操作在运用移位操作符时,有两个问题必须要清楚:(1)、在右移操作中,腾空位是填0还是符号位;(2)、什么数可以作移位的位数。答案与分析:和是指将变量中的每一位向右或向左移动,其通常形式为:右移:变量名移位的位数左移:变量名移位的位数经过移位后,一端的位被“挤

2、掉,而另一端空出的位以0填补,在C语言中的移位不是 循环移动的。(1)第一个问题的答案很简单,但要根据不同的情况而定。如果被移位的是无符号数, 则填0。如果是有符号数,那么可能填0或符号位。如果你想解决右移操作中腾空位的填 充问题,就把变量声明为无符号型,这样腾空位会被置Oo(2)第二个问题的答案也很简单:如果移动n位,那么移位的位数要不小于0 ,并且 一定要小于n o这样就不会在一次操作中把所有数据都移走。比如,如果整型数据占32位,n是一整型数据,则n31和n0都合法,而n 32和n -1都不合法。注意即使腾空位填符号位,有符号整数的右移也不相当与除以。为了证明这一点,我们可以想一下-1

3、1不可能为0。问题:位段结构struct RPR_ATD_TLV_HEADER (ULONG resl:6;ULONG type:10;ULONG resl:6;ULONG length:10;;位段结构是一种特殊的结构,在需按位访问一个字节或字的多个位时,位结构比按位运算符更 加方便。位结构定义的一般形式为:struct位结构名数据类型变量名:整型常数;数据类型变量名:整型常数;位结构变量;其中:整型常数必须是非负的整数,范围是015,表示二进制位的个数,即表示有多少位。 变量名是选择项,可以不命名,这样规定是为了排列需要。例如:下面定义了一个位结构。structunsigned incon

4、: 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位结构成员可以与其它结构成员一起使用。按位访问与设置,方便&节省例如:struct infochar name8;int age;struct addr

5、 address;float pay;unsigned state: 1;unsigned pay: 1;workers;上例的结构定义了关于一个工从的信息。其中有两个位结构成员,每个位结构成员只有一位, 因此只占一个字节但保存了两个信息,该字节中第一位表示工人的状态,第二位表示工资是否 已发放。由此可见使用位结构可以节省存贮空间。注意不要超过值限制问题:字节对齐让偶们先来看下面这个结构体:struct stulint a;char b;;来看看sizeof (stu)的结果为多少?怎么是8啊?你先别急,再来看下一个例子:struct stu2char b;int a;这个sizeof (st

6、u2)是多少?怎么还是8啊?现在创建一个结构体变量 stu2 s2 a ,0xl2345678h ; stul si 0x12345678, a 运行DEGUG,怎么样发现了什么?在第一个结构体中char b的后面内存有三个字节是添了数据的.也就是这样78 56 34 12 61 ccCC CC而在第二个结构体中CHAR B的后面内存中也添加了数据.61 cc cc cc 78 56 34 12这又是怎么回事呢?需要字节对齐当然有设计者的考虑了,原来这样有助于加快计算机的存取速度,否则就得多花指令周期了。所以,编译器通常都会对结构体进行处理,让宽度为2的基本数据类型(short 等) 都位于能

7、被2整除的地址上,让宽度为4的基本数据类型(int等)都位于能被4整除的地址上。 正是因为如此两个数中间就可能需要加入填充字节,所以结构体占的内存空间就增长了。其实字节对齐的细节和具体编译器实现相关,但一般而言,满足三个准则:1)结构体变量的首地址能够被其最宽基本类型成员的大小所整除;2)结构体每个成员相对于结构体首地址的偏移量都是成员大小的整数倍,如有需要编译器会 在成员之间加上填充字节;例如上面第二个结构体变量的地址空间。3)结构体的总大小为结构体最宽基本类型成员大小的整数倍,如有需要编译器会在最末一个 成员之后加上填充字节。例如上面第一个结构体变量。(哎呀!知道!真多嘴!)现在就可以解释

8、上面的问题了,第一个结构体变量中成员变量最宽为4(SIZEOF(INT) = 4),所 以S1变量首地址必须能被4整除。(不信你试试!)S1的大小也应该为4的整数倍。但是现在 si中有4+1的空间,所以为了满足第三个条件就在char b的后面在加上三个字节的空间以凑 够8个字节空间。第二个结构体变量S2中成员变量最大宽度为4,而且按照以前的理解inta的 地址和s2的地址相差5个字节,但是为了满足第而个条件(相差的距离-偏移地址必须是4 的整数倍)所以在char b的后面添加了三个字节的空间以保证inta的偏移地址是4的整数倍即 为4。至于涉及到结构体嵌套的问题,你也可以用上述方法总结的,只不

9、过你把被嵌套的结构体 在原地展开就行了,不过在计算偏移地址的时候被嵌套的结构体是不能原地展开的必须当作整 体。嘿嘿!偶申明一点,上述三条建议不是偶说的,是做编译器的工程师总结出来的,偶只是 借用而已。我在使用VC编程的过程中,有一次调用DLL中定义的结构时,发觉结构都乱掉了, 完全不能读取正确的值,后来发现这是因为DLL和调用程序使用的字节对齐选项不同,那么我 想问一下,字节对齐究竟是怎么一回事?答案与分析: 为了能使CPU对变量进行高效快速的访问,变量的起始地址应该具有某些特性,即所谓的“对 齐”。例如对于4字节的int类型变量,其起始地址应位于4字节边界上,即起始地址能够被4 整除。关于字

10、节对齐:1、当不同的结构使用不同的字节对齐定义时,可能导致它们之间交互变得很困难。2、在跨CPU进行通信时,可以使用字节对齐来保证唯一性,诸如通讯协议、写驱动程序 时候寄存器的结构等。三种对齐方式:1、自然对齐方式(Natural Alignment);与该数据类型的大小相等。2、指定对齐方式:ftpragma pack(8) 指定 Align 为 8;#pragma pack () 恢复到原先值3、实际对齐方式:Actual Align = min ( Order Align, Natual Align )对于复杂数据类型(比如结构等):实际对齐方式是其成员最大的实际对齐方式:Actual

11、Align = max( Actual alignl, 2, 3, ,)编译器的填充规律:1、成员为成员Actual Align的整数倍,在前面加Padding。成员Actual Align = min(结构Actual Align,设定对齐方式)2、结构为结构Actual Align的整数倍,在后面加Padding.例子分析:#pragma pack (8) 指定 Align 为 8struct STestlchar chi;long lol;char ch2; testl;pragma pack ()现在Align of STestl = 4 , sizeof STestl = 12 (4

12、*3)testl在内存中的排列如下(FF为padding ):00 04 08 12 01 FF FF FF 01 01 01 01 01 FF FF FFchi lol ch2ftpragma pack(2) 指定 Align 为 2struct STest2char ch3;STestl test; test2;#pragma pack ()现在 Align of STestl = 2, Align of STest2 = 2 , sizeof STest2 = 14 ( 7 * 2 )test2在内存中的排列如下:00 04 08 12 02 FF 01 FF FF FF 01 01 0

13、1 01 01 FF FF FFch3 chi lol ch2注意事项:1、这样一来,编译器无法为特定平台做优化,如果效率非常重要,就尽量不要使用#pragma pack,如果必须使用,也最好仅在需要的地方进行设置。2、需要加pack的地方一定要在定义结构的头文件中加,不要依赖命令行选项,因为如果 很多人使用该头文件,并不是每个人都知道应该pack。这特别表现在为别人开发库文件时,如 果一个库函数使用了 struct作为其参数,当调用者与库文件开发者使用不同的pack时,就会 造成错误,而且该类错误很不好查。3、在VC及BC提供的头文件中,除了能正好对齐在四字节上的结构外,都加了 pack,否

14、则 我们编的Windows程序哪一个也不会正常运行。4、在#pragma pack(n)后一定不要include其他头文件,若包含的头文件中改变了 align 值,将产生非预期结果。5、不要多人同时定义一个数据结构。这样可以保证一致的pack值。问题:按位运算符C语言和其它高级语言不同的是它完全支持按位运算符。这与汇编语言的位操作有些相似。C 中按位运算符列出如下:操作符作用&位逻辑与I位逻辑或八位逻辑异或-位逻辑反右移左移注意:1、按位运算是对字节或字中的实际位进行检测、设置或移位,它只适用于字符型和整数型 变量以及它们的变体,对其它数据类型不适用。2、关系运算和逻辑运算表达式的结果只能是1

15、或Oo而按位运算的结果可以取0或1以外 的值。要注意区别按位运算符和逻辑运算符的不同,例如,若x=7,贝1xa&8的值为真(两个非 零值相与仍为非零),而x&8的值为0。3、 |与&与&,与!的关系&、I和操作符把它们的操作数当作一个为序列,按位单独进行操作。比如:10 & 12 = 8,这是因为&操作符把10和12当作二进制描述1010和1100 ,所以只有当两个操作数 的相同位同时为1时,产生的结果中相应位才为1。同理,10 | 12 = 14 ( 1110 ),通过补 码运算,10 = -11 ( 11. 110101 ) 以多少为一个位序列&、|和!操作符把它们的操 作数当作真或假,并

16、且用0代表假,任何非0值被认为是真。它们返回1代表真”, 0代表假,对于&和II操作符,如果左侧的操作数的值就可以决定表达式的值,它们根 本就不去计算右侧的操作数。所以,!10是0 ,因为10非0 ; 10 a& 12是1 ,因为10和 12均非0 ; 10 | | 12也是1 ,因为10非0。并且,在最后一个表达式中,12根本就没 被计算,在表达式10 | f()中也是如此。水滴石穿C语言之extern声明辨析1基本解释extern可以置于变量或者函数前,以标示变量或者函数的定义在别的文件中,提示编译器 遇到此变量和函数时在其他模块中寻找其定义。另外,extern也可用来进行链接指定。2问题

17、:extern变量在一个源文件里定义了一个数组:char a6;在另外一个文件里用下列语句进行了声明:extern char *a;请问,这样可以吗?答案与分析:1)、不可以,程序运行时会告诉你非法访问。原因在于,指向类型T的指针并不等价于类 型T的数组。extern char *a声明的是一个指针变量而不是字符数组,因此与实际的定义不同, 从而造成运行时非法访问。应该将声明改为extern char a2)、例子分析如下,如果a口 = abed”,则外部变量a=0x61626364 (abed的ASCH码值), *2显然没有意义,如下图:a-(MinS3M0x61626364显然a指向的空间

18、(0x61626364)没有意义,易出现非法内存访问。3)、这提示我们,在使用extern时候要严格对应声明时的格式,在实际编程中,这样的错 误屡见不鲜。4)、extern用在变量声明中常常有这样一个作用,你在*. c文件中声明了一个全局的变量, 这个全局的变量如果要被引用,就放在*.h中并用extern来声明。3问题:extern函数1常常见extern放在函数的前面成为函数声明的一部分,那么,C语言的关键字extern在 函数的声明中起什么作用?答案与分析:如果函数的声明中带有关键字extern,仅仅是暗示这个函数可能在别的源文件里定义,没 有其它作用。即下述两个函数声明没有明显的区别:e

19、xtern int f ();和 int f ();当然,这样的用处还是有的,就是在程序中取代include *.h”来声明函数,在一些复杂的项 目中,我比较习惯在所有的函数声明前添加extern修饰。4问题:extern函数2当函数提供方单方面修改函数原型时,如果使用方不知情继续沿用原来的extern申明,这 样编译时编译器不会报错。但是在运行过程中,因为少了或者多了输入参数,往往会照成系统 错误,这种情况应该如何解决?答案与分析:目前业界针对这种情况的处理没有一个很完美的方案,通常的做法是提供方在自己的 xxx_pub. h中提供对外部接口的声明,然后调用方include该头文件,从而省去

20、extern这一步。 以避免这种错误。宝剑有双锋,对extern的应用,不同的场合应该选择不同的做法。5 问题:extern C在C+环境下使用C函数的时候,常常会出现编译器无法找到obj模块中的C函数定义,从 而导致链接失败的情况,应该如何解决这种情况呢?答案与分析:生造宣在编译的时候为了解决函数的多态问题,会将函数名和参数联合起来生成一个中间 的函数名称,而C语言则不会,因此会造成链接时找不到对应函数的情况,此时C函数就需要 用extern “C”进行链接指定,这告诉编译器,请保持我的名称,不要给我生成用于链接的中 间函数名。下面是一个标准的写法:在.h文件的头上#if _cplusplu

21、sextern Cftendif#endif /* _cplusplus */. h文件结束的地方ttifdef _cplusplus#if _cplusplus)ftendifftendif /* _cplusplus */水滴石穿C语言之static辨析1、概述static声明的变量在C语言中有两方面的特征:1)、变量会被放在程序的全局存储区中,这样可以在下一次调用的时候还可以保持原来的 赋值。这一点是它与堆栈变量和堆变量的区别。2)、变量用static告知编译器,自己仅仅在变量的作用范围内可见。这一点是它与全局变量的 区别。2、问题:Static的理解关于static变量,请选择下面所有

22、说法正确的内容:A、若全局变量仅在单个C文件中访问,则可以将这个变量修改为静态全局变量,以降低模 块间的耦合度;B、若全局变量仅由单个函数访问,则可以将这个变量改为该函数的静态局部变量,以降低 模块间的耦合度;C、设计和使用访问动态全局变量、静态全局变量、静态局部变量的函数时,需要考虑重入 问题;D、静态全局变量过大,可那会导致堆栈溢出。答案与分析:对于A, B:根据本篇概述部分的说明b),我们知道,A,B都是正确的。对于C:根据本篇概述部分的说明a),我们知道,C是正确的(所谓的函数重入问题,下面 会详细阐述)。对于D:静态变量放在程序的全局数据区,而不是在堆栈中分配,所以不可能导致堆栈溢出

23、, D是错误的。因此,答案是A、B、Co3、问题:不可重入函数曾经设计过如下一个函数,在代码检视的时候被提醒有bug,因为这个函数是不可重入的, 为什么?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;答案与分析:对复杂变量建立一

24、个类型别名的方法很简单,你只要在传统的变量声明表达式里用类型名 替代变量名,然后把关键字typedef加在该语句的开头就行了。(注:如果你对有些变量的声明语法感到难以理解,请参阅本系列第十篇的相关内容)。 1: int *(*a5) (int, char*);/pFun是我们建的一个类型别名typedef int *(*pFun)(int, char*);使用定义的新类型来声明对象,等价于int* (*a5) (int, char*);pFun a5; 2: void (*b10) (void (*)();首先为上面表达式蓝色部分声明一个新类型typedef void (*pFunParam)

25、();整体声明一个新类型typedef void (*pFun)(pFunParam);使用定义的新类型来声明对象,等价于void (*b10j) (void (*)();pFun b10: 3. doube (*) () (*pa) 9;首先为上面表达式蓝色部分声明一个新类型typedef double(*pFun)();整体声明一个新类型typedef pFun (*pFunParam)9;使用定义的新类型来声明对象,等价于doube(*)() (*pa) 9;pFunParam pa;水滴石穿C语言之编译器引出的问题本节主要探讨C编译器下面两方面的特点所引发的一系列常见的编程问题。对c文

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

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

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

29、st *next;main (argc, argv)答案与分析:原因很简单,稍微注意一点不难发现,在定义结构list的右花括弧后面加一个分号就可以 解决这个问题:struct listchar *item;struct list *next; 缺了这个分号可不行!好了,问题是解决了,但,你知道这个错误究竟导致了什么致命问题吗?问题不是表面上那 么简单的,0K,让我们来看看事情背后的真相。首先看一看下面这段代码:VOID Func ( struct my_struct stX) struct my_struct stY = .;Func (stY);当调用函数Func的时候,是把结构变量stY的

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

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

32、自作主 张地去调用栈中访问它的那个并不存在的第三个参数(即神秘指针),这样导致非法访问,产生 致命问题。这才是这个问题的真正根源。建议:1)、尽量将结构变量的指针而不是结构本身作为函数参数,否则函数调用时内存拷贝的开 销可不小,尤其是对那些调用频繁、结构体大的情况。2)、结构定义的后面一定要加分号,经过上面我的大段讲述,我相信你不会犯相同的错误 问题:编译器会给函数的参数隐含制造临时副本请问运行下面的Test函数会有什么样的结果?void GetMemory2(char *p, int num)*p = (char *)malloc (num);void Test(void)char *str

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

34、,运行的结果是程序崩溃,通过运行调试我们可以看到,经过GetMemory后,Test 函数中的str仍旧是NULL。可想而知,一调用 strcpy(str, hello world*):程序必然崩溃了事。原因分析:C编译器总是会为函数的每个参数制作临时副本,指针参数p的副本是_p,编译器使_p = Po如果函数体内的程序修改了的内容,就导致参数P的内容作相应的修改。这就是指针可以 用作输出参数的原因。在本例中,_P申请了新的内存,只是把_p所指的内存地址改变了,但是 P丝毫未变。所以函数GetMemory并不能输出任何东西,如果想要输出动态内存,请使用指向指 针的指针,或者,使用指向引用的指针

35、。问题:头文件和包含它的.c文件一同编译问下面的代码非常短小,看起来毫无问题,但编译器会报告一个错误,请问问题可能出现在 什么地方?include someheader. h*int myint = 0;答案与分析:不用盯着int myint = 0;看,这一句赋值应该是C语言中最简单的语句,问题肯定不会出 在它身上,那么问题只可能出现在someheader. h中,最常见的就是该头文件的最后一行的声明(函数也好,变量也好)没有用分号;结尾,那么编译器会将它和myint变量结合起来考虑, 自然就会出错了。这个问题主要是提醒你,在定位问题时思路要拓宽一点,可能要考虑一下所包含的头文件 是否有问题

36、。结论:被包含的头文件是和.c文件一起编译的,头文件中的问题会反映到.c文件编译中去 的,切记。水滴石穿C语言之可变参数问题概述C语言中有一种长度不确定的参数,形如:,它主要用在参数个数不确定的函数中,我 们最容易想到的例子是printf函数。原型:int printf( const char *format , argument.);使用例:printf(*Enjoy yourself everyday!n*);printf(*The value is %d!n”, value);这种可变参数可以说是C语言一个比较难理解的部分,这里会由几个问题引发一些对它的分析。注意:在C+中有函数重载(o

37、verload)可以用来区别不同函数参数的调用,但它还是不 能表示任意数量的函数参数。问题:printf的实现请问,如何自己实现printf函数,如何处理其中的可变参数问题?答案与分析:在标准C语言中定义了一个头文件stdarg.h专门用来对付可变参数列表,它包含了一组 宏,和一个va_list的typedef声明。一个典型实现如下:typedef char* va_list; ttdefine va_start(list) list = (char*)&va_alistttdefine va_end(list)ftdefine va_arg(list, mode)(mode*) (list

38、+= sizeof(mode)-1自己实现printf:include int printf (char* format, ,)va_list ap;va_start(ap, format);int n = vprintf(format, ap);va_end(ap);return n;)问题:运行时才确定的参数有没有办法写一个函数,这个函数参数的具体形式可以在运行时才确定?答案与分析:目前没有正规”的解决办法,不过独门偏方倒是有一个,因为有一个函数已经给我们做出 了这方面的榜样,那就是main。,它的原型是:int main(int argc, char *argv口);函数的参数是argc

39、和argvo深入想一下,”只能在运行时确定参数形式”,也就是说你没办法从声明中看到所接受的参 数,也即是参数根本就没有固定的形式。常用的办法是你可以通过定义一个void*类型的参数, 用它来指向实际的参数区,然后在函数中根据根据需要任意解释它们的含义。这就是main函数 中argv的含义,而argc,则用来表明实际的参数个数,这为我们使用提供了进一步的方便,当 然,这个参数不是必需的。虽然参数没有固定形式,但我们必然要在函数中解析参数的意义,因此,理所当然会有一 个要求,就是调用者和被调者之间要对参数区内容的格式,大小,有效性等所有方面达成一致, 否则南辕北辙各说各话就惨了。问题:可变长参数的

40、传递有时候,需要编写一个函数,将它的可变长参数直接传递给另外的函数,请问,这个要求 能否实现?答案与分析:目前,你尚无办法直接做到这一点,但是我们可以迂回前进,首先,我们定义被调用函数 的参数为va_list类型,同时在调用函数中将可变长参数列表转换为vaist,这样就可以进 行变长参数的传递了。看如下所示:void subfunc (char *fmt, va_list argp)arg = va_arg (fmt, argp); /*从argp中逐一取出所要的参数*/ void mainfunc (char *fmt, )va_list argp;va_start (argp, fmt);

41、 /* 将可变长参数转换为 va_list */ subfunc (fmt, argp); /* 将 va_list 传递给子函数 */ va_end (argp);问题:可变长参数中类型为函数指针我想使用va_arg来提取出可变长参数中类型为函数指针的参数,结果却总是不正确,为什么?答案与分析:这个与va_arg的实现有关。一个简单的、演示版的va_arg实现如下:#define va_arg(argp, type) (*(type *)(argp) += sizeof(type) - sizeof(type)其中,argp的类型是char *o如果你想用va_arg从可变参数列表中提取出函

42、数指针类型的参数,例如int (*)(),则 va_arg (argp, int (*)()被扩展为:(*(int (*) () *) (argp) += sizeof (int (*) () -sizeof (int (*)()显然,(int (*) () *)是无意义的。解决这个问题的办法是将函数指针用typedef定义成一个独立的数据类型,例如: typedef int (*funcptr)();这时候再调用va_arg(argp, funcptr)将被扩展为:(* (funcptr *)(argp) += sizeof (funcptr) - sizeof (funcptr)这样就可以

43、通过编译检查了。问题:可变长参数的获取有这样一个具有可变长参数的函数,其中有下列代码用来获取类型为float的实参:va_arg (argp, float);这样做可以吗?答案与分析:不可以。在可变长参数中,应用的是加宽”原则。也就是float类型被扩展成double; char, short被扩展成into因此,如果你要去可变长参数列表中原来为float类型的参数,需要用 va_arg (argp, double)。对 char 和 short 类型的则用 va_arg (argp, int)。问题:定义可变长参数的一个限制为什么我的编译器不允许我定义如下的函数,也就是可变长参数,但是没有任

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

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

46、在栈 中,而是放在程序中一个叫“.bss”段的地方,这个地方的内容是不会因为函数退出而被收回 的。这种办法确实能解决问题,但是这种办法同时也导致了 itoa()函数变成了一个不可重入的 函数(即不能保证相同的输入肯定有相同的输出),另外,retbuf 中的内容会被下一次的 调用结果所替代,这种办法不值得推荐。2)、在itoa()函数内部用mallocO为retbuf申请内存,并将结果存放其中,然后将retbuf 返回给调用者。由于此时retbuf位于堆(heap)中,也不会随着函数返回而程放,因此可以达 到我们的目的。但是有这样一种情况需要注意:itoa()函数的调用者在不需要retbuf的时

47、候必须把它释 放,否则就造成内存泄漏了,如果此函数和调用函数都是同一个人所写,问题不大,但如果不 是,则比较容易会疏漏此释放内存的操作。3)、将函数定义为char *itoa (int n, char *retbuf),且retbuf的空间由调用者申请 和释放,itoa()只是将转换结果存放到retbuf而已。这种办法明显比第一、二种方法要好,既避免了方法1对函数的影响,也避免了方法2对 内存分配释放的影响,是目前一种比较通行的做法。扩展分析:其实就这个问题本身而言,我想大家都可以立刻想到答案,关键在于对内存这种敏感资源 的正确和合理地利用,下面对内存做一个简单的分析:1)、程序中有不同的内存

48、段,包括:.data -己初始化全局/静态变量,在整个软件执行过程中有效;. bss -未初始化全局/静态变量,在整个软件执行过程中有效;.stack -函数调用栈,其中的内容在函数执行期间有效,并由编译器负责分配和收回;.heap-堆,由程序显式分配和收回,如果不收回就是内存泄漏。2)、自己使用的内存最好还是自己申请和释放。这可以说是一个内存分配和释放的原则,比如说上面解决办法的第二种,由itoa()分配的 内存,最后由调用者释放,就不是一个很好的办法,还不如用第三种,由调用者自己申请和释 放。另外这个原则还有一层意思是说:如果你要使用一个指针,最好先确信它已经指向合法内 存区了,如果没有就

49、得自己分配,要不就是非法指针访问。很多程序的致命错误都是访问一个 没有指向合法内存区的指针,这也包括空指针。问题:内存分配& sizeof我使用sizeof来计算一个指针变量,我希望得到这个指针变量所分配的内存块的大小,可 以吗?Char *p = NULL;int nMemSize = 0; p = malloc(1024);nMemSize = sizeof(p);答案与分析:答案是达不到你的要求,sizeof只能告诉你指针本身占用的内存大小。指针所指向的内存,如果是malloc分配的,sizeof是没有办法知道的。换句话说,malloc分配的内存是没有办法 向内存管理模块进行事后查询的,

50、当然你可以自己编写代码来维护。问题:栈内存使用下面程序运行有什么问题?char *GetString(void)char p = *hello world*;return p;/编译器将提出警告void Test4(void)char *str = NULL;str = GetStringO :/ str 的内容是垃圾cout str endl;答案与分析:返回栈内存,内存可能被销毁,也可能不被销毁,但是,出了作用域之后已被标记成可被 系统使用,所以,乱七八糟不可知内容,当然,返回的指针的内容,应该是不变的,特殊时候 是有用的,比如,可以用来探测系统内存分配规律等等。问题:内存使用相关编程规范

51、我想尽可能地避免内存使用上的问题,有什么捷径吗?答案与分析:除非做一件从没有人做过的事情,否则,都是有捷径可言的,那就是站在前人的肩膀上,现在各个大公司都有自己的编码规范,这些规范凝聚了很多的经验和教训,有较高的使用价值, 鉴于这些规范在网上流传很多,这里我就不再列出了,感兴趣的,推荐参考林锐的高质量C/C+ 编程指南。水滴石穿C语言之声明的语法概述在很多情况下,尤其是读别人所写代码的时候,对c语言声明的理解能力变得非常重要, 而C语言本身的凝练简约也使得C语言的声明常常会令人感到非常困惑,因此,在这里我用一 篇的内容来集中阐述一下这个问题。问题:声明与函数有一段程序存储在起始地址为0的一段内存上,如果我们想要调用这段程序,请问该如何 去做?答案答案是(*(void (*)( ) )0)( )o看起来确实令人头大,那好,让我们知难而上,从两个不 同的途径来详细分析这个问题。答案分析:从尾到头首先,最基本的函数声明:void function (paramList);最基本的函数调用:function(paramList);鉴于问题中的函数没有参数,函数调用可简化为functionO ;其次,根据问题描述,可以知道。是这个函数的入口地址,也就是说,0是一个函数的指针。

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

copyright@ 2023-2025  zhuangpeitu.com 装配图网版权所有   联系电话:18123376007

备案号:ICP2024067431-1 川公网安备51140202000466号


本站为文档C2C交易模式,即用户上传的文档直接被用户下载,本站只是中间服务平台,本站所有文档下载所得的收益归上传人(含作者)所有。装配图网仅提供信息存储空间,仅对用户上传内容的表现方式做保护处理,对上载内容本身不做任何修改或编辑。若文档所含内容侵犯了您的版权或隐私,请立即通知装配图网,我们立即给予删除!