跳到內容

C++ 基礎速查

C++ 在 C 的基礎上,多了會自己管理記憶體的工具:std::string、std::vector 這類容器,以及物件離開範圍時自動執行的解構子。善用這些工具,大部分情況就不必自己 malloc / free。C 和 C++ 的關係見C 與 C++ 的關係。

#include <iostream>
#include <string>
#include <vector>
int main() {
std::vector<int> v = {3, 1, 2};
std::string name = "Roy";
std::cout << name << " has " << v.size() << " numbers\n";
return 0;
}

標準函式庫的東西都放在 std 命名空間裡,所以要寫 std::cout。很多範例會在開頭寫 using namespace std; 省略 std::。小程式這樣寫沒問題,但它會把 std 裡上千個名字全部帶進來,容易和自己取的名字衝突(例如 count、max),所以比較大的程式不建議這樣寫。

寫法作用
std::cin >> x;讀一個值,會自動跳過空白和換行
while (std::cin >> x)讀到輸入結束為止
std::getline(std::cin, line);讀一整行,包含空白
std::cout << x << '\n';輸出
std::cout << std::fixed << std::setprecision(2) << d;小數點後 2 位(需要 <iomanip>)
std::setw(2) << std::setfill('0')寬度和補字元(setw 只對下一個輸出有效)
  • '\n' 和 std::endl 的差別:std::endl 除了換行,還會把緩衝區清空(flush),輸出很多行時比較慢。一般用 '\n' 就好。
  • 讀很多資料時可以在 main 開頭加上 std::ios::sync_with_stdio(false); std::cin.tie(nullptr);,在很多平台上能讓 cin / cout 快不少。加了之後就不能再混用 scanf / printf。
  • cin >> x 讀完數字後,換行字元還留在輸入裡。如果接著用 getline,會讀到一個空行,要先用 std::cin.ignore() 把它丟掉。
string_ops.cpp
// std::string:會自己管理記憶體的字串,不用擔心空間不夠。
#include <iostream>
#include <string>
int main() {
std::string s = "hello";
s += ", world"; // 串接
std::cout << s << " (size " << s.size() << ")\n";
std::cout << "substr(7, 5) = " << s.substr(7, 5) << '\n';
std::cout << "find(\"world\") = " << s.find("world") << '\n';
std::cout << "s == \"hello, world\" ? " << (s == "hello, world" ? "yes" : "no") << '\n'; // 可以直接用 == 比較內容
if (s.find("xyz") == std::string::npos) { // 找不到時回傳 npos
std::cout << "\"xyz\" not found\n";
}
return 0;
}

在這台電腦執行的結果(Windows x64、clang 23、-O2,沒有開 Sanitizer):

執行結果
hello, world (size 12)
substr(7, 5) = world
find("world") = 7
s == "hello, world" ? yes
"xyz" not found
在 Compiler Explorer 開啟 ↗可以線上修改、執行;那邊是 Linux,有些結果會和 Windows 不同
操作寫法
長度、是否為空s.size()、s.empty()
串接a + b、s += "x"、s.push_back('c')
取字元s[i](不檢查範圍)、s.at(i)(超出範圍會丟例外)
子字串s.substr(起點, 長度)
尋找s.find("abc"),找不到回傳 std::string::npos
比較==、<(字典順序)
數字和字串互轉std::to_string(42)、std::stoi("42")、std::stoll(...)

std::string 會自己管理記憶體,不用擔心空間不夠,也不用自己補 '\0'。

參考是變數的別名。函式用參考接參數,就能直接修改呼叫者的變數:

references.cpp
// 參考 (reference) 就是變數的「別名」:函式用參考接參數,就能修改呼叫者的變數。
#include <iostream>
#include <string>
void swap_by_value(int a, int b) { int t = a; a = b; b = t; } // 改的是複本,呼叫者看不到
void swap_by_ref(int &a, int &b) { int t = a; a = b; b = t; } // 改的是本人
void swap_by_ptr(int *a, int *b) { int t = *a; *a = *b; *b = t; } // C 的做法
// const 參考:不複製、也保證不修改,傳大型物件(例如 string)時常用
std::size_t length(const std::string &s) { return s.size(); }
int main() {
int x = 1, y = 2;
swap_by_value(x, y);
std::cout << "by value: x = " << x << ", y = " << y << '\n';
swap_by_ref(x, y);
std::cout << "by reference: x = " << x << ", y = " << y << '\n';
swap_by_ptr(&x, &y);
std::cout << "by pointer: x = " << x << ", y = " << y << '\n';
std::cout << "length = " << length("a very long string") << '\n';
return 0;
}

在這台電腦執行的結果(Windows x64、clang 23、-O2,沒有開 Sanitizer):

執行結果
by value: x = 1, y = 2
by reference: x = 2, y = 1
by pointer: x = 1, y = 2
length = 18
在 Compiler Explorer 開啟 ↗可以線上修改、執行;那邊是 Linux,有些結果會和 Windows 不同
參數寫法會複製嗎能修改呼叫者的變數嗎什麼時候用
int x(傳值)會不能小型別(int、double)
int &x(參考)不會能要修改外面的變數
const std::string &s(const 參考)不會不能傳大型物件,只讀不改
int *p(指標)只複製位址能(透過 *p)C 的寫法;可以是 nullptr
auto x = 3.14; // 讓編譯器推導型態(double)
for (int v : numbers) { ... } // 範圍 for:走過容器的每個元素
for (const auto &[key, value] : m) {} // 結構化綁定:一次拆開 pair 的兩個值
int max(int a, int b); // 函式多載:同名但參數型態不同
double max(double a, double b);
void greet(std::string name = "you"); // 預設參數
containers.cpp
// 最常用的三種容器:vector(動態陣列)、map(有排序的對照表)、set(不重複的集合)。
#include <iostream>
#include <map>
#include <set>
#include <string>
#include <vector>
int main() {
std::vector<int> v = {5, 3, 8};
v.push_back(1);
std::cout << "vector:";
for (int x : v) std::cout << ' ' << x;
std::cout << " (size " << v.size() << ")\n";
std::map<std::string, int> age;
age["bob"] = 20;
age["amy"] = 19;
age["bob"] += 1;
std::cout << "map:";
for (const auto &[name, a] : age) std::cout << ' ' << name << '=' << a; // 依 key 排序
std::cout << '\n';
std::set<int> s = {3, 1, 3, 2, 1};
std::cout << "set:";
for (int x : s) std::cout << ' ' << x; // 自動去除重複、排序
std::cout << " (contains 2? " << (s.count(2) ? "yes" : "no") << ")\n";
return 0;
}

在這台電腦執行的結果(Windows x64、clang 23、-O2,沒有開 Sanitizer):

執行結果
vector: 5 3 8 1 (size 4)
map: amy=19 bob=21
set: 1 2 3 (contains 2? yes)
在 Compiler Explorer 開啟 ↗可以線上修改、執行;那邊是 Linux,有些結果會和 Windows 不同
容器特性常用操作的速度
std::vector<T>動態陣列,記憶體連續尾端加入 O(1)(平均)、用索引存取 O(1)
std::map<K, V>key 會自動排序搜尋、插入 O(log n)
std::unordered_map<K, V>不排序,用雜湊表搜尋、插入平均 O(1)
std::set<T> / std::unordered_set<T>不重複的集合同上
std::pair<A, B>把兩個值綁在一起p.first、p.second
std::stack、std::queue、std::priority_queue堆疊、佇列、優先佇列加入、取出 O(1) 或 O(log n)

不知道選哪個的時候,先用 vector:它最省記憶體,也最快。

std::sort(v.begin(), v.end()); // 由小到大
std::sort(v.begin(), v.end(), [](int a, int b) { return a > b; }); // 用 lambda 自訂順序:由大到小
std::reverse(v.begin(), v.end());
auto it = std::max_element(v.begin(), v.end()); // 最大值的位置,*it 是值
v.erase(std::unique(v.begin(), v.end()), v.end()); // 去掉相鄰的重複(先排序)
long long sum = std::accumulate(v.begin(), v.end(), 0LL); // 加總(<numeric>)

[](int a, int b) { return a > b; } 叫做 lambda:直接寫在需要的地方的小函式。

class 和 struct 幾乎一樣,差別只在成員的預設存取權限:struct 預設 public(外面都能用),class 預設 private(只有自己的成員函式能用)。

最重要的兩個特殊成員函式:

  • 建構子:物件誕生時自動執行,負責初始化
  • 解構子(名字前面加 ~):物件消失時自動執行,負責清理
raii.cpp
// 建構子在物件誕生時執行,解構子在物件離開範圍時「自動」執行。
// 把「釋放資源」寫在解構子裡,就不會忘記釋放:這個技巧叫 RAII。
#include <iostream>
#include <string>
#include <utility>
struct Tracer {
std::string name;
explicit Tracer(std::string n) : name(std::move(n)) { std::cout << " construct " << name << '\n'; }
~Tracer() { std::cout << " destruct " << name << '\n'; }
};
int main() {
std::cout << "enter main\n";
Tracer a("a");
{
std::cout << "enter block\n";
Tracer b("b");
Tracer c("c");
std::cout << "leave block\n"; // b、c 在這裡被自動解構(順序和建立時相反)
}
std::cout << "leave main\n"; // a 在 main 結束時解構
return 0;
}

在這台電腦執行的結果(Windows x64、clang 23、-O2,沒有開 Sanitizer):

執行結果
enter main
construct a
enter block
construct b
construct c
leave block
destruct c
destruct b
leave main
destruct a
在 Compiler Explorer 開啟 ↗可以線上修改、執行;那邊是 Linux,有些結果會和 Windows 不同

把「釋放資源」寫在解構子裡,就保證不會忘記釋放,這個技巧叫 RAII。std::string、std::vector、智慧指標都是用這個方法自動管理記憶體的,詳見控制記憶體的必備指令。

寫法意思
const int n = 10;不能修改的變數
constexpr int N = 100;編譯時就確定的常數,可以拿來當陣列大小。C++ 用它取代 #define N 100
nullptr空指標。C++ 用它取代 NULL
  • v[i] 不檢查範圍:越界和 C 陣列一樣危險。除錯時可以改用 v.at(i),越界會丟出例外。
  • push_back 之後,舊的指標、參考、迭代器可能失效:vector 容量不夠時會搬到新的記憶體,指向舊位置的指標就懸空了。
  • m[key] 找不到時會自動新增一筆:只想查詢的話,用 m.count(key) 或 m.find(key)。
  • int 和 v.size() 比較:size() 是無號整數,i < v.size() 會出現有號、無號混用的警告。迴圈變數可以用 size_t。
  • 混用 cin 和 getline:記得先 cin.ignore() 丟掉留在輸入裡的換行。

(還沒寫)