C++ make_unique
什么是 make_unique?
std::make_unique
是C++14标准中引入的一个函数模板,用于创建和初始化 std::unique_ptr
智能指针。虽然 unique_ptr
在C++11中已经存在,但 make_unique
直到C++14才被添加到标准库中。它提供了一种更安全、更简洁的方式来创建 unique_ptr
对象。
尽管 make_unique
是C++14才正式引入的,但你可以在C++11中自己实现一个简单版本,我们后面会看到如何做。
为什么需要 make_unique?
在介绍 make_unique
之前,让我们先回顾一下传统创建 unique_ptr
的方式:
std::unique_ptr<int> ptr(new int(10));
这种方式虽然可行,但有以下几个问题:
- 异常安全问题:在复杂表达式中,可能导致内存泄漏
- 代码重复:类型需要写两次
- 可读性:没有
make_unique
清晰易读
make_unique
解决了这些问题,让代码更加简洁和安全。
基本用法
使用 make_unique
创建智能指针的基本语法如下:
#include <iostream>
#include <memory>
int main() {
// 创建指向整数的unique_ptr
std::unique_ptr<int> ptr = std::make_unique<int>(42);
// 使用指针
std::cout << "值: " << *ptr << std::endl;
// 修改指针指向的值
*ptr = 100;
std::cout << "修改后的值: " << *ptr << std::endl;
return 0;
}
输出:
值: 42
修改后的值: 100
make_unique 的工作原理
make_unique
内部实现非常直观。它使用传递给它的参数,通过 new
操作符创建对象,并将其包装在 unique_ptr
中。一个简化的实现可能如下:
template<typename T, typename... Args>
std::unique_ptr<T> make_unique(Args&&... args) {
return std::unique_ptr<T>(new T(std::forward<Args>(args)...));
}
这个实现利用了可变参数模板和完美转发,允许你传递任意数量和类型的参数来初始化对象。
创建不同类型的对象
创建基本类型
auto int_ptr = std::make_unique<int>(42);
auto double_ptr = std::make_unique<double>(3.14);
auto bool_ptr = std::make_unique<bool>(true);
创建自定义类型
#include <iostream>
#include <memory>
#include <string>
class Person {
private:
std::string name;
int age;
public:
Person(std::string n, int a) : name(std::move(n)), age(a) {
std::cout << "Person 构造函数被调用" << std::endl;
}
~Person() {
std::cout << "Person 析构函数被调用" << std::endl;
}
void introduce() const {
std::cout << "我是 " << name << ",今年 " << age << " 岁。" << std::endl;
}
};
int main() {
// 创建Person对象
auto person = std::make_unique<Person>("张三", 30);
// 使用对象
person->introduce();
// unique_ptr离开作用域时,自动删除Person对象
return 0;
}
输出:
Person 构造函数被调用
我是 张三,今年 30 岁。
Person 析构函数被调用
创建数组
从C++14开始,make_unique
也支持创建动态数组:
// 创建包含10个整数的数组
auto array_ptr = std::make_unique<int[]>(10);
// 初始化数组元素
for (int i = 0; i < 10; ++i) {
array_ptr[i] = i * i;
}
// 访问数组元素
for (int i = 0; i < 10; ++i) {
std::cout << "array_ptr[" << i << "] = " << array_ptr[i] << std::endl;
}
创建数组时,不能在 make_unique
中初始化数组元素。你需要在创建后单独初始化它们。
异常安全性
make_unique
的一个主要优点是它提供了更好的异常安全性。考虑以下代码:
// 可能存在内存泄漏的代码
void riskyFunction(std::unique_ptr<MyClass> a, std::unique_ptr<MyClass> b) {
// 函数体...
}
// 调用方式 - 潜在问题
riskyFunction(std::unique_ptr<MyClass>(new MyClass()),
std::unique_ptr<MyClass>(new MyClass()));
在上面的例子中,如果第二个 new MyClass()
抛出异常,第一个已经分配的内存可能会泄漏,因为编译器没有保证参数的求值顺序。
使用 make_unique
解决这个问题:
// 安全的代码
riskyFunction(std::make_unique<MyClass>(), std::make_unique<MyClass>());
这样即使发生异常,也不会有内存泄漏。
在C++11中实现 make_unique
如果你使用的是C++11而不是C++14或更高版本,可以自己实现一个简单的 make_unique
函数:
#include <memory>
#include <utility>
#if __cplusplus == 201103L // C++11
namespace std {
template<typename T, typename... Args>
std::unique_ptr<T> make_unique(Args&&... args) {
return std::unique_ptr<T>(new T(std::forward<Args>(args)...));
}
}
#endif
实际应用场景
场景1: 资源管理
当你需要动态分配内存并确保它在不再需要时被正确释放时:
void processFile(const std::string& filename) {
auto file = std::make_unique<std::ifstream>(filename);
if (!file->is_open()) {
std::cerr << "无法打开文件: " << filename << std::endl;
return;
}
// 处理文件...
// 不需要手动关闭文件,unique_ptr会自动处理
}
场景2: 工厂函数
工厂模式中返回继承层次结构中的对象:
class Animal {
public:
virtual ~Animal() = default;
virtual void makeSound() const = 0;
};
class Dog : public Animal {
public:
void makeSound() const override {
std::cout << "汪汪!" << std::endl;
}
};
class Cat : public Animal {
public:
void makeSound() const override {
std::cout << "喵喵!" << std::endl;
}
};
std::unique_ptr<Animal> createAnimal(const std::string& type) {
if (type == "dog") {
return std::make_unique<Dog>();
} else if (type == "cat") {
return std::make_unique<Cat>();
}
return nullptr;
}
int main() {
auto animal = createAnimal("dog");
animal->makeSound(); // 输出: 汪汪!
}
场景3: 避免循环引用
在对象关系中管理生命周期,避免循环引用:
class Child;
class Parent {
public:
void setChild(std::unique_ptr<Child> c) {
child = std::move(c);
}
private:
std::unique_ptr<Child> child;
};
class Child {
public:
void setParent(Parent* p) {
parent = p; // 使用裸指针或weak_ptr避免循环引用
}
private:
Parent* parent; // 不拥有所有权
};
int main() {
auto parent = std::make_unique<Parent>();
auto child = std::make_unique<Child>();
child->setParent(parent.get());
parent->setChild(std::move(child));
}
make_unique vs 直接使用 unique_ptr
让我们比较使用 make_unique
和直接使用 unique_ptr
构造函数的区别:
特性 | make_unique | unique_ptr(new T(...)) |
---|---|---|
代码简洁度 | 更简洁 | 较冗长 |
类型重复 | 无需重复 | 需要重复写类型 |
异常安全性 | 更安全 | 在某些情况下可能不安全 |
自定义删除器 | 不支持 | 支持 |
C++版本要求 | C++14及以上(或自定义实现) | C++11及以上 |
总结
std::make_unique
是C++14引入的一个重要工具,它为创建 unique_ptr
智能指针提供了更安全、更简洁的方式。使用 make_unique
的主要优势包括:
- 异常安全:避免可能的内存泄漏
- 代码简洁:不需要重复写类型名
- 语义清晰:明确表示意图是创建一个被管理的资源
在现代C++编程中,应该优先使用 make_unique
而不是直接调用 new
,这符合资源获取即初始化(RAII)的原则,也是安全管理内存的良好实践。
练习
-
创建一个函数,使用
make_unique
创建一个包含10个随机数的数组,然后返回这个数组的平均值。 -
实现一个简单的文件处理类,使用
unique_ptr
管理文件资源,并使用make_unique
创建该类的实例。 -
设计一个简单的游戏角色类层次结构,包含基类
Character
和派生类Warrior
、Mage
。实现一个工厂函数,使用make_unique
根据输入字符串创建相应的角色。
进一步阅读
- C++ 参考手册 - std::make_unique
- Effective Modern C++ by Scott Meyers
- C++ Core Guidelines
记住,有效使用智能指针是现代C++编程中内存管理的关键部分,而 make_unique
是这一实践的重要工具。