目录
一 设计一个类:只能在堆上创建对象
实现方式:
- 将类的构造函数私有,拷贝构造声明成私有。防止别人调用拷贝在栈上生成对象
- 提供一个静态的成员函数,在该静态成员函数中完成堆对象的创建
class HeapOnly
{
public:
static HeapOnly* GetObj()
{
return new HeapOnly;
}
private:
HeapOnly()
{}
//c++98,拷贝声明成私有
HeapOnly(const HeapOnly&);
public:
//c++11
//HeapOnly(const HeapOnly&) = delete;
};
如何调用:
int main()
{
//静态成员变量才需要在类外初始化
//此外静态成员函数的访问不需要对象,类名+访问限定符::
HeapOnly* p=HeapOnly::GetObj();
//HeapOnly copy(*p);//在栈上,所以要把拷贝函数要禁掉
std::shared_ptr<HeapOnly> sp1(HeapOnly::GetObj());
}
🍉静态变量与全局变量的区别
静态成员变量和静态成员函数属于所有类
- 全局变量:全局变量的作用域具有文件作用域,即在声明它们的文件内部可见。
- 静态变量:全局作用域的静态变量和全局变量一样具有文件作用域,但是全局的静态变量只能作用于定于它的文件,即使其他文件包含了其所在的头文件也不能使用,而全局变量可以在其他文件使用。局部作用域的静态变量(在mian函数内部声明的静态变量)作用域在花括号括起来的区域。
二 设计一个类:只能在栈上创建对象
实现方式:
- 将构造函数私有化,然后设计静态方法创建对象返回即可
- 不需要将拷贝函数私有化,因为从栈上创建的对象拷贝过后也是在栈上创建对象
//2.创建出对象只能在栈上
class StackOnly
{
public:
static StackOnly GetObj()
{
return StackOnly();
}
//第二种方法 new重载然后禁掉并且构造函数不私有 static StackOnly sso 在静态区,这是
//这种方法缺陷之一
//void* operator new(size_t size) = delete;
private:
//必须这样初始化
StackOnly()
{}
};
三 设计一个类:不能被拷贝
拷贝只会放生在两个场景中:拷贝构造函数以及赋值运算符重载,因此想要让一个类禁止拷贝,只需让该类 拷贝构造函数以及赋值运算符重载私有即可。
- C++98
class CopyBan
{
// ...
private:
CopyBan(const CopyBan&);
CopyBan& operator=(const CopyBan&);
//...
};
2. c++11
class CopyBan
{
// ...
CopyBan(const CopyBan&)=delete;
CopyBan& operator=(const CopyBan&)=delete;
//...
};
四 设计一个类:不能被继承
- c++98
class NonInherit
{
private:
NonInherit()
{}
};
C++98中构造函数私有化,派生类中调不到基类的构造函数。则无法继承
- c++11:final关键字,final修饰类,表示该类不能被继承
class A final
{
// ....
};
五 设计一个类: 只能创建一个对象(单例模式)
单例模式:一个类只能创建一个对象,即单例模式,该模式可以保证系统中该类只有一个实例,并提供一个访问它的全 局访问点,该实例被所有程序模块共享。使用设计模式的目的:为了代码可重用性、让代码更容易被他人理解、保证代码可靠性.
class Singleton
{
public:
//如果这个函数要不是静态的,就需要对象调用,我们以及
//把构造函数给禁掉了
static Singleton* GetInstance()
{
if (_pinst == nullptr)
{
_pinst = new Singleton;
}
return _pinst;
}
private:
//构造函数私有化,这样就不能随意创建对象
Singleton()
{}
//拷贝构造禁掉
Singleton(const Singleton& s) = delete;
static Singleton* _pinst;
};
//静态成员变量类似全局变量要在类外初始化
Singleton* Singleton::_pinst = nullptr;
int main()
{
//拷贝构造又构造了一个新的对象,所以单例模式要把拷贝禁掉
//Singleton copy(*Singleton::GetInstance());
}
🍎但是上述代码存在着线程安全问题,即线程都走到 _pinst=new Singleton 处其中一个线程被cpu切走,一个线程创建类后返回之后,cpu又把之前切走的线程切换回来,并加载上下文,此时线程会继续 new Singleton,这样就创建了两个对象,比符合单例模式,因为我们需要进行加锁。
class Singleton
{
public:
//如果这个函数要不是静态的,就需要对象调用,我们以及
//把构造函数给禁掉了
static Singleton* GetInstance()
{
_mtx.lock();
if (_pinst == nullptr)
{
_pinst = new Singleton;
}
_mtx.unlock();
return _pinst;
}
//拷贝构造禁掉
Singleton(const Singleton& s) = delete;
private:
//构造函数私有化,这样就不能随意创建对象
Singleton()
{}
static Singleton* _pinst;
static mutex _mtx;//静态成员函数只能访问静态成员变量因为没有this指针,所以mutex必须静态
};
//静态成员变量类似全局变量要在类外初始化
Singleton* Singleton::_pinst = nullptr;
mutex Singleton::_mtx;
再次优化:
static Singleton* GetInstance()
{
if (_pinst == nullptr)
{
_mtx.lock();
//shared_ptr<mutex> lock(&_mtx);,出了if作用域会调用析构函数进行释放资源
if (_pinst == nullptr)
{
//加锁保护的是_pinst=nullptr这段,当创建成功对象的时候,线程不会进入
//if(_pinst==nullptr)这段代码,而我们每次上锁解锁造成了资源浪费
//所在我们在外面加入了又加入if语句
_pinst = new Singleton;
}
_mtx.unlock();
}
return _pinst;
}
为了避免造成内存泄漏,我们写一个手动释放资源的函数
static void DelInstance()
{
unique_lock<mutex> lock(_mtx);
delete _pinst;
_pinst = nullptr;
}
单例模式有两种实现模式:
- 饿汉模式: 一开始(main函数之前)就创建对象
- 懒汉模式:第一次获取对象时,再创建对象
🎂上述代码我们是实现了懒汉模式的单例模式,为什么说上述实现的单例模式是懒汉模式呢?
我们从创建对象代码进行分析
static Singleton* GetInstance()
{
if (_pinst == nullptr)
{
_mtx.lock();
if (_pinst == nullptr)
{
_pinst = new Singleton;
}
_mtx.unlock();
}
return _pinst;
}
当我们在main()内调用GetInstance()获取对象的时候,刚开始_pinst==nullptr,所以会在我们获取对象的时候创建。而饿汉模式一开始(main函数之前)就创建对象,接下里我们分析一下饿汉模式
饿汉模式
namespace hungry_man
{
//1.懒汉模式 一开始(main函数之前)就创建对象
class Singleton
{
public:
static Singleton* GetInstance()
{
return &_inst;
}
Singleton(const Singleton& s) = delete;
private:
static Singleton _inst;//饿汉模式静态成员变量不是指针,可以看出是类
//静态成员变量可以看出全局变量,在main之前就以及创建好类了,所以不存在线程安全问题
Singleton()
{}
};
Singleton Singleton::_inst;
void x()
{
cout << Singleton::GetInstance() << endl;
cout << Singleton::GetInstance() << endl;
cout << Singleton::GetInstance() << endl;
}
}
int main()
{
hungry_man::x();
}
🎉:饿汉模式与懒汉模式结构上不同在一,饿汉模式的私有成员变量是 Singleton Singleton::_inst,不是指针,饿汉模式在类外对 _inst进行初始化的时候,就相当于已经在main()之前创建好对象了,当我们在main()调用x()函数的时候
即cout << Singleton::GetInstance() << endl;所获取的对象是早就在main函数之前已经创建好的,且由于在主线程之前就已经创建好,所以不存在线程安全问题,不需要加锁。
从运行结果来看我们可以得到即使我们调用了三次获取对象函数,得到的都是一个类对象。
六 饿汉和懒汉模式的对比
- 懒汉模式需要考虑线程安全问题和释放问题,实现相对于来说较为复杂,饿汉模式不存在以上问题实现简单。
- 懒汉模式是一种懒加载模式需要的时候再初始化创建对象,不会影响程序的启动,饿汉模式则相反,程序启动阶段就创建初始化对象,会导致程序启动慢。
- 如果有多个单例类,需要B依赖A,要求A单例创建初始化之后,B才能创建初始化,那么B就不能是饿汉模式,无法保证顺序。