Chapter 2 Variables and Basic Types
2.1 基本内置类型
2.1.1 算术类型
算术类型分为两类:整型(integral type,包括字符和布尔类型)和浮点型。
基本字符类型是 char,一个 char 的空间应确保可以存放机器基本字符集中任意字符对应的数字值。也就是说,一个 char 的大小和一个机器字节一样。
其他字符类型用于扩展字符类型,如 wchar_t、char16_t、char32_t、wchar_t 类型用于确保可以存放机器最大扩展字符集中的任意一个字符,类型 char16_t 和 char32_t 则为 Unicode 字符集服务。
浮点型可表示单精度,双精度和扩展精度值。通常,float 以一个字(32 bits)来表示,double 以两个字(64 bits)来表示,long double 以 3 或 4 个字(96 or 128 bits)来表示。一般来说,类型 float 和 double 分别有 7 个和 16 个有效位。
除去布尔型和扩展的字符型之外,其他整型可划分为 signed 和 unsigned 两种。而字符型被分为了三种:char、signed char 和 unsigned char。类型 char 实际表现为哪一种,具体是由编译器来决定的。
Type | Meaning | Minimum Size |
---|---|---|
bool | boolean | NA |
char | character | 8 bits |
wchar_t | wide character | 16 bits |
char16_t | Unicode character | 16 bits |
char32_t | Unicode character | 32 bits |
short | short integer | 16 bits |
int | integer | 16 bits |
long | long integer | 32 bits |
long long | long integer | 64 bits |
float | single-precision floating-point | 6 significant digits |
double | double-precision floating-point | 10 significant digits |
long double | extend-precision floating-point | 10 significant digits |
2.1.2 类型转换
# include <iostream>
int main() {
unsigned u = 10, u2 = 42;
std::cout << u2 - u << std::endl; // 32
std::cout << u - u2 << std::endl; // 4294967264
int i = 10, i2 = 42;
std::cout << i2 - i << std::endl; // 32
std::cout << i - i2 << std::endl; // -32
std::cout << i - u << std::endl; // 0
std::cout << u - i << std::endl; // 0
return 0;
}
2.1.3 字面值常量
我们的整型字面值写作十进制数、八进制数、十六进制数的形式。以 0 开头的数代表八进制数,以 0x 或 0X 开头的代表十六进制数。
默认情况下,是十进制字面值是带符号数,八进制和十六进制不确定。十进制字面值是 int、long、long long 中尺寸最小的那个,前提是这种类型可容纳当前的值。
有单引号括起来的一个字符称为 char 型字面值,双括号括起来的零个或多个字符则构成字符型字面值。
'a' // 字符字面值
'Hello World!' // 字符串字面值
转义序列
自己查书去。
2.2 变量
2.2.1 变量定义
初始化不是赋值,初始化的含义是创建变量时赋予其一个初始值,而赋值的含义是把对象的当前值擦除,而以一个新值来替代。
如果内置类型的变量未被显示初始化,它的值由定义的位置决定。定义于任何函数体外的变量被初始化为 0,定义在函数体内部的内置变量类型不被初始化,访问这类值将引发错误。类的对象如果没有显示地初始化,则其值由类确定。
2.2.2 变量声明和定义的关系
分离式编译机制(separate compilation):允许将程序分割成若干个文件,每个文件可悲独立编译。
声明(declaration)使得名字为程序所知,而定义(definition)负责创建与名字关联的实体。
如果像声明一个变量而非定义它,就在变量名钱添加关键字 extern,而不要显式地初始化变量:
extern int i; // 声明 i 而非定义 i
int j; // 声明并定义 j
extern double pi = 3.1416 // 定义
变量能且只能被定义一次,但是可以被多次声明。
2.2.3 标识符
C++ 的标识符由字母,数字和下划线组成,其中必须以字母或下划线开头。标识符的长度没有限制,但对大小写字母敏感。
用户自定义的标识符不能连续出现两个下划线,也不能以下划线紧连大写字母开头。此外,定义在函数体外的标识不能以下划线开头。
变量命名规范
- 标识符要能体现实际含义
- 变量名一般用小写字母
- 用户自定义的类名一般以大写字母开头
- 如果标识符由多个单词组成,则单词间应有明显区分
2.3 复合类型
2.3.1 引用
引用为对象起了另外一个名字,将声明符写成 & 的形式来定义引用类型,其中 d 是声明的变量名。定义引用时,程序把引用和它的初始值绑定在一起,而不是拷贝。一旦绑定,则一直绑定,无法重新绑定到新对象,因此必须初始化。
引用并非对象,只是一个已经存在的对象所起的另外一个名字。
引用本身不是一个对象,所以不能定义引用的引用。
int i, &ri = i;
i = 5; ri = 10;
std::cout << i << " " << ri << std::endl;
// 10 10
2.3.2 指针
指针本身就是一个对象,允许对指针赋值拷贝,而且在指针的生命周期内它可以先后指向几个不同的对象。
指针无须在定义时赋初值。和其他内置类型一样,在块作用域内定义的指针如果没有被初始化,也将拥有一个不确定的值。
定义指针类型的方法将声明符写成 *d 的形式,其中 d 是变量名。
获取对象的地址
取地址符 &
double dval;
double *pd = &dval; // 正确:初始值是 double 型对象的地址
double *pd2 = pd; // 正确:初始值是指向 double 对象的指针
int *pi = pd; // 错误:指针 pi 的类型和 pd 的类型不匹配
pi = &dval; // 错误:试图把 double 型对象的地址赋给 int 型指针
指针值
指针的值(即地址)应属于下列4种状态之一:
指向一个对象。
指向紧邻对象所占空间的下一个位置。
空指针,意味着指针没有指向任何对象。
-
无效指针,也就是上述情况之外的其他值。
试图拷贝或以其他的形式访问无效指针的值都将引发错误。尽管第2中和第3种形式的指针是有效的,但使用同样受到限制,试图访问此类指针(假定的)对象的行为不被允许。
利用指针访问对象
如果指针指向了一个对象,则允许用解引用符(*)来访问该对象:
int ival = 42;
int *p = &ival; // p 存放着变量 ival 的地址;p 是指向变量 ival 的指针
cout << *p; // 由符号*得到指针 p 所指的对象,输出42
*p = 0; // 由符号*得到指针 p 所指的对象, 即经过 p 为 ival 赋值
cout << *p; // 输出 0
对指针解引用会得出所指的对象,因此如果给解引用的结果赋值,实际上也就是给指针所指的对象赋值。
空指针
int *p1 = nullptr; // 等价于 int *p1 = 0;
int *p2 = 0; // 直接将 p2 初始化为字面常量 0
// 需要首先 #include cstdlib
int *p3 = NULL; // 等价于 int *p3 = 0;
2.4 const 限定符
const 对象一旦创建后就不能再改变,所以 const 对象必须初始化,且初始值可以是任意复杂的表达式。
初始化和 const
默认状态下, const 对象仅在文件内有效。当多个文件中出现了同名的 const 变量时,其实等同于在不同文件中分别定义了独立的变量。
如果想在多个文件之间共享 const 对象,必须在变量的定义之前添加 extern 关键字。
2.4.1 const 的引用
可以把引用绑定到 const 对象上,就像绑定到其他对象上一样,称之为对常量的引用。与普通引用不同的是,对常量的引用不能被用作修改它所绑定的对象:
const int ci = 1024;
const int &r1 = ci; // ok: both reference and underlying object are const
r1 = 42; // error: r1 is a reference to const
int &r2 = ci; // error: non const reference to a const object
因为不允许直接为 ci 赋值,当然也不能通过引用去改变 ci。因此,对 r2 的初始化时错误的。假设该初始化合法,则可以通过 r2 来改变它引用的对象的值,显然不正确。
初始化和对 const 的引用
引用的类型必须与其所引用对象的类型一致,但是有两个例外。第一种例外是在初始化常量引用时允许用任意表达式作为初始值,只要该表达式的结果能转换成引用的类型即可。尤其,允许作为一个常量引用绑定非常量的对象,字面值,甚至是一个表达式。
int i = 42;
const int &r1 = i; // we can bind a const int& to a plain int object
const int &r2 = 42; // ok: r1 is a reference to const
const int &r3 = r1 * 2; // ok: r3 is a reference to const
int &r4 = r * 2; // error: r4 is a plain, non const reference
对 const 的引用可能引用一个并非 const 的对象
常量引用并不限定引用的对象本身是不是一个常量。
int i = 42;
int &r1 = i; // r1 bound to i
const int &r2 = i; // r2 also bound to i; but cannot be used to change i
r1 = 0; // r1 is not const; i is now 0
r2 = 0; // error: r2 is a reference to const
2.4.2 指针和 const
指向常量的指针不能用于改变其所指对象的值。要想存放常量对象的地址,只能使用指向常量的子指针:
const double pi = 3.14; // pi is const; its value may not be changed
double *ptr = π // error: ptr is a plain pointer
const double *cptr = π // ok: cptr may point to a double that is const
*cptr = 42; // error: cannot assign to *cptr
2.3.2 提到,指针的类型必须与其所指对象的类型一致,但是有两个例外。第一种情况似乎允许令一个指向常量的指针指向一个非常量对象:
double dval = 3.14; // dval is a double; its value can be changed
cptr = &dval; // ok: but can't change dval through cptr
Tip:
It may be helpful to think of pointers and references to const as pointers or
references “that think they point or refer to const.”
const 指针
指针是对象而引用不是,因此允许把指针本身定为常量。常量指针必须初始化,而一旦初始化,它的值(就是那个地址)不能再改变。
2.4.3 顶层 const
术语顶层 const(top-level const)表示指针本身是个常量,而底层 const(low-level const)表示指针所指的对象是一个常量。
顶层 const 可以表示任意的对象是常量,如算术类型、类、指针等。而底层 const 则与指针和引用等复合类型部分有关:
int i = 0;
int *const p1 = &i; // we can't change the value of p1; const is top-level
const int ci = 42; // we cannot change ci; const is top-level
const int *p2 = &ci; // we can change p2; const is low-level
const int *const p3 = p2; // right-most const is top-level, left-most is not
const int &r = ci; // const in reference types is always low-level
2.4.4 constexpr 和常量表达式
常量表达式是指值不会改变并且在编译过程中得到计算结果的表达式。一个对象是不是常量表达式是由它的数据类型个初始值共同决定的:
const int max_files = 20; // max_files is a constant expression
const int limit = max_files + 1; // limit is a constant expression
int staff_size = 27; // staff_size is not a constant expression
const int sz = get_size(); // sz is not a constant expression
constexpr 变量
C++11 新标准规定,允许将变量声明为 constexpr 类型以便由编译器来验证变量的值是否是一个常量表达式。声明为 constexpr 的变量一定是一个常量,而且必须用常量表达式初始化:
constexpr int mf = 20; // 20 is a constant expression
constexpr int limit = mf + 1; // mf + 1 is a constant expression
constexpr int sz = size(); // ok only if size is a constexpr function
字面值类型
常量表达式的值需要在编译时得到计算,因此对声明 constexpr 时用到的类型必须有所限制。因为这些类型一般比较简单,值也显而易见,就称为“字面值类型”。
指针和 constexpr
在 constexpr 声明中如果定义了一个指针,限定符 constexpr 仅仅对指针有效,与指针所指的对象无关:
const int *p = nullptr; // p is a pointer to a const int
constexpr int *q = nullptr; // q is a const pointer to int
constexpr 把它所定义的对象置为了顶层 const。
constexpr int *np = nullptr; // np is a constant pointer to int that is null
int j = 0;
constexpr int i = 42; // type of i is const int
// i and j must be defined outside any function
constexpr const int *p = &i; // p is a constant pointer to the const int i
constexpr int *p1 = &j; // p1 is a constant pointer to the int j
2.5 处理类型
2.5.1 类型别名
有两种方法可以定义类型别名。传统的方法时使用关键字 typedef:
typedef duoble wages; // wages is a synonym for double
typedef wages base, *p; // base is a synonym for double, p for double*
含有 typedef 的声明语句定义的不再是变量而是类型别名。新标准规定了一个新的方法,使用别名声明来定义类型的别名:
using SI = Sales_item; // SI is a synonym for Sales_item
用 using 关键字作为别名声明的开始,其后紧跟别名和等号,其作用是把等号左侧的名字规定成等号右侧类型的别名。
指针、常量和类型别名
typedef char *pstring;
const pstring cstr = 0; // cstr is a constant pointer to char
const pstring *ps; // ps is a pointer to a constant pointer to char
const char *cstr = 0; // wrong interpretation of const pstring cstr, cstr use a pointerto const char
类型 pstring 是类型 char* 的别名。
2.5.2 auto 类型说明符
C++ 新标准引入了 auto 类型说明符,让编译器通过初始值来推算变量的类型。显然,auto 定义的变量必须有初始值。
复合类型、常量和 auto
编译器推断出得 auto 类型有时候和初始值得类型并不完全一样,编译器会适当地改变结果类型使其更符合初始化规则。
const int ci = i, &cr = ci;
auto b = ci; // b is an int (top-level const in ci is dropped)
auto c = cr; // c is an int (cr is an alias for ci whose const is top-level)
auto d = &i; // d is an int*(& of an int object is int*)
auto e = &ci; // e is const int*(& of a const object is low-level const)
auto 一般会忽略掉顶层 const,同时底层 const 则会保留下来。如若希望推断出的 auto 类型是一个顶层 const,需要明确指出:
const auto f = ci; // deduced type of ci is int; f has type const int
要在一条语句中定义多个变量,切记,符号 & 和 * 只从属于某个声明符,而非基本数据类型的一部分,因此初始值必须是同一类型:
auto k = ci, &l = i; // k is int; l is int&
auto &m = ci, *p = &ci; // m is a const int&;p is a pointer to const int
auto &n = i, *p2 = &ci; // error: type deduced from i is int; type deduced from &ci is const int
2.5.3 decltype 类型提示符
C++ 11 新标准引入了第二种类型说明符 decltype,它的作用是选择并返回操作数的数据类型。在此过程中,编译器分析表达式并得到它的类型,却并不计算表达式的值:
decltype(f()) sum = x; // sum has whatever type f returns
2.6 自定义数据结构
从最基本的层面理解,数据结构是把一组相关的数据元素组织起来然后使用它们的策略和方法。
2.6.1 定义 Sales_data 类型
初步定义 Sales_data 如下:
struct Sales_data {
std::string bookNo;
unsigned units_sold = 0;
double revenue = 0.0;
};
这个类以关键字 struct 开始,紧跟着类名和类体(其中类体部分可以为空)。类体由花括号包围形成了一个新的作用域。类内部定义的名字必须唯一,但是可以与类外的名字重复。
类体右侧表示结束的花括号后必须写一个分毫,因为类体后面紧跟变量名以示对该类型对象的定义,所以分号必不可少:
struct Sales_data { /* ... */ } accum, trans, *salesptr;
// equivalent, but better way to define these objects
struct Sales_data { /* ... */ };
Sales_data accum, trans, *salesptr;
2.6.2 使用 Sales_data 类
2.6.3 编写自己的头文件
为了确保各个文件中类的定义一致,类通常被定义在头文件中,而且类所在头文件的名字应该与类的名字一样。例如,把 Sales_data 类定义在名为 Sales_data.h 的头文件中。
头文件一旦被改变,相关的源文件必须重新编译以获取更新过的声明。
预处理器概述
确保头文件多次包含仍能安全工作的技术是预处理器(preprocessor),在编译之前执行,比如预处理功能 #include。
还有一项预处理功能是头文件保护符(header guard),头文件保护符依赖于预处理变量(2.3.2 节,p48)。预处理变量有两种状态:已定义和未定义。#define 指令把一个名字设定为预处理变量,另外两个指令分别检查某个指定的预处理变量是否已经定义:#ifdef 当且仅当变量已定义时为真,#ifndef 当且仅当变量未定义时为真。一旦检查结果为真,则执行后续操作直至遇到 #endif 指定为止。
使用这些功能就能有效地防止重复包含的发生:
#ifndef SALES_DATA_H
#define SALES_DATA_H
#include <string>
struct Sales_data {
std::string bookNo;
unsigned units_sold = 0;
double revenue = 0.0;
};
#endif
预处理变量无视 C++ 语言中关于作用域的规则。