详解C语言编程中预处理器的用法

预处理最大的标志便是大写,虽然这不是标准,但请你在使用的时候大写,为了自己,也为了后人。

预处理器在一般看来,用得最多的还是宏,这里总结一下预处理器的用法。

  • #include <stdio.h>
  • #define MACRO_OF_MINE
  • #ifdef MACRO_OF_MINE
  • #else
  • #endif

上述五个预处理是最常看见的,第一个代表着包含一个头文件,可以理解为没有它很多功能都无法使用,例如C语言并没有把输入输入纳入标准当中,而是使用库函数来提供,所以只有包含了stdio.h这个头文件,我们才能使用那些输入输出函数。 #define则是使用频率第二高的预处理机制,广泛用在常量的定义,只不过它和const声明的常量有所所区别:

#define MAR_VA 100
const int Con_va = 100;
...
/*定义两个数组*/
...
for(int i = 0;i < 10;++i)
{
  mar_arr[i] = MAR_VA;
  con_arr[i] = Con_va;
}

区别1,定义上MAR_VA可以用于数组维数,而Con_va则不行
区别2,在使用时,MAR_VA的原理是在文中找到所有使用本身的地方,用值替代,也就是说Con_va将只有一分真迹,而MAR_VA则会有n份真迹(n为使用的次数) 剩下三个则是在保护头文件中使用颇多。
几个比较实用的用于调试的宏,由C语言自带

  • __LINE__和__FILE__ 用于显示当前行号和当前文件名
  • __DATE__和__TIME__ 用于显示当前的日期和时间
  • __func__(C99) 用于显示当前所在外层函数的名字

上述所说的五种宏直接当成值来使用即可。

__STDC__

如果你想检验你现在使用的编译器是否遵循ISO标准,用它,如果是他的值为1。

printf("%d\n", __STDC__);

输出: 1

如果你想进一步确定编译器使用的标准版本是C99还是C89可以使用__STDC__VERSION__,C99(199901)

printf("%d\n", __STDC_VERSION__);

输出: 199901

对于#define

预处理器一般只对同一行定义有效,但如果加上反斜杠,也能一直读取下去

 #define err(flag) \
   if(flag) \
    printf("Correctly")

可以看出来,并没有在末尾添加;,并不是因为宏不需要,而是因为,我们总是将宏近似当成函数在使用,而函数调用之后总是需要以;结尾,为了不造成混乱,于是在宏定义中我们默认不添加;,而在代码源文件中使用,防止定义混乱。

预处理同样能够带来一些便利

  #define SWAP1(a, b) (a += b, b = a - b, a -= b)
  #define SWAP2(x, y) {x ^= y; y ^= x; x ^= y}

引用之前的例子,交换两数的宏写法可以有效避免函数开销,由于其是直接在调用处展开代码块,故其比拟直接嵌入的代码。但,偶尔还是会出现一些不和谐的错误,对于初学者来说:

 int v1 = 10;
 int v2 = 20;
 SWAP1(v1, v2);
 SWAP2(v1, v2);//报错

对于上述代码块的情况,为什么SWAP2报错?对于一般的初学者来说,经常忽略诸如, goto do...while等少见关键字用法,故很少见SWAP1的写法,大多集中于SWAP2的类似错误,错就错在{}代表的是一个代码块,不需要使用;来进行结尾,这便是宏最容易出错的地方 宏只是简单的将代码展开,而不会做任何处理 对于此,即便是老手也常有失足,有一种应用于单片机等地方的C语言写法可以在此借鉴用于保护代码:

 #define SWAP3(x ,y) do{ \
     x ^= y; y ^= x; x ^= y; \
     }while(0)

如此便能在代码中安全使用花括号内的代码了,并且如之前所约定的那样,让宏的使用看起来像函数。

但正所谓,假的总是假的,即使宏多么像函数,它依旧不是函数,如果真的把它当成函数,你会在某些时候错的摸不着头脑,还是一个经典的例子,比较大小:

 #define CMP(x, y) (x > y ? x : y)
 ...
 int x = 100, y = 200;
 int result = CMP(x, y++);
 printf("x = %d, y = %d, result = %d\n", x, y, result);

执行这部分代码,会输出什么呢? 答案是,不知道!至少result的值我们无法确定,我们将代码展开得到

 int result = (x > y++ ? x : y++);

看起来似乎就是y递增两次,最后result肯定是200。真是如此?C语言标准对于一个确定的程序语句中,一个对象只能被修改一次,超过一次那么结果是未定的,由编译器决定,除了三目操作符?:外,还有&&, ||或是,之中,或者函数参数调用,switch控制表达式,for里的控制语句 由此可看出,宏的使用也是有风险的,所以虽然宏强大,但是依旧不能滥用。

对于宏而言,前面说过,它只是进行简单的展开,这有时候也会带来一些问题:

 #define MULTI(x, y) (x * y)
 ...
 int x = 100, y = 200;
 int result = MULTI(x+y, y);

看出来问题了吧?展开之后会变成: int result = x+y * y; 完全违背了当初我们设计时的想法,一个比较好的修改方法是对每个参数加上括号: #define MULTI(x, y) ((x) * (y))如此,展开以后:

 int result = ((x+y) * (y));

这样能在很大程度上解决一部分问题。

如果对自己的宏十分自信,可以嵌套宏,即一个表达式中使用宏作为宏的参数,但是宏只展开这一级的宏,对于多级宏另有办法展开

 int result = MULTI(MULTI(x, y), y);

展开成:

int result = ((((x) * (y))) * (y));

对宏的应用

由于我们并不明白,在某些情况下宏是否被定义了,(NULL宏是一个例外,它可以被重复定义),所以我们可以使用一些预处理保护机制来防止错误发生

  • #ifndef MY_MACRO
  • #define MY_MACRO 10000
  • #endif

如果定义了MY_MACRO那就不执行下面的语句,如果没定义那就执行。

在宏的使用中有两个有用的操作符,姑且叫它操作符#, ##

对于# 我们可以认为#操作符的作用是将宏参数转化为字符串。

  #define HCMP(x, y) printf(#x" is equal to" #y" ? %d\n", (x) == (y))
  ...
  int x = 100, y = 200;
  HCMP(x, y);

展开以后

  printf("x is equal to y ? %d\n", (100) == (200));

注:可以自行添加编译器选项,来查看宏展开之后的代码,具体可以查询GCC的展开选项,这里不再详述。特别是在多层宏的嵌套使用情况下,但是我不太推荐,故不做多介绍。

能说的就是如何正确的处理一些嵌套使用,之所以不愿意多说也不愿意多用,是因为C预处理器就是一个奇葩
举一个典型的例子,__LINE__ 和 __FILE__的使用。

  /* 下方会说到的 # 预处理指示器,这里先用,实在看不懂,可以自己动手尝试 */
  #define WHERE_AM_I #__LINE__ " lines in " __FILE__
  ...
  fputs(WHERE_AM_I, stderr);

这样能工作吗?如果能我还讲干嘛。

  /* 常理上这应该能工作,但是编译器非说这错那错的 */
  /* 好在有前人踏过了坑,为我们留下了解决方案 */
  #define DEPAKEGE(X) #X
  #define PAKEGE(X) DEPAKEGE(X)
  #define WHERE_AM_I PAKEGE(__LINE__) " lines in " __FILE__
  ...
  fputs(WHERE_AM_I, stderr);

不要问我为什么,因为我也不知道C预处理器的真正工作机制是什么。

第一次看见这种解决方案是在 Windows 核心编程 中,这本书现在还能给我许多帮助,虽然已经渐渐淡出了书架

总结起来,即将宏参数放于#操作符之后便由预处理器自动转换为字符串常量,转义也由预处理器自动完成,而不需要我们自行添加转义符号。

对于##
它实现的是将本操作符两边的参数合并成为一个完整的标记,但需要注意的是,由于预处理器只负责展开,所以程序员必须自己保证这种标记的合法性,这里涉及到一些写法问题,都列出来

   #define MERGE(x, y) have_define_ ## (x + y)
   #define MERGE(x, y) have_define_##(x + y)
   ...
   result = MERGE(1, 3);

这里首先说明,上述写法由于习惯原因,我使用第二种,但是无论哪种都无伤大雅,效果一样。上述代码展开以后是什么呢?

result = have_define_1 + 3;

在我看来,这就有点C++中模版的思想了,虽然十分原始,但是总是有了一个方向,凭借这种方法我们能够使用宏来进行相似却不同函数的调用,虽然我们可以使用函数指针数组来存储,但需要提前知晓有几个函数,并且如果要实现动态增长还需要消耗内存分配,但宏则不同。

   inline int func_0(int arg_1, int arg_2) { return arg_1 + arg_2; }
   inline int func_1(int arg_1, int arg_2) { return arg_1 - arg_2; }
   inline int func_2(int arg_1, int arg_2) { return arg_1 * arg_2; }
   inline int func_3(int arg_1, int arg_2) { return arg_1 / arg_2; }
   #define CALL(x, arg1, arg2) func_##x(arg1, arg2)
   ...
     printf("func_%d return %d\n",0 ,CALL(0, 2, 10));
     printf("func_%d return %d\n",1 ,CALL(1, 2, 10));
     printf("func_%d return %d\n",2 ,CALL(2, 2, 10));
     printf("func_%d return %d\n",3 ,CALL(3, 2, 10));

十分简便的一种用法,在我们增加减少函数时我们不必考虑如何找到这些函数只需要记下每个函数对应的编号即可,但还是那句话,不可滥用。

   #define CAT(temp, i) (cat##i)
   //...
   for(int i = 0;i < 5;++i)
   {
     int CAT(x,i) = i*i;
     printf("x%d = %d \n",i,CAT(x,i));
   }

对于宏,在使用时一定要注意,宏只能展开当前层的宏,如果你嵌套使用宏,即将宏当作宏的参数,那么将导致宏无法完全展开,即作为参数的宏只能传递名字给外部宏

  #define WHERE(value_name, line) #value_name #line
  ...
  puts(WHERE(x, __LINE__)); //x = 11

输出: 11__LINE__

对于其他的预编译器指令,如:#program, #line, #error和各类条件编译并不在此涉及,因为使用上并未有陷阱及难点。

C和C++混合编程的情况

经常能在源代码中看见 extern "C" 这样的身影,这是做什么的?
这是为了混合编程而设计的,常出现在 C++的源代码中,目的是为了让 C++能够成功的调用 C 的标准或非标准函数。

  #if defined(__cplusplus) || defined(_cplusplus)
      extern "C" {
  #endif

      /**主体代码**/

  #if defined(__cplusplus) || defined(_cplusplus)
      }
  #endif

这样就能在C++中调用C的代码了。

在 C 中调用 C++ 的函数需要注意,不能使用重载功能,否则会失败,原因详见C++对于重载函数的实现。也可以称为 mangle

(0)

相关推荐

  • 你必须知道的C语言预处理的问题详解

    C语言预处理器执行宏替换.条件编译和文件包含.通常采用以"#"为行首的提示.下面是C语言预处理的应用场合: 1.三字母词(Trigraph Sequences) C源程序的字符集被包含在7位的ASCII字符集中,但是它是ISO 646-1983 Invariant Code Set的超集.为了让程序可以在缩减集(reduced set)中呈现出来,下面的三字母词会被替换成相应的单字符. 三字母词 单字符 ??= # ??/ \ ??' ^ ??( [ ??) ] ??! | ??<

  • 简介C/C++预处理器的一些工作

    多么令人愉快的一个问题啊 就在被带到编译器那里之前,预处理器都会对你的源代码瞧上一瞧, 做一些格式化的工作,并执行任何你在源代码里面留给它来执行的指令. 像什么? 好吧,预处理器的指令就被叫做预处理器指令,而他们都以一个#开头. 像 #include 这样? 正确. 每一个被预处理器遇到的 # 命令都会导致在某种方式上对源代码的修改. 让我们来简单的研究研究它们,然后我们就会之后这背后都是怎么运转的了. #include 包含其他库.类.接口等的头文件.预处理器实际上就只是把整个头文件复制到你的

  • C#中的预处理器指令详解

    目录 1. #define 和 #undef 2. #if.#elif.#else 和#endif 3. #warning 和 #error 4. #region 和#endregion 5. #line 6. #pragma C#中有许多名为"预处理器指令"的命令.这些命令从来不会转化为可执行代码中的命令,但会影响编译过程的各个方面. 例如,使用预处理器指令可以禁止编译器编译代码的某一部分.如果计划发布两个版本的代码,即基本版本和拥有更多功能的企业版本,就可以使用这些预处理器指令.在

  • C#预处理器指令的用法实例分析

    本文实例讲述了C#预处理器指令的用法.分享给大家供大家参考.具体用法分析如下: C#预处理器指令是在编译时调用的.预处理器指令(preprocessor directive)告诉C#编译器要编译哪些代码,并指出如何处理特定的错误和警告.C#预处理器指令还可以告诉C#编辑器有关代码组织的信息. 1. 定义符号和取消符号定义的预处理指令#define 和 #undef 预处理指令都以#号开头并位于行首前面可以出现空格符. 复制代码 代码如下: #define DEBUG #define ISSAY

  • 常用C/C++预处理指令详解

    预处理是在编译之前的处理,而编译工作的任务之一就是语法检查,预处理不做语法检查.预处理命令以符号"#"开头. 常用的预处理指令包括: 宏定义:#define 文件包含:#include 条件编译:#if.#elif.#ifndef.#ifdef.#endif.#undef 错误信息指令:#error #line指令 布局控制:#pragma 宏定义 宏定义又称为宏代换.宏替换,简称"宏".宏替换只作替换,不做计算,不做表达式求解.宏定义分带参数的宏定义和不带参数的宏

  • 深入理解C预处理器

    C 预处理器不是编译器的组成部分,是编译过程中一个单独的步骤.C预处理器只是一个文本替换工具,它会指示编译器在实际编译之前完成所需的预处理. 所有的预处理器命令都是以井号(#)开头.它必须是第一个非空字符,为了增强可读性,预处理器指令应从第一列开始. 下表包含所有重要的预处理器指令: 指令 描述 #define 定义宏 #include 包含一个源代码文件 #undef 取消已定义的宏 #ifdef 如果宏已经定义,则返回真 #ifndef 如果宏没有定义,则返回真 #if 如果给定条件为真,则

  • 详解C语言编程中预处理器的用法

    预处理最大的标志便是大写,虽然这不是标准,但请你在使用的时候大写,为了自己,也为了后人. 预处理器在一般看来,用得最多的还是宏,这里总结一下预处理器的用法. #include <stdio.h> #define MACRO_OF_MINE #ifdef MACRO_OF_MINE #else #endif 上述五个预处理是最常看见的,第一个代表着包含一个头文件,可以理解为没有它很多功能都无法使用,例如C语言并没有把输入输入纳入标准当中,而是使用库函数来提供,所以只有包含了stdio.h这个头文

  • 详解C语言编程中的函数指针以及函数回调

    函数指针: 就是存储函数地址的指针,就是指向函数的指针,就是指针存储的值是函数地址,我们可以通过指针可以调用函数. 我们先来定义一个简单的函数: //定义这样一个函数 void easyFunc() { printf("I'm a easy Function\n"); } //声明一个函数 void easyFunc(); //调用函数 easyFunc(); //定义这样一个函数 void easyFunc() { printf("I'm a easy Function\n

  • 详解C 语言项目中.h文件和.c文件的关系

    详解C 语言项目中.h文件和.c文件的关系 在编译器只认识.c(.cpp))文件,而不知道.h是何物的年代,那时的人们写了很多的.c(.cpp)文件,渐渐地,人们发现在很多.c(.cpp)文件中的声明语句就是相同的,但他们却不得不一个字一个字地重复地将这些内容敲入每个.c(.cpp)文件.但更为恐怖的是,当其中一个声明有变更时,就需要检查所有的.c(.cpp)文件. 于是人们将重复的部分提取出来,放在一个新文件里,然后在需要的.c(.cpp)文件中敲入#include XXXX这样的语句.这样即

  • 详解C++模板编程中typename用法

    typename的常规用法 typename在C++类模板或者函数模板中经常使用的关键字,此时作用和class相同,只是定义模板参数:在下面的例子中,该函数实现泛型交换数据,即交换两个数据的内容,数据的类型由_Tp决定. template <typename _Tp> inline void swap(_Tp& __a, _Tp& __b) { _Tp __tmp = __a; __a = __b; __b = __tmp; } typename的第二个用法:修饰类型 限定名和

  • 详解C语言内核中的链表与结构体

    Windows内核中是无法使用vector容器等数据结构的,当我们需要保存一个结构体数组时,就需要使用内核中提供的专用链表结构LIST_ENTRY通过一些列链表操作函数对结构体进行装入弹出等操作,如下代码是本人总结的内核中使用链表存储多个结构体的通用案例. 首先实现一个枚举用户进程功能,将枚举到的进程存储到链表结构体内. #include <ntifs.h> #include <windef.h> extern PVOID PsGetProcessPeb(_In_ PEPROCES

  • 详解C语言内核中的自旋锁结构

    提到自旋锁那就必须要说链表,在上一篇<驱动开发:内核中的链表与结构体>文章中简单实用链表结构来存储进程信息列表,相信读者应该已经理解了内核链表的基本使用,本篇文章将讲解自旋锁的简单应用,自旋锁是为了解决内核链表读写时存在线程同步问题,解决多线程同步问题必须要用锁,通常使用自旋锁,自旋锁是内核中提供的一种高IRQL锁,用同步以及独占的方式访问某个资源. 首先以简单的链表为案例,链表主要分为单向链表与双向链表,单向链表的链表节点中只有一个链表指针,其指向后一个链表元素,而双向链表节点中有两个链表节

  • 详解JavaScript异步编程中jQuery的promise对象的作用

    Promise, 中文可以理解为愿望,代表单个操作完成的最终结果.一个Promise拥有三种状态:分别是unfulfilled(未满足的).fulfilled(满足的).failed(失败的),fulfilled状态和failed状态都可以被监听.一个愿望可以从未满足状态变为满足或者失败状态,一旦一个愿望处于满足或者失败状态,其状态将不可再变化.这种"不可改变"的特性对于一个Promise来说非常的重要,它可以避免Promise的状态监听器修改一个Promise的状态导致别的监听器的行

  • 详解Java多线程编程中的线程同步方法

    1.多线程的同步: 1.1.同步机制: 在多线程中,可能有多个线程试图访问一个有限的资源,必须预防这种情况的发生.所以引入了同步机制:在线程使用一个资源时为其加锁,这样其他的线程便不能访问那个资源了,直到解锁后才可以访问. 1.2.共享成员变量的例子: 成员变量与局部变量: 成员变量: 如果一个变量是成员变量,那么多个线程对同一个对象的成员变量进行操作,这多个线程是共享一个成员变量的. 局部变量: 如果一个变量是局部变量,那么多个线程对同一个对象进行操作,每个线程都会有一个该局部变量的拷贝.他们

  • 详解Java设计模式编程中的策略模式

    定义:定义一组算法,将每个算法都封装起来,并且使他们之间可以互换. 类型:行为类模式 类图: 策略模式是对算法的封装,把一系列的算法分别封装到对应的类中,并且这些类实现相同的接口,相互之间可以替换.在前面说过的行为类模式中,有一种模式也是关注对算法的封装--模版方法模式,对照类图可以看到,策略模式与模版方法模式的区别仅仅是多了一个单独的封装类Context,它与模版方法模式的区别在于:在模版方法模式中,调用算法的主体在抽象的父类中,而在策略模式中,调用算法的主体则是封装到了封装类Context中

  • 详解Python核心编程中的浅拷贝与深拷贝

    一.问题引出浅拷贝 首先看下面代码的执行情况: a = [1, 2, 3] print('a = %s' % a) # a = [1, 2, 3] b = a print('b = %s' % b) # b = [1, 2, 3] a.append(4) # 对a进行修改 print('a = %s' % a) # a = [1, 2, 3, 4] print('b = %s' % b) # b = [1, 2, 3, 4] b.append(5) # 对b进行修改 print('a = %s'

随机推荐