一、设计一个不被拷贝的类
c++98:只声明拷贝构造和赋值重载,定义私有。
c++11:拷贝构造和赋值重载 = delete 禁止调用
二、只能在堆上创建对象
1、思路
(1)构造函数私有(禁止随意创建对象)拷贝构造和赋值重载 = delete(创建的对象是在栈上)
(2)定义静态公有函数new对象(因为一开始没有对象创建,无法调用成员函数,所以定义成静态函数,用类域调用)
2、代码实现
class HeapOnly
{
public:
static HeapOnly* GetObj()
{
return new HeapOnly;
}
private:
HeapOnly() {}
HeapOnly(const HeapOnly&) = delete;
HeapOnly& operator=(const HeapOnly&) = delete;
}
三、只能在栈上创建对象
1、思路
(1)构造函数私有(禁止随便new对象)
(2)定义静态公有函数返回构造函数创建的对象(因为一开始没有对象创建,无法调用成员函数,所以定义成静态函数,用类域调用)
(3)operator new 和 operator delete = delete(禁止new对象)
2、代码实现
class StackOnly
{
public:
StackOnly GetObj()
{
return StackOnly();
}
void* operator new(sizt_t size) = delete;
void operator delete(void* p) = delete;
private:
StackOnly() {}
}
四、设计一个不能被继承的类
c++98:构造函数私有化
c++11:类后面加 final
五、单例模式(重点)
1、单例模式介绍
目的:全局只有一个对象。
原则:所以可以创建静态对象实现,不能在 main 函数里面创建,不能在成员函数里面创建,只能在全局创建。
实现方式:饿汉模式、懒汉模式
2、饿汉模式介绍
(1)理论
在 main 函数之前就创建了一个静态对象。
(2)思路
限制对象个数,构造函数私有,拷贝构造和赋值重载 = delete
类内只声明静态对象,此对象只属于类,不属于任何对象,所以类外定义静态对象
定义静态公有函数返回对象
(3)代码实现
class ConfigInfo
{
public:
static ConfigInfo* GetInstance()
{
return &_sInfo;
}
private:
ConfigInfo() {}
ConfigInfo(const ConfigInfo&) = delete;
ConfigInfo& operator=(const ConfigInfo&) = delete;
private:
// 声明
static ConfigInfo _sInfo;
};
// 定义
ConfigInfo ConfigInfo::_sInfo;
(4)饿汉模式问题
a、饿汉模式不管有没有用到该对象在 main 函数开始前都已经创建好了,如果很多单例模式是饿汉模式,对象初始化资源又很多,势必导致迟迟不会进入 main 影响效率。
b、两个单例类如果有依赖关系,饿汉模式无法解决。
(5)饿汉模式创建对象过程
main() 函数前也会有代码要执行。编译期的时候,给 _sInfo 分配内存,运行期的时候,在main() 函数前就运行了一些指令,初始化静态对象 _sInfo ,执行了 _sInfo 的构造函数(因为类外初始化的代码,初始化就是在构造对象且赋值),所以到执行 GetInstance() 获取该实例前,对象就已经构造好了,这个就是饿汉式单例。
因为在main() 函数前就已经有这个实例了,那肯定也是多线程安全的,因为这个时候也没创建多个线程,并不会重复构造实例。
3、懒汉模式介绍
(1)理论
在第一次调用创建函数时创建对象,之后已经存在对象就不会创建。
(2)思路
限制对象个数,构造函数私有,拷贝构造和赋值重载 = delete
c++11:直接在创建函数中创建静态局部对象(之后重点讲),不在其他任何地方进行声明定义。
c++98:类内定义对象指针,类外初始化成 nullptr ,创建函数中再进行赋值。
定义静态公有函数返回对象。
(3)代码实现(c++11 静态局部对象)
class ConfigInfo
{
public:
static ConfigInfo* GetInstance()
{
static ConfigInfo info;
return &info;
}
private:
ConfigInfo() {}
ConfigInfo(const ConfigInfo&) = delete;
ConfigInfo& operator=(const ConfigInfo&) = delete;
};
(4)代码实现(c++98 对象指针)
class ConfigInfo
{
public:
static ConfigInfo* GetInstance()
{
// 双检查加锁
// t1 t2
if (_spInfo == nullptr) // 性能
{
unique_lock<mutex> lock(_mtx);
if (_spInfo == nullptr) // 线程安全
{
_spInfo = new ConfigInfo;
}
}
return _spInfo;
}
private:
ConfigInfo() {}
ConfigInfo(const ConfigInfo&) = delete;
ConfigInfo& operator=(const ConfigInfo&) = delete;
private:
static ConfigInfo* _spInfo;
static mutex _mtx;
};
ConfigInfo* ConfigInfo::_spInfo = nullptr;
mutex ConfigInfo::_mtx;
除去考虑多线程安全加锁的步骤,和饿汉模式很类似,只不过在类外定义时不是有效的,只有第一次调用 GetInstance() 函数才是真正创建了对象。
(5)多线程时的安全问题
c++11
由于静态局部变量在初始化时,在汇编指令上已经自动添加线程互斥指令了。所以如果线程A调用该函数,在初始化未完成之前,线程B不会执行该初始化操作。线程A完成初始化后,已经初始化过的变量,其它线程不会再重复进行初始化操作,从而只有一个实例对象产生。
c++98
static ConfigInfo* GetInstance()
{
if (_spInfo == nullptr)
{
_spInfo = new ConfigInfo;
}
return _spInfo;
}
一开始没有锁,在多线程情况下是不安全的,多线程进入会导致多次创建对象。
所以该函数不是可重入函数(能够被多个线程同时调用的函数,并且能保证函数结果正确,不必担心数据错误的函数)
static ConfigInfo* GetInstance()
{
unique_lock<mutex> lock(_mtx);
if (_spInfo == nullptr) // 线程安全
{
_spInfo = new ConfigInfo;
}
return _spInfo;
}
这次我们加上锁已经解决了多线程安全问题,但是会发现只有第一次创建对象时要保证线程安全,其他时候由于对象只会有一个即使函数被重入依然只有一个对象。
而上面的代码每一次都会把进入函数的线程堵在锁外面等解锁,即使对象已经被创建了,进入锁也没有意义,这样效率会有损耗。
static ConfigInfo* GetInstance()
{
if (_spInfo == nullptr) // 性能
{
unique_lock<mutex> lock(_mtx);
if (_spInfo == nullptr) // 线程安全
{
_spInfo = new ConfigInfo;
}
}
return _spInfo;
}
锁 + 双重判断就可以解决上面所有问题。
(6)深入了解静态局部对象
静态局部变量也是在全局区,和静态全局变量,全局变量一样,是在编译器就被分配了内存。
但是静态局部变量的初始化是运行到该语句时,进行初始化。
c语言是编译时分配内存和初始化的。
c++是编译时分配内存,运行时首次使用时初始化。
主要是由于c++引入对象后,要进行初始化必须执行相应构造函数和析构函数,在构造函数或析构函数中经常会需要进行某些程序中需要进行的特定操作,并非简单地分配内存。所以c++标准定为全局或静态对象是有首次用到时才会进行构造。
实际上对于这种自定义类型来说,无论全局静态变量还是局部静态变量,都是执行动态初始化,也就是都得在代码真正执行时,要调用了其构造函数才能初始化(C没有对象的概念,所以C都是编译期初始化)
简单点就是静态对象在编译时分配内存且值赋为0或null,运行时构造对象并赋值
也就是实际上分两个阶:第一是编译时的零初始化(分配内存),第二是运行时的动态初始化
其实只要知道静态和动态就好了。静态就是不需要运行程序,在运行前就能放好。动态初始化就得执行程序。
所以c++11懒汉模式原理:局部静态变量只会初始化一次的特性 + 汇编指令自动添加的锁