重写(覆盖)
子类重写定义父类相同名称、返回值和参数的虚函数
虚函数:就是父类用virtual声明的成员函数
无论子类覆盖的函数是不是虚函数,都会被自动处理成虚函数。
覆盖:指的是虚函数表中虚函数地址的覆盖
class father
{
public:
virtual void fun() {
cout << "father" << endl;
}
};
class son:public father
{
public:
virtual void fun() {
cout << "son" << endl;
}
};
int main()
{
son s1;
s1.fun(); //重写成功:son
s1.father::fun();//重写可使用作用域调用父类的
}
隐藏
是继承结构中的 把基类中的同名的函数隐藏掉 实际是作用域的隐藏
在子类中和父类的同名函数,不是重写,就是隐藏。
通过作用域 或 父类指针指向子类对象调用隐藏方法 仍可以访问被隐藏掉的基类函数
class Base
{
public:
void A() { cout << " Base::A()" << endl; };
void A(int val) { cout << " Base::A(int)" <<endl; }; //重载:同名 相同作用域 参数不同
virtual void B(int val) { cout << " Base::B(int)" << endl; }//虚函数 可以被重写
};
class Derive :public Base
{
public:
void A() { cout << " Derive::A()" << endl; };//隐藏 基类中不是虚函数 基类的作用域被隐藏 可以通过基类+作用域来访问
void B() { cout << " Derive::B()" << endl; };//隐藏 参数不同
virtual void B(int a) { cout << " Derive::B(int)" << endl; };//重写 可被子类重写
};
int main() {
Derive d;
d.A(); //Derive::A()
//d.A(10);//报错 因为基类同名被隐藏掉 只能访问自己的以A为名的函数
//优先找派生类自己作用域的A名的成员
d.Base::A();//Base::A() 隐藏仍然可以通过+作用域来访问
Base* pb = &d; //Base限制访问的是派生类里面继承来的 也就是作用域是基类的
pb->A(); //Base::A()
//pb->B();//报错 pb的指针类型是基类
((Derive*)pb)->B();// Derive::B() 可以通过强转 来访问d中作用域是派生类的成员
Base& pb1 = d; //引用和指针同理
pb1.A();
((Derive&)pb1).B();
//Derive* pb2 = new Base();//错误 不能用派生类的指针(引用)指向基类对象
}
注意:在基类和派生类之间的指针和引用中 无法进行从上到下的转换 即不能用派生类的指针(引用)指向基类的对象
重写和隐藏
在子类和父类中,在函数名相同的前提下:
父类函数为虚函数、返回值相同、参数相同的为重写,其他是隐藏
多态
字面意思:一个东西具有多种多样的形态
- 静态多态:在编译时期就确定好的调用的函数,函数重载、模板。
误区:继承中的隐藏不是静态多态,调用的时候虽然是编译期确定,但是他他并不是通过一个相同的东西产生不同的,就是普通的函数调用。
- 动态多态:在运行时期确定的函数调用,即为基类指针(引用)指向派生类对象,通过该指针(引用)调用同名覆盖方法(虚函数),同一个基类指针,指向哪个派生类对象就会调用派生类的同名覆盖方法,称为多态。
继承中,父类指针指向子类对象,调用同名函数的时候分为三种情况:
- 隐藏:指针什么类型调用谁的 作用域
- 重写:调用子类重写后的函数 多态
启动动态多态条件:有继承关系,子类重写父类虚函数,父类指针指向子类对象并且调用子类重写的虚函数。
class A
{
public:
virtual void work()
{
cout << "A work()" << endl;
}
void fun()
{
cout << "A fun()" << endl;
}
};
class B:public A
{
public:
virtual void work()
{
cout << "B work()" << endl;
}
void fun()
{
cout << "B fun()" << endl;
}
};
class C :public B
{
public:
virtual void work()
{
cout << "C work()" << endl;
}
void fun()
{
cout << "C fun()" << endl;
}
};
int main()
{
A* a = new C(); //父类指针指向子类对象
a->work(); //1.重写:调用重写后的函数 多态
a->fun(); //2.隐藏:调用指针类型的函数
}
多态的实现
多态的技术:动态绑定
动态绑定的核心:虚函数表
(1)虚函数表
- 包含虚函数的类有一个虚表
- 虚表是一个数组,其内元素是虚函数的函数指针
- 虚表中虚函数指针的赋值发生再编译器编译阶段 所以在编译阶段 虚表就构造出来了
- 当基类是由虚函数时,其派生类也有了自己的虚表(包含自己和基类的虚函数指针)
(2)虚表指针
- 虚表属于类,同一个类的所有对象使用同一个虚表
- 编译时编译器会往有虚函数的类中加上一个虚表指针_vptr,用来指向虚表vtable。当类的对象在创建时便拥有这个指针,指向同一个类的虚表。
- 验证_vptr的指针(*_vptr不可访问):先求一个不含虚函数的类占的字节数sizeof()。然后再将一个函数变成虚函数求字节数sizeof(),会发现增加4个字节。
- 一个类里面虚函数的个数,不影响对象内存的大小(vfptr),影响的是虚函数表的大小。
(3)静态、动态联编
联编:将模块或者函数合并在一起成为可执行代码的处理过程(函数调用),按照联编所进行的阶段不同,分成一下两种
静态联编(绑定):编译期阶段进绑定(函数调用)
动态联编(绑定):程序运行期间进绑定(函数调用)编译期看不出来(反汇编)
动态联编针对C++的多态,其他都是静态联编
动态编排的条件:
- 基类必须有成员函数显示声明为virtual
- 如果基类声明了虚函数,则派生类中可以不必再声明
在C++中,派生类中重写基类的虚函数时,即使不显式使用virtual关键字,编译器也会将这个函数视为虚函数,并将其添加到派生类的虚函数表(所以相当于就是虚函数 对子类来说就是虚函数)中。因此,即使在派生类中没有显式使用virtual关键字,也可以实现多态性。
动态联排案例:
class A //A虚表:vfun1函数指针 vfun2函数指针
{
private:
int m_data1, m_data2;
public:
virtual void vfun1() {};
virtual void vfun2() {};
void fun1() {};
void fun2() {};
};
class B:public A //B虚表:继承A::vfun2不变 重写vfun1指针改变
{
private:
int m_data3;
public:
virtual void vfun1() {};
void fun1() {};
};
class C:public B //C虚表:继承B::vfun1指针不变 重写vfun2指针改变
{
private:
int m_data1, m_data4; //m_data1和A中重名 调用时用作用域区分二义性
public:
virtual void vfun2() {};
void fun2() {};
};
int main()
{
B b;
A* p = &b; //父类指针指向子类对象
p->vfun1();
}
代码分析:
- b是B的对象,所以b有虚表指针_vptr指向类B的虚表
- 执行p->vfun1()时 发现p是个指针
- 调用的是虚函数
- 首先,根据_vptr来访问对象b所对应的虚表,虽然指针p是基类A类型,但是_vprt也是基类的一部分,所以可以通过p->_vptr来访问对象对应的虚表。
- 然后,在B虚表查找所调用的函数对应的条目。
- 最后,根据虚表找到的函数指针,调用函数B:vfunc1();
- 调用的是普通函数
- 到b中作用域为指针类型的函数中寻找 有的话调用 没有的话报错
- 调用的是虚函数