面试复习 设计模式

1 单例模式

单例模式是一种常用的软件设计模式,它确保一个类仅有一个实例,并提供全局访问点。在C++中,我们可以使用静态成员变量和私有构造函数来实现。

下面是C++的懒汉式和饿汉式单例模式的例子:

1.1 饿汉式(Eager Initialization)单例模式

饿汉式单例模式在程序启动时就创建了唯一的单例,所以不存在多线程同步问题。

class Singleton {
private:
    static Singleton* instance;
    Singleton() {}  // 私有化构造函数

public:
    // 删除拷贝构造函数和赋值运算符
    Singleton(const Singleton&) = delete;
    Singleton& operator=(const Singleton&) = delete;

    static Singleton* getInstance() {
        return instance;
    }
};

// 在类外部初始化
Singleton* Singleton::instance = new Singleton();

注意这两句:

    Singleton(const Singleton&) = delete;
    Singleton& operator=(const Singleton&) = delete;

在C++中,这两个语句被用来禁止类的拷贝。它们是删除函数(deleted function)的特殊类型。当一个函数被声明为删除的时候,编译器将不允许该函数被调用。

具体到你提供的例子:

Singleton(const Singleton&) = delete; 这个语句禁止了复制构造函数。也就是说,你不能创建一个新的Singleton对象作为旧的Singleton对象的复制。例如,假设s1是一个Singleton对象,以下代码是不合法的: Singleton s2 = s1;

Singleton& operator=(const Singleton&) = delete; 这个语句禁止了赋值运算符。也就是说,你不能将一个Singleton对象赋值给另一个。例如,假设s1和s2都是Singleton对象,以下代码是不合法的: s1 = s2;

将函数声明为private和声明为delete在C++中有着不同的含义,主要区别如下:

  1. Private Functions:当你将一个函数定义为private时,意味着这个函数只能在其所属的类(包括这个类的友元类和函数)中被访问和调用。尝试在类外部访问或调用这个函数会导致编译错误。私有函数通常用于在类内部执行某些操作,而这些操作对类的使用者来说是不可见的。
class MyClass {
private:
    void myPrivateFunction() { 
        // this function can only be accessed within MyClass
    }
 };
  1. Deleted Functions:你可以将一个函数定义为delete来明确表示该函数是禁止使用的。这通常用于禁止某些默认生成的函数,例如复制构造函数或赋值运算符,以防止对象的复制或某些其他不希望看到的行为。如果试图调用被删除的函数,会导致编译错误。
class MyClass {
public:
    MyClass(const MyClass&) = delete;  // Disallow copying
    MyClass& operator=(const MyClass&) = delete;  // Disallow assignment
};

总的来说,两者都可以避免函数的误用,但应用的上下文和方式有所不同。将函数声明为private是为了在类的外部隐藏它,而将函数声明为delete是为了完全禁止其使用。

1.2 懒汉式(Lazy Initialization)单例模式

懒汉式单例模式在第一次被调用时才创建单例。但是,这种方式需要考虑到多线程环境下的线程安全问题。下面的代码使用了双检锁(Double-Checked Locking)来确保线程安全。

#include <mutex>

class Singleton {
private:
    static Singleton* instance;
    static std::mutex mtx;
    Singleton() {}  // 私有化构造函数

public:
    // 删除拷贝构造函数和赋值运算符
    Singleton(const Singleton&) = delete;
    Singleton& operator=(const Singleton&) = delete;

    static Singleton* getInstance() {  
        if (instance == nullptr) {  // 第一次检查
            std::lock_guard<std::mutex> lock(mtx);
            if (instance == nullptr) {  // 第二次检查
                instance = new Singleton();  // 创建实例
            }
        }
        return instance;
    }
};

// 在类外部初始化
Singleton* Singleton::instance = nullptr;
std::mutex Singleton::mtx;

这里std::lock_guard<std::mutex> lock(mtx); 这一行代码的作用是对 mtx 这个互斥量(锁)进行上锁。对互斥量上锁可以防止多个线程同时访问同一段临界区代码,这样就可以避免数据竞态和其它并发问题。

std::lock_guard 是 C++11 中引入的一个 RAII 风格的锁管理类,它在构造函数中接收一个 std::mutex 并立即对其进行上锁,然后在析构函数中自动解锁。所以我们通常会将其在需要进行线程同步的代码块之前创建,一旦超出其生存期(即离开这个作用域),它就会自动解锁,保证了锁必定会被释放,从而避免了死锁。

在这个例子中,使用双检查锁模式(DCL, Double-Checked Locking Pattern)来实现 Singleton 设计模式,目的是为了在多线程环境下安全且高效地创建单实例对象。第一次检查 instance == nullptr 是为了避免在实例已经创建了之后还每次都要去获取锁,这样可以提高程序的运行效率;第二次检查是为了在多个线程同时通过了第一层检查并等待获取锁的时候,确保只有一个线程能够创建实例。