以下为个人学习笔记整理。参考书籍《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{...};

image-20210319105714085

# 其他友元关系

定义两个互为友元关系的类:

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

image-20210319113051635

# 访问控制

当一个类变得可见时,其决定作用的将是访问控制。嵌套类的访问控制和其他类类似。外界只能够访问其公有成员,而对保护和私有成员不可见。

# 异常

# 调用 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
}

image-20210319141539933

# 将对象用作异常类型

通常,引发异常的函数将传递一个对象。这样就可以根据对象类型的不同来区分不同的异常,另外对象本身还可以携带信息。通过携带信息了解异常的引发原因。

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 进行异常处理。

image-20210319143612356

# 其他异常特性

异常的传递总是会创建一个临时拷贝,即使异常的 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_errorunderflow_error
    • overflow_error:整型和浮点型计算过程中超过最大值的情况。
    • underflow_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_unexpectedunexpected_handler 有更多的限制,具体来说 unexpected_handler 可以:

  • 通过调用 terminateabortexit 来终止程序。
  • 引发异常。
    • 如果新引发的异常与原来的异常规则匹配,则程序将从那里开始进行正常处理。
    • 如果新引发的异常与原来的不匹配,且异常规范中没有包括 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* 类型
// 如果可以则正常运行,否则返回空指针

image-20210319165440120

# 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 = &num;
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 版本编译器编译该源文件并执行,得到的输出结果如下:

image-20210319172634267

# 参考资料:

  • C++ 类型转换之 reinterpret_cast