最近在啃一个自动驾驶 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_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参数的意思就是:解析完之后,把argc和argv瘦身。瘦身前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,也就是指针。
原因很简单:函数内部要修改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_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它要修改两样东西:
argc的值:从 2 变成 1 → 所以需要传&argc(int*)
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***,这就是三级指针。用大白话总结
你传的东西
类型
为什么
&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函数里的argc和argv变量——
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就是程序与外界交互的"第一道入口"。它让同一个编译好的二进制文件,可以通过不同的命令行输入做不同的事——这也是为什么能在不重新编译的情况下,用--angular_velocity=30.0就改变车辆角速度的原因。理解了这一点,再看项目里那些DEFINE_xxx宏和ParseCommandLineFlags调用,就不会觉得它们是从天而降的魔法了。
希望我的分享对你有所帮助,我是小小扫地僧,愿我们一起进步,砥砺前行!