C++——单例模式的原理及实现
C++——单例模式的原理及实现
(⼀)定义
  单例模式,属于创建类型的⼀种常⽤的软件设计模式。通过单例模式的⽅法创建的类在当前进程中只有⼀个实例(根据需要,也有可能⼀个线程中属于单例,如:仅线程上下⽂内使⽤同⼀个实例)。
(⼆)简介
  单例模式是设计模式中最简单的形式之⼀。这⼀模式的⽬的是使得类的⼀个对象成为系统中的唯⼀实例。要实现这⼀点,可以从客户端对其进⾏实例化开始。因此需要⽤⼀种只允许⽣成对象类的唯⼀实例的机制,“阻⽌”所有想要⽣成对象的访问。使⽤⼯⼚⽅法来限制实例化过程。这个⽅法应该是静态⽅法(类⽅法),因为让类的实例去⽣成另⼀个唯⼀实例毫⽆意义。这是百度的解释,以我个⼈的观点来说的话,其实就是在整个程序中整个类只能实例化出⼀个对象。通俗来讲,就是在某些场景下,我们之能有⼀个对象,例如,⼀个系统中可以存在多个打印任务,但是只能有⼀个正在⼯作的任务;⼀个系统只能有⼀个窗⼝管理器或⽂件系统;⼀个系统只能有⼀个计时⼯具或ID(序号)⽣成器。如在Windows中就只能打开⼀个任务管理器。如果不使⽤机制对窗⼝对象进⾏唯⼀化,将弹出多个窗⼝,如果这些窗⼝显
⽰的内容完全⼀致,则是重复对象,浪费内存资源;如果这些窗⼝显⽰的内容不⼀致,则意味着在某⼀瞬间系统有多个状态,与实际不符,也会给⽤户带来误解,不知道哪⼀个才是真实的状态。因此有时确保系统中某个对象的唯⼀性即⼀个类只能有⼀个实例⾮常重要。
(三)具体实现
单例模式的几种实现方式  ⾸先,我们先⼤致讲下我们要⽤到的知识:静态成员变量、静态成员函数
  1.静态成员变量
  静态变量(Static Variable)在计算机编程领域指在程序执⾏前系统就为之静态分配(也即在运⾏时中不再改变分配情况)存储空间的⼀类变量。与之相对应的是在运⾏时只暂时存在的⾃动变量(即局部变量)与以动态分配⽅式获取存储空间的⼀些对象,其中⾃动变量的存储空间在调⽤栈上分配与释放。
  我们⽤⼀段代码演⽰⼀下:
1 #include<iostream>
2using namespace std;
3class Person
4 {
5public:
6int a;          //定义两个变量,⼀个普通变量,⼀个静态变量
7static int b;
8 };
9int Person::b = 111; //静态变量只能在类外复制,并且需要声明作⽤域
10void 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 }
22int main(int argc, char const *argv[])
23 {
24    test01();
25return0;
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>
2using namespace std;
3class Person
4 {
5public:
6static void fun()
7    {
8        cout<<"静态成员函数"<<b<<endl;
9    }
10int a;          //定义两个变量,⼀个普通变量,⼀个静态变量
11static int b;
12 };
13int Person::b = 111; //静态变量只能在类外复制,并且需要声明作⽤域
14void test01()
15 {
16    Person p1,p2;    //定义两个类p1和p2
17    p1.fun();
18    p2.fun();
19 }
20int main(int argc, char const *argv[])
21 {
22    test01();
23return0;
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();
return0;
}
  运⾏结果:
  通过上⾯的运⾏结果,我们发现只调⽤⼀次构造函数,并且两个对象内容都是⼀直的,这是实现单例模式最简单的⽅式,但会造成内存泄漏的问题,因为没有调⽤析构函数,要想释放的话,我们还要⼿动去释放,太⿇烦了,并且在线程安全的时候也会遇到问题。那么我们如何解决这个问题呢?⼤家看下下⾯这段代码:
1 #include<iostream>
2using namespace std;
3 #include<string.h>
4class Person
5 {
6public:
7int age;
8string name;
9void show()
10    {
11        cout<<age<<""<<name<<endl;
12    }
13static Person* Createobj()
14    {
15static Person obj;
16return &obj;
17    }
18    ~Person()
19    {
20        cout<<"析构函数"<<endl;
21    }
22private:
23    Person()
24    {
25        cout<<"构造函数"<<endl;
26    }
27 };
28void 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 }
39int main(int argc, char const *argv[])
41    test01();
42return0;
43 }
  运⾏结果:
通过运⾏结果,我们可以看到,这次的⽅法成功了,既只有⼀个对象,也发⽣了析构。怎么做到的呢?我们是通过在接⼝函数中创建了⼀个静态对象,然后返回这个对象的地址,这样只要后⾯调⽤接⼝函数,都会是对这个静态对象操作,所以满⾜单例模式,也不会有内存泄漏的问题。但是这样做仍有问题,⽤户可能会⽤delete  p1的⽅法来提前销毁对象,但我们的对象不是new 出来的,如果⽤了delete 程序会出错,那怎么办嘞?
1 #include<iostream>
2using namespace std;
3 #include<string.h>
4class Person
5 {
6public:
7int age;
8string name;
9void show()
10    {
11        cout<<age<<""<<name<<endl;
12    }
13static Person& Createobj()
14    {
15static Person obj;
16return obj;
17    }
18    ~Person()
19    {
20        cout<<"析构函数"<<endl;
21    }
22private:
23    Person()
24    {
25        cout<<"构造函数"<<endl;
26    }
27 };
28void 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 }
39int main(int argc, char const *argv[])
40 {
41    test01();
42return0;
43 }
我们对上⾯的代码做⼀下改动,我们不⽤指针来接了,⽽是让接⼝函数返回对象的引⽤,然后创建时也⽤引⽤来接。这样⽤户就不能⽤delete来删除了。但是这样的话,⽤户还能通过另外⼀种类似bug的⽅法再次新建⼀个对象。
1 #include<iostream>
2using namespace std;
3 #include<string.h>
4class Person
5 {
6public:
7int age;
8string name;
9void show()
10    {
11        cout<<age<<""<<name<<endl;
12    }
13static Person& Createobj()
14    {
15static Person obj;
16return obj;
17    }
18    ~Person()
19    {
20        cout<<"析构函数"<<endl;
21    }
22    Person(const Person &obj)
23    {
24        cout<<"拷贝构造"<<endl;
25    }
26private:
27    Person()
28    {
29        cout<<"构造函数"<<endl;
30    }
31 };
32void 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 }
43int main(int argc, char const *argv[])
44 {
45    test01();
46return0;
47 }
注意看第35⾏代码,我们不⽤引⽤去接创建函数了,⽽是⽤拷贝函数的⽅法⼜新建出⼀个对象了,所以说要想消除这个bug,我们需要将拷贝构造也私有化。这样⽤户就⽆法再⽤拷贝函数了。或者也可以不⽤私有,我们可以将拷贝构造改成下⾯的样⼦。
1 Person(const Person &obj) = delete;
这样即使⽤了拷贝构造,也会被delete,就不会再创建出第⼆个对象了。
上⾯便是实现单例模式的⽅法。
(四)总结
  要想实现单例模式,有下⾯⼏步:
  ①私有化构造函数和拷贝函数。
  ②创建⼀个静态对象,并提供接⼝函数,返回该静态变量的引⽤,这样在引⽤该对象创建对象时都是针对的该对象,就⽆法再创建第⼆个了。

版权声明:本站内容均来自互联网,仅供演示用,请勿用于商业和其他非法用途。如果侵犯了您的权益请与我们联系QQ:729038198,我们将在24小时内删除。