第一章 预备知识

C++简介

C++融合了三种不同的编程方式:

  • C语言代表的过程性语言
  • 以类为代表的面向对象于艳
  • 模板支持的泛型编程

面向对象编程

面向对象编程(OOP)强调的是数据。理念是设计与问题本质特性相对应的数据格式。

泛型编程

泛型编程强调编程要独立于特定数据类型,即创建独立于类型的代码。

C与C++

C++是C的超集,尽管有一些细微差别,但这种差别很小。

第二章 开始学习C++

进入C++

C++提供了不同于C的输入输出工具,内置的cin和cout对象,使用它们需要包含头文件iostream。

main函数通常被启动代码调用,启动代码是编译程序加入到程序当中的。

如果main函数没有指明返回语句,则认为return 0;是返回语句。

C++提供两种注释的方式//(C++)或/**/(C)。C99标准在C语言中加入了//注释。

#include编译指令会让预处理器添加指定文件内容到程序中。

不同的头文件名可被不同的程序使用。

头文件类型 约定 示例 说明
C++旧式风格 以.h结尾 iostream.h C++程序可以使用
C旧式风格 以.h结尾 math.h C,C++程序可以使用
C++新式风格 没有扩展名 iostream C++程序可以使用,使用namespace std
转换后的C 加上前缀c,没有扩展名 cmath C++程序可以使用,可以使用不是C的特性,如namespace std

C++中可以使用命名空间来避免名字冲突。可以使用using编译指令来使用命名空间中的函数或类对象。

1
2
3
using namespace std;
using std::cin;
std::cout << "Hello World!" << std::endl;

cout中,可以使用endl和”\n”表示换行符。不同的是endl确保程序继续运行前刷新输出,使用”\n”不能提供这样的保证。

1
2
cout << "Hello World!\n";
cout << "Hello world!" << endl;

尽量遵循好的C++代码风格,程序会便于阅读。

C++语句

变量在使用前必须进行声明,通常是为了指出要存储的类型和程序对存储在这里的数据使用的名称。

对于声明变量,C++的做法是尽可能在首次使用变量前声明它。

cout可以根据变量的类型相应的调整行为。

1
2
cout << 12 << "12" << endl;
printf("%d %s\n", 12, "12");

类描述了一种数据类型的全部属性(包括可使用它执行的操作),对象是根据这些描述创建的实体。

函数

C++程序应当为程序中使用的每个函数提供原型。

不要混淆函数原型和函数定义。

1
2
3
4
5
int fun();
int fun()
{
return 0;
}

函数可以有返回值和无返回值。无返回值的函数用void来标识。

函数的格式为:

1
2
3
4
type functionname(argumentlist)
{
statements
}

第三章 处理数据

简单变量

在程序中,为了存储信息,必须记录三个基本属性:

  • 信息将存储在哪里
  • 要存储什么值
  • 存储何种类型的信息

声明变量恰好记录了这些信息。

变量名

C++变量名必须遵循如下规则,并尽量使用有意义的变量名:

  • 在名称中只能使用字母字符,数字和下划线(_)
  • 名称的第一个字符不能是数字
  • 区分大写字符与小写字符
  • 不能将C++关键字用作名称
  • 一两个下划线或下划线和大写字母打头的名称被保留给实现(编译器及其使用的资源)使用。以一个下划线开头的名称被保留给实现,用作全局标识符。违反这一点并不会导致编译器错误,但可能会导致行为的不确定性。
  • C++对名称没有长度限制,名称中的所有字符都有意义,但有些平台有长度限制。C99标准只保证名称中的前63个字符有意义

整形

语言只能表示所有整数的子集。C++提供多种不同的整型,以便根据程序具体要求选择最合适的整型

C++的基本整型分别是char、short、int、long和C++ 11新增的long long,其中每种类型都有有符号版本和无符号版本

C++对整型提供了灵活的标准,确保整型的最小长度

  • short至少16位
  • int至少与short一样长
  • long至少32位,且至少与int一样长
  • long long至少64位,且至少与long一样长

实际上,short是short int的简称,long是long int的简称

sizeof运算符返回类型或变量的长度,单位为字节

1
2
3
4
int a = 0;
cout << sizeof a << endl; // 变量可以这样写,类型必须加括号
cout << sizeof(a) << endl;
cout << sizeof(int) << endl;

头文件climits(limits.h)定义了符号常量来表示类型的限制

符号常量 表示
CHAR_BIT char的位数
CHAR_MAX char的最大值
CHAR_MIN char的最小值
SCHAR_MAX signed char的最大值
SCHAR_MIN signed char的最小值
UCHAR_MAX unsigned char的最大值
SHRT_MAX short的最大值
SHRT_MIN short的最小值
USHRT_MAX unsigned short的最大值
INT_MAX int的最大值
INT_MIN int的最小值
UINT_MAX unsigned int的最大值
LONG_MAX long的最大值
LONG_MIN long的最小值
ULONG_MAX unsigned long的最大值
LLONG_MAX long long的最大值
LLONG_MIN long long的最小值
ULLONG_MAX unsigned long long的最大值

C++支持类似C的符号常量,其进行简单的字符串替换。C++还提供了const来定义符号常量

C++提供了不同于C的初始化方式:

1
2
3
4
5
6
int a = 0;
int b(1);
int c{2}; // C++ 11
int d = {3}; // C++ 11
int e = {}; // set to 0
int f{}; // set to 0

无符号类型整数不能存储负数,但可以增大变量能够存储的整数的最大值

C++确保无符号整数溢出时会取范围另一端的值,但并不保证有符号整数超越限制(上溢和下溢出)时不出错,而这正是当前实现中最为常见的行为

通常int被设置为对目标计算机而言最“自然”的长度,自然长度是只计算机处理起来效率最高的长度,如果没有非常有说服力的理由选择其他类型,则应使用int。其他根据具体取值范围来选择整数类型

整型字面值(常量)是显式书写的常量。C++同C一样能以三种不同的方式来书写整数:基数为10,基数为8,基数为16。C++使用前一(两)位来标识数字常量的基数。如果第一位为1-9,则基数为10;如果第一位为0,第二位为1-7,则基数为8;如果前两位为0x或0X,则基数为16

C++通过后缀,长度来确定常量的类型。除非有理由(使用了后缀或值超出int存储范围)存储为其他类型,否则C++就将整数常量存储为int。

后缀是放在数字常量后的字母,用于表示类型。l或L表示long,u或U表示unsigned int,ul(可以采用任何一种顺序,大小写均可)表示unsigned long(由于l看起来像1,推荐使用大写L后缀)。C++提供了用于表示long long的后缀ll或LL,还提供了用于表示unsigned long long的后缀ull,Ull,uLL,ULL

对于长度,C++中,对10进制整数采用的规则与16进制和8进制稍有不同。对于不带后缀的十进制整数,将使用下面几种类型中能够存储的最小类型来表示:int、long或long long。对于不带后缀的16进制或8进制整数,将使用下面几种类型中能够存储的最小类型来表示:int、unsigned int、long、unsigned long、long long或unsigned long long

char类型是专为存储字符而设计的,编程语言将字符存储为数值,实际上char类型是一种整型,可以当做比short更小的整型

char字面值有两种表示方式:直接单引号括起来的单个字符,如’a’;使用转义字符表示的特殊字符,如’\n’

与int类型不同,char在默认情况下既不是没有符号,也不是有符号。是否有符号由C++的实现来决定,但可以显式的指定char为有符号或无符号,这在把char当做数值类型是就十分重要

bool类型的值可以为true或false,分别表示真假。字面值true和false都可以通过提升转化为int类型。任何数字值或指针值都可以被隐式转换为bool值

C++中可以使用const来修改变量声明和初始化,创建一个符号常量。其被初始化后,编译器就不允许修改该常量的值。如果在定义常量是没有初始化值,则该常量的值是不确定的,且无法修改

1
const type name = value;

浮点型

浮点数可以用来表示小数,浮点数有两种表示:一是常用的标准小数点表示,如8.2等;二是E表示法,如3.45E6表示3.45与1000000相乘的结果,指数为负表示除以10的乘方。E表示法确保数字以浮点格式存储。

有三种浮点类型:float、double和long double。C和C++对有效位数(数字中有意义的位数)的要求分别是:至少32位,至少48位且不少于float,long double至少和double一样多。可以从cfloat头文件获取这些类型的相关限制。

默认情况下的浮点常量存储为double,如果想使用float存储,请加f或F后缀

浮点数由于精度有限,在计算时可能会出现错误

signed char,short,int和long统称为符号整型;它们的无符号版本统称为无符号整型;C++ 11新增了long long。bool、char、wchar_t、符号整型和无符号整型统称为整型;C++ 11新增了char16_t和char32_t。float,double和long double统称为浮点型。整型和浮点型统称为算术类型

算术运算符

C++提供了5中基本算术运算:+、-、*、/、%

  • 对于除法,如果两个操作数都是整数,则结果为商的整数部分
  • 对于取模,两个操作数必须是整数,如果其中一个是负数,结果符合如下规则:(a/b)*b+a%b=a
  • 为了完成复杂的运算,需要规定运算符的优先级和结合性。*和/的优先级高于+和-。+,-,*,/都是从做到右结合的。从左到右的结合性意味着如果优先级相同的操作符被同时作用于同一个操作数,则首先计算左侧的操作符。从右到左亦然。

对于如下表达式,程序必须在做加法前计算乘法,但并没有指出先计算哪个乘法,这取决于实现。结合性在这里不起作用,因为两个乘号没有作用到同一个操作数

1
20*5+24*6

C++自动执行多种类型转化:

  • 将一种算术类型的值赋给另一种算术类型的变量时,C++将对值进行转换
  • 表达式中包含不同类型的类型时,C++将对值进行转换
  • 将参数传递给函数时,C++将对值进行转换

初始化和赋值进行的转换有可能会丢失数值或损失精度

以{}方式进行的初始化,不允许缩窄

在表达式中,有两种转换:一些类型在出现时就会自动转换,有些类型在与其他类型同时出现在表达式中时将被自动转换

在计算表达式时,C++将bool,char,unsigned char,signed char和short值转换为int
如果short比int短,则unsigned short类型被转换成int,如果两种类型长度相同,则unsigned short转换为unsigned int,这确保转换时不会丢失数据,同样wchar_t被提升为下列类型中第一个宽度足够的类型:int,unsigned int,long或unsigned long

不同类型进行计算时也会进行转化:

  • 如果有一个操作数类型为long double,则将另一个操作数转换为long double
  • 否则,如果有一个操作数类型为double,则将另一个操作数转换为double
  • 否则,如果有一个操作数类型为float,则将另一个操作数转换为float
  • 否则,说明操作数都是整型,则执行整型提升
  • 在这种情况下,如果两个操作数都是有符号或无符号的,且其中一个操作数级别比另一个低,则转换为较高的类型
  • 如果一个操作数为有符号的,另一个操作数为无符号的,且无符号数的级别比有符号数的级别高,则将有符号数转换为无符号操作数对应的类型
  • 否则,如果有符号类型可表示无符号类型的所有可能值,则将无符号操作数转换为有符号操作数的类型
  • 否则,将两个操作数都转换为有符号类型的无符号版本

注:传统C总是将float提升为double,即使两个操作数都是float

传递参数时的类型转换通常由C++函数原型控制,然而也可以取消原型对参数传递的控制,尽管这样做并不明智。在这种情况下,C++将对char和short(signed和unsigned)应用整型提升。另外为保持传统C语言中大量代码的兼容性,在将参数传递给取消原型对参数传递控制的函数时,C++将float参数提升为double

C++允许强制类型转换:

1
2
(type_name)value // C
type_name(value) // C++

在初始化声明中,如果使用关键字auto,而不指定变量的类型,编译器把变量的类型设置成与初始值相同

第四章 复合类型

数组

数组是一种数据格式,能够存储多个同类型的值

要创建数组,可使用声明语句,数组声明应指出一下三点:

  • 存储在每个元素中的值的类型
  • 数组名
  • 数组中的元素数

array_size指定元素数目,它必须是整型常数或const值,也可以是常量表达式,其中的值在编译时必须已知

1
type_name array_name[array_size];

数据的下标从0开始编号,编译器并不会检查使用下标的有效性

只有在定义数组时才能使用数组初始化,以后就不能使用了,不能将一个数组赋值给另一个数组
初始化数组时,提供的值可以少于数组的元素数目,编译器将其他元素设置为0
初始化时,方括号内为空,C++编译器将计算元素个数
C++ 11提供的列表初始化{}可以省略等号;大括号内不包含任何东西,则将所有元素设置为0;列表初始化进制缩窄

1
2
3
4
5
int a[4] = {0, 1, 2, 3};
int b[4];
int c[4] = {1}; // 1,0,0,0
int d[4] = {0};// 0,0,0,0
int e[4] = {};

字符串

C-风格字符串以空字符结尾,空字符写为\0,ASCII值为0,用来标记字符串的结尾

1
2
3
char a[4] = {'a', 'b', 'c', 'd'};		// not a string
char b[4] = {'a', 'b', 'c', '\0'}; // a string
char c[4] = "abc";

用引号括起来的字符串隐式的包括结尾的空字符,这种表示方式成为字符串字面值

确定存储字符串需要的最短数组时,不要忘记将结尾的空字符计算在内

字符串常量不能与字符常量互换,即’s’和”s”表示不同的含义。后者表示s和\0两个字符组成的字符串

任何两个由空白分割的字符串常量都将自动拼接成一个字符串常量,拼接时不会再字符串之间添加空格

cin使用空白来确定字符串的结束位置,并不能读取包含空白的一行字符串

cin提供的成员函数getline(char *arr, int size)提供读取一行的能力,它通过回车来确定输入结尾,但不保存换行符(用空字符来替换换行符)。getline最多读取size-1或遇到换行符结束,会在结尾自动添加空字符。

get()方法提供了另外一种读取行的方式,该方法有几种变体。与getline不同的是get不再读取并丢弃换行符,而是将其保留在输入对列中。使用不带参数的get函数可以读取一个字符

getline使用起来更简单,get使得错误检查更简单。更多介绍请参考C++手册

string类简介

string使用来比数组简单,同时提供了将字符串作为一种数据类型的方法,更多介绍可参考C++手册

C++新增了另一种类型的原始字符串,原始字符串使用“(和)”来界定边界,并使用前缀R来标识原始字符串
在原始字符串中按回车键不仅会移动到下一行,还将在原始字符串中添加回车符
还可以在界定符”(或)”的两个字符之间添加相同的其他字符,来标识结尾,这样可以在原始字符串中添加)”这样的字符

1
2
3
4
cout << R"(nihao\n)" << endl;
cout << R"(nihao\n
)" << endl;
cout << R"a(nihao\n)a" << endl;

结构简介

结构是一种更加灵活的数据格式,同一个结构可以存储多种类型的数据。结构是用户自定义的类型,定义了类型后,便可以创建这种类型的变量

C++允许在声明结构变量时省略struct关键字

结构声明的位置很重要,可以将声明放在函数中,这样这个结构只能在本函数中使用。放在函数外的声明称为外部声明,外部声明可以被其后的任何函数使用

C++支持使用列表初始化的方式初始化结构变量,等号是可选的,如果大括号内为空,则各个成员被初始化为0,且不允许缩窄

结构变量可以进行赋值

可以声明结构数组,数组中每个元素都是该结构类型的变量

与C一样,C++允许指定占用特定位数的结构成员,字段的类型应为整型或枚举,接下来是冒号,后跟着一个数字,表示使用的位数。可以使用没有名字的字段来提供间距

1
2
3
4
5
6
7
8
struct torgle_register
{
unsigned int SN: 4;
unsigned int : 4;
bool flag: 1;
bool retry: 1;
};
torgle_register tr = {14, true, true};

共用体

共用体是一种数据格式,它能够存储不同的数据类型,但只能同时存储其中的一种类型,共用体的语法与结构类似,但含义不同

匿名共用体没有名称,其成员将成为位于同地址处的变量,每次只有一个成员是当前的成员

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
struct widget
{
int type;
union
{
long id_num;
char id_char[20];
};
};
widget price;
if (price.type)
{
cin >> price.id_num
}
else
{
cin >> price.id_char
}

枚举

enum工具提供了另一种创建符号常量的方式,这种方式可以代替const。

1
enum color {red, green, yellow};

这样color成为枚举类型,大括号中的名称作为符号常量,它们对应0-2,这些常量叫做枚举量

在不进行强制类型转换的情况下,只能将定义枚举时使用的枚举常量赋值给这中枚举变量

对于枚举只定义了赋值运算符。枚举是整型,可被提升为int类型,但int类型不能自动转换为枚举类型

如果试图将不适当的值强制类型转换赋值给枚举变量,结果将是不确定的

枚举更常被用来定义符号常量,而不是定义新类型,如果不创建枚举类型,则可以省略枚举类型的名称

枚举的值可以显式指定,red默认为0,后边没有初始化的将比前一个大1,orange为7

1
enum {red, green = 4, yellow = 6, orange};

每个枚举都有取值范围,通过强制类型转化,可以将取值范围内的任何整数值赋给枚举变量,即使这个值不是枚举值

取值范围定义如下,首先找出上限,需要知道枚举的最大值,找到大于这个最大值的,最小的2的幂,将它减去1,这个值就是范围的上限。要计算下限,需要知道枚举常量的最小值,如果它不小于0,则取值范围的下限为0;否则,采用寻找上限方式相同的方式,但加上负号。如果最小的枚举值是-6,而比它小的,最大的2的幂是-8(加上负号),因此下限为-7

指针和自由存储空间

指针

指针是一个变量,其存储的是值的地址

对于常规变量,在变量名前加地址运算符(&),就可以获得它的位置

cout在显示地址时,使用16进制表示法

使用常规变量时,值是指定的量,地址是派生量。指针策略刚好相反,将地址视为指定的量,而将值视为派生量。指针用于存储值的地址

*运算符被成为间接值或解除引用运算符,将其应用于指针,可以得到该地址处存储的值

C++中,int *是一种复合类型,是指向int的指针,也可以声明其他类型的指针

指针和指针指向的值是不同的概念,它们的长度可能不同

在指针声明中可以初始化指针,被初始化的是地址,而不是指针指向的值

使用为初始化的指针,可能导致严重的运行时错误。一定要在对指针应用解除引用运算符前,将指针初始化为一个确定的、适当的地址。这是关于使用指针的金科玉律

指针和整型是截然不同的类型,不能将int类型直接赋值给指针类型,而要通过强制类型转换来进行赋值

内存管理

指针的真用用武之地是在运行阶段分配未命名的内存以存储值。这种情况只能通过指针来访问内存。

在C语言中可以使用库函数malloc来分配内存,在C++中仍然可以这样做,但C++提供了更好的方法——new运算符

new运算符的使用格式如下:

1
type_name *pointer_name = new type_name;

需要再两个地方制定数据类型,用来指定需要什么类型的内存和声明什么类型的指针

new分配的内存块通常与常规变量声明分配的内存块不同,一个在堆中分配,一个在栈中分配

当内存使用完成时,使用delete运算符能够将其归还给内存池,归还后的内存可供程序的其他部分使用,一定要配对的使用new和delete,否则将发生内存泄露

不要尝试释放已经释放的内存块,这种结果是不确定的,不能使用delete来释放声明变量获得的内存,对空指针使用delete是安全的

在编译时给数组分配内存称为静态联编,意味着数组是在编译时加入程序的,这种必须在编译时指定数组的长度。与之对应,数组在程序运行时创建被称为动态联编,这种情况程序在运行时确定数组的长度

使用下边的来创建和释放动态数组:

1
2
int *arr = new int[10];
delete [] arr;

new运算符返回第一个数组元素的地址,不同于普通变量,应使用delete []来释放整个数组

对于使用new和delete时,应注意:

  • 不要使用delete来释放不是new分配的内存
  • 不要使用delete来释放同一个内存块两次
  • 如果使用new []为数组分配内存,则应使用delete []来释放
  • 如果使用new来为一个实体分配内存,则应使用delete来释放
  • 对空指针使用delete是安全的

使用动态数组只需要把返回的指针当做数组名来使用即可,在C和C++内部使用指针来处理数组

不能修改数组名的值,但动态数组的指针是变量可以修改它的值

指针、数组和指针算术

指针和数组基本等价的原因在于指针算术和C++内部处理数组的方式

不同于整数算术,对指针来说,增加1相当于增加它指向类型的字节数,两个指针相减,得到的是他们之间的间隔(同一个数组中才有意义)

对于数组表达式arr[i],编译器将该表达式看做*(arr + i),如果使用指针,也进行这样的转换

对数组名使用sizeof得到的是数组的长度,对数组指针应用sizeof得到的是指针的长度

对数组名取地址得到的是数组的地址,数组名实际上代表数组中第一个元素的地址

1
2
int arr[10];
int (*p_arr)[10] = &arr;

在cout和多数C++表达式中,char数组名,char指针,以及用引号括起来的字符串常量都被解释为字符串的第一个字符的地址

对于字符串常量,不应尝试修改它

应使用strcpy而不是赋值运算符来将字符串赋给数组

对于结构指针,应使用箭头成员运算符(->)来访问结构成员。另一种方法是(*p)->b

可以将new和delete放在不同的函数中,但这容易遗忘使用delete

C++有三种管理数据内存的方式:自动存储、静态存储和动态存储。C++ 11新增了线程存储

在函数内部定义的常规变量使用自动存储空间,被称为自动变量。这表明它们在所属的函数被调用时自动产生,在函数结束时消亡。自动变量时一个局部变量,其作用于为包含它的代码块。代码块是被包含在花括号中的一段代码,自动变量通常存储在栈中

静态存储是在整个程序运行期间都存在的存储方式,有两种方式定义静态变量:在函数外定义它和在声明变量时使用个关键字static

new和delete提供了比自动存储和静态存储更加灵活的方法。数据的声明周期不完全受程序或函数的生存时间控制,动态分配的变量通常存储在堆中

数组、结构和指针,可以使用各种方式组合它们

模板类vector和array(C++ 11)提供了动态数组和定长数组的替代品

vector可以在运行阶段设置vector对象的长度,它使用new和delete来管理内存

vector的功能比数组强大,但付出的代价是效率稍低。array提供了固定长度的数组,使用栈来存储数据,其更方便和安全

可以像使用普通数组一样使用vector和array,vector和array对象可以使用赋值,但数组不行。

vector和array可以使用at方法来确保不使用非法索引,这将在运行时捕获错误,程序默认将中断