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 中捕获: 错误
分析:
funcA、funcB、funcC依次创建了对象 A、B、CfuncC中抛出异常- 栈解退按逆序销毁对象:C → B → A(先构造的后析构)
- 析构函数保证了资源不泄漏
重要: 这就是为什么异常安全的一个重要原则------在构造函数中获取的资源一定要在析构函数中释放。因为栈解退会保证析构函数被调用。
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 块,异常会"迷失方向":
- 在
main()中抛出的异常没有被捕获 → 调用std::terminate() - 在析构函数中抛出异常且没有被捕获 → 可能调用
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++程序更加健壮、灵活和可维护。