单例九品--第二品
- 第一品回顾
- 写在前边
- 代码部分
- 实现方式评注与思考
- 下一品设计的思考
第一品回顾
第一品传送门
第一品的实现方式完全没有阻止构建类的多个对象,在这一品中将会修复这种问题。
写在前边
- 基本思路
• 将构造、析构函数设置为私有成员
• 使用静态成员函数获取唯一的实例 - 优点
• 可以防止用户不小心构造新的实例
• (C++11 开始)系统会保证函数内部的静态成员初始化是多线程安全的 - 特点
• 缓式初始化(lazy initialization) - 缺点
• 没有完全阻止用户构造该类的多个对象
代码部分
两个文件, mian.cpp和sing.h
- mian.cpp
#include "sing.h"int main(int argc, char** argv)
{std::cout << "get value: " << Sing::Instance().val << '\n';return 0;
}
- sing.h
#pragma once
#include <iostream>class Sing
{
public:static const Sing& Instance(){static Sing inst;return inst;}
private:Sing(){std::cout << "Sing construct\n";val = 100;}~Sing(){std::cout << "Sing destroy\n";}public:int val;
};
- output
gcc编译运行结果:
命令:
gcc -c main.cpp -std=c++20
g++ -c main.o -o ./m
./m
get value: Sing construct
100
Sing destroy
实现方式评注与思考
- 构造instance接口来构造唯一实例的全局访问点,并将构造函数和析构函数写为私有,这样只能通过全局访问点instance来完成实例的构造和访问。一定程度上有效减少了在翻译单元中不小心使用(Sing sing1;)的方式定义对象的问题。
- 用static关键字修饰instance函数为了不实例化对象可以直接调用instance函数。
- 单例对象inst定义为static,是因为在首次访问全局访问点instance完成单例构造以后,这个单例将会在整个程序周期存在
- C++11标准以后规定,这种写法即便是单线程多次调用instance函数,还是多线程同时访问instance函数。全局单例对象inst只在istance首次被访问的时候构建,所以不会出现多次定义的情况。 即便是多个线程同时都去访问全局访问点,也不会造成实例的多次构造。所以系统会保证函数内部的静态成员初始化是多线程安全的
- 这种写法,单例对象在第一次使用时才会完成构建,并不是第一品实现的那样,在使用前就已经完成了构造,也就是"缓释初始化"。缓释初始化会对第一个用户不太友好,如果初始化逻辑比较复杂,极端情况下,可能会有一定的耗时。
- 这种写法,第二品的设计思路可以一定程度上减少用户不小心完成多次对象定义的问题,但是没有完全阻止用户构造该类的多个对象
比如将main.cpp写为下边这样:
#include "sing.h"int main(int argc, char** argv)
{std::cout << "get value: " << Sing::Instance().val << '\n';Sing* sing2 = new Sing(Sing::Instance());return 0;
}
这种设计方式,没有完全阻止用户构造该类的多个对象, 因为没有完成限制拷贝构造函数的权限。对于没有定义拷贝构造函数的类,在编译期会合成一个缺省的拷贝构造函数,这个函数是public的。所以Sing* sing2 = new Sing(Sing::Instance())使用拷贝构造函数完成了堆上的一个对象的建立,实现了全局访问对象inst到堆上对象sing2的赋值。并且因为堆内存的释放,需要显式的完成,不用调用析构函数(这里写的是private)的需求,因此工程是可以完成编译运行的。
下一品设计的思考
下一品将进行拷贝,移动构造函数的限制。已达到完全限制用户用户构造该类的多个对象。