跳到主要内容

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 的方式:

cpp
std::unique_ptr<int> ptr(new int(10));

这种方式虽然可行,但有以下几个问题:

  1. 异常安全问题:在复杂表达式中,可能导致内存泄漏
  2. 代码重复:类型需要写两次
  3. 可读性:没有 make_unique 清晰易读

make_unique 解决了这些问题,让代码更加简洁和安全。

基本用法

使用 make_unique 创建智能指针的基本语法如下:

cpp
#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 中。一个简化的实现可能如下:

cpp
template<typename T, typename... Args>
std::unique_ptr<T> make_unique(Args&&... args) {
return std::unique_ptr<T>(new T(std::forward<Args>(args)...));
}

这个实现利用了可变参数模板和完美转发,允许你传递任意数量和类型的参数来初始化对象。

创建不同类型的对象

创建基本类型

cpp
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);

创建自定义类型

cpp
#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 也支持创建动态数组:

cpp
// 创建包含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 的一个主要优点是它提供了更好的异常安全性。考虑以下代码:

cpp
// 可能存在内存泄漏的代码
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 解决这个问题:

cpp
// 安全的代码
riskyFunction(std::make_unique<MyClass>(), std::make_unique<MyClass>());

这样即使发生异常,也不会有内存泄漏。

在C++11中实现 make_unique

如果你使用的是C++11而不是C++14或更高版本,可以自己实现一个简单的 make_unique 函数:

cpp
#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: 资源管理

当你需要动态分配内存并确保它在不再需要时被正确释放时:

cpp
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: 工厂函数

工厂模式中返回继承层次结构中的对象:

cpp
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: 避免循环引用

在对象关系中管理生命周期,避免循环引用:

cpp
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_uniqueunique_ptr(new T(...))
代码简洁度更简洁较冗长
类型重复无需重复需要重复写类型
异常安全性更安全在某些情况下可能不安全
自定义删除器不支持支持
C++版本要求C++14及以上(或自定义实现)C++11及以上

总结

std::make_unique 是C++14引入的一个重要工具,它为创建 unique_ptr 智能指针提供了更安全、更简洁的方式。使用 make_unique 的主要优势包括:

  1. 异常安全:避免可能的内存泄漏
  2. 代码简洁:不需要重复写类型名
  3. 语义清晰:明确表示意图是创建一个被管理的资源

在现代C++编程中,应该优先使用 make_unique 而不是直接调用 new,这符合资源获取即初始化(RAII)的原则,也是安全管理内存的良好实践。

练习

  1. 创建一个函数,使用 make_unique 创建一个包含10个随机数的数组,然后返回这个数组的平均值。

  2. 实现一个简单的文件处理类,使用 unique_ptr 管理文件资源,并使用 make_unique 创建该类的实例。

  3. 设计一个简单的游戏角色类层次结构,包含基类 Character 和派生类 WarriorMage。实现一个工厂函数,使用 make_unique 根据输入字符串创建相应的角色。

进一步阅读

记住,有效使用智能指针是现代C++编程中内存管理的关键部分,而 make_unique 是这一实践的重要工具。