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 里的字符串是只读的。千万别试图修改它们,比如 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 的核心脉络:

main(argc, argv) 知识体系 int main(int argc, char *argv[]) argc (参数个数) 包含程序名本身 argv (参数字符串数组) 末尾 argv[argc] == NULL 参数都是字符串 需 atoi/strtol 转换 argv 只读不可修改 修改可能段错误 argv[0] 不一定是程序名 不同系统行为不同 嵌入式可伪造参数 提高代码可移植性 核心:argc 控制边界,argv 承载数据

总结

argc 和 argv 说白了就是操作系统和你的程序之间最朴素的沟通方式。你敲的命令行,被拆成一个一个的字符串,通过 argv 递到你手里。argc 告诉你手里有多少个字符串,防止你拿过头。

写命令行工具时,记住三点:

  1. 先检查 argc,再访问 argv。别越界。
  2. 参数全是字符串,需要数值就自己转。
  3. 别修改 argv 里的内容,那是系统给你的,不是你的。

掌握了这些,你就能写出健壮的命令行程序了。下次面试官问起 argc 和 argv,你可以自信地告诉他:这不就是操作系统递给我的小纸条嘛。

一句话记住:argc 是数数,argv 是拿东西。数清楚了再拿,别拿超了。


公众号:蓝海资料掘金营,微信deep3321