设计模式-单例模式

本文记录我对单例模式的学习。参考了如下链接:[C++设计模式——单例模式]

基本概念以及一种基本版本的实现

什么是单例模式?

何为单例模式,在GOF的《设计模式:可复用面向对象软件的基础》中是这样说的:保证一个类只有一个实例,并提供一个访问它的全局访问点。首先,需要保证一个类只有一个实例;在类中,要构造一个实例,就必须调用类的构造函数,如此,为了防止在外部调用类的构造函数而构造实例,需要将构造函数的访问权限标记为protected或private;最后,需要提供要给全局访问点,就需要在类中定义一个static函数,返回在类内部唯一构造的实例。意思很明白,使用UML类图表示如下。

  1. 这段是链接中的原文,我先说下我对这段话的理解。首先一点很明确的是,单例模式只能有一个实例,并且提供一个全局访问点。这个说的很清楚,关键是怎么实现这个东西。
  2. 肯定的是要避免生成栈对象,这点可以将析构函数放入protected实现。对于堆对象,直接通过new这种形式也不行,所以要把构造函数放入proteced。其实,看上面的原文我们知道,作者知识把构造函数放入proteced了,其实这么做也是可以的。因为此时栈对象肯定是不能生成的,此时也不能直接用new 去生成堆对象。需要提供这个全局的访问点。
  3. 仿照我之前的博文,此时提供一个静态方法,通过在这个方法之中调用new创建一个对象。这样可行的原因是内部调用,所以可行。为什么一定是静态方法,因为如果不是静态方法需要对象调用,可是对象都没有,怎么生成对象。所以,必须使用静态方法。
  4. 确定使用静态方法之后,还需要确定一点的就是,怎么保证唯一呢?仿照之前生成堆对象的方法,可以保存下这个堆对象的地址。然后,每次生成的时候判断以下,如果这个地址不为空。那么,证明已经生成了。
  5. 问题又来了,这个指针可以写成非静态的嘛?肯定也是不行的,这个指针是non static成员,它是需要对象声明周期的。所以,必须先存在一个对象,才能存在这个指针。所以,在对象还没有生成之前就要去保存它,只能是static成员。
  6. 相应的destroy方法,可以实现为静态的,保持一致嘛!当然,这个犯法不是静态的也是可以的。
  7. 下面是最大的一个问题:先看下代码
class DB {
public:

    static DB* open_db() {
        if( !dbptr ){
            dbptr = new DB(); // ???这里构造
            assert( dbptr );
        }
        return dbptr;
    }
    static void destroy_db() { 
        if( dbptr ){ // ???这里析构
            delete dbptr; 
            dbptr = NULL; 
        }
    }

public:
    virtual void print_version(){ std::cout << "Version 1.0" << std::endl;  }

protected:

    // No new DB() allowed
    DB(){ 
        if( !dbptr ){// ???这里构造
            dbptr = new DB();
            assert( dbptr );
        }
        std::cout << "DB() called." << std::endl;
    }

    // No stack object allowed
    ~DB(){ 
        if( dbptr ){// ???这里析构
            delete dbptr; 
            dbptr = NULL; 
        }
        std::cout << "~DB() called." << std::endl;
    }

private:
    // No copying allowed
    DB( const DB& ){}
    DB& operator=( const DB& ){}

private:
    static DB* dbptr;
};
DB* DB::dbptr = NULL;

上面这个问题其实困扰了我一小段时间,这是个很容易迷惑的问题。这是我第一次写出来的代码,当时我考虑到dbptr是类的成员,所以在构造函数里面构造,析构函数里面析构。最后,运行段错误。

其实仔细想一想内存布局和构造函数的作用即可,static member不在对象的内存布局当中。说的直接点,静态成员不属于对象。所以,它的初始化以及销毁不是构造函数和析构函数要负责的。

这个dbptr本质是对象的句柄,跟this指针是一样的,它并不是对象成员,所以和构造函数析构函数没有关系。它的初始化和销毁需要单独写create_db方法和destroy_db方法实现。刚好,静态方法处理静态成员。

// 正确代码1
#include <iostream>
#include <cassert>
#include <string>

class DB {
public:

    static DB* open_db() {
        if( !dbptr ){
            dbptr = new DB();
            assert( dbptr );
        }
        return dbptr;
    }
    static void destroy_db() { 
        if( dbptr ){
            delete dbptr; 
            dbptr = NULL; 
        }
    }

public:
    virtual void print_version(){ std::cout << "Version 1.0" << std::endl;  }

protected:

    // No new DB() allowed
    DB(){ 
        std::cout << "DB() called." << std::endl;
    }

    // No stack object allowed
    ~DB(){ 
        std::cout << "~DB() called." << std::endl;
    }

private:
    // No copying allowed
    DB( const DB& ){}
    DB& operator=( const DB& ){}

private:
    static DB* dbptr;
};
DB* DB::dbptr = NULL;

int main( void ){

    DB* db = DB::open_db();

    db->print_version();

    DB::destroy_db();

    return 0;
}
/*
DB() called.
Version 1.0
~DB() called.
*/

看下面这段代码,单例类具有自己的资源要管理。

// 正确代码2 - 管理自己的资源
#include <iostream>
#include <cassert>
#include <string>

class DB {
public:

    static DB* open_db(int n) {
        if( !dbptr ){
            dbptr = new DB(n); // 初始化句柄
            assert( dbptr );
        }
        return dbptr;
    }

    static void destroy_db() { 
        if( dbptr ){ // 销毁句柄
            delete dbptr; 
            dbptr = NULL; 
        }
    }

public:
    virtual void print_version(){ std::cout << "Version 1.0" << std::endl;  }

    void print_arr(){
        for( int i = 0; i < n; ++i ){
            std::cout << arr[i] << " ";
        }
        std::cout << std::endl;
    }

protected:

    // No new DB() allowed
    DB(){ 
        arr = NULL; n = 0;
        std::cout << "DB() called." << std::endl;
    }
    DB(int x) : n(x){
        arr = new int[n]; // 管理对象资源
        assert(arr);
        for( int i = 0; i < n; ++i ){
            arr[i] = i;
        }
        std::cout << "DB(int) called." << std::endl;
    }

    // No stack object allowed
    virtual ~DB(){
        if( arr ){
            delete arr; // 销毁对象资源
            arr = NULL;
        }
        std::cout << "~DB() called." << std::endl;
    }

private:
    // No copying allowed
    DB( const DB& ){}
    DB& operator=( const DB& ){}

private:
    static DB* dbptr;
private:
    int* arr;
    int n;
};
DB* DB::dbptr = NULL;

int main( void ){

    DB* db = DB::open_db(10);

    db->print_version();

    db->print_arr();

    DB::destroy_db();

    return 0;
}

下面自己仿照leveldb,自己写了个基于单例的简单内存数据库。

// 正确代码3 - 简单单例内存数据库
#include <iostream>
#include <cassert>
#include <map>
#include <string>

class DB {
public:

    static DB* open_db() {
        if( !dbptr ){
            dbptr = new DB();
            assert( dbptr );
        }
        return dbptr;
    }

    static void destroy_db() { 
        if( dbptr ){
            delete dbptr; 
            dbptr = NULL; 
        }
    }

public:

    void put_db( const std::string& key, const std::string& value ){
        mapper[key] = value;
    }
    bool get_db( const std::string& key, std::string* value ){
        if( mapper.find( key ) == mapper.end() ){
            value = NULL;
            std::cerr << "key is not exists " << std::endl;
            return false;
        }
        else{
            *value = mapper[key];
            return true;
        }
    }

protected:

    // No new DB() allowed
    DB(){ 
        std::cout << "DB() called." << std::endl;
    }

    // No stack object allowed
    virtual ~DB(){
        std::cout << "~DB() called." << std::endl;
    }

private:
    // No copying allowed
    DB( const DB& ){}
    DB& operator=( const DB& ){}

private:
    static DB* dbptr;
private:
    std::map< std::string, std::string > mapper; // mapping from string keys to string values
};
DB* DB::dbptr = NULL;

int main( void ){

    DB* db = DB::open_db();

    std::string key1 = "kang";
    std::string value1 = "98";
    std::string key2 = "xin";
    std::string value2 = "96";

    db->put_db( key1, value1 );
    db->put_db( key2, value2 );

    std::string ret;
    bool status;

    status = db->get_db( key1, &ret );
    if( status ) std::cout << ret << std::endl;

    status = db->get_db( "haha", &ret );
    if( status ) std::cout << ret << std::endl;

    DB::destroy_db();

    return 0;
}

上面的代码存在一点设计上的问题就是,这个类你是希望它可以被派生的,否则析构函数不会写成virtual,并且放在protected里面去。

那么问题来了,既然你的构造析构放在protected里面是为了能够被派生,那你把拷贝构造和赋值运算符函数放到private里面,派生类如何进行赋值和复制?因为这势必要调用基类的复制和赋值,这是没法做的!所以,统统放到proteced里面去。

// 正确代码3 - 简单单例内存数据库
#include <iostream>
#include <cassert>
#include <map>
#include <string>

class DB {
public:

    static DB* open_db() {
        if( !dbptr ){
            dbptr = new DB();
            assert( dbptr );
        }
        return dbptr;
    }

    static void destroy_db() { 
        if( dbptr ){
            delete dbptr; 
            dbptr = NULL; 
        }
    }

public:

    void put_db( const std::string& key, const std::string& value ){
        mapper[key] = value;
    }
    bool get_db( const std::string& key, std::string* value ){
        if( mapper.find( key ) == mapper.end() ){
            value = NULL;
            std::cerr << "key is not exists " << std::endl;
            return false;
        }
        else{
            *value = mapper[key];
            return true;
        }
    }

protected:

    // No new DB() allowed
    DB(){ 
        std::cout << "DB() called." << std::endl;
    }

    // No stack object allowed
    virtual ~DB(){
        std::cout << "~DB() called." << std::endl;
    }

    // No copying allowed
    DB( const DB& ){}
    DB& operator=( const DB& ){}

private:
    static DB* dbptr;
private:
    std::map< std::string, std::string > mapper; // mapping from string keys to string values
};
DB* DB::dbptr = NULL;

int main( void ){

    DB* db = DB::open_db();

    std::string key1 = "kang";
    std::string value1 = "98";
    std::string key2 = "xin";
    std::string value2 = "96";

    db->put_db( key1, value1 );
    db->put_db( key2, value2 );

    std::string ret;
    bool status;

    status = db->get_db( key1, &ret );
    if( status ) std::cout << ret << std::endl;

    status = db->get_db( "haha", &ret );
    if( status ) std::cout << ret << std::endl;

    DB::destroy_db();

    return 0;
}

当然,上面的写法也不是没有问题。因为,其实你把复制和赋值放入protected里面有什么意义嘛?因为复制和赋值的前提是必须有左值对象,可是栈对象和堆对象都是禁止的,怎么会产生左值。

最后的简化版本如下:

// 正确代码4 - 这么写也没问题,但是语义没有上面的好
// 因为这份代码没有显示的禁止复制和赋值
#include <iostream>
#include <cassert>

class DB {
public:
    static DB* create_db(){
        if( dbptr ){
            dbptr = new DB();
            assert(dbptr);
        }
        return dbptr;
    }
    static void destroy_db(){
        if( dbptr ){
            delete dbptr;
            dbptr = NULL;
        }
    }   

protected:
    DB(){}
    virtual ~DB(){}
private:
    static DB* dbptr;

};
DB* DB::dbptr = NULL;

int main( void ){

    DB* db = DB::create_db();

    DB::destroy_db();
    return 0;
}

线程安全版本的实现

先说说什么是线程安全?

线程安全这个说法我觉得从字面意思上来说是有点问题的,因为线程只是负责执行,它哪知道安不安全,真正负责安不安全问题的应该是线程执行的代码,也就是线程函数!所以应该说的是线程函数是否安全!

怎么会出现这种情况,其实这也不是什么新概念,我在Linux系统编程当中接触到了啊!比如一个全局变量g_val. 写了一个方法add(n)。可以对它执行加法n次,如果一个线程跑自然是没问题,可以加n次。但是在多线程环境下可能就会出问题,比如两个线程同时跑,应该加2n次,但是最后的结果一般都不是,这点我们之前讨论过。寄存器的问题。所谓的线程安全本质上指的是线程要执行的代码也就是线程函数是否能在多线程的环境下执行后的结果和预期达到一直。
比如,上面的add代码,如果要是线程安全的,可以加入锁的机制去实现。

锁机制

先尝试用多线程进行访问,我们看一下结果,先给出代码和运行结果:

// DB.h
#ifndef DB_H_
#define DB_H_
#include <iostream>
#include <assert.h>

class DB {
public:
    static DB* create_db(){
        if( !dbptr ){
            dbptr = new DB();
            assert( dbptr );
        }
        return dbptr;
    }
    static void destroy_db(){
        if(dbptr){
            delete dbptr;
            dbptr = NULL;
        }
    }
protected:
    DB() { std::cout << "DB() called." << std::endl; }
    virtual ~DB() { std::cout << "~DB() called." << std::endl; }
    // No copying allowed
    DB(const DB& rhs);
    DB& operator=( const DB& rhs );

private:
    static DB* dbptr;
    static pthread_mutex_t mut;
};
DB* DB::dbptr = NULL;

#endif
// main.cpp
#include <iostream>
#include <stdio.h>
#include <pthread.h>
#include <stdlib.h>
#include <errno.h>
#include <unistd.h>
#include "DB.h"
#define THREADS_NUM 2 // 线程数量

pthread_t tid_arr[THREADS_NUM];

void threads_init();
void threads_destroy();

void err_msg(int en, const char* msg);

void* thread_handle(void*);

int main( void ){

    threads_init();

    threads_destroy();

    exit( EXIT_SUCCESS );
}
void threads_init(){
    int ret;
    for(int i = 0; i < THREADS_NUM; ++i){
        ret = pthread_create( tid_arr + i, NULL, thread_handle, NULL );
        if( ret != 0 ){
            err_msg( ret, "pthread_create" );
        }
    }

}
void threads_destroy(){
    int ret;
    for( int i = 0; i < THREADS_NUM; ++i ){
        ret = pthread_join( tid_arr[i], NULL );
        if( ret != 0 ){
            err_msg( ret, "pthread_join" );
        }
    }
}

void err_msg(int en, const char* msg){
    errno = en;
    perror(msg);
    exit(EXIT_FAILURE);
}

void* thread_handle(void*) {
    std::cout << "-----------------" << pthread_self() << "------------------" << std::endl;

    DB* db = DB::create_db();
    sleep(3); // 打开数据库之后,模拟之后的行为
    std::cout << pthread_self() << " : " << db << std::endl;
    DB::destroy_db();

    std::cout << "-----------------" << pthread_self() << "------------------" << std::endl;
    return NULL;
}

结果如下:

kang@kang-pc:~/workspace/coding/c-c++/daily/20170731/test8$ sh run.sh 
----------------------------------139976290080512139976298473216------------------------------------

DB() called.
DB() called.
139976298473216139976290080512 :  : 0x7f4eb80008c0
~DB() called.
0x7f4ec00008c0
-----------------139976298473216------------------
-----------------139976290080512------------------
kang@kang-pc:~/workspace/coding/c-c++/daily/20170731/test8$ sh run.sh 
----------------------------------140265934665472140265926272768------------------------------------

DB() called.
DB() called.
140265934665472140265926272768 :  : 0x7f92300008c0
~DB() called.
0x7f92280008c0-----------------0x7f923535b700------------------

-----------------140265926272768------------------
kang@kang-pc:~/workspace/coding/c-c++/daily/20170731/test8$ sh run.sh 
----------------------------------140376729544448140376721151744------------------------------------

DB() called.
DB() called.
140376729544448 : 140376721151744 : 0x7fabfc0008c00x7fabf40008c0
~DB() called.

-----------------140376729544448------------------
-----------------140376721151744------------------
kang@kang-pc:~/workspace/coding/c-c++/daily/20170731/test8$ 

结果是上面的代码我执行了三次之后的结果,我们来分析下。
第一次的运行结果运行了两次 DB() called.证明产生了两个实例,这与单例模型是相违背的。为什么呢?
因为第一个线程创建之后,执行线程函数,在create_db的过程中,dbptr = new DB();这句话还没有执行完毕,第二个线程已经完毕并且也进入了create_db函数,此时,由于第一个线程还没有完成对dbptr的初始哈,第二个线程就来了,然后也创建了,在它创建的过程中,第一个完成了创建,dbptr被更新,返回给db1. 稍后,第二个也完成了创建,dbptr再次被更新,返回给db2。由于dbptr又被更新了,所以之前的DB对象,dbptr不再关联。虽然db1还关联,但是由于dbptr不再关联。导致,无法在最后析构。这也是为什么,构造了两次却析构了一次的原因。

不加锁导致的问题:
1. 多个实例,原因也很简单,第一个实例没有创建完成即dbptr没有完成赋值,第二个实例就开始创建了。
2. 析构一次。前一次创建的对象无法关联,内存丢失。
3. 多个实例的地址也不一样,这是当然的。


为了解决不加锁导致的多个实例的问题,我们对单例模式进行加锁。采用pthread_mutex_t类型。采用静态初始化的形式。

// DB.h
#ifndef DB_H_
#define DB_H_
#include <iostream>
#include <assert.h>
#include <pthread.h>

class DB {
public:
    static DB* create_db(){

        pthread_mutex_lock(&mut); // lock

        if( !dbptr ){
            dbptr = new DB();
            assert( dbptr );
        }

        pthread_mutex_unlock(&mut); // unlock

        return dbptr;
    }
    static void destroy_db(){
        if(dbptr){
            delete dbptr;
            dbptr = NULL;
        }
    }
protected:
    DB() { std::cout << "DB() called." << std::endl; }
    virtual ~DB() { std::cout << "~DB() called." << std::endl; }
    // No copying allowed
    DB(const DB& rhs);
    DB& operator=( const DB& rhs );

private:
    static DB* dbptr;
    static pthread_mutex_t mut;
};
DB* DB::dbptr = NULL;
pthread_mutex_t DB::mut = PTHREAD_MUTEX_INITIALIZER;

#endif
单例模式-锁机制-版本一

注意上面的实现,锁需要写成静态变量,这样可以先进行初始化。因为锁的初始化一定要在execution time之前。

// main.cpp
#include <iostream>
#include <stdio.h>
#include <pthread.h>
#include <stdlib.h>
#include <errno.h>
#include <unistd.h>
#include "DB.h"
#define THREADS_NUM 2

pthread_t tid_arr[THREADS_NUM];

void threads_init();
void threads_destroy();

void err_msg(int en, const char* msg);

void* thread_handle(void*);

int main( void ){

    threads_init();

    threads_destroy();

    exit( EXIT_SUCCESS );
}
void threads_init(){
    int ret;
    for(int i = 0; i < THREADS_NUM; ++i){
        ret = pthread_create( tid_arr + i, NULL, thread_handle, NULL );
        if( ret != 0 ){
            err_msg( ret, "pthread_create" );
        }
    }

}
void threads_destroy(){
    int ret;
    for( int i = 0; i < THREADS_NUM; ++i ){
        ret = pthread_join( tid_arr[i], NULL );
        if( ret != 0 ){
            err_msg( ret, "pthread_join" );
        }
    }
}

void err_msg(int en, const char* msg){
    errno = en;
    perror(msg);
    exit(EXIT_FAILURE);
}

void* thread_handle(void*) {
    std::cout << "-----------------" << pthread_self() << "------------------" << std::endl;

    DB* db = DB::create_db();
    sleep(3);
    std::cout << pthread_self() << " : " << db << std::endl;
    DB::destroy_db();

    std::cout << "-----------------" << pthread_self() << "------------------" << std::endl;
    return NULL;
}
/*
kang@kang-pc:~/workspace/coding/c-c++/daily/20170731/test8$ sh run.sh 
----------------------------------140408877844224140408869451520------------------------------------

DB() called.
140408877844224140408869451520 :  : 0x7fb3780008c0
~DB() called.
0x7fb3780008c0
-----------------140408869451520------------------
-----------------140408877844224------------------
*/

从上面的代码可以看出,只出现了一个实例。并且他们的地址是一样的。

单例模式-锁机制-版本二

对于上面的代码,我们仔细分析加锁解锁的地方,我们会发现。加锁的目的是担心这种情形:两个线程可能同时打开数据库的这种情形,因为只是判断指针是否为空无法区别他们,所以加锁用来实习互斥变量的访问。但是,对于两个线程在打开数据库的时间间隔稍微大一点的情形,此种方法则会降低性能。比如,线程一已经创建了单例,dbptr已经被赋值了,此时如果有线程再去访问,其实没有必要对它加锁。

因此,我们采用double check的方法。第一次check主要是避免两个线程的访问时间间隔相对较大的情形,第二次check之前加入锁机制,主要是为了区别两个线程访问时间相差不大的情形。

// DB.h
#ifndef DB_H_
#define DB_H_
#include <iostream>
#include <assert.h>
#include <pthread.h>

class DB {
public:
    static DB* create_db(){

        if( !dbptr ){ // double check

            pthread_mutex_lock(&mut); // lock

            if(!dbptr){

                dbptr = new DB();
                assert( dbptr );
            }

            pthread_mutex_unlock(&mut); // unlock
        }


        return dbptr;
    }
    static void destroy_db(){
        if(dbptr){
            delete dbptr;
            dbptr = NULL;
        }
    }
protected:
    DB() { std::cout << "DB() called." << std::endl; }
    virtual ~DB() { std::cout << "~DB() called." << std::endl; }
    // No copying allowed
    DB(const DB& rhs);
    DB& operator=( const DB& rhs );

private:
    static DB* dbptr;
    static pthread_mutex_t mut;
};
DB* DB::dbptr = NULL;
pthread_mutex_t DB::mut = PTHREAD_MUTEX_INITIALIZER;

#endif
// main.cpp
#include <iostream>
#include <stdio.h>
#include <pthread.h>
#include <stdlib.h>
#include <errno.h>
#include <unistd.h>
#include "DB.h"
#define THREADS_NUM 2

pthread_t tid_arr[THREADS_NUM];

void threads_init();
void threads_destroy();

void err_msg(int en, const char* msg);

void* thread_handle(void*);

int main( void ){

    threads_init();

    threads_destroy();

    exit( EXIT_SUCCESS );
}
void threads_init(){
    int ret;
    for(int i = 0; i < THREADS_NUM; ++i){
        ret = pthread_create( tid_arr + i, NULL, thread_handle, NULL );
        if( ret != 0 ){
            err_msg( ret, "pthread_create" );
        }
    }

}
void threads_destroy(){
    int ret;
    for( int i = 0; i < THREADS_NUM; ++i ){
        ret = pthread_join( tid_arr[i], NULL );
        if( ret != 0 ){
            err_msg( ret, "pthread_join" );
        }
    }
}

void err_msg(int en, const char* msg){
    errno = en;
    perror(msg);
    exit(EXIT_FAILURE);
}

void* thread_handle(void*) {
    std::cout << "-----------------" << pthread_self() << "------------------" << std::endl;

    DB* db = DB::create_db();
    sleep(3);
    std::cout << pthread_self() << " : " << db << std::endl;
    DB::destroy_db();

    std::cout << "-----------------" << pthread_self() << "------------------" << std::endl;
    return NULL;
}
/*
kang@kang-pc:~/workspace/coding/c-c++/daily/20170731/test8$ sh run.sh 
-----------------140518216046336------------------
DB() called.
-----------------140518207653632------------------
140518207653632 : 0x7fccec0008c0
~DB() called.
-----------------140518207653632------------------
140518216046336 : 0x7fccec0008c0
-----------------140518216046336------------------
kang@kang-pc:~/workspace/coding/c-c++/daily/20170731/test8$ sh run.sh 
----------------------------------140196417206016140196425598720------------------------------------

DB() called.
140196417206016140196425598720 :  : 0x7f82000008c0
~DB() called.
0x7f82000008c0
-----------------140196417206016------------------
-----------------140196425598720------------------
*/

从上面的结果可知,只生成了一次单例对象,析构了一次单例对象。输出的地址也是一样的。没有问题了!

静态成员机制

通过以上分析可知,锁机制可以很好的实现线程安全的单例模式。但是,也不是没有问题,由于加锁解锁机制需要付出资源的代价,所以当线程数比较多的时候,频繁的去访问这个单例需要频繁的加锁机制。成为性能的瓶颈。

那么我们分析静态成员实现的机制:

分析单例模式的基本实现,我们发现,单例模式的句柄是一个dbptr,其具体创建是在execution time的时候。那么我们再仔细分析,如果用多线程去访问的时候,我们肯定得是在程序已经运行起来之后,在execution time创建线程,然后线程对单例进行访问。在此时的时候需要进行判断dbptr是否已经被创建单例。
那么,我们考虑有没有一种机制在execution time之前,就把这个单例创建出来,那么其余的线程此时不需要再去判断是否创建,只需得到这个单例即可。因为已经创建过了。
方法就是:你在compile time的时候就把这个单例创建出来,那么程序开始执行之后,其余线程就不必再判断是否要创建了,直接得到使用即可。因为类的静态成员在compile time进行初始化,在static area进行创建。所以,此时程序代码还没有开始执行。此时创建的单例,程序开始执行之后,其余线程只需访问即可。

方法一

这种思路的基本思想和基本方法一直,还是保留dbptr,只不过初始化的时候就开辟堆对象进行初始化。有几个点需要注意:
1. 为什么是const static DB* dbptr; 这点我也是有疑惑,其实不用const也是可以的。首先需要说明白的是,这种方法并不会导致我在const_cast当中提到的undefined behaviour的情形,因为堆对象本生并不是常量,所以用常量指针指向之后,后面返回的时候用const_cast转换回去就可以了。但是,为什么要用const,我的理解就是个语义上的操作,dbptr仅仅作为db当前的句柄返回,不希望通过它返回。
2. 说的是dbptr初始化的时候,new为什么可以,因为这个任然是在类的作用于之内。

// compile time 生成单例
#include <iostream>
#include <string>
#include <map>

class DB {
public:
    static DB* create_db(){
        return const_cast<DB*>(dbptr);
    }
    static void destroy_db(){
        if(dbptr){
            delete dbptr;
            dbptr = NULL;
        }
    }
protected:
    DB() { std::cout << "DB() called." << std::endl; }
    virtual ~DB() { std::cout << "~DB() called." << std::endl; }

    // No copying allowed
    DB(DB&);
    DB& operator=(DB&);

private:
    static const DB* dbptr;
};

const DB* DB::dbptr = new DB(); // 这里看起来不合法,但是合法。DB* db = new DB();不合法,但是此时在类的作用于之内,不是外部调用。

int main(){

    DB* db1 = DB::create_db();
    DB* db2 = DB::create_db();

    std::cout << db1 << std::endl;
    std::cout << db2 << std::endl;

    DB::destroy_db();
    return 0;
}
方法二

还是希望利用在compile时期就生成单例的思路,这次我们不用堆对象。我们直接生成一个static对象就行了。这就是它的思路,把单例直接做成static object.看下面这个版本,几点注意:
1. 直接生成static对象,进入execution time的时候,直接返回它的地址就可以了。注意:static对象并不是在compile time就生成的。它和栈对象一样,什么时候用什么时候进行初始化,只不过它存在static area,生命周期很长。只会初始化一次!下次调用函数返回的还是之前的值,不会再次生成对象了,所以保证了永远只有一个单例
2. 这种情况下,禁止的语法和之前一样。栈对象,拷贝复制的情形,new不需要禁止,因为构造函数被禁止了,其实还是可以堆对象,但是由于没有提供一个外部调用去进行内部调用生成堆对象,也不行。所以,这种方式最简单有效。
3. 这种方法最大的优点不用垃圾回收,因为垃圾回收行为被挂到了static对象上面,这个程序结束,系统要收回static对象,从而触发挂在它身上的垃圾回收方法。很棒!

// static 单例
// 自动进行垃圾回收
#include <iostream>

class DB {
public:

    static DB* create_db(){
        static DB db;
        return &db;
    }

protected:
    DB() { std::cout << "DB() called." << std::endl; } 
    virtual ~DB() { std::cout << "~DB() called." << std::endl; } 

    DB( const DB& rhs );
    DB& operator=( const DB& rhs );

};

int main( void ){

    DB* db1 = DB::create_db();
    DB* db2 = DB::create_db();

    std::cout << db1 << std::endl;
    std::cout << db2 << std::endl;

    return 0;
}

垃圾回收机制讨论

单例模式的第四种实现,static object单例可以实现一个自动垃圾回收机制。

#include <iostream>
#include <cassert>

class DB {
public:

    static DB* create_db(int n){
        static DB db(n);
        return &db;
    }
public:
    void print(){
        for(int i = 0; i < n; ++i){
            std::cout << arr[i] << std::endl;
        }
    }
protected:
    DB() { std::cout << "DB() called." << std::endl; } 
    DB(int x) : n(x) { arr = new int[n]; assert(arr); for( int i = 0; i < n; ++i ) arr[i] = i; std::cout << "DB(int) called." << std::endl; }
    virtual ~DB() { delete [] arr; std::cout << "~DB() called." << std::endl; } 

    DB( const DB& rhs );
    DB& operator=( const DB& rhs );
private:
    int* arr;
    int n;
};

int main( void ){

    std::cout << "main entered: " << std::endl;
    DB* db1 = DB::create_db(3);
    DB* db2 = DB::create_db(3);

    std::cout << db1 << std::endl;
    std::cout << db2 << std::endl;

    db1->print();
    db2->print();

    return 0;
}
/*
main entered: 
DB(int) called.
0x602240
0x602240
0
1
2
0
1
2
~DB() called.
*/

下面我们讨论下这个东西?
1. 什么是垃圾回收机制?

先说我自己的理解,就是负责对自己申请资源的自动回收。如果某个对象进行了资源的申请,当对象结束使用时,此时的资源就变成了“垃圾”,那么需要对资源进行回收,也就是所谓的垃圾回收机制。

2 . 垃圾回收机制(Garbage collector)的原理是什么?

我的理解是,定义一个静态对象或者全局对象,把垃圾回收的操作都挂在上面。当该对象的生命周期结束之后,系统要负责对静态对象或者栈对象进行回收,所以系统在对该对象进行回收时会触发挂在该对象上面的资源回收操作,从而使得资源被正确回收。由于这一切行为看似都是由于系统自动回收栈对象而导致的,所以它是一种自动回收机制。

明白了这个思路之后,我们可以实现一个简单版本的带有垃圾回收机制的单例模式:

// 基本版本
#include <iostream>

class DB {
public:
    static DB* create_db(){
        if(!dbptr){
            dbptr = new DB();
            return dbptr;
        }
    }
    /*
    static void destroy(){
        if( dbptr ){
            delete dbptr;
            dbptr = NULL;
        }
    }*/
protected:
    DB() { std::cout << "DB() called." << std::endl; }
    virtual ~DB() { std::cout << "~DB() called." << std::endl; }
    // No copying allowed
    DB(DB& rhs);
    DB& operator=(DB& rhs);
protected:
    class GC{
    public:
        GC(){}
        ~GC(){
            if( dbptr ){
                delete dbptr;
                dbptr = NULL;
            }
        }
    };
    static GC gc;
private:
    static DB* dbptr;
};

DB* DB::dbptr = NULL;
DB::GC DB::gc; //  静态成员别忘了初始化

int main( void ){

    DB* db1 = DB::create_db();
    DB* db2 = DB::create_db();

    std::cout << db1 << std::endl;
    std::cout << db2 << std::endl;

    return 0;
}
/*
DB() called.
0x14d5010
0x14d5010
~DB() called.
*/

把析构行为都挂在了栈对象gc上面,static对象由系统在程序结束时进行资源回收,从而触发析构函数里面的行为,析构了整个对象。

#include <iostream>

class DB {
public:
    static DB* create_db(){
        return dbptr;
    }
protected:
    DB() { std::cout << "DB() called." << std::endl; }
    virtual ~DB() { std::cout << "~DB() called." << std::endl; }
    // No copying allowed
    DB( const DB& );
    DB& operator=( const DB& );
protected:
    class GC {
    public:
        GC() { std::cout << "GC() called." << std::endl; }
        ~GC() {
            if(dbptr){
                delete dbptr;
                dbptr = NULL;
            }
            std::cout << "~GC() called." << std::endl;
        }
    };
    static GC gc;
private:
    static DB* dbptr; // 这里没用const
};

DB* DB::dbptr = new DB(); // 类的作用域
DB::GC DB::gc;

int main( void ){

    DB* db1 = DB::create_db();
    DB* db2 = DB::create_db();

    std::cout << db1 << std::endl;
    std::cout << db2 << std::endl;

    return 0;
}

还是一样的,把析构操作挂在静态对象上面,这样程序结束时,系统析构静态对象,挂在这个静态对象上面的析构行为会对单例对象进行析构。


下面分析下下面的代码:

// 错误代码
// 栈对象不能挂析构操作
// 理论上来说全局对象和静态对象都可以,但是实际上只有静态对象才可以
#include <iostream>

class DB {
public:
    static DB* create_db(){
        return dbptr;
    }
protected:
    DB() { std::cout << "DB() called." << std::endl; }
    virtual ~DB() { std::cout << "~DB() called." << std::endl; }
    // No copying allowed
    DB( const DB& );
    DB& operator=( const DB& );
protected:
    class GC {
    public:
        GC() { std::cout << "GC() called." << std::endl; }
        ~GC() {
            if(dbptr){
                delete dbptr;
                dbptr = NULL;
            }
            std::cout << "~GC() called." << std::endl;
        }
    };
    //static GC gc;
    GC gc;
private:
    static DB* dbptr;
};

DB* DB::dbptr = new DB();
//DB::GC DB::gc;

int main( void ){

    DB* db1 = DB::create_db();
    DB* db2 = DB::create_db();

    std::cout << db1 << std::endl;
    std::cout << db2 << std::endl;

    return 0;
}

我们说一下为什么栈对象不行!

因为栈对象gc是单例对象内存布局当中的对象,它是一个栈对象,所以你要用它析构单例对象肯定不行。因为析构操作不能从内部发起。我们再仔细分析,单例对象是堆对象,程序结束前必须由程序员主动delelte进行释放,可是,现在把gc写成栈对象之后会引起一个问题,因为它在单例对象的内部当中,所以,只有单例对象被析构,才会引起gc的析构,gc的析构又会析构整个单例对象。这存在一个矛盾,既然单例对象的析构引起gc的析构,gc又有什么必要再去析构单例对象呢?

所以,就像dbptr是整个单例对象的句柄一样,我们现在需要的是在对象内部布局之外的一个对象a,系统通过析构这个a,从而导致a的析构引起单例对象的析构。应该是这样的一个逻辑,所以只能是全局对象或者静态对象,但是全局对象没有办法写在类里面,导致,只能是静态对象才可以。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值