一、一个让人意外的现象
先看这段代码,猜猜输出什么?
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::member或using Base::member可以访问被隐藏的基类成员
💡 小作业:设计
Animal基类,有speak()和speak(string language)两个版本。派生类Dog重写speak()(打印"汪汪"),但不重写带参数版本。测试Dog对象能否调用speak("english")。如果不能,用两种方法修复(作用域运算符和using声明)。
下一篇预告:第14篇《多态(一):虚函数------实现"一个接口,多种方法"》------终于到了面向对象最核心的概念:多态。基类指针指向派生类对象,调用同一个函数,执行不同的行为。这是怎么做到的?虚函数是答案。