Skip to content

LV080-main函数的参数

一、带参数的 main 函数

1. 两个参数

前面我们知道,有一种带参数的主函数形式,那么这两个参数是什么呢?

c
/* 带参数形式 */
int main(int argc, char *argv[]) 
{
    语句块;
    
    return 0;
}
argc 整型变量,表示了命令行中参数的个数(注意:文件名本身也算一个参数)
argv 字符串型变量,它是一个指向字符串的指针数组。命令行中的每个字符串都会被存储到内存中,并且分配一个指针指向它。按照惯例,这个指针数组被称为 argv(argument value),系统会使用空格把各个字符串格开。一般情况下,argv [0] 是程序本身的名称,后边依次是传入的参数。
### 2. 怎么赋值?

main 函数不能被其它函数调用, 因此不可能在程序内部取得实际值。那么,在何处把参数值赋予 main 函数呢?

实际上, main 函数的参数值是从操作系统命令行上获得的。当我们要运行一个可执行文件时,在命令行键入文件名,再输入实际参数即可把这些实参传送到 main 的参数中去。例如,在 linux 的 Shell 终端下可以这样:

shell
./file_name arg1 arg2 ...

可以看一个实例:

c
#include <stdio.h>

int main(int argc, const char *argv[]) 
{

	int i = 0;
	printf("argc = %d\n", argc);
	for(i = 0; i < argc; i++)
		printf("argv[%d] = %s\n", i, argv[i]);

	return 0;
}

然后在命令行执行:

shell
gcc test.c -Wall # 编译程序
./a.out a 1 3 "Hello World !" # 执行可执行文件

会看到终端中有如下输出信息:

shell
argc = 5
argv[0] = ./a.out
argv[1] = a
argv[2] = 1
argv[3] = 3
argv[4] = Hello World !

【注意】

(1)各个参数之间是通过空格隔开,我们要是想输入带空格的参数,可以使用 " " 包裹。

(2)在命令行的输入都将作为 字符串 形式存储于内存中。所以,即便参数是数字,也会被当做字符串,确实需要使用数字,还需注意做一下转换。

二、envp 参数

1. 一道多选题

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

A. argc

B. envp

C. main

D. argv

毫无疑问,我上来就选了 AD 两个选项,因为这两个我们很常见,但其实 B 选项也是。这个题目正确答案是 ABD。这个 envp 是啥?

2. 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,原型就是:

c
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 函数其实就是由它来调用,它的第一个参数为:

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

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

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

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

image-20231102220338587

3. 使用实例

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

c
#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 可执行文件,然后这样运行:

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

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

shell
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 命令打印出来的环境变量基本一致。

三、命令行参数解析

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

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

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

c
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 命令查看该函数的帮助手册。

c
/* 需包含的头文件 */
#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 手册中的:

c
#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);
}

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

shell
gcc main.c -Wall # 生成可执行文件 a.out

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

shell
sumu@virtual-machine:~/hk/alpha$ gcc main.c -Wall
sumu@virtual-machine:~/hk/alpha$ ./a.out 
flags=0; tfnd=0; nsecs=0; optind=1
Expected argument after options

sumu@virtual-machine:~/hk/alpha$ ./a.out -n
flags=1; tfnd=0; nsecs=0; optind=2
Expected argument after options

sumu@virtual-machine:~/hk/alpha$ ./a.out -n -t
./a.out: option requires an argument -- 't'
Usage: ./a.out [-t nsecs] [-n] name

sumu@virtual-machine:~/hk/alpha$ ./a.out -n -t 10
flags=1; tfnd=1; nsecs=10; optind=4
Expected argument after options

sumu@virtual-machine:~/hk/alpha$ ./a.out -n -t 10 -a
./a.out: invalid option -- 'a'
Usage: ./a.out [-t nsecs] [-n] name

就是这样的:

image-20260228112239245

1.3 使用实例 2

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

c
#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);
}

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

shell
sumu@virtual-machine:~/hk/alpha$ gcc main.c -Wall
sumu@virtual-machine:~/hk/alpha$ ./a.out -c hello
opt is c, oprarg is: (null)
flags=0; tfnd=0; nsecs=0; optind=2
name argument = hello

sumu@virtual-machine:~/hk/alpha$ ./a.out -chello
opt is c, oprarg is: hello
flags=0; tfnd=0; nsecs=0; optind=2
Expected argument after options

sumu@virtual-machine:~/hk/alpha$ ./a.out -c
opt is c, oprarg is: (null)
flags=0; tfnd=0; nsecs=0; optind=2
Expected argument after options

如下图所示:

image-20260228170110958

2. getopt_long()

2.1 函数说明

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

c
/* 需包含的头文件 */
#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 *类型,表示长选项结构体。在 man 手册中有 struct option 的定义,这个结构体定义在 getopt.h 中。
c
struct option {
   const char *name;
   int         has_arg;
   int        *flag;
   int         val;
};
name 表示选项的名称,比如 daemon, dir, out 等。
has_arg表示选项后面是否携带参数。该参数有三个不同值:no_argument(或者是 0)时,参数后面不跟参数值,例如--version,--help;required_argument(或者是 1)时,参数输入格式为:--参数 值 或者 --参数 = 值,例如-dir =/home;optional_argument(或者是 2)时,参数输入格式只能为:--参数 = 值。
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。
val 表示指定函数找到该选项时的返回值,或者当 flag 非空时指定 flag 指向的数据的值 val。
  • longindex :int *类型,longindex 非空,它指向的变量将记录当前找到参数符合 longopts 里的第几个元素的描述,即是 longopts 的下标值。
  • 外部变量说明(使用的时候并不需要我们声明)
optarg表示当前选项对应的参数值。
optind表示的是下一个将被处理到的参数在 argv 中的下标值。
opterr如果 opterr = 0,在 getopt、getopt_long、getopt_long_only 遇到错误将不会输出错误信息到标准输出流。opterr 在非 0 时,向屏幕输出错误。
optopt表示没有被未标识的选项。

返回值

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

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

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

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

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

使用格式】 none

注意事项

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

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

2.2 使用实例

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

c
#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);
}

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

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

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

shell
sumu@virtual-machine:~/hk/alpha$ gcc main.c -Wall
sumu@virtual-machine:~/hk/alpha$ ./a.out  -0
option 0

sumu@virtual-machine:~/hk/alpha$ ./a.out  -a
option a

sumu@virtual-machine:~/hk/alpha$ ./a.out  -c200
option c with value '200'

sumu@virtual-machine:~/hk/alpha$ ./a.out  --add
./a.out: option '--add' requires an argument

sumu@virtual-machine:~/hk/alpha$ ./a.out  --add 10
option add with arg 10

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

image-20260228113627789