CS106L Spring 2024 学习笔记

课程主页: CS106L

1. Welcome

这节课主要是对 CS106L 课程的简单介绍，作业安排，课程内容的罗列，C++ 发展历史的介绍.

课程介绍:

这门课主要介绍 C++ 的现代特性，对基本语法基本没有耗费时间，需要一定的语言基础.

作业安排:

这门课会有七个简单的小作业，目前开放的作业如下:

作业序号 | 作业标题 | 作业链接
Assignment 1 | SimpleEnroll | 前往完成
Assignment 2 | Marriage Pact | 前往完成
Assignment 3 | Make a class! | 前往完成
Assignment 4 | Weather Forecast | 前往完成
... | ... | ...

课程内容:

编号 | 课程内容
1 | C++ feature 简介
2 | 初始化，引用，Streams
3 | 容器，迭代器，指针
4 | 类，模板类，const
5 | 模板函数，函数，lambda
6 | 运算符，特殊成员函数
7 | move 语义，类型安全

发展历史:

参照第一节课的 slides 即可.

2. Types and Structs

类型:

基本类型:

C++ 提供了一下几种基本类型:

Copy
int val = 5; //32 bits
char ch = 'F'; //8 bits
float decimalVal1 = 5.0; //32 bits
double decimalVal2 = 5.0; //64 bits
bool bVal = true; //1 bit

在 string 头文件中实现了对 string 类型的支持

Copy
std::string str = "Nanomoa";

C++ 是一门静态类型语言，这意味着任何一个变量 / 函数等等都应该在运行前显示的声明其类型.

动态类型和静态类型的区别:

动态类型: 在执行代码是一行一行的检查类型，可能在运行时出现一些错误

静态类型: 在运行之前，编译时检查类型，编译错误将无法运行，且静态类型语言的代码可读性更高

在 C++ 中不能定义多个相同的函数，但是可以通过函数重载实现其不同版本.

函数重载需要满足函数名相同，但 返回值类型 / 参数列表中参数的类型 / 参数列表中参数的数量 三者至少有一项不相同.

结构体:

auto 关键字的引入:

用于代替实际类型，让编译器自动根据变量的值推断其类型.

Copy
auto a = 1; // int
auto b = 3.14; // double
auto c = 'a'; // char
auto d = "Hello"; // char*

结构体:

结构体是一组变量的集合，其中每个字段都有它们自己的类型.

示例:

Copy
struct Student {
std::string name;
int age;
};

Student stu;
stu.name = "Nanomoa";
stu.age = 20;

结构体可以作为参数传递一组信息，或者作为返回值 (可以通过结构体使得函数间接的具有多个返回值).

示例:

Copy
void printStudentInfo(Student s) {
std::cout << s.name << " " << s.age << std::endl;
}

Student getStudent() {
Student s;
s.name = "Test";
s.age = 20;
return s;
}

Student s = getStudent();
printStudentInfo(s);

结构体初始化:

Copy
Student s;
s.name = "A";
s.age = 20;

// or
s = {"A", 20};

std::pair 引入:

std::pair 是 STL 中提供的一个模板，可以接受任意一对不同类型，其包含两个字段 first 和 second.

示例:

Copy
std::pair<int, std::string> val = {1, "AA"};
std::cout << val.first << " " << val.second << std::endl; // "1 AA"

可以配合 std::pair 实现如下返回值携带操作状态的函数:

Copy
std::pair<bool, std::string> func() {
bool is_success = false;
// todo ...
if(is_success) {
return {true, "Success Info"};
} else {
return {false, "Failed Info"};
}
}

std::pair<bool, std::string> res = func();
auto res1 = func();

3. Initialization & References

初始化:

直接初始化:

方式如下:

Copy
int valInt = 1;
int valInt1(2);

注意: C++ 不在乎初始化传入的数值是否为整数 (传入浮点数会转换成整数)

例:

Copy
int valInt = 3.00; // valInt => 3
int valInt1(4.5); // valInt1 => 4

Uniform initialization:

从 C++11 开始支持，更安全，会进行类型检查，例:

Copy
int valInt{1}; // 正确
int valInt1{3.14}; // 无法通过编译

使用:

Copy
// std::map
std::map<std::string, int> ages{
{"Nanomoa", 20},
{"Test", 21}
};

// std::vector
std::vector<int> vec{1, 2, 3, 4, 5};

// struct
struct Student {
std::string name;
int age;
};

Student stu;
stu.name = "Nanomoa";
stu.age = 20;
// 等同于下面
Student s = {"Nanomoa", 20};

结构化绑定:

通过对象的元素或成员初始化多个实体，C++17 开始支持，例:

Copy
std::tuple<std::string, std::string, std::string> getClassInfo() {
std::string className = "CS106L";
std::string buildingName = "Turing Auditorium";
std::string language = "C++";
return {className, buildingName, language};
}

auto classInfo = getClassInfo();
std::string className = std::get<0>(classInfo);
std::string buildingName = std::get<1>(classInfo);
std::string language = std::get<2>(classInfo);
// 等同于下面(结构化绑定)
auto [className, buildingName, language] = getClassInfo();

引用:

引用是已存在事物的别名，符号 &, 例:

Copy
int num = 5;
int& ref = num;
ref = 10;
std::cout << ref << ' ' << num << std::endl; // 10 10

引用作为函数参数时也有同样效果:

Copy
void func(int& n) {
n = std::pow(n, 2);
}

int main() {
int num = 2;
func(2);
std::cout << num << std::endl; // 4
return 0;
}

Copy
// 传入此函数中的nums并没有被修改
void func(std::vector<std::pair<int, int>> &nums) {
for (auto [num1, num2] : nums) { // 此处通过结构化绑定copy了nums中当前pair的值
num1++; // 此处修改的是copy的pair, 而不是nums中的pair
num2++;
}
}

// 此函数中 nums 被修改
void func1(std::vector<std::pair<int, int>> &nums) {
for (auto& [num1, num2] : nums) { // 此处通过结构化绑定引用了nums中当前pair
num1++;
num2++;
}
}

左值 / 右值:

左值：可以在等号左边，也可以在等号右边

右值：只能在等号右边

例:

Copy
// x 可以作为左值
int y = x;
x = 10;
// 10 可以作为右值
int y = 10;
10 = x; // 错误

注意: 不能将右值绑定到非 const 的左值，例:

Copy
int squareN(int& num) {
return std::pow(num, 2);
}

auto val = squareN(2); // 编译错误

Const:

使用 const 声明的对象不可修改，例:

Copy
std::vector<int> vec{1, 2, 3}; // 非 const
vec.push_back(4); // 可修改
std::vector<int>& ref_vec{vec}; // 引用
ref_vec.push_back(5);
const std::vector<int> const_vec{1, 2, 3}; // const
const_vec.push_back(4); // 编译错误, 不可修改
const std::vector<int>& ref_const_vec{const_vec}; // const 引用
ref_const_vec.push_back(4); // 不可修改

注意: 对于 const 声明的对象，不能为其声明 非const 的引用，例:

Copy
const std::vector<int> vec{1, 2, 3};
std::vector<int>& ref_vec{vec}; // 错误
const std::vector<int>& const_ref_vec{vec}; // 正确

如何编译 C++:

Copy
g++ -std=c++11 main.cpp -o main

or

Copy
g++ -std=c++11 main.cpp

4. Streams

What are streams:

C++ 通用的输入输出抽象

cin and cout:

位于 iostream 头文件中

cerr and clog

cerr: 用于输出 errors

clog: 用于记录非关键事件日志

cout and cin

std::cout and the IO library:

Copy
std::cout << "Hello, World" << std::endl;

std::cout 是一个 stream

std::cout stream 是 std::ostream 的一个实例，是标准输出流

An Input Stream:

"3.14" -> Input Stream -> type conversion -> (double)(3.14)

Copy
double pi;
std::cin >> pi;
std::cout << pi / 2 << std::endl;

std::cin 是控制台 输入流

std::cout stream 是 std::istream 的一个实例，是标准输入流

stringstreams:

std::stringstream

一种将字符串看作流的方法，可用于处理混合类型的数据

Copy
std::string initial_quote = "Bjarne Stroustrup C makes it easy to shoot yourself in the foot";
std::stringstream ss(initial_quote); // 也可以 ss << initial_quote;
std::string first,
last,
language,
extracted_quote;
ss >> first >> last >> language >> extracted_quote;
std::cout << first << " " << last << " " << language << " " << extracted_quote << std::endl;

Copy
first => Bjarne
last => Stroustrup
language => C
extracted_quote => makes

extracted_quote => makes , 注意，使用 >> 运算符只会读取到下一个空格前

如果我们需要读取包括 makes 后面的所有内容，应该使用 getline(), 例:

Copy
std::string initial_quote = "Bjarne Stroustrup C makes it easy to shoot yourself in the foot";

std::stringstream ss(initial_quote);

std::string first,
last,
language,
extracted_quote;
ss >> first >> last >> language;
std::getline(ss, extracted_quote);
std::cout << first << " " << last << " " << language << " " << extracted_quote << std::endl;

Copy
first => Bjarne
last => Stroustrup
language => C
extracted_quote => makes it easy to shoot yourself in the foot

Output Streams:

一种写入数据的方式

Copy
double tao = 6.28;
std::cout << tao;

tao -> std::cout stream -> "6.28" -> flush -> console/others

std::cout stream 是 line-buffered, buffer 中的内容在flush前不会显示在目标上

line-buffered: buffer 在遇到 '\n' 时会被刷新

例:

Copy
std::cout << "Starting intense computation...";
std::this_thread::sleep_for(std::chrono::seconds(5));
std::cout << "finished computation\n";

这段代码的效果并不是 先输出 "Starting intense computation..." 等待 5s 后再输出 "finished computation"

而是等待 5s 后同时输出 "Starting intense computation..." 和 "finished computation"

因为 std::cout << "Starting intense computation..."; 没有 flush

解决该问题的方法:

Copy
std::cout << "Starting intense computation...\n";
// or
std::cout << "Starting intense computation..." << std::endl;
// or
std::cout << "Starting intense computation...";
std::cout << std::flush;

std::ios::sync_with_stdio(false) 用于取消 C++ 的标准流（例如 cout 和 cin）和 C 的标准 IO（例如 printf 和 scanf）之间的同步，可以提升 IO 的性能

但注意：禁用同步后不可将 C++ 流与 C 的标准 IO 混用，否则会导致 ub

Output File Streams:

std::ofstream, 一种向文件写入数据的方式，常用方法有is_open(), open(), close(), fail()

例:

Copy
std::ofstream ofs("hello.txt"); // 为 hello.txt 创建一个流, 如果文件不存在会先新建该文件
if (ofs.is_open()) { // 判断是否打开
ofs << "Hello CS106L" << '\n'; // 如果打开则向文件中写入内容
}
ofs.close(); // 关闭, 关闭后不可操作
ofs.open("hello.txt"); // 打开
ofs << "Open Again" << '\n'; // 写入内容

我们期望的文件内容:

Copy
Hello CS106L
Open Again

实际为:

Copy
Open Again

如果需要追加内容而不是覆盖内容，应该按照如下方式打开:

Copy
ofs.open("hello.txt", std::ios::app);

Input File Streams:

std::ifstream, 一种从文件中读取内容的方式，用法与 std::ofstream 基本一致

例:

Copy
std::ifstream ifs("hello.txt");
if (ifs.is_open()) {
std::string line1;
std::getline(ifs, line1);
std::cout << line1 << '\n';
}
if (ifs.is_open()) {
std::string line2;
std::getline(ifs, line2);
std::cout << line2 << '\n';
}

Input Streams:

std::istream, 一种读取数据的方式

std::cin 是 buffered, 每次读取到下一个 ' ' or '\n' or '\t' 位置

例:

Copy
/*
3,14 6.28
Nanomoa AAAA
*/
double pi, tao;
std::string name;
std::cin >> pi >> tao;
std::cin >> name;
std::cout << pi << " " << tao << " " << name << '\n';
// 3.14 6.28 Nanomoa

我们如果要使得name为 "Nanomoa AAAA", 应该使用 std::getline():

Copy
double pi, tao;
std::string name;
std::cin >> pi >> tao;
std::getline(std::cin, name);
std::cout << pi << " " << tao << " " << name << '\n';

但是我们发现，读取完 pi 和 tao 程序就结束了

因为此处的 std::getline(std::cin, name) 读取的是tao 后面的 '\n'

所以，我们应该先读取空格后再读取 name:

Copy
double pi, tao;
std::string name;
std::cin >> pi >> tao;
std::getline(std::cin, name);
std::getline(std::cin, name);
std::cout << pi << " " << tao << " " << name << '\n';

但是，不推荐 std::getline 和 std::cin 同时使用，因为二者解析数据的方式不同

非要共用的情况下可以使用 std::getline 配合 std::stringstream 代替:

Copy
double pi, tao;
std::string name;
std::string line;
std::getline(std::cin, line);
std::stringstream ss(line);
ss >> pi >> tao;
std::getline(std::cin, line);
name = line;
std::cout << pi << " " << tao << " " << name << std::endl;

5. Containers

(更新中...)