Cpp Primer 阅读笔记
2. 变量和基本类型
2.1 变量
2.1.1 对象的定义
具有某种数据类型的内存空间。
2.1.2 变量的初始化
💡 建议: 当你第一次使用变量时再定义它。一般来说,在对象第一次被使用的地方附近定义它是一种好的选择,因为这样做有助于更容易地找到变量的定义。更重要的是,当变量的定义与第一次被使用的地方很近时,我们也会赋给它一个比较合理的初始值。
- 变量的初始化和赋值是不同的:
- 初始化的含义: 创建变量时赋予其一个初始值
- 赋值的含义: 把对象当前的值擦除,而以一个新值来替代
- 定义于任何函数体之外的变量被初始化为
0 - 定义在函数体内部的内置类型变量将不被初始化。一个未被初始化的内置类型变量的值是未定义的,如果试图拷贝或以其他形式访问此类值,将引发错误。
1 | int main() { |
- 每个类各自决定其初始化对象的方式,而且,是否允许不经初始化就定义对象也由类自己决定。
1 |
|
2.1.3 列表初始化
作为 C++11 新标准的一部分,用花括号来初始化变量得到了全面应用,而在此之前,这种初始化的性质仅在某些受限的场合下才能使用。
1 |
|
当用于内置类型的变量时,这种初始化形式有一个重要特点:如果我们使用列表初始化,且初始值存在丢失信息的风险,则编译器报错:
1 | int main() { |
2.1.4 变量声明和定义的关系
- extern 用法示例
1 | // main.cpp |
1 | // new.cpp |
- 变量的声明和定义
1 | // 相关知识介绍 |
- C++ 是一种静态类型 (statically typed) 语言,其含义是在编译阶段检查类型,其中,检查类型的过程称为类型检查 (type checking)。
- 我们已经知道,对象的类型决定了对象所能参与的运算。在 C++ 语言中,编译器负责检查数据类型是否支持要执行的运算,如果试图执行类型不支持的运算,编译器将报错,并且不会生成可执行文件。
- 程序越复杂,静态类型检查越有助于发现问题。然而,前提是编译器必须知道每一个实体对象的类型,这就要求我们在使用某个变量之前,必须声明其类型。
2.2 复合类型
复合类型 (compound type) 是指基于其他类型定义的类型,C++ 语言有几种复合类型,本章将介绍其中的几种:引用和指针。
与我们已经掌握的变量声明相比,定义复合类型的变量要复杂很多。
- 普通变量的定义方式:
- 一条简单的声明语句由一个数据类型和紧随其后的一个变量名列表组成。
- 更通用的描述:一条声明语句由一个基本数据类型和紧随其的一个声明符列表组成。每个声明符命名了一个变量并指定该变量为与基本数据类型有关的某种类型。
目前为止,我们所接触的声明语句中,声明符其实就是变量名,此时变量的类型也就是声明的基本数据类型,其实还可能有更复杂的声明符,它基于基本数据类型得到更复杂的类型,并把它指定给变量。
2.2.1 左值引用(右值引用暂时不介绍)
引用 (reference) 为对象起了另外一个名字,引用类型引用 (refers to) 另外一种类型。通过将声明符写成 &d 的形式来定义引用类型,其中 d 是声明的变量名:
1 | int ival = 1024; |
一般在初始化变量时,初始值会被拷贝到新建的对象中。
然而定义引用时,程序把引用和它的初始值绑定 (bind)在一起,而不是将初始值直接拷贝给引用。一旦初始化完成,引用将和它的初始值对象一直绑定在一起。因为无法令引用重新绑定到另外一个对象,因此引用必须初始化。
⚠️ 注意: 引用的初始化流程有所区别
引用并非对象,相反的,它只是为一个已经存在的对象所起的另一个名字。
1 | refVal = 2; // 把2赋给refVal指向的对象,此处即是赋给了ival |
允许在一条语句中定义多个引用,其中每个引用标识符都必须以符号 & 开头:
1 | int i = 1024, i2 = 2048; // i和i2都是int |
引用只能绑定在对象上,而不能与字面值或某个表达式的计算结果绑定在一起,同时,在大部分情况下,所有的引用类型都要和与之绑定的对象严格匹配。
2.2.2 指针
指针 (pointer) 是指向另外一种类型的复合类型。
2.2.2.1 指针和引用的区别
- 指针本身就是一个对象,允许对指针赋值和拷贝。而且指针的生命周期内它可以先后指向几个不同的对象
- 指针在定义的时候无需赋初值。和其他内置类型一样,如果没初始化,也会有不确定的值(野指针)
2.2.2.2 获取对象的地址
1 | int ival = 42; |
在大部分情况下,所有的指针类型也都要和与之绑定的对象严格匹配。
2.2.2.3 空指针
空指针 (null pointer) 不指向任何对象,在试图使用一个指针之前代码可以首先检查它是否为空。以下列出几个生成空指针的方法:
1 | int *p = nullptr; // 等价于 int *p1 = 0; |
⚠️ 重要提示: 当基类指针指向派生类的时候,若基类析构函数不声明为虚函数,在析构时,只会调用基类而不会调用派生类的析构函数,从而导致内存泄露。
2.3 const 限定符
有时我们希望定义这样一种变量,它的值不能改变。为了满足这一要求,可以用 const 对变量的类型加以限定。
值得注意的是:const 对象一旦创建之后就不可以再改变,所以 const 对象必须初始化。
2.3.1 const 变量的初始化特性
1 | const int k; // 错误,k是一个未经初始化的常量 |
在不改变 const 对象的操作中还有一种是初始化,如果利用一个对象去初始化另外一个对象,则它们是不是 const 都无关紧要。
1 | int i = 42; |
尽管 ci 是整型常量,但无论如何 ci 中的值还是一个整型数,ci 的常量特征仅仅在执行改变 ci 的操作时才会发挥作用。当用 ci 去初始化 j 时,根本无需在意 ci 是不是一个常量。拷贝一个对象的值并不会改变它,一旦拷贝完成,新的对象就和原来的对象没什么关系了。
2.3.2 一般情况下,const 变量不能跨文件
- 默认状态下,const 对象仅在文件内有效
当以编译时初始化的方式定义一个 const 对象时,就如对 bufSize 的定义一样:
1 | const int bufSize = 512; // 输入缓冲区大小 |
🔴 编译器将在编译的过程中,把所有用到 bufSize 的地方都替换成对应的值。
为了执行上述替换,编译器必须知道变量的初始值。如果程序包含多个文件,则每个用了 const 对象的文件都必须得能访问到它的初始值才行。要做到这一点,就必须在每一个用到变量的文件都有对它的定义。
为了支持这一用法,同时避免对同一变量的重复定义,const 对象被设定为仅在文件内有效。当多个文件中出现了同名的 const 变量时,其实等同于在不同文件中分别定义了独立的变量。
2.3.3 如何实现 const 变量的跨文件使用?
某些时候,有这样一种 const 变量,它的初始值不是一个常量表达式,但又确实有必要在文件间共享。这种情况下,我们不希望编译器为每个文件分别生成独立的变量。相反,我们想让这类 const 对象像其他(非常量)对象一样工作:只在一个文件中定义 const,而在多个文件的声明中使用它。
- 如何只在一个文件中定义 const 变量,但在多个文件中使用它呢?
解决的办法是:对于 const 变量不管是声明还是定义都添加 extern 关键字,这样只需要定义一次就可以了。
``cpp
// file_1.cc 定义并初始化了一个常量,该常量能被其他文件访问
extern const int bufSize = fcn(); // 是定义
// file_1.h 头文件
extern const int bufSize; // 与file_1.cc 中定义的bufSize是同一个,是声明1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
如上述程序所示,`file_1.cc` 定义并初始化了 `bufSize`。因为这条语句包含了初始值,所以它是一次定义。然而,因为 `bufSize` 是一个常量,必须用 `extern` 加以限定使其被其他文件使用。
`file_1.h` 头文件中的声明也由 `extern` 做了限定,其作用是指明 `bufSize` 并非本文件所独有,它的定义将在别处出现。
> 💡 **总结**: 如果想在多个文件之间共享 const 对象,必须在变量的定义之前添加 extern 关键字。
### 2.3.4 Const 的引用
可以把引用绑定到 `const` 对象上,就像绑定到其他对象上一样,我们称之为对**常量的引用 (reference to const)**。与普通引用不同的是,对常量的引用不能被用作修改它所绑定的对象。
引用的类型必须与其所引用对象的类型一致。
```cpp
int ci = 1024;
const int &ri = ci; // 正确:引用及其对应的对象都是常量
ri = 42; // 错误:ri是对常量的引用
int &r2 = ci; // 错误:试图让一个非常量引用指向一个常量对象
“对 const 的引用”简称为”常量引用”,但严格来说,并不存在常量引用。因为引用不是一个对象,所以我们无法让引用本身恒定不变。事实上,由于 C++ 语言并不允许随意改变引用所绑定的对象,所以从这层意义上理解,所有的引用都算是常量。
1 | double i = 5.14; |
除此之外,const int& 和 int const & 是完全等价的,两者都是对整数的常量引用,const 的位置不影响语义。
1 | int i = 1024; |
1 | const int& r2 = 24; // 正确,const int temp = 24; const int& r2 = temp;(实际上) |
2.3.5 指针和 const
与引用一样,也可以令指针指向常量或非常量。类似于常量引用,指向常量的指针 (pointer to const) 不能改变其所指对象的值(不能改变指向的对象的值),即 *p 不可修改。要想存放常量对象的地址,只能使用指向常量的指针:
1 | const double pi = 3.14; // pi是个常量,它的值不可以改变 |
和常量引用一样,指向常量的指针也没有规定其所指的对象必须是一个常量。所谓指向常量的指针仅仅要求不能通过该指针改变对象的值,而没有规定那个对象的值不能通过其他途径改变。
2.3.5.1 const 指针
指针是对象,因此可以把指针本身定为常量,常量指针 (const pointer) 必须初始化,初始化成功后,它的值(存放在指针中的地址)就不能再改变了。不能改变指向,即该变量 p 不可修改。
1 | int errNumb = 0; |
2.3.6 顶层 const
- 顶层 const: 表示指针本身就是个常量,可以表示任意对象是常量,对任何数据类型都适用
- 底层 const: 表示指针指向的对象是个常量,底层 const 则与指针和引用等复合类型的基本类型部分有关
当底层 const 进行拷贝操作时,拷入和拷出的对象都必须具有相同的底层 const 资格,或者两个对象的数据类型必须能够转换。一般来说,非常量可以转换为常量,反之则不行。
2.3.7 constexpr 和常量表达式
常量表达式 (const expression) 是指不会改变并且在编译过程就能得到计算结果的表达式。显然字面值属于常量表达式,用常量表达式初始化的 const 对象也是常量表达式。
一个对象是不是常量表达式由它的数据类型和初始值共同决定,例如:
1 | const int max_files = 20; // max_files 是常量表达式 |
尽管 staff_size 的初始值是个常量,但由于 staff_size 的值可以改动,所以他并不是一个常量表达式。
同样的,sz 本身虽然是一个常量,但是他的值必须在运行时才能知晓,不可以在编译时就定下来,所以他也不是常量表达式。
2.3.7.1 constexpr 变量(constexpr 类型)
在一个复杂系统中,分辨一个初始值到底是不是常量表达式是很难的。
C++11 新标准规定,允许将变量声明为 constexpr 类型,以便编译器来验证变量的值是否是一个常量表达式,声明 constexpr 的变量一定是一个常量,并且必须要用常量表达式来初始化。
1 | constexpr int mf = 20; // 20 是常量表达式 |
注意,不可以用普通函数作为 constexpr 变量的初始值。新标准允许定义一种特殊的 constexpr 函数,我们可以用 constexpr 函数去初始化 constexpr 变量。
2.3.7.2 字面值类型
声明 constexpr 变量时用到的类型必须有所限制,一般比较简单,值也显而易见,容易得到,就把它们称为 “字面值类型”。
目前遇到的数据类型中:算术类型、引用、指针都属于字面值类型。类不属于字面值类型,也不能被定义为 constexpr。
一个 constexpr 指针的初始值必须是 nullptr 或者 0,或是存储于某个固定地址的对象。
2.3.7.3 指针与 constexpr
如果 constexpr 声明中定义了一个指针,限定符 constexpr 仅对指针有效,与指针所指向的对象无关(无法切换指向)。
1 | const int* p = nullptr; // 指向常量的指针,底层const |
与其他常量指针类似,constexpr 既可以指向常量,也可以指向变量。
1 | constexpr int *np = nullptr; // 指向整数的常量指针,顶层const |
2.4 处理类型
2.4.1 类型别名
类型别名 (type alias) 是一个名字,它是某种类型的同义词。
方法:
1 | typedef double wages; // wages是double的同义词 |
其中,typedef 作为声明语句中的基本数据类型的一部分出现,含有 typedef 的声明语句定义的不再是变量,而是类型别名。
类型别名和类型的名字等价,只要是类型的名字能出现的地方,就能使用类型别名。
⚠️ 值得注意的是:
1 | typedef int* pint; |
b 的基本数据类型是 const pint,const 是对给定类型的修饰。pstring 实际上是指向 int 的指针,因此 const pint 就是指向 int 的常量指针。
2.4.2 auto 类型说明符
auto 可以在一条语句中声明多个变量,但因为一条声明语句只能有一个基本数据类型,所以该语句中所有变量的初始基本数据类型必须一样:
1 | auto i = 0, *p = &i; // 正确;i是整数,p是整形指针 他们的基本数据类型都是int,*是修饰符 |
2.4.2.1 复合类型、常量和 auto
使用引用其实是使用引用的对象,特别是当引用被用作初始值时,真正参与初始化的其实是引用对象的值。此时编译器以引用对象的类型作为 auto 的类型。
1 | int i = 0, &r = i; |
auto 一般会忽略掉顶层 const,同时底层 const 则会保留(注重于对象,而不是指针本身)。
1 | const int ci = i, &cr = ci; |
如果希望推断出的 auto 类型是一个顶层 const,需要明确指出:
1 | const auto f = ci; // ci的推演类型为int,f是 const int |
还可以将引用的类型设为 auto:
1 | auto &g = ci; // g 是一个整形常量的引用 |