现在的位置: 首页 > 自动控制 > 工业·编程 > 正文

工作和面试中的单例

2018-10-07 11:05 工业·编程 ⁄ 共 3164字 ⁄ 字号 暂无评论

单例是什么?单例是一种特殊的类,用于确保只有一个对象,同时提供一种全局访问这个对象的方法。最近在工作中体验了一把5分钟将一个类改造成单例,感觉还是蛮不错的,所以我决定写一篇文章,跟大家交流技术和经验。

单例的原理是利用C++中的静态成员变量和静态成员函数,同时禁用构造函数的方法,达到只有一个对象实例的目的。

具体来说,设计一个单例的要点如下:

(1)类的静态成员变量是该类的指针。

(2)类的静态成员函数负责返回唯一的实例,当(1)中的指针不为空时就直接返回,否则为该指针new一个对象。

(3)类的默认构造函数访问权限设计为非public的(如protected或private)。

(4)禁用类的拷贝构造函数和赋值运算符函数(可以设置为private而不实现,或者使用C++11的=delete语法)。

可见,(3)和(4)是为了阻止通过其他方法创建对象,因此只能通过(2)中提供的静态方法获得实例。以下是一个单例的代码:

#include <cstdio>

#include <string>

 

// 一个简单的单例

class Singleton {

public:

  static Singleton* Instance() {

    if (singleton_ != NULL) {

      return singleton_;

    }

    singleton_ = new Singleton();

    return singleton_;

  }

  void SetData(std::string data) {

    singleton_->data_ = data;

  }

  std::string GetData() {

    return singleton_->data_;

  }

 private:

  Singleton() {}

  Singleton(const Singleton& other);

  Singleton& operator=(const Singleton& other);

private:

  static Singleton* singleton_;

  std::string data_;

};

 

// 初始化类的静态成员变量指针

Singleton* Singleton::singleton_ = NULL;

 

int main(int argc, char** argv) {

  // 创建对象

  Singleton* instance = Singleton::Instance();

 

  instance->SetData("Singleton test");

  Singleton* instance2 = Singleton::Instance();

  Singleton* instance3 = Singleton::Instance();

  printf("%s\n", instance->GetData().c_str());

  printf("%s\n", instance2->GetData().c_str());

  printf("%s\n", instance3->GetData().c_str());

  return 0;

}

说起单例,还要聊聊全局变量。阅读代码时有一种体验叫“全局变量厌烦症”(惭愧,我自创的),这种感觉就像在车站或商场大家在有序排队,突然有特权分子要强行插队一样糟糕。全局变量就像代码里的特权分子,它在使用之前不需要构造,而且会污染名字空间。而单例则没有这些缺点,单例本质上是一个特殊的类,使用单例时仍需要先构造再使用。

如何将一个现有的类快速改造成单例呢?答案是只需要给这个类增加一个静态成员指针(该类自身的指针),同时提供一个静态的Set和Get方法,Set方法用于将一个普通的对象指针传递给静态成员指针,Get方法直接返回静态成员指针。如下所示:

#include <cstdio>

#include <string>

#include <vector>

 

namespace tools {

 

// 字符串转换工具类

class Translate {

public:

  void Init(bool tolower_flag) {

    tolower_flag_ = tolower_flag;

  }

  void Work(const std::string src, std::string* dst);

  static void SetTranslate(Translate* translate) {

    translate_ = translate;

  }

  static Translate* GetTranslate() {

    return translate_;

  }

private:

  bool tolower_flag_;

  static Translate* translate_;

};

 

Translate* Translate::translate_ = NULL;

 

void Translate::Work(const std::string src,

                     std::string* dst) {

  if (tolower_flag_) {

    for (size_t i = 0; i < src.length(); ++i) {

      dst->push_back(tolower(src[i]));

    }

    dst->push_back('\0');

  } else {

    *dst = src;

  }

}

} // namespace tools

 

namespace app {

 

// 示例:一个不能修改参数的函数

void DoNotModifyMyParameters(const std::string data) {

  std::string mydata;

  // 获取工具类实例

  tools::Translate* translate = tools::Translate::GetTranslate();

  translate->Work(data, &mydata);

  printf("%s\n", mydata.c_str());

}

} // namespace app

 

int main(int argc, char** argv) {

  tools::Translate translate;

  translate.Init(true);

  tools::Translate::SetTranslate(&translate);

 

  std::string data = "Test TEST";

  app::DoNotModifyMyParameters(data);

  return 0;

}

细心的同学会发现,以上做法并不能保证这个类只生成一个对象,这也能叫单例吗?是的,这个类不是设计模式中的单例,但是在工程场景中,它就是单例。实际工程中有些类的对象只会被创建一份,但是并没有被设计成单例。所以“单例”是由类以外的代码保证的。

我们这样做的目的是为了方便在其他函数中使用这个类,而又不用为函数增加新的参数(真实情况是那个函数由于设计原因,不支持传新的参数,没听说过对吧?但它是存在的,在此先挖个坑,限于篇幅就不扩展开了,以后再填它)。

简而言之,工作中写单例,追求简单粗暴有效。虽然它不符合《设计模式》里规定的一些条件,属于“投机取巧”,不是一个完美的设计,但是真的很好用啊。

最后聊聊面试中的单例,常见问题有如何设计一个可以被继承的单例?多线程中如何安全的初始化单例?第一个问题比较简单,只要注意将基类的构造函数设计为protected,同时析构函数设置为virtual就好了。第二个问题注意不要使用pthread_mutex_lock+double check的做法,由于编译器会指令重排,因此上述做法是行不通的。正确做法是使用pthread_once来保证初始化代码只被执行一次。

面试主要靠积累,其次看运气,最终还是态度和细节决定成败

给我留言

留言无头像?