定义
单例模式的本意是确保在整个运行时间和运行空间内某种数据类型只有一个唯一的实例,并且提供一个全局的访问接口。
我们可从实例创建和访问两个角度,更深入的理解单例模式:(1)数据类型有且仅可创建一个实例,编程人员不能像普通的数据类型一样,随意定义此类型的实例。它是一个阉割的数据类型,限制类型实例的定义和创建。(2)此访问接口是全局唯一实例的访问接口,而非普通意义上的数据访问接口。
实现
一般单例模式按照创建对象创建和调用的时序关系可分为懒汉式和饥汉式。饥汉式一般在程序启动时创建对象,非Lazy初始化;懒汉式在真正使用时在创建,采用Lazy初始化。
饥汉式
饿汉式,就像饿汉一样,不论自身需要与否,在程序启动时即开始创建。在C++中一般声明为全局变量实现饿汉式,全局变量会在main函数执行之前创建全局变量对象,在main函数执行结束之后释放全局变量。
可执行程序有一个全局_CTOR_LIST函数指针数组,编译器在链接时会把全局变量的构造函数指针添加到_CTOR_LIST中;然后可执行程序在执行main函数之前,会遍历并执行此_CTOR_LIST中的所有函数指针,这样就完成了全局变量的构造。
同样可执行程序也存在一个全局_DTOR_LIST函数指针数组,编译器在链接时会把全局变量的析构 函数指针添加到_DTOR_LIST;在可执行程序执行main函数之后,会遍历并执行此_DTOR_LIST中的所有函数指针,这样就完成了全局变量的析构。
熟悉了C++饿汉式全局变量的构造过程,我们参考全局变量原理构造原理实现C语言饥汉式。幸运的是GCC和MSVC都提供了相应的机制实现main之前和之后调用函数。
GCC
GCC可以使用attribute关键字,声明constructor和destructor C函数,声明为constructor函数就会在main之前调用,声明为destructor的函数就会在main之后调用。
#include<stdio.h>
// 声明一个constructor属性函数,此函数会在main之前运行
__attribute__((constructor)) void before()
{
printf("run function before main\n");
}
// 声明一个destructor属性函数,此函数会在main之后运行
__attribute__((destructor)) void after()
{
printf("run function after main\n");
}
int main(int argc, char **argv)
{
printf("run main function\n");
return 0;
}
参考上述GCC main函数前执行函数原理,我们可以实现GCC版本饿汉单例版本。
单例声明
#pragma once
#include <stdio.h>
#include <stdlib.h>
typedef void File;
typedef enum BOOL
{
FALSE = 0,
TRUE = 1,
}BOOL;
typedef struct tagFileManager
{
File* (*mkFile)(const char* fileName, char const* mode);
BOOL (*rmFile)(const char* fileName);
int (*write)(File* file, const char* buf, int size);
BOOL (*exit)(const char* fileName);
BOOL (*close)(const File* file);
}FileManager;
单例实现
#include "file_manager.h"
#include <io.h>
static FileManager g_fileManager;
typedef int constructor();
static File* mkFile(const char* fileName, char const* mode);
static BOOL rmFile(const char* fileName);
static int fileWrite(File* file, const char* buf, int size);
static BOOL isExit(const char* fileName);
static BOOL fileClose(const File* file);
File* mkFile(const char* fileName, char const* mode)
{
FILE* file = NULL;
if (0 == fopen_s(&file, fileName, mode))
{
return file;
}
return NULL;
}
BOOL rmFile(const char* fileName)
{
if (isExit(fileName))
{
return !remove(fileName);
}
}
int fileWrite(File* file, const char* buf, int size)
{
return fwrite(buf, size, 1, file);
}
BOOL isExit(const char* fileName)
{
return (_access(fileName, 0) == 0);
}
BOOL fileClose(const File* file)
{
return !fclose(file);
}
__attribute__((constructor)) static int ctor()
{
g_fileManager.exit = isExit;
g_fileManager.mkFile = mkFile;
g_fileManager.rmFile = rmFile;
g_fileManager.write = fileWrite;
g_fileManager.close = fileClose;
return 0;
}
__attribute__((destructor)) static int dtor()
{
g_fileManager.exit = NULL;
g_fileManager.mkFile = NULL;
g_fileManager.rmFile = NULL;
g_fileManager.write = NULL;
g_fileManager.close = NULL;
return 0;
}
FileManager* fileManager()
{
return &g_fileManager;
}
MSVC
MSVC通过声明代码段名实现,我们声明两个特殊的段名称".CRT X I U " , " . C R T XIU",".CRT XIU",".CRTXCU",链接器会把声明在".CRT X I U " 段 中 的 函 数 , 放 在 " C 初 始 化 函 数 表 " 中 , 同 样 会 把 声 明 在 " . C R T XIU"段中的函数,放在"C初始化函数表"中,同样会把声明在".CRT XIU"段中的函数,放在"C初始化函数表"中,同样会把声明在".CRTXCU"段中的函数放在"C++初始化函数表"中。MSVC可执行程序在执行main函数之前,会首先遍历"C初始化函数表"和"C++初始化函数表"并依次执行函数。而main之后执行函数,则必须通过_onexit()注册完成。
#include <stdlib.h>
int before1()
{
printf("run function before1 before main\n");
return 0;
}
int before2()
{
printf("run function before2 before main\n");
return 0;
}
int after()
{
printf("run function after main\n");
return 0;
}
typedef int constructor();
#pragma data_seg(".CRT$XIU")
static constructor *beforeTab1[] = {before1};
#pragma data_seg(".CRT$XCU")
static constructor *beforeTab2[] = {before2};
#pragma data_seg()
int _tmain(int argc, _TCHAR *argv[])
{
_onexit(after);
printf("run main function\n");
return 0;
}
基于MSVC main函数前后执行函数的原理分析,我们可以编写出MSVC版本的饿汉单例模式
单例声明
#pragma once
#include <stdio.h>
#include <stdlib.h>
typedef void File;
typedef enum BOOL
{
FALSE = 0,
TRUE = 1,
}BOOL;
typedef struct tagFileManager
{
File* (*mkFile)(const char* fileName, char const* mode);
BOOL (*rmFile)(const char* fileName);
int (*write)(File* file, const char* buf, int size);
BOOL (*exit)(const char* fileName);
BOOL (*close)(const File* file);
}FileManager;
FileManager* fileManager();
单例实现
#include "file_manager.h"
#include <io.h>
static FileManager g_fileManager;
typedef int constructor();
static File* mkFile(const char* fileName, char const* mode);
static BOOL rmFile(const char* fileName);
static int fileWrite(File* file, const char* buf, int size);
static BOOL isExit(const char* fileName);
static BOOL fileClose(const File* file);
static int ctor();
static int dtor();
File* mkFile(const char* fileName, char const* mode)
{
FILE* file = NULL;
if (0 == fopen_s(&file, fileName, mode))
{
return file;
}
return NULL;
}
BOOL rmFile(const char* fileName)
{
if (isExit(fileName))
{
return !remove(fileName);
}
}
int fileWrite(File* file, const char* buf, int size)
{
return fwrite(buf, size, 1, file);
}
BOOL isExit(const char* fileName)
{
return (_access(fileName, 0) == 0);
}
BOOL fileClose(const File* file)
{
return !fclose(file);
}
static int ctor()
{
g_fileManager.exit = isExit;
g_fileManager.mkFile = mkFile;
g_fileManager.rmFile = rmFile;
g_fileManager.write = fileWrite;
g_fileManager.close = fileClose;
_onexit(dtor);
return 0;
}
static int dtor()
{
g_fileManager.exit = NULL;
g_fileManager.mkFile = NULL;
g_fileManager.rmFile = NULL;
g_fileManager.write = NULL;
g_fileManager.close = NULL;
return 0;
}
#pragma data_seg(".CRT$XIU")
static constructor * before[] = { ctor };
#pragma data_seg()
FileManager* fileManager()
{
return &g_fileManager;
}
总结
单例模式的是保障在整个运行时间和运行空间内某种数据类型只有一个唯一的实例,并且提供一个全局的访问接口。本文介绍了单例的目的和单例的饿汉式实现方式。下一单元我们将会继续讨论单例的懒汉式实现方式。