以下为个人学习笔记整理。参考书籍《C++ Primer Plus》
# 友元、异常和其他
# 友元
类并非只能拥有友元函数,也可以将类作为友元。这种情况下,友元类的所有方法都可以访问原始类的「私有成员」和 「保护成员」,此外还可以做更加细致的限制。例如:只将特定成员函数指定为另一个类的友元。
# 友元类
声明一个类 B
是另一个类 A
的友元类:
class A{ | |
public: | |
friend class B; // B 能访问 A 的私有成员 | |
}; |
# 友元成员函数
声明一个类成员函数为另一个类的友元函数:
class A{ | |
friend void B::func(); | |
} |
这里有一点需要注意,在声明类 A
友元成员函数时,必须事先知道类 B
的定义。但是在 B
类中大概率也会使用到类 A
,导致循环依赖的问题。这时候需要通过向前声明(forward declaration)来解决。
class A; // 向前声明 | |
class B{...}; | |
class A{...}; |
# 其他友元关系
定义两个互为友元关系的类:
class A{ | |
friend class B; | |
}; | |
class B{ | |
friend class A; | |
}; |
如果两个类需要访问对方的成员,请在事先进行定义。
# 共同的友元
如果一个函数需要同时访问两个类的私有成员,通常会有以下几种情况:
- 该函数是一个类的成员,是另一个类的友元。
- 该函数是两个类的友元。
显然,在没有明确的所属情况下,定义一个函数是两个类的友元更加的公正合理
class A; // 向前声明 | |
class B{ | |
friend void func(A& a, const B& b); | |
friend void func(const B& b, A& a); | |
}; | |
class A{ | |
friend void func(A& a, const B& b); | |
friend void func(const B& b, A& a); | |
}; | |
void func(A& a, const B& b){ | |
... | |
} | |
void func(const B& b, A& a){ | |
... | |
} |
# 嵌套类
C++ 中,可以在一个类中声明另一个类,另一个类被称为嵌套类(nested class)。
类的嵌套和包含并不相同,包含是在一个类中包含另一个类实例,而嵌套仅仅是在该类的作用域内声明和定义一个新的类,并没有构建实例。
class Out{ | |
private: | |
class Inner{//...}; | |
}; |
# 嵌套类和访问权限
# 作用域
- 如果一个类
Inner
声明在另一个类Out
的私有部分。那么只有Out
能够访问Inner
。 - 如果一个类
Inner
声明在另一个类Out
的保护部分。那么只有Out
和其派生类能够访问Inner
。 - 如果一个类
Inner
声明在另一个类Out
的公有部分。那么除了Out
和其派生类之外,还允许外界通过Out
其派生类访问Inner
。
# 访问控制
当一个类变得可见时,其决定作用的将是访问控制。嵌套类的访问控制和其他类类似。外界只能够访问其公有成员,而对保护和私有成员不可见。
# 异常
# 调用 abort ()
abort
函数会直接抛出异常,让程序终止。
#include<cstdlib> | |
int main() | |
{ | |
std::abort(); | |
} |
# 返回错误码
让程序直接终止往往太过暴力,有时候程序会提供一个返回状态用于表示本次执行的状态,通过参数指针的性质把返回值带出。
#include <cfloat> // import DBL_MAX | |
bool hmean(double a, double b, double* result){ | |
if (a == -b){ | |
*result = DBL_MAX; | |
return false; | |
}esle{ | |
*result = 2.0 * a * b / (a + b); | |
return true; | |
} | |
} |
# 异常机制
- 引发异常。
- 使用处理程序捕获异常。
- 使用
try
块。
try{ | |
// run exception code | |
}catch(const char *s){ // 指定了某个异常处理 | |
// handler exception | |
} |
# 将对象用作异常类型
通常,引发异常的函数将传递一个对象。这样就可以根据对象类型的不同来区分不同的异常,另外对象本身还可以携带信息。通过携带信息了解异常的引发原因。
catch
用于捕获异常,一个 try
语句后往往可以接上多个 catch
。
try{ | |
// ... | |
}catch(bad_hmean& b){ | |
// ... | |
}catch(bad_gmean& g{ | |
// ... | |
} |
# 异常规范和 C++11
一种理念,有时候看似前途无量,实际效果却差强人意。一个这样的例子是异常规范(exception specification),这是 C98 新增的一项功能。但 C11 却将其摒弃了。
不过 C++11 引入了一个新的规范 —— noexcept
该函数不会引发异常。不过也不推荐使用~
void func() noexcept; // 该函数不会引发异常 |
# 栈解退
运行过程中,如果引发异常而终止,程序将释放当前运行的栈内存,并且跳回上一层继续执行此步骤。
直到遇到 try
语句后将逻辑控制权交给 catch
进行异常处理。
# 其他异常特性
异常的传递总是会创建一个临时拷贝,即使异常的 catch
块中指定的是引用。
使用引用作为 catch
的目的主要是用来执行派生类的函数,实现多态。
基类异常可以捕获派生类,但是派生类不可捕获基类。
# exception 类
exception
头文件定义了 exception
类,C++ 可以把它作为其他异常的基类。这允许开发者简单的定义自己的异常。
what()
虚拟成员函数,返回一个字符串,可以在 exception
类中重新定义,用于描述异常:
class bad_hmean :public std::exception { | |
public: | |
const char* what() { return "err bad_hmean"; } | |
}; | |
class bad_gmean :public std::exception { | |
public: | |
const char* what() { return "err bad_gmean"; } | |
}; |
由于继承了 exception
类,使得异常捕获可以变得更加简单
try{ | |
//... | |
}catch(std::exception& e){ | |
//... | |
} |
# stdexcept 异常类
- logic_error:逻辑错误。
- domain_error:超出定义域。
- invalid_argument:参数有误。
- length_error:空间不够用于存储,例如字符串长度超过最大长度。
- out_of_bounds:数组越界。
- runtime_error:运行时的错误。
- range_error:计算结果不在允许范围内,且没有发生
overflow_error
和underflow_error
。 - overflow_error:整型和浮点型计算过程中超过最大值的情况。
- underflow_error:一般出现在浮点数计算比最小值还小的情况。
- range_error:计算结果不在允许范围内,且没有发生
# bad_alloc 异常和 new
使用 new
引发的 bad_alloc
异常,以往如果 new
无法申请到内存时,往往会返回一个 空指针,但现在则可以抛出 bad_alloc
异常。
# 空指针和 new
由于很多代码都是在 new
返回空指针的情况下做的逻辑,为了能够兼容这些代码, C++ 提供了一个开关。可以指定不抛出异常:
int* pi = new(std::nothrow) int; | |
int* pa = new(std::nothrow) int[500]; |
# 异常何时会迷失方向💫
异常触发后,有如下两种情况默认会引发程序终止:
- 如果其是在带有异常规范的函数中引发的,则必须与规范列表中的某种异常匹配,否则称之为意外异常(unexcepted exception)。
- 如果异常不是在函数中引发的,则必须捕获它。如果没有捕获,那么该异常被称为未捕获异常(uncaught exception)。
但凡是都有例外,如果出现「未捕获异常」,可以使用 terminate()
在异常没有捕获的时候在做一下最后的垂死挣扎😇
//terminate 函数在 exception 中的声明 | |
typedef void (*terminate_handler)(); | |
terminate_handler set_terminate(terminate_handler f) throw(); // C++98 | |
terminate_handler set_terminate(terminate_handler f) noexcept(); // C++11 | |
void terminate(); // C++98 | |
void terminate() noexcept(); // C++11 |
而 set_terminate
会将操作设置为 Quit
。(本地编译运行的时候好像没有生效,原因尚未可知)
#include<exception> | |
void Quit() { | |
std::cout << "uncaught exception\n"; | |
exit(5); | |
} | |
set_terminate(Quit); |
如果出现「意外异常」,程序将调用 unexcepted()
,这个函数也会调用 terminate()
,而该函数默认情况下会调用 abort()
。
//terminate 函数在 exception 中的声明 | |
typedef void (*unexpected_handler)(); | |
unexpected_handler set_unexpected(unexpected_handler f) throw(); // C++98 | |
unexpected_handler set_unexpected(unexpected_handler f) noexcept(); // C++11 | |
void unexpected(); // C++98 | |
void unexpected() noexcept(); // C++11 |
然而,和 set_terminate
相比 set_unexpected
的 unexpected_handler
有更多的限制,具体来说 unexpected_handler
可以:
- 通过调用
terminate
、abort
、exit
来终止程序。 - 引发异常。
- 如果新引发的异常与原来的异常规则匹配,则程序将从那里开始进行正常处理。
- 如果新引发的异常与原来的不匹配,且异常规范中没有包括
std::bad_exception
类型,则程序将调用terminate
。 - 如果新引发的异常与原来的不匹配,且异常规范中有包括
std::bad_exception
类型,则不匹配的异常将被std::bad_exception
取代。
如果想要捕获所有的异常,可以这么处理:
#include<exception> | |
usind namespace std; | |
void myUnexpected(){ | |
throw std::bad_exception(); // or just throw; | |
} | |
set_unexpected(myUnexpected); | |
void func() throw(bad_exception); | |
try{ | |
// use func() do anything... | |
}catch(bad_exception& e){ | |
// ... | |
} |
# 有关异常的注意事项
异常可能会出现在程序的任何时候,这意味着,如果某个对象是通过 new
动态分配的内存在调用析构函数回收之前,也可能引发异常,那么这块内存将不能够被释放。这将导致内存泄漏。
- 一种解决办法是在引发异常的函数中捕获异常并回收内存。
- 另一种解决办法是使用「智能指针模板」。
# RTTI
RTTI 是运行阶段类型识别(Runtime Type Identification)。
# RTTI 的用途
用来确定一个基类地址所指向的对象实际的类型。这样有助于后续的一些动态语言特性,以及反射。
# RTTI 的工作原理
C++ 通过以下三个元素来支持 RTTI 特性。
- 如果可以的话,
dynamic_cast
运算符将使用一个指向基类的指针来生成一个指向派生类的指针。否则,该运算符返回空指针。 typeid
运算符返回一个指出对象的类型的值。type_info
结构存储了有关特定类型的信息。
只能将 RTTI 用于包含虚函数的类层次结构,因为只有这种情况下才需要将派生类地址传给基类指针。
# dynamic_cast 运算符
该对象不能回答「指针指向的是哪个类对象」,但可以知道「该类型能否安全的将地址传递给特定的指针」。
class A{}; | |
class B: public A{}; | |
A* a = new a; | |
B* b = dynamic_cast<B *>(a); // 将 A* 转为 B* 类型 | |
// 如果可以则正常运行,否则返回空指针 |
# typeid 运算符和 type_info 类
typeid
运算符使得能够确定两个对象是否是同种类型。
#include <typeinfo> | |
type_info& typeid(ClassName or 结果为对象的表达式); |
通过 typeid
判断对象类型:
if typeid(ClassName) == typeid(class_obj){ | |
// ... | |
} |
如果 typeid
的参数为空,将会引发 bad_typeid
异常。 typeinfo
包含一个函数 name()
,通常情况下可以用来表示类型名。
# 使用 dynamic_cast 简化 typeid 逻辑
// typid logic | |
if typeid(ClassName_A) == typeid(class_obj){ | |
a_obj = (ClassName_A*)class_obj; | |
a_obj.func_1(); | |
a_obj.func_2(); | |
a_obj.func_3(); | |
}else if typeid(ClassName_B) == typeid(class_obj){ | |
b_obj = (ClassName_A*)class_obj; | |
b_obj.func_1(); | |
a_obj.func_2(); | |
}else { | |
b_obj.func_1(); | |
} | |
// dynamic_cast | |
class_obj.func_1(); | |
if (b_obj = dynamic_cast<ClassName_B*>(class_obj)) | |
b_obj.func_2(); | |
if (a_obj = dynamic_cast<ClassName_A*>(class_obj)) | |
a_obj.func_3(); |
# 类型转换运算符
C++ 为类型转换提供了严格限制,并添加 4 个类型转换的运算符:
- dynamic_cast :针对派生关系的向上转换。
High* h = dynamic_cast<Low*>(l);
- const_cast :针对 const 或 volatile 的类型转换。
const Type_A c_a = const_cast(const Type_A*)(a);
- static_cast :常规的类型转换,支持派生关系的上下级转换和显式类型转换。
int a = static_cast<int>(3.1);
- reinterpret_cast :用于天生危险的类型转换,该转换不会改变
const
特性,通常用于定义新的解析规则。
// 32 位和 64 位机器的 | |
int num = 0x00636261; | |
int * pnum = # | |
char * pstr = reinterpret_cast<char *>(pnum); | |
cout<<"pnum指针的值: "<<pnum<<endl; | |
cout<<"pstr指针的值: "<<static_cast<void *>(pstr)<<endl;// 直接输出 pstr 会输出其指向的字符串,这里的类型转换是为了保证输出 pstr 的值 | |
cout<<"pnum指向的内容: "<<hex<<*pnum<<endl; | |
cout<<"pstr指向的内容: "<<pstr<<endl; | |
// -------- output -------- | |
// 在 Ubuntu 14.04 LTS 系统下,采用 g++ 4.8.4 版本编译器编译该源文件并执行,得到的输出结果如下: |
# 参考资料:
- C++ 类型转换之 reinterpret_cast