《C++ Primer Plus》15. 友元、异常和其他

友元

友元并不破坏封装的性质,它是更灵活的接口

友元类

只需在类声明中加入:

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),它表明,如果类型 T1T2 的移动分配(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 块中的返回地址,返回到那里执行异常处理程序

与函数返回不同的是,栈解退会释放从 throwtry 块之间所有在栈中的自动变量

image-20250302194155176

如果没有处理某个异常的 try-catch 组合,那么程序将异常终止

其他异常特性

引发异常时编译器总是创建一个临时拷贝,即使异常规范和 catch 块中指定的是引用

class Problem { ... };

void super() throw(Problem) {
    ...
    if (...) {
        Problem oops;
        throw oops;
    }
    ...
}


try {
    super();
} catch (Problem& p) {
    ...
}

既然 throw 语句生成副本,那么为什么 catch 要使用引用呢?

原因是:引用可以执行派生类对象,基类引用可以与任何它的派生类对象相匹配

匹配的顺序是从前往后,这意味着派生类的处理块应该在基类之前

C++ 还有一个可以捕获任何异常的语句,那就是省略号:

catch (...) {
    // statement
}

所以可以将该处理块放在最后,类似 switchdefault

exception

在 C++ 中,可以把 exception 作为其他异常类的基类,它有一个 what() 虚方法,返回一个字符串

C++ 还定义了很多基于 exception 的异常类型:

stdexcept 头文件定义了:

  • logic_error:逻辑错误,它派生了
    • domain_error:定义域错误,例如反正弦函数如果参数不在 -1 到 1 之间就可以抛出该错误
    • invalid_argument:给函数传递了一个意料外的值
    • length_error:没有足够的空间来执行所需操作,例如 stringappend() 方法在合并得到的字符串长度超过最大允许长度时会引发该错误
    • 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() 函数即可

image-20250302202052702

总之,如果要捕获所有的异常,则可以这样做:

#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 为空指针,ps2ps3 都成功转换

有时需要手动开启 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,它会产生一个新的值,这个值会有与原始参数有完全相同的比特位,有一些限制:
    • 不能将指针类型转换成更小的整型或浮点型,转换到的类型必须足够存储指针
    • 不能在函数指针和数据指针之间转换