编译和链接---C语言
引言
众所周知,C语言是一门高级的编程语言,是无法被计算机直接读懂的,C语言也不同于汇编PHP,无法直接翻译成机器语言,在学习的过程中,你是否好奇过我们所敲的C语言代码,是如何一步步翻译成机器语言的呢?今天这篇博客---编译和链接,就是要带领我们解决这样的问题,那么我们开始吧!
翻译环境和执行环境
在ANSI C的任何一种实现中,存在两个不同的环境
1.翻译环境:在这个环境中,源代码被转化为可执行的机器指令(二进制指令)
2.执行环境:用于执行代码

1.翻译环境
在翻译环境中,分为编译和链接两部分

我们电脑中的编译器在将我们的代码文件编译后生成一个.obj文件(注:在Linux中会生成.o文件),这个.obj文件就是一份机器可以读懂的01010101文件(二进制文件) 。通过连接器作用最终将多份.obj文件链接生成一份可执行程序.exe文件。.obj文件和链接库链接库是指运⾏时库(它是⽀持程序运行的基本函数集合)或者第三方库。
编译分为预编译(预处理),编译和汇编三部分
在编译环境中的预编译(预处理)过程中,主要做这些工作:
- 将所有的#define 删除,并展开所有的宏定义
- 处理所有的条件编译指令,如: #if、#ifdef、#elif、#else、#endif
- 处理#include预编译指令,将包含的头⽂件的内容插⼊到该预编译指令的位置。这个过程是递归进行的,也就是说被包含的头⽂件也可能包含其他⽂件
- 删除所有的注释
- 添加⾏号和⽂件名标识,⽅便后续编译器⽣成调试信息等
- 保留所有的#pragma的编译器指令,编译器后续会使⽤
在编译环境的编译过程中,其本质是把代码翻译成汇编代码,主要执行三步:
1.词法分析
2.语法分析
3.语义分析
最后汇编是将汇编代码转成机器语言代码(二进制指令)
编译环境的第二部分链接,就是把一堆目标文件链接在一起生成可执行程序(.exe)
关于编译链接更细节的内容,大家可以参考《编译原理》和《程序员的自我修养》这两本书
2.运行环境
1.程序载入内存中。
2.程序开始执行。调用main
3.开始执行代码。这个时候将使用一个函数栈帧,存储函数的局部变量和返回地址
4.终止程序。正常/意外终止
到了这里,编译和链接的大致过程已经讲完了,但是关于编译中预处理还有很多需要知道的细节,需要单独拿出来细讲,所以想更多了解的朋友可以继续看下去。
预处理详解
1.预定义符号
在C语言中设置了一些比较方便的预定义符号,可以直接使用,预定义符号也是在预处理期间处理的
1.__FILE__ //进行编译的源文件
2.__LINE__ //文件当前的行号
3.__DATE__ //文件被编译的日期
4.__TIME__ //文件被编译的时间
5.__STDC__ //如果编译器遵循ANSI C,其值为1,否则未定义
下面代码来带大家简单使用一下
#include<stdio.h>
int main()
{printf("%s\n%d\n%s\n%s", __FILE__, __LINE__, __DATE__, __TIME__);return 0;
}

根据上方的代码运行大家应该基本就能弄清这些 预定义符号的作用了
2.#define定义的常量
基本语法
#define name stuff
//以下是实际运用
#define MAX 1000
#define MIN 100;//不要在#define定义的常量后加分号//介于其预处理暴力替换的特性,会导致一些错误//如以下代码就会出错
printf("%d",MIN);//此代码预处理过后为->printf("%d",100;);
if(condition)max = MIN;// 此处预处理过后为两条语句,会将if和else隔开,出现报错
elsemax = 0;
3.#define定义宏
#define机制包括了一个规定,允许把参数替换到文本中,这种实现被称为宏(macro)或定义宏(define marco)。
下面是宏的声明方式:
#define name( parament-list ) stuff
其中的parament-list是一个由逗号隔开的符号表,它们可能出现在stuff中。
注:参数列表的左括号必须与name紧邻,如果两者之间有空白存在,参数列表就会被解释成stuff的一部分。
使用举例:
#define SQUARE(x) x * x
这个宏接受一个参数x,如果在上述声明之后,将SQUSRE(5);放到程序中,预处理器就会将此语句替换成:5 * 5
警告:
#define定义的宏是一种暴力的替换,实在预处理过程中将语句原样替换,如果你写了如下代码
#include<stdio.h>
#define SQUARE(x) x*x
int main()
{int a = 5;printf("%d\n", SQUARE(5 + 1));return 0;
}
将会打印什么呢?

也许你会觉得会打印36(6*6),但是 结果可能与预期不符,暴力替换此语句便成为
5 + 1 * 5 + 1而不是(5 + 1)*(5 + 1)
所以最后的结果是11
如果想要36这种结果应该怎么办呢?那就不要吝啬你的()了,这样写
#define SQUARE(x) ((x)*(x))
最后代码替换为 ((5+1)*(5+1)),就为36了
所以,在用#define定义宏的时候,把括号都带上,可以尽量避免在使用宏时由于参数中的操作符或临近操作符之间不可预料的相互作用
4.带有副作用的宏参数
当宏参数在宏定义中出现超过一次的时候,如果参数带有副作用,那么你在使用这个宏的时候就可能出现危险,导致不可预测的后果。副作用就是表达式求值的时候出现的永久性效果。
例如:
x+1; //不带有副作用
x++; //带有副作用
MAX宏可以证明具有副作用的参数所引起的问题
#include<stdio.h>
#define MAX(x,y) ((x)>=(y)?(x):(y))
int main()
{int a = 5;int b = 8;int c = MAX(a++, b++);printf("%d\n%d\n%d\n", a, b, c);return 0;
}
根据暴力替换,我们知道预处理之后的结果为
c = ( (a++) > (b++) ? (a++) : (b++));
所以最后的输出结果为:a=6,b=10,z=9

5.宏替换规则
在程序中扩展#define定义符号和宏时,需要涉及几个步骤。
1.在调用宏时,首先参数进行检查,看看是否包含任何由#define定义的符号。如果有,首先被替换
2.替换文本随后被插入到程序中原来文本的位置。对于宏,参数名被其值所替换
3.最后,再次对结果文件进行扫描,看看它是否包含任何由#define定义的符号。如果是,就重复上述处理过程。
注意:
1.宏参数和#define定义中可以出现其他#define定义的符号。但对于宏,不能出现递归
2.当预处理搜索#define定义符号的时候,字符串常量的内容不被搜索
6.宏和函数的对比
宏通常被应用于执行简单的运算。
但运用函数执行比较简单的运算,如比大小时,会有两点缺点
1.⽤于调⽤函数和从函数返回的代码可能⽐实际执⾏这个⼩型计算⼯作所需要的时间更多。所以宏⽐ 函数在程序的规模和速度⽅⾯更胜⼀筹。
2. 更为重要的是函数的参数必须声明为特定的类型。所以函数只能在类型合适的表达式上使⽤。反之 这个宏怎可以适⽤于整形、⻓整型、浮点型等可以⽤于 > 来⽐较的类型。宏是类型⽆关的。
但和函数相比时,宏也有其劣势:
1.每次使⽤宏的时候,⼀份宏定义的代码将插⼊到程序中。除⾮宏⽐较短,否则可能⼤幅度增加程序 的⻓度。
2. 宏是没法调试的。
3. 宏由于类型⽆关,也就不够严谨。
4. 宏可能会带来运算符优先级的问题,导致程容易出现错。
7.#和##
1.#运算符
#运算符将宏的一个参数转换为字符串字面量,且只允许出现在带参数的宏的替换列表中。
#运算符所执行的操作可以理解为“字符串化”。
如果我们有一个变量int a = 10;想打印出:the value of a is 10.
就可以写:
#define PRINT(n) printf("the value of "#n"is %d",n)
当我们用以上方式调用的时候:
PRINT(a);//中#a就转换成了"a"
可以看看下方代码及运行
#include<stdio.h>
#define PRINT(n) printf("the value of "#n" is %d\n",n)
int main()
{int a = 10;PRINT(a);return 0;
}

2.##运算符
##可以把位于它两边的符号合成一个符号,它允许宏定义从分离的文本片段创建标识符。##被称为记号粘合
注:这样的连接必须产生一个合法的标识符。否则其结果就是未定义的。
这里来举个例子,如果我们想写一个函数用来求两个数中较大的那个,不同的类型就需要写不同的函数,像下面这样:
int int_max(int x, int y)
{return x > y ? x : y;
}float float_max(float x, float y)
{return x > y ? x : y;
}
但是基本类型很多,给每一个类型的比较都写一个函数未免太繁琐了,我们现在了解了##,便可以这样写:
#include<stdio.h>
#define GENERIC(type) \
type type##_max(type x,type y) \
{ \return x>y?x:y; \
}
//这里解释一下\符号是连行符,用这个符号可以将本行和下一行连接,相当于一行
GENERIC(int);
GENERIC(float);//想生成什么类型的直接在这声明一下就行
int main()
{float a = 10.9, b = 20.5;printf("%f\n", float_max(a, b));int m = 10, n = 20;printf("%d\n", int_max(m, n));return 0;
}

8.#undef
这条指令可以用于移除一个宏定义
#undef NAME
//如果现存的一个名字需要被重定义,它的旧名字首先要被移除
9.条件编译
在编译一个程序的时候我们如果想要将(一条或一段语句)编译或者放弃编译,可以使用条件编译指令。
比如一段调试代码,删除比较可惜,保留又碍事,可以使用选择性的编译,见代码
#include<stdio.h>
#define __DEBUG__
int main()
{int arr[10] = { 0 };for (int i = 0; i < 10; i++) {arr[i] = i;
#ifdef __DEBUG__printf("%d ", arr[i]);
#endif}return 0;
}

当你把开头#define去掉时,接下来将什么都不会打印了
下面还有一些比较常见的条件编译指令:
1.
#if 常量表达式//。。。
#endif2.多个分支的条件编译
#if 常量表达式//。。。
#elif 常量表达式//。。。
#else 常量表达式//。。。
#endif3.判断是否被定义
#if defined(sympol)
#ifdef sympol
//上方两语句意思相同
#if !defined(sympol)
#ifndef sympol
//上方两语句意思相同4.嵌套指令
没什么说的,这些指令可以相互嵌套使用
10.头文件的包含
1.本地头文件包含
#include "filename"
查找规则:
现在源文件所在目录下查找,如果该头文件未找到,编译器就像查找库函数头文件一样在标准位置查找头文件。
如果找不到则编译错误。
2.库文件包含
#include <filename.h>
查找规则:
查找头文件直接去标准路径下去查找,如果找不到就提示编译错误。
这种查找规则其实意味着库函数的头文件其实也可以用" "的形式包含,但是考虑到对头文件的区分管理和效率,依然建议用< >的形式去包含库文件。
11.头文件被反复包含问题
当你在coding的时候,是否会有时候将同一个头文件反复包含,像下面这样:
//test.c文件
#include"test.h"
#include"test.h"
#include"test.h"
#include"test.h"
#include"test.h"
int main()
{return 0;
}
如果直接这样写,test.c文件中将test.h包含五次,那么test.h文件的内容将会被拷贝5份在test.c中
如果test.h比较大,这样会使预处理的代码量加剧,会极大的影响到程序运行的效率,那么应该如何解决这种问题呢?
答案是:条件编译
//test.h头文件
#ifndef __TEST_H__
#define __TEST_H__
//头文件内容
#enif //__TEST_H__
或者
#pragma once
看看你是否在生成头文件是看到过这句代码,这句代码的意义便是防止头文件被反复包含这种问题的。
12.其他预处理指令
#error
#pragma
#line
#pragma pack()//结构体中介绍过,用来设置默认对齐数
//。。。
等等一系列预处理指令,这里就不一一赘述了
如果对相关内容有兴趣,可以参考《C语言深度解剖》
结语
到这里,关于编译,链接以及对预处理的内容基本上是介绍的差不多了,如果感觉我的博客有帮助的话,还请点个小小的赞支持一下哦,我还会继续产出更多有趣的内容。比心---♥
相关文章:
编译和链接---C语言
引言 众所周知,C语言是一门高级的编程语言,是无法被计算机直接读懂的,C语言也不同于汇编PHP,无法直接翻译成机器语言,在学习的过程中,你是否好奇过我们所敲的C语言代码,是如何一步步翻译成机器…...
SAP EXCEL上传行数限制问题(ALSM_EXCEL_TO_INTERNAL_TABLE)
标准函数ALSM_EXCEL_TO_INTERNAL_TABLE上传EXCEL函数限制上限是9999行,如果上传数据记录数超过9999行的情况,需要拷贝标准的函数封装一个自定义的函数进行处理 标准的函数ROW的长度为4位,如下图所示 因此,如果想行数的位数超过4位…...
3.召回率-机器学习模型性能的常用的评估指标
在机器学习领域,召回率是一个关键的性能指标,用于评估模型在正样本中正确识别的能力。召回率的计算涉及到模型成功检测到的正样本数量与实际正样本的总数量之比。这个指标对于很多应用场景都至关重要,尤其是在那些要求较高的领域,…...
linux安装docker--更具官网教程
1.访问https://docs.docker.com/ 2.进入download 3输入cento 或者直接访问地址Install Docker Engine on CentOS | Docker Docs 4一步一步根据官网命令走 2安装 3 4 方式一: service docker start(开启) service docker status(…...
云原生安全:风险挑战与安全架构设计策略
概述 数字化转型已经成为当今最流行的话题之一,大部分企业已经开启自身的数字化转型之旅,在未来企业只有数字化企业和非数字化企业之分。通过数字经济的加速发展,可以有效推动企业数字化转型的步伐。云计算作为数字化转型的底座和重要的载体…...
c语言-文件的读写操作
文章目录 前言一、文件基础1.1 文件的分类1.2 文件路径和文件名 二、文件的打开和关闭2.1 文件指针2.2 文件的打开和关闭 总结 前言 本篇文章介绍c语言的文件读写操作。 一、文件基础 1.1 文件的分类 在c语言中,从文件的功能角度来看,文件可分为以下两…...
Python处理日期和时间库之arrow使用详解
概要 日期和时间处理是许多应用程序中的常见任务,但在 Python 中,标准库中的 datetime 模块有时可能会让这些任务变得复杂和繁琐。幸运的是,有一个名为 Arrow 的第三方库,它提供了简化日期和时间处理的功能,使其更加直…...
架构师之路(十四)计算机网络(网络层)
前置知识(了解):计算机基础。 作为架构师,我们所设计的系统很少为单机系统,因此有必要了解计算机和计算机之间是怎么联系的。局域网的集群和混合云的网络有啥区别。系统交互的时候网络会存在什么瓶颈。 网络层提供主机…...
Spring Boot开发Spring Security
这里我对springboot不做过多描述,因为我觉得学这个的肯定掌握了springboot这些基础 导入核心依赖 <dependency><groupId>org.springframework.boot</groupId><artifactId>spring‐boot‐starter‐security</artifactId> </depen…...
gin介绍及helloworld
1. 介绍 Gin是一个golang的微框架,封装比较优雅,API友好,源码注释比较明确,具有快速灵活,容错方便等特点 对于golang而言,web框架的依赖要远比Python,Java之类的要小。自身的net/http足够简单&…...
vue3 自动引入 ref reactive...
npm i unplugin-auto-import -D vite.config.js import { defineConfig } from vite; import vue from vitejs/plugin-vue; import AutoImport from unplugin-auto-import/vite;export default defineConfig({plugins: [vue(),AutoImport({// 自动导入 Vue 相关函数࿰…...
软考复习之软件工程篇
软件生命周期 问题定义:要示系统分析员与用户进行交流,弄清”用户需要计算机解决什么问题”然后提出关于“系统目标与范围的说明”,提交用户审查和确认 可行性研究:一方面在于把待开发的系统的目标以明确的语言描述出来…...
MySQL(七)MySQL和Oracle、PostgreSQL的区别
文章目录 一、MySQL和Oracle1.1 基本差别1.2 使用区别 二、MySQL和PostgreSQL2.1 基本差别2.2 使用差别 本系列文章: MySQL(一)SQL语法、数据类型、常用函数、事务 MySQL(二)MySQL SQL练习题 MySQL(三&…...
(2)(2.4) CRSF/ELRS Telemetry
文章目录 前言 1 ArduPilot 参数编辑器 前言 !Note ELRS(ExpressLRS)遥控系统使用穿越火线协议,连接方式类似。不过,它不像穿越火线那样提供双向遥测。 TBS CRSF 接收机与 ArduPilot 的接口中包含遥测和遥控信息。…...
服务器发送http请求
1、发送GET请求 curl localhost:9009/setCreateDataItem?a1&bnihao 2、发送POST请求 curl -X POST -d a1&bnihao localhost:9009/setCreateDataItem 3、发送json格式请求: curl -H "Content-Type: application/json" -X POST -d {"abc…...
Effective Objective-C 学习第二周
理解“属性”这一概念 “属性”(property)是 Objective-C 的一项特性,用于封装对象中的数据。Objective-C 对象通常会把其所需的数据保存为各种实例变量。实例变量一般通过“存取方法”来访问。其中,“获取方法”(get…...
JS进阶-深入对象(二)
拓展:深入对象主要介绍的是Js的构造函数,实例成员,静态成员,其中构造函数和Java种的构造函数用法相似,思想是一样的,但静态成员和实例成员和java种的有比较大的差别,需要认真理解 • 创建对象三…...
【Gene Expression Prediction】Part2 Enchancer discovery
文章目录 5. 第一个讲座:Enchancer discovery5.1 STARR-seq5.2 Enchancer detection with weakly supervised learning5.3 Model performance 来自Manolis Kellis教授(MIT计算生物学主任)的课 YouTube:(Gene Expression Predictio…...
【UEFI基础】EDK网络框架(UDP4)
UDP4 UDP4协议说明 UDP的全称是User Datagram Protocol,它不提供复杂的控制机制,仅利用IP提供面向无连接的通信服务。它将上层应用程序发来的数据在收到的那一刻,立即按照原样发送到网络。 UDP报文格式: 各个参数说明如下&…...
vivado使用注意事项
记得给constrs(.xdc)限制文件设置为目标文件(set as Target Consraint File)...
React 第五十五节 Router 中 useAsyncError的使用详解
前言 useAsyncError 是 React Router v6.4 引入的一个钩子,用于处理异步操作(如数据加载)中的错误。下面我将详细解释其用途并提供代码示例。 一、useAsyncError 用途 处理异步错误:捕获在 loader 或 action 中发生的异步错误替…...
Java如何权衡是使用无序的数组还是有序的数组
在 Java 中,选择有序数组还是无序数组取决于具体场景的性能需求与操作特点。以下是关键权衡因素及决策指南: ⚖️ 核心权衡维度 维度有序数组无序数组查询性能二分查找 O(log n) ✅线性扫描 O(n) ❌插入/删除需移位维护顺序 O(n) ❌直接操作尾部 O(1) ✅内存开销与无序数组相…...
鸿蒙中用HarmonyOS SDK应用服务 HarmonyOS5开发一个医院查看报告小程序
一、开发环境准备 工具安装: 下载安装DevEco Studio 4.0(支持HarmonyOS 5)配置HarmonyOS SDK 5.0确保Node.js版本≥14 项目初始化: ohpm init harmony/hospital-report-app 二、核心功能模块实现 1. 报告列表…...
如何在网页里填写 PDF 表格?
有时候,你可能希望用户能在你的网站上填写 PDF 表单。然而,这件事并不简单,因为 PDF 并不是一种原生的网页格式。虽然浏览器可以显示 PDF 文件,但原生并不支持编辑或填写它们。更糟的是,如果你想收集表单数据ÿ…...
Python 包管理器 uv 介绍
Python 包管理器 uv 全面介绍 uv 是由 Astral(热门工具 Ruff 的开发者)推出的下一代高性能 Python 包管理器和构建工具,用 Rust 编写。它旨在解决传统工具(如 pip、virtualenv、pip-tools)的性能瓶颈,同时…...
安全突围:重塑内生安全体系:齐向东在2025年BCS大会的演讲
文章目录 前言第一部分:体系力量是突围之钥第一重困境是体系思想落地不畅。第二重困境是大小体系融合瓶颈。第三重困境是“小体系”运营梗阻。 第二部分:体系矛盾是突围之障一是数据孤岛的障碍。二是投入不足的障碍。三是新旧兼容难的障碍。 第三部分&am…...
【 java 虚拟机知识 第一篇 】
目录 1.内存模型 1.1.JVM内存模型的介绍 1.2.堆和栈的区别 1.3.栈的存储细节 1.4.堆的部分 1.5.程序计数器的作用 1.6.方法区的内容 1.7.字符串池 1.8.引用类型 1.9.内存泄漏与内存溢出 1.10.会出现内存溢出的结构 1.内存模型 1.1.JVM内存模型的介绍 内存模型主要分…...
为什么要创建 Vue 实例
核心原因:Vue 需要一个「控制中心」来驱动整个应用 你可以把 Vue 实例想象成你应用的**「大脑」或「引擎」。它负责协调模板、数据、逻辑和行为,将它们变成一个活的、可交互的应用**。没有这个实例,你的代码只是一堆静态的 HTML、JavaScript 变量和函数,无法「活」起来。 …...
Chrome 浏览器前端与客户端双向通信实战
Chrome 前端(即页面 JS / Web UI)与客户端(C 后端)的交互机制,是 Chromium 架构中非常核心的一环。下面我将按常见场景,从通道、流程、技术栈几个角度做一套完整的分析,特别适合你这种在分析和改…...
redis和redission的区别
Redis 和 Redisson 是两个密切相关但又本质不同的技术,它们扮演着完全不同的角色: Redis: 内存数据库/数据结构存储 本质: 它是一个开源的、高性能的、基于内存的 键值存储数据库。它也可以将数据持久化到磁盘。 核心功能: 提供丰…...
