C++ - char** argv与char* argv[]的区别

67
int main(int argc, char** argv)int main(int argc, char* argv[]) 中,char** argvchar* argv[] 有什么区别?
它们是相同的吗?特别注意第一个部分没有 []
8个回答

70
它们完全等价。char *argv[] 必须被理解为指向 char 的指针数组,并且数组参数会被降级为指针,因此是指向指针的指针,或者说是 char **
C 语言中也是一样的。

谢谢您的回复。您能否更详细地解释一下这个问题:“...并且数组参数被降级为指针,因此指向指针的指针或char **。” - Simplicity
5
这是一条语言规则。当你使用语法 X foo(Y a[]) 声明或定义一个函数时,它实际上变成了 X foo(Y *a)。看似传递给函数的数组参数实际上是一个指针。因为 argv 被声明为一个指针数组,所以它变成了一个指向指针的指针。 - Fred Foo
“pointer to char” 部分对于 char** argv 是清晰的。但是,第一个指针从哪里来? - Simplicity
3
让我们使用“typedef char *String”这个语句,使得“String”成为指向“char”的指针的别名。现在可以将“argv”声明为“String argv[]”,表示一个“String”类型的数组。“argv”作为参数时,实际上是指向“String”的指针,因此是“String *argv”。我希望这样能更清楚地解答你的问题,但我并不是很理解你的问题。 - Fred Foo
所以,如果我理解正确,我们可以这样说:
  1. 我们有一个指向数组的指针(*argv与argv[]相同)。
  2. 在该数组中,我们有几个指向字符的指针。
- Allen

51

它们确实完全相同。

记住数组的黄金规则:

"数组的名称是指向数组第一个元素的指针。"

因此,如果您声明以下内容:

char text[] = "一串字符。";

那么变量"text"是指向刚刚声明的字符数组中第一个字符的指针。换句话说,"text"的类型为char *。当您使用[index]访问数组元素时,实际上是将偏移量index 添加到指向数组第一个元素的指针中,然后解除引用这个新指针。因此,以下两行代码将初始化这两个变量为't':

char thirdChar = text[3];
char thirdChar2 = *(text+3);

使用方括号是语言提供的一种便利方式,可以使代码更易读。但是当您开始考虑更复杂的事情时,例如指向指针的指针,这种工作方式非常重要。char** argvchar* argv[]相同,因为在第二种情况下,“数组的名称是指向数组中第一个元素的指针”。
从这里,您还应该能够看到为什么数组索引从0开始。指向第一个元素的指针是数组的变量名(再次强调黄金法则)加上偏移量...没有偏移量!
我和我的朋友进行了辩论,讨论哪个更好在这里使用。使用char* argv[]符号表示可能更清楚地表明这实际上是一个“字符指针数组”,而不是char** argv符号,后者可以被解读为“指向字符的指针的指针”。我的观点是,后一种符号对于读者传达的信息不如前一种清晰明了。
了解它们完全相同很好,但是为了可读性,我认为如果意图是指针数组,则char* argv[]符号可以更清晰地传达这一点。

3
"需要记住的数组黄金法则"绝对不是你所写的内容。这是一个可怕、令人沮丧的误解,却始终无法消失。一个数组的名称可以“退化”为指向该数组第一个元素的指针。这与等价或身份绝对不同。 - Lightness Races in Orbit
我不知道你在说什么。:S - James Bedford
1
@JamesBedford:他的意思是数组的名称不是指向第一个元素的指针。在某些情况下,例如函数调用,它会衰变为这样的指针,这就是这里发生的事情。 - Nick Matteo
2
@JamesBedford:不,这并不是编译时与运行时的区别。像 int x[10] 这样的数组是一个由 10 个整数组成的堆栈变量。(当它的封闭作用域被放在堆栈上时,它的帧包括那 10 个整数的空间。)它不是一个指针,指针是一个包含某个东西地址的变量。制作数组时没有存储地址,而制作指针时会存储地址。但是,当您将数组传递给函数时,它会 _衰减_;函数接收到的是指针,而不是数组。 - Nick Matteo
2
考虑以下情况:当您获取数组的地址&x时,您实际上是获取了数组中数据的地址。如果x是一个指针,那么您将获得一个指向数据的指针所在内存位置的地址,但实际上并不是这样。 - Nick Matteo
显示剩余5条评论

5

该链接是指向C FAQ,但问题标记为C ++。 - Fred Foo
1
C++是C的扩展... 因此所有适用于C的内容都与C++相关。 - James Bedford
1
不是这样的。在C语言中有一些非法的操作,在C++中则是允许的,因此当某些资料说“这是非法的...”并不意味着这也适用于C++。 - Mephane
1
C和C++在这个方面非常相似。 - David Cournapeau
2
"char* argv[]: 指向数组的指针。" “T* param[]” 在语法层面上被重写为“T** param”。您可以将一个与数组完全无关的“T**”传递到该函数中。" - Lightness Races in Orbit
你总是希望从右到左阅读指针类型。因此,char *argv[] 的读法如下:字符指针数组,而不是“数组指针”。 - prd

4

实际上,它们是相同的。这是由于C/C++处理作为参数传递的数组时,数组会衰减为指针。


3

括号形式只在语句声明中有用,例如:

char *a[] = {"foo", "bar", "baz"};
printf("%d\n", sizeof a / sizeof *a);
// prints 3

因为在编译时就知道数组的大小。当您将括号形式作为参数传递给函数(main函数或其他函数)时,编译器无法知道数组在运行时的大小,因此它与char **a完全相同。我更喜欢使用char **argv,因为这样更清晰,sizeof无法像在语句声明形式上那样工作。

-1
在C和C++中,TYPE * NAMETYPE NAME[]之间存在差异。在C++中,这两种类型是不可互换的。例如,以下函数在C++中是非法的(会出现错误),但在C中是合法的(会出现警告):
int some (int *a[3]) // a is array of dimension 3 of pointers to int
{
    return sizeof a;
}

int main ()
{
    int x[3][3];
    std::cout << some(x)<< std::endl;
    return 0;
}

为了使其合法,只需将签名更改为int some (int (*a)[3])(指向3个整数的数组指针)或int some (int a[][3])。最后一个方括号中的数字必须等于参数的数量。从数组的数组转换为指针的数组是不合法的。从指向指针的指针转换为数组的数组也是不合法的。但是将指向指针的指针转换为指向指针的数组是合法的!

因此,请记住:只有最接近解引用类型的签名不重要,其他签名都很重要(当然是在指针和数组的上下文中)。

考虑我们有一个指向指向int的指针a:

int ** a;
&a     ->     a    ->    *a    ->    **a
(1)          (2)         (3)          (4)
  1. 你不能改变这个值,类型是int ***。可以被函数作为int **b[]int ***b使用。最好的方式是int *** const b
  2. 类型是int **。可以被函数作为int *b[]int **b使用。数组声明的括号可以留空或包含任何数字。
  3. 类型是int *。可以被函数作为int b[]int *b甚至void *b使用。
  4. 应该作为int参数传递。我不想深入细节,比如隐式构造函数调用。
回答你的问题:在主函数中,参数的真实类型是char ** argv,因此它可以很容易地表示为char *argv[](但不是char (*argv)[])。同时,main函数中的argv名称可以安全更改safely。你可以轻松检查它:std::cout << typeid(argv).name();(PPc = 指向字符指针)。
顺便说一下:有一个很酷的特性,可以将数组作为引用传递:
void somef(int (&arr)[3])
{
    printf("%i", (sizeof arr)/(sizeof(int))); // will print 3!
}

此外,函数可以将任何指针隐式地接受(转换)为void指针。但只能是单个指针(而不是指向指针等)。

更多阅读:

  1. Bjarne Stroustrup,《C++》第7.4章
  2. C指针FAQ

1
在C和C++中,TYPE * NAME和TYPE NAME[]之间存在差异。这是无稽之谈。在C++中,这两种类型并不可互换。实际上只有第一种类型存在。例如,以下函数是非法的(您将会收到一个错误)。这是因为int* ar[3]和int* ar[]是完全不同的东西。这些错误信息不断地传播... - Lightness Races in Orbit
clothest 是什么意思? - Dan Bechard
@Dan,我是说最近的,抱歉。 - yanpas
@yanpas 如果您愿意,可以编辑您的答案进行更正。 - Dan Bechard

-1
这是我想出来的一个简单示例,其中有两个函数(Main_1、Main_2),它们接受与主函数相同的参数。

希望这能澄清事情..

#include <iostream>

void Main_1(int argc, char **argv)
{
    for (int i = 0; i < argc; i++)
    {
        std::cout << *(argv + i) << std::endl;
    }
}

void Main_2(int argc, char *argv[])
{
    for (int i = 0; i < argc; i++)
    {
        std::cout << *(argv + i) << std::endl;
    }
}

int main()
{

    // character arrays with null terminators (0 or '\o')
    char arg1[] = {'h', 'e', 'l', 'l', 'o', 0};
    char arg2[] = {'h', 'o', 'w', 0};
    char arg3[] = {'a', 'r', 'e', '\0'};
    char arg4[] = {'y', 'o', 'u', '\n', '\0'};

    // arguments count
    int argc = 4;

    // array of char pointers (point to each character array (arg1, arg2, arg3 and arg4)
    char *argPtrs[] = {arg1, arg2, arg3, arg4};

    // pointer to char pointer array (argPtrs)
    char **argv = argPtrs;

    Main_1(argc, argv);
    Main_2(argc, argv);

    // or

    Main_1(argc, argPtrs);
    Main_2(argc, argPtrs);

    return 0;
}

输出:

hello
how
are
you

hello
how
are
you

hello
how
are
you

hello
how
are
you

-2

对于您的使用,两者都是相同的,除了以下微妙的差异:

  • Sizeof将为两者提供不同的结果
  • 由于它是一个数组,第二个可能无法重新分配到新的内存区域
  • 对于第二个,您只能使用那些有效的索引。如果尝试使用超出数组长度的数组索引,则C/C++未指定。但是,对于char **,您可以使用从0到...的任何索引
  • 第二种形式只能用作函数的形式参数。而第一种甚至可以用于在堆栈中声明变量。

4
什么?在提问者所问的情境中,这些选项都是不正确的。 - Cactus Golov

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