【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篇《多态(一):虚函数------实现"一个接口,多种方法"》------终于到了面向对象最核心的概念:多态。基类指针指向派生类对象,调用同一个函数,执行不同的行为。这是怎么做到的?虚函数是答案。

相关推荐
ch.ju1 小时前
Java Programming Chapter 3——Dynamic acquisition of array
java·开发语言
TechWayfarer1 小时前
AI的幻觉谁来买单?智能体时代的数据溯源与鉴权
开发语言·python·安全·ai
Str_Null1 小时前
Python 自动线性化 HTML/MD 表格的工程实践(一个读取表格并且提供输出的工具)
开发语言·python·html
Shadow(⊙o⊙)1 小时前
qt内详解信号和槽的基本概念+实例演示
开发语言·前端·c++·qt·学习
艾iYYY2 小时前
类和对象(详解初始化列表, static成员变量, 友元,内部类)
c语言·数据结构·c++·算法
asdzx672 小时前
使用 C# 添加或读取 Excel 公式:完整指南
开发语言·c#·excel
磊 子2 小时前
多继承和多态性
开发语言·c++
加号32 小时前
【C#】 中 BCD 字节数组转十进制字符串的原理与实现思路
开发语言·c#
AbandonForce2 小时前
C++11:列表初始化||右值和移动语义||引用折叠和完美转发||可变参数模板||lambda表达式||包装器(function bind)
开发语言·数据结构·c++·算法