1. 为什么需要禁止拷贝的类?
在C++开发中,我们经常会遇到一些特殊场景,需要设计不能被拷贝的类。这种情况在实际项目中并不少见,理解其背后的原理和实现方式对写出健壮的C++代码至关重要。
1.1 典型应用场景
资源管理类是最常见的需要禁止拷贝的案例。想象一下你设计了一个文件句柄管理类:
cpp复制class FileHandle {
public:
FileHandle(const char* filename) {
fd = open(filename, O_RDWR);
}
~FileHandle() {
close(fd);
}
private:
int fd;
};
如果允许拷贝这个类的对象,会发生什么?两个对象持有同一个文件描述符,当它们先后析构时,会导致同一个文件描述符被关闭两次 - 这绝对是灾难性的行为。
单例模式是另一个典型例子。单例的核心就是保证全局唯一实例,如果允许拷贝,就完全违背了设计初衷:
cpp复制class Singleton {
public:
static Singleton& getInstance() {
static Singleton instance;
return instance;
}
// 必须禁止拷贝
private:
Singleton() = default;
~Singleton() = default;
};
1.2 拷贝带来的问题
C++中的拷贝主要通过两个特殊成员函数实现:
- 拷贝构造函数:
ClassName(const ClassName&) - 拷贝赋值运算符:
ClassName& operator=(const ClassName&)
当这两个函数可以正常调用时,可能导致以下问题:
- 资源重复释放:如文件描述符、网络连接等
- 逻辑错误:如单例对象被复制
- 性能损耗:大对象的无意义拷贝
- 数据不一致:共享资源的状态同步问题
提示:现代C++中,移动语义(move semantics)的出现使得资源管理更加灵活,但对于确实需要禁止拷贝的类,我们仍需掌握正确的实现方式。
2. C++98的实现方式
在C++98标准中,由于缺乏直接禁止函数调用的语法,我们需要通过一些技巧来实现禁止拷贝的功能。这种方法至今仍能在一些遗留代码中看到。
2.1 私有化+不实现的经典模式
cpp复制class NonCopyable {
public:
NonCopyable() = default;
private:
// 关键点1:声明为private
NonCopyable(const NonCopyable&); // 拷贝构造
NonCopyable& operator=(const NonCopyable&); // 赋值运算符
// 关键点2:只声明不实现
};
这种实现有两个关键点:
- 私有化:防止外部直接调用
- 不实现:即使通过友元或成员函数内部调用,也会在链接阶段失败
2.2 实现原理深度解析
访问控制层面:将拷贝操作设为private,外部代码尝试拷贝时会触发"无法访问private成员"的编译错误。
链接层面:即使通过成员函数或友元函数内部调用,由于函数没有实现,链接器会报"未定义的引用"错误。
这种双重保护机制确保了:
- 编译期阻止外部调用
- 链接期阻止内部调用
2.3 实际应用示例
考虑一个数据库连接管理类:
cpp复制class DBConnection {
public:
DBConnection(const std::string& connStr) {
// 建立连接
}
~DBConnection() {
// 关闭连接
}
void executeQuery(const std::string& sql) {
// 执行查询
}
private:
DBConnection(const DBConnection&); // 禁止拷贝构造
DBConnection& operator=(const DBConnection&); // 禁止赋值
// 实际的数据库连接句柄
void* dbHandle;
};
如果尝试这样使用:
cpp复制DBConnection conn1("server=127.0.0.1");
DBConnection conn2 = conn1; // 编译错误
DBConnection conn3("server=127.0.0.1");
conn3 = conn1; // 编译错误
2.4 优缺点分析
优点:
- 兼容所有C++版本
- 不需要额外基类
- 原理简单直接
缺点:
- 错误信息不够直观
- 链接期才能发现内部调用的错误
- 代码意图表达不够明确
3. C++11的现代实现方式
C++11引入了=delete语法,让禁止拷贝的实现变得更加直观和优雅。这也是现代C++推荐的做法。
3.1 =delete语法详解
cpp复制class NonCopyable {
public:
NonCopyable() = default;
// 明确删除拷贝操作
NonCopyable(const NonCopyable&) = delete;
NonCopyable& operator=(const NonCopyable&) = delete;
};
=delete告诉编译器:"这个函数被显式删除,任何尝试使用它的代码都应该报错"。
3.2 与传统方式的对比
-
错误发现时机:
- C++98:外部调用编译期报错,内部调用链接期报错
- C++11:所有调用都在编译期报错
-
代码清晰度:
- C++98:需要理解"私有+未实现"的组合意义
- C++11:直接表达"禁止拷贝"的意图
-
访问控制:
- C++98:依赖private限制
- C++11:即使放在public区也有效
3.3 实际应用示例
现代C++中的线程类就是一个很好的例子:
cpp复制class WorkerThread {
public:
WorkerThread() {
// 线程初始化
}
~WorkerThread() {
// 线程清理
}
// 禁止拷贝
WorkerThread(const WorkerThread&) = delete;
WorkerThread& operator=(const WorkerThread&) = delete;
// 允许移动
WorkerThread(WorkerThread&&) = default;
WorkerThread& operator=(WorkerThread&&) = default;
void start() {
// 启动线程
}
};
3.4 结合移动语义
C++11还引入了移动语义,我们可以选择性禁止拷贝但允许移动:
cpp复制class ResourceHolder {
public:
ResourceHolder() = default;
// 禁止拷贝
ResourceHolder(const ResourceHolder&) = delete;
ResourceHolder& operator=(const ResourceHolder&) = delete;
// 允许移动
ResourceHolder(ResourceHolder&&) = default;
ResourceHolder& operator=(ResourceHolder&&) = default;
// 其他成员函数...
};
这种模式在资源管理类中非常常见,既保证了资源的安全转移,又防止了意外的拷贝。
4. 工程实践中的注意事项
在实际项目中,禁止拷贝的类需要特别注意一些使用细节和边界情况。
4.1 继承体系中的处理
当设计一个禁止拷贝的基类时,推荐使用专门的NonCopyable基类:
cpp复制class NonCopyable {
protected:
NonCopyable() = default;
~NonCopyable() = default;
NonCopyable(const NonCopyable&) = delete;
NonCopyable& operator=(const NonCopyable&) = delete;
};
class Derived : private NonCopyable {
// 自动获得禁止拷贝的特性
};
这种方式的优点:
- 意图明确
- 避免在每个派生类中重复声明
- 可以通过继承关系一眼看出设计意图
4.2 与STL容器的兼容性
禁止拷贝的类与STL容器一起使用时需要特别注意:
cpp复制class NoCopy {
public:
NoCopy() = default;
NoCopy(const NoCopy&) = delete;
NoCopy& operator=(const NoCopy&) = delete;
};
std::vector<NoCopy> vec; // 可以编译
vec.emplace_back(); // 可以,使用原地构造
vec.push_back(NoCopy()); // 编译错误,尝试使用拷贝
最佳实践:
- 优先使用
emplace系列函数 - 如果必须插入已有对象,考虑使用移动语义(如果允许移动)
- 或者使用指针/智能指针存储
4.3 常见陷阱与调试技巧
-
意外触发拷贝的场景:
- 函数传参时的值传递
- 函数返回时的NRVO失效
- 容器操作时的元素复制
-
调试技巧:
- 使用
static_assert验证类型特性 - 在调试版本中为拷贝操作添加实现并打印警告
- 使用类型特征检查
std::is_copy_constructible
- 使用
cpp复制static_assert(!std::is_copy_constructible<NoCopy>::value,
"NoCopy should not be copyable");
4.4 性能考量
禁止拷贝的类通常用于管理资源,因此还需要考虑:
-
移动语义的效率:
- 如果允许移动,确保移动操作是noexcept的
- 移动操作应该尽量高效
-
构造/析构成本:
- 资源获取/释放的耗时
- 异常安全保证
-
对象传递方式:
- 使用引用或指针传递
- 考虑使用工厂函数
5. 高级应用与模式扩展
掌握了基本的禁止拷贝技术后,我们可以探讨一些更高级的应用场景和模式变体。
5.1 选择性禁止拷贝
有时我们希望对某些成员函数保持拷贝能力,而禁止其他情况下的拷贝:
cpp复制class SelectiveCopy {
public:
SelectiveCopy() = default;
// 禁止常规拷贝
SelectiveCopy(const SelectiveCopy&) = delete;
SelectiveCopy& operator=(const SelectiveCopy&) = delete;
// 允许通过特定方法复制
SelectiveCopy clone() const {
SelectiveCopy result;
// 实现深拷贝逻辑
return result;
}
};
5.2 与PImpl惯用法结合
PImpl(指针到实现)惯用法常与禁止拷贝一起使用:
cpp复制// Widget.h
class Widget {
public:
Widget();
~Widget();
// 禁止拷贝
Widget(const Widget&) = delete;
Widget& operator=(const Widget&) = delete;
// 允许移动
Widget(Widget&&) noexcept;
Widget& operator=(Widget&&) noexcept;
void doSomething();
private:
struct Impl;
std::unique_ptr<Impl> pImpl;
};
5.3 线程安全考虑
对于多线程环境,禁止拷贝的类需要额外注意:
-
移动操作的线程安全:
- 确保移动操作不会导致数据竞争
- 考虑使用锁或其他同步机制
-
资源所有权的转移:
- 明确所有权转移的线程边界
- 避免悬空指针/引用
5.4 现代C++的扩展应用
C++17引入的std::optional和C++20的std::unique_resource等工具可以与禁止拷贝的类很好地配合:
cpp复制class ExclusiveResource {
// 禁止拷贝的实现...
};
std::optional<ExclusiveResource> createResource(bool condition) {
if (condition) {
return ExclusiveResource();
}
return std::nullopt;
}
6. 实际项目经验分享
在实际工程中应用禁止拷贝的类时,有一些经验教训值得分享。
6.1 代码审查常见问题
-
不必要的拷贝禁止:
- 不是所有资源类都需要禁止拷贝
- 有时深拷贝是更好的选择
-
忘记禁止赋值运算符:
- 只禁止了拷贝构造但忘了禁止赋值
- 使用
=delete可以更不容易遗漏
-
与RAII原则的冲突:
- 禁止拷贝可能导致资源管理复杂化
- 需要权衡设计目标
6.2 测试策略
对于禁止拷贝的类,测试策略需要特别考虑:
-
编译期测试:
- 静态断言验证类型特性
- 故意编写应该失败的测试代码
-
运行时测试:
- 验证移动语义的正确性(如果允许移动)
- 测试工厂函数和创建逻辑
6.3 性能优化案例
在一个高性能网络库中,我们使用禁止拷贝的连接类:
cpp复制class NetworkConnection {
public:
NetworkConnection(Socket&& socket) : socket_(std::move(socket)) {}
// 禁止拷贝
NetworkConnection(const NetworkConnection&) = delete;
NetworkConnection& operator=(const NetworkConnection&) = delete;
// 允许移动
NetworkConnection(NetworkConnection&&) = default;
NetworkConnection& operator=(NetworkConnection&&) = default;
void send(const Buffer& buf);
Buffer receive();
private:
Socket socket_;
};
这种设计带来了以下好处:
- 明确的所有权语义
- 避免意外的连接复制
- 高效的资源转移
- 清晰的接口契约
6.4 跨团队协作建议
当设计将被多个团队使用的禁止拷贝的类时:
-
文档说明:
- 明确说明禁止拷贝的原因
- 提供替代方案(如工厂函数、移动语义)
-
错误信息友好性:
- 使用static_assert提供清晰的错误提示
- 考虑提供自定义的编译错误信息
-
设计一致性:
- 在整个项目中保持统一风格
- 要么全部使用NonCopyable基类,要么全部使用=delete
7. 经典实现对比与选型建议
在实际项目中,我们需要根据具体情况选择合适的禁止拷贝实现方式。
7.1 各种实现方式对比
| 特性 | C++98私有化方案 | C++11 =delete | NonCopyable基类 |
|---|---|---|---|
| 代码清晰度 | 中等 | 高 | 高 |
| 错误发现时机 | 编译/链接期 | 编译期 | 编译期 |
| 侵入性 | 低 | 低 | 中等 |
| 派生类便利性 | 需要重复声明 | 需要重复声明 | 自动继承 |
| C++版本要求 | 所有 | C++11 | 所有 |
| 移动语义友好性 | 需要额外处理 | 容易配合 | 容易配合 |
7.2 选型建议
-
新项目(使用C++11及以上):
- 首选
=delete方式 - 代码意图表达最明确
- 编译期错误检测
- 首选
-
旧代码维护(C++98):
- 使用私有化方案
- 或者使用NonCopyable基类
-
大型项目/框架:
- 推荐统一的NonCopyable基类
- 保持代码风格一致
- 方便文档和培训
-
模板代码/库开发:
=delete更灵活- 可以配合SFINAE或概念(concepts)使用
7.3 未来演进方向
随着C++标准的演进,禁止拷贝的类也在不断发展:
-
C++20的concepts:
- 可以定义
NonCopyable概念 - 在模板中约束类型特性
- 可以定义
-
模块化:
- 通过模块导出NonCopyable接口
- 更好的封装性
-
编译期反射:
- 可能提供新的实现方式
- 更丰富的类型操作能力
8. 从语言设计角度看禁止拷贝
理解C++语言设计者为何这样设计拷贝控制,能帮助我们更好地使用这些特性。
8.1 C++对象模型基础
C++的对象模型基于几个核心原则:
- 值语义:默认情况下对象表现为值
- 确定性析构:对象生命周期明确
- 资源获取即初始化(RAII):资源管理与对象生命周期绑定
禁止拷贝的机制正是建立在这些基本原则之上的。
8.2 拷贝控制的演进历史
-
C++98时代:
- 隐式生成拷贝操作
- 需要手动禁止时技巧性较强
-
C++11:
- 引入
=delete和=default - 增加移动语义
- 拷贝控制更明确
- 引入
-
现代C++:
- 规则越来越清晰
- 编译器检查更严格
- 意图表达更直接
8.3 与其他语言的对比
-
Java/C#:
- 引用语义为主
- 拷贝问题不那么突出
- 需要深拷贝时实现Cloneable
-
Rust:
- 所有权系统内置
- 移动是默认行为
- 显式实现Copy trait才允许拷贝
-
Go:
- 值传递但通常使用指针
- 需要自己注意拷贝问题
8.4 设计哲学思考
C++的设计哲学强调:
- 零开销抽象:禁止拷贝的机制几乎无运行时开销
- 显式优于隐式:需要明确表达设计意图
- 灵活性:提供多种实现方式适应不同场景
这些原则在拷贝控制机制中得到了充分体现。
9. 模板元编程中的应用
在模板和元编程中,禁止拷贝的类有一些特殊的应用场景和技巧。
9.1 类型特征检查
我们可以使用类型特征来检测类是否可拷贝:
cpp复制template<typename T>
void process(T&& obj) {
static_assert(!std::is_copy_constructible_v<std::decay_t<T>>,
"This function requires non-copyable types");
// 实现...
}
9.2 SFINAE应用
利用SFINAE技术,可以根据是否可拷贝选择不同实现:
cpp复制template<typename T, typename = std::enable_if_t<!std::is_copy_constructible_v<T>>>
void handleResource(T&& resource) {
// 针对不可拷贝类型的实现
}
template<typename T, typename = std::enable_if_t<std::is_copy_constructible_v<T>>>
void handleResource(const T& resource) {
// 针对可拷贝类型的实现
}
9.3 模板基类模式
创建模板化的NonCopyable基类:
cpp复制template<typename Derived>
class NonCopyable {
protected:
NonCopyable() = default;
~NonCopyable() = default;
NonCopyable(const NonCopyable&) = delete;
NonCopyable& operator=(const NonCopyable&) = delete;
};
class MyResource : public NonCopyable<MyResource> {
// 自动获得禁止拷贝的特性
};
9.4 概念(Concepts)应用
C++20中可以使用概念来约束模板参数:
cpp复制template<typename T>
concept NonCopyable = !std::is_copy_constructible_v<T>;
template<NonCopyable T>
void processResource(T&& resource) {
// 只能接受不可拷贝类型的实现
}
10. 性能优化与异常安全
禁止拷贝的类通常管理着重要资源,因此需要特别注意性能和异常安全。
10.1 移动操作的优化
对于允许移动的禁止拷贝类:
-
确保移动操作是noexcept:
cpp复制class Resource { public: Resource(Resource&&) noexcept; Resource& operator=(Resource&&) noexcept; }; -
实现高效的移动语义:
- 避免不必要的资源分配
- 使用指针交换等技巧
10.2 异常安全保证
提供明确的异常安全保证:
-
基本保证:
- 异常发生时无资源泄漏
- 对象处于有效状态
-
强保证:
- 操作要么完全成功,要么完全回滚
- 对于关键操作很有价值
-
不抛保证:
- 移动操作等重要函数应尽量不抛异常
10.3 资源管理策略
-
延迟获取:
- 构造函数不直接获取资源
- 提供单独的open/init方法
-
两阶段构造:
cpp复制class File { public: File() = default; void open(const std::string& path) { // 实际打开文件 } }; -
工厂函数:
cpp复制std::unique_ptr<Resource> createResource() { auto res = std::make_unique<Resource>(); res->initialize(); return res; }
10.4 内存布局考量
禁止拷贝的类通常有特定的内存布局需求:
-
避免不必要的间接访问:
- 谨慎使用pImpl模式
- 考虑缓存友好性
-
智能指针的使用:
std::unique_ptr适合独占资源std::shared_ptr适合共享资源
-
小对象优化:
- 对于小型资源,直接内联存储
- 避免额外的堆分配
11. 设计模式中的应用
禁止拷贝的类在多种设计模式中扮演重要角色,理解这些应用有助于更好地掌握面向对象设计。
11.1 单例模式
经典的单例实现必须禁止拷贝:
cpp复制class Singleton {
public:
static Singleton& instance() {
static Singleton inst;
return inst;
}
// 禁止拷贝
Singleton(const Singleton&) = delete;
Singleton& operator=(const Singleton&) = delete;
private:
Singleton() = default;
~Singleton() = default;
};
11.2 工厂模式
抽象工厂通常也需要禁止拷贝:
cpp复制class WidgetFactory {
public:
virtual std::unique_ptr<Widget> create() = 0;
protected:
WidgetFactory() = default;
virtual ~WidgetFactory() = default;
// 禁止拷贝但允许派生
WidgetFactory(const WidgetFactory&) = delete;
WidgetFactory& operator=(const WidgetFactory&) = delete;
};
11.3 观察者模式
主题(Subject)类有时需要禁止拷贝以保证观察者的一致性:
cpp复制class Subject {
public:
void attach(Observer* o);
void detach(Observer* o);
void notify();
protected:
Subject() = default;
~Subject() = default;
// 禁止拷贝
Subject(const Subject&) = delete;
Subject& operator=(const Subject&) = delete;
private:
std::vector<Observer*> observers_;
};
11.4 状态模式
状态对象通常禁止拷贝以避免状态混乱:
cpp复制class State {
public:
virtual void handle(Context&) = 0;
virtual ~State() = default;
protected:
State() = default;
// 禁止拷贝
State(const State&) = delete;
State& operator=(const State&) = delete;
};
12. 跨API边界设计
当禁止拷贝的类需要跨越API或模块边界时,需要特别注意一些设计问题。
12.1 C接口封装
将C++类通过C API导出时:
cpp复制// C++实现
class Database {
public:
Database(const char* connStr);
~Database();
// 禁止拷贝
Database(const Database&) = delete;
Database& operator=(const Database&) = delete;
void execute(const char* sql);
};
// C API
extern "C" {
struct DatabaseHandle;
DatabaseHandle* db_open(const char* connStr);
void db_close(DatabaseHandle* db);
void db_execute(DatabaseHandle* db, const char* sql);
}
12.2 ABI稳定性考虑
确保二进制兼容性:
-
避免内联虚函数:
- 虚函数最好非内联
- 防止vtable布局变化
-
使用PImpl惯用法:
- 隐藏实现细节
- 减少头文件依赖
-
谨慎使用=delete:
- 确保不影响已有客户端代码
12.3 跨语言互操作
与其他语言交互时的策略:
-
使用智能指针包装:
cpp复制std::unique_ptr<Resource> createResource(); -
提供明确的ownership转移语义:
- 文档说明资源所有权
- 提供明确的释放函数
-
考虑COM或类似接口:
- 定义清晰的接口边界
- 使用引用计数管理生命周期
12.4 版本兼容性策略
长期维护的API需要考虑:
-
不可拷贝特性的版本演进:
- 初期可能允许拷贝
- 后期版本禁止时需要兼容
-
弃用策略:
- 先标记为deprecated
- 再完全删除
-
ABI检查工具:
- 使用工具验证兼容性
- 确保不破坏现有客户端
13. 测试与调试技巧
针对禁止拷贝的类,需要专门的测试和调试方法以确保代码质量。
13.1 静态断言验证
编译期检查类型特性:
cpp复制static_assert(!std::is_copy_constructible_v<NoCopyClass>,
"NoCopyClass should not be copyable");
static_assert(!std::is_copy_assignable_v<NoCopyClass>,
"NoCopyClass should not be copy assignable");
13.2 单元测试策略
-
编译失败测试:
- 验证拷贝操作确实会导致编译错误
- 可以使用SFINAE或concepts
-
运行时行为测试:
- 测试工厂函数
- 验证移动语义(如果允许移动)
- 测试资源管理正确性
13.3 调试技巧
-
自定义错误信息:
cpp复制class NoCopy { public: NoCopy() = default; NoCopy(const NoCopy&) { static_assert(false, "This class is not copyable"); } }; -
运行时检查:
- 在调试版本中提供拷贝操作的实现
- 打印警告信息或触发断言
-
类型特征打印:
cpp复制std::cout << std::boolalpha; std::cout << "is_copy_constructible: " << std::is_copy_constructible_v<MyClass> << '\n';
13.4 代码覆盖率
确保测试覆盖:
-
所有创建路径:
- 直接构造
- 工厂函数
- 移动构造(如果允许)
-
所有使用场景:
- 作为函数参数
- 作为返回值
- 在容器中使用
-
边界条件:
- 资源获取失败
- 异常安全测试
14. 现代C++最佳实践
随着C++标准的发展,禁止拷贝的类也有一些新的最佳实践。
14.1 五法则与三法则
-
三法则:
- 如果需要自定义析构函数、拷贝构造函数或拷贝赋值运算符中的一个,通常需要自定义全部三个
-
五法则:
- C++11扩展为还包括移动构造函数和移动赋值运算符
对于禁止拷贝的类:
- 显式删除拷贝操作
- 根据需要定义或删除移动操作
- 明确声明析构函数
14.2 默认和删除的明确使用
现代C++风格:
cpp复制class ModernNoCopy {
public:
ModernNoCopy() = default;
~ModernNoCopy() = default;
// 明确删除拷贝
ModernNoCopy(const ModernNoCopy&) = delete;
ModernNoCopy& operator=(const ModernNoCopy&) = delete;
// 明确默认移动
ModernNoCopy(ModernNoCopy&&) = default;
ModernNoCopy& operator=(ModernNoCopy&&) = default;
};
14.3 基于概念的模板设计
C++20概念可以更好地表达不可拷贝约束:
cpp复制template<typename T>
concept NonCopyable = !std::is_copy_constructible_v<T> &&
!std::is_copy_assignable_v<T>;
template<NonCopyable T>
void processResource(T&& res) {
// 只能处理不可拷贝类型
}
14.4 异常安全与事务语义
现代C++鼓励:
-
RAII包装:
- 使用智能指针管理资源
- 自定义删除器
-
事务性操作:
- 要么完全成功
- 要么完全回滚
-
不抛保证:
- 移动操作等重要函数尽量不抛异常
15. 总结与个人实践心得
在多年的C++开发中,我总结了以下关于禁止拷贝类的实践经验:
-
明确设计意图:
- 从一开始就决定类是否应该可拷贝
- 在文档中明确说明设计决策
-
一致性原则:
- 在整个项目中保持统一的禁止拷贝风格
- 要么都用
=delete,要么都用NonCopyable基类
-
移动语义的合理使用:
- 不是所有禁止拷贝的类都需要允许移动
- 移动语义应该是有意设计的,而非默认添加
-
文档和注释:
- 解释为什么禁止拷贝
- 提供替代方案的使用示例
-
测试策略:
- 编译期测试和运行时测试并重
- 特别是边界条件和异常场景
-
性能考量:
- 权衡禁止拷贝带来的灵活性和安全性收益
- 在性能关键路径上谨慎决策
-
团队共识:
- 确保团队成员理解设计决策
- 建立代码审查要点
在实际项目中,我发现禁止拷贝的类最适合以下场景:
- 管理唯一资源(文件句柄、网络连接等)
- 表示系统唯一实体(单例、全局管理器等)
- 作为复杂对象的构建器(Builder)或工厂
- 实现特定的设计模式(状态、策略等)
最后一点建议:当设计一个类时,先问自己"这个类的对象应该被拷贝吗?",这个简单的问题能帮助你做出更好的设计决策。