首页 /  技术专区  /  C 宽屏模式 >

条件编译——在C跨平台时的作用

1、c/c++程序如何实现跨平台

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

        第一种:跨平台时不需要修改源码

        直接换一个针对另一个环境的编译器来重新编译即可。


        第二种:需要修改源代码

        先修改源代码,然后再换编译器实现重新编译。

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

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

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

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

如果代码是如下两种情况的话,就是通用代码。

        - 代码只涉及c/c++基本语法

        只要不同环境支持该语言,那么基本语法都是支持的。

        - 当代码调用了库函数时,只要该库是不同平台都支持的通用库的话,调用库函数的代码也是通用代码

        比如C标准库就是一个通用库,windows、Linux、Unix等OS都支持,所以在程序中调用c标准库的printf函数时,不管是windows、Linux、unix都支持,所以跨平台时printf函数不需要修改。

例子

#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下运行,这个通用代码时不需要修改源码的,换编译器重新编译即可。

image.png

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

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

        第一种:在该环境下安装对应的库

        第二种:直接将库和“可执行程序”放在一起,发布程序时一起发布


(b)第二种:需要修改源码

        跨平台时为了减少麻烦,我们建议在程序中最好尽量只写通用代码,如此一来跨平台时源码就不用修改了。

但是问题是,有些时候只能做到80%~90%是通用代码,程序中有10%~20%的与平台相关的代码。

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

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

        第一种:写两份独立的功能完全相同的程序,一份专门针对Windows,另一分专门针对Linux

        第二种:只写一份代码,使用条件编译来处理平台相关的代码。

· 写两份独立的C程序,一个针对Windows,另一个针对Linux

- 在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;
}

                                   

                                         windows环境编译器

windows的C程序 ————————————————> 在windows下的可执行程序

- 在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;
}

                         

                                     Linux环境编译器       

Linux的C程序 ————————————————> 在Linux下的可执行程序

· 这种方式的缺点

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

不过如果80%的代码都是平台相关代码,只有20%是通用代码的话,此时写两份完全独立的程序其实更划算。

· 只写一份代码,使用条件编译来兼容。

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

#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;
}

- 修改代码中的宏,然后通过“条件编译”来保留对应平台的代码

- 使用对应平台的编译器来重新编译

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

2、java有条件编译吗,java程序跨平台时需要修改源码吗?

前面说过,只要环境安装了java虚拟机,java只需要一次编译即可到处运行,通过“只需要编译一次”的这句话我们就能感觉到,java程序跨平台时,其实不需要修改源码。

为什么java程序跨平台时不需要改源码?

因为在java程序的代码中涉及的通用代码,没有平台相关的代码,跨平台完全由“虚拟机”来完成,总之java被设计为了一种跨平台性非常好的语言。

正是由于java是一个跨平台性非常好的语言,所以当初借鉴C来设计java时,设计者果断去掉了C中“条件编译”,总之这里就是想告诉大家,java是没有条件编译这个玩意的,不过有类似可以模拟“C条件编译”东西。


3、再举一个跨平台的例子 —— 跨芯片

我们前面举了跨OS这个平台的例子,我们现在举一个跨“芯片”这个平台的例子,那么我们举什么例子呢?

我们这里举一个ST(意法半导体)的STM32标准库的例子,标准库为了同时支持STM32F427_437xx、STM32F429_439xx、STM32F40_41xx等系列芯片,大量的用到了条件编译。

之所以让库同时支持这么多系列的芯片,主要是为了减少开发成本,因为如果针对每个系列的芯片都开发一个库的话,ST开发标准库的工程师非吐血不可,所以具有极高相似性的系列芯片,都使用同一个库,肯定是明智之举。

库中80%的是通用代码,剩余20%则为与“不同系列芯片”相关的差异性代码,这些差异性代码则通过“条件编译”来选择,我们这里举一个库函数例子:

void SystemCoreClockUpdate(void)
{
    /* 通用代码 */
    /* Get SYSCLK source ----*/
    tmp = RCC->CFGR & RCC_CFGR_SWS;
    switch (tmp)
    {
        case 0x00:  /* HSI used as system clock source */
            SystemCoreClock = HSI_VALUE;
        break;
        case 0x04:  /* HSE used as system clock source */
            SystemCoreClock = HSE_VALUE;
        break;
        case 0x08:  /* PLL P used as system clock source */
            pllsource = (RCC->PLLCFGR & RCC_PLLCFGR_PLLSRC) >> 22;
            pllm = RCC->PLLCFGR & RCC_PLLCFGR_PLLM;
        /*————————————————差异性代码———————————————— */
        //定义了STM32F40_41xxx或者STM32F427_437xx或者STM32F429_439xx等宏时,代码有效
#if defined(STM32F40_41xxx) || defined(STM32F427_437xx) || defined(STM32F429_439xx) || \
        defined(STM32F401xx) || defined(STM32F412xG) || defined(STM32F413_423xx) || \
        defined(STM32F446xx) || defined(STM32F469_479xx)
            if (pllsource != 0)
            {
                /* HSE used as PLL clock source */
                pllvco = (HSE_VALUE / pllm) * ((RCC->PLLCFGR & RCC_PLLCFGR_PLLN) >> 6);
            }
            else
            {
                /* HSI used as PLL clock source */
                pllvco = (HSI_VALUE / pllm) * ((RCC->PLLCFGR & RCC_PLLCFGR_PLLN) >> 6);
            }
        //否则如果定义了STM32F410xx或者STM32F411xE宏的话,以下代码有效
#elif defined(STM32F410xx) || defined(STM32F411xE)
            if (pllsource != 0)
            {
                /* HSE used as PLL clock source */
                pllvco = (HSE_BYPASS_INPUT_FREQUENCY / pllm) * ((RCC->PLLCFGR & RCC_PLLCFGR_PLLN) >> 6);
            }  
        //同理
#endif /* STM32F40_41xxx || STM32F427_437xx || STM32F429_439xx || STM32F401xx || STM32F412xG || STM32F413_423xx ||  STM32F446xx || STM32F469_479xx */  
            pllp = (((RCC->PLLCFGR & RCC_PLLCFGR_PLLP) >>16) + 1 ) *2;
            SystemCoreClock = pllvco/pllp;      
        break;
        //同理
#if defined(STM32F412xG) || defined(STM32F413_423xx) || defined(STM32F446xx)      
        case 0x0C:  /* PLL R used as system clock source */
          
            pllsource = (RCC->PLLCFGR & RCC_PLLCFGR_PLLSRC) >> 22;
            pllm = RCC->PLLCFGR & RCC_PLLCFGR_PLLM;
            if (pllsource != 0)
            {
                /* HSE used as PLL clock source */
                pllvco = (HSE_VALUE / pllm) * ((RCC->PLLCFGR & RCC_PLLCFGR_PLLN) >> 6);
            }
            else
            {
                /* HSI used as PLL clock source */
                pllvco = (HSI_VALUE / pllm) * ((RCC->PLLCFGR & RCC_PLLCFGR_PLLN) >> 6);      
            }
         
            pllr = (((RCC->PLLCFGR & RCC_PLLCFGR_PLLR) >>28) + 1 ) *2;
            SystemCoreClock = pllvco/pllr;      
        break;
#endif /* STM32F412xG || STM32F413_423xx || STM32F446xx */
        default:
            SystemCoreClock = HSI_VALUE;
        break;
    }
}

在IDE中查看源码你会发现,如果条件编译成立的话,所包含的代码就是正常颜色,条件编译不成立,代码的颜色为灰色,我们阅读源码时,凡是灰色的代码统统忽略,因为这些不是我们当前需要关心的代码。

很多同学阅读带有条件编译源码时,往往不懂得阅读技巧,很喜欢全部通读,最后越看越糊涂,什么原因呢?

(1)差异定代码本来就是只能有一段代码有效,至于哪一段有效则由条件编译来选择,但是你在看的时候如果把所有差异代码联合在一起看,代码逻辑自然就是混乱的,肯定是越看越糊涂。

(2)没被条件编译选中的代码,与我们关心的平台(os/芯片)并不相干,而是其它平台的,对于其它平台我们并不是很熟悉,所以强行阅读不熟悉平台的代码,肯定非常痛苦。

所以大家看有条件编译的代码时,一定摸清门道,不然就会越看越糊涂。

为了更好的阅读含有大量条件编译的代码,建议使用专门的IDE或者想souceinsight这种源码阅读器来阅读,因为它们会自动区分颜色,方便阅读。


使用txt文本来阅读时是不颜色区分的,这时我们自己就需要清楚,哪些条件编译成立,哪些不成立,从而区分出哪些是我应该关心的代码,不过不建议使用txt文本的方式来阅读,实在是太不人性化。


有关条件编译这个玩意,还真不是一两句话就能完全讲清楚的,更多的只能等大家工作时,自己去慢慢去体会。



头像
0/200
图片验证码