尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

gcc/g++命令行编译实战:从预处理到链接,掌握Linux编译参数与常见错误排查

gcc/g++命令行编译实战:从预处理到链接,掌握Linux编译参数与常见错误排查 1. 为什么还在用命令行编译聊聊gcc/g的真实价值很多刚接触Linux的朋友都会有这个疑问Windows上用Visual Studio、Clion、VS Code点点鼠标就能编译运行到了Linux终端里非得敲一长串命令这不是折腾人吗我第一次用gcc的时候也是这个感觉明明图形界面那么发达的年代为什么还要回到命令行去编译代码但真正用熟了之后我才明白命令行编译器和IDE编译器的区别类似于自动挡和手动挡的区别。IDE确实方便它把所有细节都藏起来了你点一下运行按钮背后帮你把预处理、编译、汇编、链接全干完了。可一旦出了问题——比如链接报错、符号找不到、库路径不对——IDE给的报错信息往往是云里雾里的因为你根本看不到它到底执行了哪些步骤。gcc/g就不一样了每个阶段都可以单独执行、单独查看结果每一步干了什么清清楚楚。这就像你学做饭可以先看看食材长什么样、切好了是什么状态、下锅炒到什么程度而不是打开一台全自动炒菜机只知道最后端出来一盘菜中间糊没糊完全没概念。gcc和g本质上都是GNU编译器套件GNU Compiler CollectionGCC的一部分gcc偏向C语言g偏向C。它俩的关系很微妙gcc也能编译C代码g也能编译C代码但链接C标准库的行为不同这个细节后面我会专门讲清楚。对所有Linux后端开发者、嵌入式工程师、运维工程师来说掌握gcc/g不光是为了把代码编译出来更是为了读懂整个构建系统的逻辑因为Makefile、CMake最终调用的还是这些基础命令。这篇文章我会从编译的完整流程讲起然后逐项拆解最常用的编译参数再配合几个实际案例说明怎么应对最常见的报错最后聊聊优化选项和调试技巧。无论是刚入门的大学生还是工作多年的工程师相信都能从中找到对自己有用的东西。2. 搞清楚编译的全过程一条命令背后的四道工序编译器不是一步直接把源代码变成可执行文件的而是分成预处理、编译、汇编、链接四个阶段。这四道工序各有各的职责理解它们对排查问题非常关键。2.1 预处理阶段把代码摊开来看预处理是源文件被处理的第一步做的事情包括展开头文件、替换宏定义、处理条件编译指令等。比如你写了#include stdio.h预处理阶段会把stdio.h里的内容原封不动地插入到源文件中你定义了#define MAX 100预处理阶段会把所有出现MAX的地方替换成100。可以用gcc -E main.c -o main.i把预处理后的结果输出出来。我曾让一个刚学C语言的同学干过这件事他看完main.i文件后整个人是懵的——原本10行的代码膨胀成了上千行里面全是各种头文件的内容。但看完了也就懂了原来printf函数的声明是从这里来的原来头文件并不是编译的一部分而是在预处理阶段被粘贴进来的。很多奇怪的编译错误其实都出在这个阶段比如头文件相互包含导致重复定义、宏替换之后语法错误等。学会查看-E的输出等于给排查这类问题开了一扇窗。2.2 编译阶段把C代码翻译成汇编指令编译阶段把预处理后的代码翻译成汇编代码这是整个过程中技术含量最高的部分也是各类优化选项真正发力的地方。用gcc -S main.i -o main.s可以生成汇编文件。很多人觉得汇编晦涩难懂不愿意去看。但我建议至少看一次自己写的代码对应的汇编长什么样尤其是循环、递归、函数调用这些基础结构。你会发现for循环本质上就是几个比较和跳转指令的组合函数调用就是压栈、跳转、返回的过程。有了这个概念理解栈溢出、递归深度限制这些问题时就容易多了。编译阶段也是语法检查的主要阶段。编译器在这里进行词法分析、语法分析、语义分析任何不符合语法规则的写法都会被拦截下来。常见的expected ; before }这类报错就是语法分析阶段发现的。2.3 汇编阶段从汇编指令到机器码汇编阶段把汇编代码转换成机器码生成目标文件object file。用gcc -c main.s -o main.o就能完成这一步。目标文件里已经是二进制机器指令了但它还不能独立运行因为里面还留着很多待填的坑等待链接阶段去填补。在Linux下用file main.o查看会看到类似ELF 64-bit LSB relocatable的输出。这里的单词relocatable很关键表示它是可重定位的目标文件意思是里面的地址还没有最终确定需要在链接时重新定位。可以做个实验单个目标文件无法直接执行但可以查看它里面的符号信息。nm main.o会列出目标文件中定义的函数和全局变量符号如果某个函数名后面带着U标志说明它是未定义的外部引用需要链接时在其他目标文件或库中找答案。2.4 链接阶段把所有碎片拼成一个整体链接是最后一道工序也是新手遇到问题最多的一道工序。一个工程通常由多个源文件、多个目标文件组成还依赖各种静态库、动态库。链接器要把这些碎片拼在一起解决符号引用关系分配最终的内存地址生成可执行文件。最典型的链接错误有两种。一种是undefined reference to xxx意思是编译器在整个链接输入里都找不到xxx这个符号的定义。另一种是multiple definition of xxx意思是同一个符号在多个目标文件里都有定义链接器不知道该用哪个。这两种错误我在后文的常见问题排查中会给具体的回溯思路。现在只需要记住链接阶段的报错和编译阶段的报错性质完全不同编译阶段是你写错了语法链接阶段是你组织工程文件的方式有问题比如漏了某个源文件、忘了链接某个库、或者函数定义重复了。3. gcc/g 高频参数实战解析gcc/g的参数加起来有好几百个但日常开发中经常用到的其实就二三十个。把经典的那几个吃透大部分场景就够用了。这里按用途分类整理并配上实际命令。3.1 最基本的输出控制参数-o指定输出文件名。注意是-o不是-O小写字母ooutput的意思。gcc main.c -o main就不说了特别提醒如果你写gcc main.c -o然后漏了文件名编译器会把你打印出来让你重新输入典型的给你机会你不中用。-c只编译不链接生成目标文件。在分离编译的大型项目中每个源文件先各自-c生成.o文件最后再统一链接。好处是改了一个文件只需要重新编译那一个其余目标文件能直接复用。-S只编译到汇编层生成.s文件不生成目标文件。-E只预处理生成.i文件。-v显示编译过程的详细信息包括调用了哪些子程序、哪些参数。查工具链版本和环境配置问题时非常有用gcc -v输出的最后能看到gcc的完整版本号、配置参数和线程模型。3.2 告警控制参数-Wall开启绝大多数常见警告。我强烈建议所有人在编译时加上这个选项它能帮你发现很多潜在的逻辑隐患比如未使用的变量、if语句里赋值而不是比较等。训练有素的老手从来不嫌警告烦因为警告就是编译器在免费给你做代码审查。-Wextra在-Wall基础上再追加一些警告比如比较时符号性不一致、结构体成员未初始化等。-Werror把警告当作错误处理。一旦有警告编译就终止。很多严谨的项目都会开这个选项以此强制所有人保持代码干净。初学者慎重使用因为会特别打击信心。有些朋友抱怨编译时满屏红色的错误和警告看着特别慌。我的经验是从第一条开始看。编译器给出的错误信息虽然多但很多后续报错都是因为第一个错误引发了连锁反应。修完第一条后面可能会自动消失一大半。3.3 宏定义与头文件路径-D定义宏。经典使用方式是传配置参数比如gcc -DDEBUG main.c -o main代码里的#ifdef DEBUG块就会被编译进去。实际项目里也会用来传版本号比如-DVERSION\1.0.0\注意引号转义。-U取消宏定义。-I指定头文件的搜索路径。比如你把myheader.h放在./include目录下那么编译时写-I./include。头文件搜索顺序是源文件所在目录、-I指定的目录按顺序、系统默认目录比如/usr/include。-include强制包含头文件相当于在每个源文件开头加了一行#include。在自动化构建时偶尔能用到。3.4 库的链接与路径-l链接库文件。-lm表示链接libm.so数学库-lpthread链接libpthread.so线程库。注意-l后面直接跟库名不写lib前缀和.so后缀。-L指定库文件的搜索路径。你下载了一个第三方库放在/usr/local/lib下链接时需要-L/usr/local/lib -lyourlib。-static用静态链接方式生成可执行文件。适合部署到目标环境未知的机器上因为不依赖目标机的动态库但文件体积大。-Wl,rpath给链接器传参数把运行时动态库搜索路径写进可执行文件。假设可执行文件放到/opt/myapp/bin动态库放在/opt/myapp/lib链接时可以加-Wl,-rpath,/opt/myapp/lib这样运行时就不需要设置LD_LIBRARY_PATH了。3.5 调试与优化-g生成调试信息供GDB等调试器使用。不要嫌调试信息让文件变大调试问题的时候你就知道它的好了。-O0不做任何优化编译速度快调试体验最好。开发调试阶段推荐。-O1/-O2基本优化。发布版本通常用-O2性能和编译时间能取得一个不错平衡。-O3最高优化级别编译器会做更多激进优化比如循环展开、向量化等。但不一定总是比-O2快个别场景下甚至可能变慢因为生成的可执行文件变大了指令缓存的表现反而变差。这里必须说一个常见误区有些人写代码追求不加括号、用晦涩的写法觉得这样显得高级指望编译器帮忙优化。这是本末倒置。编译器优化的是代码的执行方式不是帮你修复逻辑问题。写清楚意图最重要优化交给编译器它比你想象中聪明得多。3.6 语言标准与架构相关-stdc99、-stdc11、-stdc17指定语言标准。C语言最新主流是C11和C17C则常用C11因为大量现代C项目在存量代码里都切到这个标准、C14、C17。不同的标准支持的特性不一样比如C11才开始有auto、lambda、nullptr这些好用的东西。-m64/-m32强制生成64位或32位代码。-marchnative让编译器针对本机CPU架构做优化利用本机支持的指令集。但要注意这样编出来的程序拿到其他机器上可能跑不了。3.7 gcc 和 g 到底有什么不同这是提问频率最高的问题。gcc最初是GNU C Compiler后来扩展到支持C等语言改名为GNU Compiler Collection。但在命令行工具层面gcc和g之间存在两个实际区别。第一个区别是链接的标准库。用gcc编译C源文件编译阶段没问题但链接时不会自动链接C标准库libstdc所以如果你用了std::cout、std::string这些标准库功能链接会报一堆undefined reference错误。g会自动链接标准库因此直接编译C代码时推荐用g。第二个区别是源文件后缀的处理方式。gcc处理.c文件按C语言标准处理.cpp文件也能按C编译但g对.c文件仍按C来编译这就可能导致某些C语法在g下变成错误。为了避免不必要的困扰约定俗成的做法是编译C程序用gcc编译C程序用g即使你的代码其实不用C标准库。4. 实操演示从单个源文件到一个完整项目这一部分直接用实际命令演示从最简单的单文件编译开始逐步扩展到多文件和Makefile层面。这些都是Linux开发天天要碰的东西。4.1 单文件编译的常用命令组合打开终端创建一个最简单的hello.c#include stdio.h int main(void) { printf(Hello, Linux!\n); return 0; }最基本的编译gcc hello.c -o hello ./hello这条命令背后的完整流程是预处理 → 编译 → 汇编 → 链接一步到位。如果我想看中间产物gcc -E hello.c -o hello.i # 预处理 gcc -S hello.i -o hello.s # 编译得到汇编 gcc -c hello.s -o hello.o # 汇编得到目标文件 gcc hello.o -o hello # 链接得到可执行文件中间每一步产物的名字都是:wq的命名习惯——不是我是说都是按阶段命名的。hello.i、hello.s、hello.o非常直观。这个过程不需要死记硬背用file命令查看每一个中间产物的类型就能理解每一步做了什么。首次编译建议打开告警gcc -Wall -Wextra hello.c -o hello如果你的代码里有什么隐性问题这行命令能第一时间给你提示。打开告警一开始可能会比较吵但习惯了以后你会爱上这种未雨绸缪的安全感。4.2 多文件项目从零开始组织工程实际项目很少只有一个源文件。假设下面是一个简化版本的工程结构project/ ├── include/ │ └── utils.h ├── src/ │ ├── main.c │ └── utils.c └── build/utils.h声明了一个函数#ifndef UTILS_H #define UTILS_H int add(int a, int b); #endifutils.c实现它#include utils.h int add(int a, int b) { return a b; }main.c调用它#include stdio.h #include ../include/utils.h int main(void) { printf(2 3 %d\n, add(2, 3)); return 0; }先在src目录下生成目标文件cd src gcc -Wall -Wextra -I../include -c main.c -o main.o gcc -Wall -Wextra -I../include -c utils.c -o utils.o再链接gcc main.o utils.o -o ../build/myapp这里-I../include是关键它告诉编译器头文件在上级目录的include文件夹中。如果不加这个选项#include utils.h会先在当前目录找找不到就在系统默认路径找最终报fatal error: utils.h: No such file or directory。实际工程里你通常不会手动敲这么多命令而是写进Makefile或CMakeLists.txt里。但理解这行命令以后你就能看懂Makefile里的每一个规则是什么意思了。4.3 引入第三方库一个带数学库和线程的实例现在演示一个更贴近实际场景的例子用C写一个多线程程序并用到数学库函数。#include stdio.h #include pthread.h #include math.h void *worker(void *arg) { int n *(int *)arg; double result sqrt((double)n); printf(sqrt(%d) %.4f\n, n, result); return NULL; } int main(void) { pthread_t tid; int n 42; pthread_create(tid, NULL, worker, n); pthread_join(tid, NULL); return 0; }编译命令是gcc -Wall -Wextra -g thread_demo.c -o thread_demo -lm -lpthread注意两点。一是pthread_create这些函数来自POSIX线程库Linux下需要显式加-lpthread链接二是sqrt函数来自数学库需要加-lm。很多新手写出多线程或带数学运算的代码编译时出现undefined reference却不知道怎么解决就是因为少了这两个链接参数。链接时库的写法有一定顺序讲究。粗略来说如果A库依赖B库那么B库要放在A库后面。更严谨地说因为链接器是从左到右扫描目标文件和库文件的遇到未定义符号才在后续的库中寻找。把依赖项放到依赖者的后面你的链接过程会顺畅很多。4.4 Makefile 里的 gcc/g自动化构建初体验当源文件数量增长到十几个、几十个时手动输入编译命令就是灾难。写一个简易Makefile来管理是再自然不过的选择。CC gcc CFLAGS -Wall -Wextra -g -Iinclude LDFLAGS -lm -lpthread SRCS src/main.c src/utils.c src/api.c OBJS $(SRCS:.c.o) TARGET build/myapp $(TARGET): $(OBJS) $(CC) $(OBJS) -o $(TARGET) $(LDFLAGS) %.o: %.c $(CC) $(CFLAGS) -c $ -o $ clean: rm -f $(OBJS) $(TARGET) .PHONY: clean这已经能应付不少中小型项目了。注意CC变量设为gcc如果你在写C把CC改成g或者在单独文件里定义CXX g编译规则里用$(CXX)那是C工程的通行做法。5. 进阶使用动态库、静态库与编译优化把代码封装成库是复用代码的最常见方式。Linux下库分两种静态库.a和动态库.so。二者的核心区别在于链接时发生什么。静态库在链接时把目标代码直接拷贝进可执行文件程序运行不依赖外部动态库在链接时只记录依赖关系运行时才加载因此体积小、能共享内存、更新库时无需重新编译所有程序。5.1 生成静态库并引用静态库本质上就是一个打包好的目标文件的集合。先生成目标文件再用ar工具打包gcc -Wall -c utils.c -o utils.o ar rcs libutils.a utils.oar rcs里的r表示插入文件c表示创建库s表示创建索引。生成libutils.a之后别人使用这个库链接时只需要gcc main.c -L. -lutils -o main注意-L.表示在当前目录搜索库文件。如果不加链接器只在默认路径下寻找会报找不到libutils.a。5.2 生成动态库并引用动态库的生成稍有不同需要加-fPIC和-sharedgcc -Wall -fPIC -c utils.c -o utils.o gcc -shared utils.o -o libutils.so-fPIC生成位置无关代码这是为了动态库在运行时能被加载到内存的任何地址上都能正确执行。-shared告诉编译器生成共享库。使用动态库编译程序gcc main.c -L. -lutils -o main但运行时会出现一个问题如果动态库不在系统默认搜索路径运行时就会报error while loading shared libraries: libutils.so: cannot open shared object file。解决方法有三种把库放到系统默认路径/usr/lib或/usr/local/lib需要root权限。设置环境变量export LD_LIBRARY_PATH$LD_LIBRARY_PATH:.。链接时指定rpathgcc main.c -L. -lutils -Wl,-rpath,$(pwd) -o main把运行时搜索路径直接写进可执行文件。第三种方式在部署自己写的工具时非常方便可以避免到处设置环境变量。5.3 优化相关的一个实战提醒我在写数值计算程序时曾经对比过-O0和-O3两种编译选项结果非常有意思。一个双层循环的矩阵运算-O2比-O0快了大约8倍但-O3比-O2只快了一点点。原因很简单代码里最耗时的部分在-O2时编译器已经完成了主要的优化比如循环展开、寄存器优化-O3继续加码的收益有限。更值得注意的教训是优化后程序行为可能发生变化。比如浮点数运算顺序的调整可能导致结果有微小差别有些依赖未定义行为比如有符号整数溢出的程序在优化后甚至会崩溃。所以发布版本虽然用-O2或者-O3但调试版本务必用-O0 -g否则你单步调试时看到的变量值可能和源代码对不上因为编译器已经把代码重排了。6. 常见报错汇总与排查思路这些是我在实际开发中遇到过的、以及帮别人排查时反复出现的经典错误整理成一个速查表方便你直接对号入座。6.1 头文件相关错误错误信息fatal error: xxx.h: No such file or directory说明编译器找不到头文件。排查思路按顺序走先确认头文件到底在哪个目录再用-I参数指向该目录如果头文件在系统目录下还不存在那可能缺依赖包比如#include mysql.h找不到时需要apt install libmysqlclient-dev。实战案例公司项目里有个同事把头文件添加到include/utils/子目录但Makefile里-I只写了include编译时一堆找不到头文件的报错。他折腾了半天最后发现把-Iinclude改成-Iinclude/utils就行了。有时候问题就这么简单但排查方向对了真的很节省时间。6.2 链接错误undefined reference这是新手最容易懵的错误类型。比如你写了#include stdio.h void foo(void); int main(void) { foo(); return 0; }编译能过链接时报undefined reference to foo。原因很明显foo只有声明没有定义。排查思路是是不是某个函数的声明和定义不一致是不是源文件没有参与编译和链接是不是库没有链接针对C程序还有一个常见情况函数声明和定义的命名空间不匹配。比如在头文件里声明的是namespace A { void foo(); }但实现的时候忘了写namespace A链接阶段就会报undefined reference而你检查代码时怎么看都觉得名字对得上。这是因为C编译器会把命名空间信息编入符号名两边对不上就找不到。6.3 链接错误multiple definition这个错误意味着同一个符号在多个目标文件中都有定义。最常见的场景是你把一个函数的实现直接写在头文件里然后这个头文件被两个以上的源文件包含每个源文件生成的目标文件就都带有一份这个函数的定义链接器一合并就冲突了。正确做法是头文件里放声明源文件里放定义。如果确实需要把inline函数写在头文件里那就用static inline修饰让每个编译单元各自保留一份副本链接时就不会冲突了。6.4 与C相关的特有报错C程序编译报错里频率很高的还有一类是标准库相关的比如undefined reference to std::cout这种。这一般是因为你用了gcc而不是g来链接或者忘记了C标准库的链接选项。解决办法很简单直接用g编译链接全流程。还有一种情况是使用C11以后的新特性但编译时没有指定-stdc11或更高标准导致各种语法错误比如error: nullptr was not declared in this scope。解决方案就是在编译参数里明确指定标准g -stdc11 main.cpp -o main。6.5 运行时崩溃类问题编译通过但运行崩溃这类问题通常和编译参数相关度小和代码逻辑相关度大。但有一种情况值得特别注意用了-marchnative编译的二进制拷到另一台不同型号的CPU机器上运行直接报Illegal instruction (core dumped)。这是因为本机编译时利用了一些新指令集例如AVX2目标机器不支持。解决方法是不要在生产环境用-marchnative或者用-marchx86-64这种比较保守的参数。7. 调试与优化让gcc/g成为你排查问题的利器编译不过只是问题的一半很多时候编译过了、运行结果不对才是更让人头疼的。这个时候编译器的调试信息和一些辅助工具会帮你大忙。7.1 调试信息与GDB的基本配合编译时加-g后就可以用GDB进行源码级调试。最基本的几个命令gcc -g -Wall test.c -o test gdb ./test (gdb) break main # 在main函数下断点 (gdb) run # 运行到断点 (gdb) print n # 打印变量n的值 (gdb) next # 执行下一行不进入函数 (gdb) step # 执行下一行进入函数 (gdb) backtrace # 查看函数调用栈调试整个程序的时候backtrace是我用得最多的命令之一。程序崩溃时它会直接告诉你当前所在的函数和调用链配合print查看关键变量值定位bug比靠猜快得多。7.2 用预处理器和宏控制调试输出一种很实用的做法是用条件编译来控制调试信息的输出#include stdio.h #ifdef DEBUG #define LOG(msg) printf([DEBUG] %s:%d %s\n, __FILE__, __LINE__, msg) #else #define LOG(msg) #endif int main(void) { LOG(entering main); printf(hello\n); return 0; }编译时加上-DDEBUG调试信息就会打印出来发布时去掉-DDEBUG调试输出自动消失不需要改一行代码。这种写法在做底层嵌入式开发时特别常见因为线上日志满天飞会严重影响性能。7.3 用nm、objdump、readelf来解剖二进制这几个工具虽然不直接属于gcc但它们和gcc产物的关系非常紧密。写到这里我得坦白告诉大家我最开始觉得自己把gcc命令用熟了就万事大吉直到有个老同事指着一个链接错误说你拿nm看看这个库里的符号我才意识到自己错过了什么。拿到一个.o文件或可执行文件想快速知道里面定义了哪些符号、引用了哪些外部符号nm命令比IDE和编辑器友好得多。比如nm libfoo.so | grep FunName可以快速确认某个函数是否在一个库中导出。对应的objdump -d可以反汇编二进制文件查看具体的汇编指令。用来分析编译器到底把代码优化成了什么样子非常有趣也很有用。readelf -h能查看ELF文件头信息判断文件架构、入口点等。如果你做嵌入式或者搞逆向、性能分析这三个工具早晚要掌握。8. gcc、g 与编辑器的协作别把IDE和编译器搞混了每次在群聊或者论坛看到有人问编译器是哪个IDE好VS Code没有编译器可以用吗这类问题我都会觉得有必要把编译器与编辑器的概念掰开说清楚。因为这种概念上的混淆会导致你在Linux上用vscode装了插件却编译不出东西白白浪费时间。编译器是一个翻译官它把C/C源码翻译成机器码。编辑器只是一个写字板负责让你编辑代码文本。VS Code、Vim、Emacs这些只是编辑器它们本身不编译任何东西。你在VS Code里装C/C插件实际上插件只是包装了一下gcc/g的调用界面最终干活的还是命令行里那个gcc。正是这个原因很多跨平台开发的老手在Windows上用Visual StudioMSVC写代码到了Linux上照样用gcc/g甚至在Windows上也会用MinGW的gcc替代MSVC编译器比如用VS Code写C时配置的MinGW工具链就是在Windows上调用gcc/g。理解了这一点你换IDE或者换编辑器的时候就不会慌因为编译器的定位和选择在你的脑海里是清晰的。另外一个常见问题是编译器的堆空间不足或者项目大了编译器报internal compiler errorICE。这类问题出现在资源受限的机器上时排查方法一般不是砸键盘而是看看是不是一次性编译的文件太多、并行编译层级开太高导致内存爆了。如果是Makefile里用了-j8甚至-j16可以试试降低并行数或者分步编译。编译器也是程序它也有自己的脾气和资源需求。9. 写在最后一点个人经验和学习建议这些年接触下来gcc/g给我的最大感受可以用一句俗话概括会者不难难者不会。一旦你把基础参数吃透了再去看Makefile、CMakeLists.txt甚至是一些开源项目的构建脚本你会发现它们其实就是对你日常用到的那些gcc/g命令的编排和封装根本没有太多神秘可言。如果你是学生或者刚转行做Linux开发我的建议是先别急着上IDE老老实实花两三天在终端里用gcc/g编译你的课程作业、练习代码。强迫自己经历预处理、编译、汇编、链接的每一步逼自己读一读编译器的报错信息试着用nm和objdump去解剖一个二进制文件。这个过程虽然有些枯燥但等你走完一遍后面再学CMake、交叉编译、性能优化、深入调试都会顺滑得多。我自己现在即使是在图形界面环境里写代码还是经常开着终端手动敲gcc命令做快速验证。快捷键都形成了肌肉记忆。编译速度、报错清晰度、对步骤的掌控感这些命令行工具带给我的确定感是任何花里胡哨的按钮都无法替代的。希望这篇文章能帮你少踩一些我当年踩过的坑也希望你在Linux的终端世界里找到那种一切尽在掌握的自由感。
返回列表