
最近在啃一个自动驾驶 SLAM 的开源项目编译跑通之后想改改参数看着main函数签名里的int argc, char** argv突然意识到自己虽然背得下来argc 是参数个数argv 是参数字符串数组但一直没真正想清楚——这东西在实际项目里到底图什么今天就把这个问题彻底理清楚顺便聊聊它和我们常用的 gflags 库之间的关系。一句话先给结论argc和argv的作用就是让你的程序在启动时能从外界接收信息不用每次改代码、重新编译。没有它们你的程序就是写死的有了它们你的程序才是可配置的。最直观的例子指定数据集路径假设你写了一个 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 底层干的事就是在帮你解析argc和argv。当你在终端输入./bin/motion --angular_velocity30.0 --use_quaterniontrue操作系统会把这些参数打包扔给mainargc 3程序名本身算一个 两个参数argv{./bin/motion, --angular_velocity30.0, --use_quaterniontrue}ParseCommandLineFlags做的事情就是遍历argv数组把认识的参数比如--angular_velocity解析出来转换成对应的类型赋值给FLAGS_angular_velocity然后把它们从argv里吃掉。那个true参数的意思就是解析完之后把argc和argv瘦身。瘦身前argc可能是 3瘦身后变成 1argv里只剩下{./bin/motion}。这样你后面自己的代码就不会被这些参数干扰了。如果不用库自己手动解析是什么样在比较简单的项目里程序员会自己手动解析argvint 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也就是指针。原因很简单函数内部要修改argc和argv的值把解析过的参数移除。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_velocity30.0这里有 2 个字符串./bin/motion--angular_velocity30.0argv是一个数组数组里每个元素都是char*指向一个字符串。画出来就是argv (数组名退化为 char**) │ ├── argv[0] ──→ ./bin/motion └── argv[1] ──→ --angular_velocity30.0第三层char**指向指针的指针argv的类型是char**也就是指向char*的指针。你可以把它理解为指向指针数组首元素的指针。为什么 gflags 需要char**而不是char*因为 gflags 的瘦身操作要做这件事瘦身前argv[0] ./bin/motion argv[1] --angular_velocity30.0 argc 2瘦身后argv[0] ./bin/motion argc 1它要修改两样东西argc的值从 2 变成 1 → 所以需要传argcint*argv这个数组本身把argv[1]移除或者说让argv指向一个新的更短的数组→ 所以需要传argv等等这里需要澄清一下。实际上ParseCommandLineFlags的函数签名是google::ParseCommandLineFlags(int* argc, char*** argv, bool remove_flags);注意第三个参数传的是argv类型是char***三级指针但在main里argv是char**所以argv就是char***。不过如果代码里写的是google::ParseCommandLineFlags(argc, argv, true);这里argv的类型确实是char***但函数参数声明里接收的是char***这就是三级指针。用大白话总结你传的东西类型为什么argcint*想让函数把argc从 2 改成 1argvchar***想让函数把argv这个指针数组本身替换成一个更短的为什么叫二级指针 因为argv本身是char**二级argv就是char***三级。前面说的char** 是二级指针用来修改指针数组本身这个表述其实是为了让读者容易理解——char**指向一个指针数组通过它你可以修改数组里的每个指针元素。一个更直观的类比把argv想象成一排遥控器char*每个遥控器可以控制一台电视字符串argv[0]这个遥控器 → 控制电视 ./bin/motionargv[1]这个遥控器 → 控制电视 --angular_velocity30.0argv本身作为char**就是放遥控器的架子。gflags 瘦身的时候说我要把第二个遥控器拿走。那它得知道这个架子在哪才能修改架子上的内容。所以你得告诉它架子的地址argv。解释一下“瘦身”的概念和意义瘦身是我之前用的一个比喻官方文档里的准确说法叫remove_flags移除标志。一句话概括gflags 在解析命令行时把你输入的--xxxyyy这些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帮我把所有--xxxyyy的 flag 都解析掉然后从argv里移除让我后面看到的命令行干干净净。举个例子你启动程序时输入./bin/motion --angular_velocity30.0 --use_quaterniontrue input_data.log解析前argv {./bin/motion, --angular_velocity30.0, --use_quaterniontrue, input_data.log} argc 4解析后FLAGS_angular_velocity被赋值为 30.0FLAGS_use_quaternion被赋值为 trueargv {./bin/motion, input_data.log} argc 2瘦身的本质就是flag 被 gflags 消费掉了你的程序后续处理argv时看到的就只剩真正的业务参数了。为什么必须传argc和argv现在我们回到你上一个问题里疑惑的点因为 gflags要修改你main函数里的argc和argv变量——argc从 4 变成 2需要修改int变量 → 传int*即argcargv数组本身被改写需要修改char*数组 → 传char***即argvC 函数传参是值传递想让函数修改外部的变量就必须传地址指针。这就是为什么要写argc, argv而不是argc, argv。 所以瘦身不是什么黑话就是 gflags 帮你把命令行里的配置项挑出来用掉然后给你的程序留一个干净的参数列表。这样你的业务代码里就不用自己写一堆if (strcmp(argv[i], --angular_velocity) 0)这种又臭又长的解析逻辑了。总结概念作用argc记录用户一共传了几个参数argv存储每个参数的具体字符串argv[0]程序自身的路径名argv[1]往后用户传入的实际参数说到底argc/argv就是程序与外界交互的第一道入口。它让同一个编译好的二进制文件可以通过不同的命令行输入做不同的事——这也是为什么能在不重新编译的情况下用--angular_velocity30.0就改变车辆角速度的原因。理解了这一点再看项目里那些DEFINE_xxx宏和ParseCommandLineFlags调用就不会觉得它们是从天而降的魔法了。希望我的分享对你有所帮助我是小小扫地僧愿我们一起进步砥砺前行