水滴石穿C语言之C语言的底层操作2.docx

上传人:无*** 文档编号:87074199 上传时间:2023-04-16 格式:DOCX 页数:101 大小:168.83KB
返回 下载 相关 举报
水滴石穿C语言之C语言的底层操作2.docx_第1页
第1页 / 共101页
水滴石穿C语言之C语言的底层操作2.docx_第2页
第2页 / 共101页
点击查看更多>>
资源描述

《水滴石穿C语言之C语言的底层操作2.docx》由会员分享,可在线阅读,更多相关《水滴石穿C语言之C语言的底层操作2.docx(101页珍藏版)》请在taowenge.com淘文阁网|工程机械CAD图纸|机械工程制图|CAD装配图下载|SolidWorks_CaTia_CAD_UG_PROE_设计图分享下载上搜索。

1、水滴石穿C语言之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都不合法。注意即使腾空位填符号位,有符号整数的右移也不相当与除以。为了证明这一点,我们可以想一下-

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

4、on: 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 add

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

6、stu2)是多少?怎么还是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,而且按照以前的理解int a的 地址和s2的地址相差5个字节,但是为了满足第而个条件(相差的距离偏移地址必须是4 的整数倍)所以在char b的后面添加了三个字节的空间以保证int a的偏移地址是4的整数倍即 为4。至于涉及到结构体嵌套的问题,你也可以用上述方法

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

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

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

12、 (4*3)testl在内存中的排列如下(FF为padding ):00 04 08 12 01 FF FF FF 01 01 01 01 01 FF FF FFchi - lol - ch2ttpragma pack(2) 指定 Align 为 2struct STest2(char 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

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

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

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

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

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

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

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

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

21、f _cplusplusextern Cttendifttendif /* _cplusplus */.h文件结束的地方ttifdef _cplusplus#if _cplusplus)#endifttendif /* _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 (

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

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

27、一个数组a定义在fl. 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 list(char

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

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

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

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

33、har *str = 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. hint 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+中有函数重载(overload)可以用来区别不同函数参数的调用,但它还是不 能表示任意数量的函数参数。问题:printf的实现请问,如何自己实现printf函数,如何处理其中的可变参数问题?答案与分析:在标准C

展开阅读全文
相关资源
相关搜索

当前位置:首页 > 教育专区 > 教案示例

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

工信部备案号:黑ICP备15003705号© 2020-2023 www.taowenge.com 淘文阁