背景
最近在嘗試入坑藍橋杯,於是先從 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); |