异常和智能指针

 异常

yassert旨在debug版本下生效,release版本下是无效的。因为assert这种比较粗暴的错误在debug版本下就应该被发现,release版本应该已经过滤了这类的错误了。

错误码只是给错误编号,信息太简单了,所以c++引入了throw catch。

thrown抛出异常,catch和try捕获异常。catch只能catch try里面的东西。

异常是通过抛出对象而引发的,该对象的类型决定了应该激活哪个catch的处理代码

被选中的处理代码是调用链中与该对象类型匹配且离抛出异常位置最近(不会考虑子类还是父类的Exception哪个更匹配的问题,主要看离哪个catch更近一些)的那一个。

double Division(int a, int b)
/{
	// 当b == 0时抛出异常
	if (b == 0)
		throw "Division by zero condition!";//直接跳到catch的地方,catch在哪里就跳到哪里。这里也就是Func函数,跳的是离thrown更近的这一个。如果throw但是没有匹配的catch就直接程序报错。
        //异常捕获的地方可以在division、Func、main三个函数的任意一个当中,习惯在main函数处进行捕获。
	else
		return ((double)a / (double)b);
}

void Func()
{
	int len, time;
	cin >> len >> time;

	try
	{
		cout << Division(len, time) << endl;
	}
	catch (const char* str)
	{
		cout << str << endl;
	}

	cout << "void Func()" << endl;
}

int main()
{
	try
	{
    	Func();
	}
	catch (const char* str)//抛出来的对象和次数catch的参数要匹配
	{
		cout << str << endl;
	}

	return 0;
}

异常是通过抛出对象而引发的,该对象的类型决定了应该激活哪个catch的处理代码。被选中的处理代码是调用链中与该对象类型匹配且离抛出异常位置最近的那一个。

class Exception
{
public:
	Exception(int errid, const string& msg)
		:_errid(errid)
		, _errmsg(msg)
	{}

	const string& GetMsg() const//获取错误描述
	{
		return _errmsg;
	}

	int GetErrid() const//获取错误码
	{
		return _errid;
	}

private:
	int _errid;     // 错误码
	string _errmsg; // 错误描述,
};
double Division(int a, int b)
{
	// 当b == 0时抛出异常
	if (b == 0)
	{
		//Exception err(1, "除0错误");
		//throw err;//catch捕获的err是err的拷贝,因为err出了作用域就销毁了。 抛出异常对象后,会生成一个异常对象的拷贝,因为抛出的异常对象可能是一个临时对象,所以会生成一个拷贝对象,这个拷贝的临时对象会在被catch以后销毁。(这里的处理类似于函数的传值返回)
		throw Exception(1, "除0错误");//可以用一行来替代
	}
	else
	{
		return ((double)a / (double)b);
	}
}
void Func()
{
	int len, time;
	cin >> len >> time;
	cout << Division(len, time) << endl;
}
int main()
{
	while (1)
	{
		try
		{
			Func();
		}
		catch (const Exception& e)//上面throw的e对象由于是局部变量,出了作用域就销毁了,所以和这里的catch的e对象不是同一个e对象。
		{
			cout << e.GetMsg() << endl;
		}
		catch (...) // 放到最后,捕获任意类型的异常,防止有一些异常没捕获,导致程序直接终止。当这里捕获到位置异常的时候,程序不会终止,因为是一个while(1)死循环,也就是进入下一轮的Func()。
		{
			cout << "未知异常" << endl;
		}
	}

	return 0;
}
template<class T>
void Func(T&& x)//模板才有万能引用,因为模板是有一个推演的过程,左值就折叠成为左值引用,而右值就是右值引用。

关于异常的常规玩法

实际中抛出和捕获的匹配原则有个例外,并不都是类型完全匹配,可以抛出的派生类对象, 使用基类捕获。借助继承和多态来实现。

class Exception
{
public:
	Exception(int errid, const string& msg)
		:_errid(errid)
		, _errmsg(msg)
	{}

	virtual string what() const
	{
		return _errmsg;
	}

	int GetErrid() const
	{
		return _errid;
	}

protected:
	int _errid;     // 错误码
	string _errmsg; // 错误描述
};

class SqlException : public Exception//继承
{
public:
	SqlException(int errid, const string& msg, const string& sql)
		:Exception(errid, msg)
		, _sql(sql)
	{}

	virtual string what() const//采用多态
	{
		string msg = "SqlException:";
		msg += _errmsg;
		msg += "->";
		msg += _sql;

		return msg;
	}

protected:
	string _sql;
};

class CacheException : public Exception//继承
{
public:
	CacheException(const string& errmsg, int id)
		:Exception(id, errmsg)
	{}

	virtual string what() const//采用多态
	{
		string msg = "CacheException:";
		msg += _errmsg;

		return msg;
	}
};

class HttpServerException : public Exception//继承
{
public:
	HttpServerException(const string& errmsg, int id, const string& type)
		:Exception(id, errmsg)
		, _type(type)
	{}

	virtual string what() const//采用多态
	{
		string msg = "HttpServerException:";
		msg += _errmsg;
		msg += "->";
		msg += _type;

		return msg;
	}

private:
	const string _type;//什么类型的错误
};


void SQLMgr()//第三步
{
	srand(time(0));
	if (rand() % 7 == 0)
	{
		throw SqlException(100, "权限不足", "select * from name = '张三'");
	}

	cout << "调用成功" << endl;//数据库没出错这里就打印成功
}

void CacheMgr()//第二步
{
	srand(time(0));
	if (rand() % 5 == 0)
	{
		throw CacheException("权限不足", 100);
	}
	else if (rand() % 6 == 0)
	{
		throw CacheException("数据不存在", 101);
	}

	SQLMgr();//缓存没有出错就调用数据库
}

void HttpServer()//第一步
{
	// 模拟服务器网络出错
	srand(time(0));
	if (rand() % 3 == 0)
	{
		throw HttpServerException("请求资源不存在", 100, "get");
	}
	else if (rand() % 4 == 0)
	{
		throw HttpServerException("权限不足", 101, "post");
	}

	CacheMgr();//网络层没有出错误就调用缓存
}
int main()
{
	while (1)
	{
		this_thread::sleep_for(chrono::seconds(1));

		try
		{
			HttpServer();//该函数接口下面还调用了CacheMgr()和SQLMgr()
		}
		catch (const Exception& e) // 这里捕获父类对象就可以,如果没有多态,捕捉到了父类对象也是非常无助的,因为无法进一步确定错误。
		{
			// 多态
			cout << e.what() << endl;
		}
		catch (...)
		{
			cout << "Unkown Exception" << endl;
		}
	}
	return 0;
}

异常的重新抛出 

void Func()
{	
	int* array = new int[10];
	int len, time;
	cin >> len >> time;
	cout << Division(len, time) << endl;// 这里可以看到如果发生除0错误抛出异常,程序会直接跳到catch导致下面的array没有得到释放,也就是内存泄漏。
	cout << "delete []" << array << endl;
	delete[] array;
}
//优化
void Func()
{
	// 所以这里捕获异常后并不处理异常,异常还是交给外面处理,这里捕获了再重新抛出去。
	int* array = new int[10];
	int len, time;
	cin >> len >> time;
	try
	{
		cout << Division(len, time) << endl;
		HttpServer();
	}
	catch (...)   // 异常的重新抛出,并且这里catch的是...,因为不知道Division(len, time)可能抛的是什么异常,...是一种一劳永逸的方法。
	{
		cout << "delete []" << array << endl;
		delete[] array;

		throw;   // 重新抛出异常而不是在这里处理异常,让所有的异常都可以统一在main函数中进行处理,这样内存也不会泄露。虽然是catch...,但是这里捕到什么抛什么的方式让我们可以不用针对每一个异常类型都重写一个catch重抛的函数,
	}
	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;
}

构造函数并不是为对象开空间,而是初始化对象。 最好不要在构造函数中抛出异常,否则可能导致对象不完整或没有完全初始化。析构函数主要完成资源的清理,最好不要在析构函数内抛出异常,否则可能导致资源泄漏。凡是两个步骤的(new/delete,fopen/fclose;lock/unlock)都要小心抛异常,因为可能会导致资源泄漏或者死锁。

异常的优点:1、异常对象定义好了,相比错误码的方式可以清晰准确的展示出错误的各种信息,附带各种你想要的数据,如sql语句。甚至可以包含堆栈调用的信息,这样可以帮助更好的定位程序的bug。2、 返回错误码的传统方式有个很大的问题就是,在函数调用链中,深层的函数返回了错误,那 么我们得层层返回错误,最外层才能拿到错误。3、很多的第三方库都包含异常,比如boost、gtest、gmock等等常用的库,那么我们使用它们 也需要使用异常。 4、部分函数使用异常更好处理,比如构造函数没有返回值,不方便使用错误码方式处理。比如 T& operator这样的函数,如果pos越界了只能使用异常或者assert断言终止程序处理,没办法通过返回 值表示错误。

没有异常之前是跳出循环(break)或者函数(return),有异常以后,可能跳多个函数。

智能指针

int div()
{
	int a, b;
	cin >> a >> b;
	if (b == 0)
		throw invalid_argument("除0错误");
	return a / b;
}
void Func()
{
	// 1、如果p1这里new 抛异常会如何?p1抛异常是没有任何问题的,因为new没有成功也就不涉及释放之类的问题。
	// 2、如果p2这里new 抛异常会如何?需要捕获把p1释放了
	// 3、如果div调用这里又会抛异常会如何?需要捕获把p1和p2释放了
	int* p1 = new int;
	//int* p2 = new int;//p2不能直接这样,需要额外处理一下,否则抛异常的时候会造成内存泄露。
	//cout<<div()<<endl;//同上
    try
	{
		p2 = new int;
	}
	catch (...)
	{
		delete p1;
		throw;
	}

	try
	{
		cout << div() << endl;
	}
	catch (...)
	{
		delete p1;
		delete p2;
		throw;
	}

	delete p1;
	delete p2;
}
//上面针对异常的处理方式太麻烦了于是有了智能指针
// 1、RAII,类似锁的思想用当前指针构造智能指针
// 2、像指针一样使用,也就是需要封装成一个迭代器,可以进行解引用、->等操作。
// 3、拷贝问题,这个处理起来比较棘手。
// 4、RAII是一种利用对象的声明周期来控制资源的技术,拿到一个资源之后不要自己去管,而类对象的构造和析构函数是自动调用的,这些编译器底层有保证的,交给对象的生命周期来管理。
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;
	}
private:
	T* _ptr;
};

void Func()
{
	// 1、如果p1这里new 抛异常会如何?
	// 2、如果p2这里new 抛异常会如何?
	// 3、如果div调用这里又会抛异常会如何?
	SmartPtr<int> sp1(new int(1));//如果这里new抛异常了就不会进入到构造函数当中去
	SmartPtr<int> sp2(new int(2));//如果这里new抛异常了会结束栈帧因为catch要跳转,所以sp1会调用析构函数将sp1释放。

	cout << div() << endl;

	*sp1 = 10;

	cout << *sp1 << endl;
	cout << *sp2 << endl;
}

关于智能指针拷贝构造的问题

 管理权转移:有明显的不足

//管理权转移的方法并不可取
template<class T>
class auto_ptr
{
public:
	auto_ptr(T* ptr)
		:_ptr(ptr)
	{}
	~auto_ptr()
	{
		if (_ptr)
		{
			cout << "delete:" << _ptr << endl;
			delete _ptr;
		}
	}
	// 管理权转移
	auto_ptr(auto_ptr<T>& ap)
		:_ptr(ap._ptr)
	{
		ap._ptr = nullptr;//因为我们不能让ap1和ap2都来管理这个指针,所以ap2(ap1)就是将指针的管理权限重ap1交到了ap2手中,ap1析构的时候delete nullptr是没有问题的。
	}
	T& operator*()
	{
		return *_ptr;
	}
	T* operator->()
	{
		return _ptr;
	}
private:
	T* _ptr;
};
void test_auto()
{
	auto_ptr<int> ap1(new int(1));
	auto_ptr<int> ap2(ap1);
	*ap1 = 1; // 管理权转移以后导致ap1悬空,不能访问
	*ap2 = 1;
}
//采用库里面已经封装号的auto_ptr
#include "SmartPtr.h"
#include <memory>

void test_auto()
{
	std::auto_ptr<int> ap1(new int(1));
	std::auto_ptr<int> ap2(ap1);
	*ap1 = 1; // 管理权转移以后导致ap1悬空,不能访问。
	*ap2 = 1;
}
//采用unique_ptr也就是直接简单粗暴的让这个类不能发生拷贝构造,同时将赋值运算符重载也封了更加的保险。不需要拷贝的场景用它就行。
class unique_ptr
{
public:
	unique_ptr(T* ptr)
		:_ptr(ptr)
	{}
	~unique_ptr()
	{
		if (_ptr)
		{
			cout << "delete:" << _ptr << endl;
			delete _ptr;
		}
	}
	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;
	// 防拷贝
	// 拷贝构造和赋值是默认成员函数,我们不写会自动生成,所以我们必须写
	// C++98思路:只声明不实现,但是用的人可能会在外面强行进行实现,也就是类成员函数的实现写在类外,所以再加一条,声明为私有,无法类外实现。
private:
	//unique_ptr(const unique_ptr<T>& up);
	// unique_ptr<T>& operator=(const unique_ptr<T>& up);
private:
	T* _ptr;
};
void test_unique()
{
	unique_ptr<int> up1(new int(1));
	//unique_ptr<int> up2(up1);//改行会报错
}
//库里面的玩法和上述的一样
void test_unique()
{
	std::unique_ptr<int> up1(new int(1));
	//std::unique_ptr<int> up2(up1);
}
//share_ptr是共享指针,通过计数来实现指针的拷贝
template<class T>
class shared_ptr
{
public:
	shared_ptr(T* ptr = nullptr)//默认构造函数
		:_ptr(ptr)
		, _pcount(new int(1))//资源第一次交给我的时候开辟空间存放引用计数,也就是第一个对象,而第一个对象就是构造函数生成的。
        //static int _count;//不采用静态成员去进行计数是因为静态成员属于这个类的所有对象,这样的话实例化的对象只能管理一份资源,但实际上我们需要多个对象管理多分资源,每个资源配对一个引用计数,所以采用上面的方案来管理计数。
		, _pmtx(new mutex)
	{}
	~shared_ptr()
	{
		Release();//析构函数不再采用显示调用的方式,而是封装到release里面。
	}
	void Release()//将引用减减封装起来
	{
		_pmtx->lock();
		if (--(*_pcount) == 0)
		{
			cout << "delete:" << _ptr << endl;
			delete _ptr;//不仅要释放资源(delete释放资源的同时会调用其析构函数)
			delete _pcount;//还要释放计数
			// delete _pmtx;  如何走到这一步,锁_pmtx已经释放了,下面还有进行unlock该如何解决?方案一:在这里先进行解锁,然后再释放,最后直接return返回。
            //方案二:给一个布尔值deleteFlag
            
		}
		_pmtx->unlock();
	}
    //方案二如下
    void Release()
	{
		_pmtx->lock();
		bool deleteFlag = false;//默认是false不删除
		if (--(*_pcount) == 0)
		{
			if (_ptr)//如果_ptr为空,就没有释放的必要了。
			{
				//cout << "delete:" << _ptr << endl;
				//delete _ptr;
				// 删除器进行删除
				_del(_ptr);//
			}
			delete _pcount;
			deleteFlag = true;
		}
        _pmtx->unlock();
		if (deleteFlag)
		{
			delete _pmtx;
		}
    }
	void AddCount()//将引用加加封装起来
	{
		_pmtx->lock();
		++(*_pcount);
		_pmtx->unlock();
	}

	shared_ptr(const shared_ptr<T>& sp)
		:_ptr(sp._ptr)
		, _pcount(sp._pcount)
		, _pmtx(sp._pmtx)//全部都是浅拷贝
	{
		AddCount();//拷贝完之后加加计数即可
	}
	// sp1 = sp4
	// sp1 = sp1;
	// sp1 = sp2;
	shared_ptr<T>& operator=(const shared_ptr<T>& sp)
	{
		if (_ptr != sp._ptr)//不要采用传统的this!=&sp这种判断方式,这种的只能防sp1=sp1自己复制自己,而不能防止sp2=sp1这种不同观想但是管理同一份资源的情况。这里采用判断是否管理的是同一份资源。
		{
			Release();//被赋值的成员一定要减减计数
			_ptr = sp._ptr;
			_pcount = sp._pcount;
			_pmtx = sp._pmtx;
			AddCount();//赋值之后一定要加加计数
		}
		return *this;
	}

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

	T* get()
	{
		return _ptr;
	}

	int use_count()
	{
		return *_pcount;
	}
private:
	T* _ptr;
    //static int _count;//引用计数如果采用静态成员变量的话是不行的,因为静态成员变量属于整个类,sp1=sp2、sp3=sp4这两对不同的指针需要公用相同的引用计数是不合规的,不同的指针引用计数需要独立开来。多个对象可能有多个资源,每一个资源应该配对一个引用计数。
	int* _pcount;//解决方案:资源第一次new的时候,也去new一个空间去存放引用计数,这时候构造函数需要修改一下。
	mutex* _pmtx;//锁也得是指针,因为多个智能指针访问的是同一把锁。锁使用来保证多线程当中*_pcount的线程安全的,防止多线程对_pcount进行加加或者减减
};
//测试shared_ptr
//shared_ptr本身是线程安全的,因为计数是加锁保护的
//但是shared_ptr管理的对象是否并不一定是线程安全的
void SharePtrFunc(shared_ptr<Date>& sp, size_t n, mutex& mtx)
{
	//cout << sp.get() << endl;
	for (size_t i = 0; i < n; ++i)
	{
		// 这里智能指针拷贝会++计数,智能指针析构会--计数,这里是线程安全的。
		shared_ptr<Date> copy(sp);
	}
}
void test_shared_safe()
{
	bit::shared_ptr<Date> p(new Date);
	cout << p.get() << endl;
	const size_t n = 10000;
	mutex mtx;
	thread t1(SharePtrFunc, ref(p), n, ref(mtx));//规定:线程对象想要引用传参,必须采用ref这个库函数,不能直接传。
	thread t2(SharePtrFunc, ref(p), n, ref(mtx));
	t1.join();
	t2.join();
	cout << p.use_count() << endl;
}
// shared_ptr本身是线程安全的,因为计数是加锁保护
// shared_ptr管理的对象是否是线程安全?并不是,比如说这里对对象Date的成员变量进行加加,结果并不如意,每次打印的结果都在变。
void SharePtrFunc(std::shared_ptr<Date>& sp, size_t n, mutex& mtx)
{
	//cout << &sp << endl;
	//cout << sp.get() << endl;
	for (size_t i = 0; i < n; ++i)
	{
		// 这里智能指针拷贝会++计数,智能指针析构会--计数,这里是线程安全的。
		std::shared_ptr<Date> copy(sp);

		mtx.lock();//保护share_ptr管理的对象是线程安全的

		sp->_year++;
		sp->_day++;
		sp->_month++;
		mtx.unlock();
	}
}

void test_shared_safe()
{
	//wjj::shared_ptr<Date> p(new Date);
	std::shared_ptr<Date> p(new Date);//直接用标准库里面的

	cout << p.get() << endl;
	//cout << &p << endl;

	const size_t n = 50000;
	mutex mtx;
	thread t1(SharePtrFunc, ref(p), n, ref(mtx));
	thread t2(SharePtrFunc, ref(p), n, ref(mtx));

	t1.join();
	t2.join();

	cout << p.use_count() << endl;

	cout << p->_year << endl;
	cout << p->_month << endl;
	cout << p->_day << endl;
}

 循环引用

struct ListNode
{
	ListNode* _next;
	ListNode* _prev;
	~ListNode()
	{
		cout << "~ListNode()" << endl;
	}
};
//以前的写法
void test_shared_cycle()
{
	ListNode* n1=new ListNode;
	ListNode* n2=new ListNode;
	n1->_next = n2;
	n2->_prev = n1;
    delete n1;//不建议这么写了,因为如果上面两行代码抛异常可能会导致内存泄漏
    delete n2;
}
//有了智能指针之后的写法
void test_shared_cycle()
{
	shared_ptr<ListNode> n1(new ListNode);
	shared_ptr<ListNode> n2(new ListNode);
	n1->_next = n2;//智能指针对象没法给给原生指针,这两行代码走不通。
	n2->_prev = n1;
}
//优化
struct ListNode
{
	
	bit::shared_ptr<ListNode> _next;
	bit::shared_ptr<ListNode> _prev;//虽然这里的优化可以实现n1->_next = n2赋值,但是让next和prev来参与指针的管理会导致循环引用的问题,也就是让n1和n2的引用计数加加。
	int _val;
	~ListNode()
	{
		cout << "~ListNode()" << endl;
	}
};
void test_shared_cycle()
{
	wjj::shared_ptr<ListNode> n1(new ListNode);
	wjj::shared_ptr<ListNode> n2(new ListNode);
	n1->_next = n2;
	n2->_prev = n1;//但有了新的问题就是循环引用,互相钳制,导致内存泄漏。
}
//针对循环引用的问题,有了weak_ptr
// weak_ptr
// 1、他不是常规的智能指针,不支持RAII
// 2、支持像指针一样
// 3、专门设计出来,辅助解决shared_ptr的循环引用问题
//weak_ptr可以指向资源,但是他不参与管理,也就是不增加引用计数
template<class T>
class weak_ptr
{
public:
	weak_ptr()
		:_ptr(nullptr)
	{}
	weak_ptr(const shared_ptr<T>& sp)
		:_ptr(sp.get())
	{}
	T& operator*()
	{
		return *_ptr;
	}

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

	T* get()
	{
		return _ptr;
	}

private:
	T* _ptr;
};

struct ListNode
{
	wjj::weak_ptr<ListNode> _next;//实际上我们并不想要_next参与资源的管理,也就是并不想要其增加资源的引用计数。
	wjj::weak_ptr<ListNode> _prev;//实际上我们并不想要_prev参与资源的管理,也就是并不想要其增加资源的引用计数。
	int _val;
	~ListNode()
	{
		cout << "~ListNode()" << endl;
	}
};

定制删除器

如果我们是采用智能指针封装的是new T[]这种类型的,或者是malloc,我们delete由于写死了,该如何解决呢?

//定制删除器 -- 本质上是一个可调用对象 lamda表达式 函数指针 仿函数
//采用仿函数
template<class T>
struct DeleteArray
{
	void operator()(T* ptr)
	{
		cout << "void operator()(T* ptr)" << endl;
		delete[] ptr;
	}
};

void test_shared_deletor()
{
	std::shared_ptr<Date> spa1(new Date[10], DeleteArray<Date>());//传一个删除器,默认就调用这个删除器去进行释放。
	std::shared_ptr<Date> spa2(new Date[10], [](Date* ptr){
		cout << "lambda delete[]"<<ptr << endl;
		delete[] ptr; 
	});//采用lamda表达式

	std::shared_ptr<FILE> spF3(fopen("Test.cpp", "r"), [](FILE* ptr){
    	cout << "lambda fclose" << ptr << endl;
		fclose(ptr);
	});//这里管理的是一个打开文件的指针,默认还是delete所以这里一定要传一个删除器。
}


void test_shared_deletor()
{
	bit::shared_ptr<Date> sp0(new Date);
	bit::shared_ptr<Date> spa1(new Date[10], DeleteArray<Date>());
	bit::shared_ptr<Date> spa2(new Date[10], [](Date* ptr){
		cout << "lambda delete[]:" << ptr << endl;
		delete[] ptr;
});

		bit::shared_ptr<FILE> spF3(fopen("Test.cpp", "r"), [](FILE* ptr){
			cout << "lambda fclose:" << ptr << endl;
			fclose(ptr);
		});
	}
}


 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值