即使没有创造模式,用 C++ 创造一个对象的行为也充满了危险。应该在栈上创建还是在堆上创建?那应该是一个原始指针,一个唯一的或共享的指针,还是其他什么?最后,手动创建对象是否仍然合适,或者我们是否应该将基础设施的所有关键方面的创建推迟到专门的构造,如工厂(稍后将详细介绍它们!)还是控制容器的倒置?
无论您选择哪一个选项,创建对象仍然是一件苦差事,尤其是如果构建过程很复杂或者需要遵守特殊的规则。这就是创造模式的来源:它们是与创建对象相关的常见方法。
如果你对基本的 C++ 或者智能指针不太熟悉,这里有一个简单的 C++ 对象创建方法的回顾:
- 栈分配创建一个将在栈上分配的对象。该对象将在作用域结束时被自动清理(您可以用一对花括号在任何地方创建一个人工作用域)。如果你把这个对象赋给一个变量,这个对象将在作用域的最末端调用析构函数;如果不这样做,析构函数将被立即调用。(这可能会破坏 Memento 设计模式的一些实现,我们稍后会发现。)
- 使用原始指针的堆分配将对象放在堆上(也称为自由存储)。
Foo* foo = new Foo;
创建了一个Foo
的新实例,并留下了谁负责清理对象的问题。GSL 1owner<T>
试图引入一些原始指针“所有权”的概念,但不涉及任何清理代码——你仍然必须自己编写。 - 一个唯一的指针(
unique_ptr
)可以获取一个堆分配的指针并管理它,这样当不再有对它的引用时,它会被自动清除。唯一指针确实是唯一的:你不能复制它,也不能把它传递给另一个函数而不失去对原指针的控制。 - 共享指针(
shared_ptr
)接受一个堆分配的指针并管理它,但是允许在代码中共享这个指针。只有当指针上没有组件时,拥有的指针才会被清除。 - 弱指针(
weak_ptr
)是一个智能但无所有权的指针,它保存对由shared_ptr
管理的对象的弱引用。您需要将它转换成一个shared_ptr
,以便能够实际访问被引用的对象。它的用途之一是打破shared_ptr
s 的循环引用。
如果你要返回大于一个字大小的值,有几种方法可以从函数中返回一些东西。首先,也是最明显的是:
1 Foo make_foo(int n)
2 {
3 return Foo{n};
4 }
您可能会觉得,使用前面的方法,正在制作Foo
的完整副本,从而浪费了宝贵的资源。但并不总是如此。假设您将Foo
定义为:
1 struct Foo
2 {
3 Foo(int n) {}
4 Foo(const Foo&) { cout << "COPY CONSTRUCTOR!!!\n"; }
5 };
您会发现复制构造器可能被调用零到两次:调用的确切次数取决于编译器。返回值优化(RVO)是一个编译器特性,它专门防止产生额外的副本(因为它们不会真正影响代码的行为)。然而,在复杂的场景中,你真的不能指望 RVO 会发生,但是在选择是否优化返回值的时候,我更倾向于选择 Knuth。 2
当然,另一种方法是简单地返回一个智能指针,比如一个unique_ptr
:
1 unique_ptr<Foo> make_foo(int n)
2 {
3 return make_unique<Foo>(n);
4 }
这是非常安全的,但也是固执己见的:你已经为用户选择了智能指针。他们不喜欢智能指针怎么办?如果他们更喜欢shared_ptr
呢?
第三个也是最后一个选择是使用原始指针,可能与 GSL 的owner<T>
一起使用。这样,您不是在强制清理分配的对象,而是在传递一个非常明确的信息,即这是调用者的责任:
1 owner<Foo*> make_foo(int n)
2 {
3 return new Foo(n);
4 }
你可以把这种方法看作是给用户一个提示:我正在返回一个指针,从现在开始由你来负责这个指针。当然,现在make_foo()
的调用者需要处理指针:要么正确调用delete
,要么将其包装在unique_ptr
或shared_ptr
中。请记住,owner<T>
没有提到复制。
所有这些选项都同样有效,很难说哪个选项更好。
Footnotes 1
指南支持库( https://github.com/Microsoft/GSL
)是 C++ 核心指南建议的一组函数和类型。这个库包括许多类型,其中的owner<T>
类型用于指示指针的所有权。
以《计算机编程的艺术》系列丛书而闻名的唐纳德·克努特(Donald Knuth)曾写过一篇论文,声称“过早优化是万恶之源”。C++ 让过早的优化变得非常诱人,但是你应该抵制这种诱惑,直到 A)你完全明白你在做什么;B)您实际体验到需要优化的性能效果。