【学习笔记】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;
};