21. 构造函数的私有化与工厂模式:私有构造函数、静态工厂方法、单例模式的实现
这一章我们来聊一个很有意思的话题——构造函数私有化。说实话,我刚入行那会儿,觉得构造函数不就是用来创建对象的吗?私有化它干嘛?那不是自断手脚吗?后来在项目中踩过坑,才明白这背后藏着不少设计智慧。
为什么要把构造函数藏起来?
你想想看,如果所有人都能随便 new 你的类,那这个类的生命周期、创建方式、资源管理就完全失控了。我曾在维护一个老项目时遇到过这样的问题:一个日志类被到处 new,结果内存泄漏查了整整两天。嗯,从那以后,我对构造函数的访问控制就格外敏感。
构造函数私有化,说白了就是剥夺外部直接创建对象的权利。那谁来创建?类自己来管。这就引出了两个经典模式:工厂模式和单例模式。
核心思想:将对象的创建权收归类自身,由静态方法统一管理。这样你就能在创建过程中加入各种控制逻辑——比如对象复用、参数校验、资源预分配等。
私有构造函数的基本写法
先看一个最简单的例子。把构造函数放在 private 区,外部就 new 不了了:
class Logger {
private:
Logger() {} // 私有构造函数
std::string buffer_;
public:
void log(const std::string& msg) {
buffer_ += msg + "\n";
}
};
// Logger log; // 编译错误!无法访问私有构造函数
但这样完全不能创建对象也不行啊。所以我们需要一个静态工厂方法来充当“创建入口”。
静态工厂方法:更优雅的对象创建
静态工厂方法,说白了就是一个 static 成员函数,它负责创建并返回类的实例。相比直接 new,它有几个明显的好处:
- 有名字——比如
createWithDefaultConfig()比new Logger()语义清晰得多 - 可以控制实例数量——比如只创建一个(单例)
- 可以返回子类对象——这是多态的体现
- 可以缓存或复用对象——减少内存分配开销
来看一个实际例子。我在写一个数据库连接池时就用到了这个模式:
class DatabaseConnection {
private:
DatabaseConnection(const std::string& connStr)
: conn_str_(connStr), connected_(false) {
// 实际的连接初始化逻辑
}
std::string conn_str_;
bool connected_;
public:
// 静态工厂方法
static DatabaseConnection* create(const std::string& connStr) {
// 可以在这里做参数校验
if (connStr.empty()) {
return nullptr; // 返回空指针表示创建失败
}
auto* conn = new DatabaseConnection(connStr);
conn->connect(); // 自动完成连接
return conn;
}
void connect() {
// 模拟连接
connected_ = true;
std::cout << "已连接到: " << conn_str_ << std::endl;
}
void disconnect() {
connected_ = false;
}
~DatabaseConnection() {
disconnect();
}
};
// 使用
auto* db = DatabaseConnection::create("tcp://localhost:3306");
if (db) {
// 正常使用
}
delete db;
我个人习惯:静态工厂方法返回原始指针时,一定要在文档中说明谁负责释放。更推荐返回 std::unique_ptr 或 std::shared_ptr,这样所有权就清晰了。
单例模式:全局唯一的实例
单例模式是构造函数私有化最经典的应用场景。它的目标很明确:一个类在整个程序生命周期中只有一个实例。
我见过很多单例的实现,但最稳妥的 C++11 之后的方式是 Meyer's Singleton(局部静态变量):
class ConfigManager {
private:
ConfigManager() {
// 从配置文件加载设置
loadSettings();
}
// 禁止拷贝和赋值
ConfigManager(const ConfigManager&) = delete;
ConfigManager& operator=(const ConfigManager&) = delete;
void loadSettings() {
// 模拟加载配置
settings_["timeout"] = "30";
settings_["max_connections"] = "100";
}
std::unordered_map<std::string, std::string> settings_;
public:
// C++11 线程安全的单例
static ConfigManager& getInstance() {
static ConfigManager instance; // 局部静态变量
return instance;
}
std::string getSetting(const std::string& key) const {
auto it = settings_.find(key);
return it != settings_.end() ? it->second : "";
}
void setSetting(const std::string& key, const std::string& value) {
settings_[key] = value;
}
};
// 使用
auto& config = ConfigManager::getInstance();
std::cout << config.getSetting("timeout") << std::endl;
我曾经踩过的坑:在 C++11 之前,局部静态变量不是线程安全的。如果你用老标准,多个线程同时首次调用 getInstance() 可能导致重复创建。C++11 保证了初始化只发生一次,所以放心用。
三种单例实现对比
| 实现方式 | 线程安全 | 延迟加载 | 控制权 | 推荐度 |
|---|---|---|---|---|
| Meyer's Singleton(局部 static) | 是(C++11 起) | 是 | 自动 | ⭐⭐⭐⭐⭐ |
| 饿汉式(全局 static) | 是 | 否(程序启动即创建) | 自动 | ⭐⭐⭐⭐ |
| 双检锁(Double-Checked Locking) | 是(需 atomic) | 是 | 手动 | ⭐⭐⭐ |
我个人几乎只用 Meyer's Singleton。它简洁、安全、延迟加载,而且不需要你操心锁的问题。除非你需要在程序启动时就初始化(比如某些嵌入式场景),否则它就是最佳选择。
工厂模式:更灵活的对象创建
单例解决的是“唯一性”问题。但有时候我们需要更灵活的创建策略——比如根据配置创建不同的子类对象。这时候就该工厂模式登场了。
来看一个消息解析器的例子。不同的消息类型需要不同的解析器,但调用方不想关心具体是哪个类:
class MessageParser {
protected:
MessageParser() = default; // 保护构造函数,只能被子类调用
public:
virtual void parse(const std::string& data) = 0;
virtual ~MessageParser() = default;
// 工厂方法
static MessageParser* createParser(const std::string& type);
};
class JsonParser : public MessageParser {
public:
void parse(const std::string& data) override {
std::cout << "解析 JSON 数据: " << data << std::endl;
}
};
class XmlParser : public MessageParser {
public:
void parse(const std::string& data) override {
std::cout << "解析 XML 数据: " << data << std::endl;
}
};
// 工厂方法的实现
MessageParser* MessageParser::createParser(const std::string& type) {
if (type == "json") {
return new JsonParser();
} else if (type == "xml") {
return new XmlParser();
}
return nullptr; // 未知类型
}
// 使用
auto* parser = MessageParser::createParser("json");
if (parser) {
parser->parse("{ \"key\": \"value\" }");
delete parser;
}
注意:这里我把基类的构造函数设为 protected 而不是 private,因为子类需要调用它。如果设为 private,子类就无法构造了。这是很多新手容易搞混的地方。
知识体系总览
下面这张图帮你理清本章的核心逻辑:
实际项目中的选择建议
说了这么多,到底什么时候用哪种?我根据经验给你几个判断标准:
- 只需要一个实例? → 用单例模式,Meyer's Singleton 优先
- 需要根据条件创建不同子类? → 用工厂模式,静态工厂方法就够了
- 创建过程很复杂(参数多、需要缓存)? → 用工厂模式,把创建逻辑封装起来
- 既想控制创建,又想保留扩展性? → 把构造函数设为 protected,提供静态工厂方法
注意:单例模式虽然好用,但不要滥用。我见过一个项目里十几个单例互相依赖,启动顺序乱成一团。单例适合管理配置、日志、线程池这类全局资源,但不适合所有类。
好了,这一章的内容就到这里。构造函数私有化不是限制你的手脚,而是给你一把更精细的控制权。用好它,你的代码会变得更健壮、更易维护。
公众号:蓝海资料掘金营,微信deep3321