这个谜题分为三个部分。
第一部分是 C 和 C++ 中的空格通常不重要,除了分隔在其他情况下无法区分的相邻标记。
在预处理阶段,源文本被分解为一系列标记——标识符、标点符号、数字文字、字符串文字等。稍后分析该标记序列的语法和含义。分词器是“贪婪的”,并且会构建尽可能长的有效令牌。如果你写类似
inttest;
标记器只看到两个标记 - 标识符inttest
后跟标点符号;
。在此阶段它不会被识别int
为单独的关键字(在此过程中稍后会发生)。因此,要将该行读取为一个名为 的整数的声明test
,我们必须使用空格来分隔标识符标记:
int test;
该*
字符不是任何标识符的一部分;它本身就是一个单独的标记(标点符号)。所以如果你写
int*test;
编译器看到 4 个单独的标记 - int
、*
、test
和;
. 因此,空白在指针声明中并不重要,所有的
int *test;
int* test;
int*test;
int * test;
以同样的方式解释。
谜题的第二部分是声明在 C 和 C++ 1中的实际工作方式。声明分为两个主要部分 -声明说明符序列(存储类说明符、类型说明符、类型限定符等),然后是逗号分隔的(可能已初始化)声明符列表。在声明中
unsigned long int a[10]={0}, *p=NULL, f(void);
声明说明符是unsigned long int
,声明符是a[10]={0}
,*p=NULL
和f(void)
. 声明器引入了被声明事物的名称(a
、p
和f
)以及有关该事物的数组、指针和函数的信息。声明器也可以有一个关联的初始化器。
的类型a
是“10 元素数组unsigned long int
”。该类型完全由声明说明符和声明符的组合={0}
指定,初始值由初始化器指定。类似地, 的类型p
是“指针unsigned long int
”,并且该类型再次由声明说明符和声明符的组合指定,并被初始化为NULL
。同样的道理,类型f
是“函数返回unsigned long int
”。
这是关键 - 没有“指针”类型说明符,就像没有“数组”类型说明符一样,就像没有“函数返回”类型说明符一样。我们不能将数组声明为
int[10] a;
因为运算符的操作数[]
是a
,不是int
。同样,在声明中
int* p;
的操作数*
是p
,不是int
。但是因为间接操作符是一元的并且空格不重要,如果我们这样写,编译器不会抱怨。但是,它总是被解释为int (*p);
。
因此,如果你写
int* p, q;
*
is的操作数p
,所以它会被解释为
int (*p), q;
因此,所有
int *test1, test2;
int* test1, test2;
int * test1, test2;
做同样的事情 - 在所有三种情况下,test1
is 的操作数*
因此具有类型“指针int
”,而test2
具有类型int
。
声明符可以变得任意复杂。您可以拥有指针数组:
T *a[N];
你可以有指向数组的指针:
T (*a)[N];
你可以让函数返回指针:
T *f(void);
你可以有指向函数的指针:
T (*f)(void);
您可以拥有指向函数的指针数组:
T (*a[N])(void);
您可以让函数返回指向数组的指针:
T (*f(void))[N];
您可以让函数返回指向指针数组的指针,这些指针指向返回指针的函数T
:
T *(*(*f(void))[N])(void); // yes, it's eye-stabby. Welcome to C and C++.
然后你有signal
:
void (*signal(int, void (*)(int)))(int);
读作
signal -- signal
signal( ) -- is a function taking
signal( ) -- unnamed parameter
signal(int ) -- is an int
signal(int, ) -- unnamed parameter
signal(int, (*) ) -- is a pointer to
signal(int, (*)( )) -- a function taking
signal(int, (*)( )) -- unnamed parameter
signal(int, (*)(int)) -- is an int
signal(int, void (*)(int)) -- returning void
(*signal(int, void (*)(int))) -- returning a pointer to
(*signal(int, void (*)(int)))( ) -- a function taking
(*signal(int, void (*)(int)))( ) -- unnamed parameter
(*signal(int, void (*)(int)))(int) -- is an int
void (*signal(int, void (*)(int)))(int); -- returning void
这只是触及了可能的表面。但请注意,数组、指针和函数始终是声明符的一部分,而不是类型说明符。
需要注意的一件事 -const
可以同时修改指针类型和指向类型:
const int *p;
int const *p;
以上两者都声明p
为指向const int
对象的指针。您可以编写一个新值以p
将其设置为指向不同的对象:
const int x = 1;
const int y = 2;
const int *p = &x;
p = &y;
但您不能写入指向的对象:
*p = 3; // constraint violation, the pointed-to object is const
然而,
int * const p;
声明p
为const
指向非 const的指针int
;你可以写东西p
指向
int x = 1;
int y = 2;
int * const p = &x;
*p = 3;
但您不能设置p
为指向不同的对象:
p = &y; // constraint violation, p is const
这给我们带来了第三个难题——为什么声明是这样构造的。
目的是声明的结构应该密切反映代码中表达式的结构(“声明模仿使用”)。例如,假设我们有一个指向int
named的指针数组ap
,并且我们想要访问'th 元素int
所指向的值。i
我们将按如下方式访问该值:
printf( "%d", *ap[i] );
表达式 的*ap[i]
类型为int
; 因此, 的声明ap
写成
int *ap[N]; // ap is an array of pointer to int, fully specified by the combination
// of the type specifier and declarator
声明*ap[N]
器与表达式具有相同的结构*ap[i]
。运算符*
和[]
在声明中的行为与它们在表达式中的行为相同 -[]
具有比 unary 更高的优先级*
,因此 is 的操作数*
(ap[N]
它被解析为*(ap[N])
)。
再举一个例子,假设我们有一个指向int
named数组的指针,pa
并且我们想要访问第i
' 个元素的值。我们会把它写成
printf( "%d", (*pa)[i] );
表达式的类型(*pa)[i]
是int
,所以声明写成
int (*pa)[N];
同样,适用相同的优先级和关联性规则。在这种情况下,我们不想取消引用i
' 的第 ' 个元素pa
,我们想访问指向i
' 的第 ' 元素,所以我们必须显式地将运算符与 组合在一起。pa
*
pa
,和运算符都是代码中表达式*
的一部分,因此它们都是声明中的声明符的一部分。声明器告诉您如何在表达式中使用对象。如果您有一个类似 的声明,它会告诉您代码中的表达式将产生一个值。通过扩展,它告诉您表达式产生一个类型为“指向”的值,或者。[]
()
int *p;
*p
int
p
int
int *
那么,诸如演员表和sizeof
表达式之类的东西呢,我们在哪里使用(int *)
或sizeof (int [10])
或之类的东西呢?我如何阅读类似的内容
void foo( int *, int (*)[10] );
没有声明符,*
和[]
运算符不是直接修改类型吗?
好吧,不 - 仍然有一个声明符,只是带有一个空标识符(称为抽象声明符)。如果我们用符号 λ 表示一个空标识符,那么我们可以将这些内容读作(int *λ)
、sizeof (int λ[10])
和
void foo( int *λ, int (*λ)[10] );
它们的行为与任何其他声明完全一样。 int *[10]
表示一个包含 10 个指针的数组,而int (*)[10]
表示一个指向数组的指针。
现在是这个答案的固执部分。我不喜欢将简单指针声明为的 C++ 约定
T* p;
并认为这是不好的做法,原因如下:
- 它与语法不一致;
- 它引入了混淆(正如这个问题所证明的那样,这个问题的所有重复项,关于 的含义的问题,这些
T* p, q;
问题的所有重复项等);
- 它不是内部一致的——声明一个指针数组
T* a[N]
是不对称的(除非你有写作的习惯* a[i]
);
- 它不能应用于指向数组的指针或指向函数的指针类型(除非您创建一个 typedef 以便您可以
T* p
干净地应用约定,这...否);
- 这样做的原因 - “它强调对象的指针性” - 是虚假的。它不能应用于数组或函数类型,我认为这些品质同样重要。
最后,它只是表明对两种语言的类型系统如何工作的困惑思考。
有充分的理由单独申报物品;解决不好的做法 ( T* p, q;
) 不是其中之一。如果您正确编写声明符( T *p, q;
),则不太可能引起混淆。
我认为这类似于故意将所有简单for
循环编写为
i = 0;
for( ; i < N; )
{
...
i++;
}
语法上有效,但令人困惑,意图很可能被误解。但是,该T* p;
约定在 C++ 社区中根深蒂固,我在自己的 C++ 代码中使用它,因为代码库之间的一致性是一件好事,但每次这样做都让我很痒。
- 我将使用 C 术语 - C++ 术语略有不同,但概念大致相同。