请设计一个类,只能在堆上创建对象
我们分析,当一个类只允许在堆上创建对象,原本,正常创建对象一定会调用构造函数,或者拷贝构造,要使用构造函数或者拷贝构造去创建对象(别人调用拷贝构造会在栈上生成对象),是不能保证只在堆上创建的,所以我们需要将构造函数与拷贝构造声明私有,因为无法通过构造函数与拷贝构造创建,所以我们需要引入一个静态成员函数,在此函数中创建对象
实现方式: 1. 将类的构造函数私有,拷贝构造声明成私有。防止别人调用拷贝在栈上生成对象。 2. 提供一个静态的成员函数,在该静态成员函数中完成堆对象的创建
class HeapOnly
{
public:
static HeapOnly* GetObj()//设置个静态成员函数创建对象
{
return new HeapOnly;
}
private:
HeapOnly()
{}
// C++98防拷贝:声明成私有
//HeapOnly(const HeapOnly& );
public:
// C++11 : 声明成delete
HeapOnly(const HeapOnly&) = delete;
};
int x7()
{
//HeapOnly hp;
//HeapOnly* p = new HeapOnly;
//HeapOnly* p = HeapOnly::GetObj();
std::shared_ptr<HeapOnly> sp1(HeapOnly::GetObj());
std::shared_ptr<HeapOnly> sp2(HeapOnly::GetObj());
//HeapOnly copy(*sp1);//调用拷贝构造构造函数
system("pause");
return 0;
}
请设计一个类,只能在栈上创建对象
同样的,当我们了解了只能在堆上创建对象的方式之后,我们可以仿照这种方式,创建一个仅能在栈上创建对象的类
方法一:同上将构造函数私有化,然后设计静态方法创建对象返回即可。
class StackOnly
{
public:
static StackOnly GetObj()
{
return StackOnly();
}
private:
StackOnly()
{}
};
这样操作,就可以保证对象通过调用函数创建在栈上了
还有一种方式,就是直接禁掉new 函数,就不会在堆上创建对象了,但是这也有一个问题,就是无法避免在静态区创建的对象
// 这种方案存在一定程序缺陷,无法阻止在数据段(静态区)创建对象
class StackOnly
{
public:
void* operator new(size_t size) = delete;
};
int x8()
{
StackOnly so;
//StackOnly* p = new StackOnly;
static StackOnly sso;
return 0;
}
请设计一个类,不能被拷贝
拷贝只会放生在两个场景中:拷贝构造函数以及赋值运算符重载,因此 想要让一个类禁止拷贝,只需让该类 不能调用拷贝构造函数以及赋值运算符重载即可 。
这其实就是将拷贝构造私有就可以了,别人也就无法进行拷贝了
C++98 将拷贝构造函数与赋值运算符重载只声明不定义,并且将其访问权限设置为私有即可
class CopyBan
{
// ...
private:
CopyBan(const CopyBan&);
CopyBan& operator=(const CopyBan&);
//...
};
原因: 1. 设置成私有:如果只声明没有设置成 private ,用户自己如果在类外定义了,就可以不能禁止拷贝了 2. 只声明不定义:不定义是因为该函数根本不会调用,定义了其实也没有什么意义,不写反而还简单,而且如果定义了就不会防止成员函数内部拷贝了。
C++11 扩展 delete 的用法, delete 除了释放 new 申请的资源外,如果在默认成员函数后跟上 =delete ,表示让编译器删除掉该默认成员函数。
class CopyBan
{
// ...
CopyBan(const CopyBan&)=delete;
CopyBan& operator=(const CopyBan&)=delete;
//...
};
请设计一个类,不能被继承
这个其实就是将构造函数私有,而子类若要想继承就必须要调父类的构造函数,所以无法继承
C++98 方式
class NonInherit
{
public:
static NonInherit GetInstance()
{
return NonInherit();
}
private:
NonInherit()
{}
};
C++11 方法 fifinal 关键字, fifinal 修饰类,表示该类不能被继承
class A final
{
// ....
};
单例模式
其实我们之前已经学过一些设计模式
迭代器模式--基于面向对象的三大特性之一,封装设计出来的,用一个迭代器封装以后,不暴露容器结构的情况下,统一的方式访问修改容器中的数据
适配器模式 -- 体现的是一种复用
还有一些常见的设计模式如:工厂模式、装饰器模式、观察者模式、单例模式...
一个类只能在全局(进程中)只有一个实例对象,就是单例模式
什么场景下使用?比如一个进程中有一个内存池,进程中的多线程需要内存都要到这个内存池中取,那么这个内存池的类就可以设计单例模式。
我们先来演示一个最简单的单例模式
class Singleton
{
public:
static Singleton* GetInstance()
{
if (_pinst == nullptr)
{
_pinst = new Singleton;
}
return _pinst;
}
Singleton(const Singleton& s) = delete;
private:
Singleton()
{}
static Singleton* _pinst;
};
Singleton* Singleton::_pinst = nullptr;
我们可以看到,我们首先将构造函数声明私有,使其只能通过静态的get函数来创建对象,其次在get函数中设置,第一次调用就创建对象,之后创建的直接返回这个指针,此时不管我们调用多少回也都是这个指针了,这便完成了我们最简单的单例模式
但其实这个单例模式是有问题的,它存在线程安全的问题,当有两个线程同时去调用get函数时,可能会出现都检测出为空,然后都创建了一个对象,在随后的调用中又会将第一个创建的覆盖掉,此时会出现内存泄漏
我们的解决方案就是加锁
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;//声明静态锁
};
Singleton* Singleton::_pinst = nullptr;
mutex Singleton::_mtx;//定义锁
但这样其实也并不是完美的,我们在new对象时是可能出现异常的,抛出异常导致无法解锁,所以我们还需对其进行修改
static Singleton* GetInstance()
{
//::Sleep(1000); 增加没加锁时出现线程不安全的条件(2个以上线程同时过了判断条件)
// 双检查
if (_pinst == nullptr)
{
//_mtx.lock();
unique_lock<mutex> lock(_mtx);
if (_pinst == nullptr)
{
_pinst = new Singleton;
}
//_mtx.unlock();
}
// ...
return _pinst;
}
static void DelInstance()//这个函数也可以不加,对象在生命周期结束也会自动释放
{
//unique_lock<mutex> lock(_mtx);
delete _pinst;
_pinst = nullptr;
}
Singleton(const Singleton& s) = delete;
private:
Singleton()
{}
static Singleton* _pinst;
static mutex _mtx;
};
我们加上智能锁守卫,使其解锁一定会被执行,而我们在外部还加了一个判断,这就是为了双重保险,直接阻止多个线程进入锁中的情况,我们开始的两个进程,一个堵在了unique_lock,一个进去创建对象,当进行创建过后,我们就不需要加锁了,所以在给外层加个if,进行优化
这其实就是我们单例模式中的懒汉模式,当我们走到创建对象的时候,为空,我们才开始创建对象,第一次获取对象时,创建对象
懒汉模式 如果单例对象构造十分耗时或者占用很多资源,比如加载插件啊, 初始化网络连接啊,读取文件啊等等,而有可能该对象程序运行时不会用到,那么也要在程序一开始就进行初始化,就会导致程序启动时非常的缓慢。 所以这种情况使用懒汉模式(延迟加载 )更好。
下面我们来看另一个单例模式,饿汉模式
// 饿汉模式 一开始(main函数之前)就创建对象
class Singleton
{
public:
static Singleton* GetInstance()
{
return &_inst;
}
Singleton(const Singleton&) = delete;
private:
Singleton()
{}
static Singleton _inst;
};
Singleton Singleton::_inst;
// static对象是在main函数之前创建的,这会只有主线程,所以不存在线程安全。
我们饿汉模式,指的是在一开始就创建了对象,所以不存在线程安全问题
懒汉模式与饿汉模式的对比
1、懒汉模式需要考虑线程安全和释放的问题,实现相对更复杂,饿汉模式不存在以上问题,实现简单
2、懒汉是一种懒加载模式需要时在初始化创建对象,不会影响程序的启动。饿汉模式则相反,程序启动阶段就创建初始化实力对象,会导致程序启动慢,影响体验。
3、如果有多个单例类,假设有依赖关系(B依赖A),要求A单例先创建初始化,B单例再创建初始化,那么就不能饿汉,因为无法保证创建初始化顺序,这时用懒汉我们就可以手动控制。总结一下:实际中懒汉模式还是更实用一些