为什么 char** argv 与 char* argv[] 相同?

3

我了解到,在将数组传递给函数时,编译器在幕后执行的操作是将其转换为指向数组首元素的指针。

int myArray(int arr[])

转化为

int myArray(int *arr)

大多数情况下,数组会衰变为指针,例如:

arr[0]

是一样的。
(arr +  0)

(如有错误请指正)

但是当涉及到char *argv时,就会变得很混乱,char *argv[]翻译为字符串数组。

例如:

argv[2] = "Hello"
argv[3] = "World"
**argv*argv[] 怎么会相同呢?因为 **argv 是一个指向指针的指针,而它又是如何包含 10 个不同的值呢?我觉得我可能理解错了什么。

1
一个 int* 指针变量只是指向一个 int 类型的地址,它怎么可能包含 10 种不同的值呢? - Siguza
1
更准确地说,发生的事情是“最外层”(或“最顶层”)的数组被转换为指针。如果您传递一个结构体数组,则函数将接收一个指向结构体的指针。如果您传递一个数组的数组,则函数将接收一个指向数组的指针。(但是,如果数组的数组示例令人困惑,请假装我没有提到它。) - Steve Summit
口语化地说,对于一个连续的数组而言,int*** == int** == int* == int。最终,所有指针只是存储内存地址的数字。可以根据各个数组的长度和所引用的索引来评估所讨论的数组的值。一个常见的伪例子是对于 int**,其中 arr[1][2] 可能会检索到地址 arr + (sizeof(int) * len(arr) * 1) + (sizeof(int) * 2)(这可能因编译器等而异,但基本上是这样的一个想法)。 - Rogue
argv 指向的是一个指针,而不是 10 个值。 - chux - Reinstate Monica
指针不会“包含10个不同的值”。它们可以指向10个不同值中的第一个。指针不包含它们所指向的东西。 - M.M
显示剩余3条评论
5个回答

4

通常情况下,数组会衰变为指针,比如arr[0]等价于(arr + 0)

arr[0]的计算方式相当于*( arr + 0 ),它与*arr相同。

具有数组类型的函数参数由编译器调整为指向数组元素类型的指针。

另一方面,作为参数表达式使用的数组会被隐式转换为指向其第一个元素的指针。

因此,例如这些函数声明:

void f( char * s[100] );
void f( char * s[10] );
void f( char * s[] );

这两个代码片段是等效的,并声明了同一个函数。

void f( char **s );

为了更清晰明确,只需引入一个typedef名称。例如:
typedef char *T;

那么你需要:
void f( T s[] );

因此,函数参数将由编译器进行调整

void f( T *s );

现在将typedef别名更改为其原始类型,您就会得到:

void f( char * *s );

请注意,指针s作为函数参数时并不知道数组使用了多少个元素。

例如,main函数的声明如下:

int main( int argc, char *argv[] );

这意味着它有一个额外的参数argc,允许确定传递给函数的字符串数组中的元素数量。尽管如果谈到主函数,则通常参数argc是多余的,因为字符串数组始终包含哨兵值NULL。也就是说,argv[argc]等于NULL
但通常还必须传递用作函数参数的数组中的元素数量。

"void f( char * s[100] ); void f( char * s[10] ); void f( char * s[] ); 是等价的" --> 嗯,编译器可以进行静态代码分析并发出警告,如 char *a[30]; f(a); "warning: 'f' accessing 800 bytes in a region of size 240 [-Wstringop-overflow=]" ,但使用 void f( char * s[100] ) 时能够发出警告,而使用 void f( char * s[10] ) 则无法。并非完全等价。 - chux - Reinstate Monica
@chux-ReinstateMonica 函数参数声明中没有 static 说明符。因此,可以忽略任何此类警告。 - Vlad from Moscow

1
因为根据n1570 6.7.6.3p7(重点在于我引用的部分):“参数声明为‘类型数组’将被调整为‘限定类型指针’,其中限定符(如果有的话)是数组类型派生中方括号‘【’和‘】’内指定的。如果关键字static也出现在数组类型派生的方括号‘【’和‘】’中,则对于每次调用该函数,相应实际参数的值将提供对一个具有至少与大小表达式所指定的元素数相同的数组的第一个元素的访问权限。”
每个元素char *argv[]都具有类型char *,它是指向char的指针。
因此,根据6.7.6.3p7,由char *argv[](指向char的指针数组)调整为char **argv(指向指向char的指针)。
因为它只是一个指向指针的指针,并不影响它包含10个不同值,这与其他指针没有什么不同(除了它们的大小、表示和对齐要求可能不同)。下面的图表可能会帮助您理解实际发生的情况:
argv (points to the beginning of the array of pointer to chars.)
------------------+---------+---------+
    \             |         |         |
     \ argv[0]    |argv[1]  |argv[2]  | argv[3]
      \           |         |         |
       \          |         |         |
        V  char * V  char * V char *  V char *
        +---------+---------+---------+---------+
        |  0xf00  |  0xf0C  |  0xf13  |   NULL  |   (0xf0C and 0xf13 are the addresses of the first element of the strings passed as parameters to your program.)
        +---------+---------+---------+---------+
             |           |        |
             |           |        |
             |           |        |
             V           V        V
            "my_        "hello"  "world"            ("hello" and "world" are the parameters passed to your program.)
            program"

0
"

"**" 简单地指向一个指针的指针。数组本身就像一个指针。因此,我们可以说 int **argv == *argv[] = argv[][0]。

"

0

char *argv[] 转换成一个字符串数组

如果你考虑这段代码片段

char const *arr[] = {"one", "two", "three"};

arr 被声明为指向 char 的指针数组,并使用初始化列表进行初始化。

C 标准(例如 C11 草案中的 6.7.6.2 数组声明符1)规定(重点在于下划线):

1 除了可选的类型限定符和关键字 static 外,[ 和 ] 可以限定表达式或 *。如果它们限定一个表达式(指定数组的大小),则该表达式必须具有整数类型。如果该表达式是常量表达式,则其值必须大于零。元素类型不得是不完整的或函数类型。
[...]
4 如果没有给出大小,则数组类型是不完整的类型

稍后,在 6.7.9 初始化2 中:

8初始化程序指定了对象中存储的初始值。
[...]
22如果初始化未知大小的数组,则其大小由具有显式初始化程序的最大索引元素确定。数组类型在其初始化程序列表结束时完成。


问题涉及到 argv,即main函数的第二个函数参数
JASLPanswer中所指出的那样,我们需要查看6.7.6.3函数声明符3

6 参数类型列表指定函数参数的类型,并可以声明参数的标识符。
7 将参数声明为“类型数组”的声明应调整为“类型限定符指针”,其中类型限定符(如果有)是在数组类型推导的[和]中指定的。如果关键字static也出现在数组类型演绎的[和]中,则对于对函数的每次调用,相应实际参数的值都应提供对具有至少与大小表达式指定的元素一样多的数组的第一个元素的访问。


1) http://port70.net/%7Ensz/c/c11/n1570.html#6.7.6.2

2) http://port70.net/%7Ensz/c/c11/n1570.html#6.7.9

3)http://port70.net/%7Ensz/c/c11/n1570.html#6.7.6.3


-2
在C语言中,数组和指针有着非常密切的关系。通常来说,指针是一种变量,它包含另外一些变量的地址,而对于数组来说,一个指针存储了数组的起始地址。数组名本身就充当了指向数组第一个元素的指针,如果一个指针变量存储了数组的基础地址,那么我们只需要使用指针变量就可以操作所有的数组元素。
C语言中指针和数组的区别:https://www.geeksforgeeks.org/difference-pointer-array-c/

4
“数组名称本身充当指针”这是一个常见的解释,但事实证明这是有误导性的,并最终会导致麻烦。更确切的说法是,在表达式中,当你试图使用数组的值时,你得到的是指向数组第一个元素的指针。 - Steve Summit
@SteveSummit 我甚至不喜欢“指针”这个词。我更喜欢“地址”,比如“表达式中的裸数组将计算为第一个元素的地址”。对我来说,“指针”的使用意味着可以被赋值的左值。 - Andrew Henle
3
此外,那个geeksforgeeks链接中的解释,很抱歉,相当没有用。要获取更好的解释,请在此处搜索“array pointer difference”,或查看C FAQ列表。 - Steve Summit

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