【C++】特殊类设计(下)

简介: 【C++】特殊类设计(下)

5. 设计一个类,只能创建一个对象(单例模式)重点


5.1 设计模式

设计模式(Design Pattern)是一套被反复使用、多数人知晓的、经过分类的、代码设计经验的总结。为什么会有设计模式这种东西的出现呢?

最开始的代码设计是没有一定模式的,大家都是随便写的,写的多了就发现了一些套路,最终这些套路就被总结成了设计模式。

使用设计模式的目的:

  • 为了代码可重用性、让代码更容易被他人理解、保证代码可靠性;
  • 设计模式使代码编写真正工程化;
  • 设计模式是软件工程的基石脉络,如同大厦的结构一样


5.2 单例模式

一个类只能创建一个对象,即单例模式,该模式可以保证系统中该类只有一个实例,并提供一个 访问它的全局访问点,该实例被所有程序模块共享比如在某个服务器程序中,该服务器的配置 信息存放在一个文件中,这些配置数据由一个单例对象统一读取,然后服务进程中的其他对象再 通过这个单例对象获取这些配置信息,这种方式简化了在复杂环境下的配置管理。

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


5.2.1 饿汉模式

这里的饿汉是一个形象的说法,把程序比做一个饿汉,在main函数开始前就创建这个单例对象,就像一个饿汉一样。


饿汉模式的实现原理就是:把所有的构造都私有化或者delete掉,然后重新提供一个GetInstance方法用于获取这个单例

//单例模式
//懒汉模式
class Singleton
{
public:
    static Singleton& GetInstance()//获取这个单例对象
    {
        return _ins;
    }
    //一些对应的数据操作
    void Insert(pair<string, int> val)
    {
        _mp.insert(val);
    }
    void Print()
    {
        for(auto& kv : _mp)
        {
            cout << kv.first << ":" << kv.second << endl;
        }
    }
private:
    Singleton() {}//把构造函数私有化
    Singleton(const Singleton&) = delete;//删除拷贝构造和赋值重载
    Singleton operator=(const Singleton&) = delete;
private:
    static Singleton _ins;//一个静态的“全局变量”,用于在类外访问到构造函数的
    //单例类的一些数据
    map<string,int> _mp;
};
Singleton Singleton::_ins;//在程序开始执行main函数之前就已经构造对象
int main()
{
    auto& ins1 = Singleton::GetInstance();//调用的时候使用GetInstance给单例对象取别名
    ins1.Insert({"sort", 1});
    auto& ins2 = Singleton::GetInstance();
    ins2.Insert({"string", 3});
    auto& ins3 = Singleton::GetInstance();
    ins3.Print();
    return 0;
}


d2f63c25595bd72dc07eb00607191a87.png

饿汉模式下,单例对象在main函数被调用之前就已经构造,所以不存在线程安全的问题,但是同时也存在一些缺点


  • 有的单例对象构造十分耗时或者需要占用很多资源,比如加载插件、 初始化网络连接、读取文件等等,会导致程序启动时加载速度慢。
  • 饿汉模式在程序启动时就创建了单例对象,所以即使在程序运行期间并没有用到该对象,它也会一直存在于内存中,浪费了一定的系统资源。
  • 当多个单例类存在初始化依赖关系时,饿汉模式无法控制。比如A、B两个单例类存在于不同的文件中,我们要求先初始化A,再初始化B,但是A、B谁先启动初始化是由OS自动进行调度控制的,我们无法进行控制。


5.2.2 懒汉模式

除了饿汉模式之外,还有一种单例模式的实现方法是懒汉模式,所谓懒汉模式就是在第一次使用到单例对象的时候再构造

class SlackerInstance
{
public:
    static SlackerInstance* GetInstance()
    {
        if(_pins == nullptr)
        {
            _pins = new SlackerInstance;
        }
        return _pins;
    }
    void Insert(pair<string, int> val)
    {
        _mp.insert(val);
    }
    void Print()
    {
        for(auto& kv : _mp)
        {
            cout << kv.first << ":" << kv.second << endl;
        }
    }
private:
    SlackerInstance() {}//构造函数私有化
    SlackerInstance(const SlackerInstance&) = delete;//删除拷贝构造和赋值重载
    SlackerInstance operator=(const SlackerInstance&) = delete;
private:
    static SlackerInstance* _pins;//静态的单例对象指针
    map<string, int> _mp;
};
SlackerInstance* SlackerInstance::_pins = nullptr;//初始化为空指针

19817adeb6975301d5b4b88fe340a74a.png

找bug环节:请找出上述代码的bug

  • 线程安全问题:GetInstance函数不是线程安全的,内部的new不是原子性操作;

问题的解决:在函数内部加锁

static SlackerInstance* GetInstance()
{
    mtx.lock();//加锁
    if(_pins == nullptr)//第一次获取单例对象的时候创建对象
    {
        _pins = new SlackerInstance;
    }
    mtx.unlock();//完成new操作之后解锁
    return _pins;
}


但是,此时的代码还不够完美,每次调用GetInstance函数的时候都会进行无意义的加锁解锁操作,所以这里可以使用一种双检查的方法,在锁外层再进行一次判断

static SlackerInstance* GetInstance()
{
    if(_pins == nullptr)//双检查,避免无意义的加锁解锁
    {
        mtx.lock();//加锁
        if(_pins == nullptr)//第一次获取单例对象的时候创建对象
        {
            _pins = new SlackerInstance;
        }
        mtx.unlock();//完成new操作之后解锁
    }
    return _pins;
}


但是加锁之后就会出现另一个问题:new的过程中可能抛异常,此时就没有解锁,所以这里需要捕获异常进行解锁,然后重新抛出

static SlackerInstance* GetInstance()
{
    if(_pins == nullptr)//双检查,避免无意义的加锁解锁
    {
        mtx.lock();//加锁
        try
        {
            if(_pins == nullptr)//第一次获取单例对象的时候创建对象
            {
                _pins = new SlackerInstance;
            }
        }
        catch(...)//捕获异常并重新抛出
        {
            mtx.unlock();
            throw;
        }
        mtx.unlock();//完成new操作之后解锁
    }
    return _pins;
}


但是这样写看起来很low,还是追求高级的,优雅的写法

这里我们使用RAII的思想实现对锁的自动管理

//RAII锁的类
template<class Mutex>
class LockGuard
{
public:
    LockGuard(Mutex& mtx)
    :_mtx(mtx)
    {
        _mtx.lock();
    }
    ~LockGuard()
    {
        _mtx.unlock();
    }
private:
    Mutex& _mtx;//这里需要将锁设为引用的,因为锁不允许拷贝
};


当然,在库里面也实现了相关的类

db73e027fb3bcd0be2989a0ba56c7e4c.png

static SlackerInstance* GetInstance()
{
    if(_pins == nullptr)//双检查,避免无意义的加锁解锁
    {
        //_mtx.lock();//加锁
        LockGuard<std::mutex> lg(_mtx);
        if(_pins == nullptr)//第一次获取单例对象的时候创建对象
        {
            _pins = new SlackerInstance;
        }
        //_mtx.unlock();//完成new操作之后解锁
    }
    return _pins;
}

单例对象的资源释放与数据保存

问题一:单例对象是new出来的需要进行释放吗?


由于单例对象的创建是全局的,全局资源在程序结束后会被自动回收 (进程退出后OS会解除进程地址空间与物理内存的映射)。但是我们也可以手动对其进行回收。需要注意的是,有时我们需要在回收资源之前将资源的相关数据保存到文件中,这种情况下我们就必须手动回收了。


我们可以在类中定义一个静态的 DelInstance 接口来回收与保存资源 (此函数不会被频繁调用,因此不需要使用双检查加锁)。

static void DelInstance()
{
    //保存数据文件
    //TODO
    //回收单例对象资源
    std::lock_guard<std::mutex> lg(_mtx);
    if(_pins != nullptr)
    {
        delete _pins;
        _pins = nullptr;
    }
}


当然,也可以定义一个内部的GC类,其在程序结束时自动调用析构函数

所以最终的实现方式如下

class SlackerInstance
{
public:
    static SlackerInstance* GetInstance()
    {
        if(_pins == nullptr)//双检查,避免无意义的加锁解锁
        {
            //_mtx.lock();//加锁
            LockGuard<std::mutex> lg(_mtx);
            if(_pins == nullptr)//第一次获取单例对象的时候创建对象
            {
                _pins = new SlackerInstance;
            }
            //_mtx.unlock();//完成new操作之后解锁
        }
        return _pins;
    }
    static void DelInstance()
    {
        //保存数据文件
        //TODO
        //回收单例对象资源
        std::lock_guard<std::mutex> lg(_mtx);
        if(_pins != nullptr)
        {
            delete _pins;
            _pins = nullptr;
        }
    }
    void Insert(pair<string, int> val)
    {
        _mp.insert(val);
    }
    void Print()
    {
        for(auto& kv : _mp)
        {
            cout << kv.first << ":" << kv.second << endl;
        }
    }
    class GC
    {
    public:
        ~GC()
        {
            if(_pins)
            {
                cout << "~GC()" << endl;
                DelInstance();
            }
        }
    };
private:
    SlackerInstance() {}//构造函数私有化
    SlackerInstance(const SlackerInstance&) = delete;//删除拷贝构造和赋值重载
    SlackerInstance operator=(const SlackerInstance&) = delete;
private:
    static SlackerInstance* _pins;//静态的单例对象指针
    static mutex _mtx;//互斥锁
    static GC _gc;//自动回收
    map<string, int> _mp;
};
SlackerInstance* SlackerInstance::_pins = nullptr;//初始化为空指针
mutex SlackerInstance::_mtx;
SlackerInstance::GC SlackerInstance::_gc;


另一种版本的懒汉模式的写法

class SlackerInstance2
{
public:
    static SlackerInstance2& GetInstance()
    {
        static SlackerInstance2 ins;//使用static修饰吗,第一次调用的时候构建对象,再次调用就直接使用
        return ins;
    }
    void Insert(pair<string, int> val)
    {
        _mp.insert(val);
    }
    void Print()
    {
        for(auto& kv : _mp)
        {
            cout << kv.first << ":" << kv.second << endl;
        }
    }
private:
    SlackerInstance2(){}
    SlackerInstance2(const SlackerInstance2&) = delete;
    SlackerInstance2 operator=(const SlackerInstance2&) = delete;
private:
    map<string, int> _mp;
};


相关文章
|
9天前
|
C++ 芯片
【C++面向对象——类与对象】Computer类(头歌实践教学平台习题)【合集】
声明一个简单的Computer类,含有数据成员芯片(cpu)、内存(ram)、光驱(cdrom)等等,以及两个公有成员函数run、stop。只能在类的内部访问。这是一种数据隐藏的机制,用于保护类的数据不被外部随意修改。根据提示,在右侧编辑器补充代码,平台会对你编写的代码进行测试。成员可以在派生类(继承该类的子类)中访问。成员,在类的外部不能直接访问。可以在类的外部直接访问。为了完成本关任务,你需要掌握。
48 18
|
9天前
|
存储 编译器 数据安全/隐私保护
【C++面向对象——类与对象】CPU类(头歌实践教学平台习题)【合集】
声明一个CPU类,包含等级(rank)、频率(frequency)、电压(voltage)等属性,以及两个公有成员函数run、stop。根据提示,在右侧编辑器补充代码,平台会对你编写的代码进行测试。​ 相关知识 类的声明和使用。 类的声明和对象的声明。 构造函数和析构函数的执行。 一、类的声明和使用 1.类的声明基础 在C++中,类是创建对象的蓝图。类的声明定义了类的成员,包括数据成员(变量)和成员函数(方法)。一个简单的类声明示例如下: classMyClass{ public: int
34 13
|
9天前
|
编译器 数据安全/隐私保护 C++
【C++面向对象——继承与派生】派生类的应用(头歌实践教学平台习题)【合集】
本实验旨在学习类的继承关系、不同继承方式下的访问控制及利用虚基类解决二义性问题。主要内容包括: 1. **类的继承关系基础概念**:介绍继承的定义及声明派生类的语法。 2. **不同继承方式下对基类成员的访问控制**:详细说明`public`、`private`和`protected`继承方式对基类成员的访问权限影响。 3. **利用虚基类解决二义性问题**:解释多继承中可能出现的二义性及其解决方案——虚基类。 实验任务要求从`people`类派生出`student`、`teacher`、`graduate`和`TA`类,添加特定属性并测试这些类的功能。最终通过创建教师和助教实例,验证代码
31 5
|
9天前
|
存储 算法 搜索推荐
【C++面向对象——群体类和群体数据的组织】实现含排序功能的数组类(头歌实践教学平台习题)【合集】
1. **相关排序和查找算法的原理**:介绍直接插入排序、直接选择排序、冒泡排序和顺序查找的基本原理及其实现代码。 2. **C++ 类与成员函数的定义**:讲解如何定义`Array`类,包括类的声明和实现,以及成员函数的定义与调用。 3. **数组作为类的成员变量的处理**:探讨内存管理和正确访问数组元素的方法,确保在类中正确使用动态分配的数组。 4. **函数参数传递与返回值处理**:解释排序和查找函数的参数传递方式及返回值处理,确保函数功能正确实现。 通过掌握这些知识,可以顺利地将排序和查找算法封装到`Array`类中,并进行测试验证。编程要求是在右侧编辑器补充代码以实现三种排序算法
21 5
|
9天前
|
Serverless 编译器 C++
【C++面向对象——类的多态性与虚函数】计算图像面积(头歌实践教学平台习题)【合集】
本任务要求设计一个矩形类、圆形类和图形基类,计算并输出相应图形面积。相关知识点包括纯虚函数和抽象类的使用。 **目录:** - 任务描述 - 相关知识 - 纯虚函数 - 特点 - 使用场景 - 作用 - 注意事项 - 相关概念对比 - 抽象类的使用 - 定义与概念 - 使用场景 - 编程要求 - 测试说明 - 通关代码 - 测试结果 **任务概述:** 1. **图形基类(Shape)**:包含纯虚函数 `void PrintArea()`。 2. **矩形类(Rectangle)**:继承 Shape 类,重写 `Print
27 4
|
9天前
|
设计模式 IDE 编译器
【C++面向对象——类的多态性与虚函数】编写教学游戏:认识动物(头歌实践教学平台习题)【合集】
本项目旨在通过C++编程实现一个教学游戏,帮助小朋友认识动物。程序设计了一个动物园场景,包含Dog、Bird和Frog三种动物。每个动物都有move和shout行为,用于展示其特征。游戏随机挑选10个动物,前5个供学习,后5个用于测试。使用虚函数和多态实现不同动物的行为,确保代码灵活扩展。此外,通过typeid获取对象类型,并利用strstr辅助判断类型。相关头文件如&lt;string&gt;、&lt;cstdlib&gt;等确保程序正常运行。最终,根据小朋友的回答计算得分,提供互动学习体验。 - **任务描述**:编写教学游戏,随机挑选10个动物进行展示与测试。 - **类设计**:基类
25 3
|
2月前
|
存储 编译器 C语言
【c++丨STL】string类的使用
本文介绍了C++中`string`类的基本概念及其主要接口。`string`类在C++标准库中扮演着重要角色,它提供了比C语言中字符串处理函数更丰富、安全和便捷的功能。文章详细讲解了`string`类的构造函数、赋值运算符、容量管理接口、元素访问及遍历方法、字符串修改操作、字符串运算接口、常量成员和非成员函数等内容。通过实例演示了如何使用这些接口进行字符串的创建、修改、查找和比较等操作,帮助读者更好地理解和掌握`string`类的应用。
70 2
|
2月前
|
存储 编译器 C++
【c++】类和对象(下)(取地址运算符重载、深究构造函数、类型转换、static修饰成员、友元、内部类、匿名对象)
本文介绍了C++中类和对象的高级特性,包括取地址运算符重载、构造函数的初始化列表、类型转换、static修饰成员、友元、内部类及匿名对象等内容。文章详细解释了每个概念的使用方法和注意事项,帮助读者深入了解C++面向对象编程的核心机制。
127 5
|
2月前
|
存储 编译器 C++
【c++】类和对象(中)(构造函数、析构函数、拷贝构造、赋值重载)
本文深入探讨了C++类的默认成员函数,包括构造函数、析构函数、拷贝构造函数和赋值重载。构造函数用于对象的初始化,析构函数用于对象销毁时的资源清理,拷贝构造函数用于对象的拷贝,赋值重载用于已存在对象的赋值。文章详细介绍了每个函数的特点、使用方法及注意事项,并提供了代码示例。这些默认成员函数确保了资源的正确管理和对象状态的维护。
137 4
|
2月前
|
存储 编译器 Linux
【c++】类和对象(上)(类的定义格式、访问限定符、类域、类的实例化、对象的内存大小、this指针)
本文介绍了C++中的类和对象,包括类的概念、定义格式、访问限定符、类域、对象的创建及内存大小、以及this指针。通过示例代码详细解释了类的定义、成员函数和成员变量的作用,以及如何使用访问限定符控制成员的访问权限。此外,还讨论了对象的内存分配规则和this指针的使用场景,帮助读者深入理解面向对象编程的核心概念。
192 4