Welcome to 9ilk's Code World

(๑•́ ₃ •̀๑) 个人主页: 9ilk
(๑•́ ₃ •̀๑) 文章专栏: C++
本篇博客主要是对常见特殊类的设计进行梳理总结
设计一个类不能被拷贝
拷贝只会发生在两个场景中:拷贝构造函数以及赋值运算符重载,因此想要让一个类禁止拷贝,只需让该类不能调用拷贝构造函数以及赋值运算符重载即可。
- C++98
将拷贝构造函数与赋值运算符重载只声明不定义,并且将其访问权限设置为私有即可
cs
class copyban
{
public: //让拷贝构造为私有
copyban(int data = 1)
:_data(data)
{}
private:
copyban(const copyban& cb);
copyban& operator=(const copyban& cb);
int _data = 1;
};
说明:
-
设置成私有:如果只声明没有设置成private,用户自己如果在类外定义了,就可以不能禁止拷贝了。
-
只声明不定义:不定义是因为该函数根本不会调用,定义了其实也没有什么意思,不写反而还简单,而且如果定义了就不会防止成员函数内部拷贝了。
- C++11
C++11扩展delete的语法,delete除了释放new申请的资源之外,如果在默认成员函数后跟上=delete,表示让编译器释放掉该默认成员函数。
cpp
class Copyban
{
public: //让拷贝构造为私有
Copyban(int data = 1)
:_data(data)
{}
Copyban(const Copyban& cp) = delete;
Copyban& operator=(const Copyban& cp) = delete;
private:
int _data = 1;
};
设计一个只能在堆上创建的类
- 方式一:挡住前路
-
用C++98或C++11的方式将构造函数私有或移除默认构造,防止外部调用构造函数在栈上创建对象
-
用C++98或C++11的方式将拷贝构造函数私有或移除默认拷贝构造,防止外部调用构造函数在栈上创建对象
-
向外部提供一个静态成员函数,在该函数中完成对堆对象的创建
cpp
class HeapOnly
{
public:
static HeapOnly* Createobj()
{
return new HeapOnly;
}
int Get()
{
return _data;
}
HeapOnly(const HeapOnly& hp) = delete;
HeapOnly& operator=(const HeapOnly& hp) = delete;
private:
HeapOnly()
{
cout << "Ciallo~" << endl;
}
int _data = 1;
};
- 方式二:挡住后路
-
将析构函数私有化,这样调用构造或拷贝构造在栈上创建的对象/静态对象,生命周期自动销毁时就调不了析构
-
需要专门为在堆上创建的对象设计一个接口来释放堆上的对象,因为new出来的对象生命周期结束之后不会自动调用析构
cpp
class heaponly
{
public:
heaponly()
{
}
void destroy()
{
delete this;
}
private:
~heaponly()
{
cout << " ~heaponly()" << endl;
}
int _data = 1;
};
HeapOnly* hp = new HeapOnly;
hp->destroy();
设计一个只能在栈上创建的类
- 方式一:将构造函数私有化,这样new的时候就调用不了构造函数,但是需要提供一个获取对象的static接口,该接口在栈上创建一个对象然后传值返回
cpp
class StackOnly
{
public:
static StackOnly CreateObj()
{
return StackOnly();//返回一个匿名对象
}
StackOnly(const StackOnly&& sp)
{
cout << " StackOnly(const StackOnly&& sp)" << endl;
}
private:
StackOnly()
{
cout << " StackOnly()" << endl;
}
int _data = 1;
};
StackOnly s1 = StackOnly::CreateObj();
StackOnly* s2 = new StackOnly(s1); //调用拷贝构造在堆上创建对象
这种方案的缺陷是无法防止外部调用拷贝构造函数创建对象,但是我们不能进行防拷贝,因为这个static接口返回的是一个局部对象,需要进行传值返回,因此需要调用拷贝构造函数
- 方案二:要想解决方案一的缺陷,就需要ban掉new,我们知道new是由两部分构成的,即operator new和拷贝,因此我们可以将operator new给delete,而不封拷贝构造
cpp
class StackOnly
{
public:
static StackOnly CreateObj()
{
return StackOnly();//返回一个匿名对象
}
StackOnly(const StackOnly&& sp)
{
cout << " StackOnly(const StackOnly&& sp)" << endl;
}
void* operator new(size_t size) = delete;
void operator delete(void* p) = delete;
StackOnly(const StackOnly& so) = delete;
private:
StackOnly()
{
cout << " StackOnly()" << endl;
}
int _data = 1;
};
//但是静态区的就封不死
StatckOnly s1 = StackOnly::CreatObj();
static StackOnly s2(s1);
但是缺陷又来了,这两个方案都是无法防止外部在静态区拷贝构造创建对象。此时我们可以将拷贝构造给封死,但是提供一个移动构造,此时createObj返回的临时对象被识别为右值,就会调用移动构造
cpp
class StackOnly
{
public:
static StackOnly CreateObj()
{
//编译器可以直接优化,把匿名对象给了接受返回值的对象
return StackOnly();//返回一个匿名对象
}
//一旦你自己声明了拷贝构造函数(不管是正常的还是 =delete),编译器就不会再给你合成移动构造函数了。
StackOnly(const StackOnly& sp) = delete;
StackOnly(StackOnly&& sp)
{
cout << " StackOnly( StackOnly&& sp)" << endl;
}
private:
StackOnly()
{
cout << " StackOnly()" << endl;
}
int _data = 1;
};
实际上还是存在问题的:
cpp
static StackOnly s6(std::move(s4));
Stack* s5 = new StackOnly(move(s4))
归其原因是因为createObj是传值返回的,因此是封不死的。
设计一个不能被继承的类
- C++98:给类的构造函数为私有
主要原理:子类构造函数被调用时,必须调用父类的构造函数初始化父类的那一部分成员,但父类的私有成员在子类中是不可见的,因此该类被继承之后无法创建出对象
cpp
class NonInHerit
{
public:
static NonInHerit GetInstance()
{
return NonInHerit();
}
private:
NonInHerit()
{}
};
- C++11:C++11中提供了final关键字,被final修饰的类叫做最终类,最终类无法被继承,否则会编译报错
cpp
class A final
{
//..
}:
设计一个只能创建一个对象的类(单例模式)
单例模式: 一个类只能创建一个对象,即单例模式,该模式可以保证系统中该类只有一个实例,并提供一个访问它的全局访问点,该实例被所有程序模块共享。比如在某个服务器程序中,该服务器的配置 信息存放在一个文件中,这些配置数据由一个单例对象统一读取,然后服务进程中的其他对象再 通过这个单例对象获取这些配置信息,这种方式简化了在复杂环境下的配置管理。
单例模式一共有两种实现方式:
- 懒汉模式
- 饿汉模式
饿汉模式
饿汉模式的思想是不管你将来用不用,程序启动时就创建一个唯一的实例对象
cpp
class Singleton
{
public:
static Singleton& GetInstance()
{
return _slt;
}
//拷贝和赋值私有化
Singleton(const Singleton&) = delete;
Singleton& operator=(const Singleton&) = delete;
private:
Singleton()
{
cout << "Singleton()" << endl;
}
static Singleton _slt;
};
Singleton Singleton::_slt;
饿汉模式虽然简单,但是如果这个对象初始化内容较多(比如读文件),此时会导致进程启动慢,同时在要求单例类对象初始化存在依赖关系的场景下,饿汉模式无法保证实例化的启动顺序。一般情况下,如果这个单例对象在多线程高并发环境下频繁使用,性能要求较高,此时使用饿汉模式来避免资源竞争,提高响应速度更好,因为它在程序运行时就构建好了,运行时不用竞争。
懒汉模式
如果单例对象构造十分耗时或者占用很多资源,比如加载插件啊, 初始化网络连接啊,读取 文件啊等等,而有可能该对象程序运行时不会用到,那么也要在程序一开始就进行初始化, 就会导致程序启动时非常的缓慢。 所以这种情况使用懒汉模式(延迟加载)更好。懒汉模式的核心思想是用的时候再加载。
cpp
class Singleton
{
public:
static Singleton* GetInstance()
{
if (nullptr == _slt)
{
_slt = new Singleton();
}
return _slt;
}
// //拷贝和赋值私有化
Singleton(const Singleton&) = delete;
Singleton& operator=(const Singleton&) = delete;
private:
Singleton()
{
cout << "Singleton()" << endl;
}
private:
static Singleton* _slt;
};
Singleton* Singleton::_slt = nullptr;
但是这样是存在线程安全风险的,即有可能多个线程同时发现指针为nullptr,然后同时去new多个对象,不满足单例对象的需求,此时我们需要加锁保证多线程情况相爱只调用一次new:
cpp
class Singleton
{
public:
static Singleton* GetInstance()
{
_mtx.lock();
if (nullptr == _slt)
{
_slt = new Singleton();
}
_mtx.unlock();
return _slt;
}
// //拷贝和赋值私有化
Singleton(const Singleton&) = delete;
Singleton& operator=(const Singleton&) = delete;
~Singleton()
{
delete _slt;
}
private:
Singleton()
{
cout << "Singleton()" << endl;
}
private:
static Singleton* _slt;
static std::mutex _mtx; //这里得是静态的不然在静态成员函数无法使用
};
Singleton* Singleton::_slt = nullptr;
std::mutex Singleton::_mtx;
这样难免会造成锁冲突效率较低,即使已经有线程new出对象了,后面都要进行加锁解锁访问同步代码,,因此进一步要采用**double check** 的方式来降低锁冲突的概率以提高性能,即两层判断**静态资源指针是否为空**,在第一次new出对象之后,往后获取就直接返回指针了:
cpp
class Singleton
{
public:
static Singleton* GetInstance()
{
//double check
if (nullptr == nullptr)
{
_mtx.lock();
if (nullptr == _slt)
{
_slt = new Singleton();
}
_mtx.unlock();
}
return _slt;
}
// //拷贝和赋值私有化
Singleton(const Singleton&) = delete;
Singleton& operator=(const Singleton&) = delete;
~Singleton()
{
delete _slt;
}
private:
Singleton()
{
cout << "Singleton()" << endl;
}
private:
static Singleton* _slt;
static std::mutex _mtx; //这里得是静态的不然在静态成员函数无法使用
};
Singleton* Singleton::_slt = nullptr;
std::mutex Singleton::_mtx;
除此之外,由于我们这里单例对象是new出来的,可能U会忘记释放造成内存泄漏,因此我们还可以实现一个内嵌垃圾回收类,当程序结束时就会自动调用它的析构函数释放单例对象:
cpp
class Singleton
{
public:
static Singleton* GetInstance()
{
//double check
if (nullptr == nullptr)
{
_mtx.lock();
if (nullptr == _slt)
{
_slt = new Singleton();
}
_mtx.unlock();
}
return _slt;
}
// //拷贝和赋值私有化
Singleton(const Singleton&) = delete;
Singleton& operator=(const Singleton&) = delete;
~Singleton()
{
delete _slt;
}
// 实现一个内嵌垃圾回收类
class CGarbo {
public: ~CGarbo() {
if (Singleton::_slt)
delete Singleton::_slt;
}
};
// 定义一个静态成员变量,程序结束时,系统会自动调用它的析构函数从而释放单例对象
static CGarbo Garbo;
private:
Singleton()
{
cout << "Singleton()" << endl;
}
private:
static Singleton* _slt;
static std::mutex _mtx; //这里得是静态的不然在静态成员函数无法使用
};
Singleton* Singleton::_slt = nullptr;
std::mutex Singleton::_mtx;
Singleton::CGarbo CGarbo;
由此可见,懒汉模式虽然可以使进程启动无负载,且多个单例启动顺序自由控制,但是缺点是比较复杂,需要考虑线程安全问题。在C++11之后,支持使用静态局部变量创建对象,C++11保证静态局部对象是线程安全的,而且第一次初始化后,后续生命周期直到程序结束:
cpp
static Singleton& GetInstance()
{
static Singleton inst;
return inst;
}