PPC的C/C++和人工智能学习笔记
每一篇学习笔记,都只是为了更好地掌握和理解

C语言基础(17)_预处理和宏定义

今天学习C语言的预处理,主要是学习C语言的预处理命令,重点是学习宏定义以及它的一些实战用例。

 

C程序的源代码中可包括各种编译指令,这些指令称为预处理命令。它们扩展了C程序设计的环境。应用预处理程序和注释可以简化程序开发过程,并提高程序的可读性。ANSI标准定义的C语言预处理程序包括下列12个命令:

#include,#define,#pragma,#if,#else,#elif,#endif,#ifdef,#ifndef,#undef,#error,#line一共12个,都是以#开头,每条预处理指令必须独占一行,结束不能加分号。

 

所谓预处理,就是在编译之前进行处理,通过预编译进行宏替换、条件选择代码段,然后生成最后的待编译代码,最后进行编译。

 

#include指令

程序中的#include指令要求编译程序读入另一个源文件。被读入文件的名字必须用双引号(“”)或一对尖括号(<>)包围,例如:

#include “stdio.h”

#include <stdio.h>

包含文件中可以包含其他#include指令,称为嵌套包含。允许的最大嵌套深度随编译器而变。

文件名被双括号或尖括号包围决定了对指定文件的搜索方式。文件名被尖括号包围时,搜索按编译程序作者的定义进行,一般用于搜索某些专门放置包含文件的特殊目录。当文件名被双括号包围时,搜索按编译程序实时的规定进行,一般搜索当前目录。如未发现,再按尖括号包围时的办法重新搜索一次。

通常,绝大多数程序员使用尖括号包围标准的头文件,双引号用于包围与当前程序相关的文件名(比如自己定义的头文件)。

要注意的是:包含文件的扩展名不是固定的,可以是任何扩展名,(比如.c/.cpp文件一样可以包含进来)。而习惯上,我们一般用.h或.hpp文件表示头文件。

 

#pragma指令

为实现时定义的命令,它允许向编译程序传送各种指令。它作用是设定编译器的状态或者是指示编译器完成一些特定的动作。#pragma指令对每个编译器给出了一个方法,在保持与C和C++语言完全兼容的情况下,给出主机或操作系统专有的特征。依据定义,编译指示是机器或操作系统专有的,且对于每个编译器都是不同的。(没明白啥意思)

用法1:加载库文件 #pragma comment(lib,”xxx.lib”)

例如:加载一个播放音乐的库文件

#include <mmsystem.h>

#pragma comment(lib,”winmm.lib”)

用法2:#pragma once

只要在头文件的最开始加入这条指令就能够保证头文件只被编译一次。

用法3:#pragma message(“消息文本”)

当编译器遇到这条指令时就在编译输出窗口中将消息文本打印出来。

例如:当我们在程序中定义了许多宏来控制源代码版本的时候,我们自己有可能都会忘记有没有正确的设置这些宏,此时我们可以用这条指令在编译的时候就进行检查。假设我们希望判断自己有没有在源代码的什么地方定义了_X86这个宏可以用下面的方法:

#ifdef _X64

#pragma message(“_X64 activated!”)

#endif

当我们定义了_X86这个宏以后,应用程序在编译时就会在编译输出窗口里显示该信息,从而辅助我们调试。

用法4:#pragma warning( disable : 4507 34; once : 4385; error : 164 )

定义编译警告信息是否显示,上面的意思是:

#pragma warning(disable:4507 34) //不显示4507和34号警告信息。如果编译时总是出现4507号警告和34号警告,而认为肯定不会有错误,可以使用这条指令。

#pragma warning(once:4385) // 4385号警告信息仅报告一次

#pragma warning(error:164) // 把164号警告信息作为一个错误。

用法5:progma pack(n)

指定结构体对齐方式。#pragma pack(n)来设定变量以n字节对齐方式。

还有一些其他的。。。

 

条件编译:#if,#else,#elif,#endif,#ifdef,#ifndef,#undef

#if #endif命令

满足#if后面的条件,就编译#if和#endif之间的程序段,否则不编译。

#ifdef XXX 表示如果定义了宏XXX,则条件成立;

#ifndef XXX 表示如果没有定义宏XXX,则条件成立;

#undef XXX 取消以前定义的宏定义;

条件编译的应用例子1:版本控制

可以用这个来控制软件的版本,比如你有个print函数,1种版本是简易版,1种版本是标准版,还有1种版本是豪华版,就可以这样做:

#define PRINT_VERSION 1或2或3,分别代表3个不同版本

#if PRINT_VERSION==1

这里是print简易版的实现

#elif PRINT_VERSION==2

这里是print标准版的实现

#elif PRINT_VERSION==3

这里是print豪华版的实现

#endif

根据你的不同宏定义,就可以编译出3个不同的版本。

条件编译的应用例子2:头文件只会被include 1次。

有些头文件,会有变量和结构体的定义,但是这些是不允许被2次定义的,而有时候,因为include嵌套等问题,有些头文件就会被包含多次,这样就会出问题了。

所以,一般我们在定义头文件的时候,都这样写:

#ifndef _XXX_H //习惯用 _XXX_H 来表示 xxx.h

#define _XXX_H //这里可以不需要定义为多少值,只要定义了就可以

这里是正式的头文件内容

#endif

 

#line命令

#line 100   //开始记录行数

printf(“%d\n”,__LINE__);

 

#define宏定义

可以用来替换。(符号常量)

#define ROWS  100

#define COLS   200

上面的意思就是,程序里面ROWS就是100,COLS就是200。

(一)系统中的宏:

在C语言的库中定义了很多宏,其中有些是我们常用并且比较有用的宏:

__FILE__ 当前所处文件

__LINE__ 当前所处行号,可配合 #line使用

__DATE__ 当时所处日期

__TIME__ 当时所处时间

__TIMESTAMP__ 具备时间戳的时间

使用方法:

printf(“%s\n”,__FILE__);

printf(“%d\n”,__LINE__);

printf(“%s\n”,__DATE__);

printf(“%s\n”,__TIME__);

printf(“%s\n”,__TIMESTAMP__);

(二)宏函数

比如:#define add(a,b) ((a)+(b))这样,我们就可以在程序中使用 add(1,2)这样的操作了,而且是类型无关,道理在于:因为是预处理指令,所以在编译前的处理中,编译器会自动把这些按照语法替换掉这些add(a,b)格式的代码。

 

宏函数是我们今天的重点内容:

 

宏定义陷阱1不能省掉的括号

有时可能会不经意间把宏定义成这种方式:

#define add(a,b)  a + b

看起来好像能够正常工作,但是这样的宏定义经不住考验,例如,当add宏用在如下的场景就不能正常工作了:result = add (1,2) * add(3,4); 我们希望add宏能够分别求出各自的和之后在做乘积,想法很好,只不过得不到想要的结果,其原因只要严格按照宏替换展开就明白了。展开后: result = 1+2 * 3+4; //看见这个就知道不对了,结果是11了。

所以,宏函数括号是绝对不能省的!再继续看:

#define add(a,b)  ((a) + (b))

还是上面的宏函数,内层的括号能理解,那么外层为什么还要有括号了,尝试没有最外层的括号的宏函数用在这样的情况下:result = add(1+2,3+4) * add(5+6,7+8);

如果没有最外层的括号,宏定义展开之后表达式就成为:

result = (1+2)+(3+4) * (5+6)+(7+8); 很显然并非我们所要的结果,如果有最外层的括号,那么整个表达式就没问题:result = ((1+2)+(3+4)) * ((5+6)+(7+8));

所以对于宏定义中的括号使用法则只有一个,能多用尽量多用,每一个宏参数和整个表达式都要使用括号,以保证优先级不受更改。

 

宏定义陷阱2不能多出空格

宏的调用有时看起来很像函数,但与其宏调用比起来,宏的定义更要仔细,例如:

#define  f  (x)  ((x) * 2)

这种定义有两种解释方式

① 定义了宏 f, 其中f代表(x)  ((x) * 2)

② 定义了宏 f  (x),其中x是宏参数,f  (x)宏整体代表((x) * 2)

上面2种解释只有第一种是正确的,因为在定义宏的时候不能有空格,而f与(x)之间多出了至少一个空格,所谓这个空格对于宏的定义影响巨大。所以,在定义宏函数的时候,宏名与宏参数之间不能有任何空格的存在。

不过有一点需要提醒,上述的规则只适合于宏的定义,换句话说,只要宏已经定义完毕,那么在调用时无论写成f(5)还是f   (5),其结果两者是等价的。

 

宏定义陷阱3副作用

有时宏的使用表面看起来跟函数很像,但是我们不能把两者等同起来。

例如:   #define max(a,b)  ((a) > (b) ? (a) : (b))

这个看似完美的宏(因为该有的括号都有),有时也会有问题的存在,并且这种问题还不太容易排除,这个就是所谓的“副作用”。看下面的例子:

#define max(a,b) ((a) > (b) ? (a) : (b))

int x[n] = {2,3,1};

int max_value = x[0];

int i = 1;

while(i < 3)

{

max_value = max(max_value,x[i++]);

}

printf(“max value is:> %d\n”,max_value);

运行结果为:1;我们期望是3

结果不对,产生这个问题的根本原因在于max是一个宏,此时的宏在这种环境下产生了副作用,当我们展开max宏 ((max_value) > (x[i++]) ? (max_value) : (x[i++])); 如果表达式(max_value) > (x[i++])的结果不为真实,后面x[i++]就要参与运算,换句话说,此时的x[i++]在一次的求值过程被运算了两次,这就会导致求值的错误。所以,宏在使用时要尽量避免这种会出现副作用的情况,我们可以把x[i++]中的i++单独提出进行运算,即max_value = max(max_value,x[i]); i++; 另一种方法就是把max实现为函数,函数不具有宏的副作用。

 

宏中的#与##

宏中的#,可以把宏参数用实际的参数进行替换,看这样的情形:

#define mul(a,b)  printf(“a * b = %d\n”,((a) * (b)))

mul(3,4);

运行结果: a * b = 12,我们希望能得到 3 * 4 = 12 的效果,那么这样做:

#define mul(a,b)  printf(#a “*” #b “= %d\n”,((a) * (b))) 就可以了。

 

##的作用就是进行字符之间的连接。

在一些代码生成工具里面,经常会对一些变量的命名冠以一定含义的前缀或后缀,

例如,我们输入结构体名Test,为了区分这是用户自己定义的结构体,可能冠以My_的

前缀,这时就需要用到连接符##了。

#define CLASS_NAME(name)  struct My_##name\ //要用续行符

{\

int a;\

char c;\

}

CLASS_NAME(Test);

My_Test mt = {20,’c’};

在我们使用宏CLASS_NAME时,输入的内容为Test,实则产生的是My_Test结构体名。

 

断言与宏:

先来学习一下断言:assert断言是个宏,它的原型定义在<assert.h>中,其作用是如果它的条件返回错误,则终止程序执行。用法:assert(条件表达式); 意思是我“断言”这个表达式是真的,假如为假,程序直接终止。

可以使用断言捕捉不应该发生的非法情况。不要混淆非法情况与错误情况之间的区别,后者是必然存在的并且是一定要作出处理的,所以错误情况是不能用断言的。当进行防错性编程时,如果“不可能发生”的事情的确发生了,则要使用断言进行报警。

一般使用断言对函数的参数进行确认。比如strcpy(char* s1,char* s2);这里的s1,s2为NULL的话,就可以用assert(NULL!=s1); assert(NULL!=s2);来断言。

ASSERT只有在Debug版本中才有效,如果编译为Release版本则被忽略。

使用assert()的缺点是,频繁的调用会极大的影响程序的性能,增加额外的开销。在调试结束后,可以通过在包含#include <assert.h>的语句之前插入 #define NDEBUG 来禁用assert调用,示例代码如下:

#include <stdio.h>

#define NDEBUG

#include <assert.h>

 

断言assert其实是个宏函数,我们可以自己来写代码替换它:

这个是原型:

#define assert(expression) (void)((!!(expression))|| \

(_wassert(_CRT_WIDE(#expression),_CRT_WIDE(__FILE__),(unsigned)(__LINE__)), 0) )

原型在:

#ifdef NDEBUG

#define assert(expression) ((void)0) //这里是定义为NULL?

#endif

 

我们这样写:

#define assert(exp) (void)( (exp) || (_assert(#exp, __FILE__, __LINE__), 0) )

_assert函数也可以自己给出:

void _assert(char* exp,char* file,int line){

printf(“Assert Fail:> %s\n”,exp);

printf(“File:> %s\n”,file);

printf(“Fail line:> %d\n”,line);

abort();

}

 

宏与可变参数

这个放到明天学习吧,因为看起来有点复杂。

 

(2017-03-03 www.vsppc.com)

学习笔记未经允许不得转载:PPC的C/C++和人工智能学习笔记 » C语言基础(17)_预处理和宏定义

分享到:更多 ()

评论 72

评论前必须登录!