IT数码 购物 网址 头条 软件 日历 阅读 图书馆
TxT小说阅读器
↓语音阅读,小说下载,古典文学↓
图片批量下载器
↓批量下载图片,美女图库↓
图片自动播放器
↓图片自动播放器↓
一键清除垃圾
↓轻轻一点,清除系统垃圾↓
开发: C++知识库 Java知识库 JavaScript Python PHP知识库 人工智能 区块链 大数据 移动开发 嵌入式 开发工具 数据结构与算法 开发测试 游戏开发 网络协议 系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程
数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁
 
   -> C++知识库 -> 【C语言-程序编译】一行行代码究竟是怎么一步步到可执行程序的? -> 正文阅读

[C++知识库]【C语言-程序编译】一行行代码究竟是怎么一步步到可执行程序的?

前言

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

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



1.程序编译

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

今天来大致看看

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

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

1.1 翻译环境

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

在这里插入图片描述

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

1.2 执行环境

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

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

2. 预处理

2.1 头文件的包含

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

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

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

2.2 嵌套包含头文件

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

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

防止重复包含头文件

1.#ifndef


#ifndef __TEST__H
//第一次:没定义,往下执行
//第二次:定义了,不往下执行

#define __TEST__H

#include "XXX.h"

#endif //__TEST__H (表示结束的是关于__TEST__H的#if)
  1. #pragma
#pragma

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

2.2 预定义符号

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

__FILE__ //正在进行编译的源文件

__LINE__ //文件当前的行号

__DATE__ //文件被编译的日期

__TIME__ //文件被编译的时间

__STDC__ //如果编译器遵循 ANSI C,__STDC__就被定义为 1;不遵循则未定义

2.3 #define

2.3.1 #define定义标识符

#define MAX 100

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

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

#define MAX 100

int main()
{
	printf("%d\n", MAX);//替换前
	printf("%d\n", 100);//替换后
	return 0;
}

2.3.2 #define 定义宏

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

宏的声明

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

来个例子试试:

#define SQURA(X) X*X

int main()
{
	int ret = SQURA(4);
	printf("%d\n", ret);
	return 0;
}16

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

#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

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

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

#define SQURA(X) ((X)*(X))

有副作用的宏

#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

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

替换后变成这样:

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

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

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
  • / 是续行符

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

#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,加上#变成对应字符串

##

把两个符号合成一个符号

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

2.3.5 宏 和 函数

来看看对比:

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

2.3.6 命名约定

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

2.4 #undef

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

2.5 命令行定义

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

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


3. 条件编译

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

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

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

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

常用的条件编译指令

1.普通条件编译

#if (常量表达式)
	//...
#endif

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

2.分支条件编译

#if (常量表达式)
	//...
#elif (常量表达式) //elseif
	//...
#else (常量表达式)
	//...
#endif
  1. 判断是否被定义
#define SYMBOL 1

//ifdef
#ifdef SYMBOL //定义了往下编译;没定义,从此到 #endif 都不编译
	//...(会被编译)
#endif

//ifndef
#ifndef SYMBOL//没定义就往下编译,定义了就不往下编译
	//...(不会被编译)
#endif

:其实就是看 #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;
}

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


4. 其他预处理指令

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


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

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

  C++知识库 最新文章
【C++】友元、嵌套类、异常、RTTI、类型转换
通讯录的思路与实现(C语言)
C++PrimerPlus 第七章 函数-C++的编程模块(
Problem C: 算法9-9~9-12:平衡二叉树的基本
MSVC C++ UTF-8编程
C++进阶 多态原理
简单string类c++实现
我的年度总结
【C语言】以深厚地基筑伟岸高楼-基础篇(六
c语言常见错误合集
上一篇文章      下一篇文章      查看所有文章
加:2022-07-20 18:34:21  更:2022-07-20 18:34:57 
 
开发: C++知识库 Java知识库 JavaScript Python PHP知识库 人工智能 区块链 大数据 移动开发 嵌入式 开发工具 数据结构与算法 开发测试 游戏开发 网络协议 系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程
数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁

360图书馆 购物 三丰科技 阅读网 日历 万年历 2024年5日历 -2024/5/14 1:42:44-

图片自动播放器
↓图片自动播放器↓
TxT小说阅读器
↓语音阅读,小说下载,古典文学↓
一键清除垃圾
↓轻轻一点,清除系统垃圾↓
图片批量下载器
↓批量下载图片,美女图库↓
  网站联系: qq:121756557 email:121756557@qq.com  IT数码