• 【C语言-程序编译】一行行代码究竟是怎么一步步到可执行程序的?


    前言

    源码裹上面包糠,鸡蛋液,面粉…(不是不是)

    源码经过 预处理、编译、汇编、链接 就能变成 可执行文件了



    1.程序编译

    我们的代码,经过编译器就变成 .exe格式的可执行文件,到底怎么做到的?

    今天来大致看看

    ANSI C 中的实现中,都存在两个环境:

    1. 翻译环境:把源代码转换成机器指令(可执行)
    2. 执行环境:执行代码

    1.1 翻译环境

    翻译的流程图大致是这样:

    在这里插入图片描述

    • 符号表:一种用于语言翻译器中的数据结构。在符号表中,程序源代码中的每个标识符都和它的声明或使用信息绑定在一起,比如其数据类型、作用域以及内存地址。
    • 段表:每个程序都有逻辑段,为能从物理内存中找出每个逻辑段所对应的位置,在系统中为每个进程建立一张段映射表,简称段表,段表记录了进程中每一个段在内存中的起始地址
    • 链接库:动态链接库(Dynamic Link Library 缩写为 DLL),是微软公司在微软Windows操作系统中,实现共享函数库概念的一种方式

    1.2 执行环境

    **在这里插入图片描述**

    • 载入内存一般由操作系统完成;如果是独立环境,只能手工载入(嵌入式常见)

    2. 预处理

    2.1 头文件的包含

    把头文件编译后插入到包含头文件的位置
    两种方式:

    1. 包含本地头文件: #include “filename”
    2. 包含库里的头文件:#include

    第一种的查找策略是,现在当前源文件目录下找;找不到就去标准库中找。所以包含库里的头文件也可以用第一种方法,但是不建议,因为这样效率不高,也不能区分 本地 和 库

    2.2 嵌套包含头文件

    在这里插入图片描述
    这边的嵌套包含头文件就不小心 重复包含

    已经知道,包含头文件就是把头文件编译后插入到包含头文件的位置。所以重复包含就会导致代码量急增,降低效率。因此我们要防止重复包含头文件

    防止重复包含头文件

    1.#ifndef

    
    #ifndef __TEST__H
    //第一次:没定义,往下执行
    //第二次:定义了,不往下执行
    
    #define __TEST__H
    
    #include "XXX.h"
    
    #endif //__TEST__H (表示结束的是关于__TEST__H的#if)
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    1. #pragma
    #pragma
    
    • 1

    第二种方便,但是部分较老版本的编译器无法使用

    2.2 预定义符号

    C语言中内置的预定义符号:

    __FILE__ //正在进行编译的源文件
    
    __LINE__ //文件当前的行号
    
    __DATE__ //文件被编译的日期
    
    __TIME__ //文件被编译的时间
    
    __STDC__ //如果编译器遵循 ANSI C,__STDC__就被定义为 1;不遵循则未定义
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9

    2.3 #define

    2.3.1 #define定义标识符

    #define MAX 100
    
    #define FOR for( ; ; )
    
    • 1
    • 2
    • 3
    • #define 定义标识符时,末尾不能加 “;” ,不然会把 “;” 当作标识符的一部分

    上面讲预处理的时候,提到了“替换标识符”,具体执行起来是这样:

    #define MAX 100
    
    int main()
    {
    	printf("%d\n", MAX);//替换前
    	printf("%d\n", 100);//替换后
    	return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8

    2.3.2 #define 定义宏

    宏(macro)基于 #define 机制中的一个规定:允许把参数替换到文本中

    宏的声明

    #define name(parament-list) stuff 
    
    • 1
    • name - 宏名
    • parament-list - 参数列表
    • stuff - 宏体

    来个例子试试:

    #define SQURA(X) X*X
    
    int main()
    {
    	int ret = SQURA(4);
    	printf("%d\n", ret);
    	return 0;
    }16
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10

    乍一看没问题,其实还有漏洞…

    #define SQURA(X) X*X
    
    int main()
    {
    	int ret = SQURA(4 + 2);
    	//int ret = 4 + 2 * 4 + 2;
    	printf("%d\n", ret);
    	return 0;
    }14
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11

    这是因为宏的特性:只替换,不运算

    为了防止这种错误,我们定义宏的时候不要吝啬括号:

    #define SQURA(X) ((X)*(X))
    
    • 1

    有副作用的宏

    #define MAX(X,Y) ((X)>(Y)?(X):(Y))
    
    int main()
    {
    	int a = 10;
    	int b = 20;
    	int ret = MAX(a++, b++);
    	printf("%d\n", ret);
    	return 0;
    }
    :
    a=11 b=22  ret = 21
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12

    这段代码就容易造成意料外的后果

    替换后变成这样:

    int ret = ((a++) > (b++) ? (a++) : (b++));
    
    • 1

    脑子里慢慢走一遍,就发现它又在乱改了

    2.3.3 #define 的替换规则

    调用宏时:

    1. 检查参数,如果有#define定义的符号,就先替换它们
    2. 替换宏体的参数(把传入的参数替换上来),再把处理完毕的宏体替换到程序中
    3. 对结果再次检查,如果有#define定义的符号,就重复上述操作

    还要注意两点:

    1. 宏的参数 和 #define的定义 中可以出现其他 #define定义的符号,但是宏不能实现递归
    2. 当预处理器搜索#define定义的符号,字符串常量的内容不被搜索

    2.3.4 # 和 ##

    #

    # 可以把宏的参数,变成其中接收的值 对应的字符串

    举例介绍 # 之前,先看一段代码:

    在这里插入图片描述
    把两个字符串放一起,居然自动合并了

    在这里插入图片描述

    放到宏里玩玩:

    #define PRINT(FORMAT, VALUE) \
    	printf("The value is "FORMAT"\n", VALUE)
    
    int main()
    {
    	PRINT("%d", 10);
    	PRINT("%c", 'b');
    	PRINT("%f", 6.66f);
    	return 0;
    }
    
    :
    The value is 10
    The value is b
    The value is 6.660000
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • / 是续行符

    由这个前提,结合 # 在宏中的用法,再来实践一下:

    #define PRINT(FORMAT, VALUE) \
    	printf("The value of "#VALUE" is "FORMAT"\n", VALUE)
    
    int main()
    {
    	int i = 10;
    	char c = 'b';
    	float f = 5.66f;
    	PRINT("%d", i+2);
    	PRINT("%c", c+1);
    	PRINT("%f", f+1);
    	return 0;
    }
    :
    The value of i+2 is 12 //传给VALUE的参数是i+2,加上#变成对应字符串
    The value of c+1 is c//传给VALUE的参数是c+1,加上#变成对应字符串
    The value of f+1 is 6.660000//传给VALUE的参数是f+1,加上#变成对应字符串
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17

    ##

    把两个符号合成一个符号

    • 合成的符号一定要是合法的,否则结果未定义

    2.3.5 宏 和 函数

    来看看对比:

    属性函数描述
    执行速度较快较慢预处理时直接替换 VS 要调用、创建函数栈帧
    代码长度较长较短调用一次插入一次 VS 一段代码重复使用
    易错程度较易错不易错没有类型检查,操作符优先级不明显,结果不易预测 VS 有类型检查代码和结果都清晰
    调试不可调试可调试
    递归不可递归可递归

    2.3.6 命名约定

    • 宏名:全部大写
    • 函数名:不要全部大写(具体按代码风格来)

    2.4 #undef

    :移除一个宏定义
    在这里插入图片描述
    在这里插入图片描述
    int 和 return 0 的报错不用理会,可以看到移除定义后MAX报错了

    2.5 命令行定义

    :使用未定义的符号,在启动编译后在命令行手动定义

    具体测试,等学了Linux再测吧


    3. 条件编译

    :依照条件编译指令来选择性编译

    比如:编译器的源码中,就用到了不少的条件编译,针对不同的机器

    可能会疑惑:为什么不干脆直接注释掉?

    注释就是注释,来解释和备注的;代码还是代码啊!

    常用的条件编译指令

    1.普通条件编译

    #if (常量表达式)
    	//...
    #endif
    
    • 1
    • 2
    • 3

    :如果 常量表达式 为真,编译至 #endif ;反之,从#if 到 #endif 都不编译

    2.分支条件编译

    #if (常量表达式)
    	//...
    #elif (常量表达式) //elseif
    	//...
    #else (常量表达式)
    	//...
    #endif
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    1. 判断是否被定义
    #define SYMBOL 1
    
    //ifdef
    #ifdef SYMBOL //定义了往下编译;没定义,从此到 #endif 都不编译
    	//...(会被编译)
    #endif
    
    //ifndef
    #ifndef SYMBOL//没定义就往下编译,定义了就不往下编译
    	//...(不会被编译)
    #endif
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11

    :其实就是看 #ifdef / #ifndef 这一整条指令是否为真,为真就往下编译;反之直到 #endif 都不便宜
    例子:

    #define __DEBUG__
    
    int main()
    {
    	int arr[10] = { 0 };
    	int i = 0;
    	for (i = 0; i < 10; i++)
    	{
    		arr[i] = i;
    #ifdef __DEBUG__
    		printf("%d ", arr[i]);
    #endif //__DEBUG__
    	}
    	
    	return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16

    如果要测试,就如代码所示,接着编译,可以打印出来是否赋值成功;如果不测试就把 #ifdef __DEBUG__ 和 #endif 注释掉,不编译打印的代码


    4. 其他预处理指令

    推荐到《C语言深度剖析》学习


    本期分享就到这啦,不足之处望请斧正

    培根的blog,和你共同进步!

  • 相关阅读:
    ChatGPT创造力与创新探究
    PCIE1—快速实现PCIE接口上下位机通信(一)
    二分查找(JavaScript版本)
    Pytorch 自动求导的设计与实现
    微信自动应答机器人功能
    【交叉熵损失torch.nn.CrossEntropyLoss详解-附代码实现】
    Spring事务失效可能是哪些原因
    MVCC是什么
    初识网络
    go语言实现简单认证样例
  • 原文地址:https://blog.csdn.net/BaconZzz/article/details/125848180