news 2026/9/6 6:43:36

C++ 里 argc 和 argv 到底有什么用?

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
C++ 里 argc 和 argv 到底有什么用?

最近在啃一个自动驾驶 SLAM 的开源项目,编译跑通之后想改改参数,看着main函数签名里的int argc, char** argv,突然意识到自己虽然背得下来"argc 是参数个数,argv 是参数字符串数组",但一直没真正想清楚——这东西在实际项目里到底图什么?

今天就把这个问题彻底理清楚,顺便聊聊它和我们常用的 gflags 库之间的关系。


一句话先给结论

argcargv的作用就是:让你的程序在启动时能从外界接收信息,不用每次改代码、重新编译。

没有它们,你的程序就是"写死的";有了它们,你的程序才是"可配置的"。


最直观的例子:指定数据集路径

假设你写了一个 SLAM 程序处理激光雷达数据,数据放在两个不同的地方:

  • 今天跑 KITTI 数据集,路径是/home/you/data/kitti/
  • 明天跑自己采集的数据,路径是/home/you/data/my_bag/

如果没有argc/argv,你只能在代码里把路径写死:

// 写死了,想换数据就得改代码、重新编译 std::string data_path = "/home/you/data/kitti/";

有了argc/argv,你可以这样写:

int main(int argc, char** argv) { if (argc < 2) { std::cout << "用法: ./program <数据路径>" << std::endl; return -1; } std::string data_path = argv[1]; std::cout << "正在处理数据:" << data_path << std::endl; // ... 后续逻辑 }

终端里直接传参,不用改一行代码:

./bin/your_program /home/you/data/kitti/ ./bin/your_program /home/you/data/my_bag/

这就是argc/argv最核心的价值——同一个编译好的程序,通过不同的命令行输入,表现出不同的行为。


和你项目里用的 gflags 是什么关系?

你项目里大概率见过这种写法:

DEFINE_double(angular_velocity, 10.0, "角速度(角度)制"); DEFINE_bool(use_quaternion, false, "是否使用四元数计算");

然后在main里有一行:

google::ParseCommandLineFlags(&argc, &argv, true);

其实 gflags 底层干的事,就是在帮你解析argcargv

当你在终端输入:

./bin/motion --angular_velocity=30.0 --use_quaternion=true

操作系统会把这些参数打包扔给main

  • argc= 3(程序名本身算一个 + 两个参数)
  • argv={"./bin/motion", "--angular_velocity=30.0", "--use_quaternion=true"}

ParseCommandLineFlags做的事情就是:遍历argv数组,把认识的参数(比如--angular_velocity)解析出来,转换成对应的类型,赋值给FLAGS_angular_velocity,然后把它们从argv里"吃掉"。

那个true参数的意思就是:解析完之后,把argcargv瘦身。瘦身前argc可能是 3,瘦身后变成 1,argv里只剩下{"./bin/motion"}。这样你后面自己的代码就不会被这些参数干扰了。


如果不用库,自己手动解析是什么样?

在比较简单的项目里,程序员会自己手动解析argv

int main(int argc, char** argv) { if (argc < 3) { std::cout << "用法: ./program <数据路径> <角速度>" << std::endl; return -1; } std::string path = argv[1]; // 第一个参数:路径 double angle = atof(argv[2]); // 第二个参数:角速度(字符串转数字) std::cout << "路径=" << path << ", 角速度=" << angle << std::endl; return 0; }

这种方式很原始,参数一多就容易乱,而且缺少类型检查、帮助信息生成等功能。所以才有了 gflags、Boost.Program_options 这类库来帮你自动管理。


为什么 ParseCommandLineFlags 要传指针?

你可能注意到了,调用时传的是&argc&argv,也就是指针。

原因很简单:函数内部要修改argcargv的值(把解析过的参数移除)。C++ 里想让函数修改外部变量,就得传地址。char**是二级指针,用来修改指针数组本身。如果传值,函数里改了外面也看不到。

这其实是 C++ 里最容易让人懵圈的概念之一。


先搞懂一个前提:C++ 函数传参是"值传递"

看这个最简单的例子:

void change(int x) { x = 100; // 改的是函数内部的副本 } int main() { int a = 10; change(a); // 这里 a 还是 10,没变! }

为什么a没变?因为change(a)传的是a 的值的副本。函数内部改的是副本,外面的a毫不知情。

想让函数改外面的变量,就得传地址(指针):

void change(int* x) { *x = 100; // 通过指针改外面的值 } int main() { int a = 10; change(&a); // 传地址 // 这里 a 变成了 100! }

这就是那句话的意思:想让函数修改外部变量,就得传地址。


argv为什么是char**

这得一层一层看:

第一层:char*(字符指针 → 字符串)

char* str = "hello";

str是一个指针,指向一块内存,那块内存里存着'h','e','l','l','o','\0'。所以char*可以表示一个字符串。

第二层:char* argv[](指针数组 → 多个字符串)

./bin/motion --angular_velocity=30.0

这里有 2 个字符串:

  • "./bin/motion"

  • "--angular_velocity=30.0"

argv是一个数组,数组里每个元素都是char*(指向一个字符串)。画出来就是:

argv (数组名,退化为 char**) │ ├── argv[0] ──→ "./bin/motion" └── argv[1] ──→ "--angular_velocity=30.0"

第三层:char**(指向指针的指针)

argv的类型是char**,也就是"指向char*的指针"。你可以把它理解为:指向指针数组首元素的指针


为什么 gflags 需要char**而不是char*

因为 gflags 的"瘦身"操作要做这件事:

瘦身前:

argv[0] = "./bin/motion" argv[1] = "--angular_velocity=30.0" argc = 2

瘦身后:

argv[0] = "./bin/motion" argc = 1

它要修改两样东西:

  1. argc的值:从 2 变成 1 → 所以需要传&argcint*

  2. argv这个数组本身:把argv[1]移除(或者说让argv指向一个新的更短的数组)→ 所以需要传&argv

等等,这里需要澄清一下。实际上ParseCommandLineFlags的函数签名是:

google::ParseCommandLineFlags(int* argc, char*** argv, bool remove_flags);

注意第三个参数传的是&argv,类型是char***(三级指针)!但在mainargvchar**,所以&argv就是char***

不过如果代码里写的是:

google::ParseCommandLineFlags(&argc, &argv, true);

这里&argv的类型确实是char***,但函数参数声明里接收的是char***,这就是三级指针


用大白话总结

你传的东西

类型

为什么

&argc

int*

想让函数把argc从 2 改成 1

&argv

char***

想让函数把argv这个"指针数组"本身替换成一个更短的

为什么叫"二级指针"?​ 因为argv本身是char**(二级),&argv就是char***(三级)。前面说的"char**​ 是二级指针,用来修改指针数组本身"这个表述其实是为了让读者容易理解——char**指向一个指针数组,通过它你可以修改数组里的每个指针元素。


一个更直观的类比

argv想象成一排遥控器char*),每个遥控器可以控制一台电视(字符串):

  • argv[0]这个遥控器 → 控制电视 "./bin/motion"

  • argv[1]这个遥控器 → 控制电视 "--angular_velocity=30.0"

argv本身(作为char**)就是"放遥控器的架子"。

gflags 瘦身的时候说:"我要把第二个遥控器拿走"。那它得知道这个架子在哪,才能修改架子上的内容。所以你得告诉它架子的地址(&argv)。


解释一下“瘦身”的概念和意义

"瘦身"是我之前用的一个比喻,官方文档里的准确说法叫remove_flags(移除标志)。一句话概括:

gflags 在解析命令行时,把你输入的--xxx=yyy这些"flag(标志)"识别出来、消费掉,然后把它们从argv里抽走,让argc相应变小。这样你的程序后续看到的命令行就"瘦"了——只剩下真正的业务参数,不再夹杂各种配置 flag。

官方文档怎么说

gflags 的ParseCommandLineFlags函数签名是:

google::ParseCommandLineFlags(&argc, &argv, remove_flags);

第三个参数remove_flags决定了"瘦身"的方式,有两种模式:

模式一:remove_flags = true(瘦身模式,最常用)

  • argv移除所有已识别的 flag 及其值
  • 相应减小argc
  • 函数返回后,argv只包含非 flag 的命令行参数

模式二:remove_flags = false(重排模式)

  • argc保持不变
  • argv里的参数会被重排,让所有 flag 都跑到前面
  • 函数返回第一个非 flag 参数的下标

用一个例子秒懂

假设你这样启动程序:

./bin/foo arg1 -q arg2

(这是个合法但有点奇怪的写法:业务参数arg1夹在了 flag-q前面)

如果remove_flags = true

解析后argv变成:

argv[0] = "./bin/foo" argv[1] = "arg1" argv[2] = "arg2" argc = 3

-q这个 flag 被"吃掉"了,赋值给了FLAGS_q。你的程序后面处理argv时,看到的全是干净的纯业务参数。

如果remove_flags = false

解析后argv被重排:

argv[0] = "./bin/foo" argv[1] = "-q" argv[2] = "arg1" argv[3] = "arg2" argc = 4 ← 没变

flag 被挪到了前面,函数返回下标2(即argv[2]指向第一个非 flag 参数arg1),你自己根据下标去取业务参数。


回到你的代码

你项目里写的是:

google::ParseCommandLineFlags(&argc, &argv, true);

true就是告诉 gflags:"帮我把所有--xxx=yyy的 flag 都解析掉,然后从argv里移除,让我后面看到的命令行干干净净。"

举个例子,你启动程序时输入:

./bin/motion --angular_velocity=30.0 --use_quaternion=true input_data.log

解析前:

argv = {"./bin/motion", "--angular_velocity=30.0", "--use_quaternion=true", "input_data.log"} argc = 4

解析后(FLAGS_angular_velocity被赋值为 30.0,FLAGS_use_quaternion被赋值为 true):

argv = {"./bin/motion", "input_data.log"} argc = 2

"瘦身"的本质就是:flag 被 gflags 消费掉了,你的程序后续处理argv时,看到的就只剩真正的业务参数了。


为什么必须传&argc&argv

现在我们回到你上一个问题里疑惑的点:

因为 gflags要修改main函数里的argcargv变量——

  • argc从 4 变成 2(需要修改int变量 → 传int*&argc
  • argv数组本身被改写(需要修改char*数组 → 传char***&argv

C++ 函数传参是值传递,想让函数修改外部的变量,就必须传地址(指针)。这就是为什么要写&argc, &argv,而不是argc, argv


💡 所以"瘦身"不是什么黑话,就是 gflags 帮你把命令行里的"配置项"挑出来用掉,然后给你的程序留一个干净的"参数列表"。这样你的业务代码里就不用自己写一堆if (strcmp(argv[i], "--angular_velocity") == 0)这种又臭又长的解析逻辑了。


总结

概念

作用

argc

记录用户一共传了几个参数

argv

存储每个参数的具体字符串

argv[0]

程序自身的路径名

argv[1]往后

用户传入的实际参数

说到底,argc/argv就是程序与外界交互的"第一道入口"。它让同一个编译好的二进制文件,可以通过不同的命令行输入做不同的事——这也是为什么能在不重新编译的情况下,用--angular_velocity=30.0就改变车辆角速度的原因。理解了这一点,再看项目里那些DEFINE_xxx宏和ParseCommandLineFlags调用,就不会觉得它们是从天而降的魔法了。

希望我的分享对你有所帮助,我是小小扫地僧,愿我们一起进步,砥砺前行!

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/6 6:40:55

基于ARM的数字集成电路测试系统设计与实现

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/6 6:39:36

【leetcode】42.接雨水js

文章目录 题目代码 题目 代码 核心是短板原理&#xff1a; 接的雨水 min(maxLeft, maxRight) - height[i] 但要怎么体现在代码里呢&#xff1f;一开始想的是Math.min()&#xff0c;但学习的时候发现题解里min的影都没见到&#xff0c;而是对比height[left]和height[right]就可…

作者头像 李华
网站建设 2026/9/6 6:39:19

8G显存本地跑视频生成:MiniMax H3+ComfyUI整合包实战解析

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/6 6:37:32

单片机关于按钮消抖方法

抖动原因:按钮内部铁片按下时和松开时&#xff0c;会有抖敏动&#xff0c;导致多次识别按钮按下&#xff0c;从而出现失灵不灵现象按下抖动和释放抖动时间约20毫秒解决方法:用烧写软件生成一个20毫秒延时程序放入代码中&#xff0c;其中wile用来锁定&#xff0c;key10&#xff…

作者头像 李华
网站建设 2026/9/6 6:35:42

纸袋热封胶失效,一般会表现成什么现象?

纸袋热封胶失效时&#xff0c;表现的现象通常比较明显。最常见的情况是封口开裂、分层或渗漏。这些问题及其产生原因相互关联。胶水涂布不均匀粘合强度不足&#xff0c;容易出现开裂。另外&#xff0c;热封温度不足或加热时间不够&#xff0c;也会导致胶水无法完全固化。环境湿…

作者头像 李华
网站建设 2026/9/6 6:33:51

基于SpringBoot的农商交互对接系统(源码+讲解视频+LW)

联系博主 温馨提示&#xff1a;本人主页置顶文章(点我)开头有 CSDN 平台官方提供的学长联系方式的名片&#xff01; 温馨提示&#xff1a;本人主页置顶文章(点我)开头有 CSDN 平台官方提供的学长联系方式的名片&#xff01; 温馨提示&#xff1a;本人主页置顶文章(点我)开头有 …

作者头像 李华