CS106L Learning

Types and Structs

pair

pair为一个模板类,你可以在< >中指定对应的类型

1
2
3
4
struct Pair{
fill_in_type1 first;
fill_in_type2 second;
}

我们可以使用make_pair来构建一个pair

1
std::make_pair(field1, filed2)

其中field1field2构建的pair类型为其组成pair时的类型

如我们构建以下代码

1
2
pair<bool, string> a = {true, "123"};
pair<bool, string> b = make_pair(true,"123");

auto

auto为让编译器自己推断变量类型

需要注意的是使用auto不代表着变量没有类型而是让编译器自动推断

1
2
auto a = 3; // int
auto b = 1.2 // double

Initialization and References

Initialization

pair

1
2
3
pair<bool, string> a;
a.first = true;
a.second = "123";

auto

1
auto a = pair(3,"hello") // std::pair<int, char*>

什么时候使用auto?一般来说当我们遇到一些返回类型较长的时候采用

1
2
3
4
5
6
7
8
9
10
11
12
int main() {
int a, b, c;
std::cin >> a >> b >> c;
std::pair<bool, std::pair<double, double>> result = quadratic(a, b, c);
bool found = result.first;
if (found) {
std::pair<double, double> solutions = result.second;
std::cout << solutions.first << solutions.second << endl;
}else{
std::cout << “No solutions found!” << endl;
}
}

此时我们可以使用auto来替代std::pair<bool, std::pair<double, double>>以及std::pair<double, double>

需要注意不要滥用 auto

结构化绑定

以下面例子进行说明

1
2
3
auto p = std::make_pair(“s”, 5);
string a = p.first;
int b = p.second;

我们可以使用auto来进行结构化的绑定参数

1
2
auto p = std::make_pair(“s”, 5);
auto [a, b] = p;

Reference

1
2
3
4
5
6
7
8
9
vector<int> original {1, 2};
vector<int> copy = original;
vector<int>& ref = original;
original.push_back(3);
copy.push_back(4);
ref.push_back(5);
cout << original << endl; // {1,2,3,5}
cout << copy << endl; // {1,2,4}
cout << ref << endl; // {1,2,3,5}

ref为对original的引用,当对其进行操作时等同于对original的操作

对于使用auto指向引用类型时需要注意解引用,不然会对原来的值照成影响

1
2
3
4
5
6
7
void shift(vector<std::pair<int, int>>& nums) {
for (size_t i = 0; i < nums.size(); ++i) {
auto [num1, num2] = nums[i];
num1++;
num2++;
}
}

上面的代码中使用了auto来对引用类型的num进行处理,因为由编译器自动识别会将其转换为引用类型而使得下面的num1num2做的加法改变原来num的值,正确代码如下:

1
2
3
4
5
6
7
void shift(vector<std::pair<int, int>>& nums) {
for (size_t i = 0; i < nums.size(); ++i) {
auto& [num1, num2] = nums[i];
num1++;
num2++;
}
}

需要注意一点,引用类型只能对变量进行使用,下面的用法为错误示范

1
int& a = 5; // 错误写法

Streams

stringstream

需导入头文件 #include<sstream>

ostringstream可以用来定义一个字符串流,类似于cout我们可以将对应字符串进行写入并将其进行输出

1
2
3
ostringstream oss;
oss << "123";
cout << oss.str() << endl; // 123

当我们在ostringstream初始化时使用其构造函数,之后再次使用<<进行写入时需要注意缓冲区问题,以下面代码为例子:

1
2
3
ostringstream oss("123456789");
oss << "000";
cout << oss.str() << endl;

我们通常认为输出的结果应该为:123456789000,但是实际上我们的输出为:000456789,实际上我们使用ostringstream时内部会维护一个缓冲区,其中有一个指针指向内部字符串结尾。当我们使用构造函数初始化时会将该指针指向字符串开头,因此我们再次写入字符串流的时候会将其进行覆盖,为了避免上面情况的发生我们可以在构造时加一个ostringstream::ate

1
ostringstream oss("123456789",ostringstream::ate);

之后可以保持其维护的指针,让我们的输出在最后进行添加

istringstream同样可以用来定义一个字符串流,类似于cin,我们可以将一个字符串中的某些部分进行提取出来并将其保存到我们的对应变量中

1
2
3
4
5
6
7
istringstream iss("123456789helloworld",istringstream::binary);
double number;
string unit;
iss >> number;
iss >> unit;
cout << number/5 << endl;
cout << unit << endl;

我们可以分离字符串与数字,并进行相关的计算或者是输出,我们需要注意的是当我们将前面的123456789进行修改为小数,double替换为int会发生什么问题?

1
2
3
4
5
6
7
istringstream iss("12.9helloworld",istringstream::binary);
int number;
string unit;
iss >> number;
iss >> unit;
cout << number/5 << endl;
cout << unit << endl;

我们可以看到执行number/5的值为2,后面的.9helloworldunit读取到的,可以看出其会尽最大可能进行匹配,来保存对应的输出

iostream

需导入#include<iostream>

输入输出流包含四个基本项:cin (标准输入流)、cout (标准输出流-带有缓冲)、cerr (标准错误流-不带缓冲)、clog(标准错误流-带有缓冲)

带缓冲和不带缓冲有什么区别?这里的缓冲区相当于堆栈的效果

1
2
3
4
a = 1; b = 2; c = 3;
cout<<a<<b<<c<<endl;
buffer:|3|2|1|<- (take “<-” as a poniter)
output:|3|2|<- (output 1) |3|<- (output 2) |<- (output 3)

参考:cout和printf的缓冲机制_ithzhang的博客-CSDN博客

对于cin我们可以使用cin.clear()来对其进行清空缓冲区,对于cin的一些其他方法可以查询MSDN

manipulate

endl

插入一行并且清空缓冲区,其耗时大于'\n'因此需要高精度的时候尽量减少endl的使用

ws

跳过所有的空白符知道发现另一个char

boolalpha

用于打印bool值“true”或是“false”

hex

输出十六进制数字

setpercision

自动调整打印数据的精度

setw()

我们可以使用其来进行填充输出(使用空格进行填充,填充到括号中数字的长度,默认填充在输出左边,需要填充在右边可以加一个left)

1
2
3
// #include <iomanip>
cout << "[" << setw(10) << "loading" << "]" << endl; // [ loading]
cout << "[" << left << setw(10) << "loading" << "]" << endl; // [loading ]

setfill()

使用我们指定的内容进行填充,同理于setw

Containers

Initialization

对于C++来说有许多种方式进行初始化,对于下面的代码我们可以看到其初始化因括号不同而产生不同结果

1
2
3
4
int main(){
vector<int> vec1{3}; // vector = {3}
vector<int> vec2(3); // vector = {0, 0, 0}
}

STL

Sequence Containers

其提供对序列元素的访问,通常包含以下几种

1
2
3
4
5
std::vector<T> // 需要注意vec[i] 越界导致的未定义问题
std::deque<T>
std::list<T>
std::array<T>
std::forward_list<T>

Stack:将vector/deque的功能限制在了push_backpop_back

Queue:将deque的功能限制在了push_backpop_back

Associative Conrainers

1
2
3
4
std::map<T1, T2>
std::set<T>
std::unordered_map<T1, T2>
std::unordered_set<T>

其使用键值来进行访问对应的数据,而不是通过下标

map/set为有序排列,可以更快的遍历所有的的元素,unordered map/set为无序排列,则可以更快的通过键值来寻找到对应的元素

Iterators

迭代器用于在容器上进行迭代,可以让我们在非线性容器中获取对应值

1
2
set<int> mySet;
set<int>::itrator iter = mySet.begin() // 创建一个迭代器

在顺序容器下的迭代器可以简单的理解为指针

1
2
3
4
5
6
7
8
9
10
11
// #include <set>
set<int> container;
for (int i = 0; i < 10; i++) {
container.insert(i);
}
set<int>::iterator iter = container.begin();
while (iter != container.end()) {
cout << *iter << endl;
iter++;
}
cout << endl;

当我们需要获取某个结点的时候我们不再是使用下标进行访问,而是使用迭代器,获取到对应的begin()属性后加上对应的偏移值来获取到相关数据

1
2
3
4
vector<int> myVector(10); 
auto itreVec = myVector.begin() + 3; // 不报错
list<int> myList(10);
auto itreList = myList.begin() + 3; // 报错

但是你会发现对于向量我们可以直接使用加法来获取对应的offset,而对于列表却无法使用,这和我们之前提到的迭代器应该是通用的不一样。实际上迭代器拥有几个不同的类型(Input、Output、Forward、Bidirectional、Random Access),他们之间拥有共同点也有不同之处

相同的是他们都可以同现有的迭代器中进行创建,并使用++来进行自增,以及使用==或是!=来进行比较。

Input

用于顺序,单通道输入。同时也是只读属性,即只能在表达式的右侧解引用。

如:findcount

Output

用于顺序,单通道输入。同时也是只读属性,即只能在表达式的左侧解引用。

如:copy

Forward

与组合输入和输出迭代器相同,可以多次自增。可读可写(如果不为const迭代器时)

如:replace

Bidirectional

双向迭代器与前向迭代器相同,但是可以进行自减操作

如:reversestd::map

Random Access

随机迭代器与双向迭代器相同,可以使用任意量递增或递减

如:std::vectorstd::dequestd::string

Templates And Functions

Templates

当我们需要进行一个通用的函数操作时,对应的只有变量类型不同,我们可以将其抽象为一个模板类,以下面代码为例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
pair<int, int> my_minmax(int a, int b) {
if (a < b) {
return { a,b };
}
else
{
return { b,a };
}
}
pair<double, double> my_minmax(double a, double b) {
if (a < b) {
return { a,b };
}
else
{
return { b,a };
}
}

我们最直观的可以看到两个函数就只有变量的类型不同,其他的都一样,我们将其抽象为一个模板类如下:

1
2
3
4
5
6
7
8
9
template <typename T>
pair<T, T> my_minmax(T a,T b) {
if (a < b) {
return { a,b };
}
else {
return { b,a };
}
}

需要注意的是我们需要告诉其为模板类,需要加一个template

lambda

lambda实际上是一个非常轻量级的函数,一般来说其用于定于匿名函数,使得代码更加灵活,lambda表达式与普通函数类似,也有参数列表、返回值类型和函数体,只是它的定义方式更简洁,并且可以在函数内部定义,一般写法如下:

1
2
3
auto func = [capture-clause](parameters){
// body
}

其中capture为捕获列表,其可以将上下文的变量以值或者引用的方式进行捕获在body中进行使用。parameters为参数列表,body为具体的实现语句

常用的捕获方式:

[] 什么也不捕获,无法lambda函数体使用任何

[=] 按值的方式捕获所有变量,尽量不要使用,否则所有变量会像全局变量一样被使用

[&] 按引用的方式捕获所有变量

[=, &a] 除了变量a之外,按值的方式捕获所有局部变量,变量a使用引用的方式来捕获。这里可以按引用捕获多个,例如 [=, &a, &b,&c]。这里注意,如果前面加了=,后面加的具体的参数必须以引用的方式来捕获,否则会报错。

[&, a] 除了变量a之外,按引用的方式捕获所有局部变量,变量a使用值的方式来捕获。这里后面的参数也可以多个,例如 [&, a, b, c]。这里注意,如果前面加了&,后面加的具体的参数必须以值的方式来捕获。

[a, &b] 以值的方式捕获a,引用的方式捕获b,也可以捕获多个。

[this] 在成员函数中,也可以直接捕获this指针,其实在成员函数中,[=]和[&]也会捕获this指针。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
int a = 3;
int b = 5;

// 按值来捕获
auto func1 = [a] { cout << a << endl; };
func1();

// 按值来捕获
auto func2 = [=] { std::cout << a << " " << b << endl; };
func2();

// 按引用来捕获
auto func3 = [&a] { cout << a << endl; };
func3();

// 按引用来捕获
auto func4 = [&] { cout << a << " " << b << endl; };
func4();

参考:C++ Lambda表达式的完整介绍 - 知乎 (zhihu.com)

Functions and Algorithms

Predicate

我们可以使用一个谓词来进行描述我们需要进行的操作,通过其返回值进行相应的操作

1
2
3
4
template <typename InputIt, typename DataType, typename UniPred>
int someFunc(InputIt begin, InputIt end, UniPred predicate){
// something
}

我们以某个数字在容器中出席那的次数为例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
template <typename InputIt, typename DataType, typename UniPred>
int coutTimes(InputIt begin, InputIt end, UniPred check){
// 计算 'a' 出现次数
int count = 0;
for(auto iter = begin; iter != end; ++iter){
if(check(*iter)){ // 使用 predicate 判断是否出现 'a'
count++;
}
}
return count;
}

bool predicate(char str){
return str == 'a';
}

predicate 仅为一个函数代指名,可以更换为其他函数名称

本质上谓词便是我们从外部引进一个函数指针到模板类中方便我们对其进行我们所需要的操作

当我们想要实现一个二元谓词时,但是在我们定义的模板类却是一元谓词时我们可以考虑使用Lambda函数来作为我们的函数指针进行传入

如我们想要实现两个数比较大小的一个谓词函数,并将其进行通用泛化

1
2
3
4
5
6
7
8
9
bool isLessThanLimit(int val){
return val < 5;
}
bool isLessThanLimit(int val){
return val < 6;
}
bool isLessThanLimit(int val){
return val < 7;
}

当出现上述情况时我们想将其通用泛化,若干我们构建下面函数时则需要我们输入两个变量,此时变为二元谓词

1
2
3
bool isLessThanLimit(int val, int num){
return val < num;
}

而我们的原来函数只有一个输入变量,我们需要从二元谓词转换为一元谓词时我们采用lambda函数转换如下

1
2
3
4
5
6
vector<int> data{1, 2, 3, 4, 5};
int num = 7;
auto isLessThanLimit [num](auto val) -> bool {
return val < num;
}
coutTimes(data.begin(), data.end(),isLessThanLimit);

需要注意一下C++11不支持上述写法,我们可以使用C++17或以上的C++版本

Algorithm

只选取一部分,更多可以参考 cppreference.com

std::sort

通常我们传入相应的初始迭代器以及结束的迭代器,告述其我们需要排序的范围,同时我们需要传入对应的比较方式,告诉其排序的方法

1
2
3
4
5
vector<int> num{5, 1, 3, 4, 2};
auto compareRating = [](int a, int b){
return a < b;
}
std::sort(num.begin(), num.end(), compareRating);

std::nth_element

需导入 #include <algorithm>

将第n个元素放到它的正确位置

1
2
3
4
5
6
std::vector<int> v{5, 6, 4, 3, 2, 6, 7, 9, 3};
std::nth_element(v.begin(), v.begin() + v.size()/2, v.end());
std::cout << "The median is " << v[v.size()/2] << '\n';

std::nth_element(v.begin(), v.begin()+1, v.end(), std::greater<int>());
std::cout << "The second largest element is " << v[1] << '\n';

相当于sort的一种变形,与之不同的是sort会对全部内容进行排序,相对耗时较大,而此函数相当于直接获取对应元素位置的值,同样的我们可以加一个lambda函数来确定对应的排序方式

std::stable_partition

重排序范围[first, last)中的元素,使得所有谓词p对其返回true的元素先于谓词p对其返回false的元素,同时保持元素的相对顺序

1
2
3
std::vector<int> v{0, 0, 3, 0, 2, 4, 5, 0, 7};
std::stable_partition(v.begin(), v.end(), [](int n){ return n > 0;});
// 3 2 4 5 7 0 0 0 0

同样的我们使用lambda函数来确定对应的排序方式,我们将大于0的元素返回值定义为true,因此为true的元素会在排序后放在小于等于0的元素的前方

std::copy_if

需要导入#include <numeric>

其用于在一定范围内堆所有满足谓词的所有元素复制到另一个函数中

1
2
3
std::vector<int> src_vector(5);
std::vector<int> det_vector(5);
std::copy_if(src_vector.begin(), src_vector.end(), dst_vector.begin(), [](const int item) {return item > 13; });

我们将src_vector中的变量值大于13的进行拷贝到det_vector

参考:c++11之 copy 和 copy_if 的用法 - mohist - 博客园 (cnblogs.com)

std::remove_if

remove_if的参数是迭代器,前两个参数表示迭代的起始位置和这个起始位置所对应的停止位置。最后一个参数:传入一个回调函数,如果回调函数返回为真,则将当前所指向的参数移到尾部。返回值是被移动区域的首个元素

1
std::remove_if(src_vector.begin(), src_vector.end(), [](const char item) {return isspace(item); });

其将返回的是被以东区域的第一个元素,意味着后面的空间任然存在,当我们想要删除后面的多余空间时我们可以使用erase来进行删除操作

1
2
3
4
src_vector.erase(
std::remove_if(src_vector.begin(), src_vector.end(), [](const char item) {return isspace(item); });,
src_vector.end()
)

Classes and Const Correctness

class

对于类来说纯在有构造函数与析构函数两个默认函数,构造函数用来对类进行初始化的工作,而析构函数则是对类的一些资源进行释放的一个过程

构造函数构建如下:

1
2
3
ClassName::ClassName(parameters){
statements to initialize the object;
}

构造函数在创建新对象时初始化其状态。同时其没有指定返回类型,为隐式地“返回”新建对象

析构函数构建如下:

1
2
~ClassName();
ClassName::~ClassName(){}

通常析构函数再程序释放对象时使用

Const

当我们编写了一个确定不会被修改的类后我们可以考虑使用const以防其被修改,通过编译器的报错我们可以很清楚的了解到对应的错误信息。同时当我们提供API时,客户也可以明确的明白对应传入的应该是一个不可修改的变量,以防某些数据被修改

同时注意一下const指针的使用,确定其指针为const与否与指向的是否为const

1
2
3
4
5
6
7
8
9
10
// const -> non-ocnst
int * const p;

// non-const -> const
const int* p;
int const* p;

// const -> const
const int* const p;
int const* const p;

auto 对于 & 符号以及 const 不关心,意味着这两者都可以使用auto来进行替代

Operators

operator overloading

C++中支持运算符重载,意味着我们可以将一个运算符的运算规则进行拓展,将两个基本变量外的变量进行操作运算等操作

new、new[]、delete、delete[] 实际上也是运算符,也可以同样的被重载

我们可以尝试将+进行重载处理,使其类似于append的效果

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
vector<string> operator + (vector<string>&a,const string b) {
a.push_back(b);
return a;
}

int main()
{
vector<string> data;
data = data + "Hello";
data = data + "World";
for (auto i : data) {
cout << i << " ";
}
return 0;
}

Move Semantics

emplace_back

push_back十分相似,通常我们需要将元素加入到容器尾都是使用push_back来进行完成的,emplace_back也是完成同样的工作,但是对于push_back来说通常需要我们创建一个变量,将值保存到其中,之后再使用push_back来将其进行添加,而empalce_back则解决了这一痛点

1
2
3
4
5
6
7
8
// President 为我们创建的一个类,其包含三个变量 name,country,year
std::vector<President> elections;
std::cout << "emplace_back:\n";
elections.emplace_back("Nelson Mandela", "South Africa", 1994);

std::vector<President> reElections;
std::cout << "\npush_back:\n";
reElections.push_back(President("Franklin Delano Roosevelt", "the USA", 1936)); // 需要我们传入一个对象进去而不是直接的传递值

参考:std::vector::emplace_back - cppreference.com

Deep copy and shallow copy

浅拷贝就比如像引用类型,而深拷贝就比如值类型。

浅拷贝是指源对象与拷贝对象共用一份实体,仅仅是引用的变量不同(名称不同)。对其中任何一个对象的改动都会影响另外一个对象。

深拷贝是指源对象与拷贝对象互相独立,其中任何一个对象的改动都不会对另外一个对象造成影响。

最典型的一个例子便是当我们在一个类中存在有动态分配的内存空间对象时,我们浅拷贝复制的值指向的动态内存地址与原被拷贝的地址值一样,当我们在调用析构函数进行内存释放的时候便会产生两次释放的问题,因而产生错误

当其中不存在指针之类的东西时,深浅拷贝没啥区别

std::mov

C++11的标准库<utility>提供了一个非常有用的函数 std::move()std::move()函数将一个左值强制转化为右值引用,以用于移动语义。

移动语义,允许直接转移对象的资产和属性的所有权,而在参数为右值时无需复制它们。因此通过std::move(),可以避免不必要的拷贝操作

对于小型设备来说比较有效,可以使其工作效率快一倍

我们以一个交换函数为例

1
2
3
4
5
6
template <typename T>
void swap(T a, T b){
T temp = a;
a = b;
b = temp;
}

通常我们写的一个交换函数如上,我们可以使用move来对其进行优化加速

1
2
3
4
5
6
template <typename T>
void swap(T a, T b){
T temp = std::move(a);
a = std::move(b);
b = std::move(temp);
}

Inheritance and Template Classes

Abstract Classes

如果一个类至少有一个纯虚函数,那么它就被称为抽象类(接口是抽象类的子集)。需要注意的是抽象类不能被实例化。

纯虚函数需要在声明之后加个=0;

对于虚函数中存在有纯虚函数与非纯虚函数两类,对于抽象类来说至少有一个纯虚函数的类

1
2
3
4
5
class Base{
public:
virtual void foo() = 0; // 纯虚函数
virtual void foo2(); // 非纯虚函数
};

当我们定义了一个抽象类时,当其被继承时,那么其子类需要对其进行实现

在以下情况下使用模板类:

  • 运行效率是最重要的
  • 没有共同的基础可以定义

在以下情况下选择派生类:

  • 编译时效率是最重要的
  • 想要隐藏实现
  • 不想让代码膨胀

类型转换

在C++中,除了包含C语言转换方式,还添加了另外四个显式类型转换的语法,他们分别是:static_castdynamic_castconst_castreinterpret_cast

当我们的代码加入了explicit来对构造函数进行修饰时,以为着我们只能显示调用,禁止隐式转换,此时我们便需要类型转换来完成显示转换,以下面代码为例:

1
2
3
4
5
6
7
8
9
void fun(CTest test); 

class CTest
{
public:
explicit CTest(int m = 0);
}
fun(20);//error 隐式转换
fun(static_cast<CTest>(20)); //ok 显式转换

添加编译选项-Werror=conversion也可以来禁止隐式转换

static_cast用途最广泛,除了后面三种类型转换外,其他的类型转换都能使用static_cast完成

dynamic_cast主要用于运行时的从父类指针向子类指针转换,如果转换不成功则返回nullptr

const_cast主要用于去除指针或引用类型的const属性。此操作可能会导致未定义的行为,所以需要慎用

reinterpret_cast可以将指针或引用转换为任何类型的指针或引用。其中reinterpret_cast实现依赖于编译器和硬件,可能导致未定义的行为

Template Classes

模板类与我们之前提到的函数模板相似,其中函数模板描述了如何构建一系列外观相似的函数。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
template <typename T>
class Complex {
public:
Complex(T a, T b)
{
this->a = a;
this->b = b;
}
//普通加法函数
Complex myAdd(Complex &c1, Complex &c2)
{
Complex temp(c1.a + c2.a, c1.b + c2.b);
return temp;
}
private:
T a;
T b;
};

在模板类的继承中,需要注意以下两点:

  • 如果父类自定义了构造函数,记得子类要使用构造函数列表来初始化
  • 继承的时候,如果子类不是模板类,则必须指明当前的父类的类型,因为要分配内存空间
  • 继承的时候,如果子类是模板类,要么指定父类的类型,要么用子类的泛型来指定父类

RAII and Smart Points

code path

一个代码执行的流程的个数成为代码路径的个数,我们以下面代码为例:

1
2
3
4
5
6
7
8
9
10
11
string EvaluateSalary(int idNumber) {
Employee* p = new Employee(idNumber);
if (p->GetTitle() == "CEO" || p->GetSalary() > 100000) {
delete p;
return "Salary is good";
}
else {
delete p;
return "Salary is bad";
}
}

上面的代码有多少种代码路径呢?可能大多数人回答的是三种,当if判断为真时便存在两种情况,还一种if判断为假的情况。但是实际上其代码路径远远多于三种,这是因为我们没有考虑到发送异常的情况,当我们发生异常时可能回照成意想不到的结果,比如p->GetTitle()返回的不为string类型时产生的异常使其会跳出该函数,进而执行我们意想不到的指令流程,同时也导致了可能会引发内存泄漏,无法成功的运行到我们下面的delete

RAII

RAII代表资源获取即初始化,其代表着所有的资源都应该在构造函数中获取,所有的资源都应该在析构函数中释放。

比如我们常见的文件打开操作,我们可以有以下两种写法:

1
2
3
4
5
ifstream input("something.txt");
// ------------------------------------------
ifstream input();
input.open("something.txt");
input.close();

对于第一种写法来说我们不需要对其进行写入close(),因为ifstream的析构函数帮我们对其进行了释放的工作,而下面则是需要我们自己来完成对应资源的释放。而第一种正好满足了我们RAII思想的要求

Smart Points

智能指针是近代C++最大的风格变化,使用智能指针可以帮我们实现RAII思想

C++帮我们构建了一些智能指针

1
2
3
std::unique_ptr // 独占资源所有权的指针,不可被复制
std::shared_ptr // 共享资源所有权的指针,可以被复制
std::weak_ptr // 共享资源的观察者,需要和 std::shared_ptr 一起使用,不影响资源的生命周期

当我们使用裸指针的时候需要我们进行释放内存

1
2
3
4
5
{
int* p = new int(100);
// ...
delete p; // 要记得释放内存
}

而我们可以使用智能指针来自动管理内存

1
2
3
4
5
{
std::unique_ptr<int> uptr = std::make_unique<int>(200);
//...
// 离开 uptr 的作用域的时候自动释放内存
}

回到我们最开始提到的内存泄露的问题上我们可以使用智能指针来进行替换对应原指针,来避免我们的内存泄露

1
std::shared_ptr<Employee> p = std::make_shared<Employee>(idNumber);  // C++20 才支持make_shared

Multithreading

代码通常是顺序执行的,但是线程可以并行完成一个工作

1
2
3
4
5
6
7
8
9
^                                                                                         
| --- --- --- ---
| --- --- --- ---
+---------------------------------> 单核CPU时完成的对应多线程

^
| -----------------------
| -----------------------
+---------------------------------> 多核CPU时完成的对应多线程

我们可以通过#include <thread>来进行导入多线程相关的函数

1
std::thread thd()    // 创建线程对象

只要我们创建了对象那么线程对象便开始运行,我们要想使用线程阻塞则是需要使用两种方式join()detach(),我们使用阻塞主要是糖线程的执行顺序进行改变

t.join()等待线程完成,使用t.detach()分离线程,让它在后台运行

对于多线程来说需要注意线程安全的问题,当多个线程访问一个变量并且其中有至少一个线程对其进行了写操作便会产生数据竞争的问题,进而引发程序崩溃导致未定义的结果或是错误的结果

对于上述问题我们通常使用同步机制包括互斥量、条件变量、原子操作等来解决上述问题

Mutex

互斥量(mutex)是一种用于实现多线程同步的机制,用于确保多个线程之间对共享资源的访问互斥。互斥量通常用于保护共享数据的访问,以避免多个线程同时访问同一个变量或者数据结构而导致的数据竞争问题

互斥量提供了两个基本操作:lock()unlock()。当一个线程调用 lock() 函数时,如果互斥量当前没有被其他线程占用,则该线程获得该互斥量的所有权,可以对共享资源进行访问。如果互斥量当前已经被其他线程占用,则调用 lock() 函数的线程会被阻塞,直到该互斥量被释放为止

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
#include <iostream>
#include <thread>
#include <mutex>
int shared_data = 0;
std::mutex mtx;
void func(int n) {
for (int i = 0; i < 100000; ++i) {
mtx.lock();
shared_data++;
std::cout << "Thread " << n
<< " increment shared_data to " << shared_data << std::endl;
mtx.unlock();
}
}
int main() {
std::thread t1(func, 1);
std::thread t2(func, 2);

t1.join();
t2.join();
std::cout << "Final shared_data = " << shared_data << std::endl;
return 0;
}

参考:陈子青的编程学习课堂 (seestudy.cn)


CS106L Learning
https://equinox-shame.github.io/2023/08/30/CS106L Learning/
作者
梓曰
发布于
2023年8月30日
许可协议