展开菜单
首页 精品内容 本月促销 装机必备 Windows PDF教程 macOS软件 IOS软件 Android AI 专题
全部分类

当前位置:

首页 > 编程开发 > C++继承中函数重写与覆盖详解

C++继承中函数重写与覆盖详解

函数重写实现多态,要求基类函数为虚函数且签名一致;函数覆盖则因同名函数导致基类所有同名函数被隐藏,与虚函数无关,遵循作用域查找规则。

本文目录

    函数重写实现多态,要求基类函数为虚函数且签名一致;函数覆盖则因同名函数导致基类所有同名函数被隐藏,与虚函数无关,遵循作用域查找规则。

    C++继承中函数重写与覆盖方法

    C++继承体系中,函数重写(Overriding)和函数覆盖(Hiding,有时也叫遮蔽或隐藏)是两个核心概念,它们都涉及派生类中与基类同名函数的处理,但背后的机制和意图却截然不同。简单来说,重写是多态的基石,允许我们通过基类指针或引用调用派生类的特定实现;而覆盖则是作用域和名称查找规则的体现,派生类中同名函数会“遮蔽”基类的所有同名函数,无论签名是否一致。理解这两者的差异,对于写出健壮、可维护的C++代码至关重要,否则很容易踩到意外行为的坑。

    解决方案

    在C++的继承体系中,当我们谈及基类和派生类拥有同名函数时,通常会遇到两种情况:函数重写(Overriding)和函数覆盖(Hiding,或称遮蔽)。

    函数重写(Overriding)

    函数重写是C++实现运行时多态(Runtime Polymorphism)的关键机制。它允许派生类为基类中已声明为虚(virtual)的函数提供一个特定的实现。

    特点:

    • 基类函数必须是虚函数: 这是重写的前提。
    • 函数签名必须完全一致: 包括函数名、参数列表(类型、顺序、const性)和返回类型(C++11后允许协变返回类型,即派生类返回类型是基类返回类型的派生类指针或引用)。
    • 通常通过基类指针或引用调用: 在运行时,根据实际指向或引用的对象类型,动态地调用派生类中的重写版本。
    • 访问权限: 派生类重写函数的访问权限可以与基类不同,但通常建议保持一致或更宽松。

    示例:

    #include 
    #include  // For std::unique_ptr
    
    class Base {
    public:
        virtual void greet() const {
            std::cout << "Hello from Base!" << std::endl;
        }
    
        virtual void calculate(int a, int b) {
            std::cout << "Base calculation: " << a + b << std::endl;
        }
    
        // C++11 onwards: 析构函数通常也应该是虚函数,以确保正确释放资源
        virtual ~Base() = default;
    };
    
    class Derived : public Base {
    public:
        // 使用 override 关键字明确表明这是对基类虚函数的重写
        void greet() const override {
            std::cout << "Greetings from Derived!" << std::endl;
        }
    
        // 重写基类的 calculate 函数
        void calculate(int a, int b) override {
            std::cout << "Derived calculation: " << a * b << std::endl;
        }
    };
    
    void demonstrateOverride() {
        std::unique_ptr b1 = std::make_unique();
        std::unique_ptr b2 = std::make_unique();
    
        b1->greet();        // 输出: Hello from Base!
        b2->greet();        // 输出: Greetings from Derived! (多态行为)
    
        b1->calculate(10, 5); // 输出: Base calculation: 15
        b2->calculate(10, 5); // 输出: Derived calculation: 50 (多态行为)
    }

    函数覆盖(Hiding / Shadowing)

    函数覆盖发生在派生类中定义了一个与基类同名的函数时。无论基类函数是否为虚函数,也无论它们的签名是否一致,派生类中的这个函数都会“隐藏”基类中所有同名函数。这意味着,当通过派生类对象或派生类指针/引用调用该函数时,编译器会优先查找派生类中的版本。

    特点:

    • 与虚函数无关: 即使基类函数是虚函数,如果派生类函数签名不一致,也会发生覆盖而非重写。
    • 名称查找规则: 编译器在派生类作用域中找到同名函数后,就会停止在基类中查找。
    • 影响所有同名函数: 派生类中的一个同名函数会隐藏基类中所有同名函数,包括不同参数列表的重载版本。
    • 非多态行为: 通过基类指针或引用调用时,如果基类函数不是虚函数,或者派生类函数签名不一致导致未能重写,那么将调用基类版本。

    示例:

    #include 
    
    class Base {
    public:
        void func() {
            std::cout << "Base::func()" << std::endl;
        }
    
        void func(int x) {
            std::cout << "Base::func(int): " << x << std::endl;
        }
    
        virtual void virtualFunc() { // 虚函数
            std::cout << "Base::virtualFunc()" << std::endl;
        }
    };
    
    class Derived : public Base {
    public:
        // 覆盖了 Base::func() 和 Base::func(int)
        void func(double d) {
            std::cout << "Derived::func(double): " << d << std::endl;
        }
    
        // 尝试重写虚函数,但签名不一致,导致覆盖而非重写
        void virtualFunc(int x) {
            std::cout << "Derived::virtualFunc(int): " << x << std::endl;
        }
    };
    
    void demonstrateHiding() {
        Derived d;
        d.func(3.14); // 输出: Derived::func(double): 3.14
    
        // d.func();     // 编译错误:Derived 中没有 func() 的无参版本
        // d.func(10);   // 编译错误:Derived 中没有 func(int) 的版本
    
        // 如果想调用基类的被隐藏函数,需要显式指定作用域
        d.Base::func();     // 输出: Base::func()
        d.Base::func(10);   // 输出: Base::func(int): 10
    
        Base* bPtr = &d;
        bPtr->func();          // 输出: Base::func() (通过基类指针调用基类非虚函数)
        bPtr->virtualFunc();   // 输出: Base::virtualFunc() (尽管 d 是 Derived 类型,但 Derived::virtualFunc(int)
                               // 未重写 Base::virtualFunc(),所以调用基类虚函数)
    }

    为什么C++要区分函数重写和覆盖?它们各自的实际应用场景是什么?

    C++之所以要区分重写和覆盖,是出于对语言灵活性和强类型特性的权衡考量。在我看来,这反映了C++在提供高级抽象(如多态)的同时,也保留了底层控制(如名称查找和作用域管理)的设计哲学。

    函数重写(Overriding)的实际应用场景:

    重写的核心价值在于实现多态性,即“一个接口,多种实现”。这是面向对象编程中最强大也最常用的特性之一。

    • 框架与库设计: 开发者可以定义一个抽象的基类或接口,其中包含一些虚函数,代表了某些标准行为。用户通过继承这个基类并重写虚函数,来定制自己的特定行为,而框架本身无需知道具体实现细节。比如,一个图形库中的 Shape 基类有一个 virtual void draw() 函数,Circle、Rectangle 等派生类各自实现不同的 draw() 逻辑。
    • 插件系统: 插件通常需要实现宿主程序定义的特定接口(虚函数),以便宿主程序在运行时加载并调用这些插件的功能。
    • 事件处理: GUI框架中,事件处理器基类可能有一个 virtual void handleEvent() 方法,不同的控件或用户自定义事件处理器重写此方法来响应特定事件。
    • 策略模式和模板方法模式: 这些设计模式大量依赖虚函数重写来封装可变行为或定义算法骨架。
    • 资源管理: 虚析构函数是重写的一个特殊但极其重要的应用。它确保在通过基类指针删除派生类对象时,能够正确调用派生类的析构函数,防止内存泄漏或资源未释放。

    函数覆盖(Hiding / Shadowing)的实际应用场景:

    覆盖机制更多地是C++名称查找规则的自然结果,它在大多数情况下是需要注意避免的“陷阱”,但也并非完全没有其“有意为之”的场景。

    • 避免意外的多态: 有时,派生类可能希望引入一个与基类同名但功能完全不同的函数,并且不希望它参与多态。例如,基类有一个 void print() 打印内部状态,派生类有一个 void print(std::ostream& os) 打印到指定流。如果基类的 print() 不是虚函数,或者派生类的签名不同,就会发生覆盖。这允许派生类在自己的作用域内定义一个全新的同名函数,而不影响基类的行为。
    • “禁用”或修改基类行为: 派生类可以通过覆盖来“隐藏”基类中的某些重载版本,只暴露自己认为合适的接口。例如,基类 Base 有 func() 和 func(int) 两个重载,派生类 Derived 只想提供一个 func(double),并且不希望使用者调用基类的 func() 或 func(int)。这时,Derived 中声明 func(double) 就会隐藏基类的两个 func。
    • 作为一种警告: 编译器在某些情况下对覆盖不会给出警告(尤其是在没有 override 关键字的旧代码中),这使得它成为一个常见的错误来源。但从另一个角度看,它强制开发者在处理同名函数时,必须明确理解名称查找和作用域规则。

    总的来说,重写是为多态服务的,是C++面向对象编程的基石;而覆盖更多是作用域管理和名称查找的产物,它要求开发者更加小心谨慎,避免不必要的混淆。

    如何避免函数覆盖带来的潜在问题,并有效利用override关键字?

    函数覆盖(Hiding)确实是C++继承中一个常见的陷阱,尤其是在大型项目或复杂的继承体系中。但只要我们理解其机制并遵循一些最佳实践,就能有效避免问题,并利用C++11引入的override关键字来增强代码的健壮性。

    避免函数覆盖带来的潜在问题:

    1. 深入理解名称查找规则: 这是解决问题的根本。当编译器在派生类对象上查找一个函数时,它会首先在派生类作用域中查找。一旦找到同名函数,就会停止查找,即使基类中存在签名更匹配的同名函数。这个规则导致派生类中定义一个同名函数会隐藏基类中所有同名函数,无论它们的签名如何。

    2. 使用 using 声明引入基类重载集: 如果你希望派生类能够重写基类的某个虚函数,同时又想保留基类的其他同名重载版本,那么在派生类中,你需要使用 using Base::func_name; 来将基类的所有 func_name 重载函数引入到派生类的作用域中。这样,派生类就能在自己的作用域内提供新的重载或重写虚函数,而不会隐藏基类的其他同载。

      class Base {
      public:
          virtual void print() { std::cout << "Base::print()" << std::endl; }
          void print(int x) { std::cout << "Base::print(int): " << x << std::endl; }
      };
      
      class Derived : public Base {
      public:
          using Base::print; // 将 Base 的 print() 重载集引入 Derived 作用域
      
          void print() override { // 重写 Base 的虚函数
              std::cout << "Derived::print()" << std::endl;
          }
          // 如果没有 using Base::print; 这一行,
          // 那么 Derived::print() 会隐藏 Base::print(int),
          // 导致 Derived 对象无法直接调用 print(int)。
      };
      
      void testUsing() {
          Derived d;
          d.print();      // 调用 Derived::print()
          d.print(10);    // 调用 Base::print(int) (因为 using 声明)
      }
    3. 仔细检查函数签名: 当你打算重写一个虚函数时,务必确保派生类中函数的签名(包括参数类型、参数顺序、const修饰符、引用修饰符等)与基类虚函数完全一致。即使是微小的差异(例如,基类是 const 成员函数,派生类不是),也会导致重写失败,转而变成覆盖。这通常是很难发现的bug源。

    4. 明确设计意图: 如果你确实有意让派生类中的函数隐藏基类中的同名函数,请确保这是经过深思熟虑的设计,并且在代码中添加清晰的注释说明。这种情况下,通常意味着派生类提供的功能与基类同名函数的功能完全不同,且不希望参与多态。

    有效利用 override 关键字:

    C++11引入的override上下文关键字是防止函数覆盖错误,并确保正确实现函数重写的强大工具。

    1. 显式意图,提高可读性: 当你在派生类中为一个虚函数加上 override 关键字时,你明确地告诉编译器和阅读代码的人,这个函数是旨在重写基类的一个虚函数。这大大提高了代码的可读性和维护性。

    2. 编译时错误检查: 这是 override 最重要的价值。如果一个被标记为 override 的函数实际上并没有重写任何基类的虚函数(例如,因为函数名拼写错误、参数列表不匹配、返回类型不一致、基类函数不是虚函数等),编译器会立即报错。这能有效捕获那些因签名不匹配导致的意外覆盖问题,将运行时错误提前到编译时。

      class Base {
      public:
          virtual void process(int data) const { /* ... */ }
          virtual void doSomething() { /* ... */ }
      };
      
      class Derived : public Base {
      public:
          // 正确重写
          void process(int data) const override { /* ... */ }
      
          // 编译错误:签名不匹配,无法重写 Base::process
          // void process(double data) const override { /* ... */ }
      
          // 编译错误:缺少 const,无法重写 Base::process
          // void process(int data) override { /* ... */ }
      
          // 编译错误:基类没有 doSomething(int) 虚函数可供重写
          // void doSomething(int x) override { /* ... */ }
      };
    3. 防止基类接口变更引发的问题: 如果基类中的一个虚函数签名被修改,那么所有重写了该函数的派生类,如果使用了 override 关键字,都会在编译时立即报错,提醒开发者更新派生类的实现。这有助于维护大型继承体系的一致性。

    总结来说,养成习惯在所有重写的虚函数后都加上 override 关键字,并理解 using 声明在处理重载集时的作用,是避免函数覆盖陷阱,并充分利用C++多态特性的关键。

    C++11中的final关键字在继承与函数重写中扮演了什么角色?

    C++11引入的final关键字是与override相辅相成的一个特性,它在继承和函数重写中扮演着“终结者”的角色,用于限制进一步的继承或重写。在我看来,final就像是给类或虚函数打上了一个“到此为止”的标记,为开发者提供了更精细的控制能力。

    final关键字的两种主要用途:

    1. 阻止类被继承: 当一个类被声明为 final 时,它就不能再被任何其他类继承。

      class Base final {
          // ...
      };
      
      // class Derived : public Base { // 编译错误:'Base' cannot be a base class
      //
    本站声明:本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系bd@zhengruan.com
    作者最新文章
    编程开发 函数重写
    相关文章 更多
    精品专题 更多
    装机必备

    正软商城装机必备专区,精选办公、浏览器、安全防护、影音播放、压缩解压、设计创作和系统工具等电脑常用正版软件,帮助用户快速完成新电脑软件配置。

    Windows

    正软商城Windows软件专区,汇集适用于Windows电脑的办公、设计、安全防护、影音播放、开发工具和系统优化软件,提供软件介绍、系统要求、正版授权及购买下载服务。

    PDF教程

    正软商城PDF教程频道提供PDF编辑、转换、合并、拆分、压缩及格式处理方法,同时介绍常用PDF软件和工具的使用技巧。

    macOS软件

    正软商城macOS软件专区,精选适用于Mac电脑的办公、设计、影音、效率、开发和系统工具,提供软件功能介绍、macOS兼容版本、正版授权及购买下载服务。

    IOS软件

    正软商城iOS软件专区,精选适用于iPhone和iPad的办公、学习、影音、设计、效率及AI应用,提供功能介绍、适用设备、系统要求和正版获取方式等信息。

    AI

    正软商城AI软件专区,汇集AI写作、AI绘画、AI视频、AI办公、AI编程、AI翻译、智能客服和数据分析等人工智能工具,提供功能介绍、适用平台、收费方式及正版购买信息。

    Mac软件 更多
    Aperty macOS版
    Aperty macOS版

    Aperty 是 Skylum 推出的 AI 人像修图软件,支持自然肤质修饰、瑕疵处理、面部与身体调整、预设及批量同步。

    火山引擎 网页版
    火山引擎 网页版

    火山引擎是字节跳动旗下企业级云与AI服务平台。聚焦大模型核心能力,依托字节自研豆包多模态大模型,凭借火山方舟一站式平台,提供模型精调、推理部署、智能应用搭建等全链路服务,结合高性能AI算力,助力各行业快速落地大模型数字化与智能化应用。

    立刻MV 网页版
    立刻MV 网页版

    立刻MV 面向音乐人、内容创作者和品牌团队,帮助用户低成本完成歌曲 MV、歌词视频、AI 分镜、图生视频与最终合成。

    蛙蛙写作 网页版
    蛙蛙写作 网页版

    蛙蛙写作 AI 创作工具,覆盖小说、剧本、文案,一键生成文稿,打通文字转视频全流程,高效解决创作难题。

    讯飞智作 网页版
    讯飞智作 网页版

    讯飞智作是一款基于语音合成技术、虚拟人驱动技术与数字人形象声音构建能力的 AI 数字人音视频创作平台。

    超级简历WonderCV 网页版
    超级简历WonderCV 网页版

    超级简历WonderCV是面向应届生和AI小白的智能求职平台,覆盖简历、面试与职业规划。

    音述AI 网页版
    音述AI 网页版

    音述AI,是一个以“用声音述说故事”为核心的 AI 音乐创作与声音分享社区。

    AionClaw macOS版
    AionClaw macOS版

    AionClaw 是可在桌面端运行的任务型 AI 助手,通过数字专家与技能市场组合办公、创作、数据和开发能力。

    Loomy macOS版
    Loomy macOS版

    Loomy 是科大讯飞推出的桌面 AI 助手,可通过对话、任务与工作目录协助处理办公、内容创作和资料整理。

    AirBuddy 3 macOS版
    AirBuddy 3 macOS版

    AirBuddy 3 是面向 macOS 的蓝牙外设增强工具,可集中查看 AirPods、Beats 与苹果设备电量,提供快速连接、播放控制、电量提醒、桌面小组件和多台 Mac 外设接力等功能。

    AirServer macOS版
    AirServer macOS版

    AirServer macOS版可把 Mac 转换为无线投屏接收端,通过 AirPlay 或 Google Cast 接收手机、平板和电脑画面,并支持多设备同时连接、高清镜像及会话录制,适合家庭娱乐、课堂教学、会议演示和应用展示。

    AnyDesk macOS版
    AnyDesk macOS版

    AnyDesk macOS版是一款适用于远程办公、设备维护和客户支持的远程桌面工具,可连接并操作异地计算机,支持文件传输、无人值守访问、会话权限控制及跨平台协作。

    WINDOWS 更多
    讯飞绘文 网页版
    讯飞绘文 网页版

    讯飞绘文网页版是科大讯飞推出的AI图文创作与内容运营工具,覆盖热点选题、文章生成、稿件校对、智能配图、排版发布和数据复盘等环节,适合自媒体创作者及内容团队在线使用。

    Maya(Autodesk Maya) Windows版
    Maya(Autodesk Maya) Windows版

    Autodesk Maya 是面向影视、电视、动画和游戏行业的专业三维制作软件,集多边形与 NURBS 建模、角色绑定、关键帧和动作捕捉动画、Bifrost 特效模拟、材质开发及 Arnold 渲染于一体,

    Aperty Windows版
    Aperty Windows版

    Aperty 是 Skylum 推出的 AI 人像修图软件,支持自然肤质修饰、瑕疵处理、面部与身体调整、预设及批量同步。

    火山引擎 网页版
    火山引擎 网页版

    火山引擎是字节跳动旗下企业级云与AI服务平台。聚焦大模型核心能力,依托字节自研豆包多模态大模型,凭借火山方舟一站式平台,提供模型精调、推理部署、智能应用搭建等全链路服务,结合高性能AI算力,助力各行业快速落地大模型数字化与智能化应用。

    立刻MV 网页版
    立刻MV 网页版

    立刻MV 面向音乐人、内容创作者和品牌团队,帮助用户低成本完成歌曲 MV、歌词视频、AI 分镜、图生视频与最终合成。

    蛙蛙写作 网页版
    蛙蛙写作 网页版

    蛙蛙写作 AI 创作工具,覆盖小说、剧本、文案,一键生成文稿,打通文字转视频全流程,高效解决创作难题。

    讯飞智作 网页版
    讯飞智作 网页版

    讯飞智作是一款基于语音合成技术、虚拟人驱动技术与数字人形象声音构建能力的 AI 数字人音视频创作平台。

    超级简历WonderCV 网页版
    超级简历WonderCV 网页版

    超级简历WonderCV是面向应届生和AI小白的智能求职平台,覆盖简历、面试与职业规划。

    音述AI 网页版
    音述AI 网页版

    音述AI,是一个以“用声音述说故事”为核心的 AI 音乐创作与声音分享社区。

    AionClaw Windows版
    AionClaw Windows版

    AionClaw 是可在桌面端运行的任务型 AI 助手,通过数字专家与技能市场组合办公、创作、数据和开发能力。

    Loomy Windows版
    Loomy Windows版

    Loomy 是科大讯飞推出的桌面 AI 助手,可通过对话、任务与工作目录协助处理办公、内容创作和资料整理。

    AirServer Windows版
    AirServer Windows版

    AirServer Windows版可把电脑变成AirPlay、Google Cast和Miracast无线投屏接收端,适合会议演示、课堂教学及家庭大屏分享。发送设备通常无需另装客户端,并可同时接收和智能排列多个镜像会话。