# 课程资料
- Video
- Slides/Code
- Course Reader
- Assignment
- AP1401-2
- Spring 2021 资料
- 综合资料
# 学习流程
for(int i = 0; i < 17; i++) { | |
1. 阅读 Course Reader对应章节 | |
2. 观看video | |
3. 阅读Slides | |
4. 整理Code | |
} |
以上流程完成后:
- 完成 CS106L 所有 Assignment
- 完成 AP1401-2 所有作业
# Welcome
本节课主要讲述了 C++ 的应用前景,历史发展和设计哲学。
# C++ 应用前景
# C++ 的历史
# 汇编语言
在早期阶段,尚没有高级语言这一说。程序员大多使用汇编语言编写程序,汇编语言的好处在于:
- 使用较为简单的指令进行编程
- 汇编语言执行速度较快
- 程序员可以直接操作计算机底层寄存器等
但是,汇编语言编程也有它的缺陷,缺陷就在于:
- 程序涉及到对计算机底层硬件的基础操作,而不只是处理逻辑,因此对其他程序员来说,阅读起来较为困难
- 因为汇编语言涉及到指令集架构,而指令集架构和计算机底层硬件和操作系统紧密相关,因此在一台机器上运行的程序可能无法迁移到另一台程序,简单来说就是可移植性较差
- 汇编语言编写的程序因为使用的是一些基本的操作,因此程序较长
因此, Ken Thompson
和 Dennis Ritchie
于 1972 年发明了 C 语言。
# C 语言
C 语言是一门高级语言,相较汇编,它的优势在于:
- 面向过程编程,较为简单。程序员在编写程序时,无需考虑计算机底层架构,而只需要考虑处理逻辑,因此编程较为简单
- C 语言可以由编译器编译为汇编指令,在不同的机器上,可以编译出不同的汇编指令,而后汇编器又可以将汇编指令转化为针对该计算机指令集架构的机器指令,实现 C 语言的可迁移性
- C 语言程序执行速度非常快
然而,在面对更复杂的编程问题时,C 语言也表现出了它的不足:
- C 语言是面向过程的语言,它无法面向对象。当我们需要更复杂的结构和它的一系列方法时,C 语言只为我们提供了一些有限的结构,无法满足我们对高级结构的需求
- C 语言无法对不同类型提供一个泛化的模板,对于不同类型的传入参数,我们可能需要重复写多个几乎一致的处理函数
- 写大型项目时,很多时候很难将一个问题拆解为一个面向过程的模型,不是所有问题都可以使用模块化的过程方法解决
- 写出来的程序较长
# C++
针对 C 语言的问题, Bjarne Stroustrup
于 1983 年开发了 C++ 语言,他希望能够在 C 语言基础上实现一个具有多种不同特性的高级语言。 C++ 语言一开始只是 C with classes
,实现了 C 语言面向对象的延伸。而后逐步发展,直到今天的 C++23
。
# C++ 的几大特性
- 通用语言
有的语言可以在应用到多个场景中,但是在解决特定场景问题时会显得复杂,比如 C++ 在做矩阵乘除法时,需要程序员手动编写程序,效率较低。但是 C++ 的用途很广。而有的语言,可以解决特定问题,但是并不泛用。比如 Matlab 在做科学计算时非常的常用,但是在解决其他问题时并没有 C++ 高效。 - 编译型语言
高级语言需要转换成机器可以阅读的二进制码才能被计算机执行。而高级语言分为编译型和解释型。其区别在于,解释型语言使用解释器 (Interpreters) 进行翻译,一边翻译一边执行。解释器在执行一条语句的同时,获取下一条语句。而编译型语言使用编译器进行编译。将整个源代码编译完成后,直接执行生成的二进制码。 - 静态类型语言
静态类型语言是指语句中的每个变量在声明后都有固定的类型,一旦确定,不能随意更改。而动态类型(如 Python、Javascript)会在执行的过程中动态的判断变量的类型。静态类型语言会在编译阶段检查语句是否合法,否则产生编译错误。而动态类型通常无法在编译阶段确定该语句是否有编译错误,错误均在程序运行时产生,也称为运行时错误。编译阶段排错让运行时错误出现的概率大大降低。这样无需运行即可排除程序错误。 - 多范式语言
部分语言只有单一范式,如 C 语言,无法编写面向对象程序。而 C++ 可以同时实现面向对象特性,泛型特性,和面向过程的特性。非常灵活。 - 中间语言
部分底层语言(如汇编)直接和计算机内存打交道,但是利用其写出的程序逻辑不清晰,难以理解。而部分语言无法直接对计算机底层进行操纵(如 Python、Java) 等,程序员在编写程序时就像是被禁锢,无法探索底层的奥秘。C++ 可以像 C 语言那样接触底层硬件(利用指针),也可以利用其面向对象特性构造大型程序,同时实现封装和抽象。触及底层系统和实现抽象的目的同时达到,非常便于程序员大展身手。
# C++ 的设计哲学
- 只有在需要解决特定问题时引入新的特性
- 程序员可以自由选择编程风格
- 隐藏实现细节,抽象出编程接口
- 让程序员能够完全以自己想要的方式编写程序
- 让编写的程序尽可能高效
- 编译时进行类型检查
- 可以兼容早期版本程序,也兼容 C 语言程序
# C++ 的应用
- 浏览器
- JVM
- 火星探索车
- 等等
# Type and Structs
关于类型和结构体没什么好说的,主要的用法和 C 语言中差不多。但是 string
类在使用之前需要在程序最开始处 #include<string>
,并且最好是不要使用 using namespace std
,而是使用 std::string
,对于 std::cout
和 std::cin
也是一样。这样做是为了保证程序员在自主开发时,不和 std
域内的的东西重名导致出错。
# std::pair
一种 STL
内置结构,其中包括两个域。 std::pair
相当于是个模板,其中域的类型随意。声明时使用如下格式:
std::pair<int,string> p = {1,"st"}; |
此外,还可以在程序中使用如下方法构建 std::pair
:
std::pair<int,string> p = std::make_pair(1,"st"); |
在使用 pair
时,分别用 p.first
和 p.second
来引用两个域。
# auto
类型推导
使用 auto
变量表示允许编译器自行推导值的类型。
什么时候使用 auto
?
- 使用迭代器时,我不关心值的类型
- 使用模板时,值的类型已经可以根据上下文推断出来
- 使用
lambda
时,咱不知道值是啥类型 - 没那个必要时,尽量不要将
auto
作为返回值类型
# Streams
How can we convert between string-represented data and the real thing? Streams!
本节首先介绍了什么是环境,而后引入 Stream
的概念,讲解了 Streams
在读入和写出数据时的一些特点。而 Streams
可以利用 cin
和 cout
这两种 iostream
类的对象,实现从标准输入和控制台进行读取。也可以通过 ifstream
和 ofstream
两种来实现文件的读取和写入。亦可以通过 istringstrean
和 ostringstream
来实现字符串和其他类型之间的连接。但是普通的 cin
和 cout
在使用时也可能出现读取的问题,因此我们还可以使用 std::getline()
来进行一行一次的读取。此外,在使用 Stream
时,还应该注意判别读取异常和写入异常。
# Streams Overview
# Environment
在学习 Stream 之前,我们先要了解 Environment
(环境)的概念。我们家里有温度计,我们通常通过温度计上的水银球去检测环境温度,然后将摄氏度显示在数轴上,人们通过观测数轴上的数字来查看当前温度。在这个例子中,外界就是环境,水银球就是将温度转化为实际示数的媒介,而数轴就是温度的输出,将温度显示出来供人们了解。而在程序编写的过程中,也可能存在一个外部环境,程序需要从这个环境获取信息,然后在利用一些处理逻辑来进行一些计算,最后返回输出或者将输出打印在屏幕上。这个环境可能是用户输入,也可能是外部文件,还有可能是其他程序。
# Stream 是什么
Streams is an abstraction for input and output. Streams convert between data and the string representation of data.
Stream 是程序与外部环境交流的媒介。Stream 的输入与输出可能来自用户,也可能来自程序,也可能来自其他文件。如果要将一个变量输出到终端,那么变量就会以字符串的形式打入 Stream,然后 Stream 将其输出到终端。如果要从用户输入读取数据,那么也是将用户输入转化为字符串存储在 Stream 中,然后再将其转储到变量中。
# Stream 特点
- 可以对大体积数据进行分片读取,然后存储
- 可以读取多个类型的数据
- 可以串联多个
<<
读取
# cin 与 cout:来自键盘,去往终端
cout
为 Stream 对象,它从变量中获取数据,存储到一个 Buffer 中,然后将其输出到终端上。
cin
也是 Stream 对象,它从用户输入获取值,存储到一个 Buffer 中,然后将其转储到对应类型的变量中。
在使用这两个输入流前,需要在程序开始处 #include <iostream>
# ifstream 与 ofstream:来自文件,去往文件
问题思考:
ifstream
和ofstream
分别是什么?- 还有什么特殊的读写文件类?特殊在哪?
i/ofstream
和cin/cout
在使用上有什么不一样之处?- 使用
ifstream
和ofstream
需要包含什么头文件? ifstream
怎么初始化?初始化后需要做哪些检查?ofstream
怎么初始化?初始化后需要做哪些检查?- 如果传入的文件名是
string
类型,如何处理? close()
时有哪些需要注意的地方?
# ifstream & ofstream Overview
ifstream
和 ofstream
分别从文件读取和写入文件。此外,还有一个叫做 fstream
的类型,即可以完成写入,又可以完成读取)。此外, ifstream
和 ofstream
在使用上与 cin/cout
不一样之处在于, ifstream
和 ofstream
是一个类型,而不是一个对象,但是 cin
及 cout
分别是 std::istream
和 std::ostream
类的对象。在使用对象的方法时 ( <<
和 >>
已被重载) 可以直接调用,但是在使用一个类的方法时,首先需要初始化这个类的一个对象,然后再对其方法进行调用。
# ifstream & ofstream 使用说明
# 包含头文件
#include <fstream> |
# ifstream 初始化 & 使用
可以直接使用构造函数,在参数列表中填入文件名进行初始化:
ifstream myStream("file.txt"); |
也可以在使用默认构造函数初始化后,利用类的 open()
方法打开文件:
ifstream myStream; | |
myStream.open("file.txt"); |
使用 ifstream
对象的方法与使用 cin
相似,如下:
myStream >> myInteger |
注意,在 open()
方法调用后,推荐使用 myStream.is_open()
来探测是否真的成功打开了文件。
# ofstream 初始化 & 使用
ofstream
初始化过程及使用过程与前文 ifstream
相似。若文件不存在,调用 open()
方法会新创建一个文件,否则会覆盖原有的同名文件。(所以尽量做好备份)
# 关闭流: close()
- 当流的生命周期结束时,C++ 会为你自动关闭流
- 你也可以手动使用
close()
方法关闭流
# 使用 string 作为文件名时…
注意, string
类的开发时间要晚于 ifstream
和 ofstream
,彼时 ifstream
和 ofstream
只接受 C 语言的字符串类型。因此,要将一个 string
类型的文件名传入这两个类的对象,我们必须调用 .c_str()
来将其转化为 C 语言格式的字符串。
# Stream manipulators
stream manipulator
可以让对变量及输出的处理更加方便,程序员无需手动编程实现一些较为繁琐的功能。几种常用的 stream manipulator
如下:
endl
: 输出后换行setw
: 设置输出的宽度left/right
: 通常与setw
连用,表示左补空格 / 右补空格setfill
: 在宽度一定,文字没有填满处补充特定的占位符boolalpha
: 用true/false
表示1/0
hex
: 将输入输出理解为 16 进制dec
: 输入输出为 10 进制oct
: 输入输出为 8 进制ws
: 跳过所有的空格
# Stream 异常处理
在使用 stream
进行读取时,可能会出现读取异常的情况,比如读进来的值是个字符串,但是程序想把它保存到一个 int
类中,这样就产生了类型异常。我们需要在读取后使用 cout.fail()
或 cin.fail()
来判断读取是否成功,如果有异常,我们需要手动处理异常,然后用 cin.clear()
表达异常已经处理完毕。
注意,在使用 while
循环进行读取时,尽量把异常判断放在 while
循环内,如果判断到异常则退出,否则就进行下一次读取。不要把 while
循环放在条件判断中,否则可能会导致读取异常后依然在进行输出,因此尽量使用如下的结构。
while(true) { | |
... | |
if(cin.fail()) | |
break; | |
... | |
} |
因为 stream
在读取到最后或读取错误时会返回 false
,而其他情况下会返回 stream
对象本身(也可以判断为 true
),因此我们可以使用 cin << intValue << doubleValue
之类的语句作为判断的条件,以简化上述的循环,结构如下:
while(cin << intValue << doubleVALUE) { | |
... | |
... | |
} |
# Stream 的麻烦之处
Stream 存在一个问题,如果用户多次连续读取值,而其中某一次读入的值的类型不匹配的话,则会连环影响到后面的读取。这其中的根本原因是, stream
本身是一个附带了一个读写头的 buffer
字符数组,而每次读取后,读写后都会向后移动,下一次读取的位置是上一次读取的位置 + 上一次读取的长度。比如说我们需要读取一个 int
,再读取一个 string
。上一次用户输入了 8.265
, 那么第一次就只会读入 8
,下一次读取从 .265
开始,导致 string
读取出错。
此外, cin
的特点是: cin
越过一切前导空格和换行符,在读入有效字符后,遇到空格或换行符就停止读取,见如下程序:
#include <iostream> | |
#include <string> | |
int main(void) | |
{ | |
string name; | |
string city; | |
cin >> name >> city; | |
cout << "My name is " << name << endl; | |
cout << "The city is " << city << endl; | |
return 0; | |
} |
如果输入的第一个 name
是 First Last
, city
输入的是 Wuhan
。由于 cin
遇到空格就停止读取,那么 name
中存储的值就是 First
, city
中存储的值就是 Last
,而 Wuhan
依然在 buffer
中无法读取。
因此,要解决标准的 stream
带来的麻烦,我们引入一个新的函数: getline()
# 用 getline()
函数读取标准输入
getline()
可以将输入保存在 string
中。 getline
函数的用途在于,如同他的名字,它可以一次读取一行,而 cin
每次读到空格或换行符就停止。 getline
不会忽略空格,会将其一并读入,但是 getline
遇到换行符就停止读取,并且换行符依然留存在 stream buffer
中。因此 getline
非常适合那种用户需要在这个字符串中保留空格的情况。
前文我们说过, cin
会在开始读取时越过一切前导的空格和换行符,读取有效字符后,遇到空格和换行符就停止读取,并且将其留在 stream buffer
中,以待下一次的读取。那么如果我们将 cin
和 getline
混用时,便会出现一些问题。
参考如下示例:
int dummyInt; | |
string dummyString; | |
cin >> dummyInt; | |
getline(cin,dummyString); |
cin
首先读入了一个数,然后将换行符留在了 buffer
中,但是下一次调用 getline
时, getline
遇到换行符就停止读取。导致读入的 dummyString
并不是我们下一次输入的字符串,而是一个空串。这都是因为上一个字符串的换行符还没有处理干净。
最好的解决办法是将这种原始的输入输出读取,改为调用封装好的功能完善的库函数。
# 用 getline()
函数读取文件
参考之前 cin
循环从文件读取的形式,我们可以编写一个使用 getline
循环读取文件的格式:
ifstream capitals("capitals.txt"); | |
string capital,country; | |
... // check if the file is correctly opened | |
while(getline(capitals,capital) && getlien(capitals,country)) { | |
... | |
... | |
} |
# A string buffer: stringstream
有时候,我们想要连接字符串和数字,譬如 "I ate" + 3 "peaches today"
,可是 C++ 不允许我们拼接不一样类型的值,怎么办呢?
下面我们引入一个新的 stream
类: stringstream
。通过 myStringStream << "I ate" << 3 << "peaches today"
, 我们可以实现字符串和其他类型值的拼接。
stringstream
是一个类似于 cin
和 cout
的 stream
。和 ifstream/ofstream
一样,在使用 stringstream
前,我们需要先初始化一个 stringstream
类的对象,然后再对这个对象进行读入和写出。 stringstream
和标准 iostream
的差别在于,其写入和写出的值并不保存在程序外,而是作为程序的一个变量,可以通过调用 myStringStream.str()
随时读取。
在使用 stringstream
之前,需要引入头文件 #include <sstream>
# Initialization
# 初始化结构体
# 方法一:用 .
给每个字段赋值
Student s; | |
s.name = "Frankie"; | |
s.state = "MN"; | |
s.age = 21; |
# 方法二:用 {}
直接赋值
Student s = {"Frankie", "MN", 21}; |
# 初始化 std::pair
# 方法一:用 .
给每个字段赋值
std::pair<int, string> numSuffix1 = {1,"st"}; |
# 方法二:用 {}
直接赋值
std::pair<int, string> numSuffix2; | |
numSuffix2.first = 2; | |
numSuffix2.second = "nd"; |
# 方法三:调用 std::make_pair(field1,field2)
方法
std::pair<int, string> numSuffix2 = std::make_pair(3, "rd"); |
# 初始化 std::vector
# 方法一:使用 {}
直接赋值
// a = {3,5} | |
std::vector<int> a = {3,5}; |
# 方法二:使用 vector()构造函数
赋值
std::vector<int> a(3,5); |
这种情况下,传入的 3 和 5 是构造函数的参数,3 是元素的个数,5 是重复的元素值, a={5,5,5}
# 大括号初始化 (通用)
std::vector<int> vec{1,3,5}; | |
std::pair<int, string> numSuffix1{1,"st"}; | |
Student s{"Frankie", "MN", 21}; | |
// less common/nice for primitive types, but possible! | |
int x{5}; | |
string f{"Frankie"}; |
# Structure Bindings
可以结合 auto
的自动类型推导来自动绑定值。
使用前:
auto p = | |
std::make_pair(“s”, 5); | |
string a = s.first; | |
int b = s.second; |
使用后:
auto p = | |
std::make_pair(“s”, 5); | |
auto [a, b] = p; | |
// a is string, b is int | |
// auto [a, b] = | |
std::make_pair(...); |
# References
=
默认为赋值值,如果要传入引用,必须在声明引用时加上&
;- 修改引用时便修改了值本身,而修改复制品不修改本身;
- 引用是变量的引用,修改引用前必须声明一个变量,无法对常量进行引用;
int value = 5; | |
int& ref = value; // the reference of value | |
int copy = value; // the copy of value |
此外,在使用迭代器时,如果要修改被迭代的值本身而不是他的复制品,必须使用引用,例如:
void shift(vector<std::pair<int, int>>& nums) { | |
for (auto [num1, num2]: nums) { | |
num1++; | |
num2++; | |
} | |
} |
这段代码中, auto [num1,num2]
是 nums
中每个元素的复制品,而不是本身。如果要对本身进行修改,必须使用:
for (auto& [num1,num2] : nums) |
# left-value & right-value
左值和右值的区别在于,左值通常来说是变量,而右值是字面量。左值可以出现在 =
的左边和右边,是在程序生命周期内长期有效。而右值作为字面量,无法在程序中长期存活,自然也不能出现在 =
的左边。
在使用引用时,不允许传入右值,即不允许传入字面量。我们无法对一个常量进行引用,只能对左值 (变量) 进行引用。
# const & const reference/copy
常量是不允许修改的量,使用 const
声明常量。常量的引用和复制也必须是常量,引用及复制前加 const
。
# Containers
# STL 是什么?
# STL Overview
# 为什么需要 STL?
# vector
push_back
insert
pop_back
erase
resize