【C++】--- 特殊类设计

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;
};

说明:

  1. 设置成私有:如果只声明没有设置成private,用户自己如果在类外定义了,就可以不能禁止拷贝了。

  2. 只声明不定义:不定义是因为该函数根本不会调用,定义了其实也没有什么意思,不写反而还简单,而且如果定义了就不会防止成员函数内部拷贝了。

  • 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;
};

设计一个只能在堆上创建的类

  • 方式一:挡住前路
  1. 用C++98或C++11的方式将构造函数私有或移除默认构造,防止外部调用构造函数在栈上创建对象

  2. 用C++98或C++11的方式将拷贝构造函数私有或移除默认拷贝构造,防止外部调用构造函数在栈上创建对象

  3. 向外部提供一个静态成员函数,在该函数中完成对堆对象的创建

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;

};
  • 方式二:挡住后路
  1. 将析构函数私有化,这样调用构造或拷贝构造在栈上创建的对象/静态对象,生命周期自动销毁时就调不了析构

  2. 需要专门为在堆上创建的对象设计一个接口来释放堆上的对象,因为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;
}
相关推荐
码事漫谈2 小时前
十字路口的抉择:B端与C端C++开发者的职业路径全解析
后端
sali-tec2 小时前
C# 基于halcon的视觉工作流-章68 深度学习-对象检测
开发语言·算法·计算机视觉·重构·c#
提笔了无痕3 小时前
git基本了解、常用基本命令与使用
git·后端
java1234_小锋4 小时前
Spring IoC的实现机制是什么?
java·后端·spring
喵个咪4 小时前
开箱即用的 GoWind Admin|风行,企业级前后端一体中后台框架:JWT 集成指南
后端·go
生骨大头菜4 小时前
使用python实现相似图片搜索功能,并接入springcloud
开发语言·python·spring cloud·微服务
绝不收费—免费看不了了联系我4 小时前
Fastapi的单进程响应问题 和 解决方法
开发语言·后端·python·fastapi
喵个咪4 小时前
开箱即用的 GoWind Admin|风行,企业级前后端一体中后台框架:OPA 集成指南:从原理到实践
后端·go
消失的旧时光-19434 小时前
深入理解 Java 线程池(二):ThreadPoolExecutor 执行流程 + 运行状态 + ctl 原理全解析
java·开发语言