1. 特殊类设计基础与核心原则
在C++开发中,特殊类设计是指那些需要特殊处理或具有特殊语义的类。这类设计往往涉及资源管理、对象生命周期控制等核心问题。我们先来看几个典型的特殊类场景:
- 只能在堆上创建对象的类
- 只能在栈上创建对象的类
- 禁止拷贝的类
- 禁止继承的类
- 单例类(全局唯一实例)
这些特殊类的设计都遵循一个基本原则:通过控制类的构造函数、析构函数、拷贝构造函数和赋值运算符的访问权限,来限制对象的创建方式和行为。
1.1 堆栈对象创建限制的实现
要让一个类只能在堆上创建,最直接的方法是将其析构函数设为private。这样,在栈上创建对象时,编译器会因为无法调用析构函数而报错:
cpp复制class HeapOnly {
public:
static HeapOnly* create() {
return new HeapOnly();
}
void destroy() {
delete this;
}
private:
~HeapOnly() {}
};
相反,如果想让类只能在栈上创建,可以将operator new设为private:
cpp复制class StackOnly {
public:
StackOnly() {}
~StackOnly() {}
private:
void* operator new(size_t size);
void operator delete(void* ptr);
};
提示:现代C++中更推荐使用=delete语法来明确删除这些函数,而不是简单地设为private。
1.2 不可拷贝类的实现
C++11之前,我们通过将拷贝构造函数和赋值运算符声明为private来实现不可拷贝:
cpp复制class NonCopyable {
protected:
NonCopyable() {}
~NonCopyable() {}
private:
NonCopyable(const NonCopyable&);
NonCopyable& operator=(const NonCopyable&);
};
C++11引入了更简洁的表达方式:
cpp复制class NonCopyable {
public:
NonCopyable(const NonCopyable&) = delete;
NonCopyable& operator=(const NonCopyable&) = delete;
protected:
NonCopyable() = default;
~NonCopyable() = default;
};
2. 单例模式深度解析
单例模式可能是设计模式中最著名也最常被讨论的一个。它确保一个类只有一个实例,并提供一个全局访问点。但在实际应用中,单例模式的实现远比看起来复杂。
2.1 基础单例实现及其问题
最简单的单例实现如下:
cpp复制class Singleton {
public:
static Singleton& getInstance() {
static Singleton instance;
return instance;
}
void doSomething() {}
private:
Singleton() {}
~Singleton() {}
Singleton(const Singleton&) = delete;
Singleton& operator=(const Singleton&) = delete;
};
这种实现利用了C++11的magic static特性,保证了线程安全性。但在某些场景下仍存在问题:
- 静态对象的销毁顺序问题
- 无法传递参数给构造函数
- 难以进行单元测试
2.2 线程安全的单例实现
在C++11之前,我们需要手动实现线程安全:
cpp复制class ThreadSafeSingleton {
public:
static ThreadSafeSingleton* getInstance() {
if (!instance) {
std::lock_guard<std::mutex> lock(mutex);
if (!instance) {
instance = new ThreadSafeSingleton();
}
}
return instance;
}
private:
static ThreadSafeSingleton* instance;
static std::mutex mutex;
ThreadSafeSingleton() {}
};
这种双重检查锁定模式虽然有效,但在某些平台上可能因为内存模型问题而失效。C++11的atomic和call_once提供了更好的解决方案。
2.3 单例模式的变体
根据实际需求,单例模式可以有多种变体:
- 多例模式:限制实例数量但不限于一个
- 线程局部单例:每个线程有自己的"单例"
- 可配置单例:允许在运行时决定是否使用单例
cpp复制template<typename T>
class ThreadLocalSingleton {
public:
static T& getInstance() {
static thread_local T instance;
return instance;
}
};
3. 单例模式在现代C++中的最佳实践
随着C++标准的发展,单例模式的实现方式也在不断演进。以下是几个现代C++中的最佳实践:
3.1 依赖注入与单例
单例模式常被批评的一点是它引入了全局状态,使得代码难以测试。一种改进方法是使用依赖注入:
cpp复制class Database {
public:
virtual ~Database() = default;
virtual void query() = 0;
};
class RealDatabase : public Database {
public:
void query() override { /*...*/ }
};
class MockDatabase : public Database {
public:
void query() override { /*...*/ }
};
class Application {
public:
explicit Application(Database& db) : db(db) {}
void run() { db.query(); }
private:
Database& db;
};
3.2 单例与智能指针
结合智能指针可以更好地管理单例的生命周期:
cpp复制class SmartSingleton {
public:
static std::shared_ptr<SmartSingleton> getInstance() {
static std::weak_ptr<SmartSingleton> weakInstance;
auto instance = weakInstance.lock();
if (!instance) {
instance = std::shared_ptr<SmartSingleton>(new SmartSingleton());
weakInstance = instance;
}
return instance;
}
private:
SmartSingleton() {}
};
这种实现允许单例在不再被使用时自动释放资源。
3.3 CRTP单例模板
使用CRTP(奇异递归模板模式)可以创建可复用的单例基类:
cpp复制template<typename T>
class Singleton {
public:
static T& getInstance() {
static T instance;
return instance;
}
protected:
Singleton() = default;
~Singleton() = default;
Singleton(const Singleton&) = delete;
Singleton& operator=(const Singleton&) = delete;
};
class MyClass : public Singleton<MyClass> {
friend class Singleton<MyClass>;
private:
MyClass() {}
};
4. 单例模式的常见问题与解决方案
在实际项目中,单例模式可能会遇到各种问题。以下是几个典型场景及其解决方案:
4.1 初始化顺序问题
当多个单例相互依赖时,它们的初始化顺序可能导致问题。解决方案之一是使用"构造时注册"模式:
cpp复制class DependencyManager {
public:
static DependencyManager& getInstance() {
static DependencyManager instance;
return instance;
}
void registerDependency(const std::string& name, void* dep) {
dependencies[name] = dep;
}
void* getDependency(const std::string& name) {
return dependencies[name];
}
private:
std::unordered_map<std::string, void*> dependencies;
};
class Database {
public:
Database() {
DependencyManager::getInstance().registerDependency("db", this);
}
};
4.2 单例与多线程安全
即使使用了magic static,某些操作仍需要额外的线程保护:
cpp复制class ThreadSafeSingleton {
public:
static ThreadSafeSingleton& getInstance() {
static ThreadSafeSingleton instance;
return instance;
}
void updateCounter() {
std::lock_guard<std::mutex> lock(mutex);
++counter;
}
private:
std::mutex mutex;
int counter = 0;
ThreadSafeSingleton() {}
};
4.3 单例与对象池模式
当需要管理一组资源而不是单个实例时,可以结合对象池模式:
cpp复制template<typename T>
class ObjectPool {
public:
static ObjectPool& getInstance() {
static ObjectPool instance;
return instance;
}
std::shared_ptr<T> acquire() {
std::lock_guard<std::mutex> lock(mutex);
if (pool.empty()) {
return std::shared_ptr<T>(new T(), [this](T* p) {
release(p);
});
}
auto ptr = pool.top();
pool.pop();
return std::shared_ptr<T>(ptr, [this](T* p) {
release(p);
});
}
private:
std::stack<T*> pool;
std::mutex mutex;
void release(T* obj) {
std::lock_guard<std::mutex> lock(mutex);
pool.push(obj);
}
};
5. 单例模式的替代方案
虽然单例模式在某些场景下很有用,但它并非唯一选择。以下是几种常见的替代方案:
5.1 依赖注入容器
现代C++项目越来越多地使用依赖注入容器来管理对象生命周期:
cpp复制class DIContainer {
public:
template<typename T, typename... Args>
void registerType(Args&&... args) {
factories[typeid(T).name()] = [=] {
return new T(std::forward<Args>(args)...);
};
}
template<typename T>
std::unique_ptr<T> resolve() {
auto it = factories.find(typeid(T).name());
if (it != factories.end()) {
return std::unique_ptr<T>(static_cast<T*>(it->second()));
}
return nullptr;
}
private:
std::unordered_map<std::string, std::function<void*()>> factories;
};
5.2 上下文对象模式
将全局状态封装在一个上下文对象中,显式传递:
cpp复制class AppContext {
public:
Database& getDatabase() { return *db; }
Logger& getLogger() { return *logger; }
void setDatabase(std::unique_ptr<Database> newDb) {
db = std::move(newDb);
}
void setLogger(std::unique_ptr<Logger> newLogger) {
logger = std::move(newLogger);
}
private:
std::unique_ptr<Database> db;
std::unique_ptr<Logger> logger;
};
5.3 服务定位器模式
服务定位器提供了更灵活的全局访问点:
cpp复制class ServiceLocator {
public:
static ServiceLocator& getInstance() {
static ServiceLocator instance;
return instance;
}
template<typename T>
void registerService(T* service) {
services[typeid(T).name()] = service;
}
template<typename T>
T* getService() {
auto it = services.find(typeid(T).name());
if (it != services.end()) {
return static_cast<T*>(it->second);
}
return nullptr;
}
private:
std::unordered_map<std::string, void*> services;
};
在实际项目中,我通常会根据具体需求选择最合适的模式。对于真正的全局唯一资源(如系统配置),单例模式仍然是一个不错的选择。但对于大多数其他情况,依赖注入或上下文对象模式通常能带来更好的可测试性和可维护性。