目录
一、多态的定义和实现
1.1 多态的构成条件:
1.2 虚函数的重写(覆盖):
1.3 多态的两个特殊点:
1.4 析构函数的重写:
1.5 override和final
1.6 重载,重定义(隐藏),重写(覆盖)的区别
二、抽象类
2.1 纯虚函数
2.2 接口继承和实现继承
三、多态的原理
3.1 虚函数表
3.2 验证派生类自己新增的虚函数会不会存储在派生类虚表中:
3.3 多态的原理
3.4 动态绑定与静态绑定
四、多继承中的虚函数表
4.1 普通多继承下的虚函数表:
4.2 菱形继承下的多态
五、多态相关题
一、多态的定义和实现
1.1 多态的构成条件:
构成多态需要两个条件:
1. 必须通过基类的指针或者引用调用虚函数
2. 被调用的函数必须是虚函数,且派生类必须对基类的虚函数进行重写
虚函数:被virtual修饰的类成员函数称为虚函数。
1.2 虚函数的重写(覆盖):
派生类中有一个跟基类完全相同的虚函数(即派生类虚函数与基类虚函数的返回值类型、函数名字、参数列表完全相同),称子类的虚函数重写了父类的虚函数。 (其实就是和基类的虚函数声明完全相同,函数体不同,类似于重新定义函数的行为)
1.3 多态的两个特殊点:
1. 派生类中对基类虚函数进行重写时,可以省略virtual关键字,此时该函数仍然为虚函数且构成重写。建议加上virtual声明,更规范。(因为继承后基类的虚函数被继承下来了在派生类依旧保持虚函数属性)
2. 协变:派生类对基类虚函数进行重写时,要求函数名,参数列表,返回值都相同。一个例外是返回值类型可以不同,基类虚函数返回基类对象的指针或者引用,派生类虚函数返回派生类对象的指针或者引用时,称为协变。
1.4 析构函数的重写:
回顾继承:
1. 在继承体系中,派生类的析构函数会自动调用基类析构函数去清理基类部分数据成员
2. 并且编译器会将继承体系中的析构函数函数名处理为destructor,所以若不将基类析构函数定义为虚函数,默认为隐藏关系!
那么,如果执行下面代码时,析构函数就必须重写!
class Base
{
public:
Base()
:p(new int[10])
{
}
virtual ~Base()
{
cout << "~Base()" << endl;
delete p;
}
protected:
int* p;
};
class Derived : public Base
{
public:
Derived() // 自动调用基类的默认构造函数
:p2(new double[10])
{
}
virtual ~Derived()
{
cout << "~Derived()" << endl;
delete p2;
}
protected:
double* p2;
};
int main()
{
Base* p = new Base;
delete p;
Base* p2 = new Derived;
delete p2;
return 0;
}
delete p2时,delete语句会执行 p2->destructor(); operator delete(ptr2); 调用p2所指向的析构函数,此时若不将析构函数定义为虚函数,则不会发生重写,而是隐藏。则第二个delete时,事实上,Derived类中会有两个析构函数,一个是基类的,一个是自己的。p2类型为Base*,且没有发生多态,只能调用基类的析构函数。
因此,诸如上方示例,建议将继承体系中的析构函数定义为虚函数,这样子类就可以对父类析构函数进行重写。
可以理解,编译后析构函数函数名被编译器处理为destructor(),也是为了便于设为虚函数,发生多态和动态绑定。这样delete基类指针时,才能调用正确的析构函数,即指向父类调用父类的析构函数,指向子类调用子类的析构函数。
1.5 override和final
1. final:修饰虚函数,表示该虚函数不能再被重写
2. override: 检查派生类虚函数是否重写了基类某个虚函数,如果没有重写编译报错。
1.6 重载,重定义(隐藏),重写(覆盖)的区别
重载:两个函数在同一定义域,函数名相同,参数列表不同。
重定义(隐藏):两个函数分别在基类和派生类的作用域中,函数名相同。
重写(覆盖):两个函数分别在基类和派生类的作用域中,函数名,参数,返回值相同(协变除外)
事实上,基类和派生类中,两个函数如果函数名相同,若没有构成重写,就是重定义(隐藏)
二、抽象类
2.1 纯虚函数
在虚函数的后面写上 =0 ,则这个函数为纯虚函数。包含纯虚函数的类叫做抽象类(也叫接口 类),抽象类不能实例化出对象。派生类继承后也不能实例化出对象(因为继承了纯虚函数),只有重写纯虚函数,派生 类才能实例化出对象。纯虚函数规范了派生类必须重写,另外纯虚函数更体现出了接口继承。
class Base
{
public:
virtual void Drive() = 0;
};
2.2 接口继承和实现继承
普通函数的继承是一种实现继承,派生类继承了基类函数,可以使用函数,继承的是函数的实 现,故,实际上调用基类的普通函数时,函数隐含的this指针类型为 Base* const。
虚函数的继承是一种接口继承,派生类继承的是基类虚函数的接口,目的是为了重写,达成多态,继承的是接口。所以如果不实现多态,不要把函数定义成虚函数。(因为如果基类定义虚函数,派生类不重写,则Base*->func()调用时 仍然为运行时绑定,只是因为没有重写,执行的一定是基类的虚函数,还降低了效率)
三、多态的原理
3.1 虚函数表
class Base
{
public:
Base()
:p(new int[10])
{
}
virtual ~Base()
{
delete p;
}
virtual void Func1()
{
cout << "virtual void Base::Func1()" << endl;
}
virtual void Func2()
{
cout << "virtual void Base::Func2()" << endl;
}
void Func3()
{
cout << "void Base::func3()" << endl;
}
protected:
int* p;
};
class Derived : public Base
{
public:
Derived() // 自动调用基类的默认构造函数
:p2(new double[10])
{
}
virtual ~Derived()
{
delete p2;
}
virtual void Func1() // 重写基类Func1
{
cout << "virtual void Derived::Func1()" << endl;
}
protected:
double* p2;
};
void test1()
{
Base b;
Derived d;
}
1. 每一个含有虚函数的类的实例化对象中都有一个指针成员,称为虚函数表指针(虚表指针),即上图中的_vfptr(virtual function ptr),这个指针指向一个虚函数表(简称虚表,一种函数指针数组),这个虚函数表中会存储这个类定义的所有虚函数的地址。
2. 因为基类定义了虚函数,所以每一个虚函数的地址会存储在基类的虚表指针指向的虚表中,派生类继承基类,这个虚表指针和虚表作为基类的数据成员也被继承了下来,派生类内重写了的虚函数的地址会覆盖基类的虚函数的地址。故上图中,基类和派生类的虚表中 析构函数的地址不同,Func1的地址不同,而Func2的地址相同。
3. 虚函数的重写也叫作覆盖,覆盖就是指虚表中虚函数地址的覆盖。重写是语法的叫法,覆盖是原理层的叫法。 (派生类虚表中Func1和析构函数覆盖基类的Func1和析构函数)
4. 基类的非虚函数Func3不会放进虚表中。
5. 容易混淆的问题:虚函数存在哪的?虚表存在哪的? 虚函数和普通函数一样,存储在代码段。类对象中存储的是虚表指针,而不是虚表。虚表中存储的是虚函数的地址,而不是虚函数。虚表事实上也是存储在代码段的(只读的,vs下)
6. 派生类虚表生成大致过程:a.先将基类中的虚表内容拷贝一份到派生类虚表中 b.如果派生 类重写了基类中某个虚函数,用派生类自己的虚函数覆盖虚表中基类的虚函数 c.派生类自己新增加的虚函数按其在派生类中的声明次序增加到派生类虚表的最后。
3.2 验证派生类自己新增的虚函数会不会存储在派生类虚表中:
class Base
{
public:
Base()
:p(new int[10])
{
}
//virtual ~Base()
//{
// delete p;
//}
virtual void Func1()
{
cout << "virtual void Base::Func1()" << endl;
}
virtual void Func2()
{
cout << "virtual void Base::Func2()" << endl;
}
protected:
int* p;
};
class Derived : public Base
{
public:
Derived() // 自动调用基类的默认构造函数
:p2(new double[10])
{
}
//~Derived()
//{
// delete p2;
//}
virtual void Func1() // 重写基类Func1
{
cout << "virtual void Derived::Func1()" << endl;
}
virtual void new_add()
{
cout << "virtual void Derived::new_add()" << endl;
}
protected:
double* p2;
};
typedef void(*VFPtr)();
void print_virtual_function_table(VFPtr* VFTable) // 函数指针数组
{
for (int i = 0; VFTable[i]; ++i)
{
printf("VFTable[%d] : %p\n", i, VFTable[i]);
VFTable[i]();
}
}
void test1()
{
Base b;
Derived d;
print_virtual_function_table((VFPtr*)*(int*)&d);
}
事实证明,派生类自己新增的虚函数是会按照声明顺序添加到自己的虚函数表的后端的。只是VS下的监控窗口隐藏了。
3.3 多态的原理
有了上面虚函数表和虚函数指针的基础,再来理解多态的原理。
如上,符合多态的条件:基类指针调用重写好的虚函数。
1. 多态调用:程序(进程)运行过程中,会去指针指向的对象的虚表指针指向的虚表中找到函数的地址,进行调用。所以,p2指向的是基类,调用基类的虚函数,指向派生类,调用派生类的虚函数。
2. 不满足多态的普通函数调用,编译链接时已经确定函数的地址,运行时直接调用。
感想:这里其实还存在切片的现象,比如基类指针指向派生类对象,可以理解为指针指向的是派生类中基类的这一部分, 虚表指针也属于这一部分。因此,当使用基类指针或者引用(引用同理)调用虚函数时,会去指向对象(基类对象or派生类对象)的虚表指针指向的虚表中找函数地址。发生多态现象,称为动态绑定(运行时绑定)
3.4 动态绑定与静态绑定
1. 静态绑定又称为前期绑定(早绑定),在程序编译期间确定了程序的行为,也称为静态多态, 比如:函数重载
2. 动态绑定又称后期绑定(晚绑定),是在程序运行期间,根据具体拿到的类型确定程序的具体 行为,调用具体的函数,也称为动态多态。
四、多继承中的虚函数表
4.1 普通多继承下的虚函数表:
class Base1
{
public:
virtual void func1() {
printf("Base1::func\n");
}
virtual void func2() {
cout << "Base1::func2" << endl;
}
private:
int b1;
};
class Base2 {
public:
virtual void func1() { cout << "Base2::func1" << endl; }
virtual void func2() { cout << "Base2::func2" << endl; }
private:
int b2;
};
class Derive : public Base1, public Base2 {
public:
virtual void func1() { cout << "Derive::func1" << endl; }
virtual void func3() { cout << "Derive::func3" << endl; }
private:
int d1;
};
typedef void(*VFPtr)();
void print_virtual_function_table(VFPtr* VFTable) // 函数指针数组
{
for (int i = 0; VFTable[i]; ++i)
{
printf("VFTable[%d] : %p\n", i, VFTable[i]);
VFTable[i]();
}
}
int main()
{
Base1 b;
Derive d;
print_virtual_function_table((VFPtr*)*(int*)&d);
cout << endl;
print_virtual_function_table((VFPtr*)*(int*)((char*)&d + sizeof(Base1)));
return 0;
}
1. 派生类有两个基类,每个基类都有定义虚函数,则每个基类都有虚函数表指针,派生类继承就会有两个虚函数表指针,func1进行了重写,因此两个虚表中func1的地址都进行了覆盖。func2没有覆盖。而派生类自己新增的虚函数会存储在第一个继承基类部分的虚函数表中
注:这里存在很多编译器的行为,比如这里的地址事实上并不是函数的真实地址(函数的第一条指令的地址),而是某跳转指令的地址,跳转指令会跳转至函数真正的执行语句。但是我们依然理解为虚表中存储的是虚函数的地址!
第二个点:上图中打印的派生类重写了的func1函数,函数地址不同,这里是因为Base2* 去调用这个func1时,要进行一些额外操作:Base2*指向的是基类Base2部分的虚表指针处,调用func1需要事先将此指针减一些偏移量,使其指向派生类对象的开端,即派生类对象的地址,因此函数地址不同。下图所示,执行eax - 8
下图为派生类对象直接调用func1和Base1*调用func1(指向派生类),Base2*调用func1(指向派生类)的汇编代码。
4.2 菱形继承下的多态
class A
{
public:
virtual void func1()
{
cout << "A::func1()" << endl;
}
public:
int _a;
};
//class B : public A
class B : virtual public A
{
public:
virtual void func1()
{
cout << "B::func1()" << endl;
}
virtual void func2()
{
cout << "B::func2()" << endl;
}
public:
int _b;
};
//class C : public A
class C : virtual public A
{
public:
virtual void func1()
{
cout << "C::func1()" << endl;
}
virtual void func2()
{
cout << "C::func2()" << endl;
}
public:
int _c;
};
class D : public B, public C
{
public:
virtual void func1()
{
cout << "D::func1()" << endl;
}
public:
int _d;
};
int main()
{
D d;
cout << sizeof(d) << endl;
d.B::_a = 1;
d.C::_a = 2;
d._b = 3;
d._c = 4;
d._d = 5;
//print_virtual_function_table((VFPtr*)*(int*)&d);
//cout << endl;
//print_virtual_function_table((VFPtr*)*(int*)((char*)&d + sizeof(B)));
return 0;
}
D中有BC基类数据,还有_d,BC内都是除了自己的_b _c 还有虚表指针(因为虚函数),虚基表指针(因为虚拟继承)。虚基表中存储着到本类的虚表指针的偏移量,以及基类A部分数据的偏移量。基类A中有一个虚表指针(图中的0x00349b90)和自己的_a
五、多态相关题
class A
{
public:
virtual void func(int val = 1){ std::cout<<"A->"<< val <<std::endl;}
virtual void test(){ func();}
};
class B : public A
{
public:
void func(int val=0){ std::cout<<"B->"<< val <<std::endl; }
};
int main(int argc ,char* argv[])
{
B*p = new B;
p->test();
return 0;
}
B* p 指向派生类对象。test是一个虚函数,是属于基类的,此时隐含的this指针为A* const this,将p赋值过去(注意这里调用test是一个常规的函数调用),使得this指针为基类类型,指向派生类对象,调用func,发生动态绑定和多态现象。此时调用的是派生类的虚表中存储的func,但是因为虚函数的继承是一种接口继承,目的是为了重写函数体,所以,此时函数的参数列表为int val = 1,因此结果为B->1。