数据类型决定了程序中数据和操作的意义。
2.1 基本内置类型
2.1.1 算术类型
算术类型分为两类:整型(包括字符和布尔型在内)和浮点型。
带符号类型和无符号类型
除去布尔型和扩展的字符型之外,其他整形可以划分为带符号的(signed)和无符号的(unsigned)。带符号类型可以表示正数、负数或0,而无符号类型则仅能表示大于等于0的值。
类型 int、short、long 和 long long 都是带符号的,通过在这些类型名前添加unsigned 就可以得到无符号类型。其中 unsigned int 可以缩写为 unsigned。
与其他整型不同,字符型被分为了三种:char、signed char 和 unsigned char。但字符型的表现形式只有两种:带符号型和无符号型,char 的实际表现为哪种又编译器决定。
无符号类型中所有的比特都用来存储值。
2.1.2 类型转换
类型所能表示的值的范围决定了转换的过程:
- 非布尔 → 布尔:除 0 以外均为 true。
- 布尔 → 非布尔:false → 0,true → 1。
- 浮点数 → 整数:保留小数点前的部分。
- 给无符号数赋值超范围:结果为取模后的余数。
含有无符号类型的表达式
当一个表达式中既有无符号数又有 int 值时,那个 int 值就会转换成无符号数。若 int 值为负数,则相当于将负数赋值给一个无符号数并运算,会产生意料之外的结果。
2.1.3 字面值常量
每个字面值常量都对应着一种数据类型,字面值常量的形式和值决定了它的数据类型。
整型和浮点型字面值
我们可以将整型字面值写作十进制数、八进制数和十六进制数的形式。以 0 开头的整数代表八进制数,以 0x 或0X 开头的代表十六进制数。
整型字面值具体的数据类型由它的值和符号决定。默认情况下,十进制字面值的类型是能容纳当前值的最小带符号数类型,而八进制和十六进制字面值既可能是带符号的也可能是无符号的。
浮点型字面值表现为一个小数或以科学记数法表示的指数,默认类型为double。
字符和字符串字面值
由单引号括起来的一个字符称为字符型字面值,双引号括起来的零个或多个字符则构成字符串型字面值。
字符串字面值的类型实际上是由常量字符构成的数组,编译器在每个字符串的结尾添加一个空字符(’\0’),因此字面值实际长度比它的内容多 1。
布尔字面值和指针字面值
true 和 false 是布尔类型的字面值。
2.2 变量
变量提供一个具名的、可供程序操作的存储空间。C++ 中每个变量都有其数据类型,数据类型决定变量所占内存空间的大小和布局方式等。对 C++ 程序员来说,“变量”和“对象”一般可以互换使用。
2.2.1 变量定义
初始值
当对象在创建时获得了一个特定的值,我们说这个对象被初始化了。初始化不等同于赋值,赋值的含义是把对象的当前值擦除,以一个新的值替代。
列表初始化
初始化问题复杂性,e.g. 定义一个名为 sold 的 int 变量并初始化为 0,以下代码均可实现:
1 | int sold = 0; |
用花括号来初始化变量的形式被称为列表初始化。
2.2.2 变量声明和定义的关系
为了允许把程序拆分成多个逻辑部分来编写,C++语言支持分离式编译机制,该机制允许将程序分割为若干个文件,每个文件可被独立编译。
为了支持分离式编译,C++将声明和定义区分开来。声明使得名字为程序所知,一个文件如果想使用别处定义的名字则必须包含对那个名字的声明。而定义负责创建与名字关联的实体。声明和定义都规定了变量的类型和名字,而并以还包括了申请空间,也可能会为变量赋予初始值。
如果想声明一个变量而非定义,就在变量名前添加关键字extern,而且不要显示地初始化变量:
1 | extern int i; // 声明i而非定义i |
变量只能被定义一次,但可以被多次声明。如果要在多个文件中使用同一个变量,就必须将声明和定义分离。此时变量的定义必须且只能出现在一个文件中,而其他用到该变量的文件必须且只能对其声明。
2.3 复合类型
复合类型是指基于其他类型定义的类型,其中两种为引用和指针。
2.3.1 引用
引用为对象起了另一个名字,引用类型引用另外一种类型。通过将声明符写成&d的形式来定义引用类型,其中d是声明时的变量名:
1 | int ival = 1024; // 声明了变量名为ival |
2.3.2 指针
指针时“指向”另外一种类型的复合类型。定义指针类型的方法将声明符写成 *d 的形式,其中 d 是变量名。
1 | int ip1, *ip2; //ip1是int型对象,ip2是指向一个int型对象的指针 |
获取对象的地址
指针存放某个对象的地址,要想获取该地址,需要使用取地址符(&)。
利用指针访问对象
如果指针指向了一个对象,则允许使用解引用符(*)来访问该对象。
Note:引用声明符&、指针声明符*、取地址操作符&、解引用符* 所代表的含义各不相同。
空指针
空指针不指向任何对象,在试图使用一个空指针之前代码可以首先检查它是否为空。以下是三种等价的生成空指针的方法:
1 | int *p1 = nullptr; |
void* 指针
void*指针可用于存放任意对象的地址。但由于我们并不知道这个对象到底是什么类型,也就无法确定能在这个对象上做哪些操作,故不能直接操作void*指针所指的对象。
2.3.3 理解复合类型的声明
类型修饰符仅仅只是在声明时修饰了变量,并不能作为类型的一部分,且对与该声明语句中的其他变量不产生任何作用。
声明语句中的修饰符没有个数限制。
2.4 const 限定符
关键字const可以对变量的类型加以限定,使得被限定的变量的值在定义之后不能再改变。
1 | const int bufSize = 512 |
因为const对象一旦创建后其值就不能再改变,所以const对象必须被初始化。
对const的引用可能引用一个并非const的对象
1 | int i = 42; |
2.4.3 顶层const
用名词顶层const表示指针本身是个常量,而底层const表示指针所指的对象是一个常量。当执行对象的拷贝操作时,拷入和拷出的对象必须具有相同的底层const资格,或者两个对象的数据类型必须能够转换。(待考究,p58)
2.4.4 constexpr 和常量表达式
常量表达式是指值不会改变并且在编译过程就能得到计算结果的表达式。
1 | const int max_files = 20; // 常量表达式 |
constexpr 变量
C++11 新标准规定,允许将变量声明为constexpr类型以便由编译器来验证变量的值是否是一个常量表达式。
1 | constexpr int sz = size(); // 只有当size()是一个constexpr函数时才是一条正确的语句 |
字面值类型
2.5 处理类型
2.5.1 类型别名
类型别名是一个名字,它是某种类型的同义词。使用类型别名能让复杂的类型名变得简单明了、易于理解和使用。
有两种方法可用于定义类型别名。分别是是使用关键字 typedef和别名声明using:
1 | typedef double wage; |
2.5.2 auto 类型说明符
auto类型说明符能让编译器替我们去分析表达式所属的类型。
2.5.3 decltype 类型指示符
类型说明符decltype能从表达式的类型推断出要定义的变量的类型,但不使用该表达式的值初始化变量。其作用是选择并返回操作数的数据类型。
1 | decltype(f()) sum = x; // 使用f()的返回类型初始化sum变量,而不使用f()的值 |
2.6 自定义数据结构
2.6.1 定义 Sales_data 类型
1 | struct Sales_data { // 关键字struct + 类名 |
2.6.2 使用 Sales_data 类
添加 Sales_data 对象
1 | Sales_data data1; |
Sales_data 对象读入数据
1 | std::cin >> data1.bookNO >> data1.units_sold >> price; |
2.6.3 编写自己的头文件
为了确保哥哥文件中类的定义一致,类通常被定义在头文件中,而且类所在头文件的名字应与类的名字一样。头文件通常包含那些被定义一次的实体,如类、const 和 constexpr 变量。
预处理器概述
预处理器是在编译之前执行的一段程序,可以部分地改变我们所写的程序。能确保头文件多次包含仍能安全工作。之前用到的一项预处理器功能是#include。
C++用到的另一项与处理功能是头文件保护符,头文件保护符依赖于预处理变量。预处理变量有两种状态:已定义和未定义。#define 指令把一个名字设定为预处理变量,#ifdef 当且仅当变量已经定义是为真,#ifndef 仅当变量未定义时为真。检查结果为真时,执行后续操作直至遇到#endif 指令为止。
以下代码说明了这些功能如何有效防止重复包含:
1 |
|
第一次包含该文件时,SALES_DATA_H 未定义,预处理器顺序执行代码。再次被包含时 SALES_DATA_H 已定义,#ifndef SALES_DATA_H
检查结果为假,编译器将跳过中间语句。