目录
多态的概念
不同对象的同一行为有不同的结果,从程序的角度就是,对相同类下的不同对象调用同一个成员函数时,执行的成员函数不同
多态的条件及实现
虚函数
被virtual修饰的成员函数是虚函数
class Person
{
public:
virtual void BuyT()
{
cout << "全价" << endl;
}
};
这里BuyT前面被virtual修饰了,所以BuyT就变成了虚函数,当然,必须是成员函数,才有虚函数这个概念,
虚函数的重写
虚函数存在的意义就是支持重写/覆盖
派生类中有一个跟基类完全相同的虚函数(返回类型、函数名、参数列表完全相同),则称子类的虚函数重写了基类的虚函数
重写 = 虚函数 + 三同
class Person
{
public:
virtual void BuyT()
{
cout << "全价" << endl;
}
};
class Student : public Person
{
public:
virtual void BuyT()
{
cout << "半价" << endl;
}
};
这里Person中的BuyT和Student的BuyT就构成了重写(这里其实子类的virtual可以去掉,依旧是重写,但是一般都会写上,含义会更明确、更易阅读)
多态的条件
1、必须通过基类的指针或引用调用虚函数
2、被调用的函数必须是虚函数,并且派生类必须对基类的虚函数进行重写
如果不满足多态条件时就是普通调用,看当前调用者的类型即可,调用者是父类则调用父类的成员函数,调用者是子类则调用子类的成员函数
class Person
{
public:
virtual void BuyT()
{
cout << "全价" << endl;
}
};
class Student : public Person
{
public:
virtual void BuyT()
{
cout << "半价" << endl;
}
};
void Func(Person& people)//基类引用,这里也可以是Person* people,后面只需改成people->BuyT()
{
people.BuyT();
}
void test1()
{
Person P1;
Student S1;
// 方案一:
// Person* p = &P1;
// p->BuyT();// 全价
// p = &S1;
// p->BuyT();// 半价
// 方案二:
// Person& p1 = P1;
// Person& p2 = S1;
// C++不允许引用在创建后改变,所以这里就只能再创建一个变量p2,不过大多数多态不会这么用了,
// 一般都是方案一或者方案三
// p1.BuyT();// 全价
// p2.BuyT();// 半价
//方案三:
Func(P1);// 全价
Func(S1);// 半价
}
上面我们创建了Person类的对象P1,在Func中通过基类的引用调用BuyT()函数,得到了不同的结果,达到了多态的目的
注意到这里我们传递的S1,让Func的Person对象接收时是发生了切片的
虚函数重写的细节
1、父类必须加virtual,子类重写的虚函数可以不加virtual
2、调用虚函数时不能用域作用限定符限定
class Person
{
public:
virtual void BuyT()
{
cout << "全价" << endl;
}
};
class Student : public Person
{
public:
virtual void BuyT()
{
cout << "半价" << endl;
}
};
void Func(Person& people)
{
people.Person::BuyT();
// people.Student::BuyT();
}
void test1()
{
Person P1;
Student S1;
Func(P1);
Func(S1);
}
这个例子中我们用域作用限定符限定people对象调用的是Person类的BuyT()也无法构成多态
3、协变情况下,不要求三同中返回值类型相同,就可以达到多态
协变是指返回值类型是父子类关系的指针或者引用(协变的实用意义不大但是笔试容易考)
class Person
{
public:
virtual Person* BuyT()//Person*
{
cout << "全价" << endl;
return nullptr;
}
};
class Student : public Person
{
public:
virtual Student* BuyT()//Student*
{
cout << "半价" << endl;
return nullptr;
}
};
void Func(Person* people)
{
people->BuyT();
}
void test1()
{
Person P1;
Student S1;
Func(P1);
Func(S1);
}
返回值类型用Person* Student*达到了协变的条件,也可以满足多态
并且协变的返回值满足父子类关系,并不强调是自身的父子类关系,看下例:
class A
{};
class B : public A
{};
class Person
{
public:
virtual A* BuyT()
{
cout << "全价" << endl;
return nullptr;
}
};
class Student : public Person
{
public:
virtual B* BuyT()
{
cout << "半价" << endl;
return nullptr;
}
};
void Func(Person& people)
{
people.BuyT();
}
void test1()
{
Person P1;
Student S1;
Func(P1);
Func(S1);
}
A和B是父子类关系,在Person和Student中,也可以使用AB父子类的指针作为返回值
4、析构函数的重写
由于有以下场景:
class Person
{
public:
virtual void BuyT()
{
cout << "全价" << endl;
}
~Person() { cout << "Person的析构" << endl; }
};
class Student : public Person
{
public:
virtual void BuyT()
{
cout << "半价" << endl;
}
~Student() { cout << "Student的析构" << endl; }
};
void test2()
{
//一个指针p既可以指向父类,也可以指向子类,调用父类的成员函数、调用子类的成员函数
Person* p = new Person;
delete p;// Person的析构
p = new Student;
delete p;// Person的析构
}
我们这里new Student 给到p后delete p我们想要的效果是调用子类的析构函数,但是这里却没有调用Student析构函数。这种情况就可能发生内存泄漏,当我们在析构前面加上virtual,发现,就能够调用Student的析构函数了(当然符合继承的特点,子类析构先调用父类析构,再调用子类析构)
class Person
{
public:
virtual void BuyT()
{
cout << "全价" << endl;
}
virtual ~Person() { cout << "Person的析构" << endl; }
};
class Student : public Person
{
public:
virtual void BuyT()
{
cout << "半价" << endl;
}
virtual ~Student() { cout << "Student的析构" << endl; }
};
void test2()
{
Person* p = new Person;
delete p;// Person的析构
p = new Student;// Person的析构\n Student的析构
delete p;
}
但有人会问这不满足三同,的确,外显的,析构天然是不能构成三同的,但是又大量有上述场景,所以编译器会对基类加了virtual的析构函数(子类可以不加,但是建议还是加上,方便阅读、理解)特殊处理,把基类派生类的析构都统一处理为destructor(),这样就能够构成三同,而对于delete p的两个步骤:p所在的类的析构 + operator delete§,也就是把前面这一步变成destructor(),即p->destructor的多态调用
,就能够处理上面的场景
override和final关键字
override是帮助派生类检查是否完成重写,如果没有完成重写会报错
final 修饰虚函数,表示该虚函数不能被重写(final在继承中也有应用,修饰某个类,表示该类无法被继承,final嘛,到终点了,到头了,在下面案例中会讲解)
class Car
{
public:
virtual void test()
{
cout << "Car class..." << endl;
}
};
class Car_Child :public Car
{
public:
virtual void test() final
{
cout << "Car_Child class..." << endl;
}
};
class Car_GrandChild :public Car_Child
{
public:
void test()//err 不能将final修饰的虚函数重写
{
cout << "Car_GrandChild class..." << endl;
}
};
案例
设计一个类,不能被继承
C++98的方案:把父类的构造函数私有化
class A
{
private:
A(){}
};
class B : public A
{};
void test3()
{
B b;// err A()不可访问
}
这种设计方式下需要一个成员函数,用于A对象的创建:内部调用构造函数,返回类型是A类型:
class A
{
public:
A CreatObj()
{
return A();
}
private:
A(){}
};
class B : public A
{};
但是,还有个问题,我们调用CreatObj()也是需要依靠对象的,所以我们还要把CreatObj变成static静态的,通过域作用限定符访问到这个CreatObj()即可
class A
{
public:
static A CreatObj()
{
return A();
}
private:
A(){}
};
class B : public A
{
B(){}// err A()不可访问
};
void test()
{
A::CreatObj();
}
C++11的方案:
对类加final,直接就能够限制继承
class A final
{
private:
A(){}
};
class B : public A
{};
void test3()
{
B b;// err A()不可访问
}
总结:final作用
1、修饰类,让这个类不能被继承
2、修饰虚函数,让虚函数不能被重写
重载、重写、隐藏概念对比
重载:
1、两个函数在同一作用域
2、函数名相同,参数不完全相同
重写/覆盖:
1、两个函数分别在派生类和基类
2、两个函数必须是虚函数
3、函数名、参数类型、返回值完全相同(协变除外,协变使得返回值可以是父类基类的指针、引用关系)
用基类指针、引用调用重写的虚函数以达到多态
重定义/隐藏:
1、两个函数分别在派生类和基类
2、函数名相同
重定义也就是条件更弱的重写,只要不满足虚函数 + 三同,且函数名相同,那么就是重定义,另一个角度讲,就是子类中写了一个和父类函数名相同的函数,在创建子类对象的时候调用的是子类中这个函数,调用子类这个函数,而不是调用父类的函数,这里这两个函数就构成了重定义/隐藏
多态的原理
多态的实现原理
1、我们先来用sizeof计算一下有虚函数的类的大小
class base
{
public:
base(char b = 1)
:_b(b)
{}
virtual void fun()
{
cout << "hello base" << endl;
}
protected:
char _b;
};
void test()
{
sizeof(base);// 8
}
只有一个char类型的数据,怎么sizeof大小是8?
2、我们再创建一个base类型的对象,通过监视窗口观察b中存储的内容
void test()
{
base b;
}
也就是说在创建base类型的时候还会自动生成一个指针,我们称这个指针__vfptr就是虚函数表指针(virtual funtion table point)
所有的虚函数地址都会存储在虚函数表中(虚函数都还是在代码段的)
不管创建了多少对象,这个虚函数表指针,都是使用的同一个指针
3、接着我们在构成重写的父子类对象中观察:
class base
{
public:
base(char b = 1)
:_b(b)
{}
virtual void fun1()
{
cout << "hello base fun1" << endl;
}
virtual void fun2()
{
cout << "hello base fun2" << endl;
}
protected:
char _b;
};
class child : public base
{
public:
child(base b = base(), int c = 2)
:base(b)
,_c(c)
{}
void fun1()
{
cout << "hello child fun1" << endl;
}
protected:
int _c;
};
void test3()
{
base b;
child c;
}
fun1构成重写,观察c对象,c对象中虚函数表对于fun1存储的是child::的fun1,fun2是从base中继承来的,所以是base::,观察b对象,b是基类,虚函数表中存储的都是base::
4、接下来我们在切片中进行观察
class base
{
public:
base(char b = 1)
:_b(b)
{}
virtual void fun1()
{
cout << "hello base fun1" << endl;
}
virtual void fun2()
{
cout << "hello base fun2" << endl;
}
protected:
char _b;
};
class child : public base
{
public:
child(base b = base(), int c = 2)
:base(b)
,_c(c)
{}
void fun1()
{
cout << "hello child fun1" << endl;
}
protected:
int _c;
};
void fun(base& b)//引用
{
b.fun1();
}
void test4()
{
base b;
child c;
fun(b);// hello base fun1
fun(c);// hello child fun1
}
这里通过调用fun函数,把c进行了切片,在fun函数中,依旧调用的是子类的虚函数fun1,完成了多态
下图是base b进入fun()
下图是child c进入fun()
c进行了切片,_c变量被切掉了,但是观察到__vfptr中的fun1()还是child::
也就是说编译器是通过虚函数表中的虚函数地址来进行调用函数的
我们回顾函数的调用原理,是在编译环节,就确定了函数的调用,但是虚函数这里是例外,虚函数的调用是在运行阶段通过虚函数表确定的,上述这就是实现多态的原理。
总结:通过virtual关键字,将函数变成虚函数,在创建对象时创建虚函数表,并在父子继承中两个虚函数满足三同,构成了重载,子类对象通过切片赋给了父类对象时,虚函数表,编译器也就通过这个虚函数表中函数地址去访问到了子类虚函数,由此实现了多态。
所以从原理层面判断符合多态和不符合多态:
符合多态:运行时到对象的虚函数表中找调用函数的地址
不符合多态:编译时就确定类型
多态的条件提问
多态的条件为什么不能是子类的指针或引用?子类无法通过切片拿到父类的虚函数地址,也就无法在不同对象的情况下,调用不同的函数
为什么不能是父类的对象?对象的切片和指针引用的切片不同,指针和引用在切片时不会进行拷贝,就是直接把变量和虚函数表指针给过去,对象的切片只会拷贝父类成员变量数据,但是不会拷贝虚函数表指针,使用的还是父类的虚函数表指针
多态中虚表的创建
父类的虚函数表是在编译阶段进行的,每有一个虚函数,就会把代码段中虚函数的地址放进虚函数表中(后面会验证虚函数的内存所在位置)
子类的虚函数表是有可能有父类的虚函数的,因为每一个父类虚函数指针(被virtual修饰的成员函数都是虚函数)都会被放进虚表,在子类对象创建时就会把父类的虚表放进子类的虚表中,然后子类与父类构成多态的虚函数就会进行重写,覆盖掉父类的虚函数指针,并且独属于子类自己的虚函数(用于子类与子类的子类构成多态) —— 简单来说子类的虚函数表就是:父类拷贝 + 子类覆盖 + 子类虚函数添加(所以这里可以看出,为什么函数重写也叫覆盖)
vs中虚函数表的问题
1、虚表、虚函数的存储的地址位置在哪?
2、对子类添加虚函数后在监视窗口的__vfptr中无法查看到新增虚函数的处理办法?
对问题一:我们可以用验证的方法来判断虚表虚函数存储的位置,具体方法是通过已知去判断位置,比如我们知道普通变量存储在栈,动态开辟内存存在堆,static静态变量存储在静态区,常量字符串存储在常量区,通过打印已知和位置的存储地址位置,来判断未知的存储地址位置
对问题二:问题二主要是想说明监视窗口不一定完全正确,我们在监视窗口中有违背所学或者无法判断之时,更多的要通过内存去判断,内存中的数据更加准确,或者可以查阅官方文档,查阅源代码等方式进行学习,判断,具体我们这里可以通过在vs中打印虚基表来判断,这里会涉及到一个vs对虚函数表的特性,我们利用vs的特性把虚表打印出来,vs中对虚表的结束,是通过置空nullptr来实现的(vs实现的不是太好,有的时候在修改了代码,增加了虚函数时,会出bug——虚表不以空结尾,这时,只需要清理解决方案,再重新编译即可)
这两个问题的具体代码如下,代码分析在注释中
//Car BigCount父子类多态
class Car
{
public:
Car(){}
virtual void fun1() { cout << "Car fun1" << endl; }
virtual void fun2() { cout << "Car fun2" << endl; }
};
class BigCount : public Car
{
public:
BigCount(){}
virtual void fun1() { cout << "BigCount fun1" << endl; }
virtual void fun3() { cout << "BigCount fun3" << endl; }
};
//前面的fun1 fun2 fun3的返回值、参数都相同,所以我们这里类型重定义一下,方便后序书写
//解读:FUNC_PTR是函数指针,是返回值为void,参数为无参的函数的指针
typedef void (*FUNC_PTR)();
//打印虚基表,这里FUNC_PTR* 是函数指针数组的首地址
//我们这里是传给函数虚函数表的首地址,用table存储,table就跟int array[10]; 中array的意义是相同的
void Print(FUNC_PTR* table)
{
for (int i = 0; table[i] != nullptr; i++)//以vs的特性置空作为停止条件
{
printf("[%d]:%p", i, table[i]);
cout << endl;
}
cout << endl;
}
void funC(){}//得到一个代码段的地址
void test10()
{
int a = 0;
printf(" 栈区 :%p\n", &a);
int* pa = new int;
printf(" 堆区 :%p\n", pa);//new出来的是堆区空间,pa本身的地址还是栈区中,所以是查看pa,而不是&pa
printf(" 栈区 :%p\n", &pa);//new出来的是堆区空间,pa本身的地址还是栈区中,所以是查看pa,而不是&pa
static int b = 0;
printf("静态区:%p\n", &b);
const char* str = "abc";
printf("常量区:%p\n", str);
printf("代码段:%p\n", funC);
cout << endl;
//上面都是我们已知区域的变量、函数内存位置
//下面我们通过打印未知的与已知比较,来判断,未知所在的内存位置
BigCount cd;
//这里我们&cd拿到对象的首地址,然后强制类型转换为int*,然后就拿到了前四个空间地址,对其解引用,拿到虚函数表指针,也就是虚函数表的地址
//我们将其暂时存储在int类型的tmpptr中,然后后面把这个tmpptr强制类型转换成FUNC_PTR*,让他以函数指针数组首地址的角度解读这个tmpptr,进而就能够放到Print进行打印虚函数表中的虚函数地址
int tmpptr = *(int*)(&cd);
Print((FUNC_PTR*)tmpptr);
Print((FUNC_PTR*)*(int*)(&cd));//一步到位
printf(" ??? :%p", tmpptr);//tmpptr就算虚函数表的地址
}
下图为执行结果:
这里虚函数表和静态区的str离的最近,虚函数和代码段的funC()离的最近,数据的存储不会在每个区中距离太远
这里就说明了虚函数表存储在静态区,虚函数存储在代码段
虚函数表是在常量区,和我们之前的const char* 存储的“abc”这种信息是类似的,也就是说,程序一旦运行,虚函数表就唯一确定,那么我们在创建有虚函数表的对象时,不管创建多少,虚函数表都是同一张
void test11()
{
BigCount cd1;
BigCount cd2;
BigCount cd3;
int tmpptr1 = *(int*)(&cd1);
int tmpptr2 = *(int*)(&cd2);
int tmpptr3 = *(int*)(&cd3);
//这三个地址都相同
printf("%p\n", tmpptr1);
printf("%p\n", tmpptr2);
printf("%p\n", tmpptr3);
}
最后我画了下图来解释说明对象bc中关于虚函数表的整体分布
图中解释:方框内的内容是存储的内容,不是该位置的地址
by the way,我们通过函数指针调用的函数,就不需要管是public限定,还是protected限定,还是private限定了,因为共有私有是语法层面的限制,而函数指针调用是原理层的调用,所以设想一个场景,父类中有个虚函数是放在private中,子类是无法进行访问的,但是虚函数表中是由这个虚函数地址的,所以我们是可以通过访问虚表中元素(虚函数)的方式去调用该private中的虚函数,下面是测试:
用Car中的fun4进行测试
实例分析
class A
{
public:
virtual void func(int val = 1)
{
cout << "A->" << val << endl;
}
virtual void test()
{
func();
}
};
class B : public A
{
public:
void func(int val = 0)
{
cout << "B->" << val << endl;
}
};
void test5()
{
B b;
b.test();//B->1
}
为了解答b.test()为什么结果是B->1 需要解释两层:
1、为什么调用的子类的func?func()构成了重写,b调用的是继承来的test(),这时就发生了切片,this指针就是父类指针,在test()中调用了func(),实际上就是this->func(),所以满足多态条件,父类指针调用 + 重写,所以是调用的子类的func
2、为什么val是1?回答这个问题可以去回答:为什么子类函数不需要加上virtual就可以和父类构成重写?因为编译器在检查构成重写时,只检查三同,如果不相同,那么不构成重写,而构成重定义/覆盖,如果相同,此时编译器还是使用父类的框架(virtual 函数返回值 参数类型以及参数缺省值),实现就使用的是子类的实现,虚函数的重写,重写的是实现,此时用的父类框架,那么就是父类的val以及父类的缺省值
多态分类问题
多态的分类问题主要是为了清楚多态相关的一些说法,能让自己听懂各种角度来描述多态相关的内容
静态多态和动态多态:
1、静态多态也就是静态绑定,也就是函数重载,是编译时决定程序行为
2、动态多态也就是动态绑定,也就是用函数重写实现多态,运行时决定程序行为
多继承
前面介绍的都是单继承下的多态,后面是多继承下的多态
回顾:根据多态的特性,即父类类型对于不同对象,调用不同的函数,所以子类的虚函数表就是直接用的是父类传下来,并进行重写后的虚函数表,并不会自己再创建一个虚函数表
多继承这里对于多个父类,就会创建多张虚表(一个父类一个虚表)
class Base1
{
public:
virtual void fun1() { cout << "Base1::fun1" << endl; }
virtual void fun2() { cout << "Base1::fun2" << endl; }
};
class Base2
{
public:
virtual void fun1() { cout << "Base2::fun1" << endl; }
virtual void fun2() { cout << "Base2::fun2" << endl; }
};
class Child : public Base1, public Base2
{
public:
virtual void fun1() { cout << "Child::fun1" << endl; }
virtual void fun3() { cout << "Child::fun3" << endl; }
};
void test2()
{
Base1 b1;
Base2 b2;
Child c;
}
这里子类c中的来自Base1有一张虚表__vfptr,来自Base2有一张虚表__vfptr,可能会注意到Base2的__vfptr中的[0]中有[thunk]:Child::fun1’adjustor{4}'{void},这里的adjustor是调整器,是一种机制,后面会提到
同时这里c中__vfptr[0]是Child::fun1,__vfptr[0]也是Child::fun1,所以Child的fun1重写了Base1和Base2的fun1
发问及原理
我们先提出一个问题:Derive中会有两张虚表,第一张是Base1生成的,第二张是Base2生成的,func3() 存在哪里?
我们想要直到func3存在哪里,首先需要拿到两张虚表的地址,然后进行打印验证
第一步:
之前我们的虚函数表指针都是放在类对象的开头,但是这里是两张虚表,所以必定有一张虚表放在开头,另一张虚表紧邻其后,我们可以通过上面Print(FUNC_PTR* table)函数的方式打印虚函数表指针在对象开头的虚表中的虚函数,我们会发现就是Base1的虚表
(其实顺序是和我们 : public Base1, Base2这里的继承顺序相同,先继承的Base1,那么Base1的虚表就在开头;先继承Base2,那么Base2的虚表就在开头,其实这很合理,我们先继承Base1,那么会先执行Base1的构造函数,也就会把Base1的虚表拷贝下来,拷贝下来就立刻放到首地址处了,再继承Base2的此时就只能把虚表放到Base1的虚表后面了)
这里Base2的地址可以先拿到Base1的地址,然后将其转换成(char*),并加上sizeof(Base1),这里能这么做是因为Base1中没有其他的变量,只有虚表,此时就能够定位到Base2虚函数表指针的首地址,然后将其强制类型转换成int*,对其解引用,就可以用int的方式拿到Base2的虚函数表
第二步:
后序操作和前文相同
(这里第一步还可以用切片的思想:创建一个Base2*类型的指针,然后把Child对象的地址给到它,此时会发生切片,Base2* 类型的首地址就是第二张虚函数表的地址,后序操作相同)
typedef void (*FUNC_PTR)();
void Print(FUNC_PTR* table)
{
for (int i = 0; table[i] != nullptr; i++)
{
printf("[%d]:%p\n", i, table[i]);
table[i]();
}
cout << endl;
}
class Base1
{
public:
virtual void fun1() { cout << "Base1::fun1" << endl; }
virtual void fun2() { cout << "Base1::fun2" << endl; }
};
class Base2
{
public:
virtual void fun1() { cout << "Base2::fun1" << endl; }
virtual void fun2() { cout << "Base2::fun2" << endl; }
};
class Child : public Base1, public Base2
{
public:
virtual void fun1() { cout << "Child::fun1" << endl; }
virtual void fun3() { cout << "Child::fun3" << endl; }
};
void test3()
{
Child c;
int ptr1 = *((int*)&c);
//方法一:
int ptr2 = *((int*)((char*)&c + sizeof(Base1)));
//(char*)&c
//(char*)&c + sizeof(Base1)
//(int*)((char*)&c + sizeof(Base1))
//*((int*)((char*)&c + sizeof(Base1)))
//方法二:
//Base2* tmp = &c;
//int ptr2 = *((int*)tmp);
Print((FUNC_PTR*)ptr1);
Print((FUNC_PTR*)ptr2);
}
从结果说明,Child的fun3是放在Base1中的
注意到第一次调用和第二次调用fun1的时候的函数指针不同
按道理,虚函数是存储在静态区的,应该调用同一个虚函数指针,访问同一份虚函数,这里需要通过汇编去理解
我们执行下面这段代码,去查看汇编进行理解
void test4()
{
Child c;
Base1* ptr1 = &c;
Base2* ptr2 = &c;
ptr1->fun1();
ptr2->fun1();
}
在vs中调试代码时,在调试 - 窗口中有一个反汇编,用于查看汇编指令
根据切片的性质,这段代码中ptr1和ptr2的虚表还是c的虚表,访问的fun1是Child类的fun1
ptr1:对于ptr1调用fun1,在汇编中是通过call jump直接访问到fun1
ptr2:ptr2是通过call jump jump sub然后调用fun1,这两次访问fun1,是使用同一个函数地址,ptr2汇编中有一个关键点,寄存器存储了一个差值,这个差值的大小与我们前面sizeof(Base1)的大小相同,编译器可以用这个差值,通过sub操作,然后去访问到fun1
简单来说,ptr1直接访问fun1,ptr2通过sub然后访问fun1,是通过同一个虚函数指针访问的fun1
关于ptr1能直接call eax 到 009F1195,进而jump到fun1中,ptr2为什么不能直接call eax到009F1195的解释
ptr1调用func1()时this指针和函数指针重合(this需要指向对象起始),所以可以直接call eax 009F1195,但是ptr2的this指针是指向的第二个虚函数表指针的位置,需要修正到c对象的首地址所以这里sub ecx, 4(前面在监视窗口中观察到的adjustor{4}也就是反应的这里)然后就能够去call eax 到 009F1195,进而jump到fun1
所以多继承还是遵守了静态区只会生成一个函数地址,只是ptr2 - 第二个父类包装了一层
像这里多继承情况的多态底层问题的答案在不同的平台都可能不同,解题思路就是各种的测试,调试看汇编
抽象类
抽象类是包含了纯虚函数的类,抽象类无法实例化对象,这是为了处理现实世界中没有对应的实体的情况,比如我们创建了一个animal类,我们在现实生活中并没有animal类这对象,animal是我们把cat、dog、fish这些所有的实体类集合在一起给定的一个抽象的名称:animal,所以比如我们给定动物都有的一个行为(函数)—— 前进,对于cat类、dog类,就是四条腿交替向前…,对于fish类,那么就是摆动…,我们并不会创建一个animal类对象,而是仅仅会使用其子类。
纯虚函数
纯虚函数是在虚函数后加一个 = 0;
//世界上是把奥迪、大众、林肯、牧马人...这些类抽象成了Car类
class Car
{
public:
virtual void Drive() = 0;
};
纯虚函数只需要声明,不需要定义,不需要定义也好理解:我们是无法对Car类,对animal类的某一行为(函数),做统一的解释(定义),所以解释(定义)的工作全权交给子类完成
当然不要把virtual给忘了——纯虚函数首先得是虚函数,需要有virtual关键字进行修饰
包含纯虚函数的类是抽象类 - 这句话还需要扩充为通过继承来的纯虚函数所在类也是抽象类,也无法实例化对象
class Animal
{
public:
virtual void walk() = 0;
};
class Dog : public Animal
{
};
void test5()
{
Animal anm;//err,父类无论如何都不能实例化
Dog d;//err
}
只有当派生类重写了纯虚函数,派生类才能实例化对象
class Animal
{
public:
virtual void walk() = 0;
};
class Dog : public Animal
{
public:
virtual void walk()
{
cout << "四条腿向前交替..." << endl;
}
};
class Fish : public Animal
{
public:
virtual void walk()
{
cout << "鱼鳍摆动..." << endl;
}
};
void test5()
{
//Animal anm;//err
Dog d;
Fish f;
}
虽然我们不能定义抽象类的对象,但是可以定义抽象类的指针、引用
class Animal
{
public:
virtual void walk() = 0;
};
class Dog : public Animal
{
public:
virtual void walk()
{
cout << "四条腿向前交替..." << endl;
}
};
class Fish : public Animal
{
public:
virtual void walk()
{
cout << "鱼鳍摆动..." << endl;
}
};
void Func(Animal& anm)
{
anm.walk();
}
void func(Animal* anm)
{
anm->walk();
}
void test6()
{
Dog d;
Fish f;
Func(d);//引用
Func(f);
cout << endl;
func(&d);//指针
func(&f);
}
抽象类强制了派生类重写行为
override与纯虚函数的区别?
override是帮助派生类检查是否完成重写,如果没有完成重写会报错,是一种检测机制
纯虚函数是一种思想,是一种接口继承的思想