一、请设计一个类,不能被拷贝
拷贝只会放生在两个场景中:拷贝构造函数以及赋值运算符重载,因此想要让一个类禁止拷贝,
只需让该类不能调用拷贝构造函数以及赋值运算符重载即可。
- 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;
//...
};
二、请设计一个类,只能在堆上创建对象
方法一:将类的析构函数私有。防止别人调用拷贝在栈上生成对象
编译报错:
栈上创建会自动调用析构,这里析构被私有了,就不能在栈上创建。
但这样在堆上创建后要进行delete也要调用析构函数,也就是虽然不能在栈上创建对象,但是同样也不能在堆上创建对象了。
解决方法:在类public中实现一个成员函数进行delete
class HeapOnly
{
public:
void Destroy()
{
delete this;
}
private:
~HeapOnly()
{
//...
}
};
int main()
{
//HeapOnly hp1;
//static HeapOnly hp2;
HeapOnly* hp3 = new HeapOnly;
//delete hp3;
hp3->Destroy();
return 0;
}
编译成功:
方法二:
实现方式:
1. 将类的构造函数私有,拷贝构造声明成私有。防止别人调用拷贝在栈上生成对象。
2. 提供一个静态的成员函数,在该静态成员函数中完成堆对象的创建。
完整代码:
class HeapOnly
{
public:
static HeapOnly* CreateObj()
{
return new HeapOnly;
}
private:
HeapOnly()
{
//...
}
HeapOnly(const HeapOnly& hp) = delete;
HeapOnly& operator=(const HeapOnly& hp) = delete;
};
int main()
{
HeapOnly* hp3 = HeapOnly::CreateObj();
//HeapOnly copy(*hp3);
return 0;
}
以上代码有两个细节:
1. 如果没有提供静态的成员函数:
会发生报错,调用一个类的普通成员函数是不是需要对象去调用,那我们在堆上申请的对象又正好需要调用这个函数来生成,可是我们选择都没有对象我们如何去调用这个函数呢?所以这就纯纯是先有鸡还是先有蛋的问题了。
为了解决这个问题,我们可以将这个成员函数设置成静态的。
2. 利用拷贝构造在栈上创建报错:
虽然我们不能够直接的在栈上创建对象,但是我们可以在堆上申请一个对象之后再调用一次拷贝构造然后间接的就在栈上申请对象了。
所以我们的完整代码还要将拷贝构造和赋值重载都声明成delete禁掉,所以这里调用拷贝构造报错
三、请设计一个类,只能在栈上创建对象
方法:同上将构造函数私有化,然后设计静态方法创建对象返回即可。
下面先来看这种方式的实现:
编译报错,说明不能在堆上创建。
但是我们如果在new的时候调用拷贝构造:
编译通过:
我们发现又可以在堆上成功了,说明这种方法还有漏洞。
解决方法:下面我们来分析一下:new是由operator new + 构造组成的,所以我们只要在类中重载一个new,并将new声明成delete,就可以解决这个问题。
class StackOnly
{
public:
static StackOnly CreateObj()
{
StackOnly st;
return st;
}
private:
StackOnly()
{
//...
}
// 对一个类实现专属operator new
void* operator new(size_t size) = delete;
};
运行发现报错,说明不能只有new在堆上创建对象了。
四、请设计一个类,不能被继承
- C++98方式
将构造函数私有:
基类的私有成员在派生类不可访问,而派生类要继承,需要调用基类的构造函数。所以将构造函数私有可以,让类不能被继承。
// C++98中构造函数私有化,派生类中调不到基类的构造函数。则无法继承
class NonInherit
{
public:
static NonInherit GetInstance()
{
return NonInherit();
}
private:
NonInherit()
{}
};
- C++11方法
final关键字,final修饰类,表示该类不能被继承。
class A final
{
// ....
};
五、设计一个类,只能创建一个对象(单例模式)
设计模式
设计模式(Design Pattern)是一套被反复使用、多数人知晓的、经过分类的、代码设计经验的总结。
为什么会产生设计模式这样的东西呢?就像人类历史发展会产生兵法。最开始部落之间打仗时都是人拼人的对砍。后来春秋战国时期,七国之间经常打仗,就发现打仗也是有套路的,后来孙子就总结出了《孙子兵法》。孙子兵法也是类似。
使用设计模式的目的:为了代码可重用性、让代码更容易被他人理解、保证代码可靠性。
设计模式使代码编写真正工程化;设计模式是软件工程的基石脉络,如同大厦的结构一样。
单例模式:一个类只能创建一个对象,即单例模式,该模式可以保证系统中该类只有一个实例,并提供一个访问它的全局访问点,该实例被所有程序模块共享。比如在某个服务器程序中,该服务器的配置信息存放在一个文件中,这些配置数据由一个单例对象统一读取,然后服务进程中的其他对象再通过这个单例对象获取这些配置信息,这种方式简化了在复杂环境下的配置管理。
对于单例模式我们首先需要解决一个问题:如果保证全局(一个进程中)只有一个唯一实例对象
经过上面的学习我们就可以知道,要想保证全局只有一个唯一实例对象我们需要做以下两步操作
- 构造函数私有定义。拷贝构造和赋值防拷贝禁掉
- 提供一个GetInstance获取单例对象
单例模式有两种实现模式:饿汉模式与懒汉模式
饿汉模式:就是说不管你将来用不用,程序启动时就创建一个唯一的实例对象。
下面来看饿汉模式的代码:
主要有以下三个关键步骤
1、构造函数私有 2、提供获取单例对象的接口函数 3、防拷贝
经过上面的学习我们就可以知道,为了防止被创建多个对象,所以我们要1、3这俩步,构造函数私有定义。拷贝构造和赋值防拷贝禁掉。
然后我们创建单例对象, 还要给它提供获取单例对象的接口函数。
// 饿汉模式:一开始(main函数之前)就创建单例对象
// 提供一个静态指向单例对象的成员指针,初始化时new一个对象给它
// 1、如果单例对象初始化内容很多,影响启动速度
// 2、如果两个单例类,互相有依赖关系。
// 假设有A B两个单例类,要求A先创建,B再创建,B的初始化创建依赖A
namespace hungry
{
class Singleton
{
public:
// 2、提供获取单例对象的接口函数
static Singleton& GetInstance()
{
return _sinst;
}
void func();
void Add(const pair<string, string>& kv)
{
_dict[kv.first] = kv.second;
}
void Print()
{
for (auto& e : _dict)
{
cout << e.first << ":" << e.second << endl;
}
cout << endl;
}
private:
// 1、构造函数私有
Singleton()
{
// ...
}
// 3、防拷贝
Singleton(const Singleton& s) = delete;
Singleton& operator=(const Singleton& s) = delete;
map<string, string> _dict;
// ...
static Singleton _sinst;
};
Singleton Singleton::_sinst;
void Singleton::func()
{
//
_dict["xxx"] = "1111";
}
}
int main()
{
//Singleton s1;
//Singleton s2;
cout << &hungry::Singleton::GetInstance() << endl;
cout << &hungry::Singleton::GetInstance() << endl;
cout << &hungry::Singleton::GetInstance() << endl;
//Singleton copy(Singleton::GetInstance());
hungry::Singleton::GetInstance().Add({ "xxx", "111" });
hungry::Singleton::GetInstance().Add({ "yyy", "222" });
hungry::Singleton::GetInstance().Add({ "zzz", "333" });
hungry::Singleton::GetInstance().Add({ "abc", "333" });
hungry::Singleton::GetInstance().Print();
return 0;
}
运行结果:
可以看到单例模式只能创建了一个对象
把其他的创建屏蔽掉再看运行结果:
可以看到GetInstance()之后地址都是相同的,说明只创建了一个对象。并且能实现里面简单的功能
饿汉模式适用场景:
- 如果这个单例对象在多线程高并发环境下频繁使用,性能要求较高,那么显然使用饿汉模式来避免资源竞争,提高响应速度更好。
饿汉模式不适用场景:
- 如果单例类构造函数中,要做很多配置初始化工作,导致程序启动非常慢
- 如果两个单例类,互相有依赖关系。 饿汉模式也会有问题。(比如有A和B两个单例类,要求A单例先初始化,B必须在A之后初始化。那么饿汉无法保证。
这个时候我们使用饿汉就不合适了,这时我们应该使用下面的懒汉模式。
懒汉模式
如果单例对象构造十分耗时或者占用很多资源,比如加载插件啊, 初始化网络连接啊,读取文件啊等等,而有可能该对象程序运行时不会用到,那么也要在程序一开始就进行初始化,就会导致程序启动时非常的缓慢。 所以这种情况使用懒汉模式(延迟加载)更好。
下面来看这段懒汉的代码
namespace lazy
{
//懒汉模式
class Singleton
{
public:
static Singleton* GetInstance()
{
if (_psinst == nullptr)
{
_psinst = new Singleton;
}
return _psinst;
}
void Print()
{
cout << "Print()" << _a << endl;
}
private:
Singleton()
:_a(0)
{}
//C++98 防拷贝
//Singleton(const Singleton&);
//Singleton& operator=(const Singleton&);
//C++11 防拷贝
Singleton(const Singleton&) = delete;
Singleton& operator=(const Singleton&) = delete;
int _a;
static Singleton* _psinst;
};
Singleton* Singleton::_psinst = nullptr;
}
但是这么写是会存在一些问题的。
在多线程场景下会存在线程安全的问题,因为我们的懒汉式要用的时候才会去调用GetInstance函数再去创建那个唯一的对象。
假如说现在有两个线程,分别是线程A与线程B,然后我线程A与线程B都调用了GetInstance函数,线程A与线程B都进入了if条件判断里面,此时我们线程A如果先执行实例化对象的代码,然后返回。因为我们的线程B不知道线程A已经实例化了唯一对象,此时线程B再去调用实例化对象的代码,就会导致前面的唯一对象的地址被覆盖掉,线程A的数据会丢失,并且会有内存泄漏的风险,更严重的是此时已经产生了两个实例对象,违反了单例模式的设计思想。
那我们应如何解决上面的问题呢?我们需要进行双检查加锁。
//懒汉模式
#include <mutex>
namespace lazy
{
class Singleton
{
public:
static Singleton* GetInstance()
{
// 保护第一次需要加锁,后面都不需要加锁的场景,可以使用双检查加锁
// 特点:第一次加锁,后面不加锁,保护线程安全,同时提高了效率
if (_pinst == nullptr)
{
_mtx.lock();
if (_pinst == nullptr)
{
_pinst = new Singleton;
}
_mtx.unlock();
}
return _pinst;
}
static void DelInstance()
{
_mtx.lock();
if (_pinst)
{
delete _pinst;
_pinst = nullptr;
}
_mtx.unlock();
}
void Print()
{
cout << "Print()" << _a << endl;
}
private:
Singleton()
:_a(0)
{
// 假设单例类构造函数中,要做很多配置初始化
}
~Singleton()
{
// 程序结束时,需要处理一下,持久化保存一些数据
}
Singleton(const Singleton&) = delete;
Singleton& operator=(const Singleton&) = delete;
// 实现一个内嵌垃圾回收类
class CGarbo {
public:
~CGarbo()
{
if (_pinst)
{
delete _pinst;
_pinst = nullptr;
}
}
};
int _a;
static Singleton* _pinst;
static std::mutex _mtx;
static CGarbo _gc;
};
Singleton* Singleton::_pinst = nullptr;
std::mutex Singleton::_mtx;
Singleton::CGarbo Singleton::_gc;
}
为什么是双检查加锁?
我们需要知道我们想加锁的地方只是第一次 _pinst为空的时候,我们想实例化一个唯一对象时才需要加锁,但是如果到了后面我们已经实例化出了对象,但是我们照样加锁了之后再去判断 _pinst是否为空,然后在多线程场景下频繁的加锁解锁会导致效率降低,并且也不推荐这样做。
而我们的双检查加锁里面的第二个if检查就是我先进行加锁,然后进行判断第一次_pinst是否为空,然后实例化一个唯一对象,等到下一次某个线程再调用GetInstance的时候,我们的第一个if检查发现pinst不为空,就不会进入第一个if条件判断的里面,因此也就不存在多线程场景下频繁加锁解锁,保护线程安全,同时提高了效率。
饿汉模式和懒汉模式的优缺点对比如下:
饿汉模式:
- 优点:实例化对象在类加载时完成,因此无须考虑多线程访问问题,可以确保实例的唯一性。
- 缺点:
- 饿汉式单例对象在类加载时就被创建,可能会造成系统资源的浪费,尤其是在对象占内存较大或对象初始化耗时较长的情况下。
- 如果有多个单例对象,他们之间有初始化依赖关系,饿汉模式也会有问题。(比如有A和B两个单例类,要求A单例先初始化,B必须在A之后初始化。那么饿汉无法保证。这种场景下面用懒汉就可以,懒汉可以先调用A::GetInstance(),再调用B::GetInstance().)
懒汉模式:
- 优点:实现了延迟加载,只有在第一次使用时才创建实例,节省了系统资源。(解决饿汉的缺点。因为他是第一次调用GetInstance时创建初始化单例对象)
- 缺点:必须考虑与处理多个线程同时访问的问题,需要进行双重检查锁定等机制及进行控制。