背景
最近在尝试入坑蓝桥杯,于是先从 C++ 开始学起,这里记个笔记。这里我的笔记是跟着这个教程来的。沙比学校天天整些屁事都没什么空折腾。
前言
笔者是 JS / TS 写的比较多,以前写过 C 但是有点忘了,所以文章里都是和 JS 进行对比着方便快速理解。
同时其实我还有几个小问题,嘻嘻。没有解答,就是写文章的时候提醒一下自己要全面一点,复习哈。
- 指针的
*
和&
是什么用?指针的本质是? - 和 JS 比有没有类似的,异同点是?
- C++ 和 C 有什么区别?
- 本质上迭代器和数组索引遍历是类似的,但是为什么要单独抽象一个迭代器出来?
复习
指针
- 声明指针:
int *p
; 表示一个指向 int 类型的指针。 - 取地址:
&x
获取变量 x 的内存地址。 - 解引用:
*p
访问指针 p 指向的内存地址上的数据。
#include <iostream>
using namespace std;
int main() {
int x = 10;
int *p = &x; // p 存储的是 x 的地址
cout << "x 的值是: " << x << endl; // 输出 10
cout << "p 指向的值是: " << *p << endl; // 解引用,输出 10
return 0;
}
其实就是内存和值互相指来指去的,JS 里面没有可以类比的,要说比较像的就是对象赋值,举个例子:
let obj = { value: 10 };
let ref = obj; // ref 是 obj 的引用
ref.value = 20;
console.log(obj.value); // 输出 20
JS 里面如果要避免这种情况只能深拷贝了~ 因为默认的赋值方式(如 let ref = obj
)只是将对象的引用赋值给新的变量,并没有创建新的独立对象。
STL
概念
STL 是一个 C++ 的工具库,分为三大组件,分别是容器、算法、迭代器。
容器
常见容器类型有三种,第一种是序列式容器(即元素按顺序存储,支持高效的插入和删除)。常见的有这几个:vector
,动态数组,支持随机访问;deque
,双端队列,支持高效的两端操作;list
,双向链表,支持高效的任意位置插入和删除。
第二种是关联式容器(基于键值对存储,元素通常自动排序)。常见的有这几个:set
,集合,不允许重复元素;map
,键值对映射,每个键唯一;multiset
和 multimap
,允许重复键。
最后一种是无序容器(基于哈希表存储,元素无序)。常见的有unordered_set
,无序集合;unordered_map
,无序映射。
其实换句话说 JS 基本都有类似功能的方法/库,比如说 Map 和 Set 都是自带的,JS 里面直接实例化后即可使用。虽然实现方式还是有差异,STL 的 Map 和 Set 底层通常用平衡二叉树实现,默认是有序的;而 JS 是基于 HashMap 实现的。
另外 C++ 还存在 auto
这个类型,即编译器自动推断变量类型,被叫做“类型推导关键字”,意思是让编译器推导变量的具体类型。如果你是 JS 或者 Python 写的比较多的应该马上觉得很熟悉了,这不是动态语言么?
虽然差距很大,C++ 的 auto 只是编译器编译时推到,执行后其实已经是静态了(auto x = 4
编译完会变成int x = 4
,某种程度上和#define
接近);而 JS / Python 这类动态语言在 Runtime 的类型就是动态的。
算法
算法是用来操作容器的工具集,包括排序、搜索、变换等常见操作。STL 的算法库与任何容器兼容。
常见算法有很多,如下。
- 排序类:
sort()
,stable_sort()
。 - 查找类:
find()
,binary_search()
。 - 变换类:
transform()
,replace()
。 - 集合操作:
set_union()
,set_intersection()
。
算法使用迭代器(见下文)作为操作对象,不直接与容器绑定,因此非常灵活。
其实看到这也非常眼熟,就比如 sort() 这个在 JS 的 Array 对象里最常见的方法,原来这个在 C++ 这种老登(bushi)语言里被叫统称为算法啊!不过 STL 和 JS 的 sort() 还是有些许不同的。STL 的 sort() 是一个函数,可以对任意范围内的元素排序,不仅限于容器,比如数组、列表都可以用。
而 JS 的 sort() 仅仅只是数组的一个方法,只针对数组排序;STL 的 sort() 的实现底层使用了快速排序 + 插入排序,比 JS 的 sort() 更快。
迭代器
迭代器是一种指针类似的对象,用于在容器中遍历元素。迭代器分类如下:
- 输入迭代器:只读访问,例如
std::istream_iterator
。 - 输出迭代器:只写访问,例如
std::ostream_iterator
。 - 前向迭代器:支持单向遍历,例如
std::forward_list
的迭代器。 - 双向迭代器:支持前后遍历,例如
std::list
的迭代器。 - 随机访问迭代器:支持随机访问,例如
std::vector
的迭代器。
一般常见的就两种,一种是拿 begin() 和 end() 用来返回容器的起点和终点迭代器,另外一种是用 *it
访问迭代器指向的值,++it
进行移动。
迭代器可以理解为一种“指针”,它用来在容器中遍历元素,其实这不就是遍历数组里面的值啊!但为什么弄这么复杂?因为 STL 容器不能像数组索引那样直接遍历,STL 容器都是各种各样的数据结构,不像数组那样连续,所以只能使用指针来遍历里面的值。
深入一点来说,数组在内存中的数据是连续的,你通过数组索引访问的时候,其实就是arr[i] = *(arr + i)
,arr 是这个数组的基准地址,i 就是往后推几位,数组索引本质上就是最开始的地址 + 索引数;而 STL 本身就是高层抽象的东西,STL 容器包含了很多种数据结构,每种数据结构的访问方式都是不一样的,所以 STL 库把迭代器给抽象了出来,最终作为一种库给开发人员调用。
迭代器其实就是一种抽象。它的实现完全依赖于容器的底层数据结构。如果容器是连续的(如 vector),迭代器可以直接封装指针操作;如果容器是非连续的(如 list、set),迭代器内部会包含指向当前元素的指针,同时封装了访问下一个/上一个元素的逻辑。对于复杂的数据结构(如红黑树),迭代器会依赖树的中序遍历算法来实现移动。
另外,STL 中的迭代器不仅仅是用来遍历的工具,还能通过它修改容器内容,甚至对任意范围的元素进行操作。
所以迭代器其实就是面向容器的“智能索引”,它不是简单的指针,而是高度抽象和适配不同数据结构的工具。
Vector
Vector 是 STL 容器的一种,全称叫可变数组,但如果你熟悉 JS 你就会发现这不就是里面的数组吗。STL 里面的 vector 的特性和 JS 里面很像哦,支持动态扩容(数组大小可以自动调整)、连续内存存储(可以高效地进行随机访问)、高效尾部操作(尾部插入和删除操作非常快)。感觉这里没啥好说的就纯看语法写写就好了,放一段 Example。
#include <iostream>
#include <vector>
#include <algorithm>
using namespace std;
int main() {
vector<int> v = {3, 1, 4, 1, 5};
v.push_back(9); // 添加元素 9
sort(v.begin(), v.end()); // 排序
cout << "排序后的元素: ";
for (auto num : v) {
cout << num << " ";
}
cout << endl;
v.pop_back(); // 删除最后一个元素
cout << "大小: " << v.size() << endl;
return 0;
}
问了下 AI,vector 一般在算法里有啥地方用得到?他说这三个:
- 在需要动态增长的数组时使用,例如存储所有输入数据。
- 排序和去重,常用 sort + unique 去重。
vector<int> v = {1, 2, 2, 3, 3, 4};
sort(v.begin(), v.end());
v.erase(unique(v.begin(), v.end()), v.end());
- 二分查找,用 lower_bound 和 upper_bound 高效查找元素位置。
vector<int> v = {1, 2, 4, 4, 5};
int pos = lower_bound(v.begin(), v.end(), 4) - v.begin(); // 找到第一个 >= 4 的位置
然后这个是语法:
操作 | 说明 | 示例代码 |
---|---|---|
vector<T> v; |
定义一个存储类型为 T 的动态数组 |
vector<int> v; |
v.push_back(x); |
在尾部添加元素 x |
v.push_back(10); |
v.pop_back(); |
删除尾部元素 | v.pop_back(); |
v.size(); |
获取数组大小 | int n = v.size(); |
v[i] / v.at(i) |
访问第 i 个元素(at 带边界检查) |
cout << v[0]; |
v.clear(); |
清空所有元素 | v.clear(); |
v.begin() / v.end() |
返回首迭代器和尾迭代器,用于遍历 | for (auto it = v.begin(); it != v.end(); ++it) |
v.insert(pos, x); |
在迭代器位置 pos 插入元素 x |
v.insert(v.begin(), 5); |
v.erase(pos); |
删除迭代器位置 pos 的元素 |
v.erase(v.begin()); |
sort(v.begin(), v.end()) |
对 v 进行升序排序 |
sort(v.begin(), v.end()); |
Set
set 是 STL 中的 集合容器,JS 里也有,特性一样:存储唯一值(不允许重复元素)、自动排序(默认以升序存储)、基于红黑树实现(插入、删除、查找的时间复杂度为 O(log n)
)。还是丢一段 Example。
#include <iostream>
#include <set>
using namespace std;
int main() {
set<int> s = {10, 20, 10, 30}; // 自动去重和排序
s.insert(40); // 插入元素 40
s.erase(20); // 删除元素 20
cout << "集合中的元素: ";
for (auto num : s) {
cout << num << " ";
}
cout << endl;
if (s.count(30)) {
cout << "30 存在于集合中" << endl;
}
return 0;
}
Set 常用方法有这几个:
- 高效查找和去重,集合自动去重,插入后元素总是唯一。
- 范围查找,用迭代器找到范围内的值。
auto it = s.lower_bound(15); // 找到第一个 >= 15 的元素
- 动态集合,用于实时存储和查询,如滑动窗口。
这个是语法:
操作 | 说明 | 示例代码 |
---|---|---|
set<T> s; |
定义一个存储类型为 T 的集合 |
set<int> s; |
s.insert(x); |
插入元素 x ,如果已存在则插入失败 |
s.insert(10); |
s.erase(x); |
删除值为 x 的元素 |
s.erase(10); |
s.count(x); |
检查是否存在值为 x 的元素(返回 1 或 0) |
if (s.count(5)) |
s.find(x); |
返回指向值为 x 的迭代器(找不到返回 s.end() ) |
auto it = s.find(10); |
s.size(); |
获取集合大小 | int n = s.size(); |
s.begin() / s.end() |
返回首迭代器和尾迭代器,用于遍历 | for (auto it = s.begin(); it != s.end(); ++it) |
s.clear(); |
清空集合中的所有元素 | s.clear(); |
s.lower_bound(x); |
返回指向第一个 >= x 的迭代器 |
auto it = s.lower_bound(15); |
s.upper_bound(x); |
返回指向第一个 > x 的迭代器 |
auto it = s.upper_bound(20); |