继承与多态-深入掌握oop语言最强大的机制
文章目录
- 继承与多态-深入掌握oop语言最强大的机制
- 1.继承的基本意义
- 2.派生类的构造过程
- 3.重载,隐藏,覆盖
- 4.虚函数, 静态绑定和动态绑定--面试重点
- 5.虚析构函数--重点在于什么呢时候用
- 6.再讨论虚函数和动态绑定
- 7.理解多态到底是什么
- 8.理解抽象类----纯虚函数
- 9.笔试问题讲解
1.继承的基本意义
-
继承的本质和原理?
继承的本质: 1.做代码复用; 2.
类和类的关系:
组合: a part of … 一部分的关系
继承: a kind of … 一种的关系#include <iostream> using namespace std; class A { public: int ma; protected: int mb; private: int mc; }; // A 12字节 //class B : public A //继承 A是基类/父类 B是派生类/子类 //{ //public: // void func() // { // cout << "ma:" << ma << endl; // } // int md; // int me; // int mf; //}; //class B : protected A //继承 A是基类/父类 B是派生类/子类 //{ //public: // void func() // { // // } // int md; // int me; // int mf; //}; class B : private A //继承 A是基类/父类 B是派生类/子类 { public: void func() { } int md; int me; int mf; }; //B有两部分, A+B本身, 24字节 class C : public B //继承 A是基类/父类 B是派生类/子类 { public: void func() { } int md; int me; int mf; }; int main() { }
-
继承的细节
继承方式 基类的访问限定 派生类的访问限定 (main)外部的访问限定 public public public ok protected protected no(main只能访问公有的) private 不可见,无法访问,不是私有 no protected public 降级, protected no protected protected no private 不可见,无法访问,不是私有 no private public private no protected private no private 不可见,无法访问,不是私有 no private只有自己和友元能访问!!
-
对于更多的继承, class C ,要看直接继承的B里面的各个访问限定和继承方式
-
总结:
外部只能访问对象public的成员, protected和private的成员无法直接访问
继承结构中, 派生类从基类可以继承过来private的成员, 但是派生类无法访问
protected和private的区别?----基类中定义的成员, 想被派生类访问, 但是不想被外部访问, 那么基类可以定义为protected; 若 派生类和外部都不访问, 基类中设置为private -
默认继承方式------要看派生类是class(private)还是struct(public)!!!
2.派生类的构造过程
- 派生类怎么初始化从基类继承的成员变量呢?
派生类可以从继承得到 继承的所有的成员(变量+方法), 除了构造和析构
解决办法: 调用基类的 构造函数
派生类的构造和析构, 负责初始化和清理派生类部分
派生类从基类继承来的成员–的初始化和清理由谁负责?----由基类的构造和析构 - 派生类对象构造和析构过程?
派生类调用基类构造(初始化基类继承来的成员)—>派生类自己构造—>派生类自己的析构—>调用基类的析构
#include <iostream>
using namespace std;
class Base
{
public:
Base(int data) :ma(data) { cout << "Base()" << endl; }
~Base() { cout << "~Base()" << endl; }
protected:
int ma;
};
class Derive : public Base
{
public:
/*Derive(int data) :ma(data), mb(data)*/
//改为
Derive(int data) :Base(data), mb(data)
{
cout << "Derive()" << endl;
}
~Derive()
{
cout << "~Derive()" << endl;
}
private:
int mb;
};
int main()
{
Derive d(20);
return 0;
}
/*输出:
Base()
Derive()
~Derive()
~Base()*/
3.重载,隐藏,覆盖
- 重载关系
一组函数要重载, 必须在一个作用域内;且函数名相同, 参数列表不同
基类和派生类是两个不同的作用域!! - 隐藏的关系
在继承结构中, 派生类的同名成员, 把基类的同名成员给 隐藏了, 也就是默认调用派生类的同名成员
想要基类, 就要 加基类作用域
#include <iostream>
using namespace std;
class Base
{
public:
Base(int data=10) :ma(data) { }
~Base() { }
void show() { cout << "Base:show" << endl; }
void show(int) { cout << "Base:show(int)" << endl; }
protected:
int ma;
};
class Derive : public Base
{
public:
Derive(int data=20) :Base(data), mb(data)
{
}
~Derive()
{
}
void show() { cout << "Derive:show" << endl; }
private:
int mb;
};
int main()
{
Derive d;
d.show(); // 隐藏了 基类的同名成员, 没有才去基类 Derive:show
d.Base::show(); //Base:show
//d.show(10);//Derive 没有函数重载
return 0;
}
//输出
Derive:show
Base:show
-
把继承结构, 也理解为 从上(基类)到下(派生类)的结构
-
基类对象变为–>派生类对象, 派生类对象变为—>基类对象, 基类对象(引用)指向–>派生类对象, 派生类对象(引用)指向—>基类对象
这是不强转时, 直接=的情况, 实际强转还是可以的int main() { Base b(10); Derive d(20); //派生类对象--->基类对象 类型从下到上的转换 yes //怎么理解? 派生类看做学生, 基类看做人, 想要人, 把学生给你, 是可以的 b = d; //相当于把派生类里继承的基类给了 基类 //基类对象-->派生类对象 类型从上到下的转换 no //d = b; //这是不行的, 多出来了内存 派生类自己的 //基类对象(引用)-->指向 派生类对象 类型从下到上的转换 yes Base* pb = &d; // 是可以的, 解引用这能访问 Base那么大的区域, 后面的派生类自己的那部分, 管不着, 本来也管不着 pb->show(); //yes 想要访问派生类show, 可以强转 pb->show(20); //yes //派生类对象(引用)--->指向 基类对象 类型从上到下的转换 no //Derive* pd = &b; //会访问b没有的内存区域, 属于非法访问 return 0; }
-
总结: 继承结构中, 只支持从下到上的类型转换!!!-----前提是不强转
4.虚函数, 静态绑定和动态绑定–面试重点
-
什么时候是动态绑定?—并不是有虚函数就是动态绑定, 这是误区, 这个老师这节课开始没说明白!!到了第六节课才说
1.函数是虚函数:基类中的函数必须声明为 virtual。 2.通过 指针或引用 调用:通过基类指针或基类引用调用虚函数。 这是 必要条件!!!二者必须满足 即 Derive d(20); Base *pb=&d;
-
无虚函数时:
#include <iostream> using namespace std; class Base { public: Base(int data=10) :ma(data) { } ~Base() { } void show() { cout << "Base:show" << endl; } void show(int) { cout << "Base:show(int)" << endl; } protected: int ma; }; class Derive : public Base { public: Derive(int data=20) :Base(data), mb(data) { } ~Derive() { } void show() { cout << "Derive:show" << endl; } private: int mb; }; int main() { Derive d(50); Base* pb = &d; pb->show(); // 静态(编译时期)的绑定(函数的调用) Base::show (07FF7C6ED1406h) pb->show(10); // 静态(编译时期)的绑定(函数的调用) Base::show (07FF7C6ED10DCh) cout << sizeof(Base) << endl; // 4 cout << sizeof(Derive) << endl; // 8 cout << typeid(pb).name() << endl; //class Base* __ptr64 cout << typeid(*pb).name() << endl; // class Base return 0; }
-
虚函数的总结-1:
如果类里面定义了虚函数, 编译阶段 就会给这个类类型
产生一个 惟一的 vftable 虚函数表, 这里面主要存储的内容就是RTTI指针和虚函数的地址
RTTI–run-time type infomation
程序运行时, 每一张虚函数表 都会加载到内存的 .rodata区, 只能读, 不能写 -
虚函数的总结-2:
一个类里有虚函数, 这个类定义的对象, 在其运行时, 内存的开始部分, 多存储一个 vfptr虚函数指针, 指向相应类型的 虚函数表vftable. 一个类型定义的n个对象, vfptr都指向同一个虚函数表 -
虚函数的总结-3:
一个类里 虚函数的个数, 不影响对象的大小(对象里只是指针), 影响的是 虚函数表的大小 -
虚函数的总结-4:----覆盖!!!
如果派生类中的方法和基类继承来的某个方法, 函数名,参数列表,返回值都一样, 切基类这个方法是虚函数virtual,
则 派生类的 该方法 会 自动处理成虚函数—这是cpp标准
因此, 派生类的 虚函数表里, 该函数会覆盖基类的 -
有虚函数:
Base将不再是只有ma了, 还有虚函数指针vfptr, 指向虚函数表
因此不再是4字节, 而是8字节 -
静态绑定和动态绑定
静态(编译时期)的绑定(函数的调用)
动态(运行时期)的绑定(函数的调用) -
从汇编看 静动态绑定
//静态绑定的汇编 mov rcx,qword ptr [pb] //this指针存储 call Base::show (07FF7C6ED1406h) //动态绑定: x64的反汇编 mov rax,dword ptr [pb] //把pb里面的地址给eax, 即虚函数表地址 mov rax,dword ptr [rax] //取存储虚函数表地址里的地址,即虚函数表的地址 mov rcx,qword ptr [pb] //this指针存储 call qword ptr [rax+8] //调用虚函数表里偏移8字节的地址里面的地址, 即 show的地址, 8字节是RTTI的信息
-
总体代码:
该代码在show 是不是虚函数时, 输出不一样的-----x64//是虚函数-------注意考虑内存对齐 Derive:show Base:show(int) 16 24 class Base * __ptr64 class Derive //不是虚函数 Base:show Base:show(int) 4 8 class Base * __ptr64 class Base
#include <iostream> using namespace std; class Base { public: Base(int data=10) :ma(data) { } ~Base() { } virtual void show() { cout << "Base:show" << endl; } virtual void show(int) { cout << "Base:show(int)" << endl; } protected: int ma; }; class Derive : public Base { public: Derive(int data=20) :Base(data), mb(data) { } ~Derive() { } void show() { cout << "Derive:show" << endl; } private: int mb; }; int main() { //有虚函数时 Derive d(50); Base* pb = &d; pb->show(); // 静态(编译时期)的绑定(函数的调用) //静态绑定的汇编 call Base::show (07FF7C6ED1406h) // pb 是Base指针, 如果, Base::show 是普通函数, 则进行 静态绑定 // pb 是Base指针, 如果, Base::show 是虚函数, 就进行动态绑定 /* 动态绑定: x64的反汇编 mov rax,dword ptr [pb] 把pb里面的地址给eax, 即虚函数表地址 mov rax,dword ptr [rax] 取存储虚函数表地址里的地址,即虚函数表的地址 mov rcx,qword ptr [pb] this指针存储 call qword ptr [rax+8] 调用虚函数表里偏移8字节的地址里面的地址, 即 show的地址, 这个+8, 不是RTTI的, 一般RTTI是反偏移的, 是因为覆盖是原来的不要了, 新的放在最下面 */ pb->show(10); // 静态(编译时期)的绑定(函数的调用) Base::show (07FF7C6ED10DCh) cout << sizeof(Base) << endl; cout << sizeof(Derive) << endl; cout << typeid(pb).name() << endl; //class Base* __ptr64 cout << typeid(*pb).name() << endl; // class Base return 0; }
-
命令行展示 虚函数结构
vs命令行工具—>进入文件目录—>输入
cl 文件.cpp /d1reportSingleClassLayoutDerive
-
使用命令行也解释了, call qword ptr [rax+8]
Derive::$vftable@: | &Derive_meta | 0 0 | &Base::show // 重载在这里, 看不出来, 实际这个是 int 1 | &Derive::show // 覆盖是 之前最上面的不要了, 新的放到最下面
5.虚析构函数–重点在于什么呢时候用
-
哪些函数不能实现成 虚函数?–接上一节
虚函数能产生函数地址
虚函数表位置在 vfptr里, vfptr在内存里----- 对象必须存在, 依赖对象
构造函数–不能是–虚函数, 构造函数中调用虚函数, 不会发生动态绑定, 构造函数中调用的任何函数,都是静态绑定---->这是因为在构造函数执行期间,对象的动态类型尚未完全确定,虚函数表(vtable)也没有完全初始化。
派生类构造过程–>先调用基类构造–>才调用–>派生类构造
static静态成员方法–>不能是—>虚函数, 因为不依赖对象
析构函数–>可以!!–>虚函数, 因为析构时,对象是存在的 -
特别注意:基类析构和派生类虚构!!
基类虚构是虚函数-----> 派生类析构 自动成为虚函数—> 尽管名字不一样!! -
虚析构使用实例:
为什么 不是虚析构时, 会出问题? 因为使用了
因为此时是静态绑定, Base类的指针即使指向Derive类, 使用的还是Base的析构派生类使用虚析构, 就有了虚函数表, 派生类的虚函数表还会覆盖 基类 的 虚析构, 使用自己的虚析构, 使得可以 正确析构派生类, —>动态绑定
pb是Base类, 但是指向Derive, 动态绑定, 使得本来是使用Base的析构, 但是发现是虚析构, 于是去找虚函数表, 此时虚函数表的析构已经被 Derive覆盖了, 因此使用了 Derive的析构, 后续继承基类的部分也会正确析构#include <iostream> using namespace std; class Base { public: Base(int data=10) :ma(data) { cout << "Base" << endl; } virtual ~Base() { cout << "~Base" << endl; } void show() { cout << "call Base:show" << endl; } protected: int ma; }; class Derive : public Base { public: Derive(int data=20) :Base(data), mb(data) { cout << "Derive" << endl; } ~Derive() { cout << "~Derive" << endl; } void show() { } private: int mb; }; int main() { Base* pb = new Derive(10); pb->show(); // 静态绑定,pb是Base*类型, 而*pb类型取决于show是不是虚函数, 若是, 则是Derive,因为指向了派生类的show, 且是动态绑定 delete pb; // 不使用虚析构, 会导致派生类析构没有调用-->若有指针.容易内存泄漏 /* Derive d(50); Base* pb = &d; pb->show(); */ return 0; }
-
什么时候用?—特别重点
基类的指针(引用)指向 堆上new出来的 派生类, delete 基类指针时
6.再讨论虚函数和动态绑定
-
虚函数的调用就是 动态绑定?
不是!!!
构造函数就是例外, 构造函数中调用虚函数, 不会发生动态绑定, 构造函数中调用的任何函数,都是静态绑定 -
什么时候发生动态绑定?
1.函数是虚函数:基类中的函数必须声明为 virtual。 2.通过 指针或引用 调用:通过基类指针或基类引用调用虚函数。 这是 必要条件!!!二者必须满足
-
实例:---- 一定要搞清楚什么时候动态绑定!!!
#include <iostream> using namespace std; class Base { public: Base(int data=10) :ma(data) { cout << "Base" << endl; } virtual ~Base() { cout << "~Base" << endl; } virtual void show() { cout << "call Base:show" << endl; } protected: int ma; }; class Derive : public Base { public: Derive(int data=20) :Base(data), mb(data) { cout << "Derive" << endl; } ~Derive() { cout << "~Derive" << endl; } void show() { } private: int mb; }; int main() { Base b; Derive d; b.show(); // 二者都是静态绑定, d.show(); Base* pb1 = &b; //二者是动态绑定 pb1->show(); Base* pb2 = &d; pb2->show(); Base& rb1 = b;//二者是动态绑定 pb1->show(); Base& rb2 = d; pb2->show(); Derive* pd2 = (Derive*)&b;//动态绑定---满足两个条件-----这里必须强转, 回看第三节的总结 pd2->show();//---这里是调用的基类的show, 因为实际的b是Base, 其虚函数表是基类的, 这种强转并不会 改变原本的虚函数表指向 return 0; }
7.理解多态到底是什么
-
如何理解多态?
静态的 多态: — 编译阶段就确定好 — 函数重载和模板(函数模板,类模板),动态的 多态: — 继承结构中, 基类指针(引用) 指向派生类对象, 通过该指针(引用)调用同名覆盖方法(虚函数),基类指针指向哪个派生类, 就调用哪个派生类对象的同名覆盖方法, 称为多态
多态底层是通过 动态绑定来实现的的 -
实例: 虚函数配合基类, 完成 开-闭 设计, 使用指针时切记虚析构
#include <iostream> using namespace std; // 动物的基类 class Animal { public: Animal(string name) : _name(name) {} virtual void bark() {} protected: string _name; }; class Cat : public Animal { public: Cat(string name) : Animal(name) {} void bark() { cout << _name << " bark: miao miao!" << endl; } }; class Dog : public Animal { public: Dog(string name) : Animal(name) {} void bark() { cout << _name << " bark: wang wang!" << endl; } }; class Pig : public Animal { public: Pig(string name) : Animal(name) {} void bark() { cout << _name << " bark: heng heng!" << endl; } }; //下面的API无法达到 软件设计的 开-闭 原则: 对修改关闭, 对扩展开放 //void bark(Dog &dog) //{ // dog.bark(); //} // //void bark(Pig& pig) //{ // pig.bark(); //} // //void bark(Cat& cat) //{ // cat.bark(); //} //使用基类指针 void bark(Animal *p) { p->bark(); // 虚函数 覆盖 } int main() { Dog dog("dog"); Pig pig("pig"); Cat cat("cat"); bark(&dog); bark(&cat); bark(&pig); return 0; }
-
继承的好处?
1.做代码复用
2.在基类中给所有的派生类提供统一的虚函数接口, 让派生类重写, 然后就可以多态了
8.理解抽象类----纯虚函数
-
类 是 抽象一个实体的类型
-
什么是纯虚函数?
纯虚函数(Pure Virtual Function)是 C++ 中用于定义抽象类的一种机制。纯虚函数在基类中声明但不提供实现,要求派生类必须重写(实现)该函数。包含纯虚函数的类称为抽象类,抽象类不能被实例化。在函数声明的末尾加上
= 0
,表示这是一个纯虚函数
virtual void foo() = 0;
-
什么是 抽象类?
拥有纯虚函数的类, 叫做抽象类
抽象类不能再实例化对象, 但是可以定义指针和引用变量
一般是基类作为抽象类, 派生类去实例化对象 -
实例: — 注意 外部接口是 基类
#include <iostream> using namespace std; // 动物的基类 class Animal { public: Animal(string name) : _name(name) {} virtual void bark() {} protected: string _name; }; class Cat : public Animal { public: Cat(string name) : Animal(name) {} void bark() { cout << _name << " bark: miao miao!" << endl; } }; class Dog : public Animal { public: Dog(string name) : Animal(name) {} void bark() { cout << _name << " bark: wang wang!" << endl; } }; class Pig : public Animal { public: Pig(string name) : Animal(name) {} void bark() { cout << _name << " bark: heng heng!" << endl; } }; //下面的API无法达到 软件设计的 开-闭 原则: 对修改关闭, 对扩展开放 //void bark(Dog &dog) //{ // dog.bark(); //} // //void bark(Pig& pig) //{ // pig.bark(); //} // //void bark(Cat& cat) //{ // cat.bark(); //} //使用基类指针 void bark(Animal *p) { p->bark(); // 虚函数 覆盖 } int main() { Dog dog("dog"); Pig pig("pig"); Cat cat("cat"); bark(&dog); bark(&cat); bark(&pig); return 0; }
-
实例-2:
#include <iostream> #include <string> using namespace std; // 抽象基类 Car class Car { public: Car(string name) : _name(name) {} virtual double getMilesPerGallon() = 0; // 纯虚函数 string getName() { return _name; } double getLeftMiles(double fuel) { return fuel * getMilesPerGallon(); } protected: string _name; }; // 派生类 Audi class Audi : public Car { public: Audi(string name) : Car(name) {} double getMilesPerGallon() override { return 18.0; } }; // 派生类 BMW class BMW : public Car { public: BMW(string name) : Car(name) {} double getMilesPerGallon() override { //override 是 C++11 引入的一个关键字,用于显式地标记派生类中的函数是对基类虚函数的重写。它的主要作用是提高代码的可读性和安全性,帮助开发者避免一些常见的错误。 return 19.0; } }; // 给外部提供一个统一的获取汽车剩余路程数的API void showCarLeftMiles(Car &car, double fuel) { cout << car.getName() << " left miles: " << car.getLeftMiles(fuel) << " 公里" << endl; } int main() { Audi a("奥迪"); BMW b("宝马"); showCarLeftMiles(a, 10.0); // 假设有10加仑的油 showCarLeftMiles(b, 10.0); return 0; }
9.笔试问题讲解
-
实例-1
重点: 函数调用, 参数压栈是在编译时期 就确定的
因此, 派生类虚函数参数默认值 是用不到的
默认参数值是静态绑定的,而虚函数的调用是动态绑定的。即使 基类无,派生类有,也没用#include <iostream> using namespace std; class Base { public: virtual void show(int i = 10) { cout << "call Base::show i:" << i << endl; } virtual ~Base() {} // 虚析构函数 }; class Derive : public Base { public: void show(int i = 20) override { cout << "call Derive::show---" << i << endl; } }; int main() { Base* p = new Derive(); // 使用基类指针指向派生类对象 p->show(); // 动态绑定,调用Derive::show, 但是输出却是 10, 不是20 delete p; // 释放内存 return 0; } //为什么会是10 呢 从函数调用角度讲, 先压参数列表, 才压函数符号 而在编译阶段, 看不到动态绑定, 只能看到是Base*类, 因此压入的是10 push 0Ah 编译时 mov eax, dword ptr[p] 运行时 mov ecx, deord ptr[eax] call eax push的是死的, 不会因为后面调虚函数而改变
-
实例-2
重点:成员的权限, 是在编译阶段 确定好的!!!
编译阶段只能看见 p是Base的, 而基类里是 public的
千万不要去 运行时看 成员权限!!#include <iostream> using namespace std; class Base { public: virtual void show() { cout << "call Base::show i:" << endl; } virtual ~Base() {} // 虚析构函数 }; class Derive : public Base { private: void show() override { cout << "call Derive::show---" << endl; } }; int main() { Base* p = new Derive(); p->show(); // 运行时确定 delete p; return 0; }
-
实例-3
重点: vfptr什么时候拿到虚函数表地址? 是重点!!!
每个类(无论是基类还是派生类)都有自己的虚函数表(vtable)。
每个对象(无论是基类对象还是派生类对象)都有自己的虚函数表指针(vfptr),指向其所属类的虚函数表。#include <iostream> using namespace std; class Base { public: Base() { /* push ebp mov ebp, esp sub esp, 4Ch rep stos esp<->ebp 0xCCCCCCCC (windows VS GCC/G++) 此时, 就会进行 vfptr->vftable地址 进入函数后, 第一件事就是虚函数表指针的存储 */ cout << "call Base()" << endl; clear(); } void clear() { memset(this, 0, sizeof(*this)); } virtual void show() { cout << "call Base::show()" << endl; } virtual ~Base() { cout << "call ~Base()" << endl; } }; class Derive : public Base { public: Derive() { cout << "call Derive()" << endl; } void show() override { cout << "call Derive::show()" << endl; } ~Derive() { cout << "call ~Derive()" << endl; } }; int main() { //Base* pbl = new Base(); //pbl->show(); // 动态绑定 //delete pbl; //这一段肯定会出错, vfptr是0了, 肯定访问不到了 Base* pb2 = new Derive(); pb2->show(); // 动态绑定, delete pb2; // 这一段是可以的, 先基类构造, 再派生类构造 //涉及到了 vfptr什么时候得到的vftable的 地址, 在构造函数里 //将会把派生类虚函数地址写入vfptr, return 0; }