继承+多态

目录

继承

概念

 定义格式

继承成员访问方式变化 

基类和派生类赋值转换 

切割

继承中的作用域 

派生类默认成员函数 

 继承与友元

继承与静态成员

菱形继承 

虚继承原理 

​编辑 继承与组合

 多态

 概念

多态条件 

协变 

 析构函数的重写

 C++11的override和final

抽象类 

多态原理 

 虚函数表

虚表指针何时生成 

虚函数与内联函数

虚函数与构造函数

虚函数与析构函数 

虚函数与静态成员 

 虚函数与普通函数

单继承中的虚函数表 

多继承中的虚函数表 


继承

概念

继承是使代码复用的手段,它可以在保持原特性的基础上进行功能拓展,体现面向对象程序的设计结构。

下面举例展示: 

class Person
{
public:
	string _name;
	int _age;
};
class Student:public Person
{
private:
	int _stid;//学号
};
class Teacher :public Person
{
private:
	int _tid;//工号
};

Person是基类,而Student和Teacher在继承Person的基础上又增加了自己的属性,即学号和工号。 

 定义格式

class 派生类:继承方式 基类 

继承成员访问方式变化 

类成员/继承方式public继承protected继承private继承
public成员派生类的public成员派生类的protected成员派生类的private成员
protected成员派生类的protected成员派生类的protected成员派生类的private成员
private成员派生类中不可见派生类中不可见派生类中不可见

从中我们可以发现:

  • 继承成员访问方式=min(基类访问方式,继承方式)
  • 基类的private成员在派生类中不可见,这里的不可见是指成员无法在类内和类外访问,即使已经继承到派生类中 

 注意:实际中大概率使用public继承,protected和private继承方式很少使用,并且基类成员一般是public成员。

基类和派生类赋值转换 

 派生类可以赋值给基类的对象,引用,指针,但是!基类不能赋值给派生类。

为什么派生类可以赋值给基类?他们类型不一样啊。

这里就要讲到切割了。 

切割

当子类对象赋值给基类对象时,会将基类那部分切割并赋值给基类。 

 

继承中的作用域 

  • 子类和基类都具有独立的作用域
  • 当子类和基类中的同名成员构成隐藏,也叫重定义,这种情况下子类将屏蔽对父类同名成员的访问,但可以使用基类::基类成员 访问。
  • 函数名相同就构成隐藏。

注意:函数隐藏和函数重载不同,函数重载要求处于同一作用域,而隐藏处于不同作用域。 

class Person
{
public:
	void f()
	{
		cout << "Person" << endl;
	}
	string _name;
	int _num=1;
};
class Student:public Person
{
public:
	void f()
	{
		cout << "Student" << endl;
	}
private:
	int _num=2;
};
class Teacher :public Person
{
public:
	void f()
	{
		cout << "Teacher" << endl;
	}
private:
	int _num=3;//工号
};
void test()
{
	Person p;
	Student s;
	Teacher t;
	cout << p._num << " " << s.Person::_num << " " << t.Person::_num << endl;
	s.f();
	s.Person::f();
	t.f();
	t.Person::f();
}

派生类默认成员函数 

调用规则:

  • 派生类构造函数:先调用父类构造函数初始化父类成员,再调用自己的构造函数。
  • 派生类析构函数:先调用自己的析构函数进行资源清理,然后调用父类的析构函数。
  • 派生类拷贝构造:继承的父类成员调用父类的拷贝构造,自己的成员调用自己的拷贝构造。
  • 派生类赋值重载:继承的父类成员调用父类赋值重载,自己的成员调用自己的。

如果不写派生类的默认成员函数:

a.父类继承下来的调用父类的默认成员函数

b.自己的与普通类规则一致,构造函数编译器默认生成,拷贝构造和赋值重载进行浅拷贝

子类何时需要写? 

 构造函数:当父类没有默认构造函数,需要我们进行显示调用

析构函数:当子类有资源需要清理

拷贝构造和赋值重载:涉及浅拷贝问题,需要自己写

怎么写?

class Person
{
public:
	Person(const char* name)
		:_name(name)
	{
		cout << "Person(const char* name)" << endl;
	}
	//拷贝构造
	Person(const Person& p)
	{
		_name = p._name;
		cout << "Person(const Person& p)" << endl;
	}
	Person& operator=(const Person& p)
	{
		cout << "Person& operator=(const Person& p)" << endl;
		if (this != &p)
		{
			_name = p._name;
		}
		return *this;
	}
	~Person()
	{
		delete[] ptr;
		cout << "~Person()" << endl;
	}
protected:
	string _name;
	int* ptr = new int[10];
};
class Student:public Person
{
public:
	Student(const char* name = "张三", int id = 1)
		:Person(name)
		, _id(id)
	{
		cout << "Student()" << endl;
	}
	Student(const Student &s)
		:Person(s)
		,_id(s._id)
	{
		cout << "Student(const Student &s)" << endl;
	}
	Student& operator=(const Student& s)
	{
		cout << "Student& operator=(const Student& s)" << endl;
		if (this != &s)
		{
			Person::operator=(s);
			_id = s._id;
		}
		return *this;
	}
	// 析构函数名字会被统一处理成destructor()。
	// 那么子类的析构函数跟父类的析构函数就构成隐藏
	~Student()
	{
		delete[] ptr;
		//Person::~Person();
		cout << "~Student()" << endl;
	}
	//子类析构函数结束,会自动调用父类析构函数
	//所以我们不用显示调用父类析构函数,避免析构两次
	//这样才能保证先析构子类,再析构父类
private:
	int _id;
	int* ptr = new int[10];
};
    void test()
    {
	Student s;
    }

 先初始化父类成员,然后再初始化自己,所以先析构自己,再析构父类,符合栈区规则。

注意:析构函数的名字会被统一处理为destructor,为什么呢?

这里我们先留一个悬念,等讲完多态再进行讲解。

 继承与友元

友元关系不会被继承。是父类的友元但不一定是子类的友元。 

继承与静态成员

 无论继承多少次,静态成员只有一个!整个体系中只有一个静态成员。

菱形继承 

菱形继承的缺陷在于数据冗余和二义性。 

如果进行普通继承:assistant继承了student和teacher,而teacher和student继承了person,也就是说assistant中含有两份person ,这就造成了数据冗余和二义性问题。

class Person
{
public:
	string _name; // 姓名
	int _a[10000];
};
class Student: public Person
{
public:
	int _num;
};
class Teacher : public Person
{
public:
	int _id;
};
class Assisant :public Student, public Teacher
{
protected:
	int _x;
};
int main()
{
	Assisant a;
	a._id = 1;
	a._num = 2;
	//二义性
	a.Student::_name = "小王";
	a.Teacher::_name = "大王";
	cout << sizeof(a) << endl;
}

数据冗余:assistant只需要一个数组空间,但是这样继承他就有两个数组。

二义性:assistant继承了两个名字,但是它应该只有一个名字。 

那么如果解决问题呢?下面引出虚继承。 

虚继承原理 

 解决方式:拦腰加virtual,也就是再直接继承父类的子类继承方式前virtual

class Person
{
public:
	string _name; // 姓名
	int _a[10000];
};
class Student:virtual public Person
{
public:
	int _num;
};
class Teacher :virtual public Person
{
public:
	int _id;
};
class Assisant :public Student, public Teacher
{
protected:
	int _x;
};

这样我们就解决了问题?下面讲解一下虚继承的原理。

 不使用虚继承的存储:

class A
{
public:
	int _a;
};

//class B : public A
class B :  public A
{
public:
	int _b;
};

//class C : public A
class C :  public A
{
public:
	int _c;
};

class D : public B, public C
{
public:
	int _d;
};

 

 使用虚继承的存储:

class A
{
public:
	int _a;
};

//class B : public A
class B : virtual public A
{
public:
	int _b;
};

//class C : public A
class C : virtual public A
{
public:
	int _c;
};

class D : public B, public C
{
public:
	int _d;
};

int main()
{
	D d;
	d.B::_a = 1;
	d.C::_a = 2;
	d._b = 3;
	d._c = 4;
	d._d = 5;
	d._a = 0;
	//采用虚继承后 通过偏移量找到_a 
	return 0;
}

a只有一份,但是我们发现_b和_c上面被一串莫名地址替代了,这串地址其实是用来存储b对象和c对象对_a的偏移量的,称作虚基表指针,可以通过偏移量找到_a。 

打开内存,查看偏移量。

b对象偏移量:00000014 也就是20字节

c对象偏移量:0000000c 也就是12字节

与上面内存观察一致。 

 继承与组合

  • 继承是一种is-a关系,每一个子类对象都是父类对象,简单来说关系就是:学生是人
  • 组合是一种has-a关系,B组合A,那么每一个B对象中都含有A对象,简单来说关系就是:头上有眼睛。

继承和组合访问成员方式区别: 

  • 继承可以访问父类公有成员和保护乘员
  • 组合只能访问公有成员
//继承
class A
{
public:
	int _a;
};
class B:public A  //B可以继承A的公有成员和保护成员
{
	int _b;
};
//组合
class C   
{
	int _c;
};
class D //D只能访问C的公有成员

{
	C _c;
	int _d;
};

实际当中大多使用组合:因为组合低耦合,高内聚,不同模块之间依赖程度低。

而继承依赖程度高,如果父类对象改变,势必影响所有子类,这是很危险的。

 所以优先使用组合,而不是继承!

 多态

 概念

多态有两种,一种是静态多态,还有一种是动态多态。

静态:函数重载:编译时实现

动态:通过父类的指针或引用去调用一个函数,传递不同的对象,所得结果不同,也就是不同的人去做同一件事,结果不同。原理:运行时实现。

多态条件 

  • 必须通过基类的指针或引用去调用虚函数
  • 被调用的函数必须是虚函数,并且子类必须对虚函数进行重写 

虚函数: 被virtual修饰的函数

重写条件: 

  • 是虚函数
  • 三同:返回值,函数名,参数必须相同
class Person
{
public:
	virtual void BuyTicket() 
	{
		cout << "买票-全价" << endl;
	}
};
class Student : public Person 
{
public:
	virtual void BuyTicket()
	{
		cout << "买票-半价" << endl;
	}
};
void Fun(Person& p)
{
	p.BuyTicket();
}
int main()
{
	Person p;
	Student s;
	Fun(p);
	Fun(s);
}

 

协变 

 协变是虚函数重写的一个例外,它可以让返回值不相同但也构成多态。

 协变:基类虚函数返回基类对象的指针或者引用,派生类虚函数返回派生类对象的指针或者引用时,称为协变。

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;}
};

 析构函数的重写

 为什么析构函数要完成重写?

class Person {
public:
 virtual ~Person() {cout << "~Person()" << endl;}
};
class Student : public Person {
public:
 virtual ~Student() { cout << "~Student()" << endl; }
};

编译器统一将析构函数的名字处理为destructor也是为了完成重写,下面场景演示为什么。 

当使用父类指针去管理父子对象时,再进行析构时,父子的析构函数必须构成多态,如果不构成多态,那么子类对象资源就没有被完全清理。 

//为什么析构函数要弄成虚函数进行重写
	//以下场景使用
	Person* ptr = new Person;
	Person* str = new Student;
	delete ptr;// ~Person
	delete str;// ~Student

 C++11的override和final

 final:修饰虚函数,表示该虚函数不能被重写

 override:修饰虚函数,检查该虚函数是否被重写,如果没有,编译就报错。

抽象类 

 包含纯虚函数的类称为抽象类。

 纯虚函数:虚函数后面加上=0

  • 抽象类不能实例化出对象
  • 继承抽象类的子类必须对虚函数进行重写才能实例化处对象 

体现了接口继承:子类继承父类虚函数接口,目的是为了完成重写,达成多态。

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();
}

多态原理 

 虚函数表

class Base
{
public:
	virtual void Func1()
	{
		cout << "Func1()" << endl;
	}
private:
	int _b = 1;
};

打开监视窗口

我们可以发现b中不仅存在一个_b成员,还存在一个_vfptr,_vfptr称为虚函数表指针,虚函数表内存储着虚函数地址。 

下面分析多态的原理: 

class Base
{
public:
	virtual void Func1()
	{
		cout << "Base::Func1()" << endl;
	}
	virtual void Func2()
	{
		cout << "Base::Func2()" << endl;
	}
	void Func3()
	{
		cout << "Base::Func3()" << endl;
	}
private:
	int _b = 1;
};
class Derive : public Base
{
public:
	virtual void Func1()
	{
		cout << "Derive::Func1()" << endl;
	}
private:
	int _d = 2;
};
int main()
{
	Base b;
	Derive d;
	return 0;
}

 

从中我们可以发现:Base对象内存储着虚函数func1和fun2的地址,而派生类Derive对象中对于fun1函数进行了重写,所以Derive对象虚函数表内fun1地址改变了 ,这就是多态的原理:虚函数通过虚函数表找到地址进行调用,如果子类对象传递给父类指针或引用,发生切割,就会在虚函数表内找到被重写后的函数地址,也就完成了不同对象调用不同函数这一操作。

下面提出一个问题:虚函数在哪?虚函数表在哪? 

很多人误以为虚函数在虚函数表,虚函数表在对象中,这是错误的。

其实虚函数存储在代码段,虚函数表内存储的是虚函数的地址!对象中存储的是虚函数表指针!不是虚函数表!!! 

虚表指针何时生成 

  • ps:虚函数表指针是在初始化列表之后生成的,顺序为:父类构造->子类构造->虚表生成,所以子类未构造前,如果父类构造需要调用被重写后的虚函数,那么此时调用的是父类的虚函数,不是子类重写后的虚函数!
  • 子类对象直接赋值给父类对象,虚表是不会参与拷贝的!所以如果不采用指针或引用,子类对象直接调用的永远是子类虚函数,父类永远是父类虚函数。
  • 一个类的不同对象共用一张虚表
  • 虚表编译时生成,虚表指针初始化列表后生成

虚函数与内联函数

 内联函数可以是虚函数,类内成员函数默认就是内联函数,编译器自动决定是否采用内联方式。如果不构成多态,内联函数+virtual保持内联属性,但是如果构成多态,由于虚函数地址要存到虚表内,而内联函数没有地址,所以此时内联属性消失。

虚函数与构造函数

 构造函数不能是虚函数,因为虚表指针是在构造函数初始化列表阶段生成的,如果是虚函数,那么没有虚表指针,找不到构造函数。

虚函数与析构函数 

析构函数可以是虚函数,也最好是虚函数,因为如果使用父类指针管理,那么就必须实现多态。 

虚函数与静态成员 

static和virtual不能同时使用,因为静态成员没有this指针,而虚函数又要类型::函数,或者指针->函数进行调用,找不到this指针,无法传递。 

 虚函数与普通函数

如果通过对象直接调用虚函数,那么此时虚函数与普通函数一样快,因为它不需要到虚表内找,如果通过指针或引用调用,此时构成多态,需要到虚表内寻找虚函数地址,此时普通函数快。 

单继承中的虚函数表 

class Base {
public:
	virtual void func1() { cout << "Base::func1" << endl; }
	virtual void func2() { cout << "Base::func2" << endl; }
private:
	int a;
};
class Derive :public Base {
public:
	virtual void func1() { cout << "Derive::func1" << endl; }
	virtual void func3() { cout << "Derive::func3" << endl; }
	virtual void func4() { cout << "Derive::func4" << endl; }
private:
	int b;
};

从虚函数表中可以发现d对象虚函数表内没有func3和func4的地址,这并不是不存在,而是编译器的bug,下面我们可以自己打印地址。

typedef void(*VFPTR)();
void PrintVtable(VFPTR* vtable)
{
	for (int i = 0; vtable[i] != nullptr; i++)
	{
		printf("第%d个虚函数指针:", i+1);
		cout << vtable[i] << endl;
        VFPTR f = vtable[i];
		f();
	}
}
class Base {
public:
	virtual void func1() { cout << "Base::func1" << endl; }
	virtual void func2() { cout << "Base::func2" << endl; }
private:
	int a;
};
class Derive :public Base {
public:
	virtual void func1() { cout << "Derive::func1" << endl; }
	virtual void func3() { cout << "Derive::func3" << endl; }
	virtual void func4() { cout << "Derive::func4" << endl; }
private:
	int b;
};
int main()
{
	Base b;
	Derive d;
	PrintVtable((VFPTR*)*((void* *)&b));
	cout << endl;
	PrintVtable((VFPTR*)*((void**)&d));
}

32位下虚函数表指针存储与对象的头4个字节,所以我们要取到头四个字节的值传递给printvtable函数,而64位下需要取八个字节,我们我们先将对象的地址强转成void**,然后简引用取到void*的大小,32位下4字节,64位下8字节,完美解决问题,接下来我们要将虚函数表(数组)地址传递,虚函数表内存储的是函数指针,由于数组的降级传递,所以我们要传递的是首元素地址,也就是虚函数地址的地址,也就是VFPTR*,所以再强制转换一次!(需要有C语言指针功底!)

 

多继承中的虚函数表 

typedef void(*VFPTR)();
void PrintVtable(VFPTR* vtable)
{
	for (int i = 0; vtable[i] != nullptr; i++)
	{
		printf("第%d个虚函数指针:", i+1);
		cout << vtable[i] << endl;
        VFPTR f = vtable[i];
		f();
	}
}
class Base1 {
public:
 virtual void func1() {cout << "Base1::func1" << endl;}
 virtual void func2() {cout << "Base1::func2" << endl;}
private:
 int b1;
};
class Base2 {
public:
 virtual void func1() {cout << "Base2::func1" << endl;}
 virtual void func2() {cout << "Base2::func2" << endl;}
private:
 int b2;
};
class Derive : public Base1, public Base2 {
public:
 virtual void func1() {cout << "Derive::func1" << endl;}
 virtual void func3() {cout << "Derive::func3" << endl;}
private:
 int d1;
};
int main()
{
    Derive d;
	VFPTR* vTableb1 = (VFPTR*)(*(void**)&d);
	PrintVtable(vTableb1);
	VFPTR* vTableb2 = (VFPTR*)(*(void**)((char*)&d + sizeof(Base1)));
	PrintVtable(vTableb2);
	return 0;
}

有细心的同学可能发现了Base1和Base2的虚函数表内存储的func1函数地址不同?为什么呢?他们不是都被重写了吗,按理来说应该一样,这其实是编译器的优化,优化后虚函数表内存储的是调用虚函数指令的地址,也就是jump指令的地址。

以上仅需了解,无需深入,我们只需要直到虚函数表内存储的是虚函数的地址即可。 

多继承中:派生类中未重写的虚函数地址存储在第一个继承的基类的虚函数表中。 

 以上就是对继承和多态的讲解,希望对你有所帮助。

  • 4
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

嚞譶

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值