我们通过各类C语言教科书学习过C语言的朋友们应该对C语言中的声明和初始化很熟悉了吧。由于市面上的教科书出于各种原因其实大部分都没有把声明(declaration)和初始化(initialization)这块儿讲明白,尽管这不太会妨碍我们去撸代码~[笑哭] 因此笔者将由本文通过C语言标准(基于ISO/IEC 9899:2011)上的陈述,与各位一起探讨一下C语言中的声明是怎么一回事儿~注意!这里会有大家意想不到的语法知识点哦,可不是简单的扫盲贴哦~[偷笑]
声明(declaration)我们先看一下C11标准(ISO/IEC 9899:2011)对声明的语法定义:
这里先提一点,很多C语言初学者会把声明跟语句(statement)搞混,认为声明是一条语句。这其实是不准确的,声明跟语句没有任何从属关系,它们是相互独立的表达方式。
根据上述语法描述,以下代码中用注释所标出的代码行都属于声明。
上述代码是通过Visual Studio 2022 Community Edition集成开发环境(IDE)完成,可完全通过编译。各位在实验时请把C语言标准设置为C11或C17即可。
上述代码罗列了C11标准中对声明所列出的所有有效语法的组合。所以我们可以将声明分为三大类:函数声明,变量声明,以及静态断言声明。而在对变量进行声明时可直接对它进行初始化,当然初始化也可缺省。这些都是有效合法的声明方式。
初始化(initialization)初始化将是本文的重头戏。在这里笔者将给各位还原初始化在C语言中最原本的表达形式。我们下面看以下C11标准中关于初始化的部分语法定义:
上图给出了初始化器(initializer)的完整语法定义。其中,assignment-expression(赋值表达式)所包含的语法元素非常多,几乎囊括了所有表达式种类(除了逗号表达式是它的超集之外),而其最后的叶子表达式即为基本表达式(primary-expression)。C11标准中的基本表达式语法如下图所示:
另外我们也可以注意到,这里的 initializer-list 也蕴含了 initializer 本身。下面我们将看以下代码所罗列的各种初始化的表达方式。
上述代码比较基本,而且也比较简单,各位应该一看就能明白。上述代码第28行的注释中有一个 => 符号,表示蕴含,这里也可以理解为“能推导出”。另外,上图中第39行里的“init-declarator”应该用“init-declarator-list”更恰当,尽管“init-declarator”也没啥问题,不过修改后能使得上下文保持一致,不过笔者这里懒得改了,大家知道一下即可……[捂脸]
C语言标准中有趣的约束下面好玩儿的来了。上述代码展示了我们使用初始化的通用表达方式,这些代码可以在很多教科书上看到,不足为奇。而C11标准中在讲初始化这一章的约束部分第11条有一则有趣的定义。注意!这是所有满足C11标准的C语言编译器必须要遵循的语法约束!下面我们来看一下这条内容。
下面笔者将为大家仔细剖析这段话的意思。
用于一个标量(scalar)的初始化器应该是一单个表达式。我们先要知道,啥是标量。在C语言中,所谓的标量即是指非数组、非结构体或联合体类型的对象。那么什么是“一单个表达式”呢?这里大家可能会产生困惑,因为对于 assignment-expression,它本身不就是单一的表达式么?那好,我们来看看后半句,重点来了!
可选地,可用大括号包围起来。啥意思?也就是说,我们可以既可以用 assignment-expression 这一条语法作为标量的初始化器,也可以使用 { initializer-list } 这条语法作为标量的初始化器。你没有看错!所以,这里的 single 这个单词其实是针对这一条语法来说的。因为作为 initializer-list 而言,是可通过 initializer-list , designationopt initializer 这一语法来形成多个表达式的。我们后面将重点介绍这一点。
我们接着看完这段话。此标量对象的初始值为该初始化器表达式(在类型转换后)的值;对于该初始化器表达式而言,所应用的类型约束和类型转换与单一赋值一样,取此标量的类型作为其声明类型的非限定版本。这里的意思是指,为此标量做初始化的表达式,其类型取该标量所声明的非限定版本的类型。比如说:
const short x = 100; // 这里的表达式 100 应该取为short类型,且不带const
接下来,我们就好好玩玩对标量使用 { initializer-list } 这条语法作为初始化器的形式。
以下代码无论在MSVC还是GCC、Clang编译器下均能通过编译和运行。
各位需要注意的是,我们回想一下之前第11条的第一句话,这里的 { initializer-list } 其中必须只能包含一条表达式,而不能超过一条。
而由这一语法特性,我们可以找到一个万能的可以将任何一种类型的对象(包括数组和任意一个结构体或联合体)初始化为全零的表达—— { 0 } 。关于数字 0 有关的信息,各位可查看这篇博文:C语言中万能又神奇的数字——0
此外,C99标准中引入了匿名结构体字面量的表达形式,而这个语法特性同样也适用于标量。尽管我们平时在实际项目中不太会用,因为太过蛋疼……[笑哭]不过,我们还是可以看以下这个例子:
然而上述这种匿名类型字面量表达式并非完全鸡肋,我们在有些地方是可以加以利用的。因为匿名类型字面量属于左值,因此可以对它进行取地址操作!比如,我们需要调用某个函数,而它的其中一个参数是一个指针类型。我们在这种情况下可以偷懒,直接传匿名类型字面量的地址过去即可,而无需特地声明一个变量,然后传其地址。我们看以下代码。
我们从以上代码可以看到,一个匿名类型字面量完全可以将它视为一个标准普通的变量来看待,包括其生命周期也遵循普通变量,而不同点就是没在当前上下文中给出其具体标识符名,仅此而已。所以对于一些可以偷懒的地方,或是不想抓破脑袋想变量名的场合,可以试试用这玩意儿来简化你的代码~[大笑]
不过上面代码最后也显示了,针对传匿名类型字面量地址的场合往往可以用更“规范”的匿名数组字面量来代替,尽管匿名数组字面量的表达确实比匿名标量类型字面量要稍微啰嗦点,但也就多一个字符而已……[憨笑]
,