智能指针和引用计数 -- c++学习笔记

1. 智能指针是什么

智能指针(smart pointers)是一种比原始指针(raw pointers)更健全、更安全的指针对象,用于管理指向动态分配内存(堆内存)的指针。它们有自动内存管理的功能,因此可以自动执行一些任务,例如在适当的时候释放内存资源、处理异常等。智能指针还能提高代码的可读性和健壮性,降低内存泄漏的风险,以及减少手动内存管理所导致的错误。

C++ 标准库中提供了以下几种智能指针:

  1. std::unique_ptr
    std::unique_ptr 指针具有独占所有权语义,即每个动态分配的对象在某个时刻只能由一个 std::unique_ptr 实例拥有。当 std::unique_ptr 离开作用域或者被重新赋值时,它会自动释放原先管理的堆内存。这种智能指针尤其有助于避免异常触发的内存泄漏。

  2. std::shared_ptr
    std::shared_ptr 允许多个指针共享同一个对象的所有权。它采用引用计数的方式来跟踪指向资源的指针数量。当引用计数变为0,资源自动被释放。这个智能指针在管理需要在多个对象间共享的资源时很有价值。

  3. std::weak_ptr
    std::weak_ptr 是⽤来解决 shared_ptr 相互引⽤时的死锁问题,如果说两个 shared_ptr 相互引⽤,那么这两个指针的引⽤计数永远不可能下降为0,也就是资源永远不会释放。它是对对象的⼀种弱引⽤,不会增加对象的引⽤计数,和 shared_ptr 之间可以相互转化shared_ptr 可以直接赋值给它,它可以通过调⽤ lock 函数来获得shared_ptr

使用智能指针而非原始指针,是现代 C++ 编程推荐的一种做法(RAII:资源获取即初始化)。它可以让我们更专注于解决业务逻辑问题,而不是在处理内存管理细节上花费太多精力。

2. 引用计算

2.1 什么是引用计数

如何来让指针知道还有其他指针的存在呢?这个时候我们该引入引用计数的概念了。引用计数是这样一个技巧,它允许有多个相同值的对象共享这个值的实现。引用计数的使用常有两个目的:

简化跟踪堆中(也即C++中new出来的)的对象的过程。一旦一个对象通过调用new被分配出来,记录谁拥有这个对象是很重要的,因为其所有者要负责对它进行delete。但是对象所有者可以有多个,且所有权能够被传递,这就使得内存跟踪变得困难。引用计数可以跟踪对象所有权,并能够自动销毁对象。可以说引用计数是个简单的垃圾回收体系

节省内存,提高程序运行效率。如何很多对象有相同的值,为这多个相同的值存储多个副本是很浪费空间的,所以最好做法是让左右对象都共享同一个值的实现。C++标准库中string类采取一种称为”写时复制“的技术,使得只有当字符串被修改的时候才创建各自的拷贝,否则可能(标准库允许使用但没强制要求)采用引用计数技术来管理共享对象的多个对象。

2.2 控制块

控制块(Control Block)是一个在C++智能指针(如shared_ptrweak_ptr)实现过程中用到的内部数据结构。它用于存储一些与智能指针关联的状态信息,例如引用计数器和弱引用计数器。控制块的主要作用是跟踪资源的生命周期,确保资源能够安全地在合适的时机进行释放。通常,每个资源都与一个独立的控制块关联。

在 shared_ptr 和 weak_ptr 的情况下,控制块至少包含以下两种信息:

  1. 共享计数器(Shared Count):记录当前有多少个 shared_ptr 对象共享相同的资源。当计数器为零时(即没有 shared_ptr 持有资源),资源会被释放。
  2. 弱引用计数器(Weak Count):跟踪有多少个 weak_ptr 对象引用资源(即使它们不会增加共享引用计数器)。当弱引用计数器和共享计数器都为零时,控制块才会被释放。

控制块起到了协调智能指针和指向的资源的关系的作用。通过在控制块中管理引用计数和弱引用计数,智能指针可以确保资源按预期地分配和销毁,从而提供了正确的内存管理和简化了手动内存分配/释放带来的复杂性。

 shared_ptr的底层原理:可以看看你真的了解智能指针shared_ptr吗

2.3 引用计算存放在哪?

在 C++ 中,智能指针(如 std::shared_ptr)的引用计数是存储在堆上的。这是因为智能指针对象本身可以在栈上或堆上创建,但引用计数用于共享同一底层资源的多个智能指针对象之间,因此它必须位于一个独立于栈或具体智能指针实例的位置。

实际上,std::shared_ptr 会使用一个控制块来存储引用计数以及底层资源。每当一个 std::shared_ptr 指向同一个资源时,根据需要执行拷贝构建或赋值操作,它们都会共享同一个控制块,从而实现引用计数。

这里有一个实例:

#include 
#include 

int main() {
    std::shared_ptr ptr1(new int(10));
    std::cout << "使用前引用计数: " << ptr1.use_count() << std::endl;

    std::shared_ptr ptr2(ptr1); // 让 ptr2 与 ptr1 指向同一块内存
    std::cout << "共享后引用计数: " << ptr1.use_count() << std::endl;

    ptr2.reset(); // 释放 ptr2 对内存的引用
    std::cout << "释放一个引用后引用计数: " << ptr1.use_count() << std::endl;

    ptr1.reset(); // 释放 ptr1 对内存的引用,此时引用计数变成0,内存被释放
}

输出结果:

使用前引用计数: 1
共享后引用计数: 2
释放一个引用后引用计数: 1

3 shared_ptr 的使用

shared_ptr 的一个关键特性是可以共享所有权,即多个 shared_ptr 可以同时指向并拥有同一个对象。

当最后一个拥有该对象的 shared_ptr 被销毁或者释放该对象的所有权时,对象会自动被删除。

这种行为通过引用计数实现,即 shared_ptr 有一个成员变量记录有多少个 shared_ptr 共享同一个对象。

shared_ptr 的简单用法示例:

#include 
#include 

class MyClass {
public:
    MyClass() { std::cout << "MyClass 构造函数\n"; }
    ~MyClass() { std::cout << "MyClass 析构函数\n"; }
    void do_something() { std::cout << "MyClass::do_something() 被调用\n"; }
};

int main() {
    {
        std::shared_ptr ptr1 = std::make_shared();
        {
            std::shared_ptr ptr2 = ptr1; // 这里共享 MyClass 对象的所有权
            ptr1->do_something();
            ptr2->do_something();
            std::cout << "ptr1 和 ptr2 作用域结束前的引用计数: " << ptr1.use_count() << std::endl;
        } // 这里 ptr2 被销毁,但是 MyClass 对象不会被删除,因为 ptr1 仍然拥有它的所有权
        std::cout << "ptr1 作用域结束前的引用计数: " << ptr1.use_count() << std::endl;
    } // 这里 ptr1 被销毁,同时 MyClass 对象也会被删除,因为它是最后一个拥有对象所有权的 shared_ptr

    return 0;
}

输出结果:

MyClass 构造函数
MyClass::do_something() 被调用
MyClass::do_something() 被调用
ptr1 和 ptr2 作用域结束前的引用计数: 2
ptr1 作用域结束前的引用计数: 1
MyClass 析构函数

3.1 enable_shared_from_this

使用std::enable_shared_from_this ,使用方法很简单,只需要让SomeData继承std::enable_shared_from_this,然后调用shared_from_this,例如:

#include 

struct SomeData;
void SomeAPI(const std::shared_ptr& d) {}

struct SomeData:std::enable_shared_from_this {
    static std::shared_ptr Create() {
        return std::shared_ptr(new SomeData);
    }
    void NeedCallSomeAPI() {
        SomeAPI(shared_from_this());
    }
private:
    SomeData() {}
};


int main()
{
    auto d{ SomeData::Create() };
    d->NeedCallSomeAPI();
}

关于 enable_shared_from_this 的更多原理可以看一下这篇博客: https://0cch.com/2020/08/05/something-about-enable_shared_from_this/

3.2 shared_ptr 常用 API

std::shared_ptr 提供了许多有用的 API,以下是一些常用的 API:

shared_ptr 构造函数:创建一个空的 shared_ptr,不指向任何对象。

std::shared_ptr ptr;

make_shared(args...):创建一个 shared_ptr,并在单次内存分配中同时创建对象和控制块。这比直接使用 shared_ptr 的构造函数要高效。

std::shared_ptr ptr = std::make_shared(42);

reset():释放当前 shared_ptr 的所有权,将其设置为 nullptr。如果当前 shared_ptr 是最后一个拥有对象所有权的智能指针,则会删除对象。

ptr.reset();

reset(T*):释放当前 shared_ptr 的所有权,并使其指向新的对象。如果当前 shared_ptr 是最后一个拥有对象所有权的智能指针,则会删除原对象。

ptr.reset(new int(42));

get():返回指向的对象的裸指针。注意,这个裸指针的生命周期由 shared_ptr 管理,你不应该使用它来创建另一个智能指针。

int* raw_ptr = ptr.get();

operator* 和 operator->:访问指向的对象。

int value = *ptr;
std::shared_ptr> vec_ptr = std::make_shared>();
vec_ptr->push_back(42);

use_count():返回当前 shared_ptr 的引用计数,即有多少个 shared_ptr 共享同一个对象。注意,use_count() 通常用于调试,不应该用于程序逻辑。

size_t count = ptr.use_count();

unique():检查当前 shared_ptr 是否是唯一拥有对象所有权的智能指针。等价于 use_count() == 1。

bool is_unique = ptr.unique();

swap(shared_ptr&):交换两个 shared_ptr 的内容。

std::shared_ptr ptr1 = std::make_shared(42);
std::shared_ptr ptr2 = std::make_shared(24);
ptr1.swap(ptr2);

operator bool():将 shared_ptr 隐式转换为 bool 类型,用于检查其是否为空。

if (ptr) {
    std::cout << "ptr 不为空" << std::endl;
} else {
    std::cout << "ptr 为空" << std::endl;
}

4 weak_ptr 是使用

4.1 weak_ptr 是什么?

std::weak_ptr是C++11引入的一种智能指针,主要与std::shared_ptr配合使用。

它的主要作用是解决循环引用问题、观察std::shared_ptr对象而不影响引用计数,以及在需要时提供对底层资源的访问。

解决循环引用问题:当两个或多个std::shared_ptr对象互相引用时,会导致循环引用。这种情况下,这些对象的引用计数永远不会变为0,从而导致内存泄漏。

#include 
#include 
class B; // 前向声明
class A {
public:
    std::shared_ptr b_ptr;
    ~A() {
        std::cout << "A destructor called" << std::endl;
    }
};

class B {
public:
    std::shared_ptr a_ptr;
    ~B() {
        std::cout << "B destructor called" << std::endl;
    }
};

int main() {
    {
        std::shared_ptr a = std::make_shared();
        std::shared_ptr b = std::make_shared();
        a->b_ptr = b; // A 指向 B
        b->a_ptr = a; // B 指向 A
    } // a 和 b 离开作用域,但由于循环引用,它们的析构函数不会被调用

    std::cout << "End of main" << std::endl;
    return 0;
}


std::weak_ptr可以打破这种循环引用,因为它不会增加引用计数。只需要将其中一个对象的std::shared_ptr替换为std::weak_ptr,即可解决循环引用问题。

观察std::shared_ptr对象:std::weak_ptr可以用作观察者,监视std::shared_ptr对象的生命周期。它不会增加引用计数,因此不会影响资源的释放。

#include 
#include 

class B; // 前向声明

class A {
public:
    std::shared_ptr b_ptr;
    ~A() {
        std::cout << "A destructor called" << std::endl;
    }
};

class B {
public:
    std::weak_ptr a_ptr; // 使用 weak_ptr 替代 shared_ptr
    ~B() {
        std::cout << "B destructor called" << std::endl;
    }
};

int main() {
    {
        std::shared_ptr a = std::make_shared();
        std::shared_ptr b = std::make_shared();
        a->b_ptr = b; // A 指向 B
        b->a_ptr = a; // B 对 A 使用 weak_ptr
    } // a 和 b 离开作用域,它们的析构函数会被正确调用

    std::cout << "End of main" << std::endl;
    return 0;
}

 

你可能感兴趣的:(c++面经学习,c++,学习,笔记)