49

标准是如何定义的,例如,float (*(*(&e)[10])())[5]声明一个类型为“对 10 的数组的引用,指向 () 返回指向 5 的数组的指针的函数的指针”类型的变量float

灵感来自与@DanNissenbaum 的讨论

4

2 回答 2

89

我在这篇文章中参考了 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符等。然而,我们感兴趣的一个声明说明符是类型说明符,它可能包括简单的类型关键字(、、、charintunsigned、用户定义类型的名称、cv 限定符(constvolatile)以及其他我们不知道的关心。

示例:所以decl-specifier-seq的一个简单示例,它只是一个类型说明符序列const int。另一个可能是unsigned int volatile

你可能会想“哦,所以类似的东西const volatile int int float const也是一个decl-specifier-seq吗?” 你是对的,它符合语法规则,但语义规则不允许这样的decl-specifier-seq。实际上,只允许使用一个类型说明符,除了某些组合(例如unsignedintconst与除自身之外的任何东西)并且至少需要一个非 cv 限定符(第 7.1.6/2-3 节)。

快速测验(您可能需要参考标准)

  1. 是否是const int const有效的声明说明符序列?如果不是,它是否被句法或语义规则所禁止?

    语义规则无效!const不能与自身结合。

  2. 是否是unsigned const int有效的声明说明符序列?如果不是,它是否被句法或语义规则所禁止?

    有效的!将fromconst分开并不重要。unsignedint

  3. 是否是auto const有效的声明说明符序列?如果不是,它是否被句法或语义规则所禁止?

    有效的!auto是声明说明符,但在 C++11 中更改了类别。之前它是一个存储说明符(如static),但现在它是一个类型说明符。

  4. 是否是int * const有效的声明说明符序列?如果不是,它是否被句法或语义规则所禁止?

    语法规则无效!虽然这很可能是声明的完整类型,但只有int声明说明符序列。声明说明符仅提供基本类型,而不提供复合修饰符,如指针、引用、数组等。

声明者

简单声明的第二部分是init-declarator-list。它是由逗号分隔的声明符序列,每个声明符都有一个可选的初始化器(第 8 节)。每个声明器都在程序中引入一个变量或函数。最简单的声明器形式就是您要介绍的名称 - declarator-id。该声明int x, y = 5;有一个声明说明符序列,它只是int,后跟两个声明符xy,第二个声明符有一个初始化器。但是,我们将在本文的其余部分忽略初始化程序。

声明符可以具有特别复杂的语法,因为这是声明的一部分,允许您指定变量是指针、引用、数组、函数指针等。请注意,这些都是声明符而不是声明的一部分作为一个整体。这正是int* x, y;不声明两个指针的原因——星号*是声明符的一部分,x而不是声明符的一部分y。一个重要的规则是每个声明者必须有一个声明者ID——它声明的名称。一旦确定了声明的类型(我们稍后会谈到),就会强制执行有关有效声明符的其余规则。

示例:声明符的一个简单示例是*const p,它声明了一个const指向...某物的指针。它指向的类型由其声明中的声明说明符给出。一个更可怕的例子是问题中给出的例子(*(*(&e)[10])())[5],它声明了一个对函数指针数组的引用,该数组返回指向...的指针。同样,类型的最后部分实际上是由声明说明符给出的。

您不太可能遇到如此可怕的声明符,但有时会出现类似的声明符。能够阅读问题中的声明是一项有用的技能,并且是一项伴随实践而来的技能。了解标准如何解释声明的类型会很有帮助。

快速测验(您可能需要参考标准)

  1. int const unsigned* const array[50];声明说明符和声明符有哪些部分?

    声明说明符: 声明符int const unsigned
    * const array[50]

  2. 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 确定。它列出了序列的类型,因为它们以任何顺序包含相应的说明符。因此,例如,任何包含signedchar以任何顺序排列的序列,包括char signed,都具有“signed char”类型。任何出现在声明说明符序列中的 cv 限定符都被添加到类型的前面。char const signed类型“const signed char”也是如此。这确保无论您放置说明符的顺序如何,类型都是相同的。

快速测验(您可能需要参考标准)

  1. 声明说明符序列的类型是什么int long const unsigned

    “常量无符号长整数”

  2. 声明说明符序列的类型是什么char volatile

    “挥发性炭”

  3. 声明说明符序列的类型是什么auto const

    这取决于!auto将从初始化程序中推导出来。例如,如果推断为int,则类型将为“const int”。

申报类型

现在我们有了声明说明符序列的类型,我们可以计算出整个标识符声明的类型。这是通过应用第 8.3 节中定义的递归过程来完成的。为了解释这个过程,我将使用一个运行示例。我们将计算ein的类型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 D1is (*(*(&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 段相匹配,其中Dis 的形式为( D1 )。这种情况很简单,类型T D就是 的类型T D1

T D1 = const float *(*(&e)[10])()

第 3 步让我们现在调用T D它并再次拆分它:

T = const float
D = *(*(&e)[10])()

这匹配 §8.3.1 指针 where Dis 的形式* 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 Dis 的形式相匹配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 Dis 的形式& 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> 替换为下面每个级别的,我们可以确定ein的类型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

好的!这显示了编译器如何推断声明的类型。请记住,如果有多个声明符,这将应用于标识符的每个声明。试着弄清楚这些:

快速测验(您可能需要参考标准)

  1. x声明中的类型是什么bool **(*x)[123];

    “指向 123 数组的指针指向 bool 的指针”

  2. 声明中的y和类型是什么?zint const signed *(*y)(int), &z = i;

    y是“指向(int)函数的指针,返回指向 const signed int 的指针”
    z是“对 const signed int 的引用”

如果有人有任何更正,请告诉我!

于 2012-12-10T20:46:01.270 回答
0

这是我解析的方式float const (*(*(&e)[10])())[5]。首先,确定说明符。这里的说明符是float const. 现在,让我们看看优先级。[] = () > *. 括号用于消除优先级的歧义。考虑到优先级,让我们识别变量 ID,即e. 因此,e 是对包含 10 个函数指针的数组(since [] > *)的引用,该函数(since () > *)不带参数并返回,并且是指向 5 个 float const 的数组的指针。所以说明符排在最后,其余部分根据优先级进行解析。

于 2017-04-05T17:47:45.580 回答