今天是第二章的内容,祝你好运,也祝我。

# 1. 编程语言的特征

常用的编程语言通常具备一组公共的语法特征,仅在特征的细节上有所区别。
大多数编程语言通过两种方式进一步补充其基本特征:

  • 赋予程序员自定义数据类型的权利,从而实现对语言的扩展
  • 将一些有用的功能封装成库函数提供给程序员

与大多数编程语言一样,C++ 的对象类型决定了能对该对象进行的操作,一条表达式是否合法依赖于其中参与运算的对象的类型。C++ 是一种静态数据类型语言,它的类型检查发生在编译时。因此编译器必须知道程序中每一个变量对应的数据类型。

# 2. 基本内置类型

C++ 定义了一套包括算数类型(arithmetic type)和空类型(void)在内的基本数据类型。

  • 算数类型
    • 整型(intergral type,包括字符和布尔类型在内)
    • 浮点型
C++:算数类型
类型 含义 最小尺寸
bool 布尔类型 未定义
char 字符 8 位
wchar_t 宽字符 16 位
char16_t Unicode 字符 16 位
char32_t Unicode 字符 32 位
short 短整型 16 位
int 整型 16 位
long 长整型 32 位
long long 长整型 64 位
float 单精度浮点数 6 位有效数字
double 双精度浮点数 10 位有效数字
long double 扩展精度浮点数 10 位有效数字

布尔类型(bool)的取值是真(true)或者假(false)。
Unicode 是用于表示所有自然语言中字符的标准。
数据类型 long long 是在 C++11 中新定义的。

# 3. 内置类型的机器实现

计算机以比特序列存储数据,每个比特非 0 即 1。
大多数计算机以 2 的整数次幂个比特作为块来处理内存,可寻址的最小内存块称为 "字节(byte)",存储的基本单元称为 "字(word)",它通常由几个字节组成。在 C++ 语言中,一个字节要至少能容纳机器基本字符集中的字符。大多数机器的字节由 8 比特构成,字则由 32 或 64 比特构成,也就是 4 或 8 字节。大多数计算机将内存中的每个字节与一个数字(被称为 "地址(address)")关联起来。

# 4. 符号

除去布尔类型以及扩展的字符型,其他整型可划分为

  • 带符号的(signed) 正数、负数、0
  • 无符号的(unsigned) 仅能表示大于等于 0 的值

上面表格中的 int、short、long 和 long long 都是带符号的,只要在它们前面加上 unsigned 就可以得到无符号类型,其中,unsigned int 可以缩写为 unsigned。

与其他整型不同,字符型被分为三种:

  • char 实际会根据编译器 1 表现为如下两种中的一种
  • signed char 8 比特理论上可表示 [-127,127],实际上可表示 [-128,127]
  • unsigned char 8 比特可表示 [0,255]

来尝试预测下面的代码运行结果

// test1
unsigned u = 10;
int i = -42;
std::cout << i + i << std::endl;
std::cout << u + i << std::endl;
// test2
unsigned u1 = 42, u2 = 10;
std::cout << u1 - u2 << std::endl;
std::cout << u2 - u1 << std::endl;
// test3
for (int i = 10; i >= 0; --i)
std::cout << i << std::endl;
// test4
for (unsigned u = 10; u >= 0; --u)
std::cout << u << std::endl;
// test5
unsigned u = 11;
while (u > 0) {
--u;
std::cout << u << std::endl;
}

如果你尝试运行了上面的代码,你应该会明白,不要混用带符号类型和无符号类型,你可以结合上面的示例代码和下面第六条的内容分析原因。

# 5. 类型选择

  • 明确知晓数值不可能为负时,选用无符号类型
  • 使用 int 执行整数运算,如果数值超过了 int 的表示范围,选用 long long
  • 算术表达式中尽量不要使用 char 或 bool,机器间差异会导致不可知的问题。
  • 执行浮点数运算选用 double,因为 float 精度常常不够并且计算代价并不比 double 更低,而 long double 的精度一般情况下没有必要并且运行消耗不容忽视。

# 6. 类型转换

从一种给定的类型转换(convert)为另一种相关类型,你可以尝试如下示例:

bool b = 42;
int i = b;
i = 3.14;
double pi = i;
unsigned char c = -1;
signed char c2 = 256;

下面给出转换规则:

  • 当我们把一个非布尔类型的算数值赋给布尔类型时,初始值为 0 则结果为 false,否则结果为 true。
  • 当我们把一个布尔值赋给非布尔类型时,初始值为 false 则结果为 0,初始值为 true 则结果为 1。
  • 当我们把一个浮点数赋给整数类型时,进行近似处理,结果值仅保留浮点数中小数点之前的部分。
  • 当我们把一个整数值赋给浮点类型时,小数部分记为 0,。如果该整数所占的空间超过了浮点类型的容量,精度可能有损失。
  • 当我们赋给无符号类型一个超出它表示范围的值时,结果是初始值对无符号类型表示数值总数取模后的余数。
  • 当我们赋给带符号类型一个超出它表示范围的值时,结果是未定义的(undefined)。此时程序可能继续工作、崩溃或是生成垃圾数据。

# 7. 避免无法预知和依赖于实现环境的行为

无法预知的行为源于编译器无须(有时无法)检测的错误。即使代码编译通过了,如果程序执行了一条未定义的表达式,仍有可能产生错误。但,某些情况 / 编译器下,含有无法预知行为的程序也能正确执行,但无法保证它在其他编译器,其他测试用例或再次运行仍能正常运行。所以,程序应避免依赖于实现环境的行为,即不可移植的(nonportable)程序,并且这类代码定位错误是很困难的。

# 8. 字面值常量(literal)

  • 整型和浮点型字面值
    • 整型字面值
      如果一个字面值连与之关联的最大的数据类型都放不下,将产生错误。类型 short 没有对应的字面值。
      • 十进制数
        默认是有符号类型。正如我们所常见的,但严格来说它不会是负数,确切地说是对字面值取负值。
      • 八进制数
        以 0 开头的整数
      • 十六进制数
        以 0x 开头的整数
    • 浮点型字面值
      默认是 double 类型。
      • 小数形式
      • E/e 的指数形式
  • 字符和字符串字面值
    • 字符字面值
      char 型字面值
    • 字符串字面值
      由常量字符构成的数组(array),并且编译器会在每个字符串的结尾处添加一个空字符(’\0’)用作字符串结束的标识符。因此,字符串字面值的实际长度要比它的内容多 1。
  • 转义序列
    主要为了两类程序员不能直接使用的字符服务
    • 不可打印的字符
    • C++ 语言中有特殊含义的字符
换行符 \n 横向制表符 \t 报警(响铃)符 \a
纵向制表符 \v 退格符 \b 双引号 \"
反斜线 \\ 问号 \? 单引号 \’
回车符 \r 进纸符 \f
  • 指定字面值的类型
指定字面值的类型
字符和字符串字面值
前缀 含义 类型
u Unicode 16 字符 char16_t
U Unicode 32 字符 char32_t
L 宽字符 wchar_t
u8 UTF-8(仅用于字符串字面常量) char
整型字面值 浮点型字面值
后缀 最小匹配类型 后缀 类型
u or U unsigned f or F float
l or L long l or L long double
ll or LL long long
  • 布尔字面值和指针字面值
    • 布尔字面值
      true/false
    • 指针字面值
      nullptr

# 9. 变量与对象

变量:提供一个具名的、可供程序操作的存储空间。
定义变量:首先是类型说明符(type specifier),随后紧跟着一个或多个变量名组成的列表。
对象(object):通常指一块能存储数据并具有某种类型的内存空间。对此不同人有不同看法和理解。
当对象在创建时获得了一个特定的值,我们称这个对象被初始化了。
人们常常会忽略初始化与赋值之间的差异,实则这个问题很重要,初始化不是赋值,初始化的含义是创建变量时赋予其一个初始值,而赋值的含义是把对象的当前值擦除并以一个新值替代。

# 10. 列表初始化(list initialization)

如下,都可以做到将 units_sold 这个 int 变量初始化为 0。

int units_sold = 0;
int units_sold = {0};
int units_sold{0};
int units_sold(0);

当用于内置类型的变量时,这种初始化有一个重要特点:如果我们使用列表初始化且初始值存在丢失信息的风险,则编译器将报错。

long double ld = 3.14159265358979;
int a{ld}, b = {ld};
int c(ld), d = ld;

你可以判断一下上面代码块运行的结果,是否会报错,仔细思考,可以结合本文第六条内容。
虽然这类问题看起来无关紧要,毕竟我们不会故意用 long double 的值去初始化 int 变量,但毕竟,它可能在不经意间发生,不是吗?

# 11. 默认初始化

如果定义变量时没有指定初值,则变量被默认初始化(default initialized),此时变量被赋予了 "默认值",默认值会由变量类型和定义变量的位置共同决定。
如果是内置类型的变量未被显式初始化,它的值由定义的位置决定。定义于任何函数体之外的变量被初始化为 0. 定义在函数体内部的内置类型变量将不被初始化(uninitialized)。一个未被初始化的内置类型变量的值是未定义的,如果试图拷贝或以其他方式访问此类值将引发错误。
每个类各自决定其初始化对象的方式。一些类要求每个对象都显式初始化,此时如果创造了一个该类的对象而未对其做明确的初始化操作,将引发错误。
未初始化的变量含有一个不确定的值,使用未初始化变量的值是一种错误的编程行为且很难调试。尽管大多数编译器能对一部分使用为初始化变量的行为提出警告,但严格而言,编译器并未被要求检查此类错误。
使用未初始化的变量将带来无法预计的后果,往往它会是对是错,添加无关代码导致我们误以为程序对了,但实际上代码仍是错误的。
建议初始化每一个内置类型的变量。虽然这不是必须的,但若是你无法确保不这么做时程序的安全,那这么做不失为一种最简单的方法。

# 12. 变量声明和定义的关系

为了允许把程序拆分成多个逻辑部分来编写,C++ 语言支持分离性编译(separate compilation)机制,该机制允许将程序分割为若干个文件,它们可以被独立编译。
为了支持分离性编译,C++ 语言将声明和定义区分开来。声明(declaration)使得名字为程序所知,一个文件如果想使用别处定义的名字则必须包含对那个名字的声明。而定义(definition)负责创建与名字关联的实体。
变量声明规定了变量的类型和名字,在这一点上定义与之相同。但是除此之外,定义还申请存储空间,也可能会为变量赋一个初始值。
如果想声明一个变量而非定义它,就在变量名前添加关键词 extern,并且不要显式的初始化变量。
任何包含了显示初始化的声明即成为定义。当我们给一个由 extern 关键词标记的变量赋一个初始值时,extern 的作用将被抵消,它也就不再是声明,而是定义。
在函数体内部,如果试图初始化一个由 extern 关键词标记的变量,将引发错误。
变量能且只能被定义一次,但可以被多次声明。
声明和定义区别很重要,如果要在多个文件中使用同一个变量,就必须将声明和定义分离。此时变量的定义必须且只能出现在一个文件中,而其他用到该变量的文件必须对其声明,却不能重复定义。

# 13. 静态类型

C++ 是一种静态类型(statically typed)语言,其含义是在编译阶段检查类型。其中,检查类型的过程称为类型检查(type checking)。
对象的类型决定了对象所能参与的运算,如果试图执行类型不支持的运算,编译器将报错并且不会生成可执行文件。
程序越复杂,静态类型检查越有助于发现问题。因此,我们在使用某个变量之前务必声明其类型。

# 14. 标识符

C++ 的标识符(identifier)由字母、数字和下划线组成,其中必须以字母或下划线开头。标识符的长度没有限制,但是对大小写字母敏感。
同时,C++ 也为语言本身以及标准库保留了一些名字,这些名字不能被用于标识符。
用户自定义的标识符中不能连续出现两个下划线(emmm,书里是画,很通俗易懂。。),也不能以下划线紧连大写字母开头,定义在函数体外的标识符不能以下划线开头。
下面是变量命名规范

  • 标识符要能体现实际含义
  • 变量名一般用小写字母
  • 用户自定义的类名一般以大写字母开头
  • 如果标识符由多个单词组成,则单词间应有明显区分
C++ 关键字
alignas continue friend register true
alignof decltype goto reinterpret_cast try
asm default if return typedef
auto delete inline short typeid
bool do int signed typename
break double long sizeof union
case dynamic_cast mutable static unsigned
catch else namespace static_assert using
char enum new static_cast virtual
char16_t explicit noexcept struct void
char32_t exprt nullptr switch volatile
class extern operator template wchar_t
const false private this while
constexpr float protected thread_local
const_cast for public throw
C++ 操作符 替代名
- - - - - -
and bitand compl not_eq or_eq xor_eq
and_eq bitor not or xor

# 15. 作用域(scope)

作用域是程序的一部分,在其中名字有其特定的含义。C++ 语言中大多数作用域都以花括号分隔。
同一个名字在不同的作用域中可以指向不同的实体。名字的有效区域始于名字的声明语句,以声明语句所在的作用域末端为结尾。
main 函数定义于所有花括号之外,它和其他大多数定义在函数体之外的名字一样拥有全局作用域(global scope)。全局作用域内的名字在整个程序的范围内都可使用。
而在函数块(或循环等等其他类似的)内部定义的名字,从声明它到这个函数结束我们都可以访问它,但是出了函数所在的块就无法访问了,因此说它有块作用域(block scope)。
建议当你第一次使用变量时再定义它,这样有助于更容易找到变量的定义,并且我们更容易给它赋予一个更合理的初值。
作用域能彼此包含,被包含(或者说被嵌套)的作用域称为内层作用域(inner scope),包含着别的作用域的作用域称为外层作用域(outer scope)。
如果函数有可能用到某全局变量,则不宜再定义一个同名的局部变量。

#include <iostream>
int reused = 42;
int main() {
int unique = 0;
std::cout << reused << " " << unique << std::endl;
int reused = 0;
std::cout << reused << " " << unique << std::endl;
std::cout << ::reused << " " << unique << std::endl;
return 0;
}

你可以通过上面的代码加深对作用域的理解。

# 16. 复合类型

复合类型(compound type)是指基于其他类型定义的类型。
下面将介绍引用与指针这两种。
这里更新下第 9 条中对于声明语句的定义:
一条声明语句由一个基本数据类型(base type)和紧随其后的一个声明符(declarator)列表组成。每个声明符命名了一个变量并指定该变量为与基本数据类型有关的某种类型。
目前于我们而言,声明符就是变量名,但之后会有更加复杂的声明符,它基于基本数据类型得到更复杂的类型并把它指定给变量。

# 17. 引用

C++11 中新增了一种引用:所谓的右值引用(rvalue reference),后续会有更详细的介绍。这种引用主要用于内置类。严格来说,当我们使用术语引用(reference)时,指的其实是左值引用(lvalue reference)。

引用(reference)为对象起了另外一个名字,引用类型引用(refers to)另外一种类型。通过将声明符写成 & d 的形式来定义引用类型,其中 d 是声明的变量名。
一般在初始化变量时,初始化会被拷贝到新建的对象中。然而定义引用时,程序把引用和它的初值绑定(bind)在一起,而不是将初始值拷贝给引用。一旦初始化完成,引用将和它的初始值对象一直绑定在一起。因为无法令引用重新绑定到另外一个对象,因此引用必须初始化。
引用即别名,引用并非对象,相反的,它只是为一个已经存在的对象所起的另外一个名字。
定义了一个引用之后,对其进行的所有操作都是在与之绑定的对象上进行的。
因为引用本身不是一个对象,所以不能定义引用的引用。
注意,引用只能绑定在对象上,并且引用的类型必须要和它绑定的对象严格匹配。

# 18. 指针

指针(pointer)是指向(point to)另外一种类型的复合类型。与引用类似,指针也实现了对其他对象的间接访问。
指针和引用不同点:

  • 指针本身就是一个对象,允许对指针赋值和拷贝,而且指针在其生命周期内可以先后指向几个不同的对象。
  • 指针无须在定义时赋初值。和其他内置类型一样,在块作用域内定义的指针如果没有被初始化,也将拥有一个不确定的值。

指针通常难以理解,即使是有经验的程序员也常常因为调试指针引发的错误而备受折磨。
定义指针类型的方法将声明符写成 * d 的形式,其中 d 是变量名。如果在一条语句中定义了几个指针变量,那么每个变量前面都必须有符号 * 。
指针存放某个对象的地址,想要获取该地址,需要使用取地址符(操作符 &)。
除了之后给定的两种特殊情况,其他所有指针的类型都要和它所指向的对象严格匹配。

指针的值(地址)应是如下四种状态之一:

  • 指向一个对象
  • 指向紧邻对象所占空间的下一个位置
  • 空指针,即没有指向任何对象
  • 无效指针,即上述之外其他值…

试图拷贝或以其他方式访问无效指针的值都将引发错误。编译器并不负责检查此类错误,这和使用未经初始化的变量是一样的。访问无效指针的后果无法预计,因此程序员必须清楚任意给定的指针是否有效。同样,访问未指向任何具体对象的指针也会引发不可知的后果。
如果指针指向了一个对象,则允许使用解引用符(操作符 * )来访问该对象。对指针解引用会得出所指的对象,如果给解引用的结果赋值,实际上也就是给指针所指的对象赋值。
解引用操作仅适用于那些确实指向了某个对象的有效指针。
请注意,某些符号有多重含义,没错,刚刚学习的 * 和 & 就是这样,我们完全可以将出现在不同场景下的它们看做不同的符号。
空指针(null pointer),不指向任何对象,给一个指针赋 nullptr/0/NULL 都可以使其成为空指针。注意,nullptr 是在 C++11 引入的。
nullptr 是一种特殊类型的字面值,它可以被转换成任意其他指针类型。
NULL 是预处理变量(preprocessor variable),这个变量在头文件 cstdlib 中定义,它的值就是 0。新标准下,现在的 C++ 程序最好使用 nullptr,同时尽量避免使用 NULL。
把 int 变量直接赋给指针是错误的操作,即使 int 变量的值恰好等于 0 也不行。
如同初始化变量一样,同样建议初始化全部的指针,因为指针本质上是访问内存空间,如果恰巧访问的内存空间地址原本有内容,我们又做出某些修改,那会引发很大的麻烦。如果定义时不知道指针改指向何处,那么可以把它初始化为 nullptr,这样就能让程序知道它没有指向任何具体的对象了。
最好的区分一条赋值语句到底是改变了指针的值还是指针所指向对象的值的方法就是记住赋值永远改变的是等号左侧的对象。
同理,当指针指向一个合法值,就能将它用于条件表达式中。
void * 指针是一种特殊的指针类型,可用于存放任意对象的地址。对于一个 void * 指针,我们只能:拿它和别的指针比较、作为函数的输入或输出、赋给另一个 void * 指针,不能:直接操作 void * 指针所指的对象。
简单地说,对于 void * 而言,内存空间仅仅是内存空间,它无法访问内存空间中所存的对象。(当然后面还是有办法获取 void * 所存地址的)

# 19. 复合类型的声明

其实类型修饰符只是声明符的一部分,它和基本数据类型毫无关系。
比如

int* p;
int* p1, p2; // p1是指向int的指针,p2是int
int *p1, *p2; // p1和p2都是指向int的指针
int* p1;
int* p2;

这两种写法没有对错之分,但需要保证风格统一。
当然,之前说过,指针本身也是对象,所以,* * 指针的指针,* * * 指针的指针的指针,以此类推都是存在的。
而且根据引用的定义,*& 指针的引用也是存在的。
要理解一个左值的类型,最简单的办法就是从右向左阅读得到它的定义。

int *&r = p; // 从r开始向左,第一个符号是&,第一个符号有最直接的影响,因此r是一个引用。其余部分则用来确定r引用的类型是什么,*说明r引用的是一个指针,最后通过int我们可知r引用的是一个int指针

今天的内容大概就到这里了,第二章还有一小部分内容,本章指针和引用深入可探讨的内容还是很多的,之后每天更得内容可能会少一点,会同步推一下项目以及 mit 6.1810 的。如有错误欢迎指正,如有疑问欢迎留言评论,我会尽快回复。