8

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

 2 years ago
source link: https://www.cnblogs.com/charlee44/p/16309003.html
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
neoserver,ios ssh client

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

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。

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


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK