LV01-20-C语言-main的参数

本文主要是C语言基础——main函数参数相关笔记,若笔记中有错误或者不合适的地方,欢迎批评指正😃。

点击查看使用工具及版本
Windows windows11
Ubuntu Ubuntu16.04的64位版本
VMware® Workstation 16 Pro 16.2.3 build-19376536
SecureCRT Version 8.7.2 (x64 build 2214) - 正式版-2020年5月14日
开发板 正点原子 i.MX6ULL Linux阿尔法开发板
uboot NXP官方提供的uboot,NXP提供的版本为uboot-imx-rel_imx_4.1.15_2.1.0_ga(使用的uboot版本为U-Boot 2016.03)
linux内核 linux-4.15(NXP官方提供)
STM32开发板 正点原子战舰V3(STM32F103ZET6)
点击查看本文参考资料
参考方向 参考原文
------
点击查看相关文件下载
--- ---

前面其实我们已经学习过了,main函数时有参数的,下面我们深入再了解一下。

一、main的参数

1. 一道多选题

以下属于main函数的参数的是?()

A. argc

B. envp

C. main

D. argv

毫无疑问,我上来就选了ADl两个选项,因为这两个我们很常见,但其实B选项也是。这个题目正确答案是ABD。我们后边详细学习一下这几个参数。

2. argc与argv

前边我们知道:

argc 整型变量,表示了命令行中参数的个数(注意:文件名本身也算一个参数)
argv 字符串型变量,它是一个指向字符串的指针数组。命令行中的每个字符串都会被存储到内存中,并且分配一个指针指向它。按照惯例,这个指针数组被称为argv(argument value),系统会使用空格把各个字符串格开。一般情况下,argv[0]是程序本身的名称,后边依次是传入的参数。

我们看下面的测试代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
#include <stdio.h>

int main(int argc, const char *argv[])
{
/* code */
int i = 0;
printf("argc = %d\n", argc);
for(i = 0; i<argc; i++)
{
printf("argv[%d]=%s\n", i, argv[i]);
}

return 0;
}

然后我们执行以下命令编译代码:

1
gcc test.c -Wall

然后我们会得到一个a.out文件,然后我们执行这个可执行文件:

1
./a.out 1 2 3 4 5 6

就会得到以下输出:

image-20231102214343723

3. envp

这是个什么参数?我们找一份glibc的源码,来看一看main函数原型究竟是什么。这个源码到网上搜一下应该很多,我找了一个在线的:Glibc source code (glibc-2.31) - Bootlin,然后我们找到LIBC_START_MAIN,它在libc-start.c - csu/libc-start.c - Glibc source code (glibc-2.31) - Bootlin,原型就是:

1
2
3
4
5
6
7
8
9
10
11
12
STATIC int LIBC_START_MAIN (int (*main) (int, char **, char **
MAIN_AUXVEC_DECL),
int argc,
char **argv,
#ifdef LIBC_START_MAIN_AUXVEC_ARG
ElfW(auxv_t) *auxvec,
#endif
__typeof (main) init,
void (*fini) (void),
void (*rtld_fini) (void),
void *stack_end)
__attribute__ ((noreturn));

我们平时写的main函数其实就是由它来调用,它的第一个参数为:

1
int (*main) (int, char **, char ** MAIN_AUXVEC_DECL)

可以看到这个main函数确实是有三个参数的,调用main函数的时候在这里libc-start.c - csu/libc-start.c - Glibc source code (glibc-2.31) - Bootlin

1
2
/* Nothing fancy, just call the function.  */
result = main (argc, argv, __environ MAIN_AUXVEC_PARAM);

可以看到确实是传入了三个参数。第三个参数其实就叫环境变量,我们用软件看它的调用关系就会发现,是有地方对这个环境变量做初始化的。我就没有具体去追踪了,这里我们知道它是一个char **类型就可以了,和argv类似,都是指针数组,每个成员都指向一个环境变量,并且最后以NULL结尾,大概就是这样的:

image-20231102220338587

我们可以用以下的测试代码打印出运行可执行文件的时候的环境变量:

1
2
3
4
5
6
7
8
9
10
11
12
13
#include <stdio.h>

int main(int argc, const char *argv[], char *envp[])
{
/* code */
int i = 0;
while(envp[i] != NULL)
{
printf("envp[%d]: %s\n", i, envp[i++]);
}

return 0;
}

然后我们编译得到a.out可执行文件,然后这样运行:

1
./a.out 1 2 3 4 5 6

然后我们会得到以下输出:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
hk@vm:~/1sharedfiles/test$ ./a.out 1 2 3 4 5
envp[1]: XDG_VTNR=7
envp[2]: XDG_SESSION_ID=c1
#......
envp[39]: HOME=/home/hk
envp[40]: XDG_SEAT=seat0
envp[41]: SHLVL=1
envp[42]: LANGUAGE=zh_CN:zh
#......
envp[45]: UPSTART_EVENTS=started starting
envp[46]: XDG_SESSION_DESKTOP=ubuntu
envp[47]: LOGNAME=hk
#......
envp[60]: _=./a.out
envp[61]: OLDPWD=/home/hk/1sharedfiles

我们还可以在linux终端使用env命令查看环境变量,会发现上边程序打印的与env命令打印出来的环境变量基本一致。

二、命令行参数解析

有时候我们会看到这样的命令:

1
$ arecord -D hw:0,0 -d 10 -f cd -r 44100 -c 2 -t wav test.wav

这是啥?这个其实是之前在调ALSA驱动的时候看到的,前面的arecord为工具名称,是一个可执行程序(GCC编译出来的),后边的-D这些都是参数,后来我翻了一下源码,原来这个其实就是通过main的参数传进去进行解析的。它与终端的命令很类似,反正网上都叫命令行参数,这里也这样叫吧。那么我们想要自己写一个工具,也想支持一些-或者--开头的选项,我们要怎么解析?字符串比较?当然可以啦,只是比较麻烦罢了,其实C语言已经为我们提供了两个函数来解析命令行参数。这两个函数是:

1
2
3
4
5
int getopt(int argc, char * const argv[], 
const char *optstring);
int getopt_long(int argc, char * const argv[],
const char *optstring,
const struct option *longopts, int *longindex);

就是上边两个函数啦,命令行参数可以分为两类,一类是短选项,一类是长选项,短选项在参数前加一杠”-“,长选项在参数前连续加两杠”--“,如下表(ls 命令参数)所示,其中-a,-A,-b都表示短选项,–all,–almost-all, –author都表示长选项。他们两者后面都可选择性添加额外参数。比如–block-size=SIZE,SIZE便是额外的参数。getopt函数只能处理短选项,而getopt_long函数两者都可以,可以说getopt_long已经包含了getopt的功能。

1. getopt()

1.1 函数说明

在 linux 下可以使用 man 3 getopt 命令查看该函数的帮助手册。

1
2
3
4
5
6
7
8
/* 需包含的头文件 */
#include <unistd.h>

/* 函数声明 */
int getopt(int argc, char * const argv[],
const char *optstring);
extern char *optarg;
extern int optind, opterr, optopt;

【函数说明】该函数用于解析命令行参数中带-的参数。

【函数参数】

  • argc :int 类型,通常由 main 函数直接传入,表示参数的数量。
  • argv :char *类型指针数组,是保存参数字符串的数组,通常也由 main 函数直接传入。
  • optstring :char *类型变量,一个包含正确的参数选项字符串,用于参数的解析。例如 “abc:”,其中 -a,-b 就表示两个普通选项,-c 表示一个必须有参数的选项,因为它后面有一个冒号。若是后边有两个冒号,那就代表参数是可选的,可选即可有可没有。这里需要注意一点 可选参数 选项 -c 后面跟参数的时候,一定不能有空格。但是如果是 必选参数,即选项后面只有一个冒号,则是有没有空格都可以。
  • 外部变量说明(使用的时候并不需要我们声明)

optarg:如果某个选项有参数,这包含当前选项的参数字符串。

optind:argv 的当前索引值。

opterr:正常运行状态下为 0。非零时表示存在无效选项或者缺少选项参数,并输出其错误信息。

optopt:当发现无效选项字符时,即 getopt() 方法返回 ? 字符,optopt 中包含的就是发现的无效选项字符。

【返回值】 getopt() 调用时会根据 optind 和 optstring 遍历选项和参数,如果找到一个选项字符,则返回该字符,同时更新全局变量 optind,optarg 以及静态变量 nextchar,以便下一次调用 getopt() 时可以继续扫描。如果 getopt() 被重复调用,它会依次返回每个选项字符。如果所有可识别的选项都被扫描过,将返回 -1,此时 optind 是第一个不是选项的 argv 元素的索引。

【使用格式】none

【注意事项】 none

1.2 使用实例1

点击查看实例

这个实例其实就是man手册中的:

test.c
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
#include <unistd.h>
#include <stdlib.h>
#include <stdio.h>

int main(int argc, char *argv[])
{
int flags, opt;
int nsecs, tfnd;

nsecs = 0;
tfnd = 0;
flags = 0;
while ((opt = getopt(argc, argv, "nt:")) != -1) {
switch (opt) {
case 'n':
flags = 1;
break;
case 't':
nsecs = atoi(optarg);
tfnd = 1;
break;
default: /* '?' */
fprintf(stderr, "Usage: %s [-t nsecs] [-n] name\n", argv[0]);
exit(EXIT_FAILURE);
}
}

printf("flags=%d; tfnd=%d; nsecs=%d; optind=%d\n",
flags, tfnd, nsecs, optind);

if (optind >= argc) {
fprintf(stderr, "Expected argument after options\n");
exit(EXIT_FAILURE);
}

printf("name argument = %s\n", argv[optind]);

/* Other code omitted */

exit(EXIT_SUCCESS);
}

在终端执行以下命令编译程序:

1
gcc test.c -Wall # 生成可执行文件 a.out 

然后,我们来执行一下,分别输入不同的参数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
hk@vm:~/1sharedfiles/test$ ./a.out 
flags=0; tfnd=0; nsecs=0; optind=1
Expected argument after options

hk@vm:~/1sharedfiles/test$ ./a.out -n
flags=1; tfnd=0; nsecs=0; optind=2
Expected argument after options

hk@vm:~/1sharedfiles/test$ ./a.out -n -t
./a.out: option requires an argument -- 't'
Usage: ./a.out [-t nsecs] [-n] name

hk@vm:~/1sharedfiles/test$ ./a.out -n -t 10
flags=1; tfnd=1; nsecs=10; optind=4
Expected argument after options

hk@vm:~/1sharedfiles/test$ ./a.out -n -t 10 -a
./a.out: invalid option -- 'a'
Usage: ./a.out [-t nsecs] [-n] nam

就是这样的:

image-20231104155410688

1.3 使用实例2

这个实例主要是验证一个可选参数,需要修改一下官方的demo:

点击查看实例
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
#include <unistd.h>
#include <stdlib.h>
#include <stdio.h>

int main(int argc, char *argv[])
{
int flags, opt;
int nsecs, tfnd;

nsecs = 0;
tfnd = 0;
flags = 0;
const char *optstring = "nt:c::";
while ((opt = getopt(argc, argv, optstring)) != -1) {
switch (opt) {
case 'n':
flags = 1;
break;
case 't':
nsecs = atoi(optarg);
tfnd = 1;
break;
case 'c':
printf("opt is c, oprarg is: %s\n", optarg);
break;
default: /* '?' */
fprintf(stderr, "Usage: %s [-t nsecs] [-n] name\n", argv[0]);
exit(EXIT_FAILURE);
}
}

printf("flags=%d; tfnd=%d; nsecs=%d; optind=%d\n",
flags, tfnd, nsecs, optind);

if (optind >= argc) {
fprintf(stderr, "Expected argument after options\n");
exit(EXIT_FAILURE);
}

printf("name argument = %s\n", argv[optind]);

/* Other code omitted */

exit(EXIT_SUCCESS);
}

我们编译后使用以下参数测试:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
hk@vm:~/1sharedfiles/test$ gcc test.c -Wall

hk@vm:~/1sharedfiles/test$ ./a.out -c hello
opt is c, oprarg is: (null)
flags=0; tfnd=0; nsecs=0; optind=2
name argument = hello

hk@vm:~/1sharedfiles/test$ ./a.out -chello
opt is c, oprarg is: hello
flags=0; tfnd=0; nsecs=0; optind=2
Expected argument after options

hk@vm:~/1sharedfiles/test$ ./a.out -c
opt is c, oprarg is: (null)
flags=0; tfnd=0; nsecs=0; optind=2
Expected argument after options

如下图所示:

image-20231105091627389

2. getopt_long()

2.1 函数说明

在 linux 下可以使用 man 3 getopt_long 命令查看该函数的帮助手册。

1
2
3
4
5
6
7
8
9
/* 需包含的头文件 */
#include <getopt.h>

/* 函数声明 */
int getopt_long(int argc, char * const argv[],
const char *optstring,
const struct option *longopts, int *longindex);
extern char *optarg;
extern int optind, opterr, optopt;

【函数说明】该函数用于解析命令行参数,长选项或者短选项都支持。

【函数参数】

  • argc 和 argv 和 main 函数的两个参数一致,都是由main函数传入。
  • optstring :char *类型,表示短选项字符串。形式如“a:b::cd:“,分别表示程序支持的命令行短选项有-a、-b、-c、-d,冒号含义如下:只有一个字符,不带冒号只表示选项, 如-c ;一个字符,后接一个冒号表示选项后面带一个参数,如-a 100;一个字符,后接两个冒号表示选项后面带一个可选参数,即参数可有可无, 如果带参数,则选项与参数之间不能有空格,形式应该如-b200。
  • longopts :struct option *类型,表示长选项结构体。
点击查看 struct option

在man手册中有,这个结构体定义在getopt.h中:

1
2
3
4
5
6
struct option {
const char *name;
int has_arg;
int *flag;
int val;
};

(1)name :表示选项的名称,比如daemon, dir, out等。

(2)has_arg :表示选项后面是否携带参数。该参数有三个不同值:no_argument(或者是0)时,参数后面不跟参数值,例如–version,–help;required_argument(或者是1)时,参数输入格式为:–参数 值 或者 –参数=值,例如-dir=/home;optional_argument(或者是2)时,参数输入格式只能为:–参数=值。

(3)flag :这个参数有两个意思,空或者非空。如果参数为空NULL,那么当选中某个长选项的时候,getopt_long将返回val值。例如可执行程序 –help,getopt_long的返回值为h;如果参数不为空,那么当选中某个长选项的时候,getopt_long将返回0,并且将flag指针参数指向val值,例如可执行程序 –http-proxy=127.0.0.1:80 那么getopt_long返回值为0,并且lopt值为1。

(4)val:表示指定函数找到该选项时的返回值,或者当flag非空时指定flag指向的数据的值val。

  • longindex :int *类型,longindex非空,它指向的变量将记录当前找到参数符合longopts里的第几个元素的描述,即是longopts的下标值。
  • 外部变量说明(使用的时候并不需要我们声明)

(1)optarg:表示当前选项对应的参数值。

(2)optind:表示的是下一个将被处理到的参数在argv中的下标值。

(3)opterr:如果opterr = 0,在getopt、getopt_long、getopt_long_only遇到错误将不会输出错误信息到标准输出流。opterr在非0时,向屏幕输出错误。

(4)optopt:表示没有被未标识的选项。

【返回值】

(1)如果短选项找到,那么将返回短选项对应的字符。

(2)如果长选项找到,如果flag为NULL,返回val。如果flag不为空,返回0

(3)如果遇到一个选项没有在短字符、长字符里面。或者在长字符里面存在二义性的,返回“?”

(4)如果解析完所有字符没有找到(一般是输入命令参数格式错误,例如,连斜杠都没有加的选项),返回“-1”

(5)如果选项需要参数,忘了添加参数。返回值取决于optstring,如果其第一个字符是“:”,则返回“:”,否则返回“?”。

【使用格式】none

【注意事项】

(1)longopts的最后一个元素必须是全0填充,否则会报段错误。

(2)短选项中每个选项都是唯一的。而长选项如果简写,也需要保持唯一性。

2.2 使用实例

点击查看实例

这个实例其实是man手册中提供的:

test.c
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
#include <stdio.h>     /* for printf */
#include <stdlib.h> /* for exit */
#include <getopt.h>

int main(int argc, char **argv)
{
int c;
int digit_optind = 0;

while (1)
{
int this_option_optind = optind ? optind : 1;
int option_index = 0;
static struct option long_options[] = {
{"add", required_argument, 0, 0 },
{"append", no_argument, 0, 0 },
{"delete", required_argument, 0, 0 },
{"verbose", no_argument, 0, 0 },
{"create", required_argument, 0, 'c'},
{"file", required_argument, 0, 0 },
{0, 0, 0, 0 }
};

c = getopt_long(argc, argv, "abc:d:012", long_options, &option_index);
if (c == -1)
break;

switch (c)
{
case 0:
printf("option %s", long_options[option_index].name);
if (optarg)
{
printf(" with arg %s", optarg);
}
printf("\n");
break;

case '0':
case '1':
case '2':
if (digit_optind != 0 && digit_optind != this_option_optind)
{
printf("digits occur in two different argv-elements.\n");
}
digit_optind = this_option_optind;
printf("option %c\n", c);
break;
case 'a':
printf("option a\n");
break;

case 'b':
printf("option b\n");
break;

case 'c':
printf("option c with value '%s'\n", optarg);
break;

case 'd':
printf("option d with value '%s'\n", optarg);
break;

case '?':
break;

default:
printf("?? getopt returned character code 0%o ??\n", c);
break;
}
}

if (optind < argc) {
printf("non-option ARGV-elements: ");
while (optind < argc)
printf("%s ", argv[optind++]);
printf("\n");
}

exit(EXIT_SUCCESS);
}

在终端执行以下命令编译程序:

1
gcc test.c -Wall # 生成可执行文件 a.out 

然后,我们按以下参数测试:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
hk@vm:~/1sharedfiles/test$ ./a.out -0
option 0

hk@vm:~/1sharedfiles/test$ ./a.out -a
option a

hk@vm:~/1sharedfiles/test$ ./a.out -c200
option c with value '200'

hk@vm:~/1sharedfiles/test$ ./a.out --add
./a.out: option '--add' requires an argument

hk@vm:~/1sharedfiles/test$ ./a.out --add 10
option add with arg 10

用得不多,但是这里有个参考就行,后边用到了再详细学习。