【C++】特殊类设计

2023-09-22 08:30:46

1. 设计一个类,不能被拷贝

如果一个类不能被拷贝,那么只需要让类外面不能调用类的拷贝构造和赋值重载即可

1. C++98的实现方式

将类的构造函数和赋值重载只声明不实现,并且声明为private

因为不声明的编译器会默认生成public的拷贝构造和赋值重载,因此需要声明为private的

//设计一个类不能被拷贝
class NoCopy
{
public:
    NoCopy() {}
private:
    NoCopy(const NoCopy& obj);
    NoCopy& operator=(const NoCopy& obj);
};
void test1()
{
    NoCopy obj1;
    NoCopy obj2(obj1);
}

image-20230919104530201

2. C++11的实现方式

C++11提供了新的关键字delete,用于表示禁用某个函数,那么此时我们只需要禁用拷贝构造和赋值重载即可

class NoCopy
{
public:
    NoCopy() {}
    NoCopy(const NoCopy& obj) = delete;
    NoCopy& operator=(const NoCopy& obj) = delete;
private:

};
void test1()
{
    NoCopy obj1;
    NoCopy obj2(obj1);
}

image-20230919104733942

2. 设计一个类,只能在堆上创建对象

要规定某个类的创建位置,那么自动生成的构造函数就不能再使用了,而是重新提供一个方法,用于创建对象,在这个方法内部进行一些限制即可为了保证这个类创建的对象全部都在堆上,所以需要将所有的构造函数全部封起来,然后重新提供一个函数用来创建对象即可

//设计一个只能在堆上创建对象的类
class HeapOnly
{
public:
    HeapOnly* CreateObj()
    {
        return new HeapOnly;
    }
private:
    HeapOnly() {}
};

但是这样会有一个问题:我们要怎么调用这个CreateObj函数呢?

这是一个先有鸡还是先有蛋的问题:我们得通过对象来调用类的成员函数,但是这里需要先调用成员函数来创建对象,所以这里要给CreateObj函数使用static修饰,让其可以通过类名访问(HeapOnly::CreateObj()

同时,我们还需要将拷贝构造给私有化,否则将会出现HeapOnly obj2 = *pobj;的情况,会构造一个obj2是在栈上的

所以最终的设计代码为:

//设计一个只能在堆上创建对象的类
class HeapOnly
{
public:
    static HeapOnly* CreateObj()
    {
        return new HeapOnly;
    }
private:
    HeapOnly() {}
    HeapOnly(const HeapOnly& obj) {};
};
void test()
{
    HeapOnly* pobj =  HeapOnly::CreateObj();
    HeapOnly obj2 = *pobj;
}

image-20230919154205975

当然,在C++11之后,我们可以使用delete关键字禁用拷贝构造函数

3. 设计一个类,只能在栈上创建对象

同样的,需要将默认构造函数私有化,然后重新提供一个方法,用于创建对象,在这个方法内部进行一些限制

//设计一个只能在栈上创建对象的类
class StackOnly
{
public:
    static StackOnly CreateObj()
    {
        return StackOnly();
    }
private:
    StackOnly() {}
};
void test()
{
    StackOnly obj1 = StackOnly::CreateObj();
}

这里能够禁用掉在堆上创建对象的方式,但是没有办法完全禁掉其他方式,这里可以使用static修饰对象,让其在静态区创建static StackOnly obj1 = StackOnly::CreateObj();

如果想禁止在静态区创建的话,这里可以考虑禁止拷贝构造

但是禁止了拷贝构造之后就不能使用StackOnly obj1 = StackOnly::CreateObj();这种方式调用构造对象了,因此,这个类我们是封不死的,最多只能限制不这样使用

4. 设计一个类不能被继承

4.1C++98的方式

构造函数私有化,所有派生类在构造对象的时候都会自动调用基类的构造函数,基类的构造函数被私有化之后派生类构造对象的时候无法自动调用。

class NonInherit
{
public:
    static NonInherit CreateObj()
    {
        return NonInherit();
    }
private:
    NonInherit() {}
};

class Derive : NonInherit
{
public:
    
private:
    int a;
};

void test1()
{
    Derive De;
}

image-20230918180301139

4.2 C++11之后的方式

C++11新增了final关键字,final修饰类,表示该类不能被继承

class NonInherit final
{

};

class Derive : NonInherit
{
public:
    
private:
    int a;
};

image-20230918180500850

5. 设计一个类,只能创建一个对象(单例模式)重点

5.1 设计模式

设计模式(Design Pattern)是一套被反复使用、多数人知晓的、经过分类的、代码设计经验的总结。为什么会有设计模式这种东西的出现呢?

最开始的代码设计是没有一定模式的,大家都是随便写的,写的多了就发现了一些套路,最终这些套路就被总结成了设计模式。

使用设计模式的目的:

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

5.2 单例模式

一个类只能创建一个对象,即单例模式,该模式可以保证系统中该类只有一个实例,并提供一个 访问它的全局访问点,该实例被所有程序模块共享。比如在某个服务器程序中,该服务器的配置 信息存放在一个文件中,这些配置数据由一个单例对象统一读取,然后服务进程中的其他对象再 通过这个单例对象获取这些配置信息,这种方式简化了在复杂环境下的配置管理。

单例模式有两种实现方式:饿汉模式he

5.2.1 饿汉模式

这里的饿汉是一个形象的说法,把程序比做一个饿汉,在main函数开始前就创建这个单例对象,就像一个饿汉一样。

饿汉模式的实现原理就是:把所有的构造都私有化或者delete掉,然后重新提供一个GetInstance方法用于获取这个单例

//单例模式
//懒汉模式
class Singleton
{
public:
    static Singleton& GetInstance()//获取这个单例对象
    {
        return _ins;
    }
    //一些对应的数据操作
    void Insert(pair<string, int> val)
    {
        _mp.insert(val);
    }
    void Print()
    {
        for(auto& kv : _mp)
        {
            cout << kv.first << ":" << kv.second << endl;
        }
    }
private:
    Singleton() {}//把构造函数私有化
    Singleton(const Singleton&) = delete;//删除拷贝构造和赋值重载
    Singleton operator=(const Singleton&) = delete;
private:
    static Singleton _ins;//一个静态的“全局变量”,用于在类外访问到构造函数的
    //单例类的一些数据
    map<string,int> _mp;
};

Singleton Singleton::_ins;//在程序开始执行main函数之前就已经构造对象

int main()
{
    auto& ins1 = Singleton::GetInstance();//调用的时候使用GetInstance给单例对象取别名
    ins1.Insert({"sort", 1});
    
    auto& ins2 = Singleton::GetInstance();
    ins2.Insert({"string", 3});
    
    auto& ins3 = Singleton::GetInstance();
    ins3.Print();
    
    return 0;
}

image-20230921112524743

饿汉模式下,单例对象在main函数被调用之前就已经构造,所以不存在线程安全的问题,但是同时也存在一些缺点

  • 有的单例对象构造十分耗时或者需要占用很多资源,比如加载插件、 初始化网络连接、读取文件等等,会导致程序启动时加载速度慢
  • 饿汉模式在程序启动时就创建了单例对象,所以即使在程序运行期间并没有用到该对象,它也会一直存在于内存中,浪费了一定的系统资源
  • 当多个单例类存在初始化依赖关系时,饿汉模式无法控制。比如A、B两个单例类存在于不同的文件中,我们要求先初始化A,再初始化B,但是A、B谁先启动初始化是由OS自动进行调度控制的,我们无法进行控制。

5.2.2 懒汉模式

除了饿汉模式之外,还有一种单例模式的实现方法是懒汉模式,所谓懒汉模式就是在第一次使用到单例对象的时候再构造

class SlackerInstance
{
public:
    static SlackerInstance* GetInstance()
    {
        if(_pins == nullptr)
        {
            _pins = new SlackerInstance;
        }
        return _pins;
    }
    void Insert(pair<string, int> val)
    {
        _mp.insert(val);
    }
    void Print()
    {
        for(auto& kv : _mp)
        {
            cout << kv.first << ":" << kv.second << endl;
        }
    }
private:
    SlackerInstance() {}//构造函数私有化
    SlackerInstance(const SlackerInstance&) = delete;//删除拷贝构造和赋值重载
    SlackerInstance operator=(const SlackerInstance&) = delete;
private:
    static SlackerInstance* _pins;//静态的单例对象指针
    map<string, int> _mp;
};
SlackerInstance* SlackerInstance::_pins = nullptr;//初始化为空指针

image-20230921165625440

找bug环节:请找出上述代码的bug

  • 线程安全问题:GetInstance函数不是线程安全的,内部的new不是原子性操作;

问题的解决:在函数内部加锁

static SlackerInstance* GetInstance()
{

    mtx.lock();//加锁
    if(_pins == nullptr)//第一次获取单例对象的时候创建对象
    {
        _pins = new SlackerInstance;
    }
    mtx.unlock();//完成new操作之后解锁
    return _pins;
}

但是,此时的代码还不够完美,每次调用GetInstance函数的时候都会进行无意义的加锁解锁操作,所以这里可以使用一种双检查的方法,在锁外层再进行一次判断

static SlackerInstance* GetInstance()
{
    if(_pins == nullptr)//双检查,避免无意义的加锁解锁
    {
        mtx.lock();//加锁
        if(_pins == nullptr)//第一次获取单例对象的时候创建对象
        {
            _pins = new SlackerInstance;
        }
        mtx.unlock();//完成new操作之后解锁
    }
    return _pins;
}

但是加锁之后就会出现另一个问题:new的过程中可能抛异常,此时就没有解锁,所以这里需要捕获异常进行解锁,然后重新抛出

static SlackerInstance* GetInstance()
{
    if(_pins == nullptr)//双检查,避免无意义的加锁解锁
    {
        mtx.lock();//加锁
        try
        {
            if(_pins == nullptr)//第一次获取单例对象的时候创建对象
            {
                _pins = new SlackerInstance;
            }
        }
        catch(...)//捕获异常并重新抛出
        {
            mtx.unlock();
            throw;
        }
        mtx.unlock();//完成new操作之后解锁
    }
    return _pins;
}

但是这样写看起来很low,还是追求高级的,优雅的写法

这里我们使用RAII的思想实现对锁的自动管理

//RAII锁的类
template<class Mutex>
class LockGuard
{
public:
    LockGuard(Mutex& mtx)
    :_mtx(mtx)
    {
        _mtx.lock();
    }
    ~LockGuard()
    {
        _mtx.unlock();
    }
private:
    Mutex& _mtx;//这里需要将锁设为引用的,因为锁不允许拷贝
};

当然,在库里面也实现了相关的类

image-20230921173425930

static SlackerInstance* GetInstance()
{
    if(_pins == nullptr)//双检查,避免无意义的加锁解锁
    {
        //_mtx.lock();//加锁
        LockGuard<std::mutex> lg(_mtx);
        if(_pins == nullptr)//第一次获取单例对象的时候创建对象
        {
            _pins = new SlackerInstance;
        }
        //_mtx.unlock();//完成new操作之后解锁
    }
    return _pins;
}

单例对象的资源释放与数据保存

问题一:单例对象是new出来的需要进行释放吗?

由于单例对象的创建是全局的,全局资源在程序结束后会被自动回收 (进程退出后OS会解除进程地址空间与物理内存的映射)。但是我们也可以手动对其进行回收。需要注意的是,有时我们需要在回收资源之前将资源的相关数据保存到文件中,这种情况下我们就必须手动回收了。

我们可以在类中定义一个静态的 DelInstance 接口来回收与保存资源 (此函数不会被频繁调用,因此不需要使用双检查加锁)。

static void DelInstance()
{
    //保存数据文件
    //TODO

    //回收单例对象资源
    std::lock_guard<std::mutex> lg(_mtx);
    if(_pins != nullptr)
    {
        delete _pins;
        _pins = nullptr;
    }
}

当然,也可以定义一个内部的GC类,其在程序结束时自动调用析构函数

所以最终的实现方式如下

class SlackerInstance
{
public:
    static SlackerInstance* GetInstance()
    {
        if(_pins == nullptr)//双检查,避免无意义的加锁解锁
        {
            //_mtx.lock();//加锁
            LockGuard<std::mutex> lg(_mtx);
            if(_pins == nullptr)//第一次获取单例对象的时候创建对象
            {
                _pins = new SlackerInstance;
            }
            //_mtx.unlock();//完成new操作之后解锁
        }
        return _pins;
    }
    static void DelInstance()
    {
        //保存数据文件
        //TODO
        
        //回收单例对象资源
        std::lock_guard<std::mutex> lg(_mtx);
        if(_pins != nullptr)
        {
            delete _pins;
            _pins = nullptr;
        }
    }
    void Insert(pair<string, int> val)
    {
        _mp.insert(val);
    }
    void Print()
    {
        for(auto& kv : _mp)
        {
            cout << kv.first << ":" << kv.second << endl;
        }
    }
    class GC
    {
    public:
        ~GC()
        {
            if(_pins)
            {
                cout << "~GC()" << endl;
                DelInstance();
            }
        }
    };
private:
    SlackerInstance() {}//构造函数私有化
    SlackerInstance(const SlackerInstance&) = delete;//删除拷贝构造和赋值重载
    SlackerInstance operator=(const SlackerInstance&) = delete;
private:
    static SlackerInstance* _pins;//静态的单例对象指针
    static mutex _mtx;//互斥锁
    static GC _gc;//自动回收
    map<string, int> _mp;
};
SlackerInstance* SlackerInstance::_pins = nullptr;//初始化为空指针
mutex SlackerInstance::_mtx;
SlackerInstance::GC SlackerInstance::_gc;

另一种版本的懒汉模式的写法

class SlackerInstance2
{
public:
    static SlackerInstance2& GetInstance()
    {
        static SlackerInstance2 ins;//使用static修饰吗,第一次调用的时候构建对象,再次调用就直接使用
        return ins;
    }
    void Insert(pair<string, int> val)
    {
        _mp.insert(val);
    }
    void Print()
    {
        for(auto& kv : _mp)
        {
            cout << kv.first << ":" << kv.second << endl;
        }
    }
private:
    SlackerInstance2(){}
    SlackerInstance2(const SlackerInstance2&) = delete;
    SlackerInstance2 operator=(const SlackerInstance2&) = delete;
private:
    map<string, int> _mp;
};
更多推荐

设计模式之命令模式

文章目录智能生活项目需求命令模式基本介绍命令模式的原理类图命令模式解决智能生活项目命令模式的注意事项和细节智能生活项目需求看一个具体的需求我们买了一套智能家电,有照明灯、风扇、冰箱、洗衣机,我们只要在手机上安装app就可以控制对这些家电工作。这些智能家电来自不同的厂家,我们不想针对每一种家电都安装一个App,分别控制,

opencv实现仿射变换

什么是仿射变换?代码实现importnumpyasnpimportcv2ascvimportmatplotlib.pyplotasplt#设置字体frompylabimportmplmpl.rcParams['font.sans-serif']=['SimHei']#图像的读取img=cv.imread("lena.p

K8S ingress nginx性能优化

nginx性能主要优化参数:worker_connections和worker_processes是Nginx配置中的两个重要参数,用于控制Nginx服务器的性能和并发连接处理能力。worker_connections:worker_connections参数用于指定每个Nginx工作进程(workerprocess)

OpenCV自学笔记十五:图像轮廓

目录1、查找并绘制轮廓2、矩特征3、Hu矩4、轮廓拟合5、凸包1、查找并绘制轮廓在OpenCV中,可以使用`cv2.findContours()`函数来查找图像中的轮廓,并使用`cv2.drawContours()`函数将轮廓绘制到图像上。下面是一个示例代码:importcv2#读取图像并转换为灰度图像image=cv

AI项目八:yolo5+Deepsort实现目标检测与跟踪(CPU版)

若该文为原创文章,转载请注明原文出处。一、DeepSORT简介DeepSORT是一种计算机视觉跟踪算法,用于在为每个对象分配ID的同时跟踪对象。DeepSORT是SORT(简单在线实时跟踪)算法的扩展。DeepSORT将深度学习引入到SORT算法中,通过添加外观描述符来减少身份切换,从而提高跟踪效率。这是提供两个dem

学习如何编码

在学习编码时感受到一些失败●他在编码旅途之初并没有一个明确的目标;●他从看课程和阅读教程开始,但他只会复制代码,而不关心它是如何工作的。有时候他会复制粘贴代码●他没有通过做小挑战或记笔记来强化他正在学习的东西●他没有练习编程,也没有提出自己的项目想法●当他的代码不是非常干净或高效时,他很快就感到沮丧●他失去了动力,因为

计算机视觉与深度学习-经典网络解析-GoogLeNet-[北邮鲁鹏]

这里写目录标题GoogLeNet参考GoogLeNet模型结构创新点Inception结构,它能保留输入信号中的更多特征信息去掉了AlexNet的前两个全连接层,并采用了平均池化引入了辅助分类器GoogLeNetGoogLeNet的设计主要特点是引入了Inception模块,这是一种多尺度卷积结构,可以在不同尺度下进行

Leetcode.146 LRU 缓存

题目链接Leetcode.146LRU缓存mid题目描述请你设计并实现一个满足LRU(最近最少使用)缓存约束的数据结构。实现LRUCache类:LRUCache(intcapacity)以正整数作为容量capacitycapacitycapacity初始化LRU缓存intget(intkey)如果关键字keykeyke

JVM-环境准备&性能指标&基础知识

环境准备&性能指标&基础知识环境准备JDK—工具JDK(JavaDevelopmentKit)是用于开发Java应用程序的软件开发工具集合,包括了Java运行时的环境(JRE)、解释器(Java)、编译器(javac)、Java归档(jar)、文档生成器(Javadoc)等工具。简单的说我们要开发Java程序,就需要安

五十二.PPO算法原理和实战

基于表格的方法:动态规划法、蒙特卡罗法、时序差分法等。基于值函数近似的方法:DQN及其改进方法。两类方法都基本遵循了“策略评估-策略改进”交替循环的算法框架。基于值函数的算法在实际应用中也存在一些不足,如算法难以高效处理连续动作空间任务和只能处理确定性策略而不能处理随机策略等。强化学习的最终目标是获得最优策略。将策略本

这些PLC项目调试常见错误类型,你都了解吗?

各种品牌PLC都具有自我诊断功能,但PLC修理的技巧在于,充分运用该功能进行分析,然后精确寻找问题所在。整理了当PLC呈现反常报警时,PLC修理人员需要了解的8种常见错误类型。CPU反常CPU反常报警时,应查看CPU单元衔接于内部总线上的一切器材。具体方法是顺次替换可能存在问题的单元,找出问题单元,并作相应处理。存储器

热文推荐