特殊类的设计:掌握面向对象中的细节与艺术

目录

1、禁止拷贝:让类不可被复制

方法一:只声明不定义

[方法二:C++11 关键字两用 delete 删除](#方法二:C++11 关键字两用 delete 删除)

[2. 只能在堆上创建对象的类设计](#2. 只能在堆上创建对象的类设计)

3.只能在栈上创建对象

[3.1 new 和 delete 运算符的作用介绍](#3.1 new 和 delete 运算符的作用介绍)

方法一:直接禁止堆上面创建

方法二:返回值创建

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

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

什么是单例模式:

饿汉模式:

原理:

优点

缺点:

懒汉模式

优点

缺点

🌼🌼前言:编程中的世界像一个巨大的乐园,而类则是这个乐园中的小小建筑,它们有时如流水般灵活,有时却如堡垒般坚固。当我们设计一个类时,大多关注的是功能实现和性能优化,但某些特殊场景下,类的设计变成了一门艺术,比如禁止拷贝 、实现单例模式 、或者打造多态框架 。今天我们从**"特殊类** "的角度聊聊如何设计一个既"特别"又"优雅"的类。

1**、禁止拷贝:让类不可被复制**

有时候,我们需要设计一个类,不允许用户拷贝它的实例。例如,管理某些稀有资源(如文件句柄、硬件资源等)时,拷贝可能导致资源冲突甚至灾难性后果。

😊😊 怎么做呢?

拷贝只会放生在两个场景中:拷贝构造函数以及赋值运算符重载 ,因此想要让一个类禁止拷贝 ,只需让该类不能调用拷贝构造函数以及赋值运算符重载即可。

方法一只声明不定义

拷贝构造函数与赋值运算符重载只声明不定义 ,并且将其访问权限设置为私有即可:

cpp 复制代码
#include <iostream>

class NonCopyable {
public:
    NonCopyable() {}   // 默认构造函数
    ~NonCopyable() {}  // 默认析构函数

private:
    // 只声明,不定义
    NonCopyable(const NonCopyable&);
    NonCopyable& operator=(const NonCopyable&);
};

int main() {
    NonCopyable obj1;
    // NonCopyable obj2 = obj1; // 编译时链接错误,尝试调用未定义的拷贝构造函数
    // NonCopyable obj3;
    // obj3 = obj1;            // 编译时链接错误,尝试调用未定义的拷贝赋值运算符
    return 0;
}

必须要把拷贝构造函数与赋值运算符重载的声明放在private:中。 因为外部可以定义类中的私有(private)和共有(public)成员函数。

如果只声明拷贝构造函数或拷贝赋值运算符不设置为**privatedelete** ,编译器不会主动阻止用户在类外对这些函数进行定义。用户可以在类外自行定义这些函数,从而绕过了我们试图禁止拷贝的设计。这是因为C++的访问控制只在类的声明中生效未设置访问控制的成员默认是public,允许类外定义和调用

例如:

cpp 复制代码
#include <iostream>

class NonCopyable {
public:
    NonCopyable() {}
    ~NonCopyable() {}

    // 只声明,未设置访问权限
    NonCopyable(const NonCopyable&);            
    NonCopyable& operator=(const NonCopyable&); 
};

// 用户在类外定义这些函数
NonCopyable::NonCopyable(const NonCopyable&) {
    std::cout << "Copy constructor called!" << std::endl;
}

NonCopyable& NonCopyable::operator=(const NonCopyable&) {
    std::cout << "Copy assignment operator called!" << std::endl;
    return *this;
}

int main() {
    NonCopyable obj1;
    NonCopyable obj2 = obj1; // 调用用户定义的拷贝构造函数
    obj1 = obj2;            // 调用用户定义的拷贝赋值运算符
    return 0;
}

这样原来用户在外部定义了**拷贝构造函数或拷贝赋值运算符,依然实现了拷贝构造。不符合我们的要求。将 **拷贝构造函数或拷贝赋值运算符设置为私有,在类外就无法调用他们了,从而实现了不可复制的效果。

方法二:C++11 关键字两用 delete 删除

delete :作为销毁 资源的关键字,和删除的关键字。

在C++中,可以通过删除拷贝构造函数和拷贝赋值运算符 ,来防止类的实例被拷贝。这种方式非常直接而且清晰。以下是一个示例代码:

cpp 复制代码
#include <iostream>

class NonCopyable {
public:
    NonCopyable() = default;
    ~NonCopyable() = default;

    // 删除拷贝构造函数
    NonCopyable(const NonCopyable&) = delete;

    // 删除拷贝赋值运算符
    NonCopyable& operator=(const NonCopyable&) = delete;

    void show() const {
        std::cout << "This object cannot be copied!" << std::endl;
    }
};

int main() {
    NonCopyable obj1;
    obj1.show();

    // NonCopyable obj2 = obj1; // 编译错误,调用了删除的拷贝构造函数
    // NonCopyable obj3;
    // obj3 = obj1;            // 编译错误,调用了删除的拷贝赋值运算符

    return 0;
}

2. 只能在堆上创建对象的类设计

通过将构造函数私有化 并提供静态成员函数进行对象创建 ,可以确保类的对象只能通过静态成员函数动态分配在堆上而不能直接在栈上创建

**构造函数私有化会导致外部无法直接创建对象,因为无法调用构造函数。**然而内部可以调用构造函数。

以下是实现一个只能在堆上创建对象的类的完整代码。

cpp 复制代码
#include <iostream>

class OnlyHeap {
public:
    // 提供静态方法创建对象
    static OnlyHeap* createInstance() {
        return new OnlyHeap();
    }

    // 可选:提供释放堆对象的静态方法
    static void deleteInstance(OnlyHeap* instance) {
        delete instance;
    }

    // 防止拷贝和赋值操作
    OnlyHeap(const OnlyHeap&) = delete;
    OnlyHeap& operator=(const OnlyHeap&) = delete;

    // 提供公开方法供对象使用
    void display() {
        std::cout << "This object can only be created on the heap!" << std::endl;
    }

private:
    // 私有构造函数,禁止直接调用
    OnlyHeap() {
        std::cout << "OnlyHeap constructor called." << std::endl;
    }

    // 私有析构函数,禁止直接删除栈对象(适用于不使用 deleteInstance 的设计)
    ~OnlyHeap() {
        std::cout << "OnlyHeap destructor called." << std::endl;
    }
};

int main() {
    // 在堆上创建对象
    OnlyHeap* obj = OnlyHeap::createInstance();
    obj->display();

    // 释放对象
    OnlyHeap::deleteInstance(obj);

    // 禁止以下操作,编译器会报错:
    // OnlyHeap stackObj;                   // 构造函数为私有,禁止栈上创建
    // OnlyHeap stackObjCopy = *obj;        // 拷贝构造函数被删除,禁止拷贝
    // OnlyHeap* stackObjAssign = obj;      // 赋值运算符被删除,禁止赋值
    return 0;
}

**注意:****如果 createInstance()函数不是静态的,**将会出现调用歧义。因为没有创建出OnlyHeap的对象怎么能使用类中的函数?而静态函数不同,它没有this指针(this 指针的地址等同于对象的起始地址 ),属于OnlyHeap类实例化的所有对象中,所有对象只有一份存在。

防止拷贝和赋值 通过将拷贝构造函数和拷贝赋值运算符声明为 **delete,**可以避免拷贝和赋值行为:

cpp 复制代码
OnlyHeap objCopy = *obj; // 编译错误 
OnlyHeap* objAssign = obj; // 编译错误

私有析构函数 析构函数也可以(也可以不设置) 设为私有,禁止用户直接释放栈上对象(例如:delete obj),但静态方法可以控制对象的释放。


3.只能在栈上创建对象

3.1 newdelete 运算符的作用介绍

在此之前我们要先介绍一下:newdelete 运算符的作用

  • new 运算符:它做了两件事:

    1. 调用合适的内存分配函数**(通常是 operator new)**来分配足够的内存。
    2. 在分配的内存上调用对象的构造函数。
  • delete 运算符:它做了两件事:

    1. 调用对象的析构函数。
    2. 释放内存**(通过 operator delete)。**

重载 newdelete 的作用

你可以重载 newdelete 运算符来定制对象的内存管理行为。比如,你可以自定义内存池、跟踪内存使用情况,或实现其他定制化的内存分配策略。

cpp 复制代码
void* operator new(size_t size) {
    std::cout << "Allocating " << size << " bytes using custom new." << std::endl;
    return malloc(size);  // 使用自定义的内存分配方式
}

void operator delete(void* pointer) {
    std::cout << "Releasing memory using custom delete." << std::endl;
    free(pointer);  // 使用自定义的内存释放方式
}
方法一:直接禁止堆上面创建

要设计一个只能在栈上创建对象的类,我们需要限制对象不能在堆上分配内存即不能通过 new 操作符创建对象)。这可以通过将类的 newdelete 运算符重载为私有或禁用来实现。

实现原理

  1. 禁止使用 new 运算符 : 将类的 operator newoperator new[] 声明为私有或删除,这样在类外部无法通过 new 创建对象。

  2. 禁止使用 delete 运算符 : 同样,将**operator deleteoperator delete[]** 声明为私有或删除,防止在堆上创建对象时误用 delete。(可以这样设置😊)

  3. 允许在栈上创建对象: 默认构造函数和析构函数保留为公有权限,允许在栈上正常创建和销毁对象。

下面是实现方式的详细说明和代码示例:

cpp 复制代码
#include <iostream>

class StackOnly {
public:
    StackOnly() {
        std::cout << "Object created on the stack" << std::endl;
    }

    ~StackOnly() {
        std::cout << "Object destroyed" << std::endl;
    }

private:
    // 禁止通过 new 运算符创建对象
    void* operator new(size_t) = delete;
    void* operator new[](size_t) = delete;

    // 禁止通过 delete 运算符销毁对象
    void operator delete(void*) = delete;
    void operator delete[](void*) = delete;
};

int main() {
    // 在栈上创建对象
    StackOnly obj;

    // 以下代码会导致编译错误,禁止在堆上创建对象
    // StackOnly* ptr = new StackOnly(); // 错误:new 运算符被禁用
    // delete ptr;                       // 错误:delete 运算符被禁用

    return 0;
}
方法二:返回值创建

私有构造函数,通过外部调用类中的静态函数返回值拷贝生成一个栈上面的对象。同时也要禁止operator newoperator delete 来达到目的。

  • 禁用 newdelete 运算符:这将禁止在堆上创建对象。
  • 通过静态方法创建栈上的对象CreateObj 方法返回一个栈对象,但它的返回值是通过值传递(会调用拷贝构造函数)来保证它不能通过 new 来分配内存。
cpp 复制代码
#include <iostream>

class StackOnly {
public:
    // 静态工厂函数,返回栈上的对象
    static StackOnly CreateObj() {
        return StackOnly();  // 创建栈上的对象
    }

    // 禁用 new 和 delete 运算符,防止在堆上分配内存
    void* operator new(size_t size) = delete;
    void operator delete(void* p) = delete;

private:
    // 私有构造函数,禁止外部直接创建对象
    StackOnly() : _a(0) {
        std::cout << "StackOnly object created!" << std::endl;
    }

    // 私有成员变量
    int _a;
};

int main() {
    // 通过静态方法创建栈上的对象
    StackOnly obj = StackOnly::CreateObj();  // 静态方法返回对象,并且它是在栈上创建的

    // 以下代码会导致编译错误,禁止在堆上创建对象
    // StackOnly* ptr = new StackOnly();  // 错误:new 被禁用
    // delete ptr;  // 错误:delete 被禁用

    return 0;
}

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

在 C++ 中,如果你想设计一个类,使得它不能被继承,可以通过将该类的构造函数、析构函数或其他成员函数 声明为 final或者将整个类声明为 final。这样可以阻止其他类继承该类。

final有点最终类,最终函数的意思在😊

  • final 类声明

    • 在类声明时使用了 final 关键字,表示该类不能被继承。
    • 如果尝试从该类派生一个新类,编译器会抛出错误。
  • final 关键字的作用

    • final 关键字禁止类继承。当一个类被标记为 final 时**,任何尝试从它派生的操作都会导致编译错误。**
    • 同样,final 也可以用于虚函数中,表示该函数不能被重写
cpp 复制代码
class NonInheritable final {  // 使用 final 阻止继承
public:
    NonInheritable() {
        // 构造函数
    }

    void show() {
        // 示例成员函数
        std::cout << "This is a non-inheritable class." << std::endl;
    }

private:
    int _data;
};

// 下面的代码将导致编译错误,因为 NonInheritable 被声明为 final,不能被继承。
// class Derived : public NonInheritable {
//     // 错误:无法从 'NonInheritable' 继承
// };

通过将基类的构造函数声明为私有阻止了其他类从基类继承 ,因为派生类必须调用基类的构造函数而私有构造函数无法被派生类访问

cpp 复制代码
class NonInherit {
public:
    static NonInherit GetInstance() {
        return NonInherit();
    }

private:
    NonInherit() {}  // 私有构造函数,禁止外部直接创建对象

    // 禁止拷贝构造和赋值操作
    NonInherit(const NonInherit&) = delete;
    NonInherit& operator=(const NonInherit&) = delete;
};

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

什么是单例模式:

一个类只能创建一个对象,即单例模式 ,该模式可以保证系统中该类只有一个实例,并提供一个访问它的全局访问点,该实例被所有程序模块共享。比如在某个服务器程序中,该服务器的配置信息存放在一个文件中,这些配置数据由一个单例对象统一读取,然后服务进程中的其他对象再通过这个单例对象获取这些配置信息,这种方式简化了在复杂环境下的配置管理。

在单例模式(Singleton Pattern)中,饿汉模式懒汉模式是两种常见的实现方式,它们在实例化单例对象的时机上有所不同。

饿汉模式:

饿汉模式是在程序启动时就创建单例对象,这意味着一开始就会实例化单例对象。无论你是否使用该单例对象,都会在程序启动时创建它。

实现代码如下:

cpp 复制代码
#include <iostream>

class Singleton {
private:
    // 构造函数私有化,防止外部实例化
    Singleton() {
        std::cout << "Singleton instance created." << std::endl;
    }

    // 禁用拷贝构造函数和赋值操作符
    Singleton(const Singleton&) = delete;
    Singleton& operator=(const Singleton&) = delete;

    // 静态成员变量,保存单例对象
    static Singleton instance;

public:
    // 提供静态成员函数访问单例对象
    static Singleton& getInstance() {
        return instance;
    }

    void showMessage() {
        std::cout << "This is the Singleton instance." << std::endl;
    }
};

// 静态成员变量初始化,饿汉式,在类加载时就创建实例
Singleton Singleton::instance;

int main() {
    // 访问单例对象
    Singleton& singleton1 = Singleton::getInstance();
    singleton1.showMessage();

    // 再次访问,返回同一个实例
    Singleton& singleton2 = Singleton::getInstance();
    singleton2.showMessage();

    std::cout << "Are both instances the same? " << (singleton1 == singleton2 ? "Yes" : "No") << std::endl;

    return 0;
}
原理:

1.将构造函数私有化,使得类外面不能创造

2.禁用拷贝构造函数和赋值操作符,防止复制。

3.静态类成员变量私有,防止再次创建另一个。

4.提供一个共有静态成员函数 ,引用返回唯一对象,在类外利用。

static Singleton instance;

在类中创建静态对象 , Singleton(类型) Singleton(域名)::instance; 在类外面初始化的意思。😊

优点

简单实现 :实现比较简单,只需在类加载时就初始化静态成员变量,无需考虑延迟加载的问题。

线程安全:由于对象的初始化是在类加载时完成的,而静态变量的初始化是线程安全的,因此不需要额外的线程同步机制。

保证单例:通过静态成员变量和私有化构造函数,保证了单例模式的实现,确保了对象的唯一性。

缺点:

浪费内存无论是否使用该单例对象,它都会在程序启动时就被创建。如果在程序运行过程中并没有使用到该单例对象,则会浪费内存。

不适合懒加载:因为在程序启动时就会创建对象,不能根据实际需求延迟创建实例。

懒汉模式

饿汉模式不同,懒汉模式不在程序启动时就创建单例对象,而是通过某种方式(通常是懒加载)在需要时才创建。

懒汉模式通常通过在静态成员函数中判断单例对象是否已经创建,若没有创建则进行实例化,若已经创建则直接返回现有对象。

cpp 复制代码
#include <iostream>
#include <mutex>

class Singleton {
private:
    // 构造函数私有化,防止外部实例化
    Singleton() {
        std::cout << "Singleton instance created." << std::endl;
    }

    // 禁用拷贝构造函数和赋值操作符
    Singleton(const Singleton&) = delete;
    Singleton& operator=(const Singleton&) = delete;

    // 静态指针,保存单例对象
    static Singleton* instance;
    static std::mutex mtx;  // 用于保证线程安全

public:
    // 提供静态成员函数访问单例对象
    static Singleton* getInstance() {
        if (instance == nullptr) {  // 如果实例还没有创建
            std::lock_guard<std::mutex> lock(mtx); // 确保线程安全
            if (instance == nullptr) { // 双重检查锁定
                instance = new Singleton();
            }
        }
        return instance;
    }

    void showMessage() {
        std::cout << "This is the Singleton instance." << std::endl;
    }

    // 单例销毁
    static void destroy() {
        delete instance;
        instance = nullptr;
    }
};

// 静态成员初始化
Singleton* Singleton::instance = nullptr;
std::mutex Singleton::mtx;

int main() {
    // 访问单例对象
    Singleton* singleton1 = Singleton::getInstance();
    singleton1->showMessage();

    // 再次访问,返回同一个实例
    Singleton* singleton2 = Singleton::getInstance();
    singleton2->showMessage();

    std::cout << "Are both instances the same? " << (singleton1 == singleton2 ? "Yes" : "No") << std::endl;

    // 销毁单例对象
    Singleton::destroy();

    return 0;
}

设计在堆上的原因:(😊)

在堆上创建对象的主要原因是控制对象的生命周期 。堆上的对象可以在函数调用结束后继续存在,而栈上的对象在函数返回时会被自动销毁 。**堆上的对象允许跨多个函数或类的作用域使用,适用于需要动态分配内存和控制生命周期的场景,**如需要跨多个函数或类持久化的数据。

优点
  • 延迟加载:只有在第一次访问时才会创建对象,这对于资源消耗较大的单例对象非常有利,可以避免不必要的开销。
  • 适合懒加载:在一些场景中,只有在特定条件下需要使用单例对象,懒汉模式可以很好地处理这种情况,避免提前创建对象带来的资源浪费。
缺点
  • 线程安全问题:如果不小心处理多线程访问,可能会导致并发创建多个实例的问题。为了保证线程安全,通常需要使用互斥锁,然而这也会增加一定的性能开销。
  • 实现复杂:相比于饿汉模式,懒汉模式的实现相对复杂,需要考虑线程安全、内存管理等问题。

结语😊🌼:在本文中,我们探讨了如何设计一些特殊类,确保它们只能在特定条件下创建或使用,例如单例模式、只能在堆上创建的对象以及禁止继承的类。这些设计模式不仅帮助我们更好地控制对象的生命周期和行为,还能有效地避免程序中的潜在问题,如内存泄漏或错误的对象访问。通过深入理解这些设计原则和实现方法,我们能够构建更加健壮、可维护的系统,确保代码的质量和稳定性。

希望本文的内容能为你在实际开发中提供一些思路和启发,帮助你应对更多复杂的编程挑战。如果你有任何问题或想法,欢迎留言讨论。

相关推荐
m0_7482565624 分钟前
Rust环境安装配置
开发语言·后端·rust
程序猿阿伟26 分钟前
《C++巧铸随机森林:开启智能决策新境界》
开发语言·c++·随机森林
阿客不是客32 分钟前
深入计算机语言之C++:STL之list的模拟实现
数据结构·c++·stl
假意诗人1 小时前
【NextJS】Arco Design与Next.js快速上手
开发语言·javascript·arco design
凡人的AI工具箱1 小时前
40分钟学 Go 语言高并发教程目录
开发语言·后端·微服务·性能优化·golang
pzx_0011 小时前
【论文阅读】相似误差订正方法在风电短期风速预报中的应用研究
开发语言·论文阅读·python·算法·leetcode·sklearn
每天写点bug1 小时前
【golang】匿名内部协程,值传递与参数传递
开发语言·后端·golang
抽风侠1 小时前
qt实现窗口的动态切换
开发语言·qt
祖坟冒青烟1 小时前
Qt 的构建系统
c++
liuweni1 小时前
Next.js系统性教学:深入理解缓存交互与API缓存管理
开发语言·前端·javascript·经验分享·缓存·前端框架·交互