为什么避免动态分配?
malloc
, new
)开销大,速度慢。特性 | 栈 (stack) | 堆 (heap) |
---|---|---|
分配/释放速度 | 极快 (O(1)) | 较慢 (需管理分配表,O(log n) 或更慢) |
生命周期 | 自动管理,作用域结束即释放 | 手工管理,需要显式释放 |
内存大小 | 较小 (几百 KB 到几 MB) | 较大(可达 GB 级别) |
碎片化 | 不存在碎片 | 容易碎片化 |
适用场景 | 临时变量、小对象 | 大对象、跨作用域对象 |
✅ 优化建议:
std::array
代替 std::vector
(固定大小时)。 核心思想:
预先申请一大块内存,将其划分成小块,重复利用,避免频繁调用操作系统的堆分配。
// 简化版内存池示例
class MemoryPool {
struct Block { Block* next; };
Block* freeList = nullptr;
public:
void* allocate() {
if (freeList) {
Block* b = freeList;
freeList = freeList->next;
return b;
} else {
return ::operator new(sizeof(Block));
}
}
void deallocate(void* p) {
Block* b = static_cast<Block*>(p);
b->next = freeList;
freeList = b;
}
};
定义:
C++ STL 容器支持自定义分配器,可以让容器使用你的内存池或其他内存策略。
template <typename T>
struct PoolAllocator {
using value_type = T;
MemoryPool* pool;
PoolAllocator(MemoryPool* p) : pool(p) {}
T* allocate(std::size_t n) {
return static_cast<T*>(pool->allocate());
}
void deallocate(T* p, std::size_t) {
pool->deallocate(p);
}
};
应用:
MemoryPool pool;
std::vector<int, PoolAllocator<int>> v(PoolAllocator<int>(&pool));
碎片化 = 内存分配与释放交错产生空洞,导致大块内存无法用。
✅ 使用对象池或内存池,统一分配固定大小对象。
✅ 分配内存时按对齐要求(power of two size)分配。
✅ 分配和释放模式尽量对称,减少交错分配。
✅ 对于可变大小数据,考虑 slab/arena/allocation region。
✅ 审视分配策略,预分配大块内存,集中管理。
为什么要用编译时计算?
定义
constexpr
函数可以在编译期求值,如果输入是编译时常量,则返回值也是编译时常量。
constexpr int square(int x) {
return x * x;
}
用途
特点
✅ 必须是单表达式(C++11),C++14 后可有局部变量、分支。
✅ 不允许运行时不可确定的行为(比如 I/O,动态分配)。
示例
constexpr int factorial(int n) {
return n <= 1 ? 1 : (n * factorial(n - 1));
}
constexpr int f5 = factorial(5); // 编译时计算 120
如何利用编译时计算提升性能?
✅ 用 constexpr
替代 const
,让编译器更积极求值。
✅ 在模板参数、数组大小中直接写 constexpr
表达式。
✅ 用 constexpr
生成查找表,避免运行时重复计算。
示例:预生成查找表
constexpr int fib(int n) {
return (n <= 1) ? n : (fib(n - 1) + fib(n - 2));
}
constexpr int fib_table[] = {
fib(0), fib(1), fib(2), fib(3), fib(4), fib(5)
};
编译期就展开成:
{0, 1, 1, 2, 3, 5}
无需运行时递归。
注意
⚠️ 过于复杂的编译期计算会增加编译时间。
⚠️ 某些 constexpr 运算在编译期求值失败会退回到运行时求值。
constexpr uint8_t crc8_table[256] = []{
uint8_t table[256] = {0};
for (uint16_t i = 0; i < 256; ++i) {
uint8_t crc = 0;
uint8_t c = static_cast<uint8_t>(i);
for (uint8_t j = 0; j < 8; ++j) {
const uint8_t tmp = (crc ^ c) & 0x01;
crc >>= 1;
if (tmp) crc ^= 0x8C;
c >>= 1;
}
table[i] = crc;
}
return table;
}(); // 编译时生成CRC表
constexpr if
条件编译定义
constexpr if
(C++17) 允许在模板或编译期上下文中按条件编译不同代码路径。
示例
template <typename T>
void print_type_info() {
if constexpr (std::is_integral_v<T>) {
std::cout << "Integral type\n";
} else {
std::cout << "Non-integral type\n";
}
}
编译器只生成匹配分支的代码,另一个分支会完全丢弃。
优势
✅ 可编译时选择实现逻辑,避免无效分支引入代码膨胀或编译错误。
定义
提供类型信息的编译时查询和操作工具,用于模板元编程和条件编译。
常见 type traits
trait | 作用 |
---|---|
std::is_integral |
判断 T 是否整数类型 |
std::is_floating_point |
判断 T 是否浮点型 |
std::is_same |
判断 T 和 U 是否相同 |
std::remove_const |
去除 const 修饰符 |
std::enable_if |
条件启用模板实例化 |
示例
template <typename T>
void foo(T t) {
static_assert(std::is_integral_v<T>, "T must be integral");
}
或者结合 enable_if
template <typename T>
std::enable_if_t<std::is_integral_v<T>>
process(T t) {
// 仅整数类型实例化
}
总结
技术 | 作用 | 优势 |
---|---|---|
constexpr function |
编译时计算函数值 | 减少运行时开销 |
编译时计算优化 | 静态生成查表等 | 提升效率,代码更紧凑 |
constexpr if |
条件编译不同分支 | 编译期去除无效代码 |
type traits | 编译期类型信息 | 模板元编程更强大 |
高级应用
constexpr if
)。什么是 RAII (Resource Acquisition Is Initialization)
RAII(资源获取即初始化)是一种 C++ 编程惯用法,用对象的构造函数获取资源、析构函数释放资源。
✅ 核心思想: 生命周期绑定资源管理,资源随对象自动申请和释放。
定义:
资源(如内存、文件句柄、互斥锁等)在对象构造时获取,并且绑定到对象上。
class FileHandler {
FILE* fp;
public:
FileHandler(const char* filename, const char* mode) {
fp = fopen(filename, mode);
if (!fp) throw std::runtime_error("File open failed");
}
~FileHandler() {
if (fp) fclose(fp);
}
};
➡️ 构造时 fopen
,出错直接抛异常。
定义:
对象生命周期结束(作用域结束、栈帧退出),析构函数被调用,自动释放资源。
✅ 无需显式释放,避免资源泄漏。
✅ 支持栈展开时释放资源(例如异常抛出时)。
void foo() {
FileHandler f("data.txt", "r");
// 无论函数如何返回,f 的析构都会调用 fclose
}
RAII 是实现异常安全代码的基础:
catch
里手工清理。例子:
void process() {
FileHandler f("data.txt", "r");
// 即使这里抛出异常,f 的析构也会被调用,关闭文件。
throw std::runtime_error("unexpected error");
}
资源释放与逻辑解耦,提高健壮性。
RAII 在并发编程中非常重要,用于自动管理互斥锁。
常见例子:std::lock_guard
std::mutex mtx;
void thread_safe_func() {
std::lock_guard<std::mutex> lock(mtx);
// 临界区
// lock 析构时自动释放锁
}
➡️ 退出作用域,lock
的析构函数自动释放锁,无需手工调用 mtx.unlock()
。
其他 RAII 锁类
std::unique_lock
:支持延迟锁、手工 unlock/relock。std::scoped_lock
(C++17):同时锁多个互斥量,防止死锁。优势 | 说明 |
---|---|
自动资源管理 | 生命周期自动释放资源,避免泄漏 |
异常安全 | 确保资源在异常时释放 |
可组合性强 | 可用于文件、内存、锁、套接字等任何资源 |
简化代码 | 不需要手工写繁琐的释放逻辑 |
实战场景
std::unique_ptr
, std::shared_ptr
)。std::lock_guard
, std::unique_lock
)。示例:自定义 RAII 锁
class MyLock {
std::mutex& m;
public:
MyLock(std::mutex& m_) : m(m_) { m.lock(); }
~MyLock() { m.unlock(); }
};
使用:
void func() {
static std::mutex mtx;
MyLock lock(mtx);
// 临界区
}
模板元编程(Template Metaprogramming)是利用 C++ 模板机制,在编译期完成逻辑运算和代码生成,减少运行时开销。
本质:编译时计算 + 类型编程
利用模板的递归实例化,实现编译期计算。
示例:编译时阶乘
template <int N>
struct Factorial {
static constexpr int value = N * Factorial<N - 1>::value;
};
template <>
struct Factorial<0> {
static constexpr int value = 1;
};
用法:
constexpr int f5 = Factorial<5>::value; // 120
➡ 编译期就计算完成,不产生运行时开销。
SFINAE = Substitution Failure Is Not An Error
当模板参数推导失败时,编译器不会报错,而是从重载集中剔除该模板。
示例:通过 enable_if 控制重载
#include
template <typename T>
std::enable_if_t<std::is_integral_v<T>>
func(T t) {
// 整数类型特化
}
template <typename T>
std::enable_if_t<std::is_floating_point_v<T>>
func(T t) {
// 浮点类型特化
}
编译器根据 T 类型选择合适版本,不匹配的直接忽略。
✅ 控制模板实例化条件
✅ 防止无效模板实例化导致编译失败
✅ 实现编译期多态、特化、静态接口检查
模板参数类型通过调用实参自动推导。
示例
template <typename T>
void print_type(const T& value) {
// T 的类型由实参决定
}
用法:
print_type(10); // T = int
print_type(3.14); // T = double
print_type("abc"); // T = const char*
✅ 支持引用折叠、const 推导
✅ 可与 auto
, decltype
, decltype(auto)
结合使用
✅ 可在模板中推导返回值
template <typename T1, typename T2>
auto add(T1 a, T2 b) {
return a + b;
}
编译时多态(静态多态) = 通过模板实例化在编译时生成不同的代码版本。
对比:
静态多态 | 动态多态 |
---|---|
模板实例化决定 | 虚函数运行时决定 |
无虚表,无运行时开销 | 有虚表指针,运行时分派 |
编译时展开 | 运行时分派 |
示例:编译时多态接口
template <typename T>
void process(const T& t) {
t.run(); // 编译期确定调用哪个类型的 run()
}
不同类型调用:
struct A { void run() const { /* ... */ } };
struct B { void run() const { /* ... */ } };
A a; B b;
process(a); // 生成 process
process(b); // 生成 process
➡ 无运行时分派,代码直接内联优化。
技术 | 作用 | 优势 |
---|---|---|
模板递归 | 编译时计算 | 静态常量、查表、逻辑验证 |
SFINAE | 条件启用模板 | 控制实例化,接口约束 |
类型推导 | 自动确定参数类型 | 简化接口,灵活泛型 |
编译时多态 | 编译时生成不同实现 | 零开销替代虚函数 |
⚠️ 注意事项
⚡ 编译时间可能增长(尤其模板递归深度大时)
⚡ 编译错误可能难读(C++20 concept 改善了这点)
⚡ 模板元编程写法复杂,需谨慎设计
MISRA-C++ 聚焦于消除未定义行为(Undefined Behavior)和提升代码可靠性,核心准则包括:
变量与内存安全
int x = 0;
而非 int x;
)。nullptr
)。类型与表达式安全
int
转 char
需显式强制转换)。std::numeric_limits
进行范围检查)。资源管理
new
/delete
),优先使用静态分配或对象池。接口与可维护性
const
修饰输入参数)。constexpr
或枚举定义常量(如 constexpr int MAX_SIZE = 256;
)。MISRA-C++ 基于嵌入式资源限制和安全性,明确禁用以下特性:
特性 | 禁用原因 | 替代方案 |
---|---|---|
动态内存分配 | 可能导致内存碎片、分配失败,不适合资源受限系统 | 使用静态数组、对象池或定制内存分配器 |
异常处理 | 异常栈展开会增加代码体积和运行时开销,且可能导致资源泄漏 | 用错误码或状态返回值替代 |
RTTI(运行时类型信息) | 依赖额外数据结构,增加二进制体积,且与静态分析冲突 | 使用编译时多态(模板特化)替代 |
标准库组件 | 部分STL组件(如 std::vector )依赖动态内存,且实现复杂 |
使用简化的嵌入式容器或手动实现数据结构 |
隐式类型转换 | 可能导致精度丢失或行为未定义 | 强制使用显式类型转换(如 static_cast ) |
函数重载 | 可能导致接口歧义,增加静态分析难度 | 用明确的函数名替代(如 init_uart1 ) |
用于检测代码是否符合MISRA-C++标准的工具包括:
MISRA官方工具
工业级静态分析工具
开源工具
人工审查需关注MISRA-C++规则中工具难以覆盖的逻辑和设计问题:
结构与命名规范
uart_tx_buffer
或 UartTxBuffer
)。逻辑正确性
for (;;)
需明确注释用途)。硬件交互安全性
可移植性与兼容性
__attribute__
),需明确注释。int
需确保为32位)。constexpr
、static_assert
),对模板元编程的限制更灵活。实践建议:根据项目安全等级(如ISO 26262 ASIL-D)选择对应规则集,并通过工具+人工审查双重保障代码质量。