友元
友元并不破坏封装的性质,它是更灵活的接口
友元类
只需在类声明中加入:
friend class Friend;
就能让 Friend
类成为友元,声明位置无关紧要,公有、私有和保护部分都可以
两个类可以互相是对方的友元,如:
class A;
class B;
class A {
...
friend class B;
};
class B {
...
friend class A;
};
友元成员函数
可以声明类中的某个成员为友元,例如:
class Tv;
class Remote {
...
};
class Tv {
...
friend void Remote::set_chan(Tv& t, int c);
};
要注意定义的顺序
共同友元
一个函数可以同时是两个类的友元,如:
class Analyzer;
class Probe {
...
friend void sync(Analyzer& a, const Probe& p);
friend void sync(Probe& p, const Analyzer& a);
};
class Analyzer {
...
friend void sync(Analyzer& a, const Probe& p);
friend void sync(Probe& p, const Analyzer& a);
};
嵌套类
在另一个类中声明的类叫做嵌套类
对类进行嵌套通常是为了帮助实现另一个类,并避免名称冲突
嵌套类、结构体和枚举的作用域特征:
声明位置 | 包含它的类是否可以使用 | 从包含它的类派生而来的类是否可以使用 | 在外部是否可以使用 |
---|---|---|---|
私有部分 | 是 | 否 | 否 |
保护部分 | 是 | 是 | 否 |
公有部分 | 是 | 是 | 是,通过类限定符来使用 |
嵌套类的声明位置决定了作用域,访问控制和正常的类相同
嵌套类也可以定义在模板类中
异常
调用 abort()
如果出现了除零这样的错误,处理方式之一是调用 abort()
函数(位于 cstdlib 头文件中)
其典型实现是向标准错误流发送消息 abnormal program termination(程序异常终止),然后结束程序。它还返回一个因实现而异的值告诉操作系统或父进程处理失败
返回错误码
可以用返回错误码的方式来判断是否发生了错误:
bool hmean(double a, double b, double* ans) {
if (a == -b) {
*ans = DBL_MAX;
return false;
} else {
*ans = 2.0 * a * b / (a + b);
return true;
}
}
异常机制
对异常的处理有三个组成部分:
- 引发异常
- 使用处理程序捕获异常
- 使用
try
块
throw
关键字表示引发异常,之后提供了异常的特征,这实际上表示跳转
catch
关键字指出响应的异常类型,表明当该种异常引发时,应该跳转到这个位置执行
try
块表示其中特定的异常可能被激活的代码块
将对象用作异常类型
通常,引发异常的函数将传递一个对象
可以用不同的异常类型来区分不同情况下引发的异常,并且可以携带信息
异常规范与 C++ 11
https://www.cnblogs.com/sword03/p/10020344.html
异常规范是 C++ 98 新增的功能,就跟 java 相似,标注函数可能引发的异常,而 C++ 11 将其摒弃
大概长这样:
double marm() throw(bad_thing);
double marm() throw();
但是仍然有一个非常有用,就是 noexcept
关键字:
double marm() noexcept;
其实就相当于 throw()
,该关键字告诉编译器,函数中不会发生异常,这有利于编译器对程序做更多的优化
还有运算符 noexcept()
表示有条件的 noexcept
:
void swap(Type& x, Type& y) noexcept(noexcept(x.swap(y)))
它表示,如果操作 x.swap(y)
不发生异常,那么函数 swap(Type& x, Type& y)
一定不发生异常。
一个更好的示例是 std::pair
中的移动分配函数(move assignment),它表明,如果类型 T1
和 T2
的移动分配(move assign)过程中不发生异常,那么该移动构造函数就不会发生异常。
pair& operator=(pair&& __p)
noexcept(__and_<is_nothrow_move_assignable<_T1>,
is_nothrow_move_assignable<_T2>>::value)
{
first = std::forward<first_type>(__p.first);
second = std::forward<second_type>(__p.second);
return *this;
}
以下情形鼓励使用 noexcept
:
- 移动构造函数(move constructor)
- 移动分配函数(move assignment)
- 析构函数(destructor)。这里提一句,在新版本的编译器中,析构函数是默认加上关键字
noexcept
的。
栈解退
C++ 使用栈的机制来执行函数调用,函数调用将返回地址、参数放到栈中,函数结束时根据返回地址返回到调用它的地方,同时每个函数都在结束时释放它的自动变量
在处理异常时,涉及到栈解退,如果抛出异常,那么程序会一直弹栈,直到找到一个位于 try
块中的返回地址,返回到那里执行异常处理程序
与函数返回不同的是,栈解退会释放从 throw
到 try
块之间所有在栈中的自动变量
如果没有处理某个异常的 try-catch 组合,那么程序将异常终止
其他异常特性
引发异常时编译器总是创建一个临时拷贝,即使异常规范和 catch
块中指定的是引用
class Problem { ... };
void super() throw(Problem) {
...
if (...) {
Problem oops;
throw oops;
}
...
}
try {
super();
} catch (Problem& p) {
...
}
既然 throw
语句生成副本,那么为什么 catch
要使用引用呢?
原因是:引用可以执行派生类对象,基类引用可以与任何它的派生类对象相匹配
匹配的顺序是从前往后,这意味着派生类的处理块应该在基类之前
C++ 还有一个可以捕获任何异常的语句,那就是省略号:
catch (...) {
// statement
}
所以可以将该处理块放在最后,类似 switch
的 default
exception
类
在 C++ 中,可以把 exception
作为其他异常类的基类,它有一个 what()
虚方法,返回一个字符串
C++ 还定义了很多基于 exception
的异常类型:
stdexcept 头文件定义了:
logic_error
:逻辑错误,它派生了domain_error
:定义域错误,例如反正弦函数如果参数不在 -1 到 1 之间就可以抛出该错误invalid_argument
:给函数传递了一个意料外的值length_error
:没有足够的空间来执行所需操作,例如string
的append()
方法在合并得到的字符串长度超过最大允许长度时会引发该错误out_or_bounds
:指示索引错误,一般与operator[]
有关
runtime_error
:运行时错误,运行期间发生但难以预计和防范的错误。它派生了:range_error
:计算结果不在函数允许的范围之内overflow_error
:上溢错误,例如计算结果超过最大值underflow_error
:下溢错误,例如计算结果小于浮点类型可以表示的最小非零值
C++ 中,处理 new
引起的内存分配问题的方法是引发 bad_alloc
异常
同时,还提供了一种在失败时返回空指针的 new
:
int* pi = new (std::nothrow) int;
int* pa = new (std::nothrow) int[500];
异常何时会迷失方向
异常被引发后,在两种情况下会导致问题:
- 使用了异常规范时,如果抛出的异常与异常规范列表不匹配,那么称为意外异常(unexpected exception),这会导致程序异常终止
- 如果异常没有被捕获,那么称为未捕获异常(uncaught exception),在默认情况下会导致程序异常终止,但可以修改
未捕获异常出现后,程序先调用 terminate()
,默认情况下,terminate()
会调用 abort()
,我们可以指定 terminate()
调用的函数
我们只需要:
#include <exception>
using namespace std;
void my_quit() {
...
}
int main() {
set_terminate(my_quit);
...
}
意外异常与之相似,它会调用 unexpected()
函数,这个函数默认调用 terminate()
,我们同样可以修改调用的函数,使用 set_unexpected()
函数即可
总之,如果要捕获所有的异常,则可以这样做:
#include <exception>
using namespace std;
void my_unexcepted() {
throw std::bad_exception();
}
int main() {
set_unexcepted(my_quit);
...
}
try {
function();
} catch (exception1& e) {
...
} catch (bad_exception& ex) {
...
}
有关异常的注意事项
应在设计程序时就加入异常处理功能,而不是以后再添加
异常规范不适用于模板
异常和动态内存分配并非总能协同工作,如:
void test(int n) {
double* ar = new double[n];
...
if (...) {
throw exception();
}
...
delete [] ar;
return;
}
这样的话 ar
并没有被释放,造成了内存泄漏,我们可以在最后放一个 catch
块来解决这个问题:
void test(int n) {
double* ar = new double[n];
...
try {
if (...) {
throw exception();
}
} catch (exception& ex) {
delete [] ar;
return;
}
...
delete [] ar;
return;
}
另一种解决方法是使用智能指针
RTTI
RTTI 指的是运行阶段类型识别(Runtime Type Identification)
它可以:
- 跟踪对象类型
- 在继承结构中确定类型转换的安全性
支持 RTTI 的元素:
typeid
运算符,返回一个type_info
类型的值type_info
结构存储了有关特定类型的信息dynamic_cast
检查继承层次中基类指针或引用向派生类转换的合法性
dynamic_cast
运算符
它可以回答“是否可以安全地将对象的指针赋给特定类型的指针”这样的问题
如果不能够安全地转换,那么会返回空指针
class Grand {};
class Superb : public Grand {};
class Magnificent : public Superb {};
Grand* get_one(int index) {
if (index % 3 == 0) return new Grand();
else if (index % 3 == 1) return new Superb();
else return new Magnificent();
}
int main() {
Grand* pg1 = get_one(0);
Grand* pg2 = get_one(1);
Grand* pg3 = get_one(2);
Superb* ps1 = dynamic_cast<Superb*>(pg1);
Superb* ps2 = dynamic_cast<Superb*>(pg2);
Superb* ps3 = dynamic_cast<Superb*>(pg3);
return 0;
}
上述代码中,ps1
为空指针,ps2
和 ps3
都成功转换
有时需要手动开启 RTTI 编译特性
也可以将 dynamic_cast
用于引用,此时如果不成功会抛出 bad_cast
异常
typeid
运算符和 type_info
类
typeid
运算符能够帮助我们确定两个对象是否为同一值类型,与 sizeof
相似,它可以接受两种参数:
- 类名
- 结果为对象的表达式
typeid
返回一个对 type_info
对象的引用
type_info
类重载了 ==
和 !=
运算符,可以方便地进行比较
typeid(Magnificent) == typeid(*pg)
注意一定要加上括号,这一点和 sizeof
不同
如果 pg
是一个空指针,那么会抛出 bad_typeid
异常
不要轻易使用 typeid
,很多都可以使用 dynamic_cast
类型转换运算符
Stroustrop 认为 C 语言的类型转换太过松散(例如可以将 char*
转化为 int*
),他采取了更严格的措施来限制类型转换,并且添加了 4 个类型转换运算符,让其更规范和明确:
dynamic_cast
:允许正确的向上转换const_cast
:更改 cv 修饰符,如const Type*
到Type*
、Type*
到const Type*
,除去 cv 修饰符之外的东西不能变static_cast
:向上和向下转换都可以,但是不能转换到没关系的类型reinterpret_cast
:用来处理无关类型之间的转换,但不允许删除const
,它会产生一个新的值,这个值会有与原始参数有完全相同的比特位,有一些限制:- 不能将指针类型转换成更小的整型或浮点型,转换到的类型必须足够存储指针
- 不能在函数指针和数据指针之间转换