特殊类设计及单例模式(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 函数的方法可以在程序运行过程中主动释放单例对象。

目录
相关文章
|
8月前
|
编译器 C++ 开发者
【C++篇】深度解析类与对象(下)
在上一篇博客中,我们学习了C++的基础类与对象概念,包括类的定义、对象的使用和构造函数的作用。在这一篇,我们将深入探讨C++类的一些重要特性,如构造函数的高级用法、类型转换、static成员、友元、内部类、匿名对象,以及对象拷贝优化等。这些内容可以帮助你更好地理解和应用面向对象编程的核心理念,提升代码的健壮性、灵活性和可维护性。
|
4月前
|
人工智能 机器人 编译器
c++模板初阶----函数模板与类模板
class 类模板名private://类内成员声明class Apublic:A(T val):a(val){}private:T a;return 0;运行结果:注意:类模板中的成员函数若是放在类外定义时,需要加模板参数列表。return 0;
96 0
|
4月前
|
存储 编译器 程序员
c++的类(附含explicit关键字,友元,内部类)
本文介绍了C++中类的核心概念与用法,涵盖封装、继承、多态三大特性。重点讲解了类的定义(`class`与`struct`)、访问限定符(`private`、`public`、`protected`)、类的作用域及成员函数的声明与定义分离。同时深入探讨了类的大小计算、`this`指针、默认成员函数(构造函数、析构函数、拷贝构造、赋值重载)以及运算符重载等内容。 文章还详细分析了`explicit`关键字的作用、静态成员(变量与函数)、友元(友元函数与友元类)的概念及其使用场景,并简要介绍了内部类的特性。
173 0
|
6月前
|
编译器 C++ 容器
【c++11】c++11新特性(上)(列表初始化、右值引用和移动语义、类的新默认成员函数、lambda表达式)
C++11为C++带来了革命性变化,引入了列表初始化、右值引用、移动语义、类的新默认成员函数和lambda表达式等特性。列表初始化统一了对象初始化方式,initializer_list简化了容器多元素初始化;右值引用和移动语义优化了资源管理,减少拷贝开销;类新增移动构造和移动赋值函数提升性能;lambda表达式提供匿名函数对象,增强代码简洁性和灵活性。这些特性共同推动了现代C++编程的发展,提升了开发效率与程序性能。
186 12
|
7月前
|
设计模式 安全 C++
【C++进阶】特殊类设计 && 单例模式
通过对特殊类设计和单例模式的深入探讨,我们可以更好地设计和实现复杂的C++程序。特殊类设计提高了代码的安全性和可维护性,而单例模式则确保类的唯一实例性和全局访问性。理解并掌握这些高级设计技巧,对于提升C++编程水平至关重要。
132 16
|
8月前
|
编译器 C语言 C++
类和对象的简述(c++篇)
类和对象的简述(c++篇)
|
7月前
|
编译器 C++
类和对象(中 )C++
本文详细讲解了C++中的默认成员函数,包括构造函数、析构函数、拷贝构造函数、赋值运算符重载和取地址运算符重载等内容。重点分析了各函数的特点、使用场景及相互关系,如构造函数的主要任务是初始化对象,而非创建空间;析构函数用于清理资源;拷贝构造与赋值运算符的区别在于前者用于创建新对象,后者用于已存在的对象赋值。同时,文章还探讨了运算符重载的规则及其应用场景,并通过实例加深理解。最后强调,若类中存在资源管理,需显式定义拷贝构造和赋值运算符以避免浅拷贝问题。
|
7月前
|
存储 编译器 C++
类和对象(上)(C++)
本篇内容主要讲解了C++中类的相关知识,包括类的定义、实例化及this指针的作用。详细说明了类的定义格式、成员函数默认为inline、访问限定符(public、protected、private)的使用规则,以及class与struct的区别。同时分析了类实例化的概念,对象大小的计算规则和内存对齐原则。最后介绍了this指针的工作机制,解释了成员函数如何通过隐含的this指针区分不同对象的数据。这些知识点帮助我们更好地理解C++中类的封装性和对象的实现原理。
|
7月前
|
安全 C++
【c++】继承(继承的定义格式、赋值兼容转换、多继承、派生类默认成员函数规则、继承与友元、继承与静态成员)
本文深入探讨了C++中的继承机制,作为面向对象编程(OOP)的核心特性之一。继承通过允许派生类扩展基类的属性和方法,极大促进了代码复用,增强了代码的可维护性和可扩展性。文章详细介绍了继承的基本概念、定义格式、继承方式(public、protected、private)、赋值兼容转换、作用域问题、默认成员函数规则、继承与友元、静态成员、多继承及菱形继承问题,并对比了继承与组合的优缺点。最后总结指出,虽然继承提高了代码灵活性和复用率,但也带来了耦合度高的问题,建议在“has-a”和“is-a”关系同时存在时优先使用组合。
363 6
|
7月前
|
编译器 C++
类和对象(下)C++
本内容主要讲解C++中的初始化列表、类型转换、静态成员、友元、内部类、匿名对象及对象拷贝时的编译器优化。初始化列表用于成员变量定义初始化,尤其对引用、const及无默认构造函数的类类型变量至关重要。类型转换中,`explicit`可禁用隐式转换。静态成员属类而非对象,受访问限定符约束。内部类是独立类,可增强封装性。匿名对象生命周期短,常用于临时场景。编译器会优化对象拷贝以提高效率。最后,鼓励大家通过重复练习提升技能!