单例模式

2024-06-04 19:53:20 浏览数 (2)

1.引言

项目上总是用到单例,所以做一个小总结,之前在学校学习设计模式像背八股文一样,毫无实践可言

2.解决什么问题

首先说单例是解决什么问题的呢

像我们有个两台打印机,但是点击打印的时候,只让一台来打印就可以,如果两个都打印,显然是错的,所以单例就是让类只有一个实例,比如电脑中的窗口,一个窗口只能被一个实例所控制,如果多个实例控制一个窗口,那就乱了

简单理解就是一个类只由一个它所创建的对象控制,你在A处创建一个类Dwg对象和在B处创建一个类Dwg对象,他们里面的数据是一样的

3.单例形式

单例你查资料会发现有很多种形式,这都是正常的,其中单例一般都会具有这种样子

  • 单一实例:单例类只能有一个实例。这通常是通过将构造函数设为私有来实现的,以防止外部代码创建新的实例。
  • 全局访问点:单例类提供了一个全局访问点,通常是一个静态方法,用于获取唯一的实例。这个方法通常被命名为getInstance()或类似的名称
  • 自我实例化:如果单例类的唯一实例尚未创建,那么在调用全局访问点时,单例类应该自行创建这个实例

4.代码演示一

先简单看两个单例,这两个都是单例

这个单例优点是具有垃圾回收,MyCAS类使用了一个内部类CGarhuishou来在MyCAS实例不再需要时自动删除它。这是通过在GetInstance()函数中创建一个CGarhuishou对象实现的。当GetInstance()函数结束时,这个CGarhuishou对象会被销毁,从而触发其析构函数,删除MyCAS的实例。这种方法可以确保当我们不再需要单例对象时,它能被正确地删除,从而避免内存泄漏。

又因为GetInstance()是个static,所以是在程序结束的时候,自动释放实例

回到上面全局访问点就是GetInstance()

自我实例化就是GetInstance()的实现

代码语言:javascript复制
class MyCAS
{
private:
    MyCAS() {}

private:
    static MyCAS* m_instance; 

public:
    static MyCAS* GetInstance()
    {
        if (m_instance == NULL)
        {
            m_instance = new MyCAS(); // 指针指向我们创建的实例地址
            static CGarhuishou cl;
        }
        return m_instance;
    }
    class CGarhuishou
    {
    public:
        ~CGarhuishou()
        {
            if (MyCAS::m_instance)
            {
                delete MyCAS::m_instance;
                MyCAS::m_instance = NULL;
            }
        }
    };
    void func()
    {
        cout << "测试" << endl;
    }
    int a = 5;
};

MyCAS* MyCAS::m_instance = NULL; // 需要写在类外面

4.1为什么用指针而不用对象

一般我们其实更容易见到的是MyCAS mycas,而这里则是MyCAS* m_instance

  • m_instance是一个指向MyCAS实例的指针,这个实例是通过new MyCAS()在堆上动态创建的。这个实例的生命周期由我们自己管理,也就是说,我们需要在适当的时候使用delete来删除这个实例,否则会造成内存泄漏。在这个代码中,这个工作是由CGarhuishou类的析构函数完成的。
  • mycas是一个MyCAS类的对象,它是在栈上创建的。当定义一个对象时,比如MyCAS mycas;,编译器会自动在栈上为这个对象分配内存,并在离开当前作用域时自动释放这个内存。所以,我们不需要(也不能)手动删除这个对象。

总的来说,虽然m_instancemycas都是MyCAS类的实例,但是它们在内存中的存储方式和生命周期是不同的。这也决定了它们在代码中的使用方式和适用场景。

至于为什么单例模式通常使用指针来管理唯一实例,而不是直接创建一个对象,主要有以下几个原因:

  1. 控制实例化时间:使用指针和new操作符,我们可以在需要时才创建单例对象。这种”惰性初始化”(Lazy Initialization)可以帮助我们节省系统资源,特别是当单例类的构造函数包含大量计算或需要访问外部资源时。
  2. 控制实例的生命周期:使用指针,我们可以在程序的任何地方创建和删除单例对象。这给了我们更大的灵活性来管理单例对象的生命周期。例如,我们可以在程序结束时删除单例对象,以释放其占用的资源。
  3. 全局访问:使用指针,我们可以在全局范围内访问单例对象。这是因为指针可以跨越作用域限制,使得我们可以在任何地方获取和使用单例对象。

如果我们直接创建一个对象,比如MyCAS mycas;,那么这个对象的生命周期将受到其作用域的限制,一旦离开了这个作用域,这个对象就会被自动销毁。此外,我们无法控制这个对象的实例化时间,它会在定义时就被自动创建。这些都限制了我们对单例对象的控制。

所以,虽然直接创建一个对象看起来更简单,但是在实现单例模式时,使用指针来管理唯一实例会给我们带来更大的灵活性和控制力。

4.2为什么m_instance写在类外面

在C 中,静态成员变量是属于类的,而不是属于类的某个具体对象。这意味着无论创建多少个类的对象,静态成员变量只有一份拷贝。所有的对象都会共享这个静态成员变量。

然而,静态成员变量并不像普通成员变量那样在对象被创建时自动创建和初始化。它们需要在类定义之外进行单独的定义和初始化。这就是为什么MyCAS* MyCAS::m_instance = NULL;需要写在类外面的原因。

这行代码做了两件事情:

  1. 定义了静态成员变量m_instance。虽然我们已经在类内部声明了这个变量,但是我们还需要在类外部进行定义。否则,编译器会报错,说找不到m_instance的定义。
  2. 初始化了静态成员变量m_instance。我们将它初始化为NULL,表示这个指针目前不指向任何东西。这里有个小坑,记得C 中小写的null是没有意义的

总的来说,将MyCAS* MyCAS::m_instance = NULL;写在类外面是C 语言规则的要求。这样做可以确保静态成员变量被正确地定义和初始化。

那紧接着,就会问,那为什么GetInstance()函数定义不写在类外面呢,它写在类里不会报错,毕竟它也是static

在C 中,成员函数(包括静态和非静态)的声明通常在类的定义中进行,而定义(也就是函数体的实现)可以在类的定义内部或者外部进行。

如果成员函数的定义在类的定义内部,那么这个成员函数会自动成为内联函数。内联函数可以减少函数调用的开销,但是会增加程序的大小。这对于一些小型、频繁调用的函数来说是有利的。

如果成员函数的定义在类的定义外部,那么需要使用类名和作用域解析运算符::来指明这个函数属于哪个类。这对于一些大型、复杂的函数来说是有利的,因为它们不适合做内联函数。

在这个代码中,GetInstance()函数比较简单,所以将其定义在类内部是合理的。但是如果这个函数很复杂,或者你希望将其实现隐藏起来,那么可以考虑将其定义在类外部。

总的来说,将成员函数定义在类内部还是外部,取决于具体的需求和考虑。两种方式都是符合C 规则的。

所以引出来下面这个话题

4.3类中静态成员变量和静态成员函数

静态成员变量和静态成员函数在C 中都属于类级别的成员,它们的主要区别在于它们的用途和访问方式。

  1. 静态成员变量
    • 静态成员变量是类的所有对象共享的变量。无论创建多少个类的对象,静态成员变量只有一份拷贝。
    • 静态成员变量必须在类定义之外进行单独的定义和初始化。
    • 静态成员变量可以被类的所有成员函数访问,包括非静态成员函数和静态成员函数。
  2. 静态成员函数
    • 静态成员函数没有this指针,因为静态成员函数属于类本身,而不是类的任何具体对象。
    • 静态成员函数只能访问静态成员变量,它不能访问类的非静态成员变量。
    • 静态成员函数可以在没有创建类的对象的情况下被调用,这是因为静态成员函数属于类本身,而不是属于类的任何具体对象。

总的来说,静态成员变量和静态成员函数都属于类本身,而不是属于类的任何具体对象。但是它们在用途和访问方式上有所不同。希望这个解释能帮助你理解这两者之间的区别。

注意静态成员函数是这样被调用的

代码语言:javascript复制
class MyClass {
public:
    static void staticFunction() {
        std::cout << "This is a static function." << std::endl;
    }
};

int main() {
    // 调用静态成员函数,无需创建对象
    MyClass::staticFunction();
    return 0;
}

在这个例子中,我们定义了一个名为MyClass的类,这个类有一个静态成员函数staticFunction()。在main()函数中,我们直接使用类名和作用域解析运算符::来调用这个静态成员函数,无需创建MyClass的对象。

上面也提到了this指针,下面也来讲讲

关于静态成员函数没有this指针,这是因为this指针是一个指向调用成员函数的具体对象的指针。但是静态成员函数不属于任何具体对象,它属于类本身。所以,在静态成员函数中没有this指针。这也意味着静态成员函数只能访问静态成员变量,不能访问非静态成员变量,因为非静态成员变量是属于具体对象的。

在C 中,this指针是一个特殊的指针,它指向调用成员函数的那个对象。你可以在类的非静态成员函数中使用this指针来访问调用该函数的对象的成员。以下是一个代码示例:

代码语言:javascript复制
class MyClass {
private:
    int value;

public:
    MyClass(int val) : value(val) {}

    void printValue() {
        std::cout << "The value is: " << this->value << std::endl;
    }
};

int main() {
    MyClass obj(10);
    obj.printValue();  // 输出:The value is: 10
    return 0;
}

在这个例子中,我们定义了一个名为MyClass的类,这个类有一个私有成员变量value和一个公有成员函数printValue()。在printValue()函数中,我们使用this->value来访问调用这个函数的对象的value成员。

main()函数中,我们创建了一个MyClass对象obj,并调用了它的printValue()函数。在这个函数调用过程中,this指针指向了obj对象。

this指针并不是printValue函数的参数,而是在成员函数被调用时自动提供的。在成员函数内部,你可以使用this指针来访问调用该函数的对象的成员。

例如,在以下代码中:

代码语言:javascript复制
void MyClass::printValue() {
    std::cout << "The value is: " << this->value << std::endl;
}

this指针被用来访问调用printValue()函数的对象的value成员。这里,this->value等价于访问调用该函数的对象的value成员。

5.代码演示二

代码语言:javascript复制
class Singleton {
private:
    // 私有化构造函数,防止外部创建实例
    Singleton() {}

    // 声明一个静态指针用于存储唯一的实例
    static Singleton* instance;

public:
    // 获取唯一可用的对象
    static Singleton* getInstance() {
        if (instance == nullptr) {
            instance = new Singleton();
        }
        return instance;
    }

    // 删除复制构造函数和赋值操作符
    Singleton(const Singleton&) = delete;
    void operator=(const Singleton&) = delete;
};

// 初始化静态成员变量
Singleton* Singleton::instance = nullptr;

这个代码定义了一个名为Singleton的类,它有一个私有的构造函数和一个静态的成员变量instancegetInstance()函数用于获取唯一的实例。如果instance是空的,那么就创建一个新的实例;否则,就返回已经存在的实例。

注意,我们还删除了复制构造函数和赋值操作符,以防止通过复制或赋值来创建新的实例。

6.怎么在main中创建单例

main函数中创建单例的方式是通过调用单例类的全局访问点函数,也就是获取实例的函数。以下是在main函数中如何创建和使用单例的示例:

代码语言:javascript复制
MyCAS* mycas = MyCAS::GetInstance();

Singleton* singleton = Singleton::getInstance();

在这些示例中,我们并没有直接创建单例对象,而是通过调用全局访问点函数(Singleton::getInstance()MyCAS::GetInstance())来获取单例对象。这就是单例模式的一个关键特性:我们不能直接创建单例类的对象,而只能通过全局访问点来获取唯一的实例。这样可以确保整个程序中只有一个单例对象,从而避免资源的重复使用或冲突。

7.如果我要复制一个单例怎么办呢

目前我在项目上就遇到了这个问题,现在有个对话框类单例,但是当点击一个按钮导入图纸时,对话框会被强制关闭,现在的需求是当图纸导入完成后,让对话框再重新显示出来,这就需要保存当时的单例状态(数据)

现在我了解到的有序列化和反序列化

序列化是将对象的状态信息转换为可以存储或传输的形式的过程。在C 中,可以使用各种方法来序列化对象,包括手动序列化和使用库(如Boost.Serialization)。

反序列化是将序列化的数据恢复为对象的过程。通常,反序列化函数会根据序列化数据创建一个新的对象,并将其状态设置为序列化时的状态。

实际应用发现还是有缺点的,比如类里有指针的时候就不好搞了

0 人点赞