假设有一个类a,它拥有一个类b的对象
- A负责创建和删除b的对象。
- 所有权不能转移到其他类。
- 在A的对象被创建后,B的对象永远不会被重新初始化。
通常,据我所知,在现代c++中,我们会使用unique_ptr来表示a是这个对象/引用的所有者:
// variant 1 (unique pointer)
class A {
public:
A(int param) : b(std::make_unique<B>(param)) {}
// give out a raw pointer, so that others can access and change the object
// (without transferring ownership)
B* getB() {
return b.get();
}
private:
std::unique_ptr<B> b;
};
有人建议我也可以用这种方法来代替:
// variant 2 (no pointer)
class A {
public:
A(int param) : b(B(param)) {}
// give out a reference, so that others can access and change the object
// (without transferring ownership)
B& getB() {
return b;
}
private:
B b;
};
据我所知,主要区别在于,在变体2中,内存是一致分配的,而在变体1中,B对象的内存可以分配到任何地方,指针必须首先解析才能找到它。当然,如果a的公共接口的用户可以使用B*
或B&
,这也是有区别的。但在这两种情况下,我确信所有权在我需要它时保持在A内。
我总是必须使用变体1 (unique_ptrs)来表达所有权吗?使用变体2而不是变体1的原因是什么?
所有权可以有不同的表达方式。
变体2的B b
是所有权的最简单形式。A
类的实例独占地拥有存储在b
中的对象,并且所有权不能转移到另一个对象或(成员)变量。
std::unique_ptr<B> b
表达了对std::unique_ptr<B>
所管理的对象的唯一但可转让的所有权。
但std::optional<B>
,std::vector<B>
,…或std::variant<B,C>
也表示所有权。
当然,如果a的公共接口的用户可以使用B*或B&但在这两种情况下,我确信所有权在我需要它时保持在A内。
无论成员是B b
还是std::unique_ptr<B> b
(也可以是std::optional<B>
,std::vector<B>
,…std::variant<B,C>
),都可以创建返回B*
的成员函数
变体1
class A {
public:
A(int param) : b(B(param)) {}
// give out a reference, so that others can access and change the object
B& getBRef() {
return b;
}
B* getB() {
return &b;
}
private:
B b;
};
比
更容易出错变种2
class A {
public:
A(int param) : b(std::make_unique<B>(param)) {}
// give out a raw pointer, so that others can access and change the object
// (without transferring ownership)
B* getB() {
return b.get();
}
private:
std::unique_ptr<B> b;
};
对于变体2在这种情况下,调用A
的另一个成员函数可能会使指针失效(如果该函数将另一个对象赋值给unique_ptr
)。而对于变量1返回的指针(或引用)的有效性由A
实例的生存期决定。无论如何,您必须将B *
视为非拥有的原始指针,并在文档中明确该原始指针的有效期。
您选择哪种类型的所有权取决于实际的用例。大多数时候,您将尝试保持最简单的所有权B b
。如果该对象应该是可选的,您将使用std::optional<B>
。
如果实现需要它,例如,如果你计划使用PImpl,如果数据结构可能阻止使用B b
(如在树状或图状结构的情况下),或者如果所有权必须可转让,你可能想要使用std::unique_ptr<B>
。
您错过了unique_ptr
的要点。它的创建正是为了允许通过简单的分配来转移所有权。如果所有权从未被转移过,那么通过使用containment来拥有子对象会简单得多。
内存分配在c++中并不重要,至少对于使用对象的程序员来说是这样,因为许多对象在内部使用动态分配,而不管对象的持续时间是多少。例如:std::string
。在大多数实现中,自动std::string
对象仍然会为其底层字符数组使用动态内存。
当然,库实现者应该关心他们的对象使用动态内存的方式。小字符串优化是在标准库的最新实现中发明的,以避免小字符串的动态分配(感谢NathanOliver的评论)。