- C++知识点 -- 多态
- 一、多态概念
- 1.概念
- 二、多态的定义及实现
- 1、多态的构成条件
- 2、虚函数
- 3、虚函数的重写
- 4.虚函数重写的特例
- 5、不符合多态的场景
- 6、C++11的override和final
- 7、重载、覆盖(重写)、隐藏(重定义)的对比
- 8、例题
- 三、抽象类
- 四、多态的原理
- 1、虚函数表
- 2、多态的原理
- 五、单继承和多继承关系的虚函数表
- 1、单继承中的虚表
- 2、多继承中的虚表
- 六、多态常见面试问题
- 1.例题
- 2.inline函数可以是虚函数吗
- 3.静态成员函数可以是虚函数吗
- 4.构造函数可以是虚函数吗
- 5.析构函数可以是虚函数吗
- 6.拷贝构造和赋值可以是虚函数吗
- 7.对象访问普通函数快还是虚函数快
- 8.虚函数表是在什么阶段生成的,存在哪里
一、多态概念 1.概念
多态就是完成某个行为时,不同对象去完成时会产生不同的状态。
比如买票,普通人买全价票,学生买半价票,军人优先买票。
多态是在不同继承关系的类对象,去调用同一函数,产生不同的行为。
继承中构成多态还有两个条件:
1.必须通过基类的指针或引用去调用虚函数;
2.被调用的函数必须是虚函数,且派生类必须对基类的虚函数进行重写;
用virtual关键字修饰的成员函数就是虚函数。
代码如下:
class Person
{public:
virtual void BuyTicket()
{cout<< "买票 - 全价"<< endl;
}
};
3、虚函数的重写派生类中的虚函数构成重写(覆盖)的条件有:函数名、参数和返回值类型相同,但是函数的实现不同;
如果不构成重写,就是隐藏关系;
代码如下:
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 Test()
{Person p;
Func(p);
Student st;
Func(st);
Soldier sd;
Func(sd);
}
以上代码就完整的构成了多态,其运行效果为:
不同的对象调用同一个虚函数,呈现出了不同的效果。
1.将子类中虚函数的virtual去掉
class Person
{public:
virtual void BuyTicket()
{cout<< "买票 - 全价"<< endl;
}
};
class Student : public Person
{public:
void BuyTicket()
{cout<< "买票 - 半价"<< endl;
}
};
这样依然构成重写,子类中依然是虚函数,编译器认为先把父类的虚函数继承下来了,而且是接口继承,将函数接口完整继承下来了,子类中只是将函数的实现进行重写。
2.重写的协变
返回值类型可以不同,要求必须是父子关系的指针或引用;
代码如下:
class Person
{public:
virtual Person* BuyTicket()
{cout<< "买票 - 全价"<< endl;
}
};
class Student : public Person
{public:
virtual void BuyTicket()
{cout<< "买票 - 半价"<< endl;
}
};
以上的代码是会报错的,因为只满足了返回值类型不同,并不是父子关系的指针或引用,下面的代码才是协变:
class Person
{public:
virtual Person* BuyTicket()
{cout<< "买票 - 全价"<< endl;
return this;
}
};
class Student : public Person
{public:
virtual Student* BuyTicket() //返回值是有父子关系的指针或引用
{cout<< "买票 - 半价"<< endl;
return this;
}
};
运行结果为:
上面的代码依然能够构成多态。
3.析构函数的重写
建议在继承中将析构函数定义为虚函数;
class Person
{public:
virtual ~Person()
{cout<< "~Person()"<< endl;
}
};
class Student : public Person
{public:
virtual ~Student() //子类的析构函数与父类的析构函数的函数名并不相同
{cout<< "~Student()"<< endl;
}
};
int main()
{Person* p1 = new Person;
delete p1;
Person* p2 = new Student;//父类指针指向子类对象,符合多态调用
delete p2;
return 0;
}
以上代码的运行结果为:
子类和父类的析构函数,参数类型和返回值类型都相同,编译器为了让他们构成重写,将析构函数名改写为destructor,所以,上述代码中析构函数完成了重写。
只有子类析构函数重写了父类的析构函数,这里才能正确调用,指针指向父类对象,调用父类的析构函数,指向子类对象就调用子类的析构函数。
如果析构函数不是虚函数:
class Person
{public:
~Person()
{cout<< "~Person()"<< endl;
}
};
class Student : public Person
{public:
~Student() //子类的析构函数与父类的析构函数的函数名并不相同
{cout<< "~Student()"<< endl;
}
};
int main()
{Person* ptr1 = new Person;
delete ptr1;
Person* ptr2 = new Student;//父类指针指向子类对象,符合多态调用
delete ptr2;
return 0;
}
在子类delete时,调用的还是父类的析构函数:
这里是普通调用,不符合多态,在编译时就决定了;
ptr2是Person*类型的指针,call的是Person的析构函数;
ptr1希望调用父类的析构,ptr2希望调用子类的析构,所以把析构设计成符合多态的函数名。
1.不是父类的指针或引用调用虚函数
代码如下:
class Person
{public:
virtual void BuyTicket()
{cout<< "买票 - 全价"<< endl;
}
};
class Student : public Person
{public:
virtual void BuyTicket()
{cout<< "买票 - 半价"<< endl;
}
};
void Func(Person p)
{p.BuyTicket();
}
运行结果为:
上述代码是不构成多态的。
2.不符合虚函数重写
2.1将父类虚函数的virtual去掉
class Person
{public:
void BuyTicket()
{cout<< "买票 - 全价"<< endl;
}
};
class Student : public Person
{public:
virtual void BuyTicket()
{cout<< "买票 - 半价"<< endl;
}
};
void Func(Person& p)
{p.BuyTicket();
}
运行结果为:
是不符合虚函数重写的,自然就不构成多态。
2.2参数类型不同
class Person
{public:
void BuyTicket(char)
{cout<< "买票 - 全价"<< endl;
}
};
class Student : public Person
{public:
virtual void BuyTicket(int)
{cout<< "买票 - 半价"<< endl;
}
};
void Func(Person& p)
{p.BuyTicket();
}
运行结果为:
同样不符合多态。
1.final:修饰虚函数,表示其不能再被重写(用的很少)
class Person
{public:
virtual void BuyTicket() final
{cout<< "买票 - 全价"<< endl;
}
};
class Student : public Person
{public:
virtual void BuyTicket()
{cout<< "买票 - 半价"<< endl;
}
};
编译之后会报错:
2.override:检查派生类虚函数是否重写了某个基类的虚函数,若没有重写编译报错(常用)
class Person
{public:
virtual void BuyTicket(int)
{cout<< "买票 - 全价"<< endl;
}
};
class Student : public Person
{public:
virtual void BuyTicket(char) override//参数类型不一致,未完成重写
{cout<< "买票 - 全价"<< endl;
}
};
上述代码子类的虚函数未完成重写,在后面加了override后,编译器就会报错:
override常用于检查子类虚函数重写的语法是否正确。
代码如下:
#includeusing namespace std;
class A
{public:
virtual void func(int val = 1)
{cout<< "A ->"<< val<< endl;
}
virtual void test()
{func();
}
};
class B : public A
{public:
virtual void func(int val = 0)
{cout<< "B ->"<< val<< endl;
}
};
int main()
{B* p = new B;
p->test();
return 0;
}
以上代码的输出结果为:
分析:
- A为父类,B公有继承A,继承了A的func和test函数,其中A和B的func函数构成了虚函数重写(不要求参数的缺省值相同),因此构成了多态;
- main函数中,B指针p指向B对象,用p调用了test函数,p的类型是B,而test中this指针的类型是A*,p传给this,这里用父类指针指向子类对象,构成了切片;
- 这里this指针是A*类型的,用this调用func函数,符合父类指针调用虚函数,符合多态调用,多态调用时,指针指向那个类对象,就调用哪个类中的虚函数,显然p和this指向的都是子类对象,所以这里调用的是子类中的虚函数;
- 虚函数是接口继承,普通函数数实现继承;虚函数继承时,直接将父类的函数接口继承下来,与子类的接口是无关的,子类重写的是实现,这里的接口是父类的接口,val的缺省值是1,因此函数最终的输出结果为:B ->1,选B。
如果将代码改成以下形式:
class A
{public:
virtual void func(int val)//去掉缺省值
{cout<< "A ->"<< val<< endl;
}
virtual void test()
{func(1);
}
};
class B : public A
{public:
void func(int val)
{cout<< "B ->"<< val<< endl;
}
};
int main()
{//Test();
A* p = new B;//用父类的指针指向子类对象
p->test();
return 0;
}
子类和父类的func依然构成虚函数重写;
这里用父类的指针指向子类对象,发生了切片,但指向的还是子类的对象,所以调用的函数还是子类中的虚函数,结果还是:B ->1;
最终结果与p的指针类型无关,只与它指向的对象有关。
在虚函数的后面写上 = 0,这个函数就是纯虚函数,包含纯虚函数的类叫做抽象类(接口类),抽象类不能实例化出对象,派生类继承抽象类后也不能实例化出对象,只有派生类重写了虚函数,才能实例化对象,纯虚函数规范了派生类必须重写,更好的体现出了接口继承。
代码如下:
class Car //把不想实例化出对象的父类定义为抽象类
{public:
virtual void Drive() = 0;
};
class Benz : public Car
{public:
virtual void Drive(int) //如果子类继承了抽象类却未完成虚函数重写,就会报错
{cout<< "Benz - 舒适"<< endl;
}
};
class BMW : public Car
{public:
virtual void Drive()
{cout<< "BMW - 操控"<< endl;
}
};
int main()
{Car c1;
Benz c2;
BMW c3;
return 0;
}
1.抽象类一般用于定义接口,将不想实例化出对象的类定义为抽象类;
2.抽象函数强制子类完成虚函数的重写,不重写就无法实例化,而override是检查语法是否完成重写;
创建如下对象:
class Base
{public:
virtual void func()
{cout<< "func"<< endl;
}
private:
int _b = 0;
};
int main()
{Base b;
cout<< sizeof(b)<< endl;
return 0;
}
我么可以发现sizeof(b)的结果是8,再看b对象实例化后的成员
可以发现在成员_b的上面还有一个_vfptr的成员,这叫做虚函数表指针;带有虚函数的类对象,其成员中都有一个虚函数表指针,因为选虚函数要放到虚函数表中,也简称虚表。
将Base继承给子类,代码如下:
class Base
{public:
virtual void func1()
{cout<< "Base::func1"<< endl;
}
virtual void func2() //加一个虚函数func2
{cout<< "Base::func2"<< endl;
}
void func3() //加一个普通函数func3
{cout<< "Base::func3"<< endl;
}
private:
int _b = 1;
};
class Derive : public Base
{public:
virtual void func1() //重写父类虚函数
{cout<< "Derive::func1"<< endl;
}
private:
int _d = 2;
};
int main()
{Base b;
cout<< sizeof(b)<< endl;
Derive d;
return 0;
}
通过监视窗口我们可以看到:
1.子类对象d中也有一个虚函数表指针,且和父类对象b的虚表指针不同,由于子类对func1完成了重写,虚表中的func1就是子类重写后的Detive::func1;
2.func2是虚函数,继承下来也会放进子类的虚表,而func3不是虚函数,不会放进虚表;
3.虚表本身是一个放函数指针的数组,一般情况最后会放一个nullptr(vs环境下);
4.虚表存放的是虚函数的函数指针,不是虚函数,虚函数跟普通函数一样,都存放在代码段。
通过对汇编代码的分析,我们可以总结出:
1.满足多态以后的函数调用,不是在编译时确定的,是运行起来以后再到对象中找的,程序运行时取对象中的虚表指针找到函数地址,再去调用;
2.普通函数的调用,是在编译链接时就确定函数的地址,运行时直接调用。
代码如下:
class Base
{public:
virtual void func1()
{cout<< "Base::func1"<< endl;
}
virtual void func2()
{cout<< "Base::func2"<< endl;
}
private:
int _b = 1;
};
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 _d = 2;
};
通过监视窗口看不见func3和func4,我们可以使用代码打印虚表中的函数:
class Base
{public:
virtual void func1()
{cout<< "Base::func1"<< endl;
}
virtual void func2()
{cout<< "Base::func2"<< endl;
}
private:
int _b = 1;
};
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 _d = 2;
};
typedef void(*VFPTR) (); //将指向返回值为void、没有参数的类型的函数的指针重定义为VFPTR
void PrintVTable(VFPTR vTable[])
{//依次取虚表中的指针打印并调用,调用就可以看出存的是哪个函数
cout<< "虚表地址>"<< vTable<< endl;
for (int i = 0; vTable[i] != nullptr; i++)
{printf("第%d个虚函数地址:0x%x", i, vTable[i]);//打印地址
VFPTR f = vTable[i];//用函数指针取出虚函数地址
f();//调用
}
cout<< endl;
}
int main()
{Base b1;
Base b2;
Derive d;
VFPTR* vTableb1 = (VFPTR*)(*((int*)&b1));//将b对象的地址取出,强转成int*,再解引用,就取出了b的头四个字节的数据,这个就是指向虚表的指针
//再强转成VFPTR*,因为虚表就是VFPTR类型的数组
PrintVTable(vTableb1);
VFPTR* vTableb2 = (VFPTR*)(*((int*)&b2));
PrintVTable(vTableb2);
VFPTR* vTabled = (VFPTR*)(*((int*)&d));
PrintVTable(vTabled);
return 0;
}
我么可以看出,在vs下:
1.同一个类型的对象,共用一个虚表(b1和b2);
2.不管是否完成重写名子类虚表和父类虚表都不是同一个;
3.单继承中,子类的所有虚函数,包括重写父类的虚函数和未重写的虚函数,都放在同一个虚表中。
代码如下:
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、没有参数的类型的函数的指针重定义为VFPTR
void PrintVTable(VFPTR vTable[])
{//依次取虚表中的指针打印并调用,调用就可以看出存的是哪个函数
cout<< "虚表地址>"<< vTable<< endl;
for (int i = 0; vTable[i] != nullptr; i++)
{printf("第%d个虚函数地址:0x%x", i, vTable[i]);//打印地址
VFPTR f = vTable[i];//用函数指针取出虚函数地址
f();//调用
}
cout<< endl;
}
int main()
{Derive d;
VFPTR* vTabled1 = (VFPTR*)(*((int*)&d));
PrintVTable(vTabled1);
VFPTR* vTabled2 = (VFPTR*)(*(int*)((char*)&d + sizeof(Base1)));//从Base2的虚表中取虚函数地址
PrintVTable(vTabled2);
return 0;
}
Derive多继承Base1和Base2,其中Derive重写了func1,而func1既是Base1的虚函数,也是Base2的虚函数,func3是Derive自己的虚函数,运行结果如下:
可以看出在多继承下:
1.子类中每一个继承的父类都有自己的虚表,存放父类中的虚函数;
2.子类中重写的虚函数会覆盖子类中父类虚表对应的虚函数,Base1和Base2中的func1都没覆盖为了Derive::func1;
3.子类中继承的Base1中的func1和Base2中的func1的地址不同,但它们都是Derive重写后的虚函数,最终调用的是同一个func1,只是中间多了一个步骤;
4.子类未重写的的虚函数放在第一个继承的父类的虚表中;
以下程序的输出结果是:
B和C都是虚继承A,D多继承B和C,所以B和C在D中共享一个A,所以B和C都不能去初始化D中的A对象,只能在D中单独进行A的初始化;
初始化是按照类声明的顺序来的,不是按照初始化列表的顺序,所以在D中先初始化A对象,在初始化B和C,这事就不会重复初始化A了,最后初始化D,所以答案选A。
可以,inline函数是没有地址的,而且inline只是对编译器的一个建议,当一个inline函数是虚函数时,在多态调用以后,inline就失效了,因为虚函数要放进虚表中。
3.静态成员函数可以是虚函数吗不可以,static函数没有this指针,可以直接使用类名::函数名()的方式调用,而使用类名::函数名()的方式无法访问对象的虚表,因此静态成员函数无法放进虚表,虚函数是为了实现多态,多态运行时都是去虚表中找决议,静态成员函数都是在编译时就决议了,因此它是虚函数没有价值。
4.构造函数可以是虚函数吗不可以,因为虚函数是为了实现多态调用,运行时去虚表中找对应的虚函数进行调用,对象中的虚表指针都是在构造函数初始化列表阶段才初始化的,构造函数是虚函数没有意义。
5.析构函数可以是虚函数吗可以,并且最好把基类的析构函数定义为虚函数,详情参考 二-5-3。
6.拷贝构造和赋值可以是虚函数吗拷贝构造不可以,因为拷贝构造也是构造函数,参考上面的构造函数;
赋值重载operator==()可以,但是没有实际价值。
如果虚函数不构成多态,是一样快的;
如果虚函数构成多态,调用普通函数比较快,因为构成多态调用虚函数时,运行中需要到虚表中去查找。
虚函数表是在编译阶段就生成好的,存在代码段(常量区);
构造函数初始化列表阶段初始化的是虚函数表指针,对象中存的也是虚函数表指针。
你是否还在寻找稳定的海外服务器提供商?创新互联www.cdcxhl.cn海外机房具备T级流量清洗系统配攻击溯源,准确流量调度确保服务器高可用性,企业级服务器适合批量采购,新人活动首月15元起,快前往官网查看详情吧
文章名称:C++知识点--多态-创新互联
本文网址:http://lswzjz.com/article/ddoego.html