编译入门¶

    编译入门¶

    约 2200 个字 56 行代码 1 张图片 预计阅读时间 8 分钟

    编译是什么?1

    编译是将便于人编写、阅读、维护的高级计算机语言所写作的源代码程序,翻译为计算机能解读、运行的低阶机器语言的程序,也就是可执行文件。

    编译器将原始程序(source program)作为输入,翻译产生使用目标语言(target language)的等价程序。

    源代码一般为高级语言(High-level language),如 Pascal、C、C++、C# 、Java 等,而目标语言则是汇编语言或目标机器的目标代码(Object code),有时也称作机器代码(Machine code)。

    为什么 C++ 需要编译?

    C++ 是一种高级编程语言,它提供了丰富的特性来帮助开发者编写高效、灵活的程序。然而,计算机硬件只能理解和执行机器语言指令,即二进制代码。C++ 代码是人类可读的文本形式,需要转换成机器可执行的格式。这个过程就是编译。

    编译器是将 C++ 源代码转换成机器代码的软件工具。编译过程确保了代码的语法正确性、类型安全,并且优化了代码以提高执行效率。

    机器语言、汇编语言、高级语言¶

    参考链接:

    Machine, assembly and High level Language

    深入理解计算机系统------汇编语言和机器语言

    机器语言:

    机器语言是最底层的编程语言,能被计算机的中央处理器(CPU)直接理解。它完全由二进制代码(0 和 1)构成,表示 CPU 可执行的原始指令。

    特点:

    二进制格式:机器语言指令以二进制(0 和 1)编写,因为计算机硬件通过电信号的开/关状态来识别指令。

    架构特定性:因底层硬件设计差异,不同 CPU 架构(如 Intel x86、ARM)的机器语言各不相同。

    无抽象:机器语言最贴近硬件,不提供任何抽象,程序员需直接管理内存、数据操作和控制流程。

    例子:

    10001001 11011000 的作用可能是:将寄存器 BX 的内容送到 AX 中

    汇编语言:

    汇编语言是一种低级编程语言,是机器码的符号化表示,相对便于记忆。每条汇编指令直接对应一条机器码指令,但代码使用可读的文本和助记符而非二进制。

    特点:

    可读性:汇编语言用助记符(如 MOV 表示数据移动,ADD 表示加法)代替二进制,比机器码更易理解。

    一对一映射:每条汇编指令对应特定 CPU 架构的一条机器码指令。

    架构特定性:汇编语言与 CPU 架构紧密绑定,不同机器的汇编程序需修改才能运行,程序可移植性差。

    精细控制:允许直接操作寄存器、内存地址等硬件资源。

    例子:

    mov ax,bx:将寄存器 BX 的内容送到 AX 中

    高级语言:

    高级语言是抽象层级更高的编程语言,更贴近人类语言,使编程更便捷。其设计目标是跨硬件平台的可移植性。

    特点:

    高抽象:隐藏硬件细节(如内存管理、CPU 指令),让开发者专注于问题解决。

    可移植性:程序通常无需修改即可在不同计算机上运行,语言编译器/解释器会处理硬件差异。

    丰富库支持:提供大量内置库和框架,简化开发流程。

    易读性:语法接近自然语言,代码更易编写和维护。

    自动内存管理:许多高级语言通过垃圾回收等机制自动管理内存,无需手动干预。

    例子:

    ax = bx

    解释性语言与编译性语言¶

    参考链接:

    「编译型语言」和「解释型语言」的区别

    维基百科——编译语言

    编译型语言:

    编译型语言,是指在执行之前需要将源代码编译(compile)为机器代码的编程语言,使用“编译器”。

    执行前需要编译,将程序转换为可在目标机器上运行的可执行文件,执行速度快。但每次修改源代码后,都需要重新编译,生成新的可执行文件。

    编译型语言包括:C,C++,Rust,Go 等

    解释型语言:

    解释型语言,是指执行期间动态将代码逐句解释(interpret)为机器代码,或是已经预先编译为机器代码的子程序,之后再执行的编程语言,使用“解释器”。

    解释型语言可快速调试,程序的开发整体时间相对较少。但由于需要在运行时转换为机器代码,解释型语言通常比编译型语言更慢。

    解释型语言包括:JavaScript,Python,MATLAB 等

    (选学)即时编译

    即时编译(Just-in-time compilation,缩写为 JIT),是一种执行计算机代码的方式,这种方式结合了解释执行和预先编译的优点。

    在程序运行过程中,JIT 编译器会不断分析正在执行的代码,找出频繁执行的部分,通过编译或重新编译这些部分来加速运行,(这要求编译或重新编译带来的性能提高将超过编译该代码的开销。)

    编译的流程¶

    参考链接:

    C/C++ 程序编译流程(预处理->编译->汇编->链接)

    浅谈 C/C++ 编译流程和常见编译器

    下面以 g++ 编译 hello.cpp 文件为例,介绍编译的流程。

    常见的 C/C++ 编译器

    常见的 C/C++ 编译器主要有以下两种

    GCC(GNU Compiler Collection)是一个开源的编译器集合,支持多种编程语言,包括 C 和 C++。

    gcc 用于 C 语言的编译

    g++ 用于 C++ 的编译(兼容 C 语言的编译)

    LLVM(Low Level Virtual Machine)是一个开源的编译器基础设施项目,由一系列的模块化和可重用的编译器组件构成,支持广泛的编程语言,包括但不限于 C、C++。

    clang 用于 C 语言的编译

    clang++ 用于 C++ 的编译(兼容 C 语言的编译)

    安装 GCC

    我们以及在标准开发环境中安装了 gcc 。如果您希望在您的 Linux 环境中安装,可以参考下述方法安装:

    sudo apt-get install build-essential # Debian/Ubuntu

    sudo yum install gcc-c++ # CentOS/Fedora

    预处理阶段(Preprocessing):

    移除注释。

    处理预处理指令,如 #include 和宏定义,生成预处理文件(.i)

    g++ –E hello.cpp –o hello.i

    编译阶段(Compilation):

    将预处理后的代码转换成汇编语言,生成汇编文件(.s)

    g++ –S hello.i –o hello.s

    汇编阶段(Assembly):

    将汇编语言转换成机器代码,生成二进制文件(.o)

    g++ –c hello.s –o hello.o

    链接阶段(Linking):

    将编译生成的目标文件与库文件链接在一起,生成可执行文件。

    g++ hello.o –o hello

    上述四个步骤也可以直接一步完成

    g++ hello.cpp -o hello

    编译选项¶

    参考链接:

    GCC Option Summary

    GCC 参数详解

    编译选项是向编译器传递指令的参数,用于控制编译过程的不同方面。

    常用编译选项

    选项

    意义

    -o

    指定输出文件

    -E

    只进行预处理,生成 .i 预处理文件

    -S

    只进行预处理和编译,生成 .s 汇编文件

    -c

    只进行预处理,编译,和汇编,不进行链接,生成 .o 二进制文件

    -g

    生成调试信息,供 GNU 调试器使用

    -w

    不生成任何警告信息

    -Wall

    生成所有警告信息

    -ldir

    添加 dir 为头文件搜索路径

    -Ldir

    添加 dir 为链接库搜索路径

    -std=

    编译的标准,如 c11 , c++17 等

    -O0

    不进行优化处理

    -O1

    优化生成代码

    -O2

    进一步优化

    -O3

    更进一步优化

    -Ofast

    更加激进的优化,可能影响计算精度

    -Os

    优化代码大小

    -march=

    指定目标 CPU 架构

    -mXXX

    启用 XXX 指令集

    -fopenmp

    启用 OpenMP 并行

    使用样例:

    gcc -O3 -march=native -fopenmp YOUR_CODE.c -o YOUR_PROGRAM

    编译实战¶

    动手编译一个程序

    编写源代码:

    创建如下三个文件:

    main.ctest.htest.c

    #include "test.h"

    int main(){

    hellon(3);

    hello_n(1);

    return 0;

    }

    #include

    void hellon(int);

    #include "test.h"

    static void inline hello();

    void hellon(int n){

    for(int i=0;i

    hello();

    }

    }

    static void inline hello(){

    printf("hello,world\n");

    }

    编译源代码:

    使用以下命令编译:

    gcc test.c main.c -o main

    运行程序:

    编译完成后,可以通过以下命令运行程序:

    ./main

    分步编译

    尝试使用 gcc 分步编译上述三个文件:

    先将两个 .c 文件编译为对应的 .o 文件

    再使用 gcc 将这两个文件链接得到可执行文件 main

    思考题

    上述步骤中 printf 函数的实现代码是否被编译了,如果没有,为什么最后可以成功调用 printf 函数

    编译并运行这个程序,尝试通过修改编译选项减少其运行时间。

    calculate.cpp

    #include

    #include

    int main() {

    const long long N = 200000000;

    double result = 0;

    auto start = std::chrono::high_resolution_clock::now();

    for (long long i = 0; i < N; ++i) {

    result += i;

    result /= 3;

    result /= 3;

    result /= 3;

    }

    auto end = std::chrono::high_resolution_clock::now();

    auto duration = std::chrono::duration_cast(end - start);

    std::cout << "Result: " << result << "\n";

    std::cout << "Time: " << duration.count() << " ms" << std::endl;

    return 0;

    }

    也许可以用

    -Ofast

    节选自维基百科 ↩

    2025年7月21日

    2025年5月25日