C++Primer第五版【阅读笔记】
C++Primer第五版 阅读笔记
- 第1章开始
- 1.1 编写一个简单的C++程序
- 1.1.1 编译、运行程序
- 1.2 初识输入输出
- 1.3 注释简介
- 1.4 控制流
- 1.4.1 while语句
- 1.4.2 for语句
- 1.4.3 读取数量不定的输入数据
- 1.4.4 if语句
- 1.5 类简介
- 1.5.1 Sales_item 类
- 1.5.2 初识成员函数
- 1.6 书店程序
- 第一章小结
- 第2章 变量和基本类型
- 2.1 基本内置类型
- 算术类型
- 2.1.2 类型转换
- 2.1.3 字面值常量
- 2.2 变量
- 2.2.1 变量定义
- 2.2.2 变量声明和定义的关系
- 2.2.3 标识符
- 2.2.4 名字的作用域
- 第3章 字符串、向量、数组
- 第10章 泛型算法
- 10.1 概述
- 10.2 初识泛型算法
第1章开始
学习一门新的程序设计语言的最好方法就是练习编写程序。
1.1 编写一个简单的C++程序
每个C++程序都包含一个或多个函数,其中一个必须命名为 main,操作系统通过调用 main 来运行C++程序。
一个函数定义包括:
- 返回类型。
- 函数名。
- 形参列表。
- 函数体:以 { 开始 ,以 } 结束的语句块。
main 函数的返回类型必须为 int。
return 语句包括一个值时,返回值类型必须与函数的返回类型相容。
类型相容:类型相同或能够进行隐式转换。
main 返回值被用来指示状态,0表示成功,非0的含义由系统定义,通常用来指出错误类型。
类型决定:
- 内容。
- 运算。
- 内存空间。
- 数据的意义。
1.1.1 编译、运行程序
常见源文件命名约定:.cc、.cxx、.cpp、.cp及.C。
操作系统 / 编译器 | 编译 | 运行 | 访问 main 的返回值 | 备注 |
---|---|---|---|---|
UNIX | CC prog1.cc 生成可执行文件 a.out | a.out 或者 ./a.out (指出该文件在当前目录) | echo $? | |
Windows | CC prog1.cc 生成可执行文件 prog1.exe | prog1 或者 .\prog1 (指出该文件在当前目录) | $ echo %ERRORLEVEL% | |
GNU | g++ -o prog1 prog1.cc 生成可执行文件 prog1 | ./prog1 (指出该文件在当前目录) | -o 指定可执行文件的文件名。UNIX生成 prog1 。Windows生成 prog1.exe 。省略 -o prog1 UNIX 系统生成 a.out 可执行文件。Windows 系统生成 a.exe 可执行文件。使用GNU编译器需要指定 -std=c++0x 参数来打开对C++11的支持。 | |
Visual Studio | cl /Ehsc prog1.cpp 自动生成可执行文件 prog1.exe | prog1 或 .\prog1 或 .\prog1.exe (指出该文件在当前目录) | 命令 cl 调用编译器。/Ehsc 是编译器选项,用来打开标准异常处理。生成可执行文件名字与第一个源文件名对应,后缀为.exe。 |
GUN编译器选项 -Wall 使用说明:生成所有警告信息。
Visual Studio 编译器 /W4 使用说明:开启编译器 4级 警告信息。详细见 Microsoft C/C++ 编译器和生成工具错误与警告 官方文档。
点击此处进入:Microsoft C/C++ 编译器和生成工具错误与警告官方文档
1.2 初识输入输出
C / C++ 语言未定义任何输入输出语句,而是使用标准库提供IO机制(个人理解是为了修改、拓展和升级方便)。
iostream库包含:
- 流输入类型:istream
- 流输出类型:ostream
标准库定义的4个IO库:
- 标准输入:cin
- 标准输出:cout
- 标准错误:cerr(输出警告和错误信息)
- 输出程序运行时的一般性信息:clog(可以理解为常规日志输出)
每个使用标准库设施的程序都必须包含相关的头文件。
#include指令和头文件的名字必须写在同一 行中。
通常情况下,#include指令必须出现在所有函数之外。
一般将一个程序的所有#include指令都放在源文件的开始位置。
C++中,一个表达式产生一个计算结果。
表达式:一个或多个运算对象和(通常是)一个运算符组成。
cout 中使用的 << 运算符 和 cin 中使用的 >> 运算符均返回运算符左侧对象,使得cout 和 cin 支持链式操作。
字符串字面值常量:一对双引号包围的字符序列。
endl:
- 操作符。
- 结束当前行。
- 刷新缓冲区。
注意:调试程序时添加打印语句应该保证一直刷新缓冲区,否则会出现程序崩溃时输出还留在缓冲区,影响程序崩溃位置的判断。
命名空间的作用:避免不经意的名字冲突。
标准库定义的所有名字都在命名空间 std
中。
命名空间的使用推荐:
- 建议使用(不释放命名空间):
std::cout<<"hello world"<<endl;
- 可以使用(释放部分命名空间):
using std::cout;
cout<<"hello world"<<endl;
- 尽量少用(释放整个命名空间):
using std;
cout<<"hello world"<<endl;
初始化:创建变量的同时赋值。
1.3 注释简介
错误的注释比完全没有注释更糟糕,因为它会误导读者。
C++注释种类:
- 单行注释:
//注释内容
- 多行注释:
//注释内容
//注释内容
//注释内容
- 多行注释(注释界定符不能嵌套):
/*注释内容*/
1.4 控制流
1.4.1 while语句
while(condition)
statement
执行过程:
语句块:花括号包围的零条或多条语句的序列。任何要求使用语句的地方都可以使用语句块。
1.4.2 for语句
for(init-statement ; condition ; expression)
statement
执行过程:
1.4.3 读取数量不定的输入数据
源码演示:
#include <iostream>
int main()
{
int sum = 0, value = 0;
while (std::cin >> value)
sum += value;
std::cout << "Sum is: " << sum << std::endl;
return 0;
}
运行结果:
istream 对象作为条件时,检测流状态。
如果有效,条件为真。
如果无效,条件为假。
条件为假:
- 遇到文件结束符。
- 遇到一个无效输入。
从键盘输入文件结束符:
- Windows:Ctrl + Z 或 Ctrl + D , 然后按Enter。
- UNIX(包括 Mac OS):Ctrl + D。
常见编译器可以检查出来的错误:
- 语法错误。
- 类型错误。
- 声明错误(C++程序中的名字要求先声明后使用。)。
修改错误:
- 按照报告顺序逐个修正,单个错误常常具有传递效应。
- 每修正一个错误之后立即重新编译代码,保持周期:编辑 - 编译 -调试。
1.4.4 if语句
if(condition_1)
statement_1
else if(condition_2)
statement_2
……
else if(condition_n)
statement_n
……
else
statement_other
执行过程:
注意:C++使用 =
赋值,使用 ==
作为相等运算符。两个运算符都可以出现在条件中。
常见错误:想在条件中使用 ==
,误用了 =
。
C++程序的缩进和格式:
- 不存在唯一正确的风格,但保持一致性非常重要。
- 思考风格对程序可读性和易理解性有什么影响,一旦选择了一种风格就要坚持使用。
1.5 类简介
C++中通过类定义自己的数据结构。
一个类定义了一个类型,以及与其关联的一组操作。
C++最初的一个设计焦点就是能定义使用上像内置类型一样自然的类类型。
使用头文件访问为自己应用程序所定义的类。
习惯上头文件根据定义类的名字来命名。
通常使用 .h 作为头文件后缀。
1.5.1 Sales_item 类
使用类时,不关心如何实现,只关心类对象可以执行什么操作。
每个类定义一个新的类型,类型名就是类名。
类的作者定义了类对象可以执行的所有动作。
包含来自标准库的头文件使用 <> 包围。
包含来自不属于标准库的头文件使用 " " 包围。
使用文件重定向:
大多数系统支持文件重定向,可以将标准输入输出与文件命名相关联:
todo
1.5.2 初识成员函数
成员函数:定义为类的一部分的函数,也被称为方法。
类对象使用点运算符 . 调用成员函数, . 运算符只能用于类类型的对象。
类对象 . 该类对象的成员名
运算结果为右侧运算对象指定的成员。
1.6 书店程序
第一章小结
- 编译、运行简单的C++程序。
- main函数相关。
- 定义变量,输入输出。
- if语句、for语句和while语句。
- 类的特性,创建、使用类对象。
第2章 变量和基本类型
基本语法特征:
- 整型、字符等内置类型。
- 变量。
- 表达式和语句。
- if 或者 while 等控制结构。
- 函数。
补充基本特征:
- 自定义数据类型。
- 标准库。
对象类型决定对象能够进行的操作。
表达式是否合法取决于参与运算对象的类型。(确定该类型是否支持该运算)。
运行时检查数据类型:Smalltalk 和 Python等。
编译时检查数据类型:C++。(编译器必须知道程序中每个变量对应的数据类型。)
C++语言的基本形态:
- 内置数据类型及相应的运算符。
- 程序流控制语句。
强大的能力显示于对自定义数据类型的支持,通过自定义数据结构来使语言满足需求。
C++新类型包含:
- 数据成员。
- 函数成员。
C++主要设计目标之一:让自定义数据类型像内置类型一样好用。
C++支持广泛的数据类型:
- 基本内置类型。
- 自定义数据类型。
数据类型决定:
- 数据的意义。
- 数据的取值范围。
- 数据能够进行的操作。
- 数据所占内存空间大小。
- 数据的布局方式。
2.1 基本内置类型
- 算术类型:字符、整型数、布尔值、浮点数。
- 空类型(void):不对应具体值,仅用于一些特殊场合(例如:当函数不返回任何值时,使用空类型作为返回类型)。
算术类型
- 整型。
- 浮点型。
整型:
- 字符。
- 布尔类型。
- 浮点型。
浮点型:
- 单精度。
- 双精度。
C++标准规定的类型尺寸的最小值:
布尔类型 (bool) 的取值为真(true)或 假 (false)。
字符类型:
- char:基本字符类型 ,一个 char 空间可以存放机器基本字符集中的任意字符对应的数值。
- wchar_t:可以存放机器最大扩展字符集中的任意字符。
- char16_t:为 Unicoode 字符集服务。(Unicoode 字符集可用于表示所有自然语言中字符的标准。)
- char32_t:为 Unicoode 字符集服务。
C++规定:
- 一个 int 至少和一个 short 一样大。
- 一个 long 至少和一个 int 一样大。
- 一个 long long(C++11中新定义的) 至少和一个 long一样大。
计算机以 0 和 1 组成的 bit 序列存储数据。
可寻址的最小单元:字节(Byte)。
大多数机器:1Byte = 8bit
C++中一个字节至少要容纳机器基本字符集中的任意字符。
存储的基本单元:字(word)。
字由几个字节组成,大多数为4字节或8字节。
明确内存中某个地址的含义需要知道:地址 + 类型。
浮点类型:
- 单精度:通常为1Byte,7位有效数字。
- 双精度:通常为2Byte,16位有效数字。
- 扩展精度:通常为3或4Byte,硬件具体实现不同,精度不同。
带符号类型和无符号类型:
- bool 和 扩展字符不划分。
- 带符号:正数、负数和 0 。
- 无符号:正数和 0。
- int、short、long 和 long long 是带符号的,类型名前面加上 unsigned 得到无符号类型。
- unsigned int 可以缩写为 unsigned。
- 字符被分为三种:char、unsigned char 和 signed char,实际表现只有两种:带符号的和无符号的。具体那种由编译器决定。
- 无符号类型所有 bit 用来存储值。
- 带符号类型正值和负值理论上应该保持平衡。8 bit signed char 表范围为 [-128, 127]
如何选择类型:
- 明确数值不可能为负时,选用无符号数。
- 使用 int 执行整数运算,超过 int 使用 long long。
- 算术表达式中不要使用 bool 或 char ,只有存放布尔值和字符的时候使用它们。
- 执行浮点数运算选用double,float 常常精度不够且双精度浮点数和单精度浮点数计算代价相差无几。
- long double 一般情况下没有必须使用,带来的运行时消耗不容忽视。
2.1.2 类型转换
类型所能表示的值的范围决定了转换过程:
- 非bool 的算术值赋给 bool:初始值 0 则为 true,否则为 false。
- bool 的算术值赋给 非bool:初始值 false 则为 0,否则为 1。
- 浮点数 赋值给 整型:结果仅仅保留浮点数中小数点之前的部分。
- 整型 赋值给 浮点数:小数部分记为 0 (整数所占空间超过浮点数类型容量,精度有可能损失)。
- 超出范围的值 赋值给 无符号类型:初始值对无符号类型表示数值总数取模后的余数。
- 超出范围的值 赋值给 有符号数:未定以行为。
建议
- 避免未定义行为。
- 编码时考虑程序的可移植性。
int 作为判断条件:取值0返回 false,其他取值返回 true。
布尔值用在算术表达式中:取值为 0 或者 1。(注意:不要在算术表达式中使用 bool 值。)
当一个算术表达式中既有无符号数又有 int 时,int 转换为无符号数(过程同 int 赋值给 无符号数)。
代码演示:
#include <iostream>
using namespace std;
int main()
{
unsigned u = 10;
int i = -42;
cout << i + i << endl; //-42 + (-42)
cout << u + i << endl; //4294967296 - 42 = 4294967254 + 10 = 4294967264
return 0;
}
运行结果:
无符号数和有符号符号数参与的算术运算必须确保结果为非负数:
代码演示:
#include <iostream>
using namespace std;
int main()
{
unsigned u = 1;
int i1 = -1;
int i2 = -42;
cout << i1 * u << endl; //4294967296 - 1 = 4294967295 * 1 = 4294967295
cout << i1 - u << endl; //4294967296 - 1 = 4294967295 - 1 = 4294967294
cout << i2 / u << endl; //4294967296 - 42 = 4294967254 / 1 = 4294967254
cout << i2 + u << endl; //4294967296 - 42 = 4294967254 + 1 = 4294967255
return 0;
}
运行结果:
2.1.3 字面值常量
每个字面值常量都对应一种数据类型。
字面值常量的形式和值决定了它的数据类型。
整型常量:
- 十进制:20,默认情况带符号:int 、long、long long 中选择能够容纳的尺寸最小的。
- 十进制字面值不会是负数,符号取值不在字面值内,只是对字面值去负值。
- 八进制:024,可能带符号也可能不带符号:int 、unsigned int、long、unsigned long 、long long 和 unsigned long long 中选择能够容纳的尺寸最小的。
- 十六进制:0x14,可能带符号也可能不带符号:int 、unsigned int、long、unsigned long 、long long 和 unsigned long long 中选择能够容纳的尺寸最小的。
- 最大数据类型无法存储字面值时,发生错误。
- short 类型没有对应字面值。
浮点型常量:
- 小数表示:3.1415926、0.、.001。
- 科学计数法表示,指数部分用 E 或 e:3.14159E0,0e0。
- 默认浮点型字面值为 double 类型。
字符型常量:
- 单引号括起来的字符:‘A’。
字符串常量:
- 双引号括起来的 0 个 或 多个字符。
- 本质是由常量字符构成的数组。
- 每一个字符串结尾出添加一个空字符 ‘/0’ ,字符串字面值的实际长度比内容多1。
- 如果两个字符串字面值位置紧邻且只由空格、缩进和换行符分隔,则实际上是一个整体。
转义字符:
程序员不能直接使用的字符:
- 不可打印字符(退格和其他控制字符) 。
- C++语言中有特殊含义的字符(单引号、双引号、问号、反斜杠)。
转义字符:
转移字符被当作一个字符使用。
泛化的转义字符:
- \x 后面紧跟着一个或多个十六进制数字,数字部分表示字符对应的数值,所有数字与 \ 构成转义字符。
- \ 后面紧跟着1和、2个或3个八进制数字,数字部分表示字符对应的数值,数字超过3个只有前三个与 \ 构成转义字符。
泛化的转义字符:
指定字面值的类型:
长整型字面值使用 L,不要使用 l。
指定字面值类型:
- 后缀有U:无符号类型,从 unsigned int、unsigned long 、unsigned long long 中匹配能够容纳的空间最小的。
- 后缀有L:字面值类型至少是 long。
- 后缀有LL:字面值类型至少是 LL。
- 可以将 U 和 L或 LL 放在一起使用。
布尔字面值:
- true。
- false。
指针字面值:nullptr。
2.2 变量
变量:具名的、可供程序操作的存储空间。
2.2.1 变量定义
[类型说明符] [变量名],[变量名],[变量名] = [初始值],[变量名] = [初始值],…[变量名];
对象被初始化:创建对象时获取一个特定的值。
可以使用复杂的表达式初始化变量的值。
代码演示:同一条定义语句中,可以用先定义的变量值去初始化后定义的其他变量。
double price = 109.99, discount = prince * 0.16; //使用先定义的变量初始化后定义的变量
double salePrice = applyDiscount(price, discount); //使用函数返回值初始化变量
C++中赋值和初始值是两个完全不同的操作:
- 初始化:创建变量时赋予其一个初始值。
- 赋值:把对象当前值擦除,以一个新值来替代。
C++11标准:使用列表初始化来初始化变量得到全面应用。
初始化列表用于内置类型变量时:如果初始值存在丢失信息的风险,编译器将报错。
long double ld = 3.1415926;
int a{ ld }, b = { ld }; //错误:转换未成功,因为存在丢失信息的风险
int c(ld), d = ld; //正确:转换执行,丢失部分值
内置类型默认初始化:
- 定义于函数体内:未定义。
- 定义于函数体外:初始化为 0 。
类对象决定:
- 默认初始化的方式。
- 是否不经过初始化就定义对象。
作为一种简单可靠的方法:初始化每一个内置类型的变量。
2.2.2 变量声明和定义的关系
分离式编译:将程序分割为若干个文件,每个文件独立编译。
声明:
- 使得名字为程序所知,一个文件想使用别处定义的名字则必须包含对那个名字的声明。
- 变量的类型和名字和定义相同。
- 声明而非定义一个变量:
extern int i
- 变量可以被多次声明。
定义:
- 负责创建与名字关联的实体。
- 申请内存空间,也可能为变量初始化。
- 包含初始化的声明是定义:
extern int i = 10
- 函数内部试图初始化一个由 extern 关键字标记的变量,将引发错误:不能对带有块范围的外部变量进行初始化。
- 变量只能被定义一次。
多文件使用同一个变量:
声明和定义分离,变量定义必须出现且只能出现在一个文件中,其他用到该变量的文件必须对其进行声明,却绝不能重复定义。
C++是一种静态类型语言:编译阶段检查类型。
对象类型决定所能参与的运算,试图执行类型不支持的运算,编译器将报错并不会生成可执行文件。
2.2.3 标识符
标识符规定:
- 字母、数字和下划线组成。
- 不以数字开头。
- 大小写敏感。
- 长度没有限制。
- 不能使用关键字。
- 不能连续出现两个下划线。
- 不能以下划线大写字母开头。
- 函数体外的标识符不能以下划线开头。
变量的命名规范:
- 标识符要能体现实际含义。
- 变量名一般用小写字母(index)。
- 用户自定义类名一般以大写字母开头(Sales_item)。
- 标识符由对各单词组成,单词间要有明显区分(student_loan,studentLoan)。
2.2.4 名字的作用域
名字的有效区域:始于名字的声明语句,以声明语句所在的作用域末端结束。
#include <iostream>
using namespace std;
int main() //main函数:全局作用域。声明后整个程序范围内可使用。
{
int sum = 0;//sum:块作用域。声明处开始。
for (int val = 0; val <= 10; ++val) //val:块作用域。声明出开始。
{
sum += val;
}//val 作用域结束
cout << "sum:" << sum << endl;
return 0;
}//sum 作用域结束
第一次使用变量时定义它:
- 有助于找到变量的定义。
- 赋予一个合理的初始值。
作用域嵌套:
#include <iostream>
using namespace std;
int main()
{
{
//外层作用域开始
{
//内层作用域开始
}//内层作用域结束
}//外层作用域结束
return 0;
}
作用域中声明了变量,嵌套的所有作用域中都能访问该名字。
允许内层作用域中重新定义外层作用域已有的名字。
代码演示:
#include <iostream>
using namespace std;
int reused = 42; //全局作用域
int main()
{
int unique = 0; //块作用域
cout << reused << " " << unique << endl; //42 0 输出全局变量 reused 和局部变量 unique
int reused = 0; //新建局部变量覆盖全局变量
cout << reused << " " << unique << endl; //0 0 输出局部变量 reused 和局部变量 unique
cout << ::reused << " " << unique << endl; //42 0 //显式访问全局变量 reused 和局部变量 unique
return 0;
}
运行结果:
如果函数有可能用到某全局变量,则不宜再定义一个同名的局部变量。
第3章 字符串、向量、数组
第10章 泛型算法
独立于 容器类型 和 元素类型 的通用算法。
10.1 概述
算法不直接操作容器,而是遍历两个迭代器指定的元素范围进行操作。
代码演示:在 vector 中查找元素。
#include <iostream>
#include <vector>
using namespace std;
int main()
{
int val = 5;
vector<int> vi = { 1,2,2,2,2,3,4,5,6,6,7,8,9 };
int array_i[] = { 1,2,2,2,2,3,4,5,6,6,7,8,9 };
auto result1 = find(vi.begin(), vi.end(), val);
cout << "The value1 " << val << (result1 == vi.end() ? " is no present" : " is present") << endl;
auto result2 = find(vi.begin() + 1, vi.begin() + 4, val);
cout << "The value2 " << val << (result2 == vi.end() ? " is no present" : " is present") << endl;
auto result3 = find(begin(array_i), end(array_i), val);
cout << "The value3 " << val << (result3 == end(array_i) ? " is no present" : " is present") << endl;
auto result4 = find(begin(array_i) + 1, begin(array_i) + 6, val);
cout << "The value4 " << val << (result4 == begin(array_i) + 6 ? " is no present" : " is present") << endl;
val = 2;
cout << count(vi.begin(), vi.end(), val) << endl;
val = 6;
cout << count(begin(array_i), end(array_i), val) << endl;
return 0;
}
运行结果:todo
find 返回第一个等于给定元素的迭代器,搜索失败返回第二个参数。
可以用将上述 find 操作应用于所有容器中进行查找操作。
泛型算法:不会改变底层容器的大小,可能改变容器内元素值或在容器内移动元素,但永远不会添加和删除元素。
算法可以操作插入器迭代器进行底层容器的插入操作,但算法自身永远不会做这样的操作。todo