C++特殊类设计:控制对象创建、拷贝与继承的工程实践

发布时间:2026/7/29 6:32:28
C++特殊类设计:控制对象创建、拷贝与继承的工程实践 1. 项目概述为什么我们需要“特殊类”在C的日常开发中我们大部分时间都在和普通的类打交道用new在堆上创建或者直接在栈上定义。但你是否遇到过这样的需求某个对象必须且只能在堆上分配内存比如一个管理大量动态资源的句柄类一旦在栈上创建析构时可能因为栈帧弹出顺序问题导致资源泄漏又或者你需要一个不能被复制的单例管理器防止数据出现多份副本这些场景就是“特殊类设计”的用武之地。简单来说特殊类设计就是利用C的语言特性如构造函数、析构函数的访问控制delete关键字友元等对类的实例化、拷贝、继承等行为施加精确的约束。这不仅仅是语法技巧更是软件设计意图的体现能从根本上预防某些类型的误用和bug。比如一个只能在堆上创建对象的类可以确保其生命周期完全由开发者通过new/delete或智能指针控制避免因栈对象自动析构而引发的棘手问题。而禁止拷贝的类则是实现资源唯一所有权如文件句柄、网络连接的基石。掌握这些设计模式意味着你从“会用C语法”迈向“理解C设计哲学”。接下来我将拆解几种典型的特殊类从设计思路、代码实现到背后的原理带你彻底搞懂如何“调教”一个类让它乖乖按照你的意图行事。2. 核心设计思路与原理拆解特殊类的设计核心在于控制类的六大默认成员函数构造函数、拷贝构造函数、拷贝赋值运算符、移动构造函数、移动赋值运算符和析构函数。编译器会为我们自动生成这些函数但通过显式声明和特定的访问控制我们可以改变它们的行为。2.1 控制对象创建地点堆 vs 栈对象创建在堆上还是栈上本质区别在于其生命周期的管理方式和内存来源。栈上对象通过直接定义如ClassName obj;创建。内存由编译器在栈空间自动分配对象生命周期与其作用域绑定离开作用域时自动调用析构函数并释放内存。这个过程完全自动化。堆上对象通过new运算符创建如ClassName* pObj new ClassName();。内存从堆自由存储区手动申请生命周期由程序员通过delete或智能指针管理。要让一个类只能在特定区域创建关键在于让另一种创建方式变得不可行。例如要禁止栈上创建就需要让“直接定义对象”这个语法无法通过编译。如何做到一个直接的想法是将析构函数私有化。因为栈对象在离开作用域时编译器必须能在其作用域内调用析构函数来清理资源。如果析构函数不可访问编译器就会报错从而阻止栈对象的定义。不过这只是一个起点后面我们会看到更完善的方案。2.2 禁止拷贝保护资源唯一性C默认的拷贝行为是浅拷贝按成员复制。对于管理内部资源的类如动态数组、文件指针、网络套接字浅拷贝会导致多个对象指向同一资源引发“双重释放”或“悬空指针”等灾难性问题。禁止拷贝是解决这类问题的根本方法之一。 在C11之前我们通过将拷贝构造函数和拷贝赋值运算符声明为private且不实现来达到目的。从C11开始使用 delete关键字更为直观和现代它明确告知编译器“这个函数被删除了不允许使用。”2.3 禁止继承关闭扩展之门有时我们设计一个类希望它是一个“最终”的类不允许其他类派生自它。这可能是因为类的实现非常复杂且脆弱继承可能破坏其不变式或者类本身是某个特定设计模式如一种特殊的单例的一部分继承会破坏其模式约束。C语言本身没有提供finalclass的关键字C11后对于虚函数和类有final说明符但这里指更通用的设计但我们可以通过一些技巧来实现。一种经典手法是将类的构造函数私有化或受保护然后提供一个静态公有函数来创建实例。如果再将这个类不希望被继承的类的析构函数私有化并将目标类作为其友元那么任何尝试继承的类都无法调用基类的析构函数从而导致编译错误。不过C11之后更简单直接的方式是使用final关键字来修饰类。3. 只能在堆上创建对象的类我们来深入实现一个“只能在堆上创建”的类。刚才提到将析构函数私有化可以阻止栈对象创建因为编译器无法自动调用它。但这样做的同时也阻止了delete堆对象因为delete操作也需要调用析构函数。因此一个完整的方案需要解决这个矛盾。我们的设计思路是将析构函数私有化阻止编译器在栈对象离开作用域时自动调用它。提供一个公有的销毁接口让堆对象的创建者能够手动调用析构并释放内存。通常这是一个名为Destroy或DeleteInstance的静态成员函数。将构造函数私有化或受保护防止外部直接实例化强制用户通过我们提供的静态工厂函数来创建对象。工厂函数返回指针通常是智能指针在工厂函数内部使用new创建对象并可能将其托管给智能指针以管理生命周期。3.1 基础版本实现class HeapOnly { public: // 静态工厂函数用于创建对象。返回unique_ptr管理生命周期。 static std::unique_ptrHeapOnly Create(int initVal 0) { // 这里可以做一些额外的初始化检查或日志记录 return std::unique_ptrHeapOnly(new HeapOnly(initVal)); } // 公有销毁接口。虽然unique_ptr会自动调用delete但delete需要析构函数可访问。 // 因此我们仍需提供一个友元函数或让unique_ptr能访问私有析构函数。 // 更常见的做法是在Create函数中传入一个自定义删除器给unique_ptr。 // 但为了清晰展示原理我们先提供一个Destroy函数。 void Destroy() { delete this; // 仅在非常明确对象由new创建且this指针指向堆内存时使用 } // 示例成员函数 void setValue(int val) { value_ val; } int getValue() const { return value_; } private: int value_; // 1. 私有构造函数防止直接实例化 HeapOnly(int initVal) : value_(initVal) { std::cout HeapOnly object constructed on heap with value: value_ std::endl; } // 也可以提供拷贝构造和赋值运算符的delete版本以防止拷贝 HeapOnly(const HeapOnly) delete; HeapOnly operator(const HeapOnly) delete; // 2. 私有析构函数阻止栈对象自动析构 ~HeapOnly() { std::cout HeapOnly object destroyed. std::endl; } // 注意为了使unique_ptr的默认删除器调用delete能工作它需要访问析构函数。 // 一种方法是声明std::default_deleteHeapOnly为友元。但更推荐使用自定义删除器。 // 这里为了简化我们暂时不处理使用上面的Destroy函数。 };使用示例与错误演示// 正确用法 auto objPtr HeapOnly::Create(42); // 通过工厂函数创建 objPtr-setValue(100); // objPtr离开作用域时unique_ptr会自动释放内存但需要能调用~HeapOnly()。 // 由于析构函数私有这里会导致编译错误这暴露了基础版本的问题。 // 错误用法将导致编译错误 // HeapOnly stackObj; // 错误构造函数不可访问 // HeapOnly stackObj(10); // 错误构造函数不可访问 // HeapOnly* pBad new HeapOnly(10); // 错误构造函数不可访问注意上面的基础版本有一个致命缺陷。我们用std::unique_ptr来管理内存但unique_ptr的默认删除器会尝试调用delete而delete需要析构函数可访问。由于我们将析构函数私有化了这会导致编译失败。这引出了关键一点设计特殊类时必须考虑其与现代C设施如智能指针的兼容性。3.2 改进版本兼容智能指针为了解决上述问题我们需要让智能指针能够“合法地”调用私有析构函数。有几种方法为std::unique_ptr提供自定义删除器在删除器中我们通过一个公有成员函数如Destroy来释放资源。将std::default_delete特化版本声明为友元较为hacky不推荐。更优雅的方案利用“公有析构函数但私有构造函数”并结合防止栈分配的技巧。我们可以将析构函数设为公有但通过其他方式阻止栈分配。如何阻止可以将operator new和operator delete重载为私有或受保护因为栈对象分配不调用operator new而堆对象分配必须调用它。让我们采用第三种思路实现一个更健壮的HeapOnlyV2class HeapOnlyV2 { public: // 析构函数设为公有兼容智能指针 ~HeapOnlyV2() { std::cout HeapOnlyV2 object destroyed. std::endl; } // 静态工厂函数返回unique_ptr static std::unique_ptrHeapOnlyV2 Create(int initVal 0) { // 使用std::make_unique会尝试调用构造函数但我们的构造函数是私有的。 // 所以我们必须直接使用new并将其传递给unique_ptr。 // unique_ptr会使用默认的delete而delete会调用公有的析构函数没问题。 return std::unique_ptrHeapOnlyV2(new HeapOnlyV2(initVal)); } void setValue(int val) { value_ val; } int getValue() const { return value_; } private: int value_; // 私有构造函数 HeapOnlyV2(int initVal) : value_(initVal) { std::cout HeapOnlyV2 object constructed with value: value_ std::endl; } // 关键将operator new和operator delete设为私有。 // 任何尝试使用new HeapOnlyV2的行为因为需要调用私有的operator new都会在类外导致编译错误。 // 而我们的工厂函数是成员函数可以调用私有的operator new。 static void* operator new(std::size_t size) { std::cout Custom new for HeapOnlyV2, size: size std::endl; return ::operator new(size); // 调用全局的new } static void operator delete(void* ptr) { std::cout Custom delete for HeapOnlyV2. std::endl; ::operator delete(ptr); // 调用全局的delete } // 同样禁止在栈上分配尽管构造函数私有已经基本阻止但再禁止placement new更安全 static void* operator new[](std::size_t size) delete; static void operator delete[](void* ptr) delete; // 禁止拷贝 HeapOnlyV2(const HeapOnlyV2) delete; HeapOnlyV2 operator(const HeapOnlyV2) delete; };使用验证// 正确用法 auto obj HeapOnlyV2::Create(200); // 成功工厂函数内部调用私有的operator new // obj离开作用域时unique_ptr调用delete触发公有的析构函数和私有的operator delete一切正常。 // 错误用法编译错误 // HeapOnlyV2 stackObj; // 错误构造函数私有 // HeapOnlyV2* p new HeapOnlyV2(300); // 错误operator new 私有 // auto badPtr std::make_uniqueHeapOnlyV2(); // 错误make_unique需要访问构造函数而它私有这个版本实现了我们的目标用户只能通过Create工厂函数获得一个由unique_ptr管理的堆对象无法在栈上创建也无法直接使用new。同时它完美兼容智能指针内存管理安全省心。3.3 注意事项与心得与智能指针的集成是重中之重现代C强调RAII和智能指针。设计特殊类时必须优先考虑如何与std::unique_ptr和std::shared_ptr协同工作。像第一个基础版本那样导致智能指针无法编译的设计是不可接受的。明确所有权语义HeapOnly类暗示对象所有权是独特的通常适合用std::unique_ptr管理。如果你的设计允许共享所有权工厂函数可以返回std::shared_ptr但需要同样注意析构函数的访问权限问题shared_ptr的删除器也需要能调用析构函数。防止误用Destroy函数在基础版本中我们提供了Destroy函数并在其中使用delete this。这是非常危险的操作必须确保this对象确实是用new在堆上创建的并且确保Destroy最多被调用一次。在现代C中应尽量避免让用户手动调用delete而是依赖智能指针。考虑继承情况如果HeapOnly类可能作为基类那么其析构函数通常应声明为virtual如果不是final类。但将其设为虚函数的同时又要控制访问权限需要仔细设计。通常如果希望一个类只能在堆上创建且能被多态使用可以将析构函数设为protected而非private并提供一个公有的虚销毁接口如virtual void Destroy() { delete this; }。4. 只能在栈上创建对象的类与堆上创建相反有时我们希望对象一定在栈上通常是出于性能考虑避免堆分配开销或为了确保生命周期严格绑定于作用域比如一个简单的RAII锁守卫。设计思路与堆上类对称让new操作堆分配变得不可能。最直接的方法是重载operator new并将其设为私有或delete。这样任何尝试使用new创建该对象的行为都会导致编译错误。由于栈分配不调用operator new所以不受影响。4.1 实现代码class StackOnly { public: StackOnly(int val) : data_(val) { std::cout StackOnly object constructed on stack with data: data_ std::endl; } ~StackOnly() { std::cout StackOnly object destroyed. std::endl; } int getData() const { return data_; } void setData(int val) { data_ val; } // 可以正常拷贝如果需要 StackOnly(const StackOnly) default; StackOnly operator(const StackOnly) default; private: int data_; // 关键禁止通过new在堆上创建 static void* operator new(std::size_t size) delete; static void operator delete(void* ptr) delete; // 同样禁止new[]和delete[] static void* operator new[](std::size_t size) delete; static void operator delete[](void* ptr) delete; };使用验证// 正确用法 { StackOnly stackObj(10); // 在栈上创建OK std::cout stackObj.getData() std::endl; } // 离开作用域自动析构 // 错误用法编译错误 // StackOnly* heapObj new StackOnly(20); // 错误operator new 被删除 // auto ptr std::make_uniqueStackOnly(30); // 错误make_unique内部使用new同样失败4.2 局限性分析“栈上唯一”的限制并非绝对安全静态和全局对象定义为静态局部变量、静态全局变量或全局变量的StackOnly对象其内存位置可能在数据段而非栈上但它们的构造和析构调用方式与栈对象类似且能通过编译。这取决于你的设计意图是否要禁止这类对象。作为成员变量如果StackOnly作为另一个类的成员变量而另一个类的对象在堆上创建那么该StackOnly成员实际上也位于堆内存中。但这通常是可以接受的因为它的生命周期仍然由其所属的堆对象管理并且不是通过new StackOnly单独创建的。placement new技术上讲用户可以使用placement new在任意内存地址包括栈地址上构造对象从而绕过operator new的限制。但这属于非常规的高级用法且需要用户手动管理内存一般不考虑。因此StackOnly类更多地是表达一种设计意图和预防常见误用而非一个铁壁铜墙的编译期保证。在大多数情况下它已经足够有效。5. 禁止拷贝的类禁止拷贝有两个主要目的一是避免浅拷贝带来的资源管理问题如双重释放二是表达对象独一无二的语义如单例、文件锁。5.1 C11 之前的方法在C98/03中没有delete关键字。标准做法是将拷贝构造函数和拷贝赋值运算符声明为private并且不提供它们的实现。class NonCopyableOld { public: NonCopyableOld() {} // ... 其他成员函数 ... private: // 声明为private且不实现 NonCopyableOld(const NonCopyableOld); NonCopyableOld operator(const NonCopyableOld); };这样做的原理是如果外部代码尝试拷贝会因为函数是private而编译错误。如果成员函数或友元尝试拷贝链接时会因为找不到函数定义而报链接错误。这是一种“链接期错误”而非“编译期错误”提示不够直接。5.2 现代C方法C11及以后使用 delete是最清晰、最推荐的方式。它直接在编译期给出明确错误。class NonCopyableModern { public: NonCopyableModern() default; ~NonCopyableModern() default; // 使用delete关键字明确禁止拷贝 NonCopyableModern(const NonCopyableModern) delete; NonCopyableModern operator(const NonCopyableModern) delete; // 可以允许移动语义如果需要 NonCopyableModern(NonCopyableModern) default; NonCopyableModern operator(NonCopyableModern) default; // ... 其他成员函数 ... };使用验证NonCopyableModern obj1; // NonCopyableModern obj2 obj1; // 编译错误拷贝构造函数被删除 // NonCopyableModern obj3; // obj3 obj1; // 编译错误拷贝赋值运算符被删除 // 但移动操作是允许的如果显式默认了 NonCopyableModern obj4 std::move(obj1); // OK如果移动构造函数被默认5.3 继承一个通用的NonCopyable基类这是一种常见的惯用法通过继承一个空基类来获得禁止拷贝的特性避免在每个类里重复写delete。class NonCopyable { protected: NonCopyable() default; ~NonCopyable() default; public: NonCopyable(const NonCopyable) delete; NonCopyable operator(const NonCopyable) delete; }; // 使用 class MyResourceManager : private NonCopyable { // 通常私有继承表示“实现”而非“是”的关系 public: MyResourceManager() {} // ... 无需再声明删除拷贝操作 ... };私有继承意味着MyResourceManager不是一个NonCopyable只是利用了其实现。这样MyResourceManager自动获得了不可拷贝的特性代码更简洁。Boost库中就提供了类似的noncopyable类。6. 禁止继承的类在C11之前需要一些技巧来模拟final类的效果。常见方法是利用虚继承和私有构造函数构造一个“毒丸”基类。6.1 C11 之前的技巧class FinalClassBase { private: FinalClassBase() {} friend class FinalClass; // 只允许FinalClass友元访问构造函数 }; class FinalClass : virtual public FinalClassBase { // 注意虚继承 public: FinalClass() {} }; // 尝试继承 class TryToDerive : public FinalClass { public: TryToDerive() {} // 编译错误 };原理分析FinalClass虚继承自FinalClassBase。虚继承意味着最终派生类TryToDerive需要直接调用虚基类FinalClassBase的构造函数。FinalClassBase的构造函数是private的并且只将FinalClass设为友元。因此TryToDerive在构造时试图调用FinalClassBase的私有构造函数但因为它不是友元所以没有权限导致编译失败。FinalClass本身可以构造因为它是友元。这个方法比较巧妙但也略显晦涩并且因为使用了虚继承可能会带来一些额外的开销如虚基类指针。6.2 C11 及以后使用final关键字C11引入了final说明符用于类和虚函数语义清晰直观。class FinalClassCpp11 final { // 使用final关键字 public: FinalClassCpp11() {} // ... }; class TryToDeriveAgain : public FinalClassCpp11 { // 编译错误不能以final类作为基类 };这是最简单、最标准的方式没有任何运行时开销编译期错误信息也明确。在现代C项目中应优先使用final关键字。7. 综合案例与实战心得让我们设计一个综合性的类ResourceHandle它管理一个类似文件句柄的资源要求资源句柄唯一禁止拷贝但允许移动。必须在堆上创建以便灵活控制生命周期并方便放入容器或跨线程传递通过指针或智能指针。不需要禁止继承但我们可以标记为final避免意外继承。#include iostream #include memory #include cassert class ResourceHandle final { // 禁止继承 public: // 工厂函数返回unique_ptr static std::unique_ptrResourceHandle Create(const std::string resourceId) { // 模拟资源申请 void* rawHandle acquireResource(resourceId.c_str()); if (!rawHandle) { return nullptr; // 创建失败返回空指针 } return std::unique_ptrResourceHandle(new ResourceHandle(rawHandle, resourceId)); } // 移动构造函数和移动赋值运算符 ResourceHandle(ResourceHandle other) noexcept : handle_(other.handle_), resourceId_(std::move(other.resourceId_)) { other.handle_ nullptr; // 源对象置空 std::cout ResourceHandle moved from other.resourceId_ to resourceId_ std::endl; } ResourceHandle operator(ResourceHandle other) noexcept { if (this ! other) { releaseResource(); // 释放当前资源 handle_ other.handle_; resourceId_ std::move(other.resourceId_); other.handle_ nullptr; std::cout ResourceHandle move-assigned from other.resourceId_ to resourceId_ std::endl; } return *this; } // 业务操作 void use() const { if (handle_) { std::cout Using resource: resourceId_ std::endl; // 实际使用资源的操作... } } const std::string getId() const { return resourceId_; } private: void* handle_; std::string resourceId_; // 私有构造函数只能由工厂函数调用 ResourceHandle(void* handle, const std::string id) : handle_(handle), resourceId_(id) { std::cout ResourceHandle constructed for resourceId_ std::endl; } // 禁止拷贝 ResourceHandle(const ResourceHandle) delete; ResourceHandle operator(const ResourceHandle) delete; // 禁止在栈上创建通过delete operator new static void* operator new(std::size_t size) delete; static void operator delete(void* ptr) delete; static void* operator new[](std::size_t size) delete; static void operator delete[](void* ptr) delete; // 析构函数释放资源 ~ResourceHandle() { releaseResource(); std::cout ResourceHandle destroyed for resourceId_ std::endl; } void releaseResource() { if (handle_) { // 模拟资源释放 std::cout Releasing resource: resourceId_ std::endl; handle_ nullptr; } } // 模拟资源获取函数 static void* acquireResource(const char* id) { std::cout Acquiring resource: id std::endl; // 返回一个模拟的句柄实际可能是文件描述符、内存指针等 return reinterpret_castvoid*(0xABCD1234); } };使用示例int main() { // 创建资源句柄 auto handle1 ResourceHandle::Create(File_1.txt); if (handle1) { handle1-use(); } // 无法在栈上创建 // ResourceHandle h2(test); // 错误构造函数私有 // ResourceHandle* p new ResourceHandle(...); // 错误operator new 被删除 // 无法拷贝 // auto handle2 handle1; // 错误拷贝构造被删除 // *handle2 *handle1; // 错误拷贝赋值被删除 // 但可以移动所有权转移 auto handle3 std::move(handle1); // handle1变为空 assert(!handle1); // handle1现在为空 if (handle3) { handle3-use(); } // 可以放入容器存储unique_ptr std::vectorstd::unique_ptrResourceHandle handles; handles.push_back(ResourceHandle::Create(Network_Conn_1)); handles.push_back(std::move(handle3)); // 离开作用域所有资源通过unique_ptr自动释放 return 0; }8. 常见问题与排查技巧实录在实际使用这些特殊类时你可能会遇到一些典型的编译错误或设计困惑。下面是一些常见问题及其解决方法。8.1 编译错误“无法访问私有成员”问题描述当你尝试直接实例化一个构造函数被私有的类或者拷贝一个拷贝操作被删除的类时编译器会报错“error C2248: “ClassName::ClassName”无法访问private成员”。排查思路确认设计意图首先检查这是否是你的设计本意。如果是那么你需要改用类提供的公共接口如静态工厂函数来获取对象。检查友元声明如果你的设计允许在特定位置如另一个类或函数创建该对象确保那个位置被声明为这个类的友元friend。检查继承关系如果是在派生类的构造函数中报错无法访问基类的私有构造函数说明你使用了“禁止继承”的技巧。你需要重新考虑设计是否真的需要禁止继承或者改用C11的final关键字。8.2 链接错误“无法解析的外部符号”问题描述主要出现在C98/03时代使用“私有声明且不实现”来禁止拷贝时。如果类的成员函数或友元函数内部不小心进行了拷贝操作编译能通过因为声明可见但链接时会报错“无法解析的外部符号”。排查思路定位调用点根据链接器给出的函数签名找到是哪个函数试图调用私有的拷贝构造函数或赋值运算符。审查代码检查该函数内部逻辑看是否无意中发生了拷贝。例如按值传递该类的参数、按值返回该类的对象、或在容器操作中触发了拷贝。升级到C11这是最根本的解决方法。使用 delete替代“私有不实现”这样错误会在编译期更早、更清晰地暴露出来。8.3 智能指针兼容性问题问题描述为HeapOnly类设计了私有析构函数后使用std::unique_ptrHeapOnly或std::shared_ptrHeapOnly时发生编译错误。解决方案方案一推荐采用本章第3.2节HeapOnlyV2的设计模式。将析构函数设为公有通过私有化operator new来禁止外部new同时提供返回智能指针的工厂函数。这是最兼容现代C的做法。方案二为智能指针提供自定义删除器。工厂函数返回std::unique_ptrHeapOnly, void(*)(HeapOnly*)并在删除器中调用一个公有的Destroy方法。class HeapOnlyWithDeleter { public: static std::unique_ptrHeapOnlyWithDeleter, void(*)(HeapOnlyWithDeleter*) Create() { return std::unique_ptrHeapOnlyWithDeleter, void(*)(HeapOnlyWithDeleter*)( new HeapOnlyWithDeleter(), HeapOnlyWithDeleter::DestroyStatic ); } void Destroy() { delete this; } private: HeapOnlyWithDeleter() {} ~HeapOnlyWithDeleter() {} static void DestroyStatic(HeapOnlyWithDeleter* p) { p-Destroy(); } };这种方法可行但稍显繁琐且返回类型复杂。8.4 “栈上唯一”类的误用问题描述StackOnly类通过delete operator new来禁止堆分配但用户可能将其作为另一个堆对象的成员或者使用placement new在栈内存上构造。设计权衡作为成员这通常是可接受的。StackOnly的设计初衷是防止其自身被单独new出来而不是禁止它存在于堆内存中。作为成员时其生命周期由父对象管理符合RAII思想。Placement new这是一种高级且危险的技巧通常用于自定义内存池或极端优化场景。如果你的类需要防御这种用法几乎是不可能的因为这需要干预对象的构造过程本身。在实践中我们通常假设用户不会故意使用placement new来破坏约定或者通过文档明确说明类的使用约束。8.5 关于单例模式的说明你可能注意到HeapOnly和禁止拷贝的类常常是实现单例模式Singleton的基础组件。单例模式确保一个类只有一个实例并提供全局访问点。它通常结合了以下特性私有构造函数防止随意创建。删除拷贝构造和赋值防止复制出多个实例。一个静态的公有方法如GetInstance()来返回唯一实例的引用或指针。但是单例模式本身是一个更大的、有争议的设计模式话题涉及全局状态、线程安全、测试困难等问题。本章介绍的特殊类设计是构建单例的工具但并不意味着所有禁止拷贝或堆上创建的类都是单例。要根据实际需求谨慎选择模式。