智能指针(C++11)

智能指针(C++11)_第1张图片

行到水穷处,坐看云起时。

文章目录

  • 简介
  • 内存泄漏
    • 内存泄漏分类
    • 内存泄漏检测工具
  • RAII
  • 智能指针原理
  • 智能指针使用
    • std::auto_ptr
    • std::unique_ptr
    • std::shared_ptr
      • std::shared_ptr循环引用
    • std::weak_ptr
  • 总结


简介

C++智能指针是在C++11标准引入的一项重要功能,用于改进内存管理和避免一些与传统的原始指针相关的常见问题。例如在C++中,程序员通常需要手动分配和释放内存,这可能导致一些常见问题,如内存泄漏、释放后的悬挂指针和多次释放相同的内存,而这些问题通常由人为的错误引起,因此引入智能指针是为了解决这些问题。

C++11标准引入了std::shared_ptrstd::unique_ptrstd::weak_ptr等智能指针类型,以提供更安全和便捷的内存管理方式。这些智能指针是C++标准库的一部分,可以自动跟踪资源的生命周期,当不再需要时自动释放资源。
智能指针(C++11)_第2张图片

内存泄漏

内存泄漏是指程序在动态分配内存后,却没有释放这些内存,并且失去了对这些内存的所有指向。这意味着这些内存块将一直被程序占用,直到程序结束,因为它们无法被操作系统的内存管理系统回收。并且内存泄漏还可能会导致如下问题:

  • 耗尽可用内存:内存泄漏会逐渐占用系统内存,最终导致可用内存耗尽。当内存不再可用时,系统可能会变得非常缓慢,甚至崩溃。

  • 性能下降:即使没有耗尽所有可用内存,内存泄漏也会导致程序性能下降。内存泄漏的内存分配会导致程序变得越来越慢,因为它必须不断地分配更多的内存,而无法释放不再使用的内存。

  • 不稳定性:内存泄漏可能导致程序不稳定,崩溃或出现未定义的行为。这可能会严重影响应用程序的可靠性,尤其是长时间运行的服务器应用程序。

  • 难以调试:内存泄漏通常不会引发直接的错误消息,而是在运行时逐渐积累。因此,发现和调试内存泄漏通常是一项棘手的任务。它可能需要使用专门的工具和技术来识别和修复。

  • 资源泄漏:除了内存泄漏,资源泄漏(如文件句柄、数据库连接等)也可能发生,从而导致系统无法访问重要资源。

内存泄漏是程序中常见的编程错误,特别是在使用动态内存分配时。在编程中,我们在使用指针得到便利的同时, 也不得不时刻警惕着内存泄漏。使用指针时一不注意就很容易照成内存泄漏,防不胜防。为了避免内存泄漏,在编程时应该谨慎管理内存。因此使用智能指针来确保及时释放不再使用的内存就会是一个非常好的方案。此外,定期进行内存泄漏检测和性能分析也是发现和解决内存泄漏问题的重要手段。

智能指针(C++11)_第3张图片

内存泄漏分类

内存泄漏可以分为多种类型,具体取决于泄漏的原因和发生的情况。如下:

类型 说明
动态内存泄漏 未释放分配的内存最常见的动态内存泄漏类型,它发生在程序分配了动态内存(例如使用 new 或 malloc),但未在之后释放这些内存(使用 delete 或 free)。还有就是失去指向内存的指针,在分配内存后,如果指针被重新分配或丢失,将无法释放分配的内存也会造成内存泄漏。
资源泄漏 如未关闭打开的文件句柄,导致资源泄漏。未关闭数据库连接,可能会占用数据库服务器资源。未关闭网络连接,可能会导致系统资源耗尽。
循环引用 如在使用智能指针(如 std::shared_ptr)时,如果存在循环引用,可能导致资源不被释放。这通常需要使用 std::weak_ptr 来解决。
未释放的资源 如未解锁的互斥锁,可能会导致其他线程无法访问共享资源。未关闭的操作系统内核对象(例如,信号量、共享内存等)。
循环引用 如当对象中包含回调函数,回调函数持有对象的引用,并且对象也持有回调函数的引用时,可能导致循环引用。

内存泄漏检测工具

检测内存泄漏是确保程序在运行时不会浪费内存的重要任务。有必要的话可以使用如下的一些第三方工具来帮助检测内存泄漏。

  1. Valgrind:Valgrind 是一种强大的开源工具,可用于检测内存泄漏和其他内存错误。它支持多种平台,包括Linux和macOS。使用Valgrind的memcheck工具可以检测内存泄漏。

方法:

  1. 安装 Valgrind(如果尚未安装)。
  2. 在终端中使用以下命令运行程序:valgrind --leak-check=full ./your_program。
  3. Valgrind将输出关于内存泄漏的详细信息,包括泄漏的位置和大小。
  1. ASAN(AddressSanitizer):ASAN是Clang和GCC的一个特性,可用于检测内存错误,包括内存泄漏。它集成在编译器中,可以通过编译选项启用。

方法

  1. 使用编译器选项 -fsanitize=address 编译你的程序。
  2. 运行你的程序。
  3. ASAN将在检测到内存泄漏时输出详细信息。
  1. 静态代码分析工具:一些静态代码分析工具(例如Clang静态分析器)可以检测潜在的内存泄漏问题。这些工具会在编译时分析代码并发现问题,但不会运行时捕获问题。

  2. 自定义内存分配器:你可以实现自定义内存分配器,以跟踪分配和释放的内存块。这可以用于在程序退出时报告未释放的内存。

  3. 内存泄漏检测库:一些第三方库和工具,如LeakSanitizer、Electric Fence等,专门用于检测内存泄漏问题。

综合使用上述工具和技术,可以有效地检测和解决内存泄漏问题。检测内存泄漏是编写高质量C++程序的关键步骤之一。但总的来说,处理内存泄漏最好的方法还是从源头入手,写代码时多进行代码审查,检查代码以查找潜在的内存泄漏问题,特别是在程序的关键部分。

写出高质量C++代码来扼杀内存泄漏,扼杀内存泄漏来写出高质量C++代码。啊这…

智能指针(C++11)_第4张图片

RAII

RAII是“资源获取即初始化(Resource Acquisition Is Initialization)”的缩写,它是C++中一种重要的编程范式和管理资源的惯用法。它基于C++语言的构造函数和析构函数的特性,用于确保在对象生命周期中正确获取和释放资源。RAII的核心思想是利用对象的构造函数来获取资源,而析构函数来释放资源,从而确保资源的正确管理。

如下是RAII的关键原则和应用方式:

  • 构造函数获取资源:在对象构造的时候,资源被分配和初始化。这可以是任何资源,例如动态内存、文件句柄、网络连接等。

  • 析构函数释放资源:当对象超出作用域时,其析构函数会自动被调用,从而释放资源。这确保了无论控制流如何改变,资源都将被正确释放,甚至在异常抛出的情况下也是如此。

  • 异常安全性:利用RAII的方式可以确保资源的安全释放,即使在发生异常时也不会造成资源泄漏。因为对象的析构函数会在异常发生时被自动调用。

智能指针就是RAII思想的一个出色体现,可以更方便地管理动态分配的内存。它是C++中一种重要的资源管理模式,广泛应用于各种情况,包括但不限于文件处理、网络操作、数据库连接、互斥锁和其他资源的管理。使用RAII可以避免手动管理资源的繁琐和错误,使代码更加安全、清晰,并提高可维护性和可靠性。

智能指针原理

智能指针是C++中用于管理动态内存的抽象,它们基于C++语言的特性和模板,通过智能的方式管理资源的生命周期,但是智能指针总的来说还是一个指针,因此智能指针通常重载了*->运算符,以模拟原始指针的行为,使其更容易使用。并且智能指针一般都使用对象的构造函数来获取资源,通常通过动态内存分配。资源的构造发生在智能指针被创建时,当智能指针超出作用域时,其析构函数被调用,用于释放资源。

有些智能指针还包括计数引用,例如share_ptr智能指针通常使用引用计数来跟踪资源的所有者数量。每当一个智能指针指向资源时,引用计数会增加;当一个智能指针不再指向资源时,引用计数会减少。当引用计数变为零时,资源被释放。这确保了资源只在不再需要时才会被释放。总的来说,智能指针的原理一般都包括RAII特性和重载了operator*和opertaor->,具有像指针一样的行为。

智能指针使用

std::auto_ptr

智能指针(C++11)_第5张图片

std::auto_ptr 是 C++98 标准中提供的智能指针之一,用于管理动态分配的对象。然而,它在 C++11 标准中已被弃用,因为它存在一些潜在的安全性和语义问题。std::auto_ptr 的核心思想是实现智能指针,它能够拥有动态分配的对象,允许将对象的所有权从一个 std::auto_ptr 转移到另一个。这是通过在 std::auto_ptr 的复制构造函数和赋值运算符中使用移动语义来实现的。当资源管理对象(通常是 std::auto_ptr)的所有权被转移到另一个对象时,原始资源管理对象会失去对资源的控制,从而防止资源重复释放。

以下是有关 std::auto_ptr 的一些关键特点和限制:

特点 限制
所有权转移 std::auto_ptr 具有所有权语义,这意味着它可以拥有动态分配的对象,并且可以将对象的所有权从一个 std::auto_ptr 转移到另一个。这使得它可以用于简单的资源管理。
所有权转移的问题 虽然所有权转移是 std::auto_ptr 的一个特性,但它也是它的一个问题。当一个 std::auto_ptr 拥有一个对象时,不能再创建另一个拥有相同对象的 std::auto_ptr,因为它会导致资源双重释放的问题。这是 std::auto_ptr 的一个潜在陷阱。
不适用于容器 由于上述问题,std::auto_ptr 通常不适合用于容器,如 std::vector 或 std::map,因为容器需要在拷贝元素时具备复制语义。
缺乏共享拥有权 与 std::shared_ptr 不同,std::auto_ptr 不能用于多个指针共享相同的对象。这是因为它的所有权转移特性。

总之,std::auto_ptr 是一个过时的智能指针,不推荐在现代 C++ 中使用。推荐使用 std::unique_ptr 或 std::shared_ptr 来更安全地管理动态分配的对象。

如下示例代码展示了auto_ptr的风险,在这个示例中,我们首先创建了两个 std::auto_ptr 指针:autoPtr1 和 autoPtr2。然后,我们将 autoPtr1 的所有权转移给 autoPtr2,这意味着 autoPtr1 不再拥有资源,而 autoPtr2 拥有原来的资源,此时的autoPtr1就会悬空,当我们再次解引用autoPtr1时就会导致程序奔溃。

#include 
#include 

int main() 
{
    std::auto_ptr<int> autoPtr1(new int(42));
    std::auto_ptr<int> autoPtr2;

    // 将 autoPtr1 的所有权转移给 autoPtr2
    autoPtr2 = autoPtr1;

    // 这将导致 autoPtr1 指向 nullptr,autoPtr2 拥有原来的资源
    std::cout << "autoPtr1: " << *autoPtr1 << std::endl;
    std::cout << "autoPtr2: " << *autoPtr2 << std::endl;

    // 尝试使用 autoPtr1,这会导致未定义行为,因为它已经为空指针
    // 但编译器不会发出警告
    //   
    std::cout << "autoPtr1: " << *autoPtr1 << std::endl;

    return 0;
}

智能指针(C++11)_第6张图片

也可以通过简单的代码来模拟autoptr的资源转移,如下:

template <class T>
class autoPtr
{
public:
    autoPtr(T* ptr)
        :_ptr(ptr)
    {}
    autoPtr(autoPtr<T>& aptr)
        :_ptr(aptr._ptr)
    {
        aptr._ptr = nullptr;
    }
    ~autoPtr()
    {
        if (_ptr) delete _ptr;
    }
    autoPtr<T>& operator=(autoPtr<T>& aptr)
    {
        if (this != &aptr)
        {
            if (_ptr) delete _ptr;
            _ptr = aptr._ptr;
            aptr._ptr = nullptr;
        }
        return *this;
    }

    T* operator->()
    {
        return _ptr;
    }

    T& operator*()
    {
        return *_ptr;
    }

private:
    T* _ptr;
};

std::unique_ptr

智能指针(C++11)_第7张图片

与 std::auto_ptr 不同,std::unique_ptr 是 C++11 标准引入的智能指针,用于管理动态分配的对象。它的名称 “unique” 暗示了它的一个主要特性:每个 std::unique_ptr 只能拥有被管理对象的唯一所有权,这意味着不能有多个 std::unique_ptr 同时指向相同的对象,从而防止资源泄漏,提供了更安全的资源管理。以下是它的一些特性:

特性 说明
独占所有权 每个 std::unique_ptr 实例只能拥有被管理对象的唯一所有权。这意味着不能有多个 std::unique_ptr 同时指向相同的对象。
移动语义 std::unique_ptr 支持移动语义,允许将资源的所有权从一个 std::unique_ptr 移动到另一个。这提供了有效的资源管理和转移。
自定义删除器 您可以使用自定义删除器函数来指定 std::unique_ptr 在释放资源时应该如何操作。这可以用于管理不同类型的资源,例如动态分配的数组或使用不同的释放函数。
std::make_unique C++14 引入了 std::make_unique 函数模板,用于更方便地创建 std::unique_ptr,它自动推断模板类型参数。
类型安全 std::unique_ptr 提供类型安全的资源管理,因为它的类型信息在编译时得以确定。
有效代替std::auto_ptr std::unique_ptr 是对已弃用的 std::auto_ptr 的现代替代品,提供更安全的资源管理。

当然unique_ptr也存在一些限制,如下:

限制 说明
不能进行复制 std::unique_ptr 不能直接进行复制,因为它的拷贝构造函数和拷贝赋值运算符已被删除。只能通过移动操作来传递所有权。
不支持共享所有权 std::unique_ptr 不支持共享所有权,如果需要多个指针共享同一个资源,应该使用 std::shared_ptr。
无法用于数组 std::unique_ptr 通常用于管理单个对象的资源。如果需要管理动态分配的数组,应该使用 std::unique_ptr 的特化版本 std::unique_ptr 或 std::vector。
不支持自动指针成员的容器 std::unique_ptr 无法直接存储在标准库容器中,因为它的复制构造函数和赋值运算符被删除。如果需要在容器中存储智能指针,应该使用 std::shared_ptr 或 std::weak_ptr。

总之,std::unique_ptr 提供了一种轻量级智能指针,用于管理动态分配的对象,它在许多资源管理场景中非常有用。然而,要注意它的独占性质和不能复制的限制,以及选择适当的智能指针类型以满足特定需求。要简单实现一个unique_ptr只需要简单粗暴的防止它拷贝即可,如下:

template <class T>
class unique_ptr
{
public:
	unique_ptr(T* ptr) : _ptr(ptr)
	{}
	~unique_ptr()
	{
		if (_ptr) delete _ptr;
	}
	T& operator*()
	{
		return *_ptr;
	}
	T* operator->()
	{
		return _ptr;
	}
	unique_ptr(const unique_ptr<T>& uptr) = delete;
	unique_ptr<T>& operator=(const unique_ptr<T>& uptr) = delete;
private:
	T* _ptr;
};

std::shared_ptr

智能指针(C++11)_第8张图片

基于std::unique_ptr的限制,std::shared_ptr 允许多个智能指针共享对同一块内存资源的所有权。当最后一个指向资源的 std::shared_ptr 被销毁或重置时,资源才会被释放。这样才更加的符合我们指针的使用场景。std::shared_ptr 也是 C++ 标准库中的一个智能指针类,以下是shared_ptr的一些特性以及限制:

特性 说明
共享所有权 std::shared_ptr 允许多个智能指针共享对同一块内存资源的所有权。这通过引用计数的方式实现,当最后一个指向资源的 std::shared_ptr 被销毁或重置时,资源才会被释放。
引用计数 内部维护引用计数,跟踪共享的指针数量。每次创建或复制一个 std::shared_ptr,引用计数会增加。每次销毁或重置一个 std::shared_ptr,引用计数会减少。当引用计数变为零时,相关的资源会被释放。
安全的共享 在正确使用的情况下,std::shared_ptr 可以避免常见的内存泄漏问题,因为资源会在不再需要时自动释放。
自定义删除器: 支持自定义删除器函数,用于在资源释放时执行特定的清理操作。

推荐使用 std::make_shared 来创建 std::shared_ptr,以减少动态内存分配的次数,提高性能。

限制 说明
性能开销 由于引用计数的维护,std::shared_ptr 的性能开销可能比较高。每次复制或创建 std::shared_ptr 都涉及原子操作,这可能导致在多线程环境中的性能开销。
循环引用问题 如果存在循环引用,即 A 持有 B 的 std::shared_ptr,而 B 也持有 A 的 std::shared_ptr,可能导致资源永远不会被释放。为了解决这个问题,可以使用 std::weak_ptr。
不适合所有情况 尽管 std::shared_ptr 是一个强大的工具,但它并不适合所有情况。在某些场景下,比如性能要求较高的场景,可能需要考虑其他智能指针或手动内存管理的方式。
不是线程安全的原子操作 std::shared_ptr 的引用计数操作是原子的,但它本身并不是线程安全的。如果多个线程同时修改同一个 std::shared_ptr,可能需要额外的同步机制。
不能用于管理动态数组 std::shared_ptr 不适用于管理动态数组。为了管理动态数组,应该使用 std::shared_ptr 的数组版本 std::shared_ptr 或者更适合的 std::vector。

简单模拟实现shared_ptr:

template <class T>
class shared_ptr
{
public:
	shared_ptr(T* ptr = nullptr) 
		: _ptr(ptr)
		, _preCount(new int(1))
		, _mtx(new std::mutex)
	{}

	shared_ptr(shared_ptr<T>& sp)
		: _ptr(sp._ptr)
		, _preCount(sp._preCount)
		, _mtx(sp._mtx)
	{
		addCount();
	}

	void addCount()
	{
		_mtx->lock();
		++(*_preCount);
		_mtx->unlock();
	}

	void relCount()
	{
		_mtx->lock();
		bool flag = false;
		if (--(*_preCount) == 0 && _ptr)
		{
			delete _ptr;
			delete _preCount;
			flag = true;
		}
		_mtx->unlock();
		if (flag) delete _mtx;
	}

	~shared_ptr()
	{
		relCount();
	}

	T& operator*()
	{
		return *_ptr;
	}

	T* operator->()
	{
		return _ptr;
	}

	shared_ptr<T>& operator=(shared_ptr<T>& sp)
	{
		if (this != &sp)
		{
			relCount();
			_ptr = sp._ptr;
			_preCount = sp._preCount;
			_mtx = sp._mtx;
			addCount();
		}
		return *this;
	}

private:
	T* _ptr;
	int* _preCount;
	std::mutex* _mtx;
};

std::shared_ptr循环引用

智能指针(C++11)_第9张图片

std::shared_ptr 可能会导致循环引用问题,这种问题也被称为循环依赖。循环引用发生在两个或多个对象之间互相持有对方的 std::shared_ptr,导致它们的引用计数永远不会减少到零,从而内存泄漏。如下代码:

class A;
class B;

class A 
{
public:
    wzh::shared_ptr<B> bPtr;
};

class B 
{
public:
    wzh::shared_ptr<A> aPtr;
};

int main() 
{
    wzh::shared_ptr<A> a(new A);
    wzh::shared_ptr<B> b(new B);

    // 形成循环引用
    a->bPtr = b;
    b->aPtr = a;

    return 0;
}

示例中,对象 A 持有指向对象 B 的 shared_ptr,而对象 B 持有指向对象 A 的 shared_ptr,这导致了循环引用。因此,即使在 main 函数结束时,这两个对象的引用计数不会减为零,它们的资源也不会被释放,从而导致内存泄漏。

智能指针(C++11)_第10张图片

智能指针(C++11)_第11张图片

为避免循环引用问题,可以使用 std::weak_ptr 来打破其中一个方向的共享关系。

std::weak_ptr

std::weak_ptr 也是 C++ 标准库中的一个智能指针,用于协助管理动态分配的内存资源,但它不像 std::shared_ptr 那样共享所有权,而是提供了一种弱引用的方式。std::weak_ptr 主要用于解决循环引用问题,避免内存泄漏。简单模拟weak_ptr,代码如下:

	template <class T>
	class weak_ptr
	{
	public:
		weak_ptr(T* ptr) : _ptr(ptr)
		{}

		weak_ptr(const shared_ptr<T>& sp)
			: _ptr(sp._ptr)
		{}

		weak_ptr<T>& operator=(shared_ptr<T>& sp)
		{
			if (_ptr != sp.getPtr())
			{
				_ptr = sp.getPtr();
			}
			return *this;
		}

		T* operator->()
		{
			return _ptr;
		}

		T& operator*()
		{
			return *_ptr;
		}

	private:
		T* _ptr;
	};

在上面的示例中,可以使用 std::weak_ptr 来打破其中一个方向的共享关系。因为 std::weak_ptr 不增加引用计数,只是提供对共享对象的弱引用,不影响资源的释放。如下代码:

class A 
{
public:
    wzh::weak_ptr<B> bPtr;
};

class B 
{
public:
    wzh::weak_ptr<A> aPtr;
};

int main() 
{
   
    wzh::shared_ptr<A> a(new A);
    wzh::shared_ptr<B> b(new B);

    // 形成循环引用
    a->bPtr = b;
    b->aPtr = a;

    return 0;
}

智能指针(C++11)_第12张图片
智能指针(C++11)_第13张图片

总结

文章介绍了编程中经常出现的内存泄漏到底是个什么东西,并将内存泄漏的分类也给归置了一下,顺带介绍了检测内存泄漏的工具。文中还重点介绍了智能指针的实现原理以及智能指针的使用,对auto_ptr、unique_ptr、shared_ptr和weak_ptr这几种进行了详细的介绍,对它们的功能特性以及限制都详细的列了出来,并用简单的代码来模拟实现这几种智能指针。文中还特地介绍了shared_ptr的循环引用,产生原因以及解决方法。

码文不易,如果文章对你有帮助的话就来一个三连支持一下吧

智能指针(C++11)_第14张图片

你可能感兴趣的:(C++,c++,开发语言)