这个谜题有三个部分。
第一部分是,在C和C++中,空白通常不重要,除了用于分隔无法区分的相邻标记。
在预处理阶段,源文本被分解成一系列“标记” - 标识符、标点符号、数字文字、字符串文字等。这些标记序列稍后会被分析语法和含义。标记器是“贪婪”的,会构建最长的有效标记。如果你写了类似这样的东西:
inttest;
分词器只看到两个标记 - 标识符
inttest
后面是标点符号
;
。它在这个阶段不会将
int
识别为一个单独的关键字(这在后续过程中发生)。因此,为了将该行读作一个名为
test
的整数声明,我们必须使用空格来分隔标识符标记。
int test;
*
字符不是任何标识符的一部分;它是一个独立的标记(标点符号)。因此,如果你写成:
int*test
编译器将这段代码分解为4个独立的标记 -
int
、
*
、
test
和
;
。因此,在指针声明中,空格并不重要,并且所有的
int *test;
int* test;
int*test;
int * test;
被解释的方式是相同的。
拼图的第二部分是C和C++中声明的实际工作方式¹。声明被分为两个主要部分 - 一系列的
声明说明符(存储类说明符、类型说明符、类型限定符等),后面是逗号分隔的(可能初始化的)
声明符列表。在声明中,
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;
操作数
*
的值为
p
,因此它将被解释为。
int (*p), q;
因此,所有的
int *test1, test2;
int* test1, test2;
int * test1, test2;
做同样的事情 - 在所有三种情况下,test1
都是*
运算符的操作数,因此具有类型"指向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)
然后你有 信号
:
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
然而,
int * const p;
声明`p`为指向非常量`int`的`const`指针;你可以对`p`指向的对象进行写操作。
int x = 1;
int y = 2;
int * const p = &x;
*p = 3;
但你不能将p
设置为指向不同的对象:
p = &y
这就引出了谜题的第三个部分 - 为什么声明是这样结构化的。
意图是,声明的结构应该与代码中表达式的结构紧密对应("声明模仿使用")。例如,假设我们有一个指向整数
int
的指针数组,名为
ap
,我们想要访问由第
i
个元素指向的
int
值。我们可以按以下方式访问该值:
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]
相同。运算符 *
和 []
在声明中的行为与在表达式中相同 - []
的优先级高于一元运算符 *
,因此 *
的操作数是 ap[N]
(它被解析为 *(ap[N])
)。
作为另一个例子,假设我们有一个指向名为 pa
的 int
数组的指针,并且我们想要访问第 i
个元素的值。我们会这样写:
printf( "%d", (*pa)[i] );
表达式
(*pa)[i]
的类型是
int
,因此声明应写为
int (*pa)[N];
再次强调,优先级和结合性的规则仍然适用。在这种情况下,我们不想解引用`pa`的第`i`个元素,而是想访问`pa`所指向的对象的第`i`个元素,因此我们必须明确地将`*`运算符与`pa`分组。
`*`、`[]`和`()`运算符都是代码中表达式的一部分,因此它们也是声明符的一部分。声明符告诉您如何在表达式中使用该对象。如果您有一个声明如`int *p;`,那么它告诉您在代码中的表达式`*p`将产生一个`int`值。由此推断,它告诉您表达式`p`产生的值的类型是"指向`int`的指针",即`int *`。
那么,对于像cast和
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;
考虑到以下原因,我们认为这种做法是不好的:
1. 它与语法不一致;
2. 它会引起混淆(正如这个问题所证明的,所有关于这个问题的重复问题,关于
T* p, q;
含义的问题,以及所有关于那些问题的重复问题等);
3. 它在内部不一致 - 声明一个指针数组为
T* a[N]
与使用方式不对称(除非你习惯写
* a[i]
);
4. 它不能应用于指向数组或函数的指针类型(除非你创建一个typedef来清晰地应用
T* p
约定,但...不推荐这样做);
5. 这样做的原因 - "它强调了对象的指针性质" - 是站不住脚的。它不能应用于数组或函数类型,而我认为这些特性同样重要需要强调。
总之,这只是表明对两种语言的类型系统工作方式存在困惑的思维。
有很多好的理由来单独声明变量;绕过不良实践(
T* p, q;
)并不是其中之一。如果你正确地书写你的声明符号(
T *p, q;
),你就不太可能引起混淆。
我认为这类似于故意将所有简单的
for
循环都写成
i = 0;
for( ; i < N; )
{
...
i++;
}
语法上是有效的,但容易混淆,意图很可能被误解。然而,在C++社区中,“T* p;”的约定已经根深蒂固,并且我在自己的C++代码中使用它,因为代码库的一致性是一件好事,但每次这样做都让我感到不舒服。
我将使用C术语 - C++术语略有不同,但概念基本相同。
++
运算符不能用空格分开,标识符也不能用空格分开(编译器可能仍然合法但运行时行为未定义)。考虑到 C/C++ 的语法混乱,确切的情况很难定义。 - Sulthanint* test,test2;
不能按照你的期望工作,这意味着它是错误的,是对语言的误解的结果,而int *test,test2;
才是正确的。 - endolith