【C语言进阶剖析】22、条件编译使用分析


开始之前我们先说一个问题,很多软件都有高端版本和低端版本,这是如何做到的呢,如果让两拨人分别开发高端版本和低端版本,这是可以的,但是显然很不现实,如何只做一款产品就有高端版本和低端版本呢,先别急着找答案,我们继续向下看。

1 基本概念

  • 条件编译的行为类似于 C 语言中的 if…else…
  • 条件编译是预编译指示命令,用于控制是否编译某段代码

先来看段代码,对条件编译有个直观的概念

// 22-1.c
#include<stdio.h>
#define C 1
int main()
{
    const char* s;
    #if (C == 1)
        s = "This is first printf...\n";
    #else
        s = "This is second printf...\n";
    #endif
    printf("%s", s);
    return 0;
}

代码很简单,就是一个 if…else…语句,看打印哪一行。编译,运行结果如下:

$ gcc 22-1.c -o 22-1
$ ./22-1
This is first printf...

可以看到和我们预想的结果完全一样,因为定义了宏 C 为 1,if 为真,所以打印 This is first printf…。这和 C 语言中的 if…else…类似

下面有个问题:条件编译真的和 C 语言中的 if…else…一样吗?
想知道是不是和 if…else 一样,下面进行单独编译,为了避免不必要的信息,将上面代码中的第 2 行注释,编译如下:

$ gcc -E 22-1.c -o 22-1.i

打开文件 22-1.i 如下:

# 1 "22-1.c"
# 1 "<built-in>"
# 1 "<command-line>"
# 31 "<command-line>"
# 1 "/usr/include/stdc-predef.h" 1 3 4
# 32 "<command-line>" 2
# 1 "22-1.c"



int main()
{
    const char* s;

        s = "This is first printf...\n";



    printf("%s", s);
    return 0;
}
  • 可以看到 if…else 没有了,我们定义的宏 C 也没有了。
  • 其实条件编译是用来指示预处理器的,根据条件编译保留什么代码,删除什么代码,用于控制后续编译器编译哪段代码。

2 条件编译的本质

  • 预编译器根据条件编译指令有选择的删除代码
  • 编译器不知道代码分支的存在
  • if…else…语句在运行期进行分支判断
  • 条件编译指令在预编译期进行分支判断
  • 可以通过命令行定义宏,不用在代码中 #define

通过命令行定义宏的语法如下:

  • gcc -Dmacro = value file.c
  • gcc -Dmacro file.c

前者用来定义宏的值,后者用来定义宏是否存在

下面我们就来尝试一下通过命令行定义宏

// 22-2.c
#include<stdio.h>
int main()
{
    const char* s;
    #if (C == 1)
        s = "This is first printf...\n";
    #else
        s = "This is second printf...\n";
    #endif
    printf("%s", s);
    return 0;
}

编译运行结果如下:

$ gcc 22-2.c -o 22-2
$ ./22-2
This is second printf...

打印了第 9 行代码,我们我们想打印第 7 行代码,可以通过命令行定义宏,编译运行结果如下:

$ gcc -DC=1 22-2.c -o 22-2
$ ./22-2
This is first printf...

这里在命令行定义了宏的值,打印了This is first printf…也就是说第 7 行被保留了,第 9 行被删除了。

我们来看看命令行定义宏的另外一种方式,定义宏是否存在,宏和变量不一样,宏既可以表示一个值,也可以表示一个标示符。需要修改一下代码,修改后的如下:

// 22-2.c
#include<stdio.h>
int main()
{
    const char* s;
    #ifdef C
        s = "This is first printf...\n";
    #else
        s = "This is second printf...\n";
    #endif
    printf("%s", s);
    return 0;
}

编译运行如下:

$ gcc -DC 22-2.c -o 22-2
$ ./22-2
This is first printf...

为了更透彻的理解,我们将第 2 行注释,只进行预编译,看一下预编译之后的结果:

$ gcc -DC -E 22-2.c -o 22-2.i

打开文件 22-2.i 如下:

# 1 "22-2.c"
# 1 "<built-in>"
# 1 "<command-line>"
# 1 "/usr/include/stdc-predef.h" 1 3 4
# 1 "<command-line>" 2
# 1 "22-2.c"


int main()
{
    const char* s;

        s = "This is first printf...\n";



    printf("%s", s);
    return 0;
}

可以看到使用命令行定义了宏 C,预编译器保留了第 7 行代码,删除了第 9 行代码
反过来,如果在命令行不定义宏 C,预编译器将删除第 7 行代码,保留第 9 行代码

3 #include 的本质

  • #include 的本质是将已经存在的文件内容嵌入到当前文件中
  • #include 的间接包含同样会产生嵌入文件内容的操作

在预处理时,头文件中的内容会被复制过来,有人就说了,那 #include 还有什么好学的,不就是直接复制过来吗?
下面就来考虑一个问题:间接包含同一个头文件是否会产生编译错误?具体如下:文件 test.c 中包含头文件 test.h 和 global.h,test.h 中也包含了文件 global.h,关系如下图所示,也就是说头文件 global.h 被包含了两次,那么编译能通过吗?
在这里插入图片描述
我们来尝试一下

// 22-3.c
#include <stdio.h>
#include "test.h"
#include "global.h"
int main()
{
    const char* s = hello_world();
    int g = global;
    printf("%s\n", NAME);
    printf("%d\n", g);
    return 0;
}
// test.h
#include "global.h"
const char* NAME = "test.h";
char* hello_world(){    
    return "Hello world!\n";
}
// global.h
int global = 10;

对文件进行编译,结果如下:
在这里插入图片描述
编译器提示我们变量 global 重复定义了,为了一探究竟,我们单步编译一下看看。将 #include <stdio.h> 注释,单步编译

$ gcc -E 22-3.c -o 22-3.i

打开文件 22-3.i,内容如下:

# 1 "22-3.c"
# 1 "<built-in>"
# 1 "<command-line>"
# 31 "<command-line>"
# 1 "/usr/include/stdc-predef.h" 1 3 4
# 32 "<command-line>" 2
# 1 "22-3.c"


# 1 "test.h" 1

# 1 "global.h" 1

int global = 10;
# 3 "test.h" 2
const char* NAME = "test.h";
char* hello_world(){
    return "Hello world!\n";
}
# 4 "22-3.c" 2
# 1 "global.h" 1

int global = 10;
# 5 "22-3.c" 2
int main()
{
    const char* s = hello_world();
    int g = global;
    printf("%s\n", NAME);
    printf("%d\n", g);
    return 0;
}

代码第 10 行表示这里是头文件 test.h,如何处理呢,就是直接将头文件 test.h 的内容复制过来,由于 test.h 中又包含了 global.h 所以将 global.h 的内容复制过来,第 12 行表示这是复制过来的 global.h 文件;处理完 #include “test.h” 后,再处理头文件
#include “global.h”,如第 21 行所示,同样是将 global.h 中的头文件直接复制过来。

这就出现问题了,global.h 中的内容被复制了两遍,所以出现了在 14 行和第 23 行两次定义变量 global 的情况,编译器提示我们变量 global 重复定义了。

所以头文件的重复包含在软件产品中是不正确的,会使得编译器看到多段相同的代码,那么问题来了 ,现在的软件产品,有成千上万的文件,怎么能保证不重复包含呢,这显然是不可能的,那么怎么保证重复包含也不出现错误呢?

下面我们将代码修改一下,22-3.c 的代码不改动,修改文件 test.h 和文件 global.h,如下所示:

// test.h
#ifndef _TEST_H_
#define _TEST_H_
#include "global.h"
const char* NAME = "test.h";
char* hello_world(){    
    return "Hello world!\n";
}
#endif
// global.h
#ifndef _GLOBAL_H_
#define _GLOBAL_H_
int global = 10;
#endif

以 global.h 为例,#ifndef _GLOBAL_H_ 意思是如果没有定义宏 _GLOBAL_H_ ,那么将执行第 3 行代码,#define _GLOBAL_H_,定义宏 _GLOBAL_H_,如果已经定义了宏 _GLOBAL_H_,下面的代码将直接被删除,所以只有第一次引用头文件时代码会被复制过来,同时也定义了宏,后面再次引用该头文件由于已经定义了宏,头文件中的代码将直接删除,不存在将一个文件拷贝多份的情况,避免了重复定义。test.h 也是一样的。

下面我们来编译、运行一下,结果如下:

$ gcc 22-3.c -o 22-3
$ ./22-3
test.h
10

我们再次单步编译一下,只进行预处理,看看结果如何?

$ gcc -E 22-3.c -o 22-3.i

打开文件 22-3.i

# 1 "22-3.c"
# 1 "<built-in>"
# 1 "<command-line>"
# 31 "<command-line>"
# 1 "/usr/include/stdc-predef.h" 1 3 4
# 32 "<command-line>" 2
# 1 "22-3.c"


# 1 "test.h" 1



# 1 "global.h" 1



int global = 10;
# 5 "test.h" 2
const char* NAME = "test.h";
char* hello_world(){
    return "Hello world!\n";
}
# 4 "22-3.c" 2

int main()
{
    const char* s = hello_world();
    int g = global;
    printf("%s\n", NAME);
    printf("%d\n", g);
    return 0;
}

可以看到 global.h 文件只被复制了一次。所以使用条件编译可以避免头文件被复制多份的情况,多次引用也不会出现问题,只有第一次的引用会被复制过来,后面的都不会再复制了

下面我们总结一下:

  • 条件编译可以解决头文件重复包含的编译错误

语法如下:
在这里插入图片描述

4 条件编译的意义

  • 条件编译使得我们可以按不同的条件编译不同的代码段,因而产生不同的目标代码
  • #if…#else…#endif 被预编译器处理,而 if…else…语句被编译器处理,必然被编译进目标代码
  • 实际工程中条件编译主要用于以下两种情况
    1. 不同的产品线共用一份代码
    2. 区分编译产品的调试版和发布版

利用条件编译我们就可以有选择的删除或保留代码,这样就可以编译出不同的产品,也就有了高版本和低版本。具体是怎么做的呢,我们来实际操作一下。

// 22-4.c
#include<stdio.h>
#include"product.h"
#if DEBUG
    #define LOG(s) printf("[%s:%d] %s\n", __FILE__, __LINE__, s)
#else
    #define LOG(s) NULL
#endif

#if HIGH
void f()
{
    printf("This is the high level product!\n");
}
#else
void f()
{
}
#endif

int main()
{
    LOG("Enter main()...");
    f();
    printf("1. Query Informatian.\n");
    printf("2. Record Informatian.\n");
    printf("3. Delete Informatian.\n");
    
    #if HIGH
    printf("4. High Level Query.\n");
    printf("5. Manual Service.\n");
    printf("6. Exit.\n");
    #else
    printf("4. Exit.\n");
    #endif
    LOG("Exit main()...");
    return 0;
}
// product.h
#define DEBUG 1
#define HIGH  1
  • 上面代码,第 4 行到第 8 行,如果宏 DEBUG 为真,定义一个宏 LOG(s) 打印日志,否则定义的 LOG(s) 为空,啥也不干。
  • 第 10 行到第 19 行,表示如果定义的宏 HIGH 为真,定义函数 f() ,该函数打印一条版本信息,否则,定义函数 f() ,该函数为空,啥也不干。
  • 23 行打印日志,24 行执行函数 f()
  • 第 29 行到 第 35 行表示如果宏 HIGH 为真,执行下面三条打印语句(软件高级功能),否则执行一条打印语句
  • 所以宏 DEBUG 用来表示这是一个调试版程序还是一个发布版程序,宏 HIGH 用来表示是一个高端版软件还是一个低端版软件

下面我们就来执行一下程序,执行结果如下:

$ gcc 22-4.c -o 22-4
$ ./22-4
[22-4.c:23] Enter main()...
This is the high level product!
1. Query Informatian.
2. Record Informatian.
3. Delete Informatian.
4. High Level Query.
5. Manual Service.
6. Exit.
[22-4.c:36] Exit main()...

上面的执行结构表示,这是一个调试版的高端版本,如果想要低端版本的发布产品,应该怎么做呢,很简单,修改头文件 product.h 即可,修改如下:

// product.h
#define DEBUG 0
#define HIGH  0

再次编译运行,结果如下:

$ gcc 22-4.c -o 22-4
$ ./22-4
1. Query Informatian.
2. Record Informatian.
3. Delete Informatian.
4. Exit.

结果显示。高端功能没有运行,日志也没有打印出来,所以这是一个低端的发布产品。

5 小结

1、通过编译器命令行能够定义预处理器使用的宏
2、条件编译可以避免重复包含同一个头文件
3、条件编译在工程开发中可以区别不同的产品线代码
4、条件编译可以定义产品的发布版和调试版

已标记关键词 清除标记
相关推荐
©️2020 CSDN 皮肤主题: 大白 设计师:CSDN官方博客 返回首页