C++ 重写、重载、重定义的区别
派生类的函数屏蔽了与其同名的基类函数,规则是:
a、若派生类的函数和基类的函数同名,但参数不同,此时不管有无virtual,基类的函数被隐藏
b、若派生类的函数和基类的函数同名,且参数相同,但基类没有virtual关键字,基类的函数被隐藏
重载overload:是函数名相同,参数列表不同 重载只是在类的内部存在。但是不能靠返回类型来判断。
重写override:也叫做覆盖。子类重新定义父类中有相同名称和参数的虚函数。函数特征相同。但是具体实现不同,主要是在继承关系中出现的 。
重写需要注意:
1 被重写的函数不能是static的。必须是virtual的
2 重写函数必须有相同的类型,名称和参数列表
3 重写函数的访问修饰符可以不同。尽管virtual是private的,派生类中重写改写为public,protected也是可以的
重定义 (redefining)也叫做隐藏:
子类重新定义父类中有相同名称的非虚函数 ( 参数列表可以不同 ) 。
同名函数重定义是一种静态绑定
#include<iostream>
using namespace std;
class A {
public :
void let(){
cout<<"I'm A"<<endl;
}
int num;
};
class B:public A{
public :
void let(){
cout<<"I'm b"<<endl;
}
int num;
};
int main()
{
//以上let函数为同名函数重定义
A aa;
B bb;
aa.let();
bb.let();//直接调用成员函数,子类let函数覆盖父类let函数
A *a = new A();
A *b = new B(); //以up_cast形式初始化对象指针,及体现多态的场景,体现了非virtual函数的静态绑定
B *ba = new B();
b->A::let();
b->let();
ba->A::let();
ba->let();
}
虚函数 是在基类中使用关键字 virtual 声明的函数。
当没有虚函数时,调用函数let 被编译器设置为基类中的版本,这就是所谓的静态多态,或静态链接 - 函数调用在程序执行前就准备好了。有时候这也被称为早绑定,因为 let 函数在程序编译期间就已经设置好了。
输出:
I'm A
I'm b
I'm A
I'm A
I'm A
I'm b
虚函数重写的动态绑定
#include<iostream>
using namespace std;
class A {
public :
void virtual let(){
cout<<"I'm A"<<endl;
}
int num;
};
class B:public A{
public :
void let(){
cout<<"I'm b"<<endl;
}
int num;
};
int main()
{
//以上let函数为同名函数重定义
A aa;
B bb;
aa.let();
bb.let();//直接调用对象成员函数,子类let函数覆盖父类let函数
A *a = new A();
A *b = new B(); //以up_cast形式初始化对象指针,及体现多态的场景,体现了virtual函数的动态绑定
B *ba = new B();
b->A::let();
b->let();
ba->A::let();
ba->let();
}
在派生类中重新定义基类中定义的虚函数时,会告诉编译器不要静态链接到该函数。我们想要的是在程序中任意点可以根据所调用的对象类型来选择调用的函数,这种操作被称为动态链接,或后期绑定。
输出:
I'm A
I'm b
I'm A
I'm b
I'm A
I'm b
从汇编层面学习如何实现动态绑定(虚指针和虚表)-----参考大神笔记此处做个记录
平台工具:windows、X86(X64)、vs
参考:C++中虚指针和虚表
工具:C++类layout工具
当基类有虚函数时:
每个类都有虚指针和虚表
如果不是虚继承,那么子类将父类的虚指针继承下来,并指向自身的虚表(发生在对象构造时)。有多少个虚函数(子类将父类虚函数重写的话则只计算一个),虚表里面的项就会有多少。多重继承时,可能存在多个的基类虚表与虚指针
如果是虚继承,那么子类会有两份虚指针,一份指向自己的虚表,另一份指向虚基表,多重继承时虚基表与虚基表指针有且只有一份。
VS查看内存布局 /d1 reportSingleClassLayoutXXX(XXX为类名),会打出指定类XXX的内存布局。
继承
/d1 reportSingleClassLayoutCat
#include<iostream>
using namespace std;
class Animal {
int a;
int b;
public:
void speak() {
cout << "Animal speak()" << endl;
}
virtual void run() {}
};
class Cat : public Animal {
int c;
public:
void speak() { // 与父类同名函数
cout << "Cat speak()" << endl;
}
virtual void run() {} // 与父类同名virtual函数
virtual void catRun() {} // 子类本身的函数
};
int main() {
Animal a;
Cat c;
cout << "Animal对象的大小:" << sizeof(a) << endl;
cout << "Cat对象的大小:" << sizeof(c) << endl;
Animal* animal = new Cat();
animal->speak();
Cat* cat = new Cat();
cat->speak();
cat->Animal::speak();
animal->run();
return 0;
}
静态绑定
代码中Animal和Cat类都定义了成员函数void speak()
通过基类指针调用的是基类的成员函数(程序执行之前,就以这种方式固定下来,函数调用的静态绑定)
当使用父类指针指向子类对象的时候:
观察反汇编代码 animal->speak(); 汇编代码直接调用了Animal类的speak()函数
当使用子类指针指向子类对象的时候:
cat->speak(); 汇编代码直接调用了Cat类的speak()函数
如果想要调用父类的同名函数 可以使用 子类对象指针->父类::函数 的方式
动态绑定
有以下三项条件要符合:
使用指针进行调用
指针属于up-cast后的
调用的是虚函数
根据对象指针找到对象,根据对象中的虚表指针找到虚表,再通过虚表找到虚表中的函数的地址,然后调用该函数
将上面程序主函数做出修改
int main() {
Animal* animal = new Cat();
Animal* a = new Animal();
Cat* c = new Cat();
animal->run();
return 0;
}
如果一个类中存在虚函数,对象的大小会多一个指针的大小,里面是一张虚表的地址,虚表里面存着虚函数的地址
一般情况下,虚表指针存在对象的最前面
子类对象的虚表指针指向同一个地址,父类对象的虚表指针指向另一个地址
观察反汇编代码 animal->run();
与父类子类中同名函数的调用不同,虚函数的调用并没有直接调用一个明确写出的方法
-
从animal地址取出4个字节给eax 即animal对象的地址;
-
取出animal最前面的4个字节(dword)给edx,cat对象最前面的4个字节,即虚表指针
-
然后根据edx的地址值,找到它的存储空间,取出最前面的4个字节(即Cat::run()的地址),赋值给eax
call eax 就等价于 call Cat::run()
按F11执行到这一句会跳到这里
观察edx值与虚表指针值,发现二者相同
可以结合内存验证上述结论正确性