智能指针作为指针
了解 const 与智能指针作为指针之间的关系。
有两种方式来理解智能指针。一种方式是将智能指针视为有效的指针,即指针本身或其所指向的值可以是 const,或者两者都是。
另一种视角是将智能指针看作类类型对象。本质上,它们是对原始指针的封装。由于智能指针是一个对象,通常建议将其作为 const 引用传递。但事实上,在大多数情况下,这样做并不合适。
下面我们分别探讨这两种视角。
const 与智能指针作为指针
智能指针之所以被称为“有效指针”,是因为它们能自动管理内存。因此,我们可以对指针本身或其所指向的值使用 const。
虽然存在多种智能指针,但我们的默认选择应为 std::unique_ptr,因此在示例中我们将主要使用它,除非需要展示特定概念时才使用共享指针。
const std::unique_ptr<T>
在这种情况下,是智能指针本身为 const,而不是其所指向的数据。这意味着我们无法重置该指针或更改其指向的内容。但指针所指向的数据仍然可以被修改。
1#include <iostream> 2#include <memory> 3 4int main() { 5 const std::unique_ptr<int> p = std::make_unique<int>(42); 6 ++(*p); // 正确,数据不是 const 7 // p.reset(new int{666}); // 错误,不能重置 const 指针 8 std::cout << *p << '\n'; 9}
需要注意的是,const unique_ptr 的功能受到限制。我们无法返回它,因为这需要移动指针所有权。因此,以下代码无法通过编译:
1#include <iostream> 2#include <memory> 3using namespace std; 4 5const std::unique_ptr<int> f() { 6 const std::unique_ptr<int> p = std::make_unique<int>(42); 7 return p; 8}
然而,从 C++17 开始,const 返回类型不再是问题。下面这个片段在 C++14 下会报错,但在 C++17 下可以正常工作:
1#include <iostream> 2#include <memory> 3 4const std::unique_ptr<int> f() { 5 std::unique_ptr<int> p = std::make_unique<int>(42); 6 return p; 7} 8 9int main() { 10 auto p = f(); 11}
两段代码的区别在于:第二段中 p 并未声明为 const。
std::unique_ptr<const T>
此时,指针所指向的数据是 const,但指针本身是可变的。也就是说,我们不能修改所指向的数据,但可以改变指针的指向。
1#include <iostream> 2#include <memory> 3 4int main() { 5 std::unique_ptr<const int> p = std::make_unique<const int>(42); 6 // ++(*p); // 错误,数据是 const 7 p.reset(new int{51}); // 正确,指针不是 const 8 std::cout << "Successful\n"; 9}
在表达式 std::make_unique<const int>(42) 中,const 并非强制要求,即使省略也不会导致编译失败。但从性能角度考虑,最好保留 const。如果在编译器探索器中检查,省略 const 会导致额外调用一次移动构造函数,并需要析构临时对象:
1mov DWORD PTR [rbp-20], 42 2lea rax, [rbp-32] 3lea rdx, [rbp-20] 4mov rsi, rdx 5mov rdi, rax 6call std::_MakeUniq<int>::__single_object std::make_unique<int, int>(int&&) 7lea rdx, [rbp-32] 8lea rax, [rbp-40] 9mov rsi, rdx 10mov rdi, rax 11call std::unique_ptr<int const, std::default_delete<int const> >::unique_ptr<int, std::default_delete<int>, void>(std::unique_ptr<int, std::default_delete<int> >&&) 12lea rax, [rbp-32] 13mov rdi, rax 14call std::unique_ptr<int, std::default_delete<int> >::~unique_ptr() [complete object destructor]
如果我们正确地在 std::make_unique 中保留了 const,上述代码将简化为:
1mov DWORD PTR [rbp-36], 42 2lea rax, [rbp-48] 3lea rdx, [rbp-36] 4mov rsi, rdx 5mov rdi, rax 6call std::_MakeUniq<int const>::__single_object std::make_unique<int const, int>(int&&)
总结:若希望获得一个 const 智能指针,应在左侧和右侧都加上 const,前提是使用 std::make_* 函数。通常更推荐使用 auto。
const std::unique_ptr<const T>
这是前两种情况的结合:指针本身和其所指向的数据均为 const,因此不允许任何修改。
1#include <iostream> 2#include <memory> 3 4int main() { 5 const std::unique_ptr<const int> p = std::make_unique<const int>(42); 6 // ++(*p); // 错误,数据是 const 7 // p.reset(new int{51}); // 错误,指针是 const 8}
请务必在两侧都加上 const!