[学习笔记]C语言 预处理命令(宏定义和条件编译)

    选择打赏方式


预处理指令是以#号开头的代码行,# 号必须是该行除了任何空白字符外的第一个字符。# 后是指令关键字,在关键字和 # 号之间允许存在任意个数的空白字符,整行语句构成了一条预处理指令,该指令将在编译器进行编译之前对源代码做某些转换。


本章涉及到的部分预处理指令:


指令 说明
# 空指令,无任何效果
#include 包含一个源代码文件
#define 定义宏
#undef 取消已定义的宏
#if 如果给定条件为真,则编译下面代码
#ifdef 如果宏已经定义,则编译下面代码
#ifndef 如果宏没有定义,则编译下面代码
#elif 如果前面的#if给定条件不为真,当前条件为真,则编译下面代码
#endif 结束一个#if……#else条件编译块


预处理功能是C语言特有的功能,它是在对源程序正式编译前由预处理程序完成的,程序员在程序中用预处理命令来调用这些功能。 

宏定义可以带有参数,宏调用时是以实参代换形参,而不是“值传送”。

为了避免宏代换时发生错误,宏定义中的字符串应加括号,字符串中出现的形式参数两边也应加括号。


文件包含是预处理的一个重要功能,它可用来把多个源文件连接成一个源文件进行编译,结果将生成一个目标文件。

条件编译允许只编译源程序中满足条件的程序段,使生成的目标程序较短,从而减少了内存的开销并提高了程序的效率。

使用预处理功能便于程序的修改、阅读、移植和调试,也便于实现模块化程序设计。


一、预处理指令定义

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


简单的说,预处理指令会被预处理程序在编译链接的时候处理掉,该引入的代码引入,该替换的替换,编译不同平台的版本会引入不同的头文件,剔除掉不需要的代码,只编译需要的,更好的适应不同平台


二、#include用法详解

#include叫做文件包含命令,用来引入对应的头文件(.h文件)。#include 也是C语言预处理命令的一种。

#include 的处理过程很简单,就是将头文件的内容插入到该命令所在的位置,从而把头文件和当前源文件连接成一个源文件,这与复制粘贴的效果相同。

#include 的用法有两种,如下所示: 
#include <stdHeader.h>
#include "myHeader.h"
使用尖括号< >和双引号" "的区别在于头文件的搜索路径不同: 
使用尖括号< >,编译器会到系统路径下查找头文件;
而使用双引号" ",编译器首先在当前目录下查找头文件,如果没有找到,再到系统路径下查找。

也就是说,使用双引号比使用尖括号多了一个查找路径,它的功能更为强大。

前面我们一直使用尖括号来引入标准头文件,现在我们也可以使用双引号了,如下所示: 
#include "stdio.h"
#include "stdlib.h"


stdio.h 和 stdlib.h 都是标准头文件,它们存放于系统路径下,所以使用尖括号和双引号都能够成功引入;而我们自己编写的头文件,一般存放于当前项目的路径下,所以不能使用尖括号,只能使用双引号。

约定俗成的规矩:

标准库用尖括号,自己项目的库用双引号。


关于 #include 用法的注意事项: 
一个 #include 命令只能包含一个头文件,多个头文件需要多个 #include 命令。
同一个头文件可以被多次引入,多次引入的效果和一次引入的效果相同,因为头文件在代码层面有防止重复引入的机制。
文件包含允许嵌套,也就是说在一个被包含的文件中又可以包含另一个文件。
头文件只能包含变量和函数的声明,不能包含定义,否则在多次引入时会引起重复定义错误。


三、宏定义#define用法详解

#define 叫做宏定义命令,它也是C语言预处理命令的一种。所谓宏定义,就是用一个标识符来表示一个字符串,如果在后面的代码中出现了该标识符,那么就全部替换成指定的字符串。


宏定义的一般形式为:

#define  宏名  字符串


#表示这是一条预处理命令,所有的预处理命令都以 # 开头。宏名是标识符的一种,命名规则和变量相同。字符串可以是数字、表达式、if 语句、函数等。这里所说的字符串是一般意义上的字符序列,不要和C语言中的字符串等同,它不需要双引号。


代码例子:

#include <stdio.h>

#define N 100

int main(){
    int sum = 20 + N;
    printf("%d\n", sum);
    return 0;
}

结果:

120



注意:

1.宏定义的表达式在编译阶段会被替换进程序的具体代码行里,表达式需要加括号,否则将不会被当作一个整体,从而产生歧义。

2.宏定义不需要加分号,分号也会被当成宏定义字符串的一部分参与实际代码替换。

3.宏定义必须写在函数之外,其作用域为宏定义命令起到源程序结束。如要终止其作用域可使用#undef命令。
4.引号内的的一切都是字符或者字符串的一部分,宏定义不会参与替换。

5.宏定义允许嵌套,再宏展开时会逐层替换。

6.宏定义习惯使用大写字母,但也只是约定俗成的,不强制。

7.宏定义也可以用来表示数据类型

最重要的一点:

#define定义的宏对于编译器来说,仅仅是把他们替换进相应位置,只是字符串替换,并不会有什么特殊处理。


例子代码 3:

#define PI 3.14159

int main(){
    // Code
    return 0;
}

#undef PI

void func(){
    // Code
}


例子 宏定义表示数据类型和typedef的差别

#define PIN1 int *
typedef int *PIN2;  


PIN1 a, b;
在宏代换后变成: 
int * a, b;
表示 a 是指向整型的指针变量,而 b 是整型变量。


IN2 a,b;
表示 a、b 都是指向整型的指针变量。因为 PIN2 是一个新的、完整的数据类型。


这也印证了,宏定义真的是简单的字符串替换,把PIN1的位置换成了int *而已。


四、带参数宏定义

C语言允许宏带有参数。在宏定义中的参数称为“形式参数”,在宏调用中的参数称为“实际参数”,这点和函数有些类似。
对带参数的宏,在展开过程中不仅要进行字符串替换,还要用实参去替换形参。

带参宏定义的一般形式为: 
#define 宏名(形参列表) 字符串
在字符串中可以含有各个形参。

带参宏调用的一般形式为: 
宏名(实参列表);
例如: 
#define M(y) y*y+3*y  //宏定义
// TODO:
k=M(5);  //宏调用
在宏展开时,用实参 5 去代替形参 y,经预处理程序展开后的语句为k=5*5+3*5。

说明:

1) 带参宏定义中,形参之间可以出现空格,但是宏名和形参列表之间不能有空格出现。

2) 在带参宏定义中,不会为形式参数分配内存,因此不必指明数据类型。而在宏调用中,实参包含了具体的数据,要用它们去替换形参,因此实参必须要指明数据类型。

带参宏定义的本质依然是字符串替换,不会有值传递。

3) 在宏定义中,字符串内的形参通常要用括号括起来以避免出错,对于带参宏定义不仅要在参数两侧加括号,还应该在整个字符串外加括号。

如:

#define SQ(y) ((y)*(y))


注意:由于宏定义的传参并不是真正的值传递,而是字符串替换,所以以下用法是错误的:

#include <stdio.h>

#define SQ(y) ((y)*(y))

int main(){
    int i=1;
    while(i<=5){
        printf("%d^2 = %d\n", i, SQ(i++));
    }
    return 0;
}


这样的代码并没有像函数一样传递i++的值进来,而是被替换为 ((i++)*(i++)),这样每循环一次 i 的值增加 2,所以最终只循环 3  次。


宏定义并不是函数,即使功能很简单的函数都无法取代,使用的时候千万小心谨慎!


五、宏参数的字符串化和宏参数的连接

在宏定义中,有时还会用到#和##两个符号,它们能够对宏参数进行操作。 
# 的用法
#用来将宏参数转换为字符串,也就是在宏参数的开头和末尾添加引号。例如有如下宏定义: 
#define STR(s) #s
那么: 
printf("%s", STR(c.biancheng.net));
printf("%s", STR("c.biancheng.net"));
分别被展开为: 
printf("%s", "c.biancheng.net");
printf("%s", "\"c.biancheng.net\"");
可以发现,即使给宏参数“传递”的数据中包含引号,使用#仍然会在两头添加新的引号,而原来的引号会被转义。


##的用法
##称为连接符,用来将宏参数或其他的串连接起来。例如有如下的宏定义: 
#define CON1(a, b) a##e##b
#define CON2(a, b) a##b##00
那么: 
printf("%f\n", CON1(8.5, 2));
printf("%d\n", CON2(12, 34));
将被展开为: 
printf("%f\n", 8.5e2);
printf("%d\n", 123400);

六、几个预定义宏

顾名思义,预定义宏就是已经预先定义好的宏,我们可以直接使用,无需再重新定义。

ANSI C 规定了以下几个预定义宏,它们在各个编译器下都可以使用: 
__LINE__:表示当前源代码的行号;
__FILE__:表示当前源文件的名称;
__DATE__:表示当前的编译日期;
__TIME__:表示当前的编译时间;
__STDC__:当要求程序严格遵循ANSI C标准时该标识被赋值为1;
__cplusplus:当编写C++程序时该标识符被定义。

预定义宏演示: 

#include <stdio.h>
#include <stdlib.h>

int main() {
    printf("Date : %s\n", __DATE__);
    printf("Time : %s\n", __TIME__);
    printf("File : %s\n", __FILE__);
    printf("Line : %d\n", __LINE__);

    system("pause");
    return 0;
}

结果:

TIM截图20190629204911.png


七、C语言条件编译详解

这些操作都是在预处理阶段完成的,多余的代码以及所有的宏都不会参与编译,不仅保证了代码的正确性,还减小了编译后文件的体积。

这种能够根据不同情况编译不同代码、产生不同目标文件的机制,称为条件编译。条件编译是预处理程序的功能,不是编译器的功能。


#if 的用法
#if 用法的一般格式为: 


#if 整型常量表达式1
    程序段1
#elif 整型常量表达式2
    程序段2
#elif 整型常量表达式3
    程序段3
#else
    程序段4
#endif


它的意思是:如常“表达式1”的值为真(非0),就对“程序段1”进行编译,否则就计算“表达式2”,结果为真的话就对“程序段2”进行编译,为假的话就继续往下匹配,直到遇到值为真的表达式,或者遇到 #else。这一点和 if else 非常类似。

需要注意的是,#if 命令要求判断条件为“整型常量表达式”,也就是说,表达式中不能包含变量,而且结果必须是整数;而 if 后面的表达式没有限制,只要符合语法就行。这是 #if 和 if 的一个重要区别。

#elif 和 #else 也可以省略,如下所示: 

#include <stdio.h>
int main(){
    #if _WIN32
        printf("This is Windows!\n");
    #else
        printf("Unknown platform!\n");
    #endif
   
    #if __linux__
        printf("This is Linux!\n");
    #endif

    return 0;
}

#ifdef 的用法
#ifdef 用法的一般格式为: 


#ifdef  宏名
    程序段1
#else
    程序段2
#endif
它的意思是,如果当前的宏已被定义过,则对“程序段1”进行编译,否则对“程序段2”进行编译。

也可以省略 #else: 
#ifdef  宏名
    程序段
#endif

VS/VC 有两种编译模式,Debug 和 Release。在学习过程中,我们通常使用 Debug 模式,这样便于程序的调试;而最终发布的程序,要使用 Release 模式,这样编译器会进行很多优化,提高程序运行效率,删除冗余信息。


演示代码:

#include <stdio.h>
#include <stdlib.h>
int main(){
    #ifdef _DEBUG
        printf("正在使用 Debug 模式编译程序...\n");
    #else
        printf("正在使用 Release 模式编译程序...\n");
    #endif

    system("pause");
    return 0;
}

当以 Debug 模式编译程序时,宏 _DEBUG 会被定义,预处器会保留第 5 行代码,删除第 7 行代码。反之会删除第 5 行,保留第 7 行。


#ifndef 的用法
#ifndef 用法的一般格式为: 


#ifndef 宏名
    程序段1 
#else 
    程序段2 
#endif


与 #ifdef 相比,仅仅是将 #ifdef 改为了 #ifndef。它的意思是,如果当前的宏未被定义,则对“程序段1”进行编译,否则对“程序段2”进行编译,这与 #ifdef 的功能正好相反。 
三者之间的区别
最后需要注意的是,#if 后面跟的是“整型常量表达式”,而 #ifdef 和 #ifndef 后面跟的只能是一个宏名,不能是其他的。


八、#error命令,阻止程序编译

#error 指令用于在编译期间产生错误信息,并阻止程序的编译,其形式如下: 
#error error_message
例如,我们的程序针对 Linux 编写,不保证兼容 Windows,那么可以这样做:

#ifdef WIN32
#error This programme cannot compile at Windows Platform
#endif

WIN32 是 Windows 下的预定义宏。当用户在 Windows 下编译该程序时,由于定义了 WIN32 这个宏,所以会执行 #error 命令,提示用户发生了编译错误,错误信息是: 
This programme cannot compile at Windows Platform

如图所示:

TIM截图20190630080938.png

需要注意的是:报错信息不需要加引号" ",如果加上,引号会被一起输出。


限定程序必须由C++编译例子

#ifndef __cplusplus
#error 当前程序必须以C++方式编译
#endif

版权声明:若无特殊注明,本文皆为《 8964CN 》原创,转载请保留文章出处。
本文链接:[学习笔记]C语言 预处理命令(宏定义和条件编译) http://www.8964cn.net/?post=62
正文到此结束

热门推荐

发表吐槽

你肿么看?

你还可以输入 250 / 250 个字

嘻嘻 大笑 可怜 吃惊 害羞 调皮 鄙视 示爱 大哭 开心 偷笑 嘘 奸笑 委屈 抱抱 愤怒 思考 日了狗 胜利 不高兴 阴险 乖 酷 滑稽

评论信息框

吃奶的力气提交吐槽中...


既然没有吐槽,那就赶紧抢沙发吧!