This part is still under construction.
§1 类型与结构体 Types & Structs
1.1 结构体 Structs
结构体将命名变量捆绑到一个新类型中。
1 | struct Order { |
上述例子可以使用 std::pair 代替:
1 | std::pair<std::string, int> dozen {"Eggs", 12}; |
1.2 std标准库
std是C++的标准库。C++ 标准库提供了大量预定义的类型、函数及相关组件。在使用标准库中的特定组件之前,需要通过 #include 预处理指令引入相应的头文件。
#include <utility>提供std::pair
我们需要在标准库中的语言名称前加上 std:: 前缀。如果我们使用 using namespace std; ,我们就不必这么做,但这被认为是不良的习惯,因为这会污染命名空间。比如,如果我们自定义了一个sort函数,编译器就难以区分标准库里的sort函数和我们自己的sort函数。
1.3 using 和 auto 用于简化代码
当类型名较长时,反复完整书写会降低代码的可读性与编写效率。可以使用 using 关键字为已有类型定义别名,从而简化复杂类型的表达。
1 | //不使用using关键字 |
同时,auto 关键字让编译器根据数据自己选择合适的数据类型。需要注意,auto只在定义初始决定好数据类型,后续无法更改。
1 | //延续上述例子 |
§2 初始化与引用 Initialization & References
2.1 初始化 Initialization
直接初始化(direct initialization):初始化的值如果不符合定义的类型,则强制转换。
1 | int numOne = 12.0; |
统一初始化(uniform initialization):初始化的值必须符合定义的类型。
1 | int numOne = {12}; |
结构化绑定(Structured Binding):从固定大小的数据结构中初始化多个变量。常用于访问函数返回的多个值。该数据结构的大小必须在编译时已知。
1 | std::tuple<std::string, std::string, std::string> getClassInfo() { |
2.2 引用 References
引用就是给已经存在的对象或函数起一个别名,内存地址和引用对象完全相同,通过引用可以原地改变实参的值。引用可用于函数传参等。
1 | void shift(std::vector<std::pair<int, int>> &nums) { |
2.3 使用命令行进行.cpp文件的编译(compile)
使用g++编译器按照C++23的标准将main.cpp编译成程序:
1 | g++ -std=c++23 main.cpp -o main |
如果是多文件程序,要把所有.cpp文件写在main.cpp的位置。
在Linux或macOS中,运行编译好的文件指令为:
1 | ./main |
在Windows上运行,则指令为:
1 | .\main.exe |
§3 流 Stream
3.1 流的定义
流(stream)是C++的一种通用的输入输出抽象模式,提供了一种统一的处理外部数据的方式。
cout & cin输出:(略)
fout & fin输出:
1 | //create a file called "data.txt" |
ios_base是所有流的基础,用于维护状态信息、控制信息,确保信息流正常。状态信息告诉人们流是否正常,比如failbit提示存在逻辑错误,eofbit提示已到文件末尾;控制信息表示应该如何输出数据,比如dec、hex、oct等。
ios_base的组成如下:


输入流(input streams)是一种从源读取数据的方式,继承自 std::istream ,基本运算符是 >> ;输出流(output streams)是一种向目标输出数据的方式,继承自 std::ostream ,基本运算符是 << 。istream 和 ostream 的交叉部分称为 iostream ,这部分流同时具有输入流和输出流的特征。
3.2 stringstreams 字符串流
stringstreams是把string字符串当作流处理的一种方式,适合处理混合数据类型的用例。字符串输出流为 std::ostringstream ,字符串输入流为 std::istringstream 。
1 | void foo() { |
<< 和 >> 读取字符直到任何空格为止,这些空格包括 / \n / \t / \r / \f / \v 等。
std::getline 从输入流is中读取字符到str中直到读取到delim字符。默认情况下,delim字符设置为 \n (即换行符)。特别注意:getline 函数会读取delim字符。
1 | istream& getline(istream& is, string& str, char delim); |
3.3 输出流 Output Stream
缓冲区:输出流中的字符会先存储在缓冲区,然后再刷新在目标位置。
刷新的时机:
std::cout << std::flushstd::cout << std::endl- 当程序到达末尾时
- 当缓冲区满了时
- 当相连的流互相作用时(例:在使用cin输入前,输出流必须刷新)
cerr 被用于立即输出程序的错误信息,该信息不会存在缓冲区里;而 clog 被用于输出程序的错误信息,该信息会存在缓冲区里。后者常用于非必要错误信息的排查。
REMINDER: 由于 endl 会给输出流缓冲区带来不必要的刷新,因此若只是换行建议使用 \n 。
文件输出流:把数据写入文件的方式。输出流为 std::ofstream
创建文件输出流的方式:std::ofstream out("file.txt", file_flag)
其中,file_flag 表示创建输出流的模式,主要有三种:
std::ios::trunc清空文件,从头写,是默认设置std::ios::app不清空,追加至末尾std::ios::ate打开后光标直接追加至末尾
文件输出流使用举例:
1 | int main() |
文件输入流的用法和文件输出流完全对称。
3.4 输入流 Input Stream
混合型数据类型输入时,尽量不要混用cin和getline。
1 | void cinGetline() { |
当 getline 函数进行输入时,由于会读入 \n ,因此第一行 getline 读入的是上个输入在缓冲区遗留的换行符,第二个 getline 才会读入字符串。
§4 容器 Containers
4.1 STL (Standard Template Library)
STL是C++中非常实用的一个标准库,隶属于std标准库。STL包括四个部分:容器(containers)、迭代器(iterators)、函子(functors)和算法(algorithm)。
4.2 顺序容器 Sequence Container
顺序容器实现了可以按顺序访问的数据结构,如 vector / deque / array / list 。
vector容器:可调整大小的连续数组(动态数组)。
访问vector元素:
1 | vector<int> vec(13); |
vector是如何调整大小的?当
push_back时发现size == capacity,vector 会申请一块更大的连续内存,把旧元素搬过去,再插入新元素,然后释放旧内存。时空复杂度是O(1)。一般而言,vector申请的连续内存是翻倍的。但这不是C++的强制规定。
另:
vector::insert插入中间元素时,为了保持连续存储和元素顺序,必须移动插入点之后的元素,所以它通常是O(n),不像数组下标访问那样是O(1)。
deque容器:有两端的队列。同时存在 push_back 和 push_front 。
deque容器是如何演化的?
Map(控制块)
deque 的核心可以理解为一个“指针的动态数组”。这些指针,也就是图中紫色的小格子,并不直接存放你的数据,而是存放 数据块(Data Blocks) 的内存地址。正因为如此,deque 的数据在内存中不需要像 vector 那样完全连续存放。
头尾 O(1) 增长
当你执行
push_front或push_back时,deque 会先检查当前边缘的数据块是否还有空位。如果没有空位,它就会分配一个新的固定大小的数据块,并把这个新数据块的地址放入 Map 中下一个可用的位置。因为这个过程中只需要新增一个指针,已有元素本身并不会在内存中移动。所以 deque 在增长时,指向已有元素的指针和引用通常仍然有效。
重新居中的增长方式
如果你一直向同一侧插入元素,最终会到达控制块 Map 的边界。这时 deque 不会把所有实际数据整体移动,而是会分配一个更大的指针数组,并把原来的指针复制到新数组的中间位置。
这种“双重间接寻址”机制保证了 deque 可以继续向两端增长,同时又避免了对实际数据进行大规模搬移。
随机访问的计算方式
当访问
deque[i]时,CPU 并不会从头开始一个一个遍历。它会通过计算确定元素所在的数据块和块内位置:
1
2 block = (i + offset) / block_size;
cell = (i + offset) % block_size;其中
block用来找到 Map 中对应的数据块指针,cell用来找到该数据块内部的具体元素。和 vector 的一次寻址相比,deque 通常需要两次跳转;但它同时保留了类似链表的两端灵活增长能力,以及接近数组的随机访问速度。
4.3 关联容器 Associative Container
关联容器实现了可以快速排序的有序数据结构。如:map / set
map容器:储存键值对,并按键排序。
即使没有定义键值对,编译器也会把未定义的键赋予一个默认值(通常为0)。
std::map底层通常是一棵红黑树。插入 key-value 对时,map 会按照 key 的大小把节点放进树里,并通过旋转和变色保持平衡,所以它能自动有序,并且查找、插入、删除通常都是O(log n)。
set容器:没有值的map,储存不重复的元素并排序。
4.4 无序关联容器 Unordered Associative Container
就是关联容器的无序版本,通常来说性能更快。如 unordered_set / unordered_map 。
底层实现是哈希表,因此时间复杂度更快。
总结:STL各容器的算法速度比较

§5 迭代器 Iterators
5.1 迭代器基础知识
迭代器是STL中用于遍历容器的通用位置对象。
1 | container.begin() //指向容器第一个元素的迭代器 |
迭代器的数据类型由所属的容器决定:
1 | std::map<int, int> m{{1, 2}, {3, 4}}; |
5.2 迭代器的种类
输入迭代器(input iterators):最基础的迭代器,允许读取元素。
特点:只能单次读取,只读,不可回退。
1 | int main() |
如果指向的元素是结构体,可以使用operator -> 来访问元素。
输出迭代器(output iterators):允许写入元素的迭代器。
1 | int main() |
1 | int main() |
前向迭代器(forward iterator):可以从前往后一个一个遍历元素,并且可以重复遍历同一段数据的迭代器。
特点:可以多次读取,只读,不可回退。
注意:对于流来说,前向迭代器容易导致错误,所以不要在流中使用前向迭代器。
1 | //支持的语法 |
双向迭代器(bidirectional iterator):可以从前往后遍历,也可以从后往前退回去的迭代器。
典例:std::map / std::set 的迭代器。
1 | //支持的语法 |
随机访问迭代器(random access iterator):可以像数组下标一样直接跳到任意相对位置的迭代器。
典例:std::vector / std::deque
1 | //支持的语法 |
为什么迭代器的类型很重要?
- 一些内部储存的算法需要特定的迭代器类型才能实现;
- 容器的底层实现方式决定了迭代器的种类。随机访问迭代器适用于顺序容器,因为访问很快;相比之下,关联容器的随机访问就慢很多,所以关联容器一般采用双向迭代器。
§6 类 Classes
6.1 类的定义
面向对象编程(OOP, Object-oriented Programming):以对象为中心的编程方式,专注于类的设计和实现。
类就是用户自定义的可以被声明为对象的类型。类包含一系列不同类型的对象、一组用于操作这些对象的函数,以及一组对这些对象和函数的访问限制。
容器就是在STL中被定义的类。
类包含 public 和 private 两个区域,public 是可以被外界访问的,private 只能被类里的元素访问。
构造函数(constructor):初始化新产生的类的函数。
自定义构造函数:如果类定义在头文件,则 .h 文件写:
1 | class MyID { |
在 .cpp 文件写声明:
1 | MyID::MyID(std::string name, std::string sunet, int idNumber) { //注意声明域 |
也可以(更推荐)使用成员初始化列表:
1 | MyID::MyID(std::string name, std::string sunet, int idNumber) { |
析构函数(destructor):在类执行完周期后,销毁类内变量的函数。
一般析构函数不会被显式调用,而是自动调用,当对象超出作用域的时候就会被调用。
1 | MyAnotherID::~MyAnotherID() { |
6.2 类的继承 Inheritance
类的继承指让一个子类基于父类已有的成员和接口继续扩展或改写功能,从而表达“子类是一种父类”的关系。
类的继承的两个作用:
1.动态多态:不同类型的对象可能需要同样的接口。
2.可扩展性:可以先写一个比较抽象的父类,然后再写更具体的子类。
类的继承举例:(一般写在头文件里方便使用,不过也能写在.cpp文件里)
1 | class Shape { |
上文中的 class Circle : public Shape 里的 public 指的是类的继承类别。类别主要有 public / private / protected 三类,它们的区别是:
| 继承方式 | 父类 public 成员到子类里 | 父类 protected 成员到子类里 | 父类 private 成员 |
|---|---|---|---|
public 继承 |
还是 public | 还是 protected | 子类不能直接访问 |
protected 继承 |
变成 protected | 还是 protected | 子类不能直接访问 |
private 继承 |
变成 private | 变成 private | 子类不能直接访问 |
6.3 虚继承 Virtual Inheritance
菱形继承问题:假设子类B和C都继承自父类A,一个类D又同时继承B和C,A、B、C都定义了一个 func() 函数,那么在D中,如何定义 func() 函数不会导致混淆?
答:引入虚继承。虚继承的意思是:最终派生类,也就是这里的 D,对于共同的基类 A,应该只保留一份实例,而不是通过 B 和 C 各继承一份。此处,应该让B和C变为虚继承:
1 | class A { |
结合虚拟性的多态性意味着我们可以获得动态类型。当父类指针/引用指向子类对象,并且调用的是 virtual 函数时,程序不会只看“变量声明成什么类型”,而会在运行时看“它实际指向的对象是什么类型”,然后调用对应子类的函数。
§7 类模板 Template Classes
7.1 类模板
类模板(class template)是用于生成类的模板。通过类模板,我们可以用不同的数据类型实例化出结构相似、功能相近的不同类。STL 中的 std::vector 就是一个经典例子:std::vector 本身是一个类模板,通过 vector<int>、vector<string> 等形式,可以生成用于存储不同数据类型的动态数组类。
1 | template <typename T> |
同时,非类型模板参数(non-typename template parameters)是被允许存在的。比如,有时我们会选择使用 std::array 而不是 std::vector ,为了避免堆内存分配。
1 | template<typename T, std::size_t N> |
类模板的几个注意事项:
在头文件定义的类模板,在
.cpp文件里需要再次声明template语句。如:若在头文件写
1
2
3
4
5template <typename T>
class Vector {
public:
T& at(size_t i);
}那么在
.cpp文件中就要写1
2
3
4template <typename T>
T& Vector<T>::at(size_t i) { //留意这一行的Vector<T>
// Implementation...
}对于非类型模板参数而言,cpp文件需要引用头文件;对于类型模板参数而言,头文件需要在结尾引用cpp文件。如,上例中头文件需要在末尾写上:
1
在模板参数列表中,
typename和class是等价的。(但更建议写前者,易于区分)
7.2 常量正确性 Const correctness
const 关键字让被修饰对象无法被修改。
从设计上讲,设计类的时候,给不会改变对象的函数或方法最好加上 const 。如:若cpp文件定义函数:
1 | void printVec(const Vector<int>& v) { //注意此处也有const |
则头文件要声明:
1 | template<class T> |
加上 const 关键字,是在告诉编译器,该函数或方法不会改变类内的对象。未加常量修饰的函数不能作用于加了常量修饰的对象。
此处会自然地产生一个问题:那么,加了
const关键字的函数如果要对对象作出修改,怎么办呢?以下有三种方法:
使用
const重载(overloading)定义两个该函数,一个修饰,一个不修饰,这样编译器会自己选择使用哪种。
1
2
3
4
5
6
7
8
9 template <class T>
class Vector {
public:
const T& at(size_t index) const;
//上面这行代码的两个const的意义是:
//第一个const修饰返回值,表示:返回的元素不能被改。
//第二个const修饰成员函数,表示:这个函数不能修改当前对象,并且可以被const对象调用。
T& at(size_t index);
}使用
const_cast该函数可以临时强制将某个值转换成常量,可以绕过编译器。不过一般来说非常少使用这种方法,如果这种函数需要改变对象,那就一开始别加常量修饰。
1
2
3
4
5
6
7
8
9
10
11 template <typename T>
T& Vector<T>::findElement(const T& value) {
for (size_t i = 0; i < logical_size; i++) {
if (elems[i] == elem) return elems[i];
}
throw std::out_of_range("Element not found");
}
template <typename T>
const T& Vector<T>::findElement(const T& value) const {
return const_cast<Vector<T>&>(*this).findElement(value);
}使用
mutable关键字
mutable可以绕过const关键字。这种方法一般用于debug。
1
2
3
4
5
6 struct MutableStruct {
int dontTouchThis;
mutable double iCanChange;
}
const MutableStruct cm;
cm.iCanChange = 3.14;
Comments