自学内容网 自学内容网

C++练级计划->《多态》虚函数表,菱形继承多态

目录

什么是多态?

多态的条件

虚函数:

虚函数的重写:

协变

 析构函数的重写

C++11 final 和 override

final:

override:

总结:

三重对比:重载重写重定义对比

抽象类

多态的原理

虚函数表

为什么只能是父类的指针或者引用来调用才能形成多态?

 动态绑定和静态绑定

单继承的虚函数表 

多继承的虚函数表

菱形继承和菱形虚拟函数继承


什么是多态?

现实中买票的测策略:学生 社会人 军人都是要买票的,但是不同的人买票买到的价格却可能是不一样的,学生票,普通票,军人票。虽然我们都执行了买票的操作,但是我们的操作行为是不一样的。

多态其实是一种语法特性,只有你使用时他才会出现,不使用时,这个特性不会出现如下是多态形成的条件

多态的条件

1.必须是基类的指针或者引用来调用虚函数。

当然(如果使用派生类对象直接访问,那打印出来的就是派生类函数的内容,如果是用基类的指针或者引用进行访问,会根据这个基类的赋值对象的类进行访问)括号内是继承的内容,多态特性要形成的条件是1.用基类的指针或者引用来调用虚函数

#include <iostream>  
using namespace std;  
  
class Base {  
public:  
    void show() { // 非虚函数  
        cout << "Base class show function" << endl;  
    }  
  
    virtual void virtualShow() { // 虚函数  
        cout << "Base class virtual show function" << endl;  
    }  
};  
  
class Derived : public Base {  
public:  
    void show() { // 隐藏了Base::show,而不是重写  
        cout << "Derived class show function (hiding)" << endl;  
    }  
  
    void virtualShow() override { // 重写了Base::virtualShow  
        cout << "Derived class virtual show function" << endl;  
    }  
};  
  
int main() {  
    Derived derivedObj;  
  
    derivedObj.show();           // 输出 "Derived class show function (hiding)",因为调用的是Derived::show  
    derivedObj.virtualShow();    // 输出 "Derived class virtual show function",因为调用的是重写的虚函数  
  
    Base* basePtr = &derivedObj;  
    basePtr->show();             // 输出 "Base class show function",因为调用的是Base::show(隐藏不影响基类指针调用)  
    basePtr->virtualShow();      // 输出 "Derived class virtual show function",因为调用的是重写的虚函数  
  
    return 0;  
}

如上代码我们直接用派生类对象调用虚函数打印出来的还是派生类的内容,但是我们用基类的指针指向派生类,此时调用虚函数打印出来的依然是派生类的内容。

2.形成条件2:被调用的函数必须是虚函数,并且派生类要对虚函数进行重写

综上形成多态的条件有两个:

1.必须是基类的指针或者引用来调用虚函数。

2:被调用的函数必须是虚函数,并且派生类要对虚函数进行重写。

虚函数:

它的格式如下:

class Person
{
public:
//被virtual修饰的类成员函数
virtual void BuyTicket()
{
cout << "买票-全价" << endl;
}
};

只要在函数前面加上一个virtual就可以了。

注意:

1.只要在你觉得是虚函数的函数前面加virtual,其他只作用于本类的函数不要加(因为虚函数是强制要求重写的)。

2.静态函数不能是虚函数。因为静态函数全部类中只能有一份,就算继承下去的也是那一份。如果给其加上虚函数,那就要求重写一份,此时这个重写的静态函数和原本的静态函数就冲突了。

3.虚函数的virtual和虚继承的virtual都是虚的,二者的用法是不同的,虚函数用于多态,虚继承则是为了解决菱形继承时的数据的二义性和冗余的。

虚函数的重写:

虚函数的重写也叫虚函数的覆盖,虚函数的重写就是把继承下来的虚函数里的内容(定义)重新写一遍,将原来替换下来的函数给替换。

//父类
class Person
{
public:
//父类的虚函数
virtual void BuyTicket()
{
cout << "买票-全价" << endl;
}
};
//子类
class Student : public Person
{
public:
//子类的虚函数重写了父类的虚函数
virtual void BuyTicket()
{
cout << "买票-半价" << endl;
}
};
//子类
class Soldier : public Person
{
public:
//子类的虚函数重写了父类的虚函数
virtual void BuyTicket()
{
cout << "优先-买票" << endl;
}
};

如上代码:三者的虚函数除了内容(定义不同)其他都是相同的。

此时我们在使用基类的指针或者引用去调用三者就形成了多态

void Func(Person& p)
{
//通过父类的引用调用虚函数
p.BuyTicket();
}
void Func(Person* p)
{
//通过父类的指针调用虚函数
p->BuyTicket();
}
int main()
{
Person p;   //普通人
Student st; //学生
Soldier sd; //军人

Func(p);  //买票-全价
Func(st); //买票-半价
Func(sd); //优先买票

Func(&p);  //买票-全价
Func(&st); //买票-半价
Func(&sd); //优先买票
return 0;
}

注意:在派生类中重写虚函数时是可以不用加virtual的,因为它已经把基类的虚的特性继承下来了,但是建议还是加上virtual,便于其他程序员观看。

协变

协变就是在虚函数的层次下,基类和派生类的返回值不同,因为可能会有这样的需求,要求基类的这个虚函数返回的是基类的对象指针或者引用,或者派生类返回派生类的对象的指针或者引用。

//基类
class A
{};
//子类
class B : public A
{};
//基类
class Person
{
public:
//返回基类A的指针
virtual A* fun()
{
cout << "A* Person::f()" << endl;
return new A;
}
};
//子类
class Student : public Person
{
public:
//返回子类B的指针
virtual B* fun()
{
cout << "B* Student::f()" << endl;
return new B;
}
};

在我们重写协变函数发现不同指向的指针调用函数,调用的依然是对应的虚函数,说明虚函数重写是成功的。 

int main()
{
Person p;
Student st;
//父类指针指向父类对象
Person* ptr1 = &p;
//父类指针指向子类对象
Person* ptr2 = &st;
//父类指针ptr1指向的p是父类对象,调用父类的虚函数
ptr1->fun(); //A* Person::f()
//父类指针ptr2指向的st是子类对象,调用子类的虚函数
ptr2->fun(); //B* Student::f()
return 0;
}

 析构函数的重写

析构函数如果你自己定义了,那不管有没有加virtual都是虚函数(尽管名字不同),为什么呢?

//父类
class Person
{
public:
virtual ~Person()
{
cout << "~Person()" << endl;
}
};
//子类
class Student : public Person
{
public:
virtual ~Student()
{
cout << "~Student()" << endl;
}
};

上面代码写了析构函数,试想这样的场景,我们用基类的指针指向new出来的基类对象和派生类对象,此时如果没有虚函数的定义,delete时这个基类的指针应该只会调用基类的析构函数,而对于派生类而言我们继承的定义是:先调用基类的析构函数后调用派生类的析构函数。所以析构函数默认就是虚函数

int main()
{
//分别new一个父类对象和子类对象,并均用父类指针指向它们
Person* p1 = new Person;
Person* p2 = new Student;

//使用delete调用析构函数并释放对象空间
delete p1;
delete p2;
return 0;
}

其实因为子类的析构函数是先调用父类的析构后调用自己的析构,已经是多态行为了,父类的指针按理来说调用的就是父类的析构函数,这里的多态就是把子类的析构函数重写成了先调用父类的析构函数后再去调用子类的析构函数。

注意:为什么子类和父类的析构函数构成重写,虽然表面写的时候析构函数的名字是不同的,但是在编译后,析构函数都会被统一命名为destructor();这样就形成了相同的函数。构成虚函数重写

C++11 final 和 override

final:

final:修饰虚函数,表示这个虚函数不能再被重写。

//父类
class Person
{
public:
//被final修饰,该虚函数不能再被重写
virtual void BuyTicket() final
{
cout << "买票-全价" << endl;
}
};
//子类
class Student : public Person
{
public:
//重写,编译报错
virtual void BuyTicket()
{
cout << "买票-半价" << endl;
}
};
//子类
class Soldier : public Person
{
public:
//重写,编译报错
virtual void BuyTicket()
{
cout << "优先-买票" << endl;
}
};

上述代码用final修饰person的函数,此时如果后面依然有虚函数重写就会导致编译报错。 

override:

override:检查派生类是否重写了虚函数,如果没有重写则报错

//父类
class Person
{
public:
virtual void BuyTicket()
{
cout << "买票-全价" << endl;
}
};
//子类
class Student : public Person
{
public:
//子类完成了父类虚函数的重写,编译通过
virtual void BuyTicket() override
{
cout << "买票-半价" << endl;
}
};
//子类
class Soldier : public Person
{
public:
//子类没有完成了父类虚函数的重写,编译报错
virtual void BuyTicket(int i) override
{
cout << "优先-买票" << endl;
}
};

如上代码 我们在子类的函数上加上override,这时override就会去基类判断子类是否重写对了。如果没有发现基类中有相同函数则编译报错(必须是虚函数)

总结:

override和final的优点就是,写虚函数时要求很高,要求函数名称参数等等全都相同,这就导致了程序员在写虚函数时如果犯错,这时就发现不了因为可能被认作是一个新的函数,所以这两个关键词可以用来检查虚函数,final可以用来判断派生类有没有同名函数,override可以用来解决虚函数写错的问题 

三重对比:重载重写重定义对比

重载很好理解:就是同一作用域的同名函数构成

重写和重定义的区别是:

重写要求是虚函数,函数名称参数返回值等等都是一样的只是定义不一样。

重定义要求只要函数名相同,且不是虚函数,重定义类似于基类派生类间的重载。

抽象类

先说说抽象:我们现实生活中泛指一类东西时,比如我是人,你是人,他是人,人就是抽象的;

抽象类要求在虚函数后面加上=0,表示这个函数为纯虚函数,然后只有一个类中出现了一个纯虚函数就可以叫做抽象类。抽象类不能实例化出对象,就像人是一个泛指,不能准确的指出是谁。

#include <iostream>
using namespace std;
//抽象类(接口类)
class Car
{
public:
//纯虚函数
virtual void Drive() = 0;
};
int main()
{
Car c; //抽象类不能实例化出对象,error
return 0;
}

子类继承了父类,这时父类是抽象类,那子类继承下来原本按理来说也是抽象类,但是只要把纯虚函数重写成普通函数,这时就子类就不抽象了(纯虚函数被改了)就可以实例化了。

#include <iostream>
using namespace std;
//抽象类(接口类)
class Car
{
public:
//纯虚函数
virtual void Drive() = 0;
};
//派生类
class Benz : public Car
{
public:
//重写纯虚函数
virtual void Drive()
{
cout << "Benz-舒适" << endl;
}
};
//派生类
class BMV : public Car
{
public:
//重写纯虚函数
virtual void Drive()
{
cout << "BMV-操控" << endl;
}
};
int main()
{
//派生类重写了纯虚函数,可以实例化出对象
Benz b1;
BMV b2;
//不同对象用基类指针调用Drive函数,完成不同的行为
Car* p1 = &b1;
Car* p2 = &b2;
p1->Drive();  //Benz-舒适
p2->Drive();  //BMV-操控
return 0;
}

所以说抽象类存在的意义是什么?它也可以直接用虚函数不用纯虚函数也能完成

1.既然说了人是抽象的那现实生活中抽象的东西也很多,这时也希望在编程时能有抽象这一概念,虽然没有具体的实例对象,但是有存在这个名词。所以可以更好的用来表示现实世界

2.抽象类也可以变相的要求派生类强制子类重写虚函数,因为不重写纯虚函数子类也没办法实例化出对象。

多态的原理

虚函数表

Base类实例化出对象的大小是多少?(笔试题)

class Base
{
public:
virtual void Func1()
{
cout << "Func1()" << endl;
}
private:
int _b = 1;
};

我们可以直接实例化出Base对象,然后sizeofBase对象,发现对象有8个字节

我们的_b成员是4字节,那剩下的4字节是哪里来的?

这里就能引出我们的主题:虚函数表(Virtual Function Table)也可以叫做虚表

虚函数表和虚基表的区别:虚函数表是因为虚函数出现的(多态),而虚基表则是因为菱形继承为了防止二义性和冗杂的(继承)

这里先讨论虚函数表:只有虚函数出现时这个表(指针)才会出现。

 看下面代码我们Base中有三个函数

#include <iostream>
using namespace std;
//父类
class Base
{
public:
//虚函数
virtual void Func1()
{
cout << "Base::Func1()" << endl;
}
//虚函数
virtual void Func2()
{
cout << "Base::Func2()" << endl;
}
//普通成员函数
void Func3()
{
cout << "Base::Func3()" << endl;
}
private:
int _b = 1;
};
//子类
class Derive : public Base
{
public:
//重写虚函数Func1
virtual void Func1()
{
cout << "Derive::Func1()" << endl;
}
private:
int _d = 2;
};
int main()
{
Base b;
Derive d;
return 0;
}

在Derive类中重写了Func1。此时观察虚函数表中存储了什么?

1.可以发现只有虚函数才能进入 虚函数表中 func3没有进入。虚函数也是在代码区的

2.在d对象中func1被重写为Derive类的func1.这就是重写(覆盖)原来的func1的地址重写为了派生类的func1

3.对于func3而言他是普通函数,它当然也被继承了,它的位置是在代码区的(这个类的函数都放在一个统一的代码区,不同对象调用时用的也是同一份代码,就不会冗余)

4.如果派生类有自己的虚函数而不是继承下来的,这时派生类的虚函数位置应该在继承下来的虚函数的下面。

虚函数表的初始化时间?虚函数存在哪里?虚表存在哪里? 

1.虚表在构造函数初始化阶段进行初始化的,虚表存的是虚函数的地址,而不是虚函数,

2.虚函数和普通函数一样,都是存在代码区中的,只是他的地址也存到了虚函数表中(为了重写,调用时就知道调用的是哪一个函数) 

3.虚表也是存在于代码区的

现在回想:我们构成多态的两个条件:1.必须有虚函数2.只能调用父类的指针或者引用来调用对象

为什么只能是父类的指针或者引用来调用才能形成多态?

看上图,我们有两个对象都是person类继承下来的,Mike是成人,johnson是学生

二者都有继承的或者重写buyticket函数 ,所以用person指针调用二者的butticket函数结果是不同。

这样就是多态了,同一个行为不同的形态。

Person* p1 = &Mike;
Person* p2 = &Johnson;

大家还记得切片吗?父类的指针或者引用调用子类对象,是一种切片行为

可以调用父类中有的成员 ,而父类中没有的成员 则会被切掉,不能调用。

Person p1 = Mike;
Person p2 = Johnson;

如果直接是用子类构造父类对象, 父类对象它依然是父类对象,因为在构造时首先会构造一个临时对象,这个临时对象因为是person类,所以他去调用了person的构造函数,所以指向的依然是父类的虚表,只是内置类型构造用的是子类的内容。

总结:

1.如果构成多态,是一个指针或者引用,指向的是什么类型的对象调用什么虚函数,和对象有关

2.如果不构成多态,是一个父类对象,这时父类对象依旧是父类所以虚函数表也是父类的,但是成员的构造是复制的子类的成员

 动态绑定和静态绑定

静态绑定: 静态绑定又称为前期绑定(早绑定),在程序编译期间确定了程序的行为,也成为静态多态,比如:函数重载。

动态绑定: 动态绑定又称为后期绑定(晚绑定),在程序运行期间,根据具体拿到的类型确定程序的具体行为,调用具体的函数,也称为动态多态。

 观察如下代码:

//父类
class Person
{
public:
virtual void BuyTicket()
{
cout << "买票-全价" << endl;
}
};
//子类
class Student : public Person
{
public:
virtual void BuyTicket()
{
cout << "买票-半价" << endl;
}
};
int main()
{
Student Johnson;
Person p = Johnson; //不构成多态
p.BuyTicket();
return 0;
}

这串代码我们没用引用或者指针构造p,所以不构成多态。然后我们查看汇编代码

可以发现此时调用buyticket时是直接找到函数位置并进行调用的。

而如果我们用多态的方式调用buyticket

int main()
{
Student Johnson;
Person& p = Johnson; //构成多态
p.BuyTicket();
return 0;
}

 然后查看汇编码

会发现有八条汇编指令大概意思就是我们对象要先去虚函数表中,然后通过虚函数表找到对应的虚函数,然后在进行调用。在使用时才去寻找。

对比:

可以发现1.静态绑定是直接找到函数,而动态绑定是先找到虚函数表的位置,然后在虚函数表中找到对应函数的位置。

2.在多态的两条件没够成时,函数调用是不会根据虚函数表的,而是直接调用类中函数(静态绑定)。

3.而多态构成时,才会去找虚函数表,因为只有多态构成时才有可能出现重定义,所以要根据虚函数表来找到要调用的函数(动态绑定)

单继承的虚函数表 

//基类
class Base
{
public:
virtual void func1() { cout << "Base::func1()" << endl; }
virtual void func2() { cout << "Base::func2()" << endl; }
private:
int _a;
};
//派生类
class Derive : public Base
{
public:
virtual void func1() { cout << "Derive::func1()" << endl; }
virtual void func3() { cout << "Derive::func3()" << endl; }
virtual void func4() { cout << "Derive::func4()" << endl; }
private:
int _b;
};

根据上图发现:只有重写的函数在虚函数表中被覆盖,其他都是正常继承的,然后自己类中多出来的虚函数(普通函数不进入)放在继承的下面 。

注意:基类的虚函数和派生类的虚函数都是生成了的,覆盖的意义就是把原本基类虚函数的地址改为派生类虚函数的地址

过程:

1.派生类先把基类的表继承下来,所以现在都是基类的虚函数。

2.把重写基类的虚函数在表中进行覆盖,此时原本被继承下来的虚函数的地址改为了派生类虚函数的地址。

3.派生类自己有的虚函数放在后面。

注意:在部分编译器中派生类自己的新增的虚函数可能不会在监视窗口中显示。所以此时可以使用内存级的监视窗口查看,可以找到在虚函数表中是有对应的函数地址的。 当然你也可以使用打印的方式

typedef void(*VFPTR)(); //虚函数指针类型重命名
//打印虚表地址及其内容
void PrintVFT(VFPTR* ptr)
{
printf("虚表地址:%p\n", ptr);
for (int i = 0; ptr[i] != nullptr; i++)
{
printf("ptr[%d]:%p-->", i, ptr[i]); //打印虚表当中的虚函数地址
ptr[i](); //使用虚函数地址调用虚函数
}
printf("\n");
}
int main()
{
Base b;
PrintVFT((VFPTR*)(*(int*)&b)); //打印基类对象b的虚表地址及其内容
Derive d;
PrintVFT((VFPTR*)(*(int*)&d)); //打印派生类对象d的虚表地址及其内容
return 0;
}

这个方法首先就是定义了重命名了一个虚函数指针(4字节),然后通过for循环每次打印一个指针大小的内容。

多继承的虚函数表

多继承就是一个类继承了不止一个类。

//基类1
class Base1
{
public:
virtual void func1() { cout << "Base1::func1()" << endl; }
virtual void func2() { cout << "Base1::func2()" << endl; }
private:
int _b1;
};
//基类2
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;
};

其实很好理解:如果继承了一个类会出现一个虚函数表那继承了两个类那就生成两个虚函数表就好了。

注意:虚函数表是一个地址,这个地址下存着虚函数的指针(可以想成存着指针的数组)

 

由上图就可以知道,虽然两个base中都有func1和func2,但是因为有不同的虚函数表所以覆盖的原理还是一样的。只是在于派生类自己多出来的虚函数,此时是存在第一张虚函数表中的而不是两表都存。 

当然有些编译器也会出现上述问题监视窗口不显示派生类新增的虚函数,此时依然可以用上面的两个方法,只是在定位时要注意找第二个表时要+sizeof(base1)跳过base1的内容找到base2.

菱形继承和菱形虚拟函数继承

class A
{
public:
virtual void funcA()
{
cout << "A::funcA()" << endl;
}
private:
int _a;
};
class B : virtual public A
{
public:
virtual void funcA()
{
cout << "B::funcA()" << endl;
}
virtual void funcB()
{
cout << "B::funcB()" << endl;
}
private:
int _b;
};
class C : virtual public A
{
public:
virtual void funcA()
{
cout << "C::funcA()" << endl;
}
virtual void funcC()
{
cout << "C::funcC()" << endl;
}
private:
int _c;
};
class D : public B, public C
{
public:
virtual void funcA()
{
cout << "D::funcA()" << endl;
}
virtual void funcD()
{
cout << "D::funcD()" << endl;
}
private:
int _d;
};

菱形继承就是两个父类同时继承了同一个类,此时派生类同时继承了这两个父类。

 A类对象的成员分布:

B类对象的成员分布:

C类对象的成员分布:

D类对象的成员分布:

虚基表:菱形继承防止二义性和数据冗杂的。

虚表:多态时用于存储虚函数的。 

先说菱形继承:因为菱形继承两个父类是继承于同一个类BC类中都有A对象,此时如果直接继承在D中不就出现了两份A类对象吗?为了防止这样的数据冗余和二义性,所以干脆就只存一个A类对象,所以此时在D类对象中BC类要找到A对象所以就出现了虚基表(里面存的是BC类对象找到A类对象的距离)。 

菱形虚函数继承:因为A类对象在D中单独一份了,所以D对funcA的重写是单独的。所以BC中不存在A的虚函数了,对于BC中的内容实际和多继承相同。最后多了两个虚基表用于让BC找到A对象。

注意:

正常来说不要设计出菱形虚拟继承和菱形继承,语法太复杂容易出错,同时在虚继承下由于多态在调用函数时是有多余的损耗的。


原文地址:https://blog.csdn.net/a1275174052/article/details/143573671

免责声明:本站文章内容转载自网络资源,如本站内容侵犯了原著者的合法权益,可联系本站删除。更多内容请关注自学内容网(zxcms.com)!