目录
一、多态定义
多态是函数调用的多种形态,使我们调用函数更加灵活。多态分为两种:静态多态和动态多态
1.静态多态
静态多态即函数重载,这里的静态是指编译时:
void Swap(int& s1, int& s2)
{
int temp = s1;
s1 = s2;
s2 = temp;
}
void Swap(float& s1, float& s2)
{
float temp = s1;
s1 = s2;
s2 = temp;
}
void Swap(char& s1, char& s2)
{
char temp = s1;
s1 = s2;
s2 = temp;
}
int main()
{
int a = 1, b = 2;
float c = 3.0, d = 4.0;
char e = 'z', f = 'Z';
Swap(a, b);
Swap(c, d);
Swap(e, f);
return 0;
}
看起来我们用的是一个函数,实际上这就是静态多态
2.动态多态
动态多态是指不同类型对象完成一件事的时候产生的动作不一样,那么结果也不一样。
在继承中要构成多态有两个条件,缺一不可:
(1)必须通过父类的指针或者引用调用虚函数
(2)被调用的函数必须是虚函数,且子类必须对父类的虚函数进行重写
动态多态父类指针或引用的指向:
(1)父类指针或引用指向父类,调用的就是父类虚函数
(2)父类指针或引用指向哪个子类,调用的就是哪个子类重写的虚函数
根据切片规则,父类的指针既可以指向父类,又可以指向子类,如果有多个子类,就可以指向不同类型。
(1)虚函数
被virtual修饰的成员函数叫做虚函数。
注意:
(1)只有类的非静态成员函数才可以被 virtual修饰,普通函数不可以。
(2)虽然虚函数的virtual和虚继承中的virtual是同一个关键字,但是它们之间没有关系。虚函数的vitual是为了实现多态,虚继承中的virtual是为了解决菱形继承的数据冗余和二义性。
(2)虚函数的重写
虚函数重写也叫做覆盖,在子类中重写了一个和父类中的虚函数完全相同的虚函数:包括函数名、返回值、参数列表都相同,这时候子类就重写了父类的虚函数。
注意:子类重写的虚函数的函数名、返回值、参数列表和父类一定要完全相同,否则就变成了函数重载,和继承无关。
如下,Bird类和Dog类就重写了父类的虚函数:
class Animal
{
public:
virtual void Speak()//父类虚函数
{
cout << "speak" << endl;
}
};
class Bird:public Animal
{
public:
virtual void Speak()//子类重写父类虚函数
{
cout << "chirp" << endl;
}
};
class Dog :public Animal
{
public:
virtual void Speak()//子类重写父类虚函数
{
cout << "bark" << endl;
}
};
//父类对象:会破坏多态条件,不构成多态
void fun(Animal a)
{
//传不同类型的对象,调用的是不同的函数,实现了调用的多种形态
a.Speak();
}
//父类引用-构成多态
void fun1(Animal& a)
{
//传不同类型的对象,调用的是不同的函数,实现了调用的多种形态
a.Speak();
}
//父类指针-构成多态
void fun2(Animal* pa)
{
//传不同类型的对象,调用的是不同的函数,实现了调用的多种形态
pa->Speak();
}
int main()
{
Animal a;
Bird b;
Dog d;
Animal *pa = &a;
Bird *pb = &b;
Dog *pd = &d;
fun(a);
fun(b);
fun(d);
cout << endl;
fun1(a);
fun1(b);
fun1(d);
cout << endl;
fun2(pa);
fun2(pb);
fun2(pd);
return 0;
}
如果去掉父类的虚函数关键字virtual,子类就没有重写父类的虚函数,那么就算传父类指针或父类引用也不会构成多态:
class Animal
{
public:
void Speak()//父类普通函数
{
cout << "speak" << endl;
}
};
class Bird:public Animal
{
public:
void Speak()//子类普通函数
{
cout << "chirp" << endl;
}
};
class Dog :public Animal
{
public:
void Speak()//子类普通函数
{
cout << "bark" << endl;
}
};
//父类对象:会破坏多态条件,不构成多态
void fun(Animal a)
{
//传不同类型的对象,调用的是不同的函数,实现了调用的多种形态
a.Speak();
}
//父类引用-构成多态
void fun1(Animal& a)
{
//传不同类型的对象,调用的是不同的函数,实现了调用的多种形态
a.Speak();
}
//父类指针-构成多态
void fun2(Animal* pa)
{
//传不同类型的对象,调用的是不同的函数,实现了调用的多种形态
pa->Speak();
}
int main()
{
Animal a;
Bird b;
Dog d;
Animal *pa = &a;
Bird *pb = &b;
Dog *pd = &d;
fun(a);
fun(b);
fun(d);
cout << endl;
fun1(a);
fun1(b);
fun1(d);
cout << endl;
fun2(pa);
fun2(pb);
fun2(pd);
return 0;
}
(3)虚函数重写的两个例外
① 协变(返回值类型是父子关系)
子类重写父类虚函数时,与父类虚函数返回值类型不同。即父类虚函数返回父类对象的指针或者引用,子类虚函数返回子类对象的指针或者引用时,称为协变。
// 例外 -- 协变:返回值可以不同,必须是父子关系的指针或引用
// 不一定是需要指定的父子类,也可以是单独的父子类
class A {};
class B : public A {};
class Person1
{
public:
//virtual Person1* BuyTicket()
virtual A* BuyTicket()
{
cout << "买票-全价" << endl;
return nullptr;
//return this;
}
};
class Student1 : public Person1
{
public:
//virtual Student1* BuyTicket()
virtual B* BuyTicket()
{
cout << "买票-半价" << endl;
return nullptr;
//return this;
}
};
void F2(Person1* p)
{
p->BuyTicket();
delete p;
}
int main()
{
F2(new Person1);
F2(new Student1);
}
所以虚函数重写后的返回值不一定相同 ,因为有协变的存在。
②析构函数的重写(子类与父类析构函数的名字不同)
如果父类的析构函数为虚函数,此时子类析构函数只要定义,无论是否加virtual关键字,都与父类的析构函数构成重写,虽然父类与子类析构函数名字不同。虽然函数名不相同,看起来违背了重写的规则,其实不然,这里可以理解为编译器对析构函数的名称做了特殊处理,编译后析构函数的名称统一处理成destructor。
把父类析构函数定义为虚函数,子类就可以重写父类的虚函数:
class Animal
{
public:
virtual ~Animal()
{
cout << "~Animal()" << endl;
}
};
class Bird :public Animal
{
public:
virtual ~Bird()//Bird类和Animal类的析构函数名看起来不同,但是他们构成虚构函数重写
{
cout << "~Bird()" << endl;
}
};
int main()
{
Animal a;
Bird b;
return 0;
}
由于析构时,子类对象先调用自己的析构函数进行清理,清理完后再自动调用父类的析构函数,所以打印的前两行是Bird类对象调用的析构函数,第3行调用的是Animal类对象的析构函数。
但是发现把父类和子类中的virtual都去掉,运行结果还是一样。说明在普通场景下,父类和子类的析构函数是否是虚函数,是否构成重写,没什么影响。
那在什么场景下才有影响呢?
class Animal
{
public:
~Animal()
{
cout << "~Animal()" << endl;
}
};
class Bird :public Animal
{
public:
~Bird()//Bird类和Animal类的析构函数名看起来不同,但是他们构成虚构函数重写
{
cout << "~Bird()" << endl;
}
};
int main()
{
Animal* pa = new Animal;
Animal* pb = new Bird;
//多态行为
delete pa;//pa->析构函数() + operator delete(pa)
delete pb;//pb->析构函数() + operator delete(pb)
return 0;
}
delete在释放空间的同时要调用析构函数,需要做两步操作:
(1)先调用析构函数
(2)再释放空间
pa和pb的空间都会被释放,pa指向父类对象,期望调用父类的析构函数,pb指向子类对象,期望调用子类的析构函数,指向父类调父类,指向子类调子类,期望这里达到多态行为,虽然没有明显的函数调用,但是delete操作调了析构函数。
pb指向子类对象,但是发现没有调用子类析构函数,可能存在内存泄漏:
当子类析构函数不需要清理资源时也就没什么问题,但是当子类析构函数需要清理时,这样做会存在内存泄漏 。因此多态场景下子类和父类的析构函数最好加上virtual关键字完成虚函数重写就不会导致内存泄漏了。所以上面的代码最好不要删掉析构函数前面的virtual。
另外在继承一文中,说过子类和父类的析构函数构成隐藏。原因就是表面上子类的析构函数个父类的析构函数名不同,但是为了构成重写,编译器会对析构函数名调用时,统一将父类和子类的析构函数名改成destructor( )。统一改成destructor( )构成隐藏的目的就是在这里能够调用同一个函数,达到多态指向父类对象就调父类对象,指向子类对象就调子类对象的的目的。
因此,父类函数中的virtual不能省,否则子类继承不了父类的virtual属性,无法重写父类的虚函数,如果这个函数是析构函数,那么还会造成内存泄漏。为了保持统一,父类和子类虚函数前面的virtual都不要省。
(4)C++11的final和override
①final:如果一个虚函数不想被重写,可以在虚函数后面加final
class Animal
{
public:
virtual ~Animal() final//虚函数不想被重写
{
cout << "~Animal()" << endl;
}
};
class Bird :public Animal
{
public:
virtual ~Bird()
{
cout << "~Bird()" << endl;
}
};
一旦重写final修饰的虚函数,就会报错:
如果一个类不想被继承,可以在这个类后面加final
class Animal final//Animal类不想被继承
{};
class Bird :public Animal
{};
②override:检查派生类虚函数是否重写了基类某个虚函数,如果没有重写编译报错
class Animal
{
public:
virtual void Speak()
{
cout << "speak" << endl;
}
};
class Bird :public Animal
{
public:
virtual void Speak(int i) override//用来检查子类是否完成父类虚函数的重写
{
cout << "chirp" << endl;
}
};
由于子类重写的虚函数的参数列表和父类虚函数的参数列表不同,导致子类没有成功完成重写父类的虚函数,override检查会报错:
(5)重载、重写与隐藏
重载、重写和隐藏的对比:
二、抽象类
1.纯虚函数
(1)纯虚函数
定义:在虚函数的后面写上 =0
(2)抽象类(接口类):
定义:包含纯虚函数的类
性质:抽象类不能实例化出对象。子类继承抽象类后也不能实例化出对象,只有重写纯虚函数,子类才能实例化出对象。
意义:
① 能够更好地去表示现实世界中没有实例对象是我抽象类型,如:植物、人、动物
② 体现接口继承,强制子类去重写虚函数(就算不重写,子类也是抽象类)
class Animal//抽象类
{
public:
virtual void Speak() = 0;//纯虚函数
};
class Bird :public Animal
{
public://没有重写纯虚函数
};
class Dog :public Animal
{
public:
virtual void Speak()//子类重写父类虚函数
{
cout << "bark" << endl;
}
};
抽象类不能实例化出对象:
int main()
{
Animal a;
return 0;
}
当子类没有重写父类的纯虚函数时,直接把父类的虚函数继承下来了,这个虚函数也是纯虚函数,那么这个子类就是抽象类,不能实例化出对象:
int main()
{
Bird b;
return 0;
}
当子类重写了父类虚函数:
class Animal//抽象类
{
public:
virtual void Speak() = 0;//纯虚函数
};
class Bird :public Animal
{
public:
virtual void Speak()//子类重写父类纯虚函数
{
cout << "chirp" << endl;
}
};
class Dog :public Animal
{
public:
virtual void Speak()//子类重写父类纯虚函数
{
cout << "bark" << endl;
}
};
int main()
{
Animal* pBird = new Bird;
pBird->Speak();
Animal* pDog = new Dog;
pDog->Speak();
return 0;
}
pBird 和pDog是指向父类的指针,调用了子类虚函数,看起来像是调用了同一个虚函数Speak( )。
2.接口继承和实现继承
(1)实现继承
普通函数的继承是实现继承,不是接口继承,继承的是函数的实现,可以直接使用这个函数,也是一种复用。
(2)接口继承
虚函数包括纯虚函数的继承是接口继承,子类仅仅只继承了父类接口 ,父类没有实现这个接口函数,子类要对纯虚函数进行重写达到多态的目的。
注意: 如果为了达到多态目的,那可以把父类接口定义成虚函数,并且定义了后,子类必须要重写父类的虚函数,否则就不要把普通函数定义成虚函数。
三、多态原理
1.虚函数表
了解多态原理前需要了解虚函数表。
类的大小只包含成员变量的大小,不会包含成员函数的大小,那么下面的代码应该打印4
class base
{
public:
virtual void Func1()
{
cout << "Func1()" << endl;
}
private:
int _b = 1;
};
int main()
{
base b
// 大小多少?
cout << sizeof(b) << endl; // 12
// 为啥是12捏?还有个虚函数表指针成员,大小四字节
return 0;
}
对象a只有一个成员变量_b,占4个字节。通过监视看到,对象base里面包含两个成员 ,那么另外4个字节一定是_vfptr占用的,且_vfptr里面存放的是一个地址,那么_vfptr一定是个指针:
_vfptr叫做虚函数表指针,其中v是virtual的缩写,f是function的缩写。
虚函数表也简称虚表。
由于虚函数的地址要被放到虚函数表中,因此一个含有虚函数的类中都至少有一个虚函数表指针,这个虚函数表指针指向一个虚函数。虚函数表指针用来实现多态。
那么子类的虚表中都存放了什么呢?对于如下代码
class Animal
{
public:
virtual void Speak()//父类虚函数
{
cout << "speak" << endl;
}
virtual void run()//父类虚函数
{
cout << "run" << endl;
}
void jump()//父类普通函数
{
cout << "jump" << endl;
}
public:
int legs;
};
class Bird :public Animal
{
public:
virtual void Speak()//子类重写父类虚函数
{
cout << "chirp" << endl;
}
public:
string color;
};
int main()
{
Animal a;
Bird b;
return 0;
}
从监视可以发现:
1.子类对象b中也有一个虚表指针,b对象由两部分构成,一部分是父类继承下来的成员,另一部分是自己的成员。
2.父类a对象和子类对象b虚表是不一样的,Speak完成了重写,所以b的虚表中存的是重写的Bird::Speak,所以虚函数的重写也叫作覆盖,覆盖就是指虚表中虚函数的覆盖。重写是语法的叫法,覆盖是原理层的叫法。
3. Run继承下来后是虚函数,所以放进了虚表,Jump也继承下来了,但是不是虚函数,所以不会放进虚表。
4.虚函数表本质是一个存虚函数指针的指针数组,这个数组最后面放了一个nullptr。
5.总结一下派生类的虚表生成:
a.先将基类中的虚表内容拷贝一份到派生类虚表中
b.如果派生类重写了基类中某个虚函数,用派生类自己的虚函数覆盖虚表中基类的虚函数
c.派生类自己新增加的虚函数按其在派生类中的声明次序增加到派生类虚表的最后。
6.虚函数存在哪的?虚表存在哪的?
虚表存的是虚函数指针,不是虚函数,虚函数和普通函数一样的,都是存在代码段的,只是它的指针又存到了虚表中。另外对象中存的不是虚表,存的是虚表指针。虚表在vs下存在代码段里。
2.原理
(1)构成多态
对于如下代码,子类重写了父类虚函数,且通过父类指针调用虚函数,这就满足了多态的两个条件
class Animal
{
public:
virtual void Speak()//父类虚函数
{
cout << "speak" << endl;
}
};
class Bird :public Animal
{
public:
virtual void Speak()//子类重写父类虚函数
{
cout << "chirp" << endl;
}
};
void func(Animal* pa)
{
pa->Speak();//通过父类指针调用虚函数
}
int main()
{
Animal a;
func(&a);
Bird b;
func(&b);
return 0;
}
为什么引用是父类就调父类的Speak,是子类就调子类的Speak呢?
对象a和对象b里面都没有其他成员,只有虚表指针,都是4字节。子类完成父类虚函数重写以后,子类的虚表指针指向的是重写了的子类虚函数:
指针或引用调用虚函数是怎么调的呢?
指针或引用调用虚函数时,不是编译时确定,而是运行时才到指向的对象的虚表中找对应的虚函数调用,当指针或引用指向父类对象时,调用的就是父类的虚表中的虚函数,当指针或引用指向子类对象时,调用的就是子类虚表中的虚函数
(2)不构成多态
① 如果子类没有重写父类虚函数:
class Animal
{
public:
virtual void Speak()//父类虚函数
{
cout << "speak" << endl;
}
};
class Bird :public Animal
{
public:
};
void func(Animal* pa)
{
pa->Speak();
}
int main()
{
Animal a;
func(&a);
Bird b;
func(&b);
return 0;
}
这时就破坏了多态的条件,那么子类也有虚表,但是子类虚表里的指针存的是Animal的虚函数,而不是Bird的虚函数。不满足多态条件时(子类没有重写父类虚函数/通过父类指针或引用调用虚函数),就不会到虚表里面去找,决定调哪个函数是在编译时确定这个函数的形参是哪个类型,而跟对象没有关系。
总结:
(1)构成多态,指向谁就调用谁的虚函数,跟对象有关
(2)当子类没有重写父类虚函数时,不构成多态,调用函数的入参类型是什么,调用的就是哪个的虚函数,跟对象无关,跟入参类型有关
② 不是通过父类指针或引用,而是通过父类对象调用虚函数
class Animal
{
public:
virtual void Speak()//父类虚函数
{
cout << "speak" << endl;
}
public:
int legs = 4;
};
class Bird :public Animal
{
public:
virtual void Speak()//子类重写父类虚函数
{
cout << "chirp" << endl;
}
public:
string color;
};
void func(Animal pa)//入参类型为父类对象
{
pa.Speak();
}
int main()
{
Animal a;
func(a);
Bird b;
b.legs = 2;
func(b);
return 0;
}
对象是无法实现出多态的,因为如果入参是子类对象,那么指针和引用会把父类那部分切出来,切出来后不是赋值,而是让指针指向子类里面父类的那部分,这个指针无论指向的是父类还是子类,看到的都是父类对象,给父类引用的就是父类对象,给子类引用的是切片出来的父类对象。
而构成多态时,引用和指针本身并不知道自己指向或引用的是父类对象还是子类对象,指向父类对象,那就指向或引用整个父类对象,指向子类对象,那就那看到的就是子类对象中父类那一部分,对于
a.Speak();
编译完成的指令是一样的,虽然传入的实参不同,但是看到的都是父类部分或者子类切片出来的父类部分,都是一样的动作,到对应的地方去找。
如果是对象的时候为什么不行,如果是对象涉及到切片问题,就把这个父类对象给你,你给我的是个子类对象,就把子类对象中的父类部分切片后给你,调用拷贝构造函数把父类部分切片出来,把父类成员给你;父类对象不会把虚表指针给过去,两者的虚表指针是一样的
父类对象调用完func后,pa的虚表指针存放的是父类的虚函数地址:
子类对象调用完func后,pa的虚表指针存放的还是父类的虚函数地址,但是成员变量被修改了:
这是因为多个同类型对象,只有一份虚表,因此虚表当中的内容是一样的,它们的虚表指针都指向这个虚表。
当是子类切片的时候,会把子类切出来的成员变量给func的形参(即父类对象),但不会把_vfptr给过去,因为只有一份虚表,假如切片后把虚表指针也给过去了,会出现混乱,它的虚表指针到底是父类的还是子类的,如果是直接定义出来的,那就是父类的,如果经过子类赋值,那就是子类的,但是父类对象的虚表里面怎么会有子类的虚函数呢?这显然不合理,因此不会把虚表给func的形参(父类对象)。
指针和引用是指向的,指向父类就是父类对象,指向子类就是子类当中切片出来的父类部分,让指针和引用去指向。而对象要拷贝构造,只是把值给过去。
总结:当通过父类对象调用虚函数,切片只会拷贝成员变量,不会拷贝虚表指针。
(3)汇编层面看多态
不构成多态时,编译时直接调用函数的地址
构成多态时,运行时到指向的对象的虚表中找到要调用的虚函数
四、单继承和多继承关系的虚函数表
1.单继承的虚函数表
(1)虚表初始化的时机
对象中虚表指针是在什么阶段初始化的?虚表在哪个阶段生成?
对于如下代码:
class Animal
{
public:
virtual void Speak()//父类虚函数
{
cout << "Animal::speak" << endl;
}
virtual void Run()//父类虚函数
{
cout << "Animal::run" << endl;
}
void Jump()//父类普通函数
{
cout << "Animal::jump" << endl;
}
public:
int legs = 4;
};
class Bird :public Animal
{
public:
virtual void Speak()//子类重写父类虚函数
{
cout << "Bird::chirp" << endl;
}
public:
string color = "Yellow";
};
int main()
{
Animal a;
Bird b;
return 0;
}
通过监视F11逐语句查看执行过程发现,定义对象a时,执行步骤如下:
(1)开始执行Animal的构造函数
(2)初始化Animal的成员
(3)将Animal构造函数执行完毕
发现执行完以上3步之后,虚表指针已经初始化了:
因此,虚表指针是在构造函数初始化列表阶段初始化的,虚表在编译时就已经生成了。
一个类中所有的虚函数地址,都会放到虚表中。虚表里面存放的是虚函数地址,虚函数和普通函数一样, 编译完成后,都放在代码段。
(2)子类虚表的生成过程
子类的虚表是如何生成的呢?
父类的虚表中存的是Aniaml的Speak( )和Run( )的地址。生成子类虚表时,会单独开辟一块空间,拷贝一份父类虚表过程中,会将对应虚函数位置覆盖成子类重写了父类的虚函数,如果子类没有重写,那么父类的虚函数就不会被覆盖,保留。所以子类虚表的生成过程是一个拷贝+覆盖的过程。
监视如上代码:
(1)子类重写了父类的Speak( )虚函数,所以子类会覆盖父类Speak( )位置;
(2)子类没有重写父类的Run( )虚函数,子类不会覆盖父类Run( )位置;
(3)父类的Jump( )不是虚函数, 不会出现在虚表中: 虚函数的重写是语法层的概念,覆盖是虚表实现层的概念。
在内存窗口输入虚表地址,发现里面存的是虚函数的地址,虚表作为数组,是如何知道数组结束的呢?VS在虚表结束位置放空指针,表示虚表结束了:
假如子类还有虚函数:
virtual void Fly()//飞
{
cout << "virtual Bird::fly" << endl;
}
virtual void Sing()//唱歌
{
cout << "virtual Bird::sing" << endl;
}
这两个虚函数既不是继承父类虚函数,也没有重写父类虚函数,通过监视看不到子类的这两个虚函数,但是通过内存可以看到:
也可以打印一下虚表中调用的函数:
// 打印虚表
//typedef void(*)() VF_PTR; -- 函数指针重命名不能这样定义
typedef void(*VF_PTR)(); // 声明或者重命名需防在中间
//void PintVFTable(VF_PTR tabale[])
void PintVFTable(VF_PTR* tabale)// 数组传参传首元素地址
{
for (int i = 0; tabale[i] != nullptr; ++i)
{
printf("虚表地址:%p\n",ptr);
printf("[%d]:%p->", i, tabale[i]);
VF_PTR f = tabale[i];
f();
}
cout << endl;
}
int main()
{
Animal a;
PrintVFT((VFunc*)(*((int*)&a)));
// 虚表本质是一个虚函数指针数组,存放的是虚函数地址(VF_PTR),对象里面存的是虚表地址,
// 虚表地址存放在对象的首四个字节(32位平台), 现在我们要打印出虚函数地址(VF_PTR),
// 我们得先拿到首四个字节:我们可以先拿到对象地址(&a), 然后强转成int*类型,
// 并解引用拿到int类型,然后再强转成VF_PTR*,这样才能传参成功
//
// 局限性:在不同平台下有差异。在64位平台下崩溃,32位下写死了是int,是四个字节,
// 64位下是8个字节,改进方法可以将int改成数据类型为8字节就行
// 也可以这样子:
//PintVFTable(*(VF_PTR**)&a);
// 虚表里面存的是虚函数地址(VF_PTR),虚表地址为VF_PTR*,我们要拿到虚表地址,
// 只需VF_PTR**,然后再对其解引用就拿到了VF_PTR*,就能传参成功
// 那为什么不直接 PintVFTable((VF_PTR*)&b) 捏?
// 我们要的是虚表地址,不是将对象指针强转了(VF_PTR*),这样子就成了传对象指针了
//
// 相较于上面方法优点:可以更好适应于不同平台
// VF_PTR**解引用就是VF_PTR*,VF_PTR*解引用就是VF_PTR,32位下就是四个字节,64位下就是8个字节
Bird b;
PrintVFT((VFunc*)(*((int*)&b)));
return 0;
}
2.多继承的虚函数表
以上是单继承,对于多继承,如何打印虚表函数:
class Animal
{
public:
virtual void Color()//颜色
{
cout << "virtual Animal::color" << endl;
}
virtual void Name()//名称
{
cout << "virtual Animal::name" << endl;
}
};
class Plant
{
public:
virtual void Color()//颜色
{
cout << "virtual Plant::color" << endl;
}
virtual void Name()//名称
{
cout << "virtual Plant::name" << endl;
}
};
class Coral :public Animal, public Plant
{
public:
virtual void Color()//子类重写Animal类虚函数
{
cout << "virtual Coral::color" << endl;
}
virtual void Shape()//子类重写Plant类虚函数
{
cout << "virtual Coral::shape" << endl;
}
};
typedef void(*VFunc)();//为虚表指针定义简洁的名称
void PrintVFT(VFunc* ptr)//传参虚函数指针数组
{
printf("虚表地址:%p\n", ptr);
for (int i = 0; ptr[i] != nullptr; i++)
{
printf("VFT[%d] : %p->", i, ptr[i]);
ptr[i]();
}
printf("\n");
}
int main()
{
//c继承了两个类,有两个虚表
//c的两张虚表,先继承了Animal,Animal在前面,正好Animal的头4个字节是虚表指针,Plant挨着Animal,Animal完了就是Plant
Coral c;
PrintVFT((VFunc*)(*((int*)&c)));
PrintVFT((VFunc*)(*(int*)((char*)&c + sizeof(Animal))));
//(char*)&c -- 取c的地址,强转成char*
//(char*)&c + sizeof(Animal) -- 取c的地址,强转成char*,再跨越一个Animal类的大小
return 0;
}
打印发现:
(1)两张虚表都重写了Color( )函数
(2)但两张虚表都没有重写Name( )函数,都直接继承了Name( )函数
(3)Shape( )虚函数只放在了第一张虚表中,第二张虚表没有放
五、多态面试题
1.选择题
1. 下面哪种面向对象的方法可以让你变得富有( )
A: 继承 B: 封装 C: 多态 D: 抽象
答案: A 当然是继承更富有啦
2. ( )是面向对象程序设计语言中的一种机制。这种机制实现了方法的定义与具体的对象无关,而对方法的调用则可以关联于具体的对象。
A: 继承 B: 模板 C: 对象的自身引用 D: 动态绑定
答案: D 动态绑定是函数调用时关联到具体对象
3. 面向对象设计中的继承和组合,下面说法错误的是?( )
A:继承允许我们覆盖重写父类的实现细节,父类的实现对于子类是可见的,是一种静态复用,也称为白盒复用
B:组合的对象不需要关心各自的实现细节,之间的关系是在运行时候才确定的,是一种动态复用,也称为黑盒复用
C:优先使用继承,而不是组合,是面向对象设计的第二原则
D:继承可以使子类能自动继承父类的接口,但在设计模式中认为这是一种破坏了父类的封装性的表现
答案: C 尽量少用继承,会破坏封装原则,多用组合,能降低耦合度
4. 以下关于纯虚函数的说法,正确的是( )
A:声明纯虚函数的类不能实例化 B:声明纯虚函数的类是虚基类
C:子类必须实现基类的 D:纯虚函数必须是空函数
答案: A 包含纯虚函数的类叫做抽象类,抽象类不能实例化出对象
5. 关于虚函数的描述正确的是( )
A:派生类的虚函数与基类的虚函数具有不同的参数个数和类型
B:内联函数不能是虚函数
C:派生类必须重新定义基类的虚函数
D:虚函数可以是一个static型的函数
答案: B inline函数没有地址,当inline成为虚函数后,虚表里面要放它的地址,构成多态时,根据虚函数表指针去call这个地址,就不能展开了,就忽略了内联属性,加了虚函数以后就不再是内联函数了。
6. 关于虚表说法正确的是( )
A:一个类只能有一张虚表
B:基类中有虚函数,如果子类中没有重写基类的虚函数,此时子类与基类共用同一张虚表
C:虚表是在运行期间动态生成的
D:一个类的不同对象共享该类的虚表
答案: D
对于A如果是多继承,那么这个类的对象会有多张虚表;
对于B,监视:
class Animal
{
public:
virtual void Color()//颜色
{
cout << "virtual Animal::color" << endl;
}
virtual void Name()//名称
{
cout << "virtual Animal::name" << endl;
}
};
class Coral :public Animal
{};
int main()
{
Animal a;
Coral c;
return 0;
}
发现虚表指针不同,虽然虚表指针中存放的虚函数地址相同:
对于C:虚表在编译时就已经生成了
对于D:
class Animal
{
public:
virtual void Color()//颜色
{
cout << "virtual Animal::color" << endl;
}
virtual void Name()//名称
{
cout << "virtual Animal::name" << endl;
}
};
int main()
{
Animal a;
Animal a1;
return 0;
}
监视发现:a和a1的虚表指针地址相同,虚表指针中存放的虚函数地址也相同
7. 假设A类中有虚函数,B继承自A,B重写A中的虚函数,也没有定义任何虚函数,则( )
A:A类对象的前4个字节存储虚表地址,B类对象前4个字节不是虚表地址
B:A类对象和B类对象前4个字节存储的都是虚基表的地址
C:A类对象和B类对象前4个字节存储的虚表地址相同
D:A类和B类中的内容完全一样,但是A类和B类使用的不是同一张虚表
答案:D 同第6题的B
8. 下面程序输出结果是什么? ()
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 子类构造函数必须调用父类的构造函数初始化父类的成员,因此执行D的构造函数前必须执行B和C的构造函数,执行B和C的构造函数前必须执行A的构造函数
9. 多继承中指针偏移问题?下面说法正确的是( )
class Base1 { public: int _b1; };
class Base2 { public: int _b2; };
class Derive : public Base1, public Base2 { public: int _d; };
int main()
{
Derive d;
Base1* p1 = &d;
Base2* p2 = &d;
Derive* p3 = &d;
return 0;
}
A:p1 == p2 == p3 B:p1 < p2 < p3 C:p1 == p3 != p2 D:p1 != p2 != p3
答案:C p3作为子类,普通多继承,示意图如下:Derive的成员包含Base1和Base2的成员
10. 以下程序输出结果是什么( )
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.以上都不正确
答案:B,p是子类指针,不是父类指针,test( )调用不构成多态。但有隐藏多态,p是子类指针,会把test传给this,抵用test之后会把p传给this,this在A里面,是父类指针,把p传给了this,父类指针指向了子类对象,父类的指针调func,是一个重写的虚函数,this是父类指针,子类继承了父类的虚函数,重写了函数实现,但是继承的是父类的接口定义,比如虽然子类的func函数没有加virtual,但是子类继承了父类的func函数,就是虚函数。所以就算子类重写父类虚函数时,给了缺省参数,但是根本不会用到。用的是父类接口定义+子类实现
2.问答题
1. 什么是多态?
答:不同继承关系的类对象,去调用同一函数,产生了不同的行为
2. 什么是重载、重写(覆盖)、重定义(隐藏)?
答:
重载(也叫静态多态):两个函数在同一作用域,函数名和参数相同
重写(也叫覆盖、动态多态):两个函数分别在父类和子类的作用域;函数名、参数、返回值都必须相同;两个函数都必须是虚函数
隐藏(也叫重定义):两个函数分别在父类和子类的作用域,函数名相同,两个父类和子类的同名函数不构成重写就构成重定义
3. 多态的实现原理?
答:子类重写了父类虚函数,且通过父类指针调用虚函数,这就满足了多态的两个条件。子类完成父类虚函数重写以后,子类的虚表指针指向的是重写了的子类虚函数。指针或引用调用虚函数时,不是编译时确定,而是运行时才到指向的对象的虚表中找对应的虚函数调用,当指针或引用指向父类对象时,调用的就是父类的虚表中的虚函数,当指针或引用指向子类对象时,调用的就是子类虚表中的虚函数。
4. inline函数可以是虚函数吗?
答:不能,因为inline函数没有地址,无法把地址放到虚函数表中。
inline函数没有地址,当inline成为虚函数后,虚表里面要放它的地址,构成多态时,根据虚函数表指针去call这个地址,就不能展开了,就忽略了内联属性,加了虚函数以后就不再是内联函数了。
5. 静态成员可以是虚函数吗?
答:不能,因为静态成员函数没有this指针,使用类型::成员函数的调用方式无法访问虚函数表,所以静态成员函数无法放进虚函数表。
6. 构造函数可以是虚函数吗?
答:不能,因为对象中的虚函数表指针是在构造函数初始化列表阶段才初始化的。
假如构造函数是虚函数:
①调用构造函数虚函数必须要去虚表里面找,这就要求对象必须已经被初始化出来了。
②要初始化对象,就要调构造函数虚函数,但是对象还没有构造出来,虚表还没有初始化,还找不到构造函数虚函数地址。
这就陷入了死循环
7. 析构函数可以是虚函数吗?什么场景下析构函数是虚函数?
答:可以,最好把基类的析构函数定义成虚函数。这样子类继承父类后,子类的析构函数也会成为虚函数,在这样的场景下,期望达到多态行为,子类的析构函数会在被调用完成后自动调用父类的析构函数清理父类成员。因为这样才能保证子类对象先清理子类成员再清理父类成员的顺序。多态场景下子类和父类的析构函数最好加上virtual关键字,让子类完成虚函数重写就不会导致内存泄漏了
8. 对象访问普通函数快还是虚函数更快?
答:
如果是普通对象,那么访问普通函数和虚函数是一样快的。
如果是指针对象或者是引用对象,则调用的普通函数快,因为构成多态,运行时调用虚函数需要到虚函数表中去查找,需要耗时。
9. 虚函数表是在什么阶段生成的,存在哪的?
答:虚函数表是在编译阶段就生成的,一般情况下存在代码段(常量区)的。
10. C++菱形继承的问题?虚继承的原理?
答:子类对象会有两份父类的成员,菱形继承会导致数据冗余和二义性。
虚继承通过虚基表指针的偏移量计算出父类成员的起始地址,这样就只需要在内存中存一份父类成员,解决了数据冗余和二义性的问题。
11. 什么是抽象类?抽象类的作用?
答:包含纯虚函数的类叫做抽象类。
抽象类不能实例化出对象。子类继承抽象类后也是抽象类,没有重写虚函数,不能实例化出对象,只有重写纯虚函数,子类才能实例化出对象。
① 能够更好地去表示现实世界中没有实例对象是我抽象类型,如:植物、人、动物
② 体现接口继承,强制子类去重写虚函数(就算不重写,子类也是抽象类)