C++ 学习教程,从入门到精通,C++ 友元、异常和其他特性 — 详细知识点(15)

C++ 友元、异常和其他特性 --- 详细知识点


一、友元(Friend)

1.1 为什么需要友元?

在C++中,类的 private 成员只能被该类本身的成员函数访问。但有时候,外部的函数或另一个类需要直接访问本类的私有数据,这时候就需要"友元"机制。

生活类比: 你家的保险箱只有你能开,但你信任你的好朋友,给了他一把钥匙------这就是"友元"。你主动授权,他才能进来。

用途: 友元在运算符重载(如重载 << 输出运算符)、两个类需要紧密协作时非常常见。


1.2 友元类(Friend Class)

如果将类B声明为类A的友元,那么类B的所有成员函数都可以访问类A的私有和保护成员。

示例:
cpp 复制代码
#include <iostream>
using namespace std;

class TV {
    int volume;       // 私有成员
    int channel;      // 私有成员
public:
    TV() : volume(30), channel(5) {}
    friend class Remote;  // 声明 Remote 是 TV 的友元
};

class Remote {
public:
    void showInfo(const TV& t) {
        // Remote 不是 TV 的成员函数,但因为是友元,可以直接访问私有成员
        cout << "频道: " << t.channel << ", 音量: " << t.volume << endl;
    }
};

int main() {
    TV myTV;
    Remote remote;
    remote.showInfo(myTV);  // 输出: 频道: 5, 音量: 30
    return 0;
}
要点:
  • 友元关系是单向的:A是B的友元,不代表B是A的友元。
  • 友元关系不能传递:A是B的友元,B是C的友元,不代表A是C的友元。
  • 友元关系不能继承。
  • 声明位置:在类的内部用 friend 关键字声明。

1.3 友元成员函数(Friend Member Function)

有时候我们不想让整个类都成为友元,而只想让另一个类的某个特定成员函数访问本类的私有数据。

示例:
cpp 复制代码
#include <iostream>
using namespace std;

class TV;  // 前向声明

class Remote {
public:
    void showChannel(const TV& t);  // 先声明,后定义
};

class TV {
    int channel;
public:
    TV() : channel(5) {}
    friend void Remote::showChannel(const TV& t);  // 只把这一个函数声明为友元
};

// 定义 Remote::showChannel
void Remote::showChannel(const TV& t) {
    cout << "当前频道: " << t.channel << endl;
}

int main() {
    TV myTV;
    Remote r;
    r.showChannel(myTV);  // 正常工作
    return 0;
}
为什么要前向声明?

因为 Remote::showChannel 的声明需要用到 TV 类型的参数,但此时 TV 类还没定义完,所以需要先写 class TV; 告诉编译器"有一个类叫TV"。


1.4 共同的友元(Common Friend)

有时候,函数需要同时访问两个类的私有成员。可以把这个函数同时声明为两个类的友元。

示例:
cpp 复制代码
#include <iostream>
using namespace std;

class TV;

class Remote {
    int code;
public:
    Remote() : code(0) {}
    friend void sync(TV& t, Remote& r);  // 声明友元
};

class TV {
    int channel;
public:
    TV() : channel(1) {}
    friend void sync(TV& t, Remote& r);  // 声明友元
};

// 同时访问 TV 和 Remote 的私有成员
void sync(TV& t, Remote& r) {
    cout << "TV频道: " << t.channel << ", Remote代码: " << r.code << endl;
}

int main() {
    TV tv;
    Remote remote;
    sync(tv, remote);
    return 0;
}

用途: 两个类需要协作但又不希望对方成为自己的友元类时,可以用一个共同友元函数来"居中协调"。


1.5 友元的注意事项

要点 说明
友元破坏了封装性 给了外部访问私有数据的权限
慎重使用 只在确实需要时使用,不要滥用
友元没有继承性 基类的友元不一定是派生类的友元
友元没有传递性 A的友元不是B的友元
声明位置 只能在类定义内部声明,不能在.cpp文件中补声明

二、嵌套类(Nested Class)

2.1 什么是嵌套类?

在一个类的内部定义另一个类,称为嵌套类。

cpp 复制代码
class Outer {
public:
    class Nested {      // Nested 是 Outer 的嵌套类
    public:
        void show();
    };
};

2.2 为什么需要嵌套类?

当一个辅助类只在外部类中使用时,把它放在外部类内部可以:

  • 缩小作用域:避免污染全局命名空间
  • 逻辑更清晰:表达两个类之间的从属关系
经典案例:链表节点
cpp 复制代码
#include <iostream>
using namespace std;

class LinkedList {
private:
    // 嵌套类 Node ------ 只在 LinkedList 内部使用
    struct Node {
        int data;
        Node* next;
        Node(int val) : data(val), next(nullptr) {}
    };

    Node* head;

public:
    LinkedList() : head(nullptr) {}

    void add(int val) {
        Node* newNode = new Node(val);
        newNode->next = head;
        head = newNode;
    }

    void print() const {
        Node* current = head;
        while (current) {
            cout << current->data << " -> ";
            current = current->next;
        }
        cout << "NULL" << endl;
    }

    ~LinkedList() {
        while (head) {
            Node* temp = head;
            head = head->next;
            delete temp;
        }
    }
};

int main() {
    LinkedList list;
    list.add(3);
    list.add(2);
    list.add(1);
    list.print();  // 输出: 1 -> 2 -> 3 -> NULL
    return 0;
}

外面根本不需要知道 Node 的存在,它只是链表的内部实现细节。


2.3 嵌套类与访问权限

嵌套类可以访问外部类的 public、protected、private 成员吗?答案是:不能直接访问。

嵌套类只是在语法上位于外部类内部,但它不是外部类的成员函数,所以不能直接访问外部类的私有成员。要访问的话,需要通过外部类的对象(或指针/引用)。

反过来,外部类可以访问嵌套类的所有成员(包括私有成员),因为外部类有更高层级的"管辖权"。

cpp 复制代码
class Outer {
    int secret = 42;
public:
    class Nested {
    public:
        void show(Outer& obj) {
            // cout << secret;        // 错误!不能直接访问
            cout << obj.secret << endl;  // 正确!通过对象访问
        }
    };
};

2.4 模板中的嵌套

嵌套类可以出现在模板类中,这在STL等库的实现中很常见。

cpp 复制代码
template <typename T>
class Container {
    T value;
public:
    class Iterator {          // 嵌套的迭代器类
        T* ptr;
    public:
        Iterator(T* p) : ptr(p) {}
        T& operator*() { return *ptr; }
    };

    Container(T v) : value(v) {}
    Iterator getIterator() { return Iterator(&value); }
};

int main() {
    Container<int> c(10);
    Container<int>::Iterator it = c.getIterator();
    cout << *it << endl;  // 输出: 10
    return 0;
}

三、异常(Exception)

3.1 为什么需要异常处理?

程序运行时可能遇到各种意外情况:

  • 内存不够
  • 文件打不开
  • 数组越界
  • 除以零
  • 网络断开

如果对每种错误都用返回值检测,代码会变得非常冗长,而且容易忘记处理。异常机制让我们能把"错误处理代码"和"正常逻辑代码"分离开来。

生活类比: 你寄快递,如果途中出了问题,快递公司会打电话通知你(抛出异常),你接到电话后再决定是重新寄还是换一家(捕获并处理异常)。


3.2 调用 abort() 终止程序

在不使用异常机制的情况下,遇到严重错误时可以调用 abort() 来强制终止程序。

cpp 复制代码
#include <iostream>
#include <cstdlib>  // 包含 abort()

double divide(int a, int b) {
    if (b == 0) {
        cout << "错误:除数为零!程序终止。" << endl;
        abort();  // 终止程序,会生成一个非正常退出码
    }
    return static_cast<double>(a) / b;
}

int main() {
    cout << divide(10, 2) << endl;   // 正常输出 5
    cout << divide(10, 0) << endl;   // 程序在这里终止
    return 0;
}

abort() 的问题: 太粗暴了,直接终止程序,没有给程序任何"善后"的机会。比如没有释放动态分配的内存、没有保存数据等。


3.3 返回错误码

另一种方式是通过返回值来表示错误:

cpp 复制代码
// 返回值为 true 表示成功,false 表示失败
bool divide(int a, int b, double& result) {
    if (b == 0) {
        return false;  // 表示出错
    }
    result = static_cast<double>(a) / b;
    return true;  // 表示成功
}

int main() {
    double result;
    if (divide(10, 2, result)) {
        cout << "结果: " << result << endl;
    } else {
        cout << "除数不能为零!" << endl;
    }
    return 0;
}

问题: 每次调用都要检查返回值,代码变得冗长。而且如果返回值同时承载了"有效数据"和"错误信息",设计会很混乱。


3.4 异常机制(try、throw、catch)

C++的异常机制提供了一种更优雅的方式:

复制代码
try    → 放可能出错的代码
throw  → 发生错误时抛出异常
catch  → 捕获并处理异常
基本示例:
cpp 复制代码
#include <iostream>
using namespace std;

double divide(int a, int b) {
    if (b == 0) {
        throw "除数不能为零!";  // 抛出一个字符串常量作为异常
    }
    return static_cast<double>(a) / b;
}

int main() {
    try {
        cout << divide(10, 2) << endl;   // 正常输出 5
        cout << divide(10, 0) << endl;   // 这里会抛出异常
        cout << "这行不会被执行" << endl; // 异常发生后,控制权跳转
    }
    catch (const char* msg) {  // 捕获字符串类型的异常
        cout << "异常: " << msg << endl;
    }

    cout << "程序继续运行..." << endl;  // 异常处理完后继续
    return 0;
}

输出:

复制代码
5
异常: 除数不能为零!
程序继续运行...
执行流程图解:
复制代码
try 块正常执行
     │
     ├── divide(10, 2) 正常返回 5
     │
     ├── divide(10, 0) → throw 异常
     │       │
     │       ▼
     │   跳过 try 块中剩余的代码
     │       │
     │       ▼
     └── 找到匹配的 catch 块执行
              │
              ▼
         继续执行 catch 之后的代码
多个 catch 块:
cpp 复制代码
try {
    int choice;
    cout << "选择 (1-整数异常, 2-字符串异常): ";
    cin >> choice;

    if (choice == 1) {
        throw 42;          // 抛出 int
    } else {
        throw "错误信息";  // 抛出 const char*
    }
}
catch (int e) {            // 捕获 int 类型
    cout << "整数异常: " << e << endl;
}
catch (const char* msg) {  // 捕获字符串类型
    cout << "字符串异常: " << msg << endl;
}

注意: catch 块的顺序很重要!C++从上往下匹配,一旦匹配到第一个就不再往下找。所以要把更具体的类型放在前面。


3.5 将对象用作异常类型

用自定义类对象作为异常类型,可以携带更丰富的错误信息。

cpp 复制代码
#include <iostream>
#include <string>
using namespace std;

// 自定义异常类
class MathError {
    string message;
    int errorCode;
public:
    MathError(const string& msg, int code) : message(msg), errorCode(code) {}

    string getMessage() const { return message; }
    int getCode() const { return errorCode; }
};

double divide(int a, int b) {
    if (b == 0) {
        throw MathError("除数不能为零", 1001);  // 抛出对象
    }
    if (a == 0 && b == 0) {
        throw MathError("0/0 无意义", 1002);
    }
    return static_cast<double>(a) / b;
}

int main() {
    try {
        cout << divide(10, 2) << endl;
        cout << divide(10, 0) << endl;
    }
    catch (const MathError& e) {  // 用引用捕获,避免复制
        cout << "错误码: " << e.getCode() << endl;
        cout << "错误信息: " << e.getMessage() << endl;
    }
    return 0;
}
为什么推荐用对象而不是简单的字符串?
  • 可以携带错误码、堆栈信息、时间戳等更多数据
  • 可以通过继承体系对异常进行分类
  • 使用 const 引用捕获可以避免对象复制的开销

3.6 异常规范和C++11

旧的异常规范(C++98):
cpp 复制代码
// 表示这个函数只抛出 int 类型的异常
void func() throw(int) {
    throw 42;
}

// 表示这个函数不抛出任何异常
void safeFunc() throw() {
    // ...
}

问题: 如果函数抛出了规范之外的异常,C++会调用 terminate(),而且编译器不会在编译时检查异常规范的正确性。所以这个特性被广泛认为是一个设计失误。

C++11 的替代方案:
cpp 复制代码
// noexcept 关键字:表示函数不会抛出异常
void safeFunc() noexcept {
    // 这个函数保证不抛异常
}

// 也可以用于条件判断
void func() noexcept(true) { }   // 不抛异常
void func2() noexcept(false) { } // 可能抛异常

noexcept 比旧的 throw() 规范更实用,编译器可以据此做更好的优化,而且在违反时的行为更可控。


3.7 栈解退(Stack Unwinding)

当异常被抛出时,C++会沿着函数调用栈 一层层往上找匹配的 catch,沿途每个函数中创建的局部对象都会被自动销毁(调用析构函数)。

这个过程就叫栈解退。

示例:
cpp 复制代码
#include <iostream>
using namespace std;

class Demo {
    string name;
public:
    Demo(const string& n) : name(n) {
        cout << "构造 " << name << endl;
    }
    ~Demo() {
        cout << "析构 " << name << endl;
    }
};

void funcC() {
    Demo obj("C");
    cout << "在 funcC 中抛出异常" << endl;
    throw "错误";  // 抛出异常
}

void funcB() {
    Demo obj("B");
    funcC();       // 调用 funcC
    cout << "这行不会执行" << endl;
}

void funcA() {
    Demo obj("A");
    funcB();       // 调用 funcB
    cout << "这行不会执行" << endl;
}

int main() {
    try {
        funcA();
    }
    catch (const char* msg) {
        cout << "在 main 中捕获: " << msg << endl;
    }
    return 0;
}

输出:

复制代码
构造 A
构造 B
构造 C
在 funcC 中抛出异常
析构 C
析构 B
析构 A
在 main 中捕获: 错误

分析:

  1. funcA、funcB、funcC 依次创建了对象 A、B、C
  2. funcC 中抛出异常
  3. 栈解退按逆序销毁对象:C → B → A(先构造的后析构)
  4. 析构函数保证了资源不泄漏

重要: 这就是为什么异常安全的一个重要原则------在构造函数中获取的资源一定要在析构函数中释放。因为栈解退会保证析构函数被调用。


3.8 其他异常特性

(1)异常重新抛出

一个 catch 块可以处理部分异常后,把异常重新抛出给上层调用者:

cpp 复制代码
void funcB() {
    try {
        // 可能出错的操作
        throw runtime_error("内存不足");
    }
    catch (runtime_error& e) {
        cout << "funcB: 记录日志 - " << e.what() << endl;
        throw;  // 重新抛出同一个异常
    }
}

int main() {
    try {
        funcB();
    }
    catch (runtime_error& e) {
        cout << "main: 处理 - " << e.what() << endl;
    }
    return 0;
}
(2)捕获所有异常

用 ... 可以捕获任何类型的异常:

cpp 复制代码
try {
    // 可能抛出任何类型的异常
}
catch (...) {
    cout << "捕获了某种未知异常" << endl;
}
(3)异常的继承层次

标准库提供了异常的基类 std::exception,可以利用继承来组织自定义异常:

cpp 复制代码
#include <iostream>
#include <stdexcept>
#include <string>
using namespace std;

// 自定义异常继承自标准异常
class FileNotFoundError : public runtime_error {
    string filename;
public:
    FileNotFoundError(const string& file)
        : runtime_error("文件未找到"), filename(file) {}

    string getFilename() const { return filename; }
};

void readFile(const string& filename) {
    if (filename != "data.txt") {
        throw FileNotFoundError(filename);
    }
    cout << "成功打开 " << filename << endl;
}

int main() {
    try {
        readFile("data.txt");
        readFile("abc.txt");
    }
    catch (const FileNotFoundError& e) {
        cout << "文件错误: " << e.what()
             << ", 文件名: " << e.getFilename() << endl;
    }
    return 0;
}

3.9 exception 类

C++标准库在 <exception> 头文件中定义了 std::exception 基类:

复制代码
std::exception
├── std::logic_error        (逻辑错误,可以在编码阶段避免)
│   ├── invalid_argument    (无效参数)
│   ├── domain_error        (域错误)
│   ├── length_error        (长度超限)
│   └── out_of_range        (越界)
├── std::runtime_error      (运行时错误,难以在编码阶段预防)
│   ├── range_error         (范围错误)
│   ├── overflow_error      (溢出)
│   └── underflow_error     (下溢)
└── std::bad_alloc          (内存分配失败)
常用示例:
cpp 复制代码
#include <iostream>
#include <vector>
#include <stdexcept>
using namespace std;

int main() {
    vector<int> v(5);

    try {
        // 越界访问
        cout << v.at(10) << endl;  // at() 会抛出 out_of_range
    }
    catch (const out_of_range& e) {
        cout << "越界: " << e.what() << endl;
    }

    try {
        // 内存分配失败
        vector<int> big(100000000000LL);
    }
    catch (const bad_alloc& e) {
        cout << "内存分配失败: " << e.what() << endl;
    }

    return 0;
}

建议: 尽量使用标准库提供的异常类,而不是自己造轮子。如果需要自定义异常,继承自 std::exception 的子类。


3.10 异常、类和继承的关系

当类的成员函数可能抛出异常时,异常和类的继承体系可以很好地结合:

cpp 复制代码
#include <iostream>
#include <stdexcept>
#include <string>
using namespace std;

// 基础异常类
class AppException : public runtime_error {
public:
    AppException(const string& msg) : runtime_error(msg) {}
};

// 网络相关异常
class NetworkException : public AppException {
    int statusCode;
public:
    NetworkException(int code, const string& msg)
        : AppException(msg), statusCode(code) {}
    int getStatus() const { return statusCode; }
};

// 数据库相关异常
class DatabaseException : public AppException {
    string query;
public:
    DatabaseException(const string& q, const string& msg)
        : AppException(msg), query(q) {}
    string getQuery() const { return query; }
};

void connectNetwork() {
    throw NetworkException(404, "服务器未找到");
}

void queryDatabase() {
    throw DatabaseException("SELECT * FROM users", "表不存在");
}

int main() {
    try {
        connectNetwork();
        queryDatabase();
    }
    catch (const NetworkException& e) {  // 先捕获具体类型
        cout << "网络错误 [" << e.getStatus() << "]: " << e.what() << endl;
    }
    catch (const DatabaseException& e) {
        cout << "数据库错误: " << e.what() << " (查询: " << e.getQuery() << ")" << endl;
    }
    catch (const AppException& e) {      // 最后捕获通用类型
        cout << "应用错误: " << e.what() << endl;
    }
    return 0;
}

要点: catch 块的顺序要从最具体到最通用,和 switch-case 的 default 逻辑类似。


3.11 异常何时会迷失方向

当异常被抛出后,如果没有找到任何匹配的 catch 块,异常会"迷失方向":

  1. 在 main() 中抛出的异常没有被捕获 → 调用 std::terminate()
  2. 在析构函数中抛出异常且没有被捕获 → 可能调用 std::terminate()

最危险的情况:在析构函数中抛出异常

cpp 复制代码
class Dangerous {
public:
    ~Dangerous() {
        throw "析构函数中的异常";  // 危险!
    }
};

int main() {
    try {
        Dangerous d;
    }
    catch (const char* msg) {
        cout << msg << endl;
    }
    return 0;
}

当多个对象同时销毁时(比如容器销毁),一个析构函数抛异常后,其他对象的析构函数也在执行中,此时如果再抛异常,程序会直接终止。

黄金规则: 析构函数中不要抛出异常!如果析构函数中可能出错,应该在析构函数内部用 try-catch 吞掉异常。


3.12 有关异常的注意事项

建议 说明
在层次结构的适当位置捕获 不要每个函数都 try-catch,在最合适的层级处理
异常按引用捕获 catch (const MyError& e) 而不是 catch (MyError e),避免对象切片
析构函数不抛异常 用 try-catch 在析构内部处理
不滥用异常 正常的控制流不要用异常,异常用于异常情况
使用标准异常类 尽量继承自 std::exception
及时清理资源 或者使用 RAII / 智能指针
noexcept 用于不抛异常的函数 让编译器优化和保证

四、RTTI(Run-Time Type Information,运行时类型识别)

4.1 什么是RTTI?

RTTI 允许程序在运行时(不是编译时)获取对象的实际类型信息。

为什么需要它? 在使用多态(基类指针/引用指向派生类对象)时,有时候需要知道"这个指针实际指向的是什么类型的对象"。

生活类比: 你看到一辆车(基类),但你不确定它是轿车还是卡车(派生类)。RTTI 就是让你能在运行时查"行驶证"来确认。


4.2 RTTI的用途

场景:动态类型转换的安全检查
cpp 复制代码
#include <iostream>
using namespace std;

class Animal {
public:
    virtual void speak() { cout << "..." << endl; }
    virtual ~Animal() {}
};

class Dog : public Animal {
public:
    void speak() override { cout << "汪汪!" << endl; }
    void fetch() { cout << "狗狗在捡球!" << endl; }  // Dog 特有的方法
};

class Cat : public Animal {
public:
    void speak() override { cout << "喵喵~" << endl; }
    void purr() { cout << "猫在打呼噜~" << endl; }    // Cat 特有的方法
};

void interact(Animal* animal) {
    animal->speak();  // 多态调用

    // 用 dynamic_cast 检查实际类型
    if (Dog* dog = dynamic_cast<Dog*>(animal)) {
        dog->fetch();   // 安全地调用 Dog 特有的方法
    }
    else if (Cat* cat = dynamic_cast<Cat*>(animal)) {
        cat->purr();    // 安全地调用 Cat 特有的方法
    }
}

int main() {
    Dog rex;
    Cat kitty;

    interact(&rex);   // 输出: 汪汪! 狗狗在捡球!
    interact(&kitty); // 输出: 喵喵~ 猫在打呼噜~

    return 0;
}

没有RTTI的情况下 ,你只能通过虚函数做统一调用,无法安全地"向下转换"到具体类型。dynamic_cast 就是RTTI的一个重要应用。


4.3 RTTI的工作原理

C++通过两个运算符实现RTTI:

(1)typeid 运算符

返回一个 type_info 对象,包含类型信息。需要头文件 <typeinfo>。

cpp 复制代码
#include <iostream>
#include <typeinfo>
using namespace std;

class Base {
public:
    virtual void show() {}
};

class Derived : public Base {};

int main() {
    int x = 10;
    double y = 3.14;

    // 基本类型
    cout << "x 的类型: " << typeid(x).name() << endl;
    cout << "y 的类型: " << typeid(y).name() << endl;

    // 多态中的 typeid(看实际类型)
    Base b;
    Derived d;
    Base* ptr = &d;

    cout << "b 的类型: " << typeid(b).name() << endl;
    cout << "ptr 指向: " << typeid(*ptr).name() << endl;
    // 输出取决于编译器,可能是 Derived

    // 比较类型
    if (typeid(*ptr) == typeid(Derived)) {
        cout << "ptr 实际指向 Derived 对象" << endl;
    }

    return 0;
}

注意: typeid 在有虚函数的类上使用时,返回的是运行时的实际类型;对基本类型或没有虚函数的类,则是编译时确定的类型。

(2)dynamic_cast 运算符

用于在继承层次中安全地进行类型转换:

cpp 复制代码
Base* basePtr = new Derived();

// 安全向下转换
Derived* derivedPtr = dynamic_cast<Derived*>(basePtr);
if (derivedPtr != nullptr) {
    cout << "转换成功!" << endl;
} else {
    cout << "转换失败(不是目标类型)" << endl;
}

// 对引用使用时,如果失败会抛出 bad_cast 异常
try {
    Base& baseRef = *basePtr;
    Derived& derivedRef = dynamic_cast<Derived&>(baseRef);
    cout << "引用转换成功!" << endl;
} catch (bad_cast& e) {
    cout << "引用转换失败: " << e.what() << endl;
}
dynamic_cast 的要求:
  • 基类必须有虚函数(至少有一个虚函数)
  • 转换方向:可以向上转换(派生→基类)或向下转换(基类→派生类)
各种转换运算符的对比:
运算符 作用 安全检查时机 适用场景
static_cast 编译时类型转换 编译时 明确知道类型时的转换
dynamic_cast 运行时安全转换 运行时 多态中的安全向下转换
const_cast 去掉/添加 const 编译时 修改 const 对象(慎用)
reinterpret_cast 重新解释内存 编译时 底层位模式转换(极慎用)

4.4 类型转换运算符详解

static_cast

最常见的类型转换,编译时检查:

cpp 复制代码
double pi = 3.14159;

// 显式类型转换
int intPi = static_cast<int>(pi);  // 3,截断小数部分

// 明确知道类型关系时的向下转换
Base* base = new Derived();
Derived* d = static_cast<Derived*>(base);  // 编译通过,但不检查实际类型
// 如果 base 实际指向 Base 对象,这个转换是未定义行为!
const_cast

只能用于修改 const 属性:

cpp 复制代码
const int value = 42;
const int* p = &value;

// 去掉 const
int* mutableP = const_cast<int*>(p);
// *mutableP = 100;  // 未定义行为!原始对象是 const 的

// 实际上 const_cast 在某些场景下确实有用,比如调用遗留的非 const API
reinterpret_cast

最危险的转换,只是重新解释内存中的位模式:

cpp 复制代码
int x = 65;
char* ch = reinterpret_cast<char*>(&x);
cout << *ch << endl;  // 输出 'A'(因为 65 的 ASCII 码是 'A')

// 函数指针与整数之间的转换
typedef void (*FuncPtr)();
int addr = 0x12345678;
// FuncPtr fp = reinterpret_cast<FuncPtr>(addr);  // 极度危险!

五、综合案例

将本章的知识点综合起来,看一个完整的小项目:

cpp 复制代码
#include <iostream>
#include <string>
#include <vector>
#include <memory>
#include <stdexcept>
#include <typeinfo>

using namespace std;

// ========== 异常类体系 ==========
class DeviceException : public runtime_error {
public:
    DeviceException(const string& msg) : runtime_error(msg) {}
};

class VolumeOutOfRangeException : public DeviceException {
    int volume;
public:
    VolumeOutOfRangeException(int v)
        : DeviceException("音量超出范围"), volume(v) {}
    int getVolume() const { return volume; }
};

// ========== 电视机类 ==========
class TV {
    int channel;
    int volume;
    bool isOn;

public:
    TV() : channel(1), volume(20), isOn(false) {}

    void power() {
        isOn = !isOn;
        cout << (isOn ? "电视已开机" : "电视已关机") << endl;
    }

    void setVolume(int v) {
        if (v < 0 || v > 100) {
            throw VolumeOutOfRangeException(v);
        }
        volume = v;
        cout << "音量设置为: " << volume << endl;
    }

    // 声明 Remote 为友元
    friend class Remote;
};

// ========== 遥控器类(友元类)==========
class Remote {
public:
    // 可以直接访问 TV 的私有成员
    void setChannel(TV& tv, int ch) {
        tv.channel = ch;
        cout << "频道切换到: " << ch << endl;
    }

    void showStatus(const TV& tv) {
        cout << "=== 电视状态 ===" << endl;
        cout << "频道: " << tv.channel << endl;
        cout << "音量: " << tv.volume << endl;
        cout << "电源: " << (tv.isOn ? "开" : "关") << endl;
    }
};

// ========== 演示异常处理 ==========
void demoExceptions() {
    cout << "\n--- 异常处理演示 ---" << endl;
    TV tv;

    // 正常操作
    try {
        tv.setVolume(50);   // 正常
        tv.setVolume(120);  // 抛出异常
    }
    catch (const VolumeOutOfRangeException& e) {
        cout << "捕获异常: " << e.what()
             << " (当前值: " << e.getVolume() << ")" << endl;
    }

    // 栈解退演示
    cout << "\n--- 栈解退演示 ---" << endl;
    try {
        Remote remote;
        remote.showStatus(tv);
        throw DeviceException("紧急异常");
    }
    catch (const DeviceException& e) {
        cout << "捕获: " << e.what() << endl;
    }
}

// ========== 演示 RTTI ==========
class Widget {
public:
    virtual string getType() const { return "Widget"; }
    virtual ~Widget() {}
};

class Button : public Widget {
public:
    string getType() const override { return "Button"; }
    void click() { cout << "按钮被点击" << endl; }
};

class TextBox : public Widget {
public:
    string getType() const override { return "TextBox"; }
    void type() { cout << "文本框输入中..." << endl; }
};

void demoRTTI() {
    cout << "\n--- RTTI 演示 ---" << endl;

    vector<unique_ptr<Widget>> widgets;
    widgets.push_back(make_unique<Button>());
    widgets.push_back(make_unique<TextBox>());
    widgets.push_back(make_unique<Widget>());

    for (const auto& w : widgets) {
        cout << "类型: " << w->getType() << endl;

        // 用 dynamic_cast 安全转换
        if (Button* btn = dynamic_cast<Button*>(w.get())) {
            btn->click();
        }
        else if (TextBox* tb = dynamic_cast<TextBox*>(w.get())) {
            tb->type();
        }
        else {
            cout << "普通 Widget,无特殊操作" << endl;
        }

        // 用 typeid 检查类型
        cout << "typeid: " << typeid(*w).name() << endl;
    }
}

int main() {
    cout << "========= 第15章 综合演示 =========" << endl;

    // 友元类演示
    cout << "--- 友元类演示 ---" << endl;
    TV livingRoomTV;
    Remote remote;
    livingRoomTV.power();
    remote.setChannel(livingRoomTV, 8);
    remote.showStatus(livingRoomTV);

    // 异常处理演示
    demoExceptions();

    // RTTI 演示
    demoRTTI();

    return 0;
}

六、知识点总结

知识点 核心要点
友元类 一个类的所有成员函数都能访问另一个类的私有成员;单向、不传递、不继承
友元成员函数 只让另一个类的特定成员函数访问私有数据;需要前向声明
共同友元 一个函数同时是两个类的友元,用于两个类需要协作的场景
嵌套类 类内部定义的类,缩小作用域,逻辑更清晰
嵌套类访问权限 嵌套类不能直接访问外部类私有成员,需要通过对象
abort() 强制终止程序,太粗暴,不推荐
返回错误码 传统方式,每调用一次都要检查,代码冗长
异常机制 try-throw-catch,将错误处理与正常逻辑分离
对象作为异常 用类对象携带丰富的错误信息
noexcept C++11 替代旧的异常规范,表示函数不抛异常
栈解退 异常沿调用栈传播时,自动销毁沿途的局部对象
异常继承 利用继承组织异常层次,从具体到通用捕获
RTTI 运行时获取对象真实类型信息
typeid 获取类型的 type_info 对象
dynamic_cast 运行时安全的向下类型转换(需要虚函数)
static_cast 编译时类型转换,最常用
const_cast 修改 const 属性,慎用
reinterpret_cast 底层内存重解释,极度危险

学习本章后的收获: 你将能够用友元合理地设计类之间的协作关系,用异常机制优雅地处理程序运行时的错误,用嵌套类组织代码结构,用RTTI在多态环境中安全地识别和操作具体类型的对象。这些特性让你的C++程序更加健壮、灵活和可维护。

相关推荐
ebiobiz1 小时前
极海 APM32 使用 Nimmake 编译指南
c++·python·单片机·嵌入式硬件·mcu
朝朝辞暮i1 小时前
C++ 第 26 课:对象、指针与 ->
java·开发语言·c++
fanged1 小时前
Camera学习2(V4L2和Media Controller)(TODO)
学习
2601_962885722 小时前
如何用 Python 做股价历史分位选股?(筛出处于历史低位的股票)
开发语言·python
殷色玫瑰2 小时前
C++入门基础复习:从命名空间到引用与nullptr,一篇重新捡回C++基础
java·开发语言·c++
(Charon)2 小时前
【C++面试】手写线程池:任务队列、工作线程与优雅退出
开发语言·c++
言乐63 小时前
Python概括后端原理
开发语言·python·django·virtualenv·pygame
无名猿3 小时前
拷贝构造与拷贝赋值:调用时机、深浅拷贝与复制消除
c++·内存管理·现代c++·踩坑记录
@大迁世界3 小时前
JEV RAG:更高效的 RAG 系统解决方案?
开发语言·rust