大家好!我是大聪明-PLUS!
我们继续推出"C++ 深度解析"系列文章。本系列旨在尽可能详细地介绍各种语言特性,其中一些特性相当专业。本文是本系列的第六篇文章;之前的文章列表位于第七节末尾。本系列面向具有一定 C++ 编程经验的程序员。本文将介绍变量声明和初始化。
变量是任何编程语言的基础。在静态类型语言(例如 C++)中,所有变量都必须声明;它们的类型在编译时确定,并且在程序执行期间不能更改。变量类型可以在声明时显式指定,也可以由编译器根据初始化表达式的类型推断。
在许多编程语言中,变量声明都相当简单明了,但在 C++ 中却并非如此。虽然简单的变量声明通常不会遇到任何问题,但即使是经验丰富的程序员,遇到更复杂的情况也可能感到困惑。让我们来深入探讨变量声明的种种细节。我们还将介绍变量初始化,因为变量通常在声明时就被初始化。
介绍
我们先来注意一下声明的一个特殊之处。你可以编写语法正确的声明,并且用自然语言描述变量类型,但这种类型在 C++ 中是无法解析的。例如:指向数组的引用void、指向引用的指针、引用数组、函数数组以及返回数组的函数。我们将始终描述这些情况。
1. 基本声明语法
变量声明语句的基本语法可以描述如下:
type cv-qual pr-specs name fa-specs;
在此描述中
type,--- 是主要类型,
cv-qual--- 是 cv 限定符,
pr-specs--- 是 pr 说明符列表,
name--- 是变量名(标识符),
fa-specs--- 是 fa 说明符列表。
声明中必须包含类型和变量名;其他元素是可选的。因此,最简单的声明可能如下所示:
int` `x`; `
让我们更详细地看一下这则广告的各个要素。
1.1 基本类型
以下几种方案是可行的:
- 内置类型(
int,,double等); - 用户自定义类型(用
class,struct,union,enum,声明enum class); void(有一些限制条件)auto(有一些限制条件)- 类型别名;
- 建造
decltype(expression)。
void用户定义类型、auto类型别名和的使用decltype将在第 4 节中详细讨论。
1.2. 简历限定符
cv 限定符(const-volatile)有以下几种变体:const,volatile以及它们的组合const volatile。此限定符volatile用途非常特殊,很少使用,后续示例中也不会用到。然而,此限定符的const使用频率相当高。以下是一个示例:
int` `const` `x`; `
常量的值在初始化后不能更改。
int` `const` `x` `=` `42`;
`x` `=` `0`; `
cv 限定符可以出现在底层类型之前。以下声明与前一个声明等效:
const` `int` `x`; `
第二种形式更为常见,但一些专家认为第一种形式更可取,并提出了令人信服的论据。这种形式也常用于编译器消息中。本文将采用传统形式,即cv限定符出现在底层类型之前。
cv 限定符也可以用于声明的其他部分,例如指针说明符或非静态成员函数声明。更多详细信息请参阅相关章节。
1.3. Pr 指定符
Pr 说明符(指针引用说明符)分为两类:指针说明符和引用说明符。Pr 说明符的应用顺序是从右到左。
1.3.1 指针说明符
指针说明符可以有两种形式:
*;*cv-qual。
限定符的数量没有限制,但很少使用超过两个。两种形式可以任意组合,但通常情况下,如果某个限定符带有简历限定符,则它是列表中最右边的那个。以下是一些示例:
int` `*x`;
`int` `**x`;
`int` `**const` `x`;
`const` `int` `*x`;
`int` `*const` `x`;
`const` `int` `*const` `x`; `
因此,我们看到在声明指针时,cv 限定符可以出现多次。这是由于指针的双重性质所致。一方面,指针的主要用途是作为对某个变量的引用(这里指的是广义上的引用,即用于访问另一个对象的对象),而这个变量本身就可以拥有 cv 限定符。另一方面,指针类型在 C++ 中是一个完整的类型,因此,指针变量也可以拥有自己的 cv 限定符。我们将通过示例来说明这种区别。
我们来看看这些案例:
const` `int` `x` `=` `42`;
`const` `int` `*px` `=` `&x`; `
我们不能通过x改变值来改变值px,但我们可以改变指针本身px。
*px` `=` `0`;
`px` `=` `nullptr`; `
现在,我们来看一下这些广告:
int` `x` `=` `42`;
`int` `*const` `px=` `&x`; `
x我们可以通过改变值来px改变指针本身,但我们不能改变指针本身px。
*px` `=` `0`;
`px` `=` `nullptr`; `
最后,我们来看一些结合了这两种常量类型的声明:
const` `int` `x` `=` `42`;
`const` `int` `*const` `px` `=` `&x`;
`
x这些限制是结合起来的------我们既不能通过它改变值px,也不能改变指针本身px。
*px` `=` `0`;
`px` `=` `nullptr`; `
1.3.2 链接规范
参考标识符可以有两种形式:
&;&&。
第二种变体称为右值引用(C++11 引入),而第一种变体则称为引用或左值引用。如果存在引用说明符,则只能有一个引用,并且它必须是 pr 说明符列表中最右边的那个。否则,我们实际上会得到一个指向引用的指针或引用,而这种类型在 C++ 中是不允许的。以下是一些示例:
int` `&rx`;
`int` `&&rx`;
`int` `*&rx`;
`int` `&*rx`;
`int` `&` `&rx`; `
本系列的前一篇文章专门讨论了链接。
1.3.3 使用空格
在 C++ 代码中,符号 ` *&` 和&`&` 属于分隔符类,因此不需要用空格与基本类型、cv 限定符和标识符分隔。pr 说明符本身(未解析的引用到引用类型除外)也不需要用空格分隔。以下声明在语法上正确且等效。
int` `*x`;
`int*` `x`;
`int` `*` `x`;
`int*x`; `
插入空格是为了提高代码的可读性;空格的放置规则由既定的传统、编码规则或程序员的个人喜好决定。
1.4 变量名
变量名(标识符)必须符合标识符的规则。
1.5. Fa 规范
Fa 说明符(函数数组)分为两组:函数说明符和数组说明符。
1.5.1 功能说明符
函数说明符的形式为:
(params)
此声明包含params一个参数列表(函数参数将在 3.1 节中详细讨论)。参数列表可以为空。声明的其余部分描述了函数的返回类型。返回值可以是 `None` 类型void,这意味着函数不返回任何值。
只能有一个函数说明符,并且不能与数组说明符组合使用,因为在这种情况下,我们形式上会得到一个返回函数或数组的函数,而这种类型在 C++ 中是不允许的(请参阅第 2.3 节中解析具有多个 fa 说明符的声明的算法)。
以下是一些例子:
int` `Foo`(`int` `x`);
`void` `Foo`(`double` `y`, `int` `x`);
`int` `*Foo`();
`int` `Foo`(`int` `x`)(`char` `y`);
`
1.5.2 数组说明符
数组说明符可以有两种形式:
[N];[]。
第一种情况下N,它是一个在编译时求值的整数表达式。它的值称为数组大小,必须大于零。第二种情况下,编译器根据初始化列表(参见 6.1 节)确定数组大小,但即使在这种情况下,它也必须大于零。声明的其余部分描述了数组元素的类型。数组元素的类型不能是void引用类型。数组大小是数组类型的组成部分;元素类型相同但大小不同的两个数组是不同类型的。
数组说明符不能与函数说明符组合使用,因为在这种情况下,我们实际上会得到一个函数数组,而 C++ 不允许这种类型(参见第 2.3 节中解析具有多个 fa 说明符的声明的算法)。
以下是一些例子:
int` `x`[`4`];
`int` `x`[] `=` {`1`, `2`, `3`, `4`};
`int` `*x`[`4`];
`int` `&x`[`4`];
`void` `x`[`4`];
`int` `x`[`4`](`int`);
`
可能存在多个数组说明符,在这种情况下,将声明一个数组的数组或多维数组。说明符从左到右应用。例如,语句
int` `x`[`4`][`2`];`
声明一个大小为 4 的数组,其中每个元素都是一个大小为 2 的数组,元素类型为int。
本系列的前一篇文章专门讨论了数组。
2. 扩展声明语法
基本声明语法不允许声明某些类型,例如函数指针。这是因为 `fa` 说明符的优先级高于 `pr` 说明符,因此该指令
int` `*Foo`();`
它声明了一个返回指向 `A` 的指针的函数int,而不是返回指向另一个返回 `B` 的函数的指针int。
为了解决这个问题,使用了扩展语法,该语法使用括号来确定说明符的优先级。
2.1 指针、函数引用和数组
指针和函数引用是 C++ 类型系统的重要组成部分,因为函数不能拥有函数类型的参数或返回值(没有函数式编程就无法实现),但可以使用指针或函数引用。同样,函数也不能拥有数组类型的参数或返回值,数组元素也不能是函数类型。这些限制也可以通过使用指针和引用来克服。
声明指针、函数引用或数组的语句语法可以描述如下:
type cv-qual pr-specs2(pr-specs name)fa-specs;
在此描述中
type cv-qual pr-specs2,--- 描述函数的返回类型或数组的元素类型,
pr-specs--- 函数或数组的 pr 说明符列表,
name--- 函数或数组的名称,
fa-specs--- fa 说明符列表。
以下是一些与函数相关的示例:
int` (`*pf`)(`int`);
`int` (`&rf`)(`int`);
`int` (`*const` `cpf`)(`int`);
`int` (`**ppf`)(`int`);
`int` `*`(`*pf`)(`int`);
`
以下是一些与数组相关的示例:
int` (`*pa`)[`4`];
`int` (`&ra`)[`4`];
`int` `*`(`*pa`)[`4`];
`
2.2. 带有两个 fa 说明符的声明
当一个声明使用两个或多个带有 fa 说明符的类型时,就会产生最复杂的声明,例如返回指向函数的指针的函数或指向函数的指针数组。
请记住,函数的返回类型不能是函数类型或数组类型,数组的元素类型也不能是函数类型或引用类型。
让我们考虑一下声明一个带有两个 fa 说明符的变量这种比较一般的情况。以下是对以下几种类型的声明语句的描述:返回指向函数的指针或引用的函数;返回指向数组的指针或引用的函数;以及元素类型为指向函数的指针或指向数组的指针的数组:
type cv-qual pr-specs3(pr-specs2 name fa-specs)fa-specs2;
在此声明中
type cv-qual pr-specs3,--- 描述返回类型或元素类型,--- 返回类型或元素类型的
pr-specs2pr 说明符列表,
name--- 变量名,
fa-specs--- fa 说明符(主要)列表,
fa-specs2--- 返回类型或元素类型的 fa 说明符列表。
这听起来有点令人困惑,但也没办法。例如,如果我们有一个函数返回一个指向另一个函数的指针,那么我们就有一个主要返回值------一个指向函数的指针,而该函数本身又有一个返回值,返回值的返回类型就来源于此。解读这类声明的主要难点在于,函数返回类型或数组元素类型的 fa 说明符位于主要 fa 说明符的右侧。
或许下一节中讨论的用于分析此类广告的算法以及以下示例能够提供更清晰的解释:
int` (`*f`(`int`))(`const` `char*`);
`int` (`&f`(`int`))(`const` `char*`);
`int` (`*f`(`int`))[`4`];
`int` (`&f`(`int`))[`4`];
`int` (`*a`[`2`])(`const` `char*`);
`int` (`*a`[`2`])[`4`]; `
2.3. 具有多个 fa 说明符的声明分析
我们考虑一个相对简单的算法,用于解析包含多个 fa 说明符的声明。我们假设待分析的声明语法正确,且不包含不必要的括号。我们还假设存在多个 fa 说明符。
所讨论的算法相当形式化,但并不能保证生成的类型在 C++ 中合法。例如,当有两个 fa 说明符时,可能出现以下未解析的类型:返回函数或数组的函数、函数数组,或者指向函数或数组的引用数组(参见示例 3)。
步骤 1. 查找标识符。如果标识符后没有右括号),则查找 fa 说明符。包含标识符和其后的 fa 说明符的声明部分称为声明根。如果标识符后有右括号),则查找对应的左括号,然后在右括号后查找 fa 说明符。在这种情况下,声明根将是声明中从左括号开始到 fa 说明符结束的部分。因此,声明根将符合以下两种描述之一:
name fa-specs
(pr-specs name)fa-specs
步骤 2. 将根元素替换为一个虚拟标识符。该标识符的类型在函数的情况下将是返回类型,在数组的情况下将是元素类型。如果生成的声明包含一个 fa 说明符,则过程完成;这样的声明要么是函数声明,要么是数组声明,要么是 2.1 节中讨论的类型之一。如果生成的声明包含多个 fa 说明符,则必须对该声明再次应用步骤 1。
例1.
int` (`*`(`*f`(`char`))(`int`))(`const` `char*`);`
这则广告的根源:
f`(`char`)`
这是一个接受参数的函数char。我们将根参数替换为一个虚拟变量ff,并得到该函数的返回值声明:
int` (`*`(`*ff`)(`int`))(`const` `char*`);`
此声明包含两个 fa 说明符,因此我们继续分析。此声明的根是:
`(`*ff`)(`int`)`
这是一个指向函数的指针,该函数接受一个参数int。我们将根节点替换为一个虚拟变量fff,从而得到该函数的返回值声明:
int` (`*fff`)(`const` `char*`);`
此声明包含一个 fa 说明符;它声明了一个指向函数的指针,该函数接受一个参数const char*并返回一个值int。分析完成。
把所有这些组合起来,我们得到一个函数,它接受char并返回一个指向另一个函数的指针,该函数接受int并返回一个指向另一个函数的指针,该函数接受const char*并返回int一个值。
示例 2。
int` (`*const` `a`[`2`])(`int`);`
这则案例的根源:
a`[`2`]`
这是一个大小为 2 的数组。我们将根元素替换为一个虚拟变量aa,并得到该数组中一个元素的声明:
int` (`*const` `aa`)(`int`);`
此声明包含一个 fa 说明符;它声明了一个指向函数的常量指针,该函数接受一个参数int并返回一个值int。分析完成。
把所有这些放在一起,我们得到一个大小为 2 的数组,其元素类型为指向接受int并返回的函数的常量指针int。
例3.
int` `f`(`int`)[`4`];`
这则案例的根源:
f`(`int`)`
这是一个接受参数的函数int。我们将根参数替换为一个虚拟变量ff,并得到该函数的返回值声明:
int` `ff`[`4`];`
此声明包含一个 fa 说明符;它声明了一个大小为 4 的数组,其元素类型为int。分析完成。
综上所述,我们得到一个函数,它接受int并返回一个大小为 4 的数组,数组元素的类型为 `int` int。这种类型在 C++ 中是不允许的。
2.4 使用指针、函数引用和数组
要通过指针调用函数,无需解引用该指针。假设我们声明一个指向函数的指针。
int` (`*pf`)(`int`);`
可以这样调用相应的函数:
int` `r` `=` `pf`(`42`);`
下一个函数
int` (`*f`(`int`))(`const` `char*`);`
可以这样使用:
int` `r` `=` `f`(`42`)(`"meow"`);`
而这样的功能
int` (`*`(`*f`(`char`))(`int`))(`const` `char*`);`
所以:
int` `r` `=` `f`(`'S'`)(`42`)(`"meow"`);`
初始化函数指针时,不需要使用地址运算符&。
int` `Foo`(`int`);
`int` (`*pf`)(`int`) `=` `Foo`;`
这些特性使得函数指针可以被归类为函数类型,也就是说,函数类型的实例允许使用函数调用语法。函数类型广泛应用于模板编写中。
这些特性使得函数引用实际上变得不必要;引用没有任何优势,但引用类型却有局限性(例如,你不能声明一个引用数组)。
现在我们来看数组指针。在这种情况下,解引用是必要的。例如,对于一个数组指针
int` (`*pa`)[`4`];`
元素的访问方式如下:
`(`*pa`)[`0`] `=` `125`;`
之所以需要括号,是因为索引器的优先级高于解引用运算符*。
初始化指向数组的指针时,还必须使用地址运算符&。
int` `a`[`4`];
`int` (`*pa`)[`4`] `=` `&a`;`
与函数引用不同,数组引用之所以需求量很大,是因为通过引用访问数组的语法与访问数组本身的语法并无不同。
int` `a`[`4`];
`int` (`&ra`)[`4`] `=` `a`;
`ra`[`0`] `=` `125`;`
2.5 指向类成员的指针和指向类成员函数的指针
C++ 还有一些比较特殊的类型:指向非静态类成员的指针和非静态类成员函数的指针。声明这类变量与声明普通指针或函数指针类似,但它包含一个额外的类限定符。
声明指向非静态类成员的指针的语句语法可以描述如下:
type cv-qual pr-specs2 class-name::pr-specs1 name;
在此声明中
type cv-qual pr-specs2,--- 描述成员类型,
class-name--- 是类名,
pr-specs1--- 是成员指针说明符(必需,必须包含指针说明符),
name--- 是变量名。
如果使用 fa 说明符(指针、函数引用或数组)声明类成员,则需要使用更复杂的括号声明形式。这种形式相当繁琐,此处不再赘述。
声明指向非静态类成员函数的指针的语法可以描述如下:
type cv-qual pr-specs2(class-name::pr-specs1 name)(params)qual;
在此声明中
type cv-qual pr-specs2,--- 描述成员函数的返回类型,
class-name--- 类名,
pr-specs1--- 成员函数指针说明符(必需,必须包含指针说明符),
name--- 变量名,
params--- 成员函数参数列表(可以为空),
qual--- 成员函数的 cv 限定符或引用限定符(可选,请参阅 5.9.1、5.9.2 节)。
如果成员函数的返回值使用 fa 说明符(指针、函数引用或数组)声明,则需要更复杂的声明形式,并且还需要使用括号。这相当繁琐,因此本文不做赘述。
指向类成员的指针和指向类成员函数的指针,使用类限定符和地址运算符,通过成员或成员函数的名称进行初始化&。
我们举几个例子。
class` `X`
{
`public`:
`double` `D`;
`int` `F`(`int`);
`int` `G`(`int`) `const`;
`// ...`
};
`double` `X::*pm` `=` `&X::D`;
`int` (`X::*pf`)(`int`) `=` `&X::F`;
`int` (`X::*pfс`) (`int`) `const` `=` `&X::G`;
`int` (`X::*const` `cpf`)(`int`) `=` `&X::F`;
`int` (`X::*&rpf`)(`int`) `=` `pf`;
`
使用此类指针时,必须存在对应类的实例。要访问成员或成员函数,需要使用二元运算符 `` .*,其第一个操作数是类的类型,第二个操作数是指向成员或成员函数的指针类型。此外,还有一种二元运算符 `` ->*,其区别在于第一个操作数也是指向类的指针类型。
X` `x`;
`x`.`*pm` `=` `3.14`;
`int` `r` `=` (`x`.`*pf`)(`42`);
`X` `*px` `=` `&x`;
`px->*pm` `=` `0.04`;
`r` `=` (`px->*pf`)(`125`);`
对于静态类成员,不需要特殊类型;可以使用普通指针和函数指针。
2.6 类型表达式。简单变量、数组、函数
如果我们去掉变量声明中的变量名和最后的分号,就会得到一个可以称为类型表达式的结构。这个表达式可以用作模板参数、未命名函数参数(参见 4.1 节)、动态变量的类型(参见 4.3 节)以及别名声明语句(参见 3.4 节)。根据使用上下文,类型表达式中的某些用法可能是被禁止的auto。类型表达式也经常出现在编译器消息中。
所描述的变量声明语法允许将变量分为三类:简单变量(声明时不使用主函数说明符)、数组和函数。在某些情况下,需要明确某些规则适用于哪一类变量。
3. 基本类型
本节提供有关如何使用基本类型的更多详细信息。
3.1 使用用户自定义类型
用户自定义类型包括使用关键字定义的类型class,,,,,。struct``union``enum``enum class
通常情况下,类型是单独定义的,然后变量声明时使用该类型的名称作为其基本类型。例如:
struct` `Point`
{
`int` `X`;
`int` `Y`;
};
`Point` `pt`;`
但是,您可以将用户定义的类型定义直接插入到变量声明语句中作为主类型,例如:
struct` { `int` `X`; `int` `Y`; } `pt`;`
在这种情况下,我们将声明一个匿名类型的变量。只要该类型的名称没有在其他地方使用,这样做就完全有效。这甚至可以被视为一种良好的编码风格,因为它能防止作用域被未使用的名称所淹没。
用户自定义类型的另一个应用场景是处理不完整类型。在某些情况下,编译器只需要知道所使用的名称是用户自定义类型的名称即可编译正确的代码,而无需完整的类型定义。此时,可以使用不完整声明(也称为前向声明)。使用不完整声明的类型称为不完整类型。在一些限制条件下,不完整类型可以用作声明变量时的基类型。您可以声明指向不完整类型的指针和引用、带有不完整类型参数和返回值的函数,以及指向不完整类的成员和成员函数的指针。但是,您不能声明不完整类型的变量和数组。以下是一些示例:
struct` `Point`;
`Point` `*ppt`;
`void` `Foo`(`Point` `pt`);
`Point` `pt`; `
3.2 使用 void
在某些限制条件下,该类型void可以用作声明变量时的主要类型。该类型可以用作函数的返回类型,也可以声明指向该类型的指针。类型为 `T` 的变量、元素类型为 `T` 的数组或对 `T` 的引用void不能声明。以下是一些示例:void``void``void
void` `Foo`(`int` `x`);
`void` `*pv`;
`void` `&rv`;
`void` `v`; `
3.3 自动类型检测
大多数现代静态类型编程语言都允许不显式指定变量类型,而是让编译器根据初始化表达式的类型推断变量类型。C++11 也引入了这种功能;要实现这一点,可以使用 `type` 关键字作为底层类型auto。(这个关键字出现在 C 语言的早期版本中,但后来发现它的使用是多余的,因此 C++11 采用了新的用法。)
C 语言auto允许使用 cv 限定符、pr 限定符和函数说明符。数组、非静态类成员和未初始化的静态类成员不能声明。函数参数(lambda 表达式参数除外)也不能声明。如果在单个语句中声明多个变量,则所有变量的推断主类型必须相同。
这种用法auto有一些不太明显的特点。当进行类型推断时,变量的引用和 cv 限定符会从初始化表达式中移除。也就是说,在声明中
auto x = expression;
变量的类型x永远不会是引用类型或常量类型。如果表达式expression具有引用类型,则会创建该引用所指向实例的副本。引用说明符和 cv 限定符必须显式指定。
auto &r = expression;
const auto c = expression;
const auto &rc = expression;
在使用容器时,需要考虑这个特性,因为在使用迭代器时,重载运算符*会返回对容器元素的引用。标准的容器成员函数,例如索引器、`init` at()、` returns`front()和`returns`,也会返回对元素的引用back()。
指示
auto &&rr = expression;
声明一个通用引用,在这种情况下,变量类型rr可以根据表达式的左值/右值类别推断为右值引用或左值引用expression。
以下是如何使用它auto来创建具有带多个参数的公共构造函数的类的实例:
auto x = class-name(args);
在 C++17 中,类class-name不需要支持复制或移动操作。参数列表args可以为空,并且可以使用弯括号。
有时auto别无选择,例如使用 lambda 表达式声明闭包时。以下是一个示例:
auto` `square` `=` [](`int` `x`) { `return` `x` `*` `x`; }`
autoC++14 中引入了几个新的用例。
现在可以将其用作auto函数的返回值,在这种情况下,返回类型由语句中表达式的类型决定return。以下是一个示例:
auto` `Square`(`int` `x`) { `return` `x` `*` `x`; }`
显然,在这种情况下,你不能仅仅声明一个函数;必须有函数定义,也就是函数体。
现在可以使用autolambda 表达式作为参数类型了。以下是一个示例:
`[](`auto` `x`) { `return` `x` `>` `0`; }`
另一种函数声明形式出现了------带有尾随返回类型的函数声明,如下所示:
auto name(params) -> return-type;
实际上,这里没有类型自动检测;返回类型是显式指定的。这种形式是编写某些模板所必需的,在这些模板中,返回类型取决于模板参数。
3.4 使用化名
在 C++ 中,你可以为任何类型的类型声明别名。有两种方法:第一种是从 C 语言继承而来,使用关键字 `alias` typedef;第二种是在 C++11 中引入的,使用关键字 `alias` using。
第一种方法可以描述如下:你需要声明一个变量(不使用 `\var` auto),并在语句开头添加 `\var` typedef。之后,变量名就成为该变量类型的别名。以下是一些示例:
typedef` `int` `INT`;
`typedef` `int` `*PINT`;
`typedef` `int` `INT4`[`4`];
`typedef` `int` (`*PF`)(`int`);
`
利用这种方法,typedef你可以在一条语句中声明多个别名,这是 C 语言的典型风格。
using别名声明语句使用类型表达式(参见第 2.6 节),并使用关键字 `<alias>` 。类型表达式不能使用auto`<variable>`。(类型表达式是从变量声明语句中移除变量名后得到的。)在这种情况下,别名声明语句可以写成如下形式:
using alias-name = type-expression;
在此描述中
alias-name,别名
type-expression是一个类型表达式。
之前的例子如下所示:
using` `INT` `=` `int`;
`using` `PINT` `=` `int*`;
`using` `INT4` `=` `int`[`4`];
`using` `PF` `=` `int` (`*`)(`int`);`
声明变量时,可以使用别名作为其底层类型。在某些情况下,这样做具有一定的优势。
别名包含了所有 pr 说明符和 fa 说明符,因此在单个语句中声明多个变量时(参见 4.2 节),无需为每个变量重复声明。以下是一个示例:
using` `PINT` `=` `int*`;
`PINT` `x`, `y`, `z`;`
这相当于公告
int` `*x`, `*y`, `*z`;`
别名的另一个用途是简化第 2.1 节和第 2.2 节中的复杂声明。
公告
int` (`*pf`)(`int`);`
可以改写成这样:
using` `F` `=` `int` (`int`);
`F` `*pf`;`
公告
int` (`**ppf`)(`int`);`
可以改写成这样:
using` `PF` `=` `int` (`*`)(`int`);
`PF` `*ppf`;`
公告
int` (`*f`(`int`))(`const` `char*`);`
可以改写成这样:
using` `PF` `=` `int` (`*`)(`const` `char*`);
`PF` `f`(`int`);`
公告
int` (`*`(`*f`(`char`))(`int`))(`const` `char*`);`
可以改写成这样:
using` `PF1` `=` `int` (`*`)(`const` `char*`);
`using` `PF2` `=` `PF1` (`*`)(`int`);
`PF2` `f`(`char`);`
类似地,我们也可以提高复杂广告的可读性。
通过使用别名,我们可以消除声明中的括号,这意味着我们只需要使用基本的声明语法,但在这里要适度,否则,我们不仅不会简化,反而会适得其反。
3.5 使用 decltype
C++11 引入了 `T` 构造decltype(expression)。它本质上是表达式类型的别名expression;它可以用作声明中的底层类型。以下是一个示例:
int` `k` `=` `0`;
`decltype`(`k`) `*pk`; `
编写某些模板时需要这种结构。
在 C++14 中,你可以使用作为函数的返回类型decltype(auto);在这种情况下,返回类型的推断规则与在 中略有不同auto,例如,引用和常量不会被移除。
4. 其他细节
4.1 函数参数
声明函数、指针或函数引用时,必须在括号内提供参数列表(列表可以为空)。每个参数都是一个变量声明,如上所述。如果有多个参数,则用逗号分隔。但有一个限制:声明参数时不能使用单引号("")auto。(函数模板中实现了参数类型的自动检测。)以下是一个示例:
int` `Foo`(`int` `x`, `double` `*py`, `int` (`*pf`)(`int`));`
参数名称是可选的,这意味着您只能使用类型表达式。在这种情况下,此声明可以重写如下:
int` `Foo`(`int`, `double*`, `int` (`*`)(`int`));`
参数列表可以用关键字替换void,这相当于一个空的参数列表。
参数列表可以以省略号...(或省略号)结尾,该省略号表示任意数量(包括零个)的任意类型参数。如果显式声明了参数,则可以在该省略号前加逗号(这是为了向后兼容 C 语言),但根据 C++ 规则,逗号是可选的。以下两个声明是等效的:
void` `Foo`(`int` `x`, ...);
`void` `Foo`(`int` `x`...);`
参数可以有默认值。这些参数必须是连续的,并且包含最后一个参数。默认值用符号 `default` 指定=,调用函数时可以省略相应的参数(同样,使用默认值的参数必须是连续的,并且包含最后一个参数)。为了避免语法歧义,=有时需要在符号 `default` 前加一个空格。例如:
void` `Foo`(`int` `x` `=` `0`, `int` `y` `=` `0`);`
在这种情况下,可能会出现一些挑战:
Foo`(); `// Foo(0, 0);`
`Foo`(`42`); `// Foo(42, 0);`
`Foo`(`42`, `125`);`
如果参数是函数类型或数组类型,编译器会自动转换这些参数的类型:函数会被转换为函数指针,数组会被转换为指向元素的指针。这称为类型衰减。(您可能还会遇到归约或分解等术语。)
由于函数合并,以下两个声明是等效的:
void` `Foo`(`int` `f`(`int`));
`void` `Foo`(`int` (`*f`)(`int`));`
由于数组扁平化,以下三个声明是等效的:
void` `Foo`(`int` `a`[`4`]);
`void` `Foo`(`int` `a`[]);
`void` `Foo`(`int` `*a`);`
函数向下转型通常不会被注意到,因为通过指针调用函数不需要解引用(参见第 2.4 节),但是当向下转型数组时,大小信息会丢失,并且必须以某种方式传递此大小,例如,作为单独的参数。
为了解决数组缩减问题,使用数组引用的模式可能很有用:
template<typename` `T`, `std::size_t` `N>`
`void` `Foo`(`T` (`&a`)[`N`]);`
实例化此类模板时,编译器会推断元素类型T和数组大小N(保证大于零)。只有数组才能用作参数;指针会被拒绝。许多标准库模板(例如 `<T>`、`<T>` 等)都采用了这种std::size()技术std::begin()。
4.2. 在一个语句中声明多个变量
你可以在一条语句中声明多个变量;这些变量之间用逗号分隔。例如:
const` `int` `x`, `*y`, `z`[`4`], `*f`(`int`);`
以下规则适用:主类型及其 cv 限定符将应用于每个已声明的变量,而 pr 说明符和 fa 说明符仅应用于它们各自的变量。(此规则解释了其中一种空格样式------pr 说明符和 fa 说明符与变量名之间没有空格,但 pr 说明符与类型和 cv 限定符之间有空格。)因此,此语句等价于以下四个语句:
const` `int` `x`;
`const` `int` `*y`;
`const` `int` `z`[`4`];
`const` `int` `*f`(`int`);
`
因此,我们可以在一条语句中声明不同类型的变量。但是,不建议这样做;一条语句中应该只声明一种类型的变量。此外,应避免在一条语句中声明多个函数。这些建议有助于提高代码的可读性并减少潜在的错误。
如果每个变量都重复使用相同的说明符,则可以使用别名来避免重复(参见第 3.4 节)。
4.3 动态变量
用于分配简单变量和数组的内存分为静态内存、自动内存(栈)和动态内存。目前为止,我们讨论的都是分配在静态内存和自动内存中的变量声明;在这种情况下,C++ 运行时会处理变量的创建和删除。动态变量则需要程序员手动创建和删除。要创建变量,可以使用 `new` 运算符new,它接受变量类型并返回指向已创建变量的指针。例如:
std::string` `*d` `=` `new` `std::string`;`
要定义动态变量的类型,可以使用内置类型或用户自定义类型的名称、类型别名或类型表达式。类型表达式可以包含 `&`auto和 `&` decltype(expression)。如果类型表达式包含括号,则表达式本身必须用括号括起来。函数、引用类型和用户自定义类型定义不能使用。上述使用限制同样适用于 `&` 和 ` & `。要声明一个存储 `&` 运算符返回的指针的变量,使用void`& `会很方便。以下是一些示例:auto``new``auto
auto` `d1` `=` `new` `void*`;
`auto` `d2` `=` `new` (`int`(`*`)(`int`));
`auto` `d3` `=` `new` `struct` {`int` `X`; `int` `Y`; };
`
可以创建动态数组。在这种情况下,类型表达式必须是一个数组,但其大小无需在编译时确定。它可以是任何非负整数表达式(零也是有效值)。new此时,运算符返回指向第一个元素的指针。如果类型表达式包含多个数组说明符,则只能有一个动态大小的说明符,即最左侧的说明符。对于其余说明符,其大小必须在编译时确定(也就是说,数组只能在一维上是动态的,即第一维)。动态数组的大小不属于动态数组类型的一部分;它必须单独存储。以下是一些示例:
int` `n` `=` `12`;
`int` `*d1` `=` `new` `int`[`n`];
`auto` `d2` `=` `new` `int`[`n`][`4`];
`auto` `d3` `=` `new` (`int`(`*`[`n`])(`int`)); `
要删除动态变量,请使用运算符;delete对于数组,请使用运算符delete[]。
5. 其他说明
声明变量时,可以使用一些额外的说明符。这些说明符通常放在声明语句的开头,但有些函数说明符则放在语句的结尾,参数列表之后。在某些情况下,可以组合使用多个说明符。
5.1. 外部
此说明符表示该声明本质上是对同一模块中某个位置存在的变量定义的引用。它适用于全局、命名空间作用域或局部声明的简单变量和数组。例如,假设存在以下声明:
extern` `int` `x`;`
在这种情况下,链接器将在模块中查找定义。
int` `x`;`
如果找不到定义,或者存在多个定义,链接器将返回错误。
该说明符可以应用于函数声明,但它对改进功能几乎没有作用,因为函数已经按照所述方式使用。
5.2. 静态
该说明符的含义取决于变量声明的上下文。
如果变量是在全局范围内或命名空间范围内声明的,则此说明符指定变量的局部绑定,这意味着该变量对其他编译单元不可见。
在函数内部声明简单变量或数组时,此说明符表示该变量在调用该函数之间保持其状态。
当在类中声明时,此说明符表示成员或成员函数由该类的所有实例共享,也就是说,它不使用隐藏参数this。
5.3. 内联
适用于函数,并且在 C++17 中也适用于静态类成员。
当应用于函数时,这有两个含义。首先,它提示编译器将函数体内联到调用位置(但编译器可能会忽略此提示)。其次,它向链接器发出指令,在这种情况下,链接器不会将不同编译单元中函数的多个定义视为错误,而是会在整个模块中使用其中一个定义。
如果将此说明符应用于类的静态成员,则无论类型如何,都可以在声明时初始化该成员;如果存在定义,则链接器在这种情况下不会认为在不同的编译单元中存在该成员的多个定义是错误,并且将在整个模块中使用其中一个定义。
5.4. thread_local
此说明符在 C++11 中引入。它适用于全局或命名空间作用域内声明的简单变量和数组。使用此说明符声明的变量只能在其创建的线程中访问。该变量在线程创建时创建,在线程销毁时删除。每个线程都拥有该变量的独立副本。
5.5. alignas(N)
此说明符在 C++11 中引入。它适用于简单变量、数组和类。`*`N是一个在编译时求值的表达式;其值必须是 2 的幂。然后,变量将被分配到其值为该值的倍数的地址N。例如:
alignas`(`64`) `char` `cacheline`[`64`];`
5.6. constexpr
该说明符在 C++11 中引入。它适用于简单变量、数组、函数和成员函数。对于简单变量或数组,这意味着其值在编译时计算,且不可更改。对于函数,这意味着如果其参数的值在编译时已知,则其返回值在编译时计算。(但是,此类函数也可以使用普通参数。)例如:
constexpr` `double` `PI` `=` `3.1415926535897932`;
`constexpr` `int` `Square`(`int` `x`) { `return` `x` `*` `x`; }`
5.7. 星座
该说明符是在 C++20 中引入的。它适用于函数和成员函数。这是一个更严格的选项constexpr:调用此类函数时,参数的值必须在编译时已知。
5.8. 无例外
此说明符在 C++11 中引入。它适用于函数和成员函数,并位于语句末尾,参数列表之后。此说明符确保在函数体执行期间不会抛出异常。
5.9. 可变
适用于非静态类成员;此类成员可以在 const 成员函数中修改(参见第 5.10.1 节)。
5.10. 非静态成员函数
非静态成员函数可以有多个特殊说明符和限定符。除了 `&`virtual和 `& explicit` 之外,所有这些说明符和限定符都位于语句的末尾,参数列表之后。
5.10.1. CV 限定符
成员函数的 `cv` 限定符指向隐藏参数所指向的值this,这意味着该值仅在函数体执行期间被临时赋予此限定符。该限定符const意味着函数不能修改类成员,也就是说,它不能更改调用它的变量的位图。这被视为只读函数。以下是一个示例:
class` `X`
{
`public`:
`int` `GetCount`() `const`;
`// ...`
};`
应用于非静态成员的说明符mutable解除了此限制,这意味着允许在 const 成员函数中修改此类成员(参见第 5.8 节)。
5.10.2. 引用限定符
引用限定符&(&&在 C++11 中引入)允许根据隐藏参数指向的值的类别(左值/右值)重载函数this。
class` `X`
{
`public`:
`X`();
`void` `Foo`() `&`;
`void` `Foo`() `&&`;
`// ...`
};
`X` `x`;
`x`.`Foo`(); `// Foo() &, lvalue`
`X`().`Foo`(); `// Foo() &&, rvalue
5.10.3 特殊成员职能和类型铸造操作员
特殊成员函数包括构造函数、析构函数、复制构造函数和复制赋值运算符。在 C++11 中,又增加了移动构造函数和移动赋值运算符。
该说明符=default(在 C++11 中引入)表示给定的函数应该由编译器生成。
该说明符=delete(C++11 中引入)表示禁止调用此函数,包括隐藏函数。(该说明符=delete也可以应用于任何成员函数和自由函数;这用于微调对重载函数的调用。)
该说明符explicit(位于语句开头)适用于接受单个参数的构造函数。这意味着禁止将相应参数的类型隐式转换为定义此构造函数的类类型。在 C++11 中,explicit它可以应用于任何构造函数;在这种情况下,某些使用花括号列表的通用初始化变体将被禁止(参见 6.1.5 节)。该说明符也可以应用于类型转换运算符。在这种情况下,由该运算符定义的隐式类型转换将被禁止。
5.10.4. 虚拟函数
有一些说明符仅用于虚函数。`--virtual` 说明符virtual(位于语句开头)表示该函数是虚函数,可以在派生类中重写。对于这样的虚函数,可以使用额外的说明符 `--pure` =0,它表示该函数被声明为纯虚函数,必须在派生类中重写。包含纯虚函数的类称为抽象类;不能创建此类的实例。`--virtual` 说明符override(C++11 引入)表示这是一个重写基类中声明的虚函数的虚函数。`--virtual` 说明符final(C++11 引入)表示该虚函数不能在派生类中重写。以下是一些示例:
class` `B`
{
`public`:
`B`();
`virtual` `int` `Foo`() `=` `0`;
`// ...`
};
`B` `b`;
`class` `D` : `public` `B`
{
`public`:
`int` `Foo`() `override` `final`;
`// ...`
};
`class` `E` : `public` `D`
{
`public`:
`int` `Foo`() `override`;
`// ...`
};`
5.11. 注册
这个说明符继承自 C 语言,适用于简单变量。在 C++ 中,这个说明符很少使用,并且在 C++17 中被直接忽略(尽管它仍然保留在关键字列表中)。
5.12. 呼叫协议
在声明函数、指针或函数引用时,某些编译器(例如 MSVC)允许您选择性地指定调用约定。以下是一些示例:
int` `__stdcall` `Foo`(`int`);
`int` (`__stdcall` `*pf`)(`int`) `=` `Foo`;`
如果没有指定调用约定,编译器设置选项可以指定默认调用约定。
5.13. 出口/进口
导出/导入说明符可以应用于变量、函数和类。它们没有标准化,并且取决于平台和编译器。以下是 MSVC 中的一些示例:
__declspec`(`dllexport`) `int` `Foo`(`int`);
`__declspec`(`dllexport`) `int` `Val`;`
这些说明符用于生成模块导入/导出表。
6. 变量初始化
几乎所有现代编程语言都允许在声明变量时为其指定初始值。这称为初始化,通常与声明语句在同一条语句中进行。现代编码风格建议尽可能初始化变量,几乎所有编程语言都以某种方式支持这种风格。这对于 C++ 尤其重要,因为它可能包含未初始化的变量,这些变量包含一组随机位(平凡类型,这是对 C 语言的致敬),这可能导致难以检测的错误。某些类型的变量------常量、引用以及使用 `int` 声明的变量------auto必须进行初始化,这是由编译器强制执行的。
在 C++11 中,可以使用特殊值来初始化指针nullptr。这适用于所有指针类型,包括函数指针、数组指针、成员指针和类成员函数。使用宏NULL已被弃用,应避免使用。
函数不能被初始化。函数可以有纯声明(仅指定函数名、返回值和参数)或定义(添加函数体)。
最后需要注意的是,初始化并非赋值,尽管在某些情况下会使用 `initiative` 符号=。值只能赋给先前创建的变量,而初始化发生在创建变量的过程中。
6.1 初始化语法
C++提供了多种用于初始化变量的语法结构。几乎在所有情况下,程序员都可以从几种可能的方法中进行选择。具体的选择取决于既定的传统、编码规则,或者仅仅是程序员的个人偏好。
6.1.1 简单类型和默认初始化
声明变量时未显式初始化并不总是意味着该变量未初始化。如果声明的变量类型非平凡,那么如果存在默认构造函数(此类构造函数可由编译器生成),则会自动调用该构造函数并初始化变量。如果不存在此类构造函数,则会发生错误。例如:
std::string` `s`; `
但对于平凡类型而言,情况并非如此。平凡类型(这些类型是为了向后兼容 C 语言而引入的)包括数值类型、指针、枚举,以及由平凡类型构成的类、结构体、联合体和数组。类和结构体必须满足某些附加条件:不能包含用户自定义的构造函数、析构函数、复制函数或虚函数。对于未显式初始化的平凡类型变量,默认构造函数不会被调用(尽管编译器可以生成一个),并且这些变量会被赋予未初始化状态。在这种情况下,变量的值要么为零,要么是一组随机位,具体取决于声明上下文(参见 6.2 节)。编译器有时会检测到未初始化的变量,并发出警告甚至错误。静态代码分析器在检测未初始化的变量方面更加有效。
数组的情况类似:当声明一个非平凡类型的数组而没有显式初始化时,将对数组的每个元素调用默认构造函数,但平凡类型的数组的元素将未初始化。
C++11 标准库包含名为类型属性的模板(头文件<type_traits>)。其中一个模板允许您确定一个类型是否为平凡类型。该表达式的std::is_trivial<Т>::value值为真表示类型为平凡类型,否则true为假。T``false
我们将默认初始化定义为对变量(包括平凡类型的变量)应用默认构造函数。对于平凡类型,编译器可以生成默认构造函数。对于内置类型、指针和枚举的变量,编译器使用相应的零变体;而对于类、结构体、联合体和数组,则按成员递归应用此规则。正如本节内容所示,保证默认初始化需要一些显式初始化,这取决于所选的初始化语法,将在相应的章节中讨论。请注意,并非所有类型都支持默认初始化;类型必须具有可访问的默认构造函数(无论是用户定义的还是编译器生成的),否则编译器将报错。当没有用户定义的构造函数或使用限定符声明默认构造函数时,编译器会生成默认构造函数=default。
与简单数组和动态数组不同,标准容器的元素在未指定任何值时,默认情况下都会被初始化,例如:
std::vector<int>` `x`(`10`); `
当然,在这种情况下,容器实例化的类型必须具有可访问的默认构造函数。
6.1.2 使用等号
此方法通常用于初始化内置类型、指针、引用和枚举的变量。以下是一些示例:
int` `x` `=` `42`;
`int` `*px` `=` `&x`;
`int` `&rx` `=` `x`;
`int` (`*pf`)(`int`) `=` `nullptr`;
`auto` `y` `=` `125`;
`bool` `flag` `=` `true`;`
explicit此外,具有带一个参数的非构造函数的类的实例也可以用这种方式初始化,例如:
std::string` `s` `=` `"meow"`;`
请注意,在这种情况下,该符号=不是赋值运算符。
以上大多数示例都使用了这种语法。
6.1.3. 聚合初始化
对于简单的 C 结构体和数组,使用从 C 语言继承的聚合初始化方法。在这种情况下,=使用符号 `()` 和花括号(括号可以嵌套)。以下是一些示例:
struct` `X`
{
`double` `D`;
`int` `S`[`2`];
};
`X` `x` `=` { `3.14`, { `10`, `20` } };`
使用聚合初始化初始化数组时,无需指定其大小。
int` `a`[] `=` { `1`, `2`, `3`, `4` }; `
允许聚合初始化的类型称为聚合类型。
如果列表元素少于结构体成员或数组元素,则剩余的成员或元素保证初始化为默认值。
int` `a`[`4`] `=` { `1` }; `// 1, 0, 0, 0
因此,空花括号可确保所有成员或元素的默认初始化。
在 C++20 中,聚合类型添加了指定初始化:
struct` `Point`
{
`int` `X`;
`int` `Y`;
};
`Point` `pt` `=` { .`X` `=` `1`, .`Y` `=` `2` };`
这种初始化机制借鉴自 C 语言。已初始化的成员可以省略,而且并非只能在最后省略;默认情况下它们都会被初始化。与 C 语言不同的是,成员的顺序不能更改;必须与声明时的顺序一致。
在 C++11 中,可以省略 . 符号=。
C++17 引入了一个类型属性,用于确定类型是否为聚合类型。该表达式的std::is_aggregate<Т>::value值为真表示true它是T聚合类型,false否则为假(<type_traits>参见头文件 [VJG])。
6.1.4 使用括号
此方法通常用于初始化具有可访问多参数构造函数的类的实例。以下是一些示例:
std::string` `s`(`"meow"`);
`std::string` `w`(`3`, `'W'`);
`std::string` `u`(`"12345"`, `3`); `
内置类型的变量、指针、引用和枚举也可以用这种方式初始化。以下是一些示例:
int` `x`(`42`);
`int` `*px`(`&x`);
`int` `&rx`(`x`);
`int` (`*pf`)(`int`)(`nullptr`);
`auto` `y`(`125`);
`bool` `flag`(`true`);`
在 C++20 中,可以使用括号来表示聚合类型,例如:
struct` `Point`
{
`int` `X`;
`int` `Y`;
};
`Point` `pt`(`1`, `2`);
`int` `a`[](`1`, `2`);`
空括号表示默认初始化,但在某些情况下不能使用,因为这样的语句将被解释为声明一个没有参数的函数(参见第 6.2.1 节)。
6.1.5. 通用初始化
C++11 引入了统一初始化,它使用花括号,几乎可以在任何上下文中使用,这意味着它可以取代(除少数例外情况外)所有之前的初始化选项。以下是一些示例:
int` `x`{ `42` };
`int` `*px`{ `&x` };
`int` `&rx`{ `x` };
`int` (`*pf`)(`int`){ `nullptr` };
`bool` `flag`{ `true` };
`int` `a`[]{ `1`, `2`, `3`, `4` };
`std::string` `s`{ `"meow"` };
`std::string` `u`{ `"12345"`, `3` }; `
括号可以为空,表示使用默认初始化。对于数组,每个元素都会被初始化。
int` `x`{};
`int` `*px`{};
`int` (`*pf`)(`int`){};
`bool` `flag`{};
`int` `a`[`4`]{};
`std::string` `s`{}; `
在许多情况下,花括号可以与符号结合使用=。
int` `a`[] `=` { `1`, `2`, `3`, `4` };
`std::string` `u` `=` { `"12345"`, `3` };`
但是,如果初始化了类的实例,并且将相应的构造函数声明为,则此形式将失效explicit。
正如我们前面提到的,通用初始化可以替代其他初始化选项,但仍然存在例外情况。以下是一个传统的例子:
std::vector<int>` `x`(`10`, `3`), `y`{ `10`, `3` };`
初始化时x使用括号,结果x是一个大小为 10(第一个参数)的向量,其每个元素的值均为 3(第二个参数)。y初始化时使用通用初始化,结果y是一个大小为 2(列表的大小)的向量,其元素值为 10 和 3(列表元素的值)。这种情况可能发生在类(通常是容器)的构造函数接受一个特殊类型时std::initializer_list<>,而这种特殊类型是通过通用初始化引入的。
此外,还有一些特殊的使用规则(这些规则在 C++17 中得到了明确)。在这种情况下,带符号和不带符号的auto花括号的使用是有区别的。以下是一些示例:=
auto` `x`{ `125` };
`auto` `y` `=` { `125` };
`auto` `x2`{ `125`, `78` };
`auto` `y2` `=` { `125`, `78` };
`auto` `y3` `=` { `125`, `78.0` }; `
6.2 变量声明上下文和初始化
本节根据变量声明上下文阐明初始化规则。
6.2.1. 全局变量和局部变量
我们来考虑全局变量、命名空间变量和局部变量。如果这些变量没有限定符extern,它们会在声明语句中直接初始化。在这种情况下,可以使用任何初始化语法(所有示例都在 6.1 节中),但有一个例外:不能使用空括号进行默认初始化,因为在这种情况下,这样的声明会被解释为声明一个没有参数的函数。以下是一个示例:
std::string` `s`();
`
auto在这种情况下,可以使用空花括号、c 变体,以及对于非平凡类型,可以使用不带初始化的声明来进行默认初始化,例如:
std::string` `s1`{};
`auto` `s2` `=` `std::string`();
`std::string` `s3`; `
如果未使用初始化,则对于非平凡类型将使用默认构造函数,但对于平凡类型,如果变量是在全局范围内或在命名空间范围内声明的,则该变量将包含零位,如果是在局部范围内声明的,则该变量将包含一组随机位。
6.2.2. 动态变量
动态变量使用 `@Dynamic` 运算符创建new,该运算符接受一个变量类型并返回指向该变量的指针(参见 4.3 节)。类型后可以跟一个初始化表达式,该表达式由括号或花括号内的值列表组成,不包含逗号=。这些值可以是内置类型、指针或枚举的变量的初始值,也可以是构造函数参数,或者聚合类型的聚合初始化列表的元素。(聚合初始化的括号以及指定初始化只能在 C++20 中使用。)空括号表示默认初始化。以下是一些示例:
int` `*d1` `=` `new` `int`(`5`);
`int` `**d2` `=` `new` `int*`{ `nullptr` };
`std::string` `*d3` `=` `new` `std::string`(`3`, `'W'`);`
可以创建动态数组,在这种情况下,运算符new会返回指向第一个元素的指针。例如:
int` `*d4` `=` `new` `int`[`4`]{ `1`, `2`, `3`, `4` };`
C++11 引入了动态数组的初始化功能。初始化时,可以使用花括号(C++20 中为圆括号)括起来的值列表,但不能包含前导字符=。如果列表元素少于数组大小,则剩余元素会被默认初始化。同样,空花括号也会确保所有数组元素都被默认初始化。如果初始化时使用非空列表,则无需指定数组大小。
如果未使用初始化,则对于非平凡类型的变量,将应用默认构造函数(对于数组,每个元素都应用默认构造函数),但对于平凡类型,变量将未初始化,并将包含一组随机位。
6.2.3. 班级成员
类成员初始化的规则稍微复杂一些。类成员初始化是唯一允许声明和初始化发生在不同语句中的情况。
非静态类成员可以通过两种方式初始化:在构造函数的初始化列表中初始化,或在声明时初始化。在构造函数的初始化列表中初始化成员时,请使用圆括号(在 C++11 中为花括号),但不要使用前导字符=。例如:
class` `Point`
{
`int` `m_X`;
`int` `m_Y`;
`public`:
`Point`(`int` `x`, `int` `y`) : `m_X`(`x`), `m_Y`(`y`){}
`// ...`
};`
如果初始化需要构造函数参数,则只有此选项可用。空括号表示默认初始化。
这种方法存在一个缺陷:成员初始化是按照声明顺序进行的,而不是按照它们在列表中出现的顺序进行的。如果列表元素声明顺序错误且相互引用,则可能导致错误。
C++11 引入了在声明非静态成员时对其进行初始化的功能。
class` `X`
{
`int` `m_Length`{ `0` };
`// ...`
};`
在这种情况下,您可以使用花括号和符号的初始化语法=,但不能使用圆括号。
如果未对非静态成员指定显式初始化选项,则对于非平凡类型,将使用默认构造函数。对于未初始化的平凡类型,必须考虑声明上下文和类实例的初始化情况。如果使用编译器生成的默认构造函数初始化类实例,则成员也会默认初始化。如果类实例未初始化,或者在初始化期间使用了未显式初始化成员的用户定义构造函数,则成员将未初始化,并且将包含零位或一组随机位,具体取决于类实例的声明上下文。
现在我们来看静态成员。整数类型的静态常量成员可以在声明时初始化。在 C++11 中,声明为 `static` 的静态成员constexpr必须在声明时初始化,并且可以是任何可实例化的类型constexpr。在 C++17 中,带有 `static` 说明符的静态成员inline可以在声明时初始化,而与类型无关。
class` `X`
{
`static` `const` `int` `Coeff` `=` `125`;
`static` `constexpr` `double` `Radius` `=` `13.5`;
`static` `inline` `double` `Factor`{ `0.04` };
`static` `std::string` `M`;
`// ...`
};`
在其他情况下,定义成员时必须使用初始化。
std::string` `X::M` `=` `"meow"`;`
在声明时进行初始化,可以使用花括号和符号 `#` 的初始化语法=,但不能使用圆括号。在定义时进行初始化,可以使用任何初始化语法。
如果静态成员没有显式初始化,则对于非平凡类型,将应用默认构造函数;对于平凡类型,该成员将包含零位。
6.3 示例
最后,我们将介绍一些全局或局部声明的变量的初始化选项。
以下是类型为的变量int初始化为值的示例5。
int` `x1` `=` `5`;
`int` `x2`(`5`);
`int` `x3`{ `5` };
`int` `x4` `=` { `5` };
`auto` `x5` `=` `5`;
`auto` `x6`(`5`);
`auto` `x7`{ `5` };
`auto` `x8` `=` `int`(`5`);
`auto` `x9` `=` `int`{ `5` };`
现在我们来考虑一下具有带参数构造函数的类的实例的初始化。
class` `Int`
{
`int` `m_Value`;
`public`:
`Int`(`int` `x`) : `m_Value`(`x`) {}
`// ...`
};
`Int` `i1` `=` `5`;
`Int` `i2`(`5`);
`Int` `i3`{ `5` };
`Int` `i4` `=` { `5` };
`auto` `i5` `=` `Int`(`5`);
`auto` `i6` `=` `Int`{ `5` };`
如果类构造函数Int声明为explicit,则声明i1和i4将出错。