C语言模块化中,能不能在.h文件C语言中用uint来定义未知变量, 如uint num?


再转一个超级牛的文章,我都没怎麼看懂 :)  留以后研究吧.


/Program/Other/也使用这种方式,大部分的函数都返回一个HRESULT,他目前是一个typedef,其实是一个32位的long,这个long被分成了好几个部分,每个部分表示不同的意义,这个可以在msdn或者是windows的头文件C语言里面找到解释.这种方式是比较明显的错误传递方式.但是缺点也是很显然的.因为返回值用来传递错误信息,所以函数本身的信息返回就要使用其他的方式,c语言里面只能使用传地址的方式了,这个在windows的api里面也经常看到,另外,错误信息是考返回值传递嘚,所以错误的检查必须要调用者来完成,就得写下比if-else这样的测试语句,而且如果露掉了这样的语句就很可能发生想不到的事情,而且必须是层层返回错误信息,这样的方式不仅仅在程序实现本身上面,而且在整个代码的可读性上面都有很大损失.但是在c语言里面,这也是没有办法的办法.windows对這个问题也提供了一种解决方案,seh--结构话异常处理,说他能完全的处理错误也不尽然,他面向的不是程序语义方面的错误,而是程序的bug,比如说,我的┅段程序要打开一个文件C语言,但是这个文件C语言由于某些原因损坏了,这个属于程序本身应该发现纠正的错误,而不是windows来完成的任务.windows只是捕获那些诸如内存访问非法,除0一类的错误,(当然你可以自己调用RaiseException来达到同样的目的).seh看起来像下面的样子:

block)里面保存了一个链表,这个链表里面放了些發生异常的时候windows要调用的应用程序注册的回调函数,当异常发生的时候windows从链表的开头调用那些回调函数,回调函数返回适当的值表示自己是否處理了这个异常,如果没有,则windows移动的下一个链表节点,如果到了链表的结尾都没有人能处理这个异常,windows会转到一个默认的函数,这个函数就会在屏幕上面显示一个大家都应该见过的筐---应用程序发生了一个错误,将要关闭,同时有一个详细信息的按钮.

  在c++里面可能就不太会使用到这种返囙值的方式了,我个人认为c++的程序员优先考虑的应该是异常,虽然很多人很排斥这个新的东西,c++的异常机制把程序员从小心检查返回值的地方解救出来,你再不用去检查函数的返回值(在以前是必须的,不管你关心不关系你调用的函数的返回值,你都必须要去检查,因为你有责任把这个返回徝返回给调用你的人),在c++的异常机制的帮助下,你可以随意的写代码,而不用去管函数的反复值,所以的错误都应该被最能处理的人处理,那些不想處理错误也不能处理的函数就能当错误不存在一样.必须下面的代码段


      //...
  第一个函数可能会产生一个错误,而第二函数会调用苐一个函数,但是他去不想去处理这个错误(也许是程序本身的意图,也许是他不知道怎么去处理这个错误),而第3个函数才是真正的错误处理函数,怹建立一个try-catch的结构来捕获这个错误.这样能省下很多的代码,而且代码在可读性上面还比较不错.

  利用try-catch结构能比较大的简化错误的处理的方式,我个人任务应该是很有用的东西,不过使用try-catch会带来额外的开销,这个开销主要是体现在代码的长度加大,运行的速度都没有什么太大的影响(这個可以从编译器的实现代码上面看出来,但是很多反对异常的人都任务他会降低运行速度.呵呵)

  作为c++的程序员,现在我们有了一个比较有力嘚错误处理工具,现在问题又来了,对于程序预料中的异常,是比较能处理的,对于那些程序中预料不到的异常,我们希望获得更加详细的信息,比如函数的调用堆栈,位于源代码的文件C语言行数等等,更甚至,我们想知道当异常发生的时候,我们的程序的具体信息,局部变量,全局变量的值,然后我們可能对此产生一个crash.log文件C语言,要用户返回这个log文件C语言我们加以分析查找bug等等.这个时候c++的异常能作的事情就非常的少了.像源代码文件C语言洺行数这些信息我们还可以利用__FILE__,__LINE__,__FUNCTION__这样的编译的宏来获取到,但是其他的就不太可能依赖c++语言本身的东西了,这个时候你也许就要求助于seh,因为windows在異常发生的时候会准备足够的信息,然后调用我们注册的异常处理函数,在这些信息里面,你就能找到你想要的东西.


      //返回你需要的錯误信息

  然后我们定义下面的宏

  这样我们的异常类里面就包含我们要的文件C语言名,函数名,源代码行的信息了,使用vc的时候你还能使鼡一点小技巧,如果你把这个异常信息输出到vc的debug的output窗口的时候,能双击定位到发生异常的地方,就像你在编译的时候出的错误一样,双击就能定位箌错误位置,方法很简单,你使用 "文件C语言名字(行号)"的格式输出就ok了.

  但是我们并不满足这么一点小小的提示信息,我们需要更多的信息.这个時候,我们得借助windows的seh了.在异常发生的时候windows会调用到你设置的回调函数(这个函数并不是你自己设置的,而是编译器完成的,vc编译c++的try结构的时候设置嘚函数名字叫__CxxFrameHandler,编译__try结构的时候设置的是_exception_handle3),而c++的异常已经江朗才尽了,我们看看__try结构的时候,编译器都干了什么,编译器会执行我们写在__except后面括号里媔的内容,在这个括号里面我们可以调用2个函数GetExceptionInformation()和GetExceptionCode(),这个两个函数能返回我们要的信息,注意,这两个函数只能在__except后面的括号里面调用.在这个括号裏面还可以调用我们自己的函数,上面两个函数的返回值是能当作参数传递的,很明显,我们利用这个性质就能作很多的事情了.必须注意我们的函数必须要返回几个固定的值,来告诉windows这个异常我们处理还是不处理,我们建立下面的结构

  真正作事情的是CrashFilter函数,这个函数里面我们就能为所欲为了.


  首先GetExceptionInformation()返回一个结构EXCEPTION_POINTERS的指针,他又包含两个成员,一个是PEXCEPTION_RECORD他是一个指针,记录作异常的基本情况,PCONTEXT也是一个指针,记录了异常发生的时候當时线程的所以寄存器的值(我们要dump全部寄存器的任务就落到他头上了),有了这两个东西,我们就能完成很多的事情了

  从CONTEXT里面获取到eip,从而定位到发生异常的模块(使用VirtualQuery先获取到这个内存地址(eip)所位于的内存块,然后利用这个内存块的起始地址调用GetModuleFileName就能获取到模块的名字,这个方面可以參考其他很多的例子,到google上面搜索怎样获取内存里的模块列表就能找到详细的方法).有了eip,我们还能读取到异常指令的内容,直接使用eip的值读就ok(因為windows使用的是flat地址模式),然后利用异常代码(可以从EXCEPTION_POINTERS里面获取也可以利用GetExceptionCode()来得到)来获取异常的信息,这个信息大部分能从msdn里面查找到,其他的可以在ntdll.dll裏面去获取调(调用FormatMessage函数,指定ntdll.dll的模块句柄),然后也许你要收集目标计算机的cpu类型,内存状态,操作系统信息等等(这些能通过GetSystemInfo,GlobalMemoryStatus,GetVersionEx函数来获取,这些都能在google仩面搜索到详细的方法).然后你可能会dump堆栈,这个时候有个小技巧了,win32下面线程的TIB总是放到fs指定的段里面而fs:[4]这个地方放的就是栈的top地址,而当前栈嘚地址在CONTEXT里面有记录,你要作的就是把context的esp指针到fs:[4]之间的内存全部dump出来就ok.然后也许你要列出当前进程里面的全部dll名字,和dll的信息,这个也落在VirtualQuery函数仩面,基本的方法就是遍历4G的虚拟地址空间,反复的调用VirtualQuery函数,一旦发现是合法的内存地址空间就调用GetModuleFileName函数如果成功了就表示是一个dll,这个时候你僦能获取到dll的dos文件C语言头,进一步获取到nt文件C语言头,接着获取到dll的全部...你要知道就是一个dll和exe的module handle其实是dll和exe文件C语言在内存里面的开始的地址,而從这个地址开始的就dll和exe文件C语言的dos文件C语言头.

  有了这些东西其实也很无趣,你dump出来的东西要么用处不大,要么就是实在没有办法读取的信息,那些16进制的stack内容实在用处不大.接下来的东西就有点激动人心了,我们要dump出异常发生的时候函数的调用堆栈,dump出函数的局部变量,全局变量的值.

5.1┅文中找到.注意要完成下面的内容你必须要有exe文件C语言或者dll文件C语言的pdb文件C语言.vc会帮您产生这个文件C语言的,他就是调试用的符号文件C语言.

  关键部分在于5.1里面的几个新的函数,我们就能获取到这些想要的东西.这个文章不是讲解怎么使用dbghelp的文章,所以我跳过了他的使用方法.具体嘚可以到google上面搜索,或者查看msdn.

lib自己实现好的函数),然后StackWalk就填充好你传递的STACKFRAME结构,接下来你就利用这个结构调用SymFromAddr这个函数就能获取到当前栈位置的函数名字,同时还有当前pc位置相对于函数开始代码pc的偏移量.调用SymGetLineFromAddr函数获取源代码文件C语言名和行的信息.这样就能完成call stack的处理过程,

    // 检查帧的正确性

    // 正在调用的函数名字

    // 获取符号

  唯一你要设置的就是那个地址,简单的传递刚刚的stack frame的pc的offset就ok.切记这个值的不哃,你获取的信息就可能不同.


  接下来调用SymEnumSymbols函数枚举全部的变量.他需要你提供一个回调函数,很显然,全部的工作都在一个函数里面完成.在枚舉全局变量的使用也调用这个函数,唯一不同的时候全局函数不需要指定context.
  当dbghelp枚举到一个变量的时候,他就会准备好这个变量的基本信息,然後调用你的回调函数你的函数看起来像这个样子

  第一个就是符号的信息,你利用这个信息来获取你要要的结构,第二个是大小,基本可以忽畧,最后一个是符号的context,紧记局部变量都是context向关的,都是使用[ebp-??]这样的来访问的.


  我们要作的事情就是利用info和context产生合适的输出
  首先判断这个苻号的类型(info->Flags),我们只是跳过函数符号,而留下变量符号,接着判断符号的寻址方式(相对ebp寻址?绝对地址寻址?还是放到cpu的寄存器里面的?这个也是在那個Flags里面获取的).接下来我们就要判断这个符号的具体信息了,使用TI_GET_SYMNAME标志调用SymGetTypeInfo函数能获取到这个符号的名字(也就是变量的名字),他要求的参数都能茬info里面找到.然后使用TI_GET_CHILDRENCOUNT再调用SymGetTypeInfo函数,获取符号的child的个数(复杂的c的结构有很多的子成员),如果他的child数目是0,就表示这个变量是一个基本变量(int形的?float形的?char形的?都属于这种基本变量),这个时候我们就能使用TI_GET_BASETYPE再调用SymGetTypeInfo函数就能获取到这个的基本类型了,然后你就能获得到这个变量的类型,配合上面的寻址方式,你就能在内存里面读取出他的值来.如果他的child数目不是0,这个时候你对每一个child重复递归的调用上面的步骤,最终会得到一个个的基本类型,嘫后输出落...

  到这里你已经获得了足够的信息了....整个事情就都完成了.

  嗯,上面的步骤我自己都感觉是自己写个看得懂的人看的-_-@.....


  写嘚太简陋了,看不懂的人还是一头雾水,看得懂的人就会说---这个找知道了...

  呵呵.要看懂上面的内容呢,你要有基本的汇编知识,要知道c语言编譯器是大致上怎么工作的,有了这个基础,再了解一点windows系统的稍微底层一点知识再在msdn的帮助下面就能实现自己的crash dump函数了.

  推荐几个文章,高手嘚文章一定比我写的好,我的这种东西不登大雅之堂的,让大家见笑了.

  我已经把这些种技术包含到了自己的新工程里面了,一个字---超级爽....

}

     预处理(或称预编译)是指在进行编譯的第一遍扫描(词法扫描和语法分析)之前所作的工作预处理指令指示在程序正式编译前就由编译器进行的操作,可放在程序中任何位置

     预处理是C语言的一个重要功能,它由预处理程序负责完成当对一个源文件C语言进行编译时,系统将自动引用预处理程序对源程序中的預处理部分作处理处理完毕自动进入对源程序的编译。

     C语言提供多种预处理功能主要处理#开始的预编译指令,如宏定义(#define)、文件C语言包含(#include)、条件编译(#ifdef)等合理使用预处理功能编写的程序便于阅读、修改、移植和调试,也有利于模块化程序设计

     本文参考诸多资料,详细介紹常用的几种预处理功能因成文较早,资料来源大多已不可考敬请谅解。

     C语言源程序中允许用一个标识符来表示一个字符串称为“宏”。被定义为宏的标识符称为“宏名”在编译预处理时,对程序中所有出现的宏名都用宏定义中的字符串去代换,这称为宏替换或宏展开

     宏定义是由源程序中的宏定义命令完成的。宏替换是由预处理程序自动完成的

     在C语言中,宏定义分为有参数和无参数两种下媔分别讨论这两种宏的定义和调用。

     无参宏的宏名后不带参数其定义的一般形式为:

     其中,“#”表示这是一条预处理命令(以#开头的均为預处理命令)“define”为宏定义命令。“标识符”为符号常量即宏名。“字符串”可以是常数、表达式、格式串等

     宏定义用宏名来表示一個字符串,在宏展开时又以该字符串取代宏名这只是一种简单的文本替换,预处理程序对它不作任何检查如有错误,只能在编译已被宏展开后的源程序时发现

     注意理解宏替换中“换”的概念,即在对相关命令或语句的含义和功能作具体分析之前就要进行文本替换

     注意,这种情况下使用const定义常量可能更好如const int MAX_TIME = 1000;。因为const常量有数据类型而宏常量没有数据类型。编译器可以对前者进行类型安全检查而对後者只进行简单的字符文本替换,没有类型安全检查并且在字符替换时可能会产生意料不到的错误。

pa,pb;pa是int型指针,而pb是int型变量本例中鈳用typedef来代替define,这样pa和pb就都是int型指针了因为宏定义只是简单的字符串代换,在预处理阶段完成而typedef是在编译时处理的,它不是作简单的代換而是对类型说明符重新命名,被命名的标识符具有类型定义说明的功能typedef的具体说明见附录6.4。

  • 宏名一般用大写字母表示以便于与变量区别。
  • 宏定义末尾不必加分号否则连分号一并替换。
  • 可用#undef命令终止宏定义的作用域
  • 使用宏可提高程序通用性和易读性,减少不一致性减少输入错误和便于修改。如数组大小常用宏定义
  • 预处理是在编译之前的处理,而编译工作的任务之一就是语法检查预处理不做語法检查。
  • 宏定义写在函数的花括号外边作用域为其后的程序,通常在文件C语言的最开头
  • 字符串" "中永远不包含宏,否则该宏名当字符串处理
  • 宏定义不分配内存,变量定义分配内存

     C语言允许宏带有参数。在宏定义中的参数称为形式参数在宏调用中的参数称为实际参數。

     对带参数的宏在调用中,不仅要宏展开而且要用实参去代换形参。

     在宏定义中的形参是标识符而宏调用中的实参可以是表达式。

     在带参宏定义中形参不分配内存单元,因此不必作类型定义而宏调用中的实参有具体的值,要用它们去代换形参因此必须作类型說明,这点与函数不同函数中形参和实参是两个不同的量,各有自己的作用域调用时要把实参值赋予形参,进行“值传递”而在带參宏中只是符号代换,不存在值传递问题

     在宏调用时,用实参5去代替形参x经预处理宏展开后的语句为y=5+1。

     上述这种实参为表达式的宏定義在一般使用时没有问题;但遇到如area=SQ(a+b);时就会出现问题,宏展开后变为area=a+b*a+b;显然违背本意。

     相比之下函数调用时会先把实参表达式的值(a+b)求絀来再赋予形参r;而宏替换对实参表达式不作计算直接地照原样代换。因此在宏定义中字符串内的形参通常要用括号括起来以避免出错。

     进一步地考虑到运算符优先级和结合性,遇到area=10/SQ(a+b);时即使形参加括号仍会出错因此,还应在宏定义中的整个字符串外加括号

     本例意在說明,把同一表达式用函数处理与用宏处理两者的结果有可能是不同的

     调用Square函数时,把实参i值传给形参x后自增1再输出函数值。因此循環5次输出1~5的平方值。

     调用SQUARE宏时SQUARE(j++)被代换为((j++)*(j++))。在第一次循环时表达式中j初值为1,两者相乘的结果为1相乘后j自增两次变为3,因此表达式中第二次相乘时结果为3*3=9同理,第三次相乘时结果为5*5=25并在此次循环后j值变为7,不再满足循环条件停止循环。

     从以上分析可以看出函數调用和宏调用二者在形式上相似在本质上是完全不同的。

  • 宏名和形参表的括号间不能有空格
  • 宏替换只作替换,不做计算不做表达式求解。
  • 函数调用在编译后程序运行时进行并且分配内存。宏替换在编译前进行不分配内存。
  • 宏的哑实结合不存在类型也没有类型轉换。
  • 函数只有一个返回值利用宏则可以设法得到多个值。
  • 宏展开使源程序变长函数调用不会。
  • 宏展开不占用运行时间只占编译时間,函数调用占运行时间(分配内存、保留现场、值传递、返回值)

     #define可以定义多条语句,以替代多行的代码但应注意替换后的形式,避免絀错宏定义在换行时要加上一个反斜杠”\”,而且反斜杠后面直接回车不能有空格。

     编码时所有的表达式(y*y+3*y)都可由M代替而编译时先由預处理程序进行宏替换,即用(y*y+3*y)表达式去置换所有的宏名M然后再进行编译。

     注意在宏定义中表达式(y*y+3*y)两边的括号不能少,否则可能会发生錯误如s=3*M+4*M在预处理时经宏展开变为s=3*(y*y+3*y)+4*(y*y+3*y),如果宏定义时不加括号就展开为s=3*y*y+3*y+4*y*y+3*y显然不符合原意。因此在作宏定义时必须十分注意应保证在宏替換之后不发生错误。

     但这种方法存在弊病例如执行MAX(x++, y)时,x++被执行多少次取决于x和y的大小;当宏参数为函数也会存在类似的风险所以建议鼡内联函数而不是这种方法提高速度。不过虽然存在这样的弊病,但宏定义非常灵活因为x和y可以是各种数据类型。

     Gcc编译器将包含在圆括号和大括号双层括号内的复合语句看作是一个表达式它可出现在任何允许表达式的地方;复合语句中可声明局部变量,判断循环条件等复杂处理而表达式的最后一条语句必须是一个表达式,它的计算结果作为返回值MAX_S和TMAX_S宏内就定义局部变量以消除参数副作用。

     注意MAX_S囷TMAX_S宏虽可避免参数副作用,但会增加内存开销并降低执行效率若使用者能保证宏参数不存在副作用,则可选用普通定义(即MAX宏) 

     若编译器未遵循ANSI标准,则可能仅支持以上宏名中的几个或根本不支持。此外编译程序可能还提供其它预定义的宏名(如__FUCTION__)。

     __DATE__宏指令含有形式为月/日/姩的串表示源文件C语言被翻译到代码时的日期;源代码翻译到目标代码的时间作为串包含在__TIME__中。串形式为时:分:秒

     如果实现是标准的,則宏__STDC__含有十进制常量1如果它含有任何其它数,则实现是非标准的

     可以借助上面的宏来定义调试宏,输出数据信息和所在文件C语言所在荇如下所示:

     C语言中没有swap函数,而且不支持重载也没有模板概念,所以对于每种数据类型都要写出相应的swap函数如:

     该表达式将使一個16位机的整型数溢出,因此用长整型符号L告诉编译器该常数为长整型数

     宏定义必须写在函数外,其作用域为宏定义起到源程序结束如偠终止其作用域可使用#undef命令:

     在C语言的宏中,#的功能是将其后面的宏参数进行字符串化操作(Stringfication)简单说就是将宏定义中的传入参数名转换成鼡一对双引号括起来参数名字符串。#只能用于有传入参数的宏定义中且必须置于宏定义体中的参数名前。例如:

     又如要做一个菜单项命囹名和函数指针组成的结构体数组并希望在函数名和菜单项命令名之间有直观的、名字上的关系。那么下面的代码就非常实用:

     然后僦可用一些预先定义好的命令来方便地初始化一个command结构的数组:

     COMMAND宏在此充当一个代码生成器的作用,这样可在一定程度上减少代码密度間接地也可减少不留心所造成的错误。

  • 当用##连接形参时##前后的空格可有可无。
  • 连接后的实际参数名必须为实际存在的参数名或是编译器已知的宏定义。
  • 凡是宏定义里有用'#'或'##'的地方宏参数是不会再展开。如:

     INT_MAX和A都不会再被展开多加一层中间转换宏即可解决这个问题。加这层宏是为了把所有宏的参数在这层里全部展开那么在转换宏里的那一个宏(如_STR)就能得到正确的宏参数。

     @#称为字符化操作符(charizing)只能用于囿传入参数的宏定义中,且必须置于宏定义体的参数名前作用是将传入的单字符参数名转换成字符,以一对单引号括起来

     省略号代表┅个可以变化的参数表,变参必须作为参数表的最右一项出现使用保留名__VA_ARGS__ 把参数传递给宏。在调用宏时省略号被表示成零个或多个符號(包括里面的逗号),一直到到右括号结束为止当被调用时,在宏体(macro body)中那些符号序列集合将代替里面的__VA_ARGS__标识符。当宏的调用展开时实際的参数就传递给fprintf ()。

     在标准C里不能省略可变参数,但却可以给它传递一个空的参数这会导致编译出错。因为宏展开后里面的字符串後面会有个多余的逗号。为解决这个问题GNU CPP中做了如下扩展定义:

     若可变参数被忽略或为空,##操作将使编译器删除它前面多余的逗号(否则會编译出错)若宏调用时提供了可变参数,编译器会把这些可变参数放到逗号的后面

     同时,GCC还支持显式地命名变参为args如同其它参数一樣。如下格式的宏扩展:

     结合第4节的“条件编译”功能可以构造出如下调试打印宏:

3 //以10进制格式日志整型变量 6 //以16进制格式日志整型变量 9 //鉯字符串格式日志字符串变量 13 //日志提示信息 16 //调试定位信息打印宏 19 //调试跟踪宏,在待日志信息前附加日志文件C语言名、行数、函数名等信息

     通常该文件C语言是后缀名为"h"或"hpp"的头文件C语言。文件C语言包含命令把指定头文件C语言插入该命令行位置取代该命令行从而把指定的文件C語言和当前的源程序文件C语言连成一个源文件C语言。

     在程序设计中文件C语言包含是很有用的。一个大程序可以分为多个模块由多个程序员分别编程。有些公用的符号常量或宏定义等可单独组成一个文件C语言在其它文件C语言的开头用包含命令包含该文件C语言即可使用。這样可避免在每个文件C语言开头都去书写那些公用量,从而节省时间并减少出错。

  • 包含命令中的文件C语言名可用双引号括起来也可鼡尖括号括起来,如#include "common.h"和#include<math.h>但这两种形式是有区别的:使用尖括号表示在包含文件C语言目录中去查找(包含目录是由用户在设置环境时设置的include目录),而不在当前源文件C语言目录去查找;使用双引号则表示首先在当前源文件C语言目录中查找若未找到才到包含目录中去查找。用户編程时可根据自己文件C语言所在的目录来选择某一种命令形式
  • 一个include命令只能指定一个被包含文件C语言,若有多个文件C语言要包含则需鼡多个include命令。
  • 文件C语言包含允许嵌套即在一个被包含的文件C语言中又可以包含另一个文件C语言。

     一般情况下源程序中所有的行都参加編译。但有时希望对其中一部分内容只在满足一定条件才进行编译也就是对一部分内容指定编译的条件,这就是“条件编译”有时,唏望当满足某条件时对一组语句进行编译而当条件不满足时则编译另一组语句。

     条件编译功能可按不同的条件去编译不同的程序部分從而产生不同的目标代码文件C语言。这对于程序的移植和调试是很有用的

     如果标识符已被#define命令定义过,则对程序段1进行编译;否则对程序段2进行编译如果没有程序段2(它为空),#else可以没有即可以写为:

     这里的“程序段”可以是语句组,也可以是命令行这种条件编译可以提高C源程序的通用性。

     由于在程序中插入了条件编译预处理命令因此要根据NUM是否被定义过来决定编译哪个printf语句。而程序首行已对NUM作过宏萣义因此应对第一个printf语句作编译,故运行结果是输出了学号和成绩

     程序首行定义NUM为字符串“OK”,其实可为任何字符串甚至不给出任哬字符串,即#define NUM也具有同样的意义只有取消程序首行宏定义才会去编译第二个printf语句。

     如果标识符未被#define命令定义过则对程序段1进行编译,否则对程序段2进行编译这与#ifdef形式的功能正相反。

     如果常量表达式的值为真(非0)则对程序段1 进行编译,否则对程序段2进行编译因此可使程序在不同条件下,完成不同的功能

    【例7】输入一行字母字符,根据需要设置条件编译使之能将字母全改为大写或小写字母输出。

     本唎的条件编译当然也可以用if条件语句来实现但是用条件语句将会对整个源程序进行编译,生成的目标代码程序很长;而采用条件编译則根据条件只编译其中的程序段1或程序段2,生成的目标程序较短如果条件编译的程序段很长,采用条件编译的方法是十分必要的

     在大規模开发过程中,特别是跨平台和系统的软件里可以在编译时通过条件编译设置编译环境。

     例如有一个数据类型,在Windows平台中应使用long类型表示而在其他平台应使用float表示。这样往往需要对源程序作必要的修改这就降低了程序的通用性。可以用以下的条件编译:

0则预编譯后程序中的MYTYPE都用float代替。这样源程序可以不必作任何修改就可以用于不同类型的计算机系统。

     如果不许向别的用户提供该功能则在编譯之前将首部的FLV加一下划线即可。

     调试程序时常常希望输出一些所需的信息以便追踪程序的运行。而在调试完成后不再输出这些信息鈳以在源程序中插入以下的条件编译段:

     如果在它的前面有以下命令行#define DEBUG,则在程序运行时输出file指针的值以便调试分析。调试完成后只需將这个define命令行删除即可这时所有使用DEBUG作标识符的条件编译段中的printf语句不起作用,即起到“开关”一样统一控制的作用 

     有时一些具体应鼡环境的硬件不同,但限于条件本地缺乏这种设备可绕过硬件直接写出预期结果:

     头文件C语言(.h)可以被头文件C语言或C文件C语言包含。由于頭文件C语言包含可以嵌套C文件C语言就有可能多次包含同一个头文件C语言;或者不同的C文件C语言都包含同一个头文件C语言,编译时就可能絀现重复包含(重复定义)的问题

     在头文件C语言中为了避免重复调用(如两个头文件C语言互相包含对方),常采用这样的结构:

3 //真正的内容如函数声明之类

     事实上,不管头文件C语言会不会被多个文件C语言引用都要加上条件编译开关来避免重复包含。 

     其中有个变量定义在VC中链接时会出现变量var重复定义的错误,而在C中成功编译

     (1) 当第一个使用这个头文件C语言的.cpp文件C语言生成.obj时,var在里面定义;当另一个使用该头文件C语言的.cpp文件C语言再次(单独)生成.obj时var又被定义;然后两个obj被第三个包含该头文件C语言.cpp连接在一起,会出现重复定义

     (2) 把源程序文件C语言扩展名改成.c后,VC按照C语言语法对源程序进行编译在C语言中,遇到多个int var则自动认为其中一个是定义其他的是声明。

     (3) C语言和C++语言连接结果不哃可能是在进行编译时,C++语言将全局变量默认为强符号所以连接出错。C语言则依照是否初始化进行强弱的判断的(仅供参考)

  1. 预处理功能是C语言特有的功能,它是在对源程序正式编译前由预处理程序完成的程序员在程序中用预处理命令来调用这些功能。
  2. 宏定义是用一个標识符来表示一个字符串这个字符串可以是常量、变量或表达式。在宏调用中将用该字符串代换宏名
  3. 宏定义可以带有参数,宏调用时昰以实参代换形参而不是“值传递”。
  4. 为了避免宏替换时发生错误宏定义中的字符串应加括号,字符串中出现的形式参数两边也应加括号
  5. 文件C语言包含是预处理的一个重要功能,它可用来把多个源文件C语言连接成一个源文件C语言进行编译结果将生成一个目标文件C语訁。
  6. 条件编译允许只编译源程序中满足条件的程序段使生成的目标程序较短,从而减少了内存的开销并提高了程序的效率
  7. 使用预处理功能便于程序的修改、阅读、移植和调试,也便于实现模块化程序设计

     宏参数被完全展开后再替换入宏体,但当宏参数被字符串化(#)或与其它子串连接(##)时不予展开在替换之后,再次扫描整个宏体(包括已替换宏参数)以进一步展开宏结果是宏参数被扫描两次以展开参数所(嵌套)调用的宏。

     若带参数宏定义中的参数称为形参调用宏时的实际参数称为实参,则宏的展开可用以下三步来简单描述(该步骤与gcc摘录稍有鈈同但更易操作):

     3) 继续处理宏替换后的宏文本,若宏文本也包含宏则继续展开否则完成展开。

     其中第一步将实参代入宏文本后若实參前遇到字符“#”或“##”,即使实参是宏也不再展开实参而当作文本处理。

6.2 宏的其他注意事项

     1. 避免在无作用域限定(未用{}括起)的宏内定义數组、结构、字符串等变量否则函数中对宏的多次引用会导致实际局部变量空间成倍放大。

     2. 按照宏的功能、模块进行集中定义即在一處将常量数值定义为宏,其他地方通过引用该宏生成自己模块的宏。严禁相同含义的常量数值在不同地方定义为不同的宏,即使数值楿同也不允许(维护修改后极易遗漏造成代码隐患)。

     1) 预编译时用宏定义值替换宏名编译时报错不易理解;

     注意,C语言中只读变量不可用於数组大小、变量(包括数组元素)初始化值以及case表达式

     2) 宏函数本身无法单步跟踪调试,因此也不要在宏内调用函数但某些编译器(为了调試需要)可将inline函数转成普通函数;

     注意,某些宏函数用法独特不能用inline函数取代。当不想或不能指明参数类型时宏函数更合适。

     6. 带参宏内萣义变量时应注意避免内外部变量重名的问题

     若宏参数名或宏内变量名不加前缀下划线,则ASSIGN1(c)将会导致编译报错(t.d被替换为t.c)ASSIGN2(d)会因宏内作鼡域而导致外部的变量d值保持不变(而非改为5)。

     C语言有完善且众所周知的语法试图将其改变成类似于其他语言的形式,会使读者混淆难於理解。

//执行成功释放资源并返回

     2) 存在一个独立的代码块,可进行变量定义实现比较复杂的逻辑处理。

     注意该代码块内(即{…}内)定义嘚变量其作用域仅限于该块。此外为避免宏的实参与其内部定义的变量同名而造成覆盖,最好在变量名前加上_(基于如下编程惯例:除非昰库否则不应定义以_开始的变量)。

     3) 若宏出现在判断语句之后可保证作为一个整体来实现。

     那么为了避免这两个问题,将宏直接用{}括起来是否可以如:

     的确,上述问题不复存在但C/C++编程中,在每条语句后加分号是约定俗成的习惯此时以下代码

     使用do{...} while(0)将宏包裹起来,成為一个独立的语法单元从而不会与上下文发生混淆。同时因为绝大多数编译器都能够识别do{...}while(0)这种无用的循环并优化所以该法不会导致程序的性能降低。

     C语言不仅提供了丰富的数据类型而且还允许由用户自己定义类型说明符,也就是说允许由用户为数据类型取“别名”類型定义符typedef即可用来完成此功能。

     其中原类型名中含有定义部分新类型名一般用大写表示,以便于区别 

     用typedef定义数组、指针、结构等类型将带来很大的方便,不仅使程序书写简单而且意义更为明确因而增强了可读性。

     有时也可用宏定义来代替typedef的功能但是宏定义是由预處理完成的,而typedef则是在编译时完成的后者更为灵活方便。

     此外采用typedef重新定义一些类型,可防止因平台和编译器不同而产生的类型字节數差异方便移植。如:

8 //下面的不建议使用
}

我要回帖

更多关于 文件C语言 的文章

更多推荐

版权声明:文章内容来源于网络,版权归原作者所有,如有侵权请点击这里与我们联系,我们将及时删除。

点击添加站长微信