【发布时间】:2012-12-10 20:46:01
【问题描述】:
标准是如何定义的,例如,float (*(*(&e)[10])())[5] 声明了一个类型为“对 10 的数组的引用,指向 () 的函数的指针,返回指向 5 的数组 float 的指针”?
【问题讨论】:
标签: c++ types c++11 standards declaration
标准是如何定义的,例如,float (*(*(&e)[10])())[5] 声明了一个类型为“对 10 的数组的引用,指向 () 的函数的指针,返回指向 5 的数组 float 的指针”?
【问题讨论】:
标签: c++ types c++11 standards declaration
我在这篇文章中指的是 C++11 标准
声明在C++的语法中称为simple-declaration,它是以下两种形式之一(§ 7/1):
decl-specifier-seqopt init-declarator-listopt ;
attribute-specifier-seq decl-specifier-seqopt init-declarator-list ;
attribute-specifier-seq 是一系列属性 ([[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 是否是有效的声明说明符序列?如果不是,是否被句法或语义规则所禁止?
有效!
const将unsigned与int分开并不重要。
auto const 是否是有效的声明说明符序列?如果不是,是否被句法或语义规则所禁止?
有效!
auto是声明说明符,但在 C++11 中更改了类别。之前它是一个存储说明符(如static),但现在它是一个类型说明符。
int * const 是否是有效的声明说明符序列?如果不是,是否被句法或语义规则所禁止?
语法规则无效!虽然这很可能是声明的完整类型,但只有
int是声明说明符序列。声明说明符仅提供基类型,而不提供指针、引用、数组等复合修饰符。
simple-declaration 的第二部分是 init-declarator-list。它是由逗号分隔的 声明符 序列,每个都有一个可选的初始化程序(第 8 节)。每个声明器都在程序中引入一个变量或函数。最简单的声明器形式就是您要介绍的名称 - declarator-id。声明int x, y = 5; 有一个声明说明符序列,它只是int,后跟两个声明符x 和y,第二个声明符有一个初始化器。但是,我们将在本文的其余部分忽略初始化程序。
声明符可以具有特别复杂的语法,因为这是声明的一部分,允许您指定变量是指针、引用、数组、函数指针等。请注意,这些都是 declarator 而不是整个声明。这正是int* x, y; 不声明两个指针的原因——星号* 是x 声明符的一部分,而不是y 声明符的一部分。一个重要的规则是每个声明器必须有一个 declarator-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的类型是什么?
"const unsigned long int"
声明说明符序列char volatile的类型是什么?
“挥发性字符”
声明说明符序列auto const的类型是什么?
这取决于!
auto将从初始化程序中推导出来。例如,如果推导为int,则类型为“const int”。
现在我们有了声明说明符序列的类型,我们可以计算出整个标识符声明的类型。这是通过应用第 8.3 节中定义的递归过程来完成的。为了解释这个过程,我将使用一个运行示例。我们将在float const (*(*(&e)[10])())[5] 中计算出e 的类型。
第一步 第一步是将声明拆分为T D 形式,其中T 是声明说明符序列,D 是声明符。所以我们得到:
T = float const
D = (*(*(&e)[10])())[5]
T 的类型当然是“const float”,正如我们在上一节中确定的那样。然后我们寻找与D 的当前形式相匹配的§8.3 小节。您会发现这是 §8.3.4 数组,因为它声明它适用于 T D 形式的声明,其中 D 具有以下形式:
D1 [constant-expressionopt]attribute-specifier-seqopt
我们的D 确实是D1 是(*(*(&e)[10])()) 的那种形式。
现在想象一个声明T D1(我们已经摆脱了[5])。
T D1 = const float (*(*(&e)[10])())
它的类型是“T”。本节说明我们的标识符 e 的类型是“T”,其中 T D1的类型。
这就是递归!我们递归地计算出声明的内部部分的类型,在每一步都去掉一点。
第 2 步 所以,和以前一样,我们将新声明拆分为 T D 的形式:
T = const float
D = (*(*(&e)[10])())
这与第 8.3/6 段相匹配,其中 D 的形式为 ( D1 )。这种情况很简单,T D的类型就是T D1的类型:
T D1 = const float *(*(&e)[10])()
第 3 步我们现在将其称为 T D 并再次拆分:
T = const float
D = *(*(&e)[10])()
这匹配§8.3.1 指针,其中D 的形式为* D1。如果T D1 的类型为“T”,则T D 的类型为“T”。所以现在我们需要T D1的类型:
T D1 = const float (*(&e)[10])()
第 4 步我们称之为T D 并将其拆分:
T = const float
D = (*(&e)[10])()
这匹配 §8.3.5 函数,其中 D 的形式为 D1 ()。如果T D1 的类型为“T”,则T D 的类型为“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 的类型为“T”,那么T D 的类型为“T”。所以现在我们需要T D1的类型:
T D1 = const float (&e)[10]
第 7 步拆分:
T = const float
D = (&e)[10]
我们再次匹配 §8.3.4 数组,D 的形式为 D1 [10]。如果T D1 的类型为“T”,则T D 的类型为“T”。那么T D1的类型是什么?
T D1 = const float (&e)
第 8 步再次应用括号步骤:
T D1 = const float &e
第 9 步拆分:
T = const float
D = &e
现在我们匹配§8.3.2 引用,其中D 的形式为& D1。如果T D1 的类型为“T”,则T D 的类型为“T”。那么T D1的类型是什么?
T D1 = const float e
第 10 步当然只是“T”!在这个级别没有
我们完成了!
所以现在如果我们看一下我们在每一步中确定的类型,将下面每个级别的 float const (*(*(&e)[10])())[5] 中的e 的类型:
<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
不错!这显示了编译器如何推断声明的类型。请记住,如果有多个声明符,这将应用于标识符的每个声明。试着弄清楚这些:
快速测验(您可能需要参考标准)
声明bool **(*x)[123];中的x是什么类型?
"指向 123 数组的指针指向 bool 的指针"
声明int const signed *(*y)(int), &z = i;中的y和z的类型是什么?
y是“指向 (int) 函数的指针,该函数返回指向 const signed int 的指针”z是“对 const signed int 的引用”
如果有人有任何更正,请告诉我!
【讨论】:
declarator-id: 1. id-expression 2. ::opt nested-name-specifier opt type-name第二条规则我看不懂,你能举个例子说明第二条规则吗? .我认为类名(标识符,模板ID)在这种情况下成为声明符的一部分?谢谢
这是我解析float const (*(*(&e)[10])())[5] 的方式。首先,确定说明符。这里的说明符是float const。现在,让我们看看优先级。 [] = () > *。括号用于消除优先级的歧义。考虑到优先级,让我们识别变量 ID,即e。因此,e 是对一个数组的引用(从[] > * 开始),该数组包含 10 个指向函数的指针(从 () > * 开始),它不带参数并返回,并且是一个指向 5 个浮点 const 的数组的指针。所以说明符排在最后,其余部分根据优先级进行解析。
【讨论】: