文章目录
1 开始
本章介绍 C++ 的大部分基础内容:类型、变量、表达式、语句及函数。在这个过程中,我们会简要介绍如何编译及运行程序。
1.1 简介
C++ 语言经过几十年的发展,已经从当初仅仅关注机器效率渐渐开始更加关注开发效率。这很大程度是由于别的语言的冲击, Python 、 Javascript 这些语言的开发效率相对来说要高不少,渐渐 C++ 也往这方面考虑。由于 C++ 即想保持机器效率,又想最大化开发效率,导致现在语言特性繁多而且还在不断增加,这是不得已的妥协。
C++11 是目前最广为接受的现代 C++ 标准,近些年还发展出了 C++14 和 C++17 标准,都是在 C++11上进行小幅扩展的。这本书所讲的内容都是关于 C++11 的。
C++11 具有语言更加统一,标准库更易用更安全并且更高效,动手写自己的库也更加容易的特点。比如auto 关键字的广泛使用导致程序员可以忽略类型细节,将精力放在解决问题上而不是语言细节上。而智能指针和移动容器(move-enabled containers)使得程序员可以更少关注资源管理(resource management)方面的细节,从而可以更加安全高效的写复杂的库。
C++ 是一门很大的语言,包含了为各种问题定制的方案。其中一些仅适用于大的项目,而小项目用不上。所以并不是所有程序员需要了解每个特性的所有细节。每个人都需要了解语言的核心部分,而一些高级以及很偏门(special-purpose)的主题可以快速浏览,并在真正需要时才用心研究这些特性。 C++ 语言中还有一些概念对于理解整个语言有至关重要的意义,这些部分值的好好理解。
现代 C++ 语言由以下三部分组成:
1. 从 C 语言继承而来的低层(low-level)语言细节;
2. 允许定义自己的类型的 OOP 编程方式,从而构建大的程序和系统;
3. 丰富的标准库;
1.2 最简单的C++程序
int main()
{
return 0;
}
所有 C++ 程序都包含一个以上的函数,其中最重要的 main 函数,操作系统通过调用 main 函数来运行程序。函数包含四个元素:返回类型、函数名字、形参列表、函数体。 main 函数被指定返回 int 类型, int 类型是内置类型,也就是语言本身提供的类型。通常 main 函数返回 0 表示程序运行正常,返回非 0 值表示遇到错误。函数体是以 { 开头的语句块。函数体中的 return 语句将终止函数的执行,并返回一个值给调用者,返回的值的类型必须与函数的返回类型一致。对于返回类型为 void 的函数, return; 将直接将控制权返回调用者,而执行到函数末尾也将隐式的返回。
关键概念:类型
类型在任何一门语言中都具有极其重要的地位,类型定义了数据内容和作用于这些数据上的操作,没有一门语言是没有类型的。差异在于有的语言允许自定义类型,有的语言只能使用语言内置的类型,有的语言允许变量是动态类型,有的语言要求变量只能是固定的类型。我们定义的数据保存在变量中,而所有变量都必须要有一个类型。
输入输出
C++ 语言本身是不包含输入输出的。 C++ 跟很多别的语言一样通过提供一个 IO 库来实现输入输出。本书中使用的输入输出库时 iostream 库, iostream 库有两个基本的输入输出类型, istream 用于输入,ostream 用于输出。流(stream)是从 IO 设备读取或写入的一连串字符。术语 stream 的含义就是字符以序列(sequence)的形式被产生和消费。 C++ IO 库定义了 istream 类型的标准输入对象 cin, ostream 类型的标准输出对象 cout,另外两个 ostream 对象 cerr 表示标准误, clog 产生程序执行的通用信息。一般clog 用的比较少。标准输入/输出从命令行窗口读取和写入,是最基本的输入输出方式。
#include 是预编译指令,通常用来包含程序需要的头文件,通常 #include 放在源文件的顶部。std::cout << "Enter two numbers:" << std::endl;
C++ 中的表达式由至少一个操作数和一个操作符组成,通常会产生一个结果。 表达式和表达式可以组成新的表达式,所以说表达式的定义是递归的。 << 操作符有两个操作数,左边的是 ostream 类型对象,右边的是需要输出的值,这个表达式的结果是左边的值,也就是 std::cout 对象本身。由于这样的设计多个<< 串行,这种操作也称之为流式操作(flow operation),操作返回操作对象本身。
std::endl 是一个特殊的值,称之为操纵器(manipulator),这种数据可以对流本身进行设置,会影响流的状态。写入 endl 将会插入一个换行符到流中,并且刷新流缓冲。刷新缓冲将保证写入的数据被真正写入到输出流中,而不是暂存在内存中。其实呢,刷新流只是将写的数据提交给操作系统,操作系统本身也有一个文件缓冲,只有当文件缓冲积累到一定程度时才会真正写入到文件中,这是对文件系统的优化,避免每次写入都对磁盘进行操作。
C++ 对 C 的一个重要改进就是名称空间(namespace), C 中所有的外部可见名字都在同一个名称空间中,所以 C 发展出了一个简单有效的方式来避免名称冲突,就在以 <lib>_ 为前缀, lib 就是你开发的库名字。名称空间像作用域一样将名字限制住,不同名称空间中的相同名字不会冲突。所有的标准库的名字都定义在 std 名称空间中。要使用名称空间中的名字必须加上名称限定符(:: operator),如 std::cout 引用标准库中的 cout 名字。
int v1 = 0, v2 = 0;
这条语句涉及到变量的定义和初始化, C++ 中的初始化比 C 复杂不少。 C++ 的一大目标就是使得自定义的类型与内置类型的操作方式统一, C++ 在这方面做了许多努力同时这些方面的确也是很复杂的。当定义一个变量时同时提供一个初始的值(这里是 0)就将变量给初始化了。 std::cin >> v1 >> v2;
>> 操作符与上面的 << 操作符很类似,这里以 istream 类型对象为左操作数,以 v1 和 v2 变量为右操作数,功能是读取用户的输入并将值保存在 v1 和 v2 变量中。这个操作符也返回左操作数,所以可以将多个 >> 串在一起组成一个单一语句。当然分开写也是可以的。
std::cout << "The sum of " << v1 << " and " << v2<< " is " << v1+v2 << std::endl;
这个语句与输出提示的语句一样,唯一值得注意的是现在 << 处理了两种不同类型:字符串类型和 int类型。 C++ 的库函数定义很多重载操作符来应对不同类型的操作数。
1.3 注释
注释是程序员对程序语句的解释,注释会被编译器完全过滤掉,所以对程序运行本身没什么影响。注释的主要功能是提供给程序员看的,可能是别的 review code 的同事也可能就是作者自己。注释是人与人的一种沟通方式。注释通常用来解释一个复杂的算法,解释变量的含义以及解释某段晦涩代码的执行过程。但是注释不易过多,过度的注释反而妨碍阅读者的视线和思维,真正好的代码应该是明的,从阅读代码中就可以清晰明白代码的意图,如果代码需要注释才能让人看懂通常意味着代码很可能隐藏着 bug 。而且必须保证注释的含义与代码真实的含义一致,否则就是一种误导,必然会招致阅读者的咒骂,所以在更新代码时务必更新注释。要么干脆就把代码写的足够简单,从而省略掉注释。
C++ 中有两种注释:单行(//) 注释和跨行注释(/**/)。单行注释将在行末结束,其中可以包含任何字符包括 // 符号本身。跨行注释是从 C 中继承过来的,这种跨行注释内容是 /* 和对应匹配的 */ 间的内容,内容中不能包含结束符,否则就是语法错误。这种注释最大的好处是可包含换行符,因而也成为块注释符。很多跨行注释都会在行首写一个前导 * 字符来标示多行注释。开始时 C 语言只支持这种块注释,后来受到了 C++ 的影响也开始支持单行注释。这里抠一个细节,块注释可以放在任何空白符可以放的地方,这些地方不包括字符串内部、变量名内部、别的注释内部。而单行注释将会导致行的后半段全部变成注释,所以通常放在行尾或占据一整行。
1.4 控制流
一般代码的执行都是顺序的,而控制流可以改变程序执行的顺序。只有极少的程序不需要控制流。控制流包括循环、分支、返回、跳出等。下面会简单介绍几个控制流语句。
while (condition)
statement
while 循环将在给定条件为 true 时重复执行,直到条件变为 false。所谓条件(condition)是一个表达式,这个表达式会产生一个 true 或 false 的值。 while 循环会先测试条件是否为真,并在条件为真的情况下执行语句块,然后再测试条件,在决定是继续执行还是结束循环。语句块由一条或多条在大括号中的 语句组成。语句块也是语句,可以放在任何语句可以存在的地方。通常,程序会在语句块中改变条件测试的变量的状态。
for (init-statement; condition; update-expresion)
statement
for 循环将控制变量的初始化、测试和更新放在了同一个位置,方便程序员查看修改。 for 循环也用来遍历容器对象,这是绝大部分应用程序都会用到的特性。因而,在实际运用中 for 循环会比 while 循环运用的更多。 for 语句由控制部分和主体部分组成。其中控制部分有三部分:初始化语句、条件语句和更新表达式。初始化语句中定义的控制变量只存在于 for 循环中,出了循环这个控制变量将不会存在。初始化语句只会在开始执行 for 循环时执行一次,条件将在每次将要进入主体语句前测试,测试如果为 true 将执行语句,否则将直接退出 for 循环。每次执行完主体语句之后就执行更新表达式来更新控制变量。然后接着重复测试条件与执行主体语句,直到最后测试条件为 false 。 C++ 并没有规定这三个部分需要包含什么特定语句,其实你可以放任何表达式在里边,甚至所有部分留空都可以,如果留空将执行 forever loop 。
while (std::cin >> value)
当我们将 istream 对象作为条件时,效果是测试流的状态。如果流没有遇到任何错误或者到达文件末尾(end-of-file),测试将返回 true 否则将返回 false 。 以这种方式测试 istream 实质上是在 istream 类中设置到 bool 内置类型的转换函数。
编译器的工作
编译器的工作是将源文件代码重新生成为操作系统可以识别的格式,在 Linux 下是 ELF 格式。编译器不能识别程序作者的意图,但是可以发现程序的语法错误、类型匹配错误以及声明错误。
4. 语法错误:任何该有分号的地方没有分号,括号不匹配之类的错误;
5. 类型匹配错误:给 int 类型变量赋值字符串类型值之类的错误;
6. 声明错误:没有声明就使用变量或者重复声明变量;
编译很可能在程序中有错误的时候产生非常的错误信息,这些信息过多几乎可以肯定没办法定位到所有错误。正确的做法是从上到下进行修复,每次修复一个就重新编译一下,然后看看是否还有错误。这个循环叫 edit-compile-debug .
Warning: 在 C++ 中 = 号用于赋值, == 用于比较。两个操作符都可以放在条件表达式中。
1.5 类的介绍
在 C++ 中通过定义 类 来定义用户自定义数据结构。类定义了一个类型以及与类型相关的操作,这是 C++ 的数据封装和抽象的核心。类是 C++ 最重要的特性之一,事实上设计 C++ 的一个主要目的就是使得类类型表现得跟内置类型一样自然,写法上一致并且行为上也一致。这个 Java 很不一样, Java 没有以表现一致为目的而是为了高效进行对象资源管理。相对来说 Java 会安全的更多。
类包括名字、结构和操作。通常结构会被放在头文件中,而操作定义在 cpp 文件中。通常头文件名字与类型一致。头文件后缀有以下一些选择: .h 、 .hpp 、 .hxx 。标准库的头文件通常没有任何后缀,这是编译器所不关注的,而 IDE 可能会关注头文件后缀。
为了使用类,不必关心内部实现细节,这是类的实现者需要关注的。用户要关注的是此类对象所能提供的操作。所有的类(class)定义了一个类型(type),并且类型名与类名一致。如本章的例子: Sales_item 类定义了 Sales_item 类型。当定义了类之后就可以像内置类一样使用。
Sales_item item;
以上语句声明了一个类型为 Sales_item 的对象 item ,在 C++ 和 C 语言倾向于认为所有的变量都是对象,包括内置类型的变量,而不仅限于自定义的类型变量。所有这些变量可以作为函数的参数,被输入输出符读写,被等号赋值,使用加号对两个对象相加,当然也可以用 += 符号进行操作。
关键概念:类定义行为
在阅读文本中的程序时需要注意的是 Sales_item 的作者定义了这个自定义类型对象的所有行为(action), Sales_item 类定了当对象定义时发生了什么,赋值时发生了什么,以及做加法、输入输出时发生了什么。总的来说,类的作者定义了自定义类型对象的所有操作,从而所有类的操作可以从类的结构看出。
#include <iostream>
#include "Sales_item.h"
int main()
{
Sales_item item1, item2;
std::cin >> item1 >> item2;
std::cout << item1+item2 << std::endl;
return 0;
}
来自标准库的头文件用尖括号(< >)包围,来自程序自定义的头文件用双引号(" ")包围。值得注意的是这里对两个 Sales_item 对象进行输入输出以及做加法。加法的实际意义由类的作者对 Sales_item 对象进行定义。
item1.isbn() == item2.isbn();
这里的成员函数是 isbn,成员函数(member function)是被定义为类一部分的函数,有时也被称为方法(methods)。通常用对象去调用成员函数, item1.isbn 中使用点号操作符来说明“取 item1 对象中的 isbn 成员” ,点号操作符(.)只能作用于类的对象。 其左操作数必须是类的对象,右操作数必须是类的一个成员名,结果就是取对象的一个成员。当用点号操作符访问一个成员函数时,通常是想调用该函数。我们使用调用运算符(())来调用函数,调用运算符是一对圆括号,里边放置实参(argument)列表(可能为空)。成员函数 isbn 并不接收参数。因而 item1.isbn() 调用对象 item1 的成员函数 isbn,函数返回 item1 中保存的 ISBN 书号。
7. buffer 缓冲: 用来存储数据的一段内存, IO 通常都会有缓冲用于输入和输出,对于应用程序来说缓冲是透明的。输出缓冲可以被刷新,从而强制写入到目的地。 cin 的读入会刷新 cout 缓冲, cout 的缓冲也会在程序结束时刷新。
8. built-in type 内置类型:由语言定义的类型,比如 int 、 long、 double 等;
9. class 类:一种用于定义自己的数据结构及相关操作的机制。类是 C++ 中最基本的特性之一。
10. class type 类类型:类定义的类型,类名就是类型名;
11. data structure 数据结构:数据及其上所允许的操作的一种逻辑组合;
12. expression 表达式:计算的最小单元。一个表达式由一个或多个操作数以及一个或多个操作符组成。表达式通常会产生一个结果。
13. function 函数:命名的计算单元;
14. initialize 初始化:当一个对象创建时同时赋予值;
15. standrad library 标准库:每一个 C++ 编译器必须支持的类型和函数集合。
16. statement 语句:程序的一部分,指定了在当程序执行时进行什么动作。一个表达式接一个分号就是一条语句。 其它类型的语句包括 if 、 for 、 while 语句,所有这些语句又可以包含别的语句。
17. uninitialized variable 未初始化变量:没有给初始值的变量。当类类型没有给初始值时其初始化行为由类自己定义。函数内部的内置类型初始化在未显式初始化时其值是未定义的。尝试使用未初始化的值是错误的,并且是 bug 的常见原因。
18. variable 变量:具名对象;
C++ 最基本特性
每一个广泛使用的编程语言都提供一些共通的特性,虽然它们之间的细节有差别。理解这些特性细节是理解语言的第一步。几乎所有语言都提供如下特性:
19. 内置类型,如整数、字符等;
20. 变量;
21. 表达式和语句;
22. 控制结构,如 if 和 while 来控制动作的条件执行或循环执行;
23. 函数,用于定义可调用的计算单元;
大部分语言在这些基础特性上提供两种扩展特性: 1. 使得程序员可以定义自己的类型类扩展语言; 2.提供标准库来提供有用的函数和类型而不是内建在语言中, IO 库就是一个例子;
在 C++ 中类型规定了对象可以执行的操作。一个表达式是否是合法的取决表达式中的对象的类型。一些语言提供运行时类型检查,相反, C++ 是静态语言,类型在编译时已经作出检查。因而,编译器了解每个名字的类型。
由于 C++ 允许程序员定义新的数据结构, 其表达力得到了极大的提升。程序员因此可以将语言塑造成适合将要解决的问题,而不需要语言设计者提前知晓这些问题的存在。 C++ 中最重要的特性就是类,类允许程序员定义自己的类型。这种类型也被称为“类类型” ,从而与内置类型区分开来。一些语言只能让程序员定义类型的数据内容,而 C++ 还允许程序员定义类型的操作。 C++ 的一个主要设计目标就是让程序员定义自己的类型,从而与内置类型一样容易使用。 C++ 标准库就是这方面的一个典范。
2 内置类型
类型是语言的基础,类型告诉我们数据的含义和可执行的操作。 C++ 提供类型的扩展机制。语言仅定义了若干基础类型(字符、整数、浮点数),并且提供了让我们定义自己的类型的机制。标准库使用这些机制提供了功能复杂的类,如:可变长度字符串 string 类,向量 vector 类等。
本章描述 C++ 的内置类型以及开始描述 C++ 提供的定义复杂类型的机制。
2.1 内置类型
C++ 的内置类型集合几乎与 C 完全一致,除了多了一个 bool 类型, C 从 C99 开始通过头文件引入
了 bool 类型。除此之外,包括字符类型、各种有符号无符号的整数类型以及浮点数类型,以及特殊类型 void。void 主要用于指示函数不返回任何值,不接收任何参数以及作为 C 中的通用指针。
2.1.1 算术类型
算术类型包含两种形式:整数类型(包含字符和布尔类型)和浮点数类型。内置类型的大小在不同的机器上很可能是不一致的,标准只说明了编译器必须保证的最小尺寸,但是编译器可以提供更大的尺寸,不同的尺寸导致可表示的数字范围不一样。 bool 类型仅用于表示真值 true 和 false,我们不关注其尺寸。char 类型保证容纳机器的基本字符集中的字符,通常是 ASCII 字符集, char 长度是一个机器字节。 int 类型表示宿主机器的整型自然尺寸(the natural size),通常机器在这个大小上做算术运算是最快的。 short 和long 以及 long long 用于修饰 int 表示各种不同长度的整型。标准保证 short 的长度不长于 int 长度, int长度不长于 long 类型, long 类型则不长于 long long 类型,现代计算机中 short 通常为 16 位, int 为 32位, long 在 32 位机器上是 32 位,在 64 位机器上是 64 位。 long long 是由新标准引入的,用的比较少。
除此之外, C++ 还支持扩展的字符类型, wchar_t 保证可以容纳机器的最大扩展字符集中的任何字符。char16_t 和 char32_t 则用于 Unicode 字符集,它们的长度如类型名中的数字所示。
浮点数类型常用的有两种:单精度 float 类型和双精度 double 类型还有扩展精度 long double 类型。标准指定了最少有效位比特,大部分编译器会提供更高的精度,但程序员不应该依赖编译器实现细节。目前所有现代计算机都遵循 IEEE 754 浮点数标准。 long double 可能是 96 位或 128 位,通常用于容纳特殊目的的浮点数硬件,且精度在不同实现之间不同。
内置类型的机器级表示
任何数据在机器中都是一串 bit,每个比特装载一个 0 或 1 ,对于机器本身来说这些数据是没有固定
的意义的,这些指代整数、那些指代浮点数、这里是数据段、那里是代码段,机器中没有定义这些。机器可操作的最小数据块是 byte (字节),但是字节不是机器最自然的处理方式,如果对汇编有所了解会知道为了处理字节, 32 位 CPU 是先处理成 32 位整型,再截断成字节。因而 int 类型也被成为机器字(word),现在更为广泛使用的是 CPU 以 64 位为机器字。所谓机器字就是处理器的寄存器大小。在任何机器中每个字节都会有自己的地址,从某个地址开始的连续字节可以被解释为不同的类型,关键在于看程序格式如何 处理。如:连续的 1 个字节可以处理成 char 类型, 4 个字节可以处理成整数类型或者单精度浮点数,内存内容的具体含义取决于程序赋予地址的类型。类型决定了需要使用多少比特以及怎样解释比特的含义。
有符号和无符号类型
在讲解之前先提示有符号和无符号类型混用是 bug 的常见原因。在相互的类型转换混杂算术运算容易导致超出期望的结果,所以建议不要混用这两种类型。在后面会讲解有符号和无符号类型之间的转换规则。除了 bool 和扩展字符类型(wchar_t, char16_t, char32_t)外,别的整数类型(short, int, long, long long)既可以是 signed 或者 unsigned ,其默认是有符号类型,在类型前面加上 unsigned 就变成无符号类型。unsigned int 可以缩写为 unsigned 。字符类型比较特殊,分为三种明确区分的类型: char, signed char,unsigned char 。 char 可能是 signed char 或 unsigned char 中的一种,具体取决于编译器实现。目前所有现代计算机都用二进制补码来表示有符号整数类型。
决定使用何种内置类型的建议C++ 和 C 一样将内置类型设计的尽可能贴近硬件,因而算术类型被定义的很宽泛,为的就是满足不同硬件的特性。这种定义规则遵循最小可用原则,尽可能适用于尽可能多的硬件。然后,建议是程序员在编写程序时应该通过限定具体的类型来规避这种复杂性。 有以下几点建议:
24. 当知道值不可能为负数时用 unsigned 类型;
25. 在算数运算中使用 int , short 的运算速度和容量都不及 int,而 long 在 32 位机器下雨 int 大小
一致。如果值超出 int 的最小保证范围,使用 long long 。
26. 不要将 char 和 bool 类型用于算术表达式,将它们用于专用的场景。因为 char 可能是有符号或者无符号的,所以真的要使用的话就明确指定 signed char 或者 unsigned char 。这里需要说明一下:标准保证了 ASCII 中的字符数值都是正数,如果只是用 char 提供值而不存储值的话,是可以直接使用 char 的。
27. 将 double 用于浮点运算, float 通常精度不够而且 double 的运算时间可能还优于 float , long
double 除非在特殊场景下几乎不会使用到。
2.1.2 类型转换
相关的算术类型可以进行转换,这些转换是隐式的,意味着不需要强转(cast)就能实现。当我们需要一种类型的对象但是提供了另外一种类型的对象,这时候就会发生自动转型。
转换过程将发生什么定义规则如下:
28. 当给一个非布尔值算术类型赋值一个 bool 类型对象时, false 就取值 0, true 将取值 1 。
29. 给 bool 类型对象赋值一个非布尔值时, 0 将取值 false,非 0 值取值 true 。
30. 将一个浮点数赋值给整型时,小数点后的值被截断,只保留小数点前的数值;
31. 将整数赋值给一个浮点数时,小数部分为 0 ,如果这个整数超出了浮点数的精度范围,精度将丢失;
32. 将一个超出范围的值赋值给无符号值,结果值将取此值对无符号值的最大范围的模,如: unsigned char的取值范围是 0~255,如果被赋的值大于等于 256 ,则将对 256 取模再赋值, -1 取模为 255;
33. 当将一个超出范围的值赋值给有符号类型时,结果是未定义的;
2.1.3 字面量
字面量用来描述数字、字符和字符串的值,字面量是常量。每个字面量都有类型,字面量的形式和值决定了其类型。
整型可以写做十进制、八进制或者十六进制。以 0 开头的是八进制,是 0x 或 0X 开头的是十六进制。如:20(十进制) 024(八进制) 0x14(十六进制)。通常,十进制是有符号的,而八进制和十六进制可以是无符号或者有符号的。十进制数字顺序从 int, long 或 long long 中选择最小可容纳数值的类型。八进制和十六进制则顺序从 int, unsigned, long, unsigned long, long long 或 unsigned long long 中查找适合的类型。如果数值大于最大的类型的范围则会产生错误。没有 short 类型的字面量。可以在值后加上 L 或 l 明确表示值为 long 类型,或者后缀为 U 或 u 表示无符号类型, ul 或 UL 则明确指示 unsigned long ,后缀 ll或 LL 表示 long long 类型。以上后缀适用于十进制、八进制和十六进制。如: 0XFUL 是 unsigned long 类型的值 15 , 1234L 则是 long 类型的值 1234 。
浮点数字面量既可以包含小数点(如: 123.4)也可以包含指数(1e-2),指数可以用 E 或 e 指示,指数值为 -N 表示小数点左移 N 位,正数表示右移。当浮点数没有后缀时表示的 double 类型值。只有当明确加了 f 或 F 后缀时才表示 float 常量。 l 或 L 后缀表示 long double 类型。
字符常量值是一个整数。字符写做单引号中的单个字符如:‘x’ ,值是字符在机器字符集中的数字表示值。通常不会直接在代码中写明字符的数字表示值,因为可能每个字符集的数字表示不太一样。字符常量在算术运算就像别的整数一样,尽管它们最常用的场景是和别的字符进行比较。 C++ 和 C 中定义了几个可以 的字符,这些字符通常是不可打印或者在字符串中有特殊含义。如: \n \r \t \\ \? \' \"。任意字符可以用八进制转义序列 \ooo 或十六进制转义序列 \xhh 表示。特殊的转义 \0 表示值为 0 的字符,也就是空字符。通常写做 \0 而不是数字 0 是为了强调其字符属性。
字符串是双引号中的 0 个或多个字符。 如: “I am a string” 和 "" 。 字符串字面量就是字符数组,并且编译器会在字符串的末尾隐式加上一个 \0 字符。所以字符串的真正长度比看起来多了一个字符。如: “A”有两个字符。以上描述的转义同样适用于字符串。两个相邻的字符串(中间只有空白符)会在编译期间拼接成一个字符串,通常如果字符串太长时会这么做。
true 和 false 是 bool 类型的常量。 nullptr 是指针的常量,在 C 中一般写做 NULL 宏。字面量一定是常量,只有常量参与运算的表达式为常量表达式。常量表达式可以在编译期间就求值,并且可以在常量出现的地方使用。
2.2 变量
变量提供程序可操作的具名内存。每个 C++ 变量都有类型,类型决定了变量内存的尺寸和布局以及
可保存的值的范围,类型还决定了变量可以进行的操作。在 C++ 中变量和对象是可互换的。
2.2.1 变量定义
变量定义包含类型名和其后的一个或多个变量名,变量名之间用逗号分割,并且以分号结束。定义可以为一个或多个变量提供初始值。
int sum = 0, value, units_sold = 0;
Sales_item item;
std::string book("0-201-78345-X"); <iostream>
上面的定义包含了内置类型和用户自定义类型,并且都有包含初始化值。对象在创建时赋予特定的值成为初始化。初始化值可以是任意复杂的表达式,只要值是对应的类型即可。当在同一条定义语句中定义两个或以上的变量时,前面的变量将马上对后面的变量可见,因而,可以用前面的变量对后面的变量进行初始化。如:
double price = 109.99, discount = price * 0.16;
在 C++ 中初始化是一个非常之复杂的话题,这本书中会一次次的提及。究其原因在于 C++ 中在定义变量时可以对用户类型进行初始化, C 语言中由于没有这种用户定义类型所以初始化直接而简单, Java 中的用户类型对象只存在于堆中,用户定义时仅仅定义了一个对真正的对象的引用,因而都没有 C++ 这么复杂。在 C++ 中许多程序员无法理解 = 号用于初始化变量,大家都倾向于认为这种形式的初始化是赋值,但是在 C++ 中初始化和赋值是完全不同的操作。其实对于 C++ 的内置类型这两种操作区别并不明显,真正区别在于用户定义类型,当初始化用户定义类型对象时调用的是构造函数,而赋值时调用的是赋值操作函数。
初始化不是赋值。初始化发生在变量创建时给定一个值,而赋值是将对象原有的值擦除并替换成新值。
列初始化
C++ 中的初始化变得更复杂的原因还在于 C++ 定义了多种初始化方式。如一下均为初始化
units_sold 的方式:
int units_sold = 0;
int units_sold = {0};
int units_sold{0};
int units_sold(0);
新标准允许初始化使用 {} 大括号的形式,这种形式的初始化在之前的版本仅允许在初始化列表时使用,此种方式成为列初始化(list initialization)。大括号形式的初始化子(initializer)现在可以用于初始化对象, 或者在一些情况下用于对对象赋予新值。用这种方式来初始化内置类型有一个重要特性,就是编译不允许
丢失信息的初始化,如:
long double ld = 3.1415926536;
int a{ld}, b = {ld}; //不允许的
默认初始化
当定义变量时不显式进行初始化,将执行默认初始化(default initialized),默认初始化变量将获得默认值。至于这个默认值是什么则取决于变量类型以及变量在何处定义。
没有显式初始化的内置类型,如果定义在函数外面将初始化为 0,如果定义在函数内部则值是未定义的。使用未定义的值进行任何计算或者赋值给其它变量是错误的,而且是非常常见的错误。类类型控制本类型对象的初始化过程,不管对象定义在函数内部还是外部。如果类没有定义默认构造函数,就无法构造不显式初始化的对象。大部分类都会定义默认构造函数,类会给对象提供合适的默认值,如:string 类默认初始化为空字符串。有一些类无法提供合适的默认值,所以必须显式提供初始值。
总结来说:函数内的未初始化内置类型变量的值是未定义的,没显式初始化的类对象初始化值由类的默认构造函数控制。
2.2.2 变量声明和定义
C++ 沿用了 C 的分离编译方式,分离编译允许我们将程序拆分成多个源文件,而且可以单独编译,最后再将所有的编译出来的 .o 文件链接在一起。当将程序拆分成多个文件后,需要一种能够在多个文件中共享变量的机制。定义在任何函数外部的函数、类、变量对于整个程序来说都是可见的。
2.2.3 标识符
程序中的名字都是标识符,遵循标识符的规则。大部分的标识符都继承自 C 语言,具体说就是标识符(identifier)必须由字母、数字或下划线组成,其它字符都是非法的。
2.2.4 名字的域
作用域(scope)是程序的一部分,名字在其中拥有特定的含义。 C++ 中的作用域用大括号 {} 来分割各个作用域。相同的名字可以在不同的作用域中指代不同的实体(函数、类、变量)。名字从定义的位置直到声明它的作用域结束的位置都是可见的。
2.3 复合类型
复合类型指的是用别的类型定义的类型。 C++ 中有好几种复合类型,这里重点描述引用(reference)和指针(pointer)。这里给出一个更通用的声明的含义,声明指的是基础类型(base type)加上一系列声明符(declarator)。基础类型是普通类型,是自己可以描述自己的类型,如:内置类型和类类型。声明符
是变量名字以及可选的类型描述符,如:指针描述符 *,引用描述符 &,数组描述符 [] 。
(未完待续)
Comments | NOTHING