文章目錄
- 資源管理
-
- 資源訪問
-
- 指向資源句柄或描述符的變量,在資源釋放后立即賦予新值
- lambda函數
-
- 當lambda會逃逸出函數外面時,禁止按引用捕獲局部變量
- 避免lambda表達式使用默認捕獲模式
- 資源分配與回收
-
- 避免出現delete this操作
- 使用恰當的方式處理new操作符的內存分配錯誤
- 合理選擇值類型、智能指針、裸指針或引用
- 使用RAII技術管理資源的生命周期
- 使用`std::make_unique`而不是`new`創建`std::unique_ptr`
- 使用`std::make_shared`而不是`new`創建`std::shared_ptr`
- 標準庫
-
- 字符串
-
- 1.string_view
- 2.不要保存std::string類型的c_str和data成員函數返回的指針
- 3.確保用于字符串操作的緩沖區有足夠的空間容納字符數據和結束符,并且字符串以null結束符結束
- 4.避免使用atoi、atol、atoll、atof函數
- 容器與迭代器
-
- 1.確保容器索引或迭代器在有效范圍
- 2.使用有效的迭代器和指向容器元素的指針與引用
- 3.在不需要修改迭代器指向的對象時,應使用const_iterator
- 4.確保目的區間已經足夠大或者在算法執行時可以增加大小
- 5.如果需要刪除容器中的元素,必須在std::remove、std::remove_if類算法之后調用容器的erase方法
- 禁用rand函數產生用于安全用途的偽隨機數
- 并發與并行
-
- std::thread和std::mutex與std::condition_variable不能拷貝,只能移動
- 編寫多線程程序必須避免數據競爭
- 盡量縮短在臨界區內停留的時間
- 多線程程序中要特別留意對象的生命周期
- 使用條件變量的wait方法時,必須外加條件判斷,并在循環中等待
- 不要直接調用mutex的方法
- 使用C++語言和標準庫的機制實現線程安全的單例初始化
- 不要在信號處理函數中訪問共享對象
資源管理
資源訪問
- 外部數據作為數組索引或者內存操作長度時,需要校驗其合法性
- 內存申請前,必須對申請內存大小進行合法性校驗,防止申請0長度內存,或者過多地、非法地申請內存。
- 在傳遞數組參數時,不應單獨傳遞指針。當函數參數類型為數組(不是數組的引用)或者指針時,若調用者傳入數組,則在參數傳遞時數組會退化為指針,其數組長度信息會丟失,容易引發越界讀寫等問題。
- 禁止將局部變量的地址傳遞到其作用域外
指向資源句柄或描述符的變量,在資源釋放后立即賦予新值
“指向資源句柄或描述符的變量”包括:指針、文件描述符、socket描述符以及其他指向資源的變量。
以指針為例,當指針成功申請了一段內存之后,在這段內存釋放以后,如果其指針未立即設置為nullptr,也未分配一個新的對象,那這個指針就是一個懸空指針。
如果再對懸空指針操作,可能會發生重復釋放或訪問已釋放內存的問題,造成安全漏洞。消減該漏洞的有效方法是將釋放后的指針立即設置為一個確定的新值,例如:設置為nullptr。
對于全局性的資源句柄或描述符,在資源釋放后,應該馬上設置新值,以避免使用其已釋放的無效值;對于只在單個函數內使用的資源句柄或描述符,應確保資源釋放后其無效值不被再次使用。
【反例】
int* a = new int{1};
delete a;
...
delete a; // 錯誤,會導致double free錯誤
【正例】
int* a = new int{1};
delete a;
...
a = nullptr; // 正確
delete a; // 避免了內存重復釋放
注:默認的內存釋放函數針對空指針不執行任何動作。
【正例】
如下代碼中,在資源釋放后,對應的變量應該立即賦予新值。
Socket s = INVALID_SOCKET;
int fd = -1;
...
CloseSocket(s);
s = INVALID_SOCKET;
...
close(fd);
fd = -1;
...
lambda函數
當lambda會逃逸出函數外面時,禁止按引用捕獲局部變量
如果一個 lambda 不止在局部范圍內使用,禁止按引用捕獲局部變量,比如它被傳遞到了函數的外部,或者被傳遞給了其他線程的時候。lambda按引用捕獲就是把局部對象的引用存儲起來。如果 lambda 的生命周期會超過局部變量生命周期,則可能導致內存不安全。
【反例】
void Foo()
{int local = 0;// 按引用捕獲 local,當函數返回后,local 不再存在,因此 Process() 的行為未定義threadPool.QueueWork([&] { Process(local); });
}
【正例】
void Foo()
{int local = 0;// 按值捕獲 local, 在Process() 調用過程中,local 總是有效的threadPool.QueueWork([local] { Process(local); });
}
避免lambda表達式使用默認捕獲模式
lambda表達式提供了兩種默認捕獲模式:按引用(&)和按值(=)。
默認按引用捕獲會隱式的捕獲所有局部變量的引用,容易導致訪問懸空引用。相比之下,顯式的寫出需要捕獲的變量可以更容易的檢查對象生命周期,減小犯錯可能。
默認按值捕獲會隱式的捕獲this
指針,實際等同于按引用捕獲了成員變量。如果存在靜態變量,還會讓閱讀者誤以為lambda復制了一份靜態變量。從C++20開始,通過[=]
默認捕獲this將變為deprecated的。所以,當lambda表達式中使用了類成員變量或靜態變量時,不宜使用按值默認捕獲模式。
因此,通常應當明確寫出lambda需要捕獲的變量,而不是使用默認捕獲模式。
【反例】
auto Fun()
{int addend = 0;static int baseValue = 0;return [=]() { // 實際上只復制了addend++baseValue; // 修改會影響靜態變量的值return baseValue + addend;};
}
【正例】
auto Fun()
{int addend = 0;static int baseValue = 0;return [addend, value = baseValue]() mutable { // 使用C++14的捕獲初始化一個變量++value; // 不會影響Fun函數中的靜態變量return value + addend;};
}
在 C++ 11 和更高版本中,Lambda 表達式(通常稱為 Lambda)是一種在被調用的位置或作為參數傳遞給函數的位置定義匿名函數對象(閉包)的簡便方法。lambda表達式與任何函數類似,具有返回類型、參數列表和函數體。與函數不同的是,lambda能定義在函數內部。lambda表達式具有如下形式
[ capture list ] (parameter list) -> return type { function body }
- capture list,捕獲列表,局部變量對于lambda函數體是不可見的,需要通過捕獲的方式獲得。捕獲只針對于lambda函數的作用域內可見的非靜態局部變量。 lambda表達式可以直接使用靜態變量,而不需要被捕獲。lambda函數可以無條件訪問全局變量、作用域內的靜態變量。捕獲可以分為按值捕獲和按引用捕獲。
- parameter list,參數列表。從C++14開始,支持默認參數,并且參數列表中如果使用auto的話,該lambda稱為泛化lambda(generic lambda);
- return type,返回類型,這里使用了返回值類型尾序語法(trailing return type synax)。可以省略,這種情況下根據lambda函數體中的return語句推斷出返回類型,就像普通函數使用decltype(auto)推導返回值類型一樣;如果函數體中沒有return,則返回類型為void。
- function body,與任何普通函數一樣,表示函數體
資源分配與回收
- new和delete配對使用,new[]和delete[]配對使用
- 自定義new/delete操作符需要配對定義,且行為與被替換的操作符一致
避免出現delete this操作
delete this操作是自己銷毀自己,在此之后再訪問到該對象的成員時,可能造成未定義行為。
【例外】
在資源管理器、生命周期管理器等場景中可以使用delete this操作,此時應滿足在delete this 操作后不再提供任何能夠訪問到this的入口,并且被delete的對象是由普通的new分配的。
使用恰當的方式處理new操作符的內存分配錯誤
默認的new
操作符在內存分配失敗時,會拋出std::bad_alloc
異常,而使用了std::nothrow
參數的new
操作符在內存分配失敗時,會返回nullptr
。
因此,需要針對不同場景來處理new
操作符的內存分配錯誤:
- 對于不會返回
nullptr
的new
操作,不要對返回值做空指針檢查。如果new操作失敗拋出異常,則不會執行后面的代碼,因此檢查空指針是多余的操作。 - 對于可能會返回
nullptr
的new
操作,與對待malloc
等內存分配函數一樣,需要對返回值做空指針檢查,如:使用了std::nothrow
的new
操作
合理選擇值類型、智能指針、裸指針或引用
通用原則:
- 使用值類型 T 或 unique_ptr 來表達獨占所有權
- 如果需要轉移所有權,應使用智能指針,而不是使用T*或T&作為參數
- 原生指針 T* 和引用 T& 不表達所有權概念
- 不涉及所有權轉移的場景,應優先使用T*或T&作為參數,而不是智能指針。例如:不應使用 const unique_ptr& 類型作為參數
- 當函數的返回類型為T*時,應當表示一個位置,而非傳遞所有權。返回的指針所指向的對象必須在調用者的作用域內有效。如果返回值不可能為空,則優先返回引用
智能指針:
- 使用
shared_ptr<T>
來表達共享所有權。如果資源只有一個所有者,應使用unique_ptr<T>
而不是shared_ptr<T>
- 使用
unique_ptr<T>
作為函數的參數和返回值,代表所有權轉移 - 使用
shared_ptr
或unique_ptr
代替auto_ptr
。auto_ptr
在C++11中已標識為deprecated,在C++17中已去除 - 使用智能指針時也需要注意對象的生命周期,例如:使用
get()
返回的指針時,1)如果智能指針釋放了其管理的對象,則該指針變成了無效指針;2)不能使用該指針初始化另一個智能指針;…
函數參數:
- 使用
T&&
或者unique_ptr<T>
類型作為參數,代表這個資源的所有權是從外部移動進來的 - 使用
T
類型做為參數,代表這個函數內部擁有資源的所有權,資源可能是拷貝或者移動進來的 - 使用
unique_ptr<T>&
類型作為參數,代表這個函數可能重置這個unique_ptr
的指向 - 使用
shared_ptr<T>
類型作為參數,代表這個函數也是這個資源的其中一個擁有者 - 使用
shared_ptr<T>&
類型作為參數,代表這個函數可能重置這個shared_ptr
的指向 - 使用
const T&
類型作為參數,代表這個函數對資源是只讀的,且不管理資源的釋放 - 使用
T&
類型作為參數,代表這個函數對資源可讀寫,且不管理資源的釋放 - 使用
const T*
類型作為參數,代表這個參數可能為空,這個函數對資源是只讀的,且不管理資源的釋放 - 使用
T*
類型作為參數,代表這個參數可能為空,這個函數對資源可讀寫
數組和字符串:
- 使用
T*
或T&
作為參數,代表指向的是一個T
元素,而不是一組元素。即便是指針指向一組元素中的其中一個,也不應使用指針算術運算指向其他元素。如果要表達指向的是一組元素,應明確表達這個區間的開始和結束 - 如果是表達字符串類型,應優先使用
std::string
、std::string_view
(C++17)或類似的自定義類型 - 如果是表達固定大小的數組類型,應優先使用
std::array
、std::span
(C++20)或類似的自定義類型
使用RAII技術管理資源的生命周期
RAII代表 resource acquisition is initialization。它可以用于避免手工資源管理的復雜性。
資源的獲取和釋放是成對操作(例如new/delete,fopen/fclose,lock/unlock 等),恰好能對應C++語言對稱的構造函數和析構函數。利用C++對象的生命周期來管理資源的生命周期,是一種常見的策略。
使用std::make_unique
而不是new
創建std::unique_ptr
本條款適用于C++14及之后的版本。C++14開始增加了std::make_unique
,提供與std::make_shared
類似的方式構造unique_ptr
。
相對于先 new
出裸指針再構造 unique_ptr
,直接使用 make_unique
的優點有:
make_unique
可以更明確的避免裸指針和智能指針混用。- 使用
make_unique
更簡潔。
【例外】
因為技術原因,希望使用 unique_ptr
、又無法使用 make_unique
的,可以不使用 make_unique
。
目前的已知場景有:
- 使用
make_unique
時,不支持自定義deleter
。在需要自定義deleter
的場景,建議在自己的命名空間實現定制版本的make_unique
。 - 如果分配內存需要自定義的內存分配方式(如使用 placement new、nothrow版本的new等)的話,也沒法直接使用
make_unique
。一種實際的場景是如果想對 C 的變長結構體(尾項為靈活數組成員)使用unique_ptr
,需要先分配比結構體大小更大的內存空間,然后使用 placement new 來進行初始化操作。對于這種情況,建議把unique_ptr
的創建封裝到一個單獨的函數里。 - C++20 之前不能用
std::make_unique
對沒有構造函數的 C 結構體進行聚合初始化。可以考慮使用下面的自定義版本。
// C++17 的 std::make_unique 不能調用聚合初始化(C++20 里已解決);下面的工具函數解決了這個問題
template <typename T, typename... Args>
std::unique_ptr<T> MakeUnique(Args &&... args)
{if constexpr (std::is_constructible_v<T, Args...>) {return std::unique_ptr<T>{new T(std::forward<Args>(args)...)};} else {return std::unique_ptr<T>{new T{std::forward<Args>(args)...}};}
}
使用std::make_shared
而不是new
創建std::shared_ptr
std::shared_ptr
管理兩個實體:
- 控制塊(存儲引用計數,deleter等)
- 管理對象
std::make_shared
創建std::shared_ptr
,會一次性在堆上分配足夠容納控制塊和管理對象的內存。 而使用std::shared_ptr<SomeClass>(new SomeClass)
創建std::shared_ptr
,除了new SomeClass
會觸發一次堆分配外,std::shard_ptr
的構造函數還會觸發第二次堆分配,產生額外的開銷。
【例外】
類似std::make_unique
,因為技術原因,希望使用 shared_ptr
、又無法使用 make_shared
的,可以不使用 make_shared
。
標準庫
字符串
1.string_view
C++17開始增加了std::string_view
類型,該類型可以減少字符串復制操作,提升程序性能。在C++17及之后的版本中,建議使用std::string_view
表示字符串常量,在C++17之前可以使用C風格的字符串常量。
當函數參數為只讀字符串時,在C++17及之后的版本中使用std::string_view
類型。
void Fun(std::string_view str) {...
}