1.多态的概念
通俗来说,就是多种形态,具体点就是去完成某个行为,当不同的对象去完成时会 产生出不同的状态。
举例:比如买票这个行为,当普通人买票时,是全价买票;学生买票时,是半价买票;军人 买票时是优先买票。
代码示例:
当在基类Animal的代码 void sleep()和void Eat()前面添加了virtual 就实现了下图所示的调用。(这就是多态)
多态的实现
例二代码演示:(多态)
cpp
//需求:实现一个绘图软件
//如果是圆,画圆
//如果是矩形,画矩形
//如果是三角形 画三角形
class Shape
{
public:
//虚函数:即被virtual修饰的类成员函数称为虚函数。
virtual void Draw()
{
cout << "未知图形 暂时无法绘制" << endl;
}
};
//圆
class Cirle :public Shape
{
public:
Cirle(double r)
:_r(r)
{}
void Draw()
{
cout << " o " << endl;
}
private:
double _r;
};
//矩形
class Rect :public Shape
{
public:
Rect(double length, double width)
:_length(length)
, _width(width)
{}
void Draw()
{
cout << "口" << endl;
}
private:
double _length;
double _width;
};
//三角形
class Trangle :public Shape
{
public:
Trangle(double a, double b, double c)
:_a(a)
, _b(b)
, _c(c)
{}
void Draw()
{
cout << "🔺" << endl;
}
private:
double _a;
double _b;
double _c;
};
void DrawShape(Shape& s)
{
s.Draw();
}
int main()
{
Cirle c(2);
DrawShape(c);
Rect rect(1, 2);
DrawShape(rect);
Trangle r(3, 4, 5);
DrawShape(r);
return 0;
}
动态多态实现条件:
必须在继承前提下,子类必须重写基类的虚函数(注意:被virtual关键字修饰的成员函数称为虚函数)
关于虚函数调用:通过基类的指针或者引用调用虚函数 才可以实现多态
实现多态:在程序运行时,根据基类的指针或者引用,指向不同类的对象,编译器会选择对应类中的虚函数进行调用。
如果类中那个方法想要实现多态的效果,则该方法必须为虚函数,并且在子类中必须要被重写。
虚函数的重写:
虚函数的重写(覆盖):派生类中有一个跟基类完全相同的虚函数(即派生类虚函数与基类虚函数的 返回值类型、函数名字、参数列表完全相同),称子类的虚函数重写了基类的虚函数。
重写相关条件:
1.基类中要被重写的成员函数必须时虚函数
2.子类虚函数和基类虚函数的原型要一致,即:返回值类型 方法名字 以及参数列表必须完全相同
3.子类虚函数前virtual关键字可以不用添加 建议子类虚函数之前最好将virtual加上
4.基类和子类的虚函数访问权限可以不同,一般下保持一致,而且基类的虚函数基本都是public
重写例外:
1.析构函数重写(基类和子类析构函数的名字不同)
如果基类的析构函数为虚函数,此时派生类析构函数只要定义,无论是否加virtual关键字, 都与基类的析构函数构成重写,虽然基类与派生类析构函数名字不同。虽然函数名不相同, 看起来违背了重写的规则,其实不然,这里可以理解为编译器对析构函数的名称做了特殊处 理,编译后析构函数的名称统一处理成destructor。
一般情况下在继承体系中建议将基类的析构函数设置为虚函数----为什么?
cppclass B { public: virtual ~B() { cout << "B::~B()" << endl; } }; class D :public B { public: ~D() { cout << "D::~D()" << endl; } }; void ReleaseHeapObj(B* & pb) { if (pb) { delete pb; pb = nullptr; } } int main() { B* pb = new B; ReleaseHeapObj(pb); pb = new D; ReleaseHeapObj(pb); return 0; }
在上述代码中,如果不把基类的析构函数设置成虚函数就会造成内存泄漏。
2.协变:基类虚函数返回基类虚函数的指针 或 引用,并且子类虚函数返回子类对象的指针或者引用。(派生类重写基类虚函数时,与基类虚函数返回值类型不同。即基类虚函数返回基类对象的指 针或者引用,派生类虚函数返回派生类对象的指针或者引用时,称为协变。)
cpp
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;}
};
只要返回值满足父子关系,基类返回值返回基类的指针,子类的返回值返回子类的指针,就是协变就能构成重写。
同名隐藏和重写的区别?
相同点:
1.两个函数都是在继承体系中,一个在基类中,一个在子类中
2.两个函数的名字都是相同的
上述两个条件满足之后,则一定是同名隐藏,但不一定是重写。
重写:在同名隐藏的基础上限制更加严格:
1.重写中基类的函数必须是虚函数,但是同名隐藏没有要求
2.重写要求基类和子类虚函数的原型必须一致(析构和协变例外)
同名隐藏只要求方法名字相同即可,返回值类型以及参数列表是否相同没有要求。
C++11 override 和 final
**override:**只能修饰子类的虚函数,作用:让编译器在编译代码时,帮助检测是否重写了基类的某个虚函数。如果重写了则编译通过,否则编译失败。
cpp
class Car{
public:
virtual void Drive(){}
};
class Benz :public Car {
public:
virtual void Drive() override
{
cout << "Benz-舒适" << endl;
}
};
final:
1.修饰类:表明该类不能被继承
cpp
class B final{};
//编译报错,因为B在定义时被final修饰了 因此B类是不能被继承的
//class D:public B{};
2.修饰虚函数:修饰虚函数表明该虚函数不想被其子类重写,final实际上也只能修饰子类的虚函数。
final修饰基类的虚函数实际上是没有意义:在基类中,既然将func设置为虚函数,表明后续要在func上实现多态,既然要在该方法上实现多态,则该方法必须要被子类重写。
cpp
class Car
{
public:
virtual void Drive() final {}
};
class Benz :public Car
{
public:
virtual void Drive() {cout << "Benz-舒适" << endl;}
};
抽象类
例如图像类
shape是不具体的,有三角形的图形,有圆的图形,不能说:有一个图形的图形
正常情况下:shape类是不能创建对象的,该类本身就不具体,即抽象的,所以也不应该创建其对象,将类似shape的类:抽象类---->将不能创建对象的类称为抽象类。
在虚函数名之后跟上=0,表明该虚函数为纯虚函数
将包含纯需函数的类称为抽象类,抽象类特性 :不能创建对象。
cpp
class Car
{
public:
virtual void Drive() = 0;
};
class Benz :public Car
{
public:
virtual void Drive()
{
cout << "Benz-舒适" << endl;
}
};
class BMW :public Car
{
public:
virtual void Drive()
{
cout << "BMW-操控" << endl;
}
};
void Test()
{
Car* pBenz = new Benz;
pBenz->Drive();
Car* pBMW = new BMW;
pBMW->Drive();
}
抽象类好处:
1.代码实现更加符合逻辑:即有些不具体的类就是不应该让其创建对象
2.不能花费时间去考虑纯虚函数中的代码该怎么写
3.抽象类实际规范了:后序子类要实现的虚函数的原型---->将接口规范化
注意:抽象类一定要被继承
在子类中,要对抽象类中的所有的纯虚函数进行重写,否则子类也是抽象类
即:子类将抽象类中的纯虚函数全部重写之后则则子类就可以创建对象,否则子类也是抽象类。
多态原理
注意:不同的编译器,对于多态底层实现原理细节上可能会有不同
1.对象模型:带有虚函数的类对象的模型
关于下面的代码,他的sizeof(B)大小是多少呢?
cpp
class B
{
public:
virtual void Func1()
{
cout << "Func1()" << endl;
}
private:
int _b = 1;
};
通过观察测试我们发现B对象是8bytes,除了_b成员,还多一个__vfptr放在对象的前面(注意有些 平台可能会放到对象的最后面,这个跟平台有关),对象中的这个指针我们叫做虚函数表指针(v代 表virtual,f代表function)。一个含有虚函数的类中都至少都有一个虚函数表指针,因为虚函数 的地址要被放到虚函数表中,虚函数表也简称虚表。
如果我们在增加B类中的虚函数数量最后大小是多少呢? 结果还是8bytes
cpp
class D
{
public:
void func()
{
cout << "D::func()" << endl;
}
virtual void func4()
{
cout << "D::func()" << endl;
}
virtual void func1()
{
cout << "D::func()" << endl;
}
virtual void func2()
{
cout << "D::func()" << endl;
}
virtual void func3()
{
cout << "D::func()" << endl;
}
int _d;
};
结论是:是该类中所有虚函数的地址,并且虚表中虚函数的地址和虚函数在类中生成的先后次序一致。
同一个类的对象共享一张虚表 。
子类虚表的构建过程 :
1.将基类虚表中内容原封不动的拷贝到子类的虚表中。
示例:
2.如果子类重写了某个基类的虚函数,则编译器会用子类虚函数地址去替换子类虚表中相同偏移量位置的基类虚函数地址。
示例:
cpp
class B
{
public:
virtual void func1()
{
cout << "B::func()" << endl;
}
virtual void func2()
{
cout << "B::func()" << endl;
}
virtual void func3()
{
cout << "B::func()" << endl;
}
int _b;
};
class D :public B
{
public:
virtual void func1()
{
cout << "B::func()" << endl;
}
virtual void func3()
{
cout << "B::func()" << endl;
}
public:
int _d;
};
int main()
{
D d;
d._b = 1;
d._d = 2;
return 0;
}
对于上述的代码:
3.如果子类增加了新的虚函数,则将新增加的虚函数 按照其在子类中声明的先后次序依次增加到子类虚表的最后。
- func2继承下来后是虚函数,所以放进了虚表,如果有一个func4也继承下来了,但是不是虚函 数,所以不会放进虚表。
5.虚函数表本质是一个存虚函数指针的指针数组,一般情况这个数组最后面放了一个nullptr。
虚函数存在哪的?虚表存在哪的?
注意 虚表存的是虚函数指针,不是虚函数,虚函数和普通函数一样的,都是存在代码段的,只是 他的指针又存到了虚表中。另外对象中存的不是虚表,存的是虚表指针。
动态绑定与静态绑定
-
静态绑定又称为前期绑定(早绑定),在程序编译期间确定了程序的行为,也称为静态多态, 比如:函数重载
-
动态绑定又称后期绑定(晚绑定),是在程序运行期间,根据具体拿到的类型确定程序的具体 行为,调用具体的函数,也称为动态多态。
单继承和多继承关系的虚函数表
单继承就跟上面内容一样。
多继承函数虚表:
cpp
class B1
{
public:
virtual void func1()
{
cout << "B1::func1()" << endl;
}
virtual void func2()
{
cout << "B1::func2()" << endl;
}
int _b1;
};
class B2
{
public:
virtual void func3()
{
cout << "B2::func3()" << endl;
}
virtual void func4()
{
cout << "B2::func4()" << endl;
}
int _b2;
};
class D :public B1, public B2
{
public:
virtual void func1()
{
cout << "D::func1()" << endl;
}
virtual void func4()
{
cout << "D::func4()" << endl;
}
virtual void func5()
{
cout << "D::func5()" << endl;
}
int _d;
};
int main()
{
D d;
d._b1 = 1;
d._b2 = 2;
d._d = 3;
return 0;
}
注意: 在这个多继承中,子类新增加的虚函数地址放到第一张虚表的最后。
例题:
1.以下程序输出结果是什么()
cpp
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;
}
2.多继承中指针偏移问题?下面说法正确的是
cpp
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