在C语言中,命令行参数`argv`的类型是什么?

18
我正在阅读C Primer Plus一篇关于命令行参数argv的文章,我不太理解这个句子。
它说:
程序将命令行字符串存储在内存中,并将每个字符串的地址存储在指针数组中。该数组的地址存储在第二个参数中。按照惯例,这个指向指针的指针被称为argv,表示参数值。
这是否意味着命令行字符串以指向char数组的指针数组的形式存储在内存中?

1
这是否意味着命令行字符串在内存中被存储为指向字符数组的指针数组?是的。我认为整个混淆是由于“程序将命令行字符串存储在内存中...”引起的;关键是所有这些都发生在main()被调用之前。Main()只是一个函数,它带有两个参数:一个int和一个指向字符串指针数组的指针。 - joop
1
@joop 如果我们要严谨一点,argv并不是“指向字符串指针数组的指针”。整个问题实际上是关于“指向数组”和“指向数组第一个元素的指针”的区别。 - M.M
@joop 这是一个“语言律师”问题,这意味着它涉及标准C,在其中没有“crt0”,并且参数的设置并不重要,只要argv按照C标准规定的方式运行即可。 - M.M
@joop:在Linux(以及其他使用SysV ABI的操作系统)中,argv数组在进程启动时以适合传递给main的引用格式存储在内存中。因此,crt0 libc启动代码除了将指针传递给main()之外,不必对argv做任何处理。在Linux中,内核将argv和环境块放置在用户空间堆栈的顶部。System V ABI的x86版本在线上可以查看 - Peter Cordes
另一个候选项是 *什么是 int argc, char *argv[]?*。 - Peter Mortensen
显示剩余3条评论
7个回答

27

argv 是一个 char ** 类型的指针。它不是一个数组。它是一个指向指向 char 的指针的指针。命令行参数存储在内存中,每个内存位置的地址存储在数组中。这个数组是一个指向 char 的指针数组。argv 指向这个数组的第一个元素。

                  一些
                  数组
+-------+ +------+------+-------------+------+ argv ----------> | | | | | | | | 0x100 +------> | | | . . . . . . | | 程序名称1 0x900 | | | | | | | | | +------+------+-------------+------+ +-------+ 0x100 0x101 | | +------+------+-------------+------+ | 0x205 | | | | | | Arg1 0x904 | +------> | | | . . . . . . | | | | . | | | | | +-------+ +------+------+-------------+------+ | . | . 0x205 0x206 | . | | . | . | . | +-------+ . +------+------+-------------+------+ | | | | | | | Argargc-1 | 0x501 +------> | | | . . . . . . | | | | | | | | | +-------+ +------+------+-------------+------+ | | 0x501 0x502 | NULL | | | +-------+
0xXXX 表示内存地址

1. 在大多数情况下,argv [0] 表示程序名称,但如果从主机环境中无法获取程序名称,则 argv [0] [0] 表示空字符。


1
@SouravGhosh; 好的。最终 argv 指向一个 char * 数组的第一个元素,这可能是标准将其在这种特定情况下称为数组的原因。但 argv 的类型是 char ** - haccks
2
非常有用的ASCII模式! - Tim
3
作为一点小问题,"some array" 应该再多一个元素来存储一个空指针。 - jamesdlin
3
值得注意的是:argv [0] 不保证保存程序名称(但大多数情况下确实如此),而只保存代表程序名称的信息:https://dev59.com/SnI-5IYBdhLWcg3wBjhR - Daniel Jour
@DanielJour:“表示”这个词不是因为文件名可以用其他方式表示而存在的吗(例如,NTFS使用UTF-16编码),他们需要指定在这里使用的哪种表示法?比“表示”这个词更重要的问题是,“用于调用程序的名称”并不是非常具体——它不一定是文件名(例如Unix登录shell),即使是,也没有人说过可能已经使用了哪个目录或文件名扩展名来解析它。或者它可能只是"" - SamB
显示剩余9条评论

17
直接引用C11,第5.1.2.2.1/p2章节的程序启动部分(我加粗了部分内容):
```C++ int main(int argc, char *argv[]) { /* ... */ } // 如果argc的值大于零,数组成员argv[0]到argv[argc-1](含)必须包含指向字符串的指针 ```
另外,还有一句话:
```C++ // 以及argv数组指向的字符串... ```
所以,基本上,`argv`是一个指向字符串数组第一个元素的指针。可以使用“替代形式”来更清晰地表达这个概念:
```C++ int main(int argc, char **argv) { /* ... */ } // 可以重述为指向空字符结尾的char数组的第一个元素的指针数组,但我更喜欢使用字符串。 ```
注意:
以上答案中提到的“指向数组第一个元素的指针”用法,请参考§6.3.2.1/p3:
```C++ // 除非它是sizeof运算符、_Alignof运算符或一元&运算符的操作数,或者是用于初始化数组的字符串字面量, // 否则具有类型“类型数组”的表达式将转换为具有类型“指向该数组对象的初始元素的指针”的表达式, // 并且不是左值。 ```

@Jin 字符串初始元素的地址。 - Sourav Ghosh
1
由于数组被传递给函数(main),因此它会折叠成一个指针。所以argv是一个指向数组的指针。尝试使用sizeof argvargv++,你会发现argv是一个指针。 - Klas Lindbäck
@KlasLindbäck 好的,我追加了答案以消除任何困惑。 :) - Sourav Ghosh
@KlasLindbäck; 所以argv是指向数组的指针:不是的。argv是一个指向指向char的指针的指针。 - haccks
@SouravGhosh 字符串是可以存储在数组中的一些内容。字符串不是一个数组(反之亦然)。 - M.M
显示剩余12条评论

11

这个主题太混乱了,以下是情况:

  • 有一个类型为char *的数组,它有argc+1个元素。
  • argv指向该数组的第一个元素。
  • 还有argc个其他类型为char且长度各异的数组,包含表示命令行参数的空终止字符串。
  • 指针数组的每个元素都指向char数组中的第一个字符;除了指针数组的最后一个元素是空指针。

有时人们会写“指向X类型数组的指针”,意思是“指向X类型数组的第一个元素的指针”。你必须使用上下文和类型来确定他们是否实际上是这个意思。


这个问题肯定有一个完全相同的副本存在。 - Peter Mortensen

1

是的,完全正确。

argvchar** 或者 char*[],或者说是 char* 指针的数组。

因此,argv[0] 是一个 char*(字符串),而 argv[0][0] 则是一个 char


1
@blue112 这样描述很容易让人感到困惑。还有一种存储方式是 "a list of\0strings like\0so\0",它是一个以零结尾的字符串数组。这 argv 的工作方式,但它 被用于 (例如在 Linux 内核的 cmdline procfile 中)。char* 不是一个字符串。 - user824425
1
@haccks 它指向一个 char 指针数组的第一个元素。 - M.M
argv不是“仅仅是char*指针的数组”,这种说法很误导人,请考虑将该部分从您的回答中删除。 - einpoklum
@Rhymoid 标准允许命令行参数以这种方式在内存中布局,每个指针指向下一个字符串的开头等。 - M.M
@M.M 我相信它确实如此,但这并不是 argv 本身的含义。 - user824425
显示剩余7条评论

0
是的。 argv 的类型是 char**,即指向 char 指针的指针。基本上,如果将 char* 视为字符串,则 argv 是指向字符串数组的指针。

字符串不一定要在数组中,但指向它们的指针必须在数组中。此外,指针数组的末尾是一个空指针。 - Marichyasana
argv 的类型是 char**,即指向字符数组指针的数组的指针。不行,绝对不行。 - haccks
@haccks 能详细说明一下吗? - Peter K
char ** 读作 指向字符指针的指针,而不是 指向指针数组的数组指针。这使得 argv 的类型为 char *((*)[])[] - haccks
@haccks 对不起,我本来想让它更直观一些,但实际上是完全错误的。 - Peter K

0

严格来说,argv必须具备一些属性才能成为数组。让我们考虑其中的一些:

¹/ 不能有一个由空指针指向的数组,因为空指针保证是与任何对象的地址不同。因此,在下面的代码中,argv不能是一个数组:

#include <assert.h>
int main(int argc, char *argv[]) {
    if (argv) return main(0, 0);
    assert(argv == 0); // argv is a null pointer, not to be dereferenced
}

²/ 给数组赋值是无效的。例如,char *argv[] = { 0 }; argv++; 是一个约束违规,但是 int main(int argc, char *argv[]) { argv++; } 可以编译并运行良好。因此,我们必须从这一点得出结论,即在声明为参数时,argv 不是一个数组,而是一个(可能)指向数组的指针。(这实际上与第1点相同,但从不同的角度来看,因为使用空指针作为 argv 调用 main 实际上是重新分配了 argv,这是我们不能对数组做的事情)。

³/ ... 正如 C 标准所说:

sizeof 运算符的另一个用途是计算数组中元素的数量:sizeof array / sizeof array[0]

例如:

#include <assert.h>
int main(int argc, char *argv[]) {
    size_t size = argc+1; // including the NULL
    char *control[size];
    assert(sizeof control / sizeof *control == size); // this test passes, because control is actually an array
    assert(sizeof argv   / sizeof *argv    == size); // this test fails for all values of size != 1, indicating that argv isn't an array
}

⁴/ 一元的 & 取地址运算符被定义为,当应用于数组时,将产生不同类型的相同值,例如:

#include <assert.h>
int main(int argc, char *argv[]) {
    char *control[42];
    assert((void *) control == (void *) &control); // this test passes, because control is actually an array
    assert((void *) argv    == (void *) &argv); // this test fails, indicating that argv isn't an array
}

-1

argv 是一个指向字符指针的数组。

以下代码显示了 argv 的值,argv 的内容,并对由 argv 内容指向的内存执行内存转储。希望这能阐明间接寻址的含义。

#include <stdio.h>
#include <stdarg.h>

print_memory(char * print_me)
{
    char * p;
    for (p = print_me; *p != '\0'; ++p)
    {
        printf ("%p: %c\n", p, *p);
    }

    // Print the '\0' for good measure
    printf ("%p: %c\n", p, *p);

}

int main (int argc, char ** argv) {
    int i;

    // Print argv
    printf ("argv: %p\n", argv);
    printf ("\n");

    // Print the values of argv
    for (i = 0; i < argc; ++i)
    {
        printf ("argv[%d]: %p\n", i, argv[i]);
    }
    // Print the NULL for good measure
    printf ("argv[%d]: %p\n", i, argv[i]);
    printf ("\n");

    // Print the values of the memory pointed at by argv
    for (i = 0; i < argc; ++i)
    {
        print_memory(argv[i]);
    }

    return 0;
}

示例运行:

$ ./a.out Hello World!
argv: ffbfefd4

argv[0]: ffbff12c
argv[1]: ffbff134
argv[2]: ffbff13a
argv[3]: 0

ffbff12c: .
ffbff12d: /
ffbff12e: a
ffbff12f: .
ffbff130: o
ffbff131: u
ffbff132: t
ffbff133:
ffbff134: H
ffbff135: e
ffbff136: l
ffbff137: l
ffbff138: o
ffbff139:
ffbff13a: W
ffbff13b: o
ffbff13c: r
ffbff13d: l
ffbff13e: d
ffbff13f: !
ffbff140:

$

你有一个从 ffbff12cffbff140 这么大的连续数组,其中包含命令行参数(这不是标准保证的连续性,但通常是这样做的)。argv 只包含指向该数组的指针,因此您知道在哪里查找单词。 argv 是一个指向指针的指针,指向字符。

它不在C或POSIX标准中,但是根据System V ABI标准,它可能保证是连续的。 - Random832
https://software.intel.com/sites/default/files/article/402129/mpx-linux64-abi.pdf的第34页描述了该数组需要和不需要的详细信息:“参数字符串、环境字符串和辅助信息在信息块中没有特定顺序,并且它们不需要紧凑地分配。”[但是,“信息块”本身是一个定义明确的内存顶部区域,被定义为包含所有字符串]。显然,这只适用于符合此标准的系统。 - Random832

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接