文章目录
- [1. 设计一个类,不能被拷贝](#1. 设计一个类,不能被拷贝)
- [2. 设计一个类,只能在堆上创建对象](#2. 设计一个类,只能在堆上创建对象)
- [3. 设计一个类,只能在栈上创建对象](#3. 设计一个类,只能在栈上创建对象)
- [4. 设计一个类,不能被继承](#4. 设计一个类,不能被继承)
-
- [4.1 方法一](#4.1 方法一)
- [4.2 方法二](#4.2 方法二)
- [4.3 额外补充](#4.3 额外补充)
- [5. 设计模式](#5. 设计模式)
- [6. 设计一个类,只能创建一个对象(单例模式)](#6. 设计一个类,只能创建一个对象(单例模式))
-
- [6.1 饿汉模式](#6.1 饿汉模式)
- [6.2 懒汉模式](#6.2 懒汉模式)
- [6.3 懒汉模式的线程安全问题](#6.3 懒汉模式的线程安全问题)
- [7. 最简单的懒汉模式实现](#7. 最简单的懒汉模式实现)
1. 设计一个类,不能被拷贝
拷贝只会发生在两个场景中:拷贝构造函数以及赋值运算符重载,因此想要让一个类禁止拷贝,只需让该类不能调用拷贝构造函数以及赋值运算符重载即可。
C++98:将拷贝构造函数与赋值运算符重载只声明不定义,并且将其访问权限设置为私有即可。
cpp
class CopyBan
{
// ...
private:
CopyBan(const CopyBan&);
CopyBan& operator=(const CopyBan&);
//...
};
原因:
- 设置成私有:如果只声明没有设置成 private,用户自己如果在类外定义了,就可以不能禁止拷贝了。
- 只声明不定义:不定义是因为该函数根本不会调用,定义了其实也没有什么意义,不写反而还简单,而且如果定义了就不会防止成员函数内部拷贝了。
C++11 :扩展 delete 的用法,delete 除了释放 new 申请的资源外,如果在默认成员函数后跟上 =delete,表示让编译器删除掉该默认成员函数。
cpp
class CopyBan
{
CopyBan(const CopyBan&) = delete;
CopyBan& operator=(const CopyBan&) = delete;
// ...
};
2. 设计一个类,只能在堆上创建对象
正常来说,创建对象的方式如下所示:
cpp
class HeapOnly
{
public:
};
int main()
{
HeapOnly hp1; // 栈上申请
static HeapOnly hp2; // 静态区
HeapOnly* hp3 = new HeapOnly; // new出来的
return 0;
}
但是我如果只想在堆上申请呢?
实现方式:
- 将类的构造函数私有,拷贝构造声明成私有。防止别人调用拷贝在栈上生成对象。
- 提供一个静态的成员函数,在该静态成员函数中完成堆对象的创建。
C++98 写法代码如下所示:
cpp
class HeapOnly
{
public:
static HeapOnly* CreateObject()
{
cout << "static HeapOnly* CreateObject()" << endl;
return new HeapOnly;
}
private:
// 构造函数私有
HeapOnly() {}
// C++98
// 1.只声明, 不实现。因为实现可能会很麻烦,而你本身不需要。
// 2.声明成私有
HeapOnly(const HeapOnly&); // 拷贝构造私有
};
int main()
{
HeapOnly* hp3 = HeapOnly::CreateObject();
return 0;
}
C++11 写法代码如下所示:
cpp
class HeapOnly
{
public:
static HeapOnly* CreateObject()
{
cout << "static HeapOnly* CreateObject()" << endl;
return new HeapOnly;
}
private:
// 构造函数私有
HeapOnly() {}
// C++11
HeapOnly(const HeapOnly&) = delete;
};
int main()
{
HeapOnly* hp3 = HeapOnly::CreateObject();
return 0;
}
这里比较推荐 C++11 这种写法。
3. 设计一个类,只能在栈上创建对象
构造函数私有化,然后设计静态方法创建对象返回即可。
cpp
class StackOnly
{
public:
static StackOnly CreateObj()
{
// 这里和堆不同的是:需要返回一个对象
StackOnly st;
return st;
// 写法等价于下面
//return StackOnly();
}
// 对一个类实现专属的【operator new】
// 禁掉【operator new】以后,可以把下面用 【new + 调用拷贝构造】申请对象给禁掉
void* operator new(size_t size) = delete;
void operator delete(void* p) = delete;
private:
// 构造函数私有
StackOnly()
:_a(0)
{}
private:
int _a;
};
int main()
{
StackOnly st1 = StackOnly::CreateObj();
StackOnly copy(st1);
// new = operator new + 构造
StackOnly* st2 = new StackOnly(st1); // 这里会编译报错
return 0;
}
但这里有一个关键问题需要点破:你的类并没有真正 "完全禁止堆上创建对象"。
问题出在哪?虽然写了:
cpp
void* operator new(size_t size) = delete;
void operator delete(void* p) = delete;
这确实可以禁止:
cpp
new StackOnly;
但是在 main 函数里写的这一句:
cpp
StackOnly* st2 = new StackOnly(st1);
这行代码仍然是非法的(编译报错),因为:new StackOnly(st1) 本质还是调用了:
operator new + 构造函数
而你已经把 operator new 禁掉了,所以这里 根本不会绕过限制,而是直接编译失败。
运行结果如下所示:

4. 设计一个类,不能被继承
4.1 方法一
C++98 中构造函数私有化,那么派生类中就调不到基类的构造函数,进而无法继承。
代码如下所示:
cpp
class NonInherit
{
public:
static NonInherit GetInstance()
{
return NonInherit();
}
private:
NonInherit()
{}
};
原理是:子类构造时,必须调用父类的构造函数,但是父类的 NonInherit() 是 private 的,故子类访问不到。
运行结果如下所示:

4.2 方法二
第二种:在 C++11 中有一个 final 关键字,被 final 修饰的类,表示该类不能被继承。
cpp
class A final
{
// ....
};
原理:final 是语言级限制,编译器直接禁止。
运行结果如下:

4.3 额外补充
下面这段代码本质上是一个 静态工厂函数(static factory function),作用是在类内部创建对象,并把对象返回出去。
cpp
static NonInherit GetInstance()
{
return NonInherit();
}
static 表示这是一个 类函数(静态成员函数),特点是:
- 不需要对象就能调用
- 用类名直接访问
访问方式如下:
cpp
NonInherit obj = NonInherit::GetInstance();
那么 NonInherit GetInstance() 是什么意思?其实说的是:返回值是 NonInherit 对象(按值返回)
也就是说:
cpp
return NonInherit();
会创建一个临时对象,然后返回给调用者
为什么要这样写?因为把构造函数设成了 private 私有:
cpp
private:
NonInherit() {}
那么在外部就不能像下面这样写:
cpp
NonInherit obj; // ❌ 不允许
解决办法就是让类 自己创建对象,然后交给你使用:
cpp
NonInherit obj = NonInherit::GetInstance(); // ✅
5. 设计模式
设计模式(Design Pattern)是一套被反复使用、多数人知晓的、经过分类的、代码设计经验的总结。
为什么会产生设计模式这样的东西呢?就像人类历史发展会产生兵法。最开始部落之间打仗时都是人拼人的对砍。后来春秋战国时期,七国之间经常打仗,就发现打仗也是有套路的,后来孙子就总结出了《孙子兵法》。孙子兵法也是类似。
使用设计模式的目的:为了代码可重用性、让代码更容易被他人理解、保证代码可靠性。设计模式使代码编写真正工程化,它是软件工程的基石脉络,如同大厦的结构一样。
6. 设计一个类,只能创建一个对象(单例模式)
一个类只能创建一个对象,即单例模式。该模式可以保证系统中该类只有一个实例,并提供一个访问它的全局访问点,该实例被所有程序模块共享。
比如在某个服务器程序中,该服务器的配置信息存放在一个文件中,这些配置数据由一个单例对象统一读取,然后服务进程中的其他对象再通过这个单例对象获取这些配置信息,这种方式简化了在复杂环境下的配置管理。
单例模式有两种实现模式:
- 饿汉模式:就是说不管你将来用不用,程序启动时就创建一个唯一的实例对象。
- 懒汉模式:就是说只有在第一次使用时,才创建这个唯一的实例对象(按需创建)。
6.1 饿汉模式
比如,我现在有一份代码,它可以创建多个对象
cpp
class Singleton
{
private:
map<string, string> dict;
};
int main()
{
Singleton s1;
Singleton s2;
return 0;
}
运行结果如下:可以看到 s1 和 s2 的地址是不一样的

但是我现在只想让它创建一个对象
cpp
class Singleton
{
public:
// 第三步:提供一个获取单例的接口
static Singleton* GetInstance()
{
return &m_instance;
}
private:
// 第一步:构造函数私有
Singleton() {};
// 第二步:C++98写法:防拷贝
//Singleton(Singleton const&);
//Singleton& operator=(Singleton const&);
// or
// 第二步:C++11写法:防拷贝
Singleton(Singleton const&) = delete;
Singleton& operator=(Singleton const&) = delete;
private:
map<string, string> _dict;
// 1.1 必须在类里面进行声明
static Singleton m_instance;
};
// 1.2 然后在类外面定义
Singleton Singleton::m_instance; // 在程序入口之前就完成单例对象的初始化
int main()
{
Singleton* s1 = Singleton::GetInstance();
Singleton* s2 = Singleton::GetInstance();
cout << "s1: " << &s1 << endl;
cout << "s2: " << &s2 << endl;
return 0;
}
运行结果如下:可以看到 s1 和 s2 的地址是相同的

饿汉模式是在程序启动阶段就已经创建好了唯一的实例对象,也就是说,在进入 main 函数之前,m_instance 就已经初始化完成了。
现在我们还可以添加其他成员函数
cpp
// 饿汉模式
class Singleton
{
public:
// 第三步:提供一个获取单例的接口
static Singleton* GetInstance()
{
return &m_instance;
}
public:
void Add(const pair<string, string>& kv)
{
_dict[kv.first] = kv.second;
}
void Print()
{
for (const auto& e : _dict)
{
cout << e.first << ":" << e.second << endl;
}
cout << endl;
}
private:
// 第一步:构造函数私有
Singleton() {};
// 第二步:C++11写法:防拷贝
Singleton(Singleton const&) = delete;
Singleton& operator=(Singleton const&) = delete;
private:
map<string, string> _dict;
// 1.1 必须在类里面进行声明
static Singleton m_instance;
};
// 1.2 然后在类外面定义
Singleton Singleton::m_instance; // 在程序入口之前就完成单例对象的初始化
然后在主函数里面定义调用即可
cpp
int main()
{
// 定义
Singleton::GetInstance()->Add({ "xxx", "111" });
Singleton::GetInstance()->Add({ "yyy", "222" });
Singleton::GetInstance()->Add({ "zzz", "333" });
Singleton::GetInstance()->Add({ "abc", "123" });
// 打印
Singleton::GetInstance()->Print();
return 0;
}
运行结果如下:

饿汉模式的优缺点如下:
- 优点:实现简单,而且天然线程安全,因为初始化在程序启动阶段完成。
- 缺点:首先如果单例对象初始化内容很多,那么会影响启动速度。其次,如果有两个单例类,并且互相有依赖关系,那么实例启动的顺序就不确定。(比如 A、B 两个单例类,要求 A 先创建,B 再创建,B 的初始化创建会依赖 A)
那么饿汉模式适用于,如果这个单例对象在多线程高并发环境下频繁使用,性能要求较高,那么显然使用饿汉模式来避免资源竞争,提高响应速度更好。
6.2 懒汉模式
如果单例对象构造十分耗时或者占用很多资源,比如加载插件、初始化网络连接、读取文件等等,而有可能该对象程序运行时不会用到,那么也要在程序一开始就进行初始化,就会导致程序启动时非常的缓慢。所以这种情况使用懒汉模式(延迟加载)更好。
懒汉模式就是说不在程序启动时创建对象,而是在第一次使用时才创建。
cpp
// 懒汉模式
class Singleton
{
public:
// 提供获取单例的接口
static Singleton* GetInstance()
{
if (m_instance == nullptr)
{
m_instance = new Singleton;
}
return m_instance;
}
private:
// 构造函数私有
Singleton() {};
// 防拷贝
Singleton(Singleton const&) = delete;
Singleton& operator=(Singleton const&) = delete;
private:
map<string, string> _dict;
// 指针形式的单例
static Singleton* m_instance;
};
// 类外初始化
Singleton* Singleton::m_instance = nullptr;
懒汉模式是在第一次调用 GetInstance 时才创建对象,如果一直不调用,就不会创建对象。它的优点是按需创建,不会浪费资源;缺点是线程不安全,在多线程环境下可能会创建多个对象,需要加锁处理。
另外,一般单例不需要释放,但是还是会有一些特殊场景,比如:
- 1、需要显示释放。
- 2、程序结束时,需要做一些特殊动作(持久化)
如下所示:
cpp
public:
// 释放接口
static void DelInstance()
{
if (m_instance)
{
delete m_instance;
m_instance = nullptr;
}
}
private:
// 析构函数私有
~Singleton()
{
cout << "~Singleton()" << endl;
}
假设我现在单例模式创建出来的对象已经有值了,但是我想要手动的去销毁,然后重新创建。
cpp
int main()
{
// 定义
Singleton::GetInstance()->Add({ "xxx", "111" });
Singleton::GetInstance()->Add({ "yyy", "222" });
Singleton::GetInstance()->Add({ "zzz", "333" });
Singleton::GetInstance()->Add({ "abc", "123" });
// 打印
Singleton::GetInstance()->Print();
// 手动销毁
Singleton::DelInstance();
// 重新定义
Singleton::GetInstance()->Add({ "apple", "苹果" });
Singleton::GetInstance()->Print();
return 0;
}
运行结果如下:

除此之外,我们还可以实现一个内嵌垃圾回收类(它是一个内部类),如下所示:
cpp
// 懒汉模式
class Singleton
{
public:
// 实现一个内嵌垃圾回收类
class GC
{
public:
~GC()
{
Singleton::DelInstance();
}
};
static GC _gc;
};
// 类外初始化
Singleton::GC Singleton::_gc;
那么我们可以把原始数据先保存到文件中去,然后再添加新的文件
cpp
int main()
{
// 定义
Singleton::GetInstance()->Add({ "xxx", "111" });
Singleton::GetInstance()->Add({ "yyy", "222" });
Singleton::GetInstance()->Add({ "zzz", "333" });
Singleton::GetInstance()->Add({ "abc", "123" });
// 打印
Singleton::GetInstance()->Print();
// 重新定义
Singleton::GetInstance()->Add({ "www", "000" });
Singleton::GetInstance()->Print();
return 0;
}
运行结果如下所示:

此时我们打开文件可以看到,之前定义的内容已经被写进去了,做了持久化处理

6.3 懒汉模式的线程安全问题
饿汉模式:程序一启动就创建好单例对象(main 函数之前),无需考虑多线程并发,线程安全天然保证。
而懒汉模式:第一次调用 GetInstance() 时才创建对象,如果两个线程同时调用,那么 GetInstance() 函数里面的【判断是否存在 + 创建对象】不是原子操作,可能同时创建多个实例,破坏单例唯一性,从而存在线程安全问题。
代码如下所示:
cpp
#include <iostream>
#include <map>
#include <thread>
using namespace std;
// 懒汉模式单例
class Singleton
{
public:
// 提供获取单例的接口(线程不安全版)
static Singleton* GetInstance()
{
if (m_instance == nullptr) // 判断+创建不是原子操作
{
m_instance = new Singleton;
}
return m_instance;
}
void PrintAddress()
{
cout << "Singleton instance address: " << this << endl;
}
private:
Singleton() {} // 构造函数私有
Singleton(const Singleton&) = delete;
Singleton& operator=(const Singleton&) = delete;
private:
map<string, string> _dict; // 示例资源
static Singleton* m_instance;
};
// 类外初始化
Singleton* Singleton::m_instance = nullptr;
int main()
{
// 创建两个线程,同时调用 GetInstance()
thread t1([]() {
Singleton* s1 = Singleton::GetInstance();
s1->PrintAddress();
});
thread t2([]() {
Singleton* s2 = Singleton::GetInstance();
s2->PrintAddress();
});
t1.join();
t2.join();
return 0;
}
其中,if (m_instance == nullptr) 与 new Singleton 不是原子操作,那么两个线程可能同时通过判断,导致创建两个对象。
运行程序多次,会发现打印的地址可能不同,说明出现了线程安全问题。
那么我们可以通过加锁来解决这个问题,代码如下所示:
cpp
// 懒汉模式单例(双检锁)
class Singleton
{
public:
// 提供获取单例的接口(线程安全版)
static Singleton* GetInstance()
{
// 双检查
if (m_instance == nullptr)
{
unique_lock<mutex> lock(_mtx);
if (m_instance == nullptr)
{
m_instance = new Singleton;
}
}
return m_instance; // 修正:保证始终返回实例
}
void PrintAddress()
{
cout << "Singleton instance address: " << this << endl;
}
private:
Singleton() {} // 构造函数私有
Singleton(const Singleton&) = delete;
Singleton& operator=(const Singleton&) = delete;
private:
map<string, string> _dict; // 示例资源
static Singleton* m_instance;
static mutex _mtx; // 锁对象
};
// 类外初始化
Singleton* Singleton::m_instance = nullptr;
mutex Singleton::_mtx;
int main()
{
// 启动多个线程同时获取单例
thread t1([]() { Singleton::GetInstance()->PrintAddress(); });
thread t2([]() { Singleton::GetInstance()->PrintAddress(); });
thread t3([]() { Singleton::GetInstance()->PrintAddress(); });
t1.join();
t2.join();
t3.join();
return 0;
}
运行结果如下:

这里使用了双检锁:
- 外层 if 避免每次都加锁,提高效率。
- 内层加锁保证第一次创建对象时只有一个线程能执行。
总结一句话
懒汉模式线程安全问题的本质是:判断和创建不是原子操作,多个线程可能同时创建实例。
7. 最简单的懒汉模式实现
下面是一个最简单的懒汉模式实现。
cpp
// 懒汉模式单例
class Singleton
{
public:
// 提供获取单例的接口
static Singleton& GetInstance()
{
// 局部的静态对象会在第一次调用时初始化
static Singleton inst;
return inst;
}
private:
Singleton() {} // 构造函数私有
Singleton(const Singleton&) = delete;
Singleton& operator=(const Singleton&) = delete;
};
那么这份代码是不是线程安全的呢?
在 C++11 之前它不是安全的,但是,在 C++11 之后它可以保证局部静态对象的初始化是线程安全的,即只初始化一次。
- 以前:局部静态变量初始化 不是线程安全的,需要手动加锁。
- 以后:局部静态变量初始化 线程安全,编译器保证只初始化一次。