习惯用vscode的运行键一键编译当接触我的第一个项目时发现怎么能把多个文件合到一块运行。gcc刚好帮我解决了这个问题…1. gcc 和 g 到底什么关系g 是 GCC 套件里专门编译 C 的。那gcc和g的区别是什么gcc编译 C 文件不会自动链接 C 标准库g编译 C 文件自动链接 C 标准库libstdc当你用gcc去编译一个.cpp时链接时会报一堆undefined reference to std::xxx因为gcc不知道要拉 C 标准库。g会自动处理这件事。验证一下这是用gcc编译.cpp的错误 解决方法有两种一是继续用gcc不过需要手动加-lstdc第二种就方便多了直接用g编译即可2. 装 g由于本人在linux平台上进行开发所以这里只提供linux系统上的安装以及版本查看指令…LinuxUbuntu/Debiansudoaptupdatesudoaptinstallg# 验证g--version输出大概长这样g (Ubuntu 13.3.0-6ubuntu2~24.04.1) 13.3.0 Copyright (C) 2023 Free Software Foundation, Inc.3. 编写简单cpp程序使用G编译简单实现hello g代码hello.cpp#includeiostreamintmain(){std::coutHello, g!\n;return0;}编译运行g hello.cpp-ohello ./hello输出Hello, g!命令拆开g调用编译器hello.cpp源文件-o hello指定输出文件名。不写-o生成a.out。读到这里你可能会有疑惑-o到底是什么意思怎么就能把一个源文件变成可执行文件如果你有计算机系统基础那么你可能知道到底发生了什么。4. 拆开编译竟有四个阶段你敲g hello.cpp -o hello里面其实跑完了四步源文件 (.cpp)---[1] 预处理 (Preprocessing) ---.i 文件展开 #include、#define---[2] 编译 (Compilation) ---.s 汇编文件 ---[3] 汇编 (Assembly) ---.o 目标文件机器码---[4] 链接 (Linking) ---可执行文件不过在现实场景中只需要g xxx -o即可一键代替这四个阶段4.1 准备例子math.cpp#includeiostream#includecmath#defineAUTHORrr// 宏预处理阶段展开intmain(){doublex4.0;doubleystd::sqrt(x);std::coutAuthor: AUTHOR\n;std::coutsqrt(x) y\n;return0;}4.2 阶段一预处理g-Emath.cpp-omath.i-E告诉 g只做预处理到这就停。# 知识补充wc -l 统计行数-w 单词数-c 字节数wc-lmath.cpp# 输出:11 math.cppwc-lmath.i# 输出:46756 math.i# 看最后几行AUTHOR 已经被替换成 rrtail-5math.i# 找到输出std::cout Author: rr \n;C 的头文件比 C 的庞大很多——iostream展开后行数远超stdio.h里面全是模板和流的声明。这也是 C 编译比 C 慢的一个原因每个.cpp都要独立展开一遍头文件。4.3 阶段二编译把预处理后的代码翻译成汇编g-Smath.i-omath.s# 或者一步到位g-Smath.cpp-omath.s看生成的汇编输入指令cat math.s完整汇编代码.file math.cpp .text #APP .globl _ZSt21ios_base_library_initv .section .rodata .LC1: .string Author: .LC2: .string rr .LC3: .string \n .LC4: .string sqrt( .LC5: .string ) #NO_APP .text .globl main .type main, function main: .LFB2585: .cfi_startproc endbr64 pushq %rbp .cfi_def_cfa_offset 16 .cfi_offset 6, -16 movq %rsp, %rbp .cfi_def_cfa_register 6 subq $16, %rsp movsd .LC0(%rip), %xmm0 movsd %xmm0, -16(%rbp) movq -16(%rbp), %rax movq %rax, %xmm0 call sqrtPLT movq %xmm0, %rax movq %rax, -8(%rbp) leaq .LC1(%rip), %rax movq %rax, %rsi leaq _ZSt4cout(%rip), %rax movq %rax, %rdi call _ZStlsISt11char_traitsIcEERSt13basic_ostreamIcT_ES5_PKcPLT movq %rax, %rdx leaq .LC2(%rip), %rax movq %rax, %rsi movq %rdx, %rdi call _ZStlsISt11char_traitsIcEERSt13basic_ostreamIcT_ES5_PKcPLT movq %rax, %rdx leaq .LC3(%rip), %rax movq %rax, %rsi movq %rdx, %rdi call _ZStlsISt11char_traitsIcEERSt13basic_ostreamIcT_ES5_PKcPLT leaq .LC4(%rip), %rax movq %rax, %rsi leaq _ZSt4cout(%rip), %rax movq %rax, %rdi call _ZStlsISt11char_traitsIcEERSt13basic_ostreamIcT_ES5_PKcPLT movq %rax, %rdx movq -16(%rbp), %rax movq %rax, %xmm0 movq %rdx, %rdi call _ZNSolsEdPLT movq %rax, %rdx leaq .LC5(%rip), %rax movq %rax, %rsi movq %rdx, %rdi call _ZStlsISt11char_traitsIcEERSt13basic_ostreamIcT_ES5_PKcPLT movq %rax, %rdx movq -8(%rbp), %rax movq %rax, %xmm0 movq %rdx, %rdi call _ZNSolsEdPLT movq %rax, %rdx leaq .LC3(%rip), %rax movq %rax, %rsi movq %rdx, %rdi call _ZStlsISt11char_traitsIcEERSt13basic_ostreamIcT_ES5_PKcPLT movl $0, %eax leave .cfi_def_cfa 7, 8 ret .cfi_endproc .LFE2585: .size main, .-main .section .rodata .type _ZNSt8__detail30__integer_to_chars_is_unsignedIjEE, object .size _ZNSt8__detail30__integer_to_chars_is_unsignedIjEE, 1 _ZNSt8__detail30__integer_to_chars_is_unsignedIjEE: .byte 1 .type _ZNSt8__detail30__integer_to_chars_is_unsignedImEE, object .size _ZNSt8__detail30__integer_to_chars_is_unsignedImEE, 1 _ZNSt8__detail30__integer_to_chars_is_unsignedImEE: .byte 1 .type _ZNSt8__detail30__integer_to_chars_is_unsignedIyEE, object .size _ZNSt8__detail30__integer_to_chars_is_unsignedIyEE, 1 _ZNSt8__detail30__integer_to_chars_is_unsignedIyEE: .byte 1 .align 8 .LC0: .long 0 .long 1074790400 .ident GCC: (Ubuntu 13.3.0-6ubuntu2~24.04.1) 13.3.0 .section .note.GNU-stack,,progbits .section .note.gnu.property,a .align 8 .long 1f - 0f .long 4f - 1f .long 5 0: .string GNU 1: .align 8 .long 0xc0000002 .long 3f - 2f 2: .long 0x3 3: .align 8 4:是不是一脸懵逼了没事都这样。但是注意几点你会发现1.字符串常量.LC1: .string Author: .LC2: .string “rr”代码里的字符串在汇编里直接摆在这。2.名字修饰Name Mangling—— C 特有的现象call sqrtPLT ← C 函数名字干干净净call _ZStlsISt11char_traitsIcEERSt13… ← C 的 cout一长串乱码区别很明显sqrt 就叫 sqrt但 cout 变成了 _ZStls…。因为 C 支持函数重载编译器必须给每个重载版本编一个唯一的名字。用 cfilt 能还原执行该代码rrrr-VMware-Virtual-Platform:~/桌面$ cfilt _ZStlsISt11char_traitsIcEERSt13basic_ostreamIcT_ES5_PKc std::basic_ostreamchar, std::char_traitschar std::operator std::char_traitschar (std::basic_ostreamchar, std::char_traitschar , char const*)哦。原来就这。。这就是 cout 。这就是 C 名字修饰——有重载就必须修饰C 没有重载所以不用修饰。4.4 阶段三汇编汇编 → 机器码g-cmath.s-omath.o# 或者g-cmath.cpp-omath.o看目标文件里的符号filemath.o# 输出:math.o: ELF 64-bit LSB relocatable, x86-64, version 1 (SYSV), not strippednm math.o# 别想多了nm是name list的意思用于查看目标文件中有哪些符号(例如函数变量....)# 输出:# 0000000000000000 T main# U sqrt# U _ZNSolsEd#0000000000000019 r _ZNSt8__detail30__integer_to_chars_is_unsignedIjEE#000000000000001a r _ZNSt8__detail30__integer_to_chars_is_unsignedImEE#000000000000001b r _ZNSt8__detail30__integer_to_chars_is_unsignedIyEE# U _ZSt21ios_base_library_initv# U _ZSt4cout# U _ZStlsISt11char_traitsIcEERSt13basic_ostreamIcT_ES5_PKc# T:当前函数在你目前的文件里实现了# U:说明符号引用了但没有实现需要链接时去别的文件里找# r:只读数据(常量,类型标记...)4.5 阶段四链接把.o和库拼在一起补上所有未定义符号g math.o-omath-lm-lm链接数学库(libm.so)。C 标准库默认链接不需要额外指定。./math_demo# 输出:#Author: rr#sqrt(4) 24.6 小结阶段命令输入 → 输出产物预处理g -E.cpp→.i纯文本展开后的 C 代码编译g -S.i→.s纯文本汇编代码能看到名字修饰汇编g -c.s→.o二进制未链接的机器码链接g.o→ 可执行二进制完整的可执行文件g math.cpp -o math就是背后默默跑完了上面四步。5. 常用编译选项5.1 输出控制-ofile# 指定输出文件名不写默认 a.out-c# 只编译到 .o不链接-S# 只编译到汇编 (.s)-E# 只做预处理-save-temps# 保留所有中间文件 (.i .s .o)5.2 警告-Wall# 常用警告不是 all是最常用的那批-Wextra# 额外警告-Werror# 警告当成错误有警告就编译失败-Wpedantic# 严格标准检查-Wshadow# 变量名遮蔽警告C 里特别有用# 日常组合g-Wall-Wextra-Werrorprogram.cpp-oprogram-Wshadow在 C 里值得单独开——成员变量和局部变量同名的时候会提醒你这种 bug 不太好找。5.3 优化等级-O0# 不优化默认调试用-O1# 轻度优化-O2# 标准优化发布版本推荐-O3# 激进优化-Os# 优化体积g-Wall-Wextra-g-O0main.cpp-omain5.4 调试信息-g# 生成调试信息GDB 用5.5 语言标准-stdc17# C17目前最稳-stdc20# C20concept、ranges、模块等新特性-stdc23# C23部分特性还没完全支持# 强制严格标准g-stdc20-Wpedanticmain.cpp-omainGCC 对 C20 的完整支持要到 GCC 12 以上GCC 14 基本全覆盖。用 C20 之前先确认版本。5.6 路径与链接-Ipath# 头文件搜索路径-Lpath# 库文件搜索路径-lname# 链接 libname.a 或 libname.so# 例子g -I./include -L./lib-lfoomain.cpp-omain6. 多文件编译6.1 先看结构calc/ ├── main.cpp # 入口 ├── calc.hpp # 声明 └── calc.cpp # 实现6.2 代码calc.hpp —— 头文件声明有啥函数#ifndefCALC_HPP#defineCALC_HPPdoubleadd(doublea,doubleb);doublesub(doublea,doubleb);doublemul(doublea,doubleb);doublediv(doublea,doubleb);#endifcalc.cpp —— 实现#includecalc.hpp#includestdexceptdoubleadd(doublea,doubleb){returnab;}doublesub(doublea,doubleb){returna-b;}doublemul(doublea,doubleb){returna*b;}doublediv(doublea,doubleb){if(b0.0)throwstd::runtime_error(除数为零);returna/b;}main.cpp —— 主程序#includeiostream#includecalc.hppintmain(){doublex10.0,y3.0;std::coutx y add(x,y)\n;std::coutx - y sub(x,y)\n;std::coutx * y mul(x,y)\n;std::coutx / y div(x,y)\n;return0;}6.3 一步到位g main.cpp calc.cpp-ocalc ./calc文件少可以这样但每次改一个文件全部重编效率低。6.4 分步编译# 各编各的g-cmain.cpp-omain.o g-ccalc.cpp-ocalc.o# 链接g main.o calc.o-ocalc改一个文件只需要重编那个文件再链接就行。7. 静态库 vs 动态库7.1 静态库# 编译目标文件g-ccalc.cpp-ocalc.o# ar archive把 .o 打包成 .a 文件# rcs 替换/创建/索引ar rcs libcalc.a calc.o# -L. 告诉编译器在当前目录(.)找库# -lcalc 链接 libcalc.a省略lib和.a自动匹配g main.cpp -L.-lcalc-ocalc静态库直接塞进可执行文件搬哪都能跑。7.2 动态库# -fPIC Position Independent Code位置无关代码# 动态库加载地址不确定这个指令记得加奥g-c-fPICcalc.cpp-ocalc.o# -shared 生成共享库.so Shared Objectg-sharedcalc.o-olibcalc.so g main.cpp -L.-lcalc-ocalc运行的时候大概率报错rrrr-VMware-Virtual-Platform:~/桌面$ ./calc ./calc: error while loading shared libraries: libcalc.so: cannot open shared object file: No such file or directory因为系统不知道去哪找这个.so文件# LD_LIBRARY_PATH 告诉系统去哪搜 .so# .当前目录$LD_LIBRARY_PATH保留原来的路径exportLD_LIBRARY_PATH.:$LD_LIBRARY_PATH./calc# 你会发现 运行成功了...g背后的原理相当复杂本人能力有限参考学习了诸多资料加以个人想法生成了该文章。作为c开发爱好者g是要掌握的在日常开发中g xxx.cpp -o xxx一键生成可执行文件再带上-Wall -Wextra -g -O0大部分编译问题都能搞定。今后本人会针对C 开发路线中的知识细节问题进行学习心得分享感谢观看。