news 2026/6/13 19:41:02

【c++面向对象编程】第13篇:继承(三):同名隐藏与作用域覆盖

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
【c++面向对象编程】第13篇:继承(三):同名隐藏与作用域覆盖

一、一个让人意外的现象

先看这段代码,猜猜输出什么?

cpp

#include <iostream> using namespace std; class Base { public: void func() { cout << "Base::func()" << endl; } void func(int x) { cout << "Base::func(int)" << endl; } }; class Derived : public Base { public: void func() { cout << "Derived::func()" << endl; } }; int main() { Derived d; d.func(); // 输出什么? d.func(10); // 能编译吗? return 0; }

答案:

  • d.func()输出Derived::func()— 这很合理,派生类覆盖了基类的版本

  • d.func(10)编译错误!编译器说没有匹配的函数

为什么会这样?基类明明有一个func(int),为什么说找不到?

这就是“同名隐藏”规则:一旦派生类定义了同名函数(不管参数是否相同),基类的所有同名函数都会被隐藏。编译器只看到派生类的func(),不会去基类里找其他重载版本。


二、同名隐藏的规则

核心规则

当派生类中声明了与基类同名的成员(变量或函数),基类的同名成员会被“隐藏”——即使在派生类中不可见。

  • 对于函数:隐藏的是名字,不是单个函数。基类中所有同名函数(无论参数)都会被隐藏。

  • 对于变量:派生类的同名变量会隐藏基类的同名变量。

函数隐藏示例

cpp

class Base { public: void print() { cout << "Base::print()" << endl; } void print(int x) { cout << "Base::print(int)" << endl; } void show() { cout << "Base::show()" << endl; } }; class Derived : public Base { public: void print() { cout << "Derived::print()" << endl; } // 同名函数 // 注意:没有重写show() }; int main() { Derived d; d.print(); // ✅ Derived::print() // d.print(10); // ❌ 错误!Base::print(int) 被隐藏了 d.show(); // ✅ 可以,show没有被隐藏(没有同名) }

变量隐藏示例

cpp

class Base { public: int value = 10; }; class Derived : public Base { public: int value = 20; // 同名变量,隐藏基类的value }; int main() { Derived d; cout << d.value << endl; // 输出20(派生类的) // cout << d.Base::value; // 输出10(用作用域运算符访问被隐藏的) }

三、隐藏 vs 重载 vs 重写

这是新手最容易混淆的三个概念,必须区分清楚:

概念发生条件作用域效果
重载同一作用域,函数名相同参数不同同一个类内部增加多个版本,编译器根据参数选择
重写派生类重写基类的虚函数继承体系,函数签名完全相同实现多态,运行时动态绑定
隐藏派生类定义了同名成员(不要求签名相同)继承体系基类同名成员被屏蔽

关键区别图

cpp

class Base { public: void f(int); // Base::f(int) virtual void g(); // Base::g() }; class Derived : public Base { public: void f(double); // 隐藏!不是重载(不同作用域),不是重写(参数不同) void g() override; // 重写!虚函数,签名相同 };

一句话记住

  • 重载:同一个类,同一个名字,不同参数

  • 重写:不同类(继承),虚函数,相同签名

  • 隐藏:不同类,同名即隐藏(不看参数)


四、如何访问被隐藏的基类成员?

方法1:作用域运算符::

cpp

class Base { public: void func() { cout << "Base::func()" << endl; } void func(int x) { cout << "Base::func(int) " << x << endl; } }; class Derived : public Base { public: void func() { cout << "Derived::func()" << endl; } void callBaseFunc() { Base::func(); // 调用基类无参版本 Base::func(100); // 调用基类带参版本 } }; int main() { Derived d; d.func(); // Derived版本 d.Base::func(); // 基类无参版本 d.Base::func(42); // 基类带参版本 }

方法2:使用using声明(推荐,将基类版本引入派生类作用域)

cpp

class Derived : public Base { public: using Base::func; // 把基类的所有func重载引入派生类作用域 void func() { cout << "Derived::func()" << endl; } }; int main() { Derived d; d.func(); // Derived版本(派生类自己的优先级更高) d.func(10); // ✅ 现在可以了!调用Base::func(int) }

using声明的效果:基类的同名函数变成了派生类的重载集的一部分。编译器会同时考虑派生类和基类的版本进行重载解析。


五、为什么会有隐藏规则?

这看起来像一个“缺陷”,但背后有设计考量:

1. 避免意外的继承

假设基类后来添加了一个新的重载版本:

cpp

// 原有代码 class Base { public: void process(int x) { ... } }; class Derived : public Base { public: void process(double d) { ... } // 原本只处理double };

如果C++没有隐藏规则,基类新增的process(string)会突然出现在派生类中,可能导致意想不到的重载决议。隐藏规则让派生类“主动选择”哪些基类成员可见。

2. 维护封装性

派生类可以完全“替换”基类的某个名字,而不受基类未来添加新重载的影响。


六、完整例子:图形系统中的隐藏问题

cpp

#include <iostream> #include <string> using namespace std; // 基类:形状 class Shape { public: void draw() { cout << "绘制通用形状" << endl; } void draw(string color) { cout << "用" << color << "颜色绘制形状" << endl; } virtual double getArea() { return 0; } }; // 派生类:圆形 class Circle : public Shape { private: double radius; public: Circle(double r) : radius(r) {} // 重写虚函数 double getArea() override { return 3.14159 * radius * radius; } // 隐藏!定义了同名函数 void draw() { cout << "绘制圆形,半径=" << radius << endl; } // 如何让基类的draw(string)也能用? // 方法1:手动转发 void drawWithColor(string color) { Shape::draw(color); } // 方法2:using声明(推荐) using Shape::draw; // 把Shape的所有draw引入作用域 }; int main() { Circle c(5.0); cout << "=== 演示隐藏 ===" << endl; c.draw(); // Circle::draw() // c.draw("红色"); // 如果不加using,这行会编译错误 cout << "\n=== 加上using后 ===" << endl; c.draw("红色"); // ✅ 现在可以了,调用Shape::draw(string) cout << "\n=== 通过作用域运算符访问 ===" << endl; c.Shape::draw(); // 基类无参版本 c.Shape::draw("蓝色"); // 基类带参版本 cout << "\n=== 面积计算 ===" << endl; cout << "圆形面积: " << c.getArea() << endl; return 0; }

输出:

text

=== 演示隐藏 === 绘制圆形,半径=5 === 加上using后 === 用红色颜色绘制形状 === 通过作用域运算符访问 === 绘制通用形状 用蓝色颜色绘制形状 === 面积计算 === 圆形面积: 78.5397

七、隐藏的“例外”:虚函数重写

如果派生类的函数与基类的虚函数签名完全相同,这不是隐藏,而是重写(override),是实现多态的基础。

cpp

class Base { public: virtual void draw() { cout << "Base::draw" << endl; } virtual void draw(int x) { cout << "Base::draw(int)" << endl; } }; class Derived : public Base { public: void draw() override { cout << "Derived::draw" << endl; } // 重写Base::draw() // Base::draw(int) 没有被重写,但会被隐藏! }; int main() { Derived d; d.draw(); // Derived::draw(重写的版本) // d.draw(10); // ❌ 隐藏了!即使基类版本是虚函数 }

注意:虚函数重写只针对完全相同签名的函数。同名但参数不同的版本仍然会被隐藏。


八、最佳实践建议

1. 避免不必要的同名成员

除非你有明确意图(如重写虚函数),否则不要给派生类的成员取和基类相同的名字。

2. 需要基类重载时使用using

cpp

class Derived : public Base { public: using Base::func; // 把基类的所有func带过来 void func() { ... } // 添加自己的版本 };

3. 重写虚函数时永远使用override关键字

cpp

void draw() override { ... } // 明确表达意图,编译检查签名是否正确

4. 访问被隐藏成员时显式使用作用域运算符

cpp

d.Base::func(); // 代码阅读者一眼就知道你在调用基类版本

九、三个常见错误

1. 以为派生类会重载基类函数

cpp

class Base { public: void process(int x) { cout << "int" << endl; } }; class Derived : public Base { public: void process(double d) { cout << "double" << endl; } }; Derived d; d.process(10); // 输出 "double"!不是 "int"

因为process(double)隐藏了基类的process(int)10被隐式转换为double调用派生类版本。

2. 忘记using导致编译错误

cpp

class Derived : public Base { void func() { ... } // 隐藏了Base::func(int) }; // 外部想调用d.func(10) → 编译错误

3. 搞混隐藏和重写

cpp

class Base { public: virtual void show() { cout << "Base" << endl; } }; class Derived : public Base { public: void show(int x) { cout << "Derived" << endl; } // 这是隐藏,不是重写! // 因为参数不同,show(int)隐藏了Base::show() };

十、这一篇的收获

你现在应该理解:

  • 同名隐藏:派生类定义了与基类同名的成员,基类的同名成员(所有重载版本)被隐藏

  • 隐藏发生在名字级别,不看参数、不看是否为虚函数

  • 重载是同一作用域,重写是虚函数相同签名,隐藏是派生类屏蔽基类

  • Base::memberusing Base::member可以访问被隐藏的基类成员

💡 小作业:设计Animal基类,有speak()speak(string language)两个版本。派生类Dog重写speak()(打印"汪汪"),但不重写带参数版本。测试Dog对象能否调用speak("english")。如果不能,用两种方法修复(作用域运算符和using声明)。


下一篇预告:第14篇《多态(一):虚函数——实现“一个接口,多种方法”》——终于到了面向对象最核心的概念:多态。基类指针指向派生类对象,调用同一个函数,执行不同的行为。这是怎么做到的?虚函数是答案。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/5/16 18:27:06

告别乱码和不同步!手把手教你用Kotlin在Android上完美解析和显示SRT字幕

告别乱码和不同步&#xff01;手把手教你用Kotlin在Android上完美解析和显示SRT字幕 在视频播放应用中&#xff0c;字幕的准确解析和流畅显示是提升用户体验的关键环节。然而&#xff0c;许多开发者在处理SRT字幕时常常遇到乱码、时间轴错位、性能卡顿等问题。本文将带你用Kotl…

作者头像 李华
网站建设 2026/5/14 22:32:46

2026 AI企业推荐排行 技术创新榜 场景落地/全球布局 专业评测

一、摘要据赛迪顾问发布的《2026年全球AI技术创新与落地报告》显示&#xff0c;全球AI技术创新迭代速度持续加快&#xff0c;75%的企业将技术创新能力作为选型核心指标&#xff0c;62%的用户关注场景落地深度与全球化服务能力&#xff0c;46%的政企用户反映AI企业缺乏全流程技术…

作者头像 李华