什么是声明和声明,它们的types是如何被标准解释的?

例如float (*(*(&e)[10])())[5]声明一个types为“指向10的指针的数组的指针”的variables返回指向数组的指针5 float “?

受@DanNissenbaum的讨论启发

我在这篇文章中提到了C ++ 11标准

声明

我们所关心的types的声明在C ++语法中被称为简单声明 ,它是以下两种forms之一(§7/ 1):

decl-specifier-seq opt init-declarator-list opt ;
attribute-specifier-seq decl-specifier-seq opt init-declarator-list ;

attribute-specifier-seq是一系列属性( [[something]]和/或alignment说明符( alignas(something) )。 既然这些不影响声明的types,我们可以忽略它们和上述两种forms中的第二种。

声明说明符

所以我们声明的第一部分decl-specifier-seq由声明说明符组成。 这些包括一些我们可以忽略的东西,如存储说明符( staticextern等),函数说明符( inline等), friend说明符等等。 然而,我们感兴趣的一个声明说明符是types说明符 ,它可能包括简单types关键字( charintunsigned等),用户定义types的名称,cv-qualifiers( constvolatile )等我们不关心。

例子 :所以一个decl-specifier-seq的简单例子就是const inttypes的说明符序列。 另一个可能是unsigned int volatile

你可能会想“哦,所以像const volatile int int float const这样的东西也是一个decl-specifier-seq ? 你说得对,它符合语法的规则,但语义规则不允许这样的decl-specifier-seq 。 事实上,除了某些组合(例如unsigned int或者const除了它本身以外的任何东西)以及至less需要一个非cv-qualifier(§7.1.6/ 2-3)之外,只允许使用一个types说明符。

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

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

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

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

    有效! const无关紧要,从int分离unsigned

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

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

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

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

声明符

简单声明的第二部分是init-declarator-list 。 它是由逗号分隔的一系列声明符,每个都带有一个可选的初始化符(§8)。 每个声明符都将一个variables或函数引入到程序中。 声明符最简单的forms就是您要介绍的名称 – 声明符ID 。 声明int x, y = 5; 有一个只是int的声明说明符序列,后面跟着两个声明符xy ,其中第二个声明符有一个初始化符。 然而,我们将会忽略这篇文章的其余部分的初始化。

一个声明符可以有一个特别复杂的语法,因为这是声明的一部分,它允许你指定variables是否是指针,引用,数组,函数指针等等。注意,这些都是声明的一部分,而不是声明作为一个整体。 这正是int* x, y;的原因int* x, y; 不会声明两个指针 – 星号*x的声明符的一部分,而不是y的声明符的一部分。 一个重要的规则就是每个声明符都必须有一个声明符id声明的名字。 关于有效申报人的规则的其余部分,一旦确定了申报types,我们会强制执行(稍后会介绍)。

例子 :一个简单的声明的例子是*const p ,它声明了一个const指针… …。 它指向的types由声明中的声明说明符给出。 (*(*(&e)[10])())[5]是一个更可怕的例子,它声明了一个返回指针的函数指针数组的引用,types的最后一部分实际上是由声明说明符给出的。

你不可能遇到这样可怕的宣言,但有时会出现类似的宣言。 能够阅读像问题中的那样的声明是一种有用的技能,并且是一种随练习而来的技能。 了解标准如何解释声明的types是有帮助的。

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

  1. int const unsigned* const array[50];哪个部分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

声明types

现在我们知道一个声明由一个声明符指定符序列和一个声明符列表组成,我们可以开始思考如何确定一个声明的types。 例如,可能很明显, int* p;p定义为“指向int的指针”,但是对于其他types,它并不那么明显。

包含多个声明符的声明,比如2个声明符,被认为是特定标识符的两个声明。 也就是说, int x, *y; 是标识符xint x和声明标识符yint *y的声明。

types在标准中表示为类似英语的句子(比如“int指针”)。 这种英文forms的声明types的解释分为两部分。 首先确定声明说明符的types。 其次,recursion程序适用于整个声明。

声明说明符types

声明说明符序列的types由标准的表10确定。 它列出了序列的types,因为它们以任何顺序包含相应的说明符。 因此,例如,包含signedchar的任何顺序的任何序列都具有“signed char”types。 出现在声明说明符序列中的任何cv限定符都会添加到types的前面。 所以char const signed为“const signed char”。 这样可以确保不pipe按照什么顺序放置说明符,types都是一样的。

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

  1. 什么是声明说明符序列的typesint long const unsigned

    “const unsigned long int”

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

    “挥发性字符”

  3. 声明说明符序列auto const的types是什么?

    这取决于! auto会从初始化器中推导出来。 如果推断为int ,则types将是“const int”。

声明types

现在我们有了声明说明符序列的types,我们可以计算出一个标识符的整个声明的types。 这是通过应用§8.3中定义的recursion过程来完成的。 为了解释这个过程,我将使用一个运行的例子。 我们将在float const (*(*(&e)[10])())[5]e的types。

步骤1第一步是将声明拆分为TDforms,其中T是声明说明符序列, D是声明符。 所以我们得到:

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

T的types当然是“const float”,正如我们在上一节中所确定的那样。 然后,我们查找与当前Dforms相匹配的§8.3的小节。 你会发现这是§8.3.4数组,因为它声明它适用于表单TD声明,其中D的forms如下:

D1 [ 常量expression式opt ] 属性 – 说明符 – select

我们的D确实是这样的forms,其中D1(*(*(&e)[10])())

现在想象一个声明T D1 (我们已经摆脱了[5] )。

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

它的types是“<some stuff> T ”。 本节指出,我们的标识符e的types是“<some stuff> 5 T数组”,其中<some stuff>与虚构声明的types相同。 所以要计算出其余的types,我们需要计算出T D1的types。

这是recursion! 我们recursion地计算出声明的内部部分的types,在每一步中都剥离了它的一部分。

步骤2和以前一样,我们把新的声明分成TDforms:

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

这符合段落§8.3/ 6,其中D( D1 )的forms。 这种情况很简单, TD的types简单就是T D1的typesT D1

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

第3步现在我们打电话给这个道明,再把它分开:

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

这符合§8.3.1指针,其中D的forms是* D1 。 如果T D1types是“<some stuff> T ”,那么TDinput“<some stuff> T指针”。 所以现在我们需要T D1的types:

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

第四步我们称之为TD并分解:

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

这符合§8.3.5的function,其中DD1 ()的forms。 如果T D1有types“<some stuff> T ”,那么TD有types“<some stuff>()返回T函数”。 所以现在我们需要T D1的types:

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

第5步我们可以应用与第2步相同的规则,其中声明符只是简单的括起来,最后是:

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

第6步当然,我们把它分开:

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

我们再次将§8.3.1指针与forms* D1 D相匹配。 如果T D1types是“<some stuff> T ”,那么TDinput“<some stuff> T指针”。 所以现在我们需要T D1的types:

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

第7步拆分:

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

我们再次将§8.3.4数组与D的formsD1 [10]匹配。 如果T D1types是“<some stuff> T ”,那么TDtypes是“<some stuff> 10 T ”。 那么T D1的types是什么?

 T D1 = const float (&e) 

第8步再次应用括号步骤:

 T D1 = const float &e 

第9步拆分:

 T = const float D = &e 

现在我们匹配§8.3.2引用,其中D的forms是& D1 。 如果T D1types是“<some stuff> T ”,那么TDinput“<some stuff> T ”。 那么T D1的types是什么?

 T D1 = const float e 

第10步当然这只是“T”! 这个级别没有<some stuff>。 这是由§8.3/ 5中的基本情况规则给出的。

我们完成了!

因此,现在,如果我们看看每一步所确定的types,用下面的每一层代替<some stuff>,我们可以e float const (*(*(&e)[10])())[5]来确定e的typesfloat 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 

太好了! 这样就显示了编译器如何推导出声明的types。 请记住,如果有多个声明符,它将应用于每个标识符的声明。 尝试搞清楚这些:

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

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

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

  2. 在声明中, yz是什么types? int const signed *(*y)(int), &z = i;

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

如果有人有任何更正,请让我知道!

这是我parsingfloat const (*(*(&e)[10])())[5] 。 首先,确定说明符。 这里的说明符是float const 。 现在,我们来看看优先级。 [] = () > * 。 括号用于区分优先级。 优先考虑,让我们确定variablesID,即e 。 所以,e是一个对函数( () > * )的10个指针的数组引用(因为[] > * ),它们没有参数和返回值,而是一个指向5个常量的数组的指针。 所以说明符是最后一个,其余的根据优先级进行parsing。