C++高级特性实战解析:虚函数、友元、异常与多态应用

📅 2026/7/12 10:06:35 👁️ 阅读次数 📝 编程学习
C++高级特性实战解析:虚函数、友元、异常与多态应用

1. 项目概述:深入C++核心机制的实战精讲

每次翻开《C++ Primer》的第15章,看到“友元、异常和其他高级特性”这个标题,很多C++学习者,包括当年的我,都会有种既期待又头疼的感觉。期待是因为这一章讲的都是能让代码“活”起来、更健壮、更灵活的核心机制;头疼则是因为这些概念,尤其是当它们和前面章节的虚函数、多态交织在一起时,理解起来容易,但真正用对、用好、用出精髓却很难。我自己在从学生到工程师的成长路上,没少在这些特性上栽跟头,也正是在一次次调试和重构中,才逐渐摸清了它们的脾气。

这一章的内容,远不止是语法规则的罗列。它实际上是在教你如何用C++这门语言,去构建一个既坚固又富有弹性的软件结构。虚函数和多态是面向对象编程的“灵魂”,它让我们的代码能够以统一的接口处理不同的对象,实现“开闭原则”。而友元和异常,则是两个强大的“特种工具”:友元打破了严格的封装边界,在需要高效协作的类之间架起桥梁;异常则提供了一套非局部的错误处理流程,让我们的程序在遇到意外时能从容应对,而不是直接崩溃。至于其他高级特性,如运行时类型识别(RTTI)、枚举类、成员指针等,则是解决特定复杂问题的“瑞士军刀”。

本篇文章,我将以一个过来人的视角,结合十多年开发中积累的实战经验和踩过的坑,为你深度拆解《C++ Primer》第15章的精髓。我不会照本宣科,而是聚焦于这几个核心问题:为什么需要这些特性?它们解决了什么实际编程痛点?在什么场景下该用,什么场景下不该用?以及,当它们组合使用时,会产生哪些奇妙的化学反应或危险的陷阱?无论你是正在啃这本经典教材的学生,还是希望夯实C++底层功力的开发者,相信这篇融合了原理、代码与实战心得的解读,都能让你对C++的理解再上一个台阶。

2. 核心特性深度解析:从“是什么”到“为什么”

2.1 虚函数与多态:动态绑定的艺术与代价

虚函数和多态,是C++实现运行时多态性的基石。教科书上通常这么定义:通过基类的指针或引用调用虚函数时,实际调用的是指针或引用所指向的派生类对象的版本。这句话背下来容易,但理解其背后的“动态绑定”机制以及它带来的设计自由度和性能考量,才是关键。

2.1.1 虚函数表(vtable)的幕后真相

当你在一个类中声明一个虚函数时,编译器会为该类生成一个虚函数表。这是一个静态数组,存储了该类所有虚函数的地址。同时,该类的每个对象都会隐含一个指针(vptr),指向这个虚函数表。当通过基类指针调用pb->print()时,实际发生的是:

  1. 通过pb找到对象的vptr
  2. 通过vptr找到类的虚函数表。
  3. 在虚函数表中找到print函数对应的槽位。
  4. 跳转到该槽位存储的地址执行。

这个过程就是动态绑定,它发生在运行时。与之相对的是静态绑定,即对非虚函数的调用,在编译期就确定了具体函数地址。

注意:虚函数表是属于类的,而不是对象的。同一个类的所有对象共享同一份虚函数表。vptr才是每个对象独有的。这解释了为什么增加虚函数不会改变单个对象的大小(vptr大小固定),但会引入一个间接寻址的开销。

2.1.2 多态性的三个铁律与一个“坑”

要让多态正确工作,必须满足三个条件,这在很多资料里都有提及:

  1. 继承关系。
  2. 基类有虚函数,派生类覆盖(override)它。
  3. 通过基类的指针或引用来调用。

但这里有一个极易被忽视的“坑”:覆盖(override)不等于重载(overload),更不等于隐藏(hide)。在C++11之前,我们全靠程序员自己小心。例如:

class Base { public: virtual void func(int x) { std::cout << "Base::func(int)" << std::endl; } }; class Derived : public Base { public: // 意图是覆盖,但写错了参数类型,这实际上是一个新函数,隐藏了基类的虚函数。 virtual void func(double x) { std::cout << "Derived::func(double)" << std::endl; } }; int main() { Base* pb = new Derived(); pb->func(5); // 输出什么?答案是:Base::func(int) delete pb; return 0; }

你以为会调用Derived的版本,但由于参数类型不匹配,Derived::func(double)并没有覆盖Base::func(int),它只是隐藏了基类的同名函数。通过基类指针调用时,仍然调用基类的版本,多态失效。

2.1.3 C++11的override与final标识符这正是C++11引入overridefinal标识符的意义所在。override明确告诉编译器:“我就是要覆盖基类的虚函数”。如果签名不匹配,编译器会直接报错,将运行时错误提前到编译期。

class Derived : public Base { public: virtual void func(double x) override; // 编译错误!签名不匹配,无法覆盖。 virtual void func(int x) override; // 正确,明确表示覆盖。 };

final则用于类或虚函数,表示“禁止继承”或“禁止进一步覆盖”。这是一个强大的设计工具,可以明确你的意图,防止类被误继承或虚函数被意外修改。

class Base final { }; // 这个类不能被继承 class Base { public: virtual void func() final; // 这个虚函数在派生类中不能再被覆盖 };

2.1.4 性能考量与使用场景虚函数的动态绑定带来了灵活性,但也付出了代价:每次调用需要一次额外的指针解引用和跳转,并且阻碍了编译器的内联优化。在性能极其敏感的场合(如高频循环、底层硬件驱动),需要谨慎评估。然而,在绝大多数应用层、框架层代码中,虚函数带来的设计收益远大于其微小的性能损耗。它使得程序架构清晰,易于扩展,符合面向对象的设计原则。

2.2 友元:打破封装的“特洛伊木马”

友元(friend)是C++中一个颇具争议的特性。它允许一个类或函数访问另一个类的私有(private)和保护(protected)成员。这相当于在严密的封装墙上开了一扇后门。

2.2.1 友元的本质与正确使用姿势友元关系不是传递的(A是B的友元,B是C的友元,不意味着A是C的友元),也不是对称的(A是B的友元,不意味着B是A的友元),更不是继承的(这是最常见的误解,我们后面详细说)。它是一种强耦合关系,声明友元就等于说:“我完全信任你,把我的内部细节对你开放。”

那么,什么时候该用友元?一个经典且合理的场景是重载运算符,特别是为非成员函数重载运算符时。例如,重载<<用于输出自定义类:

class MyClass { private: int secret; public: MyClass(int s) : secret(s) {} // 声明全局函数operator<<为友元,使其能访问secret friend std::ostream& operator<<(std::ostream& os, const MyClass& obj); }; std::ostream& operator<<(std::ostream& os, const MyClass& obj) { os << "MyClass secret: " << obj.secret; // 可以访问私有成员secret return os; }

这里,operator<<作为非成员函数,需要访问MyClass的私有数据来实现输出,友元是最直接、最清晰的解决方案。

2.2.2 友元与继承的微妙关系:一个常见的误解很多初学者,包括一些经验不足的开发者,会认为“基类的友元也是派生类的友元”。这是一个危险的误解。我们通过一个例子来彻底澄清:

class Base { private: int base_private; protected: void base_protected() {} public: friend class FriendOfBase; // 声明友元类 }; class FriendOfBase { public: void access(Base& b) { b.base_private = 10; // OK: 友元可以访问Base的私有成员 b.base_protected(); // OK: 友元可以访问Base的保护成员 } }; class Derived : public Base { private: int derived_private; }; int main() { Derived d; FriendOfBase fob; // fob.access(d); // 这行代码从概念上理解:传递一个Derived对象给需要Base引用的函数,是允许的。 // 但是,在access函数内部,fob操作的是“d对象中继承自Base的那部分”,它仍然只能访问Base的私有/保护成员。 // 它绝对无法访问Derived独有的derived_private成员! }

关键在于:友元关系是针对类而言的,不是针对对象FriendOfBaseBase的友元,意味着FriendOfBase的成员函数可以访问任何一个Base类型对象的私有和保护成员。当把一个Derived对象d传给fob.access()时,参数b绑定的是d中属于Base的子对象部分。因此,fob可以访问d中从Base继承来的base_private,但它对Derived自身新增的derived_private一无所知,也没有访问权限。

所以,结论是:友元不能被继承。派生类不会自动获得基类友元的访问权限。如果你需要让某个函数或类能访问整个类继承体系的所有私有成员,你必须在每个类中单独声明友元。这通常是一个糟糕的设计信号,意味着你的类继承体系可能耦合度过高,需要重新审视设计。

2.3 异常处理:构建健壮程序的安全网

C++的异常机制提供了一种将错误检测与错误处理分离的机制。基本模型是try,catch,throw

2.3.1 异常相对于错误码的优势在C语言中,我们通常通过函数返回值(错误码)或全局变量(如errno)来报告错误。这种方式有几个缺点:

  1. 污染正常返回值:函数本应返回的结果,需要与错误码共用同一个通道。
  2. 错误处理必须立即进行:调用者必须检查返回值,否则错误会被忽略。
  3. 多层调用链处理繁琐:底层函数出错,需要每一层调用者都检查并传递错误码,代码冗长。

异常机制解决了这些问题:

  • 分离关注点:正常逻辑和错误处理逻辑分开。
  • 自动传播:异常会沿着调用栈向上自动传播,直到被捕获。中间函数无需关心。
  • 类型安全:可以抛出任何类型的对象(通常是派生自std::exception的类),携带丰富的错误信息。

2.3.2 异常安全保证:三个级别使用异常不仅仅是try-catch,更重要的是编写“异常安全”的代码。一个函数提供的异常安全保证分为三个级别:

  1. 基本保证(Basic Guarantee):如果抛出异常,程序状态仍然有效,无资源泄漏,但对象的具体状态可能不可预测。
  2. 强保证(Strong Guarantee):如果抛出异常,程序状态完全回滚到函数调用前的样子。这通常通过“拷贝-交换”(copy-and-swap)惯用法实现。
  3. 不抛保证(Nothrow Guarantee):承诺函数绝不抛出异常。析构函数、内存释放函数(如operator delete)通常应提供此保证。

在资源管理类(如智能指针、文件句柄类)中,我们利用RAII(Resource Acquisition Is Initialization)技术,将资源获取放在构造函数中,释放放在析构函数中。由于析构函数在栈展开时会被自动调用,从而保证了即使发生异常,资源也能被正确释放,这是实现基本保证和强保证的关键。

2.3.3 异常规范与noexceptC++98/03中有动态异常规范(throw(type)),但实践证明它难以用好且影响性能,在C++11中已被弃用。取而代之的是noexcept说明符。

  • noexcept表示函数不会抛出任何异常。如果它抛出了,程序会直接调用std::terminate()终止。这给了编译器更大的优化空间。
  • noexcept还可以作为一个运算符,用于判断一个表达式是否可能抛出异常(noexcept(expr))。 移动构造函数和移动赋值运算符通常应标记为noexcept,这会使标准库容器(如std::vector)在重新分配内存时,优先使用高效的移动操作而非拷贝操作。

2.3.4 异常的使用误区

  • 不要将异常用于正常的控制流:异常处理开销比正常函数返回大,只应用于真正的异常、罕见情况。
  • 避免在析构函数中抛出异常:如果栈展开过程中析构函数又抛出异常,程序会直接终止。
  • 按引用捕获异常catch (const std::exception& e)可以避免对象切片,也能捕获所有派生类异常。
  • 不要捕获所有异常然后什么都不做catch (...) { /* 默默忽略 */ }是调试的噩梦。

3. 高级特性组合应用与实战陷阱

当虚函数、友元、异常这些特性单独使用时,规则相对清晰。但当它们在复杂的类继承体系中交织在一起时,就会产生一些微妙甚至反直觉的行为。理解这些组合场景,是区分普通C++使用者和高手的关键。

3.1 虚函数、多态与友元的联动分析

让我们深入分析网络资料中那个经典的例子,它完美展示了虚函数如何与友元互动,并揭示了“友元不可继承”这一原则在动态绑定下的表现。

class A; class B { private: virtual void print() { std::cout << "B::print" << std::endl; } // 私有虚函数 public: friend class A; // A是B的友元 }; class A { public: void func(B* pb) { pb->print(); // 因为友元关系,A可以访问B的私有虚函数print } }; class D : public B { private: virtual void print() override { std::cout << "D::print" << std::endl; } // 覆盖虚函数 }; int main() { D d; A a; a.func(&d); // 输出什么? return 0; }

输出结果是:D::print

过程解析:

  1. AB的友元,因此A::func(B* pb)可以合法地调用pb->print(),即使B::print()是私有的。
  2. D公有继承自B,并覆盖了虚函数print()
  3. main中,a.func(&d)传递了一个指向D对象的指针(B*类型)。
  4. func内部,pb->print()是一个通过基类指针对虚函数的调用。
  5. 由于动态绑定,实际调用的是pb所指对象(即d)的虚函数表中所指向的函数,也就是D::print()
  6. 虽然D::print()D类中是私有的,但访问控制(public/private)是在编译时、根据调用点的静态类型来检查的
  7. func内部,调用点是pb->print()pb的静态类型是B*。因为AB的友元,所以对B::print()的访问是允许的。编译器只检查这一点。
  8. 运行时,由于多态,实际执行的是D::print()。只要对B::print()的访问合法,运行时就不会再检查D::print()的访问权限。这就像拿到了一把打开基类私有房间的钥匙(友元),通过这扇门(基类指针),你进入了派生类的房间(调用派生类虚函数),而派生类房间的私密性在此路径下不被检查。

关键结论:友元关系与多态机制是正交的。友元解决了编译时的访问权限问题,而多态决定了运行时执行哪个函数。两者结合,可以实现一种“受控的”跨类、跨继承层次的特定函数调用。但这绝不意味着A成为了D的友元。A无法直接访问D的非虚私有成员,也无法通过D的指针直接调用D::print()(如果D没有继承自B的话)。

3.2 异常处理在继承与多态中的最佳实践

异常类本身也经常构成一个继承体系,最顶层的通常是std::exception。利用多态来处理异常非常强大。

3.2.1 设计异常类继承体系

#include <stdexcept> #include <string> class MyBaseException : public std::runtime_error { public: MyBaseException(const std::string& msg) : std::runtime_error(msg) {} virtual ~MyBaseException() = default; // 基类析构函数应为虚函数 virtual std::string getDetails() const { return "Base exception"; } }; class NetworkException : public MyBaseException { public: NetworkException(const std::string& msg, int error_code) : MyBaseException(msg), m_error_code(error_code) {} std::string getDetails() const override { return "Network error code: " + std::to_string(m_error_code); } private: int m_error_code; }; class DatabaseException : public MyBaseException { // ... 类似,包含数据库特定的信息 };

3.2.2 捕获与处理

void riskyOperation() { // ... 可能抛出 NetworkException 或 DatabaseException throw NetworkException("Connection timeout", 10060); } void process() { try { riskyOperation(); } catch (const NetworkException& e) { // 优先捕获更具体的异常 std::cerr << "Network issue: " << e.what() << ", Details: " << e.getDetails() << std::endl; // 尝试重连或降级处理 } catch (const DatabaseException& e) { std::cerr << "Database issue: " << e.what() << std::endl; // 回滚事务 } catch (const MyBaseException& e) { // 捕获基类,处理所有其他派生自MyBaseException的未知异常 std::cerr << "Known exception type: " << e.what() << std::endl; } catch (const std::exception& e) { // 捕获所有标准异常 std::cerr << "Standard exception: " << e.what() << std::endl; } catch (...) { // 最后兜底,捕获所有其他未知类型的异常(非推荐做法,仅用于记录日志等最终处理) std::cerr << "Unknown exception caught!" << std::endl; throw; // 通常重新抛出,让上层处理 } }

要点

  • 按派生顺序捕获:先捕获最具体的异常类型,最后捕获最通用的(如std::exception...)。
  • 使用引用捕获:避免对象切片,也避免不必要的拷贝。
  • 利用多态:在异常类中定义虚函数(如getDetails),可以在捕获基类引用时调用到派生类的具体实现,获取更丰富的错误信息。

3.3 运行时类型识别(RTTI)与dynamic_cast

RTTI(Runtime Type Identification)是C++的另一个高级特性,它允许程序在运行时获取对象的类型信息。主要涉及两个运算符:typeiddynamic_cast

3.3.1 dynamic_cast:安全的下行转换dynamic_cast主要用于在继承层次结构中安全地进行向下转型(从基类指针/引用到派生类指针/引用)。

class Base { public: virtual ~Base() {} }; // 至少有一个虚函数,RTTI才有效 class Derived : public Base { public: void specificFunc() {} }; void process(Base* pb) { // 不安全的下行转换(C风格或static_cast),如果pb不是指向Derived,行为未定义 // Derived* pd = (Derived*)pb; // Derived* pd = static_cast<Derived*>(pb); // 安全的动态转换 Derived* pd = dynamic_cast<Derived*>(pb); if (pd) { // 转换成功 pd->specificFunc(); // 安全调用派生类特有函数 } else { // 转换失败,pb可能指向其他派生类或就是Base std::cout << "Not a Derived object." << std::endl; } }

dynamic_cast在运行时检查转换的有效性。如果转换合法(pb确实指向一个Derived对象或其派生类对象),则返回目标类型的指针;否则返回nullptr(对于指针)或抛出std::bad_cast异常(对于引用)。

3.3.2 typeid 运算符typeid运算符返回一个std::type_info对象的引用,其中包含类型信息。

Base* pb = new Derived(); std::cout << typeid(*pb).name() << std::endl; // 输出可能是类似 `class Derived` 的字符串(取决于编译器) if (typeid(*pb) == typeid(Derived)) { // 运行时类型是Derived } delete pb;

注意:要使typeid对多态类型(有虚函数的类)返回动态类型(即实际对象的类型),操作数必须是一个解引用的指针或引用(如typeid(*pb))。如果操作数是类型本身(如typeid(pb)),它返回的是静态类型(Base*)。

3.3.3 使用建议与性能RTTI(尤其是dynamic_cast)会带来运行时开销,因为它通常需要查询对象的类型信息(与虚函数表相关)。过度使用dynamic_cast往往是设计不佳的表现,可能意味着你应该更多地使用虚函数和多态。但在某些框架、插件系统或需要处理多种未知派生类型的场景中(如对象序列化/反序列化),它又是不可或缺的工具。

4. 综合案例:设计一个简单的图形绘制框架

让我们用一个综合性的小案例,将虚函数、多态、友元、异常等特性串联起来,模拟一个简单的图形绘制框架。

4.1 框架设计与基类定义

首先,我们定义一个抽象的图形基类Shape。它使用虚函数来定义通用接口,并利用RAII管理资源(如绘图上下文)。

#include <iostream> #include <vector> #include <memory> #include <stdexcept> // 自定义异常类,用于图形绘制错误 class GraphicsException : public std::runtime_error { public: GraphicsException(const std::string& msg, int errorId) : std::runtime_error(msg), m_errorId(errorId) {} int getErrorId() const { return m_errorId; } private: int m_errorId; }; // 模拟一个简单的绘图上下文资源,用RAII管理 class DrawingContext { public: DrawingContext() { std::cout << "Acquiring drawing context...\n"; // 模拟可能发生的资源获取失败 if (/* 某种失败条件 */ false) { throw GraphicsException("Failed to acquire drawing context", 1001); } } ~DrawingContext() { std::cout << "Releasing drawing context...\n"; } // 禁止拷贝 DrawingContext(const DrawingContext&) = delete; DrawingContext& operator=(const DrawingContext&) = delete; }; // 抽象图形基类 class Shape { public: Shape() = default; virtual ~Shape() = default; // 虚析构函数,确保派生类对象能被正确销毁 // 纯虚函数,定义接口 virtual void draw() const = 0; virtual double area() const = 0; // 一个非虚的公共函数,可能内部调用虚函数(模板方法模式) void drawWithInfo() const { std::cout << "Drawing shape with area: " << area() << std::endl; draw(); } protected: // 保护成员,派生类可以访问 mutable DrawingContext m_context; // 假设每个Shape持有一个绘图上下文 };

4.2 具体图形类与友元应用

我们实现两个具体的图形类:CircleRectangle。为了演示友元,我们假设有一个独立的GeometryCalculator类,它需要直接访问Circle的私有数据来进行高性能几何计算。

// 前置声明 class GeometryCalculator; class Circle : public Shape { private: double m_radius; // GeometryCalculator 被声明为友元,可以访问私有成员 friend class GeometryCalculator; public: explicit Circle(double r) : m_radius(r) { if (r <= 0) { throw std::invalid_argument("Circle radius must be positive."); } } void draw() const override { std::cout << "Drawing a circle with radius " << m_radius << std::endl; // 使用 m_context 进行实际绘制... } double area() const override { return 3.14159 * m_radius * m_radius; } double getRadius() const { return m_radius; } // 公共接口获取半径 }; class Rectangle : public Shape { private: double m_width, m_height; public: Rectangle(double w, double h) : m_width(w), m_height(h) { if (w <= 0 || h <= 0) { throw std::invalid_argument("Rectangle dimensions must be positive."); } } void draw() const override { std::cout << "Drawing a rectangle " << m_width << "x" << m_height << std::endl; // 使用 m_context 进行实际绘制... } double area() const override { return m_width * m_height; } }; // 友元类:进行复杂的几何计算 class GeometryCalculator { public: // 可以直接访问Circle的私有成员m_radius static double calculateMomentOfInertia(const Circle& c) { return 0.5 * c.m_radius * c.m_radius * c.m_radius * c.m_radius; // 假设的公式 // 注意:这里访问了 c.m_radius,而不是 c.getRadius() } // 对于非友元类Rectangle,只能通过公共接口 static double calculateMomentOfInertia(const Rectangle& r) { double w = r.area() / r.m_height; // 无法直接访问m_width,这里只是演示,计算不准确 // 实际上,因为Rectangle未声明友元,我们无法访问其m_width/m_height。 // 这迫使GeometryCalculator为Rectangle提供不同的、或许更低效的接口。 // 这正体现了友元的使用需要权衡:它为特定类提供了高效访问,但破坏了封装。 return w * w * w * w / 12.0; // 假设的公式 } };

4.3 多态容器与异常安全操作

现在我们创建一个多态的图形集合,并演示异常安全的操作。

class DrawingBoard { public: // 使用智能指针管理多态对象,避免内存泄漏 void addShape(std::unique_ptr<Shape> shape) { m_shapes.push_back(std::move(shape)); } // 绘制所有图形:多态的经典应用 void drawAll() const { for (const auto& shape : m_shapes) { shape->draw(); // 动态绑定,调用正确的draw() } } // 计算总面积 double totalArea() const { double total = 0.0; for (const auto& shape : m_shapes) { total += shape->area(); // 动态绑定 } return total; } // 一个可能抛出异常的操作:查找特定类型的图形 template<typename T> const T* findFirstOfType() const { for (const auto& shape : m_shapes) { // 使用dynamic_cast进行安全的类型向下转换 const T* derived = dynamic_cast<const T*>(shape.get()); if (derived) { return derived; } } // 使用异常来报告“未找到”,这比返回nullptr或特殊值更清晰(对于这个API而言) throw std::runtime_error("Shape of specified type not found."); } private: std::vector<std::unique_ptr<Shape>> m_shapes; };

4.4 主函数演示与综合流程

int main() { DrawingBoard board; try { // 1. 添加图形,可能抛出异常(如无效参数) board.addShape(std::make_unique<Circle>(5.0)); board.addShape(std::make_unique<Rectangle>(4.0, 6.0)); board.addShape(std::make_unique<Circle>(2.5)); // board.addShape(std::make_unique<Circle>(-1.0)); // 这会抛出 std::invalid_argument // 2. 多态调用 std::cout << "--- Drawing all shapes ---\n"; board.drawAll(); std::cout << "\n--- Total area ---\n"; std::cout << "Total area: " << board.totalArea() << std::endl; // 3. 使用友元类进行计算 const Circle* firstCircle = board.findFirstOfType<Circle>(); if (firstCircle) { std::cout << "\n--- Using friend class ---\n"; double inertia = GeometryCalculator::calculateMomentOfInertia(*firstCircle); std::cout << "Moment of inertia of first circle: " << inertia << std::endl; } // 4. 处理“未找到”异常 // 假设我们尝试找一个不存在的类型(需要定义另一个Shape派生类,这里省略) // board.findFirstOfType<SomeNonExistentShape>(); // 会抛出 std::runtime_error // 5. 演示dynamic_cast和typeid std::cout << "\n--- RTTI Demo ---\n"; for (const auto& shape : board.getShapes()) { // 假设board有一个getShapes方法返回引用 std::cout << "Shape type: " << typeid(*shape).name() << std::endl; if (auto* circle = dynamic_cast<const Circle*>(shape.get())) { std::cout << " It's a circle with radius " << circle->getRadius() << std::endl; } else if (auto* rect = dynamic_cast<const Rectangle*>(shape.get())) { std::cout << " It's a rectangle.\n"; } } } catch (const std::invalid_argument& e) { std::cerr << "Invalid argument error: " << e.what() << std::endl; return 1; } catch (const std::runtime_error& e) { std::cerr << "Runtime error: " << e.what() << std::endl; return 1; } catch (const GraphicsException& e) { std::cerr << "Graphics error [" << e.getErrorId() << "]: " << e.what() << std::endl; return 1; } catch (const std::exception& e) { // 捕获所有其他标准异常 std::cerr << "Standard exception: " << e.what() << std::endl; return 1; } catch (...) { std::cerr << "Unknown exception occurred!" << std::endl; return 1; } std::cout << "\nProgram finished successfully." << std::endl; return 0; }

4.5 案例总结与经验要点

这个案例虽然简单,但涵盖了第15章多个核心特性的综合应用:

  1. 虚函数与多态Shape基类定义了draw()area()纯虚函数,CircleRectangle分别实现。DrawingBoard::drawAll()totalArea()是典型的多态应用。
  2. 友元GeometryCalculator被声明为Circle的友元,可以直接访问其私有半径进行计算,展示了友元在需要突破封装进行紧密协作时的用法。同时,我们也看到了它对Rectangle的不便,强调了友元应谨慎使用。
  3. 异常
    • 使用了标准异常std::invalid_argumentstd::runtime_error
    • 自定义了GraphicsException继承自std::runtime_error,并添加了错误码,展示了异常类的继承与扩展。
    • main函数中的try-catch块按从具体到一般的顺序捕获异常,进行了集中错误处理。
    • DrawingContext的构造函数可能抛出异常,其析构函数会自动调用,是RAII和异常安全基本保证的体现。
  4. RAIIDrawingContext类在构造函数中获取资源,在析构函数中释放,确保了异常安全。
  5. RTTI:在findFirstOfType模板函数和最后的RTTI演示中,使用了dynamic_cast进行安全的下行转换,并使用typeid获取类型信息。
  6. 智能指针:使用std::unique_ptr<Shape>管理多态对象生命周期,避免了手动delete和内存泄漏。

通过这个案例,你可以清晰地看到,这些“高级特性”并非孤立的语法点,而是构建一个健壮、灵活、可维护的C++程序所必需的工具。理解它们各自的行为以及相互之间的影响,是编写高质量C++代码的关键。在实际项目中,你需要根据具体情况权衡使用,例如是否真的需要友元,异常的使用范围如何界定,RTTI的开销是否可接受等,这都需要结合具体的性能要求、设计复杂度和团队规范来决定。