Linux系统编程:(十三)环境变量

1. 命令行参数

在 C 语言中,main 函数不仅可以不带参数运行,还能通过命令行接收外部传入的数据。这种机制叫做命令行参数 ,它让我们的程序可以在启动时根据不同的输入表现出不同的行为。比如 ls -lgcc main.c这些命令,后面的内容就是通过命令行参数传递给程序的。

那么,这些参数是如何在程序内部被接收和处理的呢?在 C 语言中,这通常是通过 argc(参数个数计数器)和 argv(参数字符串数组)来实现的。为了直观地理解它们的工作原理,我们可以先看一个简单的演示程序:它通过循环遍历并打印出所有接收到的命令行参数,你可以观察当我们在终端分别执行 ./code./code a./code a b时,程序是如何识别并输出这些参数的。

cpp 复制代码
  1 #include<stdio.h>
  2 
  3 
  4 //main函数有参数吗?
  5 int main(int argc,char *argv[])
  6 {
  7   for(int i = 0;i<argc;i++)
  8   {
  9     printf("argv[%d]:%s\n",i,argv[i]);                                                                                                                                                                       
 10   }
 11   return 0;
 12 }

对上面程序编译运行后在命令行的输出结果如下:

理解了命令行参数的基本接收方式后,我们自然会想到:如何让程序根据不同的参数执行不同的操作?这就像是给程序配备了一个简单的"控制面板"。

在实际开发中,我们通常希望用户通过选项(如 -a-b-c)来告诉程序要做什么,而不是让它盲目运行。因此,接收到这些参数后,程序还需要进行解析和判断 。下面的例子展示了如何通过 strcmp来识别具体的选项,并根据用户的输入执行对应的功能。同时,为了让程序更健壮,它还会检查参数的数量是否符合预期------如果不对,就打印出用法提示(Usage),引导用户正确使用。

cpp 复制代码
  1 #include<stdio.h>
  2 #include<string.h>
  3 
  4 //main函数有参数吗?
  5 int main(int argc,char *argv[])
  6 {
  7   if(argc!=2)
  8   {
  9     printf("Usage: %s [-a|-b|-c]\n",argv[0]);
 10     return 1;                                                                                                                                                                                                
 11   }
 12 
 13   const char *arg = argv[1];
 14   if(strcmp(arg,"-a")==0)
 15     printf("这是功能1\n");
 16   else if(strcmp(arg,"-b")==0)
 17     printf("这是功能2\n");
 18   else if(strcmp(arg,"-c")==0)
 19     printf("这是功能3\n");
 20   else
 21     printf("Usage: %s [-a|-b|-c]\n",argv[0]);
 22   return 0;
 23 }

对上面程序编译运行后在命令行的输出结果如下:

通过这两个示例,我们可以清晰地看到命令行参数的完整工作脉络:

1. 基本机制:argc 与 argv

main函数的参数 int argcchar *argv[]是程序与操作系统之间的"桥梁"。argc负责记录用户输入的参数总个数(包含程序本身的路径),而 argv则是一个字符串数组,依次存储着每一个参数的具体内容。正如第一个程序所示,通过遍历 argv,程序能够"看见"用户在命令行中输入的所有内容。

2. 实用开发:解析与校验

仅仅接收参数还不够,更重要的是对这些参数进行解析校验 。第二个程序展示了实际开发中的常见模式:首先检查 argc确保参数数量符合要求(如必须恰好是 2 个),如果不符合就打印 Usage 提示;然后使用 strcmp比对 argv中的具体字符串,根据用户的选项(如 -a-b)分支执行不同的功能。

总结来说 ,命令行参数为程序提供了灵活的运行时交互能力,它让程序不再是"硬编码"的执行流,而是可以根据外部指令动态调整行为的工具。掌握 argc/argv的使用以及参数解析的基本逻辑,是编写实用、健壮的命令行工具的基础。

2. 环境变量

从上面的例子可以看出,当我们运行自己编写的程序时,必须在可执行文件名前加上 ./,以明确指定程序位于当前目录;但执行系统自带的命令时却无需如此。这背后的关键就在于环境变量 的存在。环境变量可以帮助系统找到目标二进制文件!

如果想让我们自己的可执行程序在运行时前面不带./,只需要以root的权限将二进制文件(可执行文件)拷贝到/usr/bin/路径下面,便可以实现。

使用cp指令:

bash 复制代码
sudo cp [二进制/可执行文件名称] /usr/bin/

演示:

但是注意:强烈不推荐将自己写的文件拷贝到系统的/usr/bin/路径下!

Linux系统中存在一个环境变量:PATH----系统中搜索指令的默认搜索路径

在Linux中,如果要查看所有环境变量,使用env指令:

bash 复制代码
env

env的构成:名称+内容

如果要查看特定环境变量的内容,比如PATH,则使用如下指令:

bash 复制代码
echo $PATH

示例输出结果如下:

bash 复制代码
[cyq@VM-0-5-centos lesson15]$ echo $PATH
/usr/local/bin:/usr/bin:/usr/local/sbin:/usr/sbin:/home/cyq/.local/bin:/home/cyq/bin

当我们执行一条命令时,系统会先读取环境变量 PATHPATH中包含多个路径,各路径之间以冒号 :分隔。系统会按照这些路径的顺序逐一查找对应的可执行文件。如果在所有路径中都未找到,则会在命令行中返回"命令未找到"的错误提示。

当然,除了前面提到的将二进制文件拷贝到对应路径下之外,也可以将二进制文件当前所处的路径添加到PATH环境变量中,指令如下:

bash 复制代码
PATH=$PATH:[二进制文件所在的当前路径]

演示如下:

当退出xshell后,系统会自动清除我们往PATH中添加的路径。

2.1 从存储空间的角度理解环境变量

当你打开一个终端,背后其实跑着一个 bash进程。每当你敲下一行命令并回车,bash并不会直接跑去硬盘上翻文件,而是先做一件很重要的事:它给你要运行的命令准备了两张表

第一张叫命令行参数表 ,里面存着你敲的那些东西,比如 ls -l /home,拆成 ["ls", "-l", "/home"]。第二张叫环境变量表 ,里面存着一堆 KEY=VALUE的字符串 ,比如 PATH=/usr/local/bin:/usr/binHOME=/root等等。这两张表都保存在 bash进程自己的内存里。接下来,bash会创建一个新的子进程(就是你即将运行的那个程序),然后把这两张表的副本 一股脑儿塞给这个子进程。相当于 bash对子进程说:"喏,这是你要的参数,这是当前的环境配置,拿去吧。"所以从存储的角度看,环境变量其实就是一块存放在进程内存里的字符串数组 ,每个进程启动时都会从父进程那里继承一份完整的副本。你可以在程序里随意修改自己的那份,但不会影响父进程或其他进程------就像每个人手里都有一本独立的通讯录,你改自己的那本,别人手里的还是原来的样子。而 PATH只是这本通讯录里的一条记录罢了。当你运行一个不带路径的命令(比如 ls),系统就会翻开这条记录,按里面的目录顺序挨个去找有没有叫 ls的可执行文件。找到了就执行,找不到就报错。

简单总结:环境变量就是进程启动时随身携带的一份"配置小抄",存在自己的内存里,随进程生灭,靠继承传递。

2.2 环境变量从哪里来?(配置与持久化)

我们在终端中能直接使用的命令,之所以不需要输入完整路径,是因为系统有一个名为 PATH的环境变量,它像一张"快捷方式清单",告诉系统去哪些目录里寻找可执行程序。

1. 配置文件的加载

这张"清单"并不是凭空产生的,而是由系统在启动终端(Shell)时,自动从用户主目录下的隐藏配置文件中读取并生成的。关键的配置文件主要有两个:

  • .bashrc
  • .bash_profile

你可以输入以下命令进入主目录并查看这两个文件:

bash 复制代码
cd ~
ls -al

输出结果如下:

2. 如何让自定义程序全局可用?

默认情况下,系统只会在预设的路径中寻找命令。如果我们想让自己写的可执行程序也能像系统命令一样直接输入名字就能运行,就需要将程序所在的目录路径永久添加到 PATH变量中

操作步骤:

  1. 使用 vim编辑器打开 .bash_profile文件:

    bash 复制代码
    vim .bash_profile
  2. 在文件末尾找到或添加 PATH变量的配置行。例如,假设你的程序放在 /home/whb/code/merge_class/lesson15目录下,你需要将该路径追加到原有变量值的末尾:

    bash 复制代码
    PATH=$PATH:$HOME/.local/bin:$HOME/bin:/home/whb/code/merge_class/lesson15
  3. 保存并退出编辑器。

演示图片:

原始配置文件:

往配置文件中添加新路径:

3. 使配置生效

为了让刚才的修改立即生效,你需要关闭当前的 Xshell 终端并重新打开。再次输入以下命令,你会发现输出结果中已经包含了新添加的路径:

bash 复制代码
echo $PATH

输出结果:

注: ​ 建议将修改后的路径通过 export命令导出,以确保它对当前 Shell 会话及其后续启动的所有子进程生效。具体做法是在 .bash_profile.bashrc中添加如下一行(也就是在原来的基础上在前面加上一个词:export):

bash 复制代码
export PATH=$PATH:$HOME/.local/bin:$HOME/bin:/home/whb/code/merge_class/lesson15

export的作用是把这个变量标记为"环境变量",明确告诉 Shell:"这个变量不仅要我自己知道,还要传给以后启动的所有子进程。" 这样,任何在你终端里运行的程序都能读到这个新的 PATH。不过对于 PATH这个变量来说,大部分 Shell(包括 bash)其实默认就已经把它 export 了,所以你直接在配置文件里写 PATH=$PATH:xxx而不加 export,通常也是可以的。这个注更多是一种保险写法。

4. 为什么要配置环境变量?

通过这个操作我们就能明白,为什么在安装 Python 或其他开发软件时,安装向导总会提示"配置环境变量"了。

配置环境变量的核心目的,就是省去每次执行程序时都要手动输入完整路径(如 ./)的麻烦,让系统能够自动识别并执行我们指定的程序。

2.3 更多的环境变量

2.3.1 HOME

PATH,系统中还有许多预定义的环境变量,它们就像是系统的"全局字典"。其中最重要的一位是 HOME

HOME是一个指向**当前用户"家目录"**的绝对路径变量。在 Linux 系统中,每个用户都有一个专属的私人空间,用于存放个人文件和配置文件。

你可以通过 $符号加变量名来读取它的值。正如下图演示输入 echo $HOME所看到的,系统返回了 /home/cyq

注意:这里的 cyq正是当前登录的用户名,Linux 默认会在 /home/目录下为每个用户创建一个同名文件夹作为家目录。

为什么它很有用?

HOME变量的核心价值在于**"免硬编码"** 。当你编写 Shell 脚本或配置环境变量(例如上一节提到的 .bash_profile)时,如果直接写死绝对路径(如 /home/whb/code/...),一旦换一台电脑或用户名变了,脚本就会失效。

最佳实践 是引用 $HOME,写成 $HOME/code/...。这样无论在哪台机器上运行,系统都会自动把它解析为当前用户的真实家目录路径,极大地增强了脚本的通用性和可移植性。

2.3.2 SHELL

你可能听说过"Shell"这个词,它其实就是我们常说的"终端"、"命令行"。但严格来说,Shell 是一个程序 ,它负责接收你敲进去的命令,然后交给操作系统去执行。而 SHELL这个环境变量,就记录了当前你正在使用的是哪一个 Shell 程序

1. 怎么查看?

bash 复制代码
echo $SHELL

在我的机器上,它会输出:

bash 复制代码
/bin/bash

这说明我现在用的是 Bash(Bourne Again SHell),这是 Linux 上最常用的 Shell 之一。

2. 常见的 Shell 有哪些?

  • /bin/bash:Bash,最常见,几乎所有的 Linux 发行版默认都用它。

  • /bin/zsh:Zsh,功能更强大,macOS Catalina 之后默认改用 Zsh。

  • /bin/sh :最原始的 Bourne Shell,很多脚本为了兼容性会指定用 #!/bin/sh

  • /bin/fish:Fish,号称"友好交互式 Shell",开箱即用,语法更现代。

3. 为什么要知道 SHELL变量?

因为不同的 Shell 语法略有不同!举个例子:

  • 在 Bash 里,数组的定义是 arr=(1 2 3)

  • 在 Zsh 里,数组下标从 1 开始,而 Bash 从 0 开始。

  • 有些 Shell 不支持某些高级特性(比如 [[ ]]条件判断)。

如果你写了一个脚本,开头写了 #!/bin/bash,但你的 SHELL环境变量显示的是 /bin/zsh,那你直接在终端里运行脚本可能会遇到奇怪的问题。SHELL变量告诉你当前交互式环境是哪个壳,方便你调试或选择正确的语法。

4. 如何临时切换 Shell?

假如你想试试 Zsh,但又不想永久改变,可以直接在终端输入:

bash 复制代码
zsh

此时你会进入 Zsh 环境,再执行 echo $SHELL,你会发现它依然显示 /bin/bash------ 因为 $SHELL记录的是登录时的默认 Shell,而不是当前正在跑的 Shell。要查看当前真正的 Shell 进程,可以用:

bash 复制代码
ps -p $$

5. 如何永久更换默认 Shell?

如果你想彻底换成 Zsh,可以使用 chsh命令(change shell):

bash 复制代码
chsh -s /bin/zsh

然后退出终端重新登录,再 echo $SHELL就会发现变成了 /bin/zsh

2.3.3 USER和LOGNAME

在终端里,有两个环境变量都用来记录"当前用户是谁":USERLOGNAME。你可能会好奇,既然都是用户名,为什么要有两个?

1. 先看看它们长什么样

bash 复制代码
echo $USER
echo $LOGNAME

在我的机器上,输出都是:

bash 复制代码
cyq
cyq

看起来一模一样。绝大多数情况下,它们俩的值是完全相同的。

2. 它们的区别在哪里?

虽然结果相同,但来源不同

  • USER :最早来自 BSD Unix 系统,后来被广泛采用。它是一个非标准的、但约定俗成的环境变量。很多程序依赖它来判断当前用户。

  • LOGNAME :来自 POSIX 标准(IEEE 1003.1),是官方规定的标准环境变量 。理论上,符合 POSIX 的系统都应该设置 LOGNAME

简单说:LOGNAME是"正规军",USER是"民间常用"。但在 Linux 中,两者都会被正确设置,所以平时你用哪个都一样。

3. 什么情况下它们会不一样?

虽然很少见,但在某些特殊场景下,它们可能不一致:

  • 当你通过 su切换到其他用户时,如果没有使用 su -(带连字符),USER可能不会更新,而 LOGNAME通常会更新。

  • 某些老旧系统或特定环境下,可能只设置了其中一个。

举个例子:

bash 复制代码
# 先看当前用户
echo "USER=$USER, LOGNAME=$LOGNAME"
# 输出:USER=cyq, LOGNAME=whb

# 切换到 root(不带 -)
su root
# 输入密码后
echo "USER=$USER, LOGNAME=$LOGNAME"
# 可能输出:USER=cyq, LOGNAME=root

看到了吧?USER还停留在原来的用户,而 LOGNAME已经变成了 root。这是因为 su没有模拟完整登录环境,只更新了部分变量。

  1. 写脚本时应该用哪个?

推荐使用 LOGNAME ,因为它更符合标准,行为更稳定。但如果你写的是跨平台脚本(比如 macOS 和 Linux 都跑),USER的兼容性更好(macOS 默认也设 USER)。最稳妥的做法是:两者都检查 ,或者直接用 whoami命令获取当前有效用户名。

bash 复制代码
current_user=$(whoami)
echo "当前用户是:$current_user"

whoami会查询当前进程的有效用户 ID,比环境变量更可靠。

5. 它们和 HOME的关系

还记得之前说的 HOME吗?HOME通常也是根据用户名来的:/home/$USER/home/$LOGNAME。但记住,直接使用 $HOME才是王道,不要自己去拼接路径。

小结

USERLOGNAME就像你的身份证号和护照号------正常情况下都指向同一个人,但偶尔会有细微差别。日常使用随便选一个就行,但如果写严肃的脚本,建议用 LOGNAMEwhoami,免得在边界情况翻车。

2.3.4 HISTSIZE

如果你发现终端按上箭头 翻历史时,总是只能翻到最近一小段、或者反过来历史长到离谱(甚至怀疑隐私问题),多半就是 HISTSIZE(以及它的兄弟 HISTFILESIZE)在起作用。

先说本质:HISTSIZE是 Bash 用来限制"当前 Shell 会话里保留多少条命令历史"的阈值 。它影响你能通过 history/ !<编号>/ Ctrl+r检索到的条目数量。


1) HISTSIZE到底管什么?

在 Bash 里,你敲过的命令会被维护成一个历史列表(内存里) ;退出/某些触发条件下,Bash 会把这部分历史写到磁盘文件(默认是 ~/.bash_history)。

  • HISTSIZE:控制"内存中的命令历史列表"最多能存多少条。

    • 达到上限后,最早的条目会被挤掉(先进先出)。
  • HISTFILE :告诉 Bash "历史要落到哪个文件"(默认一般是 ~/.bash_history)。

  • HISTFILESIZE:控制"历史文件(磁盘)"最大保留多少条(Bash 在读写时会裁剪文件)。

一句话区分(很好记):

  • HISTSIZE= 你现在这个 shell 里"能记住多少条"

  • HISTFILESIZE= 磁盘历史文件"最多留多少条"

2) 怎么看?怎么改?

bash 复制代码
# 看看当前值(常见默认往往是 500 或 1000,取决于发行版)
echo "$HISTSIZE"

# 看看历史文件在哪
echo "$HISTFILE"

# 看你当前历史有多少条(不一定等于 HISTSIZE,只是"现在长度")
history | wc -l

临时改(只对当前 shell 生效):

bash 复制代码
HISTSIZE=2000
# 顺手也把磁盘那份容量拉开,避免"能记 2000 但文件只留 500"
HISTFILESIZE=2000

想要永久生效 ,一般写进你的 Bash 配置里(交互式登录/非登录都会读到的最佳落点是 ~/.bashrc):

bash 复制代码
# ~/.bashrc
export HISTSIZE=2000
export HISTFILESIZE=2000

# 顺手把时间打出来(可选但非常推荐)
export HISTTIMEFORMAT="%F %T "

# 避免重复/无关命令把历史撑满(可选但很实用)
export HISTCONTROL=ignoredups:erasedups

改完让它立刻生效:

bash 复制代码
source ~/.bashrc
# 或关掉终端重开一个新终端(更稳)

3) 一个最容易踩的"误解坑"

很多人以为:HISTSIZE设很大 = 历史文件里永远有那么多条

严格说:不一定 。因为历史文件还受 HISTFILESIZE裁剪;而且不同终端窗口各自有自己一份历史列表,默认情况下它们"覆盖式写回",可能产生互相覆盖/丢历史的现象。

如果你希望多个终端窗口的历史尽量不互相覆盖,常见博客级解法是让 Bash 每次提示符刷新时追加写入(而不是只在退出时覆盖):

bash 复制代码
# ~/.bashrc
shopt -s histappend
PROMPT_COMMAND="history -a; $PROMPT_COMMAND"

这里不多展开副作用,但你至少要知道:HISTSIZE本身不解决"多窗口合并历史",它只解决"单会话容量上限"。

小结

HISTSIZE看起来像"环境变量",但它本质是 Bash 的会话状态变量 :决定你的命令历史列表能长到哪儿;真想管好"历史能回溯多远、磁盘里留多少、是否带时间戳、是否去重",通常需要把它和 HISTFILE / HISTFILESIZE / HISTTIMEFORMAT / HISTCONTROL一起配套配置。

2.3.5 PWD和OLDPWD

当用户在终端中执行 pwd命令时,系统会返回当前工作目录的路径。实际上,这一信息早已被存储在环境变量 PWD ​ 之中。与之对应,环境变量 OLDPWD ​ 则记录了用户上一次访问的目录路径

1. 先看看它们长什么样

bash 复制代码
echo $PWD
echo $OLDPWD

假设你先在 /home/whb,然后 cd /tmp,那么:

  • $PWD输出 /tmp

  • $OLDPWD输出 /home/whb

PWD代表 Present Working Directory(当前工作目录),OLDPWD代表 Old Present Working Directory(上一次的工作目录)。每次你 cd切换目录时,Bash 会自动更新这两个变量。

2. 它们有什么用?

场景一:快速来回切换

你肯定见过这个命令:

bash 复制代码
cd -

这条命令的作用是切换到上一个目录 。它的原理是什么?其实就是读取 $OLDPWD。当你执行 cd -时,Bash 会执行 cd "$OLDPWD",然后自动交换 PWDOLDPWD的值。所以你再执行一次 cd -,又会回到原来的目录。这在两个目录之间频繁切换时特别方便,比如编译源码和查看文档之间来回跳转。

场景二:脚本里记录路径

如果你写一个脚本,需要在某个目录执行操作,然后回到原来的位置,可以这样:

bash 复制代码
# 记住当前目录
original_dir=$PWD

# 切换到目标目录
cd /some/target/dir
# ... 做一些操作 ...

# 回到原来的目录
cd "$original_dir"

当然你也可以直接用 cd -,但手动保存 $PWD更灵活,因为你可以多次切换后还能回到最初的起点。

3. PWDpwd命令的区别

$PWD是环境变量,由 Shell 维护。而 pwd是一个外部命令(也可能是 Shell 内置命令),它从内核获取当前目录。绝大多数情况下两者一致,但有一种例外:如果你用 cd进入了一个符号链接目录,$PWD会显示符号链接的路径,而 pwd -P会显示真实的物理路径。例如:

bash 复制代码
# 假设 /link 是指向 /real/path 的软链接
cd /link
echo $PWD       # 输出 /link
pwd -P          # 输出 /real/path

所以如果你在意路径的真实性,可以用 pwd -P而非 $PWD

4. OLDPWD的注意事项

  • 如果你刚打开终端,还没有 cd过,$OLDPWD是空的。

  • 某些情况下(比如用 pushd/ popd),$OLDPWD的行为可能和预期略有不同,但基本遵循"上次 cd 去的目录"这一原则。

小结

PWD告诉你现在在哪,OLDPWD告诉你刚才 在哪。而 cd -这个看似简单的快捷键,背后就是 OLDPWD在默默发挥作用。下次你用 cd -在两个目录间反复横跳时,记得感谢这对好兄弟。

至于其他环境变量,这里就不再逐一展开了,感兴趣的朋友可以自行查阅了解。

3. 获得环境变量的方法

在介绍获取环境变量方法之前,补充一个知识点:在命令行中添加/消除环境变量的指令:

1. 增加/修改环境变量:export

临时设置(当前终端生效,关闭后失效):

bash 复制代码
# 设置一个全新的变量
export MY_VAR="hello"

# 追加到已有变量(比如 PATH)
export PATH=$PATH:/new/path

# 同时设置多个变量
export VAR1=value1 VAR2=value2

#导入一个已经存在的本地变量i
export i

永久设置(写入配置文件):

编辑 ~/.bashrc~/.bash_profile,在文件末尾添加:

bash 复制代码
export MY_VAR="hello"
export PATH=$PATH:/new/path

然后执行 source ~/.bashrc使其立即生效,或重新打开终端。

2. 消除环境变量:unset

用法:

bash 复制代码
# 删除单个变量
unset MY_VAR

# 删除 PATH 中的某个路径?不行,unset 只能删整个变量
# 如果想从 PATH 中去掉某个路径,需要重新赋值
export PATH=$(echo $PATH | sed 's|:/bad/path||')

注意:unset会让变量彻底消失,echo $MY_VAR将输出空。如果只是想置空(保留变量名但值为空),可以用 export MY_VAR=""


接下来介绍获取环境变量的三个方法:

方法 1:通过 main函数的第三个参数 env

原理

main函数的标准签名除了 int argc, char *argv[]外,还可以接受第三个参数 char *env[]。这个参数就是由父进程(Shell)传递过来的环境变量表。它是一个以 NULL结尾的字符串数组,每个元素形如 "KEY=VALUE"

验证代码:

cpp 复制代码
#include <stdio.h>
#include <string.h>

// main有参数吗?有
// 最多可以有几个?3个
// 是父进程传递给我们的
int main(int argc, char *argv[], char *env[])
{
    (void)argc;
    (void)argv;

    for(int i = 0; env[i]; i++)
    {
        printf("env[%d] -> %s\n", i, env[i]);
    }
    return 0;
}

编译运行输出结果:

从这个输出结果侧面体现出子进程的环境变量是继承了父进程bash的环境变量。

代码说明

cpp 复制代码
int main(int argc, char *argv[], char *env[])
  • argc:命令行参数个数(此处未使用,用 (void)argc;抑制警告)。

  • argv:命令行参数数组(同样未使用)。

  • env:环境变量表,可直接遍历打印。

特点

  • 最简单直接,无需额外函数调用。

  • 只能在 main函数内部访问,无法在子函数中直接获取(除非传参)。

方法 2:使用标准库函数 getenv()

原理

C 标准库提供了 getenv(const char *name)函数,它通过内部查找环境变量表,返回指定 name对应的 value字符串指针。若不存在则返回 NULL。该函数声明在 <stdlib.h>中。

验证代码:

cpp 复制代码
#include <stdio.h>
#include <stdlib.h>
#include <string.h>

int main(int argc, char *argv[], char *env[])
{
    (void)argc;
    (void)argv;
    (void)env;

    char *value = getenv("PATH");
    if(value==NULL) return 1;
    
    printf("PATH->%s\n", value);
}

编译并运行:

bash 复制代码
[cyq@VM-0-5-centos lesson15]$ ./code
PATH->/usr/local/bin:/usr/bin:/usr/local/sbin:/usr/sbin:/home/cyq/.local/bin:/home/cyq/bin

可以观察到getenv函数获取到了PATH环境变量中的内容。

代码说明

cpp 复制代码
const char *value = getenv("PATH");
  • 只需传入变量名,无需手动遍历数组。

  • 常用于获取单个环境变量,如 PATHUSERHOME等。

进阶用法

可以利用 getenv实现简单的权限控制---只允许特定用户执行程序,例如:

cpp 复制代码
const char *who = getenv("USER");
if (strcmp(who, "whb") == 0) {
    // 允许执行
} else {
    printf("Only Whb!!\n");
}

编译运行后可以发现,只有当用户为指定用户whb时才能正常执行程序,如不是指定用户则会输出:only whb!!

注意

  • 返回的指针指向静态内存,不应修改其内容。

  • 多线程环境中需考虑线程安全(可使用 getenv_r或锁机制)。

方法 3:通过全局变量 environ

原理

Linux 系统(以及其他 Unix-like 系统)在 <unistd.h>中定义了一个全局变量 extern char **environ,它同样指向环境变量表的首地址。实际上,main函数的第三个参数 env就是从这个全局变量复制而来的。

验证代码:

cpp 复制代码
  1 #include<stdio.h>
  2 #include<string.h>
  3 #include<stdlib.h>
  4                                                                                                                                                                                                              
  5 extern char **environ;
  6 int main(int argc, char *argv[])
  7 {
  8   (void)argc;
  9   (void)argv;
 10 
 11   for (int i = 0; environ[i]; i++)  
 12   {
 13     printf("environ[%d] -> %s\n", i, environ[i]);
 14   }
 15   return 0;
 16 }  

编译并运行的输出结果为:

代码说明

cpp 复制代码
extern char **environ;
for (int i = 0; environ[i]; i++) {
    printf("environ[%d] -> %s\n", i, environ[i]);
}
  • 无需修改 main函数签名,在任何函数中都可以通过 extern声明来访问。

  • 效果与方法 1 完全相同,但更灵活(可在任意函数中使用)。

注意

  • 某些编译器可能需要显式声明 extern char **environ;,或者包含 <unistd.h>

  • getenv不同,直接修改 environ的内容会影响整个进程的环境变量表(危险操作,不建议)。


小结

方法 访问方式 适用场景
mainenv参数 函数参数 仅在 main中一次性获取全部变量
getenv() 标准库函数 获取单个变量,安全简便
extern char **environ 全局变量 在任意函数中获取全部变量

这三种方法在 Linux 系统下均有效,底层都指向同一份内存数据。推荐日常开发优先使用 getenv,既简洁又安全。

环境变量的组织方式

每个程序都会收到⼀张环境表,环境表是⼀个字符指针数组,每个指针指向⼀个以'\0'结尾的环境

字符串。

6. 理解环境变量的特性

环境变量具有全局特性!!!


接下来补充两个概念:

6.1 本地变量

本地变量不会被子进程继承,只在bash内部被使用!

可以很简单地设定一个本地变量

bash 复制代码
i=10

查看这个本地变量的内容:

bash 复制代码
echo $i

演示:

bash 复制代码
[cyq@VM-0-5-centos lesson15]$ i=10
[cyq@VM-0-5-centos lesson15]$ echo $i
10

使用env指令只能查看所有环境变量,但是使用set指令既可以查看环境变量也可以查看本地变量:

bash 复制代码
set

这也就说明:bash会记录两套变量:环境变量和本地变量。

6.2 内建命令

我们在执行一条外部命令(比如我们自己写的程序)时,Shell 会创建一个子进程来运行它。这个子进程会继承父进程(bash)的环境变量,但子进程对环境变量的任何修改都无法回传给父进程。那么问题来了:当我们执行 export命令来增设环境变量时,如果它也是一个外部命令,它就会在子进程中修改环境变量,而父进程 bash 根本得不到这个修改,这样 export就失去了意义。

事实是,export并不是外部命令,而是一条内建命令。它不需要创建子进程,而是由 bash 自己亲自执行,直接调用内部的函数或系统调用来修改当前 bash 进程的环境变量表。这样一来,新增的环境变量自然就留在了当前的 Shell 环境中,后续启动的任何子进程都能继承到它。

总结

本文主要介绍了C语言中的命令行参数和环境变量的概念、作用与使用方法。命令行参数通过main函数的argc和argv参数实现,使程序能够接收并处理外部输入;环境变量则是系统配置的关键部分,影响程序的行为和路径查找。文章详细讲解了如何获取和操作环境变量,包括通过main函数的第三个参数、getenv函数以及全局变量environ三种方法,并区分了环境变量与本地变量的差异。此外,还介绍了常见环境变量如PATH、HOME、SHELL等的用途及配置方式,强调了环境变量的全局特性及其在进程间的继承机制,为编写灵活、健壮的命令行程序提供了基础指导。

相关推荐
程序员黑豆1 小时前
AI全栈开发 - Java:基本数据类型 vs 引用数据类型的内存存储
java·前端·ai编程
Black蜡笔小新1 小时前
自动化AI算法训练服务器DLTM一体化训推平台构建企业专属AI能力中台
人工智能·算法·自动化
howard20051 小时前
3.9 初探Shell命令
linux·shell命令
zh路西法1 小时前
基于yaml-cpp的C++参数服务器设计2:多级参数配置
linux·服务器·c++
sjsjs111 小时前
力扣3558. 给边赋权值的方案数 I
算法·leetcode·职场和发展
布朗克1681 小时前
34 JVM深入理解
java·jvm
hujinyuan201601 小时前
2025年12月中国电子学会青少年机器人技术等级考试试卷(四级) 真题+答案
算法·机器人
啦啦啦啦啦zzzz1 小时前
算法总结(双指针)
c++·算法·双指针
Flittly2 小时前
【AgentScope Java新手村系列】(4)结构化输出
java·spring boot·spring·ai