深入C++06:深入掌握OOP最强大的机制

📕深入掌握OOP最强大的机制

1. 继承的基本意义

类与类之间的关系:①组合:a part of ... 一部分的关系;②继承: a kind of ... 属于同一种的关系;

继承的本质:a. 代码的复用(相同的特征行为抽象出来作为基类)

三种继承关系以及各种注意点看初识C++中的继承笔记;

访问权限表复习:

image-20220326152235361

问题:如果我们不写继承方式的话,那它会以什么方式继承呢?例子如class B : A

要看具体情况:要看派生类是用class定义还是用struct定义!!!!①class定义:默认继承方式是private;②struct定义:默认继承方式是public;

所以上述的例子相当于class B : private A

2.派生类的构造过程

看👀😆一定要去看初识C++类继承派生相关笔记;都涵盖了这些内容!!!!!!

问题一:派生类如何初始化从基类继承来的成员变量呢?

1.派生类可以从基类继承来所有的成员(成员变量和成员方法),除构造函数和析构函数。
2.因此我们通过调用基类相应的构造函数来初始化。
3.派生类的构造函数和析构函数负责初始化和清理派生类部分;派生类从基类继承来的成员的初始化和清理由基类的构造和析构函数来负责。

问题二:派生类对象析构和构造过程是如何的呢?

1.派生类调用基类的构造函数,初始化从基类继承来的成员。
2.调用派生类自己的构造函数,初始化派生类自己特有的成员。
3.派生类对象的作用域到期,先调用派生类的析构函数,释放派生类成员可能占用的外部资源。
4.调用基类的析构函数,释放派生类内存中从基类继承来的成员可能占用的外部资源。

3.重载、隐藏、覆盖

看👀😆一定要去看初识C++类继承派生相关笔记;都涵盖了这些内容!!!!!!

基类派生类里面相关的成员方法我们经常使用三种关系来描述它们,即:重载、隐藏、覆盖关系

重载: 一组函数要重载,必须处在同一个作用域当中;而且函数名字相同,参数列表不同。

隐藏(作用域的隐藏): 在继承结构当中,派生类的同名成员,把基类的同名成员给隐藏掉了,即作用域的隐藏,只要名字即可相同!

覆盖: 父子类中的同名、同参数、同返回值的多个成员函数,从子到父形成的关系称为覆盖关系,虚函数中会发生,看下一节

隐藏关系例子:

class Base
{
public:
	Base(int data = 10) :ma(data){}
	void show()//1
	{
		cout << "Base::show()" << endl;
	}
	void show(int)//2
	{
		cout << "Base::show(int)" << endl;
	}
private:
	int ma;
};

class Derive : public Base
{
public:
	Derive(int data = 20):Base(data), mb(data){}
    void show()//3
	{
		cout << "Derive::show()" << endl;
	}
private:
	int mb;
};

int main()
{
	Derive d;
	d.show();
	d.show(10);//报错,因为Derive中没有show(int)方法;如果把Derive中show方法删除,既可以成功,Derive中找不到函数,会到Base作用域中找;
	return 0;
}

1、2属于函数重载关系,1和3、2和3都属于隐藏关系;

类型转换:之前笔记有写得很清楚了!总结一下:

在继承结构中进行上下的类型转换,默认只支持从下到上的类型的转换。除非进行强转,但强转不安全,会涉及内存的非法访问。

4.虚函数、静态绑定和动态绑定(非常核心重点)

虚函数:在某基类中声明为 virtual 并在一个或多个派生类中被重新定义的成员函数。

look👀😆:
1.一个类里面定义了虚函数,那么编译阶段(不是运行阶段),编译器需给这个类类型产生一个唯一的vftable虚函数表。虚函数表中主要存储的内容就是RTTI指针和虚函数的地址。当程序运行时,每一张虚函数表都会加载到内存的.rodata区(只读数据区)。
2.一个类里面定义了虚函数,那么这个类定义的对象,其运行时,内存中开始部分,多存储一个vfptr虚函数指针,指向相应类型的虚函数表vftable。一个类型定义的n个对象它们的vfptr指向都是同一张虚函数表。
3.一个类里面虚函数的个数不影响的对象内存大小,影响的是虚函数表的大小。
4.如果派生类中的方法和基类继承来的某个方法,返回值、函数名、参数列表都相同,而且基类的方法是virtual虚函数,那么派生类的这个方法自动处理成虚函数,即覆盖关系。

静态绑定和动态绑定的更好理解:

①静态绑定:

#include <iostream>
#include <typeinfo>
using namespace std;

class Base
{
public:
	Base(int data = 10): ma(data){}
	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){}
	void show()
	{
		cout << "Derive::show()" << endl;
	}
private:
	int mb;
};

int main()
{
	Derive d(50);
	Base *pb = &d;//上转型
	pb->show();//静态绑定  call Base::show(01612DAh)
	pb->show(10);//静态绑定 call Base::show(01612B2h)
	
	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;
}

编译期间指定了函数的调用,这就是是静态绑定,汇编图片如下:

image-20220328164509576

结果:image-20220328170522453

②动态绑定:

class Base
{
public:
	Base(int data = 10): ma(data){}
	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){}
	void show() //继承的虚函数
	{
		cout << "Derive::show()" << endl;
	}
private:
	int mb;
};

如果一个类里面定义了虚函数,那么编译阶段,编译器需给这个类类型产生一个唯一的vftable虚函数表。虚函数表中主要存储的内容就是RTTI指针和虚函数的地址。该类类型各对象中的vfptr指向第一个虚函数的起始地址! 基类与派生类中vftable表如图所示:

img

img

结果:image-20220328170719931

解析一:对结果上面动态绑定与静态绑定执行流程
分析一下:pb->show();
pb->show();编译阶段发现show()为Base类型,到基类作用域查看Base::show(),若show()为普通函数,就进行静态绑定call Base::show();若编译阶段指针为Base类型,到基类作用域查看Base::show(),发现show()为虚函数,就进行动态绑定,动态绑定汇编如下:

1.mov eax,dword ptr[pb] //将虚函数表的地址vfptr放入eax寄存器
2.mov ecx,dword ptr[eax] //将vfptr存的地址的4字节内存&Derive::show()地址放入ecx
3.call ecx //调用ecx,取虚函数地址,只有在运行时候才知道寄存器的地址,找到哪个地址调用哪个函数,这就是动态绑定,运行时期的绑定;

解析二:

sizeof变化的原因: 有虚函数多了vfptr指针;

解析三:

pb的类型:如果Base没有虚函数,* pb识别的就是编译时期的类型。* pb就是Base类型;
如果Base有虚函数,* pb识别的就是运行时期的类型:RTTI类型,即Derive类型;

构造函数:(调用任何函数都是静态绑定的)
1.构造函数不能称为虚函数; 原因:构造函数是生成对象的过程,对象还没存在,必然没有vfptr指针,也找不到vftable;
2.构造函数中调用虚函数也不会发生动态绑定。 原因:派生类对象构造顺序 基类 --> 派生类,此时基类调用虚函数,派生类还没有构造,无法调用动态绑定派生类的虚函数;(这里不一定正确,多看看)

static静态成员方法:静态成员方法调用不依赖对象,因此也不能成为虚函数。

5.虚析构函数

析构函数:可以成为虚函数,调用时候对象存在,在析构函数前加上virtual关键字。

看一段代码:

class Base
{
public:
	Base(int data) :ma(data)
	{
		cout << "Base()" << endl;
	}
	~Base()
	{
		cout << "~Base()" << endl;
	}
	virtual void show()
	{
		cout << "call Base::show()" << endl;
	}
protected:
	int ma;
};

class Derive : public Base
{
public:
	Derive(int data):Base(data), mb(data),ptr(new int(data))
	{
		cout << "Derive()" << endl;
	}
	~Derive()
	{
		delete ptr;
		cout << "~Derive() " << endl;
	}
private:
	int mb;
	int *ptr;
};

int main()
{
	Base *pb = new Derive(10);
	pb->show();//静态绑定pb Base*   *pb Derive
	delete pb;

	return 0;
}

执行结果:

在这里插入图片描述

问题:派生类的析构函数没有被调用到,内存泄露!

问题分析:pb的类型是Base类型,因此delete调用析构函数先去Base中找Base::~Base(),对于析构函数的调用就是静态绑定,没有机会调用派生类的析构函数,最后发生内存泄露!

解决方法: 将基类的析构函数定义为虚析构函数,派生类的析构函数自动成为虚函数。 pb的类型是Base类型,调用析构时去Base中找Base::~Base发现它为虚函数,发生动态绑定。而在派生类的虚函数表找到:&Derive:: ~derive,所以用派生类析构函数将自己部分进行析构,再调用基类的析构函数将基类部分析构。

问题:什么时候需要把基类的析构函数必须实现成虚函数?

基类的指针(引用)指向堆上new出来的派生类对象的时候,delete调用析构函数的时候,必须发生动态绑定,否则会导致派生类的析构函数无法调用

6.再谈动态绑定

问题:虚函数的调用一定就是动态绑定吗?

不是!!!类构造函数中,调用虚函数是静态绑定;对象本身调用虚函数,也是静态绑定;

直接上例子:

class Base
{
public:
	Base(int data = 0):ma(data){}
	virtual void show()
	{
		cout << "Base::show()" << endl;
	}
protected:
	int ma;
};

class Derive : public Base
{
public:
	Derive(int data = 0):Base(data), mb(data){}
	void show()
	{
		cout << "Derive::show()" << endl;
	}
private:
	int mb;
};

int main()
{
	Base b;
	Derive d;

	//①对象本身调用虚函数,静态绑定!因为自己怎么样都是调用自己的函数;没有转型;
	b.show();//虚函数 call Base::show();
	d.show();//虚函数 call Derive::show();
    
    //②派生类(基类也一样)指针调用派生类对象,派生类(基类也一样)引用调用派生类对象,是动态绑定!
    Derive *pd1 = &d;
	pd1->show();//Derive::show();
	Derive &rd1 = d;
	rd1.show();//Derive::show();
    Base *pb2 = &d;//基类指针指向派生类对象
    pb2->show();//Derive::show();
    
    //③强制类型转换(虽然会有访问错误),是动态绑定!
    Derive *pd3 = (Derive*)&b;
	pd3->show(); //最终还是调用Base::show(),因为是Base对象

	return 0;
}
//可通过反汇编查看是否是动态绑定;

👀总结:

  • 用对象本身调用虚函数,是静态绑定。
  • 动态绑定:虚函数前面必须是指针或引用调用才能发生动态绑定:基类指针指向基类对象,基类指针指向派生类对象,都是动态绑定。
  • 如果不是通过指针或者引用来调用虚函数,那就是静态绑定。

7.理解多态到底是什么

如何解释多态?(面试重点)

多态:多态字面理解为多种形态。多态分为静多态与动多态。

多态分为:

静态的多态:编译时期的多态:函数重载、模板(函数模板、类模板)

动态的多态: 运行时期的多态。在继承结构中,基类指针(引用)指向派生类对象,通过该指针(引用)调用同名覆盖方法(虚函数),基类指针指向哪个派生类对象,就会调用哪个派生类对象的同名覆盖方法,成为动多态。多态底层是通过动态绑定来实现的。 基类指针指向谁就访问谁的vfptr,继而继续访问vftable,从vftable拿出来的就是相应的派生类的方法;

静态多态、动态多态例子:

//静态的多态:函数重载
bool compare(int, int){}
bool compare(double, double){}

compare(10, 20); //call compare_int_int,在编译阶段就确定好调用的函数版本
compare(10.5, 20.5);//call compare_double_double

//静态的多态:模板,模板实例化发生在编译阶段
template<typename T>
bool compare(T a, Tb){}

compare(10, 20); //=>int 实例化一个compare<int>
compare(10.5, 20.5); //=>double 实例化一个compare<double>

//动态多态:以上个例子为例:
//动物基类
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;
	}
};
//错误写法:这样高耦合一一对应地写,若添加更多的新的动物,派生类对象越多,bark()方法还需要继续增加。相应的实体类若删除,其对应接口也要删除,不满足“开-闭”原则;
/**
void bark(Cat &cat)
{
	cat.bark();
}

void bark(Dog &dog)
{
	dog.bark();
}

void bark(Pig &pig)
{
	pig.bark();
}
**/
//正确写法:使用统一的基类类型接受派生类对象,再增加新的派生类型,API接口不用修改,很方便;
void bark(Animal *p)
{
	p->bark();
}

int main()
{
	Cat cat("小猫");
	Dog dog("小狗");
	Pig pig("小猪");

	bark(&cat);
	bark(&dog);
	bark(&pig);

	return 0;
}

由此也可以得知:继承的好处:

可以做代码的复用;②在基类中给所有派生类提供统一的虚函数接口,让派生类进行重写,然后就可以使用多态

8.理解抽象类

抽象类:拥有纯虚函数的类叫做抽象类

注意👀:抽象类不能再实例化对象了,但是可以定义指针和引用变量

问题一:抽象类和普通类有什么区别 ?

抽象类一般不是用来定义某一个实体类型的,不能实例化对象的,但是可以定义指针和引用变量。它所做的事情是:1.让所有的派生类类通过继承基类直接复用该属性。2.给所有的派生类保留统一的覆盖/重写接口;普通类定义指针,引用变量都可以,实例化对象也可以

问题二:一般把什么类设计成抽象类?

基类。 这个基类并不是为了抽象某个实体类型而存在的(比如动物抽象了无意义,猫狗这些实体可以抽象),它所做的事情是:1.让所有的派生类类通过继承基类直接复用该属性。2.给所有的派生类保留统一的覆盖/重写接口;

例子:改动一下上面的Animal类

因为我们定义Animal不是为了抽象某个实体,而是①提供string_name给所有动物实体复用这个属性;②给派生类有bark()方法,让所有派生类有统一的覆盖/重写接口;

我们初衷就是改成抽象类,那么修改一下:

/动物基类  泛指  类-》抽象一个实体的类型
class Animal
{
public:
	Animal(string name):_name(name){}
	virtual void bark() = 0;//纯虚函数!!!!
protected:
	string _name;
};

例子:看看车的例子:

#include <iostream>
#include <typeinfo>
#include <string>
using namespace std;

class Car
{
public:
	Car(double oil, string name): _oil(oil), _name(name) {}
	double getLeftMiles() {
		return _oil * this->getMilePerGallon();
	}
	string getName() {
		return _name;
	}
private:
	double _oil;
	string _name;
	virtual double getMilePerGallon() = 0;//纯虚函数,根据具体的汽车而定1L油跑的公里数,其实单独设成一个变量也可以解决问题;
};
class BMW : public Car
{
public:
	BMW(double oil, string name):Car(oil, name) {}
	double getMilePerGallon() {
		return 10.1;
	}
private:
};
class Audi : public Car
{
public:
	Audi(double oil, string name) :Car(oil, name) {}
	double getMilePerGallon() {
		return 11.1;
	}
private:
};
//给外部提供一个统一的获取汽车剩余路程数的API
void showLeftMiles(Car &p) {
	cout << "我的车型为:" << p.getName() << " 我还可以走:" << p.getLeftMiles() << endl;
}
int main() {
	Audi* temp = new Audi(10, "奥迪");
	BMW* test = new BMW(10, "宝马");
	showLeftMiles(*temp);
	showLeftMiles(*test);
	return 0;
}

结果:

image-20220328233909018

9.继承多态笔试题实战

题目一:猫狗叫问题

//动物基类  泛指  类-》抽象一个实体的类型
class Animal
{
public:
	Animal(string name):_name(name){}
	//纯虚函数
	virtual void bark() = 0;
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;
	}
};

int main()
{
	Animal *p1 = new Cat("加菲猫");
	Animal *p2 = new Dog("二哈");

	int *p11 = (int*)p1;
	int *p22 = (int*)p2;
	int tmp = p11[0];//p11[0]访问的Cat的前4个字节
	p11[0] = p22[0];//p22[0]访问的Cat的前4个字节
	p22[0] = tmp;//Cat的vfptr存放Dog的vftable地址,Dog的vfptr存放Cat的vftable地址,输出结果交换

	p1->bark();
	p2->bark();

	delete p1;
	delete p2;

	return 0;
}

所以输出结果:image-20220329004418877

题目二:继承结构中基类派生类同名覆盖方法不同默认值问题

class Base
{
public:
	virtual void show(int i = 10)
	{
		cout << "call Base::show i:" << i << endl;
	}
};

class Derive : public Base
{
public:
	virtual void show(int i = 20)
	{
		cout << "call Derive::show i:" << i << endl;
	}
};

int main()
{
	Base *p = new Derive();
	p->show();
    /*
    我们来看一下函数调用过程,调用一个函数时要先压参数(编译阶段),若没有传入实参,会将形参默认值压栈。运行时候才发生动态绑定调用派生类的show方法,编译阶段编译器只能看见基类Base的show,将基类的10压栈,参数压栈在编译时期确定好的。真正调用时不管是静态还是动态绑定,只要是show()方法形参压栈的值是固定的10;
       /*

        push 0Ah =》函数调用,参数压栈在编译时期确定好的
        mov eax, dword ptr[p]
        mov ecx, dword ptr[eax]
        call ecx
        */
    */
	delete p;

	return 0;
}

所以结果为:image-20220329010730800

题目三:派生类的方法写成私有的可以正常调用吗

class Base
{
public:
	virtual void show()
	{
		cout << "call Base::show" << endl;
	}
};

class Derive : public Base
{
private:
	virtual void show()
	{
		cout << "call Derive::show" << endl;
	}
};

int main()
{
	Base *p = new Derive();
	p->show();
	delete p;

	return 0;
}

成功调用:image-20220329011002643

分析一下:p->show()最终能够调用Derive的show,是在运行时期才确定的。但是成员方法的访问权限是不是public,是在编译阶段就需要确定好的,编译阶段会检测程序符不符合c++规则,比如访问权限等编译阶段编译器只能看见Base中的show为public,编译器可以调用;但最终调用的是基类的方法还是派生类的方法取决于形成的汇编指令是静态绑定还是动态绑定,因为为动态绑定最后成功调用派生类的show。

如果基类Base的show改成private就会错误!

问题四:下面两段代码是否正确

class Base
{
public:
	Base()
	{
		cout << "call Base()" << endl;
		clear();
	}
	void clear()
	{
		memset(this, 0, sizeof(*this));//Base大小赋为0
	}
	virtual void show()
	{
		cout << "call Base::show() " << endl;
	}
};

class Derive : public Base
{
public:
	Derive()
	{
		cout << "call Derive() " << endl;
	}
	void show()
	{
		cout << "call Derive::show() " << endl;
	}
};

int main()
{
	//情况1
	Base *pb1 = new Base();
	pb1->show();//动态绑定
	delete pb1;

	//情况2
	Base *pb2 = new Derive();;
	pb2->show();//动态绑定
	delete pb2;

	return 0;
}

情况1:系统奔溃image-20220329011956722

image-20220329011917054

分析一下:基类的对象内存如图,vfptr指向Base vftable,当调用clear()时,将基类的对象内存清为0,虚函数指针也变为0地址,进行动态绑定时,访问不到,调用时出错,程序崩溃。

在这里插入图片描述

情况2:成功输出image-20220329012325482

分析一下:我们vfptr里面存储的是vftable的地址,我们首先底层来看指令哪里将vfptr写入vftable中。

push ebp 
mov ebp,esp//函数帧栈底
sub esp,4Ch
rep stos esp<->ebp 0xCCCCCCCC //linux不会初始化

//先了解一下这个:①每一个函数进来首先push ebp:将调用方函数站点地址压进来,调用方函数站点地址放入当前函数栈底。再将esp赋给ebp,ebp指向当前函数栈底,sub esp 4Ch为当前函数开辟栈帧,rep sots esp<->ebp当前函数栈帧初始化为0;
//②准备工作做完之后,指向当前函数第一行指令。若该类中有虚函数,生成的对象前4个字节有vfptr,会在函数栈帧开辟完之后将vfptr写入vftable,才执行函数第一行指令。vfptr <—>&Base::vftable;
//③每次层构造函数都会执行刚才的步骤,派生类中会将vfptr<—>&Derive::vftable。

构造new Derive() : 首先调用基类的构造,构造基类部分,然后调用派生类构造,构造派生类部分, 基类和派生类中各会有一个vfptr;

当我们new一个Derive对象时,首先调用基类构造,基类构造首先会将基类的vftable写入vfptr,再调用clear会将虚函数的值清为0;再调用派生类的构造函数,派生类构造函数压完栈初始化后会将&Derive::vftable的地址写入到虚函数指针中。当我们用指针调用show()vfptr是有效的,能够从虚函数表中取出来派生类重写的show(),因此执行成功。
在这里插入图片描述

posted @ 2022-05-12 19:37  D-booker  阅读(311)  评论(0编辑  收藏  举报