【C++】特殊类设计

🦄个人主页 :修修修也

🎏所属专栏 :C++

⚙️操作环境 :Visual Studio 2022


目录

📌不能被拷贝的类

设计思路

具体实现

📌只能在堆上创建对象的类

设计思路

具体实现

📌只能在栈上创建对象的类

设计思路

具体实现

📌不能被继承的类

设计思路

具体实现

📌只能创建一个对象的类(单例模式)

什么是设计模式

什么是单例模式

饿汉模式

懒汉模式

结语


在日常的开发中, 我们可能会遇到以下几种类的设计需求, 比如我们要求这个类不能被拷贝, 又或者这个类只能在堆/栈上创建对象等等。下面为大家介绍一下这些类的设计思路:

📌不能被拷贝的类

设计思路

拷贝只会发生在两个场景中: 拷贝构造函数以及赋值运算符重载, 因此想让一个类禁止拷贝, 只需要让该类不能调用拷贝构造函数以及赋值运算符重载即可。

具体实现

C++98版:

首先我们要知道, 拷贝构造和赋值运算符重载如果我们自己不手动实现, 那么系统是会默认给自动生成的, 为了防止系统自动生成的导致可以使用构造函数和赋值运算符重载, 所以我们要自己手动实现后再禁用。

具体做法就是: 将拷贝构造函数与赋值运算符重载只声明不定义,并且将其访问权限设置为私有即可。

原因:

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

能禁止拷贝了

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

具体实现代码:

cpp 复制代码
class CopyBan
{
public:
	CopyBan(int val)
		:a(val)
	{}
    //...

private:
	CopyBan(const CopyBan&);
	CopyBan& operator=(const CopyBan&);

	int a;
    //...
};

测试结果:可以看到, 禁用后该类确实无法被拷贝了


C++11版:

C++11版思路和C++98一样, 只是C++11扩展了delete的用法,delete除了释放new申请的资源外,如果在默认成员函数后跟上=delete,表示让编译器删除掉该默认成员函数。

具体实现代码:

cpp 复制代码
class CopyBan
{
public:
	CopyBan(int val)
		:a(val)
	{}

	CopyBan(const CopyBan&) = delete;
	CopyBan& operator=(const CopyBan&) = delete;

	int a;
};

测试结果: 可以看到删除后该类同样无法进行拷贝了


📌只能在堆上创建对象的类

设计思路

要让对象只能在堆上创建, 核心是要阻止栈/全局/静态对象的构造, 因此我们的思路1是:

  1. 将类的构造函数私有,拷贝构造声明成私有。防止别人调用拷贝在栈上生成对象。

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

思路2是:

因为栈区/全局/静态的内存空间都是由操作系统自动管理的, 所以这三个地方创建的对象出了生命周期会自动调用析构函数, 那我们直接把析构函数私有化, 这样系统检测不到析构函数, 自然就不会允许在栈区/全局/静态区创建对象了。

具体实现

思路1实现代码:

cpp 复制代码
//只能在堆上创建
class HeapOnly
{
public:
	static HeapOnly* CreateObject(int val)
	{
		return new HeapOnly(val);
	}
private:
	HeapOnly(int val)
		:a(val)
	{}
	HeapOnly(const HeapOnly&) = delete;
	int a;
};

测试结果: 可以看到, 除了调用封装的固定Create接口在堆上创建对象, 其他任何形式想创建栈上的或全局的或静态的都不可以。

思路2实现代码:

cpp 复制代码
class HeapOnly
{
public:
	HeapOnly(int val)
		:a(val)
	{}
	void Destory(){
		delete this;
	}
private:
	~HeapOnly()
	{}
	int a;
};

测试结果:


📌只能在栈上创建对象的类

设计思路

要让对象只能在栈上创建, 核心是要禁用堆内存分配操作,同时允许栈内存分配, 因此我们的思路是:

  1. 将类的构造函数私有,禁用operator new和operator delete函数。防止别人调用在堆上生成对象。

  2. 提供一个静态的成员函数,在该静态成员函数中完成栈对象的创建并返回给外部。

具体实现

实现代码:

cpp 复制代码
//只能在栈上创建
class StackOnly
{
public:
	static StackOnly CreateObject(int val)
	{
		StackOnly st(val);
		return st;
	}

	void* operator new(size_t size) = delete;
	void operator delete(void* p) = delete;
private:
	StackOnly(int val)
		:a(val)
	{}
	int a;
};

测试结果:


📌不能被继承的类

设计思路

C++98实现思路:

将基类构造函数私有化, 因为规定派生类初始化时必须调用基类的构造函数, 那将基类构造函数私有化之后, 派生类无法访问基类构造函数, 于是也无法构造对象, 故无法继承。

C++11实现思路:

使用final关键字, final关键字修饰的类无法被继承。

具体实现

c++98实现:

cpp 复制代码
//不能被继承的类
class NonInherit
{
public:
	static NonInherit GetInstance()
	{
		return NonInherit();
	}
private:
	NonInherit()
	{}
};

测试结果: 果然派生类无法正常使用构造函数创建对象:

C++11实现:

cpp 复制代码
class NonInherit final
{
public:
	NonInherit()
	{}
};

测试结果:


📌只能创建一个对象的类(单例模式)

什么是设计模式

设计模式(Design Pattern)是一套被反复使用、多数人知晓的、经过分类的、代码设计经验的总结。

使用设计模式的目的:为了代码可重用性、让代码更容易被他人理解、保证代码可靠性。 设计模式使代码编写真正工程化;设计模式是软件工程的基石脉络,如同大厦的结构一样。

什么是单例模式

一个类只能创建一个对象,即单例模式,该模式可以保证系统中该类只有一个实例,并提供一个访问它的全局访问点,该实例被所有程序模块共享。

单例模式的设计思想首先就是将构造函数私有化, 外部不能够随意创建实例, 只能通过给定的接口去拿到那个唯一的实例, 而这个唯一的实例是通过静态成员来实现的。

就比如我们日常开发中经常使用到的日志类, 就是一个很适合使用单例模式来实现的类。即全局只需要一个log对象, 后续有任何日志信息需要输出, 就直接调用那一个唯一的全局log对象即可。针对于唯一实例创建的时机不同, 单例模式有两种实现模式, 饿汉模式和懒汉模式, 下面分别为大家介绍一下:

饿汉模式

饿汉模式, 顾名思义就是我有食物我就去吃。对应到单例模式的设计里, 就是我不管后续会不会用到这个单例, 程序启动时就去把这个唯一的实例对象创建出来, 这是饿汉模式对于单例创建的一个设计思想。

具体到实际代码的实现上, 我们就选择将静态成员声明在类里, 直接在全局定义,这样程序一启动这个实例就会被初始化。

注意, 饿汉模式思想的优点是: 实现比较简单, 不需要考虑多线程中单例模式的安全问题。

但饿汉模式思想的缺点是: 如果这个实例创建后没有被使用, 并且这个实例自身占用了非常多的资源的话, 那么将会大大拖慢程序的启动速度, 从而导致程序性能下降。并且如果有两个单例类A和B, B是依赖A实例化的, 那么饿汉模式是无法保证它们的实例化顺序的, 这样就可能会导致实例化失败, 同样也是饿汉模式的一大无法避免的问题。
实现代码:

cpp 复制代码
//饿汉模式
class EagerSingleton 
{
public:
    static EagerSingleton& getInstance() 
    {
        return _instance; // 直接返回已初始化的静态实例
    }

    void doSomething() 
    { /* 操作单例对象的业务方法 */ }

    // 禁用拷贝和赋值保证单例特性
    EagerSingleton(const EagerSingleton&) = delete;
    EagerSingleton& operator=(const EagerSingleton&) = delete;

private:
    EagerSingleton() = default; // 私有构造函数
    ~EagerSingleton() = default;

    static EagerSingleton _instance; // 静态成员声明
};

// 静态成员类外定义
EagerSingleton EagerSingleton::_instance;


int main()
{
    EagerSingleton::getInstance().doSomething();

    return 0;
}

懒汉模式

懒汉模式, 顾名思义就是我饿了我再去吃。对应到单例模式的设计里, 就是我后续用到这个单例了, 我再去创建这个唯一的实例对象。这就是懒汉模式对于单例创建的一个设计思想。

具体到实际代码的实现上,我们选择将静态成员设置为一个单例对象指针, 然后在get函数里再去实现如果第一次被调用就new一个实例出来, 从而做到了使用时实例化。

注意, 懒汉模式思想的优点是: 不会拖慢程序的启动速度, 只有有需要的时候才会去初始化, 所以程序性能会比饿汉模式要高。并且因为是根据需要初始化的, 所以可以保证不同单例初始化的前后顺序。

但饿汉模式思想的缺点 是: 实现比较难, 需要考虑多线程中创建单例模式的安全问题
常规实现代码:

cpp 复制代码
//懒汉模式
class LazySingleton
{
public:
    static LazySingleton& getInstance()
    {
        if (_pinstance == nullptr)//双重检查, 只有第一次创建实例时才加锁
        {
            unique_lock<mutex> lock(_mtx);
            if (_pinstance == nullptr)//如果是第一次调用,则创建实例
            {
                _pinstance = new LazySingleton;
            }
        }
        return *_pinstance; // 返回已初始化的静态实例
    }

    //防止需要显示调用释放单例
    static void DelInstance()
    {
        if (_pinstance)
        {
            delete _pinstance;
            _pinstance = nullptr;
        }
    }

    void doSomething()
    { /* 操作单例对象的业务方法 */}

    // 禁用拷贝和赋值保证单例特性
    LazySingleton(const LazySingleton&) = delete;
    LazySingleton& operator=(const LazySingleton&) = delete;

private:
    LazySingleton() = default; // 私有构造函数
    ~LazySingleton() = default;

    static LazySingleton* _pinstance; // 静态成员声明
    static mutex _mtx;
};

// 静态成员类外定义
LazySingleton* LazySingleton::_pinstance;

C++11之后的编译器保证局部静态变量的初始化是线程安全的, 极简实现代码:

cpp 复制代码
//懒汉模式
class LazySingleton
{
public:
    static LazySingleton& getInstance()
    {
        //局部的静态对象, 是在第一次调用时初始化
        //C++11之前这里不是线程安全, C++11之后可以保证局部静态对象的初始化是线程安全的
        static LazySingleton inst;
        return inst; 
    }

    void doSomething()
    { /* 操作单例对象的业务方法 */}

    // 禁用拷贝和赋值保证单例特性
    LazySingleton(const LazySingleton&) = delete;
    LazySingleton& operator=(const LazySingleton&) = delete;

private:
    LazySingleton() = default; // 私有构造函数
    ~LazySingleton() = default;
};

结语

希望这篇关于 特殊类设计 的博客能对大家有所帮助,欢迎大佬们留言或私信与我交流.

学海漫浩浩,我亦苦作舟!关注我,大家一起学习,一起进步!

相关文章推荐

【C++11】智能指针

【C++11】左值引用、右值引用、移动语义和完美转发

【C++】STL标准模板库容器set

【C++】模拟实现二叉搜索(排序)树

【C++】模拟实现priority_queue(优先级队列)

【C++】模拟实现queue

【C++】模拟实现stack

【C++】模拟实现list

【C++】模拟实现vector

【C++】标准库类型vector

【C++】模拟实现string类

【C++】标准库类型string

【C++】构建第一个C++类:Date类

【C++】类的六大默认成员函数及其特性(万字详解)

【C++】什么是类与对象?


相关推荐
weixin_42849849几秒前
在Lua中使用轻量级userdata在C/C++之间传递数据和调用函数
c语言·c++·lua
爱看书的小沐2 分钟前
【小沐学GIS】基于C++绘制二维瓦片地图2D Map(QT、OpenGL、GIS)
c++·qt·gis·opengl·glfw·glut·二维地图
名字不要太长 像我这样就好7 分钟前
【iOS】源码阅读(二)——NSObject的alloc源码
开发语言·macos·ios·objective-c
追逐梦想之路_随笔26 分钟前
gvm安装go报错ERROR: Failed to use installed version
开发语言·golang
海风极客28 分钟前
《Go小技巧&易错点100例》第三十三篇
开发语言·后端·golang
coding_rui33 分钟前
C++模板笔记
c++·模板·类模板
C++ 老炮儿的技术栈34 分钟前
C++中什么是函数指针?
c语言·c++·笔记·学习·算法
再睡一夏就好38 分钟前
C语言常见的文件操作函数总结
c语言·开发语言·c++·笔记·学习笔记
喜欢便码1 小时前
xml与注解的区别
xml·java·开发语言
一舍予1 小时前
八股文-js篇
开发语言·前端·javascript