C++智能指针/C++新特性

智能指针(smart_ptr)

前备知识

  1. 工厂函数:是一个普通函数(或静态方法),通常是类外部的,用于返回类的实例。工厂函数可以访问类的私有构造函数。
    • 将类的实例化的过程封装,直接向外返回一个对象的指针或者引用
  2. reset方法
    • reset 会删除当前智能指针所持有的对象(如果有),然后将智能指针置空(对于不带参数的 reset),或者管理一个新的对象(对于带参数的 reset)。但是在shared_ptr中如果引用计数大于1,只会给引用计数减一

一、shared_ptr共享指针

1.1情景与定义
  • 多个裸指针指向同一块内存地址,即使被delete了,也有可能导致改内存空间不再被使用,从而引发内存泄漏。可以使用共享指针,当该地址的引用计数缩减为0的时候,就会重载该块地址
1.2内存占用
  • 控制块、初始化内容对象在堆上,指针本身在栈上
  • 控制块std::shared_ptr 会为每个被管理的对象创建一个控制块。控制块中包含了:
    • 引用计数(shared count)
    • 自定义删除器(deleter)
    • 自定义分配器(如果有的话)
  • 创建的时候会先创建控制块,然后让指针本身指向控制块

在这里插入图片描述

  • 需要避免控制块的重复创建,比如已经创建了指向一个地址的shared_ptr,如果该地址上的shared_ptr没有被释放,那么不可以再在这个地址上创建shared_Ptr。例如:从成员函数中生成 std::shared_ptr 实例和使用裸指针创建 std::shared_ptr 的问题

    #include <iostream>
    #include <memory>
    
    void loggingDeleter(int* ptr) {
        std::cout << "Deleting pointer: " << ptr << std::endl;
        delete ptr;
    }
    
    int main() {
        int* rawPtr = new int(42);
    
        // 使用裸指针创建多个 std::shared_ptr 实例
        std::shared_ptr<int> sp1(rawPtr, loggingDeleter);
        std::shared_ptr<int> sp2(rawPtr, loggingDeleter); // 错误:rawPtr 被多次管理
    
        return 0; // 此时 rawPtr 被 delete 两次
    }
    
    #include <iostream>
    #include <memory>
    #include <vector>
    
    class Widget : public std::enable_shared_from_this<Widget> {
    public:
        void process(std::vector<std::shared_ptr<Widget>>& processedWidgets) {
            processedWidgets.emplace_back(this); // 错误:this 被用来创建新的 std::shared_ptr  这里会直接构造一个函数,避免拷贝或者移动,导致一块地址多次初始化共享指针
        }
    };
    
    int main() {
        auto widget = std::make_shared<Widget>();
        std::vector<std::shared_ptr<Widget>> processedWidgets;
        widget->process(processedWidgets); // 此时 Widget 对象的控制块可能会被创建多次
    
        return 0;
    }
    
    //解决方案
    void process(std::vector<std::shared_ptr<Widget>>& processedWidgets) {
        processedWidgets.emplace_back(shared_from_this()); // 正确:使用 shared_from_this 获取现有 std::shared_ptr
    }
    
1.3引用计数、make_shared、移动构造、移动赋值
  1. 对象本身不会再分配一个空间给引用计数,计数和对象的联动使用会加大内存消耗std::shared_ptr<test> a(new test());。而make_shared是一种工厂函数,会使得引用计数和对象连续分布,减少消耗std::shared_ptr a = make_shared<test>()

  2. 引用计数操作必须是原子性的,可能在线程A中std::shared_ptr实例正在执行析构,发试图将引用计数减少1,而在线程B中被执行了拷贝构造。两个线程引起对引用计数的竞争条件。

  3. 移动构造和移动赋值 通常比拷贝构造和拷贝赋值更高效,因为它们避免了引用计数的自增操作。移动操作只涉及指针的转移,不涉及对引用计数的修改,从而减少了性能开销。

    拷贝构造和拷贝赋值 涉及引用计数的增加,这可能会带来性能开销,尤其是在引用计数操作频繁的情况下。

    class MyClass {
    public:
        MyClass(MyClass&& other) noexcept;
    };
    
    MyClass obj1;
    MyClass obj2 = std::move(obj1);  // 通过移动构造创建 obj2
    
    class MyClass {
    public:
        MyClass& operator=(MyClass&& other) noexcept;
    };
    
    MyClass obj1;
    MyClass obj2;
    obj2 = std::move(obj1);  // 通过移动赋值将 obj1 的资源转移到 obj2
    
1.4make_shared with new and make_shared
  • std::make_shared 内部使用单次内存分配来同时分配对象和控制块。它通过一次 new 操作分配一大块内存,这块内存包含了对象和控制块。因此,控制块和对象都在堆上。
  • newstd::shared_ptr: 需要两次内存分配,一次为对象,另一次为控制块。这可能导致额外的内存分配开销,并且对象和控制块不一定存储在连续的内存区域中。
1.5共享指针的创建和销毁

std::shared_ptr<Entity> e1(new Entity());

std::shared_ptr<Entity> e1 = std::make_shared<Entity>();

auto e1 = std::make_shared<Entity>();

std::shared_ptr<Entity> e2 = e1;

std::shared_ptr<Entity> e2 = std::move(e1);

foo(std::move(e1)); // 引用数量不变

foo(e1); // 引用+1

#include <memory>
#include <iostream>

using namespace std;

class Ball {
public:
    Ball() {
        cout << "构造执行" << endl;
    };
    ~Ball() {
        cout << "析构执行" << endl;
    };
    void Bounce() {
        cout << "A Ball Bounce" << endl;
    }
};

int main() {
    // shared_ptr<int> p = new int(100);
    shared_ptr<int> p = make_shared<int>(100); //隐式转换,给共享指针类实例化,且效率更高。
    
    // 裸指针进行的操作其也可以
    cout << *p << endl;
    
    // 使用共享指针
    shared_ptr<Ball> p1 = make_shared<Ball>();  //构造执行
    cout << p1.use_count() << endl;  //1
    shared_ptr<Ball> p2 = p1;
    cout << p1.use_count() << " " << p2.use_count() << endl;  //2 2
    shared_ptr<Ball> p3 = p1;
    cout << p1.use_count() << " " << p2.use_count() << " " << p3.use_count() << endl;  //3 3 3
    p1.reset();
    p2.reset();
    p3.reset();  //析构执行
    
    return 0;
}
  • 注意虽然可以裸指针和共享指针指向同一内存空间Ball* p4 = p.get()(get方法获取),但只要共享指针引用计数为0,对应内存空间会直接被销毁,会变为野指针。

  • 生命周期,堆是每个线程公用的,每个线程的引用都消失了,最后才会消失

    在这里插入图片描述

  • 使用 std::make_shared():总是会创建一个新的控制块并返回一个指向该控制块的 std::shared_ptr 实例。

1.6自定义deleter
  • 见unique_ptr的deleter
1.7自定义reset和别名
#include  <iostream>
#include  <memory>

using namespace std;

struct Bar { int i = 123;};
struct Foo { Bar bar; };

void close_file(FILE* fp) {
    if (fp == nullptr) {
        return;
    }
    fclose(fp);
    std::cout << "file closed" << std::endl;
}

int main() {
    FILE* fp = fopen("../CMakeLists.txt", "r");
    shared_ptr<FILE> sfp{fp, close_file};
    cout << "file opened" << endl;  //file opened
    cout << sfp.use_count() << endl;  //1

    shared_ptr<Foo> f = make_shared<Foo>();cout << f.use_count()<< endl;// 1
    shared_ptr<Bar> b(f,&(f->bar));cout << f.use_count()<< endl; // 2
    cout << b->i << endl;// 123
    return 0;
}
// file closed

  • 可以不delete内存,而关闭文件。

  • f为共享指针的别名,可以通过这样的操作使得b指针可以访问f指针的属性,注意b和f都必须是共享指针

二、unique_ptr

2.1情景和使用场景
  • 常用在工厂函数中,将unique_ptr作为工厂函数发返回值
2.2内存占用
  • std::unique_ptr 对象(即管理的指针和它自身的其他成员)是存储在栈上(如果它作为局部变量存在)。它只包含一个裸指针(或其他智能指针的内部数据结构),该指针指向堆上的对象。
2.3不可赋值,可以移动

std::unique_ptr<Entity> e1 = new Entity() (错误),不是普通指针类型

std::unique_ptr<Entity> e1(new Entity())(正确),不推荐

std::unique_ptr<Entity> e1 = make_uniuqe<entity>();(正确)

auto e2 = std::unique_ptr<Entity>(正确)

std::unique_ptr<Entity> e2 = e1(错误),无法赋值

std::unique_ptr<Entity> e2 = std::move(e1)(正确),转移

foo(std::move(e1))(正确),转移后的指针

2.4unique_ptr生命周期

在这里插入图片描述

2.5自定义deleter
  • 可以使用lamda表达式、普通函数、任意函数

    auto dele = [](file* file){
        make_log(file);
        delete dele;
    }
    
    template<typename Ts> std::unique_ptr<file, deceltype(dele)> makefile(Ts& param) {
        std::unique_ptr<param, deceltype(dele)> fileTest(nullptr, dele);
        if(/*应该分配一个Stock类型的对象*/){
            fileTest.reset(new Stock(std::forward<Ts>(params)));
        }
        else if(/*应该分配一个Bond类型的对象*/){
            fileTest.reset(new Bond(std::forward<Ts>(params)));
        }
        else if(/*应该分配一个RealEstate类型的对象*/){
            fileTest.reset(new RealEstate(std::forward<Ts>(params)));
        }
    }
    
  • 当使用一个自定义的deleter时,需要将deleter的类型也传给std::unique_ptr用于模板参数。记住这一点,这和std::shared_ptr很不同!

2.6unique_ptr or shared_ptr

类型声明和灵活性:

  • 对于 unique_ptr,deleter 类型是类型的一部分。这意味着不同 deleter 的 unique_ptr 是不同的类型,即使它们指向相同类型的对象。
  • 对于 shared_ptr,deleter 类型不是类型的一部分。所有 shared_ptr<Widget> 都是相同的类型,无论它们使用什么 deleter。

绑定时机:

  • unique_ptr 的 deleter 在编译时绑定。这可能导致更高的性能,因为编译器可以进行更多优化。
  • shared_ptr 的 deleter 在运行时绑定。这提供了更大的灵活性,但可能会有轻微的性能开销。
  • 总结:shared_ptr 使用类型擦除来实现其灵活性,而 unique_ptr 则依赖于编译时的类型信息。

多态性:

  • shared_ptr 的设计允许更多的多态行为。你可以在运行时决定使用哪个 deleter,而不会改变 shared_ptr 的类型。
  • unique_ptr 的设计更加静态,deleter 类型在编译时确定。
#include <memory>
#include <vector>
#include <iostream>

struct Widget {
    virtual ~Widget() = default;
    virtual void doSomething() = 0;
};

struct WidgetA : Widget {
    void doSomething() override { std::cout << "WidgetA doing something\n"; }
};

struct WidgetB : Widget {
    void doSomething() override { std::cout << "WidgetB doing something\n"; }
};

// 自定义删除器
auto deleterA = [](Widget* w) {
    std::cout << "Deleting with deleterA\n";
    delete w;
};

auto deleterB = [](Widget* w) {
    std::cout << "Deleting with deleterB\n";
    delete w;
};

int main() {
    // unique_ptr例子
    std::unique_ptr<Widget, decltype(deleterA)> upA(new WidgetA(), deleterA);
    std::unique_ptr<Widget, decltype(deleterB)> upB(new WidgetB(), deleterB);
    
    // 不能运行,A和B类型不一样
    // std::vector<std::unique_ptr<Widget, decltype(deleterA)>> uniqueVec{std::move(upA), std::move(upB)};

    // shared_ptr例子
    std::shared_ptr<Widget> spA(new WidgetA(), deleterA);
    std::shared_ptr<Widget> spB(new WidgetB(), deleterB);
    
    // 可以运行,类型一样
    std::vector<std::shared_ptr<Widget>> sharedVec{spA, spB};
    
    upA->doSomething();
    upB->doSomething();
    
    for(const auto& sp : sharedVec) {
        sp->doSomething();
    }

    return 0;
}
2.7类型擦除和虚函数类似,下面是对比和相似之处
  1. 运行时多态:
    • 虚函数和类型擦除都实现了运行时多态,允许在运行时决定调用哪个函数。
  2. 动态分发:
    • 两者都使用某种形式的动态分发机制来调用正确的函数。
  3. 额外的内存开销:
    • 虚函数使用虚函数表(vtable)。
    • 类型擦除通常使用某种形式的函数指针或函数对象。

区别:

  1. 实现机制:

    • 虚函数:使用虚函数表和虚指针。
    • 类型擦除:通常使用模板和运行时多态的组合。
  2. 灵活性:

    • 虚函数:限于继承层次结构。
    • 类型擦除:可以用于不相关的类型。
  3. 编译时要求:

    • 虚函数:需要在基类中声明。
    • 类型擦除:不需要预先声明,可以在使用时定义接口。
  4. 性能:

    • 虚函数:通常有较小的运行时开销。
    • 类型擦除:可能有更大的开销,取决于实现。
    #include <iostream>
    #include <memory>
    #include <vector>
    
    // 使用虚函数的方式
    class Animal {
    public:
        virtual void makeSound() const = 0;
        virtual ~Animal() = default;
    };
    
    class Dog : public Animal {
    public:
        void makeSound() const override { std::cout << "Woof!" << std::endl; }
    };
    
    class Cat : public Animal {
    public:
        void makeSound() const override { std::cout << "Meow!" << std::endl; }
    };
    
    // 使用类型擦除的方式
    class AnimalConcept {
        struct Concept {
            virtual void makeSound() const = 0;
            virtual ~Concept() = default;
        };
        
        template<typename T>
        struct Model : Concept {
            T object;
            Model(T obj) : object(std::move(obj)) {}
            void makeSound() const override { object.makeSound(); }
        };
        
        std::unique_ptr<Concept> pimpl;
    
    public:
        template<typename T>
        AnimalConcept(T obj) : pimpl(std::make_unique<Model<T>>(std::move(obj))) {}
        
        void makeSound() const { pimpl->makeSound(); }
    };
    
    struct Cow {
        void makeSound() const { std::cout << "Moo!" << std::endl; }
    };
    
    int main() {
        // 使用虚函数
        std::vector<std::unique_ptr<Animal>> animals;
        animals.push_back(std::make_unique<Dog>());
        animals.push_back(std::make_unique<Cat>());
        
        for (const auto& animal : animals) {
            animal->makeSound();
        }
        
        // 使用类型擦除
        std::vector<AnimalConcept> animalConcepts;
        animalConcepts.emplace_back(Dog{});
        animalConcepts.emplace_back(Cat{});
        animalConcepts.emplace_back(Cow{}); // 注意:Cow 不需要继承任何基类
        
        for (const auto& animal : animalConcepts) {
            animal.makeSound();
        }
        
        return 0;
    }
    
2.8将unique_ptr转换为shared_ptr
std::unique_ptr<int> uptr = std::make_unique<int>(42);
std::shared_ptr<int> sptr = std::move(uptr);  // 转换为 std::shared_ptr
  • 转换 std::unique_ptrstd::shared_ptr:这会创建一个新的控制块,并将对象的所有权从 std::unique_ptr 转移到 std::shared_ptr
2.9get方法

尽管 std::shared_ptrstd::unique_ptr 都可以通过 get() 方法获取裸指针,但 不推荐 用这个方法在它们之间转换。直接使用裸指针来转换会失去智能指针的自动内存管理,导致严重的错误,例如双重删除或悬挂指针。

std::shared_ptr<int> sptr = std::make_shared<int>(42);
int* rawPtr = sptr.get();
std::unique_ptr<int> uptr(rawPtr);  // 非常危险!可能导致双重删除

在上面的代码中,std::shared_ptrstd::unique_ptr 都会尝试删除 rawPtr 指向的对象,导致 未定义行为

2.10总结:
  1. std::unique_ptr是一种小型,快速,只能移动的智能指针,管理资源时有独占式语义
  2. 默认情况下,资源的释放通过调用delete操作符实现,但也可以自定义deleter。有状态的deleter和函数指针都会引起std::unique_ptr实例的内存占用增加
  3. 从std::unique_ptr到std::shared_ptr的转换极为简单

三、weak_ptr

3.1特性
  • 它是一种智能指针,与 std::shared_ptr 配合使用。

    它指向 shared_ptr 管理的对象,但不增加引用计数。

    它可以检测所指向的对象是否已被释放。

  • 不能直接访问所指对象(没有 * 和 -> 操作符)。

    可以转换为 shared_ptr 来访问对象。

    可以检查所指对象是否还存在。

3.2使用和特性
  • 在shared_ptr的基础上使用weak_ptr,在使用的时候进行判断,如果shared_ptr消失了就不使用了,否则就继续使用
#include <iostream>
#include <memory>

class Entity {
public:
    Entity() {
        std::cout << "Created Entity!" << std::endl;
    }
    ~Entity() {
        std::cout << "Destroyed Entity!" << std::endl;
    }
};

void observe(std::weak_ptr<Entity> ew) {
    if(std::shared_ptr<Entity> spt = ew.lock()) {
        std::cout << spt.use_count() << std::endl;
        std::cout << "entity still alive" << std::endl;
    } else {
        std::cout << "entity was espried:(" << std::endl;
    }
}

void ex4() {
    puts("--------");
    puts("Entering ex4");
    std::weak_ptr<Entity> ew;
    {
        puts("Entering ex4::scope1");
        auto e1 = std::make_shared<Entity>();
        std::cout << e1.use_count() << std::endl;
        ew = e1;
        std::cout << e1.use_count() << std::endl;
        observe(ew);
        puts("Leaving ex4::scope1");
    }
}

int main() {
    ex4();
    
    return 0;
}
--------
Entering ex4
Entering ex4::scope1
Created Entity!
1
1
2
entity still alive
Leaving ex4::scope1
Destroyed Entity!
3.3应用场景
#include <iostream>
#include <memory>
#include <vector>
#include <unordered_map>

// 1. 基本用法
void basic_usage() {
    auto sp = std::make_shared<int>(42);
    std::weak_ptr<int> wp = sp;

    if (auto locked = wp.lock()) {
        std::cout << "Value: " << *locked << std::endl;
    } else {
        std::cout << "Object no longer exists" << std::endl;
    }
}

// 2. 检查对象是否存在
void check_existence() {
    std::weak_ptr<int> wp;
    {
        auto sp = std::make_shared<int>(42);
        wp = sp;
        std::cout << "wp expired? " << std::boolalpha << wp.expired() << std::endl;
    }
    std::cout << "wp expired? " << std::boolalpha << wp.expired() << std::endl;
}

// 3. 缓存系统
class Widget {};

std::shared_ptr<Widget> loadWidget(int id) {
    return std::make_shared<Widget>();
}

std::shared_ptr<Widget> getCachedWidget(int id) {
    static std::unordered_map<int, std::weak_ptr<Widget>> cache;
    
    if (auto sp = cache[id].lock()) {
        return sp;
    } else {
        auto sp = loadWidget(id);
        cache[id] = sp;
        return sp;
    }
}

// 4. 观察者模式
class Subject;

class Observer {
public:
    virtual void update() = 0;
    virtual ~Observer() = default;
};

class Subject {
    std::vector<std::weak_ptr<Observer>> observers;
public:
    void addObserver(std::shared_ptr<Observer> observer) {
        observers.push_back(observer);
    }
    
    void notify() {
        for (auto it = observers.begin(); it != observers.end();) {
            if (auto sp = it->lock()) {
                sp->update();
                ++it;
            } else {
                it = observers.erase(it);
            }
        }
    }
};

// 5. 打破循环引用
class B;

class A {
public:
    std::shared_ptr<B> b_ptr;
    ~A() { std::cout << "A destroyed" << std::endl; }
};

class B {
public:
    std::weak_ptr<A> a_ptr;  // 使用 weak_ptr 而不是 shared_ptr
    ~B() { std::cout << "B destroyed" << std::endl; }
};

void cyclic_reference() {
    auto a = std::make_shared<A>();
    auto b = std::make_shared<B>();
    a->b_ptr = b;
    b->a_ptr = a;
}

int main() {
    basic_usage();
    check_existence();
    getCachedWidget(1);
    cyclic_reference();
    return 0;
}

四、总结

  1. 能用unique_ptr就用unique_ptr,不行就使用shared_ptr,因为use_count占内存
  2. weak_ptr几乎不使用
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值