前言
在 C++ 面向对象编程体系中,类是封装数据与行为的核心单元。实际开发中常需面对具有特殊约束的场景:例如防止对象拷贝以规避资源重复释放风险,限定对象创建位置(仅堆或仅栈)以规范内存管理,禁止类被继承以保障核心逻辑不被篡改,或是确保类仅存在一个实例以实现全局资源统一调度。
本文系统拆解五种典型特殊类的实现逻辑与技术细节,结合 C++ 语言特性提供完整代码示例,并分析方案的优缺点与适用场景。
设计一个不能被拷贝的类
只需让该类不能调用拷贝构造函数以及赋值运算符重载即可。
- C++98 方法:将拷贝构造函数与赋值运算符重载只声明不定义,并且将其访问权限设置为私有。
- C++11 方法:用
delete禁止生成拷贝构造函数和赋值运算符重载的默认成员函数。
设计一个只能在堆上创建对象的类
方法:让这个类不能被拷贝并且构造函数设置成私有的,然后再提供一个静态的成员函数,在该静态成员函数中完成堆对象的创建。
class HeapOnly {
public:
static HeapOnly* CreateObj() {
return new HeapOnly;
}
private:
HeapOnly() {}
HeapOnly(const HeapOnly& hp) = delete;
HeapOnly& operator=(const HeapOnly& hp) = delete;
};
设计一个只能在栈上创建对象的类
方法:先把构造函数私有化并且禁用 operator new,然后再搞个静态成员函数去构造对象。
引申:关于这里的
operator new,这个的话默认调用的是全局new,这类里面这样搞了之后这个类就用不了new了。
class StackOnly {
public:
static StackOnly CreateObj() {
StackOnly st;
return st;
}
private:
StackOnly() {}
void* operator new(size_t size) = delete;
};
设计一个不能被继承的类
方法:
- C++98 方法:让构造函数私有化就行了。
- C++11 方法:用
final关键字。
设计一个只能创建一个对象的类 (也叫做单例模式)
单例模式的概念:就是一个类只能创建一个对象。 作用:该模式可以保证系统中该类只有一个实例,并提供一个访问它的全局访问点,该实例被所有程序模块共享。
单例模式的两种实现方法
饿汉模式
优点:实现起来简单。 缺点:
- 如果单例对象初始化内容很多,影响启动速度。
- 如果两个单例类,互相有依赖关系,这时候对实例化的顺序有要求——但是饿汉模式控制不了实例化的顺序(尤其是在不同的编译单元)。
namespace hungry {
class Singleton {
public:
static Singleton& GetInstance() {
return _sinst;
}
private:
Singleton() {}
// 这里是防拷贝,演示的是 C++11 的方式
Singleton(const Singleton& s) = delete;
Singleton& operator=(const Singleton& s) = delete;
static Singleton _sinst;
};
Singleton Singleton::_sinst;
}
懒汉模式
namespace lazy {
class Singleton {
public:
static Singleton& GetInstance() {
if (_psinst == nullptr) {
_psinst = new Singleton;
}
return *_psinst;
}
static void DelInstance() {
if (_psinst) {
delete _psinst;
_psinst = nullptr;
}
}
class tmp {
public:
~tmp() {
lazy::Singleton::DelInstance();
}
};
private:
Singleton() {}
// 防拷贝
Singleton(const Singleton& s) = delete;
Singleton& operator=(const Singleton& s) = delete;
static Singleton* _psinst;
static tmp _tmp;
};
Singleton* Singleton::_psinst = nullptr; // 这意味着程序启动时,单例对象并没有被创建,仅仅是定义了一个指向它的空指针。
Singleton::tmp Singleton::_tmp;
}
一般单例不需要释放:因为程序结束的时候,操作系统会自动去回收的(用的不是析构函数)。 除非特殊场景:
- 中途需要显示释放。
- 程序结束时,需要做一些特殊动作(比如持久化)。
这里的第二种情况的持久化就是需要在程序结束时将数据写入其他地方——这个时候就需要调用析构函数,用析构函数写入——tmp 就保证了他的析构函数会起作用。


