文章目录
1、了解
多态,就是多种形态,不同的对象去完成某个行为时会有不同的状态/结果。
在函数类型前加上virtual就表示这是虚函数
class Person
{
public:
virtual void Buy() { cout << "买票-全价" << endl; }
};
class Student : public Person
{
public:
//发生了重写/覆盖
virtual void Buy() { cout << "买票-半价" << endl; }
};
void Func(Person& p)
{
p.Buy();
}
int main()
{
Person ps;
Student st;
Func(ps);
Func(st);
return 0;
}
结果就是一个全价一个半价,虽然发生了覆盖,但是不影响最终的结果,传哪个类的对象就打印什么。
多态的条件就是虚函数的重写和父类的指针或者引用去调用
刚才的代码,如果去掉引用或者重写或者两个都去掉,那就2个全价
这样的场景
class Person {
public:
~Person() { cout << "~Person()" << endl; }
};
class Student : public Person {
public:
~Student() { cout << "~Student()" << endl; }
};
int main()
{
Person* ptr1 = new Person;
Person* ptr2 = new Student;
delete ptr1;
delete ptr2;
return 0;
}
ptr1释放时调用父类析构,但是ptr2也调用了父类析构,这是因为出现了隐藏关系,这时候多态就能派上用场了。
2、多态的条件
虚函数的重写——函数名、参数、返回值都相同
父类指针或者引用去调用
如果不满足多态,就看调用者的类型,调用这个类型的成员函数
满足多态,就看指向的对象的类型,调用这个类型的成员函数
class Person
{
public:
void Buy() { cout << "买票-全价" << endl; }
};
class Student : public Person
{
public:
virtual void Buy() { cout << "买票-半价" << endl; }
};
void Func(Person& p)
{
p.Buy();
}
像这样父类没有virtal,不是多态,是隐藏,只不过子类对象去调用才能体现出隐藏。
Student st;
st.Buy();
用引用,那么可以在外面创建出对象后,Func(st)传过去即可,而用指针则不能,会显示Student类型的不能转换成Person*类型的,所以就得Func(new Student)。
如果父类函数有virtual,但是子类没有,那就输出一个全价和一个半价,但是这里仍然是多态。
子类可以不写,父类写了,编译器会认为子类重写了这个虚函数,这叫接口继承,子类会继承父类的函数,但是里面的实现会重写成子类的。
1、析构函数重写
class Person
{
public:
virtual void Buy() { cout << "买票-全价" << endl; }
~Person()
{
cout << "~Person()" << endl;
}
};
class Student : public Person
{
public:
//发生了重写/覆盖
void Buy() { cout << "买票-半价" << endl; }
~Student()
{
cout << "~Student()" << endl;
}
};
void Func(Person* p)
{
p->Buy();
delete p;
}
int main()
{
Func(new Person);
Func(new Student);
return 0;
}
delete有两个部分,调用析构部分,然后再调用operator delete,而析构函数会被处理成destructot,也就是ptr1->destrutctor(),所以两个对象会构成隐藏,而都是父类指针指向的,符合上面所说,所以会调用父类的。
现在析构函数不是多态,所以要加上另一个条件,把父类析构也写上virtual就可以形成多态了。virtual ~Person。
当子类结束后,就会调用子类的析构,然后最后在析构父类指针,所以最后有一个~Person()。
2、协变
虚函数的重写有三个相同,函数名、参数、返回值,有一个例外就是返回值不同。但不能无脑不同,它要求必须是父子关系的指针或者引用。
class Person
{
public:
virtual Person* Buy()
{
cout << "买票-全价" << endl;
return this;
}
};
class Student : public Person
{
public:
virtual Student* Buy()
{
cout << "买票-半价" << endl;
return this;
}
};
这时候也会一个打印全价,一个打印半价。它可以不只是现有类的指针,比如说在这之前定义A父类和B子类,就可以用A和B的指针。返回值类型也得对应,返回类对象就不行。
虚函数重写的例外有两个,一个是接口继承,一个是协变。接口继承就是父类写了virtual,子类就可以不写virtual也同样构成多态。
3、关键字final和override
final
修饰虚函数后这个虚函数不能被重写
class Car
{
public:
virtual void Drive() final {}
};
class Benz : public Car
{
public:
virtual void Drive() { cout << "Benz-舒适" << endl; }
};
int main()
{
return 0;
}
override
检查是否完成重写,不是就报错
class Car
{
public:
virtual void Drive() {}
};
class Benz : public Car
{
public:
virtual void Drive() override { cout << "Benz-舒适" << endl; }
};
int main()
{
return 0;
}
4、重写(覆盖)、重载、隐藏(重定义)对比
重载:两个函数在同一作用域;函数名相同,参数不同
重写:两个函数分别在基类和派生类的作用域;函数名/参数/返回值都必须相同(协变例外);两个函数必须是虚函数
重定义:两个函数分别在基类和派生类的作用域;函数名相同;两个基类和派生类的同名函数不构成重写就是重定义
3、抽象类
在虚函数后写上=0,这个函数就是纯虚函数,包含纯虚函数的类是抽象类,这种类不能实例化出对象。
一个类继承抽象类后也是抽象类,因为包含纯虚函数。如果重写了纯虚函数,子类的那个函数就不是纯虚函数,这时候子类就可以实例化对象了。
4、多态的原理
1、虚函数表
class Base
{
public:
virtual void Func1()
{
cout << "Func1()" << endl;
}
private:
int _b = 1;
char _ch;
};
int main()
{
cout << sizeof(Base) << endl;
Base bb;
return 0;
}
32位下,结果是12。按照内存对齐,两个变量要占8个字节,那么多出来的4是什么?
多出来的这个指针就是虚函数表指针。
2、原理
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;
}
func函数里,传父类对象,就会有父类的虚函数,传子类对象,就会有子类的虚函数。父类虚表存父类虚函数,子类虚表存子类虚函数。如果不构成多态,那就什么类调用就用什么类的虚函数;如果是多态,就去指向的对象的虚表里去找。所以p这个指针看的只是虚表,它并不知道是哪个类对象。
为什么虚函数表不只放在类对象里,而是要找单独一个区域去放虚函数表?因为可能有多个虚函数。虚函数表本质是一个虚函数指针数组,如果有多个虚函数,就会有多个虚函数表。发生了重写,虚函数表就会被覆盖,没有就不会覆盖。虚函数表是在编译时就准备好的。虚函数表按声明顺序来确定数组下标的。
为什么父类指针或者引用可以,但实例化出的对象不可以形成多态?
父类指针或者引用可以指向虚函数表,子类会把父类的那部分切出来,让它指向对象,对应的还是子类的虚函数表。如果是对象,父类对象没问题,但如果是子类对象,那么会发生拷贝,把父类的内容拷贝到子类,那虚表会拷贝呢?不会,这个风险大,拷贝了可能很多映射关系就乱了。
---------------------------------------------------------------------------------------------------------------
虚函数和普通函数一样,存在代码段。
在main栈帧里,类实例化了对象,对象有一个指针,指向虚函数表,表里存的就是虚函数的地址。
3、打印虚表
class Base {
public:
virtual void func1() { cout << "Base::func1" << endl; }
virtual void func2() { cout << "Base::func2" << endl; }
void func3() { cout << "Derive::func3" << endl; }
private:
int _b = 1;
};
class Derive :public Base {
public:
virtual void func1() { cout << "Derive::func1" << endl; }
virtual void func4() { cout << "Derive::func4" << endl; }
private:
int _d = 2;
};
int main()
{
Base b;
Derive d;
return 0;
}
如果在子类中写一个父类没有的函数,前面加上virtual,这时候如果调用监视窗口会发现它不在虚表,但是真的不在吗?内存窗口也不一定能看得出来,这样的话就得打印虚表来看了。
typedef void(*VF_PTR)();//函数指针的typedef需要把新定义的名字放在括号里,所以VF_PTR是void(*)()
void PrintVFTable(VF_PTR table[])
{
for (int i = 0; table[i] != nullptr; ++i)
{
printf("[%d]:%p\n", i, table[i]);
}
cout << endl;
}
int main()
{
Base b;
Derive d;
//要找到这个虚表,根据之前所写,虚表指针在这个对象的前4/8个字节
PrintVFTable((VF_PTR*)(*(int*)&b));
PrintVFTable((VF_PTR*)(*(int*)&d));
return 0;
}
我们可以把类对象的地址强制转为int*类型,再解引用就得到了地址的前4个字节,然后把这个结果再转成VF_PTR类型传过去就好了。
另外的写法就是二级指针
PrintVFTable((*(VF_PTR**)&b));
PrintVFTable((*(VF_PTR**)&d));
但是第一种只能在32位下走,第二种两个都行。第一种int改成longlong就可以适应64位。
补全打印虚表函数
void PrintVFTable(VF_PTR* table)
{
for (int i = 0; table[i] != nullptr; ++i)
{
printf("[%d]:%p->", i, table[i]);
VF_PTR f = table[i];
f();
}
cout << endl;
}
就可以打印出来两个对象的虚表。func4也在其中。
虚表是编译阶段生成的,对象中虚表指针是在构造函数的初始化列表初始化的,虚表存在哪里?
可以通过这个方法来看。
int x = 0;
static int y = 0;//静态区
int* z = new int;
const char* p = "asdasd asda";//常量区
printf("栈对象: %p\n", &x);
printf("堆对象: %p\n", z);
printf("静态区对象: %p\n", &y);
printf("常量区对象: %p\n", p);
printf("b对象虚表: %p\n", *((int*)&b));
printf("d对象虚表: %p\n", *((int*)&b));
离常量区近,应当是在常量区,不过也有编译器放在静态区。
4、多继承
class Base1 {
public:
virtual void func1() { cout << "Base1::func1" << endl; }
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;
};
int main()
{
Derive d;
return 0;
}
d有两张虚表,base1和base2,func1发生了重写,两个虚表都有各自的func2,func3在哪里?我们可以打印虚表,但是有两个虚表,base1放在前头,所以按照之前的办法就只打印了base1,2没打印,可以用+sizeof(Base1)或者用偏移来打印base2。
//PrintVFTable((VF_PTR*)(*(int*)((char*)&d + sizeof(Base1))));
Base2* ptr2 = &d;//用Base2类的指针指向Derive的对象,指针就会指向d中Base2的部分,自然而然就发生了偏移
PrintVFTable((VF_PTR*)(*(int*)(ptr2)));
最后结果放在第一个虚表中。
上面的代码中,func1重写了,但是地址不一样,但它们确实重写了。
Base1* ptr1 = &d;
Base2* ptr2 = &d;
ptr1->func1();
ptr2->func1();
反汇编代码中,ptr1->func1() call到了Base1的虚表地址,call后是一个jmp指令,jmp后面括号里的就是函数真实的地址,然后开始建立栈帧等;ptr2->func1() call的地址就不一样,jmp后函数的地址也不一样,并且在这次jmp后接下来的反汇编出现了一句sub,一句jmp,然后再接一句jmp,才到真正执行的函数的地址。它之所以这样走,是因为里面有一个sub,那行代码后还有一个ecx,ecx就是this指针;ptr1调用的func是子类的函数,ptr1指向对象的开始,ecx就指向对象的开始,所以不需要另外的处理;但是ptr2并不是这样,此时this并没有指向对象的开始,所以多出来的步骤是为了修正this指针。
5、动静态绑定
- 静态绑定又称为前期绑定(早绑定),在程序编译期间确定了程序的行为,也称为静态多态,
比如:函数重载,cin, cout- 动态绑定又称后期绑定(晚绑定),是在程序运行期间,根据具体拿到的类型确定程序的具体
行为,调用具体的函数,也称为动态多态。- 之前买票Buy()的汇编代码很好的解释了什么是静态(编译器)绑定和动态(运行时)绑
定。
6、其它
class A
{
public:
virtual void func()
{}
public:
int _a;
};
class B : virtual public A
{
public:
virtual void func()
{}
public:
int _b;
};
class C : virtual public A
{
public:
virtual void func()
{}
public:
int _c;
};
class D : public B, public C
{
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;
}
ABC都有函数,如果D不写,就会警告,说A的func函数重写不明确。A的虚表应当放B还是C重写的函数?无法确定,这时候就需要D去完成最终的重写。初始化也一样,D里面写了代码,BCA都对A初始化了,但是A得用自己的初始化,如果没写A对自己的初始化,那就报错。
如果BC有新增的虚函数,不会放进A的虚表。不看A的话,D继承BC,D应当有两个虚表,是B和C的,那再加上A,就有了A的公共的虚表
D有三个虚表,包括一张A的虚表,全称虚函数表(指向虚函数的表,里面存放函数指针),两张B和C的虚基表(存有偏移量)。e4 f4 b4是虚表指针,38和ac是虚基表指针。
5、总结
1、多态分为动静态多态,静态多态是编译时的多态,通过函数重载或者模板来实现多态,动态多态是运行时的多态,通过虚函数来实现。
2、内联函数不可以是虚函数。内联函数没有地址,因为它不需要链接,直接在调用处展开,它不能声明和定义分离。所以它不能成为虚函数,因为虚函数地址要放到虚表里。但是实际上这样写inline virtual 还是能通过,因为内联对编译器仅仅是一个建议,内联+虚函数,且符合多态,就会按照多态走,编译器会忽略内联属性,否则就还是内联。
3、静态函数不能是虚函数。虚函数是通过对象去调用的,而静态函数不需要这样限制,谁都可以调用,且它也没有this指针。可以使用类型::成员函数的方式区调用静态成员,但是这样无法访问虚函数表。static和virtual放在一起会直接报错。
4、构造函数不能是虚函数。虚函数地址在虚表,虚表指针是在初始化列表时生成的,这样就造成了逻辑错误,代码无法正常运行。子类必须要显式调用父类构造函数,不能打扰父类的构造,所以构造函数不能成为虚函数。
5、拷贝构造和赋值也不能是虚函数,拷贝构造和构造的原因类似;而重写了赋值函数会影响操作者要改变的变量,虽然赋值函数可以写virtual。
6、析构函数建议写成虚函数。
7、对于普通对象,普通和虚函数一样快;如果是指针对象或者引用对象,形成了多态,普通函数快,因为运行时调用虚函数需要到虚函数表找。
8、虚函数表是在编译阶段生成的,一般情况下存在代码段(常量区)或者静态区
9、虚函数表是在多态里的,虚基表是为了解决多继承的;虚函数表有虚函数的地址,虚基表存有偏移量,解决数据冗余和二义性。
结束。