C语言之预处理
这是2016年的最后一篇博客,年初定的计划是写12篇博客,每月一篇,1/3转载,2/3原创,看来是实现不了了! -- 题外话。今天要写的东西是C语言中的预处理器,我们常说的宏定义的用法。为什么要写这个东西呢,原因很简单:之前对预处理了解不深。如果你对C语言只是了解或者是仅仅在大学中学习过C语言,说到预处理估计你只知道下面这条语句:(因为我就是这种情况,哈哈!)
#define name value
我再学习预处理直接的驱动力是看了php的源码,开头一大推的宏定义器,之前'掌握'的一点#define的用法太少了,根本看不懂源码中宏的处理逻辑和运行的路径。所以再学习预处理器很有必要,里面好多东西其实并不难,只是你没有接触到,等你学习了,就感觉容易了。
一、宏定义和使用中的坑
这小节采用先给代码再说明的形式,这样你可以看看每个代码的运行结果是否和你预期的一致!
宏是什么,宏就是#define机制把指定的参数替换的文本中,这样的实现方式就是宏。使用宏定义可以抽出频繁调用的函数,加快执行的速度。定义如下:#define name(参数) 执行体... “参数”可以是使用逗号分隔的参数列表,这些参数可以被应用到执行体中,必须要注意的是“参数”的左括号必须和宏名字紧邻,不然编辑器会报错,或者被解释成执行体中的一部分。比如你写了一个 TEST(a) a * a 调用执行的时候写上 TEST(1) 实际执行的是替换后的 1 * 1。
凡事都有利弊,宏定义固然使用方便,并且有着函数不可比拟的执行速度,但是宏定义中存在不少的坑,下面就说一说这个坑。看下面的代码:
#include <stdio.h> #define TEST(a) a * a int main() {
int b = TEST();
int c = TEST(+);
printf("b=%d, c=%d", b, c);
printf("\n\n");
}
没有执行的情况下,你感觉得到的结果是多少呢!好多人不加思索的说:b=4,c=9。如果真是这样,就不存在坑了,实际打印出来是:b=4, c=5 ,为什么c的值和预想的会有偏差,其实你把执行体中的值替换一下试试,就不难发现问题了,当输入1+2的时候,宏替换成了 1+2*1+2,当然就是5了。好了明白了,那你学会了吗?学会了再看一个:
#include <stdio.h> #define TEST(a,b) ((a) > (b) ? (a) : (b)) int main() {
int zyf = ;
int abc = ;
int ret = TEST(zyf++, abc++);
printf("zyf=%d,abc=%d,ret=%d", zyf, abc, ret);
printf("\n\n");
}
输出多少呢,如果是 zyf=2,abc=3,ret=3 就错了,实际结果是:zyf=2,abc=4,ret=3 。道理和前面的一样,只看替换后的结果才能真正看到答案。
这样的问题防不胜防,怎样才能解决呢,其实办法很简单,错误的原因是执行的顺序和我们预想的不一样,那添加小括号应该可以解决这种问题。 比如 (a) * (a)。这样其实也不是最万全的办法,比如你看这个:ADD(a) (a) + (a) ,如果这样调用:ADD(2) * 5 ,这样又不行了,被替换成了 (a) + (a) * 5 执行顺序和预想的还是不一样,所以还要在最外层加上括号:((a) + (a)),这样就解决了。
二、预定义符号
C语言中有几个预定义的符号,还是有必要和大家说上一说,先看一段代码:
#include <stdio.h>
#include <stdlib.h>
#define VAR_DUMP printf( \
"[\n \tfile:%s\n" \
"\tline:%d\n" \
"\ttime:%s %s\n" \
"\tvalue:%d\n]", \
__FILE__, __LINE__, __DATE__, __TIME__, value \
)
int main() {
int value = ;
VAR_DUMP;
printf("\n\n");
}
是不是和你在大学学习的有点不一样,最简单的宏定义可以使用#define name value 的方式,当然也可以把值写成一个函数,运行的时候直接替换函数。这个宏定义是封装了调试方法,是打印变量内容能像PHP中var_dump()或者print_r()函数一样,打印出变量的内容。
从这段代码中能学习到几点内容:
1、使用#define可以使任何文本替换到程序中,在主程序中你可以随意使用VAR_DUMP。
2、宏定义不以分号结束,如果非常长的宏定义,你可以在末尾加上反斜杠来分行,保持代码易读性。
3、你可以定义频繁调用的函数为宏定义,这样可以加快执行的速速,具体原因后面会说到。
4、C语言有几个预定的符号需要我们知道,很多时候特别有用:
__FILE__ 预编译的文件名
__LINE__ 文件当前行的行号(执行到这一行)
__DATE__ 文件编译的日期
__TIME__ 文件编译的具体时间
__STDC__ 是否遵循ANSI C (不常用)
最后附上运行结果,如图:
三、宏替换的过程
在程序的编译阶段,宏先被执行替换,一般要涉及下面的步骤:
1、调用宏的地方看是否 进行了 #define定义,如果是就进行替换。
2、把替换的文本信息插入到替换的位置,其中参数被替换成了实际的值。
3、#define可以包含其他定义的#define定义的东西,需要注意的是不能出现递归的情况。
因为替换存在临近字段自动结合,所以可以使用一些巧妙的方案:
#include <stdio.h>
#include <stdlib.h> #define VAR_DUMP(A,B)\
printf("Value of " #B " is " A "\n", B) int main(){
int x = ;
VAR_DUMP("%d", x+);
}
四、条件编译和其他宏用法
在大型的C程序中你能看到许多的条件编译,比如可以根据当前的环境加载不同的宏配置,或者在编译的时候加上直极预设的编译条件。这些东西的实现都离不开条件编译。
1、条件嵌套,#if #endif 原型:
#if condition
执行体
#endif
可以根据condition来确定执行体要不要执行,以此来控制在不同的环境下编译成不同的系统。看下面的代码,当把DEBUG定义成非0值时,MAX宏定义是存在的,当定义成0时,程序就会报错。
#include <stdio.h> #define DEBUG 0
#if DEBUG
#define MAX(a) ((a) * (a))
#endif int main() {
int b = MAX();
int c = MAX(+);
printf("b=%d, c=%d", b, c);
printf("\n\n");
}
当然#if 也可以与#elif嵌套使用,这样就和我们在函数里使用if else一样了,下面是一段php源码中的一段话,你能看到编译php指定不同的参数,检查不同的环境等等都可以通过预处理中的条件编译开完成。
#ifndef PHP_H
#define PHP_H #ifdef HAVE_DMALLOC
#include <dmalloc.h>
#endif #define PHP_API_VERSION 20100412
#define PHP_HAVE_STREAMS
#define YYDEBUG 0 #include "php_version.h"
#include "zend.h"
#include "zend_qsort.h"
#include "php_compat.h"
#include "zend_API.h" #undef sprintf
#define sprintf php_sprintf /* PHP's DEBUG value must match Zend's ZEND_DEBUG value */
#undef PHP_DEBUG
#define PHP_DEBUG ZEND_DEBUG #ifdef PHP_WIN32
# include "tsrm_win32.h"
# include "win95nt.h"
# ifdef PHP_EXPORTS
# define PHPAPI __declspec(dllexport)
# else
# define PHPAPI __declspec(dllimport)
# endif
# define PHP_DIR_SEPARATOR '\\'
# define PHP_EOL "\r\n"
#else
# if defined(__GNUC__) && __GNUC__ >=
# define PHPAPI __attribute__ ((visibility("default")))
# else
# define PHPAPI
# endif # define THREAD_LS
# define PHP_DIR_SEPARATOR '/'
# define PHP_EOL "\n"
#endif #ifdef NETWARE
/* For php_get_uname() function */
#define PHP_UNAME "NetWare"
#define PHP_OS PHP_UNAME
#endif #if HAVE_ASSERT_H
#if PHP_DEBUG
#undef NDEBUG
#else
#ifndef NDEBUG
#define NDEBUG
#endif
#endif
#include <assert.h> #else /* HAVE_ASSERT_H */
#define assert(expr) ((void) (0))
#endif /* HAVE_ASSERT_H */ #define APACHE 0
#if HAVE_UNIX_H
#include <unix.h>
#endif #if HAVE_ALLOCA_H
#include <alloca.h>
#endif #if HAVE_BUILD_DEFS_H
#include <build-defs.h>
#endif
. . .
2、是否已经被定义
被定义:#if define() 或者是#ifdef
不被定义:#if !define() 或者是#ifndef
前者的写法虽然没有后者精炼,但是前者有更多的使用场景,比如下面这种,可以进行嵌套执行。
#if defined(DEBUG)
#ifdef DEBUGTWO
#define TEST(a) a * a
#endif
#endif
3、移除一个宏定义,当不再使用一个宏定义后,可以使用undef来把不需要的宏移除,原型:
#undef name
五、宏命名规则和与函数区别
从前面的使用中我们可以看到,宏的使用规则和函数真是一模一样,但是本质上还是有区别的,在使用中怎样区别宏和函数,涉及到代码规范和代码的可读性问题。标准的宏使用应该使用大写字母,这样在程序中任意地方使用宏都会知道这是一个宏定义。比如前面用到的 #define TEST(a) ((a) * (a))。
宏与函数区别有以下几点:
1、执行速度上,宏定义更快,函数因为需要调用栈,存在调用,返回,保存现场的系统开销,所以比宏要慢。
2、代码长度上,宏在代码长度上实际是增长的,每一处的使用宏都会把name替换成宏内容如果大量使用,会是代码显著增长,函数代码只有一份,比较节省代码空间。
3、参数类型上,宏没有参数类型,只要可以 使用都行。函数不一样,函数有参数类型确定性。正式因为这样,有些宏能巧妙的利用这一点,完成函数不能完成的任务,看下面代码(书上看的),巧妙的利用传递类型无限制的特点自动开辟想要的各种类型空间:
#include <stdio.h>
#include <stdlib.h> #define CREATE_P(nums, type) ((type *) malloc((nums) * sizeof(type))) int main(){
int nums = ;
CREATE_P(nums, int);
}
4、宏定义和函数的使用场景,宏定义一般在程序的开头,函数转化成宏定义一定要考虑成本问题,短小精炼的函数转化成宏使用时最好的,功能负责的函数转化成宏就有点得不偿失了。
六、文件包含
1、本地文件包含和库文件包含
文件包含在大型系统中必然会用到,大型系统宏定义巨多无比,不可能把所有的宏定义都复制到每个文件中,那么文件包含就能解决这种问题。
实际上编辑器支持两种文件包含,一种是我们经常会用的库文件的包含,比如上面我们看到的:#include <stdio.h>,还有一种是本地文件包含,说白了就是我们自己写的文件,包含的原型如下:
#include <filename>
#include "filename"
这两种方式都可以进行文件的包含,不同的是第一种是库文件的包含,标准的C库函数都会以.h扩展名结尾,第二种是本地文件包含,当编辑器看到第二种方式时,优先查找本路径下得本地库文件,如果没有找到就会像包含库文件那样在指定的路径下去找,这时第二种和第一种就差不多了。第二种包含方式在编码习惯上也是比较好的,别人看你的代码很容易知道这个文件是库函数还是你自己写的。
1、嵌套文件包含
大型系统中不仅有大量的文件包含,还会有大量的嵌套文件包含,看下面的例子:
a.h,b.h,c.h,define.c文件,其中a,b,c,define文件的内容如下:
a.h:
#include "c.h"
void var_dumpa(){
test obja;
obja.a[] = ;
printf("obja.a[1]: %d\n", obja.a[]);
} b.h:
#include "c.h"
void var_dumpb(){
test objb;
objb.a[] = ;
printf("objb.a[1]: %d\n", objb.a[]);
} c.h:
#include <stdlib.h>
#include <stdio.h> typedef struct test{
int a[];
}test; define.c:
#include <stdio.h>
#include "a.h"
#include "b.h" int main() {
var_dumpa();
var_dumpb();
printf("\n\n");
}
ab文件包含c文件,define.c文件文件引用a,b文件后会引发一个错误:typedef struct test类型错误,因为c.h文件被包含了两次,像这种情况在大型系统中会经常遇到,或者说,你会发现重复引用库文件也不会报错,由此可见,库文件一定是使用了解决办法。其实解决这种错误的方案就是采用条件编译,当这个文件引入到另一个文件中后我们可以设置一个宏定义,比如:
#include <stdlib.h>
#include <stdio.h> #ifndef PATH_C_H
#define PATH_C_H 1
typedef struct test{
int a[];
}test;
#endif
因为每次编译编译器都会读入整个头文件,如果把所有的文件都加上这个条件编译的话,那交叉引用文件产生的重复宏编译问题就解决了,运行如下:
好了,就写这么多吧,重新梳理了对宏定义的认识和基本的使用。时间仓促,出错的地方请大婶们一定指出,万分感谢!
http://www.cnblogs.com/zyf-zhaoyafei/p/6237295.html
C语言之预处理的更多相关文章
- C语言: 预处理
1. 字符映射 键盘有多种标准规格,例如常用的IBM 104键盘标准,然而不是所有键盘都能打出像#这样的符号,因此C语言的预处理引入了字符映射机制.如果程序员要求,预处理会按照约定对源代码中的字符进行 ...
- C语言之预处理命令
/**************************************************************************** Title:C之预处理命令 Time:201 ...
- R语言数据预处理
R语言数据预处理 一.日期时间.字符串的处理 日期 Date: 日期类,年与日 POSIXct: 日期时间类,精确到秒,用数字表示 POSIXlt: 日期时间类,精确到秒,用列表表示 Sys.date ...
- C语言之预处理详解
C语言之预处理详解 纲要: 预定义符号 #define #define定义标识符 #define定义宏 #define的替换规则 #与## 几点注意#undef 带副作用的宏参数 宏和函数的对比 命名 ...
- iOS开发系列--C语言之预处理
概述 大家都知道一个C程序的运行包括编译和链接两个阶段,其实在编译之前预处理器首先要进行预处理操作,将处理完产生的一个新的源文件进行编译.由于预处理指令是在编译之前就进行了,因此很多时候它要比在程序运 ...
- Linux C编程学习之C语言简介---预处理、宏、文件包含……
C的简介 C语言的结构极其紧凑,C语言是一种模块化的编程语言,整个程序可以分割为几个相对独立的功能模块,模块之间的相互调用和数据传递是非常方便的 C语言的表达能力十分强大.C语言兼顾了高级语言和汇编语 ...
- 【C语言入门教程】2.8 C 语言的预处理命令
预处理命令是在程序编译阶段进行执行的命令,用于编译与特定环境相关的可执行文件.预处理命令扩展了 C 语言,本节将选择其中一些常用的预处理命令进行讲解. 2.8.1 宏替换命令 宏替换命令的作用类似于对 ...
- c语言编译预处理和条件编译执行过程的理解
在C语言的程序中可包括各种以符号#开头的编译指令,这些指令称为预处理命令.预处理命令属于C语言编译器,而不是C语言的组成部分.通过预处理命令可扩展C语言程序设计的环境. 一.预处理的工作方式 1.1. ...
- C语言的预处理命令
C语言编译器处理时经过的第一个步骤是预处理,就是从.c文件处理为.i文件.在预处理时编译器做了一些展开替换的处理. 1>头文件展开,即将#include "stdio.h"类 ...
随机推荐
- 【.net 深呼吸】细说CodeDom(4):类型定义
上一篇文章中说了命名空间,你猜猜接下来该说啥.是了,命名空间下面就是类型,知道了如何生成命名空间的定义代码,之后就该学会如何声明类型了. CLR的类型通常有这么几种:类.接口.结构.枚举.委托.是这么 ...
- 阿里云直播 C# SDK 如何使用
阿里云直播SDK的坑 1.直播云没有单独的SDK,直播部分被封装在CDN的相关SDK当中. 2.针对SDK,没有相关Demo. 3.针对SDK,没有相关的文档说明. 4.针对SDK的说明,官网上的说明 ...
- 【趣事】用 JavaScript 对抗 DDOS 攻击 (下)
上一篇:http://www.cnblogs.com/index-html/p/js-network-firewall.html 对抗 v2 之前的那些奇技淫巧,纯属娱乐而已,并不能撑多久. 但简单. ...
- C# - 值类型、引用类型&走出误区,容易错误的说法
1. 值类型与引用类型小总结 1)对于引用类型的表达式(如一个变量),它的值是一个引用,而非对象. 2)引用就像URL,是允许你访问真实信息的一小片数据. 3)对于值类型的表达式,它的值是实际的数据. ...
- ajax
常见的HTTP状态码状态码:200 请求成功.一般用于GET和POST方法 OK301 资源移动.所请求资源移动到新的URL,浏览器自动跳转到新的URL Moved Permanently304 未修 ...
- JavaScript的继承实现方式
1.使用call或apply方法,将父对象的构造函数绑定在子对象上 function A(){ this.name = 'json'; } function B(){ A.call(this); } ...
- jQuery学习之路(2)-DOM操作
▓▓▓▓▓▓ 大致介绍 jQuery作为JavaScript库,继承并发扬了JavaScript对DOM对象操作的特性,使开发人员能方便的操作DOM对象. ▓▓▓▓▓▓ jQuery中的DOM操作 看 ...
- 【踩坑速记】二次依赖?android studio编译运行各种踩坑解决方案,杜绝弯路,总有你想要的~
这篇博客,只是把自己在开发中经常遇到的打包编译问题以及解决方案给大家稍微分享一下,不求吸睛,但求有用. 1.大家都知道我们常常会遇到dex超出方法数的问题,所以很多人都会采用android.suppo ...
- 深入理解CSS六种颜色模式
前面的话 赏心悦目的颜色搭配让人感到舒服,修改元素颜色的功能让人趋之若鹜.但颜色规划不当,会让网站用户无所适从.颜色从<font color="">发展至今,保留了很多 ...
- 解决Android Studio 无法显示Layout视图问题
在Android Studio 当中,如果你选择的SDK的版本 与你所显示的视图版本不一致时,会出现这个错误 Exception raised during rendering:com/android ...