自成立以來,C++已成為世界上最常用的程式設計語言之一。 編寫完善的 C++ 程式不但執行快速,而且有效率。 這種語言比其他語言更靈活:它既能在最高層級的抽象層面運作,也能在底層的矽晶片層級運作。
C++提供高度優化的標準連結庫。 它可讓您存取低階硬體功能,以將速度最大化,並將記憶體需求降到最低。 C++ 幾乎可以開發任何類型的程式:遊戲、裝置驅動程式、High-Performance 運算、雲端、桌面、嵌入式及行動應用程式,還有更多。 甚至其他程式語言的函式庫和編譯器也是用 C++ 撰寫的。
C++ 的其中一個原始需求是與 C 語言的回溯相容性。 因此,C++ 允許 C 風格的程式設計,包含原始指標、陣列、null 終止字元字串及其他功能。 它們能帶來優異的效能,但也可能引發錯誤和複雜度。
C++ 的演進強調了大幅減少使用 C 風格慣用語的功能。 當您需要 C 程式設計工具時,舊的 C 程式設計設施仍然存在。 不過,在現代 C++ 程式碼中,你應該會越來越少需要它們。 新式C++程式代碼更簡單、更安全、更優雅,而且仍然盡可能快。
下列各節提供新式C++的主要功能概觀。 除非另有說明,否則此處所列的功能可在 C++11 和更新版本中使用。 在Microsoft C++編譯程式中,您可以設定 /std 編譯程式選項,以指定要用於項目的標準版本。
資源和智慧型指標
C 樣式程序設計中的其中一個主要 Bug 類別是 記憶體流失。 洩漏通常是因為未對使用 new 配置的記憶體呼叫 delete 所造成。 現代 C++ 強調資源取得即初始化(RAII)原則。
這個想法很簡單。 資源,例如堆積記憶體、檔案句柄和套接字,應該由物件 擁有 。 該物件會在其建構函式中建立或接收新配置的資源,並在解構函式中刪除它。 RAII 原則可確保當擁有該資源的物件離開作用域時,所有資源都能正確地歸還給作業系統。
為了支援 RAII 原則的輕鬆採用,C++ 標準函式庫提供了三種智慧指標類型:
智慧指標負責分配與刪除它所擁有的記憶體。 下列範例顯示一個具有陣列成員的類別,該成員是在呼叫 make_unique() 時於堆積中配置的。 該 unique_ptr 類別封裝了對 new 和 delete 的呼叫。 當物件 widget 超出範圍時, unique_ptr 會呼叫解構子並釋放分配給陣列的記憶體。
#include <memory>
class widget
{
private:
std::unique_ptr<int[]> data;
public:
widget(const int size) { data = std::make_unique<int[]>(size); }
void do_something() {}
};
void functionUsingWidget() {
widget w(1000000); // lifetime automatically tied to enclosing scope
// constructs w, including the w.data gadget member
// ...
w.do_something();
// ...
} // automatic destruction and deallocation for w and w.data
盡可能使用智慧型指標來管理堆積記憶體。 如果您必須明確使用 new 和 delete 運算符,請遵循RAII的原則。 如需詳細資訊,請參閱物件存留期和資源管理 (RAII)。
std::string 和 std::string_view
C 樣式字串是 Bug 的另一個主要來源。 藉由使用 std::string 和 std::wstring,您幾乎可以排除與 C 樣式字串相關聯的所有錯誤。 您也能享有可用於搜尋、附加、前置加入等操作的成員函式所帶來的好處。 兩者都針對速度進行高度優化。 當將字串傳遞給僅需唯讀存取權限的函式時,在 C++17 中,您可以使用 std::string_view 來進一步提升效能。
std::vector 和其他標準程式庫容器
標準連結庫容器全都遵循 RAII 的原則。 它們為元素的安全周遊提供反覆運算器。 它們在效能上高度優化,並經過嚴格測試正確性。 藉由使用這些容器,您可以消除自定義數據結構中可能引入的 Bug 或效率不足。 在 C++ 中使用 vector 作為循序容器,而不是原始數位。
vector<string> apples;
apples.push_back("Granny Smith");
使用 map (not unordered_map) 作為預設的關聯容器。 針對變質和多重案例使用 set、 multimap和 multiset 。
map<string, string> apple_color;
// ...
apple_color["Granny Smith"] = "Green";
當你需要效能優化時,可以考慮使用:
- 未排序的關聯容器,例如
unordered_map。 這些容器的每個元素額外開銷較低,且可在固定時間內查找,但也更難以正確且有效率地使用。 - 已排序
vector。 如需詳細資訊,請參閱演算法。
請勿使用 C 樣式陣列。 對於需要直接存取資料的舊版 API,請改用存取子方法,例如 f(vec.data(), vec.size()); 。 如需容器的詳細資訊,請參閱 C++標準連結庫容器。
標準函式庫演算法
在你認為必須為程式寫自訂演算法之前,先先查閱 C++ 標準函式庫 的演算法。 標準程式庫包含種類持續增加的各式演算法,可用於許多常見操作,例如搜尋、排序、篩選和隨機化。 數學函式庫十分龐大。 在 C++17 和更新版本中,會提供許多演算法的平行版本。
以下是一些重要的範例:
-
for_each:預設的遍歷演算法,以及以for為基礎的範圍迴圈。 -
transform:用於容器元件的非原位修改。 -
find_if:預設搜尋演算法。 -
sort、lower_bound和其他預設排序和搜尋演算法。
若要撰寫比較子,請使用嚴格的 <,並在可行情況下使用 具名 lambda。
auto comp = [](const widget& w1, const widget& w2)
{ return w1.weight() < w2.weight(); }
sort( v.begin(), v.end(), comp );
auto i = lower_bound( v.begin(), v.end(), widget{0}, comp );
auto,而非明確指定的型別名稱
C++11 引進 auto 了用於變數、函式和範本宣告的 關鍵詞。
auto 指示編譯程式推斷對象的類型,因此您不需要明確輸入它。
auto 當推斷型別是巢狀範本時,特別有用:
map<int,list<string>>::iterator i = m.begin(); // C-style
auto i = m.begin(); // modern C++
以範圍為基礎的 for 迴圈
以 C 風格對陣列和容器進行迭代容易出現索引錯誤,而且寫起來也很繁瑣。 若要消除這些錯誤,並讓您的程式碼更容易閱讀,請對標準程式庫容器和原始陣列使用以範圍為基礎的 for 迴圈。 如需詳細資訊,請參閱 以範圍為基礎的 for 語句。
#include <iostream>
#include <vector>
int main()
{
std::vector<int> v {1,2,3};
// C-style
for(int i = 0; i < v.size(); ++i)
{
std::cout << v[i];
}
// Modern C++:
for(auto& num : v)
{
std::cout << num;
}
}
constexpr 表達式而非巨集
C 和 C++ 中的巨集是編譯前由預處理器處理的標記。 巨集令牌的每個實例都會在編譯檔案之前,以其定義的值或表達式取代。 巨集通常用於 C 樣式程式設計,以定義編譯時間常數值。 不過,巨集容易出錯且難以偵錯。 在現代 C++ 中,對於編譯期常數,您應優先使用 constexpr 變數:
#define SIZE 10 // C-style
constexpr int size = 10; // modern C++
統一初始化
在新式C++中,您可以針對任何類型使用大括號初始化。 初始化陣列、向量或其他容器時,這種初始化形式特別方便。 在下列範例中,v2 會使用三個 S 執行個體進行初始化。
v3 會使用三個 S 實例初始化,而 實例本身會使用大括號初始化。 編譯程式會根據 宣告的 v3型別來推斷每個專案的型別。
#include <vector>
struct S
{
std::string name;
float num;
S(std::string s, float f) : name(s), num(f) {}
};
int main()
{
// C-style initialization
std::vector<S> v;
S s1("Norah", 2.7);
S s2("Frank", 3.5);
S s3("Jeri", 85.9);
v.push_back(s1);
v.push_back(s2);
v.push_back(s3);
// Modern C++:
std::vector<S> v2 {s1, s2, s3};
// or...
std::vector<S> v3{ {"Norah", 2.7}, {"Frank", 3.5}, {"Jeri", 85.9} };
}
如需詳細資訊,請參閱 大括弧初始化。
移動語意
新式C++提供 行動語意,讓您能夠消除不必要的記憶體複本。 在舊版的語言中,在某些情況下,無法避免複製。 移動作業會將資源的擁有權從某個物件傳輸到下一個物件,而不進行複本。 某些類別擁有資源,例如堆積記憶體、檔句柄等等。
當您實作資源擁有類別時,您可以為其定義 移動建構函式 和 移動指派運算符 。 在不需要複本的情況下,編譯程式會在多載解析期間選擇這些特殊成員。 如果有定義移動建構函式,標準程式庫的容器類型會呼叫物件的移動建構函式。 如需詳細資訊,請參閱移動建構函式和移動指派運算元(C++)。
Lambda 運算式
在 C 樣式程式設計中,函式可以使用函式指標傳遞至另一個函式。 函式指標不方便維護和瞭解。 他們所指的功能可能在原始碼的其他地方被定義,遠離它被調用的那個點。 此外,它們不具型別安全性。
現代 C++ 提供了函式物件,也就是覆寫了 operator() 運算子的類別,因此可以像函式一樣被呼叫。 建立函式物件最方便的方式是使用內嵌 Lambda 表達式。 以下範例展示了如何使用 lambda 表達式傳遞函式物件,該 find_if 函式在向量中的每個元素上呼叫該函式物件:
std::vector<int> v {1,2,3,4,5};
int x = 2;
int y = 4;
auto result = find_if(begin(v), end(v), [=](int i) { return i > x && i < y; });
Lambda 運算式 [=](int i) { return i > x && i < y; } 可以讀取為「接受類型 int 單一自變數的函式,並傳回布爾值,指出自變數是否大於 x 和小於 y。」請注意,變數 x 和 y 周圍的內容都可以在 Lambda 中使用。 該 [=] 指定這些變數會依值擷取。 換句話說,lambda 運算式有自己的這些值副本。
例外狀況
新式C++強調例外狀況,而不是錯誤碼,這是報告和處理錯誤狀況的最佳方式。 如需詳細資訊,請參閱 新式C++例外狀況和錯誤處理的最佳做法。
std::atomic
使用C++標準連結庫 std::atomic 結構和相關類型進行線程間通訊機制。
std::variant (C++17)
聯集通常用於 C 樣式程式設計,藉由讓不同類型的成員佔用相同的記憶體位置來節省記憶體。 聯合體不安全,且容易出現程式錯誤。 C++17 將 std::variant 類別引入為更健全且安全的工會替代方案。
std::visit 函式可用於以型別安全的方式存取 variant 類型的成員。