嵌入式或旧编译器不支持std::function或RTTI,且信号槽需零开销、无分配、避免虚函数,故用函数指针数组;但成员函数指针与自由函数指针类型不兼容,须统一为void()(void, int)签名,配合void*传对象指针和静态包装器实现带状态回调,注意生命周期与签名严格匹配,遍历时禁边调用边修改数组。

为什么不用 std::function 就要手写函数指针数组
因为嵌入式环境或旧编译器(比如 GCC 4.6、MSVC 2010)不支持 std::function 或 RTTI,而信号槽又不能依赖虚函数表做动态分发——否则每个槽都要继承基类,侵入性强。函数指针数组是零开销、无分配、纯栈操作的底线方案。
但要注意:C++ 中普通成员函数指针和自由函数指针**类型不兼容**,不能混存在同一数组里。所以必须二选一:
- 只支持
void(*)()类型的自由函数(最简单,但无法捕获对象状态) - 用模板 + 静态成员函数 +
void*用户数据绕过(推荐,兼顾对象绑定与零分配)
如何用 void* + 静态函数实现带对象上下文的回调
核心思路是把对象指针塞进 void*,再通过静态函数中转调用成员函数。这不是“万能委托”,但足够轻量。
示例结构体定义:
立即学习“C++免费学习笔记(深入)”;
struct Slot {
void* obj;
void (*func)(void*, int); // 统一签名:接收 obj 和一个 int 参数
};
注册时这样写:
struct MyButton {
void onClick(int code) { /* ... */ }
static void onClickWrapper(void* obj, int code) {
static_cast<MyButton*>(obj)->onClick(code);
}
};
// 注册
Slot s = {&btn, &MyButton::onClickWrapper};
容易踩的坑:
-
obj指针生命周期必须长于槽的调用周期,否则野指针 - 所有
func必须严格匹配签名,C++ 不检查void(*)(void*, int)和void(*)(int)的兼容性 - 不能用 lambda(即使不捕获,非静态 lambda 也无法转成函数指针)
信号触发时怎么安全遍历并调用槽数组
不能边遍历边删——这是多线程和单线程都存在的经典陷阱。常见错误是循环中调用 disconnect() 导致数组收缩、索引错位甚至越界读。
正确做法是两阶段处理:
- 第一遍:标记待移除项(比如置
func = nullptr) - 第二遍:压缩数组(memmove 或手动拷贝),或用独立的“待清理列表”延迟释放
单线程下最简方案:
for (int i = 0; i < count; ++i) {
if (slots[i].func) {
slots[i].func(slots[i].obj, value);
}
}
// 再扫一遍清理空槽(可选)
性能影响:每次触发都是 O(n),不适合高频小信号(如每帧 1000+ 次 emit)。这时应换哈希表或信号分组,但那就超出“简单”范畴了。
为什么不要在槽里调用 disconnect() 或 connect()
因为你的槽数组很可能正在被 emit() 的 for 循环迭代。此时修改数组长度或内容,会直接破坏循环逻辑——不是崩溃就是跳过某些槽。
真实场景中常遇到的问题:
- UI 按钮点击后立即
disconnect()自己,结果下一次点击没响应(实际是这次就漏掉了) - 槽里又触发另一个信号,形成递归 emit,而数组在中间被改写
解决办法只有两个:
- 用标志位延迟处理,比如设
pending_disconnect = true,等 emit 返回后再清理 - 彻底禁止槽内修改连接关系,把解绑逻辑提到上层事件循环中
这看起来约束强,但正是“简单信号槽”能保持确定性的代价——它不模拟 Qt 那种队列化投递,也不引入额外内存分配或锁。


















