文章目录
C++特殊类设计
请设计一个类,不能被拷贝
拷贝只会放生在两个场景中:拷贝构造函数以及赋值运算符重载,因此想要让一个类禁止拷贝,只需让该类不能调用拷贝构造函数以及赋值运算符重载即可。
C++98 版本设计思想:
将拷贝函数和赋值运算符重载设置为私有化,因为私有化杜绝了用户在类外实现这个函数,如果在公有里面只声明就能绕过限制。
cpp
class CopyBan{
public:
CopyBan(){}
private:
CopyBan(const CopyBan &);
CopyBan& operator=(const CopyBan&);
};
C++11版本设计思想:
C++11就简单许多因为C++11拓展了delete的使用访问,我们只需要在不想实现的类后面加上= delete即可。
cpp
class CopyBan
{
public:
CopyBan() {}
CopyBan(const CopyBan &) = delete;
CopyBan &operator=(const CopyBan &) = delete;
};
请设计一个类,只能在堆上创建对象
C++98 和 C++11的设计思想一致,都是通过类内的静态成员函数来访问私有的构造函数。
那么为什么是类内的静态成员函数呢?
- 类内成员函数,需要先构造变量才能访问类内的函数接口构造函数都私有了这么让你生成成员变量;
- 私有构造函数不能被类外的函数访问;
- 类内的静态成员函数就不需要先构造再使用,并且类内的静态成员函数可以访问私有的构造函数。
cpp
class HeapOnly{
public:
static HeapOnly* CreateObject()
{
return new HeapOnly;
}
private:
HeapOnly() {}
};
请设计一个类,只能在栈上创建对象
这个和只能堆上创建对象的设计思路一致,不过这个只能在栈上创建对象需要将operator new/new[]和operator delete/delete[]给禁止,C++98就是私有化,C++11就是加 = delete。
C++98 版本设计思想:
cpp
class StackOnly {
public:
static StackOnly CreateObj() { return StackOnly(); }
private:
StackOnly() : _a(0) {}
int _a;
static void* operator new(std::size_t);
static void operator delete(void*);
static void* operator new[](std::size_t);
static void operator delete[](void*);
};
C++11 版本设计思想:
cpp
class StackOnly {
public:
static StackOnly CreateObj() { return StackOnly(); }
void* operator new(std::size_t) = delete;
void operator delete(void*) = delete;
void* operator new[](std::size_t) = delete;
void operator delete[](void*) = delete;
private:
StackOnly() : _a(0) {}
int _a;
};
请设计一个类,不能被继承
首先我们要了解一下C++继承的机制是什么,我们知道C++的继承,派生类在构造前会去调用基类的构造函数,那么我们只需要将构造函数私有化即可。但是这个方法是C++11之前的设计方法,因为C++11添加了一个新关键字final该关键字就显示的声明该类不能被继承。
C++98:构造函数私有化。派生类在初始化列表必须调用基类的构造函数,而基类构造函数私有,派生类无法调用,因此无法继承。
cpp
class NonInherit
{
public:
static NonInherit GetInstance()
{
return NonInherit();
}
private:
NonInherit()
{}
};
C++11:使用final关键字修饰类。
cpp
class NonInherit final {
public:
NonInherit() {}
};
请设计一个类,只能创建一个对象(单例模式)
设计模式:
设计模式(Design Pattern)是一套被反复使用、多数人知晓的、经过分类的、代码设计经验的总结。为什么会产生设计模式这样的东西呢?就像人类历史发展会产生兵法。最开始部落之间打仗时都是人拼人的对砍。后来春秋战国时期,七国之间经常打仗,就发现打仗也是有套路的,后来孙子就总结出了《孙子兵法》。孙子兵法也是类似。
使用设计模式的目的:为了代码可重用性、让代码更容易被他人理解、保证代码可靠性。设计模式使代码编写真正工程化;设计模式是软件工程的基石脉络,如同大厦的结构一样。
单例模式:
一个类只能创建一个对象,即单例模式,该模式可以保证系统中该类只有一个实例,并提供一个访问它的全局访问点,该实例被所有程序模块共享。 比如在某个服务器程序中,该服务器的配置信息存放在一个文件中,这些配置数据由一个单例对象统一读取,然后服务进程中的其他对象再通过这个单例对象获取这些配置信息,这种方式简化了在复杂环境下的配置管理。
单例模式有两种实现模式:
饿汉模式:程序启动时(main函数之前)就创建好实例。
- 优点:简单,线程安全(因为C++静态变量初始化在main之前)。
- 缺点:如果单例对象构造极耗时,会导致进程启动变慢;若多个单例存在,初始化顺序不确定。
cpp
class SingletonHungry {
public:
static SingletonHungry* GetInstance() {
return &m_instance;
}
// 防拷贝(C++11)
SingletonHungry(const SingletonHungry&) = delete;
SingletonHungry& operator=(const SingletonHungry&) = delete;
private:
SingletonHungry() {} // 私有构造
// 静态成员变量在程序入口前就初始化完成
static SingletonHungry m_instance;
};
// 在类外定义并初始化
SingletonHungry SingletonHungry::m_instance;
如果这个单例对象在多线程高并发环境下频繁使用,性能要求较高,那么显然使用饿汉模式来避免资源竞争,提高响应速度更好。
懒汉模式:第一次使用时才创建。
- 优点:延迟加载,启动无负载。
- 缺点:需要解决线程安全问题。
如果单例对象构造十分耗时或者占用很多资源,比如加载插件啊, 初始化网络连接啊,读取文件啊等等,而有可能该对象程序运行时不会用到,那么也要在程序一开始就进行初始化,就会导致程序启动时非常的缓慢。 所以这种情况使用懒汉模式(延迟加载)更好。
设计懒汉模式时需要注意的问题:
- 线程安全问题:
- 解决方法:双检锁(Double-Checked Locking Pattern)
- 第一次判断(外层):为了提高效率。如果实例已经存在,直接返回指针,根本不需要去获取锁(减少锁的竞争,锁的开销很大)。
- 第二次判断(内层):为了线程安全。当两个线程同时通过第一次判断后,它们会竞争锁。线程A抢到锁并创建了实例,释放锁;线程B随后获得锁,如果不再判断一次,就会重复 new 创建新对象。
- 解决方法:双检锁(Double-Checked Locking Pattern)
- 内存泄漏问题
懒汉模式是在堆上new出来的,那么就要思考谁来delete。- 解决方法:内嵌垃圾回收类
- 通常是在内部实现严格垃圾回收类,利用静态对象生命周期结束时自动调用析构函数的特性来
delete单例。
- 通常是在内部实现严格垃圾回收类,利用静态对象生命周期结束时自动调用析构函数的特性来
- 解决方法:内嵌垃圾回收类
cpp
class SingletonLazy {
public:
static SingletonLazy* GetInstance() {
// 第一次检查:提高效率,避免每次都加锁
if (nullptr == m_pInstance) {
m_mtx.lock();
// 第二次检查:保证线程安全,防止重复创建
if (nullptr == m_pInstance) {
m_pInstance = new SingletonLazy();
}
m_mtx.unlock();
}
return m_pInstance;
}
// 内部垃圾回收类,程序结束前自动析构,释放单例内存
class CGarbo {
public:
~CGarbo() {
if (m_pInstance) {
delete m_pInstance;
m_pInstance = nullptr;
}
}
};
private:
SingletonLazy() {} // 私有构造
SingletonLazy(const SingletonLazy&) = delete;
SingletonLazy& operator=(const SingletonLazy&) = delete;
static SingletonLazy* m_pInstance;
static std::mutex m_mtx;
static CGarbo m_garbo; // 声明垃圾回收对象
};
// 静态成员初始化
SingletonLazy* SingletonLazy::m_pInstance = nullptr;
std::mutex SingletonLazy::m_mtx;
SingletonLazy::CGarbo SingletonLazy::m_garbo;
懒汉模式设计这么复杂,那么有没有简单一点的设计方法,有的有的。在C++11中明确说明了局部的静态变量初始化是线程安全的。所以现代写法:
cpp
class SingletonModern {
public:
static SingletonModern& GetInstance() {
// C++11 保证局部静态变量的初始化是线程安全的(Magic Statics)
// 且不需要显式加锁,程序退出时会自动调用析构函数
static SingletonModern instance;
return instance;
}
SingletonModern(const SingletonModern&) = delete;
SingletonModern& operator=(const SingletonModern&) = delete;
private:
SingletonModern() {}
};
这时候就有人说了程序设计的时候还要考虑用饿汉还是懒汉好麻烦那么有没有取其精华,去其糟粕的做法,有的有的。现代写法的懒汉的单例模式严格来说不应该叫懒汉,因为这种写法同时满足了懒汉模式的延迟加载和自动释放、饿汉模式的初始化阶段线程安全。所以现代写法就是单例模式本身,不需要区分饿汉和懒汉。
完