gpt4 book ai didi

【重学C++】02脱离指针陷阱:深入浅出C++智能指针

转载 作者:我是一只小鸟 更新时间:2023-05-21 14:31:23 26 4
gpt4 key购买 nike

文章首发

【重学C++】02 脱离指针陷阱:深入浅出 C++ 智能指针 。

前言

大家好,今天是【重学C++】系列的第二讲,我们来聊聊C++的智能指针.

为什么需要智能指针

在上一讲《 01 C++如何进行内存资源管理 》中,提到了对于堆上的内存资源,需要我们手动分配和释放。管理这些资源是个技术活,一不小心,就会导致内存泄漏.

我们再给两段代码,切身体验下原生指针管理内存的噩梦.

                        
                          void foo(int n) {
    int* ptr = new int(42);
    ...
    if (n > 5) {
	    return;
    }
    ...
    delete ptr;
}


void other_fn(int* ptr) {
	...
};
void bar() {
    int* ptr = new int(42);
    other_fn(ptr);
    // ptr == ?
}


                        
                      

在 foo 函数中,如果入参 n > 5, 则会导致指针 ptr 的内存未被正确释放,从而导致内存泄漏.

在 bar 函数中,我们将指针 ptr 传递给了另外一个函数 other_fn ,我们无法确定 other_fn 有没有释放 ptr 内存,如果被释放了,那 ptr 将成为一个悬空指针, bar 在后续还继续访问它,会引发未定义行为,可能导致程序崩溃.

上面由于原生指针使用不当导致的内存泄漏、悬空指针问题都可以通过智能指针来轻松避免.

C++智能指针是一种用于管理动态分配内存的指针类。基于RAII设计理念,通过封装原生指针实现的。可以在资源(原生指针对应的对象)生命周期结束时自动释放内存.

C++标准库中,提供了两种最常见的智能指针类型,分别是 std::unique_ptr 和 std::shared_ptr 。 接下来我们分别详细展开介绍.

吃独食的 unique_ptr

std::unique_ptr 是 C++11 引入的智能指针,用于管理动态分配的内存。每个 std::unique_ptr 实例都拥有对其所包含对象的唯一所有权,并在其生命周期结束时自动释放对象.

创建 unique_ptr 对象

我们可以 std::unique_ptr 的构造函数或 std::make_unique 函数(C++14支持)来创建一个 unique_ptr 对象,在超出作用域时,会自动释放所管理的对象内存。示例代码如下:

                        
                          #include <memory>
#include <iostream>
class MyClass {
public:
    MyClass() {
        std::cout << "MyClass constructed" << std::endl;
    }

    ~MyClass() {
        std::cout << "MyClass destroyed" << std::endl;
    }
};
int main() {
	std::unique_ptr<MyClass> ptr1(new MyClass);
	// C++14开始支持std::make_unique
    std::unique_ptr<int> ptr2 = std::make_unique<int>(10);
    return 0;
}

                        
                      

代码输出:

                        
                          MyClass constructed
MyClass destroyed

                        
                      

访问所管理的对象

我们可以像使用原生指针的方式一样,访问 unique_ptr 所指向的对象。也可以通过 get 函数获取到原生指针.

                        
                          MyClass* naked_ptr = ptr1.get();
std::cout << *ptr2 << std::endl; // 输出 10


                        
                      

释放/重置所管理的对象

使用reset 函数可以释放 unique_ptr 所管理的对象,并将其指针重置为 nullptr 或指定的新指针。 reset`大概实现原理如下 。

                        
                          template<class T> 
void unique_ptr<T>::reset(pointer ptr = pointer()) noexcept { 
	// 释放指针指向的对象
	delete ptr_; 
	// 重置指针
	ptr_ = ptr;
}

                        
                      

该函数主要完成两件事:

  1. 释放 std::unique_ptr 所管理的对象,以避免内存泄漏。
  2. std::unique_ptr 重置为 nullptr 或管理另一个对象。

code show time

                        
                          #include <iostream>
#include <memory>

class MyClass {
public:
    MyClass() {
        std::cout << "MyClass constructed" << std::endl;
    }

    ~MyClass() {
        std::cout << "MyClass destroyed" << std::endl;
    }
};

int main() {
    // 创建一个 std::unique_ptr 对象,指向一个 MyClass 对象
    std::unique_ptr<MyClass> ptr(new MyClass);

    // 调用 reset,将 std::unique_ptr 重置为管理另一个 MyClass 对象
    ptr.reset(new MyClass);
    return;
}


                        
                      

移动所有权

一个对象资源只能同时被一个 unique_ptr 管理。当尝试把一个 unique_ptr 直接赋值给另外一个 unique_ptr 会编译报错.

                        
                          #include <memory>
int main() {
    std::unique_ptr<int> p1 = std::make_unique<int>(42);
    std::unique_ptr<int> p2 = p1; // 编译报错
    return 0;
}

                        
                      

为了把一个 std::unique_ptr 对象的所有权移动到另一个对象中,我们必须配合 std::move 移动函数.

                        
                          #include <memory>
#include <iostream>
int main() {
    std::unique_ptr<int> p1 = std::make_unique<int>(42);
    std::unique_ptr<int> p2 = std::move(p1); // ok
    std::cout << *p2 << std::endl; // 42
    std::cout << (p1.get() == nullptr) << std::endl; // true
    return 0;
}

                        
                      

这个例子中, 我们把 p1 通过 std::move 将其管理对象的所有权转移给了 p2 , 此时 p2 接管了对象,而 p1 不再拥有管理对象的所有权,即无法再操作到该对象了.

乐于分享的 shared_ptr

shared_ptr 是C++11提供的另外一种常见的智能指针,与 unique_ptr 独占对象方式不同, shared_ptr 是一种共享式智能指针,允许多个 shared_ptr 指针共同拥有同一个对象,采用引用计数的方式来管理对象的生命周期。当所有的 shared_ptr 对象都销毁时,才会自动释放所管理的对象.

创建 shared_ptr 对象

同样的,C++也提供了 std::shared_ptr 构造函数和 std::make_shared 函数来创建 std::shared_ptr 对象.

                        
                          #include <memory>
int main() {
	std::shared_ptr<int> p1(new int(10));
	std::shared_ptr<int> p2 = std::make_shared<int>(20);
	return;
}

                        
                      

多个 shared_ptr 共享一个对象

可以通过赋值操作实现多个 shared_ptr 共享一个资源对象,例如 。

                        
                          std::shared_ptr<int>p3 = p2;

                        
                      

shared_ptr 采用引用计数的方式管理资源对象的生命周期,通过分配一个额外内存当计数器.

当一个新的shared_ptr被创建时,它对应的计数器被初始化为1。每当赋值给另外一个 shared_ptr 共享同一个对象时,计数器值会加1。当某个 shared_ptr 被销毁时,计数值会减1,当计数值变为0时,说明没有任何 shared_ptr 引用这个对象,会将对象进行回收.

C++提供了 use_count 函数来获取 std::shared_ptr 所管理对象的引用计数,例如 。

                        
                          std::cout << "p1 use count: " << p1.use_count() << std::endl;

                        
                      

释放/重置所管理的对象

可以使用 reset 函数来释放/重置 shared_ptr 所管理的对象。大概实现原理如下(不考虑并发场景) 。

                        
                          void reset(T* ptr = nullptr) {
	if (ref_count != nullptr) { 
		(*ref_count)--;
		if (*ref_count == 0) { 
			delete data; 
			delete ref_count; 
		} 
	} 
	data = ptr; 
	ref_count = (data == nullptr) ? nullptr : new size_t(1); 
}


                        
                      

data 指针来存储管理的资源,指针 ref_count 来存储计数器的值.

在 reset 方法中,需要减少计数器的值,如果计数器减少后为 0,则需要释放管理的资源,如果减少后不为0,则不会释放之前的资源对象.

如果reset指定了新的资源指针,则需要重新设置 data 和 ref_count,并将计数器初始化为 1。否则,将计数器指针置为 nullptr 。

shared_ptr使用注意事项

避免循环引用

由于 shared_ptr 具有共享同一个资源对象的能力,因此容易出现循环引用的情况。例如:

                        
                          struct Node { 
	std::shared_ptr<Node> next; 
};

int main() {
	std::shared_ptr<Node> node1(new Node);
	std::shared_ptr<Node> node2(new Node); 
	node1->next = node2; 
	node2->next = node1;
}

                        
                      

在上述代码中, node1 和 node2 互相引用,在析构时会发现计数器的值不为0,不会释放所管理的对象,产生内存泄漏.

为了避免循环引用,可以将其中一个指针改为 weak_ptr 类型。 weak_ptr 也是一种智能指针,通常配合 shared_ptr 一起使用.

weak_ptr是一种弱引用,不对所指向的对象进行计数引用,也就是说,不增加所指对象的引用计数。当所有的 shared_ptr 都析构了,不再指向该资源时,该资源会被销毁,同时对应的所有 weak_ptr 都会变成 nullptr ,这时我们就可以利用 expired() 方法来判断这个 weak_ptr 是否已经失效.

我们可以通过 weak_ptr 的 lock() 方法来获得一个指向共享对象的 shared_ptr 。如果 weak_ptr 已经失效, lock() 方法将返回一个空的 shared_ptr .

下面是 weak_ptr 的基本使用示例:

                        
                          #include <iostream>
#include <memory>

int main() {
    std::shared_ptr<int> sp = std::make_shared<int>(42);
    // 创建shared_ptr对应的weak_ptr指针
    std::weak_ptr<int> wp(sp);

	// 通过lock创建一个对应的shared_ptr
    if (auto p = wp.lock()) {
        std::cout << "shared_ptr value: " << *p << std::endl;
        std::cout << "shared_ptr use_count: " << p.use_count() << std::endl;
    } else {
        std::cout << "wp is expired" << std::endl;
    }

	// 释放shared_ptr指向的资源,此时weak_ptr失效
    sp.reset();
    std::cout << "wp is expired: " <<  wp.expired() << std::endl;
    return 0;
}

                        
                      

代码输出如下 。

                        
                          shared_ptr value: 42
shared_ptr use_count: 2
wp is expired: 1

                        
                      

回到 shared_ptr 的循环引用问题,利用weak_ptr不会增加shared_ptr的引用计数的特点,我们将Node.next的类型改为 weak_ptr , 避免node1和node2互相循环引用。修改后代码如下 。

                        
                          ```cpp
struct Node { 
	std::weak_ptr<Node> next; 
};

int main() {
	std::shared_ptr<Node> node1(new Node);
	std::shared_ptr<Node> node2(new Node); 
	node1->next = std::weak_ptr<Node>(node2); 
	node2->next = std::weak_ptr<Node>(node1); ;
}

                        
                      

避免裸指针与 shared_ptr 混用

先看看以下代码 。

                        
                          int* q = new int(9);
{
	std::shared_ptr<int> p(new int(10));
	...
	q = p.get();
}
std::cout << *q << std::endl;

                        
                      

get 函数返回 std::shared_ptr 所持有的指针,但是不会增加引用计数。所以在shared_ptr析构时,将该指针指向的对象给释放掉了,导致指针 q 变成一个悬空指针.

避免一个原始指针初始化多个 shared_ptr

                        
                          int* p = new int(10);
std::shared_ptr<int> ptr1(p);
// error: 两个shared_ptr指向同一个资源,会导致重复释放
std::shared_ptr<int> ptr2(p);

                        
                      

总结

避免手动管理内存带来的繁琐和容易出错的问题。我们今天介绍了三种智能指针: unique_ptr 、 shared_ptr 和 weak_ptr 。 每种智能指针都有各自的使用场景。 unique_ptr 用于管理独占式所有权的对象,它不能拷贝但可以移动,是最轻量级和最快的智能指针。 shared_ptr 用于管理多个对象共享所有权的情况,它可以拷贝和移动。 weak_ptr 则是用来解决 shared_ptr 循环引用的问题.

下一节,我们将自己动手,从零实现一个C++智能指针。敬请期待 。

最后此篇关于【重学C++】02脱离指针陷阱:深入浅出C++智能指针的文章就讲到这里了,如果你想了解更多关于【重学C++】02脱离指针陷阱:深入浅出C++智能指针的内容请搜索CFSDN的文章或继续浏览相关文章,希望大家以后支持我的博客! 。

26 4 0
Copyright 2021 - 2024 cfsdn All Rights Reserved 蜀ICP备2022000587号
广告合作:1813099741@qq.com 6ren.com