C++之异常&智能指针&其他
- 异常
- 关于函数异常声明
- 异常的优劣
- 智能指针
- auto_ptr
- unique_ptr
- shared_ptr
- weak_ptr
- 定制删除器
- 智能指针的历史与boost库
- 特殊类
- 单例模式
- 饿汉和懒汉的优缺点
- C++四种类型转换
- C++IO流
- 结语
异常
try括起来的的代码块中可能有throw一个异常(可以是任意类型)的操作,catch可以捕捉throw出来的异常。
- throw出异常后,代码执行会直接跳转到catch的地方。可能引发内存泄漏、文件描述符忘关等严重问题。解决办法如下:
void func()
{int* x = new int;throw x;cout << "delete x" << endl;delete x;//走不到这里
}
int main()
{try{func();}catch (int* y){delete y;cout << "delete y" << endl;}
}
- catch的类型只有和throw的类型匹配,才能被catch到。
- 当有多个catch可以接收异常时,走最近的。比如先catch一个父类,再catch一个子类,抛出一个子类,那就会直接走父类的catch,因为切片可以接收,又是靠前的。
- catch接收的其实是throw的原对象的临时拷贝,与函数传值返回类似,拷贝出来的临时对象在catch接收后销毁,而原对象出作用域销毁。
- catch(…)可以捕获任意异常。通常加在最后,捕获后,程序可以继续运行,代码健壮性提升。
- 通常会在最外层统一捕获、处理异常。所以中间层通常会捕获异常后再抛出去。
下面展示一个异常机制的常见用法:
class Exception
{
public:Exception(int id, const string& msg):_errid(id), _errmsg(msg){}int getId() { return _errid; }string getmsg() { return _errmsg; }virtual string what()const { return _errmsg; }
protected:string _errmsg;int _errid;
};
class SqlException :public Exception
{
public:SqlException(int id, const string& msg):Exception(id, msg){}virtual string what()const{return "SqlException:" + _errmsg;}
};
class NetworkException :public Exception
{
public:NetworkException(int id, const string& msg):Exception(id, msg){}virtual string what()const{return "NetworkException:" + _errmsg;}
};
class HttpException :public Exception
{
public:HttpException(int id, const string& msg, const string& type):Exception(id, msg), _type(type){}virtual string what()const{return "HttpException:" + _type + _errmsg;}
private:string _type;
};
int main()
{try{//run();SqlException se(1, "sqlwrong");throw(se);}catch (const Exception& e)//配合多态{cout << e.what() << endl;}catch (...){cout << "Unknown Exception" << endl;}
}
关于函数异常声明
C++98支持在函数声明后加上throw(),括号里是可能抛出的异常的类型,throw(A,B,C)表示可能抛出A/B/C中某个类型的异常,但如果抛了别的类型,也不会报错,而且不强制让加这个声明,太自由了,没什么人遵守。
C++11支持在函数声明后加上noexcept,如果这个函数不抛异常的话。而且如果它抛异常,哪怕是它本身不抛异常但内部嵌套了一个抛异常的函数,程序执行也会终止。所以noexcept关键字也算好用,但是也不强制写。
异常的优劣
异常机制的优点:
- 相比于c语言的错误码方式,可以增添错误信息,帮助定位bug。
- 对于多层函数调用的错误码要作为返回值层层传递,较为繁琐,而抛异常则直接跳到最外层捕获异常。
- 一些函数只能抛异常来终止程序,比如构造里面new失败。
异常机制的缺点:
- 抛异常会导致执行流乱跳,不利于调试分析。
- C++没有垃圾回收机制,容易导致内存泄漏、死锁及文件描述符泄露等问题。
- C++标准库的异常体系定义的不好,大家都搞了自己的一套,混乱。
总结:异常机制利大于弊,推荐使用,而且要规范使用,比如加上noexcept。
但是呢,针对上面提到的内存泄漏,我们也提出了解决方案,也就是下文的RAII风格的智能指针。
智能指针
RAII(Resource Acquisition Is Initialization,译资源获取即初始化),是一种利用对象生命周期来控制、管理资源的设计风格。具体是,对象构造时获取资源,析构时释放资源,全自动的管控。
智能指针原型smartptr的模拟实现:
//智能指针,支持RAII,可以像指针一样使用
template<class T>
class SmartPtr
{
public:SmartPtr(T*ptr):_ptr(ptr){}T& operator*() { return *_ptr; }T* operator->() { return _ptr; }~SmartPtr() { delete _ptr; }
private:T* _ptr;
};
这样可以防止new后面程序抛异常跳过delete阶段,因为智能指针出作用域析构自动释放指针。
但是,新问题来了,如果有人拷贝了这个智能指针,那它们都指向同一块资源,delete两次会程序崩溃,如何解决?
有人说深拷贝一个不就好了。但是智能指针就是要像指针一样玩,指针拷贝也就是赋值,就是浅拷贝,指向同一块资源,所以排除。
auto_ptr
这时,有大佬提出了一个解决方案,资源管理权转移,也就是auto_ptr的方案:
namespace lky
{template<class T>class auto_ptr{public:auto_ptr(T* ptr) :_ptr(ptr) {}//管理权转移auto_ptr(auto_ptr& ap){_ptr = ap._ptr;ap._ptr = nullptr;}T& operator*() { return *_ptr; }T* operator->() { return _ptr; }~auto_ptr() { delete _ptr; }//delete/free nullptr是没问题的,不放心也可以检查😂private:T* _ptr;};void test_auto(){auto_ptr<int>ap1 = new int(1);auto_ptr<int>ap2(ap1);*ap1 = 2;//管理权转移后,ap1悬空,不能被访问,库里的auto_ptr也是如此*ap2 = 2;}
}
这是C++98就被纳入库中的auto_ptr方案。对于不熟悉它的特性的人去使用它,他是不知道被拷贝的对象悬空的,这时还去使用就崩,这样一个“坑货”就被纳进了标准库。。。它被骂了很多年,很多公司明确规定不能使用auto_ptr。后来,C++11才收录了unique_ptr,shared_ptr和weak_ptr。为什么叫收录呢,在后面boost库再讲。
unique_ptr
实现思路很简单,禁掉拷贝构造还有赋值即可。也就是要么在private声明不实现,要么=delete关键字禁掉。
namespace lky
{template<class T>class unique_ptr{public:unique_ptr(T* ptr) :_ptr(ptr) {}unique_ptr(const unique_ptr&) = delete;unique_ptr& operator=(const unique_ptr&) = delete;T& operator*() { return *_ptr; }T* operator->() { return _ptr; }~unique_ptr() { delete _ptr; }private:T* _ptr;};void test_unique(){unique_ptr<int>up1 = new int(1);unique_ptr<int>up2(up1);//会报错,无法引用已删除的函数}
}
平常不需要指针拷贝的场景,用unique_ptr就行。
但是,如果非要指针拷贝呢?
shared_ptr
于是乎,有了后面的share_ptr通过一个引用计数解决:
namespace lky
{template<class T>class shared_ptr{public:shared_ptr(T* ptr = nullptr):_ptr(ptr), _pcount(new int(1)), _pmtx(new mutex){}shared_ptr(const shared_ptr<T>& sp):_ptr(sp._ptr), _pcount(sp._pcount), _pmtx(sp._pmtx){AddCount();}void Release(){_pmtx->lock();bool DeleteLock = false;if (--*_pcount == 0){if (_ptr){cout << "delete:" << _ptr << endl;delete _ptr;}delete _pcount;DeleteLock = true;}_pmtx->unlock();if (DeleteLock){delete _pmtx;}}void AddCount(){_pmtx->lock();++*_pcount;_pmtx->unlock();}T* get(){return _ptr;}int use_count(){return *_pcount;}shared_ptr<T>& operator=(const shared_ptr<T>& sp){//if (&sp != this)//防不完全,管同一块资源的也要防if (_ptr != sp._ptr){Release();_ptr = sp._ptr;_pcount = sp._pcount;_pmtx = sp._pmtx;AddCount();}return *this;}T& operator*(){return *_ptr;}T* operator->(){return _ptr;}~shared_ptr(){Release();}private:T* _ptr;int* _pcount;mutex* _pmtx;};void test_shared_ptr(){lky::shared_ptr<int>sp1(new int(1));lky::shared_ptr<int>sp2(new int(777));lky::shared_ptr<int>sp3(sp1);lky::shared_ptr<int>sp4(sp1);sp4 = sp1;sp3 = sp2;}struct Date{int _year = 0;int _month = 0;int _day = 0;};void isSharedSafe(shared_ptr<Date>& sp, size_t N, mutex& mtx){//cout << sp.get() << endl;for (size_t i = 0; i < N; i++){lky::shared_ptr<Date>copy(sp);sp->_year++;sp->_month++;sp->_day++;}}void test_shared_ptr_safe(){lky::shared_ptr<Date>sp(new Date);//cout << sp.get() << endl;const size_t N = 100000;mutex mtx;thread t1([&]() {//cout << sp.get() << endl;for (size_t i = 0; i < N; i++){lky::shared_ptr<Date>copy(sp);sp->_year++;sp->_month++;sp->_day++;}});//线程传参,默认传值传参,想传引用就用库函数ref。thread t2(isSharedSafe, std::ref(sp), N, std::ref(mtx));//这里因为在调thread的构造,相当于封装了一层。传引用断层了t1.join();t2.join();cout << "count:" << sp.use_count() << endl;//恒为1,线程安全cout << "y:" << sp->_year << endl;cout << "m:" << sp->_month << endl;cout << "d:" << sp->_day << endl;}struct ListNode{lky::shared_ptr<ListNode> _prev;lky::shared_ptr<ListNode> _next;int _val;~ListNode() { cout << "~ListNode()" << endl; }};//当shared_ptr遇到循环引用问题...void test_shared_ptr_cycle(){ListNode ln1;ListNode ln2;lky::shared_ptr<ListNode>sp1(&ln1);lky::shared_ptr<ListNode>sp2(&ln2);sp1->_next = sp2;sp2->_prev = sp1;//可以看到,没有调用ListNode的析构,如果注释此行,就有了}
}
int main()
{//lky::test_shared_ptr();//lky::test_shared_ptr_safe();//可以看到shared_ptr的引用计数操作因为加锁保护,是线程安全的//但是shared_ptr管理的对象操作不一定线程安全lky::test_shared_ptr_cycle();
}
首先,shared_ptr本身是线程安全的,包括引用计数++ - -,但是他管理的对象可能没加锁或不是原子的之类,导致对象操作不一定线程安全。
关于循环引用问题:
- 当栈上的sp2先析构,Release让LN2的引用计数减到1,然后sp1再析构,又让LN1的引用计数减到1,就结束了。没错,堆上动态开辟的可空间我们并没有手动释放,os更不会管(但os会在程序结束后将空间资源全部回收),自然没有调用ListNode的析构。
- 而且,就算不在堆上,在栈上,释放完sp1和sp2,该释放LN2,先调它的_next一个nullptr的析构,然后_prev的析构让LN1的引用计数减到0,LN1释放,先调它的_next的析构让LN2的引用计数减到0,就再次进入LN2的析构,就再次先调_next的析构,这时就产生两次加锁导致的死锁问题了,程序崩溃。当然了,假设没锁,让引用计数一直陪它们减减,它们互相牵制,最后也是谁也释放不了。
于是,为了解决循环引用问题,有了weak_ptr。
weak_ptr
weak_ptr的基本思路很简单,指向同一块资源,不增加引用计数即可。
如下:
namespace lky
{//记得把shared_ptr的get给上consttemplate<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;}private:T* _ptr;};struct ListNode{lky::weak_ptr<ListNode> _prev;lky::weak_ptr<ListNode> _next;int _val;~ListNode() { cout << "~ListNode()" << endl; }};//当shared_ptr遇到循环引用问题...void test_weak_ptr_cycle(){lky::weak_ptr<ListNode>wp1(new ListNode);//单参数隐式类型转换lky::weak_ptr<ListNode>wp2(new ListNode);wp1->_next = wp2;wp2->_prev = wp1;//可以看到,没有调用ListNode的析构,如果注释此行,就有了}//weak_ptr不支持RAII,但支持像指针一样使用,专门辅助shared_ptr解决循环引用
}
int main()
{lky::test_weak_ptr_cycle();
}
可以看到,weak_ptr不支持RAII,但支持像指针一样使用,专门辅助shared_ptr解决循环引用。它可以指向资源,但不参与管理,不增加引用计数。
库中实现的复杂的多,大几千行工程级代码,考虑了诸如过期(资源已经释放,但仍指向)等问题,还要全面配合shared_ptr。
定制删除器
其实就是一种可调用对象。因为智能指针可能指向自定义类型数组,需要delete[ ],而析构时默认使用delete,就会引发问题(前面讲过释放位置不对),于是需要外界传一个可调用对象来自定义删除动作。
shared_ptr和unique_ptr的构造都有允许传定制删除器的函数:
以unique_ptr为例,模拟实现如下:
namespace lky
{template<class T>class unique_ptr{public:unique_ptr(T* ptr) :_ptr(ptr) {}template<class D>unique_ptr(T* ptr, D del) : _ptr(ptr), _del(del) {}unique_ptr(const unique_ptr&) = delete;unique_ptr& operator=(const unique_ptr&) = delete;T& operator*() { return *_ptr; }T* operator->() { return _ptr; }~unique_ptr(){//delete _ptr; _del(_ptr);}private:T* _ptr;function<void(T*)>_del = [](T* ptr) {cout << "delete:" << ptr << endl;delete ptr; };};void test_unique_del(){unique_ptr<int>up1 = new int(1);unique_ptr<int>up2(new int[10], [](int* ptr) {cout << "delete[]:" << ptr << endl;delete[] ptr;});}
}
int main()
{lky::test_unique_del();
}
用function来包装接收可调用对象。库里实现不太一样,但功能一样。
智能指针的历史与boost库
- C++98有了第一个智能指针auto_ptr
- boost库搞出了scoped_ptr,shared_ptr和weak_ptr。boost是C++标准委员会成员搭建的准标准库,类似抢先服,很多好用的东西都被C++标准库引进吸收。
- C++11引入unique_ptr(对应scoped_ptr),shared_ptr和weak_ptr。
特殊类
- 只能在堆上创建的类
法一,封构造,还需禁拷贝赋值
class HeapOnly
{
public:static HeapOnly* CreateObj(){HeapOnly* hp = new HeapOnly;return hp;}HeapOnly(const HeapOnly&) = delete;HeapOnly& operator=(const HeapOnly&) = delete;
private:HeapOnly() {}
};
法二,封析构
class HeapOnly
{
public:void Destroy(){delete this;}
private:~HeapOnly() {}
};
- 只能在栈上创建的类
class StackOnly
{
public:static StackOnly CreateObj(){return StackOnly();}StackOnly(const StackOnly&) { cout << "StackOnly(const StackOnly)" << endl;}StackOnly(const StackOnly&) = delete;StackOnly(StackOnly&&){}//法二:将 new 和 delete 声明为 private/*void* operator new(size_t) = delete;void* operator new[](size_t) = delete;void operator delete(void*) = delete;void operator delete[](void*) = delete;*/
private:StackOnly() { cout << "StackOnly()" << endl; }
};
int main()
{StackOnly so1 = StackOnly::CreateObj();//StackOnly* so2 = new StackOnly(so1);//堆上可以禁掉static StackOnly so2 = StackOnly::CreateObj();//static禁不掉
}
注意,这里确切来说,静态区的static对象禁不掉的。
单例模式
设计模式,可以理解为大佬们做出大量工程,总结经验后得出的代码最佳设计方式。
单例模式,适用于一个进程中只有一份实例的类。除此之外,还有工厂模式、观察者模式,自行了解。
单例模式分为饿汉模式和懒汉模式,下为饿汉模式:
class Singleton//饿汉模式:main函数之前就创建对象
{
public:static Singleton* GetInstance(){return _ins;}void Push(int x){_v.push_back(x);}void Print(){for (auto& x : _v) { cout << x << " "; }cout << endl;}
private:Singleton(){}Singleton(const Singleton&) = delete;Singleton& operator=(const Singleton&) = delete;
private:static Singleton* _ins;vector<int>_v;//假设存储全局都要用的只此一份的名单
};
Singleton* Singleton::_ins = new Singleton;//static成员定义可以使用成员函数
饿汉线程安全,main函数之前都没有多线程。
下为懒汉模式:
#include<mutex>
class Singleton//懒汉模式:第一次获取实例对象才创建
{
public:static Singleton* GetInstance(){ //双检查加锁if (_ins == nullptr)//提高效率,防止频繁上锁{_mtx.lock();if (_ins == nullptr){_ins = new Singleton;}_mtx.unlock();}return _ins;}void Push(int x){_v.push_back(x);}void Print(){for (auto& x : _v) { cout << x << " "; }cout << endl;}static void DelInstance(){_mtx.lock();if (_ins){delete _ins;}_mtx.unlock();}class GC//GC析构时自动delete单例,防止忘记DelInstance{public:~GC() { DelInstance(); }};static GC gc;~Singleton(){//...//持久化,程序结束时把数据写到文件里}
private:Singleton() {}Singleton(const Singleton&) = delete;Singleton& operator=(const Singleton&) = delete;private:static Singleton* _ins;vector<int>_v;//假设存储全局都要用的只此一份的名单static mutex _mtx;
};
Singleton* Singleton::_ins = nullptr;
mutex Singleton::_mtx;
Singleton::GC Singleton::gc;
int main()
{Singleton* st = Singleton::GetInstance();st->Push(1);st->Print();
}
懒汉需要加锁保证线程安全。
一般情况下,单例不需要手动释放,因为单例全局都要用,最后被os自动回收即可,但如果有提前释放需求,推荐内部类GC。
懒汉2.0:封掉构造,在GetInstance里定义static的单例对象,也不需要加锁。但是C++11之后静态局部变量的初始化才是线程安全的。
饿汉和懒汉的优缺点
饿汉:优点,比懒汉简单;缺点,创建出来时不需要使用却占用资源,拖慢程序启动速度(单例对象创建可能要IO等),并且无法保证多个单例之间的创建顺序。
懒汉:优点就是没有上面的缺点,缺点就是复杂点。
C++四种类型转换
为了规范c语言强制类型转换的安全清晰使用、避免隐式类型转换引发的问题如精度丢失等,C++提出了四种类型转换,如下:
- static_cast:用于类型相近的类型转换,比如
double a=1.23;int b=static_cast<int>(a);
- reinterpret_cast:用于类型差别较大的类型转换,比如
int a=0;int*p=reinterpret_cast<int*>(a);
- const_cast:用于去掉const属性。
const int a = 0;
int* p = const_cast<int*>(&a);
*p = 3;
cout << a << endl;//还是0
cout << *p << endl;//3
a还是0,因为编译器做优化,可能把a的值放进寄存器,也可能把const属性的a进行类似宏替换处理。加上volatile关键字防止编译器优化即可。
- dynamic_cast:用于将父类指针/引用转为子类指针/引用。
- 首先,由于子类可能比父类多成员,父类强转子类后就可能访问越界。但是对于本来就是子类切片得来的父类,将其转回子类,是没问题的。
- dynamic_cast只能用于父类有虚函数的类。dynamic就在暗示你包含多态,这与它的底层实现有关,也就是虚表有关。
- dynamic_cast先检查能否转成,能成则成,否则返回0。这也是它安全的原因。
- 注意,父类对象肯定是永远无法转为子类对象的。
- 切片/赋值兼容:子切出来父的那一部分,天然支持的,无论对象、指针还是引用。注意区别。
class A
{
public:virtual void func(){}//必要的int _a = 1;
};
class B :public A
{
public:int _b = 2;
};
int main()
{A* pa = new A;//B* pb = (B*)pa;//不安全,可能越界B* pb = dynamic_cast<B*>(pa);//安全,不可能越界/*cout << pa << endl;cout << pb << endl;if (pb)//被置空,因为可能越界{cout << pb->_a << endl;cout << pb->_b << endl;//强转的pb打出来的是随机值,越界了}*///对于可以成功dynamic_cast的情况A* pa2 = new B;//B* pb2 = (B*)pa2;//不安全,可能越界B* pb2 = dynamic_cast<B*>(pa2);//安全,不可能越界cout << pa2 << endl;cout << pb2 << endl;if (pb2)//成功转换,因为整体本身就是个子类对象{cout << pb2->_a << endl;cout << pb2->_b << endl;}
}
C++IO流
相比c语言的IO操作,C++的面向对象,且更好支持自定义类型的IO。
关于IO操作,这里只提关键点,其余自行查找资料。
首先库里常用的ostream,istream对标c的printf,scanf,iostream继承前两者,功能兼具。后面同理,一个文件流,一个字符串流。最常用的还是<<和>>以及getline。
值得一提的是while(cin>>x){cout<<x;}
>>返回的明明是istream对象引用,为什么可以转成bool判断真假?
就是因为operator bool这个函数重载,支持从istream隐式类型转换转为bool。同理,operator int支持该对象转为int。这样就支持自定义类型转为内置类型。
还有一点,二进制读写,读写对象中不能有string,因为读写的其实是string的str指针和size和capacity,那程序结束指针就销毁了,再读就野指针了。
结语
OK,C++学习博客到此为止,完结撒花!Linux篇再见🥰😘