条件编译,大致用在三类地方:

  1. 文件内容被重复include时,去掉重包含的内容
  2. 帮助我们的程序的跨平台
  3. 辅助调试程序

一、文件重复包含

文件内容被重复include时,去掉重包含的内容

(1)以C标准库的头文件stdio.h头为例
1)windowsstdio.h

#ifndef _STDIO_H_
#define_STDIO_H_

//头文件的内容
...

#endif //头文件的结尾

2)Linux下的stdio.h头文件

#ifndef _STDIO_H
#define _STDIO_H   

//头文件的内容
...

#endif

疑问:都是stdio.h,但是它们的宏咋不一样呢,一个叫_STDIO_H__STDIO_H

答:如果你仔细阅读这两个stdio.h的话,你会发现这它们里面的内容也不一样,
这个并不奇怪,说明它们是由不同团队编写的,一个是用在windows这边,另一个是用在Linux这边

疑问:宏的头尾怎么都带_呢?

答:这个是库、OS的标识符的命名习惯,目的是方便识别,
因为你一看到带_打头和结尾你就知道,那一定是库、或者OS等标识符

我们自己的应用程序尽量不要使用这种命名格式,以便与库、OS、框架等的标识符进行区别


二、跨平台概念

条件编译帮助我们的程序(C/C++)实现跨环境(平台)

1. 什么是跨平台

所谓的跨平台,就是让“同一个程序”能够应对多个环境,可以在多个环境下运行
这个“环境”主要包含两个方面,一个“软件系统环境”,另一个是“硬件系统环境”

软件系统:

主要指OS,比如有的是windows的,有的是unix的
不同的系统本身又分为了很多不同的版本,比如以windows为例,有xp、w7、w8、w10等

硬件系统:

主要指CPU,比如有的是Intel的,有的是AMD的,有的是ARM的,
同一类型下的CPU也分为不同的版本(架构)。

裸机程序跨环境:

跨的是硬件环境,比如这个程序本来是在某个CPU上运行的,
现在要到另一个CPU上运行,这时程序就涉及到跨硬件环境运行


2. 不同语言所写的程序,是如何实现跨平台的

语言大致分为两类,“编译型语言”和“解释型语言”,参考《九、直接、间接解释器,脚本程序

对于“编译型语言程序”来说,跨环境的核心靠的是编译器,
你想让程序在不同的环境上运行,就需要使用针对不同环境的编译器来重新编译

疑问:对于编译型语言所写程序的跨环境来说,是不是只要换一个针对不同环境的编译器就可以了?

如果不需要修改源码的话,直接换一个编译器就可以了,
但是如果需要修改源码的话,就需要先修改源码,然后再使用不同环境的编译器来重新编译

我们这里介绍使用“条件编译”来实现C/C++跨平台,其实就与C/C++跨平台时源码的修改有关

修改源码、然后编译得到不同环境的可执行程序,其实就是我们常说的移植的过程


Java、JavaScript、VBScript、Perl、Python、Ruby、C#等都是解释型语言,

Java程序被编译后,所得到的并不是可以被直接运行的机器指令,而是一些Java字节码(Java伪指令),
这些字节码放在了.class字节码文件中,我们编译Java程序时所看到的各种.class文件,就是这么来的,
运行java程序时,运行的也是.class文件

运行.class文件时,其实就是运行里面存放的字节码,但是字节码不是机器指令,不能被CPU直接执行,
因此字节码需要被“虚拟机”解释(翻译为)为机器指令后,CPU才能执行

虚拟机:就是一个翻译软件,将字节码翻译(解释)为对应环境的机器指令,虚拟机也被称解释器,
不过要注意的是,虚拟机在翻译时是一句一句进行的,也就是每翻译一句CPU就执行一句,

  1. 解释性语言:跨平台性更好,但是运行效率相对较差
  2. 编译型语言:跨平台性较差些,但是运行效率会更好些

三、C/C++程序如何实现跨平台

c/c++属于典型的编译型语言,跨平台时大致分两种情况

  1. 跨平台时不需要修改源码
    直接换一个针对另一个环境的编译器来重新编译即可
  2. 需要修改源代码
    先修改源代码,然后再换编译器实现重新编译

第一种:不需要修改源码,直接换一个环境的编译器重新编译

什么样的C/C++程序跨平台时,不需要修改源码

只要程序不包含“平台差异代码”,包含的都是“通用代码”的话,跨平台时,不需要修改源码。

疑问:什么样的代码是“通用代码”?

  • 代码只涉及C/C++基本语法,只要不同环境支持该语言,那么基本语法都是支持的
  • 当代码调用了库函数时,只要该库是不同平台都支持的通用库的话,调用库函数的代码也是通用代码
    比如C标准库就是一个通用库,windowsLinuxUnixOS都支持,
    所以在程序中调用C标准库的printf函数时,不管是windowsLinuxUnix都支持

· 例子

#include <stdio.h>

int main(void)
{
    int a = 100;

    /* 只与基本语法归相关的代码 */
    while(1)
    {
        a = a + 10; 
        if(a==100) break;
    }

    printf("a = %d\n", a); //不同环境都支持的通用库函数接口

    return 0;
}

以上这个是通用代码,跨平台时不管是在Windows下、还是在Linux下运行,
这个通用代码时不需要修改源码的,换编译器重新编译即可

由于C标准库几乎被大多数的OS支持,所以C程序中有printfscanfmalloc等时,
不管在什么平台下都能用,跨平台时不需要修改

如果平台不支持你要的库,但是你还想用,你就必须自己来搞定这个库,有两种搞定方法,

  1. 第一种:在该环境下安装对应的库
  2. 第二种:直接将库和“可执行程序”放在一起,发布程序时一起发布

第二种:需要修改源码

跨平台时为了减少麻烦,我们建议在程序中最好尽量只写通用代码,如此一来跨平台时源码就不用修改了,
但有些时候只能做到80%~90%是通用代码,程序中有10%~20%的与平台相关的代码

比如因为某些特殊原因,C程序中需要直接调用OS API,但是不同的OSOS API又有区别,
OS API相关的代码就是典型的平台相关的代码

这里以Windows、Linux为例,为了让我们的C/C++程序能够很好的面对Windows、Linux
有如下两种解决办法:

  1. 第一种:写两份独立的功能完全相同的程序,一份专门针对Windows,另一分专门针对Linux
  2. 第二种:只写一份代码,使用条件编译来处理平台相关的代码

Windows下运行的C程序

#include <windows.h>  //windows OS API所需的头文件

int main(void)
{
    /* 通用代码 */
    int i = 0;

    while(1)
    {
        if(i>100) break;
        else i++;
    }

    /* 平台相关代码:windows的操作文件的OS API */
    HANDLE hfile = CreateFile(".\file", GENERIC_READ | GENERIC_WRITE, FILE_SHARE_WRITE \
    | FILE_SHARE_READ, NULL, OPEN_EXISTING, NULL, NULL);
    int dwRead = 0;

    WriteFile(hfile, &i, sizeof(i), &dwRead, 0);   //将i写到file中

    return 0;
}

Linux下运行的C程序

/* Linux的OS API所需的头文件 */
#include <sys/types.h>
#include <sys/stat.h>
#include <unistd.h>
#include <fcntl.h>

int main(void)
{
    /* 通用代码 */
    int i = 0;

    while(1)
    {
        if(i>100) break;
        else i++;
    }

    /* 平台相关代码:Linux的操作文件的OS API */
    int fd = open("./file", O_RDWR | O_CREAT, 0774);
    write(fd, &i, sizeof(i));  //将i写到file

    return 0;
}

这种方式的缺点

由于%80~%90都是相同的通用代码,仅为了那一点平台相关代码的不同,就要写两份独立的程序,
显然是不是很合适,最起码很浪费时间,如果写一份就能搞定的话,这是最好的

只写一份代码,使用条件编译来兼容。
这里举一个非常简单的例子,这个例子不具有实用性,但是确实能够说明说明“条件编译”对于跨平台的重
要性。

#define WINDOWS

#ifdef WINDOWS
# include <windows.h>
#elif defined LINUX
# include <sys/types.h>
# include <sys/stat.h>
# include <unistd.h>
# include <fcntl.h>
#endif


int main(void)
{
    /* 通用代码 */
    int i = 0;

    while(1)
    {
        if(i>100) break;
        else i++;
    }


#ifdef WINDOWS
    /* 平台相关代码:windows的操作文件的OS API */
    HANDLE hfile = CreateFileA(".\file", GENERIC_READ | GENERIC_WRITE, FILE_SHARE_WRITE \
    | FILE_SHARE_READ, NULL, OPEN_EXISTING, NULL, NULL);
    int dwRead = 0;
    WrieFile(hfile, &i, sizeof(i), &dwRead, 0);   //将i写到file中

#elif defined LINUX
    /* 平台相关代码:Linux的操作文件的OS API */
    int fd = open("./file", O_RDWR | O_CREAT, 0774);
    write(fd, &i, sizeof(i));  //将i写到file中

#endif

    return 0;
}
  • 修改代码中的宏,然后通过“条件编译”来保留对应平台的代码
  • 使用对应平台的编译器来重新编译

在真实开发中修改源码时,其实不仅仅只会打开和关闭条件编译,有时还需要修改代码中其它相关数据。


四、调试

条件编译在调试中的作用:

  1. 注销代码
  2. 开、关调试宏

使用条件编译来注销代码,代码块的注销,使用条件编译更方便

#if 0    //0就是注销(去掉)代码,1就保留代码

int fun(int va)
{
    int a = 100;

    return a*va;
}

#endif

int main(void)
{
    int ret = 0;

    #if 0
    ret = fun(1000);

    printf("ret = %d\n", ret);
    #endif 

    retutn 0;
}

开、关调试宏

#include <stdio.h>

#ifdef DEBUG1
# define DBG1 printf("%s %d %s\n", __FILE__, __LINE__, __func__);  
#else
# define DBG1
#endif


#ifdef DEBUG2
# define DBG2(info1, info2) printf(info1, info2);                                               
#else
# define DBG2(info1, info2)
#endif

void exchange(int *p1, int *p2)
{      
    DBG1
    int tmp = 0;
    DBG1

    tmp = *p1;
    DBG1
    *p1 = *p2;
    DBG1
    *p2 = tmp;
    DBG1
}

int main(void)
{       
    int a = 10; 
    int b = 30; 

    DBG2("%s\n", "1111");

    DBG1                            
    exchange(&a, &b);
    DBG1

    printf("a=%d, b=%d\n", a, b);

    DBG1

    return 0;
}

通过条件编译可以将调试宏快速可打开和关闭,方便快速查看调试结果,
等调试完毕后,我们再将程序中的调试宏删掉


五、配置文件

为什么需要配置文件?

当一个C/C++程序非常庞大时,程序中往往会有很多的条件编译,
如果全都自己在源码中一个一个的修改相关宏来打开和关闭条件编译的话,这会非常的麻烦,
此时就需要用到配置文件来帮我们自动修改相应的宏,以打开和关闭相应的条件编译

使用配置文件来打开、关闭条件编译的原理

配置文件其实是一个脚本文件

  1. 修改配置文件:通过配置文件来决定,我想定义哪些宏,想删除哪些宏
  2. 执行配置文件:生成config.h,配置文件会往config.h中输入各种条件编译需要的宏定义
  3. 编译程序:在源码所有.c/.h中包含config.h
    预编译时就可以通过config.h中的宏来打开和关闭条件编译了

是不是只要程序中有条件编译,就用需要用到配置文件?当然不是!

什么情况下,我们没必要使用配置文件

  1. 如果你的条件编译是用来注释代码,以及调试程序用的,这些条件编译完全由我们自己手动定义宏来打开和关闭
  2. 如果代码很简单,全都是通用代码,根本没有用到任何的条件编译,此时根本不需要配置文件
  3. 你的C虽然用到条件编译了,但是用的非常少,
    这样的话我们完全可以自己在源码中手动修改各种宏定义来打开和关闭条件编译,也不需要配置文件

总之在我们平时的开发中,对于我们自己写的C程序来说,几乎用不到配置文件

我们什么时候会用到配置文件呢?

  1. 如果你或者你的团队编写的C工程项目非常复杂,里面涉及大量的条件编译,此时就需要用到配置文件,
    不过这种情况很少见,真的遇到时,那就需要我们自己写配置文件了
  2. 下载移植官方C/C++源码时,源码一定会提供配置文件
    比如我们后面移植ubootLinux内核,ubootLinux内核的代码不可能我们自己写,
    所以必须到官网去下载官方提供源码,然后修改源码并编译它

官方一定会提供配置文件给我们,我们直接修改配置文件即可,不需要阅读源码,
就算阅读源码,也只需要阅读关键部分的源码即可

其实在实际开发中,我们更多的是去阅读和修改已有的配置文件,而不是制作配置文件