指针解引用运算符 - 句法规则

3

我不确定星号操作符的正确用法。请看以下示例:

#include<stdio.h>

int main() {
   char  *w[3];      //Array of pointers
   w[0] = "Apple";
   w[1] = "Pear";
   w[2] = "Peach";

    printf("w[0] = %s, w[1] = %s, w[2] = %s\n", w[0], w[1], w[2]);   

    char **p = &w[0];
    char ***q = &p;
    printf("&w[0] = %p, *p = %s, p = %p, q = %p, *q = %p, **q = %s\n",
            &w[0],      *p,      p,      q,      *q,      **q);

return 0;
}

我的指针使用期望:


这里是需要翻译的内容。
int n = 3;
int *a = &n;    ->  a = &n  ->  *a = 3
int **b = &a;   ->  b = &a  ->  *b = &n  ->  **b = 3
int ***c = &b;  ->  c = &b  ->  *c = &a  ->  **c = &n  ->   ***c = 3

上面的代码中,p 是一个指向指针的指针。 p 保存了 &w[0] 的地址,而 *p 返回了 &w[0] 中的值。难道不应该使用 **p = "Apple" 吗? 同样地,难道不应该使用 ***q = "Apple",而不是 **q 吗?
我找不到资源来清楚地说明指向N个指针的指针运算符的正确使用。在这方面,任何建议都将不胜感激。我希望我已经能够充分地表达我的问题。

你可以通过将编译器设置为最高警告级别并进行调试来解决这个问题。不过,我更喜欢使用 const char* w[3] 作为类型。 - Bathsheba
这是一个很好的建议。我会采纳它,谢谢。 - Worice
4个回答

3
现在我将尝试对指针进行一些澄清,并为了便于理解,现在我将以简化的方式阐述事情。
在C中,指针是一个变量,就像任何其他变量一样,其类型是内存中的地址。
不幸的是,仅有一个指向进程内存中某个位置的地址是不够的,无法正确地与指针所指向的其他实体(通常是变量)进行交互而不会破坏它们。出于这个原因,语言提供了指针的“资格”,允许用户指定它指向的对象(类型),并允许编译器选择使用的方式来访问内存以符合指向的对象。
说到这里,回到你最初的问题,现在应该清楚了,指针是一个变量,可以保存C语言中存在的任何类型(基本或派生)的地址。
现在我们进入正式部分。要与指针交互,我们需要一些运算符来处理它们,基本上是一个运算符,它将解析为变量的地址,一个运算符,它将从指针中解析出变量的值,以及一个声明运算符,用于“声明”指针。
让我们从“解引用运算符*”开始。该运算符返回指向对象的值(解引用)。但它也是指针的“声明符”,因为它是指针最自然的视觉表示。请看以下声明:
int * p_int;

按照C风格的声明方式来读,从右到左可以说:

p_int是一个变量,其解引用后给出一个int值。

它是一个指针

现在,如果我们声明一个变量p_int作为指向类型为int的对象的指针,当我们对其进行解引用时,编译器知道为了返回一个int值,它必须访问从指针所指向的内存字节开始的内存字节,并将请求的一些字节(在我们使用的机器/编译器上)打包在一起形成一个int

无论如何,指针,就像其他任何变量一样,必须初始化或分配一个有效地址,然后才能用于某些事情。因此,我们必须初始化/分配一个与其指向的对象类型兼容的值。如果我们有一个变量:

int an_int;

如果是兼容类型,它就可以适应范围。但指针保存的是对象的地址,因此我们无法直接赋值:

p_int = an_int;  //The compiler will trigger an error for incompatible type

为了给它指定一个地址,我们必须获取变量在内存中的地址。我们需要一元运算符&,它会返回应用于其上的对象的地址。
p_int = &an_int;  //we assign to p_int the address in memory of an_int

当然,我们可以通过解引用指针来再次访问存储在地址中的值:
int another_int = *p_int;

在结束之前,我们必须谈论一种C语言为数组保留的一种特殊处理。在C语言中,数组的名称会自动转换为其第一个元素的地址(我们将在下面看到这个标准存在哪些限制)。这意味着数组声明后的2行代码是等价的:
int array_of_int[10];
int *p_int  = array_of_int;
int *p_int1 = &array_of_int[0];

即使是 int *p_int1 = &array_of_int; 也等同于下面我们将要看到的原因。

现在我们来考虑你的例子。声明:

char   *w[3];
char  **p = &w[0];
char ***q = &p;

必须按照"w是一个包含3个指向字符的指针的数组,p是一个指向指向int类型指针的指针,q是一个指向指向指向int类型指针的指针的指针。"阅读。解码如下:数组w的每个元素都保存一个char的地址,如果在内存中有一个以该地址开头的char数组,则根据我们之前关于数组的传递属性所述,我们可以说每个w元素保存了未指定维度的3个char数组的第一个char的地址。当然,这些数组中的每一个都包含三个单词“Apple”,“Pear”和“Peach”。在声明中:
char  **p = &w[0];

我们创建了一个变量,用于保存地址,即存储在数组w的第0个元素中的指向char类型指针的值所在的内存地址。需要注意的是,w [0]将给出字符串“Apple”开始的地址,而不是保存字符串“Apple”开始地址的地址。因此,我们使用一元运算符&来获取这样的地址。
真正有趣的一点是声明中的两个星号是声明性的,而不是操作符。为了澄清,请考虑:
char  **p;
p = &w[0];

这与之前的完全相同,但在第一行中,我们声明了一个指向指针的char,在第二行中,我们将其赋值为w的第一个元素的地址。

这应该足以解释问题的其他部分。

现在让我们更加正式地查看C标准。我们已经说过,在某些情况下,数组和指针会被编译器自动转换。这在ISO/IEC 9899:2011 § 6.3.2.1 "Lvalues, arrays, and function designators" subsection 3中明确说明:

除非它是sizeof运算符或一元&运算符的操作数,或者是用于初始化数组的字符串字面量,否则具有类型“type的数组”的表达式将转换为具有类型“type的指针”的表达式,该指针指向数组对象的初始元素而不是lvalue。如果数组对象具有寄存器存储类,则行为未定义。

这也解释了为什么对数组操作数使用&运算符仍然解析为第一个数组对象的地址。


有趣的是,在声明中的两个星号是声明性的,而不是操作符本身。谢谢! - Worice
在C语言中,数组的名称被赋值为其第一个元素的地址,这种说法是不正确的。实际上,数组标识符并非被赋值,而是被转换。此外,在某些情况下,当数组标识符作为sizeof或一元运算符&的操作数时,或者作为用于初始化数组的字符串字面量时,它们并不会被转换为指针。如果不区分这些情况,可能会导致混淆,特别是对于初学者来说。 - ad absurdum
@DavidBowling 谢谢。范围是给出一个最简单和清晰的答案,易于新手理解。但是添加更严格的解释以完善阐述肯定更好。我编辑了答案。 - Frankie_C
@Frankie_C -- 我通常对类似于“在大多数表达式中,数组标识符被转换为指向数组第一个元素的指针”这样的内容感到满意。通常不需要引用,我同意您不想用不必要的细节来混淆视听。我只是试图避免绝对的说法,比如“数组名称被转换为指针”,这可能会被学习者误解,例如当他们遇到像size_t len = sizeof arr / sizeof *arr;这样的代码时。尽管如此,这是一个很好的答案。+1 - ad absurdum

1
上面,p 是一个指向指针的指针。p 持有 &w[0] 的地址。
您说得对,p 是一个指向指针的指针。
但是您说的 p 持有 &w[0] 的地址是不正确的。正确的说法是“p 持有 w[0] 的地址”。
& 是读作“地址”,这就是为令赋值 char **p = &w[0]; 合法的原因。
考虑到:
w[0] 是一个 char *。 这意味着 &w[0] 是一个 char **,与 p 的类型兼容。

因此,p 被分配了 w[0] 的地址。这相当于说 "p 指向 w[0]"。因此,*p 是一个引用 w[0]*pw[0] 都具有类型 char * 并指向内存中相同的字符串 "Apple"

请注意,字面字符串 "Apple" 的类型是 char [],因为在 C 中,字符串被实现为 char 数组。这对于初始化 w 透明地强制转换为 char *

(感谢 @David Bowling 进行更正)


0

简单示例:

   p          w[0]
 -------     -------     -------------------- 
|       |   |       |   |      
|  0x05 |   |  0x00 |   |  'A'  p' 'p' 'l' 'e' 
 -------     -------     -------------------- 
 &p 0x07   &w[0] 0x06      0x00  ------------

*p会给你0x00

**p会给你'A'


应该是'A',不是吗?你可能需要在p'之前再加一个' - Yunnosch
你是否想解释一下这些地址只是简化的例子?否则可能会被误解为偏移量或者过于字面化。 - Yunnosch
1
这个回答不太清楚。您是在暗示 &p0x06 吗?&w[0]0x05 吗? - ad absurdum
考虑列出p的值(即0x05),可以描述为“存储在p中的地址”。 - Yunnosch
请解释图形符号以提高清晰度。 - lockcmpxchg8b
您提出的过于简化的地址方案是不可能实现的。首先,由于“Apple”的空终止符存储在那里,因此无法将&w[0]的值存储在0x05处。但是,&w[0]不是一个_lvalue_,它是一个临时值,不像图表所示,没有存储在内存中。这是一个令人困惑和混乱的图表。 - ad absurdum

0

printf("%s", p) 期望 p 的类型为 char *。 如果 p 的类型为 char **,则需要对其进行解引用以获取 char *


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