C++学习笔记-异常和智能指针
异常处理
大概流程:检查(try)、抛出(throw)和捕捉(catch)。将需要检查的语句放在try块中,当出现异常时throw用于发出一个异常信息,形象地称为抛出异常,而catch用于捕捉异常信息,如果捕捉到异常信息后,就在catch块中处理。
异常处理的一般执行过程:
(1)程序正常执行到达try块后,接着执行try块内的代码。
(2)如果在执行try块内的代码或在try块内的代码中调用的任何函数期间没有引起异常,那么跟在try块后的catch块将不执行,程序将从最后一个catch块后面的语句继续执行下去。
(3)如果在执行try块内的代码期间或在try块内的代码中调用的任何函数中有异常被抛出,编译器从能够处理抛出的异常类型的本层或上层调用函数中寻找一个catch块用以捕获此异常。异常处理机制将按其catch块在try块后出现的顺序查找合适的catch块。
(4)如果找到了一个匹配的catch块,也就是catch块捕获到此异常了,则形参通过throw语句中抛出的表达式进行初始化。当形参被初始化之后,将开始销毁catch块对应的try块开始到和抛出异常语句throw之间所有自动变量(也就是非静态的局部变量),然后再执行catch块中的语句处理异常,最后程序跳转到最后一个catch块后面的语句继续执行。
(5)如果没有找到匹配的catch块,则自动终止程序
值得注意的是, 在销毁catch对应的try块开始和到抛出异常throw之间的所有自动变量的时候, 如果delete语句不在这之间, 也就不会执行, 而如果自动变量是在堆空间中, 就会造成内存泄漏, 因此C++引入了智能指针
示例:
class Double
{
private:
double num; // 数据值
public:
Double(double n = 0): num(n) // 构造函数
{ cout << num << ":构造函数" << endl; }
~Double(){ cout << num << ":析构函数" << endl; }// 析构函数
operator double() const { return num; } // 类型转换函数
double Sqrt() const
{
if (num < 0) throw "被开方数不能为负!";// 抛出异常
return sqrt(num); // 返回平方根
}
};
int main()
{
try // 检查异常
{
Double x1(9), x2(-9); // 定义对象
cout << x1 << "的平方根为" << x1.Sqrt() << endl;
// 输出x1的平方根
cout << x2 << "的平方根为" << x2.Sqrt() << endl;
// 输出x2的平方根
}
catch (char *str) // 捕捉异常
{ // 处理异常
cout << "异常信息:" << str << endl; // 输出异常信息
}
}
输出信息:
9:构造函数
-9:构造函数
9的平方根为3
-9:析构函数
9:析构函数
异常信息:被开方数不能为负!
注意:
异常匹配并不要求throw语句抛出的表达式类型与catch块的参数类型匹配得十分完美。
抛出派生类对象类型可以与catch块的基类参数类型相匹配。
异常处理catch块的排列顺序应该将基类参数放在派生类参数的后面。
catch (…) 可捕获所有类型异常。
智能指针
智能指针是基于RAII(Resource Acquisition Is Initialization,资源获取就是初始化)机制实现的类(模板),具有指针的行为(重载了operator*与operator->操作符),智能指针实际上是栈对象,并非指针类型
所有智能指针都重载了“operator->”操作符,直接返回对象的引用,用以操作对象。访问智能指针原来的方法则使用“.”操作符
优点:第一、我们不需要显式地释放资源。第二、采用这种方式,对象所需的资源在其生命期内始终保持有效 ——我们可以说,此时这个类维护了一个不变式(invariant)。这样,通过该类对象使用资源时,就不必检查资源有效性的问题,可以简化逻辑、提高效率。
unique_ptr指针
unique_ptr“唯一”拥有其所指对象,同一时刻只能有一个unique_ptr指向给定对象(通过禁止拷贝语义、只有移动语义来实现)
智能指针对象可用作判断条件,拥有所管理对象时为true,否则为false
unique_ptr<T> up; //管理T类型对象的智能指针对象
up //用作判断条件
*up //所指对象
**up->member** //所指对象成员
**up.get ()** //获得传统“裸指针” 这里注意.和->的不同, **.是智能指针对象本身的操作, ->是对智能指针所指向对象的操作**
up.release (); //放弃管理权
使用示例
#include <memory> //智能指针需要头文件
//智能指针的创建
unique_ptr<int> u_i; //创建空智能指针
u_i.reset(new int(3)); //"绑定”动态对象
unique_ptr<int> u_i2(new int(4));//创建时指定动态对象
//所有权的变化
//u_i2 = u_i; //错误,不可复制赋值
u_i2 = std::move(u_i); //ok,可以移动赋值
int *p_i = u_i2.release(); //释放所有权
unique_ptr<string> u_s(new string("abc"));
//unique_ptr<string> u_s2 = u_s; //错误,不可复制构造
unique_ptr<string> u_s2 = std::move(u_s);//可以移动构造
//所有权转移(通过移动语义),u_s所有权转移后,变成“空指针”
u_s2=nullptr;//显式销毁所指对象,同时智能指针变为空指针。与u_s2.reset()等价
// nullptr是C++ 11提供的空指针
智能指针的几个使用场景
(1) 动态资源的异常安全保证(利用其RAII特性):
void foo()
{//不安全的代码,存在内存泄露危险
X *px = new X;
px->DoSome ();// 处理, 可能发生异常
delete px; // may not go here
}
void foo()
{//异常安全的代码,不存在内存泄露危险 。
unique_ptr<X> px(new X);
px->DoSome ();// 处理, 可能发生异常
}
(2)返回函数内创建的动态资源,X为某个类名
unique_ptr<X> foo()
{ unique_ptr<X> px(new X);
px->DoSome ();// 处理, 可能发生异常
return px; //移动语义
}
(3)可放在容器中(弥补了C++ 98 auto_ptr不能作为容器元素的缺点)
vector<unique_ptr<string>> v;
unique_ptr<string> p1(new string("abc"));
//错误 v.push_back(p1); //unique_ptr不可复制构造和复制赋值
v.push_back(std::move(p1));//这里需要显式移动语义,unique_ptr并无copy语义
p1.reset (new string("Doug"));
v.push_back(std::move(p1));//这里需要显式移动语义,unique_ptr并无copy语义
cout << *v[0] << endl;
cout << *v[1] << endl;
shared_ptr指针
shared_ptr可以有多个共享对象, 内部置有计数器, 当一个共享资源有新的指针指向时计数器加一, 当其中一个指针释放时计数器减一, 当计数器减为零时,则请求的资源删除
shared_ptr可以从一个裸指针、另一个shared_ptr、或者一个weak_ptr构造
共享指针的几个陷阱
陷阱1.不要把一个原生指针给多个shared_ptr管理
int* ptr = new int;
shared_ptr<int> p1(ptr);
shared_ptr<int> p2(ptr); //logic error
p1,p2都离开作用域,它们都销毁ptr,会导致两次释放同一块内存。
正确的做法是将原始指针赋给智能指针后,以后的操作都要针对智能指针了。
参考代码如下:
shared_ptr<int> p1 = make_shared<int>();
shared_ptr<int> p2(p1); //ok
陷阱2. 不要在函数实参里创建shared_ptr
fun ( shared_ptr<int>(new int), g( ) ); //有缺陷
可能的过程是先new int,然后调用函数g,再构造智能指针对象; 如果调用函数g期间发生异常,此时shared_ptr<int>智能指针对象没有创建,造成int内存泄露
正确的做法是分步:
shared_ptr<int> p(new int());
fun (p, g());
//因进入死循环无法释放资源问题
shared_ptr作为被保护的对象的成员时,小心因循环引用造成无法释放资源。
struct A;
struct B;
struct A
{
std::shared_ptr<B> m_b;
};
struct B
{
std::shared_ptr<A> m_a;
};
std::shared_ptr<A> ptrA(new A);
std::shared_ptr<B> ptrB(new B);
ptrA->m_b = ptrB;
ptrB->m_a = ptrA;
ptrA和ptrB所管理对象引用次数都为2, ptrA和ptrB离开作用域时引用计数都为1,导致内存没有被释放。解决方案需借助weak_ptr
weak_ptr不改变所指向对象的引用计数,因此,称为“弱指针”。
struct A; struct B;
struct A
{
std::shared_ptr<B> m_b;
};
struct B
{
std::weak_ptr<A> m_a;
};
std::shared_ptr<A> ptrA(new A);
std::shared_ptr<B> ptrB(new B);
ptrA->m_b = ptrB;
ptrB->m_a = ptrA;
ptrA所管理对象引用次数为1,ptrB所管理对象引用次数为2, ptrA先离开作用域时,引用计数减1后为0,会自动删除管理的动态分配A类对象,并进一步导致内部共享指针m_b计数减1,ptrB离开作用域时也会减1,可达到正确删除ptrB所管理对象目的。ptrB先离开作用域时,引用计数减1后为1,下次 ptrA再离开作用域时会自动删除管理的动态分配A类对象,并进一步导致内部共享指针m_b计数减1后为0,可达到正确删除ptrB所管理对象目的。借助weak_ptr打破了循环。
关于weak_ptr指针