cen's blog cen's blog
首页
  • 编程文章

    • markdown使用
  • 学习笔记

    • C++学习
    • C++数据结构
    • MySQL
    • Linux
    • 网络编程
算法
  • Git
  • ProtoBuf
  • 分类
  • 标签
  • 归档
关于
GitHub (opens new window)

cen

十年饮冰,难凉热血
首页
  • 编程文章

    • markdown使用
  • 学习笔记

    • C++学习
    • C++数据结构
    • MySQL
    • Linux
    • 网络编程
算法
  • Git
  • ProtoBuf
  • 分类
  • 标签
  • 归档
关于
GitHub (opens new window)
  • 类和对象
  • 内存管理
  • 泛型模板
  • string
  • vector
  • list
  • stack和queue
  • priority_queue
  • 继承
  • 多态
  • set和map
  • bitset
  • C++11
  • 异常
  • 智能指针
    • 内存泄漏
    • 避免内存泄漏
    • RAII
    • auto_ptr
    • unique_ptr
    • shared_ptr
      • 原理
      • 获取指向自身的 shared_ptr
    • weak_ptr
      • 循环引用问题
      • 解决循环引用问题
      • 提升为 shared_ptr
  • 特殊类设计
  • function和bind
  • 右值引用和移动语义
  • thread
  • C++学习笔记
cen
2025-02-04
目录

智能指针

# 内存泄漏

内存泄漏指因为疏忽或错误造成程序未能释放已经不再使用的内存的情况。内存泄漏并不是指内存在物理上的消失,而是应用程序分配某段内存后,因为设计错误,失去了对该段内存的控制,因而造成了内存的浪费。

内存泄漏的危害:长期运行的程序出现内存泄漏,影响很大,如操作系统、后台服务等等,出现内存泄漏会导致响应越来越慢,最终卡死。

# 避免内存泄漏

  1. 工程前期良好的设计规范,养成良好的编码规范,申请的内存空间记着匹配的去释放。
  2. 采用 RAII 思想或者智能指针来管理资源。
  3. 有些公司内部规范使用内部实现的私有内存管理库。这套库自带内存泄漏检测的功能选项。

总结: 内存泄漏非常常见,解决方案分为两种:1、事前预防型。如智能指针等。2、事后查错型。如泄漏检测工具。

# RAII

RAII(Resource Acquisition Is Initialization)是一种利用对象生命周期来控制程序资源(如内存、文件句柄、网络连接、互斥量等等)的简单技术。 在对象构造时获取资源,接着控制对资源的访问使之在对象的生命周期内始终保持有效,最后在对象析构的时候释放资源。借此,我们实际上把管理一份资源的责任托管给了一个对象。这种做法有两大好处:

  • 不需要显式地释放资源。
  • 采用这种方式,对象所需的资源在其生命期内始终保持有效。
// 原理:
// 1)智能指针体现在把裸指针进行了一次面向对象的封装,在构造函数中初始化资源地址,在析构函数中负责释放资源
// 2)利用栈上的对象出作用域自动析构这个特点,在智能指针的析构函数中保证释放资源
template<class T>
class SmartPtr {
public:
    SmartPtr(T* ptr) : ptr_(ptr) {}

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

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

    ~SmartPtr() {
        delete ptr_;
        ptr_ = nullptr;
    }
private:
    T* ptr_;
};

int main() {
    SmartPtr<int> smartPtr(new int(1000));
    *smartPtr = 1;
    std::cout << *smartPtr << std::endl;
    return 0;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30

智能指针的原理就是:

  1. 在对象构造时获取资源,在对象析构的时候释放资源,利用对象的生命周期来控制程序资源,即 RAII 特性。
  2. 对*和->运算符进行重载,使得该对象具有像指针一样的行为。
    SmartPtr<int> smartPtr(new int(1000));
    SmartPtr<int> smartPtr1(smartPtr);
1
2

在这里,默认的拷贝构造函数做的是浅拷贝,两个智能指针都持有一个 new int 资源,ptr2 先析构释放了资源,到 ptr1 析构的时候,就成了 delete 野指针了,造成程序崩溃。所以这里引出来智能指针需要解决的两件事情:

  • 怎么解决智能指针的浅拷贝问题
  • 多个智能指针指向同一个资源的时候,怎么保证资源只释放一次,而不是每个智能指针都释放一次,造成代码运行不可预期的严重后果

# auto_ptr

管理权转移

// auto_ptr
auto_ptr(auto_ptr& __a) throw() : _M_ptr(__a.release()) { }

element_type* release() throw() {
	element_type* __tmp = _M_ptr;
	_M_ptr = 0;
	return __tmp;
}
1
2
3
4
5
6
7
8

包含在memory这个头文件中,C++98 中引入的智能指针,auto_ptr 通过管理权转移的方式解决智能指针的拷贝问题,保证一个资源在任何时刻都只有一个对象在对其进行管理,处理办法是直接把前面的 auto_ptr 都置为 nullptr,只让最后一个 auto_ptr 持有资源。

int main() {
	std::auto_ptr<int> ap1(new int(1));
	std::auto_ptr<int> ap2(ap1);
	*ap2 = 10;
	//*ap1 = 20; //error,ap1为nullptr,智能使用ap2

	std::auto_ptr<int> ap3(new int(1));
	std::auto_ptr<int> ap4(new int(2));
	ap3 = ap4;
	return 0;
}
1
2
3
4
5
6
7
8
9
10
11

但一个对象的管理权转移后也就意味着,该对象不能再用对原来管理的资源进行访问了,否则程序就会崩溃。

# unique_ptr

防拷贝

// unique_ptr:
// Disable copy from lvalue.
unique_ptr(const unique_ptr&) = delete;
unique_ptr& operator=(const unique_ptr&) = delete;
1
2
3
4

unique_ptr 是 C++11 中引入的智能指针,unique_ptr 通过防拷贝的方式解决智能指针的拷贝问题,也就是简单粗暴的防止对智能指针对象进行拷贝,这样也能保证资源不会被多次释放。

int main() {
	std::unique_ptr<int> up1(new int(0));
	//std::unique_ptr<int> up2(up1); //error
	return 0;
}
1
2
3
4
5

但防拷贝其实也不是一个很好的办法,因为总有一些场景需要进行拷贝。

# shared_ptr

引用计数

使用资源的时候,引用计数加 1,否则减 1,为 0 的话直接释放 shared_ptr 是 C++11 中引入的智能指针,shared_ptr 通过引用计数的方式解决智能指针的拷贝问题。

  • 每一个被管理的资源都有一个对应的引用计数,通过这个引用计数记录着当前有多少个对象在管理着这块资源。
  • 当新增一个对象管理这块资源时则将该资源对应的引用计数进行++,当一个对象不再管理这块资源或该对象被析构时则将该资源对应的引用计数进行--。
  • 当一个资源的引用计数减为 0 时说明已经没有对象在管理这块资源了,这时就可以将该资源进行释放了。
  • 此外,避免同一个指针创建多个 shared_ptr,

通过这种引用计数的方式就能支持多个对象一起管理某一个资源,也就是支持了智能指针的拷贝,并且只有当一个资源对应的引用计数减为 0 时才会释放资源,因此保证了同一个资源不会被释放多次。

int main() {
	cl::shared_ptr<int> sp1(new int(1));
	cl::shared_ptr<int> sp2(sp1);
	*sp1 = 10;
	*sp2 = 20;
	cout << sp1.use_count() << endl; //2
  // use_count成员函数,用于获取当前对象管理的资源对应的引用计数
	cl::shared_ptr<int> sp3(new int(1));
	cl::shared_ptr<int> sp4(new int(2));
	sp3 = sp4;
	cout << sp3.use_count() << endl; //2
	return 0;
}
1
2
3
4
5
6
7
8
9
10
11
12
13

# 原理

shared_ptr 本身没有成员变量,继承基类的两个成员:

element_type * _Ptr{nullptr}; 		// 指向资源的指针
_Ref_count_base * _Rep{nullptr}; 	// 指向资源引用计数的指针
class __declspec(novtable) _Ref_count_base {
    // common code for reference counting
private:
	/* _Uses记录了资源的引用计数,也就是引用资源的shared_ptr
	的个数;_Weaks记录了weak_ptr的个数,相当于资源观察者的
	个数,都是定义成基于CAS操作的原子类型,增减引用计数时时
	线程安全的操作
	*/
	_Atomic_counter_t _Uses;
	_Atomic_counter_t _Weaks;
}
1
2
3
4
5
6
7
8
9
10
11
12
13

# 获取指向自身的 shared_ptr

class A {
public:
    A() : mptr(new int) {

        cout << "A()" << endl;
    }

    ~A() {

        cout << "~A()" << endl;
        delete mptr;
        mptr = nullptr;
    }

    // A类提供了一个成员方法,返回指向自身对象的shared_ptr智能指针。
    shared_ptr<A> getSharedPtr() {
        return shared_ptr<A>(this);
    }

private:
    int *mptr;
};

int main() {
    shared_ptr<A> ptr1(new A());
    shared_ptr<A> ptr2 = ptr1->getSharedPtr();

    /* 按原先的想法,上面两个智能指针管理的是同一个A对象资源,但是这里打印都是1
    导致出main函数A对象析构两次,析构逻辑有问题*/
    cout << ptr1.use_count() << endl;
    cout << ptr2.use_count() << endl;

    return 0;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34

如果一个类要提供一个函数接口,返回一个指向当前对象的 shared_ptr 智能指针怎么办?方法就是继承enable_shared_from_this类,然后通过调用从基类继承来的shared_from_this()方法返回指向同一个资源对象的智能指针 shared_ptr

class A : public enable_shared_from_this<A> {
public:
	// ...
    // A类提供了一个成员方法,返回指向自身对象的shared_ptr智能指针。
    shared_ptr<A> getSharedPtr() {
        // return shared_ptr<A>(this);
        return shared_from_this();
    }
	// ...
};
1
2
3
4
5
6
7
8
9
10

# weak_ptr

解决循环引用问题

weak_ptr 是 C++11 中引入的智能指针,weak_ptr 不是用来管理资源的释放的,它主要是用来解决 shared_ptr 的循环引用问题的,不能使用资源,只能观察资源的情况。

# 循环引用问题

例如,定义如下的结点类:

struct ListNode {
	ListNode* _next;
	ListNode* _prev;
	int _val;
	~ListNode() {
		cout << "~ListNode()" << endl;
	}
};

int main() {
	ListNode* node1 = new ListNode;
	ListNode* node2 = new ListNode;

	node1->_next = node2;
	node2->_prev = node1;
	//...
	delete node1;
	delete node2;
	return 0;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20

为了防止程序中途返回或抛异常等原因导致结点未被释放,我们将这两个结点分别交给两个 shared_ptr 对象进行管理,这时为了让连接节点时的赋值操作能够执行,就需要把 ListNode 类中的 next 和 prev 成员变量的类型也改为 shared_ptr 类型.

struct ListNode {
	std::shared_ptr<ListNode> _next;
	std::shared_ptr<ListNode> _prev;
	int _val;
	~ListNode() {
		cout << "~ListNode()" << endl;
	}
};
int main() {
	std::shared_ptr<ListNode> node1(new ListNode);
	std::shared_ptr<ListNode> node2(new ListNode);

	node1->_next = node2;
	node2->_prev = node1;
	//...
	return 0;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17

这时程序运行结束后两个结点都没有被释放,但如果去掉连接结点时的两句代码中的任意一句,那么这两个结点就都能够正确释放,根本原因就是因为这两句连接结点的代码导致了循环引用。

  1. 当资源对应的引用计数减为 0 时对应的资源才会被释放,因此资源 1 的释放取决于资源 2 当中的 prev 成员,而资源 2 的释放取决于资源 1 当中的 next 成员。
  2. 而资源 1 当中的 next 成员的释放又取决于资源 1,资源 2 当中的 prev 成员的释放又取决于资源 2,于是这就变成了一个死循环,最终导致资源无法释放。

# 解决循环引用问题

weak_ptr 是弱智能指针,shared_ptr 是强智能指针:

弱智能指针 weak_ptr 区别于 shared_ptr 之处在于:

  • weak_ptr 不会改变资源的引用计数,只是一个观察者的角色,通过观察 shared_ptr 来判定资源是否存在
  • weak_ptr 持有的引用计数,不是资源的引用计数,而是同一个资源的观察者的计数
  • weak_ptr 没有提供常用的指针操作,无法直接访问资源,需要先通过 lock 方法提升为 shared_ptr 强智能指针,才能访问资源 weak_ptr 支持用 shared_ptr 对象来构造 weak_ptr 对象,构造出来的 weak_ptr 对象与 shared_ptr 对象管理同一个资源,但不会增加这块资源对应的引用计数。

将 ListNode 中的 next 和 prev 成员的类型换成 weak_ptr 就不会导致循环引用问题了,此时当 node1 和 node2 生命周期结束时两个资源对应的引用计数就都会被减为 0,进而释放这两个结点的资源。比如:

struct ListNode {
	std::weak_ptr<ListNode> _next;
	std::weak_ptr<ListNode> _prev;
	int _val;
	~ListNode() {
		cout << "~ListNode()" << endl;
	}
};
int main() {
	std::shared_ptr<ListNode> node1(new ListNode);
	std::shared_ptr<ListNode> node2(new ListNode);

	cout << node1.use_count() << endl;
	cout << node2.use_count() << endl;
	node1->_next = node2;
	node2->_prev = node1;
	//...
	cout << node1.use_count() << endl;
	cout << node2.use_count() << endl;
	return 0;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21

# 提升为 shared_ptr

class B;
class A{
public:
    A() {
        std::cout << "A()" << std::endl;
    }
    ~A() {
        std::cout << "~A()" << std::endl;
    }
    void func() {
        std::cout << "A::func()" << std::endl;
    }
    std::weak_ptr<B> s_ptr;
};

class B{
public:
    B() {
        std::cout << "B()" << std::endl;
    }
    ~B() {
        std::cout << "~B()" << std::endl;
    }

    void CallFunc() {
        std::shared_ptr<A> a_ptr = s_ptr.lock();    // 提升为强智能指针
        a_ptr->func();
    }
    std::weak_ptr<A> s_ptr;
};

int main() {
    std::shared_ptr<A> a_ptr(new A);
    std::shared_ptr<B> b_ptr(new B);
    std::cout << a_ptr.use_count() << std::endl;
    a_ptr->s_ptr = b_ptr;
    b_ptr->s_ptr = a_ptr;
    b_ptr->CallFunc();
    std::cout << a_ptr.use_count() << std::endl;
    return 0;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
上次更新: 2026/03/20, 11:19:56
异常
特殊类设计

← 异常 特殊类设计→

最近更新
01
Cmake
11-29
02
thread
11-15
03
动态规划
11-08
更多文章>
Theme by Vdoing | Copyright © 2024-2026 京ICP备2020044002号-3 京公网安备11010502056119号
  • 跟随系统
  • 浅色模式
  • 深色模式
  • 阅读模式