几种单例模式
单例是一个类的单一实例,类或者结构体只有一个实例,
是指在内存中只会创建且仅创建一次对象的设计模式。在程序中多次使用同一个对象且作用相同时,为了防止频繁地创建对象使得内存飙升,单例模式可以让程序仅在内存中创建一个对象,让所有需要调用的地方都共享这一单例对象。
单例特点:
1 在任何情况下,单例类永远只有一个实例存在。
2 单例需要有能力为整个系统提供这一唯一实例。
实现一(单线程使用,多线程不安全)
#include <iostream>
using namespace std;
class Singleton
{
private:
Singleton(){}
public:
static Singleton* instance()
{
if(_instance == 0)
_instance = new Singleton();
return _instance;
}
private:
static Singleton* _instance;
};
Singleton* Singleton::_instance = 0;
上面这种实现在单线程环境下是没有问题的,可是多线程下就有问题了。
分析:
1 线程A进入函数instance执行判断语句,这句执行后就挂起了,这时线程A已经认为_instance为NULL,但是线程A还没有创建singleton对象。
2 又有一个线程B进入函数instance执行判断语句,此时同样认为_instance变量为null,因为A没有创建singleton对象。线程B继续执行,创建了一个singleton对象。
3 稍后,线程A接着执行,也创建了一个新的singleton对象。
4 创建了两个对象!
从上面分析可以看出,需要对_instance变量加上互斥锁:
实现二(多线程安全,加锁代价高)
#include <iostream>
#include <mutex>
using namespace std;
std::mutex mt;
class Singleton
{
private:
Singleton(){}
public:
static Singleton* instance()
{
mt.lock(); // 加锁
if(_instance == 0)
_instance = new Singleton();
mt.unlock(); // 解锁
return _instance;
}
private:
static Singleton* _instance;
};
Singleton* Singleton::_instance = 0;
上锁后是解决了线程安全问题,但是有些资源浪费。稍微分析一下:每次instance函数调用时候都需要请求加锁,其实并不需要,instance函数只需第一次调用的时候上锁就行了。这时可以用DCLP解决。
#include <iostream>
#include <mutex>
using namespace std;
std::mutex mt;
class Singleton
{
private:
Singleton(){}
public:
static Singleton* instance()
{
if(_instance == 0)
{
mt.lock();
if(_instance == 0)
_instance = new Singleton();
mt.unlock();
}
return _instance;
}
private:
static Singleton* _instance;
public:
int atestvalue;
};
Singleton* Singleton::_instance = 0;
这个版本很不错,又叫“双重检查”Double-Check。下面是说明:
- 第一个条件是说,如果实例创建了,那就不需要同步了,直接返回就好了。不然,我们就开始同步线程。
- 第二个条件是说,如果被同步的线程中,有一个线程创建了对象,那么别的线程就不用再创建了。
实现四(C++ 11版本最简洁的跨平台方案)(推荐版本)
局部静态变量不仅只会初始化一次,而且还是线程安全的。
#include <iostream>
using namespace std;
class Singleton
{
public:
// 注意返回的是引用
static Singleton& getInstance()
{
static Singleton value; //静态局部变量
return value;
}
private:
Singleton() = default;
Singleton(const Singleton& other) = delete; //禁止使用拷贝构造函数
Singleton& operator=(const Singleton&) = delete; //禁止使用拷贝赋值运算符
};
int main()
{
Singleton& s1 = Singleton::getInstance();
cout << &s1 << endl;
Singleton& s2 = Singleton::getInstance();
cout << &s2 << endl;
return 0;
}
区分全局变量、全局静态变量、局部静态变量
#include <iostream>
using namespace std;
int A1()
{
static int a = 10;
a++;
cout << a << endl;
return a;
}
int main(int argc, const char * argv[]) {
A1();
A1();
A1();
int a1=A1();
cout << "a1="<< a1 << endl;
return 0;
}
结果:
11
12
13
14
a1=14
全局变量、静态局部变量、静态全局变量都在静态存储区分配空间,而局部变量在栈分配空间,static变量只初始化一次。
全局变量本身就是静态存储方式,静态全局变量当然也是静态存储方式。这两者在存储方式上没有什么不同。区别在于非静态全局变量的作用域是整个源程序,当一个源程序由多个源文件组成时,非静态的全局变量在各个源文件中都是有效的。而静态全局变量则限制了其作用域,即只在定义该变量的源文件内有效,在同一源程序的其他源文件中不能使用它。由于静态全局变量的作用域局限于一个源文件内,只能为该源文件内的函数公用,因此可以避免在其他源文件中引起错误。
-
1、静态变量会被放在程序的静态数据存储区里,这样可以在下一次调用的时候还可以保持原来的赋值。这一点是他与堆栈变量和堆变量的区别。
-
2、变量用static告知编译器,自己仅仅在变量的作用域范围内可见。这一点是他与全局变量的区别。
从以上分析可以看出,把局部变量改变为静态变量后是改变了他的存储方式,即改变了他的生存期。把全局变量改变为静态变量后是改变了他的作用域,限制了他的使用范围,因此static这个说明符在不同的地方起的作用是不同的。
不同类型的变量在内存中的位置:
-
1、 已经初始化的全局变量存放与data数据段;未初始化的全局变量存放与bss数据段。
-
2、 静态的全局变量存放与data数据段
-
3、 局部变量存放在栈上。
-
4、 静态局部变量,并不是在调用函数时分配函数返回时释放,而是像全局变量一样静态分配,存放data数据段,但它的作用域在函数中起作用。
-
引用作为返回值
通过使用引用来替代指针,会使 C++ 程序更容易阅读和维护。C++ 函数可以返回一个引用,方式与返回一个指针类似。当函数返回一个引用时,则返回一个指向返回值的隐式指针。用引用作函数的返回值的最大的好处是在内存中不产生返回值的副本。
引用作为返回值,须遵守以下规则:
- 能返回局部变量的引用。主要原因是局部变量会在函数返回后被销毁,因此被返回的引用就成为了”无所指”的引用,类似于野指针,程序会进入未知状态。
- 不能返回函数内部new分配的内存的引用。虽然不存在局部变量的被动销毁问题,可对于这种情况又面临其它尴尬局面。例如,被函数返回的引用只是作为一 个临时变量出现,而没有被赋予一个实际的变量,那么这个引用所指向的空间(由new分配)就无法释放,造成memory leak。
- 可以返回类成员的引用,但最好是 const,这样可以避免在无意的情况下破坏该类的成员。
参考
https://zhuanlan.zhihu.com/p/62014096
https://blog.csdn.net/weixin_40539125/article/details/81938529