• C++——单例模式的原理及实现


    C++——单例模式的原理及实现

    (一)定义

      单例模式,属于创建类型的一种常用的软件设计模式。通过单例模式的方法创建的类在当前进程中只有一个实例(根据需要,也有可能一个线程中属于单例,如:仅线程上下文内使用同一个实例)。

    (二)简介

      单例模式是设计模式中最简单的形式之一。这一模式的目的是使得类的一个对象成为系统中的唯一实例。要实现这一点,可以从客户端对其进行实例化开始。因此需要用一种只允许生成对象类的唯一实例的机制,“阻止”所有想要生成对象的访问。使用工厂方法来限制实例化过程。这个方法应该是静态方法(类方法),因为让类的实例去生成另一个唯一实例毫无意义。这是百度的解释,以我个人的观点来说的话,其实就是在整个程序中整个类只能实例化出一个对象。通俗来讲,就是在某些场景下,我们之能有一个对象,例如,一个系统中可以存在多个打印任务,但是只能有一个正在工作的任务;一个系统只能有一个窗口管理器或文件系统;一个系统只能有一个计时工具或ID(序号)生成器。如在Windows中就只能打开一个任务管理器。如果不使用机制对窗口对象进行唯一化,将弹出多个窗口,如果这些窗口显示的内容完全一致,则是重复对象,浪费内存资源;如果这些窗口显示的内容不一致,则意味着在某一瞬间系统有多个状态,与实际不符,也会给用户带来误解,不知道哪一个才是真实的状态。因此有时确保系统中某个对象的唯一性即一个类只能有一个实例非常重要。

    (三)具体实现

      首先,我们先大致讲下我们要用到的知识:静态成员变量、静态成员函数

      1.静态成员变量

      静态变量(Static Variable)在计算机编程领域指在程序执行前系统就为之静态分配(也即在运行时中不再改变分配情况)存储空间的一类变量。与之相对应的是在运行时只暂时存在的自动变量(即局部变量)与以动态分配方式获取存储空间的一些对象,其中自动变量的存储空间在调用栈上分配与释放。

      我们用一段代码演示一下:

     1 #include<iostream>
     2 using namespace std;
     3 class Person
     4 {
     5     public:
     6     int a;          //定义两个变量,一个普通变量,一个静态变量
     7     static int b;
     8 };
     9 int Person::b = 111; //静态变量只能在类外复制,并且需要声明作用域
    10 void test01()
    11 {
    12     Person p1,p2;    //定义两个类p1和p2
    13     p1.a = 100;       //p1的a赋值100
    14     cout<<"p1.b: "<<p1.b<<" p2.b: "<<p2.b<<endl;  //通过该步的输出,我们发现p1和p2的b同时被9行的代码给赋值了
    15     p1.b = 20;    //我们修改p1.b
    16     p2.a = 3;     
    17     cout<<"p1.a: "<<p1.a<<" p2.a: "<<p2.a<<endl; 
    18     cout<<"p1.b: "<<p1.b<<" p2.b: "<<p2.b<<endl; //修改p1.b后,我们注意观察p1和p2的b
    19     p2.b = 123;
    20     cout<<"p1.b: "<<p1.b<<" p2.b: "<<p2.b<<endl;  //修改p2.b后,我们注意观察p1和p2的b
    21 }
    22 int main(int argc, char const *argv[])
    23 {
    24     test01();
    25     return 0;
    26 }

      运行结果:

      这段代码是为了给大家演示一下静态变量的情况,首先我们是在类中定义了两个变量,一个是普通变量a,一个是静态变量b。接着我们要注意一下静态变量的赋值:

        ①静态变量不能在类内赋值。

        ②静态变量在全局变量赋值时要声明作用域。

      按照上面的约束,我们在给b赋值111之后,在test01()中先实例化出两个对象p1和p2,并把p1.a赋值100,接着输出p1.b和p2.b,我们发现两个值都是111,接着我们又修改p1.b=20,再输出我们会发现p1.b和p2.b都等于20,而p1.a和p2.a我们可以对比看出,两者互不影响。再接着,我们修改p2.b=123,再输出我们又发现两者结果又同时被修改,这就是静态变量,从这个类实例出的对象的静态成员变量是共享的

      小总结一下:

       ①静态变量不能在类内赋值。

       ②静态变量在全局变量赋值时要声明作用域。

       ③这个类实例出的对象的静态成员变量是共享。

       ④静态成员变量在类内声明,声明的作用只是限制静态成员变量作用域。

       ⑤静态成员变量存放在静态全局区。

      2.静态成员函数

      静态成员函数主要用来访问静态数据成员而不访问非静态数据成员,我们只需要记住下面三点即可:

      ①静态成员函数只能访问静态成员变量,不能访问非静态成员变量。

      ②可以通过类的作用域访问静态成员变量。

      ③可以通过对象访问静态成员变量。

        上代码

     1 #include<iostream>
     2 using namespace std;
     3 class Person
     4 {
     5     public:
     6     static void fun()
     7     {
     8         cout<<"静态成员函数"<<b<<endl;
     9     }
    10     int a;          //定义两个变量,一个普通变量,一个静态变量
    11     static int b;
    12 };
    13 int Person::b = 111; //静态变量只能在类外复制,并且需要声明作用域
    14 void test01()
    15 {
    16     Person p1,p2;    //定义两个类p1和p2
    17     p1.fun();
    18     p2.fun();
    19 }
    20 int main(int argc, char const *argv[])
    21 {
    22     test01();
    23     return 0;
    24 }

      运行结果

      这段代码应该没什么可讲的,就是通过fun()函数访问静态成员变量b,只需要记住上面的几点即可。

      后面将进入我们今天的主菜。

      3.单例模式

      在文章的开头我已经给大家介绍了单例模式,大家看了定义之后,觉得应该如何去让类只能实例化出一个对象呢?大家首先想到的肯定是限制它的构造函数吧,没错,第一步就是限制构造函数,这样我们就无法使用普通的实例化方法去创建新的对象了(这一步的代码就不用我写了吧,把构造函数私有化即可)。接着第二步呢?很明显,我们要到前面讲的静态成员变量和静态成员函数吧。我们定义一个静态指针变量同时再提供一个唯一的接口函数来访问这个静态变量。代码如下:

    #include<iostream>
    using namespace std;
    #include<string.h>
    class Person
    {
        public:
        int age;
        string name;
        void show()
        {
            cout<<age<<" "<<name<<endl;
        }
        static Person* Createobj()
        {
            if(single == nullptr)
            {
                single = new Person;
            }
            return single;
        }
        ~Person()
        {
            cout<<"析构函数"<<endl;
        }
        private:
        Person()
        {
            cout<<"构造函数"<<endl;
        }
        static Person* single;
    };
    Person* Person::single = nullptr;
    void test01()
    {
        Person *p1 = Person::Createobj();
        Person *p2 = Person::Createobj();
        p1->age = 10;
        p1->name = "stronger";
        p2->age = 20;
        p2->name = "zjf";
        p1->show();
        p2->show();    
    }
    int main(int argc, char const *argv[])
    {
        test01();
        return 0;
    }

      运行结果:

      通过上面的运行结果,我们发现只调用一次构造函数,并且两个对象内容都是一直的,这是实现单例模式最简单的方式,但会造成内存泄漏的问题,因为没有调用析构函数,要想释放的话,我们还要手动去释放,太麻烦了,并且在线程安全的时候也会遇到问题。那么我们如何解决这个问题呢?大家看下下面这段代码:

     1 #include<iostream>
     2 using namespace std;
     3 #include<string.h>
     4 class Person
     5 {
     6     public:
     7     int age;
     8     string name;
     9     void show()
    10     {
    11         cout<<age<<" "<<name<<endl;
    12     }
    13     static Person* Createobj()
    14     {
    15         static Person obj;
    16         return &obj;
    17     }
    18     ~Person()
    19     {
    20         cout<<"析构函数"<<endl;
    21     }
    22     private:
    23     Person()
    24     {
    25         cout<<"构造函数"<<endl;
    26     }
    27 };
    28 void test01()
    29 {
    30     Person *p1 = Person::Createobj();
    31     Person *p2 = Person::Createobj();
    32     p1->age = 10;
    33     p1->name = "stronger";
    34     p2->age = 20;
    35     p2->name = "zjf";
    36     p1->show();
    37     p2->show();    
    38 }
    39 int main(int argc, char const *argv[])
    40 {
    41     test01();
    42     return 0;
    43 }

      运行结果:

    通过运行结果,我们可以看到,这次的方法成功了,既只有一个对象,也发生了析构。怎么做到的呢?我们是通过在接口函数中创建了一个静态对象,然后返回这个对象的地址,这样只要后面调用接口函数,都会是对这个静态对象操作,所以满足单例模式,也不会有内存泄漏的问题。但是这样做仍有问题,用户可能会用delete  p1的方法来提前销毁对象,但我们的对象不是new 出来的,如果用了delete 程序会出错,那怎么办嘞?

     1 #include<iostream>
     2 using namespace std;
     3 #include<string.h>
     4 class Person
     5 {
     6     public:
     7     int age;
     8     string name;
     9     void show()
    10     {
    11         cout<<age<<" "<<name<<endl;
    12     }
    13     static Person& Createobj()
    14     {
    15         static Person obj;
    16         return obj;
    17     }
    18     ~Person()
    19     {
    20         cout<<"析构函数"<<endl;
    21     }
    22     private:
    23     Person()
    24     {
    25         cout<<"构造函数"<<endl;
    26     }
    27 };
    28 void test01()
    29 {
    30     Person& p1 = Person::Createobj();
    31     Person& p2 = Person::Createobj();
    32     p1.age = 10;
    33     p1.name = "stronger";
    34     p2.age = 20;
    35     p2.name = "zjf";
    36     p1.show();
    37     p2.show();    
    38 }
    39 int main(int argc, char const *argv[])
    40 {
    41     test01();
    42     return 0;
    43 }

    我们对上面的代码做一下改动,我们不用指针来接了,而是让接口函数返回对象的引用,然后创建时也用引用来接。这样用户就不能用delete来删除了。但是这样的话,用户还能通过另外一种类似bug的方法再次新建一个对象。

      

     1 #include<iostream>
     2 using namespace std;
     3 #include<string.h>
     4 class Person
     5 {
     6     public:
     7     int age;
     8     string name;
     9     void show()
    10     {
    11         cout<<age<<" "<<name<<endl;
    12     }
    13     static Person& Createobj()
    14     {
    15         static Person obj;
    16         return obj;
    17     }
    18     ~Person()
    19     {
    20         cout<<"析构函数"<<endl;
    21     }
    22     Person(const Person &obj)
    23     {
    24         cout<<"拷贝构造"<<endl;
    25     }
    26     private:
    27     Person()
    28     {
    29         cout<<"构造函数"<<endl;
    30     }
    31 };
    32 void test01()
    33 {
    34     Person& p1 = Person::Createobj();
    35     Person p2 = Person::Createobj();
    36     p1.age = 10;
    37     p1.name = "stronger";
    38     p2.age = 20;
    39     p2.name = "zjf";
    40     p1.show();
    41     p2.show();    
    42 }
    43 int main(int argc, char const *argv[])
    44 {
    45     test01();
    46     return 0;
    47 }

    注意看第35行代码,我们不用引用去接创建函数了,而是用拷贝函数的方法又新建出一个对象了,所以说要想消除这个bug,我们需要将拷贝构造也私有化。这样用户就无法再用拷贝函数了。或者也可以不用私有,我们可以将拷贝构造改成下面的样子。

    1 Person(const Person &obj) = delete;

    这样即使用了拷贝构造,也会被delete,就不会再创建出第二个对象了。

    上面便是实现单例模式的方法。

    (四)总结

      要想实现单例模式,有下面几步:

      ①私有化构造函数和拷贝函数。

      ②创建一个静态对象,并提供接口函数,返回该静态变量的引用,这样在引用该对象创建对象时都是针对的该对象,就无法再创建第二个了。

     

  • 相关阅读:
    P3478 [POI2008]STA-Station
    P2015 二叉苹果树
    P2014 选课 (树型背包模版)
    求树的每个子树的重心
    求树的直径
    Javascript--防抖与节流
    JavaScript中call和apply的区别
    解决谷歌浏览器“此Flash Player与您的地区不相容,请重新安装Flash”问题(最新版)
    matlab实验代码(总)
    表达式树
  • 原文地址:https://www.cnblogs.com/953-zjf/p/Singleton.html
Copyright © 2020-2023  润新知