为什么C语言中的argv(参数向量)被定义为指针,为什么需要将其零位定义为程序名称?

3
#include <stdio.h>
int main(int argc, char *argv[])
{
 int i;
 for(i=1;i<argc;i++)
  printf("%s%s", argv[i], (i<argc-1)? " ":"");
 printf("\n");
 return 0;
} 

上面给出了一个简单的C程序,用于输出命令行输入。这里的argc是参数计数器。argv被称为包含参数的数组。我的问题是:为什么要将其定义为字符数组的指针而不是普通数组?还有,为什么需要将它的零号元素(argv [0])定义为调用程序的名称。
我是一名初学者,请从高层次的角度解释一下。

3
如果char *表示C风格字符串,那么最好称之为“指向字符的指针数组”。换句话说,如果char * x[],则意味着x是一个C风格字符串的数组。 - Brandin
回答你的另一个问题,让你的循环从 i=0 开始,然后尝试以不同的方式从 shell 中调用你的命令,例如 ./foo bar baz/path/to/foo bar baz,并进行实验,看看你的环境是否会传输 argv[0] 时有任何差异。 - Brandin
如果 char *x[] 是一个参数声明,它意味着 x 是一个指向指针的指针,它可能指向 char* 指针数组的一个元素,也可能不是,并且这些 char* 指针可能或可能不指向 C 风格字符串;参数声明并未指定。 - Keith Thompson
@KeithThompson 是的。这就是为什么在声明中可以等价地写char **x。是的,char *并不自动意味着C风格的字符串。此外,有趣的是,C风格字符串的定义没有提供任何可靠的方法来测试某个东西是否是C风格字符串。我不确定它是否在C标准或POSIX中,但我认为在argv的特殊情况下,规范实现中必须包含一个C风格字符串数组,其最后一个元素的值为(char *)0 - Brandin
这个视频解释了一切:http://www.youtube.com/watch?v=gRdfX7ut8gw - Hot Licks
显示剩余2条评论
4个回答

4

argv 被定义为指针而不是数组,因为在 C 语言中不存在数组参数。

你可以定义类似于数组参数的东西,但它会在编译时被“调整”为数组类型;例如,这两个声明是完全等价的:

int foo(int param[]);
int foo(int param[42]); /* the 42 is quietly ignored */
int foo(int *param);    /* this is what the above two declarations really mean */

main的定义可以写成以下两种形式之一:

int main(int argc, char *argv[]) { /* ... */ }

或者作为
int main(int argc, char **argv) { /* ... */ }

两者完全等价(在我看来,第二个更清楚地表达了实际发生的情况)。
在C中,数组类型在某种意义上是次要的类型。几乎总是通过指向元素的指针来操作数组的代码,执行指针算术以遍历元素。 comp.lang.c FAQ的第6节解释了数组和指针之间经常令人困惑的关系。
(如果有人告诉你数组实际上是指针,那么他们是错的;数组和指针是不同的东西。)
至于为什么argv [0]指向程序名称,那只是因为它很有用。一些程序在错误消息中打印它们的名称;其他程序可能会根据它们被调用的名称改变它们的行为。将程序名称与命令行参数捆绑在一起是一个相当任意的选择,但它很方便并且有效。

3

char *argv[]是一个指针,它已经衰变为一个char *数组。例如,调用如下命令:

$ ./command --option1 -opt2 input_file

可以视为:

char *argv[] = {
    "./command",
    "--option1",
    "-opt2",
    "input_file",
    NULL,
};
main(4, argv);

基本上在main函数之外有一个字符串数组,并且它在main函数中传递给你:

    char *argv[]
    \- --/     ^
      V        |
      |   It was an array
      |
of strings

关于argv[0]作为调用命令的原因,主要是历史原因。我不知道最初想到这个的人是怎么想的,但至少有一个用处。

想象一下一个程序,比如vimgawk。这些程序可能会安装符号链接(如viawk),它们指向同一个程序。因此,运行vimvi(或类似地运行gawkawk)可能会执行完全相同的程序。然而,通过检查argv [0],这些程序可以知道它们是如何被调用的,并可能进行相应的调整。

据我所知,我上面提到的这两个程序实际上都没有这样做,但它们可以这样做。例如,通过名为vi的符号链接调用的vim可以打开一些兼容性选项。或者,以awk的方式调用gawk可以关闭一些GNU扩展。在现代世界中,如果他们想要这样做,他们可能会创建脚本来提供正确的选项。


char *argv[](作为参数)与 char** 完全等价,它不是指向 char* 数组的指针;它是指向 char* 数组的第一个元素的指针。指向 char* 和指向 char* 数组的指针具有不同的类型。 - Keith Thompson
无论是作为 vimviewvimdiffrviewrvim(或者可能作为 vi)被调用,vim 的行为都会有所不同。 - Jonathan Leffler
"char *argv[]是一个指向char *数组的指针。" - 我不会这样说。调用main的实体甚至不一定是用C语言编写的。一个数组表达式会衰变成为指向其第一个元素的指针,但并不一定有任何数组表达式。有两个不同的规则在起作用:数组类型的参数在编译时被调整为指针类型的参数,而数组类型的表达式在运行时被转换(“衰变”)为指针值。 - Keith Thompson
@KeithThompson:没有异议,双关语未打算。 - Crowman
@KeithThompson,未定义行为并不意味着库禁止这样做。它意味着标准没有强制要求使用malloc,但也没有禁止。实际上,glibc的实现在execvpe中根据配置使用allocamalloc - Shahbaz
显示剩余10条评论

2

你所提出的问题最好的回答是“根据定义”。也就是说,这是一个由委员会设计商定了一套规则。

C11标准中提到:(请看加粗部分)

5.1.2.2.1 程序启动
1程序启动时调用的函数名为main。实现不声明此函数的原型。它应该定义为返回类型为int且没有参数的格式: int main(void) { /* ... */ } 或者带有两个参数(在本处称为argc和argv,尽管可以使用任何名称,因为它们是在声明它们的函数中局部的): int main(int argc, char argv[]) { / ... */ } 或等效形式;10)或以其他某种实现定义的方式。
2如果声明了它们,则主函数的参数必须遵守以下约束:
— argc的值应为非负数。
— argv[argc]应为null指针。
— 如果argc的值大于零,则数组成员argv [0]到argv [argc-1](含)应包含指向字符串的指针,在程序启动之前由主机环境赋予实现定义的值。
意图是提供在程序启动之前从托管环境中确定的信息给程序。如果主机环境无法提供具有大写字母和小写字母的字符串,则实现应确保字符串以小写形式接收。
— 如果argc的值大于零,则argv [0]指向的字符串表示程序名称;如果程序名称不可从主机环境获取,则argv [0] [0]应为null字符。如果argc的值大于1,则argv [1]到argv [argc-1]指向的字符串表示程序参数。
— 参数argc和argv以及由argv数组指向的字符串应该可以被程序修改,并在程序启动和程序终止之间保留其最后存储的值。


2
这不是回答问题的最佳方式,因为它暗示我们应该无条件服从委员会,而且它没有解释委员会为什么这样做。开发C标准的委员会有他们的理由,有相关文档记录,让人们了解语言设计的原因是很有用的。 - Eric Postpischil
1
有时候理解某些决定是为了标准化而做出的,可能并没有太大意义,这对我非常有帮助。就是这样 - 继续前进。 - spemble

1
因为在C语言中,数组元素的大小必须在编译时确定,所以它不被定义为普通数组。虽然char *的大小是已知的,但你的参数的大小(长度)却不是。 argv[0] 包含被调用进程的名称,因为可以通过任意名称来调用它,例如exec系列的调用可以指定它想要的内容,而你可以通过符号链接来调用程序。 argv [0]允许程序根据调用名称提供不同的功能。

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