C++封装性

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++ 类成员 + 多线程 + 信号处理函数 这个组合场景下,它违反了多个安全原则。

  1. volatile 不能保证多线程内存可见性
  • 规则: volatile 关键字仅告诉编译器“不要优化这个变量,每次都要从内存读”,它不保证 CPU 层面的内存顺序(Memory Ordering)和多核缓存一致性。
  • 风险:ARM 等弱内存序架构上,线程 A 修改了 keep_running,线程 B 可能因为 CPU 缓存未同步而一直读到旧值(1),导致线程无法退出。
  • 结论: 多线程同步应使用 std::atomic (C++) 或 __atomic (C11),它们包含必要的内存屏障(Memory Barrier)。
  1. 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

上一篇