• 【C++】特殊类的设计


    目录

    一,不能拷贝的类

    二,只在堆区建对象的类

    三,只在栈区建对象的类

    四,不能被继承的类

    五,单例模式的设计

    5-1,饿汉模式

    5-2,懒汉模式


    介绍:

            平常应用中我们可能需要设计一些特殊的类,本文先一一说明这些最常用的设计。


    一,不能拷贝的类

            禁止拷贝的类只需让该类不能调用拷贝构造函数以及赋值运算符重载即可。C++98的做法是将拷贝构造函数与赋值运算符重载只声明不定义,并且将其访问权限设置为私有即可,两步缺一不可。若只生命不定义但却没有设置私有,那么用户可以在类外自己定义;若没有声明,编译器会默认生成。

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

    //C++98做法

    class CopyBan
    {
        // ...

    private:
        CopyBan(const CopyBan&);
        CopyBan& operator=(const CopyBan&);
        //...
    };

    //C++11做法

    class CopyBan
    {
        // ...
        CopyBan(const CopyBan&) = delete;
        CopyBan& operator=(const CopyBan&) = delete;
        //...
    };


    二,只在堆区建对象的类

    方法一:针对构造函数

            首先这里要将普通的构造函数私有化,且提供一个静态成员函数,在该静态成员函数中完成堆对象的创建。非静态成员函数由于属于类,使用时需创建类对象,显然不能使用。其次,这里还要将拷贝构造和赋值运算符禁掉,因为这些默认函数发生拷贝时是在栈区上拷贝的。

    //只在堆上建对象

    class HeapOnly
    {
    public:
        template
        static HeapOnly* CreateObj(Args&&... args) 
        {
            /*堆区new建立对象,在类中调用私有构造,因为这里的构造函数只能接收0或2个参数,这里模板参数包只能传0或2个参数*/
            return new HeapOnly(args...);
        }
        //这里必须禁掉拷贝构造和赋值禁掉
        HeapOnly(const HeapOnly&) = delete;
        HeapOnly& operator=(const HeapOnly&) = delete;
    private:
        //构造私有化
        HeapOnly()
        {}
        HeapOnly(int x, int y)
            :_x(x)
            ,_y(y)
        {}

        int _x = 0;
        int _y = 0;
    };

    int main()
    {
        //HeapOnly ho1;这里无法调用普通构造


        //new会调用构造函数,这里无法调用私有构造,必须借助静态函数
        //HeapOnly* ho2 = new HeapOnly;

        //当调用CreateObj类中new出HeapOnly对象返回时,这里使用指针接收堆区已经建立的对象空间

        HeapOnly* ho3 = HeapOnly::CreateObj();
        HeapOnly* ho4 = HeapOnly::CreateObj(1,1);

        //HeapOnly copy(*ho3);没有禁掉拷贝构造和赋值的情况下这里会在栈区上建立对象
        return 0;
    }

    方法二:针对析构函数

            这里构造函数正常使用,利用对象生命周期结束时析构函数的自动调用来控制对象不能建立。堆区建立的对象是通过特定的方式释放的,比如delete,不会自动调用析构函数,释放空间时可专门设置一个函数来释放。

    class HeapOnly
    {
    public:
        HeapOnly()
        {}
        HeapOnly(int x, int y)
            :_x(x)
            ,_y(y)
        {}
     
        /*方式一: 禁掉析构函数,使其不能自动调用,但不建议,因为析构都是要调用的,这里直接将其封死了*
        //~HeapOnly() = delete; 

        //析构函数不能自动调用,因此不能再使用delete,这里使用Destroy函数释放空间
        void Destroy()
        {
            delete this; //delete底层分两部,先调用析构然后再free这块空间
        }

    private:
        //方式二: 析构函数私有化,也是使其不能自动调用,因为其私有化
        ~HeapOnly()
        {
            cout << "~HeapOnly()" << endl;
        }

        int _x;
        int _y;
    };

    int main()
    {
        /*普通的调用均失败,因为这里生命周期结束时会自动调用析构函数,而析构函数被禁止或私有化了*/
        //HeapOnly ho1;

     
        /*堆区建立对象,只会调用构造函数,不会自动调用析构函数,若不专门释放进程结束时全部销毁*/
        HeapOnly* ptr1 = new HeapOnly;
        ptr1->Destroy(); 

        //智能指针的引入,必须使用删除器,因为这里类的析构不能调用了
        shared_ptr ptr2(new HeapOnly, [](HeapOnly* ptr2) {ptr2->Destroy(); });
        return 0;
    }


    三,只在栈区建对象的类

            这里设计的思想与上一样,只是这里不能将拷贝构造禁掉,因为这里的静态成员函数返回的是已经在类中建立好的对象,接收时会调用拷贝构造,这里编译器可能会优化成一个普通的构造,但即便如此,这里也不能违背原语法设计。

            拷贝构造没有被禁掉,这里会导致 new 出来在堆区上进行拷贝构造建立对象,这里可重载一个operator new,并将其禁掉,这意味着您不能使用标准的 new 运算符来在堆上动态创建对象。

    class StackOnly
    {
    public:
        template
        static StackOnly CreateObj(Args&&... args)
        {
            /*匿名对象构造,不能将拷贝构造禁掉,语法上是先构造再拷贝构造,即便优化了成了单单构造,但底层原理还是一样的*/
            return StackOnly(args...); 
        }

        //只将赋值运算符禁掉
        //StackOnly(const StackOnly&) = delete;

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

        /*拷贝构造没有被禁掉,可能导致堆区new一个对象,这里可采用专门重载一个类专属的operator new,然后将其禁掉,相当于不能使用new*/
        void* operator new(size_t n) = delete; //operator new的第一个形参必须是size_t类型

    private:
        //构造私有化
        StackOnly()
        {}
        StackOnly(int x, int y)
            :_x(x)
            ,_y(y)
        {}

        int _x = 0;
        int _y = 0;
    };

    int main()
    {
        StackOnly so1 = StackOnly::CreateObj();
        StackOnly so2 = StackOnly::CreateObj(1,1);

        /*拷贝构造没有被禁掉,可能导致堆区建立,这里采用禁掉重载的operaotr new解决,使其不能使用new*/
        //StackOnly* so3 = new StackOnly(so1); 

        return 0;
    }


    四,不能被继承的类

            C++98处理不能被继承的类的方式是构造函数私有化,派生类中调不到基类的构造函数,则无法继承。C++11引入 final关键字,final修饰类,表示该类不能被继承。

    //C++98做法
    class NonInherit
    {
    public:
        static NonInherit GetInstance()
        {
            return NonInherit();
        }
    private:
        NonInherit()
        {}
    };

    //C++11做法
    class NonInherit  final
    {
        // ....
    };


    五,单例模式的设计

            单例模式是指一个类只能创建一个对象,该模式可以保证系统中该类只有一个实例,并提供一个访问它的全局访问点,此实例被所有程序模块共享,整个程序在当前类中只有一份。它与设计模式比较类似,都是在整个程序中被反复使用的一种技术。

            单例模式在程序中通常用于服务作用,整个程序都依靠单例对象完成指定功能,一般情况下设计出一个静态对象来管理,而不是局部对象。

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

    5-1,饿汉模式

           饿汉的设计模式是程序启动时就创建一个唯一的实例对象,不管在整个程序运行时是否使用。

           这里的设计思路是将构造函数私有化,拷贝构造和赋值运算符禁掉,在类中声明一个类类型的私有静态成员,在类外进行初始化,通过一个函数返回类的静态对象,无论运用多少次此函数,整个类永远都只有这一个实例。

    namespace hunger
    {
        class Single
        {
        public:
            //这里返回实例是必须返回地址,不能返回对象,因为值拷贝会发生拷贝构造
            static Single* GetInstance() //通过这个全局访问点来访问
            {
                return &_sin;
            }

            void Print()
            {
                cout << _x << endl;
                cout << _y << endl;

                for (auto& e : _vstr)
                {
                    cout << e << " ";
                }
                cout << endl;
            }

            void AddStr(int x, int y, const string& s)
            {
                _x = x;
                _y = y;
                _vstr.push_back(s);
            }

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

        private:
            Single(int x = 0, int y = 0, const vector& vstr = { "yyyyy","xxxx" })
                :_x(x)
                , _y(y)
                , _vstr(vstr)
            {}

            int _x;
            int _y;
            vector _vstr;

            /*静态成员对象,不存在对象中,存在静态区,相当于全局的,定义在类中,受类域限制,整个类只有一份,无论实例化出多少个对象*/
            static Single _sin; 
        };
        //初始化类的唯一实例_sin,无论实例化出多少个对象永远只有这一个成员
        Single Single::_sin(1, 1, { "陕西","四川" }); 
    }

    测试实例:

    int main()
    {
        //hunger::Single s1; 普通构造创建失败
        //hunger::Single s2(s1); 拷贝构造创建失败


        hunger::Single* s1 = hunger::Single::GetInstance(); 
        hunger::Single* s2 = hunger::Single::GetInstance(); 

        cout << s1 << "  " << s2 << endl;

        s1->Print(); 
        s2->AddStr(10, 20, "甘肃");
        s1->Print();
        return 0;
    }

    饿汉的优缺点:

            优点:设计简单,并无复杂的逻辑。

            缺点:首先,饿汉模式可能会导致进程启动慢,如果单例对象数据较多,构造初始化成本较高,那么会影响程序启动的速度,即迟迟进不了main函数。其次,如果有多个单例类对象实例启动顺序不确定,若多个单例类有初始化启动依赖关系,饿汉将无法控制,如:A和B两个单例,假设要求A先初始化,B再初始化,饿汉无法保证。

    5-2,懒汉模式

            懒汉模式是在第一次使用实例对象时才创建对象,此模式完美解决了饿汉模式下的弊端。

            C++11之前懒汉模式的设计思路是使用时在堆区上动态开辟空间实例化出对象。由于手动释放空间有可能会出意外且某些场景下也不方便手动释放,所以这里采用内部类方式来实现自动释放空间。内部类中,这里可以设计出一个内部类类型的静态成员,当程序结束时该静态类会自动调用内部类的析构函数,我们利用这一点,在内部类的析构函数中释放外部类动态开辟的空间。

            注意:这种版本的设计还存在线程安全的问题。

    namespace lazy
    {
        class Singleton
        {
        public:
            static Singleton* GetInstance()
            {
                //防止实例化出多个对象,这里要有条件限制
                if (_psint == nullptr)
                {
                    _psint = new Singleton;
                }
                return _psint;
            }

            static void DelInstance()
            {
                if (_psint)
                {
                    //delete静态外部类,调用静态外部类的析构函数
                    delete _psint;
                    _psint = nullptr;
                }
            }

            void Print()
            {
                cout << _x << endl;
                cout << _y << endl;

                for (auto& e : _vstr)
                {
                    cout << e << " ";
                }
                cout << endl;
            }

            void AddStr(int x, int y, const string& s)
            {
                _x = x;
                _y = y;
                _vstr.push_back(s);
            }

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

        private:
            Singleton(int x = 0, int y = 0, const vector& vstr = { "yyyyy","xxxx" })
                :_x(x)
                , _y(y)
                , _vstr(vstr)
            {}
            ~Singleton()
            {
                cout << "~Singleton()" << endl;
            }

            int _x;
            int _y;
            vector _vstr;

            static Singleton* _psint;

            //采用内部类自动释放空间
            class GC
            {
            public:
                ~GC()
                {
                    //释放静态外部类开辟的动态表空间
                    Singleton::DelInstance();
                }
            };
            /*注意:要使用静态类,防止实例化出多个对象进行多次空间释放,虽然这里不影响,完全没必要*/
            static GC gc; 
        };

        Singleton* Singleton::_psint = nullptr;
        //类外定义初始化必须要写上,类内只是声明,若不定义,析构函数是不会调用的
        Singleton::GC Singleton::gc; 
    }

            C++11及之后由于新型语法的加入,这块可以很简单的设计且还能保证线程安全——直接在函数内部创建静态局部对象,使用时直接自动初始化且没有设计动态空间的开辟。由于大部分代码与上面都是一样的,这里不再全部写入。

    namespace lazy
    {

        //C++1及之后设计
        class Singleton
        {
        public:
            static Singleton* GetInstance()
            {
                //局部的静态对象,第一次调用函数时构造初始化
                static Singleton _sinst;  //注意:只有C++11及之后才支持此种语法
                return &_sinst;
            }

            ..........

        private:
            ..........
            /*由于是在GetInstance()函数内部中创建的静态类对象_sinst,所以当程序结束时,内部静态对象_sinst会调用私有的析构函数*/

            ~Singleton()
            {
                cout << "~Singleton()" << endl;
            }

            int _x;
            int _y;
            vector _vstr;
        };

    }

    测试实例:

    int main()
    {
        lazy::Singleton* s1 = lazy::Singleton::GetInstance();
        lazy::Singleton* s2 = lazy::Singleton::GetInstance();
        cout << s1 << "  " << s2 << endl;

        s1->Print();

        s2->AddStr(10, 20, "甘肃");
        s1->Print();

        //显示释放内存,若不自己调用结束时会自动调用
        //s2->DelInstance();

        cout << "xxxxxxxxxxxxxxxxxxxxxxxxxxx" << endl;
        return 0;
    }

            下面的输出样例适应于上面两种版本,输出都相同。 

    懒汉的优缺点:

           优点:解决了饿汉程序启动时可能导致的负载情况,且多个单例实例启动的顺序可以自由控制

           缺点:比较复杂

  • 相关阅读:
    租用独立服务器有哪些常见的误区?
    19:A*B问题
    弹性盒子自动换行小Demo
    关于软件设计师考试中的算法
    电脑截图怎么转换成文字?学会这个方法,轻松实现
    基于语义分割的相机外参标定
    VMware vCenter Server 7 升级
    将字体颜色设置为渐变色 --字体倾斜--数组转字符串--旋转(一些样式的设置)
    rust切片
    电子电气架构 --- 关于DoIP的一些闲思 下
  • 原文地址:https://blog.csdn.net/m0_74246469/article/details/139334945