特殊类设计及单例模式(C++)

简介: 特殊类设计及单例模式(C++)

请设计一个类,只能在堆上创建对象

限制析构函数

将析构函数设为私有,然后公开一个Delete接口以释放资源:

class HeapOnly
{
public:
    void Delete()
    {
        delete this;
    }
private:
    ~HeapOnly() // 私有析构函数
    {
        cout << "~HeapOnly()" << endl;
    }
private:
    int _a;
};
int main()
{
    HeapOnly* ptr = new HeapOnly;
    ptr->Delete();
    return 0;
}

这段代码定义了一个名为 HeapOnly 的类,它只能在堆上创建对象。它包含一个公共成员函数 Delete,用于删除对象。该函数通过调用 delete this 来实现。

main 函数中,首先使用 new 在堆上创建一个 HeapOnly 对象,并将其地址存储在指针变量 ptr 中。然后调用该对象的 Delete 成员函数来删除它。

这样做的目的是确保只能在堆上创建和删除对象。由于析构函数是私有的,因此无法在栈上创建或删除对象。但是,可以通过调用公共成员函数 Delete 来删除堆上的对象。

当析构函数被声明为私有时,对象只能在堆上生成。这是因为栈上内存由编译器自动分配和释放。分配时会调用构造函数,释放时会调用析构函数。当我们把析构函数私有化后,编译器便不会在栈上创建对象了。

限制构造函数

如果将构造函数私有,就没办法实例化对象了,所以新增一个对外接口Create申请资源,控制对象是new出来的。

class HeapOnly
{
public:
    static HeapOnly* Create()
    {
        return new HeapOnly;
    }
private:
    HeapOnly() // 私有构造函数
        :_a(0)
    {}
    // 屏蔽拷贝构造函数和赋值运算符重载函数
    HeapOnly(const HeapOnly& hp) = delete;
    HeapOnly& operator=(const HeapOnly& hp) = delete;
private:
    int _a;
};
int main()
{
    HeapOnly* hp = HeapOnly::Create();
    delete hp;
    return 0;
}

static关键字用于Create()函数,使得它成为一个静态成员函数。这意味着它可以在不创建类实例的情况下调用。这是必要的,因为构造函数是私有的,所以不能直接创建类实例。

拷贝构造函数和赋值运算符重载函数被设置为delete是为了防止类的实例被复制或赋值。这样可以确保只能通过调用静态成员函数Create()来创建类的实例。

delete关键字用于禁用特定的函数。它通常用于防止类的实例被复制或赋值,或者防止某些操作在类上执行。例如,如果您有一个类,它的实例不应该被复制或赋值,那么您可以将拷贝构造函数和赋值运算符重载函数设置为delete

此外,delete关键字还可以用于禁用某些类型之间的隐式转换。例如,如果您有一个类,并且不希望它能够隐式地从整数类型转换,则可以将相应的构造函数设置为delete

请设计一个类,只能在栈上创建对象

限制构造函数

私有构造函数,提供一个公有的Create接口,用它在栈上创建对象。

class StackOnly
{
public:
    static StackOnly Create()
    {
        StackOnly st;
        return st;
    }
private:
    StackOnly() // 私有构造函数
        :_a(0)
    {}
private:
    int _a;
};
int main()
{
    StackOnly st = StackOnly::Create();
    static StackOnly copy1(st);
    StackOnly* copy2 = new StackOnly(st);
    return 0;
}

这段代码定义了一个名为 StackOnly 的类,它只能在栈上创建。它通过将构造函数声明为私有来实现这一点,这样就无法在堆上使用 new 关键字创建对象。但是,它提供了一个静态成员函数 Create() 来创建对象。

然而,在 main 函数中,尽管不能直接使用构造函数创建对象,但仍然可以通过复制构造函数来创建对象。因此,这段代码并没有完全实现只能在栈上创建的目的。

因为不能将构造函数设置为私有,也不能用=delete的方式将拷贝构造函数删除,因为Create函数中创建的是局部对象,返回局部对象会调用拷贝构造函数生成临时拷贝。

限制new和delete

class StackOnly
{
public:
    static StackOnly Create()
    {
        return StackOnly();
    }
private:
    void* operator new(size_t size) = delete;
    void operator delete(void* p) = delete;
    //C++98
//  void* operator new(size_t size);
//  void operator delete(void* p);
private:
    int _a;
};

这段代码定义了一个名为 StackOnly 的类,它只能在栈上创建。这是通过将 newdelete 操作符设置为 = delete 来实现的,这样就禁止了在堆上分配和释放对象的操作。

当你将函数声明为 = delete 时,它表示该函数被删除,不能使用。在这种情况下,它用于防止用户使用 newdelete 来在堆上创建和销毁对象。

newdelete 操作符用于在堆上分配和释放内存。当你使用 new 来创建一个对象时,它会在堆上调用operator new()分配内存并调用构造函数来初始化对象。当你使用 delete 来销毁一个对象时,它会调用析构函数来清理对象并调用operator delete()释放堆上的内存。

在这段代码希望 StackOnly 类的对象只能在栈上创建。为了实现这一点,禁止了在堆上分配和释放对象的操作,即通过将 newdelete 操作符设置为 = delete 来实现。这样,如果用户尝试使用 new 来创建一个 StackOnly 对象,编译器会报错,因为该操作符已被删除。

C++98的写法:

newdelete默认调用的是全局的operator new()operator delete(),但如果一个类重载了专属的operator new()operator delete(),那么newdelete就会调用这个专属的函数。所以只要把operator new()operator delete()屏蔽掉,那么就无法再使用new在堆上创建对象了。

然而,这段代码仍然允许用户在静态区创建对象,即:

StackOnly st = StackOnly::Create();
static StackOnly copy1(st);

所以一个类不能严格满足只能在栈上创建对象。

请设计一个类,不能被继承

C++98

class FinalClass2
{
public:
    static FinalClass2* Create()
    {
        return new FinalClass2();
    }
private:
    FinalClass2() 
    {}
    ~FinalClass2() 
    {}
};

将类的构造函数声明为私有,然后提供一个静态成员函数来创建对象。这样,如果你尝试去继承 FinalClass2 类并创建子类对象,编译器会报错,因为子类无法访问基类的构造函数。

然而,这个类仍然可以被继承,只是编译器不会报错,只不过被继承后无法实例化出对象。

C++11

在 C++11 中,你可以使用 final 关键字来定义一个不能被继承的类。例如:

class FinalClass final
{
public:
    FinalClass() 
    {}
    ~FinalClass() 
    {}
};

这样,如果你尝试去继承 FinalClass 类,编译器会报错。

请设计一个类,只能创建一个对象(单例模式)

在面向对象语言中,许多设计模式都由继承关系和多态实现。其中,继承关系带来的开销比实现多态小得多,主要原因就是实例化对象的次数和对象本身的规模会影响效率,所以有了这样的设计模式:

只允许创建一个对象,因此节省内存,加快对象访问速度,即单例模式(Singleton,或单件模式)。单例模式可以保证系统中该类只有一个实例,并提供一个访问它的全局访问点,该实例被所有程序模块共享。

设计模式(Design Pattern),设计模式就是一套被反复使用、多数人知晓的、经过分类编目的、代码设计经验的总结。使用设计模式的目的就是为了可重用代码、让代码更容易被他人理解、保证代码可靠性程序的重用性。

因此,当对象需要被公用的场合时适合使用,例如多个模块使用同一个数据源连接对象等等。

懒汉模式和饿汉模式是两种常见的单例模式实现方式。它们的主要区别在于实例化对象的时间不同:

  • 饿汉模式则是在类加载时(进入main函数之前)就创建实例,也就是说,在程序启动时就已经创建了该对象。这种方式可以提高程序运行速度,但可能会浪费内存空间。饿汉式是线程安全的,在类创建的同时就已经创建好一个静态的对象供系统使用,以后不再改变。这种方式加载类对象,我们称作:预先加载方式。
  • 懒汉模式是指在第一次调用时才创建实例,也就是说,只有当需要使用该对象时才会创建它。这种方式可以节省内存空间,但可能会影响程序运行速度。懒汉式如果在创建实例对象时不加上synchronized则会导致对对象的访问不是线程安全的。这种方式加载类对象,我们称作:延迟加载方式。

饿汉模式

可以使用静态变量和静态函数来实现饿汉模式:

  1. 私有构造函数,删除或私有拷贝构造函数和赋值运算符重载函数,以禁止在类的外部实例化或拷贝对象;
  2. 提供一个指向单例对象的static指针(当然也可以是引用),也可以按需要增加例如获取内存地址的接口;
  3. 提供一个全局访问点获取单例对象(返回值可以是指针或引用)。

示例

class Singleton
{
public:
    // 提供一个全局访问点获取单例对象
    static Singleton* GetInstance() // instrance 名词,实例
    {
        return _inst;
    }
private:
    Singleton() // 私有构造函数
    {}
    // 删除或私有拷贝构造函数和赋值运算符重载函数
    Singleton(const Singleton&) = delete;
    Singleton& operator=(const Singleton&) = delete;
    // 提供一个指向单例对象的指针
    static Singleton* _inst;
};
// 在类加载时(进入main函数之前)就创建实例
// 注意,对象在main函数定义域之外实例化
Singleton* Singleton::_inst = new Singleton;

其中:Singleton 类有一个静态成员变量 _inst,它在类加载时就被初始化为一个新创建的 Singleton 对象。这个对象是在 main 函数之前创建的。

关于为什么_inst是static的:

它是静态的,因为它是单例模式(包括饿汉模式和懒汉模式)的核心。它用于存储单例对象的唯一实例。静态成员变量属于类,而不属于任何一个特定的对象。这意味着,无论创建多少个 Singleton 对象,它们都共享同一个 _inst 变量。

在单例模式中,我们希望只有一个实例存在。因此,我们需要使用静态成员变量来存储这个实例,以保证它在整个程序运行期间都是唯一的。此外,由于 getInstance() 函数也是静态的,所以它只能访问静态成员变量。因此,_inst 必须是静态的。

Singleton 类的构造函数是私有的,这意味着不能在类外部直接创建 Singleton 对象。拷贝构造函数和赋值运算符重载函数也被删除或设为私有,以防止通过拷贝或赋值来创建多个实例。类提供了一个静态成员函数 GetInstance(),用于获取单例对象。当我们调用这个函数时,它会返回已经初始化好的 _inst 对象。

这段代码实现了一个饿汉模式的单例类,它在类加载时就创建了一个单例对象,并提供了一个全局访问点来获取这个对象。

特点

优点:

  • 线程安全。由于静态变量的初始化是在主线程执行之前完成的,所以饿汉模式是线程安全的。
  • 程序运行速度快。由于饿汉模式在程序启动时就已经创建了实例,所以每次调用 GetInstance() 函数时都不需要再进行判断和创建实例,可以提高程序运行速度。

缺点:

  • 在一个程序中如果有多个单例,并且有先后创建初始化顺序要求时,饿汉无法控制。例如,程序两个单例类A 和 B,假设要求A先创建初始化,B再创建初始化。然而静态成员谁先初始化是不确定的,尤其是多个文件 (单个文件可能是按顺序的)。
  • 增加程序启动时间。由于饿汉模式在类加载时就创建实例,所以它会增加程序启动时间。
  • 可能浪费内存空间。如果最终没有使用该对象,则会浪费内存空间。

使用饿汉模式的情况:

  • 当对象的创建和初始化时间不重要时,可以使用饿汉模式。因为饿汉模式在类加载时就创建实例,所以它会增加程序启动时间。
  • 当对象需要频繁使用时,可以使用饿汉模式。因为饿汉模式在程序启动时就已经创建了实例,所以每次调用 getInstance() 函数时都不需要再进行判断和创建实例,可以提高程序运行速度。
  • 当对象的创建对线程安全有要求时,可以使用饿汉模式。因为静态变量的初始化是在主线程执行之前完成的,所以它是线程安全的。

懒汉模式

可以使用静态变量和静态函数来实现饿汉模式:

  1. 私有构造函数,删除或私有拷贝构造函数和赋值运算符重载函数,以禁止在类的外部实例化或拷贝对象;
  2. 提供一个指向单例对象的static指针(当然也可以是引用),并在程序入口(main函数)之前将其初始化为nullptr;
  3. 提供一个全局访问点获取单例对象(返回值可以是指针或引用)。

示例1

class Singleton
{
public:
    // 提供一个全局访问点获取单例对象
    static Singleton* GetInstance()
    {
        if (_inst == nullptr)
        {
            _inst = new Singleton;
        }
        return _inst;
    }
private:
    // 私有构造函数,删除或私有拷贝构造函数和赋值运算符重载函数
    Singleton()
    {}
    Singleton(const Singleton&) = delete;
    Singleton& operator=(const Singleton&) = delete;
    // 提供一个指向单例对象的static指针
    static Singleton* _inst;
};
// 在程序入口(main函数)之前将其初始化为nullptr
Singleton* Singleton::_inst = nullptr;

在这段代码中,Singleton 类有一个私有构造函数,以防止外部创建多个实例。它还提供了一个静态成员函数 GetInstance() 来获取唯一的实例。如果该实例尚未创建,则会在第一次调用 GetInstance() 时创建它。此外,该类还将拷贝构造函数和赋值运算符声明为删除,以防止复制单例对象。

Singleton 类还定义了一个静态成员变量 _inst,用于存储唯一的实例。在程序入口(main函数)之前,将其初始化为 nullptr

当你调用 Singleton::GetInstance() 时,它会检查 _inst 是否为 nullptr。如果是,则创建一个新的 Singleton 实例并将其赋值给 _inst。然后返回 _inst 的值。

这样,在整个程序运行期间,无论你调用多少次 Singleton::GetInstance() ,都只会返回同一个 Singleton 实例。

特点

懒汉模式的优点是:

  • 能控制实例化多个对象的顺序。
  • 延迟初始化:只有在第一次使用时才会创建单例对象,避免了不必要的资源浪费。
  • 简单易实现:相比其他单例模式实现方式,懒汉模式更加简单直接。

懒汉模式的缺点是:

  • 线程不安全:如果多个线程同时调用 GetInstance() ,可能会创建多个实例。可以通过加锁来解决这个问题,但会增加复杂度和降低性能。
  • 延迟加载可能导致程序启动变慢:如果单例对象初始化需要大量时间和资源,那么在第一次调用 GetInstance() 时可能会导致程序启动变慢。

总之,懒汉模式适用于单例对象初始化简单且不需要立即使用的情况。如果需要考虑线程安全或者希望尽早初始化单例对象,则可以考虑其他实现方式。

懒汉模式适用于以下情况:

  • 当对象的创建和初始化时间较长时,可以使用懒汉模式。因为懒汉模式在第一次调用时才创建实例,所以它不会增加程序启动时间。
  • 当对象不一定会被使用时,可以使用懒汉模式。因为懒汉模式只有在需要使用该对象时才会创建它,所以如果最终没有使用该对象,则不会浪费内存空间。
  • 当对象的创建对线程安全没有要求时,可以使用懒汉模式。因为懒汉模式在多线程环境下可能会创建多个实例,所以如果对线程安全有要求,则需要额外的同步措施来保证线程安全。

示例2

  1. 私有构造函数,删除或私有拷贝构造函数和赋值运算符重载函数,以禁止在类的外部实例化或拷贝对象;
  2. 提供一个全局访问点获取单例对象(返回值可以是指针或引用)。
class Singleton
{
public:
    // 提供一个全局访问点获取单例对象
    static Singleton* GetInstance()
    {
        static Singleton inst;
        return &inst;
    }
private:
    // 私有构造函数,删除或私有拷贝构造函数和赋值运算符重载函数
    Singleton()
    {}
    Singleton(const Singleton&) = delete;
    Singleton& operator=(const Singleton&) = delete;
};

优点:

  • 第一次调用GetInstance函数时才会定义这个静态的单例对象,保证了全局只有这一个唯一实例。
  • 是线程安全的,因为现在的C++标准保证多线程初始化static变量不会发生数据竞争,可以视为原子操作。
  • 该方法属于懒汉模式,因为局部静态变量不是在程序运行(main函数)之前初始化的,而是在第一次调用GetInstance函数时初始化的。

缺点:这段代码中的单例对象是在静态区创建的,这意味着它会在程序启动时就被创建,并且在整个程序运行期间都存在。

这种实现方式可能会导致一些问题。例如,如果单例对象依赖于其他对象或资源,那么它可能会在这些依赖项初始化之前就被创建,从而导致错误。

此外,在程序结束时,静态区中的对象会被自动销毁。但是,由于 C++ 没有定义静态对象销毁的顺序,因此可能会出现单例对象依赖的其他静态对象已经被销毁,但单例对象仍然试图访问它们的情况。

单例对象的释放

一般情况下,单例对象不需要释放的,因为程序运行期间可能会使用单例对象。在进程正常结束后,资源会被自动释放。但有些特殊场景需要手动释放,比如单例对象析构时,要进行一些持久化(往文件、数据库写数据等)操作。

内嵌垃圾回收类(自动释放)

这是一个简单的示例代码,演示了如何使用内部垃圾回收类来实现单例对象的资源释放:

class Singleton
{
public:
    static Singleton* getInstance()
    {
        if (m_pInstance == nullptr)
            m_pInstance = new Singleton();
        return m_pInstance;
    }
private:
    Singleton() {}
    ~Singleton() {}
    static Singleton* m_pInstance;
    // 垃圾回收类
    class GC
    {
    public:
        ~GC()
        {
            if (m_pInstance != nullptr)
            {
                delete m_pInstance;
                m_pInstance = nullptr;
            }
        }
    };
    static GC gc; // 静态成员
};
Singleton* Singleton::m_pInstance = nullptr;
Singleton::GC Singleton::gc;

在上面的代码中,我们定义了一个内部垃圾回收类 GC,并且在 Singleton 类中定义了一个此类的静态成员 gc。程序结束时,系统会自动析构此静态成员,此时,在 GC 类的析构函数中析构 Singleton 实例,就可以实现 m_pInstance 的自动释放。

特点

优点:可以自动释放单例对象,避免了内存泄漏的问题。它不需要程序员手动调用 delete 来释放单例对象,也不需要注册释放函数或提供释放接口。

缺点:法能在程序结束时释放单例对象,如果需要在程序运行过程中释放单例对象,则需要使用其他方法。

主动释放

在单例类中编写一个DestoryInstance函数,通过它释放单例对象的资源,当不再需要该单例对象时就可以主动调用DestoryInstance释放单例对象。

下面是一个简单的示例代码:

class Singleton
{
public:
    static Singleton* getInstance()
    {
        if (m_pInstance == nullptr)
            m_pInstance = new Singleton();
        return m_pInstance;
    }
    static void DestoryInstance()
    {
        if (m_pInstance != nullptr)
        {
            delete m_pInstance;
            m_pInstance = nullptr;
        }
    }
private:
    Singleton() {}
    ~Singleton() {}
    static Singleton* m_pInstance;
};
Singleton* Singleton::m_pInstance = nullptr;

在上面的代码中,我们定义了一个静态成员函数 DestoryInstance,用于释放单例对象。当需要释放单例对象时,只需调用此函数即可。

特点

优点:可以在程序运行过程中主动释放单例对象,而不需要等到程序结束时才能释放。这对于一些需要在运行过程中释放资源的应用程序来说非常有用。

缺点:需要程序员手动调用 DestoryInstance 函数来释放单例对象。如果忘记调用此函数,可能会导致内存泄漏的问题。

这种方法与使用内部垃圾回收类来实现单例对象的资源释放的方法相比,主要区别在于释放单例对象的时机不同。使用内部垃圾回收类的方法只能在程序结束时释放单例对象,而使用 DestoryInstance 函数的方法可以在程序运行过程中主动释放单例对象。

目录
相关文章
|
6天前
|
存储 编译器 C++
【c++】类和对象(中)(构造函数、析构函数、拷贝构造、赋值重载)
本文深入探讨了C++类的默认成员函数,包括构造函数、析构函数、拷贝构造函数和赋值重载。构造函数用于对象的初始化,析构函数用于对象销毁时的资源清理,拷贝构造函数用于对象的拷贝,赋值重载用于已存在对象的赋值。文章详细介绍了每个函数的特点、使用方法及注意事项,并提供了代码示例。这些默认成员函数确保了资源的正确管理和对象状态的维护。
29 4
|
7天前
|
存储 编译器 Linux
【c++】类和对象(上)(类的定义格式、访问限定符、类域、类的实例化、对象的内存大小、this指针)
本文介绍了C++中的类和对象,包括类的概念、定义格式、访问限定符、类域、对象的创建及内存大小、以及this指针。通过示例代码详细解释了类的定义、成员函数和成员变量的作用,以及如何使用访问限定符控制成员的访问权限。此外,还讨论了对象的内存分配规则和this指针的使用场景,帮助读者深入理解面向对象编程的核心概念。
25 4
|
30天前
|
存储 编译器 对象存储
【C++打怪之路Lv5】-- 类和对象(下)
【C++打怪之路Lv5】-- 类和对象(下)
27 4
|
30天前
|
编译器 C语言 C++
【C++打怪之路Lv4】-- 类和对象(中)
【C++打怪之路Lv4】-- 类和对象(中)
23 4
|
30天前
|
存储 安全 C++
【C++打怪之路Lv8】-- string类
【C++打怪之路Lv8】-- string类
21 1
|
1月前
|
C++
C++单例模式
C++中使用模板实现单例模式的方法,并通过一个具体的类A示例展示了如何创建和使用单例。
29 2
|
1月前
|
存储 编译器 C++
【C++类和对象(下)】——我与C++的不解之缘(五)
【C++类和对象(下)】——我与C++的不解之缘(五)
|
1月前
|
编译器 C++
【C++类和对象(中)】—— 我与C++的不解之缘(四)
【C++类和对象(中)】—— 我与C++的不解之缘(四)
|
1月前
|
C++
C++番外篇——对于继承中子类与父类对象同时定义其析构顺序的探究
C++番外篇——对于继承中子类与父类对象同时定义其析构顺序的探究
53 1
|
1月前
|
编译器 C语言 C++
C++入门4——类与对象3-1(构造函数的类型转换和友元详解)
C++入门4——类与对象3-1(构造函数的类型转换和友元详解)
19 1