前言:看这篇文章前需要前置知识“【C++11新特性】:一场编程语言的‘基因改造’”与“C++继承全揭秘:原来编程也能‘拼爹’”,学完这篇你会知道程序出错也能不让他停下来,以及可以彻底解决内存泄漏的方法。智能指针是C++11里的一个很重要的知识点,大家要重点掌握!

目录

一、异常

1. 异常的概念

2. try-throw-catch语句

3. 程序执行过程

4. 查找匹配的处理代码

5. 异常重新抛出

6. noexcept

7. 标准库的异常

8. 异常安全

二、智能指针

1. RAII和智能指针的设计思路

2. C++标准库智能指针的使用及模拟实现(重点)

(1)auto_ptr

① 介绍及使用

② 模拟实现

(2)unique_ptr

① 介绍及使用

② 模拟实现

(3)shared_ptr

① 介绍及使用

② 定制删除器

③ 模拟实现

(4)weak_ptr

① shared_ptr循环引⽤问题

② 介绍及使用

3. boost库介绍

4. 内存泄漏

(1)概念及危害

(2)如何检测内存泄漏(了解)

(3)如何避免内存泄漏



一、异常

1. 异常的概念

      异常处理机制允许程序中独立开发的部分能够在运行时就出现的问题进行通信并做出相应的处理,异常使得我们能够将问题的检测与解决问题的过程分开,程序的一部分负责检测问题的出现,然后解决问题的任务传递给程序的另一部分,检测环节无须知道问题的处理模块的所有细节。

      C语言主要通过错误码的形式处理错误,错误码本质就是对错误信息进行分类编号,拿到错误码以后还要去查询错误信息,比较麻烦。C++异常可以抛出任意类型的对象,携带更丰富的错误信息。

2. try-throw-catch语句

使用 throw 抛出异常,try/catch 块捕获并处理异常,下面是一个简单的例子:

#include <thread> 
double Divide(int a, int b)
{
	try 
	{
		if (b == 0) 
			throw string("Divide by zero condition!");
		return (double)a / b;
	}
	catch (const string& errmsg) 
	{
		cout << errmsg << endl;
	}
}

使用方法:

  • try和catch必须紧挨在一起使用,两者单拎出任何一个都不能使用,它俩中间夹了其他语句也会报错,必须挨在一起,但一个try能有多个catch。
  • 程序出现问题时,我们通过抛出(throw)一个对象来引发一个异常,只要程序遇到 throw 就会抛异常,不管它是不是在 try 里,而抛了异常就会找 catch,没找到就会报错,找到了执行catch里的语句。
  • throw 后面写的是任何类型的表达式或者对象,catch 后面写 throw 能对应的类型,匹配不上就不会进入catch。(throw 就像是调用函数传的参数,catch后面就好像是函数接收的实参)
  • 抛出异常对象后,会生成一个异常对象的拷贝,因为抛出的异常对象可能是一个局部对象,所以会生成一个拷贝对象,这个拷贝的对象会在catch子句后销毁。(这里的处理类似于函数的传值返回)

3. 程序执行过程

        抛出异常后,程序暂停当前函数的执行,开始寻找与之匹配的catch子句,首先检查throw本身是否在try块内部,如果在,则查找匹配的catch语句,如果有匹配的,则跳到catch的地方进行处理。如果当前函数中没有try/catch子句,或者有try/catch子句但是类型不匹配,则退出当前函数,继续在外层调用函数链中查找,上述查找的catch过程被称为栈展开。如果到达main函数,依旧没有找到匹配的catch子句,程序会调用标准库的 terminate 函数终止程序如果找到匹配的catch子句处理后,catch子句代码会继续执行。

void test1()
{
	try
	{
		cout << "执行1" << endl;
		throw "error";
		cout << "执行2" << endl;
	}
	//catch(const char* e)
	catch (int t)
	{
		cout << "执行3" << endl;
	}

	cout << "执行4" << endl;
}

void test2()
{
	try
	{
		test1();
		cout << "执行5" << endl;
	}
	//catch(const char* e)
	catch (int t)
	{
		cout << "执行6" << endl;
	}

	cout << "执行7" << endl;
}
int main()
{
	try 
	{
		test2();
		cout << "执行8" << endl;
	}
	catch(const char* e)
	//catch(int t)
	{
		cout << "执行9" << endl;
	}
	cout << "执行10" << endl;
}

这是一个很好的例子帮助你理解程序是怎么跳跃的。

我在这里演示一个异常在test2中被捕获的:

注意点:

  • throw 以后的语句是没用的,它一定不会被执行,直到异常被捕获了。
  • 程序的执行从throw位置跳到与之匹配的catch模块,catch可能是同一函数中的一个局部的catch,也可能是调用链中另一个函数中的catch,控制权从throw位置转移到了catch位置。这里还有两个重要的含义:沿着调用链的函数可能提早退出;一旦程序开始执行异常处理程序,沿着调用链创建的对象都将销毁。

      只有拥有自动存储期的局部对象才会被销毁。例如,使用 new 在堆上创建的对象不会被自动 delete,这就是为什么强烈建议使用智能指针(它们本身是局部对象,会在析构时释放其管理的堆内存)来管理动态资源。

4. 查找匹配的处理代码

        一般情况下抛出对象和catch是类型完全匹配的,如果有多个类型匹配的,就选择离他位置更近的那个。但是也有一些例外,允许从非常量向常量的类型转换,也就是权限缩小;允许数组转换成指向数组元素类型的指针,函数被转换成指向函数的指针允许从派生类向基类类型的转换,这个点非常实用,实际中继承体系基本都是用这个方式设计的。如果到main函数,异常仍旧没有被匹配就会终止程序,不是发生严重错误的情况下,我们是不期望程序终止的,所以一般main函数中最后都会使用catch(...),它可以捕获任意类型的异常,但是它不知道异常错误是什么。

#include<thread>
#include<iostream>
#include<string>
using namespace std;

class Exception
{
public:
	Exception(const string& errmsg, int id)
		:_errmsg(errmsg)
		, _id(id)
	{}
	virtual string what() const
	{
		return _errmsg;
	}
	int getid() const
	{
		return _id;
	}

protected:
	string _errmsg;
	int _id;
};

class SqlException : public Exception
{
public:
	SqlException(const string& errmsg, int id, const string& sql)
		:Exception(errmsg, id)
		, _sql(sql)
	{}
	virtual string what() const
	{
		string str = "SqlException:";
		str += _errmsg;
		str += "->";
		str += _sql;
		return str;
	}
private:
	const string _sql;
};

class CacheException : public Exception
{

public:
	CacheException(const string& errmsg, int id)
		:Exception(errmsg, id)
	{}
	virtual string what() const
	{
		string str = "CacheException:";
		str += _errmsg;
		return str;
	}
};

class HttpException : public Exception
{
public:
	HttpException(const string& errmsg, int id, const string& type)
		:Exception(errmsg, id)
		, _type(type)
	{}
	virtual string what() const
	{
		string str = "HttpException:";
		str += _type;
		str += ":";
		str += _errmsg;
		return str;
	}

private:
	const string _type;
};

void SQLMgr()
{
	if (rand() % 7 == 0)
	{
		throw SqlException("权限不足", 100, "select * from name = '张三'");
	}
	else
	{
		cout << "SQLMgr 调用成功" << endl;
	}
}
void CacheMgr()
{
	if (rand() % 5 == 0)
	{
		throw CacheException("权限不足", 100);
	}
	else if (rand() % 6 == 0)
	{
		throw CacheException("数据不存在", 101);
	}
	else
	{
		cout << "CacheMgr 调用成功" << endl;
	}
	SQLMgr();
}

void HttpServer()
{
	if (rand() % 3 == 0)
	{
		throw HttpException("请求资源不存在", 100, "get");
	}
	else if (rand() % 4 == 0)
	{
		throw HttpException("权限不足", 101, "post");
	}
	else
	{
		cout << "HttpServer调用成功" << endl;
	}
	CacheMgr();
}

int main()
{
	srand(time(0));
	while (1)
	{
		this_thread::sleep_for(chrono::seconds(1));
		try
		{
			HttpServer();
		}
		catch (const Exception& e) 
		// 这里捕获基类,基类对象和派生类对象传过来都可以被捕获,这叫切片
		{
		cout << e.what() << endl;
		}
		catch (...)
		{
			cout << "Unkown Exception" << endl;
		}
	}
	return 0;
}

为什么说允许从派生类向基类类型的转换很方便呢?

        一个大项目分好几个人写,那每个人都要写自己的异常,那最后捕获的时候多麻烦,一个一个写它们的异常类型吗?那这时候我每个人都写一个用来捕获异常的类,它们都继承一个基类,那最后写的时候,只捕获基类不就可以了,向上面的例子一样。

5. 异常重新抛出

       有时catch到一个异常对象后,需要对错误进行分类,其中的某种异常错误需要进行特殊的处理,其他错误则重新抛出异常给外层调用链处理。捕获异常后需要重新抛出,直接 throw;就可以把捕获的对象直接抛出。

       比如,我们聊天时发送消息,发送失败了有可能是网络问题,也有可能是对方给你拉黑了,如果是后者,那就直接抛出了,如果是前者,我们希望它多试几次,如果一直不行再抛出。那就需要对这种异常进行特殊处理。

#include <iostream>
#include <string>
#include <ctime>
using namespace std;
//
// 自定义异常基类
class MyException 
{
public:
    MyException(const string& msg,int n) : message(msg), num(n){}
    virtual string what() const { return message; }
    int getid() const { return this->num; }
    virtual ~MyException() = default;
protected:
    string message;
    int num;
};

// 网络异常
class NetworkException : public MyException 
{
public:
    NetworkException() : MyException("网络不稳定,发送失败", 101) {}
};

// 被拉黑异常
class BlockedException : public MyException 
{
public:
    BlockedException() : MyException("你已被对方拉黑,发送失败",102) {}
};

// 模拟发送消息(随机抛出异常)
void sendMessageImpl(const string& message) 
{
    int random = rand() % 5;  

    if (random == 0) 
        throw NetworkException();  // 网络问题
    else if (random == 1)
        throw BlockedException();  // 被拉黑
    else 
        cout << "消息发送成功: " << message << endl;
}

// 智能发送消息(网络问题重试,拉黑直接抛出)
void sendMessage(const string& message) 
{
    for (int i = 0; i < 3; i++)
    {
        try
        {
            sendMessageImpl(message);
            return;
        }
        catch (const MyException& e)
        {
            if (e.getid() == 101)
            {
                if (i == 3)
                    throw;
                cout << "开始第" << i + 1 << "重试" << endl;
            }
            else  throw;
        }
    }
}

int main()
{
    srand(time(0));
    string str;
    while (cin >> str)
    {
        try
        {
            sendMessage(str);
        }
        catch (const MyException& e)
        {
            cout << e.what() << endl << endl;
        }
        catch (...)
        {
            cout << "Unkown Exception" << endl;
        }
    }
    return 0;
}

6. noexcept

        对于用户和编译器而言,预先知道某个程序会不会抛出异常大有裨益,知道某个函数是否会抛出异常有助于简化调用函数的代码。在C++11中,函数参数列表后面加noexcept表示不会抛出异常,啥都不加表示可能会抛出异常。

  • 编译器并不会在编译时检查noexcept,也就是说如果一个函数用noexcept修饰了,但是同时又包含了throw语句或者调用的函数可能会抛出异常,编译器还是会顺利编译通过的(有些编译器可能会报个警告)。但是一个声明了noexcept的函数抛出了异常,程序会调用 terminate 终止程序。

  • noexcept(expression)还可以作为一个运算符去检测一个表达式是否会抛出异常,可能会则返回false,不会就返回true。(它不会去真正检测)

double Divide(int a, int b) noexcept
{
	// 当b == 0时抛出异常 
	if (b == 0)
	{
		throw "Division by zero condition!";
	}
	return (double)a / (double)b;
}

int main()
{
	try
	{
		int len, time;
		cin >> len >> time; //(9 8->正常执行,9 0->中止程序)
		cout << Divide(len, time) << endl;
	}
	catch (const char* errmsg)
	{
		cout << errmsg << endl;
	}
	catch (...)
	{
		cout << "Unkown Exception" << endl;
	}

	int i = 0;
	cout << noexcept(Divide(1, 2)) << endl;   //1
	cout << noexcept(Divide(1, 0)) << endl;   //1
	cout << noexcept(++i) << endl;            //1
	return 0;
}

7. 标准库的异常

        C++标准库也定义了一套自己的一套异常继承体系库,基类是exception,所以我们日常写程序,需要在主函数捕获exception即可,要获取异常信息,调用what函数,what是一个虚函数,派生类可以重写。

int main() {
    std::vector<int> numbers = { 1, 2, 3 };

    try 
    {
        std::cout << numbers.at(4) << std::endl;
    }
    //catch (const std::out_of_range& e) {
    //    std::cout << e.what() << std::endl;
    //}
    catch (const std::exception& e) {
        std::cout << e.what() << std::endl;  //invalid vector subscript
    }

    return 0;
}

8. 异常安全

  • 异常抛出后,后面的代码就不再执行,前面申请了资源(内存、锁等),后面进行释放,但是中间可能会抛弃常就会导致资源没有释放,这里由于异常就引发了资源泄漏,产生安全性的问题。中间我们需要捕获异常,释放资源后面再重新抛出,当然后面智能指针讲的RAII方式解决这种问题是更好的。

  • 其次析构函数中,如果抛出异常也要谨慎处理,比如析构函数要释放10个资源,释放到第5个时抛出异常,则也需要捕获处理,否则后面的5个资源就没释放,也资源泄漏了。《Effective C++》第8个条款也专门讲了这个问题,别让异常逃离析构函数。

double Divide(int a, int b)
{
	// 当b == 0时抛出异常 
	if (b == 0)
	{
		throw "Division by zero condition!";
	}
	return (double)a / (double)b;
}

void Func()
{
	// 这里可以看到如果发生除0错误抛出异常,另外下面的array没有得到释放。 
	// 所以这里捕获异常后并不处理异常,异常还是交给外层处理,这里捕获了再 
	// 重新抛出去。 
	int* array = new int[10];
	try
	{
		int len, time;
		cin >> len >> time;
		cout << Divide(len, time) << endl;
	}
	catch (...)
	{
		// 捕获异常释放内存 
		cout << "delete []" << array << endl;
		delete[] array;

			throw; // 异常重新抛出,捕获到什么抛出什么 
	}
	cout << "delete []" << array << endl;
	delete[] array;
}

int main()
{
	try
	{
		Func();
	}
	catch (const char* errmsg)
	{
		cout << errmsg << endl;
	}
	catch (const exception& e)
	{
		cout << e.what() << endl;
	}
	catch (...)
	{
		cout << "Unkown Exception" << endl;
	}
	return 0;
}

是很挫吧,好,下面我们来讲智能指针!

二、智能指针

1. RAII和智能指针的设计思路

(1)RAII是Resource Acquisition Is Initialization的缩写,他是一种管理资源的类的设计思想,本质是一种利用对象生命周期来管理获取到的动态资源,避免资源泄漏,这里的资源可以是内存、文件指针、网络连接、互斥锁等等。RAII在获取资源时把资源委托给一个对象,接着控制对资源的访问,资源在对象的生命周期内始终保持有效,最后在对象析构的时候释放资源,这样保障了资源的正常释放,避免资源泄漏问题。

(2)智能指针类除了满足RAII的设计思路,还要方便资源的访问,所以智能指针类还会想迭代器类一样,重载 operator*/operator->/operator[] 等运算符,方便访问资源。

(3)智能指针可以有效的解决内存泄漏,而异常安全只是可能导致内存泄漏的其中一种情况。

#include <iostream>
#include <string>
using namespace std;

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

	~SmartPtr()
	{
		cout << "delete[] " << _ptr << endl;
		delete[] _ptr;
	}

	// 重载运算符,模拟指针的行为,方便访问资源 
	T& operator*()
	{
		return *_ptr;
	}
	T* operator->()
	{
		return _ptr;
	}
	T& operator[](size_t i)
	{
		return _ptr[i];
	}

private:
	T* _ptr;
};

double Divide(int a, int b)
{
	if (b == 0)
		throw "Divide by zero condition!";
	else
		return (double)a / (double)b;
}

void Func()
{
	// 这里使用RAII的智能指针类管理new出来的数组以后,程序简单多了 
	SmartPtr<int> sp1 = new int[10];
	SmartPtr<int> sp2 = new int[10];

	for (size_t i = 0; i < 10; i++)
	{
		sp1[i] = sp2[i] = i;
	}
	int len, time;
	cin >> len >> time;
	cout << Divide(len, time) << endl;
}

int main()
{
	try
	{
		Func();
	}
	catch (const char* errmsg)
	{
		cout << errmsg << endl;
	}
	catch (const exception& e)
	{
		cout << e.what() << endl;
	}
	catch (...)
	{
		cout << "未知异常" << endl;
	}
	return 0;
}

2. C++标准库智能指针的使用及模拟实现(重点)

        C++标准库中的智能指针都在<memory>这个头文件下面,我们包含<memory>就可以是使用了,智能指针有好几种,除了weak_ptr他们都符合RAII和像指针一样访问的行为,原理上而言主要是解决智能指针拷贝问题时的思路不同。

智能指针在拷贝(赋值)时为什么会有问题?

        智能指针我们还是想实现指针的功能,所以拷贝的时候一定是浅拷贝,不是深拷贝,因为我拷贝后肯定是希望两个对象指向同一个资源,不可能是新开一个对象,但这样当我析构时,就可能不止析构一次,但一块内存只能被析构一次,这就有问题了。

(1)auto_ptr

① 介绍及使用

      auto_ptr是C++98时设计出来的智能指针,他的特点是拷贝时把被拷贝对象的资源的管理权转移给拷贝对象,这是一个非常糟糕的设计,因为他会导致被拷贝对象悬空,访问报错的问题,C++11设计出新的智能指针后,强烈建议不要使用auto_ptr。其他C++11出来之前很多公司也是明令禁止使用这个智能指针的。

int main() {
    // 创建 auto_ptr
    auto_ptr<int> ap1(new int(42));
    cout << "ap1: " << *ap1 << endl;  // 输出 42
    
    // 拷贝构造 - 所有权转移
    auto_ptr<int> ap2(ap1);
    
    cout << "ap2: " << *ap2 << endl;  // 输出 42
    // cout << "ap1: " << *ap1 << endl;  // 运行时错误!ap1 已是空指针
    
    return 0;
}
② 模拟实现
namespace DaYuanTongXue
{
	template<class T>
	class auto_ptr
	{
	public:
		auto_ptr(T* ptr = nullptr)
			: _ptr(ptr)
		{}

		~auto_ptr()
		{
			delete _ptr;
		}

		auto_ptr(auto_ptr<T>& sp)
			: _ptr(sp._ptr)
		{
			sp._ptr = nullptr; 
		}

		auto_ptr<T>& operator=(auto_ptr<T>& sp)
		{
			if (this != &sp)
			{
				if (_ptr) delete _ptr;
				_ptr = sp._ptr;
				sp._ptr = nullptr;
			}
			return *this;
		}

		T operator*()
		{
			return *_ptr;
		}

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

	private:
		T* _ptr;
	};
}

(2)unique_ptr

① 介绍及使用

       unique_ptr是C++11设计出来的智能指针,他的名字翻译出来是唯一指针,他的特点是不支持拷贝,只支持移动。如果不需要拷贝的场景就非常建议使用他。但是使用移动语义也有可能使指针悬空,但这不是unique_ptr的错。

int main()
{
	unique_ptr<int> ptr1(new int(42));
	unique_ptr<string> ptr2(new string("Hello"));
	unique_ptr<vector<int>> ptr3(new vector<int>());
	unique_ptr<int> ptr4;

	cout <<  *ptr1 << endl;
	cout << ptr1.get() << endl;  // 获取原始指针

	//重载operator bool(自定义类型向内置类型的转换)
	cout << "ptr1是否为空: " << (ptr1 ? "否" : "是") << endl;
	cout << "ptr4是否为空: " << (!ptr4 ? "是" : "否") << endl;

	unique_ptr<string> ptr5 = move(ptr2);
    // unique_ptr<int> up2 = up1;        //编译错误!
	return 0;
}
② 模拟实现

(我们实现的都是最简单的版本,实际远远比这复杂)

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

	~unique_ptr()
	{
		delete _ptr;
	}

	unique_ptr(unique_ptr<T>& sp) = delete;
	unique_ptrr<T>& operator=(unique_ptr<T>& sp) = delete;

	unique_ptr(unique_ptr<T>&& sp)
		: _ptr(sp._ptr)
	{
		sp._ptr = nullptr;
	}

	unique_ptr<T>& operator=(unique_ptr<T>&& sp)
	{
		if (this != &sp)
		{
			if (_ptr) delete _ptr;
			_ptr = sp._ptr;
			sp._ptr = nullptr;
		}
		return *this;
	}

	T operator*()
	{
		return *_ptr;
	}

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

private:
	T* _ptr;
};

(3)shared_ptr

① 介绍及使用

        shared_ptr是C++11设计出来的智能指针,他的名字翻译出来是共享指针,他的特点是支持拷贝,也支持移动。如果需要拷贝的场景就需要使用他了。底层是用引用计数的方式实现的。

struct Date
{
	int _year;
	int _month;
	int _day;
	Date(int year = 1, int month = 1, int day = 1)
		:_year(year)
		, _month(month)
		, _day(day)
	{}

	~Date()
	{
		cout << "~Date()" << endl;
	}
};

void shared_ptr_test()
{
	shared_ptr<Date> sp1 = make_shared<Date>(2025, 11, 28);
    cout << sp1->_year << endl;
	auto sp2 = make_shared<int>(100);
	auto sp3 = make_shared<vector<int>>();
    auto a = move(sp2);
	if (sp3) 
		cout << "sp3管理着资源" << endl;
	// make_shared 比直接构造更高效(单次内存分配)
	shared_ptr<Date> sp4(new Date(2025, 11, 28));  
	cout << "sp1引用计数: " << sp1.use_count() << endl;  // 1
	auto sp5 = sp1;  // 拷贝构造
	auto sp6 = sp1;  // 拷贝构造
	cout << "三次拷贝后引用计数: " << sp1.use_count() << endl;  // 3

	shared_ptr<Date> sp7 = make_shared<Date>(2025, 11, 28);
	sp7 = sp1;  // 拷贝赋值,同时原来的sp7被析构
	cout << "赋值后引用计数: " << sp1.use_count() << endl;  // 4

	sp1 = sp4;
	cout << "赋其它值后引用计数: " << sp1.use_count() << endl;  // 4
	//析构sp4的资源
	//析构sp1的资源
}

/*
2025
sp3管理着资源
sp1引用计数: 1
三次拷贝后引用计数: 3
~Date()
赋值后引用计数: 4
赋其它值后引用计数: 2
~Date()
~Date()
*/

智能指针的构造函数被声明为 explicit,禁止隐式类型转换。

//报错
shared_ptr<Date> sp5 = new Date(2024, 9, 11);
unique_ptr<Date> sp6 = new Date(2024, 9, 11);

它们试图将尝试将 Date* 隐式转换为 shared_ptr<Date>/unique_ptr<Date>

// 如果没有 explicit,这种危险代码会编译通过:
void process(shared_ptr<Date> ptr) {
    // 管理资源
}
Date* raw_ptr = new Date(2024, 9, 11);
process(raw_ptr);  // 如果允许隐式转换,raw_ptr 会被两个地方管理!
delete raw_ptr;    // 重复释放!
② 定制删除器

       智能指针析构时默认是进行delete释放资源,这也就意味着如果不是new出来的资源,交给智能指针管理,析构时就会崩溃。智能指针支持在构造时给一个删除器,所谓删除器本质就是一个可调用对象,这个可调用对象中实现你想要的释放资源的方式,当构造智能指针时,给了定制的删除器,在智能指针析构时就会调用删除器去释放资源。(仅仅是释放资源,代替了delete,和引用计数无关)。

因为new[ ]经常使用,所以为了简洁一点,unique_ptr和shared_ptr都特化了一份[ ]的版本,这样使用就可以管理new[]的资源:

unique_ptr<Date[]> up1(new Date[5]); 
shared_ptr<Date[]> sp1(new Date[5]);

这两个删除器的方法还不统一,unique_ptr 要在类模板上传,而shared_ptr要在函数模板上传,前者用起来比较别扭,建议用传仿函数的方法,具体看下面的例子。

void del()
{
	unique_ptr<Date[]> up1(new Date[5]);
	shared_ptr<Date[]> sp1(new Date[5]);

	// 仿函数对象做删除器 
	unique_ptr<Date, DeleteArray<Date>> up2_1(new Date[5], DeleteArray<Date>());
	unique_ptr<Date, DeleteArray<Date>> up2_2(new Date[5]);
	shared_ptr<Date> sp2(new Date[5], DeleteArray<Date>());

	// 函数指针做删除器  
	unique_ptr<Date, void(*)(Date*)> up3(new Date[5], DeleteArrayFunc<Date>);
	shared_ptr<Date> sp3(new Date[5], DeleteArrayFunc<Date>);

	// lambda表达式做删除器 
	auto delArrOBJ = [](Date* ptr) {delete[] ptr; };
	unique_ptr<Date, decltype(delArrOBJ)> up4(new Date[5], delArrOBJ);
	shared_ptr<Date> sp4(new Date[5], delArrOBJ);

	// 实现其他资源管理的删除器 
	shared_ptr<FILE> sp5(fopen("Test.cpp", "r"), Fclose());
	shared_ptr<FILE> sp6(fopen("Test.cpp", "r"), [](FILE* ptr) {
		cout << "fclose:" << ptr << endl;
		fclose(ptr);
		});

}
③ 模拟实现

知识要点:

  • 引用计数设计:引用计数必须在堆上,供所有拷贝共享;不能用静态成员,否则所有 shared_ptr 都会共享同一个计数。多个 shared_ptr 指向资源时就++引用计数, shared_ptr 对象析构时就--引用计数,引用计数减到0时代表当前析构的shared_ptr是最后一个管理资源的对象,则析构资源。

  • 不用模板参数 D 来声明成员变量 _del,而是用 std::function<void(T*)>,否则那就要写到最外面了的类上了,和 unique_ptr 一样了。

  • 检查自赋值,不能只检查相同对象,还要检查拷贝后的对象,这一点很重要。

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

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

	void release()
	{
		(*_pcount)--;
		if ((*_pcount) == 0)
		{
			_del(_ptr);
			delete _pcount;
			_ptr = nullptr;
			_pcount = nullptr;

		}
	}

	~shared_ptr()
	{
		release();
	}

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

	shared_ptr<T>& operator=(const shared_ptr<T>& sp)
	{
		if (this->_ptr != sp->_ptr)
		{
			release();

			_ptr = sp._ptr;
			_pcount = sp._pcount;
			_del = sp._del;
			++(*_pcount);
		}
		return *this;
	}

	int use_count() const
	{
		return *_pcount;
	}

	T operator*()
	{
		return *_ptr;
	}

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

private:
	T* _ptr;
	int* _pcount;
	std::function<void(T*)> _del = [](T* ptr) {delete ptr};
};

(4)weak_ptr

       shared_ptr大多数情况下管理资源非常合适,支持RAII,也支持拷贝。但是在循环引用的场景下会导致资源没得到释放内存泄漏,所以我们要认识循环引用的场景和资源没释放的原因,并且学会使用weak_ptr解决这种问题。

① shared_ptr循环引⽤问题
struct ListNode {
	int data;
	shared_ptr<ListNode> next;
	shared_ptr<ListNode> prev;  // 相互引用!
	ListNode(int val) : data(val) 
	{
		cout << "ListNode构造: " << data << endl;
	}
	~ListNode() 
	{
		cout << "ListNode析构: " << data << endl;
	}
};

void circularReferenceDemo() 
{
	// 创建两个节点
	auto node1 = make_shared<ListNode>(1);
	auto node2 = make_shared<ListNode>(2);

	cout << "创建后引用计数:" << endl;
	cout << "node1: " << node1.use_count() << endl;  // 1
	cout << "node2: " << node2.use_count() << endl;  // 1

	// 建立双向链接 - 形成循环引用
	node1->next = node2;  // node2 引用计数: 1 → 2
	node2->prev = node1;  // node1 引用计数: 1 → 2

	cout << "建立链接后引用计数:" << endl;
	cout << "node1: " << node1.use_count() << endl;  // 2
	cout << "node2: " << node2.use_count() << endl;  // 2

} 
// 期待释放,但实际不会!

  • ListNode1.next 是 ListNode1 的成员变量
  • 只有 ListNode1 被释放时,ListNode1.next 才会析构
  • 但 ListNode1 被 ListNode2.prev 引用着,无法释放
  • 同理,ListNode2 被 ListNode1.next 引用着,也无法释放

至此逻辑上成功形成回旋镖似的循环引用,谁都不会释放就形成了循环引用,导致内存泄漏

② 介绍及使用

       weak_ptr不支持RAII,也不支持访问资源,所以我们看文档发现weak_ptr构造时不支持绑定到资源,只支持绑定到shared_ptr,绑定到shared_ptr时,不增加shared_ptr的引用计数,那么就可以解决上述的循环引用问题,即把ListNode结构体中的_next和_prev改成weak_ptr,weak_ptr绑定到shared_ptr时不增加它的引用计数,_next和_prev不参与资源释放管理逻辑,就成功打破了循环引用,解决了这里的问题。

        weak_ptr也没有重载operator*和operator->等,因为他不参与资源管理,那么如果他绑定的shared_ptr已经释放了资源,那么他去访问资源就是很危险的。weak_ptr支持expired检查指向的资源是否过期,use_count也可获取shared_ptr的引用计数,weak_ptr想访问资源时,可以调用lock(涉及多线程)返回一个管理资源的shared_ptr,如果资源已经被释放,返回的shared_ptr是一个空对象,如果资源没有释放,则通过返回的shared_ptr访问资源是安全的。

int main()
{
	std::shared_ptr<string> sp1(new string("111111"));
	std::shared_ptr<string> sp2(sp1);
	std::weak_ptr<string> wp = sp1;
	cout << wp.expired() << endl;
	cout << wp.use_count() << endl;
	// sp1和sp2都指向了其他资源,则weak_ptr就过期了 
	sp1 = make_shared<string>("222222");
	cout << wp.expired() << endl;
	cout << wp.use_count() << endl;
	sp2 = make_shared<string>("333333");
	cout << wp.expired() << endl;
	cout << wp.use_count() << endl;
	wp = sp1;
	//std::shared_ptr<string> sp3 = wp.lock();

	auto sp3 = wp.lock();
	cout << wp.expired() << endl;
	cout << wp.use_count() << endl;
	*sp3 += "###";
	cout << *sp1 << endl;

	return 0;
}

3. boost库介绍

Boost库是一个高质量的、开源的、跨平台的C++库集合,被认为是C++标准库的延伸和试验场。

4. 内存泄漏

(1)概念及危害

什么是内存泄漏?

       内存泄漏指因为疏忽或错误造成程序未能释放已经不再使用的内存,一般是忘记释放或者发生异常释放程序未能执行导致的。内存泄漏并不是指内存在物理上的消失,而是应用程序分配某段内存后,因为设计错误,失去了对该段内存的控制,因而造成了内存的浪费。

内存泄漏的危害:

       普通程序运行一会就结束了出现内存泄漏问题也不大,进程正常结束,页表的映射关系解除,物理内存也可以释放。长期运行的程序出现内存泄漏,影响很大,如操作系统、后台服务、长时间运行的客户端等等,不断出现内存泄漏会导致可用内存不断变少,各种功能响应越来越慢,最终卡死。

int main()
{
   // 申请一个1G未释放,这个程序多次运行也没啥危害 
   // 因为程序马上就结束,进程结束各种资源也就回收了 
   char* ptr = new char[1024 * 1024 * 1024];
   cout << (void*)ptr << endl;
   return 0;
}

(2)如何检测内存泄漏(了解)

(3)如何避免内存泄漏

  • 工程前期良好的设计规范,养成良好的编码规范,申请的内存空间记着匹配的去释放。ps:这个理想状态。但是如果碰上异常时,就算注意释放了,还是可能会出问题。需要下一条智能指针来管理才有保证。

  • 尽量使用智能指针来管理资源,如果自己场景比较特殊,采用RAII思想自己造个轮子管理。

  • 定期使用内存泄漏工具检测,尤其是每次项目快上线前,不过有些工具不够靠谱,或者是收费

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



后记:智能指针在面试笔试中都比较常考,尤其是shared_ptr,其实像继承、多态、异常、智能指针等等,这些都是在一些项目中才会使用的。我们C++的学习终于要接近尾声了,接下来还有一些扩展内容,如果有帮助,麻烦大家点个小心心吧!

Logo

有“AI”的1024 = 2048,欢迎大家加入2048 AI社区

更多推荐