【C++】unique_ptr
作为函数入参的使用方法
1. 各种情况举例
1.1 作为值传递,实参为左值(错误)
void func(std::unique_ptr<int> in_ptr) { // 值传递
*in_ptr = 20;
}
int main() {
std::unique_ptr<int> ptr(new int(10));
func(ptr); // 通过左值传递
if (!ptr)
std::cout << "ptr is nullptr" << std::endl;
else
std::cout << "*ptr = " << *ptr << std::endl;
return 0;
}
// 编译出错
std::unique_ptr
不可复制,只能移动。func
通过值传递接收一个std::unique_ptr
,会尝试使用拷贝构造函数,但std::unique_ptr
的拷贝构造函数是被删除的,所以会导致编译错误。
所以需要使用std::move
将ptr
转换为右值引用,从而触发std::unqiue_ptr
的移动构造函数。
1.2 作为值传递,实参为右值引用
void func(std::unique_ptr<int> in_ptr) { // 值传递
*in_ptr = 20;
}
int main() {
std::unique_ptr<int> ptr(new int(10));
func(std::move(ptr); // 右值引用
if (!ptr)
std::cout << "ptr is nullptr" << std::endl;
else
std::cout << "*ptr = " << *ptr << std::endl;
return 0;
}
// 终端输出 ptr is nullptr
函数调用时,会用传进来的实参ptr
构造参数in_ptr
的副本,因为std::uniptr_ptr
不可复制,只能移动,所以调用std::unique_ptr
的移动构造函数,将
所有权从实参ptr
转移给了临时副本in_ptr
,只要函数返回,资源的生命周期就结束了,即使func
中存在auto local_ptr = std::move(in_ptr)
,因为将所有权转移给了local_ptr
局部变量,在函数返回时,local_ptr
会被销毁,指向的资源会被释放。
1.3 作为右值引用传递,实参为右值引用
void func(std::unique_ptr<int> &&in_ptr) {
*in_ptr = 20;
}
int main() {
std::unique_ptr<int> ptr(new int(10));
func(std::move(ptr)); // 右值引用
if (!ptr)
std::cout << "ptr is nullptr" << std::endl;
else
std::cout << "*ptr = " << *ptr << std::endl;
return 0;
}
// 终端输出 *ptr = 20
右值引用不会触发std::unique_ptr
的移动构造函数或移动赋值函数,不会转移ptr
的所有权,可以认为in_ptr
只是ptr
的一个别名。只要在func
中没有转移所有权,func
返回后,ptr
仍然拥有资源的所有权。
但是如果func
中有资源所有权的转移,则ptr
会失去所有权。
void func(std::unique_ptr<int> &&in_ptr) {
*in_ptr = 20;
auto local_ptr = std::move(in_ptr); // 移动构造
}
int main() {
std::unique_ptr<int> ptr(new int(10));
func(std::move(ptr)); // 右值引用
if (!ptr)
std::cout << "ptr is nullptr" << std::endl;
else
std::cout << "*ptr = " << *ptr << std::endl;
return 0;
}
// 终端输出 ptr is nullptr
因为在auto local_ptr = std::move(in_ptr)
中,会触发unique_ptr
的移动构造函数,所有权会被转移。当func
返回后,local_ptr
生命周期结束,所有用的资源会被释放。
1.4 作为引用传递,实参为左值
void func(std::unique_ptr<int> &in_ptr) {
*in_ptr = 20;
}
int main() {
std::unique_ptr<int> ptr(new int(10));
func(ptr); //左值
if (!ptr)
std::cout << "ptr is nullptr" << std::endl;
else
std::cout << "*ptr = " << *ptr << std::endl;
return 0;
}
// 终端输出 *ptr = 20
ptr
本身被传递,而不是它的副本,所以参数传递的时候不会触发std::unique_ptr
的移动构造。只要在func
中没有转移所有权,func
返回后,ptr
仍然拥有资源的所有权。
但是如果func
中有资源所有权的转移,则ptr
会失去所有权。
void func(std::unique_ptr<int> &in_ptr) {
*in_ptr = 20;
auto local_ptr = std::move(in_ptr); // 移动构造
}
int main() {
std::unique_ptr<int> ptr(new int(10));
func(ptr); //左值
if (!ptr)
std::cout << "ptr is nullptr" << std::endl;
else
std::cout << "*ptr = " << *ptr << std::endl;
return 0;
}
// 终端输出 ptr is nullptr
因为在auto local_ptr = std::move(in_ptr)
中,会触发unique_ptr
的移动构造函数,所有权会被转移。当func
返回后,local_ptr
生命周期结束,所拥有的资源会被释放。
1.5 作为传递指针
void func(int *in_ptr) {
*in_ptr = 20;
}
int main() {
std::unique_ptr<int> ptr(new int(10));
func(ptr.get()); // 原始指针
if (!ptr)
std::cout << "ptr is nullptr" << std::endl;
else
std::cout << "*ptr = " << *ptr << std::endl;
return 0;
}
// 终端输出 *ptr = 20
使用原始指针可以访问ptr
管理的内存,并且不会影响ptr
的所有权。但这种方式存在潜在的危险,违反智能指针设计初衷。
2. 有const
修饰的情况
2.1 const
左值引用
const
修饰的是引用本身,即in_ptr
本身不能被修改,但指向的内容能否修改需要看in_ptr
指向的资源是否是const
的。
void func(const std::unique_ptr<int> &in_ptr) { // in_ptr本身不能修改,但指向的资源可以修改
*in_ptr = 20;
}
int main() {
std::unique_ptr<int> ptr(new int(10));
func(ptr); //左值
if (!ptr)
std::cout << "ptr is nullptr" << std::endl;
else
std::cout << "*ptr = " << *ptr << std::endl;
return 0;
}
// 终端输出 *ptr = 20
void func(const std::unique_ptr<const int> &in_ptr) { // in_ptr本身不能修改,并且指向的资源也不可以修改
*in_ptr = 20; // ❌ 错误: 指向的内容不允许修改
}
int main() {
std::unique_ptr<const int> ptr(new int(10));
func(ptr); //左值
if (!ptr)
std::cout << "ptr is nullptr" << std::endl;
else
std::cout << "*ptr = " << *ptr << std::endl;
return 0;
}
// 编译错误
在C++中,任何值可以绑定到const
左值引用,因为const
保证了不会修改原来对象。
void func(const std::unique_ptr<int> &in_ptr) { // in_ptr本身不能修改,但指向的资源可以修改
*in_ptr = 20;
}
int main() {
std::unique_ptr<int> ptr(new int(10));
func(std::move(ptr)); // 右值引用
if (!ptr)
std::cout << "ptr is nullptr" << std::endl;
else
std::cout << "*ptr = " << *ptr << std::endl;
return 0;
}
// 终端输出 *ptr = 20
在这里,std::move
其实有点多余,因为并不会触发移动构造,而且容易误解为意图转移所有权。
2.2 const
右值引用
与const
左值引用相同, const
修饰的是引用本身,即in_ptr
本身不能被修改,但指向的内容能否修改需要看in_ptr
指向的资源是否是const
的。
void func(const std::unique_ptr<int> &&in_ptr) { // in_ptr本身不能修改,但指向的资源可以修改
*in_ptr = 20;
}
int main() {
std::unique_ptr<int> ptr(new int(10));
func(std::move(ptr)); //右值引用
if (!ptr)
std::cout << "ptr is nullptr" << std::endl;
else
std::cout << "*ptr = " << *ptr << std::endl;
return 0;
}
// 终端输出 *ptr = 20
void func(const std::unique_ptr<const int> &&in_ptr) { // in_ptr本身不能修改,指向的资源也不可以修改
*in_ptr = 20; // ❌ 错误: 指向的内容不允许修改
}
int main() {
std::unique_ptr<const int> ptr(new int(10));
func(std::move(ptr)); //右值引用
if (!ptr)
std::cout << "ptr is nullptr" << std::endl;
else
std::cout << "*ptr = " << *ptr << std::endl;
return 0;
}
// 编译错误
不推荐使用,没有任何意义。使用右值引用主要意义是用来支持移动语义,当使用右值引用时意图是可能需要转移对象的所有权了,但加了const
就不允许修改对象本身了,这就造成了矛盾。
3 总结
- 入参形式为左值引用或右值引用,从结果表现上来看,完全一致。但一般:
- 当需要直接操作
std::unique_ptr
对象,但不改变其所有权的情况下,函数入参使用左值引用,建议使用const std::unique_ptr<T> &
类型作为函数入参; - 当可能会改变
std::unique_ptr
对象的所有权的情况下,对象在函数调用后可能会变成nullptr
,函数入参使用右值引用,建议使用std::unique_ptr<T> &&
类型作为函数入参。【注意:转移所有权后,原对象相当于一个空壳,可以重新赋值,但不能再使用它。】 【不止针对std::unique_ptr
,其他类型同样是这样的,但对应没有资源转移的场景,右值引用没有实际意义。】
- 是否转移了资源的所有权,只需要抓住一个点:是否触发了
std::unique_ptr
的移动构造或移动赋值。
std::move
只是生成一个右值表达式,其类型是右值引用类型(结果是临时的),从而允许资源被转移,和资源转移没有实质上的关系。
比如:std::move(val)
本身是不会影响val
的,真正转移val
的资源所有权的是移动构造或移动赋值操作。
std::string a = "hello";
std::string b = std::move(a); // 触发移动构造,a 的资源转移给了 b
// a 本身还是 std::string类型,仍然存在,但资源被转移了
// std::move(a) 是 std::string &&类型
std::sting a = "hello";
std::string &&b = std::move(a); // 右值引用变量 b 的初始化绑定,不触发移动构造
// a 本身还是 std::string类型,仍然存在,资源还没有被转移
// std::move(a) 是 std::string &&类型
- 【混淆点】左值和左值引用,右值和右值引用,引用(引用变量)和引用类型
左值和右值是值的分类,与值的类型没有关系。
广义上的引用包括左值引用和右值引用,指的是具体的变量别名,所以引用(不管是左值引用,还是右值引用)都是左值。引用本身不是一个对象,也不占用独立的内存。
比如std::unique_ptr<int> &&ptr2 = std::move(ptr1)
,这里ptr2
是一个右值引用,但本身是一个左值。
在 C++ 中,引用(包括右值引用)必须在初始化时就绑定一个对象,不能像普通变量那样先声明,再定义。引用一旦绑定,永远不能重新绑定到别的对象上。
引用类型是一种变量类型,比如int&
、std::unique_ptr<T> &&
等,可以类比于int
、double
变量类型。
右值只能在右值引用变量初始化时绑定到右值引用变量。
std::string&& r = std::string("hello"); // ✅ 这是定义并初始化 r,用右值绑定
// ===========================================================================
std::string&& r; // ❌ 只定义,未初始化(非法,其实不能这么写)
r = std::string("world"); // ❌ 错误:不是初始化,不能用右值赋值给右值引用本身
右值引用变量之间可以移动赋值,这不是重新绑定右值,而是对引用的对象进行赋值,绑定的对象是不变的,只是对象的内容发生了变化。
std::string&& a = std::string("hello");
std::string&& b = std::string("world");
a = std::move(b); // 移动赋值 ✅ 合法,但注意:这是把 b 所引用的对象移动赋值给 a 所引用的对象,a所引用的对象本身(那块内存)位置是不变的
a = b; // 拷贝赋值 ✅ 合法,但注意:这是把 b 所引用的对象移动赋值给 a 所引用的对象,a所引用的对象本身(那块内存)位置是不变的
- 【混淆点】绑定到和绑定
int a = 2;
int &b = a;
✅【推荐说法】a
绑定到(bind to)b
❌【不推荐说法】b
绑定a