【學習筆記】C++代碼規范整理
一、匿名空間namespace
匿名命名空間(Anonymous Namespace)是一種特殊的命名空間聲明方式,其作用是將聲明的成員限定在當前編譯單元(源文件)內可見,類似于使用 static 關鍵字修飾全局變量 / 函數的效果。
特性 | 匿名命名空間 | static 修飾全局符號 |
---|---|---|
作用域 | 當前編譯單元(源文件) | 當前編譯單元 |
鏈接屬性 | 內部鏈接(Internal Linkage) | 內部鏈接 |
可修飾類型 | 變量、函數、類、結構體等所有實體 | 僅變量、函數 |
語法靈活性 | 可嵌套在其他命名空間中 | 不可嵌套 |
外部訪問規則 | 在匿名命名空間所屬的源文件內,可直接調用成員函數外部通過命名空間名+“::”訪問 | 通過中間接口封裝 |
外部訪問namespace內部接口:
// test.h(頭文件,聲明具名命名空間)
namespace MyNamespace {void sharedFunc(); // 聲明為具名命名空間成員
}// test.cpp(源文件,定義具名命名空間函數)
#include "test.h"
namespace MyNamespace {void sharedFunc() { // 具名命名空間,可跨文件訪問std::cout << "Shared function called.\n";}
}// other.cpp(其他源文件,包含頭文件并調用)
#include "test.h"
int main() {MyNamespace::sharedFunc(); // 合法,通過命名空間名訪問return 0;
}
訪問static關鍵字定義的接口函數:通過中間接口封裝
// file1.c
static void privateFunc() { /* ... */ } // 內部函數void callPrivateFunc() { // 公有接口privateFunc(); // 內部調用
}// file2.c
extern void callPrivateFunc(); // 聲明公有接口
int main() {callPrivateFunc(); // 通過公有接口間接調用return 0;
}
二、結構體對齊
在結構體中合理安排數據成員的布局可以有效減少內存占用。
核心原則:
? 1. 先放占用空間大的成員,再放小的成員:減少成員之間的填充字節。
? 2. 相同大小的成員分組排列:避免小成員插入大成員之間導致的零散填充。
內存對齊規則(以常見編譯器為例):
? ● 每個成員的起始地址必須是其自身大小的整數倍(如 int 占 4 字節,起始地址需是 4 的倍數)。
? ● 結構體的總大小必須是最大成員大小的整數倍。
struct BadLayout {char a; // 1字節,起始地址對齊0,占用1字節 地址:0double b; // 8字節,起始地址需是8的倍數 → 填充7字節,占用8字節 地址:8-15int c; // 4字節,起始地址需是4的倍數(當前地址是16),占用4字節 地址:16-19
};
// 總大小:1(a)+7(填充)+8(b)+4(c) = 20 → 按最大成員8字節對齊,最終大小24字節
struct GoodLayout {double b; // 8字節,起始地址對齊8,占用8字節 地址:0-7int c; // 4字節,起始地址對齊4(當前地址8是4的倍數),占用4字節 地址:8-11char a; // 1字節,起始地址對齊1(當前地址12),占用1字節 地址:12
};
// 總大小:8+4+1 = 13 → 按最大成員8字節對齊,最終大小16字節(節省8字節)
三、#pragma once
#pragma once 是一種預處理指令,用于確保頭文件在編譯過程中只被包含一次,從而防止因重復包含導致的編譯錯誤,如 “重復定義”(multiple definition)問題。
四、虛析構函數
一個析構函數不為virtual 的類,就是一個不愿被繼承的類。
當基類析構函數不是虛函數時,要是通過基類指針刪除派生類對象,系統只會調用基類的析構函數,而不會調用派生類的析構函數。這就可能使派生類特有的資源(像動態分配的內存、文件句柄、網絡連接等)無法被釋放,進而造成內存泄漏。
五、const
const 關鍵字用于聲明一個對象或變量是不可變的,即其值在初始化后不能被修改。
類的成員函數后面加const,表明這個函數不會對這個類對象的數據成員作任何改變。
六、盡量使用棧內存
程序運行中創建對象時主要在兩個地方,棧和堆。
在棧中創建對象(或數組)是編譯期確定的,因此開銷為零。
在堆中申請內存是運行期行為,申請、釋放都有開銷,并且存在內存碎片可能。
1.內部碎片的產生:
因為所有的內存分配必須起始于可被 4、8 或 16 整除(視處理器體系結構而定)的地址或者因為MMU的分頁機制的限制,決定內存分配算法僅能把預定大小的內存塊分配給客戶。假設當某個客戶請求一個43字節的內存塊時,因為沒有適合大小的內存,所以它可能會獲得 44字節、48字節等稍大一點的字節,因此由所需大小四舍五入而產生的多余空間就叫內部碎片。
2.外部碎片的產生:
頻繁的分配與回收物理頁面會導致大量的、連續且小的頁面塊夾雜在已分配的頁面中間,就會產生外部碎片。
假設有一塊一共有100個單位的連續空閑內存空間,范圍是099。如果你從中申請一塊內存,如10個單位,那么申請出來的內存塊就為09區間。這時候你繼續申請一塊內存,比如說5個單位大,第二塊得到的內存塊就應該為1014區間。如果你把第一塊內存塊釋放,然后再申請一塊大于10個單位的內存塊,比如說20個單位。因為剛被釋放的內存塊不能滿足新的請求,所以只能從15開始分配出20個單位的內存塊。現在整個內存空間的狀態是09空閑,1014被占用,1524被占用,2599空閑。其中09就是一個內存碎片了。如果1014一直被占用,而以后申請的空間都大于10個單位,那么09就永遠用不上了,變成外部碎片。
七、減少宏的使用
因為宏只是簡單的文本替換,缺乏類型檢查,因此不推薦使用。
除非絕對必要(如條件編譯),完全避免使用宏定義常量,統一采用 const(運行時常量)或 constexpr(編譯時常量)
// 宏方式(不推薦)
#define BUFFER_SIZE 1024
#define APP_VERSION "1.0.0.12" // 無類型// constexpr方式(推薦)
constexpr int BUFFER_SIZE = 1024;
constexpr const char* APP_VERSION = "10.0.0.22"; // 必須加 const或者:
#include <string_view>
constexpr std::string_view APP_VERSION = "10.0.0.22"; // C++17+
所以一般正常的宏定義就可以直接用constexpr代替了。const就用作常量使用即可。
關鍵字 | 常量性質 | 初始化時機 | 典型用途 |
---|---|---|---|
const | 運行時常量 | 運行時初始化 | 值在運行時確定(如配置文件讀取) |
constexpr | 編譯時常量 | 編譯時初始化 | 值必須在編譯期確定(如數組長度、模板參數 |
八、nullptr 代替宏NULL
C++11 之前,宏NULL 代表空指針,但是被定義為0,存在類型歧義。采用C++11 新增的關鍵字nullptr 代替NULL。
九、固定數組使用std::array 容器
C++ 11 新增了std::array 容器,用來存放固定大小的數組,訪問元素時具有越界檢查功能。
std::array<int, 5> arr = {1,2,3,4,5};
// arr[10] = 0; // 原生數組:未定義行為(可能崩潰)
arr.at(10) = 0; // 拋出 std::out_of_range 異常,安全捕獲錯誤
原生數組需通過 sizeof(arr)/sizeof(arr[0]) 計算長度,而 std::array 直接提供 size() 方法:
std::array<int, 5> arr;
std::cout << arr.size(); // 直接獲取,編譯期常量,安全高效
十、動態數組使用std::vector
使用std::vector 代替new[],利用new[] 動態申請內存,要用delete[] 釋放,容易發生內存泄漏。std::vector離開作用域自動釋放。并且返回的指針本身并沒有包含size 信息,訪問時不會進行越界檢查。std::vector 可以自動管理內存,并且訪問內存時可以進行邊界檢查。
std::vector<int> vec(5);
// vec[10] = 0; // 未定義行為(可能崩潰)
vec.at(10) = 0; // 拋出 std::out_of_range 異常,安全捕獲錯誤
十一、unordered_map 代替std::map
C++ 11 新增了unordered_map,采用hash table 的方式實現,插入和查找數據都是O(1)速度。std::map采用的好像是紅黑樹。
十二、using代替typedef
使用using 代替typedef 定義類型別名。
typedef int IntType; // 定義類型別名
typedef void (*FuncPtr)(int); // 定義函數指針別名using IntType = int; // 等價于 typedef
using FuncPtr = void (*)(int); // 等價于 typedef// 模板別名(typedef 無法實現)
template<typename T>
using Vec = std::vector<T>;
// 模板別名(using 專屬)
template<typename T>
using MapString = std::map<std::string, T>;MapString<int> age_map; // 等價于 std::map<std::string, int>// 若用 typedef 實現相同功能,需借助模板類+typedef
template<typename T>
struct MapString {typedef std::map<std::string, T> type;
};MapString<int>::type age_map; // 語法冗余
十三、enum class代替enum
在 C++ 中,enum class(強類型枚舉) 是 C++11 引入的特性,用于替代傳統的 enum(普通枚舉)。相比普通枚舉,enum class 提供了更嚴格的類型安全和作用域控制,解決了傳統枚舉的諸多缺陷。
作用域控制(避免命名沖突)
// 普通枚舉(命名沖突)
enum Color { RED, GREEN, BLUE };
enum TrafficLight { RED, YELLOW, GREEN }; // 錯誤:重復定義 RED、GREEN// 強類型枚舉(無沖突)
enum class Color { RED, GREEN, BLUE };
enum class TrafficLight { RED, YELLOW, GREEN };Color c = Color::RED; // 必須通過枚舉類型訪問
TrafficLight t = TrafficLight::RED; // 無命名沖突
類型安全(禁止隱式轉換)
enum OldEnum { A, B };
enum class NewEnum { A, B };void func(int x) { /* ... */ }func(A); // 普通枚舉:合法(隱式轉換為 int)
// func(NewEnum::A); // 錯誤:強類型枚舉不可隱式轉換
func(static_cast<int>(NewEnum::A)); // 必須顯式轉換
顯式底層類型
// 指定底層類型為 uint8_t(節省內存)
enum class Status : uint8_t {OK = 0,ERROR = 1,PENDING = 2
};// 普通枚舉無法指定底層類型,可能浪費內存
enum OldStatus {OK, // 通常為 int(4字節)ERROR,PENDING
};
十四、重寫明確使用override
在子類中重寫父類的虛函數時,虛函數的簽名(函數名+參數)必須與父類中的完全一樣。
如果稍有不同,就會被編譯器當作重載(overload)。
class Shape {
public:virtual double area() const = 0;
};class Circle : public Shape {
public:double area() const override { return 3.14 * r * r; } // 明確重寫
private:double r;
};