[本节目标]
掌握常见特殊类的设计方式
1.请设计一个类,不能被拷贝
拷贝只会放生在两个场景中:拷贝构造函数以及赋值运算符重载,因此想要让一个类禁止拷贝, 只需让该类不能调用拷贝构造函数以及赋值运算符重载即可。
⭐C++98:将拷贝构造函数与赋值运算符重载只声明不定义,并且将其访问权限设置为私有即可。
class CopyBan
{
// ...
private:
CopyBan(const CopyBan&);
CopyBan& operator=(const CopyBan&);
//...
};
原因:
⭐1. 设置成私有:如果只声明没有设置成private,用户自己如果在类外定义了,就不能禁 止拷贝了
⭐2. 只声明不定义:不定义是因为该函数根本不会调用,定义了其实也没有什么意义,不写 反而还简单,而且如果定义了就不能防止成员函数内部拷贝了。
⭐C++11:C++11扩展delete的用法,delete除了释放new申请的资源外,如果在默认成员函数后跟上 =delete,表示让编译器删除掉该默认成员函数。
class CopyBan
{
// ...
CopyBan(const CopyBan&) = delete;
CopyBan& operator=(const CopyBan&) = delete;
//...
};
在c++标准库中对于IO流的对象不能拷贝就是采用的c++11的方式
2. 请设计一个类,只能在堆上创建对象
实现方式:
⭐1. 将类的构造函数私有,拷贝构造声明成私有。防止别人调用拷贝在栈上生成对象。
⭐2. 提供一个静态的成员函数,在该静态成员函数中完成堆对象的创建
class HeapOnly
{
private:
int _x;
vector<int> _a;
};
int main()
{
HeapOnly ho1;//栈上开辟
HeapOnly* ho2 = new HeapOnly;//堆上开辟
return 0;
}
如果我们没有任何设置,此时在内存任意地方我们就可以创建对象,但是我们要知道一点,无论我们在哪个地方创建对象,都要去调用默认的构造函数(如果我们没有显示实现),当我们把构造函数私有时,此时就不能创建对象了。
此时就能阻止在类外栈上创建对象,但是此时也阻止了在类外堆上开辟空间,但是我们现在的场景是只能在堆上开辟,怎么做呢?但是咱们在类里面还是可以创建对象的,类是我们自己的,我们可以写死,只写在堆上开辟空间的代码。
class HeapOnly
{
public:
// 模板的可变参数
template <class... Args>
HeapOnly* CreatObj(Args&&... args)
{
return new HeapOnly(args...);//调用构造函数
}
private:
HeapOnly()// 构造私有化
{}
int _x;
int _y;
HeapOnly(int x, int y)
: _x(x)
, _y(y)
{}
vector<int> _a;
};
但是此时我们又会发现一个问题,此时我们调用CreatObj调不动。
但是我们就是想通过CreatObj来创建对象,但是Creatobj又需要我们的对象去调用,所以会出现报错。我们可以设置成静态成员函数,并且静态成员里面调用构造函数不需要this,因为我们是直接new出来的,直接会去调用构造函数,而静态成员函数通过类名加上域访问限定符即可调用。
class HeapOnly
{
public:
// 模板的可变参数
template <class... Args>
static HeapOnly* CreatObj(Args&&... args)
{
return new HeapOnly(args...);//调用构造函数
}
private:
HeapOnly()// 构造私有化
{}
int _x;
int _y;
HeapOnly(int x, int y)
: _x(x)
, _y(y)
{}
vector<int> _a;
};
int main()
{
HeapOnly* ho1 = HeapOnly::CreatObj();
HeapOnly* ho2 = HeapOnly::CreatObj(1,2);
return 0;
}
那我们当前的设计有没有什么漏洞呢?
我还要封掉拷贝构造函数和赋值重载,虽然赋值重载是堆上开辟的空间,但是此时是默认的赋值重载,进行的是浅拷贝,有时候我们不期望值拷贝,所以我们也封掉。
HeapOnly(const HeapOnly&) = delete;
HeapOnly& operator=(const HeapOnly&) = delete;
⭐方法二:把析构函数封掉
// 只能在堆上
class HeapOnly
{
public:
HeapOnly()
{}
HeapOnly(int x, int y)
:_x(x)
,_y(y)
{}
void Destroy()
{
delete this;
}
HeapOnly(const HeapOnly&) = delete;
HeapOnly& operator=(const HeapOnly&) = delete;
private:
// 析构函数私有
~HeapOnly()
{
cout << "~HeapOnly()" << endl;
}
int _x;
int _y;
vector<int> _a;
};
此时我们还可以把堆上开辟的空间交给智能指针来管理。
3. 请设计一个类,只能在栈上创建对象
⭐同上将构造函数私有化,然后设计静态方法创建对象返回即可。
class StackOnly
{
public:
// 模板的可变参数
template <class... Args>
static StackOnly CreatObj(Args&&... args)
{
return StackOnly(args...);//调用拷贝构造函数 + 构造函数
// 返回栈上面的匿名对象,传值返回
}
//StackOnly(const StackOnly&) = delete;//不能封死了
StackOnly& operator=(const StackOnly&) = delete;
private:
StackOnly()// 构造私有化
{}
int _x;
int _y;
StackOnly(int x, int y)
: _x(x)
, _y(y)
{}
vector<int> _a;
};
int main()
{
StackOnly so1 = StackOnly::CreatObj();
StackOnly so2 = StackOnly::CreatObj(1,1);
return 0;
}
但是此时又出现一个漏洞了,我们拷贝构造没有封掉
还记得我们的new会干什么嘛?它会调用operator new + 构造函数,拷贝构造函数也是构造函数的一种,我们既然不能封掉拷贝构造,那么我们来封掉operator new,但是operator new是一个全局的,所以我们在类里面自己定义就不会调用全局的,然后我们再封掉自己写的operator new即可。
class StackOnly
{
public:
// 模板的可变参数
template <class... Args>
static StackOnly CreatObj(Args&&... args)
{
return StackOnly(args...);//调用拷贝构造函数 + 构造函数
// 返回栈上面的匿名对象,传值返回
}
//StackOnly(const StackOnly&) = delete;//不能封死了
StackOnly& operator=(const StackOnly&) = delete;
// 重载一个类专属的operator new
void* operator new(size_t n) = delete;
private:
StackOnly()// 构造私有化
{}
int _x;
int _y;
StackOnly(int x, int y)
: _x(x)
, _y(y)
{}
vector<int> _a;
};
int main()
{
StackOnly so1 = StackOnly::CreatObj(); //在栈上开辟
StackOnly so2 = StackOnly::CreatObj(1,1); //在栈上开辟
//error
StackOnly* so3 = new StackOnly(so1);//在堆上开辟
//调用不了构造函数,可以调用拷贝构造函数啊
return 0;
}
4.请设计一个类,不能被继承
⭐C++98方式:C++98中构造函数私有化,派生类中调不到基类的构造函数,因为私有在派生类中不可见,则无法继承
class Person
{
public:
private:
Person()//构造函数私有化
{}
int _sex;//性别
};
class Student : public Person
{
public:
Student()// error:无法访问 private 成员(在“Person”类中声明)
{}
private:
string _name;
int _num;//学号
};
⭐C++11方法 :final关键字,final修饰类,表示该类不能被继承。
class Person final
{
public:
private:
Person()//构造函数私有化
{}
int _sex;//性别
};
//error:"Student": 无法从 "Person" 继承,因为它已声明为 "final"
class Student : public Person
{
public:
Student()
{}
private:
string _name;
int _num;//学号
};
5. 请设计一个类,只能创建一个对象(单例模式)
设计模式:
设计模式(Design Pattern)是一套被反复使用、多数人知晓的、经过分类的、代码设计经验的
总结:为什么会产生设计模式这样的东西呢?就像人类历史发展会产生兵法。最开始部落之间打 仗时都是人拼人的对砍。后来春秋战国时期,七国之间经常打仗,就发现打仗也是有套路的,后 来孙子就总结出了《孙子兵法》。孙子兵法也是类似。
使用设计模式的目的:为了代码可重用性、让代码更容易被他人理解、保证代码可靠性。 设计模 式使代码编写真正工程化;设计模式是软件工程的基石脉络,如同大厦的结构一样。
单例模式:
一个类只能创建一个对象,即单例模式,该模式可以保证系统中该类只有一个实例,并提供一个 访问它的全局访问点,该实例被所有程序模块共享。比如在某个服务器程序中,该服务器的配置 信息存放在一个文件中,这些配置数据由一个单例对象统一读取,然后服务进程中的其他对象再 通过这个单例对象获取这些配置信息,这种方式简化了在复杂环境下的配置管理。
⭐饿汉模式:就是说不管你将来用不用,程序启动时就创建一个唯一的实例对象。
我们首先要做的就是将构造函数私有,在类外不让你能创建对象。
那我们肯定就要在类里面完成,我们可以按照之前的方式,来一个CreatObj,我这里取名字为GetInstance,但是按照之前那样写我们依然是可以创建多个对象,怎么办呢?我们这里可以使用一个全局变量的对象,GetInstance的时候就直接返回这个全局对象,这样就能保证只有一个对象,但是这个全局变量我们定义在类外又不行了,构造函数私有化了呀,我们不能初始化这个全局变量呀,我们可以把这个全局变量定义在类里面,加上static修饰,静态成员变量不存在对象中,在静态区,而相当于全局变量,只不过此时受类域限制,我们来手撕一下。
// 饿汉
class Singleton
{
public:
// 饿汉:一开始(main之前)就创建出对象
static Singleton* GetInstance()
{
return &_sint;
}
// 获取数据
void Print()
{
cout << _x << endl;
cout << _y << endl;
for (auto& e : _vstr)
{
cout << e << " ";
}
cout << endl;
}
void AddStr(const string& s)
{
_vstr.push_back(s);
}
Singleton(Singleton const&) = delete;
Singleton& operator=(Singleton const&) = delete;
private:
Singleton(int x = 0, int y = 0, const vector<string>& vstr = { "yyyyy","xxxx" })
: _x(x)
, _y(y)
, _vstr(vstr)
{}
// 想让一些数据,当前程序只有一份,那就可以把这些数据放到这个类里面
// 再把这个类设计成单例,这个数据就只有一份了
int _x;
int _y;
vector<string> _vstr;
// 静态成员对象,不存在对象中,存在静态区,相当于全局的,定义在类中,受类域限制
// 此时就可以访问构造函数
static Singleton _sint;
};
// 在程序入口之前就完成单例对象的初始化
Singleton Singleton::_sint(1, 1, { "武汉" ,"黄石" });
光这样说获取都是一个对象的证据还是不太足够,我们直接打印地址看看。
⭐问题:
// 1、如果当前单例对象数据较多,构造初始化成本较高,那么会影响程序启动的速度。迟迟进不了main函数
// 2、多个单例类有初始化启动依赖关系,饿汉无法控制。假设:A和B两个单例,假设要求A先初始化,B再初始化,饿汉无法保证,因为都是全局变量,而且还在不同的文件
如果这个单例对象在多线程高并发环境下频繁使用,性能要求较高,那么显然使用饿汉模式来避 免资源竞争,提高响应速度更好。
⭐懒汉模式
如果单例对象构造十分耗时或者占用很多资源,比如加载插件啊, 初始化网络连接啊,读取文件啊等等,而有可能该对象程序运行时不会用到,那么也要在程序一开始就进行初始化, 就会导致程序启动时非常的缓慢。 所以这种情况使用懒汉模式(延迟加载)更好。
// 懒汉
class Singleton
{
public:
static Singleton* GetInstance()
{
// 第一次调用时,创建单例对象
// 此时调用构造函数
// 线程安全问题,需要加锁
if(_psint == nullptr)
_psint = new Singleton;
return _psint;
}
// 获取数据
void Print()
{
cout << _x << endl;
cout << _y << endl;
for (auto& e : _vstr)
{
cout << e << " ";
}
cout << endl;
}
void AddStr(const string& s)
{
_vstr.push_back(s);
}
Singleton(Singleton const&) = delete;
Singleton& operator=(Singleton const&) = delete;
private:
Singleton(int x = 0, int y = 0, const vector<string>& vstr = { "yyyyy","xxxx" })
: _x(x)
, _y(y)
, _vstr(vstr)
{}
int _x;
int _y;
vector<string> _vstr;
static Singleton* _psint;
};
Singleton* Singleton::_psint = nullptr;
我们此时调用的是默认的构造函数,那我们能不能使用传参的构造函数呢?怎么使用呢?
但是此时我们的单例对象是new出来的,我们就需要对这个资源进行释放,此时依然要使用静态成员函数,为什么呢?
// 懒汉
class Singleton
{
public:
static Singleton* GetInstance()
{
// 第一次调用时,创建单例对象
// 此时调用构造函数
// 线程安全问题,需要加锁
if(_psint == nullptr)
_psint = new Singleton;
return _psint;
}
static void DelInstance()
{
if (_psint)
{
delete _psint;
_psint = nullptr;
}
}
// 获取数据
void Print()
{
cout << _x << endl;
cout << _y << endl;
for (auto& e : _vstr)
{
cout << e << " ";
}
cout << endl;
}
void AddStr(const string& s)
{
_vstr.push_back(s);
}
Singleton(Singleton const&) = delete;
Singleton& operator=(Singleton const&) = delete;
~Singleton()
{
// 把数据写到文件
cout << "~Singleton()" << endl;
}
private:
Singleton(int x = 0, int y = 0, const vector<string>& vstr = { "yyyyy","xxxx" })
: _x(x)
, _y(y)
, _vstr(vstr)
{}
int _x;
int _y;
vector<string> _vstr;
static Singleton* _psint;
};
Singleton* Singleton::_psint = nullptr;
运行一下:
如果我们没有显示调用呢?或者忘记咋办?再借助一个内部类来帮我们释放,为啥要内部类,内部类只能定义类的人来操作,如果我们放在外面,就能定义多个gc类对象,虽然按照逻辑不会错,但是没必要定义这么多,但是此时和上面有一个区别,上面是显示调用析构函数,而下面是程序结束的时候借助gc对象调用析构函数。
// 懒汉
class Singleton
{
public:
static Singleton* GetInstance()
{
// 第一次调用时,创建单例对象
// 此时调用构造函数
// 线程安全问题,需要加锁
if(_psint == nullptr)
_psint = new Singleton;
return _psint;
}
static void DelInstance()
{
if (_psint)
{
delete _psint;
_psint = nullptr;
}
}
// 获取数据
void Print()
{
cout << _x << endl;
cout << _y << endl;
for (auto& e : _vstr)
{
cout << e << " ";
}
cout << endl;
}
void AddStr(const string& s)
{
_vstr.push_back(s);
}
Singleton(Singleton const&) = delete;
Singleton& operator=(Singleton const&) = delete;
~Singleton()
{
// 把数据写到文件
cout << "~Singleton()" << endl;
}
private:
Singleton(int x = 0, int y = 0, const vector<string>& vstr = { "yyyyy","xxxx" })
: _x(x)
, _y(y)
, _vstr(vstr)
{}
int _x;
int _y;
vector<string> _vstr;
static Singleton* _psint;
// 内部类 - 以防在外面被定义多个
class GC // 单纯为了析构单例对象
{
public:
~GC()
{
Singleton::DelInstance();
}
};
static GC gc;
};
Singleton* Singleton::_psint = nullptr;
Singleton::GC Singleton::gc;//出了作用域就析构
那咱们学了智能指针,咱不得用起来呀!
class Singleton
{
public:
static Singleton* GetInstance()
{
// 第一次调用时,创建单例对象
// 此时调用构造函数
// 线程安全问题,需要加锁
// unique_lock<mutex> lock(mtx); 写在这里会频繁加锁解锁
// 实际上我们只想在第一次加锁,后面条件不满足直接返回即可
// 双检查来解决
if (_psint == nullptr) // 提效
{
unique_lock<mutex> lock(mtx);
if (_psint == nullptr) // 保证单例线程安全
_psint = new Singleton;
}
return _psint;
}
static void DelInstance()
{
if (_psint)
{
delete _psint;
_psint = nullptr;
}
}
// 获取数据
void Print()
{
cout << _x << endl;
cout << _y << endl;
for (auto& e : _vstr)
{
cout << e << " ";
}
cout << endl;
}
void AddStr(const string& s)
{
_vstr.push_back(s);
}
Singleton(Singleton const&) = delete;
Singleton& operator=(Singleton const&) = delete;
~Singleton()
{
// 把数据写到文件
cout << "~Singleton()" << endl;
}
private:
Singleton(int x = 0, int y = 0, const vector<string>& vstr = { "yyyyy","xxxx" })
: _x(x)
, _y(y)
, _vstr(vstr)
{}
int _x;
int _y;
vector<string> _vstr;
static Singleton* _psint;
static mutex mtx;
static shared_ptr<Singleton> ptr;//需要在类里面,否则无法访问_psint
};
Singleton* Singleton::_psint = nullptr;
mutex Singleton::mtx;
shared_ptr<Singleton> Singleton::ptr(Singleton::_psint, [](Singleton* ptr1) { ptr1->DelInstance(); });
int main()
{
cout << Singleton::GetInstance() << endl;
// 获取的都是同一个对象
Singleton::GetInstance()->Print();
Singleton::GetInstance()->Print();
cout << Singleton::GetInstance() << endl;
Singleton::GetInstance()->AddStr("黄石");
Singleton::GetInstance()->Print();
cout << Singleton::GetInstance() << endl;
}
运行一下,看看此时有没有问题。
太复杂啦!有没有更简单的方法。
class Singleton
{
public:
static Singleton* GetInstance()
{
// 局部的静态对象,第一次调用函数时构造初始化
// C++11及之后这样写才可以
// C++11之前无法保证这里的构造初始化是线程安全
// 此时我们没有通过new申请资源,也就不用进行释放资源的动作
// 静态变量只会初始化一次,后面这个代码就不执行了
static Singleton _sinst;
return &_sinst;
}
// 获取数据
void Print()
{
cout << _x << endl;
cout << _y << endl;
for (auto& e : _vstr)
{
cout << e << " ";
}
cout << endl;
}
void AddStr(const string& s)
{
_vstr.push_back(s);
}
Singleton(Singleton const&) = delete;
Singleton& operator=(Singleton const&) = delete;
~Singleton()
{
// 把数据写到文件
cout << "~Singleton()" << endl;
}
private:
Singleton(int x = 0, int y = 0, const vector<string>& vstr = { "yyyyy","xxxx" })
: _x(x)
, _y(y)
, _vstr(vstr)
{}
int _x;
int _y;
vector<string> _vstr;
};
这里我们要区分和饿汉的区别,饿汉是把static Singleton _sinst定义成了全局变量,main函数之前我们就创建好了对象,而懒汉是把static Singleton _sinst定义成局部的,但是它的生命周期同样是全局的,需要我们自己去显示调用才会创建这个对象,而且我们定义成静态只会初始化一次,也满足只有一个对象的情况,我们可以对比他们的区别,先来懒汉。
我们再来看看饿汉
懒汉不仅解决了在单例数据过多,导致迟迟不能加载到main函数,同时对于初始化顺序又需要的,我们可以通过显示调用构造函数来保持顺序要求。