22. main 函数的参数:argc 和 argv 到底怎么用?
很多初学者写 C 语言,main 函数永远都是 int main(void)。直到有一天,看到别人写的 int main(int argc, char *argv[]),心里就犯嘀咕:这俩家伙是干嘛的?
我当年刚学的时候也困惑过。后来在做一个命令行工具时,才真正搞明白——argc 和 argv 就是操作系统传给程序的「小纸条」。你想想看,你在终端敲一行命令,程序怎么知道你敲了啥?就是靠这两个参数传进来的。
argc 和 argv 的本质
先说定义:
- argc(argument count):参数个数。注意,程序名本身也算一个参数。
- argv(argument vector):参数字符串数组。每个元素是一个字符串指针。
举个例子。你在终端输入:
./myapp -f config.txt --verbose
那么操作系统会这样传参:
- argc = 4
- argv[0] = "./myapp"
- argv[1] = "-f"
- argv[2] = "config.txt"
- argv[3] = "--verbose"
- argv[4] = NULL(数组末尾有个空指针做哨兵)
嗯,这里要注意:argv[argc] 一定是 NULL。这是 C 标准规定的,你可以利用这个特性来遍历参数。
一个最基础的例子
我习惯先写个最简单的 demo 来验证理解:
#include <stdio.h>
int main(int argc, char *argv[])
{
printf("参数个数: %d\n", argc);
for (int i = 0; i < argc; i++) {
printf("argv[%d] = %s\n", i, argv[i]);
}
return 0;
}
编译运行:
$ gcc demo.c -o demo
$ ./demo hello world 123
参数个数: 4
argv[0] = ./demo
argv[1] = hello
argv[2] = world
argv[3] = 123
看到没?所有参数都是字符串。就算你敲了 123,传进来的也是 "123" 这个字符串。如果你要当整数用,得自己用 atoi() 或 strtol() 转换。
argv[1][0] = 'X'——这在某些系统上会崩溃。我在项目中就见过有人这么干,结果在 Linux 上跑得好好的,换到某个嵌入式平台就段错误了。
两种常见的 argv 写法
你可能见过两种写法:
int main(int argc, char *argv[]) // 写法一
int main(int argc, char **argv) // 写法二
这两种完全等价。我个人习惯用第一种,因为更直观——argv[] 一看就知道是个数组。但如果你要写函数传参,比如把 argv 传给另一个函数,那就只能用 char ** 了。
实战:写一个简易命令行工具
光说不练假把式。咱们写个实用的例子:一个支持 -n 和 -v 选项的打印工具。
#include <stdio.h>
#include <string.h>
int main(int argc, char *argv[])
{
int verbose = 0;
int count = 1;
// 解析参数
for (int i = 1; i < argc; i++) {
if (strcmp(argv[i], "-v") == 0) {
verbose = 1;
} else if (strcmp(argv[i], "-n") == 0) {
// 下一个参数应该是数字
if (i + 1 < argc) {
count = atoi(argv[++i]);
} else {
printf("错误: -n 后面需要跟一个数字\n");
return 1;
}
} else {
printf("未知选项: %s\n", argv[i]);
return 1;
}
}
// 执行功能
for (int i = 0; i < count; i++) {
if (verbose) {
printf("[%d] Hello, World!\n", i + 1);
} else {
printf("Hello, World!\n");
}
}
return 0;
}
运行效果:
$ ./tool -n 3 -v
[1] Hello, World!
[2] Hello, World!
[3] Hello, World!
这个例子虽然简单,但涵盖了参数解析的核心逻辑:遍历 argv,根据选项做分支处理。我在实际项目中,很多嵌入式调试工具的命令行解析,底层逻辑都是这个套路。
嵌入式场景下的特殊用法
说到嵌入式,你可能觉得:单片机程序哪来的命令行参数?
其实不然。我做过一个项目,Bootloader 需要根据启动模式选择不同的固件。我们就在 main() 里通过某种方式「伪造」了 argc 和 argv:
// 伪代码:从固定内存地址读取参数
int main(void)
{
char *fake_argv[4];
int fake_argc = 0;
// 从备份区读取启动参数
read_from_backup_region(buf);
fake_argv[0] = "bootloader";
fake_argv[1] = buf; // 比如 "recovery"
fake_argc = 2;
// 然后调用真正的逻辑
return real_main(fake_argc, fake_argv);
}
这样做的好处是:核心逻辑可以在 PC 上先调试好,再移植到嵌入式平台。你想想看,如果所有逻辑都依赖硬件外设,调试起来多痛苦?
main(argc, argv) 作为「可移植层」的入口。PC 上跑的时候从命令行传参,嵌入式上跑的时候从配置区构造参数。这样一套代码,两个平台都能用。
避坑指南
我曾经踩过一个坑:在解析参数时,直接用 argv[i] 去访问,却没检查 i 是否越界。结果用户少输了一个参数,程序直接段错误。后来我养成了一个习惯:访问 argv[i] 之前,先确认 i < argc。
还有一点:argv[0] 不一定是程序名。在有些系统上,它可能是空字符串,甚至是完全不同的内容。所以别太依赖 argv[0] 来获取程序路径——用 /proc/self/exe(Linux)或 GetModuleFileName()(Windows)更靠谱。
知识体系总览
下面这张图帮你理清 argc 和 argv 的核心脉络:
总结
argc 和 argv 说白了就是操作系统和你的程序之间最朴素的沟通方式。你敲的命令行,被拆成一个一个的字符串,通过 argv 递到你手里。argc 告诉你手里有多少个字符串,防止你拿过头。
写命令行工具时,记住三点:
- 先检查 argc,再访问 argv。别越界。
- 参数全是字符串,需要数值就自己转。
- 别修改 argv 里的内容,那是系统给你的,不是你的。
掌握了这些,你就能写出健壮的命令行程序了。下次面试官问起 argc 和 argv,你可以自信地告诉他:这不就是操作系统递给我的小纸条嘛。
一句话记住:argc 是数数,argv 是拿东西。数清楚了再拿,别拿超了。
公众号:蓝海资料掘金营,微信deep3321