C Language · Command Line · Reference Sheet

argc / argv 命令行参数详解

含三种转换写法 · 解引用对照 · NULL 哨兵原理 · VS 2022 配置

01 · 示例程序(三个版本)

版本一:ASCII 减法(仅限单个数字 0–9)

'0'=48,'1'=49……字符相减即得数值。限制:输入 12 只取首字符 '1',多位数结果错误。

#include <stdio.h>

int main(int argc, char const* argv[]) {
    if (argc < 3) { printf("用法: add <数字1> <数字2>\n"); return 1; }
    int a = *argv[1] - '0';   // '3'-'0' = 51-48 = 3
    int b = *argv[2] - '0';   // '5'-'0' = 53-48 = 5
    printf("%d\n", a + b);
    return 0;
}

add 3 58  add 12 344(错误:仅取首字符 '1'+'3' = 1+3)

版本二:atoi()(atoi功能:把ASCII码转成int,无错误检测)

可处理多位整数,但输入 abc 时静默返回 0,无法区分错误与合法零值。

#include <stdio.h>
#include <stdlib.h>

int main(int argc, char const* argv[]) {
    if (argc < 3) { printf("用法: add <数字1> <数字2>\n"); return 1; }
    printf("%d\n", atoi(argv[1]) + atoi(argv[2]));
    return 0;
}

add 123 456579

版本三:strtol()(阅读完06/07/08部分再看此代码,含错误检测)

endptr 指向转换停止处:若不是 '\0',说明含非数字字符。

#include <stdio.h>
#include <stdlib.h>

int main(int argc, char const* argv[]) {
    if (argc < 3) { printf("用法: add <数字1> <数字2>\n"); return 1; }
    char *end1, *end2;
    long a = strtol(argv[1], &end1, 10);
    long b = strtol(argv[2], &end2, 10);
    if (*end1 != '\0' || *end2 != '\0') {
        printf("输入错误:请输入整数\n"); return 1;
    }
    printf("%ld\n", a + b);
    return 0;
}

add 123 456579

add 123abc 456end1 指向 'a'*end1 != '\0' 触发 → 输入错误:请输入整数


02 · 参数说明

int main(int argc, char const* argv[])
参数类型全称含义
argcintargument count命令行参数个数(含程序名,至少为 1)
argvchar**argument vector指向字符串指针数组的指针

执行 add 123 456 时:

argc=3   argv[0]="add"  argv[1]="123"  argv[2]="456"  argv[3]=NULL
注意: argv[0] 是程序名,用户参数从 argv[1] 开始。

03 · 解引用写法对照(以 argv[1]="123" 为例)

写法类型说明
argvchar**指针数组首地址指向存放 argv[0..3] 的内存块
argv[1]char*数组中第二个元素值第二格指针,指向字符串 "123"
argv[1][0]char'1'(49)首字符,下标写法
*argv[1]char'1'(49)同上,解引用写法,完全等价

逐字符访问:下标写法 vs 指针算术写法

// 下标写法                    // 指针算术写法(完全等价)
argv[1][0]  →  '1'  (49)      *(argv[1] + 0)  →  '1'  (49)
argv[1][1]  →  '2'  (50)      *(argv[1] + 1)  →  '2'  (50)
argv[1][2]  →  '3'  (51)      *(argv[1] + 2)  →  '3'  (51)
argv[1][3]  →  '\0' ( 0)      *(argv[1] + 3)  →  '\0' ( 0)  ← 结束符
等价原理: C 中 a[i]*(a+i) 的语法糖,编译后机器码完全相同。argv[1]+1 将指针后移 1 字节再解引用。

04 · argv[3] = NULL 是谁设置的?

操作系统不直接调用 main(),中间有一层 C 运行时库启动代码(CRT),由编译器自动链接。

操作系统// 把命令行字符串整体交给进程
↓ 1. 按空格切割命令行字符串
↓ 2. 分配 char* 指针数组
↓ 3. 填入每个 argv[i] 的地址
↓ 4. 写入 argv[argc] = NULL ← 就是这里
↓ 5. 设置 argc = 参数个数
CRT 启动代码
你写的 main(argc, argv)
C 标准要求: C11 明确规定 argv[argc] 必须为 NULL——与字符串以 '\0' 结尾同一哲学,哨兵值标记边界,遍历无需额外长度。所有平台(MSVC / GCC / Clang)均遵守。
// 可不用 argc,靠 NULL 判断结束
char **p = argv;
while (*p != NULL) { printf("%s\n", *p); p++; }

05 · VS 2022 设置命令行参数

步骤操作
1右键项目名 → Properties(属性)
2左侧选 Configuration Properties(配置属性)→ Debugging(调试)
3找到 Command Arguments(命令参数),填入 123 456,点 OK,按 F5Ctrl+F5 运行
注意: VS 自动将程序路径填入 argv[0],Command Arguments 只需填 argv[1] 起的内容,参数间用空格分隔。

06 · atoi vs strtol

特性atoistrtol
签名int atoi(const char*)long strtol(const char*, char**, int)
错误检测❌ 无,静默返回 0✅ endptr 可检测非法字符
溢出处理❌ 未定义行为✅ 返回 LONG_MAX/MIN,设 errno
进制❌ 仅十进制✅ 2–36 进制,base=0 自动检测
适用快速原型生产代码(推荐)

strtol 参数

long strtol(const char *str, char **endptr, int base);
// str     要转换的字符串
// endptr  转换结束后指向第一个非数字字符;不需要传 NULL
// base    进制:10=十进制,0=自动检测前缀(0x→16, 0→8)

base=0 自动检测

strtol("0xff", NULL, 0);  // → 255(十六进制)
strtol("077",  NULL, 0);  // →  63(八进制)
strtol("99",   NULL, 0);  // →  99(十进制)

07 · 为什么 strtol 第二个参数是 char** 而不是 char*?

核心原因:strtol 需要把一个 char* 的值写回给调用者,而在 C 里,要修改调用者的变量,必须传入该变量的地址

类比:用 int* 修改整数

// 想让函数修改调用者的 int,要传 int*
void set(int *p) { *p = 42; }

int x = 0;
set(&x);          // 传地址
printf("%d", x);  // 42

同理:用 char** 修改 char*

调用者声明了 char *end,要让 strtol 能写入它,就传 &end,类型是 char**

strtol 内部简化示意

long strtol(const char *str, char **endptr, int base) {
    const char *p = str;
    // ... 解析数字,p 移到第一个非数字字符 ...
    if (endptr != NULL)
        *endptr = (char *)p;   // 写回调用者的 end 变量
    return result;
}

为什么是 (char*)p 而不是直接写 p?

p 的类型是 const char*,而 *endptr 的类型是 char*,两者带不带 const 不一样,直接赋值会丢掉 const

*endptr = p;          // ❌ const char* 赋给 char*,丢掉 const
                      //    编译器警告:discards 'const' qualifier
*endptr = (char *)p;  // ✅ 强制转换,显式告知编译器「我知道在丢 const,故意的」
为什么 strtol 要这样设计?
str 声明为 const char*,承诺函数内部只读不写字符串——这是对的。
endptr 回传给调用者,调用者拿到后可能需要修改字符串,所以回传 char* 而非 const char*
两者类型不同,(char*) 强转是必要的妥协。

08 · 为什么是 *endptr = (char*)p 而不是 endptr = (char*)p?

endptr 是函数的局部变量,存的是调用者 &end 传进来的地址。修改 endptr 本身只改了局部变量,函数返回后消失;必须解引用才能写回调用者的 end

两种写法的区别

// 调用者
char *end;
strtol("123abc", &end, 10);   // 传入 &end,函数内 endptr = &end

// ❌ endptr = (char*)p
//    只修改了函数内部的局部变量 endptr 本身
//    函数返回后 endptr 消失,调用者的 end 完全没被动

// ✅ *endptr = (char*)p
//    解引用 endptr,找到调用者的 end 变量
//    把值写进去,调用者能看到变化

用地址图表示

endptr 存的是 0x5000(即 end 的地址)

endptr = (char*)p
→ 把局部变量 endptr 改成其他地址
→ 0x5000 处的 end 没有被碰到       ← 调用者看不到任何变化

*endptr = (char*)p
→ 去 0x5000 那个地址
→ 把那里的内容改掉                  ← end 被改掉,调用者能看到

类比 int*

int x = 0;
int *ptr = &x;

ptr  = 42;   // ❌ 类型错误,且即使能赋值也只改了 ptr 本身,x 不变
*ptr = 42;   // ✅ 通过地址找到 x,把 x 改成 42
规律: 要让函数修改类型为 T 的变量,参数必须是 T*,函数内部用 *param = 值 写回。
这里 T = char*,参数是 char**,写回用 *endptr = (char*)p
endptr = p 改的是指针本身,不是指针指向的内容,两者本质不同。