第4章:环境变量与多版本 GCC 切换

编译器说「找不到头文件」、程序说「打不开共享库」、构建系统悄悄用了旧版 GCC——每个报错背后都有真凶,而线索,都藏在一个叫「环境变量」的东西里

🔍

本章导师:柯南

核心方法论:真相只有一个

「案发现场不会说谎。头文件找不到、库加载失败、编译器版本不对——这些报错看起来各有各的说法,但它们指向同一个事实:程序在按它看到的线索找东西,而线索是环境变量给的。别急着改代码,先跟我一起把现场看一遍:变量从哪来、传给谁、什么时候生效。真相只有一个,查清楚了,一个 export 就能破案。」

4.1 环境变量是什么:藏在进程里的全局配置

先给环境变量(environment variable)下个定义,说人话:环境变量就是操作系统给每个进程附带的一块「键值对记事板」——上面写着一行行「名字=值」,比如 PATH=/usr/local/bin:/usr/bin。进程启动时,操作系统把这块记事板原样塞给它;进程里的程序(包括编译器、链接器、动态加载器)随时可以翻开这块记事板,按名字查值,来决定自己该去哪找文件、该用什么配置。这就是它叫「环境」变量的原因:它描述的是这个进程所处的环境,而不是程序内部的某个局部变量。

第二个关键机制是继承:每个新进程都会从启动它的父进程那里复制一份环境变量记事板。你在终端敲一条命令,终端 shell 就把自己那份复制给这条命令对应的进程;你在 C++ 程序里用 system()fork() 启动子进程,子进程同样拿到你这份的副本。注意是「复制」——子进程改了记事板,父进程完全不知道。这一条是理解后面所有「为什么不生效」问题的地基:改环境变量,影响的永远只是「从改动那一刻之后启动的子孙进程」。先学会看这块记事板:

# 查看单个环境变量的值
echo $PATH
# /usr/local/bin:/usr/bin:/usr/local/sbin:/usr/sbin:/home/helios/.local/bin

# printenv 也可以,只打印你要的那个变量
printenv PATH
# 查看全部环境变量(env 和 printenv 等价,set 会连 shell 变量一起列)
env
printenv
set
# 继承演示:子进程复制父进程的记事板
export TRACK="cpp"
bash -c 'echo $TRACK'   # 子 bash 能看到,输出 cpp
exit                        # 回到原来的 shell

对 C++ 开发者来说,环境变量之所以绕不开,是因为整个工具链都在读这块记事板:编译器读它找头文件和库(C_INCLUDE_PATHLIBRARY_PATH),构建系统读它决定用哪个编译器(CCCXX),运行时的动态加载器读它找动态库(LD_LIBRARY_PATH),shell 读它找命令(PATH)。第 2 章我们见识过「库」是别人写好的代码合集,第 3 章亲手编译安装了 GCC——现在这些工具要协同工作了,环境变量就是它们之间传递信息的「公共黑板」。本章就围绕这块黑板展开:先认全变量,再学会让它们按你的意愿生效。

柯南提示

区分两个容易混淆的概念:shell 变量(如 a=1)只在当前 shell 里存在,子进程看不到;环境变量是经过 export 的变量,会被复制给子进程。真相只有一个判断标准:export 没?没 export,子进程一律看不见。

4.2 四大搜索路径变量:编译、链接、运行各管一段

把一个 C++ 源文件变成能跑的程序,要经过三个阶段,每个阶段都要「找文件」,而且找的是不同的文件、用的是不同的变量。先看全貌:编译期,编译器要找到 #include 的头文件;链接期,链接器要找到库文件(.a / .so);运行期,动态加载器要找到动态库(.so)把它装进内存。三个阶段的「找文件」是三个完全不同的程序干的,所以三个阶段的搜索路径也由三组不同的环境变量控制:

# 三阶段全景图(记住这张图,后面排错全靠它)
main.cpp(源代码)
   │
   ▼  ① 编译期:找 #include 的头文件
   │      C_INCLUDE_PATH    (C 头文件,等价于 -I 参数)
   │      CPLUS_INCLUDE_PATH(C++ 头文件,等价于 -I 参数)
main.o(目标文件)
   │
   ▼  ② 链接期:找库文件
   │      LIBRARY_PATH(等价于 -L 参数)
app(可执行文件)
   │
   ▼  ③ 运行期:找动态库 .so
   │      LD_LIBRARY_PATH(动态链接器 ld.so 读取)
./app(跑起来)

逐个认。C_INCLUDE_PATH:C 语言头文件的搜索路径,编译 .c 文件时,#include "xxx.h" 在当前目录找不到时,编译器就会按这个变量列出的目录挨个找。CPLUS_INCLUDE_PATH 是它的 C++ 孪生兄弟,编译 .cpp 文件时生效——注意 C++ 程序普遍用的是它,而不是 C_INCLUDE_PATH。这两个变量解决的是「#include <json/json.h> 报 fatal error: 没有那个文件或目录」这类编译期报错:

# 编译期:让 gcc/g++ 额外去 /opt/include 找头文件(真实配置写法)
export C_INCLUDE_PATH=/opt/include
export CPLUS_INCLUDE_PATH=/opt/include

# 验证:-H 让 gcc 打印头文件搜索过程,看它去哪找了
gcc -H main.c

接下来是链接期与运行期这对「双胞胎」,最容易搞混,也是本章最重要的区分。LIBRARY_PATH链接期的库搜索路径:链接器要把 -ljsoncpp 之类的库名解析成具体的 libjsoncpp.alibjsoncpp.so 文件,就去这个变量列出的目录找——它解决的是「ld: cannot find -ljsoncpp」这类链接期报错。LD_LIBRARY_PATH运行期的动态库搜索路径:可执行文件已经生成,运行那一刻,动态链接器 ld.so(动态链接器,负责在程序启动时把 .so 加载进内存的程序)按它列出的目录找 .so——它解决的是「error while loading shared libraries: libjsoncpp.so: cannot open shared object file」这类运行期报错。一句话记牢:链接期用 LIBRARY_PATH,运行期用 LD_LIBRARY_PATH,一个管「造」一个管「跑」,少一个都不行

# 链接期:让链接器去 /opt/lib 找库文件(libjsoncpp.so / libjsoncpp.a)
export LIBRARY_PATH=/opt/lib

# 运行期:让动态链接器去 /opt/lib 找动态库
export LD_LIBRARY_PATH=/opt/lib

# 多个路径用冒号分隔,按从左到右的顺序搜索
export LD_LIBRARY_PATH=/opt/lib:/usr/local/lib:$LD_LIBRARY_PATH
变量作用阶段找什么谁在读等价命令行参数
C_INCLUDE_PATH编译期C 头文件(.h)gcc-I
CPLUS_INCLUDE_PATH编译期C++ 头文件(.h/.hpp)g++-I
LIBRARY_PATH链接期库文件(.a / .so)链接器 ld-L
LD_LIBRARY_PATH运行期动态库(.so)动态链接器 ld.so-Wl,-rpath
柯南提示

环境变量是「全局兜底」,命令行参数是「精准指定」,而且命令行参数优先级更高:gcc 会先看 -I/-L 参数,再看对应的环境变量。所以排错时先检查 CMakeLists.txt 或 Makefile 里有没有写死 -I/-L——如果有,光改环境变量可能根本不生效,因为命令行参数已经「先声夺人」了。

4.3 CC / CXX / PATH:让构建系统找到「对的」编译器

机器上装了多个 GCC(比如第 3 章源码装到 /usr/local 的 GCC 5.1,和系统自带的 GCC 4.8.5),构建系统该怎么知道你想用哪一个?答案是两个约定俗成的变量:CC(C 编译器路径)和 CXX(C++ 编译器路径)。它们是 C/C++ 构建世界的「老规矩」:make、configure 脚本、CMake 在探测编译器时,都会参考这两个变量。把第 3 章编译安装的 GCC 5.1 指给构建系统,就是这么几行(这也是素材笔记里真实用过的配置):

# 显式指定编译器:/usr/local 下是第 3 章自己编译安装的 GCC 5.1
export CC=/usr/local/bin/gcc
export CXX=/usr/local/bin/g++

# 验证:gcc -v 会打印实际路径(configured with: 上方会显示完整路径)
gcc --version
g++ --version
# CMake 里对应的正规写法:用 -D 指定编译器(比环境变量更明确)
cmake -S . -B build \
      -DCMAKE_C_COMPILER=/usr/local/bin/gcc \
      -DCMAKE_CXX_COMPILER=/usr/local/bin/g++

但这里有个连环问题:你明明 export 了 CXX=/usr/local/bin/g++,为什么在终端敲 g++,跑的还是系统旧版?因为终端找命令靠的是另一个变量——PATH。说人话:PATH 是 shell 的「命令搜索清单」,里面用冒号列了一串目录。你敲 g++ 时,shell 按 PATH 里目录的从左到右顺序逐个找,找到第一个 g++ 就用它。系统默认 PATH 里 /usr/bin 排在前面,而旧版 g++ 恰恰躺在 /usr/bin/g++——所以不管你 export 了什么 CXX,敲命令找的还是它。要验证「真相」:

# 看 PATH 的搜索顺序(哪个目录在前,哪个优先)
echo $PATH
# /usr/local/bin:/usr/bin:...(/usr/local/bin 在前,才会优先用新装的)

# which 告诉你「敲 g++ 实际会执行哪一个」——真相在这里
which g++
# /usr/bin/g++   ← 系统旧版,不是你想用的 /usr/local/bin/g++

# type -a 把 PATH 里所有同名的命令全部列出来
type -a gcc
# gcc is /usr/bin/gcc
# gcc is /usr/local/bin/gcc

所以想让「敲 g++ 就用新版」,得把新版所在目录插到 PATH 最前面:export PATH=/usr/local/bin:$PATH。注意是「插到前面」——放在 $PATH 后面就永远抢不过 /usr/bin。这两个变量(CC/CXX 管「构建系统用谁」,PATH 管「shell 找谁」)各管一摊,第 4.4 节讲完生效机制后,4.5 节会给出完整的多版本共存方案。

柯南提示

CMake 探测编译器的优先级,真相是:缓存/命令行 -DCMAKE_CXX_COMPILER 最高,其次才是环境变量 CXX,最后才是系统默认。所以如果你明明 export 了 CXX,CMake 却还是用旧版——先检查 build 目录的 CMakeCache.txt 里是不是已经缓存了旧编译器路径,删掉 build 目录重新配置(第 2 章的招)往往就好了。

4.4 export 与生效范围:临时生效 vs 永久生效

现在到了「为什么改了不生效」的破案环节。环境变量的修改有两种生命周期。第一种是临时生效:在终端里敲 export FOO=bar,它立刻在当前 shell 生效,并且只对「从这个 shell 启动的子进程」有效——你关掉这个终端窗口,一切归零;同一个终端里另开一个子 shell,继承的也是改动之后的值。这种方式的优点是不污染系统,缺点是换个终端就没了:

# 临时生效:只对当前 shell 及其子进程有效,关终端即失效
export MYLIB=/opt/mylib
echo $MYLIB                    # /opt/mylib

# 新开一个子 shell 试试:能继承到
bash -c 'echo $MYLIB'            # /opt/mylib

# 但!关掉这个终端、重新开一个:$MYLIB 是空的
# 因为新终端是新进程,记事板是全新的

第二种是永久生效:把 export 写进 shell 的启动配置文件,让每个新终端启动时自动执行一遍。bash 的配置文件中,最常用的是 ~/.bashrc(每次打开交互式终端都会读)和 ~/.bash_profile(登录 shell 才读)。写完不会立刻生效——当前这个终端是启动时读的,已经读过了;要让改动落地,要么重开终端,要么手动 source ~/.bashrc(source 的意思就是「把这个文件在当前 shell 里执行一遍」)。「改完不生效」这个新手第一坑,九成是忘了这一步:

# 永久生效第一步:把 export 写进 ~/.bashrc(追加一行)
echo 'export MYLIB=/opt/mylib' >> ~/.bashrc

# 第二步:让当前终端立即生效(或干脆重开一个终端)
source ~/.bashrc
echo $MYLIB                    # /opt/mylib
# 排查「为什么不生效」的标准三连:
# ① 写对文件了吗?(~/.bashrc 只管交互终端)
grep MYLIB ~/.bashrc

# ② source 了吗 / 重开终端了吗?
source ~/.bashrc

# ③ 变量值里有没有引用问题?(单引号不展开 $,双引号会展开)
export MYLIB="$HOME/mylib"   # 双引号:$HOME 会展开成 /home/helios
export MYLIB='$HOME/mylib'   # 单引号:字面量就是 $HOME/mylib

还有两个容易踩的细节。一是追加 vs 覆盖:改 PATH 这类「本来就有一长串」的变量,几乎永远要用 $PATH 把旧值接回来(export PATH=/usr/local/bin:$PATH),直接 export PATH=/usr/local/bin 等于把系统命令全弄丢,连 ls 都找不到了。二是全系统 vs 当前用户:只想给自己用就写 ~/.bashrc;想让机器上所有用户都生效,写 /etc/profile/etc/environment(需要 root 权限,动手前三思)。日常开发,写自己的 ~/.bashrc 就够了。

注意export PATH=/usr/local/bin 这种写法是危险的——它把 PATH 整个覆盖了。改 PATH 必须带上旧值:export PATH=/usr/local/bin:$PATH。万一已经写坏导致 ls、vim 全失效:临时补救用绝对路径执行命令(/usr/bin/ls),然后赶紧把 PATH 改回来。

4.5 多版本 GCC 共存:绝对路径 / 软链接 / SCL 三方案

为什么非要折腾多版本共存?因为 C++11/14/17 的特性(auto、lambda、智能指针……)需要足够新的编译器,而生产服务器(典型如 CentOS 7)自带的 GCC 4.8.5 太老:它对 C++11 的支持不完整(比如 <regex> 长期不可用),更别提 C++14/17。但直接卸载重装系统 GCC 又很危险——yum、内核模块、一堆系统工具都依赖它。真相是:新版本和系统版本完全可以共存,关键是让它们「各就各位、按需切换」。有三种方案,从简到繁:

方案一:直接用绝对路径,零配置零风险。第 3 章源码安装的 GCC 5.1 在 /usr/local/bin/gcc,系统旧版在 /usr/bin/gcc,两者互不干扰。想用新版,编译时直接敲全路径;想让 CMake 用新版,用 -D 指定。这是最干净的做法,不碰任何环境变量——前提是代码里真的用了只有新版才支持的特性,比如下面这段 C++11 的 lambda 与 auto,GCC 4.8.5 编起来就未必顺畅:

// lambda.cpp:C++11 特性演示(GCC 5.1 完整支持)
#include <vector>
#include <algorithm>
#include <iostream>
int main() {
  std::vector<int> nums = {3, 1, 4, 1, 5};
  auto cnt = std::count_if(nums.begin(), nums.end(),
                          [](int x) { return x > 2; });
  std::cout << "大于 2 的个数: " << cnt << std::endl;
  return 0;
}
# 方案一:绝对路径,指哪打哪(第 3 章装好的 GCC 5.1 完整支持 C++11)
/usr/local/bin/g++ -std=c++11 lambda.cpp -o app
./app
# 大于 2 的个数: 3

# CMake 工程:-D 指定编译器,效果等同 CC/CXX 但更明确
cmake -S . -B build \
      -DCMAKE_C_COMPILER=/usr/local/bin/gcc \
      -DCMAKE_CXX_COMPILER=/usr/local/bin/g++

方案二:软链接切换,让「默认」变成新版。软链接(symlink)就是一个指向别的文件的「快捷方式」——把 /usr/bin/g++ 这个入口重新指到新版,那么所有不带路径敲 g++ 的地方都用新版。用 ln -sf 创建/覆盖软链接(-s 表示软链接,-f 表示已存在就覆盖)。但强烈建议先把旧版备份,并且想清楚:改的是系统公共入口,yum 等系统组件可能因此出问题。RHEL/CentOS 还提供 alternatives 机制(Debian/Ubuntu 对应 update-alternatives)做受管理的切换,比裸 ln 安全,但 CentOS 7 的 gcc 默认不在它的管理列表里,需要先注册——日常个人开发,用下面的手动方式就够:

# 方案二:软链接切换(动手前先备份旧版!)
sudo mv /usr/bin/gcc /usr/bin/gcc.orig      # 备份
sudo mv /usr/bin/g++ /usr/bin/g++.orig
sudo ln -sf /usr/local/bin/gcc /usr/bin/gcc  # 让系统默认指向新版
sudo ln -sf /usr/local/bin/g++ /usr/bin/g++

# 验证:which 和 --version 都指向新版才算成功
which g++ && g++ --version
# 想切回去?把备份还原即可
sudo mv /usr/bin/gcc.orig /usr/bin/gcc
sudo mv /usr/bin/g++.orig /usr/bin/g++

方案三:SCL 软件集(Software Collections)——Red Hat 官方的「环境隔离」方案,也是素材笔记里真实走过的流程。SCL 的思路和方案一、二都不同:它把新版工具链装在独立的目录(/opt/rh/devtoolset-7/)里,然后通过 scl enable 命令临时改环境变量(主要是 PATH 和 LD_LIBRARY_PATH),让「当前这个 shell 及其子进程」看到新版——退出即恢复,系统完全不被污染。这正是 4.1 节「环境变量只影响子孙进程」机制的绝佳应用。完整流程(CentOS 7 实测):

# 方案三:SCL 软件集升级 GCC(CentOS 7 实测流程)
# ① 安装 SCL 软件源(centos-release-scl 提供 sclo-rh 仓库)
yum install centos-release-scl scl-utils-build

# ② 列出可用的 devtoolset 集合
yum list all --enablerepo='centos-sclo-rh' | grep devtoolset
# ③ 安装 devtoolset-7 的 gcc 与 g++(GCC 7,完整支持 C++11/14)
yum install devtoolset-7-gcc devtoolset-7-gcc-c++

# ④ 切换:进入 devtoolset-7 环境(仅对当前环境生效)
scl enable devtoolset-7 bash
gcc -v    # 此刻是 GCC 7.x

# ⑤ 退出环境,回到系统默认
exit
gcc -v    # 又变回 4.8.5,系统毫发无损

scl enable devtoolset-7 bash 这一行值得拆开看:它做了三件事——在 PATH 最前面插入 /opt/rh/devtoolset-7/root/usr/bin(所以敲 gcc 就是新版)、设置对应的 LD_LIBRARY_PATH(新版 g++ 的动态库也在那)、然后启动一个新的 bash。你在新 bash 里编译、运行,用的是 GCC 7;敲 exit 退出这个 bash,一切还原。想一劳永逸也可以把 source /opt/rh/devtoolset-7/enable 写进 ~/.bashrc——但那就失去「按需切换」的意义了,日常推荐还是临时 enable。三方案对比:

方案原理影响范围适合场景
① 绝对路径直接调用 /usr/local/bin 下的新版仅当前命令单文件编译、临时验证,最安全
② 软链接把 /usr/bin/g++ 入口指向新版全系统默认确定让所有构建都用新版(有风险)
③ SCLscl enable 临时改 PATH 等变量当前 shell 会话多版本按需切换,Red Hat 系推荐
柯南提示

SCL 的 devtoolset 不止 7 一个版本:devtoolset-6、devtoolset-7、devtoolset-8、devtoolset-9、devtoolset-10、devtoolset-11 都有(yum list all --enablerepo='centos-sclo-rh' | grep devtoolset 能看到全部)。想要 C++17 的 std::filesystem,选 devtoolset-8 及以上;CentOS 7 上最高可用的基本是 devtoolset-11(GCC 11)。选版本的原则只有一个:够用就行,别追新。

4.6 排错实战:三个经典案发现场

案发现场一:改了环境变量,程序还是老样子。破案思路(4.4 节已铺垫):先确认改的是不是「当前 shell 正在用的那份记事板」——export 完立刻在当前终端测试没问题,但 IDE、构建服务、新终端可能读的是另一份配置;再确认写对了文件(~/.bashrc vs ~/.bash_profile);最后用 which/type -a 验证「实际用的是哪个」而不是「我以为用的是哪个」。真相只有一个判断标准:让程序把它看到的路径打出来——echo $PATHgcc -v、CMake 配置输出里的 The CXX compiler identification is ...,全是破案线索:

# 案发现场一的标准取证流程
echo $PATH          # PATH 顺序对吗?新版目录在 /usr/bin 前面吗?
which gcc            # 敲 gcc 实际执行谁?
type -a gcc          # 所有候选都在哪?
gcc -v 2>&1 | grep -i configure   # 编译器真实路径在 configured with 上方

案发现场二(C++ 新手必踩):编译链接全通过,运行时报「cannot open shared object file」。典型场景:用了 -ljsoncpp 链接成功(说明 LIBRARY_PATH 或 -L 生效了),生成 ./app,一运行却报 error while loading shared libraries: libjsoncpp.so.25: cannot open shared object file: No such file or directory。为什么?因为链接期和运行期是两个程序、两份配置:链接器知道库在哪(链接期变量管着),但运行时的动态链接器 ld.so 不知道(它只认 LD_LIBRARY_PATH 等运行期配置)。修法:把动态库目录补进 LD_LIBRARY_PATH,或者编译时用 -Wl,-rpath 把路径「焊死」进可执行文件;用 ldd ./app 看可执行文件到底缺哪些 .so:

# 案发现场二:链接成功、运行失败
./app
# error while loading shared libraries: libjsoncpp.so.25:
# cannot open shared object file: No such file or directory

# 取证:ldd 列出 ./app 需要的全部动态库,带 not found 的就是凶手
ldd ./app
# libjsoncpp.so.25 => not found   ← 就是它

# 破案:告诉动态链接器库在哪
export LD_LIBRARY_PATH=/opt/lib:$LD_LIBRARY_PATH
./app   # 正常了
# 另一个解法:编译时用 -Wl,-rpath 把库路径写进可执行文件
# 这样不用设 LD_LIBRARY_PATH 也能跑(路径跟随程序走)
g++ main.cpp -L/opt/lib -ljsoncpp -Wl,-rpath,/opt/lib -o app

案发现场三:PATH 顺序导致的「幽灵版本」。明明装了新版,gcc --version 却还是旧版——十有八九是 PATH 里旧版目录排在前面(/usr/bin 在前,/usr/local/bin 在后),shell 按顺序先撞上了旧版。破案用 which(只报第一个命中)和 type -a(全列出来);修复就是 4.4 节那招:export PATH=/usr/local/bin:$PATH,新版目录插到最前面。另外提醒一句:改了 PATH 后,已经在跑的进程(比如 IDE 的构建任务、后台服务)不会自动感知——它们的环境在启动那一刻就固定了,必须重启它们,这是 4.1 节「继承」机制的必然推论:

# 案发现场三:PATH 顺序不对,新版永远轮不到
echo $PATH
# /usr/bin:/usr/local/bin:...  ← /usr/bin 在前,旧版 gcc 抢先了

# 修复:把新版目录插到最前面
export PATH=/usr/local/bin:$PATH
which gcc    # 现在指向 /usr/local/bin/gcc

把三起案件串起来,你会发现柯南的老话依然成立:每个报错背后都有一条真实的「查找路径」,环境变量决定了这条路径长什么样。编译期查 echo $CPLUS_INCLUDE_PATH、链接期查 echo $LIBRARY_PATH、运行期查 ldd ./app + echo $LD_LIBRARY_PATH、编译器版本查 which gcc + gcc -v——按图索骥,没有破不了的案。下一章我们带着这套「按图索骥」的功夫去学 vcpkg,你会发现包管理工具干的事,本质上就是替你把这张「查找路径图」画好、配齐。

注意LD_LIBRARY_PATH 是排查利器,但别滥用——它影响的是所有程序(包括系统命令)的动态库查找,设得太宽可能让某个程序加载到错误的 .so 版本(经典事故:设了指向旧版库的 LD_LIBRARY_PATH,导致 ls、vim 全部报错)。优先用 -Wl,-rpath 把路径写进自己的可执行文件;LD_LIBRARY_PATH 留给临时验证和确实需要的场景。

章末练习

练习 1:概念四连 入门

用一句话分别解释:环境变量PATHexportSCL(Software Collections)

提示

四个概念对应「记事板 / 搜索清单 / 传递开关 / 隔离箱子」四个角色。

参考答案

环境变量:操作系统给每个进程附带的键值对「记事板」,程序按名字查值来决定去哪找文件、用什么配置;PATH:shell 的「命令搜索清单」,按冒号分隔的目录顺序找命令,先到先得;export:把 shell 变量标记为可继承,让子进程也能看到它;SCL:Red Hat 的软件集机制,把新版工具链装在独立目录,用 scl enable 临时改环境变量实现按需切换,退出即还原。

练习 2:阶段配对 入门

把四个变量 C_INCLUDE_PATHCPLUS_INCLUDE_PATHLIBRARY_PATHLD_LIBRARY_PATH 分别配到正确的阶段:编译期找头文件(C)/ 编译期找头文件(C++)/ 链接期找库 / 运行期找动态库。

提示

回想 4.2 节的三阶段全景图:头文件 → 目标文件 → 可执行文件 → 跑起来,每个箭头对应一个变量。

参考答案

C_INCLUDE_PATH → 编译期找 C 头文件;CPLUS_INCLUDE_PATH → 编译期找 C++ 头文件;LIBRARY_PATH → 链接期找库文件;LD_LIBRARY_PATH → 运行期找动态库。记忆口诀:INCLUDE 管编译、LIBRARY 管链接、LD_LIBRARY 管运行(LD 指动态链接器 ld.so)。

练习 3:场景修复 进阶

你把 jsoncpp 的动态库装到了 /opt/lib,头文件在 /opt/include。依次遇到三个报错,请分别写出用环境变量的修法:(a) 编译报 fatal error: json/json.h: No such file or directory;(b) 链接报 cannot find -ljsoncpp;(c) 编译链接都过了,运行 ./appcannot open shared object file

提示

(a) 是编译期头文件问题;(b) 是链接期库问题;(c) 是运行期动态库问题——三个不同阶段的变量,别用混。

参考答案

(a) export CPLUS_INCLUDE_PATH=/opt/include(C++ 程序用 CPLUS_INCLUDE_PATH,C 程序用 C_INCLUDE_PATH);(b) export LIBRARY_PATH=/opt/lib;(c) export LD_LIBRARY_PATH=/opt/lib。注意 (c) 不能靠前两个变量解决——链接器和动态链接器各看各的,这是 4.6 节案发现场二的核心教训。

练习 4:SCL 实战切换 挑战

在 CentOS 7(或任何有 yum 的 RHEL 系)上实操:① yum install centos-release-scl scl-utils-build 装源;② 用 yum list all --enablerepo='centos-sclo-rh' | grep devtoolset 看看有哪些版本;③ 装 devtoolset-7-gcc devtoolset-7-gcc-c++;④ scl enable devtoolset-7 bash 后写一个用 lambda + auto 的 C++11 程序编译运行;⑤ exit 退出后再编译同一个程序,观察报错——记录 ④⑤ 两个环境下 gcc -v 的输出差异,并解释为什么同一个源码在两个环境里命运不同。

提示

④⑤ 的差异根源在 PATH:scl enable/opt/rh/devtoolset-7/root/usr/bin 插到了 PATH 最前面(用 echo $PATH 对比两个环境验证);旧 GCC 4.8.5 对 C++11 支持不完整,lambda 的某些写法可能直接编不过。

参考答案

④ 环境里 gcc -v 显示 GCC 7.x(完整支持 C++11),lambda/auto 程序编译运行正常;⑤ exit 后 PATH 还原,gcc -v 回到 4.8.5,同一份源码可能报 C++11 特性不支持(比如 <regex> 相关代码)或能编但警告。本质:SCL 不装不卸任何东西,只通过环境变量(PATH/LD_LIBRARY_PATH)改变「当前 shell 看到的编译器」——这正是 4.1 节「环境变量只影响子孙进程」机制的实战印证。若机器上没有 yum,可改用方案一(绝对路径 /usr/local/bin/g++)做同样的对比实验。