CPP 學習筆記
秋招的時候(嵌入式方向)面試官常問到 C++,因此花了幾天過了一下基礎知識,本文為學習筆記。快速學習的經驗:如果有其他語言基礎的情況下,想要學習一門新語言,讓 AI 幫你列一下這個語言的學習大綱或者目錄,然後針對目錄中的每個知識點讓 AI 講解並給出示例,這樣非常快就能學完一門語言,當然這只是個人的速成經驗,如果要熟練掌握一門語言還是要腳踏實地的一個個知識點去學習練習。
目錄
- 一、C++ 基本語法
- 二、面向對象
- 三、STL容器
- 四、算法
- 五、其他
- 結語
一、C++ 基本語法
如果之前沒有接觸過面嚮對象語言的話,在學習 C++ 之前需要先建立一些概念和思想,不然可能連基礎 Hello World 程序都會疑惑,比如
std :: cout是什麼意思。
在 C 語言中,全局作用域內不允許出現兩個同名的函數。C++ 通過引入命名空間和類,巧妙地解決了這個問題。它們就像是給標識符(函數、變量等)加上了“姓氏”或“地址”,從而避免了命名衝突。具體到類的概念:每個類都定義了一個獨立的作用域。因此,在不同的類中,完全可以定義名稱、參數和返回值都完全相同的成員函數。在調用時,為了明確指出我們想調用的是哪個類中的函數,就需要使用類名來限定,格式通常為 類名::函數名 或通過對象來調用。::表示作用域解析運算符,用於指明空間或者類的作用域。
比如下面的例子,理解思想即可,具體語法接下來慢慢學習。
#include <iostream>
// 定義一個 Dog(狗)類
class Dog {
public:
// Dog 類中的 speak 函數
void speak() {
std::cout << "汪汪!" << std::endl;
}
};
// 定義一個 Cat(貓)類
class Cat {
public:
// Cat 類中的 speak 函數(與 Dog 類中的函數同名同參數同返回類型)
void speak() {
std::cout << "喵喵!" << std::endl;
}
};
int main() {
// 創建 Dog 類和 Cat 類的對象
Dog myDog;
Cat myCat;
// 調用函數時,通過對象來區分所屬的類
std::cout << "狗説:";
myDog.speak(); // 調用的是 Dog::speak()
std::cout << "貓説:";
myCat.speak(); // 調用的是 Cat::speak()
return 0;
}
1.1 變量
這裏只介紹一下 C++ 風格字符串,因為跟 C 語言有所區別。
其實下面的例子會涉及到很多知識點,大家先有個印象,知道 C++ 字符串怎麼定義初始化即可。
#include <iostream> // 基本輸入輸出頭文件,相當於C的stdio
#include <string> // 必須包含這個頭文件
using namespace std; // 聲明瞭 std 空間了之後,後面的 cout 等都不用再添加 std:: 了
int main() {
// 多種初始化方式
string s1; // 默認初始化,空字符串 ""
string s2 = "Hello"; // 拷貝初始化
string s3("World"); // 直接初始化
string s4(5, 'A'); // 初始化由5個'A'組成的字符串 "AAAAA"
string s5(s2); // 用s2初始化s5,s5內容為 "Hello"
cout << "s1: " << s1 << endl;
cout << "s2: " << s2 << endl;
cout << "s3: " << s3 << endl;
cout << "s4: " << s4 << endl;
cout << "s5: " << s5 << endl;
return 0;
}
1.2 命名空間
前面有介紹過。在 C++ 項目中,同一個函數名稱可能會在不同的文件中多次出現,為了解決這種命名衝突,就有了命名空間的概念。std是 C++ 標準庫的命名空間,是一個龐大的工具集。
// 不使用命名空間,在使用 std 中的工具時都需要加前綴,如
std::cout << " " << endl;
// 使用了命名空間之後,不用加前綴,默認是使用 std 空間中的工具
using namespace std;
cout << " " << endl;
1.3 常用頭文件
<iostream> // 輸入輸出:cin, cout
<string> // 字符串類 std::string
<vector> // 向量容器(動態數組)
<algorithm> // 排序、查找、最大最小值等算法
<cmath> // 數學函數:pow, sqrt, sin, 等
<cstdlib> // 隨機數生成、內存分配等
<ctime> // 時間函數如 time, clock 等
<fstream> // 文件流:讀寫文件
1.4 輸入輸出操作
std::cin >> age;
std::cout << "Age: " << age << std::endl; /* 表明std命名空間中的cout隊形 */
int x = 10;
class A {
public:
static int x;
};
int A::x = 20;
std::cout << x; // 全局的x
std::cout << A::x; // A類中的x
-
**cout **對象表述標準輸出流。
-
**cin **對象表示標準輸入流。
1.5 引用
引用就是某個變量的別名,它並不獨立地佔用內存,而是直接綁定到另一個已有的變量。你對引用做的任何操作,實際上就是對原變量的操作,區別於 C 語言的指針。
int a = 10;
int &x = a;
void addOne(int& x) {
x += 1;
}
int& getRef(int& x) {
return x;
}
// 遍歷容器中的每一個元素
// vec 是一個容器,例如 std::vector<int> vec = {1, 2, 3};
// n 是 vec 中的一個元素。
// int& 表示 n 是對該元素的 引用(reference),這樣你可以直接修改容器中的元素。
for (int& n : vec) {
n += 1; // 修改 vec 中的元素
}
1.6 常用關鍵字
/* 類與面向對象 */
class 定義類
struct 定義結構體(默認 public)
public 公有訪問權限
private 私有訪問權限(類默認)
protected 受保護訪問權限
virtual 虛函數(支持多態)
override 明確重寫父類虛函數(C++11)
final 禁止子類重寫(C++11)
this 當前對象指針
new 動態分配內存
delete 釋放動態內存
friend 友元函數/類
explicit 禁止構造函數隱式轉換
inline 請求內聯函數
/* 模板和泛型編程 */
template 模板定義
typename 表示類型參數
constexpr 編譯期常量(C++11)
/* 類型 */
int* 指針類型
int& 引用類型
int[] 數組類型
std::string 字符串(C++類)
-
new
核心功能是在堆上分配內存,並返回指向該內存的一個指針。銷燬使用delete。主要功能有兩個:
- 為單個對象分配內存。、
- 為數組分配內存。
// 為單個對象分配內存 pointer = new TypeName; pointer = new TypeName(initial_value); // new運算符會首先向操作系統申請一塊足夠存儲`TypeName`類型的內存。如果內存分配成功會自動調用這個對`TypeName`類型的構造函數,在這塊新分配的內存中初始化一個對象。 // 基於此,接下來介紹一下什麼是構造函數 // 對於一個類class Person,在該類型的對創建時(new)構造函數會被自動調用,為該對象賦初值。 // 構造函數可重載(參數列表不同即可) // 構造函數名必須與類名一致 /** * pref: 構造函數舉例 */ #include <iostream> #include <string> class Person { public: // 默認構造函數(無參數) Person() { name = "Unknown"; age = 0; std::cout << "Default constructor called." << std::endl; } // 帶參數的構造函數 Person(std::string n, int a) { name = n; age = a; std::cout << "Parameterized constructor called." << std::endl; } void display() { std::cout << "Name: " << name << ", Age: " << age << std::endl; } private: std::string name; int age; }; int main() { Person p1; // 調用默認構造函數 Person p2("Alice", 30); // 調用帶參數的構造函數 p1.display(); p2.display(); return 0; } // 分配數組 int* arr = new int[10]; // 分配並默認初始化10個int delete[] arr; // 正確釋放數組 // 自動計算大小 int* p = new int; -
new 和 melloc 的區別
-
new 可以自動計算內存大小。
-
new 可以自動調用構造函數,delete 可以自動調用析構函數。
-
new 返回正確類型的指針,無需強制類型轉換。
-
1.7 分支結構
與其他語言大差不差,不多介紹。
1.8 數組
C++ 兼容 C語言的數組操作,但一般不使用,下面是 C++ 風格的數組定義。
這裏只講一些初始化的基本操作,更詳細內容見第四章 vector 節。
#include <array>
#include <vector>
using namespace std;
// std::array:固定長度
array<int, 5> arr = {1,2,3,4,5};
// std::vector:動態數組,用的最多的
// <>中的是數組存放的數據類型,可以是 char, int, long等
// 更具體的使用見第四張的 vector 節。
vector<int> v1; // 空vector
vector<int> v2(5); // 5個元素,默認初始化為0
vector<int> v3(5, 10); // 5個元素,初始化為10
vector<int> vec = {1,2,3};
v.push_back(10); // 在末尾添加元素
v.pop_back(); // 刪除末尾元素
1.9 字符串
// 構造方式
std::string s1; // 空字符串
std::string s2("hello"); // C 字符串構造
std::string s3(s2); // 拷貝構造
std::string s4(s2, 1, 2); // 從 s2[1] 開始取 2 個字符
std::string s5(5, 'x'); // “xxxxx”
// 賦值
s1 = "world";
s1.assign("abc", 3);
s1 += "!";
s1.append(s2);
//
std::string s = "Hi"; 創建字符串
s.length() or s.size() 獲取長度
s[0], s.at(1) 訪問某個字符
s += " world"; 拼接字符串
s.substr(0, 3) 子串
s.find("ice") 查找子串
s1 == s2, s1 < s2 比較字符串
s.begin() s.end()
.clear():空字符串,但不改變 capacity。
.insert(...):在指定位置插入字符或子串。
.erase(pos, len) 或 .erase(it1, it2):刪除字符或範圍。
.replace(...):替換指定範圍為其他內容。
.push_back(c) 與 .append(...):末尾添加字符或子串
1.10 指針
int* p = nullptr; // 空指針,避免野指針
int arr[3] = {1, 2, 3};
int* p = arr; // 等價於 int* p = &arr[0];
1.11 函數
-
函數的基本定義與 C 語言相同,不再介紹。
-
函數重載
函數重載 允許在同一個作用域內,定義多個同名的函數,但這些函數的參數列表必須不同(要麼參數類型不同,要麼參數個數不同)。它的核心目的是:為執行概念上相似但操作數據類型或數量不同的任務,提供一個統一的函數接口,使得代碼更直觀、更易讀。
#include <iostream> using namespace std; // 重載函數:add // 版本1:處理兩個整數 int add(int a, int b) { cout << "調用 int add(int, int)" << endl; return a + b; } // 版本2:處理兩個浮點數 double add(double a, double b) { cout << "調用 double add(double, double)" << endl; return a + b; } // 版本3:處理兩個字符串(連接) string add(const string& a, const string& b) { cout << "調用 string add(const string&, const string&)" << endl; return a + b; } int main() { cout << add(5, 3) << endl; // 調用版本1,輸出整數8 cout << add(2.5, 3.7) << endl; // 調用版本2,輸出浮點數6.2 cout << add("Hello, ", "World!") << endl; // 調用版本3,輸出字符串 "Hello, World!" return 0; }
1.12 結構體
#include <vector>
#include <string>
struct student {
std::string name; // 姓名
int age; // 年齡
double score; // 分數
};
// 使用示例
int main() {
std::vector<student> v;
// 方式1:使用初始化列表直接構造
v.push_back({"Mike", 18, 75.0});
// 方式2:先創建對象再添加
student stu1 = {"John", 20, 85.5};
v.push_back(stu1);
// 方式3:使用emplace_back(更高效)
// 注:emplace_back 是 C++ 新特性,在容器章節會常用到
v.emplace_back("Alice", 19, 92.0);
return 0;
}
二、面向對象
2.1 封裝
- 封裝是將數據和操作數據的方法捆綁到一個單元中。對外部隱藏對象的內部實現細節,僅通過有限受控的接口與外部進行交互。
2.1.1 成員權限
- public、private、protected
// public: 任何地方都可以訪問。類的內部、子類、類的外部(通過對象)都可以直接訪問public成員。
// private: 只能在類的內部訪問。子類和類的外部都無法直接訪問private成員。
// protected: 只能在類的內部和其派生類(子類)中訪問。類的外部無法訪問。為繼承設計的“半私有”成員。
#include <iostream>
#include <string>
class BankAccount {
// --- 私有部分:數據和內部實現 ---
private:
// 數據成員(屬性)被設為private,以保護它們
std::string ownerName;
double balance;
int accountNumber;
// 一個私有輔助函數,用於內部記錄日誌,外部不需要知道
void logTransaction(const std::string& action, double amount) {
std::cout << "[LOG] Account " << accountNumber << ": " << action << " of " << amount << std::endl;
}
// --- 公共部分:對外接口 ---
public:
// 構造函數:用於創建和初始化對象
BankAccount(std::string name, int accNum, double initialDeposit) {
ownerName = name;
accountNumber = accNum;
// 即使是初始存款,也通過deposit方法,以保證邏輯統一
balance = 0; // 先設為0
deposit(initialDeposit); // 再調用存款方法
std::cout << "Account for " << ownerName << " created successfully." << std::endl;
}
// 公共接口:存款
void deposit(double amount) {
if (amount > 0) {
balance += amount;
logTransaction("Deposit", amount);
} else {
std::cout << "Error: Deposit amount must be positive." << std::endl;
}
}
// 公共接口:取款
void withdraw(double amount) {
if (amount <= 0) {
std::cout << "Error: Withdrawal amount must be positive." << std::endl;
return;
}
if (amount > balance) {
std::cout << "Error: Insufficient funds. Withdrawal failed." << std::endl;
} else {
balance -= amount;
logTransaction("Withdrawal", amount);
}
}
// 公共接口:查詢餘額
// 注意:它返回一個副本,而不是balance的引用,防止外部通過引用修改
double getBalance() const {
return balance;
}
// 公共接口:顯示賬户信息
void displayInfo() const {
std::cout << "------------------------" << std::endl;
std::cout << "Account Holder: " << ownerName << std::endl;
std::cout << "Account Number: " << accountNumber << std::endl;
std::cout << "Current Balance: " << balance << std::endl;
std::cout << "------------------------" << std::endl;
}
}; // 類定義結束
int main() {
// 創建一個BankAccount對象
BankAccount myAccount("Alice", 12345678, 1000.0);
myAccount.displayInfo();
// 通過公共接口進行操作
std::cout << "\n--- Attempting to deposit 500 ---" << std::endl;
myAccount.deposit(500.0);
myAccount.displayInfo();
std::cout << "\n--- Attempting to withdraw 200 ---" << std::endl;
myAccount.withdraw(200.0);
myAccount.displayInfo();
std::cout << "\n--- Attempting to withdraw 2000 (insufficient funds) ---" << std::endl;
myAccount.withdraw(2000.0);
myAccount.displayInfo();
// --- 以下代碼是錯誤的,無法通過編譯,體現了封裝的安全性 ---
// myAccount.balance = 1000000; // 錯誤!'double BankAccount::balance' is private
// myAccount.accountNumber = 999; // 錯誤!'int BankAccount::accountNumber' is private
// myAccount.logTransaction("Hacking", 0); // 錯誤!'void BankAccount::logTransaction(...)' is private
return 0;
}
-
const關鍵字
void displayInfo() const; 表示這個函數是隻讀的,不會修改對象的內容。
-
構造函數的一種寫法
class MyClass {
private:
int secret;
public:
MyClass(int s) : secret(s) {}
// 聲明友元函數
friend void showSecret(const MyClass& obj);
};
// secret(s)表示將成員secret屬性賦值為s
2.1.2 友元
// 友元函數:非成員函數可以訪問類的私有成員
class MyClass {
private:
int secret;
public:
MyClass(int s) : secret(s) {}
// 聲明友元函數
friend void showSecret(const MyClass& obj);
};
// 友元函數定義(可以訪問 MyClass 的私有成員)
void showSecret(const MyClass& obj) {
std::cout << "Secret: " << obj.secret << std::endl;
}
int main() {
MyClass obj(42);
showSecret(obj); // 輸出: Secret: 42
return 0;
}
// 友元類:一個類的所有成員函數能夠訪問另一個類的所有私有成員
class SecretHolder {
private:
int data;
public:
SecretHolder(int d) : data(d) {}
// 聲明友元類
friend class FriendClass;
};
class FriendClass {
public:
void accessSecret(const SecretHolder& holder) {
std::cout << "Data: " << holder.data << std::endl; // 合法訪問
}
};
int main() {
SecretHolder holder(100);
FriendClass friendObj;
friendObj.accessSecret(holder); // 輸出: Data: 100
return 0;
}
// 友元成員函數:某個類的成員函數可以訪問另一個類的私有成員
class A {
private:
int x;
public:
A(int val) : x(val) {}
// 聲明類 B 的成員函數為友元
friend void B::printX(const A& obj);
};
class B {
public:
void printX(const A& obj) {
std::cout << "A::x = " << obj.x << std::endl; // 合法訪問
}
};
int main() {
A a(10);
B b;
b.printX(a); // 輸出: A::x = 10
return 0;
}
2.1.3 構造函數
拷貝構造函數
默認構造函數,帶參數的構造函數,初始化列表
-
更推薦初始化列表的方式,對於類類型成員,初始化列表是直接調用其構造函數進行初始化;而在構造函數體內賦值,則是先調用默認構造函數創建一個臨時對象,然後再用賦值操作符覆蓋。前者效率更高。
-
拷貝構造函數
用於創建一個與現有對象完全相同的新對象,它的參數是對同類對象的常量引用const ClassName&。如果沒有定義拷貝構造函數,編譯器會生成一個默認的拷貝構造函數,它會進行淺拷貝。
淺拷貝
危險性:默認的淺拷貝在拷貝默認的指針成員的時候,只是拷貝一個指針,指針的內容不會被拷貝,這會導致調用析構函數的時候可能會重複釋放同一塊內存。
#include <cstring>
class ShallowCopy {
private:
char* data;
public:
ShallowCopy(const char* str) {
data = new char[strlen(str) + 1];
strcpy(data, str);
}
// ~ShallowCopy() { delete[] data; } // 析構函數,後面會講
// 默認拷貝構造函數是淺拷貝,會導致問題
};
void problem_demo() {
ShallowCopy s1("Hello");
ShallowCopy s2 = s1; // 調用默認拷貝構造函數,s1.data 和 s2.data 指向同一塊內存
// 當 s1 和 s2 析構時,會嘗試 delete[] 同一塊內存兩次,導致程序崩潰!
}
深拷貝
當類中包含指針成員或動態分配的資源時,你必須顯式地定義拷貝構造函數,自己為新對象分配獨立的內存並複製內容,這就是深拷貝(Deep Copy)。
class DeepCopy {
private:
char* data;
public:
DeepCopy(const char* str) {
data = new char[strlen(str) + 1];
strcpy(data, str);
}
// 顯式定義的拷貝構造函數,實現深拷貝
DeepCopy(const DeepCopy& other) {
data = new char[strlen(other.data) + 1]; // 1. 為新對象分配新內存
strcpy(data, other.data); // 2. 複製內容
std::cout << "深拷貝構造函數被調用" << std::endl;
}
~DeepCopy() {
delete[] data;
}
};
2.1.4 析構函數
釋放資源、自動調用、不能重載、先構造的對象後析構,後構造的對象先析構。
如果在構造函數中使用了new,那麼就需要在析構函數中顯式定義一個析構函數來釋放這些資源。如果不定義,那麼默認的析構函數只會消除指針本身,而不會釋放指針指向的內存。
delete[]操作符專門用於釋放new[]創建的動態數組。
| 操作 | 創建單個對象 | 創建對象數組 |
|---|---|---|
| 分配內存 | new int |
new int[10] |
| 釋放內存 | delete p |
delete[] p_arr |
#include <iostream>
#include <cstring>
class ResourceHolder {
private:
char* buffer;
int size;
public:
// 構造函數:獲取資源
ResourceHolder(int s) : size(s) {
buffer = new char[size]; // 動態分配內存
std::cout << "構造函數: 分配了 " << size << " 字節的內存" << std::endl;
}
// 析構函數:釋放資源
~ResourceHolder() {
delete[] buffer; // 釋放動態分配的內存
std::cout << "析構函數: 釋放了內存" << std::endl;
buffer = nullptr; // 好習慣,避免懸垂指針
}
void doSomething() {
std::cout << "正在使用資源..." << std::endl;
}
};
void createAndDestroy() {
std::cout << "進入 createAndDestroy 函數" << std::endl;
ResourceHolder holder(1024); // 創建對象,調用構造函數
holder.doSomething();
std::cout << "即將離開 createAndDestroy 函數" << std::endl;
} // 離開作用域,holder 對象被銷燬,自動調用析構函數
int main() {
createAndDestroy();
std::cout << "已回到 main 函數" << std::endl;
return 0;
}
2.1.5 類對象作為類成員
關鍵點是類對象的成員初始化列表。成員對象的構造順序由它們在類中的聲明順序決定
#include <iostream>
#include <string>
// 部件類:引擎
class Engine {
public:
Engine(const std::string& type) : m_type(type) {
std::cout << "Engine " << m_type << " constructed." << std::endl;
}
void start() {
std::cout << "Engine " << m_type << " is starting... Vroom!" << std::endl;
}
~Engine() {
std::cout << "Engine " << m_type << " destructed." << std::endl;
}
private:
std::string m_type;
};
// 部件類:輪子
class Wheel {
public:
Wheel(int id) : m_id(id) {
std::cout << "Wheel " << m_id << " constructed." << std::endl;
}
void rotate() {
std::cout << "Wheel " << m_id << " is rotating." << std::endl;
}
~Wheel() {
std::cout << "Wheel " << m_id << " destructed." << std::endl;
}
private:
int m_id;
};
// 組合類:汽車
class Car {
public:
Car(const std::string& engineType)
// 關鍵點:成員初始化列表
: m_engine(engineType), // 調用 Engine 的構造函數
m_wheel1(1), // 調用 Wheel 的構造函數
m_wheel2(2),
m_wheel3(3),
m_wheel4(4) {
std::cout << "Car constructed." << std::endl;
}
void drive() {
std::cout << "Car is about to drive." << std::endl;
m_engine.start();
m_wheel1.rotate();
m_wheel2.rotate();
m_wheel3.rotate();
m_wheel4.rotate();
}
~Car() {
std::cout << "Car destructed." << std::endl;
}
private:
// Car 對象“擁有”這些對象
Engine m_engine;
Wheel m_wheel1;
Wheel m_wheel2;
Wheel m_wheel3;
Wheel m_wheel4;
};
int main() {
std::cout << "--- Creating a Car ---" << std::endl;
Car myCar("V8");
std::cout << "\n--- Driving the Car ---" << std::endl;
myCar.drive();
std::cout << "\n--- Destroying the Car ---" << std::endl;
// myCar 離開作用域,析構函數被自動調用
return 0;
}
2.1.6 靜態成員
所有創建的對象都會自動擁有類定義中的靜態成員變量
// 構造函數
BankAccount(const std::string& name, double initialBalance)
: m_ownerName(name), m_balance(initialBalance) {
// 每次創建新賬户,總賬户數和總存款額都增加
s_totalAccounts++;
s_totalBalance += initialBalance;
std::cout << "Account for " << m_ownerName << " created. Balance: " << m_balance << std::endl;
}
private:
// 普通成員變量:每個對象都有一份自己的拷貝
std::string m_ownerName;
double m_balance;
// 靜態成員變量:所有對象共享同一份
static int s_totalAccounts;
static double s_totalBalance;
};
2.1.7 存儲模型
類的成員變量和成員函數分開存儲的核心思想是數據與行為的分離。
以一段代碼為例,圖解面向對象的內存模型。類成員函數存放在.text區域,非靜態成員變量放在棧區,靜態成員變量放在靜態數據區。
#include <iostream>
class MyClass {
public:
// 1. 非靜態成員變量
int mA;
int mB;
// 2. 靜態成員變量
static int mC; // 聲明,不佔用對象內存
// 3. 非靜態成員函數
void show() {
std::cout << "mA: " << mA << ", mB: " << mB << std::endl;
}
// 4. 靜態成員函數
static void staticShow() {
std::cout << "Static member mC: " << mC << std::endl;
}
};
// 靜態成員變量的定義(分配內存)
int MyClass::mC = 100;
int main() {
std::cout << "Size of MyClass: " << sizeof(MyClass) << " bytes" << std::endl; // 輸出多少?
MyClass obj1;
MyClass obj2;
obj1.mA = 10;
obj1.mB = 20;
obj2.mA = 30;
obj2.mB = 40;
obj1.show(); // 輸出 mA: 10, mB: 20
obj2.show(); // 輸出 mA: 30, mB: 40
MyClass::staticShow(); // 輸出 Static member mC: 100
return 0;
}
/*****************************************************************************
+---------------------------+
| 代碼區 |
| +-----------------------+ |
| | void MyClass::show() | | <-- 所有對象共享
| +-----------------------+ |
| | void MyClass::stati.. | | <-- 所有對象共享
| +-----------------------+ |
+---------------------------+
+---------------------------+
| 靜態數據區 |
| +-----------------------+ |
| | MyClass::mC (100) | | <-- 全局唯一
| +-----------------------+ |
+---------------------------+
+---------------------------+
| 棧區 |
| +-----------------------+ |
| | obj1 | |
| | mA: 10 | |
| | mB: 20 | |
| +-----------------------+ |
| | obj2 | |
| | mA: 30 | |
| | mB: 40 | |
| +-----------------------+ |
+---------------------------+
*************************************************************************************/
| 特性 | 成員變量 | 成員函數 |
|---|---|---|
| 存儲位置 | 非靜態: 存儲在對象內部 (棧/堆) 靜態: 存儲在靜態數據區 | 所有函數: 存儲在代碼區 |
| 拷貝數量 | 非靜態: 每個對象一份 靜態: 整個類一份 | 所有函數: 整個類一份,所有對象共享 |
| 與對象關係 | 非靜態: 定義了對象的“狀態” 靜態: 定義了類的“全局狀態” | 定義了對象的“行為”或“操作邏輯” |
| 訪問機制 | 直接通過對象地址訪問 | 非靜態: 通過 this 指針隱式訪問對象成員 靜態: 無 this 指針,不能訪問非靜態成員 |
對 sizeof 的影響 |
非靜態: 直接決定對象大小 靜態: 無影響 | 所有函數: 無影響 |
2.1.8 this指針
既然成員函數在內存中只有一份,那調用不同對象的成員函數時,函數內部是如何區分要操作哪個對象呢。
當調用一個非靜態成員函數時,編譯器會隱式地把調用該函數的對象的地址指針作為第一個參數傳遞進去,這就是this指針,類型是className* const。obj1.show()在編譯器看來更像是show(&obj1)。
// 對於 obj1.show(),this 指向 obj1,所以 this->mA 就是 obj1.mA(值為10)。
// 對於 obj2.show(),this 指向 obj2,所以 this->mA 就是 obj2.mA(值為30)。
2.1.9 靜態成員函數
在 C++ 中,靜態成員函數(Static Member Function) 是類的成員函數,但它不屬於類的某個具體對象,而是屬於類本身。靜態成員函數可以直接通過類名調用,無需創建類的實例(對象),並且它只能訪問類的靜態成員(靜態變量或其他靜態函數),不能直接訪問非靜態成員(普通變量或普通函數)。
靜態成員函數沒有 this 指針,因此無法訪問類的非靜態成員(變量或函數),因為非靜態成員屬於對象實例。
聲明週期與類相同,靜態成員函數在程序加載時就被初始化,直到程序結束才銷燬。
調用:
class MyClass {
public:
static void StaticFunction(); // 靜態成員函數聲明
};
MyClass::StaticFunction(); // 直接通過類名調用
空指針訪問成員函數
核心思想是函數調用並不依賴與對象地址。成員函數是存放在代碼區的,使用nullptr調用成員函數是可以調用的,只是傳入的this指針是nullptr。
-
如果成員函數沒有涉及到成員變量,可以正常執行。
-
涉及成員變量時會因為this->mA => nullptr->mA而導致程序崩潰。
-
類中存在虛函數
- 當一個類有虛函數時,編譯器會為這個類創建一個隱藏的表,叫做虛函數表
vptr。這個表存放了所有虛函數的地址。 - 對於對象opj,obj->vptr->doSomething()
a. 通過對象指針 `p` 找到對象本身。 b. 從對象的內存中讀取 `vptr`(虛函數表指針)。 c. 通過 `vptr` 找到虛函數表。 d. 在虛函數表中查找 `doSomething` 的地址。 e. 跳轉到該地址執行函數。 - 當一個類有虛函數時,編譯器會為這個類創建一個隱藏的表,叫做虛函數表
2.1.10 虛函數
虛函數是在基類中使用 virtual 關鍵字聲明的成員函數。它允許你在派生類中對該函數進行重寫(Override),並且當你通過基類的指針或引用來調用該函數時,程序會動態地根據指針或引用實際所指向的對象類型,來調用相應派生類中的版本,而不是基類的版本。
核心目的是實現運行時多態,也成為動態綁定,就是用一個統一的接口,去處理多種不同類型的對象。
#include <iostream>
// 基類:動物
class Animal {
public:
// 使用 virtual 關鍵字聲明為虛函數
virtual void speak() {
std::cout << "Some generic animal sound!" << std::endl;
}
};
// 派生類:狗
class Dog : public Animal {
public:
// 重寫 speak 函數 (override關鍵字是C++11引入的,推薦使用,讓意圖更清晰)
void speak() override {
std::cout << "Woof! Woof!" << std::endl;
}
};
// 派生類:貓
class Cat : public Animal {
public:
void speak() override {
std::cout << "Meow!" << std::endl;
}
};
int main() {
Dog myDog;
Cat myCat;
Animal* animalPtr1 = &myDog;
Animal* animalPtr2 = &myCat;
std::cout << "Calling speak() via pointers (with virtual):" << std::endl;
animalPtr1->speak(); // 現在它會正確地叫 "Woof! Woof!"
animalPtr2->speak(); // 現在它會正確地叫 "Meow!"
return 0;
}
2.2 C++運算符重載
什麼是運算符重載:相對於某個class來説,重新定義已有的運算符,使得其工作在我們期待的情況下。例如
Vector v1(1, 2), v2(3, 4);
Vector v3 = v1 + v2; // 希望實現向量相加
2.2.1 運算符重載的語法
- 成員函數的形式
// Vector: 返回值
// Vector:: : 表示這是一個成員函數,屬於Vector類。
// const Vector& other: 表示一個常量引用,避免拷貝開銷,保證只讀
Vector Vector::operator+(const Vector& other) const;
class Vector {
public:
double x, y;
// 構造函數
Vector(double x = 0, double y = 0) : x(x), y(y) {}
// 重載 + 運算符
Vector operator+(const Vector& other) const {
return Vector(x + other.x, y + other.y); // 返回新對象
}
};
int main() {
Vector v1(1.0, 2.0);
Vector v2(3.0, 4.0);
Vector v3 = v1 + v2; // 調用 operator+,結果為 (4.0, 6.0)
std::cout << "v3: (" << v3.x << ", " << v3.y << ")" << std::endl;
return 0;
}
- 非成員函數的形式
通常使用friend關鍵字,友元函數。
class Vector {
// ... 其他成員 ...
friend Vector operator+(const Vector& a, const Vector& b);
};
Vector operator+(const Vector& a, const Vector& b) {
return Vector(a.x + b.x, a.y + b.y);
}
2.2.2 常見運算符重載
只要某個表達式裏出現了你自定義的類型,並且用到了某個運算符,而這個運算符對該類型沒有現成的、可用的實現,編譯器就會去查找是否存在針對該類型、該運算符的重載函數。找到了就用,找不到就報錯。
Vector& Vector::operator=(const Vector& other) {
if (this != &other) { // 防止自賦值
x = other.x;
y = other.y;
}
return *this;
}
int& Vector::operator[](int index) {
if (index == 0) return x;
else if (index == 1) return y;
else throw std::out_of_range("Index out of range");
}
// std::ostream& os是輸出流對象,如std::cout
std::ostream& operator<<(std::ostream& os, const Vector& v) {
os << "(" << v.x << ", " << v.y << ")";
return os;
}
std::istream& operator>>(std::istream& is, Vector& v) {
is >> v.x >> v.y;
return is;
}
// 前置 ++
Vector& Vector::operator++() {
++x;
++y;
return *this;
}
// 後置 ++(用 int 參數區分,沒有邏輯原因,就是一個佔位參數,用於區分)
Vector Vector::operator++(int) {
Vector temp = *this;
++(*this);
return temp;
}
// 關係運算符重載
bool operator<(const Person& other) const {
return age < other.age;
}
bool operator>(const Person& other) const {
return age > other.age;
}
bool operator<=(const Person& other) const {
return age <= other.age;
}
bool operator>=(const Person& other) const {
return age >= other.age;
}
輸入輸出流重載説明:std::ostream& operator<<(std::ostream& os, const Vector& v)
首先這是一個函數,函數名字是operator<<,函數的輸出類型是std::ostream&,輸入類型是std::ostream& const Vector& v相當於(std::cout << v)的重載後的輸出可以是std::cout以便格式化輸出Vector類型之後還能夠繼續鏈式輸出其他內容。
// 輸入輸出流重載
#include <iostream>
struct Vector {
double x, y;
};
// 輸出流重載
std::ostream& operator<<(std::ostream& os, const Vector& v) {
os << "(" << v.x << ", " << v.y << ")";
return os;
}
// 輸入流重載
std::istream& operator>>(std::istream& is, Vector& v) {
is >> v.x >> v.y;
return is;
}
int main() {
Vector v1, v2;
// 輸入
std::cout << "Enter Vector 1 (x y): ";
std::cin >> v1; // 例如輸入: 1.0 2.0
std::cout << "Enter Vector 2 (x y): ";
std::cin >> v2; // 例如輸入: 3.0 4.0
// 輸出
std::cout << "Vector 1: " << v1 << std::endl; // 輸出: Vector 1: (1.0, 2.0)
std::cout << "Vector 2: " << v2 << std::endl; // 輸出: Vector 2: (3.0, 4.0)
return 0;
}
2.2.3 函數運算符重載
class Adder {
public:
int operator()(int a, int b) {
return a + b;
}
};
Adder add;
int result = add(3, 4); // 看起來像函數調用,實際是調用 operator()
// 匿名函數對象
std::cout << Adder()(3, 4) << std::endl;
2.3 繼承
概念:繼承允許我們創建一個新類(派生類),這個新類會繼承一個基類的屬性和行為,實現代碼的重用和類之間的層次關係。
核心思想:提取出一類物品的公共屬性和行為。比如貓狗鳥等都屬於動物,動物就可以作為父類。
繼承是多態的基礎。
子類可以對父類的成員函數進行重寫。虛函數的重寫是為了實現多態。
2.3.1 語法格式
// class Manager : public Employee {
#include <iostream>
#include <string>
// 基類:員工
class Employee {
public:
// 基類的構造函數
Employee(std::string name, int id, double salary)
: m_name(name),
m_id(id),
m_salary(salary) {}
// 基類的成員函數
void work() {
std::cout << m_name << " (ID: " << m_id << ") is working." << std::endl;
}
void showInfo() {
std::cout << "Name: " << m_name << ", ID: " << m_id << ", Salary: " << m_salary << std::endl;
}
protected: // protected成員在基類和派生類中都可以訪問
std::string m_name;
int m_id;
double m_salary;
};
// 派生類:經理,公有繼承自Employee
class Manager : public Employee {
public:
// 派生類的構造函數,需要調用基類的構造函數來初始化基類部分
Manager(std::string name, int id, double salary, double bonus)
: Employee(name, id, salary),
m_bonus(bonus) {} // 初始化列表
// 派生類新增的成員函數
void manageTeam() {
std::cout << m_name << " is managing the team." << std::endl;
}
// 派生類可以重寫(覆蓋)基類的函數
void showInfo() {
// 先調用基類的showInfo()顯示共同信息
Employee::showInfo(); // 使用作用域解析符調用基類版本
// 再顯示派生類特有的信息
std::cout << "Bonus: " << m_bonus << std::endl;
}
private:
// 派生類新增的成員變量
double m_bonus;
};
int main() {
Employee emp("Alice", 1001, 8000.0);
emp.work(); // Alice (ID: 1001) is working.
emp.showInfo(); // Name: Alice, ID: 1001, Salary: 8000
std::cout << "---------------------" << std::endl;
Manager mgr("Bob", 2001, 15000.0, 5000.0);
mgr.work(); // Bob (ID: 2001) is working. (繼承自Employee)
mgr.manageTeam(); // Bob is managing the team. (Manager自己的)
mgr.showInfo(); // 調用的是Manager重寫後的版本
// Name: Bob, ID: 2001, Salary: 15000
// Bonus: 5000
return 0;
}
2.3.2 繼承方式
有公有,保護,私有三種方式。繼承方式決定了基類中的成員在派生類中的訪問權限。繼承方式是為了限制“外部”對“基類部分”的訪問,而不是限制派生類內部對基類成員的訪問。總之,派生類對基類的訪問權限取決於基類和繼承方式的最小權限。只有兩個都是public時外部才能訪問。
無論哪種繼承方式,基類的 private 成員永遠無法被派生類直接訪問。它們雖然被繼承了(存在於派生類對象中),但對派生類來説是“不可見”的。派生類只能通過基類提供的 public 或 protected 接口來間接訪問它們。
2.3.3 繼承中的構造/析構函數
構造函數和析構函數不能被繼承,但是在創建派生類對象時,基類的構造函數會自動被調用。
構造函數的調用順序:先調用基類的構造函數,再調用派生類自己的構造函數。
// Manager的構造函數
Manager(std::string name, int id, double salary, double bonus)
: Employee(name, id, salary), // 在初始化列表中調用基類構造函數
m_bonus(bonus) { // 初始化派生類自己的成員
// 函數體
}
析構函數的調用順序相反。
2.3.4 繼承的內存佈局
派生類對象包含了基類的所有非靜態成員變量,以及派生類自己新增的非靜態成員變量。這些成員在內存中通常是連續存放的,基類的部分在前,派生類的部分在後。
成員函數(包括虛函數)並不存儲在每個對象中。它們存儲在代碼段。每個對象中只存儲一個指向虛函數表的指針(如果類有虛函數),通過這個指針來找到正確的函數版本。
2.3.5 繼承中的靜態成員
無論繼承出多少個派生類,整個繼承體系中只有一個靜態成員的實例。
- 靜態成員變量:被所有基類和派生類的對象共享。
- 靜態成員函數:沒有
this指針,只能訪問靜態成員。它同樣被繼承,但無法被重寫為虛函數(因為虛函數依賴於this指針和虛表)。
靜態成員函數可以通過基類或者派生類的作用域來訪問。
class Base {
public:
static int s_count;
static void printCount() {
std::cout << "Count: " << s_count << std::endl;
}
};
int Base::s_count = 0; // 靜態成員初始化
class Derived : public Base {
// ...
};
int main() {
Base b;
Derived d;
b.s_count = 10;
d.s_count = 20; // 修改的是同一個 s_count
Base::printCount(); // 輸出 Count: 20
Derived::printCount(); // 輸出 Count: 20
return 0;
}
2.3.7 多繼承和菱形繼承問題
多繼承會引入複雜性,最主要的問題是命名衝突。如果多個基類中有同名的成員,那麼在派生類中訪問時,必須使用作用域解析符來明確指出要訪問哪個基類的成員。
class A {
public:
void foo() { std::cout << "A::foo()" << std::endl; }
};
class B {
public:
void foo() { std::cout << "B::foo()" << std::endl; }
};
class C : public A, public B {
// ...
};
int main() {
C c;
// c.foo(); // 錯誤!對 'foo' 的訪問不明確
c.A::foo(); // 正確,調用A的foo
c.B::foo(); // 正確,調用B的foo
return 0;
}
菱形繼承
當一個派生類通過多個路徑繼承同一個基類時,就會形成菱形結構。
/******************************************************************************************************
Person
/ \
Teacher Student
\ /
TeachingAssistant
Person 類
Teacher 類繼承 Person
Student 類繼承 Person
******************************************************************************************************/
class Person {
public:
int m_age;
};
class Teacher : public Person {};
class Student : public Person {};
class TeachingAssistant : public Teacher, public Student {};
int main() {
TeachingAssistant ta;
// ta.m_age = 25; // 錯誤!對 'm_age' 的訪問不明確
ta.Teacher::m_age = 25; // 修改Teacher路徑上的m_age
ta.Student::m_age = 26; // 修改Student路徑上的m_age
// 現在ta對象中有兩個不同的m_age值,數據不一致了!
return 0;
}
菱形繼承會導致,內存浪費,數據不一致,訪問二義性。
虛繼承
為解決菱形繼承的問題,引入的虛繼承。
作用:虛繼承確保在繼承體系中,無論被繼承多少次,共享的基類(如 Person)只會有一個實例。
語法:在繼承路徑的“腰部”使用 virtual 關鍵字。即在直接繼承共同基類的派生類(Teacher 和 Student)的繼承聲明中使用 virtual。
class Person {
public:
int m_age;
};
// 使用虛繼承
class Teacher : virtual public Person {};
class Student : virtual public Person {};
class TeachingAssistant : public Teacher, public Student {};
int main() {
TeachingAssistant ta;
ta.m_age = 25; // 正確!不再有二義性,因為只有一個m_age
std::cout << ta.m_age << std::endl; // 輸出 25
std::cout << ta.Teacher::m_age << std::endl; // 輸出 25 (同一個)
std::cout << ta.Student::m_age << std::endl; // 輸出 25 (同一個)
return 0;
}
虛繼承的原理(簡述):
虛繼承的實現通常通過虛基類指針和虛基類表。
- 每個繼承了虛基類的類(如
Teacher,Student)的對象中,會多一個虛基類指針。 - 這個指針指向一個虛基類表,表中記錄了從當前對象位置到共享的虛基類(
Person)子對象的偏移量。 - 這樣,無論通過
Teacher還是Student的路徑,都能通過查表找到同一個Person子對象。
2.3.8 同名成員
分為同名成員變量的處理和同名成員函數的處理。
只要派生類中存在與基類同名的函數,那基類中所有的重載函數都會被隱藏,必須使用作用域的方式調用。
核心思想是:派生類的同名成員會“隱藏”基類的同名成員,使得通過派生類對象直接訪問時,只能訪問到派生類自己的版本。
#include <iostream>
#include <string>
class Base {
public:
int m_value; // 基類的成員變量
Base() : m_value(100) {
std::cout << "Base constructor, m_value = " << m_value << std::endl;
}
};
class Derived : public Base {
public:
int m_value; // 派生類的同名成員變量
Derived() : m_value(200) {
std::cout << "Derived constructor, m_value = " << m_value << std::endl;
}
void printValues() {
std::cout << "--- Inside Derived::printValues ---" << std::endl;
// 直接訪問 m_value,訪問的是派生類自己的 m_value
std::cout << "Derived's m_value = " << m_value << std::endl;
// 使用作用域解析運算符 :: 來訪問被隱藏的基類成員
std::cout << "Base's m_value = " << Base::m_value << std::endl;
std::cout << "----------------------------------" << std::endl;
}
};
int main() {
Derived d;
d.printValues();
// 通過對象直接訪問
std::cout << "--- Accessing via object ---" << std::endl;
std::cout << "d.m_value = " << d.m_value << std::endl; // 訪問的是 Derived::m_value
// 如何通過對象訪問基類的 m_value?
// 必須使用作用域解析運算符
std::cout << "d.Base::m_value = " << d.Base::m_value << std::endl; // 訪問的是 Base::m_value
return 0;
}
同名成員函數的處理比成員變量更復雜,因為它涉及到函數重載和函數重寫(覆蓋)的概念。C++的規則是:只要函數名相同,基類的所有同名函數都會被隱藏,無論參數列表是否相同。
這是一個非常重要的區別,很多人會誤以為如果參數列表不同,就會構成重載。但請注意:重載只能發生在同一個作用域內。 基類和派生類是不同的作用域。
#include <iostream>
class Base {
public:
// 基類中有三個同名但參數不同的函數
void display() {
std::cout << "Base::display() (no args)" << std::endl;
}
void display(int i) {
std::cout << "Base::display(int) with i = " << i << std::endl;
}
void display(double d) {
std::cout << "Base::display(double) with d = " << d << std::endl;
}
};
class Derived : public Base {
public:
// 派生類中定義了一個同名函數
void display(int i) {
std::cout << "Derived::display(int) with i = " << i << std::endl;
}
};
int main() {
Derived d;
std::cout << "Trying to call display() functions from a Derived object:" << std::endl;
// d.display(); // 編譯錯誤! Base::display() 被隱藏了
// d.display(3.14); // 編譯錯誤! Base::display(double) 被隱藏了
d.display(10); // OK,調用的是 Derived::display(int)
std::cout << "\nCalling Base's hidden functions explicitly:" << std::endl;
// 使用作用域解析運算符調用基類的版本
d.Base::display(); // OK
d.Base::display(20); // OK
d.Base::display(3.14); // OK
return 0;
}
如果我們只是想在派生類中“暴露”基類的某個重載版本,而不是重新實現它,可以使用using聲明。這會把基類的函數名“引入”到派生類的作用域中,使其參與重載解析。
class Derived : public Base {
public:
// 將 Base 類中所有的 display 函數引入到 Derived 的作用域
using Base::display;
// 現在,Derived 類中相當於有了以下函數:
// void display(); (from Base)
// void display(int); (from Base)
// void display(double); (from Base)
// void display(int); (defined below)
// 這個 Derived::display(int) 會和 Base::display(int) 形成重載
void display(int i) {
std::cout << "Derived::display(int) with i = " << i << std::endl;
}
};
int main() {
Derived d;
d.display(); // 現在可以了!調用 Base::display()
d.display(3.14); // 現在可以了!調用 Base::display(double)
d.display(10); // 調用 Derived::display(int) (通常更精確的匹配或非虛函數優先)
return 0;
}
2.4 多態
核心思想:同一個函數調用,作用於不同的對象,會產生不同的行為。
靜態多態:也稱為編譯時多態。在程序編譯期間就確定了具體要調用哪個函數。主要通過函數重載和模板實現。
動態多態:也稱為運行時多態。在程序運行期間,根據對象的實際類型來動態地確定調用哪個函數。主要通過繼承和虛函數實現。
絕大多數情況下指的是動態多態。
動態多態實現的三個關鍵要素:繼承,虛函數,基類指針或引用。
2.4.1 虛指針和虛函數表
當一個類中存在至少一個虛函數時,編譯器會為這個類創建一個虛函數表。這個表是一個靜態的、函數指針的數組。
虛指針時在創建含虛函數的類的對象時,編譯器在對象的內存佈局中插入的一個額外指針vptr指向該對象所屬類的虛函數表。
編譯時:
- 編譯器為
Base類創建一個虛函數表,Base::speak()的地址被放入表中。 - 編譯器為
Derived類也創建一個虛函數表。由於Derived重寫了speak(),所以Derived::speak()的地址被放入表中,覆蓋了從基類繼承來的位置。 - 當創建
Base對象時,其vptr指向Base的虛函數表。 - 當創建
Derived對象時,其vptr指向Derived的虛函數表。
運行時:
當你通過基類指針ptr調用ptr->speak()時,程序並不會在編譯時就硬編碼要調用Base::speak()。
- 獲取
ptr所指向的對象。 - 找到該對象內部的
vptr。 - 通過
vptr找到對應的虛函數表。 - 在虛函數表中,找到
speak()函數對應的條目。 - 調用該條目中存儲的函數地址。
因為ptr在運行時可能指向Base對象,也可能指向Derived對象,所以它內部的vptr也就分別指向了不同的虛函數表,從而最終調用了不同版本的speak()函數。這就是“動態綁定”或“遲綁定”的精髓。
#include <iostream>
#include <string>
// 1. 基類
class Animal {
public:
// 2. 虛函數
// 使用 virtual 關鍵字聲明,表示這個函數可以被派生類重寫
virtual void speak() const {
std::cout << "Some generic animal sound..." << std::endl;
}
// 虛析構函數!非常重要!後面會講為什麼。
virtual ~Animal() {
std::cout << "Animal destructor called." << std::endl;
}
};
// 1. 繼承
class Dog : public Animal {
public:
// 3. 重寫 虛函數
// 函數簽名必須與基類的虛函數完全一致(除了協變返回類型,這裏不展開)
// override 關鍵字是C++11引入的,強烈推薦使用!
// 它可以讓編譯器檢查你是否真的重寫了一個基類的虛函數,防止因拼寫錯誤等問題導致隱藏而非重寫。
void speak() const override {
std::cout << "Woof! Woof!" << std::endl;
}
~Dog() {
std::cout << "Dog destructor called." << std::endl;
}
};
class Cat : public Animal {
public:
void speak() const override {
std::cout << "Meow!" << std::endl;
}
~Cat() {
std::cout << "Cat destructor called." << std::endl;
}
};
// 一個統一的接口,它不關心傳進來的是Dog還是Cat,只要是Animal就行
void letAnimalSpeak(const Animal& animal) { // 使用基類引用
animal.speak(); // 發生動態多態調用
}
int main() {
Dog myDog;
Cat myCat;
std::cout << "--- Direct calls ---" << std::endl;
myDog.speak(); // 直接調用,調用Dog::speak()
myCat.speak(); // 直接調用,調用Cat::speak()
std::cout << "\n--- Polymorphic calls via reference ---" << std::endl;
letAnimalSpeak(myDog); // 傳入Dog對象,內部調用Dog::speak()
letAnimalSpeak(myCat); // 傳入Cat對象,內部調用Cat::speak()
std::cout << "\n--- Polymorphic calls via pointer ---" << std::endl;
// 4. 使用基類指針
Animal* ptr1 = new Dog();
Animal* ptr2 = new Cat();
ptr1->speak(); // 通過基類指針調用,調用Dog::speak()
ptr2->speak(); // 通過基類指針調用,調用Cat::speak()
std::cout << "\n--- Destructing objects ---" << std::endl;
delete ptr1; // 如果Animal的析構函數不是virtual,這裏只會調用Animal的析構函數,導致Dog的析構函數不被調用,內存泄漏!
delete ptr2;
return 0;
}
2.4.2 虛析構函數
如果一個基類的指針指向一個派生類的對象,當通過這個基類指針調用delete時,如果派生類的析構函數沒有virtual,就無法清除派生類的資源。
黃金法則:如果一個類設計出來是為了被繼承,並且它擁有虛函數,那麼它的析構函數也必須是虛函數。
2.4.3 override 和 final 關鍵字
override是一個説明符,説明當前函數是重寫的基類的一個虛函數(編譯器會對重寫虛函數的正確與否進行檢查)。
final説明符,在函數後面告訴編譯器不能被進一步的派生類重寫。放在類名後,表示該類不能被繼承。
2.4.4 純虛函數
派生類必須對該函數進行重寫。如果一個類中包含了至少一個純虛函數,那麼這個類就被稱為抽象類。
抽象類不能被實例化。你不能創建一個抽象類的對象。抽象類通常被用作基類,定義一個通用的接口規範,強制所有派生類都必須實現這個接口。它是實現“接口與實現分離”設計思想的強大工具。
// Shape 是一個抽象類,定義了所有“形狀”應該有的接口
class Shape {
public:
// 純虛函數,計算面積
virtual double area() const = 0;
// 純虛函數,繪製形狀
virtual void draw() const = 0;
virtual ~Shape() = default; // 虛析構函數,使用default生成默認實現
};
class Circle : public Shape {
private:
double radius;
public:
Circle(double r) : radius(r) {}
// 必須重寫所有純虛函數,否則Circle也會成為抽象類
double area() const override {
return 3.14159 * radius * radius;
}
void draw() const override {
std::cout << "Drawing a circle." << std::endl;
}
};
// Shape s; // 錯誤!Shape是抽象類,不能實例化
Circle c(10); // 正確,Circle重寫了所有純虛函數,是具體類
Shape* ptr = new Circle(5); // 正確,基類指針可以指向派生類對象
ptr->draw(); // 調用 Circle::draw()
delete ptr;
2.4.5 多態的優勢
- 可擴展性:當你需要增加一個新的派生類時(比如增加一個
Bird類),你只需要編寫Bird類本身並實現它自己的speak()函數。調用多態行為的代碼(如letAnimalSpeak函數)完全不需要任何修改! 這使得系統維護和升級變得異常輕鬆。 - 解耦:多態使得高層模塊(調用方)只依賴於基類的抽象接口,而不依賴於具體的派生類實現。這大大降低了模塊間的耦合度,符合“依賴倒置原則”(DIP)。
- 代碼簡潔與複用:你可以用統一的代碼處理多種不同類型的對象,避免了大量的if-else或switch語句來進行類型判斷和分支處理。代碼更簡潔,邏輯更清晰。
- 框架設計:幾乎所有的大型C++框架(如Qt、MFC、遊戲引擎等)都深度依賴多態。框架定義了一系列抽象基類(接口),用户通過繼承這些基類並實現其虛函數,來將自己的代碼“掛載”到框架中運行。
2.5 模板
泛型編程:泛型編程的思想是編寫與類型無關的代碼。意味着你可以編寫一個通用的算法或數據結構,而不用預先指定它要操作的具體數據類型(如int, double, string等)。當你使用這個模板時,編譯器會根據你提供的具體類型,自動生成一個對應類型的、可執行的代碼版本。
2.5.1 函數模板
模板聲明: template <typename T>,告訴編譯器接下來的東西是一個模板。
typename T:定義了一個模板參數T。T是一個佔位符,代表任何數據類型。typename關鍵字表明T是一個類型名。你也可以使用class關鍵字(template <class T>),在大多數情況下它們是等價的,但現代C++更推薦使用typename,因為它語義更清晰。
#include <iostream>
#include <string>
// 定義一個函數模板
template <typename T> // T 是一個類型模板參數
void mySwap(T& a, T& b) {
T temp = a;
a = b;
b = temp;
std::cout << "通用版本的 mySwap 被調用。" << std::endl;
}
int main() {
int x = 10, y = 20;
mySwap(x, y); // 使用 int 類型
std::cout << "x: " << x << ", y: " << y << std::endl; // x: 20, y: 10
double d1 = 1.1, d2 = 2.2;
mySwap(d1, d2); // 使用 double 類型
std::cout << "d1: " << d1 << ", d2: " << d2 << std::endl; // d1: 2.2, d2: 1.1
std::string s1 = "hello", s2 = "world";
mySwap(s1, s2); // 使用 std::string 類型
std::cout << "s1: " << s1 << ", s2: " << s2 << std::endl; // s1: world, s2: hello
return 0;
}
過程説明
- 推導模板參數:編譯的時候看到傳入的參數
xy都是int類型的,於是推導出模板參數T應該是int。 - 實例化:編譯器使用
int換掉所有的佔位符T,生成一個全新的,專門處理int的函數。 - 編譯這個新生成的函數。
2.5.2 類模板
類模板允許定義一個與類型無關的類家族。最經典的例子就是標準庫中的容器,如std::vector, std::list, std::map等。
template <typename T>
class ClassName {
public:
// 類成員可以使用 T
T memberVar;
void memberFunction(T param);
// ...
};
類模板的實例化
Array<int> intArray(5);
一個簡單的Array類模板
#include <iostream>
#include <stdexcept> // 用於 std::out_of_range
// 定義一個類模板
template <typename T>
class Array {
private:
T* m_data;
int m_size;
public:
// 構造函數
Array(int size) : m_size(size) {
if (size <= 0) {
throw std::invalid_argument("Array size must be positive.");
}
m_data = new T[size]; // 根據類型 T 分配內存
}
// 析構函數
~Array() {
delete[] m_data;
}
// 訪問元素 (重載 [])
T& operator[](int index) {
if (index < 0 || index >= m_size) {
throw std::out_of_range("Index out of range.");
}
return m_data[index];
}
// 獲取大小
int getSize() const {
return m_size;
}
// 禁止拷貝構造和賦值,簡化示例
Array(const Array&) = delete;
Array& operator=(const Array&) = delete;
};
int main() {
// 實例化一個存儲 int 的 Array
Array<int> intArray(5);
for (int i = 0; i < intArray.getSize(); ++i) {
intArray[i] = i * 10;
}
std::cout << "Int Array: ";
for (int i = 0; i < intArray.getSize(); ++i) {
std::cout << intArray[i] << " "; // 輸出: Int Array: 0 10 20 30 40
}
std::cout << std::endl;
// 實例化一個存儲 double 的 Array
Array<double> doubleArray(3);
doubleArray[0] = 3.14;
doubleArray[1] = 2.71;
doubleArray[2] = 1.41;
std::cout << "Double Array: ";
for (int i = 0; i < doubleArray.getSize(); ++i) {
std::cout << doubleArray[i] << " "; // 輸出: Double Array: 3.14 2.71 1.41
}
std::cout << std::endl;
return 0;
}
如果成員函數的定義在類體外,必須再次聲明為模板,並且作用域解析符要寫成ClassName<T>::。
template <typename T>
T& Array<T>::operator[](int index) {
// ... 實現
}
2.5.3 模板的工作原理
兩個編譯階段
-
模板定義檢查,只進行一些與類型定義無關的檢查。例如,檢查模板語法是否正確,是否有不匹配的括號、缺少分號等。
-
模板實例化
a. 當編譯器遇到一個模板的具體使用時(如
mySwap(x, y)或Array<int>),它會進行模板參數推導。b. 用推導出的具體類型(如
int)去替換模板中的所有T,生成一個完整的、具體的函數或類定義。這個過程叫做實例化。c. 這時,編譯器才會對實例化後的代碼進行完整的、與類型相關的檢查。 如果
T是int,它就檢查int是否支持=操作。如果T是一個自定義類MyClass,它就檢查MyClass是否有拷貝賦值運算符。
由於模板的實例化需要看到完整的模板定義,所以C++標準規定,模板的定義(而不僅僅是聲明)通常必須放在頭文件中。
- 原因:當你在
main.cpp中使用Array<int>時,編譯器需要Array類的完整定義來生成Array<int>的代碼。如果Array的成員函數定義在一個單獨的.cpp文件(如Array.cpp)中,那麼編譯main.cpp的編譯單元就看不到這些定義,無法實例化,會導致鏈接錯誤。 - 實踐:因此,我們習慣將類模板及其所有成員函數的定義都寫在同一個頭文件(
.h或.hpp)中。這就是所謂的“包含模型”。
2.5.4 模板的非類型參數
模板參數不僅可以是類型(typename T),還可以是值。這被稱為非類型模板參數。非類型參數通常是整型(int, size_t)、枚舉、指針或引用。最常見的就是整型,用來指定大小。改進上面的Array類,讓數組的大小在編譯時就確定下來,而不是在運行時通過構造函數傳入。
數組大小在編譯時確定,可以在棧上分配內存,避免了堆分配(new/delete)的開銷,速度更快。
大小是類型的一部分,StaticArray<int, 5>和StaticArray<int, 10>是兩種完全不同的類型,編譯器可以防止將它們混用。
標準庫中的例子:std::array就是一個典型的非類型參數模板:std::array<int, 10>。
#include <iostream>
// 模板參數列表現在有兩個:
// T: 類型參數,表示元素類型
// N: 非類型參數,表示數組大小,它是一個 size_t 類型的常量
template <typename T, size_t N>
class StaticArray {
private:
T m_data[N]; // 在棧上分配一個固定大小的數組
public:
// 不再需要構造函數指定大小,大小由 N 決定
// 析構函數也不需要了,因為 m_data 在棧上自動管理
T& operator[](int index) {
// 可以移除運行時大小檢查,因為大小在編譯時已知
// 但為了安全,可以保留
if (index < 0 || index >= N) {
// 簡單處理,實際中應拋出異常
exit(1);
}
return m_data[index];
}
int getSize() const {
return N;
}
};
int main() {
// 實例化時,必須同時提供類型和大小
StaticArray<int, 5> intArray; // 創建一個包含5個int的靜態數組
for (int i = 0; i < intArray.getSize(); ++i) {
intArray[i] = i * 10;
}
std::cout << "Static Int Array: ";
for (int i = 0; i < intArray.getSize(); ++i) {
std::cout << intArray[i] << " "; // 輸出: Static Int Array: 0 10 20 30 40
}
std::cout << std::endl;
// 編譯器知道 N 的值,可以進行編譯時優化
// 例如,循環展開等
// StaticArray<double, 10> doubleArray;
return 0;
}
2.5.5 模板中的特化
特化的語法:名字後面跟一個<>,裏面放特化的類型。
用於處理非常規的特定類型。
模板函數特化
template <typename T>
void print(T value) {
std::cout << "通用打印: " << value << std::endl;
}
// 針對 const char* 的特化版本
// 語法: template<> 返回類型 function_name<具體類型>(參數列表)
template<>
void print<const char*>(const char* value) {
std::cout << "C風格字符串特化打印: " << static_cast<const void*>(value) << std::endl;
}
// 注意:現代C++更傾向於使用函數重載而不是函數模板特化,因為重載的規則更直觀、更少陷阱。上面的例子用重載寫會更簡單:
void print(const char* value) { // 直接重載一個 const char* 版本
std::cout << "C風格字符串重載打印: " << static_cast<const void*>(value) << std::endl;
}
int main() {
int i = 123;
print(i); // 調用通用版本
const char* str = "Hello";
print(str); // 調用 const char* 的特化版本
return 0;
}
類模板特化
全特化是針對某個固定參數組和的特化,偏特化是一類類型組和的特化。
// 通用版本
template <typename T1, typename T2>
class Pair {
private:
T1 first;
T2 second;
public:
Pair(T1 f, T2 s) : first(f), second(s) {}
void print() {
std::cout << "通用 Pair: (" << first << ", " << second << ")" << std::endl;
}
};
// 針對 Pair<bool, bool> 的全特化
template<> // 表示這是一個特化
class Pair<bool, bool> { // 指定所有模板參數的具體類型
private:
// 可以用更高效的方式存儲,比如用一個 byte 的兩個 bit
bool m_first;
bool m_second;
public:
Pair(bool f, bool s) : m_first(f), m_second(s) {}
void print() {
std::cout << "特化 Pair<bool, bool>: ("
<< std::boolalpha << m_first << ", "
<< std::boolalpha << m_second << ")" << std::endl;
}
};
// 偏特化:當兩個參數都是指針時
template <typename T1, typename T2> // 參數列表變了,現在是兩個基礎類型
class Pair<T1*, T2*> { // 特化聲明,表示我們特化的是 T1* 和 T2*
private:
T1* first;
T2* second;
public:
Pair(T1* f, T2* s) : first(f), second(s) {}
void print() {
std::cout << "指針 Pair 偏特化: (" << *first << ", " << *second << ")" << std::endl;
}
};
int main() {
Pair<int, double> p1(10, 3.14);
p1.print(); // 調用通用版本
Pair<bool, bool> p2(true, false);
p2.print(); // 調用 Pair<bool, bool> 的特化版本
int a = 100;
double b = 2.718;
Pair<int*, double*> p3(&a, &b);
p3.print(); // 調用指針偏特化版本
return 0;
}
2.5.6 類模板作為函數參數
template <typename T>
class Box {
private:
T m_content;
public:
Box(T content) : m_content(content) {}
T getContent() const { return m_content; }
};
template <typename U>
void printBoxByValue(Box<U>& box) { // 注意這裏是按值傳遞
std::cout << "Box Content (by value): " << box.getContent() << std::endl;
}
2.5.7 類模板與繼承
派生類不是類模板
// 基類:一個通用的數組類模板
template <typename T>
class Array {
protected:
T* m_data;
size_t m_size;
public:
Array(size_t size) : m_size(size), m_data(new T[size]) {}
virtual ~Array() { delete[] m_data; } // 虛析構函數是好習慣
// 提供基本訪問接口
T& operator[](size_t index) {
if (index >= m_size) throw std::out_of_range("Index out of range");
return m_data[index];
}
const T& operator[](size_t index) const {
if (index >= m_size) throw std::out_of_range("Index out of range");
return m_data[index];
}
size_t getSize() const { return m_size; }
};
// 派生類:一個專門處理 int 數組的類,增加了統計功能
// 它明確地繼承自 Array<int>
class IntStatisticArray : public Array<int> {
public:
// 使用基類的構造函數 (C++11 特性)
using Array<int>::Array;
// 新增功能:計算數組元素的總和
int sum() const {
int total = 0;
for (size_t i = 0; i < this->m_size; ++i) { // 注意 this-> 的使用
total += this->m_data[i];
}
return total;
}
// 新增功能:計算數組的平均值
double average() const {
if (this->m_size == 0) return 0.0;
return static_cast<double>(sum()) / this->m_size;
}
};
派生類是類模板
// 派生類模板:一個安全的數組,它本身也是一個模板
template <typename T>
class SafeArray : public Array<T> {
public:
using Array<T>::Array; // 繼承構造函數
// 新增安全的訪問方法
void set(size_t index, const T& value) {
if (index >= this->m_size) throw std::out_of_range("Set: Index out of range");
this->m_data[index] = value;
}
T get(size_t index) const {
if (index >= this->m_size) throw std::out_of_range("Get: Index out of range");
return this->m_data[index];
}
};
2.5.8 類模板函數類外實現
// 1. 類模板的聲明
template <typename T> // 或 template <class T>
class MyClass {
public:
void memberFunction(T param); // 成員函數在類內聲明
};
// 2. 成員函數在類外實現
// 格式:template <...> 返回類型 類名<T>::函數名(參數列表) { ... }
template <typename T>
void MyClass<T>::memberFunction(T param) {
// 函數體實現
// ...
}
2.5.9 類模板分文件編寫
/* classxx.hpp */
#pragma once
#include <iostream>
#include <string>
// 1. 類模板的聲明
template <typename T> // 或 template <class T>
class MyClass {
public:
void memberFunction(T param); // 成員函數在類內聲明
};
// 2. 成員函數在類外實現
// 格式:template <...> 返回類型 類名<T>::函數名(參數列表) { ... }
template <typename T>
void MyClass<T>::memberFunction(T param) {
// 函數體實現
// ...
}
2.5.10 類模板與友元
全局函數類內實現
template <typename T1, typename T2>
class Person {
friend void printPerson(Person<T1, T2> p)
{
std::cout << "姓名" << p.m_Name << "年齡" << p.m_Age << std::endl;
}
public:
Person(T1 name, T2 age)
{
m_Name = name;
m_Age = m_Age;
}
T1 m_Name;
T2 m_Age;
}
全局函數類外實現
template <typename T1, typename T2>
class Person;
// 函數模板的函數實現
template <typename T1, typename T2>
void printPerson(Person<T1, T2> p)
{
std::cout << "姓名" << p.m_Name << "年齡" << p.m_Age << std::endl;
}
template <typename T1, typename T2>
class Person {
// 函數模板的函數聲明,需要讓編譯器提前知道有這麼一個模板存在
friend void printPerson<>(Person<T1, T2> p);
public:
Person(T1 name, T2 age)
{
m_Name = name;
m_Age = m_Age;
}
T1 m_Name;
T2 m_Age;
}
三、STL容器
STL大概有六類:容器,算法,迭代器,偽函數,適配器,空間配置器。
常用的容器有動態數組,棧,隊列等。
容器可以分為序列容器,關聯容器,無序容器三類。
- 序列容器有動態數組,雙端隊列,雙向鏈表,單向鏈表等。
std::vector std::deque std::list std::forward_list
- 關聯容器有集合和映射
std::set std::map
td::multiset std::multimap
// 它們分別是 set 和 map 的變體,唯一的區別是允許鍵重複。
- 無序容器有set 和 map 的無序版本。
std::unordered_set std::unordered_map
3.1 容器適配器
容器適配器不是完整的容器,它們是基於其他容器實現的,提供了特定的接口,限制了容器的功能。
/*
1. std::stack - 棧
特點:後進先出。
默認底層容器:deque。
核心操作:push() (入棧), pop() (出棧), top() (訪問棧頂)。
2. std::queue - 隊列
特點:先進先出。
默認底層容器:deque。
核心操作:push() (入隊), pop() (出隊), front() (訪問隊首), back() (訪問隊尾)。
3. std::priority_queue - 優先隊列
特點:元素被賦予優先級,訪問時總是優先級最高的元素先出隊(默認是最大堆)。
默認底層容器:vector。
核心操作:push() (插入), pop() (移除頂部元素), top() (訪問頂部元素)。
*/
容器選擇:
/*
如何選擇合適的容器?
這是一個非常實際的問題,可以參考下面的決策流程:
需要“後進先出”或“先進先出”的行為嗎?
是:使用 stack 或 queue。
否:繼續。
需要根據鍵快速查找值(字典功能)嗎?
是:
需要元素按鍵排序嗎?是 -> 用 map / multimap。
不需要排序,只追求最快查找速度?是 -> 用 unordered_map / unordered_multimap。
否:繼續。
需要存儲一組唯一的元素,並快速判斷元素是否存在嗎?
是:
需要元素自動排序嗎?是 -> 用 set / multiset。
不需要排序,只追求最快查找速度?是 -> 用 unordered_set / unordered_multiset。
否:繼續。
主要操作是隨機訪問(通過下標訪問元素)嗎?
是:首選 std::vector。它在幾乎所有情況下都是最佳選擇。
否:繼續。
是否需要在序列的頭部和尾部都高效地插入/刪除元素?
是:用 std::deque。
否:繼續。
是否需要在序列的中間位置頻繁地插入/刪除元素,並且不關心隨機訪問?
是:用 std::list 或 std::forward_list。
否:回到第 4 步,std::vector 通常是默認的最佳選擇。
總結:
默認首選 vector:它的性能在大多數場景下都非常出色,尤其是緩存友好性。
需要字典/集合:如果需要排序,用 map/set;如果追求極致速度,用 unordered_map/unordered_set。
需要雙端操作:用 deque。
需要中間頻繁插入/刪除:用 list。
需要特定數據結構:用 stack, queue, priority_queue。
*/
3.2 迭代器的常用函數
#include <iterator>
std::list<int> lst = {1, 2, 3};
auto it = lst.begin();
advance(it, 2); // it 指向 3
auto it_next = next(it, 2); // 返回 it + 2
auto it_prev = prev(it, 1); // 返回 it - 1
int dist = distance(v.begin(), v.end()); // 返回容器大小
auto it = vec.begin();
auto cit = vec.cbegin(); // const 迭代器
// 反向迭代器
for (auto rit = vec.rbegin(); rit != vec.rend(); ++rit) {
std::cout << *rit << " ";
}
3.3 string(字符串)
// 初始化
string s1; // 空字符串
string s2 = "Hello"; // 直接賦值
string s3("World"); // 構造函數初始化
string s4(5, 'A'); // "AAAAA"(重複字符)
s.size();
s.empty();
s1 = "C++"; // 直接賦值
s.append(" C++");
s.insert(5, "Java ");
// find(): 判斷是否包含字串,返回首次出現的索引(未找到返回 string::npos)
size_t pos = s.find("World");
if (pos != string::npos)
pos = s.rfind("l"); // 最後一次出現 'l' 的索引(輸出 9)
s.replace(6, 5, "C++"); // 從索引 6 開始替換 5 個字符 → "Hello C++"
s.erase(5, 6); // 從索引 5 開始刪除 6 個字符 → "Hello"
s.pop_back(); // 刪除最後一個字符(C++11 起)
string sub = s.substr(6, 5); // 從索引 6 開始取 5 個字符 → "World"
// 方法 1: 索引遍歷
for (size_t i = 0; i < s.size(); ++i) {
cout << s[i] << " "; // H e l l o
}
// 方法 2: 範圍 for 循環(C++11 起)
for (char c : s) {
cout << c << " "; // H e l l o
}
輸入輸出
// 讀取單詞(遇到空格停止)
cout << "Enter a word: ";
cin >> s;
// 讀取整行(包括空格)
cout << "Enter a line: ";
cin.ignore(); // 清除輸入緩衝區中的換行符
getline(cin, s);
// 讀取帶空格的字符串
getline(cin, s);
// 分別讀取兩個單詞
std::cin >> s; // 讀取第一個單詞(可選)
std::cin.ignore(); // 跳過空格
std::getline(std::cin, s1); // 讀取剩餘部分(包括空格)
3.4 vector(序列)
#include <vector>
vector<int> vec; // 聲明一個存儲int的vector
vector<int> v1; // 空vector
vector<int> v2(5); // 5個元素,默認初始化為0
vector<int> v3(5, 10); // 5個元素,初始化為10
vector<int> v4 = {1, 2, 3, 4, 5}; // 初始化列表
vector<int> v5(v4.begin(), v4.end()); // 從其他容器複製
v.size(); // 返回元素數量
v.empty(); // 是否為空
v.push_back(10); // 在末尾添加元素
v.pop_back(); // 刪除末尾元素
v.insert(v.begin() + 2, 99); // 在指定位置插入
v.erase(v.begin() + 1); // 刪除指定位置元素
v.clear(); // 清空所有元素
v[0]; // 訪問元素(不檢查邊界)
v.at(0); // 訪問元素(檢查邊界,越界拋出異常)
v.front(); // 第一個元素
v.back(); // 最後一個元素
for (vector<int>::iterator it = v.begin(); it != v.end(); ++it) {
cout << *it << " ";
}
for (auto num : v) {
cout << num << " ";
}
3.5 list(雙向鏈表)
#include <list>
list<int> myList; // 聲明一個存儲int的list
list<int> l1; // 空list
list<int> l2(5); // 5個元素,默認初始化為0
list<int> l3(5, 10); // 5個元素,初始化為10
list<int> l4 = {1, 2, 3, 4, 5}; // 初始化列表
list<int> l5(l4.begin(), l4.end()); // 從其他容器複製
l.size(); // 返回元素數量
l.empty(); // 是否為空
l.push_back(10); // 在末尾添加元素
l.push_front(5); // 在頭部添加元素
l.pop_back(); // 刪除末尾元素
l.pop_front(); // 刪除頭部元素
auto it = l.begin();
// list不能隨機訪問,因此需要藉助迭代器進行插入數據
l.insert(it, 99); // 在開頭插入99
l.insert(next(it, 2), 88); // 在第3個位置插入88
l.erase(it); // 刪除指定位置元素
l.clear(); // 清空所有元素
l.front(); // 第一個元素
l.back(); // 最後一個元素
for (list<int>::iterator it = l.begin(); it != l.end(); ++it) {
cout << *it << " ";
}
// C++11 起更簡潔的寫法
for (auto num : l) {
cout << num << " ";
}
l.remove(5); // 刪除所有等於5的元素
l.remove_if([](int n){ return n % 2 == 0; }); // 刪除所有偶數
l.unique(); // 刪除相鄰的重複元素(需先排序)
l.sort(); // *排序(默認升序)
l.sort(greater<int>()); // *降序排序
l.merge(anotherList); // 合併兩個已排序的list
l.splice(it, anotherList); // 將anotherList剪接到it位置前
l.reverse(); // 反轉鏈表
3.6 stack(棧)
#include <stack>
stack<int> s; // 聲明一個存儲int的stack
stack<int, vector<int>> s1; // 使用vector作為底層容器
stack<int, list<int>> s2; // 使用list作為底層容器
stack<int, deque<int>> s3; // 使用deque作為底層容器(默認)
s.empty(); // 檢查棧是否為空
s.size(); // 返回棧中元素數量
s.push(10); // 壓入元素到棧頂
s.pop(); // 移除棧頂元素(不返回)
s.top(); // 訪問棧頂元素
// 括號匹配檢查
// default: 判斷棧是否為空:如果棧為空,説明前面沒有開括號與之匹配,直接返回 false。
// 判斷棧頂元素是否等於當前字符:如果不相等,説明括號類型不匹配,返回 false。
bool isValidParentheses(const string& s) {
stack<char> st;
for (char c : s) {
switch (c) {
case '(': st.push(')'); break;
case '[': st.push(']'); break;
case '{': st.push('}'); break;
default:
if (st.empty() || st.top() != c) return false;
st.pop();
}
}
return st.empty();
}
3.7 queue(隊列)
#include <queue>
queue<int> q; // 聲明一個存儲int的queue
queue<int, deque<int>> q1; // 使用deque作為底層容器(默認)
queue<int, list<int>> q2; // 使用list作為底層容器
// 注意:queue不能使用vector作為底層容器
q.empty(); // 檢查隊列是否為空
q.size(); // 返回隊列中元素數量
q.push(10); // 在隊尾添加元素
q.pop(); // 移除隊首元素(不返回)
q.front(); // 訪問隊首元素
q.back(); // 訪問隊尾元素
/****************************操作*************************************/
queue<string> tasks;
// 添加元素到隊尾
tasks.push("Write code");
tasks.push("Compile");
tasks.push("Test");
tasks.push("Debug");
// 查看隊列大小
cout << "Queue size: " << tasks.size() << endl;
// 訪問隊首和隊尾元素
cout << "First task: " << tasks.front() << endl;
cout << "Last task: " << tasks.back() << endl;
// 處理隊列中的任務
while (!tasks.empty()) {
cout << "Processing: " << tasks.front() << endl;
tasks.pop(); // 移除已處理的任務
}
3.8 set(有序關聯容器)
在 C++ 中,std::set 是一個有序關聯容器,存儲唯一的元素,並自動按升序(默認)或自定義順序排序。它基於紅黑樹(Red-Black Tree)實現,支持高效的插入、刪除和查找操作(時間複雜度均為 O(logn)**)。
#include <set>
// 默認升序排序
set<int> s1 = {3, 1, 4, 1, 5, 9, 2, 6};
set<int, greater<int>> s2 = {3, 1, 4, 1, 5, 9, 2, 6};// 降序
set<int> s;
s.insert(3); // {3}
s.insert(1); // {1, 3}
s.insert(4); // {1, 3, 4}
s.insert(1); // 重複元素不會被插入
s.erase(3); // 刪除值為 3 的元素
s.erase(s.begin()); // 刪除迭代器指向的元素
if (s.find(4) != s.end()) {} // 查找
if (s.count(1)) {}
for (auto it = s.begin(); it != s.end(); ++it) {} // 遍歷
for (int x : s) {}
3.9 map(有序哈希)
在 C++ 中,std::map 是一個基於紅黑樹(Red-Black Tree)實現的有序關聯容器,它存儲鍵值對(key-value),並按照鍵的升序排列(默認使用 std::less<Key> 比較)。std::map 提供 O(logn) 時間複雜度的插入、刪除和查找操作,適用於需要有序遍歷的場景。
注意的點:
- 鍵值唯一
- 元素默認按鍵的升序排列
#include <map>
map<string, int> wordCount; // 鍵為string,值為int
wordCount["apple"] = 5; // 插入數據
wordCount["banana"] = 3;
wordCount["orange"] = 7;
// 初始化列表方式
map<string, int> scores = {
{"Alice", 90},
{"Bob", 85},
{"Charlie", 95}
};
m.size()
m.empty()
// 插入元素
m["apple"] = 10;
m.insert({"banana", 20});
m.insert(make_pair("cherry", 30));
m.emplace("date", 40);
// 訪問元素
cout << m["apple"] << endl;
auto it = m.find("cherry");
if (it != m.end()) { cout << "cherry: " << it->second << endl; }
// it->first 就是 Key(鍵);it->second 就是 Value(值)
// 遍歷
for (const auto& pair : m) {
cout << pair.first << ": " << pair.second << endl;
}
for (auto it = m.begin(); it != m.end(); ++it) {
cout << it->first << " -> " << it->second << endl;
}
for (auto rit = m.rbegin(); rit != m.rend(); ++rit) {
cout << rit->first << " = " << rit->second << endl;
}
// 刪除
m.erase("apple"); // 按鍵刪除
m.erase(m.begin()); // 按迭代器刪除
m.erase(m.begin(), m.find("cherry")); // 刪除範圍 [begin, cherry)
m.clear()
// 判斷
if (m.count("banana") > 0) {
cout << "banana exists!" << endl;
}
if (m.find("cherry") != m.end()) {
cout << "cherry exists!" << endl;
}
std::map適用於需要有序遍歷的場景,如按字母順序存儲單詞。std::unordered_map適用於需要快速查找的場景,如字典、緩存。
3.10 unordered_map & unordered_set(鍵值對&單值)
無序關聯容器
| 特性 | std::unordered_map |
std::unordered_set |
|---|---|---|
| 底層實現 | 哈希表(鏈地址法) | 哈希表(鏈地址法) |
| 時間複雜度 | 平均 O(1),最壞 O(n) | 平均 O(1),最壞 O(n) |
| 是否有序 | ❌ 無序 | ❌ 無序 |
| 鍵是否唯一 | 鍵唯一(值可重複) | 鍵唯一(無值) |
| 適用場景 | 快速查找、緩存、字典 | 快速去重、集合運算 |
| 頭文件 | #include <unordered_map> |
#include <unordered_set> |
#include <unordered_map>
unordered_map<string, int> wordCount;
wordCount["apple"] = 5;
unordered_map<string, int> scores = {
{"Alice", 90},
{"Bob", 85},
{"Charlie", 95}
};
m.insert({"banana", 20});
m.insert(make_pair("cherry", 30));
m.emplace("date", 40);
cout << m["apple"] << endl;
cout << m.at("banana") << endl;
auto it = m.find("cherry");
if (it != m.end()) {
cout << "cherry: " << it->second << endl;
}
for (const auto& pair : m) {
cout << pair.first << ": " << pair.second << endl;
}
for (auto it = m.begin(); it != m.end(); ++it) {
cout << it->first << " -> " << it->second << endl;
}
m.erase("apple"); // 按鍵刪除
m.erase(m.begin()); // 按迭代器刪除
if (m.count("banana") > 0) {
cout << "banana exists!" << endl;
}
if (m.find("cherry") != m.end()) {
cout << "cherry exists!" << endl;
}
// 給鍵添加值
unordered_map<string, vector<string>> m;
m[sorted_s].push_back(s);
#include <unordered_set>
unordered_set<string> fruits = {"apple", "banana", "orange"};
fruits.insert("cherry");
fruits.emplace("date");
if (fruits.count("banana")) {
cout << "banana exists!" << endl;
}
// 遍歷(順序不確定!)
for (const auto& fruit : fruits) {
cout << fruit << endl;
}
fruits.erase("apple"); // 刪除 "apple"
fruits.erase(fruits.begin()); // 刪除第一個元素(順序不確定!)
3.11 priority_queue(默認最大堆)
#include <queue> // 包含 priority_queue 的定義
//最小堆
std::priority_queue<int, std::vector<int>, std::greater<int>> min_pq;
// 默認最大堆
// 自定義實現最小堆的方法
class Solution {
public:
static bool cmp(pair<int, int>& m, pair<int, int>& n) {
return m.second > n.second;
}
// 最小堆
priority_queue<
pair<int, int>,
vector<pair<int, int>>,
decltype(&cmp)
> q(cmp);
};
| 操作 | 説明 | 示例 |
|---|---|---|
pq.push(x) |
插入元素 x |
pq.push(10); |
pq.pop() |
刪除堆頂元素 | pq.pop(); |
pq.top() |
返回堆頂元素(不刪除) | int max = pq.top(); |
pq.empty() |
檢查隊列是否為空 | if (pq.empty()) { ... } |
pq.size() |
返回隊列大小 | int n = pq.size(); |
3.12 emplace
std::vector<std::pair<int, std::string>> vec;
vec.emplace_back(42, "hello"); // 直接構造 pair,無需臨時對象
vec.push_back(std::make_pair(42, "hello")); // 需要構造臨時 pair
std::deque<std::string> dq;
dq.emplace_back("world"); // 直接構造字符串
dq.emplace_front("hello"); // 直接構造字符串
std::list<std::pair<int, int>> lst;
lst.emplace(lst.begin(), 1, 2); // 在頭部直接構造 pair(1, 2)
std::set<std::string> s;
s.emplace("apple"); // 直接構造字符串
s.insert(std::string("apple")); // 需要構造臨時對象
std::map<int, std::string> m;
m.emplace(1, "one"); // 直接構造 pair(1, "one")
m.insert(std::make_pair(1, "one")); // 需要構造臨時 pair
std::unordered_set<std::string> us;
us.emplace("banana"); // 直接構造字符串
std::unordered_map<int, std::string> um;
um.emplace(2, "two"); // 直接構造 pair(2, "two")
std::stack<std::pair<int, int>> stk;
stk.emplace(1, 2); // 直接構造 pair(1, 2)
std::priority_queue<std::pair<int, int>> pq;
pq.emplace(3, 4); // 直接構造 pair(3, 4)
四、算法
#include <algorithm>
std::reverse(words.begin(), words.end()); // 可以反轉各種數據類型
std::swap(arr[i], arr[j]); // 數據交換
sort(v.begin(), v.end()); // 升序
sort(v.begin(), v.end(), greater<int>()); // 降序
stable_sort(v.begin(), v.end());
auto it = find(v.begin(), v.end(), 8);
if (it != v.end()) cout << "Found";
sort(v.begin(), v.end());
if (binary_search(v.begin(), v.end(), 8)) cout << "Yes"; //有序區間二分查找
auto it1 = lower_bound(v.begin(), v.end(), 8);
auto it2 = upper_bound(v.begin(), v.end(), 8);
int a = 3, b = 7;
cout << min(a, b) << " " << max(a, b);
auto it_min = min_element(v.begin(), v.end());
auto it_max = max_element(v.begin(), v.end());
rotate(v.begin(), v.begin() + 2, v.end()); // 左旋2位
sort(v.begin(), v.end());
auto it = unique(v.begin(), v.end());
v.erase(it, v.end());
// 數值運算
#include <numeric>
int sum = accumulate(v.begin(), v.end(), 0); // 求和
vector<int> res;
partial_sum(v.begin(), v.end(), back_inserter(res)); // res = [a, a+b, a+b+c, a+b+c+d]
// back_inserter是一個迭代器適配器。當你向這個迭代器“賦值”時(比如 *it = value),它不會覆蓋某個已有位置的值,而是會自動調用 res.push_back(value),將這個新值 value 追加(append)到 res 的末尾。
// 因為 res 一開始是空的!如果我們直接使用 res.begin() 作為目標,partial_sum 嘗試向一個空容器的起始位置寫入數據,會導致未定義行為(通常是程序崩潰)。back_inserter 解決了這個問題:它告訴 partial_sum:“你每算出一個結果,就把它當作一個新元素,添加到 res 的屁股後面去。” 這樣,res 會自動增長,完美地容納所有計算結果。
五、其他
stringstream
std::stringstream是C++標準庫中的一個類。用於項標準輸入輸出流一樣讀寫字符串數據。
#include <sstream>
std::string s("hello world");
std::stringstream ss(s);
std::string word;
std::vector<std::string> words;
while (ss >> word) {
words.push_back(word);
}
std::cout << words[0] << words[1];
// 在vector<std::string> words(2) 的情況下可以使用words[i]=word;賦值,不然在沒有分配空間的時候賦值會發生錯誤
reinterpret_cast<new_type>(expression)
// 強制類型轉換
// 指針類型轉換
int num = 65;
char* p = reinterpret_cast<char*>(&num); // int* 轉 char*
// 整數轉換為指針
uintptr_t addt = 0x12345678;
int* ptr = reinterpret_cast<int*>(addr);
// 不相關類型之間的轉換
struct A { int x; };
struct B { int y; };
A a{10};
B* b = reinterpret_cast<B*>(&a);
cout << b->y << endl; // 輸出 10,數據本身沒變,只是按 B 的佈局去讀。
iomanip
// setprecision(n) —— 設置有效數字或小數位數
float num = 3.14159; // 不加 fixed:控制的是有效數字位數
cout << fixed << setprecision(2) << num << endl; // 輸出 3.14
// 輸出對齊操作
cout << left << setw(10) << 42 << endl; // 左對齊
cout << right << setw(10) << 42 << endl; // 右對齊
cout << internal << setw(10) << -42 << endl; // 符號在最左,數字右對齊
cout << setw(8) << setfill('0') << 42 << endl; // 00000042
// 進制輸出
cout << hex << 255 << endl; // ff
cout << dec << 255 << endl; // 255
cout << oct << 255 << endl; // 377
// 十六進制操作
std::cout << std::uppercase << std::hex << x << std::endl; // FF
std::cout << std::showbase << std::hex << x << std::endl; // 0xff
extern "C"
告訴C++編譯器按照C語言的規則處理函數的聲明和定義。
- 在C++文件中使用C函數
// 在 C++ 文件中聲明C函數
extern "C" {
#include "c_library.h" // 假設這是 C 語言的頭文件
}
// 確保只有C++編譯器會處理extern "C"
#ifdef __cplusplus
extern "C" {
#endif
void cpp_function_for_c(int x); // C 兼容的函數聲明
#ifdef __cplusplus
}
#endif
- 在C文件中使用C++定義的函數
// C++文件中定義C兼容函數。cpp_file.cpp
#include <iostream>
extern "C" void hello_from_cpp() {
std::cout << "Hello from C++!" << std::endl;
}
// c_file.c
void hello_from_cpp(); // 聲明為 C 函數
int main() {
hello_from_cpp(); // 調用 C++ 函數
return 0;
}
climits
| 宏 | 含義 | 典型值(32位系統) |
|---|---|---|
INT_MAX |
int 的最大值 |
2147483647 |
INT_MIN |
int 的最小值 |
-2147483648 |
UINT_MAX |
unsigned int的最大值 |
4294967295 |
CHAR_BIT |
一個 char 的位數(通常是 8) | 8 |
CHAR_MAX |
char 的最大值 | 127 或 255(取決於是否為 signed char) |
CHAR_MIN |
char 的最小值 | -128 或 0 |
SHRT_MAX |
short 的最大值 |
32767 |
SHRT_MIN |
short 的最小值 |
-32768 |
USHRT_MAX |
unsigned short 的最大值 |
65535 |
LONG_MAX |
long 的最大值 |
2147483647 或更大 |
LONG_MIN |
long 的最小值 |
-2147483648 或更小 |
ULONG_MAX |
unsigned long 的最大值 |
4294967295 或更大 |
LLONG_MAX |
long long 的最大值 |
9223372036854775807 |
LLONG_MIN |
long long 的最小值 |
-9223372036854775808 |
ULLONG_MAX |
unsigned long long 的最大值 |
18446744073709551615 |
輸入輸出
讀取一個不定長序列
vector<int> nums;
int num;
while (cin >> num) { // 循環讀取直到輸入結束或非法輸入
nums.push_back(num);
}
整行讀取
string line;
getline(cin, line);
stringstream ss(line);
int num;
while (ss >> num) {
nums.push_back(num);
}
結語
本文對 C++ 部分知識點進行了總結,學習過程中填填補補所以會有點亂,後續會持續補充整理。
Steady progress!