C语言的那些小秘密之预处理

时间:2015-06-14 来源:网络

  预处理是C语言的一个重要知识点,它能改善程序设计的环境,有助于编写易移植、易调试的程序。因此,我们有必要掌握好预处理命令,在自己编程的时候灵活的使用它,使得编写的程序结构优良,更加易于调试和阅读。接下来我尽可能的把预处理中重要知识点向读者讲解清楚,使读者能够在自己以后编程的过程中熟练的使用预处理命令。

  C语言的预处理主要有三个方面:

  1、文件的包含

  2、宏定义

  3、条件编译

  一、文件包含的形式有下面两种

  1、#include "文件名"

  2、#include <文件名>

  它们之间的区别在于:<文件名>系统到头文件目录查找文件, "文件名"则先在当前目录查找,如果没有才到头文件目录查找;当然我们也可以使用在命令行来指定头文件路径方法。还要注意就是如果在源文件包含的头文件之间出现调用的情况,那么被调用的头文件要出现在调用头文件的前面。

  二、宏定义

  宏定义的使用有两种形式,一种不带参数,而另外一种带参数。

  1、不带参数

  格式: #define 标识符 字符串

  相信上面这个格式大家并不陌生,下面还是来看看如何使用吧。当然在讲解之前我们的看看使用过程中的如下几个注意要点:

  (1)预处理不做语法检查,所以我们选用的时候要尤其小心

  (2)宏定义写在函数的花括号外边,作用域为其后的程序,通常在文件开头部分,直到用#undef命令终止宏定义的作用域

  (3)不要在字符串中使用宏,如果宏名出现在字符串中那么将按照字符串进行处理

  下面来看段代码的使用。

  [html] view plaincopy#include

  #define N 9

  int main ()

  {

  int i,a[N];

  for(i=0;i

  {

  a[i]=i;

  printf("%dt",a[i]);

  if((i+1)%3==0)

  printf("n");

  }

  //#undef N

  printf("%dn",N);

  }

  运行结果为:

  [html] view plaincopy0 1 2

  3 4 5

  6 7 8

  9

  Press any key to continue

  我们在此主要是介绍下宏的作用域问题,当在以上代码中注释掉#undef N时,接下来的打印语句能够正常的打印出;但是当我们没有注释掉#undef N的时候就会出现error C2065: 'N' : undeclared identifier错误,提示N没有定义。接下来看看带参数的宏的使用。

  2、带参数

  #define 宏名(参数表) 字符串

  注意要点:

  (1)宏名和参数的括号间不能有空格

  (2)宏替换只作替换,不做计算,不做表达式求解,这点要尤其注意

  (3)函数调用在编译后程序运行时进行,并且分配内存。宏替换在编译前进行,不分配内存

  (4)宏的哑实结合(所谓的哑实结合类似于函数调用过程中实参替代形参的过程)不存在类型,也没有类型转换。

  (5)宏展开使源程序变长,函数调用不会

  下面来看看linux下一个典型的应用:

  #define min(x,y) ({ typeof(x) _x = (x); typeof(y) _y = (y); (void) (&_x == &_y); _x < _y ? _x : _y; })

  #define max(x,y) ({ typeof(x) _x = (x); typeof(y) _y = (y); (void) (&_x == &_y); _x > _y ? _x : _y; })

  在上面的两个宏中我们发现有这么一句代码(void) (&_x == &_y);可能不少读者有点发懵的感觉,这啥意思呢?!其实我们细细分析就知道,首先看看“==”,这是一个逻辑表达式,它要求两边的比较类型必须一致,如果我们的&x和&y类型不一致,如一个为char*,另一个为int*,不是同一个类型,当我们使用gcc编译的时候就会出现警告信息,vc6则会报错error C2446: '==' : no conversion from 'char *' to 'int *'。这句代码(void) (&_x == &_y); 在此的功能就相当于执行一个简单的判断操作,我们用来判断x和y的类型是否一致。别小看了这句代码,如果学会了使用它会给你的代码带来不少的便捷。下面给出一个小小的事例:

  [cpp] view plaincopy#include

  void print()

  {

  printf("hello world!!!n");

  return ;

  }

  void main(int argc,char*argv)

  {

  print();

  return ;

  }

  运行结果为:

  [cpp] view plaincopyhello world!!!

  Press any key to continue

  现在我们来修改下代码后看看运行结果:

  [cpp] view plaincopy#include

  void print()

  {

  printf("hello world!!!n");

  return ;

  }

  void main(int argc,char*argv)

  {

  #define print() ((void)(3))

  print();

  return ;

  }

  运行结果为:

  [cpp] view plaincopyPress any key to continue

  这儿的结果没有了我们之前的那句hello world!!!,可以看出这个时候函数并没有被调用,这是因为我们使用了#define print() ((void)(3)),使得之后调用函数print()转换为了一个空操作,所以这个函数在接下来的代码中都不会被调用了,就像被“冲刷掉”了一样。看到这儿你是不是想起我们之前的那篇《C语言的那些小秘密之断言》了呢,我们同样可以使用这种方法来实现断言的关闭,方法与之类似,在此就不再讲解了,有兴趣的读者可以自己试试。讲到这儿似乎应该结束了,但是细心的读者会有另外一个疑惑?在#define min(x,y) ({ typeof(x) _x = (x); typeof(y) _y = (y); (void) (&_x == &_y); _x < _y ? _x : _y; })中,我们为什么要使用像typeof(y) _y = (y)这样的转换呢?而不直接使用typeof(x)==typeof(y)或者(void) (&x == &y); x < y ? x : y; 呢?如果我们使用typeof(x)==typeof(y)就好比使用了char==int一样,这是不允许的。我们使用一个typeof(y) _y = (y)这样的转换,这是为了防止x和y为一个表达式的情况,如x=i++之类的,如果不转换的话i++就多执行了几次操作,得到的不是我们想要的结果,但是如果我们使用了typeof(y) _y = (y)这样的转换,那就不会出现这样的问题了。下面我们来看看如何使用宏定义实现变参,先看看实现方法。

  #define print(...) printf(__VA_ARGS__)

  看看上面的宏,其中“...”指可变参数。实现的可变参数的实现方式就是使用“...”所代表的内容替代__VA_ARGS__,看看下面的代码就知道了。

  [cpp] view plaincopy#include

  #define print(...) printf(__VA_ARGS__)

  int main(int argc,char*argv)

  {

  print("hello world----%dn",1111);

  return 0;

  }

  运行结果为:

  [cpp] view plaincopyroot@ubuntu:/home/shiyan# ./arg

  hello world----1111

  接着往下看。

  #define printf (tem, ...) fprintf (stdout, tem, ## __VA_ARGS__)

  如有对fprintf不熟悉的读者可以自己查查函数手册,在此不再讲解。

  [cpp] view plaincopy#include

  #define print(temp, ...) fprintf(stdout, temp, ##__VA_ARGS__)

  int main(int argc,char*argv)

  {

  print("hello world----%dn",1111);

  return 0;

  }

  运行结果为:

  [cpp] view plaincopyroot@ubuntu:/home/shiyan# ./arg

  hello world----1111

  temp在此的作用为设定输出字符串的格式,后边“...”为可变参数。现在问题来了,我们在宏定义中为什么要使用“##”呢?如果我们没有使用##会怎么样呢?看看下面的代码:

  [cpp] view plaincopy#include

  #define print(temp, ...) fprintf(stdout, temp, __VA_ARGS__)

  int main(int argc,char*argv)

  {

  print("hello worldn");

  return 0;

  }

  编译时发生了如下错误:

  [cpp] view plaincopyroot@ubuntu:/home/shiyan# gcc arg.c -o arg

  arg.c: In function ‘main’:

  arg.c:7:2: error: expected expression before ‘)’ token

  为什么会出现上面的错误呢,现在我们来分析下,我们进行下宏替换,print("hello worldn")就变为了fprintf(stdout, "hello worldn",)这样我们就发现了后面出现了一个逗号,所以导致了错误,如果有“##”就不会出现这样的错误了,这是因为如果可变参数被忽略或为空的时候,“##”操作将使预处理器去除掉它前面的那个逗号。如果存在可变参数时候,它也能正常工作。讲了“##”,我们当然也要讲讲“#”。先来看看下面一段代码:

  [cpp] view plaincopy#include

  #define return_exam(p) if(!(p))

  {printf("error: "#p" file_name:%stfunction_name:%stline:%d .n",

  __FILE__, __func__, __LINE__); return 0;}

  int print()

  {

  return_exam(0);

  }

  int main(int argc,char*argv)

  {

  print();

  printf("hello world!!!n");

  return 0;

  }

  运行结果为:

  [cpp] view plaincopyroot@ubuntu:/home/shiyan# ./arg

  error: 0 file_name:arg.c function_name:print line:9 .

  hello world!!!

  我们发现在运行结果中打印出了出错的文件名、函数名、以及行号。采用宏定义来检测函数的返回值是否正确,仅仅是为了体现出我们要讲解的宏,所以代码做了最大的简化工作,读者在自己编写代码时候要学会这样的检测方式。“#”的作用就是将其后面的宏参数进行字符串化操作,就是在宏变量进行替换之后在其左右各加上一个上双引号,这就使得"#p"变味了""p""我们发现这样的话刚好两边的“""”就消失了。下面来看看最后一个知识点条件编译。

  三、条件编译

  条件编译命令#if、#else、#elif、#endif、#ifdef、#ifndef,条件编译指令的意思很简单,跟我们学习的if语句类似。

  一般格式

  #if 常量表达式

  程序段1;

  [#else

  程序段2;]

  #endif

  功能:当表达式为非0(“逻辑真”)时,编译程序段1,否则编译程序段2。

  一般格式

  #ifdef 标识符

  程序段1;

  [#else

  程序段2;]

  #endif

  功能:当“标识符”已经被#define命令定义过,则编译程序段1,否则编译程序段2。

  #ifndef 标识符

  程序段1;

  [#else

  程序段2;]

  #endif

  功能:当“标识符”未被#define命令定义过,则编译程序段1,否则编译程序段2。

  学习了条件编译指令之后,我们在调试代码的时候,就不要再随心所欲的删减代码了,如果我们不想某段代码被编译就可以使用条件编译指令来将其注释掉。如:

  #if (0)

  注释代码段;

  #endif

  就可以实现代码的注释了,需要的时候也可以将其启用,而不会为需要重新编辑代码时,发现已被删除而头疼了。

  其中值得注意的地方为,常量表达式在编译时求值,所以表达式只能是常量或者已经定义过的标识符,不能为变量,也不可以为那些在编译时候求值的操作符,如sizeof。

  下面来看段代码:

  [cpp] view plaincopy#include

  #define N 1

  int main(int argc,char*argv)

  {

  int a=3;

  #if(a)

  printf("#if后面的表达式为变量n");

  #endif

  #if(N)

  printf("#if后面的表达式已定义,且不为0---successn");

  #else

  printf("#if后面的表达式已定义,且不为0---failn");

  #endif

  return 0;

  }

  运行结果为:

  [cpp] view plaincopy#if后面的表达式已定义,且不为0---success

  Press any key to continue

  看看上面的代码我们的表达式为变量a时并没有打印出来,所以我们不能在其后的表示中使用变量。如果我们使用sizeof操作符会怎么样呢?为了加深印象看看下面的代码后结果吧。

  [cpp] view plaincopy#include

  int main(int argc,char*argv)

  {

  int a=9;

  #if(sizeof(a))

  printf("#if后面的表达式含有sizeof操作符n");

  #endif

  return 0;

  }

  编译出现了如下错误:

  [cpp] view plaincopyfatal error C1017: invalid integer constant expression

  所以我们在使用条件编译的时候要牢记这两点,常量表达式不能为变量和含有sizeof等在编译时求值的操作符。

  接下来看看这里要讲的最后一个#pragma指令。

  一般格式为:

  #pragma 参数

  下面给出几种经常使用的形式

  1、#pragma message("消息")

  看看下面一段代码。

  [cpp] view plaincopy#include

  #define FDSA

  int main(int argc,char*argv)

  {

  #ifdef FDSA

  #pragma message("FDSA 已经定义过了")

  #endif

  return 0;

  }

  编译的时候我们可以在编译输出窗口中看到了输出“FDSA 已经定义过了”,通过这种方式我们可以在一些我们想要的地方输出很多我们需要的信息。

  2、#pragma once

  如果我们在头文件的开头部分加入这条指令,那么就能保证我们的头文件仅仅被编译一次。

  3、#pragma hdrstop

  该指令表示编译头文件到此为止,后面的无需在进行编译了。

  4、#pragma pack()

  设定字节的对齐长度,这个指令我们在《C语言的那些小秘密之字节对齐》中已经讲解了,在此不再复述。

  5、#pragma warning(disable:M N;once:H;error:K)

  表示不显示M和N号的警告信息,H号警告信息只报告一次,把K号警告信息作为一个错误来处理。

  到此关于预处理的讲解就结束了。由于本人水平有限,博客中的不妥或错误之处在所难免,殷切希望读者批评指正。同时也欢迎读者共同探讨相关的内容,如果乐意交流的话请留下你宝贵的意见。

关键词:C语言预处理

加入微信
获取电子行业最新资讯
搜索微信公众号:EEPW

或用微信扫描左侧二维码

相关文章

查看电脑版