智能指针 std::unique_ptr 的定义和应用

本文最后更新于:2024年5月21日 中午

Unique poiter 和原始指针一样快, 而且她不能复制, 只能转移所有权 (move-only).

unique_ptr 定义和使用

unique_ptr 的创建有两种方法

#include <stdio.h>
#include <iostream>
#include <memory>
#include <string.h>

struct Person {
    std::string Name;
    Person(std::string name) : Name(name){};
};

int main() {
    // unique_ptr 创建方法 1
    std::unique_ptr<Person> person1(new Person("Jason"));
    std::unique_ptr<int> uniquePtr1(new int);
    std::unique_ptr<int> uniquePtr2; // 空指针, nullptr

    std::cout << "Name of person1: " << person1->Name << std::endl;
    std::cout << "Value of uniquePtr1: " << *uniquePtr1 << std::endl;
    if (!uniquePtr2) {
        std::cout << "uniquePtr2 is a nullptr" << std::endl;
    }

    // unique_ptr 创建方法 2
    auto person2 = std::make_unique<Person>("Rose");
    std::cout << "Name of person2: " << person2->Name << std::endl;

    return 0;
}
Name of person1: Jason
Value of uniquePtr1: 0
uniquePtr2 is a nullptr
Name of person2: Rose

unique_ptr 可以通过 release() 放弃所有权, 可以通过 std::move() 完成所有权的转移, 或者通过 reset() 放弃所有权或者指向一个全新的对象

struct Person {
    std::string Name;
    Person(std::string name) : Name(name){};
};

int main() {
    auto person2 = std::make_unique<Person>("Rose");
    std::cout << "Name of person2: " << person2->Name << std::endl;

    // person2 放弃所有权
    person2.release();
    if (!person2) {
        std::cout << "person2 is now a nullptr." << std::endl;
    }

    std::cout << "-----------------------------" << std::endl;

    // person3 通过 move() 将所有权转移给 person4
    auto person3 = std::make_unique<Person>("OJBK");
    std::unique_ptr<Person> person4 = std::move(person3);

    std::cout << "Name of person4: " << person4->Name << std::endl;
    if (!person3) {
        std::cout << "person3 is now a nullptr." << std::endl;
    }

    std::cout << "-----------------------------" << std::endl;

    // reset() 放弃或者转移所有权
    auto person5 = std::make_unique<Person>("Preston");
    auto person6 = std::make_unique<Person>("OJBK");
    person5.reset();
    person6.reset(new Person("NewOne"));

    std::cout << "Name of person6: " << person6->Name << std::endl;
    if (!person5) {
        std::cout << "person5 is now a nullptr." << std::endl;
    }

    return 0;
}
Name of person2: Rose
person2 is now a nullptr.
-----------------------------
Name of person4: OJBK
person3 is now a nullptr.
-----------------------------
Name of person6: NewOne
person5 is now a nullptr.

unique_ptr 也可以作为函数参数或者函数返回值

struct Person {
    std::string Name;
    Person(std::string name) : Name(name){};
};

void printName(std::unique_ptr<Person> person) {
    std::cout << "Name of person: " << person->Name << std::endl; 
}

std::unique_ptr<Person> createPerson() {
    auto person = std::make_unique<Person>("Olivia");
    return person;
}

int main() {
    auto person = std::make_unique<Person>("Jason");

    // unique_ptr 传入函数参数应该搭配 move() 使用, 因为他没法复制
    printName(std::move(person));
    
    // 但是 unique_ptr 一旦被传入的话, person 就丧失了对象的所有权
    if (!person) {
        std::cout << "person is now a nullptr." << std::endl;
    }

    // 这里调用的是 move assignment, 转移返回的 unique_ptr 的所有权
    auto newPerson = createPerson();
    std::cout << "Name of new person: " << newPerson->Name << std::endl;

    return 0;
}
Name of person: Jason
person is now a nullptr.
Name of new person: Olivia

应用 1: 控制指向对象的生命周期

当你使用一个 std::unique_ptr 指向一个对象时, 你可以通过这个智能指针来控制这个对象的生命周期, 当指针出了函数的作用域的时候, 这个指针会被销毁 (调用指针的析构函数), 这个时候指针指向的对象也会被销毁.

// 假设有一个 Animal 的父类和三个子类: Dog, Cat, Tiger

// 定义一个返回指向 Animal 对象的智能指针的函数
template <typename... Ts>
std::unique_ptr<Animal> createAnimal(Ts&&... args);

// 函数的使用
{
    ...
    auto animal_ptr = createAnimal(args);
    ...
} // 销毁 animal_ptr

应用 2: 自定义删除器 (deleter_type)

// 这里的工厂函数使用了 C++14 的函数返回类型推导
// auto = std::unique_ptr<Animal, decltype(delAnimal)>
template <typename... Ts>
auto createAnimal(Ts&&... args) {
    // 使用 lambda 创建一个自定义的删除器
    auto delAnimal = [](Animal* animal_ptr) {
        makeLogEntry(animal_ptr); // logging
        delete animal_ptr; // 销毁这个指针及其指向的对象
    };

    // 创建一个空的 std::unique_ptr, 然后根据条件指向对应的对象
    // 这里的删除器类型需要作为第二个参数传递进去
    std::unique_ptr<Animal, decltype(delAnimal)> ptr(nullptr, delAnimal);

    // 根据条件来判断动物的类型
    if (...) {
        // 这里使用 reset 来接管通过 new 创建的对象所有权
        // 使用 std::forward() 将参数完美转发出去
        ptr.reset(new Dog(std::forward<Ts>(args)...));
    } else if (...) {
        ptr.reset(new Cat(std::forward<Ts>(args)...));
    } else if (...) {
        ptr.reset(new Tiger(std::forward<Ts>(args)...));
    }
    
    return ptr;
}

这里需要注意的是, 自定义删除器中的指针类型是 Animal*, 这意味着无论 createAnimal() 函数中创建的是哪个子类对象, 他在删除器中都是作为 Animal 被删除的. 这个时候我们要允许基类 Animal 有虚析构函数 (virtual destructor)

class Animal {
public:
    ...
    virtual ~Animal();
    ...
}

在使用默认的删除器的时候, std::unique_ptr 的大小可以假设跟原始指针的大小一样; 但是在使用函数指针形式的的自定义删除器的时候, std::unique_ptr 的体积有可能会增大. 对于函数对象形式的删除器来说, 变化的大小取决于函数对象中存储的状态多少, 无状态函数 (stateless function) 对象对大小没有影响. 如果需要使用到上下文信息, 那么就尽量少使用, 不然 std::unique_ptr 的体积可能会过大. 平时在使用

// 无状态的 lambda 自定义删除器
auto delAnimal1 = [](Animal* animal_ptr) {
    makeLogEntry(animal_ptr); 
    delete animal_ptr; 
};

// 这里返回类型大小就是 Animal* 的大小
template <typename... Ts>
std::unique_ptr<Animal, decltype(delAnimal1)> createAnimal(Ts&&... args);


// 函数形式的自定义删除器
void delAnimal2(Animal* animal_ptr) {
    makeLogEntry(animal_ptr); 
    delete animal_ptr; 
}

// 返回类型大小是 Animal* 指针 + 一个函数指针的大小
template <typename... Ts>
std::unique_ptr<Animal, void (*)(Animal*)> createAnimal(Ts&&... args);

Aside: 无状态函数 && 有状态函数

无状态函数 (stateless function): 通常是指不携带任何额外状态或者上下文信息的函数; 比如不捕获变量的 lambda 表达式

有状态函数 (stateful function): 有状态函数刚好相反, 他依赖于某种形式的内部状态或者上下文. 最常见的实现方式是使用成员变量的成员函数

/* 无状态函数例子 */
int add(int a, int b) { return a + b; }



/* 状态函数例子 */
class StatefulCounter {
public:
    StatefulCounter(): count(0) {};
    void increment() { count++; };
    void decrement() { count--; };
    int getCount() const { return count; }
private:
    int count;
}


int main() {
    // add() 是一个无状态函数, 他不依赖任何外部状态
    int (*func_ptr)(int, int) = add;
    int result = func_ptr(3, 5);

    StatefulCounter counter;
    counter.increment();
    // getCount() 是一个状态函数, 他返回的数值依赖类成员 count
    int current_count = counter.getCount();
}

应用 3: 转移所有权

除了使用 reset() 将所有权转让给另外一个 std::unique_ptr 之外, std::unique_ptr 还可以轻松转换为 std::shared_ptr.

// 将 std::shared_ptr 转化为 std::unique_ptr
std::shared_ptr<Animal> sp = createAnimal(args);

std::unique_ptr 很适合作为工厂函数的返回类型, 当调用者想要共享所有权的时候, 他可以直接将 std::unique_ptr 转换为 std::shared_ptr


智能指针 std::unique_ptr 的定义和应用
https://ruidah.github.io/2023/10/27/unique-ptr/
作者
RDH
发布于
2023年10月27日
许可协议