banner
Nanomoa

Nanomoa's Blog

"Bytes and Brackets: My Journey in Go and C++ Exploration."
github
zhihu
telegram
twitter

CS106L Spring 2024 学习笔记

课程主页: CS106L

1. Welcome#

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

课程介绍:#

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

作业安排:#

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

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

课程内容:#

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

发展历史:#

参照第一节课的 slides 即可.

2. Types and Structs#

类型:#

基本类型:#

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

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 类型的支持

std::string str = "Nanomoa";

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

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

  • 动态类型: 在执行代码是一行一行的检查类型,可能在运行时出现一些错误
  • 静态类型: 在运行之前,编译时检查类型,编译错误将无法运行,且静态类型语言的代码可读性更高

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

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

结构体:#

auto 关键字的引入:#

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

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

结构体:#

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

示例:

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

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

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

示例:

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);

结构体初始化:

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

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

std::pair 引入:

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

示例:

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

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

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#

初始化:#

直接初始化:#

方式如下:

int valInt = 1;
int valInt1(2);

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

例:

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

Uniform initialization:#

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

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

使用:

// 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 开始支持,例:

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();

引用:#

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

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

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

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

int main() {
    int num = 2;
    func(2);
    std::cout << num << std::endl; // 4
    return 0;
}
// 传入此函数中的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++;
    }
}

左值 / 右值:#

  • 左值:可以在等号左边,也可以在等号右边
  • 右值:只能在等号右边

例:

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

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

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

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

Const:#

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

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 的引用,例:

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

如何编译 C++:#

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

or

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:

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)

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

std::cin 是控制台 输入流

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

stringstreams:#

std::stringstream#

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

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;
first => Bjarne 
last => Stroustrup 
language => C 
extracted_quote => makes

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

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

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;
first => Bjarne 
last => Stroustrup 
language => C 
extracted_quote => makes it easy to shoot yourself in the foot

Output Streams:#

一种写入数据的方式

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' 时会被刷新

例:

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

解决该问题的方法:

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()

例:

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'; // 写入内容

我们期望的文件内容:

Hello CS106L
Open Again

实际为:

Open Again

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

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

Input File Streams:#

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

例:

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::cinbuffered, 每次读取到下一个 ' ' or '\n' or '\t' 位置

例:

/*
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():

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:

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::getlinestd::cin 同时使用,因为二者解析数据的方式不同

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

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#

(更新中...)

加载中...
此文章数据所有权由区块链加密技术和智能合约保障仅归创作者所有。