题目:编写C++中的两个类

要求:一个只能在栈中分配空间 一个只能在堆中分配

在 C++ 面试与底层机制探讨中,“设计一个只能在栈上分配或只能在堆上分配的类”是一个经典问题。我们通过两段简洁的代码演示了其基础实现原理 。本文旨在以更符合工程实践和现代 C++ 标准的方式,对该问题进行重构与深度解析,并采用清晰的排版提升可读性。


一、基础实现与问题诊断
#include "stdafx.h"
#include <iostream>
using namespace std;
class CHeapOnly
{
public:
    CHeapOnly()
    {
        cout << "Constructor() of CHeapOnly!" << endl;
    }
public:
    void Destroy() const
    {
        delete this;
    }
private:
    ~CHeapOnly()
    {
        cout << "Destructor of CHeapOnly!" << endl;
    }
};
class CStackOnly
{
public:
    CStackOnly()
    {
        cout << "Constructor of CStackOnly!" << endl;
    }
    ~CStackOnly()
    {
        cout << "Destrucotr of CStackOnly!" << endl;
    }
private:
    void* operator new(size_t)
    {
    }
};
int main()
{
    //use of CHeapOnly
    CHeapOnly* pHeap = new CHeapOnly;
    pHeap->Destroy();
    //err use of CHeapOnly
    //CHeapOnly objHeap;
    //use of CStackOnly
    CStackOnly objStack;
    //error use of CStackOnly
    //CStackOnly* pStack = new CStackOnly;
    return 0;
}

基础实现分析如下:

  • CHeapOnly(堆限定):通过将析构函数声明为 private,使得编译器在栈对象离开作用域时无法自动调用析构函数,从而阻止栈上分配。对象释放需通过一个公共的 Destroy() 成员函数调用 delete this 完成 。
  • CStackOnly(栈限定):通过将 operator new 重载并设为 private,使得 new 表达式无法在类外部调用,从而阻止堆上分配 。

然而,上述实现存在明显的设计缺陷与使用陷阱:

  1. CHeapOnly 违反 RAII:依赖手动调用 Destroy() 进行资源释放,极易导致内存泄漏。
  2. 破坏继承与多态CHeapOnly 的私有析构函数使得其无法作为基类被安全继承。
  3. 限制不完整CStackOnly 未禁止 operator new[],无法阻止通过 new Type[N] 进行的数组形式堆分配。
  4. 与现代 C++ 容器不兼容:两类对象均难以直接作为元素存入 std::vector 等标准容器。
二、现代 C++ 重构实现

以下使用 C++11/14/17 特性进行重构,实现更安全、表达更清晰、限制更完整的类。

1. 堆限定类 (HeapOnly) 的重构

重构目标:使用工厂方法强制堆分配,并用智能指针自动管理生命周期。

// HeapOnlyV2.hpp
#include <memory>

class HeapOnlyV2 {
public:
    // 删除拷贝构造和赋值,防止意外复制(通常堆唯一对象是单例或资源句柄)
    HeapOnlyV2(const HeapOnlyV2&) = delete;
    HeapOnlyV2& operator=(const HeapOnlyV2&) = delete;

    // 移动操作可根据需要定义或删除
    // HeapOnlyV2(HeapOnlyV2&&) = default;
    // HeapOnlyV2& operator=(HeapOnlyV2&&) = default;

    // 唯一的公开创建接口:返回一个被 std::unique_ptr 管理的实例
    static std::unique_ptr<HeapOnlyV2> Create() {
        // 使用 std::make_unique 需要构造函数可访问,故需将其设为私有
        // 此处返回 new 的结果,由 unique_ptr 接管
        return std::unique_ptr<HeapOnlyV2>(new HeapOnlyV2());
    }

    void doSomething() {
        // 业务逻辑
    }

private:
    // 构造函数私有化,禁止外部直接构造(包括栈上和直接的 ::new)
    HeapOnlyV2() = default;
    // 析构函数无需私有化,因为 unique_ptr 能正确调用它
    ~HeapOnlyV2() = default;

    // 可选:将 operator new 设为私有或删除,进一步防止直接的堆分配(但非必须,因构造函数已私有)
    void* operator new(size_t) = delete;
    void* operator new[](size_t) = delete;
    void operator delete(void*) = delete;
};

// 使用示例
int main() {
    auto obj = HeapOnlyV2::Create(); // 正确:对象在堆上,由智能指针管理
    obj->doSomething();
    // 无需手动 delete,obj 离开作用域时自动释放

    // HeapOnlyV2 stackObj; // 错误:构造函数不可访问
    // HeapOnlyV2* rawPtr = new HeapOnlyV2; // 错误:operator new 被删除
    return 0;
}
2. 栈限定类 (StackOnly) 的重构

重构目标:使用 = delete 明确禁止所有堆分配操作符,并考虑移动语义。

// StackOnlyV2.hpp
class StackOnlyV2 {
public:
    StackOnlyV2() = default;
    ~StackOnlyV2() = default;

    // 允许拷贝和移动(根据实际语义决定)
    StackOnlyV2(const StackOnlyV2&) = default;
    StackOnlyV2(StackOnlyV2&&) = default;
    StackOnlyV2& operator=(const StackOnlyV2&) = default;
    StackOnlyV2& operator=(StackOnlyV2&&) = default;

    void doSomething() {
        // 业务逻辑
    }

private:
    // 明确删除所有堆内存分配函数
    void* operator new(size_t) = delete;
    void* operator new[](size_t) = delete;
    void operator delete(void*) = delete;
    void operator delete[](void*) = delete;

    // 注意:placement new 通常无法也不应被删除,因为它不分配内存。
    // void* operator new(size_t, void* ptr) { return ptr; } // 默认实现
};

// 使用示例
int main() {
    StackOnlyV2 stackObj; // 正确:在栈上分配
    stackObj.doSomething();

    // StackOnlyV2* heapObj = new StackOnlyV2; // 错误:operator new 被删除
    // StackOnlyV2* heapArray = new StackOnlyV2[5]; // 错误:operator new[] 被删除

    // 通过 placement new 在静态或全局内存上构造(技术上可行,但违背设计意图)
    // alignas(StackOnlyV2) static char buffer[sizeof(StackOnlyV2)];
    // StackOnlyV2* p = new (buffer) StackOnlyV2(); // 需要显式调用析构
    // p->~StackOnlyV2();
    return 0;
}
三、技术对比与选型指南
特性维度 传统实现 (博客方法) 现代重构方案 推荐度与说明
生命周期管理 手动调用 Destroy(),易泄漏。 依赖智能指针 (unique_ptr) 或栈自动析构,符合 RAII。 强烈推荐重构方案。自动化管理避免错误。
代码表达清晰度 通过访问控制 (private) 隐含意图。 使用 = delete 明确表达“禁止使用”,编译器错误信息更友好。 推荐重构方案。意图更明确。
继承兼容性 CHeapOnly 作为基类会导致派生类无法析构。 HeapOnlyV2 通过工厂返回 unique_ptr,通常暗示不可继承,或需特殊设计。 需根据设计意图决定。若需继承,需提供受保护的虚析构函数。
STL 容器兼容性 几乎不兼容。 StackOnlyV2 可直接用于容器(元素在容器内存中)。HeapOnlyV2 需存储 unique_ptr 重构方案兼容性更好
阻止分配完整性 CStackOnly 未禁止 new[] StackOnlyV2 明确删除了 newnew[] 推荐重构方案。限制更彻底。
移动语义支持 未涉及,默认生成的可能不符合预期。 可显式定义或删除,控制更精细。 推荐重构方案。适应现代 C++。
四、实战应用场景与建议
  1. 何时使用堆限定类?

    • 对象生命周期超出创建作用域:需在函数间传递所有权。
    • 大对象或资源句柄:避免栈溢出,或需要精确控制释放时机(如文件、网络连接)。
    • 多态对象工厂:工厂方法返回基类指针,指向在堆上分配的派生类对象。
    • 建议:直接使用 std::unique_ptrstd::shared_ptr 作为返回类型,无需刻意将类设计为“仅堆分配”。工厂模式 + 智能指针是更通用、更安全的范式。
  2. 何时使用栈限定类?

    • 小型、轻量、作用域局部的对象:如迭代器、RAII 锁守卫 (std::lock_guard)。
    • 要求极高性能,避免堆分配开销:在实时系统中。
    • 防止用户误用导致生命周期错误:例如,一个代表“当前事务上下文”的对象,其生命周期必须与某个代码块绑定。
    • 建议:大多数情况下,依靠编码规范即可。仅在确有强烈约束时使用 = delete 禁用 operator new记住,std::lock_guard 就是一个经典的“栈限定”思想的应用。
  3. 终极建议:优先使用更高级别的抽象

    • 不要滥用内存分配限制:这通常是过度设计的标志。C++ 的核心优势在于零成本抽象和对资源的直接控制,但应通过接口设计架构模式(如依赖注入、策略模式)来体现,而非通过限制类的底层分配能力。
    • 控制内存来源,而非分配方式:如果目标是确保对象来自特定内存池(如避免碎片),应重载类的 operator new/delete 或使用自定义分配器,而不是禁止某种分配方式。这样类仍然可以通用地与 STL 算法和容器协作。

总结:博客中的技巧是理解 C++ 对象模型和访问控制的有趣案例 。但在生产代码中,应优先采用基于智能指针的资源管理、工厂模式以及现代 C++ 的清晰语义(= delete, final),来构建更安全、更灵活、更易维护的系统。将“内存分配位置”这一底层细节,提升为“对象生命周期管理”这一更高层次的设计考量。


参考来源

Logo

AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。

更多推荐