C++學習:六個月從基礎到就業——內存管理:new/delete操作符
本文是我C++學習之旅系列的第十七篇技術文章,也是第二階段"C++進階特性"的第二篇,主要介紹C++中動態內存管理的核心操作符——new和delete。查看完整系列目錄了解更多內容。
引言
在上一篇文章中,我們深入探討了堆和棧的概念以及它們在內存管理中的作用。本文將聚焦于C++中用于動態內存分配和釋放的基本工具——new
和delete
操作符。與許多高級語言不同,C++允許程序員直接控制內存的分配和釋放,這提供了極大的靈活性,但同時也帶來了更多的責任和潛在的陷阱。
動態內存管理是現代軟件開發中不可或缺的部分,尤其是當處理大型數據結構、運行時大小未知的數據或需要在程序執行過程中持久存在的對象時。理解new
和delete
的工作原理及正確使用方法,對于編寫高效、穩定的C++程序至關重要。
new和delete基礎
new操作符概述
new
操作符用于動態分配內存,它執行三個主要步驟:
- 分配足夠大的未初始化內存來存儲指定類型的對象
- 調用構造函數初始化對象(如果是類類型)
- 返回指向新創建對象的指針
基本語法:
Type* ptr = new Type; // 分配單個對象
Type* arr = new Type[size]; // 分配對象數組
delete操作符概述
delete
操作符用于釋放動態分配的內存,它執行兩個主要步驟:
- 調用對象的析構函數(如果是類類型)
- 釋放內存
基本語法:
delete ptr; // 釋放單個對象的內存
delete[] arr; // 釋放數組的內存
簡單示例
下面是一個使用new
和delete
的基本示例:
#include <iostream>class Simple {
public:Simple() {std::cout << "Simple constructor called" << std::endl;}~Simple() {std::cout << "Simple destructor called" << std::endl;}void sayHello() {std::cout << "Hello from Simple object!" << std::endl;}
};int main() {// 分配單個對象Simple* obj = new Simple;obj->sayHello();delete obj; // 釋放單個對象std::cout << "-------------------" << std::endl;// 分配對象數組Simple* objArray = new Simple[3];objArray[0].sayHello();objArray[1].sayHello();objArray[2].sayHello();delete[] objArray; // 釋放數組return 0;
}
輸出結果:
Simple constructor called
Hello from Simple object!
Simple destructor called
-------------------
Simple constructor called
Simple constructor called
Simple constructor called
Hello from Simple object!
Hello from Simple object!
Hello from Simple object!
Simple destructor called
Simple destructor called
Simple destructor called
new操作符詳解
分配過程詳解
當我們使用new
操作符時,它實際執行以下操作:
- 調用底層的內存分配函數(通常是
operator new
)分配足夠的原始內存 - 將原始內存轉換為適當的類型指針
- 使用適當的構造函數初始化對象(對于非POD類型)
new的變體
帶初始化的new
int* p1 = new int; // 未初始化值
int* p2 = new int(); // 初始化為0
int* p3 = new int(42); // 初始化為42// C++11后,可以使用統一初始化語法
int* p4 = new int{42}; // 初始化為42
帶位置的new (placement new)
Placement new允許在預先分配的內存位置構造對象,而不分配新內存:
#include <iostream>
#include <new> // 為placement new包含此頭文件class Complex {
private:double real;double imag;public:Complex(double r, double i) : real(r), imag(i) {std::cout << "Constructor called." << std::endl;}~Complex() {std::cout << "Destructor called." << std::endl;}void print() const {std::cout << real << " + " << imag << "i" << std::endl;}
};int main() {// 分配一塊足夠大的內存,但不構造對象char memory[sizeof(Complex)];// 在預先分配的內存上構造對象Complex* obj = new(memory) Complex(3.0, 4.0);obj->print();// 顯式調用析構函數(不要使用delete,因為內存不是通過new分配的)obj->~Complex();return 0;
}
Placement new主要用于:
- 優化內存分配(避免多次分配/釋放)
- 內存池實現
- 對象的精確放置(如硬件通信緩沖區)
nothrow new
默認情況下,當new
無法分配內存時會拋出std::bad_alloc
異常。但我們也可以使用nothrow
形式,失敗時返回nullptr
而不是拋出異常:
#include <iostream>
#include <new>int main() {// 嘗試分配巨大的內存(可能失敗)int* hugeArray = new(std::nothrow) int[1000000000000];if (hugeArray == nullptr) {std::cout << "Memory allocation failed." << std::endl;} else {std::cout << "Memory allocation succeeded." << std::endl;delete[] hugeArray;}return 0;
}
分配數組
分配數組時,new
會記錄數組的大小,以便delete[]
能正確釋放全部內存:
int* array = new int[10]; // 分配10個整數的數組// 使用數組...delete[] array; // 釋放整個數組
注意:當分配數組時必須使用delete[]
而非delete
來釋放內存,否則可能導致未定義行為。
二維數組的分配
有幾種方式可以分配二維數組:
// 方法1:使用連續內存(推薦)
int rows = 3, cols = 4;
int* matrix = new int[rows * cols];
matrix[row * cols + col] = value; // 訪問元素// 方法2:數組的數組(指針數組)
int** matrix = new int*[rows];
for (int i = 0; i < rows; i++) {matrix[i] = new int[cols];
}// 使用...// 釋放方法2的內存
for (int i = 0; i < rows; i++) {delete[] matrix[i];
}
delete[] matrix;
使用std::vector替代動態數組
通常,使用std::vector
比原始動態數組更安全:
#include <vector>// 一維vector
std::vector<int> vec(10); // 大小為10的向量// 二維vector
std::vector<std::vector<int>> matrix(rows, std::vector<int>(cols));
matrix[row][col] = value; // 訪問元素
std::vector
會自動管理內存分配和釋放,減少了內存泄漏和錯誤的風險。
delete操作符詳解
釋放過程詳解
當我們使用delete
操作符時,它執行以下操作:
- 如果指針非空,調用對象的析構函數(對于非POD類型)
- 調用底層的內存釋放函數(通常是
operator delete
)來釋放內存
delete與析構函數
delete
的關鍵功能之一是調用對象的析構函數。對于類對象,析構函數負責清理資源(如關閉文件、釋放其他動態分配的內存等)。
class ResourceManager {
private:int* data;public:ResourceManager() {data = new int[1000];std::cout << "Resource acquired" << std::endl;}~ResourceManager() {delete[] data;std::cout << "Resource released" << std::endl;}
};int main() {ResourceManager* rm = new ResourceManager();delete rm; // 調用析構函數,確保data被正確釋放return 0;
}
如果不調用delete
,析構函數不會被執行,導致data
指向的內存泄漏。
delete[]與數組
delete[]
操作符用于釋放通過new[]
分配的數組。它確保數組中的每個對象都正確調用其析構函數。
class MyClass {
public:MyClass() {std::cout << "Constructor called" << std::endl;}~MyClass() {std::cout << "Destructor called" << std::endl;}
};int main() {MyClass* array = new MyClass[3];// 輸出: Constructor called (3次)delete[] array; // 對數組中的每個對象調用析構函數// 輸出: Destructor called (3次)return 0;
}
使用錯誤的刪除形式可能導致嚴重問題:
- 使用
delete
釋放通過new[]
分配的內存:可能只調用第一個對象的析構函數,導致其他對象的資源泄漏 - 使用
delete[]
釋放通過new
分配的單個對象:未定義行為,可能導致崩潰
new和delete的內部機制
operator new與operator delete函數
new
和delete
操作符實際上調用了底層的函數operator new
和operator delete
來執行內存分配和釋放:
void* operator new(std::size_t size);
void operator delete(void* ptr) noexcept;void* operator new[](std::size_t size);
void operator delete[](void* ptr) noexcept;
這些函數可以被重載,以實現自定義內存管理策略:
#include <iostream>
#include <cstdlib>// 重載全局operator new
void* operator new(std::size_t size) {std::cout << "Custom global operator new: " << size << " bytes" << std::endl;void* ptr = std::malloc(size);if (!ptr) throw std::bad_alloc();return ptr;
}// 重載全局operator delete
void operator delete(void* ptr) noexcept {std::cout << "Custom global operator delete" << std::endl;std::free(ptr);
}class MyClass {
public:MyClass() {std::cout << "MyClass constructor" << std::endl;}~MyClass() {std::cout << "MyClass destructor" << std::endl;}// 類特定的operator new重載void* operator new(std::size_t size) {std::cout << "MyClass::operator new: " << size << " bytes" << std::endl;void* ptr = std::malloc(size);if (!ptr) throw std::bad_alloc();return ptr;}// 類特定的operator delete重載void operator delete(void* ptr) noexcept {std::cout << "MyClass::operator delete" << std::endl;std::free(ptr);}
};int main() {// 使用全局operator newint* pi = new int;delete pi;std::cout << "-------------------" << std::endl;// 使用類特定的operator newMyClass* obj = new MyClass;delete obj;return 0;
}
輸出:
Custom global operator new: 4 bytes
Custom global operator delete
-------------------
MyClass::operator new: 1 bytes
MyClass constructor
MyClass destructor
MyClass::operator delete
內存對齊與內存布局
new
操作符確保分配的內存滿足對象的對齊要求。不同類型有不同的對齊要求:
std::cout << "alignof(char): " << alignof(char) << std::endl;
std::cout << "alignof(int): " << alignof(int) << std::endl;
std::cout << "alignof(double): " << alignof(double) << std::endl;
一個復雜對象的內存布局可能受到填充(padding)和對齊(alignment)的影響:
struct Padded {char c; // 1字節// 可能有填充int i; // 4字節// 可能有填充double d; // 8字節
};std::cout << "sizeof(Padded): " << sizeof(Padded) << std::endl;
// 可能大于13(1+4+8)字節,由于內存對齊
常見的new/delete問題與陷阱
內存泄漏
內存泄漏發生在動態分配的內存未被釋放時:
void memoryLeak() {int* ptr = new int[100];// 無匹配的delete[],內存泄漏
}
懸掛指針(野指針)
懸掛指針指向已釋放的內存:
int* createAndDestroy() {int* ptr = new int(42);delete ptr; // 內存已釋放return ptr; // 返回懸掛指針
}int main() {int* dangling = createAndDestroy();*dangling = 10; // 未定義行為:寫入已釋放的內存return 0;
}
重復釋放
重復釋放同一內存會導致未定義行為:
int* ptr = new int;
delete ptr; // 第一次釋放,正確
delete ptr; // 第二次釋放,未定義行為
使用錯誤的釋放形式
不匹配的分配和釋放形式會導致未定義行為:
int* single = new int;
delete[] single; // 錯誤:應使用deleteint* array = new int[10];
delete array; // 錯誤:應使用delete[]
自定義析構函數前提下的釋放問題
如果基類沒有虛析構函數,通過基類指針刪除派生類對象會導致未定義行為:
class Base {
public:~Base() { // 非虛析構函數std::cout << "Base destructor" << std::endl;}
};class Derived : public Base {
public:~Derived() {std::cout << "Derived destructor" << std::endl;}
};int main() {Base* ptr = new Derived;delete ptr; // 只調用Base析構函數,不調用Derived析構函數return 0;
}
std::nothrow的限制
std::nothrow
只影響new
的內存分配部分,構造函數仍然可能拋出異常:
class ThrowingCtor {
public:ThrowingCtor() {throw std::runtime_error("Constructor failed");}
};int main() {try {// nothrow只對內存分配部分起作用,構造函數異常仍會傳播ThrowingCtor* obj = new(std::nothrow) ThrowingCtor;} catch (const std::exception& e) {std::cout << "Caught exception: " << e.what() << std::endl;}return 0;
}
替代方案與現代C++實踐
智能指針
現代C++提供了智能指針,自動管理動態內存的生命周期:
#include <memory>// 獨占所有權
std::unique_ptr<int> p1(new int(42));
// 或更好的方式
std::unique_ptr<int> p2 = std::make_unique<int>(42); // C++14// 共享所有權
std::shared_ptr<int> p3 = std::make_shared<int>(42);
std::shared_ptr<int> p4 = p3; // 現在p3和p4共享同一個對象
智能指針會在超出作用域時自動釋放其管理的內存,無需顯式調用delete
。
容器
標準庫容器如std::vector
、std::list
等自動管理內存:
std::vector<int> vec;
for (int i = 0; i < 1000; ++i) {vec.push_back(i); // 自動擴容,無需手動內存管理
}
RAII(資源獲取即初始化)原則
使用RAII原則設計類,在構造函數中獲取資源,在析構函數中釋放資源:
class ResourceWrapper {
private:Resource* resource;public:ResourceWrapper(const std::string& resourceName) {resource = acquireResource(resourceName);}~ResourceWrapper() {releaseResource(resource);}// 禁止復制ResourceWrapper(const ResourceWrapper&) = delete;ResourceWrapper& operator=(const ResourceWrapper&) = delete;// 允許移動ResourceWrapper(ResourceWrapper&& other) noexcept : resource(other.resource) {other.resource = nullptr;}ResourceWrapper& operator=(ResourceWrapper&& other) noexcept {if (this != &other) {releaseResource(resource);resource = other.resource;other.resource = nullptr;}return *this;}// 使用資源的方法...
};
內存池和分配器
對于性能關鍵的應用,可以使用自定義內存池來優化頻繁的小內存分配:
#include <cstddef>
#include <new>class SimpleMemoryPool {
private:struct Block {Block* next;};Block* freeList;static const size_t BLOCK_SIZE = 1024;public:SimpleMemoryPool() : freeList(nullptr) {}~SimpleMemoryPool() {Block* block = freeList;while (block) {Block* next = block->next;std::free(block);block = next;}}void* allocate(size_t size) {if (size > BLOCK_SIZE - sizeof(Block*)) {return std::malloc(size);}if (!freeList) {// 分配一批新塊char* memory = reinterpret_cast<char*>(std::malloc(BLOCK_SIZE * 10));if (!memory) return nullptr;// 將新內存塊鏈接到自由列表for (int i = 0; i < 10; ++i) {Block* block = reinterpret_cast<Block*>(memory + i * BLOCK_SIZE);block->next = freeList;freeList = block;}}// 使用自由列表的第一個塊Block* block = freeList;freeList = block->next;return block;}void deallocate(void* ptr, size_t size) {if (size > BLOCK_SIZE - sizeof(Block*)) {std::free(ptr);return;}// 將塊添加回自由列表Block* block = reinterpret_cast<Block*>(ptr);block->next = freeList;freeList = block;}
};// 為特定類型實現自定義new和delete
class MyObject {
private:static SimpleMemoryPool pool;public:void* operator new(size_t size) {return pool.allocate(size);}void operator delete(void* ptr, size_t size) {pool.deallocate(ptr, size);}// 類的其他成員...
};SimpleMemoryPool MyObject::pool;
性能考量
new/delete的開銷
動態內存分配涉及多種開銷:
- 調用操作系統分配內存:通常需要系統調用,這是昂貴的操作
- 查找合適的內存塊:內存管理器需要查找足夠大的空閑塊
- 記錄分配信息:存儲元數據(如分配大小)
- 對齊處理:確保內存對齊
- 構造和析構:調用構造函數和析構函數
對于頻繁的小內存分配和釋放,這些開銷可能會顯著影響性能。
性能優化策略
可以采用以下策略優化動態內存管理:
- 減少分配次數:預分配、重用對象
- 批量分配:一次分配多個對象
- 使用內存池:為特定大小的對象預分配內存
- 避免碎片化:使用適當的分配策略
- 考慮替代方案:使用棧內存(當對象較小且生命周期有限時)
棧與堆的性能對比
棧分配通常比堆分配快得多:
#include <iostream>
#include <chrono>
#include <vector>const int ITERATIONS = 1000000;void stackAllocation() {for (int i = 0; i < ITERATIONS; ++i) {int array[10]; // 棧分配array[0] = i;}
}void heapAllocation() {for (int i = 0; i < ITERATIONS; ++i) {int* array = new int[10]; // 堆分配array[0] = i;delete[] array;}
}void vectorAllocation() {for (int i = 0; i < ITERATIONS; ++i) {std::vector<int> vec(10); // 使用std::vectorvec[0] = i;}
}template<typename Func>
double measureTime(Func func) {auto start = std::chrono::high_resolution_clock::now();func();auto end = std::chrono::high_resolution_clock::now();std::chrono::duration<double, std::milli> duration = end - start;return duration.count();
}int main() {std::cout << "Stack allocation: " << measureTime(stackAllocation) << " ms" << std::endl;std::cout << "Heap allocation: " << measureTime(heapAllocation) << " ms" << std::endl;std::cout << "Vector allocation: " << measureTime(vectorAllocation) << " ms" << std::endl;return 0;
}
運行此代碼會明顯看出棧分配比堆分配快得多,而std::vector
通常介于兩者之間。
實際應用案例
實現簡單的字符串類
下面是使用動態內存管理實現簡單字符串類的示例:
#include <iostream>
#include <cstring>
#include <algorithm>class SimpleString {
private:char* data;size_t length;// 確保有足夠的空間void ensureCapacity(size_t newLength) {if (newLength > length) {// 分配新內存(添加一些額外空間以減少重新分配次數)size_t newCapacity = std::max(newLength, length * 2);char* newData = new char[newCapacity + 1]; // +1 用于空終止符// 復制現有數據if (data) {std::strcpy(newData, data);delete[] data;}data = newData;length = newCapacity;}}public:// 默認構造函數SimpleString() : data(nullptr), length(0) {data = new char[1];data[0] = '\0';}// 從C風格字符串構造SimpleString(const char* str) : data(nullptr), length(0) {if (!str) {data = new char[1];data[0] = '\0';} else {length = std::strlen(str);data = new char[length + 1];std::strcpy(data, str);}}// 復制構造函數SimpleString(const SimpleString& other) : data(nullptr), length(0) {length = other.length;data = new char[length + 1];std::strcpy(data, other.data);}// 移動構造函數SimpleString(SimpleString&& other) noexcept : data(other.data), length(other.length) {other.data = nullptr;other.length = 0;}// 析構函數~SimpleString() {delete[] data;}// 復制賦值SimpleString& operator=(const SimpleString& other) {if (this != &other) {delete[] data;length = other.length;data = new char[length + 1];std::strcpy(data, other.data);}return *this;}// 移動賦值SimpleString& operator=(SimpleString&& other) noexcept {if (this != &other) {delete[] data;data = other.data;length = other.length;other.data = nullptr;other.length = 0;}return *this;}// 連接操作符SimpleString operator+(const SimpleString& other) const {SimpleString result;result.ensureCapacity(length + other.length);std::strcpy(result.data, data);std::strcat(result.data, other.data);return result;}// 獲取C風格字符串const char* c_str() const {return data;}// 獲取長度size_t size() const {return std::strlen(data);}// 訪問元素char& operator[](size_t index) {return data[index];}const char& operator[](size_t index) const {return data[index];}// 設置新值void assign(const char* str) {if (!str) return;size_t newLength = std::strlen(str);ensureCapacity(newLength);std::strcpy(data, str);}// 追加字符串void append(const char* str) {if (!str) return;size_t currentLength = std::strlen(data);size_t appendLength = std::strlen(str);ensureCapacity(currentLength + appendLength);std::strcat(data, str);}
};int main() {// 測試構造函數SimpleString s1("Hello");SimpleString s2(" World");// 測試連接SimpleString s3 = s1 + s2;std::cout << s3.c_str() << std::endl; // 輸出:Hello World// 測試復制和賦值SimpleString s4 = s1;std::cout << s4.c_str() << std::endl; // 輸出:Hellos4 = s2;std::cout << s4.c_str() << std::endl; // 輸出:World// 測試修改s4[0] = 'w'; // 改為小寫std::cout << s4.c_str() << std::endl; // 輸出:world// 測試appends4.append("!");std::cout << s4.c_str() << std::endl; // 輸出:world!return 0;
}
這個例子展示了如何使用new
和delete
在堆上管理字符串數據,包括深拷貝、移動語義和內存重分配邏輯。
自定義對象池
對于需要頻繁創建和銷毀的小對象,可以實現對象池來提高性能:
#include <iostream>
#include <vector>
#include <memory>template<typename T, size_t BlockSize = 100>
class ObjectPool {
private:// 表示內存塊的結構struct Block {union {T value;Block* next;};// 禁用構造和析構函數以允許在union中使用TBlock() : next(nullptr) {}~Block() {}};Block* freeList;std::vector<std::unique_ptr<Block[]>> blocks;public:ObjectPool() : freeList(nullptr) {}~ObjectPool() {// blocks的vector會自動清理分配的內存}// 分配對象template<typename... Args>T* allocate(Args&&... args) {if (freeList == nullptr) {// 分配新的內存塊auto newBlock = std::make_unique<Block[]>(BlockSize);// 初始化自由列表for (size_t i = 0; i < BlockSize - 1; ++i) {newBlock[i].next = &newBlock[i + 1];}newBlock[BlockSize - 1].next = nullptr;freeList = &newBlock[0];blocks.push_back(std::move(newBlock));}// 使用自由列表的第一個塊Block* block = freeList;freeList = block->next;// 使用placement new構造對象return new (&block->value) T(std::forward<Args>(args)...);}// 釋放對象void deallocate(T* object) {if (!object) return;// 調用析構函數object->~T();// 將塊添加回自由列表Block* block = reinterpret_cast<Block*>(object);block->next = freeList;freeList = block;}
};// 測試用的類
class TestObject {
private:int id;public:TestObject(int i) : id(i) {std::cout << "TestObject " << id << " constructed." << std::endl;}~TestObject() {std::cout << "TestObject " << id << " destructed." << std::endl;}int getId() const { return id; }
};int main() {// 創建對象池ObjectPool<TestObject, 5> pool;// 分配一些對象std::vector<TestObject*> objects;for (int i = 0; i < 10; ++i) {objects.push_back(pool.allocate(i));}// 使用對象for (auto obj : objects) {std::cout << "Object ID: " << obj->getId() << std::endl;}// 釋放一些對象for (int i = 0; i < 5; ++i) {pool.deallocate(objects[i]);objects[i] = nullptr;}// 分配更多對象(會重用已釋放的內存)for (int i = 0; i < 5; ++i) {objects[i] = pool.allocate(i + 100);}// 清理所有對象for (auto obj : objects) {if (obj) {pool.deallocate(obj);}}return 0;
}
對象池可以顯著減少內存分配和釋放的開銷,特別是對于頻繁創建和銷毀的小對象。
最佳實踐
總結一些使用new
和delete
的最佳實踐:
-
避免裸指針:盡可能使用智能指針(
std::unique_ptr
、std::shared_ptr
)// 不推薦 Resource* res = new Resource(); // 使用后... delete res;// 推薦 std::unique_ptr<Resource> res = std::make_unique<Resource>(); // 自動管理生命周期
-
優先使用標準庫容器:它們已經在內部處理好內存管理
// 不推薦 int* array = new int[size]; // 使用后... delete[] array;// 推薦 std::vector<int> array(size);
-
確保匹配
new
與delete
、new[]
與delete[]
:// 正確 int* single = new int; delete single;int* array = new int[10]; delete[] array;
-
遵循RAII原則:在構造函數中獲取資源,在析構函數中釋放資源
-
檢查內存分配失敗:使用異常處理或
std::nothrow
// 使用異常 try {int* array = new int[1000000000]; } catch (const std::bad_alloc& e) {std::cerr << "Memory allocation failed: " << e.what() << std::endl; }// 使用nothrow int* array = new(std::nothrow) int[1000000000]; if (!array) {std::cerr << "Memory allocation failed" << std::endl; }
-
基類使用虛析構函數:確保通過基類指針正確刪除派生類對象
class Base { public:virtual ~Base() = default; };
-
考慮移動語義:當涉及大量數據轉移時
class MovableResource { public:// 移動構造函數MovableResource(MovableResource&& other) noexcept: data(other.data) {other.data = nullptr;}// 移動賦值運算符MovableResource& operator=(MovableResource&& other) noexcept {if (this != &other) {delete[] data;data = other.data;other.data = nullptr;}return *this;}private:int* data; };
-
使用工具檢測內存問題:Valgrind、AddressSanitizer等
總結
new
和delete
操作符是C++中動態內存管理的基本工具,它們允許程序員在運行時分配和釋放內存。理解它們的工作原理、正確用法和潛在問題對于編寫健壯的C++程序至關重要。
然而,在現代C++中,我們通常應該傾向于使用更高級的抽象(如智能指針、容器和RAII類)來自動管理內存,這有助于減少常見錯誤如內存泄漏、懸空指針和重復釋放。
關鍵記憶點:
new
分配內存并調用構造函數,delete
調用析構函數并釋放內存- 數組使用
new[]
分配,delete[]
釋放 - 裸指針管理的內存需要顯式釋放,否則會導致內存泄漏
- 現代C++提供更安全的替代方案:智能指針、容器和RAII
在下一篇文章中,我們將討論內存泄漏的檢測與避免,這是C++內存管理中的一個重要主題。
這是我C++學習之旅系列的第十七篇技術文章。查看完整系列目錄了解更多內容。