栏目分类:
子分类:
返回
名师互学网用户登录
快速导航关闭
当前搜索
当前分类
子分类
实用工具
热门搜索
名师互学网 > IT > 软件开发 > 后端开发 > C/C++/C#

一起玩转c语言之内功篇———从编写到运行(一)

C/C++/C# 更新时间: 发布时间: IT归档 最新发布 模块sitemap 名妆网 法律咨询 聚返吧 英语巴士网 伯小乐 网商动力

一起玩转c语言之内功篇———从编写到运行(一)

引言

当你写好了一个c程序,点击vc等集成开发环境的运行按钮时,有没有好奇过,这磁盘上的test.c文件究竟是怎样被加工成可执行文件,又被加载到内存中去运行的?

编译前:

编译后运行:

 

在接下来的几篇文章里,我将结合实例,来详细说说这其中发生的故事。


 概览

总体上来看,一个c语言程序,由磁盘上的一份源文件变成内存中的一个进程,会经历5个过程,分别是

  • 预处理
  • 编译
  • 汇编
  • 链接
  • 加载

 今天,我们先来看预处理。


预处理

预处理,其实就是在源代码被编译前,做一些文本性质的操作,这主要包括:

  • 删除注释
  • 插入#include语句包含的头文件内容
  • 根据#define语句进行宏替换
  • 根据#if 语句删除部分条件编译的内容
1)插入头文件:

预处理器会根据#include语句指定的文件名,在特定的文件路径中去查找文件,然后用头文件的内容替换掉#include语句。

例:

源文件test.c:

 预处理后的test.i文件:

在预处理后的文件中,原来的主函数内容没有变,只是上面多了几百行的头文件内容 。

预处理器支持两种不同的#include语句:       

        1)#include<文件名>:

        这通常用于声明一些库文件,此时的文件查找路径采用编译器定义的默认位置进行查找,在UNIX系统中,默认位置一般是/usr/include。

        2)#include"文件名":

        这通常用于我们自己定义的头文件,此时会先去源文件的当前目录进行查找,查找失败后再去默认位置进行查找。

2)宏替换:

宏替换,就是根据#define语句定义的规则,对源文件进行批量替换。

 平时写程序,我们经常会为数字常量定义宏:

//源程序: test.c 
#include
#define MAX_SIZE 100
int main(){
    printf("%d",MAX_SIZE);
    return 0;
}

//预处理后的程序:test.i
......
int main(){
    printf("%d",100);
    return 0;
}

除了定义常量宏,我们还可以定义带参数的宏:

//源程序: test.c 
#include
#define SUB(x,y) (x-y)
int main(){
    int a = 1;
    int b = 2;
    printf("%d",SUB(1,2));
    return 0;
}

//预处理后的程序:test.i
......
int main(){
    int a = 1;
    int b = 2;
    printf("%d",(1-2));
    return 0;
}

定义语句宏:

//源程序: test.c 
#include
#define do_forever for(;;)
int main(){
    do_forever{
        printf("Hello world");//无限打印hello world
    }
    return 0;
}

//预处理后的程序:test.i
......
int main(){
    for(;;){
        printf("Hello world");
    }
    return 0;
}

语句宏,可以让我们调试程序更加方便:

//源程序: test.c 
#include
#define DEBUG_PRINT printf("x=%d,y=%d,z=%dn",x,y,z)
int main(){
    int x=1,y=2,z=5;
    for(int i=0;i<10;i++){
       x*=2;
       y+=x;
       z=x+y;
       DEBUG_PRINT;//直接用宏插入调试语句,打印变量的中间值
    }
    return 0;
}

//预处理后的程序:test.i
......
int main(){
    int x=1,y=2,z=5;
    for(int i=0;i<10;i++){
       x*=2;
       y+=x;
       z=x+y;
       printf("x=%d,y=%d,z=%dn",x,y,z);
    }
    return 0;
}

我们甚至可以替换多行的语句:

//源程序: test.c 
#include
#define PRINT_HELLO for(int i=0;i<10;i++){/
                    printf("Hello world");/
                   }
int main(){
    PRINT_HELLO
    return 0;
}

//预处理后的程序:test.i
......
int main(){
    for(int i=0;i<10;i++){
        printf("Hello world");
    }
    return 0;
}

但是像这样的多行替换,并不建议大家使用。如果程序中出现多次PRINT_HELLO,我们不如将其实现为一个函数。用宏去替换多行语句,会导致预处理后的程序有大量重复的代码。

3)条件编译:

大家想想这样一个问题: 如果我们为了能多次调试程序,想要永久的保留调试语句,但又不想让程序在非调试状态时打印出debug信息,这时该怎么办呢?

//源程序: test.c 
#include
#define DEBUG_PRINT printf("x=%d,y=%d,z=%dn",x,y,z)
int main(){
    int x=1,y=2,z=5;
    for(int i=0;i<10;i++){
       x*=2;
       y+=x;
       z=x+y;
       DEBUG_PRINT;//不想永久删除它
    }
    return 0;
}

//预处理后的程序:test.i
......
int main(){
    int x=1,y=2,z=5;
    for(int i=0;i<10;i++){
       x*=2;
       y+=x;
       z=x+y;
       printf("x=%d,y=%d,z=%dn",x,y,z);
    }
    return 0;
}

针对此类问题,c语言给我们提供了一个方便的工具,这就是条件编译:

条件编译的格式:

#if   一个宏常量
      语句
      ...
#elif 一个宏常量
      语句
      ...
...
#else 
      语句
      ...
#endif
处理逻辑:
预处理器自上而下检查每个分支中的宏常量的值,值为1则保留该分支的语句,
删除其他分支,值都不为1则保留else分支的语句

利用条件编译,我们就可以通过控制宏常量的值,来优雅的保留调试语句了:

//源程序: test.c 
#include
#define IS_DEBUG 0
#define DEBUG_PRINT printf("x=%d,y=%d,z=%dn",x,y,z)
int main(){
    int x=1,y=2,z=5;
    for(int i=0;i<10;i++){
       x*=2;
       y+=x;
       z=x+y;
       #if IS_DEBUG
           DEBUG_PRINT;
       #ENDIF
    }
    return 0;
}

//预处理后的程序:test.i
......
int main(){
    int x=1,y=2,z=5;
    for(int i=0;i<10;i++){
       x*=2;
       y+=x;
       z=x+y;
            

    }
    return 0;
}

除此之外,通过条件编译,我们还能很容易的用一份程序编译出两个不同的最终版本,比如:

普通版本:

//源程序: test.c 
#define IS_VIP 0

void vip_service(){
    ......
}
void common_service(){
    ......
}

int main(){
   #if IS_VIP
        vip_service();
   #else
        common_service();
   return 0;
}



//预处理后的程序:test.i
......
void vip_service(){
    ......
}
void common_service(){
    ......
}

int main(){
   

   
        common_service();
    return 0;
}

vip版本:

//源程序: test.c 
#define IS_VIP 1

void vip_service(){
    ......
}
void common_service(){
    ......
}

int main(){
   #if IS_VIP
        vip_service();
   #else
        common_service();
   return 0;
}



//预处理后的程序:test.i
......
void vip_service(){
    ......
}
void common_service(){
    ......
}

int main(){
       vip_service();
    
    return 0;

}

总结

预处理部分的内容到这里就讲完啦,让我们来复习下:

预处理过程主要包括:

  • 删除注释
  • 插入头文件中的内容
    • <>声明:常用于库文件,会在默认目录查找
    • “ “ 声明:  常用于自定义头文件,会先在当前源文件目录查找
  • 宏替换
    • 常量替换
    • 带参替换
    • 单行,多行语句替换
  • 条件编译
    • 用#if...#else...#endif条件分支的形式实现条件编译,预处理器通过检测每个分支下的宏常量的值,来选择性的保留或删除语句。

以上,就是本文的全部内容啦 ! 下篇文章,我将详细讲讲预处理后的第二个步骤——编译!

——————————————————手动分割————————————————————

彩蛋

最后给大家看一个有趣的程序,大家思考下程序最后打印出的值是多少?欢迎大家与我交流,咱们下篇文章见!!!

#include
#define MAX(a,b) ((a)>(b)?(a):(b))
int main(){
    int x = 3;
    int y = 4;
    int z;
    z = MAX(x++,y++);
    printf("x =%d,y=%d,z=%d",x,y,z);
    return 0;
}

 (答案在评论区)

转载请注明:文章转载自 www.mshxw.com
本文地址:https://www.mshxw.com/it/433262.html
我们一直用心在做
关于我们 文章归档 网站地图 联系我们

版权所有 (c)2021-2022 MSHXW.COM

ICP备案号:晋ICP备2021003244-6号