• 【C++】特殊类设计


    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);
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14

    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);
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14

    image-20230919104733942

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

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

    //设计一个只能在堆上创建对象的类
    class HeapOnly
    {
    public:
        HeapOnly* CreateObj()
        {
            return new HeapOnly;
        }
    private:
        HeapOnly() {}
    };
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11

    但是这样会有一个问题:我们要怎么调用这个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;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17

    image-20230919154205975

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

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

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

    //设计一个只能在栈上创建对象的类
    class StackOnly
    {
    public:
        static StackOnly CreateObj()
        {
            return StackOnly();
        }
    private:
        StackOnly() {}
    };
    void test()
    {
        StackOnly obj1 = StackOnly::CreateObj();
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15

    这里能够禁用掉在堆上创建对象的方式,但是没有办法完全禁掉其他方式,这里可以使用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;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23

    image-20230918180301139

    4.2 C++11之后的方式

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

    class NonInherit final
    {
    
    };
    
    class Derive : NonInherit
    {
    public:
        
    private:
        int a;
    };
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12

    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;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46

    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;//初始化为空指针
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31

    image-20230921165625440

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

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

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

    static SlackerInstance* GetInstance()
    {
    
        mtx.lock();//加锁
        if(_pins == nullptr)//第一次获取单例对象的时候创建对象
        {
            _pins = new SlackerInstance;
        }
        mtx.unlock();//完成new操作之后解锁
        return _pins;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11

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

    static SlackerInstance* GetInstance()
    {
        if(_pins == nullptr)//双检查,避免无意义的加锁解锁
        {
            mtx.lock();//加锁
            if(_pins == nullptr)//第一次获取单例对象的时候创建对象
            {
                _pins = new SlackerInstance;
            }
            mtx.unlock();//完成new操作之后解锁
        }
        return _pins;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13

    但是加锁之后就会出现另一个问题: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;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21

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

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

    //RAII锁的类
    template<class Mutex>
    class LockGuard
    {
    public:
        LockGuard(Mutex& mtx)
        :_mtx(mtx)
        {
            _mtx.lock();
        }
        ~LockGuard()
        {
            _mtx.unlock();
        }
    private:
        Mutex& _mtx;//这里需要将锁设为引用的,因为锁不允许拷贝
    };
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17

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

    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;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14

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

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

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

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

    static void DelInstance()
    {
        //保存数据文件
        //TODO
    
        //回收单例对象资源
        std::lock_guard<std::mutex> lg(_mtx);
        if(_pins != nullptr)
        {
            delete _pins;
            _pins = nullptr;
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13

    当然,也可以定义一个内部的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;
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64
    • 65
    • 66

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

    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;
    };
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
  • 相关阅读:
    第十二章 旋转和横向运动
    十一、DHT11 温湿度检测(OLED显示)
    AIR-CAP2702I-H-K9/AIR-CAP3602I-H-K9刷固件讲解
    呼叫系统使用webRTC网页软电话到底好不好?
    FPGA USB host原型验证流程及调试手段
    Docker数据集与自定义镜像:构建高效容器的关键要素
    期末网页设计作业素材 (民宿 5页 带地图)
    js兼容性的汇总
    第十一章、python的异常处理------try except异常处理及其对模块Traceback的调用
    YOLOv1-v7全系列解析
  • 原文地址:https://blog.csdn.net/weixin_63249832/article/details/133156665