CS106L Learning
Types and Structs
pair
pair
为一个模板类,你可以在< >
中指定对应的类型
1 |
|
我们可以使用make_pair
来构建一个pair
1 |
|
其中field1
、field2
构建的pair
类型为其组成pair
时的类型
如我们构建以下代码
1 |
|
auto
auto
为让编译器自己推断变量类型
需要注意的是使用
auto
不代表着变量没有类型而是让编译器自动推断
1 |
|
Initialization and References
Initialization
pair
1 |
|
auto
1 |
|
什么时候使用auto
?一般来说当我们遇到一些返回类型较长的时候采用
1 |
|
此时我们可以使用auto
来替代std::pair<bool, std::pair<double, double>>
以及std::pair<double, double>
需要注意不要滥用 auto
结构化绑定
以下面例子进行说明
1 |
|
我们可以使用auto
来进行结构化的绑定参数
1 |
|
Reference
1 |
|
ref
为对original
的引用,当对其进行操作时等同于对original
的操作
对于使用auto指向引用类型时需要注意解引用,不然会对原来的值照成影响
1 |
|
上面的代码中使用了auto
来对引用类型的num
进行处理,因为由编译器自动识别会将其转换为引用类型而使得下面的num1
、num2
做的加法改变原来num
的值,正确代码如下:
1 |
|
需要注意一点,引用类型只能对变量进行使用,下面的用法为错误示范
1
int& a = 5; // 错误写法
Streams
stringstream
需导入头文件
#include<sstream>
ostringstream
可以用来定义一个字符串流,类似于cout
我们可以将对应字符串进行写入并将其进行输出
1 |
|
当我们在ostringstream
初始化时使用其构造函数,之后再次使用<<
进行写入时需要注意缓冲区问题,以下面代码为例子:
1 |
|
我们通常认为输出的结果应该为:123456789000
,但是实际上我们的输出为:000456789
,实际上我们使用ostringstream
时内部会维护一个缓冲区,其中有一个指针指向内部字符串结尾。当我们使用构造函数初始化时会将该指针指向字符串开头,因此我们再次写入字符串流的时候会将其进行覆盖,为了避免上面情况的发生我们可以在构造时加一个ostringstream::ate
1 |
|
之后可以保持其维护的指针,让我们的输出在最后进行添加
istringstream
同样可以用来定义一个字符串流,类似于cin
,我们可以将一个字符串中的某些部分进行提取出来并将其保存到我们的对应变量中
1 |
|
我们可以分离字符串与数字,并进行相关的计算或者是输出,我们需要注意的是当我们将前面的123456789
进行修改为小数,double
替换为int
会发生什么问题?
1 |
|
我们可以看到执行number/5
的值为2
,后面的.9helloworld
为unit
读取到的,可以看出其会尽最大可能进行匹配,来保存对应的输出
iostream
需导入
#include<iostream>
输入输出流包含四个基本项:cin (标准输入流)、cout (标准输出流-带有缓冲)、cerr (标准错误流-不带缓冲)、clog(标准错误流-带有缓冲)
带缓冲和不带缓冲有什么区别?这里的缓冲区相当于堆栈的效果
1 |
|
对于cin
我们可以使用cin.clear()
来对其进行清空缓冲区,对于cin
的一些其他方法可以查询MSDN
manipulate
endl
插入一行并且清空缓冲区,其耗时大于'\n'
因此需要高精度的时候尽量减少endl
的使用
ws
跳过所有的空白符知道发现另一个char
boolalpha
用于打印bool
值“true”或是“false”
hex
输出十六进制数字
setpercision
自动调整打印数据的精度
setw()
我们可以使用其来进行填充输出(使用空格进行填充,填充到括号中数字的长度,默认填充在输出左边,需要填充在右边可以加一个left)
1 |
|
setfill()
使用我们指定的内容进行填充,同理于setw
Containers
Initialization
对于C++来说有许多种方式进行初始化,对于下面的代码我们可以看到其初始化因括号不同而产生不同结果
1 |
|
STL
Sequence Containers
其提供对序列元素的访问,通常包含以下几种
1 |
|
Stack:将vector/deque
的功能限制在了push_back
和pop_back
Queue:将deque
的功能限制在了push_back
和pop_back
Associative Conrainers
1 |
|
其使用键值来进行访问对应的数据,而不是通过下标
map/set
为有序排列,可以更快的遍历所有的的元素,unordered map/set
为无序排列,则可以更快的通过键值来寻找到对应的元素
Iterators
迭代器用于在容器上进行迭代,可以让我们在非线性容器中获取对应值
1 |
|
在顺序容器下的迭代器可以简单的理解为指针
1 |
|
当我们需要获取某个结点的时候我们不再是使用下标进行访问,而是使用迭代器,获取到对应的begin()
属性后加上对应的偏移值来获取到相关数据
1 |
|
但是你会发现对于向量我们可以直接使用加法来获取对应的offset
,而对于列表却无法使用,这和我们之前提到的迭代器应该是通用的不一样。实际上迭代器拥有几个不同的类型(Input、Output、Forward、Bidirectional、Random Access),他们之间拥有共同点也有不同之处
相同的是他们都可以同现有的迭代器中进行创建,并使用++
来进行自增,以及使用==
或是!=
来进行比较。
Input
用于顺序,单通道输入。同时也是只读属性,即只能在表达式的右侧解引用。
如:find
、count
Output
用于顺序,单通道输入。同时也是只读属性,即只能在表达式的左侧解引用。
如:copy
Forward
与组合输入和输出迭代器相同,可以多次自增。可读可写(如果不为const
迭代器时)
如:replace
Bidirectional
双向迭代器与前向迭代器相同,但是可以进行自减操作
如:reverse
、std::map
Random Access
随机迭代器与双向迭代器相同,可以使用任意量递增或递减
如:std::vector
、std::deque
、std::string
Templates And Functions
Templates
当我们需要进行一个通用的函数操作时,对应的只有变量类型不同,我们可以将其抽象为一个模板类,以下面代码为例:
1 |
|
我们最直观的可以看到两个函数就只有变量的类型不同,其他的都一样,我们将其抽象为一个模板类如下:
1 |
|
需要注意的是我们需要告诉其为模板类,需要加一个template
lambda
lambda
实际上是一个非常轻量级的函数,一般来说其用于定于匿名函数,使得代码更加灵活,lambda
表达式与普通函数类似,也有参数列表、返回值类型和函数体,只是它的定义方式更简洁,并且可以在函数内部定义,一般写法如下:
1 |
|
其中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 |
|
Functions and Algorithms
Predicate
我们可以使用一个谓词来进行描述我们需要进行的操作,通过其返回值进行相应的操作
1 |
|
我们以某个数字在容器中出席那的次数为例
1 |
|
predicate 仅为一个函数代指名,可以更换为其他函数名称
本质上谓词便是我们从外部引进一个函数指针到模板类中方便我们对其进行我们所需要的操作
当我们想要实现一个二元谓词时,但是在我们定义的模板类却是一元谓词时我们可以考虑使用Lambda函数来作为我们的函数指针进行传入
如我们想要实现两个数比较大小的一个谓词函数,并将其进行通用泛化
1 |
|
当出现上述情况时我们想将其通用泛化,若干我们构建下面函数时则需要我们输入两个变量,此时变为二元谓词
1 |
|
而我们的原来函数只有一个输入变量,我们需要从二元谓词转换为一元谓词时我们采用lambda函数转换如下
1 |
|
需要注意一下C++11不支持上述写法,我们可以使用C++17或以上的C++版本
Algorithm
只选取一部分,更多可以参考 cppreference.com
std::sort
通常我们传入相应的初始迭代器以及结束的迭代器,告述其我们需要排序的范围,同时我们需要传入对应的比较方式,告诉其排序的方法
1 |
|
std::nth_element
需导入
#include <algorithm>
将第n个元素放到它的正确位置
1 |
|
相当于sort的一种变形,与之不同的是sort会对全部内容进行排序,相对耗时较大,而此函数相当于直接获取对应元素位置的值,同样的我们可以加一个lambda
函数来确定对应的排序方式
std::stable_partition
重排序范围[first, last)
中的元素,使得所有谓词p
对其返回true
的元素先于谓词p
对其返回false
的元素,同时保持元素的相对顺序
1 |
|
同样的我们使用lambda
函数来确定对应的排序方式,我们将大于0
的元素返回值定义为true
,因此为true
的元素会在排序后放在小于等于0
的元素的前方
std::copy_if
需要导入
#include <numeric>
其用于在一定范围内堆所有满足谓词的所有元素复制到另一个函数中
1 |
|
我们将src_vector
中的变量值大于13的进行拷贝到det_vector
中
std::remove_if
remove_if
的参数是迭代器,前两个参数表示迭代的起始位置和这个起始位置所对应的停止位置。最后一个参数:传入一个回调函数,如果回调函数返回为真,则将当前所指向的参数移到尾部。返回值是被移动区域的首个元素
1 |
|
其将返回的是被以东区域的第一个元素,意味着后面的空间任然存在,当我们想要删除后面的多余空间时我们可以使用erase
来进行删除操作
1 |
|
Classes and Const Correctness
class
对于类来说纯在有构造函数与析构函数两个默认函数,构造函数用来对类进行初始化的工作,而析构函数则是对类的一些资源进行释放的一个过程
构造函数构建如下:
1 |
|
构造函数在创建新对象时初始化其状态。同时其没有指定返回类型,为隐式地“返回”新建对象
析构函数构建如下:
1 |
|
通常析构函数再程序释放对象时使用
Const
当我们编写了一个确定不会被修改的类后我们可以考虑使用const
以防其被修改,通过编译器的报错我们可以很清楚的了解到对应的错误信息。同时当我们提供API时,客户也可以明确的明白对应传入的应该是一个不可修改的变量,以防某些数据被修改
同时注意一下const
指针的使用,确定其指针为const
与否与指向的是否为const
1 |
|
auto 对于 & 符号以及 const 不关心,意味着这两者都可以使用auto来进行替代
Operators
operator overloading
C++中支持运算符重载,意味着我们可以将一个运算符的运算规则进行拓展,将两个基本变量外的变量进行操作运算等操作
new、new[]、delete、delete[] 实际上也是运算符,也可以同样的被重载
我们可以尝试将+
进行重载处理,使其类似于append
的效果
1 |
|
Move Semantics
emplace_back
与push_back
十分相似,通常我们需要将元素加入到容器尾都是使用push_back
来进行完成的,emplace_back
也是完成同样的工作,但是对于push_back
来说通常需要我们创建一个变量,将值保存到其中,之后再使用push_back
来将其进行添加,而empalce_back
则解决了这一痛点
1 |
|
Deep copy and shallow copy
浅拷贝就比如像引用类型,而深拷贝就比如值类型。
浅拷贝是指源对象与拷贝对象共用一份实体,仅仅是引用的变量不同(名称不同)。对其中任何一个对象的改动都会影响另外一个对象。
深拷贝是指源对象与拷贝对象互相独立,其中任何一个对象的改动都不会对另外一个对象造成影响。
最典型的一个例子便是当我们在一个类中存在有动态分配的内存空间对象时,我们浅拷贝复制的值指向的动态内存地址与原被拷贝的地址值一样,当我们在调用析构函数进行内存释放的时候便会产生两次释放的问题,因而产生错误
当其中不存在指针之类的东西时,深浅拷贝没啥区别
std::mov
C++11的标准库<utility>
提供了一个非常有用的函数 std::move()
,std::move()
函数将一个左值强制转化为右值引用,以用于移动语义。
移动语义,允许直接转移对象的资产和属性的所有权,而在参数为右值时无需复制它们。因此通过std::move()
,可以避免不必要的拷贝操作
对于小型设备来说比较有效,可以使其工作效率快一倍
我们以一个交换函数为例
1 |
|
通常我们写的一个交换函数如上,我们可以使用move
来对其进行优化加速
1 |
|
Inheritance and Template Classes
Abstract Classes
如果一个类至少有一个纯虚函数,那么它就被称为抽象类(接口是抽象类的子集)。需要注意的是抽象类不能被实例化。
纯虚函数需要在声明之后加个
=0;
对于虚函数中存在有纯虚函数与非纯虚函数两类,对于抽象类来说至少有一个纯虚函数的类
1 |
|
当我们定义了一个抽象类时,当其被继承时,那么其子类需要对其进行实现
在以下情况下使用模板类:
- 运行效率是最重要的
- 没有共同的基础可以定义
在以下情况下选择派生类:
- 编译时效率是最重要的
- 想要隐藏实现
- 不想让代码膨胀
类型转换
在C++中,除了包含C语言转换方式,还添加了另外四个显式类型转换的语法,他们分别是:static_cast
、dynamic_cast
、const_cast
和reinterpret_cast
当我们的代码加入了explicit
来对构造函数进行修饰时,以为着我们只能显示调用,禁止隐式转换,此时我们便需要类型转换来完成显示转换,以下面代码为例:
1 |
|
添加编译选项
-Werror=conversion
也可以来禁止隐式转换
static_cast
用途最广泛,除了后面三种类型转换外,其他的类型转换都能使用static_cast
完成
dynamic_cast
主要用于运行时的从父类指针向子类指针转换,如果转换不成功则返回nullptr
const_cast
主要用于去除指针或引用类型的const
属性。此操作可能会导致未定义的行为,所以需要慎用
reinterpret_cast
可以将指针或引用转换为任何类型的指针或引用。其中reinterpret_cast
实现依赖于编译器和硬件,可能导致未定义的行为
Template Classes
模板类与我们之前提到的函数模板相似,其中函数模板描述了如何构建一系列外观相似的函数。
1 |
|
在模板类的继承中,需要注意以下两点:
- 如果父类自定义了构造函数,记得子类要使用构造函数列表来初始化
- 继承的时候,如果子类不是模板类,则必须指明当前的父类的类型,因为要分配内存空间
- 继承的时候,如果子类是模板类,要么指定父类的类型,要么用子类的泛型来指定父类
RAII and Smart Points
code path
一个代码执行的流程的个数成为代码路径的个数,我们以下面代码为例:
1 |
|
上面的代码有多少种代码路径呢?可能大多数人回答的是三种,当if
判断为真时便存在两种情况,还一种if
判断为假的情况。但是实际上其代码路径远远多于三种,这是因为我们没有考虑到发送异常的情况,当我们发生异常时可能回照成意想不到的结果,比如p->GetTitle()
返回的不为string
类型时产生的异常使其会跳出该函数,进而执行我们意想不到的指令流程,同时也导致了可能会引发内存泄漏,无法成功的运行到我们下面的delete
处
RAII
RAII
代表资源获取即初始化,其代表着所有的资源都应该在构造函数中获取,所有的资源都应该在析构函数中释放。
比如我们常见的文件打开操作,我们可以有以下两种写法:
1 |
|
对于第一种写法来说我们不需要对其进行写入close()
,因为ifstream
的析构函数帮我们对其进行了释放的工作,而下面则是需要我们自己来完成对应资源的释放。而第一种正好满足了我们RAII
思想的要求
Smart Points
智能指针是近代C++
最大的风格变化,使用智能指针可以帮我们实现RAII
思想
C++
帮我们构建了一些智能指针
1 |
|
当我们使用裸指针的时候需要我们进行释放内存
1 |
|
而我们可以使用智能指针来自动管理内存
1 |
|
回到我们最开始提到的内存泄露的问题上我们可以使用智能指针来进行替换对应原指针,来避免我们的内存泄露
1 |
|
Multithreading
代码通常是顺序执行的,但是线程可以并行完成一个工作
1 |
|
我们可以通过#include <thread>
来进行导入多线程相关的函数
1 |
|
只要我们创建了对象那么线程对象便开始运行,我们要想使用线程阻塞则是需要使用两种方式join()
与detach()
,我们使用阻塞主要是糖线程的执行顺序进行改变
t.join()
等待线程完成,使用t.detach()
分离线程,让它在后台运行
对于多线程来说需要注意线程安全的问题,当多个线程访问一个变量并且其中有至少一个线程对其进行了写操作便会产生数据竞争的问题,进而引发程序崩溃导致未定义的结果或是错误的结果
对于上述问题我们通常使用同步机制包括互斥量、条件变量、原子操作等来解决上述问题
Mutex
互斥量(mutex
)是一种用于实现多线程同步的机制,用于确保多个线程之间对共享资源的访问互斥。互斥量通常用于保护共享数据的访问,以避免多个线程同时访问同一个变量或者数据结构而导致的数据竞争问题
互斥量提供了两个基本操作:lock()
和 unlock()
。当一个线程调用 lock()
函数时,如果互斥量当前没有被其他线程占用,则该线程获得该互斥量的所有权,可以对共享资源进行访问。如果互斥量当前已经被其他线程占用,则调用 lock()
函数的线程会被阻塞,直到该互斥量被释放为止
1 |
|