🎯 本节目标

掌握C++中常见特殊类的设计方式,为面试和实际开发打下坚实基础。

📖 前言

在C++开发中,我们经常需要设计一些具有特殊限制的类,比如不能被拷贝的类、只能在特定内存区域创建对象的类等。这些特殊类设计不仅是面试中的高频考点,更是实际项目中保证代码安全性和性能的重要手段。本文将详细讲解5种常见特殊类的设计方法,并提供可直接运行的完整代码示例。

🔒 1. 请设计一个类,不能被拷贝

🛡️ 问题分析

拷贝主要发生在两个场景中:

  1. 拷贝构造函数 - 用于对象初始化时的拷贝
  2. 赋值运算符重载 - 用于对象之间的赋值操作

要禁止一个类的拷贝,只需让该类不能调用这两个函数即可。

📝 C++98实现方式

#include <iostream>

class CopyBan_Cpp98 {
public:
    CopyBan_Cpp98() {
        std::cout << "CopyBan_Cpp98 构造函数被调用" << std::endl;
    }
    
    void show() {
        std::cout << "这是一个不能被拷贝的类(C++98版本)" << std::endl;
    }

private:
    // 关键:将拷贝构造函数和赋值运算符声明为私有且不实现
    CopyBan_Cpp98(const CopyBan_Cpp98&);            // 只声明,不定义
    CopyBan_Cpp98& operator=(const CopyBan_Cpp98&); // 只声明,不定义
};

void test_CopyBan_Cpp98() {
    CopyBan_Cpp98 obj1;
    obj1.show();
    
    // 以下代码编译时会报错:
    // CopyBan_Cpp98 obj2(obj1);  // 错误:拷贝构造函数不可访问
    // CopyBan_Cpp98 obj3 = obj1; // 错误:拷贝构造函数不可访问
    // obj2 = obj1;               // 错误:赋值运算符不可访问
}

🚀 C++11实现方式(推荐)

#include <iostream>

class CopyBan_Cpp11 {
public:
    CopyBan_Cpp11() {
        std::cout << "CopyBan_Cpp11 构造函数被调用" << std::endl;
    }
    
    void show() {
        std::cout << "这是一个不能被拷贝的类(C++11版本)" << std::endl;
    }

    // 使用delete关键字明确删除拷贝操作
    CopyBan_Cpp11(const CopyBan_Cpp11&) = delete;
    CopyBan_Cpp11& operator=(const CopyBan_Cpp11&) = delete;
};

void test_CopyBan_Cpp11() {
    CopyBan_Cpp11 obj1;
    obj1.show();
    
    // 以下代码编译时会报更清晰的错误信息
    // CopyBan_Cpp11 obj2(obj1);  // 错误:使用已删除的函数
}

💡 设计原理说明

  • 设置为私有:防止用户在类外定义这些函数
  • 只声明不定义:即使通过友元或其他方式访问,链接时也会失败
  • C++11的delete:更清晰、更安全,编译器会给出明确的错误信息

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

🎯 设计思路

  1. 将构造函数设为私有,防止直接栈上创建
  2. 将拷贝构造函数设为私有或删除,防止通过拷贝在栈上创建
  3. 提供静态成员函数来创建堆对象

📝 完整实现代码

#include <iostream>
#include <memory>

class HeapOnly {
public:
    // 静态工厂方法:创建堆对象
    static HeapOnly* CreateObject() {
        return new HeapOnly();
    }
    
    // 使用智能指针的版本(推荐)
    static std::unique_ptr<HeapOnly> CreateUniqueObject() {
        return std::unique_ptr<HeapOnly>(new HeapOnly());
    }
    
    void show() {
        std::cout << "这是一个只能在堆上创建的对象" << std::endl;
        std::cout << "对象地址: " << this << std::endl;
    }
    
    // 必须提供析构函数
    ~HeapOnly() {
        std::cout << "HeapOnly 对象被销毁" << std::endl;
    }

private:
    // 构造函数私有化
    HeapOnly() {
        std::cout << "HeapOnly 构造函数(私有)" << std::endl;
    }
    
    // 禁止拷贝构造
    HeapOnly(const HeapOnly&) = delete;
    HeapOnly& operator=(const HeapOnly&) = delete;
    
    // C++11:禁止移动构造和移动赋值
    HeapOnly(HeapOnly&&) = delete;
    HeapOnly& operator=(HeapOnly&&) = delete;
};

void test_HeapOnly() {
    // 正确:在堆上创建对象
    HeapOnly* ptr1 = HeapOnly::CreateObject();
    ptr1->show();
    delete ptr1;
    
    // 更安全的方式:使用智能指针
    auto ptr2 = HeapOnly::CreateUniqueObject();
    ptr2->show();
    
    // 错误:不能在栈上创建
    // HeapOnly obj;  // 编译错误:构造函数不可访问
    
    // 错误:不能拷贝
    // HeapOnly obj2(*ptr1);  // 编译错误:拷贝构造函数被删除
}

🔍 使用场景

  • 需要控制对象生命周期的场景
  • 对象较大,避免栈溢出
  • 需要延迟初始化的对象

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

🎯 设计思路

  1. 将构造函数设为私有
  2. 提供静态方法创建并返回栈对象
  3. 重载operator new为delete,禁止new操作

📝 完整实现代码

#include <iostream>

class StackOnly {
public:
    // 静态工厂方法:创建栈对象
    static StackOnly CreateObject() {
        return StackOnly();
    }
    
    void show() {
        std::cout << "这是一个只能在栈上创建的对象" << std::endl;
        std::cout << "对象值: " << _value << std::endl;
    }
    
    void setValue(int val) {
        _value = val;
    }
    
    int getValue() const {
        return _value;
    }

    // 关键:禁止在堆上创建
    void* operator new(size_t size) = delete;
    void operator delete(void* ptr) = delete;
    
    // 禁止在堆上创建数组
    void* operator new[](size_t size) = delete;
    void operator delete[](void* ptr) = delete;

private:
    // 构造函数私有化
    StackOnly() : _value(0) {
        std::cout << "StackOnly 构造函数(私有)" << std::endl;
    }
    
    // 禁止拷贝构造(可选,根据需求)
    // StackOnly(const StackOnly&) = delete;
    
    int _value;
};

void test_StackOnly() {
    // 正确:在栈上创建对象
    StackOnly obj = StackOnly::CreateObject();
    obj.setValue(42);
    obj.show();
    
    // 错误:不能在堆上创建
    // StackOnly* ptr = new StackOnly();  // 编译错误:operator new被删除
    // StackOnly* arr = new StackOnly[5]; // 编译错误:operator new[]被删除
    
    // 错误:不能通过new调用拷贝构造
    // StackOnly obj2 = StackOnly::CreateObject();
    // StackOnly* ptr3 = new StackOnly(obj2); // 编译错误:operator new被删除
}

💡 注意事项

  • 这种设计会影响类的正常使用,需谨慎考虑
  • 通常用于需要严格控制内存分配的场景

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

📝 C++98实现方式

#include <iostream>

class NonInheritable_Cpp98 {
public:
    // 静态方法获取实例
    static NonInheritable_Cpp98 GetInstance() {
        return NonInheritable_Cpp98();
    }
    
    void show() {
        std::cout << "这是一个不能被继承的类(C++98版本)" << std::endl;
    }

private:
    // 构造函数私有化
    NonInheritable_Cpp98() {
        std::cout << "NonInheritable_Cpp98 构造函数" << std::endl;
    }
    
    // 关键:派生类无法访问基类的私有构造函数
    // 因此无法继承
};

// 尝试继承会编译失败
// class Derived : public NonInheritable_Cpp98 {}; // 错误:基类构造函数不可访问

🚀 C++11实现方式(推荐)

#include <iostream>

class NonInheritable_Cpp11 final {
public:
    NonInheritable_Cpp11() {
        std::cout << "NonInheritable_Cpp11 构造函数" << std::endl;
    }
    
    void show() {
        std::cout << "这是一个不能被继承的类(C++11 final版本)" << std::endl;
    }
};

// 使用final关键字后,尝试继承会直接编译失败
// class Derived : public NonInheritable_Cpp11 {}; // 错误:不能继承final类

void test_NonInheritable() {
    NonInheritable_Cpp11 obj;
    obj.show();
}

🔍 final关键字详解

  • final 可以修饰类,表示该类不能被继承
  • final 也可以修饰虚函数,表示该虚函数不能被重写
  • C++11引入,代码更清晰,意图更明确

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

📚 设计模式简介

设计模式是一套被反复使用、多数人知晓的、经过分类的代码设计经验总结。就像《孙子兵法》总结战争规律一样,设计模式总结软件设计的规律。

单例模式:保证一个类只有一个实例,并提供一个全局访问点。

🍔 饿汉模式(Eager Singleton)

#include <iostream>

class SingletonEager {
public:
    // 获取单例实例
    static SingletonEager* GetInstance() {
        return &m_instance;
    }
    
    void show() {
        std::cout << "饿汉单例模式,实例地址: " << this << std::endl;
        std::cout << "计数器: " << ++counter << std::endl;
    }
    
    // 禁止拷贝和赋值
    SingletonEager(const SingletonEager&) = delete;
    SingletonEager& operator=(const SingletonEager&) = delete;

private:
    // 构造函数私有
    SingletonEager() : counter(0) {
        std::cout << "饿汉单例初始化(程序启动时)" << std::endl;
    }
    
    static SingletonEager m_instance;  // 静态成员,程序启动时初始化
    int counter;
};

// 静态成员初始化(在程序入口之前完成)
SingletonEager SingletonEager::m_instance;

void test_SingletonEager() {
    SingletonEager* p1 = SingletonEager::GetInstance();
    SingletonEager* p2 = SingletonEager::GetInstance();
    
    p1->show();
    p2->show();
    
    std::cout << "p1 == p2: " << (p1 == p2) << std::endl;
}

✅ 优点:实现简单,线程安全
❌ 缺点:可能导致程序启动慢,多个单例初始化顺序不确定

😴 懒汉模式(Lazy Singleton)

#include <iostream>
#include <mutex>
#include <memory>

class SingletonLazy {
public:
    // 获取单例实例(线程安全版本)
    static SingletonLazy* GetInstance() {
        // 双重检查锁定(Double-Checked Locking)
        if (m_instance == nullptr) {
            std::lock_guard<std::mutex> lock(m_mutex);
            if (m_instance == nullptr) {
                m_instance = new SingletonLazy();
            }
        }
        return m_instance;
    }
    
    // 使用现代C++的智能指针版本(C++11以上)
    static std::shared_ptr<SingletonLazy> GetInstanceSmart() {
        static std::once_flag onceFlag;
        static std::shared_ptr<SingletonLazy> instance;
        
        std::call_once(onceFlag, []() {
            instance = std::shared_ptr<SingletonLazy>(new SingletonLazy());
        });
        
        return instance;
    }
    
    void show() {
        std::cout << "懒汉单例模式,实例地址: " << this << std::endl;
        std::cout << "调用次数: " << ++callCount << std::endl;
    }
    
    // 禁止拷贝和赋值
    SingletonLazy(const SingletonLazy&) = delete;
    SingletonLazy& operator=(const SingletonLazy&) = delete;

private:
    // 构造函数私有
    SingletonLazy() : callCount(0) {
        std::cout << "懒汉单例初始化(第一次调用时)" << std::endl;
    }
    
    ~SingletonLazy() {
        std::cout << "懒汉单例析构" << std::endl;
    }
    
    // 内部垃圾回收类
    class CGarbo {
    public:
        ~CGarbo() {
            if (SingletonLazy::m_instance != nullptr) {
                delete SingletonLazy::m_instance;
                SingletonLazy::m_instance = nullptr;
            }
        }
    };
    
    static SingletonLazy* m_instance;
    static std::mutex m_mutex;
    static CGarbo garbo;  // 静态成员,程序结束时自动析构
    int callCount;
};

// 静态成员初始化
SingletonLazy* SingletonLazy::m_instance = nullptr;
std::mutex SingletonLazy::m_mutex;
SingletonLazy::CGarbo SingletonLazy::garbo;

void test_SingletonLazy() {
    SingletonLazy* p1 = SingletonLazy::GetInstance();
    SingletonLazy* p2 = SingletonLazy::GetInstance();
    
    p1->show();
    p2->show();
    
    std::cout << "p1 == p2: " << (p1 == p2) << std::endl;
    
    // 使用智能指针版本
    auto p3 = SingletonLazy::GetInstanceSmart();
    auto p4 = SingletonLazy::GetInstanceSmart();
    p3->show();
    p4->show();
}

✅ 优点:延迟加载,启动速度快,内存使用更高效
❌ 缺点:实现复杂,需要注意线程安全

🧪 Meyers’ Singleton(C++11最优雅实现)

#include <iostream>

class SingletonMeyers {
public:
    static SingletonMeyers& GetInstance() {
        static SingletonMeyers instance;  // C++11保证线程安全
        return instance;
    }
    
    void show() {
        std::cout << "Meyers单例模式,实例地址: " << this << std::endl;
    }
    
    // 禁止拷贝和赋值
    SingletonMeyers(const SingletonMeyers&) = delete;
    SingletonMeyers& operator=(const SingletonMeyers&) = delete;

private:
    SingletonMeyers() {
        std::cout << "Meyers单例初始化" << std::endl;
    }
    
    ~SingletonMeyers() {
        std::cout << "Meyers单例析构" << std::endl;
    }
};

void test_SingletonMeyers() {
    SingletonMeyers& s1 = SingletonMeyers::GetInstance();
    SingletonMeyers& s2 = SingletonMeyers::GetInstance();
    
    s1.show();
    s2.show();
    
    std::cout << "&s1 == &s2: " << (&s1 == &s2) << std::endl;
}

🎮 完整测试程序

#include <iostream>

// 所有类的定义放在这里...

int main() {
    std::cout << "=== C++特殊类设计测试程序 ===" << std::endl << std::endl;
    
    std::cout << "1. 测试不可拷贝类:" << std::endl;
    // test_CopyBan_Cpp98();
    test_CopyBan_Cpp11();
    
    std::cout << "\n2. 测试堆上创建类:" << std::endl;
    test_HeapOnly();
    
    std::cout << "\n3. 测试栈上创建类:" << std::endl;
    test_StackOnly();
    
    std::cout << "\n4. 测试不可继承类:" << std::endl;
    test_NonInheritable();
    
    std::cout << "\n5. 测试单例模式:" << std::endl;
    std::cout << "5.1 饿汉模式:" << std::endl;
    test_SingletonEager();
    
    std::cout << "\n5.2 懒汉模式:" << std::endl;
    test_SingletonLazy();
    
    std::cout << "\n5.3 Meyers单例:" << std::endl;
    test_SingletonMeyers();
    
    std::cout << "\n=== 测试完成 ===" << std::endl;
 }

📊 设计模式对比总结

模式 实现方式 线程安全 性能 适用场景
不可拷贝 C++98私有声明 / C++11 delete 不涉及 无影响 资源管理类、唯一性对象
堆上创建 私有构造+静态工厂 中等 需要控制生命周期的对象
栈上创建 删除operator new 小型临时对象
不可继承 私有构造 / final关键字 无影响 工具类、最终实现类
饿汉单例 静态成员初始化 启动时就需要初始化的单例
懒汉单例 双重检查锁定 需要额外同步 中等 延迟初始化、资源消耗大的单例
Meyers’单例 局部静态变量 是(C++11+) C++11及以上环境推荐使用

🎯 本章小结

本章深入探讨了C++中五种经典的特殊类设计模式,每种模式都针对特定的设计需求:

  1. 不可拷贝类:通过禁用拷贝构造函数和赋值运算符,保护资源不被意外复制,适用于文件句柄、网络连接等资源管理类。

  2. 堆上创建类:将构造函数私有化,通过静态工厂方法在堆上创建对象,实现对对象生命周期的完全控制。

  3. 栈上创建类:删除operator newoperator delete,强制对象只能在栈上分配,适用于小型临时对象。

  4. 不可继承类:使用私有构造函数或C++11的final关键字,防止类被继承,适用于工具类或最终实现类。

  5. 单例模式:确保一个类只有一个实例,并提供全局访问点。重点介绍了三种实现方式:

    • 饿汉模式:线程安全但可能浪费资源
    • 懒汉模式:延迟初始化但需要同步机制
    • Meyers’单例:C++11及以上最优雅的实现

这些设计模式体现了C++面向对象编程的精髓,通过语言特性(构造函数、析构函数、访问控制、静态成员等)实现设计约束,是高质量C++代码的重要组成部分。

💼 大厂面试题精选

腾讯面试题

题目:请设计一个线程安全的单例模式,要求支持延迟初始化,并解释为什么你的实现是线程安全的。

参考答案

class ThreadSafeSingleton {
private:
    ThreadSafeSingleton() = default;
    ~ThreadSafeSingleton() = default;
    ThreadSafeSingleton(const ThreadSafeSingleton&) = delete;
    ThreadSafeSingleton& operator=(const ThreadSafeSingleton&) = delete;
    
    static std::atomic<ThreadSafeSingleton*> instance;
    static std::mutex mtx;

public:
    static ThreadSafeSingleton* getInstance() {
        ThreadSafeSingleton* tmp = instance.load(std::memory_order_acquire);
        if (tmp == nullptr) {
            std::lock_guard<std::mutex> lock(mtx);
            tmp = instance.load(std::memory_order_relaxed);
            if (tmp == nullptr) {
                tmp = new ThreadSafeSingleton();
                instance.store(tmp, std::memory_order_release);
            }
        }
        return tmp;
    }
    
    // 业务方法
    void doSomething() {
        // 业务逻辑
    }
};

// 初始化静态成员
std::atomic<ThreadSafeSingleton*> ThreadSafeSingleton::instance(nullptr);
std::mutex ThreadSafeSingleton::mtx;

线程安全分析

  1. 使用std::atomic确保指针操作的原子性
  2. 使用双重检查锁定减少锁竞争
  3. 使用内存序保证可见性和顺序一致性

阿里巴巴面试题

题目:设计一个不能被拷贝的智能指针类,要求支持RAII和移动语义。

参考答案

template<typename T>
class UniquePtr {
private:
    T* ptr;
    
    // 禁用拷贝
    UniquePtr(const UniquePtr&) = delete;
    UniquePtr& operator=(const UniquePtr&) = delete;
    
public:
    // 构造函数
    explicit UniquePtr(T* p = nullptr) : ptr(p) {}
    
    // 移动构造函数
    UniquePtr(UniquePtr&& other) noexcept : ptr(other.ptr) {
        other.ptr = nullptr;
    }
    
    // 移动赋值运算符
    UniquePtr& operator=(UniquePtr&& other) noexcept {
        if (this != &other) {
            delete ptr;
            ptr = other.ptr;
            other.ptr = nullptr;
        }
        return *this;
    }
    
    // 析构函数
    ~UniquePtr() {
        delete ptr;
    }
    
    // 操作符重载
    T& operator*() const { return *ptr; }
    T* operator->() const { return ptr; }
    T* get() const { return ptr; }
    
    // 资源释放
    void reset(T* p = nullptr) {
        delete ptr;
        ptr = p;
    }
    
    // 资源转移
    T* release() {
        T* temp = ptr;
        ptr = nullptr;
        return temp;
    }
};

字节跳动面试题

题目:请解释C++11中final关键字的两种用法,并说明为什么final类不能被继承。

参考答案

// 用法1:类不能被继承
class Base final {
public:
    virtual void foo() {
        std::cout << "Base::foo()" << std::endl;
    }
};

// 错误:不能继承final类
// class Derived : public Base {};

// 用法2:虚函数不能被重写
class Base2 {
public:
    virtual void bar() final {
        std::cout << "Base2::bar()" << std::endl;
    }
};

class Derived2 : public Base2 {
public:
    // 错误:不能重写final函数
    // void bar() override {}
};

原理分析

  1. final关键字在编译期进行检查,违反规则会导致编译错误
  2. 对于final类,编译器会阻止任何继承它的尝试
  3. 对于final函数,编译器会阻止任何重写它的尝试
  4. final提供了更强的设计约束,有助于代码维护和性能优化(某些情况下允许编译器做更多优化)

华为面试题

题目:设计一个只能在栈上创建的数组类,要求支持动态大小和边界检查。

参考答案

template<typename T, size_t N>
class StackArray {
private:
    T data[N];
    
    // 禁止堆上分配
    void* operator new(size_t) = delete;
    void operator delete(void*) = delete;
    void* operator new[](size_t) = delete;
    void operator delete[](void*) = delete;
    
public:
    StackArray() = default;
    ~StackArray() = default;
    
    // 访问元素(带边界检查)
    T& operator[](size_t index) {
        if (index >= N) {
            throw std::out_of_range("Index out of bounds");
        }
        return data[index];
    }
    
    const T& operator[](size_t index) const {
        if (index >= N) {
            throw std::out_of_range("Index out of bounds");
        }
        return data[index];
    }
    
    // 获取大小
    size_t size() const { return N; }
    
    // 迭代器支持
    T* begin() { return data; }
    T* end() { return data + N; }
    const T* begin() const { return data; }
    const T* end() const { return data + N; }
    
    // 数据访问
    T* get() { return data; }
    const T* get() const { return data; }
};

面试技巧与建议

  1. 理解原理:不仅要会写代码,更要理解每种设计模式背后的原理和适用场景
  2. 考虑线程安全:在多线程环境下,设计模式需要考虑线程安全性
  3. 掌握现代C++:熟悉C++11/14/17/20的新特性,如deletefinal、移动语义等
  4. 代码规范:注意代码的健壮性、可读性和可维护性
  5. 性能考虑:了解不同实现方式的性能影响,特别是在高频调用场景下

更多推荐