标准是如何定义的,例如,float (*(*(&e)[10])())[5]
声明一个类型为“对 10 的数组的引用,指向 () 返回指向 5 的数组的指针的函数的指针”类型的变量float
?
2 回答
我在这篇文章中参考了 C++11 标准
声明
我们关心的类型声明在 C++ 语法中称为simple -declaration,它是以下两种形式之一(§7/1):
decl-specifier-seq opt init-declarator-list opt;
属性说明符序列 decl 说明符序列opt init-declarator-list;
属性说明符序列是一系列属性 ( [[something]]
) 和/或对齐说明符 ( alignas(something)
)。由于这些不影响声明的类型,我们可以忽略它们以及上述两种形式中的第二种。
声明说明符
因此,我们声明的第一部分decl-specifier-seq由声明说明符组成。其中包括一些我们可以忽略的东西,例如存储说明符(static
,extern
等)、函数说明符(inline
等)、说明friend
符等。然而,我们感兴趣的一个声明说明符是类型说明符,它可能包括简单的类型关键字(、、、char
等int
)unsigned
、用户定义类型的名称、cv 限定符(const
或volatile
)以及其他我们不知道的关心。
示例:所以decl-specifier-seq的一个简单示例,它只是一个类型说明符序列const int
。另一个可能是unsigned int volatile
。
你可能会想“哦,所以类似的东西const volatile int int float const
也是一个decl-specifier-seq吗?” 你是对的,它符合语法规则,但语义规则不允许这样的decl-specifier-seq。实际上,只允许使用一个类型说明符,除了某些组合(例如unsigned
与int
或const
与除自身之外的任何东西)并且至少需要一个非 cv 限定符(第 7.1.6/2-3 节)。
快速测验(您可能需要参考标准)
是否是
const int const
有效的声明说明符序列?如果不是,它是否被句法或语义规则所禁止?语义规则无效!
const
不能与自身结合。是否是
unsigned const int
有效的声明说明符序列?如果不是,它是否被句法或语义规则所禁止?有效的!将from
const
分开并不重要。unsigned
int
是否是
auto const
有效的声明说明符序列?如果不是,它是否被句法或语义规则所禁止?有效的!
auto
是声明说明符,但在 C++11 中更改了类别。之前它是一个存储说明符(如static
),但现在它是一个类型说明符。是否是
int * const
有效的声明说明符序列?如果不是,它是否被句法或语义规则所禁止?语法规则无效!虽然这很可能是声明的完整类型,但只有
int
声明说明符序列。声明说明符仅提供基本类型,而不提供复合修饰符,如指针、引用、数组等。
声明者
简单声明的第二部分是init-declarator-list。它是由逗号分隔的声明符序列,每个声明符都有一个可选的初始化器(第 8 节)。每个声明器都在程序中引入一个变量或函数。最简单的声明器形式就是您要介绍的名称 - declarator-id。该声明int x, y = 5;
有一个声明说明符序列,它只是int
,后跟两个声明符x
和y
,第二个声明符有一个初始化器。但是,我们将在本文的其余部分忽略初始化程序。
声明符可以具有特别复杂的语法,因为这是声明的一部分,允许您指定变量是指针、引用、数组、函数指针等。请注意,这些都是声明符而不是声明的一部分作为一个整体。这正是int* x, y;
不声明两个指针的原因——星号*
是声明符的一部分,x
而不是声明符的一部分y
。一个重要的规则是每个声明者必须有一个声明者ID——它声明的名称。一旦确定了声明的类型(我们稍后会谈到),就会强制执行有关有效声明符的其余规则。
示例:声明符的一个简单示例是*const p
,它声明了一个const
指向...某物的指针。它指向的类型由其声明中的声明说明符给出。一个更可怕的例子是问题中给出的例子(*(*(&e)[10])())[5]
,它声明了一个对函数指针数组的引用,该数组返回指向...的指针。同样,类型的最后部分实际上是由声明说明符给出的。
您不太可能遇到如此可怕的声明符,但有时会出现类似的声明符。能够阅读问题中的声明是一项有用的技能,并且是一项伴随实践而来的技能。了解标准如何解释声明的类型会很有帮助。
快速测验(您可能需要参考标准)
int const unsigned* const array[50];
声明说明符和声明符有哪些部分?声明说明符: 声明符
int const unsigned
:* const array[50]
volatile char (*fp)(float const), &r = c;
声明说明符和声明符有哪些部分?声明说明符: 声明符
volatile char
#1:声明符(*fp)(float const)
#2:&r
声明类型
现在我们知道声明是由声明符说明符序列和声明符列表组成的,我们可以开始思考声明的类型是如何确定的。例如,int* p;
定义p
为“指向 int 的指针”可能很明显,但对于其他类型,它就不是那么明显了。
具有多个声明符的声明,比如说 2 个声明符,被认为是特定标识符的两个声明。也就是说,int x, *y;
是一个标识符x
,int x
的声明,和一个标识符y
,的声明int *y
。
类型在标准中表示为类似英语的句子(例如“pointer to int”)。这种类似英语的形式的声明类型的解释分两部分进行。首先,确定声明说明符的类型。其次,对整个声明应用递归过程。
声明说明符类型
声明说明符序列的类型由标准的表 10 确定。它列出了序列的类型,因为它们以任何顺序包含相应的说明符。因此,例如,任何包含signed
并char
以任何顺序排列的序列,包括char signed
,都具有“signed char”类型。任何出现在声明说明符序列中的 cv 限定符都被添加到类型的前面。char const signed
类型“const signed char”也是如此。这确保无论您放置说明符的顺序如何,类型都是相同的。
快速测验(您可能需要参考标准)
声明说明符序列的类型是什么
int long const unsigned
?“常量无符号长整数”
声明说明符序列的类型是什么
char volatile
?“挥发性炭”
声明说明符序列的类型是什么
auto const
?这取决于!
auto
将从初始化程序中推导出来。例如,如果推断为int
,则类型将为“const int”。
申报类型
现在我们有了声明说明符序列的类型,我们可以计算出整个标识符声明的类型。这是通过应用第 8.3 节中定义的递归过程来完成的。为了解释这个过程,我将使用一个运行示例。我们将计算e
in的类型float const (*(*(&e)[10])())[5]
。
步骤 1第一步是将声明拆分为声明T D
说明T
符序列和声明符的形式D
。所以我们得到:
T = float const
D = (*(*(&e)[10])())[5]
的类型T
当然是“const float”,正如我们在上一节中确定的。然后我们寻找 §8.3 中与当前形式相匹配的D
. 您会发现这是 §8.3.4 数组,因为它声明它适用于具有以下形式的形式T D
的声明D
:
D1 [
常量表达式选择]
属性说明符序列选择
我们D
确实是那种形式 where D1
is (*(*(&e)[10])())
。
现在想象一个声明T D1
(我们已经摆脱了[5]
)。
T D1 = const float (*(*(&e)[10])())
它的类型是“<some stuff> T
”。本节说明我们的标识符 , 的类型e
是“<some stuff> array of 5 T
”,其中 <some stuff> 与虚构声明的类型相同。所以要算出类型的其余部分,我们需要算出 的类型T D1
。
这就是递归!我们递归地计算出声明的内部部分的类型,在每一步都去掉它的一部分。
第 2 步因此,和以前一样,我们将新声明拆分为以下形式T D
:
T = const float
D = (*(*(&e)[10])())
这与第 8.3/6 段相匹配,其中D
is 的形式为( D1 )
。这种情况很简单,类型T D
就是 的类型T D1
:
T D1 = const float *(*(&e)[10])()
第 3 步让我们现在调用T D
它并再次拆分它:
T = const float
D = *(*(&e)[10])()
这匹配 §8.3.1 指针 where D
is 的形式* D1
。如果T D1
类型为“<some stuff> T
”,则T D
类型为“<some stuff> pointer to T
”。所以现在我们需要以下类型T D1
:
T D1 = const float (*(&e)[10])()
第 4 步我们调用它T D
并将其拆分:
T = const float
D = (*(&e)[10])()
这与 §8.3.5 Functions where D
is 的形式相匹配D1 ()
。如果T D1
具有类型“<some stuff> T
”,则T D
具有类型“<some stuff> function of () returned T
”。所以现在我们需要以下类型T D1
:
T D1 = const float (*(&e)[10])
第 5 步我们可以应用与第 2 步相同的规则,其中声明符被简单地加上括号以结束:
T D1 = const float *(&e)[10]
第 6 步当然,我们将其拆分:
T = const float
D = *(&e)[10]
我们再次将 §8.3.1 指针与D
形式匹配* D1
。如果T D1
类型为“<some stuff> T
”,则T D
类型为“<some stuff> pointer to T
”。所以现在我们需要以下类型T D1
:
T D1 = const float (&e)[10]
步骤 7拆分:
T = const float
D = (&e)[10]
我们再次匹配 §8.3.4 数组,D
格式为D1 [10]
。如果T D1
类型为“<some stuff> T
”,则T D
类型为“<some stuff> array of 10 T
”。那么什么是T D1
类型?
T D1 = const float (&e)
步骤 8再次应用括号步骤:
T D1 = const float &e
步骤 9拆分:
T = const float
D = &e
现在我们匹配 §8.3.2 References where D
is 的形式& D1
。如果T D1
类型为“<some stuff> T
”,则T D
类型为“<some stuff> reference to T
”。那么是什么类型的T D1
呢?
T D1 = const float e
第 10 步当然是“T”!在这个级别没有<some stuff>。这是由第 8.3/5 节中的基本案例规则给出的。
我们完成了!
所以现在如果我们查看我们在每个步骤中确定的类型,将 <some stuff> 替换为下面每个级别的,我们可以确定e
in的类型float const (*(*(&e)[10])())[5]
:
<some stuff> array of 5 T
│ └──────────┐
<some stuff> pointer to T
│ └────────────────────────┐
<some stuff> function of () returning T
| └──────────┐
<some stuff> pointer to T
| └───────────┐
<some stuff> array of 10 T
| └────────────┐
<some stuff> reference to T
| |
<some stuff> T
如果我们将这一切结合在一起,我们得到的是:
reference to array of 10 pointer to function of () returning pointer to array of 5 const float
好的!这显示了编译器如何推断声明的类型。请记住,如果有多个声明符,这将应用于标识符的每个声明。试着弄清楚这些:
快速测验(您可能需要参考标准)
x
声明中的类型是什么bool **(*x)[123];
?“指向 123 数组的指针指向 bool 的指针”
声明中的
y
和类型是什么?z
int const signed *(*y)(int), &z = i;
y
是“指向(int)函数的指针,返回指向 const signed int 的指针”
z
是“对 const signed int 的引用”
如果有人有任何更正,请告诉我!
这是我解析的方式float const (*(*(&e)[10])())[5]
。首先,确定说明符。这里的说明符是float const
. 现在,让我们看看优先级。[] = () > *
. 括号用于消除优先级的歧义。考虑到优先级,让我们识别变量 ID,即e
. 因此,e 是对包含 10 个函数指针的数组(since [] > *
)的引用,该函数(since () > *
)不带参数并返回,并且是指向 5 个 float const 的数组的指针。所以说明符排在最后,其余部分根据优先级进行解析。