C++进阶—智能指针

第一章:为什么需要智能指针?

double Division(int len, int time) {
	if (time == 0) throw invalid_argument("除0错误");
	return (double)len / (double)time;
}

void Func() {
	int* p = new int[10];

	int len, time;
	cin >> len >> time;
	cout << Division(len, time) << endl;//如果抛异常,这行之后的语句都不会执行

	delete[] p;//抛异常会导致p未释放
	cout << "delete[] " << p << endl;
}

int main() {
	try {
		Func();
	}
	catch (exception& e) {
		cout << e.what() << endl;
	}
	return 0;
}

在 Division 函数中抛出异常,会导致 Func 函数中的 delete[] p 无法执行,从而引发内存泄漏。虽然可以在 Func 内部捕获异常、释放内存后重新抛出,但这会导致代码变得复杂且难以维护,特别是当有多个需要释放的资源时。

第二章:内存泄漏

2.1 什么是内存泄漏,内存泄漏的危害

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

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

2.2 内存泄漏分类

C/C++程序中一般我们关心两种方面的内存泄漏:

堆内存泄漏(Heap leak)
堆内存指的是程序执行中依据须要分配通过malloc / calloc / realloc / new等从堆中分配的一块内存,用完后必须通过调用相应的 free或者delete 删掉。假设程序的设计错误导致这部分内存没有被释放,那么以后这部分空间将无法再被使用,就会产生Heap Leak。

系统资源泄漏
指程序使用系统分配的资源,比方套接字、文件描述符、管道等没有使用对应的函数释放掉,导致系统资源的浪费,严重可导致系统效能减少,系统执行不稳定。

2.3 如何检测内存泄漏

2.4如何避免内存泄漏

  1. 工程前期良好的设计规范,养成良好的编码规范,申请的内存空间记着匹配的去释放。ps:这个理想状态。但是如果碰上异常时,就算注意释放了,还是可能会出问题。需要下一条智能指针来管理才有保证。
  2. 采用RAII思想或者智能指针来管理资源。
  3. 有些公司内部规范使用内部实现的私有内存管理库。这套库自带内存泄漏检测的功能选项。
  4. 出问题了使用内存泄漏工具检测。ps:不过很多工具都不够靠谱,或者收费昂贵。

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

第三章:智能指针的使用及原理

3.1 RAII

RAII(Resource Acquisition Is Initialization)是一种利用对象生命周期来控制程序资源(如内存、文件句柄、网络连接、互斥量等等)的简单技术。

在对象构造时获取资源,接着控制对资源的访问使之在对象的生命周期内始终保持有效,最后在对象析构的时候释放资源。借此,我们实际上把管理一份资源的责任托管给了一个对象。这种做法有两大好处:

  • 不需要显式地释放资源。
  • 采用这种方式,对象所需的资源在其生命期内始终保持有效。
// 使用RAII思想设计的SmartPtr类
class SmartPtr {
public:
	SmartPtr(int* ptr)
		:_ptr(ptr) {
	}

	~SmartPtr() {
		delete[] _ptr;
		cout << "delete[] " << _ptr << endl;
	}
private:
	int* _ptr;
};
double Division(int len, int time) {
	if (time == 0) throw invalid_argument("除0错误");
	return (double)len / (double)time;
}
void Func() {
	int* p = new int[10];
	//用智能指针释放,无论是否抛异常都能正常释放
	SmartPtr<int> sp(p);
	int len, time;
	cin >> len >> time;
	cout << Division(len, time) << endl;
}
//不抛异常时的执行流程:Func() 函数结束,sp是栈对象,离开作用域时自动调用 ~SmartPtr()。
//抛异常时的执行流程:由于异常抛出,Func() 的当前执行被中断,栈展开(Stack Unwinding) 开始:
//C++会析构 Func()中的所有栈对象(这里是sp)。sp.~SmartPtr() 被调用,执行 delete[] _ptr,释放内存。
int main() {
	try {
		Func();
	}
	catch (exception& e) {
		cout << e.what() << endl;
	}

	return 0;
}

改为模板

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

	~SmartPtr() {
		delete[] _ptr;
		cout << "delete[] " << _ptr << endl;
	}
private:
	T* _ptr;
};

double Division(int len, int time) {
	if (time == 0) throw invalid_argument("除0错误");
	return (double)len / (double)time;
}

void Func() {
	//RAII
	//1.资源获取(Acquisition):new int[10] 动态分配 10 个 int 的内存空间(资源获取)。
	//2.立即初始化管理对象(Initialization)
	//将原始指针 int* 立即传递给 SmartPtr<int> 的构造函数,由 sp1 接管所有权。
	//3.生命周期绑定
	//sp1 是栈对象,其析构函数会在 Func() 结束时(无论正常返回或异常)自动调用,释放 int[10]。
	SmartPtr<int> sp1(new int[10]);
	SmartPtr<double> sp2(new double[10]);
	int len, time;
	cin >> len >> time;
	cout << Division(len, time) << endl;
}

int main() {
	try {
		Func();
	}
	catch (exception& e) {
		cout << e.what() << endl;
	}

	return 0;
}

3.2 智能指针的原理

上述的SmartPtr还不能将其称为智能指针,因为它还不具有指针的行为。指针可以解引用,也可以通过->去访问所指空间中的内容,因此:AutoPtr模板类中还得需要将* 、->重载下,才可让其像指针一样去使用

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

	~SmartPtr() {
		delete _ptr;
		cout << "~SmartPtr() -> " << _ptr << endl;
	}

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

private:
	T* _ptr;
};

总结一下智能指针的原理:

  1. RAII特性
  2. 重载operator*和opertaor->,具有像指针一样的行为。

3.3 std::auto_ptr

C++98版本的库中就提供了auto_ptr的智能指针。下面演示的auto_ptr的使用及问题。
auto_ptr的实现原理:管理权转移的思想,下面简化模拟实现了一份bit::auto_ptr来了解它的原理。

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

	~auto_ptr() {
		if (_ptr) {
			delete _ptr;
			cout << "delete->" << _ptr << endl;
			_ptr = nullptr;
		}
	}

	//管理权转移 ap2(ap1)
	auto_ptr(auto_ptr<T>& ap)
		:_ptr(ap._ptr) {
		ap._ptr = nullptr;
	}

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

private:
	T* _ptr;
};

int main() {
	std::auto_ptr<int> sp1(new int);
	std::auto_ptr<int> sp2(sp1); // 管理权转移

	// sp1悬空
	*sp2 = 10;
	cout << *sp2 << endl;
	cout << *sp1 << endl;
	return 0;
}
// 结论:auto_ptr是一个失败设计,很多公司明确要求不能使用auto_ptr

3.4 std::unique_ptr

C++11中开始提供更靠谱的unique_ptr

unique_ptr文档

unique_ptr的实现原理:简单粗暴的防拷贝,下面简化模拟实现了一份UniquePtr来了解它的原理

C++98方法一:只声明不实现,不让编译器自动生成。

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

	~unique_ptr() {
		delete _ptr;
		cout << "delete->" << _ptr << endl;
	}

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

	//C++98方法一:只声明不实现,不让编译器自动生成。
	unique_ptr(const unique_ptr<T>& up);

private:
	T* _ptr;
};

//但这样可能导致别人在类外面实现
template <class T>
unique_ptr<T>::unique_ptr(const unique_ptr<T>& up) {}

C++98方法二:声明为私有

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

	~unique_ptr() {
		delete _ptr;
		cout << "delete->" << _ptr << endl;
	}

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

private:
	//C++98方法二:声明为私有
	unique_ptr(const unique_ptr<T>& up);
	unique_ptr<T>& operator=(const unique_ptr<T>& up);
private:
	T* _ptr;
};

C++11

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

	~unique_ptr() {
		delete _ptr;
		cout << "delete->" << _ptr << endl;
	}

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

	//C++11
	unique_ptr(const unique_ptr<T>& up) = delete;
	unique_ptr<T>& operator=(const unique_ptr<T>& up) = delete;

private:
	T* _ptr;
};

3.5 std::shared_ptr

C++11中开始提供更靠谱的并且支持拷贝的shared_ptr

std::shared_ptr文档

shared_ptr的原理:是通过引用计数的方式来实现多个shared_ptr对象之间共享资源。例如:比特老师晚上在下班之前都会通知,让最后走的学生记得把门锁下。

  1. shared_ptr在其内部,给每个资源都维护了着一份计数,用来记录该份资源被几个对象共享。
  2. 对象被销毁时(也就是析构函数调用),就说明自己不使用该资源了,对象的引用计数减一。
  3. 如果引用计数是0,就说明自己是最后一个使用该资源的对象,必须释放该资源
  4. 如果不是0,就说明除了自己还有其他对象在使用该份资源,不能释放该资源,否则其他对象就成野指针了。
template<class T>
	class shared_ptr {
	public:
		shared_ptr(T* ptr = nullptr)
			:_ptr(ptr)
			, _pcount(new int(1)) {
			//构造时开辟存计数的空间,且将计数置为1
		}
        
        //版本一
		~shared_ptr() {
			if (--(*_pcount) == 0) {
				delete _ptr;
				delete _pcount;
				cout << "delete->" << _ptr << endl;
			}
		}
		shared_ptr<T>& operator=(const shared_ptr<T>& sp) {
			//sp赋值给this,this指向的计数(*_pcount)无论如何都要--
			//不能自己给自己赋值。不仅指相同对象,管理同一资源的不同对象也不可以
			//否则可能出现--(*_pcount) == 0导致自己被释放
			if (_ptr != sp._ptr) {
				if (--(*_pcount) == 0) {
					delete _ptr;
					delete _pcount;
				}
				_ptr = sp._ptr;
				_pcount = sp._pcount;
				++(*_pcount);
			}
			return *this;
		}

		//版本二:复用
		void release() {
			if (--(*_pcount) == 0) {
				delete _ptr;
				delete _pcount;
				//cout << "delete->" << _ptr << endl;
			}
		}
		~shared_ptr() { release(); }
		shared_ptr<T>& operator=(const shared_ptr<T>& sp) {
			if (_ptr != sp._ptr) {
				release();
				_ptr = sp._ptr;
				_pcount = sp._pcount;
				++(*_pcount);
			}
			return *this;
		}

		shared_ptr(const shared_ptr<T>& sp)
			:_ptr(sp._ptr)
			, _pcount(sp._pcount) {
			++(*_pcount);
		}

		T& operator*() { return *_ptr; }
		T* operator->() { return _ptr; }
		int use_count() const { return *_pcount; }
		T* get() const { return _ptr; }
	private:
		T* _ptr;
		//int _count;//不可以,智能指针拷贝后两个对象的_count不同步
		//static int _count;//不可以,如果两个智能指针管理两个资源,_count的统计就乱了
		int* _pcount;//去堆开辟空间存计数
	};

std::shared_ptr 的循环引用
该方式可能导致new失败后抛异常无法释放
struct ListNode {
	int val;
	ListNode* next;
	ListNode* prev;
};
void test_shared_ptr2() {
	
	ListNode* n1 = new ListNode;
	ListNode* n2 = new ListNode;
	n1->next = n2;
	n2->prev = n1;
}

循环引用

struct ListNode {
	int val;
	////节点链接时智能指针和节点指针类型不匹配
	//ListNode* next;
	//ListNode* prev;
	shared_ptr<ListNode> next;
	shared_ptr<ListNode> prev;

	//如果节点被智能指针释放会调用析构函数
	~ListNode() { cout << "~ListNode()" << endl; }
};
void test_shared_ptr2() {
	//用智能指针管理,避免抛异常后无法释放
	shared_ptr<ListNode> n1 = new ListNode;
	shared_ptr<ListNode> n2 = new ListNode;
	
	n1->next = n2;
	n2->prev = n1;
}

循环引用导致内存泄漏

n1 的引用计数 = 2(n1 本身 + n2->prev)
n2 的引用计数 = 2(n2 本身 + n1->next)
n2先析构。n2的引用计数2 → 1(因为n1->next仍然持有ListNode2)
n2的引用计数 = 1,所以 ListNode2 不会释放。
n1后析构。n1的引用计数2 → 1(因为n2->prev仍然持有ListNode1)
n1的引用计数 = 1,所以 ListNode1 不会释放。
最终,n1和n2被销毁了,但ListNode1被n2->prev持有;ListNode2被n1->next持有
 

解决方案:在引用计数的场景下,把节点中的_prev和_next改成weak_ptr就可以了
原理就是,n1->_next = n2;和n2->_prev = n1;时weak_ptr的_next和_prev不会增加node1和node2的引用计数。

template<class T>
class shared_ptr {
public:
	shared_ptr(T* ptr = nullptr)
		:_ptr(ptr)
		, _pcount(new int(1)) {
		//构造时开辟存计数的空间,且将计数置为1
	}

	void release() {
		if (--(*_pcount) == 0) {
			delete _ptr;
			delete _pcount;
			//cout << "delete->" << _ptr << endl;
		}
	}
	~shared_ptr() { release(); }
	shared_ptr<T>& operator=(const shared_ptr<T>& sp) {
		if (_ptr != sp._ptr) {
			release();
			_ptr = sp._ptr;
			_pcount = sp._pcount;
			++(*_pcount);
		}
		return *this;
	}

	shared_ptr(const shared_ptr<T>& sp)
		:_ptr(sp._ptr)
		, _pcount(sp._pcount) {
		++(*_pcount);
	}

	T& operator*() { return *_ptr; }
	T* operator->() { return _ptr; }
	int use_count() const { return *_pcount; }
	T* get() const { return _ptr; }
private:
	T* _ptr;
	int* _pcount;//去堆开辟空间存计数
};

template<class T>
class weak_ptr {
public:
	//当执行 new ListNode 时:先分配内存。
	//然后调用 ListNode 的构造函数(隐式生成的默认构造函数)。
	//ListNode 的默认构造函数会调用 next 和 prev 的默认构造函数(因为它们是成员变量)。
	//如果 weak_ptr 没有默认构造函数,ListNode 的默认构造会失败。
	weak_ptr()
		:_ptr(nullptr) {
	}

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

	weak_ptr<T>& operator=(const shared_ptr<T>& sp) {
		_ptr = sp.get();
		return *this;
	}

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

上面的示例中都是new单个对象,用delete销毁。如果new多个对象就需要用delete[]销毁。怎么解决?

template<class T>
struct DelArray {
	void operator()(T* ptr) { delete[] ptr; }
};
void test_shared_ptr3() {
	//shared_ptr<ListNode> sp1(new ListNode[10]);
	//new[] 需要delete[],但shared_ptr是delete

	bit::shared_ptr<ListNode> sp1(new ListNode[3], DelArray<ListNode>());//仿函数
	bit::shared_ptr<ListNode> sp2(new ListNode[3], [](ListNode* ptr) {delete[] ptr; });//lambda
	bit::shared_ptr<FILE> sp3(fopen("Smart Pointers.cpp", "r"), [](FILE* ptr) {fclose(ptr); });//lambda
	bit::shared_ptr<ListNode> sp4(new ListNode);
}

定制删除器

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

	template<class D>
	shared_ptr(T* ptr, D del)
		: _ptr(ptr)
		, _pcount(new int(1))
		, _del(del) {
	}
	//function<void(T*)> _del;
	//参数 D 的本质:它是一个"释放资源的方式",可以处理单个对象或多个对象(比如数组的 delete[])
	//这个释放方式可以是:仿函数、Lambda 表达式、普通函数指针、其他任何可调用实体(总之是个可调用对象)
	//function 的作用:作为通用的包装器,统一存储这些不同类型的可调用对象

	void release() {
		if (--(*_pcount) == 0) {
			//仿函数和lambda都是可调用对象
			_del(_ptr);
			delete _pcount;
			//cout << "delete->" << _ptr << endl;
		}
	}
	~shared_ptr() { release(); }
	shared_ptr<T>& operator=(const shared_ptr<T>& sp) {
		if (_ptr != sp._ptr) {
			release();
			_ptr = sp._ptr;
			_pcount = sp._pcount;
			++(*_pcount);
		}
		return *this;
	}

	shared_ptr(const shared_ptr<T>& sp)
		:_ptr(sp._ptr)
		, _pcount(sp._pcount) {
		++(*_pcount);
	}

	T& operator*() { return *_ptr; }
	T* operator->() { return _ptr; }
	int use_count() const { return *_pcount; }
	T* get() const { return _ptr; }
private:
	T* _ptr;
	int* _pcount;//去堆开辟空间存计数
	//D _del;//不可行,D是构造函数的模板参数,不是整个类的
	function<void(T*)> _del = [](T* ptr) {delete ptr; };
	//给缺省值的原因:如果使用这个智能指针管理资源,且该资源没有传删除器时调用的是默认构造函数。
	//如果不初始化_del,那么析构时_del为空就报错
};

第四章:C++11和boost中智能指针的关系

  1. C++ 98 中产生了第一个智能指针auto_ptr.
  2. C++ boost给出了更实用的scoped_ptr和shared_ptr和weak_ptr.
  3. C++ TR1,引入了shared_ptr等。不过注意的是TR1并不是标准版。
  4. C++ 11,引入了unique_ptr和shared_ptr和weak_ptr。需要注意的是unique_ptr对应boost的scoped_ptr。并且这些智能指针的实现原理是参考boost中的实现的。

作业

1. 下面关于内存泄漏的说法正确的是()

A.如果对程序影响不是很大的情况下,泄漏一两个字节不是很重要
B.内存没有释放时,进程在销毁的时候会统一回收,不用担心
C.内存泄漏不一定会对系统马上造成影响,可以不着急进行处理
D.写代码时要有良好的编码规范,万一发生内存泄漏要及时处理

答案:D
A:错误,一两个字节不处理时可能会因小失大,很多个两字节,就是很大的一块内存空间
B:错误,虽然进程退出时会回收,但是进程为退出时可能会影响程序性能甚至会导致崩溃
C:错误,只要发现了就要及时处理,否则,说不定什么时候程序就会崩溃
D:正确,内存泄漏最好不要发生,万一发生了一定要及时处理

2. 下面那个说法可以表示资源泄漏()

A.从商店买东西
B.借钱不还
C.买房子交首付
D.办信用卡

答案:B
从系统中动态申请的资源,一定要记着及时归还,否则别人可能就使用不了,或者申请失败。
就像借钱一样,有借有还再借不难

3. 以下那个误操作不属于资源泄漏()

A.打开的文件忘记关闭
B.malloc申请的空间未通过free释放
C.栈上的对象没有通过delete销毁
D.内存泄漏属于资源泄漏的一种,但资源泄漏不仅仅是内存泄漏

答案:C
A:属于,打开的文件用完时一定要关闭
B:属于,堆上申请的空间,需要用户显式的释放
C:不属于,栈上的对象不需要释放,函数结束时编译器会自动释放
D:正确,资源泄漏包含的比较广泛,比如文件未关闭、套接字为关闭等

4. 关于RAII下面说法错误的是()

A.RAII的实现方式就是在构造函数中将资源初始化,在析构函数中将资源清理掉
B.RAII方式管理资源,可以有效避免资源泄漏问题
C.所有智能指针都借助RAII的思想管理资源
D.RAII方式管理锁,有些场景下可以有效避免死锁问题

答案:C
A:正确
B:正确
C:错误,weak_ptr不能单独管理资源,必须配合shared_ptr一块使用,解决shared_ptr中存在的循环引用问题
D:正确

5. 下面关于weak_ptr的说法错误的是()

A.weak_ptr与shread_ptr的实现方式类似,都是通过引用计数的方式实现的
B.weak_ptr的对象可以独立管理资源
C.weak_ptr的唯一作用就是解决shared_ptr中存在的循环引用问题
D.weak_ptr一般情况下都用不到

答案:B
A:正确,weak_ptr和shared_ptr都是通过引用计数实现,但是在底层还是有区别的
B:错误,weak_ptr不能单独管理资源,因为其给出的最主要的原因是配合shared_ptr解决其循环引用问题
C:正确,处理解决shared_ptr的循环引用问题外,别无它用
D:正确

6. 下面关于shared_ptr说法错误的是 ( )

A.shared_ptr是C++11才正式提出来的
B.shared_ptr对象之间可以共享资源
C.shared_ptr可以应用于任何场景
D.shared_ptr是借助引用计数的方式实现的

答案:C
A:正确
B:正确,参考shared_ptr的实现原理
C:错误,有些场景下shared_ptr可能会造成循环引用,必须与weak_ptr配合使用
D:正确

7. 下面关于unique_ptr说法错误的是()

A.unique_ptr是C++11才正式提出的
B.unique_ptr可以管理一段连续空间
C.unique_ptr不能使用其拷贝构造函数
D.unique_ptr的对象之间不能相互赋值

答案:B
A:正确
B:错误,C++11中提供的智能指针都只能管理单个对象的资源,没有提供管理一段空间资源的智能指针
C:正确,因为unique_ptr中已经将拷贝构造函数和赋值运算符重载delete了
D:正确,原因同C

8. 下面关于auto_ptr的说法错误的是()

A.auto_ptr智能指针是在C++98版本中已经存在的
B.auto_ptr的多个对象之间,不能共享资源
C.auto_ptr的实现原理是资源的转移
D.auto_ptr完全可以正常使用

答案:D
A:正确
B:正确,因为auto_ptr采用资源管理权转移的方式实现的,比如:用ap1拷贝构造ap2时,ap1中的资源会转移给ap2,而ap1与资源断开联系
C:正确
D:错误,可以使用,但是不建议用,因为有缺陷,标准委员会建议:什么情况下都不要使用auto_ptr
 

评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值