# 基本内置类型

C++ 定义了一套包括算术类型(arithmetic type)和空类型(void)在内的基本数据类型。其中,算术类型包含了字符、整数型、布尔值和浮点数。空类型不对应具体的值,仅用于一些特殊的场合,例如最常见的是,当函数不返回任何值时使用空类型作为返回类型。

# 算术类型

算术类型分为两类:整型(integral,包括字符和布尔类型在内)和浮点型。

类型含义最小尺寸
bool布尔类型未定义
char字符8 位
wchar_t宽字符16 位
char16_tUnicode 字符16 位
char32_tUnicode 字符32 位
short短整型16 位
int整型16 位
long长整型32 位
long long长整型64 位
float单精度浮点数6 位有效数字
double双精度浮点数10 位有效数字
long double拓展精度浮点数10 位有效数字

上述表格列出了 C++ 标准规定的算术类型的尺寸的最小值,同时允许编译器赋予这些类型更大的尺寸

# 整型与浮点型

布尔类型(bool)的取值是真(True)或者假(False)。

基本的字符类型是 char ,一个 char 的空间应确保可以存放机器基本字符集(如 ASCII 表)中任意字符对应的数字值,即,一个 char 的大小和一个机器字节一样。

wchar_t 类型用于确保可以存放机器最大扩展字符集中的任意一个字符,类型 char16_tchar32_t 则为 Unicode 字符集服务(Unicode 是用于表示所有自然语言中字符的标准)。

Unicode 为每种语言中的每个字符设定了统一并且唯一的二进制编码,现在用的是 UCS-2,即,2 个字节编码。UTF-8,UTF-16,UTF-32 均为字符编码方案。

大多数机器的字节(byte)由 8 比特(bit)构成,字(word)则由 32 或 64 比特构成,也就是 4 或 8 字节。

  • 计算机以比特序列存储数据
  • 字节是可寻址的最小内存块
  • 字是内存的基本单元

浮点型可表示单精度、双精度和扩展精度值。通常, float 以 1 个字(32 比特)来表示, double 以 2 个字(64 比特)来表示, long double 以 3 或 4 个字(96 或 128 比特)来表示。一般来说,类型 floatdouble 分别有 7 和 16 个有效位。

浮点数在机器内用指数形式表示,分解为:数符,尾数,指数符,指数。

# 带符号类型和无符号类型

除布尔型和扩展的字符型之外,其他整型可以划分为带符号的(signed)和无符号的(unsigned)两种。

  • 带符号类型可以表示正数、负数或 0
  • 无符号类型仅能表示大于等于 0 的值。

带符号类型: intshortlonglong long

在这些类型名前添加 unsigned 就可以得到对应的无符号类型

即,无符号类型: unsigned intunsigned shortunsigned longunsigned long long

类型 unsigned int 可以缩写为 unsigned

字符型分为三种: charsigned charunsigned char 。其中, signed char 类型和 unsigned char 类型分别为带符号类型和无符号类型, char 类型实际上会表现为上述两种形式中的一种,具体是哪种由编译器决定。

无符号类型中所有比特都用来存储值。

C++ 标准并没有规定带符号类型应如何表示,但是约定了在表示范围内正值和负值的量应该平衡。

8 比特的 signed char 理论上应该可以表示 -127127 区间内的值,大多数现代计算机将 8 比特的 signed char 实际的表示范围定为 -128~127

# 如何选择类型

当明确知晓数值不可能为负时,选用无符号类型。

使用 int 执行整数运算。若数值超出 int 的表示范围,选用 long long

在算术表达式中不要使用 charbool ,只有在存放字符或布尔值时才使用它们。

因为类型 char 在一些机器上是有符号的,而在另一些机器上又是无符号的,所以使用 char 进行运算特别容易出问题。如果需要使用一个不大的整数,则应该明确指定类型是 signed char 或者 unsigned char

执行浮点数运算选用 double

这是因为 float 通常精度不够而且双精度浮点数和单精度浮点数的计算代价相差无几。事实上,对于某些机器来说,双精度运算甚至比单精度还快。 long double 提供的精度在一般情况下是没有必要的,况且它带来的运行时消耗也不容忽视。

# 类型转换

类型转换是指,将对象从一种给定的类型转换(convert)为另一种相关类型。

# 转换规则(简介)

类型所能表示的值的范围决定了转换的过程:

  1. 当我们把一个非布尔类型的算术值赋给布尔类型时,初始值为 0 则结果为 false ,否则结果为 true

  2. 当我们把一个布尔值赋给非布尔类型时,初始值为 false 则结果为 0 ,初始值为 true 则结果为 1

  3. 当我们把一个浮点数赋给整数类型时,进行了近似处理。结果值将仅保留浮点数中小数点之前的部分。

  4. 当我们把一个整数值赋给浮点类型时,小数部分记为 0 。如果该整数所占的空间超过了浮点类型的容量,精度可能有损失。

  5. 当我们给无符号类型赋一个超出它表示范围的值时,结果是初始值对无符号类型表示数值总数取模后的余数。例如, 8 比特大小的 unsigned char 可以表示 0255 区间内的值,如果我们赋了一个区间以外的值,则实际的结果是该值对 256 取模后所得的余数。因此,把 -1 赋给 8 比特大小的 unsigned char 所得的结果是 255

  6. 当我们给带符号类型赋一个超出它表示范围的值时,结果是未定义的(undefined)。此时,程序可能继续工作、可能崩溃,也可能生成垃圾数据。

# 含有无符号类型的表达式

当一个算术表达式中既有无符号数又有 int 值时,那个 int 值就会转换成无符号数。

如果表达式里既有带符号类型又有无符号类型,当带符号类型取值为负时会出现异常结果,这是因为带符号数会自动地转换成无符号数

例如,在一个形如 a * b 的式子中,如果 a = -1b = 1 ,而且 ab 都是 int ,则表达式的值显然为 -1 。然而,如果 aint ,而 bunsigned ,则结果须视在当前机器上 int 所占位数而定。在我们的环境里,结果是 4294967295

切勿混用带符号类型和无符号类型

当从无符号数中减去一个值时,不管这个值是不是无符号数,我们都必须确保结果不能是一个负值。

无符号数不会小于 0 这一事实同样关系到循环的写法。

// 以降序的形式逐个输出数字10到0
for (int i = 10; i >= 0; --i)
    std::cout << i <<std::endl;

// 错误:变量u永远也不会小于0,循环条件一直成立
for (unsigned u = 10, u >= 0; --u)
    std::cout << u <<std::endl;

上例中,当 u 等于 0 时这次迭代输出 0 ,然后继续执行 for 语句里的表达式。表达式 --uu 当中减去 1 ,得到的结果 -1 并不满足无符号数的要求,此时 -1 被自动地转换成一个合法的无符号数。假设 int 类型占 32 位,则当 u 等于 0 时, --u 的结果将会是 4294967295

# 字面值常量

字面值常量(literal):形如 42 的值,一望而知。

每个字面值常量都对应一种数据类型,字面值常量的形式和值决定了它的数据类型。

# 整型和浮点型字面值

可以将整型字面值写作十进制数、八进制数或十六进制数的形式

  • 以 0 开头的整数代表八进制数

  • 以 0x 或 0X 开头的代表十六进制数

     20 //十进制
     024 //八进制
     0x14 //十六进制
    

整型字面值具体的数据类型由它的值和符号决定。默认情况下,十进制字面值是带符号类型,八进制和十六进制字面值既可能是带符号的也可能是无符号的。

  • 十进制字面值的类型是 intlonglong long 中尺寸最小的那个(例如,三者当中最小是 int),当然前提是这种类型要能容纳下当前的值
  • 八进制和十六进制字面值的类型是能容纳其数值的 intunsigned intlongunsigned longlong longunsigned long long (即,带符号的和无符号的 intlonglong long )中的尺寸最小者。如果一个字面值连与之关联的最大的数据类型都放不下,将产生错误

类型 short 没有对应的字面值

尽管整型字面值可以存储在带符号数据类型中,但严格来说,十进制字面值不会是负数。如果我们使用了一个形如 - 42 的负十进制字面值,那个负号并不在字面值之内,它的作用仅仅是对字面值取负值而已。

浮点型字面值表现为一个小数或以科学计数法表示的指数,其中指数部分用 E 或 e 标识:

3.14159
3.14159E0
0.
0e0
.001

默认的,浮点型字面值是一个 double

# 字符和字符串字面值

由单引号括起来的一个字符称为字符型字面值

双引号括起来的零个或多个字符则构成字符串型字面值

'a' // 字符字面值
"Hello World!" // 字符串字面值

字符串字面值的类型实际上是由常量字符构成的数组(array)

编译器在每个字符串字面值的结尾处添加一个空字符 ( '\0' ) 。因此,字符串字面值的实际长度要比它的内容多 1。

例如,字面值 'A' 表示的就是单独的字符 A,而字符串 "A" 则代表了一个字符的数组,该数组包含两个字符:一个是字母 A、另一个是空字符。

如果两个字符串字面值位置紧邻且仅由空格、缩进和换行符分隔,则它们实际上是一个整体。

// 当书写的字符串字面值比较长,写在一行里不太合适时,分多行书写字符串字面值
std::cout << "a really, really long string literal "
          << "that spans two lines" << std::endl;

# 转义序列

有两类字符程序员不能直接使用:

  • 一类是不可打印(nonprintable)的字符,如退格或其他控制字符,因为它们没有可视的图符
  • 另一类是在 C++ 语言中有特殊含义的字符(单引号、双引号、问号、反斜线)

这些情况需要用到转义序列(escape sequence)

C++ 语言规定的转义序列包括:

换行符 \n
纵向制表符 \v
反斜线 \\
回车符 \r
横向制表符 \t
退格符 \b
问号 \?
进纸符 \f
报警(响铃)符 \a
双引号 \"
单引号 \'

在程序中,上述转义序列被当作一个字符使用。

std::cout << '\n';          // 转到新一行
std::cout << "\tHi!\n";     // 输出一个制表符,输出"Hi!",转到新一行

我们也可以使用泛化的转义序列,其形式是:

  • \x 后紧跟 1 个或多个十六进制数字
  • \ 后紧跟 1 个、2 个或 3 个八进制数字

其中,数字部分表示的是字符对应的数值

假设使用的是 Latin-1 字符集,以下是一些示例:(对应 ASCII 表)
\7 响铃
\12 换行符
\40 空格
\0 空字符
\115 字符 M
\x4d 字符 M

例如:

std::cout << "Hi! \x4dO\115!\n";    // 输出"Hi MOM!",转到新一行
std::cout << '\115' << '\n';        // 输出"M",转到新一行

注意:

  • 如果反斜线 \ 后面跟着的八进制数字超过 3 个,只有前 3 个数字与 \ 构成转义序列。例如, "\1234" 表示 2 个字符,即八进制数 123 对应的字符以及字符 4

  • \x 要用到后面跟着的所有数字,例如, "\x1234" 表示一个 16 位的字符,该字符由这 4 个十六进制数所对应的比特唯一确定。因为大多数机器的 char 型数据占 8 位,所以上面这个例子可能会报错。一般来说,超过 8 位的十六进制字符,都是与 下一节所示表格的某个前缀作为开头的扩展字符集 一起使用

# 指定字面值的类型

通过添加前缀和后缀,可以改变整型、浮点型和字符型字面值的默认类型。

字符和字符串字面值:

前缀含义类型
uUnicode 16 字符char16_t
UUnicode 32 字符char32_t
L宽字符wchar_t
u8UTF-8 (仅用于字符串字面值常量)char

整型字面值:

后缀最小匹配类型
u 或 Uunsigned
l 或 Llong
ll 或 LLlong long

浮点型字面值:

后缀最小匹配类型
f 或 Ffloat
l 或 Llong double

当使用一个长整型字面值时,请使用大写字母 L 来标记,因为小写字母 l 和数字 1 容易混淆

L'a'        // 宽字符型字面值,类型是 wchar_t
u8"hi!"     // UTF-8 字符串字面值,类型是 char
42ULL       // 无符号整型字面值,类型是 unsigned long long
1E-3F       // 单精度浮点型字面值,类型是 float
3.14159L    // 扩展精度浮点型字面值,类型是 long double

对于一个整型字面值来说,我们能分别指定它是否带符号以及占用多少空间。

  • 如果后缀中有 U ,则该字面值属于无符号类型,也就是说,以 U 为后缀的十进制数、八进制数或十六进制数都将从 unsigned intunsigned longunsigned long long 中选择能匹配的空间最小的一个作为其数据类型
  • 如果后缀中有 L ,则字面值的类型至少是 long
  • 如果后缀中有 LL ,则字面值的类型将是 long longunsigned long long 中的一种
  • 可以将 ULLL 合在一起使用。例如,以 UL 为后缀的字面值的数据类型将根据具体数值情况或者取 unsigned long ,或者取 unsigned long long

# 布尔字面值和指针字面值

truefalse 是布尔类型的字面值

nullptr 是指针字面值

# 变量

C++ 中的每个变量都有其数据类型,数据类型决定着变量所占内存空间的大小和布局方式、该空间能存储的值的范围,以及变量能参与的运算

对 C++ 程序员来说,“变量(variable)” 和 “对象(object)” 一般可以互换使用。通常情况下,对象是指一块能存储数据并具有某种类型的内存空间

# 变量定义

变量定义的基本形式是:首先是类型说明符(type specifier),随后紧跟由一个或多个变量名组成的列表,其中变量名以逗号分隔,最后以分号结束

列表中每个变量名的类型都由类型说明符指定,定义时还可以为一个或多个变量赋初值,例如:

int sum = 0, value, // sum, value, and units_sold have type int
    units_sold = 0; // sum and units_sold have initial value 0
Sales_item item;    // item has type Sales_item (see § 1.5.1 (p. 20))
// string is a library type, representing a variable-length sequence of characters
std::string book("0-201-78345-X"); // book initialized from string literal

# 初始化

当对象在创建时获得了一个特定的值,我们说这个对象被初始化(initialized)了

在同一条定义语句中,可以用先定义的变量值去初始化后定义的其他变量,例如:

// ok: price is defined and initialized before it is used to initialize discount
double price = 109.99, discount = price * 0.16;
// ok: call applyDiscount and use the return value to initialize salePrice
double salePrice = applyDiscount(price, discount);

初始化不是赋值

  • 初始化的含义是创建变量时赋予其一个初始值
  • 赋值的含义是把对象的当前值擦除,而以一个新值来替代

# 列表初始化

列表初始化(List Initialization): 用花括号来初始化变量,例如:

int units_sold = {0};
int units_sold{0};

当我们使用列表初始化以初始化内置类型的变量时,如果初始值存在丢失信息的风险,编译器将报错

例如,使用一个 long double 型的值来初始化一个 int 型的变量,此时可能丢失数据(至少 ld 的小数部分会丢失掉,而且 int 可能也存不下 ld 的整数部分),所以编译器拒绝 ab 的初始化请求:

long double ld = 3.1415926536;
int a{ld}, b = {ld}; // error: narrowing conversion required
int c(ld), d = ld;   // ok: but value will be truncated

# 默认初始化

默认初始化(default initialized):如果定义变量时没有指定初值,则变量会被赋予 “默认值”

  1. 如果是 内置类型的变量 未被显式初始化,它的值由定义的位置决定

    • 定义于任何函数体之外的变量被初始化为 0
    • 定义在函数体内部的内置类型变量将不被初始化(uninitialized)
  2. 每个类各自决定其初始化对象的方式,而且,是否可以不经初始化就定义对象也由类自己决定

    • 绝大多数类都支持无须显式初始化而定义对象,即,为对象提供了一个合适的默认值。例如, string 类规定:如果没有指定初值,则生成一个空串
    • 一些类要求每个对象都显式初始化。此时,如果创建了一个该类的对象却并未对其做明确的初始化操作,将引发错误

定义于函数体内的内置类型的对象如果没有初始化,则其值未定义
类的对象如果没有显式地初始化,则其值由类确定

未初始化的变量含有一个不确定的值,使用未初始化的变量将带来无法预计的后果。

建议初始化每一个内置类型的变量

# 变量声明

C++ 语言支持分离式编译(separatecompilation)机制,即,允许将程序分割为若干个文件,每个文件可被独立编译

特别地,一个文件如果想使用别处定义的名字则必须包含对那个名字的声明

  • C++ 是一种静态类型(statically typed)语言,即,在编译阶段检查变量的类型。其中,检查类型的过程称为类型检查(type checking)
  • 我们已经知道,对象的类型决定了对象所能参与的运算。在 C++ 语言中,编译器负责检查数据类型是否支持要执行的运算,如果试图执行类型不支持的运算,编译器将报错并且不会生成可执行文件
  • 程序越复杂,静态类型检查越有助于发现问题。然而,前提是编译器必须知道每一个实体对象的类型,这就要求我们在使用某个变量之前必须声明其类型

变量声明(declaration):规定了变量的类型和名字

变量定义:不仅规定了变量的类型和名字,还申请存储空间,也可能会为变量赋一个初始值

如果想声明一个变量,就在变量名前添加 extern 关键字 ,注意,不要显式地初始化变量 ,例如:

extern int i;   // declares but does not define i
int j;          // declares and defines j

任何包含显式初始化的声明即成为定义 ,即,如果 extern 语句包含初始值,那么它就不再是声明,而是定义,例如:

extern double pi = 3.1416; // definition

在函数体内部,如果试图初始化一个由 extern 关键字标记的变量,将引发错误。即,包含有 extern 标记的定义,例如, extern double pi=3.14; ,不能放在函数体内部

变量能且只能被定义一次,但是可以被多次声明

如果要在多个文件中使用同一个变量,必须将声明和定义分离:

  • 变量的定义必须出现在且只能出现在一个文件中
  • 其他用到该变量的文件必须对其进行声明,但不能重复定义

# 标识符

C++ 的标识符(identifier)由 字母数字下划线 组成,并且,必须以字母或下划线开头

标识符的长度没有限制,但是,对大小写字母敏感 ,例如:

// defines four different int variables
int somename, someName, SomeName, SOMENAME;

C++ 关键字 和 操作符替代名 不能被用作标识符

此外,也还需要注意:

  • 标识符不能连续出现两个下划线
  • 不能以下划线紧连大写字母开头
  • 定义在函数体外的标识符不能以下划线开头

变量命名规范变量命名有许多约定俗成的规范,以有效提高程序的可读性:

  • 标识符要能体现实际含义
  • 变量名一般用小写字母,如 index ,而不使用 IndexINDEX
  • 用户自定义的类名一般以大写字母开头,如 Sales_item
  • 如果标识符由多个单词组成,则单词间应有明显区分,如下划线命名法 student_loan 或驼峰命名法 studentLoan ,而不要使用 studentloan

# 名字的作用域

不论是在程序的什么位置,使用到的每个名字都会指向一个特定的实体:变量、函数、类型等。然而,同一个名字如果出现在程序的不同位置,也可能指向的是不同实体

作用域(scope),可简单理解为 名字的有效区域

  • C++ 语言中大多数作用域都以花括号分隔

  • 同一个名字在不同的作用域中可能指向不同的实体

  • 名字的有效区域始于名字的声明语句,止于声明语句所在的作用域末端

A scope is a part of the program in which a name has a particular meaning.

例如:

#include <iostream>
int main() {
    int sum = 0;
    // sum values from 1 through 10 inclusive
    for (int val = 1; val <= 10; ++val)
        sum += val;  // equivalent to sum = sum + val
    std::cout << "Sum of 1 to 10 inclusive is "
              << sum << std::endl;
    return 0;
}

上例中, main 定义于所有花括号之外,具有 全局作用域(global scope),在整个程序的范围内都可使用; sum 定义于 main 函数所限定的作用域内,在 main 函数作用域内任意位置都可以访问,但无法在 main 函数之外访问,即,具有 块作用域(block scope)

一般来说,在对象第一次被使用的地方附近定义它是一种好的选择

作用域可以彼此包含,被包含(或者说被嵌套)的作用域称为 内层作用域(inner scope),如上例中 val 的作用域;包含着别的作用域的作用域称为 外层作用域(outer scope),如上例中 sum 的作用域

作用域中一旦声明了某个名字,它所嵌套着的所有作用域中都能访问该名字。但与此同时,允许在内层作用域中重新定义外层作用域已有的名字:

#include <iostream>
// Program for illustration purposes only: It is bad style for a function
// to use a global variable and also define a local variable with the same name
int reused = 42;  // reused has global scope
int main()
{
    int unique = 0; // unique has block scope
    // output #1: uses global reused; prints 42 0
    std::cout << reused << " " << unique << std::endl;
    int reused = 0; // new, local object named reused hides global reused
    // output #2: uses local reused; prints 0 0
    std::cout << reused << " " << unique << std::endl;
    // output #3: explicitly requests the global reused; prints 42 0
    std::cout << ::reused << " " << unique << std::endl;
    return 0;
}

如果函数有可能用到某个全局变量,则不宜再定义一个同名的局部变量

# 复合类型

复合类型(compound type)是指基于基本数据类型定义的类型

这里将介绍两种复合类型:引用指针

# 引用

这里的 “引用(reference)” ,指的其实是 “左值引用(lvalue reference)”

引用(reference)为对象起了另外一个名字,即,别名

通过将声明符写成 &d 的形式来定义引用类型,其中 d 是声明的变量名

如下所示, refValival 的一个引用

int ival = 1024;
int &refVal = ival; // refVal 指向 ival(是 ival 的另一个名字)
int &refVal2;       // 报错:引用必须被初始化

定义引用时,程序把 引用 和 它的初始值 绑定(bind)在一起,而不是将初始值拷贝给引用

  • 一旦初始化完成,引用 将和 它的初始值对象 一直绑定在一起
  • 注意,无法将引用重新绑定到另外一个对象,因此,引用必须初始化

# 引用即别名

引用并不是对象,它只是为一个已经存在的对象额外起一个名字

对 引用 进行的所有操作都是在与之绑定的对象上进行的:

  • 为 引用 赋值,实际上是把值赋给了 引用所绑定的对象
  • 获取 引用 的值,实际上是获取 引用所绑定的对象 的值
  • 以 引用 作为初始值,实际上是以 引用所绑定的对象 作为初始值

例如:

refVal = 2;               // 把2赋给refVal指向的对象,即,赋给ival
int ii = refVal;          // 与ii = ival执行结果一样
int &refVal3 = refVal;    // 正确:refVal3绑定到了那个与refVal绑定的对象上,这里就是绑定到ival上
int i = refVal;           // 正确:i被初始化为ival的值

引用本身不是一个对象,所以不能定义引用的引用

# 引用的定义

允许在一条语句中定义多个引用,其中,每个引用标识符都必须以符号 & 开头,例如:

int i =1024, i2 = 2048; // i 和 i2 都是 int
int &r = i, r2 = i2;    // r 是 i 的引用,r2 是 int 型对象
int i3 = 1024, &ri = i3;// i3 是 int 型对象,ri 是 i3 的引用
int &r3 = i3, &r4 = i2; // r3 和 r4 都是引用

除了两种例外情况,其他所有 引用的类型 都要严格匹配于 与之绑定的对象

并且,引用只能绑定在对象上,而不能与字面值或某个表达式的计算结果绑定在一起

int &refVal4 = 10;      // 错误:引用类型的初始值必须是一个对象,而不能是字面值
double dval = 3.14;
int &refVal5 = dval;    // 错误:此处引用类型的初始值必须是 int 型对象

# 指针

指针(pointer)是 “指向(point to)” 另外一种类型的复合类型

与引用类似,指针也可实现对其他对象的间接访问

与引用不同的是:

  1. 指针本身是一个对象,允许对指针赋值和拷贝
  2. 指针可以先后指向不同的对象
  3. 指针无须在定义时赋初值

如果在块作用域内定义一个指针,但没有初始化指针,指针将拥有一个不确定的值

通过将声明符写成 *d 的形式来定义指针,其中 d 是变量名

如果在一条语句中定义多个指针变量,每个变量前面都必须有符号 * ,例如:

int *ip1, *ip2;  // ip1 和 ip2 都是指向 int 型对象的指针
double dp, *dp2; // dp2 是指向 double 型对象的指针,dp 是 double 型对象

# 获取对象的地址

指针存放的是某个对象的地址

要获取某对象的地址,需使用 取地址符( & 操作符)

int ival = 42;
int *p = &ival; // p 存放变量 ival 的地址,或者说,p 是指向变量 ival 的指针
                // 把 p 定义为一个指向 int 的指针,随后初始化 p 令其指向名为 ival 的 int 对象

引用不是对象,没有实际地址,因此,不能定义指向引用的指针

除两种特殊情况外,其他所有 指针的类型 都要和 它所指向的对象 严格匹配,例如:

double dval;
double *pd = &dval;     // 正确:初始值是 double 型对象的地址
double *pd2 = pd;       // 正确:初始值是 double 对象的指针

int *pi = pd;           // 错误:指针 pi 的类型和 pd 的类型不匹配
pi = &dval;             // 错误:试图把 double 型对象的地址赋给 int 型指针

实际上,在声明语句中,指针的类型 用于确定 指针所指对象的类型 。因此,二者必须匹配

# 指针值

指针的值(即地址),应属下列 4 种状态之一:

  1. 指向一个对象
  2. 指向紧邻对象所占空间的下一个位置
  3. 空指针,即,指针没有指向任何对象
  4. 无效指针,即,上述情况之外的其他值

拷贝 / 访问无效指针的值将会引发错误

# 利用指针访问对象

如果指针指向了一个对象,则允许使用 解引用符( * 操作符),来访问该对象

int ival = 42;
int *p = &ival; // p 是指向变量 ival 的指针
cout << *p;     // 解引用,访问指针所指对象,输出 42

对指针解引用可以得到指针所指的对象

因此,给解引用所得结果赋值,也就是给指针所指对象赋值

*p = 0;     // 经由指针 p 为 p 所指对象(即,ival)赋值
cout << *p; // 输出 0

解引用操作仅适用于那些确实指向了某个对象的有效指针

# 空指针

空指针(null pointer)不指向任何对象

生成空指针的方法:

int *p1 = nullptr;
int *p2 = 0;
int *p3 = NULL;     // 需要首先 #include cstdlib

获得空指针最直接的办法:用字面值 nullptr 来初始化指针

  • nullptr 是一种特殊类型的字面值,它可以被转换成任意其他的指针类型
  • C++ 程序中,建议使用 nullptr ,而尽量避免使用 NULL

int 变量直接赋给指针是错误的操作,即使 int 变量的值恰好等于 0 也不行

int zero = 0;
pi = zero;      // 错误:不能把 int 变量直接赋给指针

尽管 C++ 语法上规定 “指针无须在定义时初始化”,但是,使用未经初始化的指针很可能会在运行时引发错误

因此,建议初始化所有的指针,并且,尽量在定义对象之后才定义指向它的指针。如果确实不清楚指针应该指向何处,可以把它初始化为 nullptr 或者 0

# 赋值和指针

指针和引用都能实现对其他对象的间接访问

  • 引用本身不是一个对象。一旦定义了引用,就无法再将其绑定到另外的对象,以后每次使用这个引用都是访问它最初绑定的那个对象
  • 指针可以指向新的对象:给指针赋值就是令它存放一个新的地址,从而指向一个新的对象
pi = &ival; // pi 的值被改变,现在 pi 指向了 ival
*pi = 0;    // ival 的值被改变,指针 pi 并没有改变

# 其他指针操作

判断 truefalse

  • 如果指针的值是 0 ,即,空指针,对应的条件值取 false
  • 如果指针非空,对应的条件值是 true

对于两个类型相同的合法指针,可以用相等操作符( == )和不相等操作符( != )来比较它们,其结果为布尔类型的值

  • 如果两个指针所存放的地址值相同,则它们相等
  • 反之,它们不相等

两个指针相等,对应有三种可能:

  • 两个指针都为空
  • 两个指针指向同一个对象
  • 两个指针都指向了同一个对象的下一地址

需要注意的是,一个指针指向某对象,同时,另一个指针指向另外对象的下一地址,此时也有可能出现这两个指针值相同的情况,即指针相等

# void * 指针

void * 是一种特殊的指针类型,可用于存放任意对象的地址

但是,我们无法直接确定该地址中到底是个什么类型的对象

double obj = 3.14, *pd = &obj;
void *pv = &obj;    // 正确:void * 能存放任意类型对象的地址
pv = pd;            // 正确

void * 指针的作用:

  • 与别的指针比较
  • 作为函数的输入或输出
  • 赋给另外一个 void * 指针

注意,我们并不知道这个对象到底是什么类型,所以,不能直接操作 void * 指针所指的对象(因为无法确定能在这个对象上做哪些操作)

给定指针 p ,你能知道它是否指向了一个合法的对象吗?如果能,叙述判断的思路;如果不能,说明原因

  • 如果在定义 p 时曾用 nullptr 或者 0 初始化 p ,判断 p 是否指向合法的对象,只需把 p 作为 if 语句的条件即可,如果 p 的值是 nullptr ,则条件为假;反之,条件为真
  • 如果没有注意 p 的初始化,可把 if(p) 置于 try 结构中,当程序块顺利执行时,表示 p 指向了合法的对象;当程序块出错跳转到 catch 语句时,表示 p 没有指向合法的对象

# 复合类型的声明

变量的定义包括:一个 基本数据类型(base type)、一组 声明符(declarator)

在同一条定义语句中,基本数据类型只能有一个,但声明符的形式却可以有好几种,即,一条定义语句可能定义出不同类型的变量,例如:

int i = 1024, *p = &i, &r = i;  // i 是一个 int 型对象
                                // p 是一个指向 int 型对象的指针
                                // r 是一个 int 对象的引用

上例中, int 是基本数据类型, *& 是类型修饰符

类型修饰符与变量名共同组成声明符

# 定义多个变量

在定义语句中,类型修饰符( *& )仅作用于紧随其后的单个变量,而不是作用于本次定义的全部变量

涉及指针或引用的声明,一般有两种写法:

  1. 把类型修饰符和变量标识符写在一起,这种形式强调了变量所具有的复合类型

    int *p1, *p2;   //p1和p2都是指向int型对象的指针
    
  2. 把修饰符和类型名写在一起,并且每条语句只定义一个变量,这种形式强调本次声明定义了一种复合类型

    int* p1;    //p1是指向int型对象的指针
    int* p2;    //p2是指向int型对象的指针
    

推荐采用第一种写法,即,* (或是 & )与变量名连在一起

# 指向指针的指针

指针是内存中的对象,有自己的地址,因此,允许把指针的地址再存放到另一个指针当中

通过 * 的个数可以区分指针的级别

  • ** 表示指向指针的指针
  • *** 表示指向指针的指针的指针
  • 以此类推

例如:

int ival = 1024;
int *pi = &ival;    // pi 指向一个 int 型的数
int **ppi = &pi;    // ppi 指向一个 int 型的指针

下图描述了它们之间的关系:

解引用 int 型指针会得到一个 int 型的数,类似地,解引用指向指针的指针会得到一个指针

对于指针的指针而言,如果想要访问最原始的那个对象,需要对指针的指针做两次解引用,例如:

cout << ival;   // direct value
cout << *pi;    // indirect value
cout << **ppi;  // doubly indirect value

# 指向指针的引用

引用本身不是一个对象,因此不能定义指向引用的指针。但指针是对象,所以存在对指针的引用

int i = 42;
int *p;         // p 是一个指向 int 型对象的指针
int *&r = p;    // r 是一个对指针 p 的引用,即,r 是 p 的别名

r = &i;         // 给 r 赋值一个地址,相当于给 p 赋值一个地址,即,令 p 指向 i
*r = 0;         // 给 r 的解引用(也就是 p 指向的对象)赋值,即,给 i 赋值 0

想要理解变量 r 的类型,最简单的办法是从右向左阅读

  • 离变量名最近的符号对变量的类型有最直接的影响, int *&r = p; 中的符号 & 说明 r 是一个引用
  • 声明符的其余部分用以确定 r 引用的类型,此例中的符号 * 说明 r 引用的是一个指针
  • 最后,基本数据类型 int 指出,指针指向一个 int 型对象,即, r 引用的是一个 int 指针

对于一条比较复杂的指针或引用的声明语句,从右向左阅读有助于弄清楚它的真实含义

# const 限定符

# const

关键字 const 可以限定变量的类型,使得变量成为常量

const int bufSize = 512;
bufSize = 1024; // error: attempt to write to const object

const 对象一旦创建后,其值就不能再改变,因此, const 对象必须初始化

const 对象的初始值可以是任意复杂的表达式

const int i = get_size();  // ok: initialized at run time
const int j = 42;          // ok: initialized at compile time
const int k;               // error: k is uninitialized const

# 初始化 和 const

只能对 const 对象执行不改变其内容的操作

例如, const int 和普通的 int 一样,都能参与算术运算,也都能转换成一个布尔值,等等

再例如,可以利用 const 对象去初始化另一个对象

int i = 42;
const int ci = i;    // ok: the value in i is copied into ci
int j = ci;          // ok: the value in ci is copied into j

默认情况下, const 对象仅在文件内有效,当多个文件中出现了同名的 const 变量时,相当于在不同文件中分别定义了独立的变量

如果想要只在一个文件中定义 const ,而在其他多个文件中声明并使用它,解决方案为:对于 const 变量,不管是声明还是定义,都添加 extern 关键字

// file_1.cc defines and initializes a const that is accessible to other files
extern const int bufSize = fcn(); // definition
// file_1.h
extern const int bufSize; // declaration, same bufSize as defined in file_1.cc

如上述程序所示, file_1.cc 定义并初始化了 bufSize 。因为这条语句包含了初始值,所以它是一次定义,然而,因为 bufSize 是一个常量,必须用 extern 加以限定使其被其他文件使用

file_1.h 头文件中的声明也由 extern 做了限定,其作用是指明 bufSize 并非本文件所独有,它的定义将在别处出现

任何包含了显式初始化的声明,就成为了定义

如果想在多个文件之间共享 const 对象,必须在变量的定义之前添加 extern 关键字

# const 的引用

对常量的引用(reference to const): 把引用绑定到 const 对象上

对常量的引用不能被用于修改它所绑定的对象,即,不能够通过该引用修改对象的值

但实际上,可以通过其他途径修改该对象的值

const int ci = 1024;
const int &r1 = ci; //正确:引用及其对应的对象都是常量
                    // 不存在通过引用 r1 修改 ci 的风险,故而语法正确

r1 = 42;
int &r2 = ci;       // 错误:试图让一个非常量引用指向一个常量对象
                    // 存在通过 r2 修改 ci 的风险,故而语法错误

术语:常量引用是对 const 的引用

  • C++ 程序员们经常把词组 “对 const 的引用” 简称为 “常量引用”
  • 严格来说,并不存在常量引用,因为引用不是一个对象

复合类型 中指出,引用的类型必须与其所引用对象的类型一致,但是有两个例外

  • 第一种例外情况:在初始化常量引用时,允许用任意表达式作为初始值,只要该表达式的结果能转换成引用的类型即可。尤其,允许将一个常量引用绑定到非常量的对象、字面值,甚至是一个一般表达式

    int i = 42;
    const int &r1 = i;      // 允许将 const int & 绑定到一个普通 int 对象上。仅仅是说,不能通过 r1 修改 i 的值
    const int &r2 = 42;     // 正确:r2 是一个对 const 的引用。不存在通过 r2 修改 42 的风险
    const int &r3 = r1 * 2; // 正确:r3 是一个对 const 的引用。不存在通过 r3 修改 r1 * 2 的风险
    int &r4 = r1 * 2;       // 错误:r4 是一个普通的非常量引用。存在通过 r4 修改 r1 * 2 的风险
    

可以这样理解这一例外情形:将一个 const 引用与一个非常量对象绑定,仅仅是限制了 “不能通过该 const 引用修改对象的值” ,因此,不存在通过 const 引用修改对象值的风险,语法正确

对 const 的引用可能引用一个并非 const 的对象

必须认识到,常量引用仅限定了引用可参与的操作,并未限定引用所指对象本身是否为常量

常量引用的对象也可能是个非常量,所以允许通过其他途径改变它的值

int i = 42;
int &r1 = i;        // 引用r1绑定对象i
const int &r2 = i;  // 常量引用 r2 绑定对象 i ,不允许通过 r2 修改 i 的值
r1 = 0;             // r1 并非常量引用,可以通过 r1 修改 i 的值
r2 = 0;             // 错误:r2 是常量引用,不允许通过 r2 修改 i 的值
i = 36;             // i 不是常量,可以直接为 i 赋值

上例中, r2i 的常量引用,但 r1 不是 i 的常量引用,因此,可以通过 r1 修改 i 的值,并且, i 本身不是常量,可以直接为 i 赋新值

# 指针 与 const

# 指向常量的指针

与引用一样,也可以令指针指向常量或非常量

指向常量的指针(pointer to const):不能用于改变其所指对象的值

如何定义一个指向常量的指针:

  • * 放在变量名之前
  • 基本数据类型放在 * 之前
  • const 放在基本数据类型之前

例如:

const double *cptr = &pi;

要想存放常量对象的地址,只能使用指向常量的指针

即,限定了 “不能通过该指针修改常量对象的值”

const double pi = 3.14;     // pi 是一个常量
double *ptr = &pi;          // 错误:ptr 是一个普通指针。存在通过解引用 *ptr 修改 pi 的值的风险,故语法错误
const double *cptr = &pi;   // 正确: cptr 是一个指向常量的指针。不存在通过 *cptr 修改 pi 的值的风险,故语法正确
*cptr = 42;                 // 错误:cptr 是指向常量的指针,不能给 *cptr 赋值

复合类型 中指出,指针的类型必须与其所指对象的类型一致,但是有两个例外:

  • 第一种例外情况:允许让一个指向常量的指针指向一个非常量的对象

    double dval = 3.14;     // dval是一个双精度浮点数,它的值可以改变
    cptr = &dval;           // 正确,但是不能通过 cptr 修改 dval 的值
    

可以这样理解这一例外情形:将一个常量指针指向一个非常量的对象,仅仅是限制了 “不能通过该指针修改对象的值” ,因此,不存在通过常量指针修改对象值的风险,语法正确

和常量引用一样,指向常量的指针也没有规定其所指的对象必须是一个常量。指向常量的指针仅仅要求不能通过该指针改变对象的值,而没有规定那个对象的值不能通过其他途径改变

试试这样想吧:所谓指向常量的指针或引用,不过是指针或引用 “自以为是” 罢了,它们觉得自己指向了常量,所以自觉地不去改变所指对象的值

# const 指针(常量指针)

指针是对象,因此,允许把指针本身定为常量,即,常量指针(const 指针)

常量指针(const pointer)必须初始化,并且,一旦初始化完成,它的值(即,存放在指针中的那个地址)就不能再改变

换而言之,一旦常量指针初始化完成,就不能再令其指向新的对象

如何定义一个常量指针:

  • const 放在变量名之前
  • * 放在 const 之前
  • 基本数据类型放在 * 之前

例如:

int *const curErr = &errNum;

其中,把 * 放在 const 关键字之前,用以说明指针是一个常量,这样的书写形式隐含着一层意味,即,不变的是指针本身的值,而非指向的那个值

int errNum = 0;
int *const curErr = &errNum;    // curErr 将一直指向 errNum
const double pi = 3.14;
const double *const pip = &pi;  // pi 是一个指向常量的常量指针

要想弄清楚这些声明的含义,最行之有效的办法是从右向左阅读

int *const curErr = &errNum; 这一语句而言

  • curErr 最近的符号是 const ,意味着 curErr 本身是一个常量对象,对象的类型由声明符的其余部分确定
  • 声明符中的下一个符号是 * ,意思是 curErr 是一个常量指针
  • 最后,基本数据类型 int 说明该常量指针指向的是一个 int 对象

类似地,我们也能推断出, pip 是一个常量指针,它指向的对象是一个 double 型常量

常量指针,只是说指针本身是一个常量,并不是说不能通过指针修改其所指对象的值,能否这样做完全依赖于所指对象的类型

在上述示例中

  • pip 是一个指向常量的常量指针,因此,不论是 pip 所指的对象值、还是 pip 自己存储的那个地址,都不能改变
  • curErr 指向的是一个非常量的整数,因此,可以用 curErr 去修改 errNum 的值

# 顶层 const 与 底层 const

指针本身是不是常量,以及指针所指的对象是不是一个常量,是两个相互独立的问题

顶层 const(top-levelconst):指针本身是一个常量

底层 const(low-level const):指针所指的对象是一个常量

更一般的说法:

  • 顶层 const 可以表示任意的对象是常量,并且,对任何数据类型的对象都适用,如算术类型、类、指针等
  • 底层 const 则与指针和引用等复合类型的基本类型部分有关

比较特殊的是,指针类型既可以是顶层 const ,也可以是底层 const ,即,指向常量的常量指针

int i = 0;
int *const pi = &i;         // 顶层 const
const int ci = 42;          // 顶层 const
const int *p2 = &ci;        // 底层 const

const int *const p3 = p2;   // 底层 const,顶层 const
const int &r = ci;          // 底层 const

当执行对象的拷贝操作时,顶层 const 不受影响(执行拷贝操作并不会改变顶层 const 对象的值,因此,拷入和拷出的对象是否是常量都没什么影响)

i = ci;     // 正确:拷贝对象 ci 的值,顶层 const 对此操作无影响
p2 = p3;    // 正确:p2 和 p3 指向的对象类型相同,p3 顶层 const 的部分不影响

当执行对象的拷贝操作时,拷入和拷出的对象必须具有相同的底层 const 资格,或者,两个对象的数据类型必须能够转换(一般来说,非常量可以转换成常量,反之则不行)

int *p = p3;        // 错误:p3 包含底层 const 的定义,而 p 没有。存在通过 p 修改 p3 所指对象的值的风险,故语法错误
p2 = p3;            // 正确:p2 和 p3 都是底层 const 。不存在通过 p2 修改 p3 所指对象的值的风险,故语法正确
p2 = &i;            // 正确:int * 能转换成 const int * 。p2 是一个指向常量的指针
int &r = ci;        // 错误:普通的 int & 不能绑定 int 常量。存在通过引用 r 修改常量 ci 值的风险,故语法错误
const int &r2 = i;  // 正确:const int & 可以跟一个普通 int 对象绑定。r2 是一个指向常量的指针

# constexpr 和 常量表达式

常量表达式(const expression)是指值不会改变,并且在编译阶段就能得到计算结果的表达式

  • 字面值属于常量表达式
  • 用常量表达式初始化的 const 对象也是常量表达式(编译器将在编译过程中把用常量表达式初始化的 const 对象替换成对应的值)

一个对象(或表达式)是不是常量表达式由它的数据类型和初始值共同决定,例如:

const int max_files = 20;           // 是常量表达式
const int limit = max_files + 1;    // 是常量表达式
int staff_size = 27;                // 不是常量表达式(值会改变)

const int sz = get_size();          // 不是常量表达式(无法在编译阶段得到计算结果)

# constexpr 变量

C++ 11 标准规定,允许将变量声明为 constexpr 类型,以便由编译器来验证变量的值是否是一个常量表达式

因此,声明为 constexpr 的量一定是一个常量,并且,必须用常量表达式来初始化

constexpr int mf = 20;          // 20 是常量表达式
constexpr int limit = mf + 1;   // mf + 1 是常量表达式
constexpr int sz = size();      // 只有当 size 是一个 constexpr 函数时才正确

虽然不能使用普通函数作为 constexpr 变量的初始值,但是,C++ 11 标准允许定义一种特殊的 constexpr 函数。其中,函数应该足够简单,以使得编译时就可以计算其结果。于是,就能用 constexpr 函数去初始化 constexpr 变量了(详见 constexpr 函数

Generally, it is a good idea to use constexpr for variables that you intend to use as constant expressions.

# 字面值类型

常量表达式的值需要在编译时就得到计算,因此,对声明 constexpr 时用到的类型必须有所限制。由于这些类型一般比较简单,值也显而易见、容易得到,就把它们称为字面值类型(literal type)

算术类型、引用和指针都属于字面值类型

自定义类 Sales_item 、 IO 库、 string 类型不属于字面值类型,不能被定义成 constexpr

尽管指针和引用都能定义成 constexpr ,但它们的初始值却受到严格限制

  • 一个 constexpr 指针的初始值必须是 nullptr 或者 0 ,或者是存储于某个固定地址中的对象
  • 一般来说,定义在函数体内的变量并非存放在固定地址中,因此, constexpr 指针不能指向这样的变量
  • 对于定义在所有函数体之外的对象,其地址固定不变,故而能用来初始化 constexpr 指针
  • 此外,允许函数定义一类有效范围超出函数本身的变量,这类变量和定义在函数体之外的变量一样,也有固定地址

# 指针 和 constexpr

如果在 constexpr 声明中定义了一个指针,限定符 constexpr 仅对指针有效,与指针所指的对象无关

相当于一个常量指针,表示指针本身是常量,无法指向别的对象

const int *p = nullptr;         // 指向常量的指针
int const *q1 = nullptr;        // 常量指针
constexpr int *q2 = nullptr;    // 常量指针

关键在于, constexpr 把它所定义的对象置为了顶层 const

与其他常量指针类似, constexpr 指针既可以指向常量也可以指向一个非常量

# 处理类型

# 类型别名

类型别名(type alias):某种类型的同义词

有两种方法可用于定义类型别名:

  1. 使用关键字 typedef

    typedef double wages;   // wages是double的同义词
    typedef wages base, *p; // base是double的同义词, p是double*的同义词
    

    其中,关键字 typedef 作为声明语句中的基本数据类型的一部分出现

    含有 typedef 的声明语句,其定义的不再是变量,而是类型别名

    这里的声明符可以包含类型修饰符,因此,可以由基本数据类型构造出复合类型

  2. 使用别名声明(aliasdeclaration):

    using SI = Sales_item;  // SI是Sales_item的同义词
    

    用关键字 using 作为别名声明的开始,其后紧跟别名和等号,其作用是把等号左侧的名字规定为等号右侧类型的别名

类型别名和类型的名字等价,只要是能使用类型名字的地方,就能使用类型别名

wages hourly, weekly;    // 等效于double hourly, weekly;
SI item;                 // 等效于Sales_item item

# 指针、常量和类型别名

如果某个类型别名指代的是复合类型或常量,对于使用类型别名来定义变量的声明语句而言,不能简单地用原复合类型来代替类型别名

例如,以下声明语句用到了类型 pstring ,它实际上是类型 char * 的别名

typedef char *pstring;
const pstring cstr = 0; // cstr是一个指向char对象的常量指针,相当于char *const cstr = 0
const pstring *ps;      // ps是一个指针,它的对象是一个指向char的常量指针
                        // 因为const pstring是一个指向char的常量指针类型

上述两条声明语句的基本数据类型都是 const pstring ,其中

  • const 是对 pstring 类型的修饰,用以说明 pstring 类型的变量 cstr 是常量
  • pstring 是类型 char * 的别名,即,指向 char 的指针

因此, const pstring 是指向 char 的常量指针,而非指向常量字符的指针(若直接用 char * 替换掉 pstring ,会把 cstr 误解成一个指向常量的指针)

const pstring cstr = 0;   // cstr是指向char的常量指针
const char *cstr = 0;     // 对const pstring cstr的错误理解,将cstr错当成一个指向char常量的指针

# auto 类型说明符

C++11 新标准引入了 auto 类型说明符

auto 类型说明符:让编译器通过初始值来推算变量的类型

auto 定义的变量必须有初始值

//根据val1和val2相加的结果来推断item的类型
auto item = val1 + val2;

使用 auto 时,可以在一条语句中声明多个变量。但是需要注意,使用 auto 的声明语句只能有一个基本数据类型,即,该语句中所有变量初始值的基本数据类型必须一样

auto i = 0, *p = &i;      // ok: i是一个int型变量,p是一个指向int型对象的指针
auto sz = 0, pi = 3.14;   // error: sz和pi的类型不一致,根据sz会推导为int型,但pi会推断为float/double型

# 复合类型、常量 和 auto

编译器推断出来的 auto 类型有时候和初始值的类型并不完全一样,编译器会适当地改变结果类型使其更符合初始化规则

  1. 当引用被用作初始值时,真正参与初始化的其实是引用对象的值,编译器以引用对象的类型作为 auto 的类型

    int i = 0, &r = i;
    auto a = r;  // a是int型(r是int型对象i的引用)
    
  2. auto 一般会忽略掉顶层 const ,而底层 const 则会保留下来

    const int ci = i, &cr = ci;
    auto b = ci;  // b是一个int型(顶层const被忽略)
    auto c = cr;  // c是一个int型
    auto d = &i;  // d是一个指向int的指针
    auto e = &ci; // e是一个指向int常量的指针(底层const被保留)
    
  3. 如果希望推断出的 auto 类型是一个顶层 const ,需要在 auto 前添加 const 限定符以明确指出

    const auto f = ci; // ci的类型是int,f是一个int常量(顶层const)
    
  4. 可以将引用的类型设为 auto ,此时,引用的初始化规则仍然适用

    auto &g = ci;       // g is a const int&. 因为ci是int常量,不允许存在通过g修改ci的风险,故而g是常量引用
    auto &h = 42;       // error: 不能为非常量引用绑定字面值。字面值常量42的类型为int,故auto推断为int型,此时存在通过h修改42的风险,故而语法错误
    const auto &j = 42; // ok: 可以为常量引用绑定字面值
    

    设置一个类型为 auto 的引用时,初始值中的顶层 const 属性仍然保留。即,不能通过引用修改所指对象的值

  5. 在一条语句中定义多个变量时,切记,符号 &* 只从属于某个声明符,而非基本数据类型的一部分。因此,初始值必须是同一种类型

    auto k = ci, &l = i;    // k是int型对象,l是int型对象i的引用
    auto &m = ci, *p = &ci; // m是一个int型常量引用,p是指向int常量的指针
    auto &n = i, *p2 = &ci; //error: 根据i推断的类型是int; 根据&ci推断的类型是const int。(存在通过p2修改ci的风险)
    

# decltype 类型指示符

C++11 新标准引入了类型说明符 decltype ,它的作用是选择并返回操作数的数据类型

编译器分析表达式并得到它的类型,却不实际计算表达式的值

decltype(f()) sum = x;

编译器并不实际调用函数 f ,而是使用 当调用发生时 f 的返回值类型 作为 sum 的类型

如果 decltype 使用的表达式是一个变量,则 decltype 返回该变量的类型(包括顶层 const 和引用在内)

const int ci = 0, &cj = ci;
decltype(ci) x = 0; // x的类型是const int
decltype(cj) y = x; // y的类型是const int &
decltype(cj) z;     // error: z是一个引用,必须初始化

需要指出的是,引用从来都作为其所指对象的同义词出现,只有用在 decltype 处是一个例外:在 decltype 中,引用和引用所指对象是不同的数据类型,如上例, ciconst int 型,而 cjconst int&

# decltype 和 引用

如果 decltype 使用的表达式不是一个变量,则 decltype 返回表达式结果对应的类型

//decltype的结果可以是引用类型
int i = 42, *p = &i, &r = i;
decltype(r + 0) b;  // ok: 加法的结果是int型,故而b是一个(未初始化)的int对象
decltype(*p) c;     // error: c是int&型,必须初始化

因为 r 是一个引用,因此 decltype(r) 的结果是引用类型。如果想让结果类型是 r 所指对象的类型,可以把 r 作为表达式的一部分(例如, r + 0

如果表达式执行的是解引用操作, decltype 将得到引用类型。解引用指针可以得到指针所指的对象,而且还能给这个对象赋值。因此, decltype(*p) 的结果类型就是 int& ,而非 int

如果 decltype 使用的是一个不加括号的变量,则得到的结果就是该变量的类型;如果给变量加上了一层或多层括号,编译器会把它当成是一个表达式,这样的 decltype 会得到引用类型

// decltype of a parenthesized variable is always a reference
decltype((i)) d;    // error: d 是 int &,必须初始化
decltype(i) e;      // ok: e 是一个(未被初始化的)int

换而言之

  • 对于 decltype((variable)) (注意是双层括号),结果类型永远是引用类型

  • 对于 decltype(variable) ,只有当 variable 本身就是一个引用时,结果类型才是引用类型

# auto 和 decltype

autodecltype 的区别:

  1. auto 类型说明符用编译器计算变量的初始值来推断其类型,而 decltype 虽然也让编译器分析表达式并得到它的类型,但是不实际计算表达式的值

  2. 编译器推断出来的 auto 类型有时候和初始值的类型并不完全一样,编译器会适当地改变结果类型使其更符合初始化规则。例如, auto 一般会忽略掉顶层 const ,而把底层 const 保留下来。与之相反, decltype 会保留变量的顶层 const

  3. auto 不同, decltype 的结果类型与表达式形式密切相关:如果 decltype 使用的是一个不加括号的变量,则得到的结果就是该变量的类型;如果给变量加上了一层或多层括号,则编译器将推断得到引用类型

# 自定义数据结构

数据结构是 把一组相关的数据元素组织起来然后使用它们 的策略和方法

C++ 语言允许用户以类的形式自定义数据类型,而库类型 stringistreamostream 等也都是以类的形式定义的

# 定义 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;

一般来说,最好不要把对象的定义和类的定义放在一起

也可以使用 C++ 语言提供的另外一个 关键字 class 来定义数据结构

类的定义可以与 main 函数放在同一个文件内。但是,我们通常在头文件中定义类,并且类所在头文件的名字应与类的名字一样

例如,库类型 string 在名为 string 的头文件中定义。又如,我们应该把 Sales_data 类定义在名为 Sales_data.h 的头文件中

# 类数据成员

类体定义类的成员,我们在上述例子中定义的类只有数据成员(datamember)

类的数据成员定义了类的对象的具体内容,每个对象有自己的一份数据成员拷贝。修改一个对象的数据成员,不会影响其他 Sales_data 的对象

定义数据成员的方法:

  • 首先说明一个基本类型
  • 随后紧跟一个或多个声明符

C++ 11 标准规定,可以为数据成员提供一个类内初始值(in-class initializer)

  • 创建对象时,类内初始值将用于初始化数据成员
  • 没有初始值的成员将被默认初始化

对类内初始值的限制:要么放在花括号里,要么放在等号右边,而不能使用圆括号

# 使用 Sales_data 类

详见:自己动手实现对于 Sales_data 类的操作

关键在于,使用点操作符( . )读入对象的成员,如

Sales_data data1;                               // data1为Sales_data类型的对象
std::cin >> data1.bookNo >> data1.units_sold;   // 写入对象data1的bookNo成员和units_sold成员

# 编写自己的头文件

类通常被定义在头文件中,并且,类所在头文件的名字应与类的名字一样

头文件通常包含那些只能被定义一次的实体,如:类、 constconstexpr 变量等

头文件也经常用到其他头文件的功能。因此,有必要在书写头文件时做适当处理,使其遇到多次包含的情况也能安全和正常地工作

头文件一旦改变,相关的源文件必须重新编译以获取更新过的声明

# 预处理器概述

确保头文件多次包含时仍能安全工作的常用技术是 预处理器(preprocessor)

预处理器是在编译之前执行的一段程序,可以部分地改变我们所写的程序。之前已经用到了一项预处理功能 #include ,当预处理器看到 #include 标记时就会用指定的头文件的内容代替 #include

C++ 程序还会用到的一项预处理功能是 头文件保护符(headerguard),头文件保护符依赖于预处理变量

  • 预处理变量有两种状态:已定义和未定义
  • #define 指令把一个名字设定为预处理变量
  • #ifdef#ifdef 两个指令分别检查某个指定的预处理变量是否已经定义
    • #ifdef 指令:当且仅当变量已定义时为真
    • #ifndef 指令:当且仅当变量未定义时为真
  • 一旦检查结果为真,则执行后续操作直至遇到 #endif 指令为止

使用这些功能就能有效地防止重复包含的发生

例如:

#ifndef SALES_DATA_H    // SALES_DATA_H即为预处理变量,ifndef是if not defined的缩写。若未定义SALES_DATA_H,则执行后续操作,直到遇到#endif指令
#define SALES_DATA_H
#include <string>
struct Sales_data {
    std::string bookNo;
    unsigned units_sold = 0;
    double revenue = 0.0;
};
#endif

第一次包含 Sales_data.h 时, #ifndef 的检查结果为真,预处理器将顺序执行后面的操作直至遇到 #endif 为止。此时,预处理变量 SALES_DATA_H 的值将变为已定义,而且 Sales_data.h 也会被拷贝到我们的程序中来。后面如果再一次包含 Sales_data.h ,则 #ifndef 的检查结果将为假,编译器将忽略 #ifndef#endif 之间的部分

预处理变量无视 C++ 语言中关于作用域的规则

整个程序中的预处理变量包括头文件保护符必须唯一,通常的做法是基于头文件中类的名字来构建保护符的名字,以确保其唯一性。为了避免与程序中的其他实体发生名字冲突,一般把预处理变量的名字全部大写

注意:头文件即使(目前还)没有被包含在任何其他头文件中,也应该设置保护符。头文件保护符很简单,程序员只要习惯性地加上就可以了,没必要太在乎你的程序到底需不需要

# 术语表

地址(address) :是一个数字,根据它可以找到内存中的一个字节。

别名声明(alias declaration) :为另外一种类型定义一个同义词:使用 “名字 = 类型” 的格式将名字作为该类型的同义词。

算术类型(arithmetic type) :布尔值、字符、整数、浮点数等内置类型。

数组(array) :是一种数据结构,存放着一组未命名的对象,可以通过索引来访问这些对象。

auto :是一个类型说明符,通过变量的初始值来推断变量的类型。

基本类型(base type) :是类型说明符,可用 const 修饰,在声明语句中位于声明符之前。基本类型提供了最常见的数据类型,以此为基础构建声明符。

绑定(bind) :令某个名字与给定的实体关联在一起,使用该名字也就是使用该实体。例如,引用就是将某个名字与某个对象绑定在一起。

字节(byte) :内存中可寻址的最小单元,大多数机器的字节占 8 位。

类成员(class member) :类的组成部分。

复合类型(compound type) :是一种类型,它的定义以其他类型为基础。

const :是一种类型修饰符,用于说明永不改变的对象。 const 对象一旦定义就无法再赋新值,所以必须初始化。

常量指针(const pointer) :是一种指针,它的值永不改变。

常量引用(const reference) :是一种习惯叫法,含义是指向常量的引用。

常量表达式(const expression) :能在编译时计算并获取结果的表达式。

constexpr :是一种函数,用于代表一条常量表达式。

转换(conversion) :一种类型的值转变成另外一种类型值的过程。C++ 语言支持内置类型之间的转换。

数据成员(data member) :组成对象的数据元素,类的每个对象都有类的数据成员的一份拷贝。数据成员可以在类内部声明的同时初始化。

声明(declaration) :声称存在一个变量、函数或是别处定义的类型。名字必须在定义或声明之后才能使用。

声明符(declarator) :是声明的一部分,包括被定义的名字和类型修饰符,其中类型修饰符可以有也可以没有。

decltype :是一个类型说明符,从变量或表达式推断得到类型。

默认初始化(default initialization) :当对象未被显式地赋予初始值时执行的初始化行为。由类本身负责执行的类对象的初始化行为。全局作用域的内置类型对象初始化为 0;局部作用域的对象未被初始化即拥有未定义的值。

定义(definition) :为某一特定类型的变量申请存储空间,可以选择初始化该变量。名字必须在定义或声明之后才能使用。

转义序列(escape sequence) :字符特别是那些不可打印字符的替代形式。转义以反斜线开头,后面紧跟一个字符,或者不多于 3 个八进制数字,或者字母 x 加上 1 个十六进制数。

全局作用域(global scope) :位于其他所有作用域之外的作用域。

头文件保护符(header guard) :使用预处理变量以防止头文件被某个文件重复包含。

标识符(identifier) :组成名字的字符序列,标识符对大小写敏感。

类内初始值(in-class initializer) :在声明类的数据成员时同时提供的初始值,必须置于等号右侧或花括号内。

在作用域内(in scope) :名字在当前作用域内可见。

被初始化(initialized) :变量在定义的同时被赋予初始值,变量一般都应该被初始化。

内层作用域(inner scope) :嵌套在其他作用域之内的作用域。

整型(integral type) :参见算术类型。

列表初始化(list initialization) :利用花括号把一个或多个初始值放在一起的初始化形式。

字面值(literal) :是一个不能改变的值,如数字、字符、字符串等。单引号内的是字符字面值,双引号内的是字符串字面值。

局部作用域(local scope) :是块作用域的习惯叫法。

底层const(low-level const) :一个不属于顶层的 const ,类型如果由底层常量定义,则不能被忽略。

成员(member) :类的组成部分。

不可打印字符(nonprintable character) :不具有可见形式的字符,如控制符、退格、换行符等。

空指针(null pointer) :值为 0 的指针,空指针合法但是不指向任何对象。

nullptr :是表示空指针的字面值常量。

对象(object) :是内存的一块区域,具有某种类型,变量是命名了的对象。

外层作用域(outer scope) :嵌套着别的作用域的作用域。

指针(pointer) :是一个对象,存放着某个对象的地址,或者某个对象存储区域之后的下一地址,或者 0。

指向常量的指针(pointer to const) :是一个指针,存放着某个常量对象的地址。指向常量的指针不能用来改变它所指对象的值。

预处理器(preprocessor) :在 C++ 编译过程中执行的一段程序。

预处理变量(preprocessor variable) :由预处理器管理的变量。在程序编译之前,预处理器负责将程序中的预处理变量替换成它的真实值。

引用(reference) :是某个对象的别名。

对常量的引用(reference to const) :是一个引用,不能用来改变它所绑定对象的值。对常量的引用可以绑定常量对象,或者非常量对象,或者表达式的结果。

作用域(scope) :是程序的一部分,在其中某些名字有意义。

C++ 的作用域:全局、类、块

  • 全局(global) —— 名字定义在所有其他作用域之外。

  • 类(class) —— 名字定义在类内部。命名空间(namespace)—— 名字定义在命名空间内部。

  • 块(block) —— 名字定义在块内部。名字从声明位置开始直至声明语句所在的作用域末端为止都是可用的。

分离式编译(separate compilation) :把程序分割为多个单独文件的能力。

带符号类型(signed) :保存正数、负数或 0 的整型。

字符串(string) :是一种库类型,表示可变长字符序列。

struct :是一个关键字,用于定义类。

临时值(temporary) :编译器在计算表达式结果时创建的无名对象。为某表达式创建了一个临时值,则此临时值将一直存在直到包含有该表达式的最大的表达式计算完成为止。

顶层const(top-level const) :是一个 const ,规定某对象的值不能改变。

类型别名(type alias) :是一个名字,是另外一个类型的同义词,通过关键字 typedef 或别名声明语句来定义。

类型检查(type checking) :是一个过程,编译器检查程序使用某给定类型对象的方式与该类型的定义是否一致。

类型说明符(type specifier) :类型的名字。

typedef :为某类型定义一个别名。当关键字 typedef 作为声明的基本类型出现时,声明中定义的名字就是类型名。

未定义(undefined) :即 C++ 语言没有明确规定的情况。不论是否有意为之,未定义行为都可能引发难以追踪的运行时错误、安全问题和可移植性问题。

未初始化(uninitialized) :变量已定义但未被赋予初始值。一般来说,试图访问未初始化变量的值将引发未定义行为。

无符号类型(unsigned) :保存大于等于 0 的整型。

变量(variable) :命名的对象或引用。C++ 语言要求变量要先声明后使用。

void* :可以指向任意非常量的指针类型,不能执行解引用操作。

void类型 :是一种有特殊用处的类型,既无操作也无值。不能定义一个 void 类型的变量。

字(word) :在指定机器上进行整数运算的自然单位。一般来说,字的空间足够存放地址。32 位机器上的字通常占据 4 个字节。

&运算符(&operator) :取地址运算符。

*运算符(* operator) :解引用运算符。解引用一个指针将返回该指针所指的对象,为解引用的结果赋值也就是为指针所指的对象赋值。

#define :是一条预处理指令,用于定义一个预处理变量。

#endif :是一条预处理指令,用于结束一个 #ifdef#ifndef 区域。

#ifdef :是一条预处理指令,用于判断给定的变量是否已经定义。

#ifndef :是一条预处理指令,用于判断给定的变量是否尚未定义。

参考:C++ Primer 中文版(第 5 版)

阅读次数