大家好,我是程序员小青蛙。今天介绍特殊类的设计。

在 C++ 面向对象编程中,我们通常通过类的封装、继承和多态来实现通用的对象模型。但在实际工程场景中,我们经常需要对类的行为施加特殊限制:比如某个类只能创建一个实例、只能在堆上分配内存、不能被拷贝或继承。这些特殊类设计是 C++ 进阶开发的核心技巧,也是后端开发面试的高频考点。本文将结合代码示例,系统讲解 5 种最常用的特殊类设计方法,并深入解析单例模式的线程安全实现。

一、只能在堆上创建对象的类(HeapOnly)

需求背景

有些类的对象生命周期需要完全由程序员手动控制,或者对象体积过大不适合放在栈上(栈空间通常只有几 MB),此时需要限制该类只能通过new在堆上创建。

实现思路

核心是阻止栈上构造和拷贝构造,只提供静态工厂方法来创建堆对象:

  1. 将构造函数和拷贝构造函数设为私有,防止外部直接调用构造函数在栈上创建对象,也防止通过拷贝栈对象生成新对象
  2. 提供一个静态成员函数,内部通过new创建堆对象并返回其指针

完整代码实现

class HeapOnly {
public:
    // 静态工厂方法:唯一的对象创建入口
    static HeapOnly* CreateObject() {
        return new HeapOnly();
    }

    // 禁用拷贝构造和赋值运算符(C++11推荐)
    HeapOnly(const HeapOnly&) = delete;
    HeapOnly& operator=(const HeapOnly&) = delete;

private:
    // 构造函数私有:禁止外部直接构造
    HeapOnly() {}
    // C++98兼容写法:只声明不定义,且设为私有
    // HeapOnly(const HeapOnly&);
    // HeapOnly& operator=(const HeapOnly&);
};

关键注意点

如果不禁用拷贝构造,会出现漏洞:

HeapOnly* ptr = HeapOnly::CreateObject();
HeapOnly obj = *ptr; // 拷贝构造生成栈对象,破坏限制

因此必须同时禁用拷贝构造和赋值运算符。

二、只能在栈上创建对象的类(StackOnly)

需求背景

有些类的对象生命周期应该与函数调用栈绑定,比如轻量级的临时对象、不允许动态分配的资源句柄,此时需要限制该类只能在栈上创建。

实现方法一:静态工厂返回栈对象

与堆上类思路类似,构造函数私有,静态方法返回栈上的临时对象(依赖 C++ 返回值优化 RVO):

class StackOnly {
public:
    static StackOnly CreateObject() {
        return StackOnly(); // 返回栈临时对象,RVO优化后无拷贝
    }

private:
    StackOnly() {} // 构造函数私有
};

缺点:无法阻止在堆上用定位 new 构造对象(需要额外屏蔽)。

实现方法二:屏蔽operator newoperator delete

new运算符的底层会调用全局的void* operator new(size_t)函数,我们可以将该函数私有化,从而禁止外部使用new创建对象:

class StackOnly {
public:
    StackOnly() {} // 构造函数公有,允许栈上直接构造

private:
    // 屏蔽普通new和delete
    void* operator new(size_t size) = delete;
    void operator delete(void* p) = delete;
    // 屏蔽定位new(防止在堆内存上手动构造)
    void* operator new(size_t, void* place) = delete;
};

优点:可以直接在栈上定义对象StackOnly obj;,使用更自然,是更推荐的写法。

三、不能被拷贝的类(CopyBan)

需求背景

对于独占资源的类(如互斥锁、文件句柄、数据库连接),拷贝会导致资源重复释放或所有权混乱,因此需要禁止类的拷贝行为。

实现原理

C++ 中对象的拷贝只会发生在两个场景:拷贝构造函数赋值运算符重载。只要禁用这两个函数,就能完全禁止拷贝。

两种实现方式

C++98 方式:私有声明不定义
class CopyBan {
public:
    CopyBan() {}

private:
    // 只声明不定义,且设为私有
    CopyBan(const CopyBan&);
    CopyBan& operator=(const CopyBan&);
};

原理

  • 设为私有:防止外部调用这两个函数
  • 不定义:防止类内部成员函数不小心调用,导致链接错误
C++11 方式:= delete关键字

C++11 扩展了delete的用法,用= delete修饰默认成员函数,会让编译器直接删除该函数,调用时直接编译报错,语义更清晰:

class CopyBan {
public:
    CopyBan() {}

    // 直接删除拷贝构造和赋值运算符
    CopyBan(const CopyBan&) = delete;
    CopyBan& operator=(const CopyBan&) = delete;
};

四、不能被继承的类(NonInherit)

需求背景

当一个类的设计不希望被扩展,或者继承会破坏其封装性时,需要禁止该类被继承。

两种实现方式

C++98 方式:构造函数私有化

派生类构造时必须先调用基类的构造函数,如果基类构造函数私有,派生类无法调用,因此无法继承:

class NonInherit {
public:
    static NonInherit GetInstance() {
        return NonInherit();
    }

private:
    NonInherit() {} // 构造函数私有
};
C++11 方式:final关键字(推荐)

C++11 新增了final关键字,用final修饰的类表示 "最终类",任何试图继承该类的行为都会直接编译报错:

class A final {
    // 类的成员
};

// 编译错误:无法继承final类A
class B : public A {};

这种方式语义明确,代码简洁,是 C++11 及以后的标准写法。

五、只能创建一个对象的类(单例模式)

单例模式是最常用的设计模式之一,它保证一个类在整个程序中只有一个实例,并提供一个全局访问点。典型应用场景:配置管理器、日志器、数据库连接池、线程池等。

单例模式主要分为饿汉模式懒汉模式两种实现。

1. 饿汉模式(Eager Singleton)

核心思想

不管程序是否会用到这个单例,在程序启动(main 函数执行前)就完成实例的初始化

代码实现
class Singleton {
public:
    // 全局唯一访问点
    static Singleton* GetInstance() {
        return &m_instance;
    }

    // 禁用拷贝
    Singleton(const Singleton&) = delete;
    Singleton& operator=(const Singleton&) = delete;

private:
    // 构造函数私有
    Singleton() {}

    // 静态成员变量:程序入口前初始化
    static Singleton m_instance;
};

// 类外初始化静态成员
Singleton Singleton::m_instance;
优缺点分析
优点 缺点
实现简单,代码量少 程序启动慢(如果单例初始化耗时)
天然线程安全(静态变量在 main 前单线程初始化) 多个单例的初始化顺序无法控制(若 A 依赖 B 但 A 先初始化,会出错)
适用场景

单例对象初始化耗时短、程序启动时必须使用、多线程高并发场景下对性能要求高。


2. 懒汉模式(Lazy Singleton)

核心思想

延迟加载:只有第一次调用GetInstance()获取实例时,才创建对象。解决了饿汉模式启动慢和初始化顺序不可控的问题。

线程安全问题与解决方案

懒汉模式在多线程环境下存在线程安全问题:多个线程同时进入GetInstance()时,可能会创建多个实例。下面逐步讲解正确的线程安全实现。

❌ 错误写法 1:无锁
static Singleton* GetInstance() {
    if (_psins == nullptr) {
        _psins = new Singleton(); // 多线程同时进入会创建多个实例
    }
    return _psins;
}
❌ 错误写法 2:单检查加锁
static Singleton* GetInstance() {
    _smtx.lock();
    if (_psins == nullptr) {
        _psins = new Singleton();
    }
    _smtx.unlock();
    return _psins;
}

问题:每次调用GetInstance()都要加锁,性能极低,尤其是单例创建完成后,加锁完全是多余的。

✅ 正确写法 1:双检查锁(DCL)

双检查锁(Double-Checked Locking)是兼顾线程安全性能的经典实现:

#include <mutex>

class Singleton {
public:
    static Singleton* GetInstance() {
        // 第一次检查:对象创建后,直接返回,无需加锁
        if (_psins == nullptr) {
            std::lock_guard<std::mutex> lock(_smtx); // RAII智能锁,自动释放
            // 第二次检查:防止多个线程通过第一次检查后,排队创建多个对象
            if (_psins == nullptr) {
                _psins = new Singleton();
            }
        }
        return _psins;
    }

    // 禁用拷贝
    Singleton(const Singleton&) = delete;
    Singleton& operator=(const Singleton&) = delete;

private:
    Singleton() {}

    static Singleton* _psins;
    static std::mutex _smtx;

    // 内嵌垃圾回收类:程序结束时自动释放单例
    class CGarbo {
    public:
        ~CGarbo() {
            if (Singleton::_psins) {
                delete Singleton::_psins;
            }
        }
    };
    static CGarbo Garbo;
};

// 类外初始化静态成员
Singleton* Singleton::_psins = nullptr;
std::mutex Singleton::_smtx;
Singleton::CGarbo Singleton::Garbo;

双检查锁原理

  • 第一次nullptr检查:提高性能,对象创建后不再加锁
  • 加锁:保证同一时间只有一个线程进入临界区
  • 第二次nullptr检查:防止多个线程通过第一次检查后,排队创建多个实例
✅ 正确写法 2:静态局部变量懒汉(C++11 + 推荐)

C++11 标准规定:函数内的静态局部变量初始化是线程安全的,编译器会保证只有一个线程执行初始化代码。这是目前最简洁的单例实现:

class Singleton {
public:
    static Singleton& GetInstance() {
        // 静态局部变量:第一次调用时初始化,线程安全
        static Singleton instance;
        return instance;
    }

    // 禁用拷贝
    Singleton(const Singleton&) = delete;
    Singleton& operator=(const Singleton&) = delete;

private:
    Singleton() {}
};

优点

  • 代码最简洁,无需手动管理锁和内存
  • 线程安全(C++11 及以后)
  • 程序结束时自动调用析构函数释放资源

注意:C++11 之前的编译器不保证静态局部变量初始化的线程安全,请勿使用。


饿汉模式 vs 懒汉模式对比

对比维度 饿汉模式 懒汉模式(双检查锁) 懒汉模式(静态局部变量)
初始化时机 程序启动时 第一次使用时 第一次使用时
线程安全 是(C++11+)
实现复杂度 简单 中等 最简单
启动速度
初始化顺序控制 不能
推荐指数 ⭐⭐⭐ ⭐⭐⭐⭐ ⭐⭐⭐⭐⭐

总结

特殊类设计的核心思想是通过控制成员访问权限和利用 C++ 语言特性,限制类的默认行为,从而满足特定的业务需求:

  • 限制堆 / 栈创建:通过私有构造函数或屏蔽operator new实现
  • 禁止拷贝:删除拷贝构造和赋值运算符
  • 禁止继承:使用final关键字(C++11+)
  • 单例模式:优先使用 C++11 静态局部变量懒汉实现,兼顾简洁性和线程安全

这些技巧不仅能提高代码的健壮性和可维护性,也是后端开发面试中的必考点。掌握这些特殊类设计,能让你在实际开发中写出更优雅、更符合工程规范的 C++ 代码。

更多推荐