目录
1.采用第一张虚表的指针加上第一张虚表的大小的形式找到第二张虚表
一、什么是多态
语法
1.重写的概念以及条件
2.多态的概念以及条件
原理
1.虚表是什么?
2.多态的原理是什么?
3.普通调用、编译时决议和多态调用,运行时决议的区别是什么?
多态的概念:通俗来说,就是多种形态,具体点就是去完成某个行为,当不同的对象去完成时会产生出不同的状态。
比方说去买票:普通成年人买普通的票,学生买半价的学生票,军人不需要排队买票
这里的普通人,学生和军人就是三个不同类型的对象。
①正常卖票 ②半价买票 ③优先买票
还有比方说红包
新用户要鼓励使用 所以红包多给一点 rand()%99
老用户并且经常使用 rand()%2
老用户且不经常使用 rand()%66
如何用代码来实现多态?
多态的实现条件是虚函数!(这个virtual是虚函数,不是上一节的虚拟继承!!)
只用成员变量才能添加virtual,变成虚函数。
在继承的时候,三同(函数名、参数、返回值)都相同的时候,就构成了虚函数重写(覆盖条件)
不符合重写,就是隐藏关系
多态的两个条件
多态的两个条件
1、虚函数的重写
2、父类指针或者引用去调用虚函数
#include <iostream>
using namespace std;
class Person {
public:
//只有成员函数才能添加virtual,变成虚函数
virtual void BuyTicket() { cout << "买票-全价" << endl;}
};
class Student : public Person {
public:
//这里的重名的BuyTicket就是虚函数重写/覆盖(函数名,参数和返回值都要求相同)
virtual void BuyTicket() { cout << "买票-半价" << endl; }
/*注意:在重写基类虚函数时,派生类的虚函数在不加virtual关键字时,虽然也可以构成重写(因
为继承后基类的虚函数被继承下来了在派生类依旧保持虚函数属性),但是该种写法不是很规范,不建议
这样使用*/
/*void BuyTicket() { cout << "买票-半价" << endl; }*/
};
class Soldier : public Person {
public:
virtual void BuyTicket() { cout << "优先-买票" << endl; }
};
void Func(Person &P)
{
P.BuyTicket();
}
int main()
{
Person ps;
Student st;
Soldier sd;
Func(ps);
Func(st);
Func(sd);
return 0;
}
这里我们发现只要传入的指针或引用指向的对象不同,调用的函数和方法也不同!!
1、不是父类的指针或者引用
如果我们改成这样,也就是没有使用父类指针或者引用去调用函数,我们就发现我们的多态效果就失效了。
void Func(Person P) { P.BuyTicket(); }
2.不符合重写 --(不是虚函数)
我们将父类的virtual,让其不是虚函数,破坏了我们上面的条件,多态就失效了
class Person { public: //只有成员函数才能添加virtual,变成虚函数 void BuyTicket() { cout << "买票-全价" << endl;} };
特例:子类的方法的virtual去掉了还是虚函数,因为默认是将父类的虚函数继承下来了。所以子类的函数不加virtual依旧构成重写。(实际中最好还是加上)
class Student : public Person { public: void BuyTicket() { cout << "买票-半价" << endl; } };
2.不符合重写--参数不同(三同中去掉一个)
#include <iostream> using namespace std; class Person { public: //只有成员函数才能添加virtual,变成虚函数 virtual void BuyTicket(int) { cout << "买票-全价" << endl;} }; class Student : public Person { public: //这里的重名的BuyTicket就是虚函数重写/覆盖(函数名,参数和返回值都要求相同) virtual void BuyTicket(char) { cout << "买票-半价" << endl; } }; class Soldier : public Person { public: virtual void BuyTicket(long) { cout << "优先-买票" << endl; } }; void Func(Person &P) { P.BuyTicket(1); } int main() { Person ps; Student st; Soldier sd; Func(ps); Func(st); Func(sd); return 0; }
特例1:重写的协变
特例:重写的协变。返回值可以不同,要求必须是父子关系的指针或者引用,这样也是多态,是可以的!!!!。
#include <iostream> using namespace std; class Person { public: //只有成员函数才能添加virtual,变成虚函数 virtual Person* BuyTicket() { cout << "买票-全价" << endl; return this; } }; class Student : public Person { public: //这里的重名的BuyTicket就是虚函数重写/覆盖(函数名,参数和返回值都要求相同) virtual Student* BuyTicket() { cout << "买票-半价" << endl; return this; } }; void Func(Person &P) { P.BuyTicket(); } int main() { Person ps; Student st; Func(ps); Func(st); return 0; }
特例2:不是当前类的父子指针
只要是父子指针或者引用就可以,不一定是要当前类的父类和子类!,比方说下面我们的返回值就是A和B的指针也是可以构成多态的!
#include <iostream> using namespace std; class A { public: //只有成员函数才能添加virtual,变成虚函数 virtual A* BuyTicket() { cout << "买票-全价" << endl; return this; } }; class B : public A { public: //这里的重名的BuyTicket就是虚函数重写/覆盖(函数名,参数和返回值都要求相同) virtual B* BuyTicket() { cout << "买票-半价" << endl; return this; } /*注意:在重写基类虚函数时,派生类的虚函数在不加virtual关键字时,虽然也可以构成重写(因 为继承后基类的虚函数被继承下来了在派生类依旧保持虚函数属性),但是该种写法不是很规范,不建议 这样使用*/ /*void BuyTicket() { cout << "买票-半价" << endl; }*/ }; class Person { public: //只有成员函数才能添加virtual,变成虚函数 virtual A* BuyTicket() { cout << "买票-全价" << endl; A a; return &a; } }; class Student : public Person { public: //这里的重名的BuyTicket就是虚函数重写/覆盖(函数名,参数和返回值都要求相同) virtual B* BuyTicket() { cout << "买票-半价" << endl; B b; return &b; } }; void Func(Person &P) { P.BuyTicket(); } int main() { Person ps; Student st; Func(ps); Func(st); return 0; }
如果不是父子关系的指针或者引用就会报错
同时子类和父类的指针不能颠倒!!
#include <iostream> using namespace std; class Person { public: //只有成员函数才能添加virtual,变成虚函数 virtual Person BuyTicket() { cout << "买票-全价" << endl; return *this; } }; class Student : public Person { public: //这里的重名的BuyTicket就是虚函数重写/覆盖(函数名,参数和返回值都要求相同) virtual Student BuyTicket() { cout << "买票-半价" << endl; return *this; } }; void Func(Person &P) { P.BuyTicket(); } int main() { Person ps; Student st; Func(ps); Func(st); return 0; }
练习
以下程序输出结果是什么()
#include <iostream>
using namespace std;
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;
}
A: A->0
B: B->1
C: A->1
D: B->0
E: 编译出错
F: 以上都不正确
p->test()子类的指针调用test(),多态的条件没有一个是满足的,所以将虚函数当初普通函数处理(虚函数和普通函数也没啥区别,就是虚函数能完成多态的处理)
调用test,需要传参,test()(这里我们的test是无参的,所以就是将自身的this指针传进去,也就是一个A*this贺子珍)需要将A*this传递进去,调用func();是this指针调用func()(this->func());符合多态调用(父子指针)。
这个this指向谁就调用谁。而A*的后面这个this是指向子类的,因为我们的p就是指向子类的,所以调用子类的func函数。
但是这道题的子类B中虽然并没有加virtual,但是按照我们上面的说法,这个子类依旧构成重写,是我们上面讲过的一个特例,所以调用的依旧是我们B中的func(),也就是B->
然而虚函数重写是接口继承,重写实现
普通函数继承是实现继承。
这里的父类就像是一个接口,就是我们子类将父类的接口给拿过来。
缺省参数一不一样是不会影响我们的继承的,但是这里的传入的类型不写是不行的。
所以我们的接口是这一段,所以默认的缺省参数也就是1!!!
(这里的p传给A*this的时候会发生切片,将B中A的部分切割出来)
B
(不推荐写这样的代码!!!!)
正常的话应该改成这样,这样的结果还是B->1
#include <iostream>
using namespace std;
class A
{
public:
virtual void func(int val){ std::cout<<"A->"<< val <<std::endl;}
virtual void test(){ func(1);}
};
class B : public A
{
public:
void func(int val){ std::cout<<"B->"<< val <<std::endl; }
};
int main(int argc ,char* argv[])
{
B*p = new B;
p->test();
return 0;
}
那如果我们将指针p改成A类型的指针,结果会是什么?
#include <iostream>
using namespace std;
class A
{
public:
virtual void func(int val){ std::cout<<"A->"<< val <<std::endl;}
void test(){ func(1);}
};
class B : public A
{
public:
void func(int val){ std::cout<<"B->"<< val <<std::endl; }
};
int main(int argc ,char* argv[])
{
A*p = new B;
p->test();
return 0;
}
test()处的this指针还是A* this,这里的this指针也就是p,变成了父类的指针,也就是this->func(1)
多态的两个条件,
①父类指针或者引用调用虚函数(这里的this指针是父类的指针)
②func是虚函数的重写,这里就是多态调用,跟this的类型根本没有关系)指向谁调用谁,这个父类的指针p所指向的对象是子类!!!
所以我们看的是指针所指向的对象是不是子类和父类的关系,并不是指针的类型是父类的指针还是子类的指针!!!
这样写调用的才是父类
#include <iostream>
using namespace std;
class A
{
public:
virtual void func(int val){ std::cout<<"A->"<< val <<std::endl;}
void test(){ func(1);}
};
class B : public A
{
public:
void func(int val){ std::cout<<"B->"<< val <<std::endl; }
};
int main(int argc ,char* argv[])
{
A*p = new A;
p->test();
return 0;
}
二、多态的原理
#include <iostream>
using namespace std;
class Base
{
public:
virtual void Func1()
{
cout<<"Func1()"<<endl;
}
private:
int _b=1;
char _ch='A';
};
int main()
{
cout<<sizeof (Base)<<endl;
}
这里的Base中会多存一个指针,称为虚表指针!虚函数是会进入虚表的。
虚函数是会把它的地址放入虚表的 virtual function table(虚函数表)
所以我们这里int为四个字节,char为一个字节,这样就变成了我们的第一个8字节对齐,然后再加上一个虚表指针,就变成了16字节。
对象里面没有虚表,有的是虚表的指针
虚表的本质上是一个数组,是一个函数指针的数组。
class Person {
public:
virtual void BuyTicket() { cout << "买票-全价" << endl; }
};
class Student : public Person {
public:
virtual void BuyTicket() { cout << "买票-半价" << endl; }
};
void Func(Person& p)
{
p.BuyTicket();
}
int main()
{
Person Mike;
Func(Mike);
Student Johnson;
Func(Johnson);
return 0;
}
1. 观察下图的红色箭头我们看到,p是指向mike对象时,p->BuyTicket在mike的虚表中找到虚函数是Person::BuyTicket。
2. 观察下图的蓝色箭头我们看到,p是指向johnson对象时,p->BuyTicket在johson的虚表中找到虚函数是Student::BuyTicket。
3. 这样就实现出了不同对象去完成同一行为时,展现出不同的形态。
4. 反过来思考我们要达到多态,有两个条件,一个是虚函数覆盖,一个是对象的指针或引用调用虚函数。
5. 再通过下面的汇编代码分析,看出满足多态以后的函数调用,不是在编译时确定的,是运行起来以后到对象的中取找的。不满足多态的函数调用时编译时确认好的。
多态调用
p->BuyTicket(); // p中存的是mike对象的指针,将p移动到eax中 001940DE mov eax,dword ptr [p] // [eax]就是取eax值指向的内容,这里相当于把mike对象头4个字节(虚表指针)移动到了edx 001940E1 mov edx,dword ptr [eax] // [edx]就是取edx值指向的内容,这里相当于把虚表中的头4字节存的虚函数指针移动到了eax 00B823EE mov eax,dword ptr [edx] // call eax中存虚函数的指针。这里可以看出满足多态的调用, // 不是在编译时确定的,是运行起来以后到对象的中取找的。 001940EA call eax 00头1940EC cmp esi,esp
普通调用
// 首先BuyTicket虽然是虚函数,但是mike是对象,不满足多态的条件, // 所以这里是普通函数的调用转换成地址时, // 是在编译时已经从符号表确认了函数的地址,直接call 地址 mike.BuyTicket(); 00195182 lea ecx,[mike] 00195185 call Person::BuyTicket (01914F6h)
总结:多态的本质原理,符合多态的两个条件
那么调用时,会到指向对象的虚表中找到对应的虚函数地址,进行调用。
多态调用,程序运行时去指向对象的虚表中找到函数的地址,进行调用。
普通函数的调用,编译链接时确定函数的地址,运行时直接调用。
虚函数被编译完了,不是存储在虚表当中的。
函数编译好了都是指令,都是放在公共的代码段当中的。
虚表里面存放的是函数的地址,所以虚函数编译好了还是放在代码段当中的,我们的虚表中所存放的仅仅是虚函数的地址
#include <iostream>
using namespace std;
class Person {
public:
virtual void BuyTicket() { cout << "买票-全价" << endl; }
protected:
int _age = 1;
};
class Student : public Person {
public:
virtual void BuyTicket() { cout << "买票-半价" << endl; }
protected:
int _id;
};
// 多态的条件:
// 1、虚函数的重写
// 2、父类指针或者引用去调用
void Func1(Person& p)
{
p.BuyTicket();
}
void Func2(Person p)
{
p.BuyTicket();
}
int main()
{
Person ps;
Student st;
Func1(ps);
Func1(st);
Func2(ps);
Func2(st);
return 0;
}
我们观察到父类对象和子类对象并不是指向同一张虚表的,因为这个虚表被重写了,也就是被覆盖成了子类的虚函数。
(虚表(虚函数表)的本质就是一个函数指针的数组!指针数组里面存储的全部都是虚函数的指针。)
然后我们观察到我们上面的两个func函数不同,然后相同的代码传进去只有第一个func呈现出了多态的效果
一旦构成了多态,编译器就会从我们的虚函数表中找到对应的虚函数。从而我们就做到了指向谁调用谁。
(这里的引用如果指向父类,就调用父类的方法,如果指向子类,就调用子类的方法。)
构成多态的调用:运行时到指向对象的虚表中找到对应的要调用的虚函数的地址,所以p指向谁调用谁。
不构成多态的调用:编译时确定调用函数的地址。是由你本身的类型决定的,并不是由你的指向的对象决定的。
1)将父类的virtual去掉
#include <iostream>
using namespace std;
class Person {
public:
void BuyTicket() { cout << "买票-全价" << endl; }
protected:
int _age = 1;
};
class Student : public Person {
public:
virtual void BuyTicket() { cout << "买票-半价" << endl; }
protected:
int _id;
};
// 多态的条件:
// 1、虚函数的重写
// 2、父类指针或者引用去调用
void Func1(Person& p)
{
p.BuyTicket();
}
void Func2(Person p)
{
p.BuyTicket();
}
int main()
{
Person ps;
Student st;
Func1(ps);
Func1(st);
Func2(ps);
Func2(st);
return 0;
}
我们从下面的测试代码的结果中,我们可以看到迫害了父类的virtual,就不构成虚函数重载了。
这个时候的对象中根本就没有虚表,所以是编译时决议。
2)父类有虚函数,子类没有重写
#include <iostream>
using namespace std;
class Person {
public:
virtual void BuyTicket() { cout << "买票-全价" << endl; }
protected:
int _age = 1;
};
class Student : public Person {
public:
// virtual void BuyTicket() { cout << "买票-半价" << endl; }
protected:
int _id;
};
// 多态的条件:
// 1、虚函数的重写
// 2、父类指针或者引用去调用
void Func1(Person& p)
{
p.BuyTicket();
}
void Func2(Person p)
{
p.BuyTicket();
}
int main()
{
Person ps;
Student st;
Func1(ps);
Func1(st);
Func2(ps);
Func2(st);
return 0;
}
从测试结果中,我们看到如果子类中根本没有重载父类的虚函数,那么也不构成多态。
这时父类的虚表和子类的虚表里面都是父类的那个虚函数,因为虽然你没有覆盖,但这里还是按照多态的运行机制去运行的
(也就是说如果你创建了虚函数,但并没有重写,去构成多态,那么不但浪费空间,并且运行还会变慢,因为还要从虚函数表中查找虚函数的地址)
虚表中的存储内容的探索
同一个类型的对象共用同一张虚表
class Person
{
public:
virtual void BuyTicket()
{
cout << "Person::买票-全价" << endl;
}
};
int main()
{
Person p1;
Person p2;
}
父类和子类的虚表是不同的,其子类中的内容跟父类是不一样的。父类的虚表里面存了父类的虚函数,子类的虚表里面存储的是子类的虚函数!
class Person
{
public:
virtual void BuyTicket()
{
cout << "Person::买票-全价" << endl;
}
};
class Student : public Person {
public:
virtual void BuyTicket()
{
cout << "Student::买票-半价" << endl;
}
};
int main()
{
Person p1;
Person p2;
Student s1;
Student s2;
}
那如果我没有完成重写,比方说我将上面的子类中的虚函数给注释掉,那么子类中用的也是父类当中的虚函数。
class Person
{
public:
virtual void BuyTicket()
{
cout << "Person::买票-全价" << endl;
}
};
class Student : public Person {
public:
};
int main()
{
Person p1;
Person p2;
Student s1;
Student s2;
}
父类的当中的虚表还是Person的虚表,子类的虚表还是Student的虚表,虽然其中的内容都是一样的。
所以说,不管是否完成重写,子类虚表跟父类虚表都不是同一个
这里专指在VS的情况下的测试结果。
如果我的子类中还有没有被重写的虚函数呢?
只要是虚函数,虚函数的地址都会被放入虚表当中。
class Person
{
public:
virtual void BuyTicket()
{
cout << "Person::买票-全价" << endl;
}
virtual void Func1()
{
cout << "Person::Func1()" << endl;
}
};
class Student : public Person {
public:
virtual void BuyTicket()
{
cout << "Student::买票-半价" << endl;
}
virtual void Func2()
{
cout << "Student::Func2()" << endl;
}
};
int main()
{
Person p1;
Person p2;
Student s1;
Student s2;
}
我们观察到父类的func1是进了虚表的,那么我们的func2是否进入了虚表呢?
要的!但是我们的监视窗口中被隐藏掉了,看不到了。
那我现在就想看到student中有没有func2怎么办呢!
(在内存窗口中可以看到)
打印虚函数表
虚函数表是一个函数指针数组,我们只需要打印这个数组当中的内容就可以了
打印子类的虚函数表
class Person
{
public:
virtual void BuyTicket()
{
cout << "Person::买票-全价" << endl;
}
virtual void Func1()
{
cout << "Person::Func1()" << endl;
}
};
class Student : public Person {
public:
virtual void BuyTicket()
{
cout << "Student::买票-半价" << endl;
}
virtual void Func2()
{
cout << "Student::Func2()" << endl;
}
};
//这里的typedef之后,我们这里的VFPTR就变成了一个指向没有返回值没有参数的函数的指针
typedef void(*VFPTR)();
//下面两种的写法是等价的
//void PrintVFTable(VFPTR* table)
void PrintVFTable(VFPTR table[])
{
//在VS下的每一张虚表的最后都会加一个空指针
//g++下面只能是知道有几个虚函数,然后直接写死虚函数的个数
for (size_t i = 0; table[i] != nullptr; ++i)
//for (size_t i = 0; i < n; ++i)
{
printf("vft[%d]:%p->", i, table[i]);
// table[i]();
//上面的这一行被注释掉的代码和我们下面的两行代码的功能都是相同的
//只要是函数指针加上()就是能调用对应的函数的!
VFPTR pf = table[I];
//无论这个函数有没有参数或者返回值都一样,因为它被强制类型转换了
pf();
//这里我们并不是正常地去访问,并不受访问限定符的限制
}
cout << endl;
}
int main()
{
// 同一个类型的对象共用一个虚表
Person p1;
Person p2;
// vs下 不管是否完成重写,子类虚表跟父类虚表都不是同一个
Student s1;
Student s2;
//虚表的地址在对象的头四个字节或者头部的八个字节
//取出s1的地址,然后将这个指针强制转换成(int*)类型,就能够得到头部部的4个字节了。
//如果是64位平台,也就是说想要取出对象头部的八个字节,我们这里就强制类型转换成long long*,就能够看到头部的8个字节了!
//32位的平台使用下面的这个代码
// PrintVFTable((VFPTR*)*(int*)&s1);
//64位平台使用下面这个代码
PrintVFTable((VFPTR*)*(long long*)&s1);
return 0;
}
所以从我们上面的打印结果中是可以看到这个fun2()的!因为这个函数都已经被调用了!
第一个位置是重写的student的虚函数
第二个位置是没有被重写的父类的虚函数
第三个位置是子类的没有被重写的虚函数
所以虚函数都要进入虚表!
打印父类的虚表
父类这里的虚表也是在对象的头部的四个字节或者是八个字节的位置。
class Person
{
public:
virtual void BuyTicket()
{
cout << "Person::买票-全价" << endl;
}
virtual void Func1()
{
cout << "Person::Func1()" << endl;
}
};
class Student : public Person {
public:
virtual void BuyTicket()
{
cout << "Student::买票-半价" << endl;
}
virtual void Func2()
{
cout << "Student::Func2()" << endl;
}
};
//这里的typedef之后,我们这里的VFPTR就变成了一个指向没有返回值没有参数的函数的指针
typedef void(*VFPTR)();
//下面两种的写法是等价的
//void PrintVFTable(VFPTR* table)
void PrintVFTable(VFPTR table[])
{
//在VS下的每一张虚表的最后都会加一个空指针
//g++下面只能是知道有几个虚函数,然后直接写死虚函数的个数
for (size_t i = 0; table[i] != nullptr; ++i)
//for (size_t i = 0; i < n; ++i)
{
printf("vft[%d]:%p->", i, table[i]);
// table[i]();
//上面的这一行被注释掉的代码和我们下面的两行代码的功能都是相同的
//只要是函数指针加上()就是能调用对应的函数的!
VFPTR pf = table[I];
//无论这个函数有没有参数或者返回值都一样,因为它被强制类型转换了
pf();
//这里我们并不是正常地去访问,并不受访问限定符的限制
}
cout << endl;
}
int main()
{
// 同一个类型的对象共用一个虚表
Person p1;
Person p2;
// vs下 不管是否完成重写,子类虚表跟父类虚表都不是同一个
Student s1;
Student s2;
//虚表的地址在对象的头四个字节或者头部的八个字节
//指针之间是可以相互转换的
//取出s1的地址,然后将这个指针强制转换成(int*)类型,就能够得到头部部的4个字节了。
//如果是64位平台,也就是说想要取出对象头部的八个字节,我们这里就强制类型转换成long long*,就能够看到头部的8个字节了!
//我们这里是取出对象头四个或八个字节的值,这个值就是我们的虚表的地址,然后我们将这个虚标的地址传给我们上面打印虚表的函数。
//(并不是将这个对象的头四个或者八个字节的地址取出来!我们想要的是指!,需要解引用的!)
// PrintVFTable((VFPTR*)*(long long*)&s1);
PrintVFTable((VFPTR*)*(long long*)&p1);
return 0;
}
多态去调用的时候,和我们这里的方式是相似的,不过是通过汇编的写法
因为我们上面的vs是会在虚表的最后面放置一个空值来代表虚标的结束的,但是我们linux下的g++是没有这样的设置的,所以我们需要将我们想要打印虚表几个参数的个数作为参数传递给我们的虚表打印函数
class Person
{
public:
virtual void BuyTicket()
{
cout << "Person::买票-全价" << endl;
}
virtual void Func1()
{
cout << "Person::Func1()" << endl;
}
};
class Student : public Person {
public:
virtual void BuyTicket()
{
cout << "Student::买票-半价" << endl;
}
virtual void Func2()
{
cout << "Student::Func2()" << endl;
}
};
//这里的typedef之后,我们这里的VFPTR就变成了一个指向没有返回值没有参数的函数的指针
typedef void(*VFPTR)();
void PrintVFTable(VFPTR* table, size_t n)
{
//在VS下的每一张虚表的最后都会加一个空指针
//g++下面只能是知道有几个虚函数,然后直接写死虚函数的个数
for (size_t i = 0; table[i] != nullptr; ++i)
//for (size_t i = 0; i < n; ++i)
{
printf("vft[%d]:%p->", i, table[i]);
// table[i]();
//上面的这一行被注释掉的代码和我们下面的两行代码的功能都是相同的
//只要是函数指针加上()就是能调用对应的函数的!
VFPTR pf = table[i];
//无论这个函数有没有参数或者返回值都一样,因为它被强制类型转换了
//这里是成员函数的调用
pf();
//这里我们并不是正常地去访问,并不受访问限定符的限制
}
cout << endl;
}
int main()
{
// 同一个类型的对象共用一个虚表
Person p1;
Person p2;
// vs下 不管是否完成重写,子类虚表跟父类虚表都不是同一个
Student s1;
Student s2;
//虚表的地址在对象的头四个字节或者头部的八个字节
//取出s1的地址,然后将这个指针强制转换成(int*)类型,就能够得到头部部的4个字节了。
//如果是64位平台,也就是说想要取出对象头部的八个字节,我们这里就强制类型转换成long long*,就能够看到头部的8个字节了!
PrintVFTable((VFPTR*)*(long long*)&s1,3);
PrintVFTable((VFPTR*)*(long long*)&p1,2);
return 0;
}
子类的虚表
父类的虚表
多继承中的虚表
Derive继承了Base1,继承了Base2,我们将func1重写了,func2并没有重写,并且还有一个独立的func3
我们的Derive的大小为多少
class Base1 {
public:
virtual void func1() { cout << "Base1::func1" << endl; }
virtual void func2() { cout << "Base1::func2" << endl; }
private:
int b1 = 1;
};
class Base2 {
public:
virtual void func1() { cout << "Base2::func1" << endl; }
virtual void func2() { cout << "Base2::func2" << endl; }
private:
int b2 = 2;
};
class Derive : public Base1, public Base2 {
public:
virtual void func1() { cout << "Derive::func1" << endl; }
virtual void func3() { cout << "Derive::func3" << endl; }
private:
int d = 3;
};
int main()
{
Derive d;
cout << sizeof(d) << endl;
return 0;
}
上面的代码在32位系统下的结果为20,在64位系统下的结果为40.
我们的多继承中的Derive的大小由三个部分构成
base1的部分:Base1是8,因为有一个int类型的b1(4个字节),还有一个虚表指针(4字节32位系统下)
base2的部分:Base2是8,因为有一个int类型的b2(4个字节),还有一个虚表指针(4字节32位系统下)
base1和base2的内存对齐按照其最大对齐数进行对齐,都是8
Derive自己的部分:是derive自己的int类型的4个字节
所以在32位的系统下,我们Derive的大小为8+8+4=20
在64位的系统下,我们一个指针的大小为8字节,然后我们的最大对齐数是8,所以我们在64位的平台下的存储方式为:
第一个base1的int类型占据了第一个8字节,然后它的虚表指针占据了第二个八字节,
第二个base2的int类型占据了第三个8字节,然后它的虚表指针占据了第四个八字节
最后一个然后derive本身还有一个int类型占据了四字节,然后由于内存对齐,所以第五个八字节中的位置全部都归derive
一共是5个8字节,所以是40字节的大小。
Base1和Base2中的func1都是需要被重写的!
这个我们Derive中独有的func3是放进Base1虚表还是Base2的虚表?
运行下面的测试代码,将我们的第一张的虚表打印出来
//这里的typedef之后,我们这里的VFPTR就变成了一个指向没有返回值没有参数的函数的指针
typedef void(*VFPTR)();
void PrintVFTable(VFPTR* table)
{
//在VS下的每一张虚表的最后都会加一个空指针
//g++下面只能是知道有几个虚函数,然后直接写死虚函数的个数
for (size_t i = 0; table[i] != nullptr; ++i)
//for (size_t i = 0; i < n; ++i)
{
printf("vft[%d]:%p->", i, table[i]);
// table[i]();
//上面的这一行被注释掉的代码和我们下面的两行代码的功能都是相同的
//只要是函数指针加上()就是能调用对应的函数的!
VFPTR pf = table[i];
//无论这个函数有没有参数或者返回值都一样,因为它被强制类型转换了
pf();
//这里我们并不是正常地去访问,并不受访问限定符的限制
}
cout << endl;
}
class Base1 {
public:
virtual void func1() { cout << "Base1::func1" << endl; }
virtual void func2() { cout << "Base1::func2" << endl; }
private:
int b1 = 1;
};
class Base2 {
public:
virtual void func1() { cout << "Base2::func1" << endl; }
virtual void func2() { cout << "Base2::func2" << endl; }
private:
int b2 = 2;
};
class Derive : public Base1, public Base2 {
public:
virtual void func1() { cout << "Derive::func1" << endl; }
virtual void func3() { cout << "Derive::func3" << endl; }
private:
int d = 3;
};
int main()
{
Derive d;
// 32位系统下使用下面的代码
// PrintVFTable((VFPTR*)(*(int*)&d));
// 64位系统使用下面的代码
PrintVFTable((VFPTR*)(*(long long*)&d));
return 0;
}
取出第二张虚表
1.采用第一张虚表的指针加上第一张虚表的大小的形式找到第二张虚表
//这里的typedef之后,我们这里的VFPTR就变成了一个指向没有返回值没有参数的函数的指针
typedef void(*VFPTR)();
void PrintVFTable(VFPTR* table)
{
//在VS下的每一张虚表的最后都会加一个空指针
//g++下面只能是知道有几个虚函数,然后直接写死虚函数的个数
for (size_t i = 0; table[i] != nullptr; ++i)
//for (size_t i = 0; i < n; ++i)
{
printf("vft[%d]:%p->", i, table[i]);
// table[i]();
//上面的这一行被注释掉的代码和我们下面的两行代码的功能都是相同的
//只要是函数指针加上()就是能调用对应的函数的!
VFPTR pf = table[i];
//无论这个函数有没有参数或者返回值都一样,因为它被强制类型转换了
pf();
//这里我们并不是正常地去访问,并不受访问限定符的限制
}
cout << endl;
}
class Base1 {
public:
virtual void func1() { cout << "Base1::func1" << endl; }
virtual void func2() { cout << "Base1::func2" << endl; }
private:
int b1 = 1;
};
class Base2 {
public:
virtual void func1() { cout << "Base2::func1" << endl; }
virtual void func2() { cout << "Base2::func2" << endl; }
private:
int b2 = 2;
};
class Derive : public Base1, public Base2 {
public:
virtual void func1() { cout << "Derive::func1" << endl; }
virtual void func3() { cout << "Derive::func3" << endl; }
private:
int d = 3;
};
int main()
{
Derive d;
// cout << sizeof(d) << endl;
//
// 32位系统下使用下面的代码
// PrintVFTable((VFPTR*)(*(int*)&d));
// 64位系统使用下面的代码
// PrintVFTable((VFPTR*)(*(long long*)&d));
//第二个虚表在中间,如何打印第二章虚表?
//声明的时候,谁先继承,谁在前面
//转换成char*的意义就在于转换成char*之后+1就是加一个char的长度,也就是1个字节,所以我们想要跳过base1的大小1,就需要先将我们的d转换为char*
// 32位系统下使用下面的代码
// PrintVFTable((VFPTR*)(*(int*)((char*)&d+sizeof(Base1))));
// 64位操作系统使用下面的代码
PrintVFTable((VFPTR*)(*(long long*)((char*)&d+sizeof(Base1))));
return 0;
}
2.采用切片的形式找到我们的第二张虚表
对于上面的第一种方法,我们的第二种方法,仅仅是我们的main函数中发生了如下的改写
int main()
{
Derive d;
Base2* ptr2 = &d;
//切片的时候是会发生自动偏移的
//将一个子类对象的指针赋给父类的时候,父类会自动偏移到子类对象中,子类继承当前父类部分的位置
//32位系统使用下面的代码
// PrintVFTable((VFPTR*)(*(int*)(ptr2)));
//64位系统使用下面的代码
PrintVFTable((VFPTR*)(*(long long*)(ptr2)));
return 0;
}
我们观察到我们的Derive中独有的func3并不在第二张虚表当中!
为什么我们两张虚表里面的func1不一样?
我重写了Base1和Base2中func1,其应该都是调用我们的Derive中的func1,为什么虚表中的地址是不一样的?!
这里涉及到的核心问题是指针偏移的问题。
即使我们使用下面的代码将func1打印出来,我们的func1和两张虚表中的func1都是不一样的。
class Base1 {
public:
virtual void func1() { cout << "Base1::func1" << endl; }
virtual void func2() { cout << "Base1::func2" << endl; }
private:
int b1 = 1;
};
class Base2 {
public:
virtual void func1() { cout << "Base2::func1" << endl; }
virtual void func2() { cout << "Base2::func2" << endl; }
private:
int b2 = 2;
};
class Derive : public Base1, public Base2 {
public:
virtual void func1() { cout << "Derive::func1" << endl; }
virtual void func3() { cout << "Derive::func3" << endl; }
private:
int d = 3;
};
typedef void(*VFPTR) ();
void PrintVTable(VFPTR vTable[])
{
cout << " 虚表地址>" << vTable << endl;
for (int i = 0; vTable[i] != nullptr; ++i)
{
printf(" 第%d个虚函数地址 :%p,->", i, vTable[i]);
VFPTR f = vTable[i];
f();
}
cout << endl;
}
int main()
{
Derive d;
VFPTR* vTableb1 = (VFPTR*)(*(long long*)&d);
PrintVTable(vTableb1);
VFPTR* vTableb2 = (VFPTR*)(*(long long*)((char*)&d+sizeof(Base1)));
PrintVTable(vTableb2);
cout<<(void*)&Derive::func3<<endl;
printf("%p\n",&Derive::func1);
//编译时决议,直接跳转
d.func1();
Base1* ptr1=&d;
ptr1->func1();
Base2* ptr2=&d;
ptr2->func1();
return 0;
}
这里其实是编译器对我们的函数多封装了几层
我们下面仅仅是修改了main函数中的写法
下面的ptr1和ptr2都是符合多态的要求的。
下面我们的ptr1和ptr2指向的func1() 调用的都是我们对象虚函数表中的func1的地址,
int main()
{
// // 符合多态,去指向对象虚函数表中去找func1的地址调用
Base1* ptr1 = &d;
ptr1->func1();
Base2* ptr2 = &d;
ptr2->func1();
return 0;
}
对于我们这里的ptr1,eax存的是虚表的第一个位置的地址,这个call调用的地址其实是jump指针的地址
我们这里的00b414fb就是我们jump指令的地址
那我们的jump指令再jump一下,就跳转到了我们函数的地址
jump就是跳到我们的函数的位置,在我们的VS下都是这样封装了一层
所以我们的这里两个func1的地址不同,是我们两个函数调用的的jump指令的地址不同导致的!
在VS下面,无论是虚函数,还是普通函数都是这样的。
vs下函数的地址都是我们jump指令的地址
jump之后jump跳转到的地址才是我们真正的地址
第一个Base1的调用就是我们上面所讲的调用方式,但是我们第二个Base2并不是这样的。
编译器对我们的ecx减去了8
也就是说我们的第二个虚函数所跳转的jump并不是我们的函数,中间多做了一个事情,主要是对ecx进行了一些操作。
我们ecx所存的是this指针。
先对ecx-8然后再jump,再jump,等操作。
为什么要对ecx-8?
这个ecx就是要减回到刚才的位置。
因为我们的ptr2指向的是这个位置
ptr1指向我们当前对象的起始这个位置,传给this指针,调用func1,func1是子类的虚函数,子类是要看整个类对象的,才能将这个方法重写的。
ptr1没问题,指向我们子类的对象的开始
但是ptr2并不是指向我们当前的对象的开始,减8是为了减回我们当前对象的起始位置,也就是我们ptr1的位置,也就是回到我们当前位置的起始位置,这样才是正确的调用。
(这里的8是base1的大小,如果base1的大小发生变化,这里可能也就不是8了)
(这两个地址一样反而会报错,但是最终调用的都是相同的函数)
1. 派生类对象d中也有一个虚表指针,d对象由两部分构成,一部分是父类继承下来的成员,虚表指针也就是存在部分的另一部分是自己的成员。
2. 基类b对象和派生类d对象虚表是不一样的,这里我们发现Func1完成了重写,所以d的虚表中存的是重写的Derive::Func1,所以虚函数的重写也叫作覆盖,覆盖就是指虚表中虚函数的覆盖。重写是语法的叫法,覆盖是原理层的叫法。
3. 另外Func2继承下来后是虚函数,所以放进了虚表,Func3也继承下来了,但是不是虚函
数,所以不会放进虚表。
4. 虚函数表本质是一个存虚函数指针的指针数组,一般情况这个数组最后面放了nullptr。
5. 总结一下派生类的虚表生成:a.先将基类中的虚表内容拷贝一份到派生类虚表中 b.如果派生类重写了基类中某个虚函数,用派生类自己的虚函数覆盖虚表中基类的虚函数 c.派生类自己新增加的虚函数按其在派生类中的声明次序增加到派生类虚表的最后。
6. 这里还有一个问题:虚函数存在哪的?虚表存在哪的? 答:虚函数存在虚表,虚表存在对象中。注意上面的回答的错的。但是很多童鞋都是这样深以为然的。注意虚表存的是虚函数指针,不是虚函数,虚函数和普通函数一样的,都是存在代码段的,只是他的指针又存到了虚表中。另外对象中存的不是虚表,存的是虚表指针。
编译时多态vs运行时多态
编译时多态
建议在继承中析构函数定义成虚函数
// 建议在继承中析构函数定义成虚函数
class Person {
public:
virtual ~Person() { cout << "~Person()" << endl; }
//int* _ptr;
};
class Student : public Person {
public:
// 析构函数名会被处理成destructor,所以这里析构函数完成虚函数重写
virtual ~Student() { cout << "~Student()" << endl; }
};
int main()
{
Person* ptr1 = new Person;
delete ptr1;
Person* ptr2 = new Student;
delete ptr2;
//Person p;
//Student s;
return 0;
}
这里的第一个Person的析构完成的是对于ptr1指向的Person的析构,后面的有我们的类的空间是开辟在堆上面的,在构造的时候是先构造父类,然后构造子类,然后根据栈的出栈顺序,所以先析构的是子类,也就是我们的Student,然后再析构我们的子类继承的Person类中的相关属性。所以再析构Person。(子类当中还有一个父类对象)
(这里是不存在重复析构的问题的! )
这样子类和父类的析构函数就完成了重写。
这两个析构函数没有返回值,参数相同,但是函数名不相同哇,为什么完成了重写呢?
因为析构函数被统一处理成了distructor
析构函数为什么要完成重写呢?
// 建议在继承中析构函数定义成虚函数
class Person {
public:
~Person() { cout << "~Person()" << endl; }
//int* _ptr;
};
class Student : public Person {
public:
// 析构函数名会被处理成destructor,所以这里析构函数完成虚函数重写
~Student() { cout << "~Student()" << endl; }
};
int main()
{
Person* ptr1 = new Person;
delete ptr1;
Person* ptr2 = new Student;
delete ptr2;
//Person p;
//Student s;
return 0;
}
在不加virtual的情况下,我们发现我们的上面代码的析构函数只吸够了两个person类,并没有析构子类,并没有完成正确的析构!
上面的代码中,我们有一个父类的指针new了一个子类的对象
这个ptr2本应该在析构的时候去析构子类的对象,但是由于我们这里将virtual删掉了,也就是没有构成虚函数重载,这里仅仅是一个普通调用,普通调用也就是调用指针本身的类型的析构函数,也就是析构的函数call destructor,也就是call了person的destructor,但是其实我们想call的是student的destructor。
所以我们期望这个指针指向父类调用父类的析构函数,指向子类调用子类的析构函数,所以我们需要将这个析构函数变成虚函数的重写!
(我们上面说过了,只要父类的函数前面加了virtual,那么子类的三同函数不加virtual也会构成虚函数重载,所以我们这里仅仅是在父类的析构函数前面加上virtual,就能够将我们所有的子类都构成多态!)
(虽然子类可以不加virtual,只要父类中的函数前面加了virtual,但是我们最好还是全部都加上virtual)
子类的析构函数重写父类的析构函数,这里才能正确调用
指向父类,调用父类析构函数
指向子类,调用子类析构函数
三、C++11override和final
1.final
①修饰一个类,这个类不能被继承
②修饰一个虚函数,这个虚函数不能被重写(这个场景出现的次数极少)
class Car
{
public:
virtual void Drive() final {}
};
class Benz :public Car
{
public:
virtual void Drive() { cout << "Benz-舒适" << endl; }
};
2.override
override加在子类的虚函数当中,检查子类的虚函数是否完成重写,要是没有完成重写是会报错的!
class Car{
public:
virtual void Drive(){}
};
class Benz :public Car {
public:
// 检查子类虚函数是否完成重写
virtual void Drive() override { cout << "Benz-舒适" << endl; }
};
int main()
{
return 0;
}
上面这样写是正确的,但是如果我们不完成重写的话,比方说下面这样,就会报错
3.重载,重写和重定义的对比
它们三者都是函数之间的关系
四、抽象类
在虚函数的后面写上 =0 ,则这个函数为纯虚函数。包含纯虚函数的类叫做抽象类(也叫接口类),抽象类不能实例化出对象。
派生类继承后也不能实例化出对象,只有重写纯虚函数,派生类才能实例化出对象。
纯虚函数规范了派生类必须重写,另外纯虚函数更体现出了接口继承。
(抽象派:在现实世界中没有对应的实体)
比方说植物,是一个虚拟的大类的统称,并不是一个具体的实体,是没有办法实例化的!然后玫瑰花是一个具体的实体,就可以继承这个植物,然后重写植物中的各个函数。
class Car
{
public:
virtual void Drive() = 0;
};
class BMW : public Car
{
public:
};
class Benz :public Car
{
public:
};
int main()
{
Car c;
BMW b;
return 0;
}
不能实例化出对象!
这里的c和b都不能实例化出对象的!如果父类不能实例化出对象,子类也是不能实例化出对象的!
所以为什么将其称为接口?
接口就是提出一定的要求,统一的要求,函数参数是什么,返回值是什么等等,然后就可以分别取实现接口中的一个个函数。
某种程度上,override是检查是否重写,这里的接口是间接强制你重写父类的虚函数
class Car
{
public:
virtual void Drive() = 0;
};
class BMW : public Car
{
public:
virtual void Drive()
{
cout << "操控-好开" << endl;
}
};
class Benz :public Car
{
public:
virtual void Drive()
{
cout << "Benz-豪华舒适" << endl;
}
};
int main()
{
// Car c;
// BMW b;
Car* ptr = new BMW;
ptr->Drive();
ptr = new Benz;
ptr->Drive();
return 0;
}
接口继承和实现继承
虚函数的重写其实是一种接口继承。我重写了以后,子类不写virtual都可以,因为我重写的是父类的实现!比方说我的父类中的虚函数的默认参数是5,然后我子类重写的父类的虚函数的默认参数是1,因为虚函数的重写是一种接口继承,所以实际上用的还是父类的接口,所以我们真正去调用的时候,其默认的参数还是我们父类的5,而不是我们子类的1!
普通继承是一种实现继承,所继承的是一整个函数的实现,
不想实现多态就不要用虚函数!!
五、菱形继承
这里的B重写了A,C重写了A,但是D继承了B,C,那么我D应该用谁的重写呢?
所以这里的D就会直接报错
所以在语法上,这里的D必须去重写!不然我的D根本不知道应该去用谁的重写。
class A
{
public:
virtual void func1()
{
cout << "A::func1()" << endl;
}
public:
int _a;
};
// class B : public A
class B : virtual public A
{
public:
public:
int _b;
};
// class C : public A
class C : virtual public A
{
public:
public:
int _c;
};
class D : public B, public C
{
public:
virtual void func1()
{
cout << "C::func1()" << endl;
}
public:
int _d;
};
int main()
{
D d;
d.B::_a = 1;
d.C::_a = 2;
d._b = 3;
d._c = 4;
d._d = 5;
return 0;
}
那如果我们的B、C都去重写A类中的func1
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;
}
public:
int _b;
};
// class C : public A
class C : virtual public A
{
public:
virtual void func1()
{
cout << "C::func1()" << endl;
}
public:
int _c;
};
class D : public B, public C
{
public:
public:
int _d;
};
int main()
{
D d;
d.B::_a = 1;
d.C::_a = 2;
d._b = 3;
d._c = 4;
d._d = 5;
return 0;
}
这个时候编译就会报错,告诉我们的重写是不明确的,因为我们的B和C同时重写了A中的func1虚函数,B、C的虚表中都有对于A中的func1的重写的函数,那么我们的D去继承B和C的时候,就不知道应该是用B中重写的方法还是C中重写的方法。
因为这里加了虚继承,所以我们的D中并不是两张虚表,因为虚继承需要解决多继承中的数据冗余和二义性,所以D中只有一个A。所以我们的D并不知道这里应该是用B的还是C的func1。所以我们这里的D必须去重写A中的方法func1!
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;
}
public:
int _b;
};
// class C : public A
class C : virtual public A
{
public:
virtual void func1()
{
cout << "C::func1()" << endl;
}
public:
int _c;
};
class D : public B, public C
{
public:
virtual void func1()
{
cout << "C::func1()" << endl;
}
public:
int _d;
};
int main()
{
D d;
d.B::_a = 1;
d.C::_a = 2;
d._b = 3;
d._c = 4;
d._d = 5;
return 0;
}
这里跌虚表中只有D重写A的func1
如果我们这时B增加一个func2,C也增加一个func2
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 << "C::func1()" << endl;
}
public:
int _d;
};
int main()
{
D d;
d.B::_a = 1;
d.C::_a = 2;
d._b = 3;
d._c = 4;
d._d = 5;
return 0;
}
这个时候有三个虚表!
因为我这里原来只有一张虚表,这个虚表是虚基类A的
现在B有虚函数,C也有虚函数func2并没有重写A
那B,C的虚函数放入A的虚表中就不合适了,所以现在就需要分别增加B、C的虚表
练习
下面程序输出结果是什么? ()
#include<iostream>
using namespace std;
class A{
public:
A(char *s) { cout<<s<<endl; }
~A(){}
};
class B:virtual public A
{
public:
B(char *s1,char*s2):A(s1) { cout<<s2<<endl; }
};
class C:virtual public A
{
public:
C(char *s1,char*s2):A(s1) { cout<<s2<<endl; }
};
class D:public B,public C
{
public:
D(char *s1,char *s2,char *s3,char *s4):B(s1,s2),C(s1,s3),A(s1)
{ cout<<s4<<endl;}
};
int main() {
D *p=new D("class A","class B","class C","class D");
delete p;
return 0;
}
A:class A class B class C class D B:class D class B class C class A
C:class D class C class B class A D:class A class C class B class D
这一份A是B、C共享的,所以我们这里D中的初始化列表中,对于A的初始化最好有A自己去调用。否则会产生二义性。
这里的s4肯定是最后去打印的。
这里的编译器不会重复地去构造。D中只有一份A,所以只会调用一次A的构造,也就是
A(s1),所以A最先被初始化。
因为这里是按声明顺序去运行的,所以最先是A
然后按照顺序分别是B和C(是先继承B然后再去继承C的)
所以选A
(由于我们的A已经被初始化了,所以我们的B和C中对于A的初始化函数并不用去调用)
那B和C中的初始化列表中的对A的构造函数由什么用呢?
万一单独调用B,C的构造函数就派上用场了
A
问答题
1. 什么是多态?
不同的对象去做同一件事情结果不一样。
2. 什么是重载、重写(覆盖)、重定义(隐藏)?上面已经将过了
3. 多态的实现原理?去对应的虚表里面找
4. inline函数可以是虚函数吗?内联函数就是在函数调用的地方进行展开。
内联函数没有地址,是直接展开的,不需要地址,但是虚函数需要放入虚函数表中,需要地址。所以这里的内联函数和虚函数是不能共存的!
但是编译器是允许同时存在的,是可以编译成功的。
(inline本身就是一个建议性的关键字,并不是说你将其设置为inline,它就一定是inline,编译器在发现并不能变成inline之后,就会忽略inline属性,转而将其变成虚函数。也就是说,在多态调用中,inline就失效了)
答:可以,不过编译器就忽略inline属性,这个函数就不再是inline,因为虚函数要放到虚表中去。
5. 静态成员可以是虚函数吗?没有this指针,直接可以使用A::Func2(),虚函数是为了实现多态,多态都是运行时去虚表找决议。
static成员函数都是在编译时决议。它是虚函数没有价值,虚函数是运行时决议。
答:不能,因为静态成员函数没有this指针,使用类型::成员函数的调用方式无法访问虚函数表,所以静态成员函数无法放进虚函数表。
6. 构造函数可以是虚函数吗?答:不能,因为对象中的虚函数表指针是在构造函数初始化列表阶段才初始化的。
virtual函数是为了实现多态,运行时去虚表找对应虚函数进行调用
对象中虚表指针都是构造函数初始化阶段才初始化的。
但是构造函数没有调用之前是没有虚表的,虚表指针都是随机值,没办法实现多态。
多态必须是在虚表创建好之后才能从对应的虚表中找到对应虚函数的地址。
构造函数虚函数是没有意义的。
7. 析构函数可以是虚函数吗?什么场景下析构函数是虚函数?答:可以,并且最好把基类的析构函数定义成虚函数。
在上面我们已经提过了。
8.拷贝构造和赋值(operator=)可不可以是虚函数?
答:
拷贝构造:不可以是虚函数。拷贝构造也是属于构造函数。答案和构造一样,参考上面的6.
赋值:可以!语法上是可以的!但是赋值被设计成了多态其实没有太大的意义。
因为赋值的子类其实应该是子类的部分赋值子类的部分,父类的部分赋值父类的部分。
但是我们这里可以实现父类赋值给子类(切片是子类赋值给父类)
9. 对象访问普通函数快还是虚函数更快?虚函数不构成多态调用,一样快。
虚函数构成多态调用,普通函数块,因为多态调用是运行时去虚表中找虚函数的地址的!
答:首先如果是普通对象,是一样快的。如果是指针对象或者是引用对象,则调用的普通函数快,因为构成多态,运行时调用虚函数需要到虚函数表中去查找。
10. 虚函数表是在什么阶段生成的,存在哪的?(虚函数表并不是构造函数初始化阶段生成的,不是存在对象里面的!)
构造函数初始化列表阶段初始化的是虚函数表指针,对象中存的也是虚函数表指针!
答:虚函数表是在编译阶段就生成的,一般情况下存在代码段(常量区)的。
class B { public: B() { } }; int main() { B bb; printf("虚表:%p\n",*((int*)&bb)); static int x=0; printf("static变量:%p\n",&x); const char* ptr="hello world"; printf("常量:%p\n",ptr); int y=0; printf("局部变量:%p\n",&y); printf("new变量:%p\n",new int); }
跟常量和静态变量挨得更近一点
在(代码段)常量区,因为虚表具有只读属性。
并且地址是从上往下的,我们这里的虚标的地址是最小的,
常量一般是在编译阶段就生成好了的。
在进程运行的时候会将常量从常量区取过来,加载到对应的段。
11. C++菱形继承的问题?虚继承的原理?答:参考继承。注意这里不要把虚函数表和虚基表搞混了。
11. 什么是抽象类?抽象类的作用?答:参考(抽象类)。抽象类强制重写了虚函数,另外抽象类体现出了接口继承关系。