一文讲透 C++ / Java 中方法重载(Overload)与方法重写(Override)在调用时机等方面的区别
在学习 C++ 或 Java 的面向对象时,方法重载(Overload) 和 方法重写(Override) 都是对 同名函数 的某种二次操作,容易混淆,这篇文章就来系统梳理其区别,以及它们分别在什么时候决定调用目标。
文章目录
- [一文讲透 C++ / Java 中方法重载(Overload)与方法重写(Override)在调用时机等方面的区别](#一文讲透 C++ / Java 中方法重载(Overload)与方法重写(Override)在调用时机等方面的区别)
-
- [1 什么是方法重载(Overload)](#1 什么是方法重载(Overload))
-
- [1.1 Java 示例](#1.1 Java 示例)
- [1.2 C++ 示例](#1.2 C++ 示例)
- [1.3 Overload 函数在编译期决定调用](#1.3 Overload 函数在编译期决定调用)
- [2 什么是方法重写(Override)](#2 什么是方法重写(Override))
-
- [2.1 Java 示例](#2.1 Java 示例)
- [2.2 C++ 示例](#2.2 C++ 示例)
- [2.3 Override 在运行时决定调用](#2.3 Override 在运行时决定调用)
- [3 二者区别](#3 二者区别)
-
- [3.1 重载:先根据调用处的参数列表来选签名](#3.1 重载:先根据调用处的参数列表来选签名)
- [3.2 重写:根据对象选实现](#3.2 重写:根据对象选实现)
- [4 例子](#4 例子)
-
- [例 1](#例 1)
- [例 2](#例 2)
- [例 3](#例 3)
- [5 C++ 和 Java 在这方面的异同](#5 C++ 和 Java 在这方面的异同)
-
- [5.1 Java:普通实例方法默认支持重写分派](#5.1 Java:普通实例方法默认支持重写分派)
- [5.2 C++:必须使用 virtual 才有动态绑定](#5.2 C++:必须使用 virtual 才有动态绑定)
- [6 结语](#6 结语)
1 什么是方法重载(Overload)
方法重载(Overload) 指的是:在同一个类 中,允许存在多个方法名相同 ,但参数列表不同 的方法。
参数列表不同可以表现为参数个数/类型/顺序等任何不同。但注意,返回值不同,不能单独构成重载。
1.1 Java 示例
java
class Demo {
void print(int a) {
System.out.println("print(int)");
}
void print(double a) {
System.out.println("print(double)");
}
void print(String a) {
System.out.println("print(String)");
}
}
调用:
java
public class Main {
public static void main(String[] args) {
Demo d = new Demo();
d.print(10);
d.print(3.14);
d.print("hello");
}
}
输出:
java
print(int)
print(double)
print(String)
1.2 C++ 示例
cpp
#include <iostream>
using namespace std;
class Demo {
public:
void print(int a) {
cout << "print(int)" << endl;
}
void print(double a) {
cout << "print(double)" << endl;
}
void print(string a) {
cout << "print(string)" << endl;
}
};
int main() {
Demo d;
d.print(10);
d.print(3.14);
d.print("hello");
return 0;
}
1.3 Overload 函数在编译期决定调用
编译器在编译阶段,根据实参类型和方法签名,决定调用哪个函数。也就是说,重载属于静态绑定(Static Binding) ,也叫编译时多态。
例如:
java
d.print(10);
编译器看到参数 10 是 int,就会在编译阶段直接匹配到:
java
print(int)
运行时不会再重新选择。
2 什么是方法重写(Override)
方法重写(Override) 指的是 子类重新实现父类中已经存在的方法 ,要求方法名相同、参数列表相同、返回值相同(说白了就是只有函数体里面在干啥的内容不同),且访问权限不能更严格。它体现面向对象中的 继承 和多态。
2.1 Java 示例
java
class Parent {
void show() {
System.out.println("Parent show()");
}
}
class Child extends Parent {
@Override
void show() {
System.out.println("Child show()");
}
}
调用:
java
public class Main {
public static void main(String[] args) {
Parent p = new Child();
p.show();
}
}
输出:
java
Child show()
2.2 C++ 示例
在 C++ 中,重写通常要配合 virtual。
cpp
#include <iostream>
using namespace std;
class Parent {
public:
virtual void show() {
cout << "Parent show()" << endl;
}
};
class Child : public Parent {
public:
void show() override {
cout << "Child show()" << endl;
}
};
int main() {
Parent* p = new Child();
p->show();
return 0;
}
输出:
cpp
Child show()
2.3 Override 在运行时决定调用
重写的特点是:编译器只知道"这个调用是一个虚方法调用/可重写方法调用",真正执行父类版本还是子类版本,要到运行时根据对象的实际类型决定 。因此,重写属于 动态绑定(Dynamic Binding) ,是一种 运行时多态。
3 二者区别
在此之前现介绍一个名词:
函数签名(Function Signature) 是用来唯一标识一个函数的重要信息集合,由 函数名称 和 参数类型列表(包括顺序) 组成,用于区分不同的函数实现。(注意,函数签名不包含其返回值)
3.1 重载:先根据调用处的参数列表来选签名
看下面的代码:
java
class Demo {
void test(int x) {
System.out.println("int");
}
void test(double x) {
System.out.println("double");
}
}
调用:
java
Demo d = new Demo();
d.test(10);
编译器会在编译时,根据参数列表 10 去匹配,从而选择 test(int)。这里选的这个参数列表就是函数签名。
3.2 重写:根据对象选实现
看这段代码:
java
class Parent {
void show() {
System.out.println("Parent");
}
}
class Child extends Parent {
@Override
void show() {
System.out.println("Child");
}
}
调用:
java
Parent p = new Child();
p.show();
编译时只能确定 p 的静态类型是 Parent、调用的方法签名是 show(),但真正执行 Parent.show() 还是 Child.show(),要到运行时 看实际对象类型 new Child()。
4 例子
例 1
java
class Parent {
void show(Object o) {
System.out.println("Parent show(Object)");
}
}
class Child extends Parent {
void show(String s) {
System.out.println("Child show(String)");
}
}
调用:
java
Parent p = new Child();
p.show("hello");
很多人第一眼会以为输出是:
java
Child show(String)
但实际上输出是:
java
Parent show(Object)
为什么?
- 第一步:编译期选签名
变量 p 的 静态类型(变量被定义时的类型) 是 Parent,所以编译器只会在 Parent 中查找可调用的方法。
Parent 中只有:
java
show(Object)
因此编译器在编译时就选定了它。
- 运行期看是否被重写
运行时(这个时候变量才被 new 分配内存)对象确实是 Child,但 Child 里定义的是:
java
show(String)
这不是对 show(Object) 的重写(参数列表都不一样了),而是一个新的重载方法。所以运行时找不到 show(Object) 的重写版本,就去调用父类的:
java
Parent show(Object)
这说明 重载看引用类型和参数类型,编译期决定;重写看实际对象类型,运行期决定。
例 2
java
class Parent {
void f(int x) {
System.out.println("Parent f(int)");
}
void f(double x) {
System.out.println("Parent f(double)");
}
}
class Child extends Parent {
@Override
void f(int x) {
System.out.println("Child f(int)");
}
}
调用:
java
Parent p = new Child();
p.f(10);
p.f(3.14);
输出:
java
Child f(int)
Parent f(double)
分析:
- 对于
p.f(10)
- 编译期:根据参数
10选中f(int) - 运行期:
Child重写了f(int),所以执行Child f(int)
- 对于
p.f(3.14)
- 编译期:根据参数
3.14选中f(double) - 运行期:
Child没有重写f(double),所以执行Parent f(double)
这再次说明 先重载匹配签名,再重写分派实现。
例 3
java
class A {
void show(Object o) {
System.out.println("A Object");
}
void show(String s) {
System.out.println("A String");
}
}
class B extends A {
@Override
void show(Object o) {
System.out.println("B Object");
}
}
调用:
java
A a = new B();
a.show("hello");
输出是什么?
答案:
java
A String
- 第一步:编译期选签名
变量 a 的静态类型是 A,参数是 "hello",因此编译器优先匹配:
java
show(String)
- 运行期看重写
运行时对象是 B,但 B 只重写了:
java
show(Object)
没有重写 show(String),所以最终调用的还是:
java
A String
5 C++ 和 Java 在这方面的异同
虽然 C++ 和 Java 都有 Overload 和 Override,二者都遵循重载在编译时决定,重写在运行时决定的特点,都体现了面向对象中的多态思想,但二者在语法特点上有所不同。
5.1 Java:普通实例方法默认支持重写分派
在 Java 中,普通实例方法默认就是虚方法机制。也就是说,只要是子类重写父类实例方法,调用通常都会表现为动态绑定。
例如:
java
Parent p = new Child();
p.show();
会自动调用子类实现。
5.2 C++:必须使用 virtual 才有动态绑定
在 C++ 中,如果父类方法没有加 virtual,即使子类写了同名同参函数,也不会产生真正意义上的动态绑定。
例如:
cpp
#include <iostream>
using namespace std;
class Parent {
public:
void show() {
cout << "Parent" << endl;
}
};
class Child : public Parent {
public:
void show() {
cout << "Child" << endl;
}
};
int main() {
Parent* p = new Child();
p->show();
}
输出是:
cpp
Parent
因为没有 virtual,调用 在编译期就按指针类型 Parent* 决定了。
只有写成:
cpp
virtual void show()
才会发生运行时动态绑定。
6 结语
函数(C++)/ 方法(Java)调用通常分两步:先在编译 期确定调用哪个方法签名,再在运行期 确定调用哪个方法实现。
Overload 在编译时决定,Override 在运行时决定,因为重载解决的是 调用哪个方法签名 ,重写解决的是 调用哪个方法实现 。