当前位置: 首页 > news >正文

C++编译/链接模型

C++编译/链接模型

笔记来自:深蓝学院《C++基础与深度解析》

源程序作为整体一个源文件进行一次性编译

早期版本中,从C++源代码到可执行文件的简单加工模型,无法有效处理大型程序的问题
只要修改了部分代码就要全部重新“加工”(编译)

源程序分块为多个源文件分别编译

引入了分块处理的方法,即将原程序分为多个源文件进行编译,生成目标文件,并通过链接这些目标文件来生成可执行程序。

这种方法相比简单加工模型更加高效,能处理大型程序,并通过编译和链接两个步骤将C++源代码转换为可执行程序

如果只是需要修改源文件1,那修改完后只需要重新编译源文件1即可,而不是早期版本的需要全部源文件都要重新编译

分块处理衍生出来的概念

定义/声明

1.定义是变量或函数的实际存储和初始化
2.声明是告诉编译器某个变量或函数的存在,但不进行存储或初始化。
3.声明用于在不同源文件中共享变量或函数定义。

假设源程序中有多个地方使用了同一个变量或函数
如果该源程序只有一个源文件,那么直接定义一个变量或函数即可在该源文件中多处使用
如果将源程序分块为多个源文件,就要区分变量的定义和声明,在头文件中声明,多个源文件引入头文件后,对变量进行定义

头文件/源文件

1.头文件包含函数的声明和变量的声明。(存放声明)
2.源文件包含函数的定义和变量的定义。(存放定义)
3.头文件和源文件的引用提升了代码书写效率

翻译单元

翻译单元是源文件与相关头文件(包含直接和间接引用的头文件)合并,并忽略应忽略的预处理语句后的产物。简单来说,就是把源文件中引用的头文件展开,同时忽略预处理语句后的结果

源文件 + 相关头文件(直接头文件/间接头文件)- 应忽略的预处理语句
把源文件中引用的头文件拿过来展开之后忽略掉预处理语句

一处定义原则

“一处定义原则” 是 C++ 为了避免程序在编译和链接过程中出现重复定义错误而设定的规则

程序级别只有一处定义:一般函数
翻译单元级别只有一处定义:内联函数、类、模板

一个源文件被分为多个源文件,这多个源文件ABC中,ABC每个单独源文件内中不能定义相同的内联函数、类、模板,比如两个相同内联函数inline void func1()在同一个源文件中这种情况不允许,但同一个内联函数inline void func1()可以分别出现在A、B、C中

程序级别只有一处定义:一般函数:在整个 C++ 程序里,每个一般函数只能有一个定义。因为链接器在把多个目标文件组合成可执行程序时,如果发现同一个一般函数存在多个不同定义,就没办法确定该用哪个,进而导致链接错误。例如,有source1.cpp和source2.cpp两个源文件,要是它们都定义了void func() { /* 函数体 */ },连接时就会出错。正确做法是在一个源文件里定义func函数,在其他需要使用的源文件里通过声明(void func();)来使用它

翻译单元级别只有一处定义:内联函数、类、模板:翻译单元指的是源文件加上它引用的头文件,并忽略预处理语句后的内容。内联函数、类和模板在每个翻译单元中都只能有一个定义。这是因为内联函数在编译时会把函数体直接插入到调用处,如果不同翻译单元的定义不同,可能会导致行为不一致;类定义描述了对象的结构和行为,不同翻译单元的类定义不同,对该类对象的使用也会混乱;模板是生成具体函数或类的蓝图,不同翻译单元对同一模板定义不同,生成的实例也会有差异。比如定义了内联函数inline int add(int a, int b) { return a + b; },可以在多个源文件对应的翻译单元中使用,但每个翻译单元里的定义必须一致,通常把它放在头文件中,让包含该头文件的翻译单元都能使用相同的定义。

编译链接模型的实例

1.预处理阶段将源代码转换为翻译单元。
2.编译阶段将翻译单元转换为汇编代码。
3.汇编阶段将汇编代码转换为目标文件。
4.链接阶段将目标文件合并为可执行文件。

预处理

作用:处理源文件中的预处理指令(以#开头,如#include 、#define 、#if 、#ifdef 等 )。具体操作包括头文件包含(将#include指定的头文件内容插入到源代码对应位置 )、宏替换(把代码中宏定义的符号替换为对应内容 )、条件编译(根据#if等指令的条件判断决定代码是否参与后续编译 ),还会去除注释、生成编译阶段所需符号(如行号 )等,生成纯源语言代码(去除预处理指令相关特殊标记 )。
示例:若源文件有#include <stdio.h> ,预处理时会把stdio.h文件内容插入;若有#define PI 3.14 ,代码里所有PI都会被替换成3.14 。
命令:在 GCC 编译器中,用-E选项,如gcc source.c -E -o source.i ,source.i为预处理后文件。

将源文件转换为翻译单元的过程
– 防止头文件被循环展开
● #ifdef 解决方案
● #pragma once 解决方案

头文件循环展开问题:
在main.cpp中包含了"header1.h",而header1.h又引入了"header2.h"。问题在于header2.h中再次包含了"header1.h",形成了循环引用。当预处理器处理main.cpp时,会不断递归展开这些头文件,导致无限循环。

1.头文件循环展开是指一个头文件直接或间接地包含了另一个头文件,导致无限循环。
2.解决方法是使用ifdef、ifndef等宏来防止循环展开。
3.新的C++标准中引入了pragma once,能够更好地防止头文件循环展开。

#ifdef解决方案:利用预处理指令#ifdef(如果定义)、#ifndef(如果未定义)和#endif来防止头文件的重复包含。在头文件开头使用#ifndef检查一个特定的宏是否已定义,如果未定义则执行后续内容,并定义该宏;在文件结尾使用#endif结束条件编译。

// header1.h
#ifndef HEADER1_H // 如果未定义HEADER1_H
#define HEADER1_H// 头文件内容,如函数声明、类定义等
void function1();#endif
// header2.h
#ifndef HEADER2_H // 如果未定义HEADER2_H
#define HEADER2_H// 头文件内容
#include "header1.h" // 包含header1.h
void function2();#endif

#pragma once解决方案:#pragma once是一种更现代、简洁的方式,它告诉编译器该头文件只应被包含一次。

// header1.h
#pragma once// 头文件内容
void function1();
// header2.h
#pragma once// 头文件内容
#include "header1.h"
void function2();

编译

作用:对预处理后的源文件进行词法分析(将代码分解成关键字、标识符、字面量等标记 )、语法分析(检查语法结构是否正确,构建语法树 )、语义分析(检查语义合法性,如类型匹配 ),并进行代码优化,最终将其转换为汇编代码。该过程能发现代码中的语法错误和语义错误。
示例:对于int a = 1 + 2; 这样的 C 代码,编译阶段会转换为对应汇编指令实现计算和赋值操作。
命令:在 GCC 中用-S选项,如gcc source.i -S -o source.s ,source.s是生成的汇编文件

– 将翻译单元转换为相应的汇编语言表示
– 编译优化
– 增量编译 V.S. 全部编译

编译优化
1.编译优化能够提高程序执行速度,但可能不利于调试。
2.优化级别越高,生成的汇编代码越少,执行速度越快。
3.debug编译和release编译的区别在于是否进行优化(debug模式编译优化的比较少易于调试)

未进行编译优化

编译优化后
中间步骤无法设置断点进行调试,因为优化后可能会省略很多步骤,导致中间步骤无法设置断点

增量编译和全部编译Rebuild(只针对源文件)
1.增量编译只编译修改过的源文件,提高编译效率。
2.全部编译重新编译所有源文件,确保程序正确性。
3.增量编译可能无法正确处理头文件修改的情况,需要全部编译。

汇编

作用:将汇编代码转换为机器码,生成目标文件(如.o或.obj )。汇编器把每条汇编指令翻译成计算机硬件能理解执行的二进制机器指令,一个汇编指令可能对应多个机器指令。
示例:汇编器把汇编代码里的mov指令转成对应二进制机器码。
命令:在 GCC 中用-c选项 ,如gcc source.s -c -o source.o ,source.o为生成的目标文件。

链接

作用:把多个目标文件(编译、汇编生成的 )和库文件合并成一个可执行文件。过程包括符号解析(查找并确定目标文件中引用的外部符号,如函数、变量地址 )、地址重定位(为程序代码和数据分配运行时内存地址 )。
示例:程序调用标准库printf函数,链接阶段会把标准库中printf函数实现代码链接到可执行文件。
命令:在 GCC 中直接用gcc命令,如gcc source1.o source2.o -o program ,program为生成的可执行文件

– 合并多个目标文件,关联声明与定义
– 连接( Linkage )种类:内部连接、外部连接、无连接
– 链接常见错误:找不到定义 undefined reference 属于链接错误,原因是只有声明没有定义

http://www.xdnf.cn/news/632161.html

相关文章:

  • Fuzz 模糊测试篇JS 算法口令隐藏参数盲 Payload未知文件目录
  • 基于 ColBERT 框架的后交互 (late interaction) 模型速递:Reason-ModernColBERT
  • 广东省省考备考(第十九天5.24)—言语(第七节课)
  • Qwen2.5 VL 多模态融合阶段(3)
  • python炸鱼船
  • java基础(继承)
  • 【免费使用】剪Y专业版 8.1/CapCut 视频编辑处理,素材和滤镜
  • Spyglass:跨时钟域同步(长延迟信号)
  • 异步处理与事件驱动中的模型调用链设计
  • 5.24 打卡
  • 【电池】极端环境对锂离子电池的影响-【2.5万字解析】
  • 数值分析(电子和通信类)
  • 什么是电离层闪烁
  • WPS 64位与EndNote21.5工作流
  • 【大模型】TableLLM论文总结
  • 力扣刷题(第三十六天)
  • C++ class和struct的区别
  • AI专题:如何把DeepSeek变成你的AI个人助手
  • 虚拟环境中的PyQt5 Pycharm设置参考
  • 多态的总结
  • http协议和session会话
  • 变革性的聚变路线:基于FRC构型的可控核聚变
  • leetcode 862. 和至少为 K 的最短子数组
  • 2025深圳国际无人机展深度解析:看点、厂商与创新亮点
  • 何谓第二大脑?读书笔记
  • 进一步学习线程相关知识
  • 《打破枷锁:Python多线程GIL困境突围指南》
  • AUTOSAR图解==>AUTOSAR_SRS_LIN
  • 【MySQL】第十弹——事务
  • 夏日旅行(广度优先搜索)