1. 多态的概念
多态的概念:通俗来说,就是多种形态,具体点就是去完成某个行为,当不同的对象去完成时会 产生出不同的状态。
比如买票这个行为,当普通人买票时,是全价买票;学生买票时,是半价买票;军人买票时是优先买票。
class person
{
public:
virtual void BuyTicket()//虚函数(成员函数才能是虚拟的,写到类外会报错)
//这里的virtual关键字跟继承那里的同名但是作用不一样
{
cout << "买票全价" << endl;
}
};
class student :public person
{
public:
virtual void BuyTicket()
{
cout << "买票半价" << endl;
}
};
class soldier :public person
{
public:
//虚函数的重写,覆盖条件:虚函数+(要求函数名,参数,返回值都要相同)
//不符合重写,就是隐藏关系
virtual void BuyTicket()
{
cout << "优先买票" << endl;
}
};
//多态的条件
//1.虚函数的重写
//2.父类的指针或者引用去调用虚函数
void Func1(person &p)
{
p.BuyTicket();//不符合多态的时候我们是按照类型去走的,这里是person调用的就是person的函数
}
//破坏多态的情况
void Func2(person p)
{
p.BuyTicket();
}
int main()
{
person p1;
student s1;
soldier s2;
Func1(p1);
Func1(s1);
Func1(s2);
Func2(p1);
Func2(s1);
Func2(s2);
return 0;
}
上面这种情况是取消了对象访问采用指针或者引用的方式
如果我们取消了父类的virtual
这里写一个占位参数,如果调用传入整形值的话,依旧符合多态,如果这里改成char下面调用传入的还是整形,就会失去多态。
由此我们得出多态的条件缺一不可。
这里有一个特例,如果把子类的virtual去掉,仍然是虚函数,因为它认为是把父类的virtual继承下来了,重写是它的实现,所以我们的子类即使没写virtual也会被当作虚函数处理。
特例1:子类虚函数不加virtual,依旧构成重写(实际最好加上)
特例2:重写的协变,返回值可以不同,要求必须父子关系的指针或者引用
如果传对象就会报错
这里func满足多态的条件,父类指针this,发生了调用虚函数,之后又对func虚函数进行了重写。
这里为什么要把函数存入表里面而不是存在对象里面?我们构造多个函数来观察
首先多态原理的关键是虚函数会存虚表,对象里面存的不是虚表,而是虚表的指针。重写之后父类会有一个虚表
这里person有八个字节,一是自己本身的int成员,二是指针指向虚函数表,虚函数表实则是数组,存的是函数指针
另外一个对象的虚函数表存的地址跟第一个对象是不同的,func都相同,但是buyticket不同,因为这个函数被重写了。
如果构成多态,怎么区分谁的对象调用谁的函数,这里要从虚函数表去找,如果是父类对象,父类对象从虚函数表call一个地址,通过指针去找。
多态的本质原理是符合多态的条件,调用时会到指向的对象的虚表中找到对应的虚函数地址,进行调用。
不构成多态,在编译链接的时候去call一个地址,到时谁的对象就去找他的成员函数。符合多态,在编译链接完了之后才去找。
这是多态正常被执行,第一个call是调用虚函数表,后面的call是去检查栈帧。
这是去除指针调用之后多态失效的情况下,我们发现就是成员函数的调用
如果我们子类虚函数没写,他是采取多态的编译链接之后去寻找的方式,还是正常找成员函数的方式呢?
我们调试发现,这里仍旧符合多态的虚表调用方式,因为编译器在判断的时候,只要父类出现了虚函数,默认这一步骤就已经符合了,无论你子类是否重写了,仍旧是正常去找虚表,只不过这里的虚表存储的跟父类函数相同,所以调用出的结果都一样。
虚函数重写的两个例外:
1. 协变(基类与派生类虚函数返回值类型不同)
派生类重写基类虚函数时,与基类虚函数返回值类型不同。即基类虚函数返回基类对象的指 针或者引用,派生类虚函数返回派生类对象的指针或者引用时,称为协变。
class A{};
class B : public A {};
class Person {
public:
virtual A* f() {return new A;}
};
class Student : public Person {
public:
virtual B* f() {return new B;}
};
2. 析构函数的重写(基类与派生类析构函数的名字不同) 如果基类的析构函数为虚函数,此时派生类析构函数只要定义,无论是否加virtual关键字, 都与基类的析构函数构成重写,虽然基类与派生类析构函数名字不同。虽然函数名不相同, 看起来违背了重写的规则,其实不然,这里可以理解为编译器对析构函数的名称做了特殊处 理,编译后析构函数的名称统一处理成destructor。
都去除了virtual,这种结果没有报错。
//建议在继承中析构函数定义成虚函数
class person
{
public:
//virtual ~person()
~person()
{
cout << "~person()" << endl;
}
};
class student :public person
{
public:
//virtual ~student()
~student()
{
cout << "~student()" << endl;
}
};//这里为什么函数名不相同,但是完成了重写呢?
//析构函数名会被处理成destructor,所以这里析构函数完成虚函数重写
int main()
{
//person p;
//student s;
//上面这种普通场景是不会出现问题的
person* ptr1 = new person;
delete ptr1;
person* ptr2 = new student;//底层 ptr2->destructor() operator delete(ptr2)
//没有virtual的话,这里不构成多态,call的就是ptr2类型的指针,ptr2是person的指针
delete ptr2;
return 0;
}
C++11 override 和 final
C++对函数重写的要求比较严格,但是有些情况下由于疏忽,可能会导致函数 名字母次序写反而无法构成重载,而这种错误在编译期间是不会报出的,只有在程序运行时没有 得到预期结果才来debug会得不偿失,因此:C++11提供了override和final两个关键字,可以帮 助用户检测是否重写。
在继承部分final就出现过了,final修饰这个类这个类就不能被继承;
1. final:修饰虚函数,表示该虚函数不能再被重写
class Car
{
public:
virtual void Drive() final {}
};
class Benz :public Car
{
public:
virtual void Drive() {cout << "Benz-舒适" << endl;}
};
2. override: 检查派生类虚函数是否重写了基类某个虚函数,如果没有重写编译报错。
class Car{
public:
virtual void Drive(){}
};
class Benz :public Car {
public:
virtual void Drive() override {cout << "Benz-舒适" << endl;}
};
重载、覆盖(重写)、隐藏(重定义)的对比
3. 抽象类
概念
在虚函数的后面写上 =0 ,则这个函数为纯虚函数。包含纯虚函数的类叫做抽象类(也叫接口 类),抽象类不能实例化出对象。派生类继承后也不能实例化出对象,只有重写纯虚函数,派生 类才能实例化出对象。纯虚函数规范了派生类必须重写,另外纯虚函数更体现出了接口继承。
//抽象类(包含纯虚函数的叫抽象类)不能实例化对象
//抽象就是在现实中没有对应的实体
class car
{
public:
virtual void dirve() = 0;
};
class BMW:public car
{
virtual void dirve()
{
cout << "操控好开" << endl;
}
};
int main()
{
//car c1;
BMW b1;//父类是纯虚函数,子类不重写就构建不出对象。
return 0;
}//overide是检查是否重写,纯虚是强制重写
普通函数的继承是一种实现继承,派生类继承了基类函数,可以使用函数,继承的是函数的实 现。虚函数的继承是一种接口继承,派生类继承的是基类虚函数的接口,目的是为了重写,达成 多态,继承的是接口。所以如果不实现多态,不要把函数定义成虚函数。正因为有了接口继承,才导致子类可以不用写virtual,因为用的是父类的接口,用的是子类的实现。
class person
{
public:
virtual void buytickets()
{
cout << "买票-全价" << endl;
}
};
class student :public person
{
public:
/*virtual void buytickets()
{
cout << "买票-半价" << endl;
}*/
};
int main()
{
//同一个类型的对象共用一个虚表
person p1;
person p2;
student s1;//如果子类没有进行重写,子类的虚表跟父类还不是一个虚表(在vs下)
student s2;
return 0;
}
虚函数表
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;
}//但是这里在调试里面被隐藏了。但是父类的虚函数func1是能被找到的,这里一系列的机制是编译器导致的
};
//现在就想看student里面的func2应该怎么看呢?(我们知道这里的函数虚表是函数指针数组)
//监视窗口即使用内存看,也只能基本确认,因为只能看见三个地址,一个是继承,一个是重写,一个应该是func2,但是
//不确定
//为了方便 typedef
// typedef void(*) VFTR 这是编不过的
typedef void(*VFTR)();
//手动写一个打印函数的表
void PrintVFTable(VFTR table[])
{
//假设已经拿到了虚函数表的地址(虚函数表是一个函数指针数组)
//只需要打印这个表即可
//void(*ptr)();//函数指针的定义
for (size_t i = 0; table[i] != nullptr;i++)//在vs下面虚表的结尾的位置都会
//给一个空指针给00 00 00...,G++就不行,得写死
{
printf("VFT[%d]:%p", i, table[i]);
//table[i]();
VFTR pf = table[i];
pf();//只要有函数指针加()都可以调用里面的函数的
}
}
int main()
{
person p1;
student s1;
//取对象虚表的地址,虚表的地址在对象的头四个字节或者头八个字节
//32位里面取就是头四个字节
//指针才能强转
//*(int*)&s1;//!=(int)s1 因为两个毫不相干的无法强转(八个字节就是long long*了)
//PrintVFTable(*(int*)&s1);//int*解引用是int,形参是一个vfptr*,两个不对应会报错
PrintVFTable((VFTR*)*(int*)&s1);//应该在做一次强转就不报错了。
return 0;
}
多继承中的虚函数表
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 Drive :public Base1, public Base2
{
public:
virtual void func1() { cout << "Drive::func1()" << endl; }
virtual void func3() { cout << "Drive::func3" << endl; }
private:
int d1=3;
};
typedef void(*VFTR)();
void PrintVFTable(VFTR table[])
{
for (size_t i = 0; table[i] != nullptr;i++)
{
printf("VFT[%d]:%p", i, table[i]);
VFTR pf = table[i];
pf();
}
}
int main()
{
Drive d1;
//先取对象地址&d1,之后再去(int*)强转取前四个(对象中前四个存的虚表地址)之后*解引用转变为int
//在VFTR强转一下,符合函数指针类型,传入即可。
PrintVFTable((VFTR*)*(int*)&d1);
//cout << sizeof(d1) << endl;//20 继承的base1,base2,base1是8,base2也是8。
//两个都含成员并且有一个虚表指针
//PrintVFTable((VFTR*)*(int*)&d1+sizeof(Base1));//这里这样写会崩溃
//强转的优先级高,强转完是int*,int*+1是加4个字节(指针+1是加类型的大小)
//应该这样写,先强转成char*,char*的意义是加1就加一个字节,否则你加由于操作符的优先级是+int*变成4;
PrintVFTable((VFTR*)*(int*)((char*)&d1+sizeof(Base1)));
return 0;
}
//这里vs监视窗口看不见子类增加的函数。
这里也可以不用指针强转,也可以直接用切片;
Base2*ptr2=&d1;
PrintVFTable((VFTR*)*(int*)(ptr2));
这里drive里面已经重写的base1里面的func1,为什么它们的地址依旧不同?
关于菱形继承:如果虚拟继承多个的话出现了菱形继承,如果是 a为头 b c分别为腰,d为底部的菱形。a写一个func函数如果这时候bc中两个都重写了,这时候会报错,只能由d来重写,因为a是接口,他出现了两个重写,不知道用哪个,就会报错。最好不用菱形虚拟继承,对象模型太复杂。
多态总结:
1. 什么是多态?
一种函数多种形态;
2. 什么是重载、重写(覆盖)、重定义(隐藏)?
函数名相同,参数不同,返回值相同,构成重载。
重定义是函数分别在父类子类中都有且函数参数,返回值,函数名都相同,两个函数必须是虚函数。
重写,不构成重定义就是重写
3.多态的实现原理?
虚函数表的使用(见上)
4. inline函数可以是虚函数吗?
可以,不过编译器就忽略inline属性,这个函数就不再是 inline,因为虚函数要放到虚表中去。inline只是一种建议,就算你写了inline他也正常编译,因为inline函数没有函数地址是直接在函数内部展开的,但是多态必须要生成虚函数,肯定得有地址,所以即使写了inline还是虚函数。
5. 静态成员可以是虚函数吗?答:不能,因为静态成员函数没有this指针,使用类型::成员函数的调用方式无法访问虚函数表,所以静态成员函数无法放进虚函数表。静态成员函数都是在编译时决议。它是虚函数没有价值。
6. 构造函数可以是虚函数吗?答:不能,因为对象中的虚函数表指针是在构造函数初始化列表阶段才初始化的。构造阶段this先去找虚函数表,之后把这个值给对象。
7.析构函数可以是虚函数吗?什么场景下析构函数是虚函数?答:可以,并且最好把基类的析 构函数定义成虚函数。
8.拷贝构造和operator=可以是虚函数?
拷贝构造不可以,拷贝构造也是构造函数,跟构造的原因相同。赋值语法上是可以的,但是实际上没有什么意义。
9. 对象访问普通函数快还是虚函数更快?
首先如果是普通对象,是一样快的。如果是指针 对象或者是引用对象,则调用的普通函数快,因为构成多态,运行时调用虚函数需要到虚函 数表中去查找。
10. 虚函数表是在什么阶段生成的,存在哪的?
虚函数表是在编译阶段就生成的,一般情况下存在代码段(常量区)的。构造函数初始化的是虚函数表指针,对象中存的也是虚函数表指针。