• C++从静态类型到单例模式


    1. 概述

    很多的知识,学习的时候理解其实并不是很深,甚至觉得是是不太必要的;而到了实际使用中遇到了,才有了比较深刻的认识。

    2. 详论

    2.1. 静态类型

    2.1.1. 静态方法成员

    比如说类的静态成员函数。从学习中我们可以知道,类的静态成员表示这个类成员直接属于类本身;无论实例化这个类对象多少次,静态成员都只是一份相同的副本。那么什么时候去使用这个特性呢?一个很简单的例子,假设我们实现了很多函数:

    void FunA() {}
    
    void FunB() {}
    
    void FunC() {}
    

    这些函数如果具有相关性,都是某个类型的工具函数,那么我们可以将其封装成一个工具类,并将其方法成员都定义成静态的:

    class Utils {
    public:
      static void FunA() {}
    
      static void FunB() {}
    
      static void FunC() {}
    };
    

    这样做的好处很多:

    1. 体现了面向对象的思想。并且,这些方法在类中本来就只需要一份就可以了,节省了程序内存。
    2. 避免在全局作用域定义函数。一般的编程认为,定义在全局作用域的变量或者方法是不太好的。
    3. 方便使用:只用记住Utils这个类的名字,就可以在IDE输入提示的帮助下快熟输入想要的函数。

    2.1.2. 静态数据成员

    一个顺理成章的问题就是,既然静态方法成员这么好用,那么我们使用静态数据成员也挺好的吧?一般情况下确实如此,比如我们给这个工具类定义一个静态数据成员pai:

    class Utils {
    public:
      static void FunA() {}
    
      static void FunB() {}
    
      static void FunC() {}
    
      static double pai;
    };
    
    double Utils::pai = 3.1415926;
    

    但是有一个问题在于,简单的数据成员能够通过赋值来初始化,如果是一个比较复杂的数据成员呢?一个例子就是std::map容器数据成员,需要经过多次插入操作来初始化。这个时候只是通过赋值就很难实现了。

    不仅如此,使用类的静态数据成员还会遇到一个相互依赖的问题,如参考文献2中所述。由于静态变量的初始化顺序是不定的,很可能会导致静态变量A初始化需要静态变量B,但是静态变量B却没有完成初始化,从而导致出错的问题。

    2.2. 单例模式

    2.2.1. 实现

    C++并没有静态类和静态构造函数的概念。在参考文献1中,论述了一些用C++去实现静态构造函数,从而更加合理的去初始化静态数据成员的办法。其中一个实现是:我们需要的类按照正常的非静态成员类去设计,但是我们可以把这个类作为另一个包装类的静态成员变量,这样就能完美实现静态构造函数。

    正是这个实现给了我灵感:我们想要的不是访问类的静态成员变量,而是单例模式。不想像C一样使用全局函数或者全局变量,又不想每次都去实例化一个对象,那么我们需要的是单例模式。参考文献3中给出了单例模式的最佳实践:

    class Singleton {
     public:
      ~Singleton() { std::cout << "destructor called!" << std::endl; }
      Singleton(const Singleton&) = delete;
      Singleton& operator=(const Singleton&) = delete;
      static Singleton& get_instance() {
        static Singleton instance;
        return instance;
      }
    
     private:
      Singleton() { std::cout << "constructor called!" << std::endl; }
    };
    
    int main() {
    
      Singleton& instance_1 = Singleton::get_instance();
      Singleton& instance_2 = Singleton::get_instance();
    
      return 0;
    }
    

    这段代码的说明如下:

    1. 构造函数和析构函数都存在,无论多复杂的成员,都可以对数据成员初始化和释放。
    2. 构造函数时私有的,所以无法直接声明和定义。
    3. 拷贝构造函数和赋值构造函数都被删除,因此无法进行拷贝和赋值。
    4. 只能通过专门的实例化函数get_instance()进行调用。

    在实例化函数get_instance()内部,实例化了一个自身的局部的静态类。静态局部变量始终存放在内存的全局数据区,只在第一次初始化,从第二次开始,它的值不会变化,是第一次调用后的结果值。并且最后,返回的是这个静态局部变量的引用。

    2.2.2. 问题

    无论从哪方面看,上述的单例实现,都符合单例的设计模式:全局只提供唯一一个类的实例,在任何位置都可以通过接口获取到那个唯一实例,无法拷贝也无法赋值。但是也有几个问题值得讨论。

    第一个问题是,在多线程的环境下,初始化是否会造成冲突或者生成了两份实例?关于这一点不用担心,从C++11标准开始,局部静态变量的初始化是线程安全的。

    第二,在参考文献4中讨论了这样一个问题:C++单例模式跨DLL是不是就是会出问题?静态变量是单个编译单元的静态变量,如果动态库和可执行文件都引用了get_instance()的实现,那么动态库和可执行文件会分别保有一份自己的实例。解决方法是要么将get_instance()放入到cpp中,要么使用DLL的模块导入导出接口的规则,也就是dllexport和dllimport。

    第三,单例模式还有基于模块的实现,不过我觉得模板的实现太复杂,第二个问题就是使用模板导致的,这里就不讨论了。

    3. 参考

    1. C++静态构造函数
    2. 解决静态全局变量初始化的相互依赖问题
    3. C++ 单例模式总结与剖析
    4. C++单例模式跨DLL是不是就是会出问题?
  • 相关阅读:
    [HDU 2089]不要62
    [WC 2011]Xor
    [BJOI 2011]元素
    [NOIp 2014]解方程
    [UVa 1326]Jurassic Remains
    [BZOJ 2152]聪聪可可
    [IOI 2011]Race
    [测试题]打地鼠
    [POJ 2828]Buy Tickets
    [测试题]gene
  • 原文地址:https://www.cnblogs.com/charlee44/p/16309003.html
Copyright © 2020-2023  润新知