
C++封装性
本篇文章是笔者在编写C++代码时遇到对于C++封装性问题的思考,由于笔者一直对C比较熟悉,C++是半吊子入门,没有系统学习,所以如有提出简单的问题望大家见谅长期更新
全局变量定义在哪
在编写代码时遇到需要在源文件定义一个全局变量,如果是C语言我可能会直接定义成一个全局变量,但是在C++中我是应该定义在类成员中还是就定义成源文件中的全局变量呢
在 C++ 中,当在类的源文件(.cpp)中需要使用一个变量时,选择将其定义为类的私有成员变量还是源文件作用域的全局变量,取决于变量的用途、生命周期、是否需要对象实例,以及封装性和可维护性等因素。
类的私有成员变量
这种方式是较为推荐的一种方式
// MyClass.h
class MyClass {
private:
int m_counter; // 每个对象独立
public:
void increment();
};
// MyClass.cpp
void MyClass::increment() {
m_counter++;
}
优点:
- 封装性好,符合面向对象设计原则
- 每个对象拥有独立状态
- 易于测试、维护和扩展
- 避免命名冲突
适用场景:
- 变量与对象状态相关
- 每个对象需要独立的数据
笔者在大部分时间都适用这种方式,因为基本上很多时候需要的全局变量都是和某个功能集合相关,所以在C++中有类定义的时候就直接变成类的私有成员变量
类的静态成员变量
适用于创建了多个类对象实例
// MyClass.h
class MyClass {
private:
static int s_instanceCount; // 所有对象共享
public:
static int getInstanceCount();
};
// MyClass.cpp
int MyClass::s_instanceCount = 0;
int MyClass::getInstanceCount() {
return s_instanceCount;
}
优点:
- 所有对象共享一份数据
- 仍属于类作用域,保持封装性
- 不需要对象实例即可访问(通过类名)
适用场景:
- 需要统计对象数量、共享配置等
- 数据与类相关,但不依赖具体对象
源文件作用域全局变量
// MyClass.cpp
namespace {
int g_helperValue = 0; // 文件作用域,外部不可见
}
void someHelperFunction() {
g_helperValue++;
}
优点:
- 实现简单,无需类实例
- 使用匿名命名空间可限制作用域,避免污染全局命名空间
缺点:
- 破坏封装性,难以追踪变量来源
- 不利于单元测试和并发安全
- 生命周期为整个程序,可能引发资源管理问题
适用场景:
- 仅用于当前 .cpp 文件内部的辅助逻辑
- 不依赖对象状态,且无需跨文件访问
一般来说定义的全局变量都需要用匿名空间,这样就不会去污染全局命名空间
不属于类的辅助函数处理
在C语言中,有很多被static修饰的辅助函数,不会被外部调用,但是又会被接口函数内部调用,并且这些辅助函数还会调用到类的私有成员(也就是上面本身应该是全局变量的情况)那么在C++中应该把这类函数放置在哪里呢?
心矛盾在于:static 函数(无论是类的静态成员函数,还是源文件内的自由函数)没有 this 指针,因此无法直接访问非静态的私有成员变量。因为私有成员变量属于具体的对象实例,而 static 函数不属于任何对象实例。
修改为私有成员函数
这是最符合面向对象设计原则的做法。既然函数需要操作对象的状态(私有成员),它就应该属于这个对象。
为什么之前可能想把它写成 static? 通常是为了“隐藏实现细节”或“避免污染头文件”。但私有成员函数(private member function) 本身就对外部隐藏了,且定义在 .cpp 中完全没问题。
// MyClass.h
class MyClass {
private:
int m_data;
// 1. 声明为私有成员函数,而不是 static
void helperProcess();
public:
void publicInterface();
};
// MyClass.cpp
// 2. 实现时不需要 static 关键字
void MyClass::helperProcess() {
// 可以直接访问 m_data,因为有 this 指针
m_data += 10;
}
void MyClass::publicInterface() {
// 接口调用辅助函数
helperProcess();
}
优点:
- 自然访问
this和所有成员变量。 - 封装性最好,逻辑内聚。
- 继承和多态友好(如果是虚函数)。
传递对象实例作为参数
如果该函数定义在 .cpp 内部且不属于类(自由函数),它默认无法访问 private 成员。你需要将其声明为类的 friend,或者通过 public 接口访问(不推荐)。
// MyClass.h
class MyClass {
private:
int m_data;
// 1. 声明为 static 成员,或者 friend
static void helperProcess(MyClass* self);
// 或者:friend void helperProcess(MyClass* self);
public:
void publicInterface();
};
// MyClass.cpp
// 2. 实现 static 函数
void MyClass::helperProcess(MyClass* self) {
// 通过指针访问私有成员
self->m_data += 10;
}
void MyClass::publicInterface() {
// 3. 调用时传入 this
helperProcess(this);
}
优点:
- 函数无状态,易于单独测试。
- 明确表明该函数不依赖隐式状态。
缺点:
- 语法稍显啰嗦(每次都要传
this)。 - 如果是
.cpp内的自由函数,需要friend声明,略微破坏封装。
Lambda 表达式
更高级的语法适用Lambda表达式,这里就不详述了
pthread_create调用类成员线程函数
非静态成员函数不能直接用作 pthread_create 的线程函数。
非静态成员函数在编译后会多一个隐藏的 this 参数,所以它的类型和普通函数指针完全不兼容,无法直接转换。
解决方案
static 成员函数 + 传递 this 指针(兼容 pthread)
// AV_DEV.h
class AV_DEV {
public:
void startVideoThread();
private:
int m_videoData;
// ✅ 真正的业务逻辑:非静态成员函数,可访问私有成员
void* get_video_impl();
// ✅ 静态线程入口:符合 pthread_create 要求,做参数转发
static void* get_video(void* param);
};
// AV_DEV.cpp
// 1️⃣ 静态函数实现:负责把 void* 转回对象指针
void* AV_DEV::get_video(void* param) {
// 把传入的 void* 强制转回 AV_DEV*
AV_DEV* self = static_cast<AV_DEV*>(param);
// 调用真正的成员函数
return self->get_video_impl();
}
// 2️⃣ 真正的业务逻辑:可以自由访问私有成员
void* AV_DEV::get_video_impl() {
// ✅ 这里可以直接访问 m_videoData 等私有成员
m_videoData = 100;
// ... 你的视频处理逻辑 ...
return nullptr;
}
// 3️⃣ 启动线程:传入 this 指针
void AV_DEV::startVideoThread() {
pthread_t thread;
// ⚠️ 注意:第四个参数传入 this,会在静态函数中接收
pthread_create(&thread, nullptr, get_video, this);
// 可选:保存 thread 句柄以便后续 join/detach
}
使用 std::thread(C++11)
直接用 std::thread 更简单、更安全,它原生支持绑定成员函数
#include <thread>
// AV_DEV.h
class AV_DEV {
public:
void startVideoThread();
private:
int m_videoData;
// ✅ 普通成员函数即可,无需 static
void* get_video();
};
// AV_DEV.cpp
void* AV_DEV::get_video() {
// ✅ 直接访问私有成员
m_videoData = 100;
// ... 业务逻辑 ...
return nullptr;
}
void AV_DEV::startVideoThread() {
// ✅ std::thread 自动处理 this 指针绑定
std::thread t(&AV_DEV::get_video, this);
// 二选一:
t.detach(); // 分离线程,后台运行
// 或
// t.join(); // 等待线程结束(通常不在这里调用)
// 💡 建议:用成员变量保存 thread 对象,便于后续管理
}
全局线程标志
在多线程场景下,一般需要一个停止标志位作为通知线程停止的标志。当主程序收到终止信号后,在信号处理函数中将标志位置位,然后通知到各个线程停止运行
volatile sig_atomic_t keep_running = 1;
void sigint_handler(int sig) {
// 使用 write() 而不是 printf,因为 write() 是信号安全的
const char msg1[] = "\n[SIGNAL] Received SIGINT/SIGTERM\n";
write(STDOUT_FILENO, msg1, sizeof(msg1) - 1);
if (sig == SIGINT || sig == SIGTERM) {
keep_running = 0;
const char msg2[] = "[SIGNAL] Signal handler done\n";
write(STDOUT_FILENO, msg2, sizeof(msg2) - 1);
}
}
虽然 volatile sig_atomic_t 是处理信号的经典 C 语言写法,但在 C++ 类成员 + 多线程 + 信号处理函数 这个组合场景下,它违反了多个安全原则。
volatile不能保证多线程内存可见性
- 规则:
volatile关键字仅告诉编译器“不要优化这个变量,每次都要从内存读”,它不保证 CPU 层面的内存顺序(Memory Ordering)和多核缓存一致性。 - 风险: 在 ARM 等弱内存序架构上,线程 A 修改了
keep_running,线程 B 可能因为 CPU 缓存未同步而一直读到旧值(1),导致线程无法退出。 - 结论: 多线程同步应使用
std::atomic(C++) 或__atomic(C11),它们包含必要的内存屏障(Memory Barrier)。
sig_atomic_t的适用范围
- 规则:
sig_atomic_t保证的是 同一个线程内 被信号中断时的原子性。 - 风险: 它并没有严格保证 不同线程之间 的原子性(虽然在 x86 上
int通常是原子的,但这依赖于硬件实现,不是标准保证)。 - 结论: 跨线程通信标志位,首选
std::atomic<bool>。
所以要使用全局 std::atomic 标志位
//主函数文件
// 全局,只用于信号通知,职责单一
std::atomic<bool> g_keep_running{true};
void sigint_handler(int sig) {
// 使用 write() 而不是 printf,因为 write() 是信号安全的
const char msg1[] = "\n[SIGNAL] Received SIGINT/SIGTERM\n";
write(STDOUT_FILENO, msg1, sizeof(msg1) - 1);
if (sig == SIGINT || sig == SIGTERM) {
g_keep_running.store(false, std::memory_order_release);
const char msg3[] = "[SIGNAL] Signal handler done\n";
write(STDOUT_FILENO, msg3, sizeof(msg3) - 1);
}
}
在其他类文件使用外部声明:
extern std::atomic<bool> g_keep_running;
void thread_func() {
while g_keep_runningg.load(std::memory_order_acquire)) {
// ...
}
}
信号处理函数不要做过多操作,将退出标志位设为 static std::atomic<bool> 全局变量,信号处理函数仅负责将其置为 false