22-5 std::unique_ptr

在本章开头,我们讨论了在某些情况下使用指针可能导致错误和内存泄漏的问题。例如,当函数提前返回或抛出异常时,若未正确删除指针,就可能引发此类问题。

#include <iostream>

void someFunction()
{
    auto* ptr{ new Resource() };

    int x{};
    std::cout << "Enter an integer: ";
    std::cin >> x;

    if (x == 0)
        throw 0; // the function returns early, and ptr won’t be deleted!

    // do stuff with ptr here

    delete ptr;
}

既然我们已经介绍了移动语义的基础知识,现在可以回到智能指针类的话题。尽管智能指针还具备其他特性,但其核心特征在于:它管理由智能指针使用者提供的动态分配资源,并确保在适当时间(通常是智能指针作用域结束时)正确清理动态分配的对象。

正因如此,智能指针本身绝不应被动态分配(否则可能导致智能指针自身未被正确释放,进而使其拥有的对象无法被释放,引发内存泄漏)。通过始终将智能指针分配在栈上(作为局部变量或类的组合成员),我们能确保当包含智能指针的函数或对象结束时,智能指针会正确地退出作用域,从而保证其拥有的对象被正确释放。

C++11标准库提供了4种智能指针类:std::auto_ptr(已于C++17移除)、std::unique_ptr、std::shared_ptr和std::weak_ptr。其中std::unique_ptr是目前最常用的智能指针类,我们将优先讲解该类。后续课程将分别讲解 std::shared_ptr 和 std::weak_ptr。


std::unique_ptr

std::unique_ptr 是 C++11 中替代 std::auto_ptr 的实现。它应用于管理任何未被多个对象共享的动态分配对象。也就是说,std::unique_ptr 应完全拥有其所管理的对象,不与其他类共享该所有权。std::unique_ptr 定义在 头文件中。

下面来看一个简单的智能指针示例:

#include <iostream>
#include <memory> // for std::unique_ptr

class Resource
{
public:
	Resource() { std::cout << "Resource acquired\n"; }
	~Resource() { std::cout << "Resource destroyed\n"; }
};

int main()
{
	// allocate a Resource object and have it owned by std::unique_ptr
	std::unique_ptr<Resource> res{ new Resource() };

	return 0;
} // res goes out of scope here, and the allocated Resource is destroyed

image

由于此处的 std::unique_ptr 在栈上分配,因此它最终必定会脱离作用域,而当作用域结束时,它将删除其所管理的 Resource。

与 std::auto_ptr 不同,std::unique_ptr 正确实现了移动语义。

#include <iostream>
#include <memory> // for std::unique_ptr
#include <utility> // for std::move

class Resource
{
public:
	Resource() { std::cout << "Resource acquired\n"; }
	~Resource() { std::cout << "Resource destroyed\n"; }
};

int main()
{
	std::unique_ptr<Resource> res1{ new Resource{} }; // Resource created here
	std::unique_ptr<Resource> res2{}; // Start as nullptr

	std::cout << "res1 is " << (res1 ? "not null\n" : "null\n");
	std::cout << "res2 is " << (res2 ? "not null\n" : "null\n");

	// res2 = res1; // Won't compile: copy assignment is disabled
	res2 = std::move(res1); // res2 assumes ownership, res1 is set to null

	std::cout << "Ownership transferred\n";

	std::cout << "res1 is " << (res1 ? "not null\n" : "null\n");
	std::cout << "res2 is " << (res2 ? "not null\n" : "null\n");

	return 0;
} // Resource destroyed here when res2 goes out of scope

这将输出:

image

由于 std::unique_ptr 是基于移动语义设计的,因此禁止使用复制初始化和复制赋值。若需转移由 std::unique_ptr 管理的内容,必须采用移动语义。在上面的程序中,我们通过 std::move 实现此目的(该操作将 res1 转换为右值,从而触发移动赋值而非复制赋值)。


访问受管对象

std::unique_ptr 重载了 operator* 运算符和 operator-> 运算符,可用于返回其管理的资源。operator* 运算符返回受管资源的引用,operator-> 运算符返回指针。

请注意,std::unique_ptr 可能并非始终管理着对象——这可能是因为它被空创建(使用默认构造函数或传入 nullptr 作为参数),或是因为其管理的资源已被移至另一个 std::unique_ptr。因此在使用这些运算符前,我们应先检查 std::unique_ptr 是否实际持有资源。所幸验证方式很简单:std::unique_ptr 提供了一个转换为 bool 的方法,若该指针正在管理资源则返回 true。

示例如下:

#include <iostream>
#include <memory> // for std::unique_ptr

class Resource
{
public:
	Resource() { std::cout << "Resource acquired\n"; }
	~Resource() { std::cout << "Resource destroyed\n"; }
};

std::ostream& operator<<(std::ostream& out, const Resource&)
{
	out << "I am a resource";
	return out;
}

int main()
{
	std::unique_ptr<Resource> res{ new Resource{} };

	if (res) // use implicit cast to bool to ensure res contains a Resource
		std::cout << *res << '\n'; // print the Resource that res is owning

	return 0;
}

这将输出:

image

在上面的程序中,我们使用重载的operator*运算符获取由std::unique_ptr res拥有的Resource对象,然后将其发送给std::cout进行打印。


std::unique_ptr 与数组

与 std::auto_ptr 不同,std::unique_ptr 足够智能,能够判断使用标量删除还是数组删除,因此它既适用于标量对象,也适用于数组。

然而,与使用 std::unique_ptr 管理固定数组、动态数组或 C 风格字符串相比,std::array 或 std::vector(或 std::string)几乎总是更优的选择。

最佳实践:
优先选择 std::array、std::vector 或 std::string,而非使用智能指针管理固定数组、动态数组或 C 风格字符串。


std::make_unique

C++14新增了一个名为std::make_unique()的函数。这个模板函数构造模板类型的对象,并使用传递给函数的参数对其进行初始化。

#include <memory> // for std::unique_ptr and std::make_unique
#include <iostream>

class Fraction
{
private:
	int m_numerator{ 0 };
	int m_denominator{ 1 };

public:
	Fraction(int numerator = 0, int denominator = 1) :
		m_numerator{ numerator }, m_denominator{ denominator }
	{
	}

	friend std::ostream& operator<<(std::ostream& out, const Fraction &f1)
	{
		out << f1.m_numerator << '/' << f1.m_denominator;
		return out;
	}
};


int main()
{
	// Create a single dynamically allocated Fraction with numerator 3 and denominator 5
	// We can also use automatic type deduction to good effect here
	auto f1{ std::make_unique<Fraction>(3, 5) };
	std::cout << *f1 << '\n';

	// Create a dynamically allocated array of Fractions of length 4
	auto f2{ std::make_unique<Fraction[]>(4) };
	std::cout << f2[0] << '\n';

	return 0;
}

上述代码输出:

image

使用 std::make_unique() 是可选的,但相较于自行创建 std::unique_ptr,更推荐采用此方法。这是因为使用 std::make_unique 的代码更简洁,且在配合自动类型推导时能减少编写量。此外,在 C++14 中,它解决了因 C++ 未明确规定函数参数的求值顺序而可能引发的异常安全问题。

最佳实践:
使用 std::make_unique() 替代手动创建 std::unique_ptr 并调用 new


关于异常安全问题的详细说明

对于那些好奇上述“异常安全问题”究竟是什么的人,以下是对该问题的描述。

考虑如下表达式:

some_function(std::unique_ptr<T>(new T), function_that_can_throw_exception());

编译器在处理此调用时拥有很大灵活性。它可能先创建新的 T 对象,接着调用可能抛出异常的函数 function_that_can_throw_exception(),最后创建管理动态分配 T 的 std::unique_ptr。若 function_that_can_throw_exception() 抛出异常,则已分配的 T 对象将不会被释放——因为负责释放的智能指针尚未创建。这将导致 T 对象内存泄漏。

而 std::make_unique() 不会出现此问题,因为对象 T 的创建与 std::unique_ptr 的创建都在 std::make_unique() 函数内部完成,执行顺序毫无歧义。

该问题已在 C++17 中修复,因为函数参数的求值不再允许交错执行。


从函数中返回 std::unique_ptr

std::unique_ptr 可以通过值安全地从函数中返回:

#include <memory> // for std::unique_ptr

std::unique_ptr<Resource> createResource()
{
     return std::make_unique<Resource>();
}

int main()
{
    auto ptr{ createResource() };

    // do whatever

    return 0;
}

在上面的代码中,createResource() 以值的方式返回一个 std::unique_ptr。如果该值未被赋值给任何对象,则临时返回值将超出作用域,资源将被清理。若进行赋值(如 main() 所示),在 C++14 及更早版本中,将通过移动语义将资源从返回值转移至被赋值对象(此例中为 ptr);而在 C++17 及更新版本中,返回操作将被省略。这使得通过 std::unique_ptr 返回资源比返回原始指针安全得多!

通常情况下,您不应通过指针(任何形式)或引用(除非存在特定且充分的理由)返回 std::unique_ptr。


将 std::unique_ptr 传递给函数

若需函数接管指针所指向内容的所有权,请按值传递 std::unique_ptr。请注意,由于复制语义已被禁用,您需要使用 std::move 实际传递该变量。

#include <iostream>
#include <memory> // for std::unique_ptr
#include <utility> // for std::move

class Resource
{
public:
	Resource() { std::cout << "Resource acquired\n"; }
	~Resource() { std::cout << "Resource destroyed\n"; }
};

std::ostream& operator<<(std::ostream& out, const Resource&)
{
	out << "I am a resource";
	return out;
}

// This function takes ownership of the Resource, which isn't what we want
void takeOwnership(std::unique_ptr<Resource> res)
{
     if (res)
          std::cout << *res << '\n';
} // the Resource is destroyed here

int main()
{
    auto ptr{ std::make_unique<Resource>() };

//    takeOwnership(ptr); // This doesn't work, need to use move semantics
    takeOwnership(std::move(ptr)); // ok: use move semantics

    std::cout << "Ending program\n";

    return 0;
}

上述程序输出:

image

请注意,在此情况下,资源的所有权已转移给 takeOwnership(),因此该资源在 takeOwnership() 结束时被销毁,而非在 main() 结束时。

然而,大多数情况下,您并不希望函数获取资源的所有权。

虽然可以使用const引用传递std::unique_ptr(允许函数在不获取所有权的情况下使用对象),但更优雅的做法是直接传递资源本身(通过指针或引用,取决于空值是否为有效参数)。这使函数能够保持对调用方资源管理方式的中立性。

若需从 std::unique_ptr 获取原始指针,可使用 get() 成员函数:

#include <memory> // for std::unique_ptr
#include <iostream>

class Resource
{
public:
	Resource() { std::cout << "Resource acquired\n"; }
	~Resource() { std::cout << "Resource destroyed\n"; }
};

std::ostream& operator<<(std::ostream& out, const Resource&)
{
	out << "I am a resource";
	return out;
}

// The function only uses the resource, so we'll accept a pointer to the resource, not a reference to the whole std::unique_ptr<Resource>
void useResource(const Resource* res)
{
	if (res)
		std::cout << *res << '\n';
	else
		std::cout << "No resource\n";
}

int main()
{
	auto ptr{ std::make_unique<Resource>() };

	useResource(ptr.get()); // note: get() used here to get a pointer to the Resource

	std::cout << "Ending program\n";

	return 0;
} // The Resource is destroyed here

上述程序输出:

image


std::unique_ptr 与类

当然,你可以将 std::unique_ptr 用作类中的组合成员。这样,你无需担心确保类析构函数删除动态内存,因为当类对象被销毁时,std::unique_ptr 会自动被销毁。

然而,若类对象未被正确销毁(例如动态分配后未正确释放),则其成员 std::unique_ptr 也不会被销毁,从而导致由 std::unique_ptr 管理的主体内存无法释放。


误用 std::unique_ptr

误用 std::unique_ptr 有两种常见方式,但都可轻易避免。首先,不要让多个对象管理同一资源。例如:

Resource* res{ new Resource() };
std::unique_ptr<Resource> res1{ res };
std::unique_ptr<Resource> res2{ res };

虽然从语法上讲这是合法的,但最终结果将是 res1 和 res2 都试图删除该资源,从而导致未定义行为。

其次,不要手动从 std::unique_ptr 下删除资源。

Resource* res{ new Resource() };
std::unique_ptr<Resource> res1{ res };
delete res;

若如此操作,std::unique_ptr 将尝试删除已被删除的资源,再次导致未定义行为。

请注意,std::make_unique() 可防止上述两种情况意外发生。


测验时间

问题 #1

将以下程序中使用普通指针的部分转换为在适当位置使用 std::unique_ptr:

#include <iostream>

class Fraction
{
private:
	int m_numerator{ 0 };
	int m_denominator{ 1 };

public:
	Fraction(int numerator = 0, int denominator = 1) :
		m_numerator{ numerator }, m_denominator{ denominator }
	{
	}

	friend std::ostream& operator<<(std::ostream& out, const Fraction &f1)
	{
		out << f1.m_numerator << '/' << f1.m_denominator;
		return out;
	}
};

void printFraction(const Fraction* ptr)
{
	if (ptr)
		std::cout << *ptr << '\n';
	else
		std::cout << "No fraction\n";
}

int main()
{
	auto* ptr{ new Fraction{ 3, 5 } };

	printFraction(ptr);

	delete ptr;

	return 0;
}

显示解决方案

#include <memory> // for std::unique_ptr
#include <iostream>

class Fraction
{
private:
	int m_numerator{ 0 };
	int m_denominator{ 1 };

public:
	Fraction(int numerator = 0, int denominator = 1) :
		m_numerator{ numerator }, m_denominator{ denominator }
	{
	}

	friend std::ostream& operator<<(std::ostream& out, const Fraction &f1)
	{
		out << f1.m_numerator << '/' << f1.m_denominator;
		return out;
	}
};

// This function uses a Fraction object, so we just pass the Fraction itself
// That way we don't have to worry about what kind of smart pointer (if any) the caller may be using
void printFraction(const Fraction* ptr)
{
	if (ptr)
		std::cout << *ptr << '\n';
	else
		std::cout << "No fraction\n";
}

int main()
{
	auto ptr{ std::make_unique<Fraction>(3, 5) };

	printFraction(ptr.get());

	return 0;
}

image

posted @ 2026-01-28 10:42  游翔  阅读(1)  评论(0)    收藏  举报