尽信书不如无书,用实践学透继承与多态底层

一、继承
1、定义格式:class 派生类:继承方式+基类

class Student : public Person

2、访问权限:
a.基类的private成员在派生类中不可见,语法上限制访问(类里面和类外面都不能用)。跟private也不一样,类外面不能使用,类里面可以使用。
b.基类private成员在派生类中不能被访问,如果基类成员不想在类外直接被访问,但需要在派生类中能访问,就定义为protected(可以看出,保护成员限定符是应继承才出现的)。
c.权限:public>protected>private,取权限小的那一个。
d.class默认私有,struct默认共有。
e.在实际运用中一般使用都是public继承。
3、赋值
 · 父类不能赋值给子类。
 · 子类赋值给父类:赋值兼容转化(切割,切片)。
aa2c9b0ad14b46bd8e1361ee843b721d.png

#include<iostream>
using namespace std;

class Person
{
public:
	string _name = "peter"; // 姓名
	int _age = 18;  // 年龄
};

class Student : public Person
{
protected:
	int _stuid; // 学号
};

int main()
{
	int i = 0;
	const double& d = i;//产生临时变量

	Person p;
	Student s;

	Person p1 = s;// 赋值兼容转换(切割,切片)
	Person& rp = s;//不产生临时变量(成为父类部分对象的别名)

	return 0;
}

4、作用域:
a.在继承体系中,基类和派生类都有独立的作用域。
b.隐藏/重定义:在继承体系中,基类和派生类都有独立的作用,若子类和父类有同名成员,子类成员将屏蔽父类,对同名成员的直接访问。
c.如果是成员函数的隐藏,只需要函数名相同,就构成隐藏。
d.在实际中,在继承体系里面最好不要定义同名的成员。

例题: 

 • 两个fun构成什么关系? a、隐藏/重定义   b、重载   c、重写/覆盖  d、编译报错
答案:a  (父子类域中,成员函数名相同就构成隐藏)

 • 编译结果是什么?答案:如图所示

#include<iostream>
using namespace std;

class Person
{
public:
	void fun()
	{
		cout << "Person::func()" << endl;
	}

protected:
	string _name = "小星星"; // 姓名
	int _num = 114; 	   // 身份证号
};

// 隐藏/重定义:子类和父类有同名成员,子类的成员隐藏了父类的成员
class Student : public Person
{
public:
	void fun(int i)
	{
		cout << "Student::func()" << endl;
	}

protected:
	int _num = 514; // 学号
};

注意:重载需要在同一个作用域,若不同作用域(的“重载”),即为隐藏/重定义

5、派生类的默认成员函数
a.构造函数
• 不能在类里面显示调用初始化列表初始化父类成员或者基类成员。只能初始化派生类。
• 派生类必须(自动)调用父类的构造函数初始化父类的成员。
• 若没有父类的默认构造,则向匿名对象一样初始化父类成员(继承成员声明在自定义成员声明的前面)。
b.析构函数
• 由于后面多态的原因,析构函数的函数名被特殊处理了,统一处理成destructor。
• 显示调用父类析构无法保证先子后父,所以子类析构函数完成就自动调用父类析构。

#include<iostream>
using namespace std;

class Person
{
public:
	//父类默认构造
	Person(const char* name = "月")
			: _name(name)
	{
		cout << "Person()" << 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()
	{
		cout << "~Person()" << endl;
		delete _pstr;
	}
protected:
	string _name ; // 姓名
	string* _pstr = new string("114");
};

class Student : public Person
{
public:
	// 子类默认构造
	Student(const char* name = "星", int id = 514)
		:Person(name)//显示调用父类构造(初始化顺序:继承成员声明默认在自定义成员声明的前面)
		,_id(id)
	{}
	//子类拷贝构造
	Student(const Student& s)
		:Person(s)
		,_id(s._id)
	{}
	//子类运算符重载
	Student& operator=(const Student& s)
	{
		if (this != &s)
		{
			Person::operator=(s);//显示调用父类运算符重载
			_id = s._id;
		}
		return *this;
	}
	//子类析构
	~Student()
	{
		//自动调用父类析构,无需显示调用父类析构
		//Person::~Person();
		cout << *_pstr << endl;
		delete _ptr;
	}
protected:
	int _id;
	int* _ptr = new int;
};

6、其他
 • 友元关系不能继承。
 • 静态成员属于父类和派生类,在派生中不会单独拷贝一份,只是继承了使用权。

7、菱形继承:数据冗余和二义性问题

7a10015837394ef6a5714bf07a4799e4.png

菱形继承底层原理讲解:

d4959a106fdf499bba2ff873c0152faa.png解决方案:虚继承

#include<iostream>
using namespace std;

class Person
{
public:
	string _name; // 姓名
	int _age;
};

class Student : virtual public Person
{
protected:
	int _num; //学号
};

class Teacher : virtual public Person
{
protected:
	int _id; // 职工编号
};

class Assistant : public Student, public Teacher
{
protected:
	string _majorCourse; // 主修课程
};

菱形虚拟继承底层原理讲解:先取到偏移量,计算_a在对象中的地址,再访问023cb526dee64347845631f90eb4fd36.png

建议:多继承(按照声明顺序初始化)谨慎使用,避免使用菱形继承


8、继承和组合:
软件工程讲究高内聚低耦合:
• public继承是一种is a的关系,也就是说,每个派生类对象都是一个基类对象。
• 组合是一种has a的关系,假设B组合了A,每个B对象中都有一个A对象。

 

二、多态

1、虚函数:被virtual修饰的类成员函数称为虚函数(只有成员函数才能成为虚函数)。
虚函数的重写(覆盖):派生类中有一个跟基类完全相同的虚函数(即派生类虚函数与基类虚函数的返回值类型函数名字参数列表完全相同),称子类的虚函数重写了基类的虚函数。

class Person {
public:
	virtual	void BuyTicket() const { cout << "买票-全价" << endl; }
};

class Student : public Person {
public:
	virtual void BuyTicket() const { cout << "买票-半价" << endl; }
};

2、多态:不同对象传递调用不同函数,多态调用看指向的对象,普通对象看当前类型。

• 普通函数继承是一种实现继承,多态函数继承是一种接口继承
• 多态条件:
a.调用函数重写的虚函数。
b.基类指针或者引用。

void func1(const Person& p)//引用
{
	p.BuyTicket();
}

void func2(const Person* p)//指针
{
	p->BuyTicket();
}

int main()
{
	Person pp;
	func1(pp);

	Student st;
	func2(&st);//赋值兼容转化

	return 0;
}

3、虚函数重写的一些细节(大坑):
重写的条件本来是虚函数加三同,但是有一些例外:
a.派生类的重写虚函数可以不加virtue(建议都加上)。
b.协变,返回的值可以不同,但是要求返回值必须(同时)是父子关系指针和引用。

4、构造函数加virtual也为虚函数重写,并且类析构函数都被处理成destructor这个统一的名字。
以下为析构函数必须构成重写的情况,否则将产生内存泄漏

int main()
{
	//Person p;
	//Student s;

	Person* p = new Person;
	p->BuyTicket();
	delete p;

	p = new Student;
	p->BuyTicket();
	delete p; // p->destructor() + operator delete(p)

	// 这里我们期望p->destructor()是一个多态调用,而不是普通调用

	return 0;
}

5、final:修饰虚函数,表示该虚函数不能再被重写
Override:帮助派生检查是否完成重写,如果没有就会报错
• 设计不想被继承类,如何设计?
法1:基类构造函数私有。(C++98)
法2:基类加一个final(最终类)(C++11)

总结:

03b2b78c760845369aca2740efcc7e68.jpeg
6、虚函数重写原理讲解
• 虚函数会放在代码段中,虚表里存的是虚函数的地址
• 覆盖是原理上的概念,重写是语法上的概念(实现子类调子类,父类调父类)
• 普通调用在编译时确定地址,符合多态运行时到指向对象的虚函数表中,找调用函数的地址
例:以下程序输出结果是什么?   答案:B->1

#include<iostream>
using namespace std;

class A
{
public:
    virtual void func(int val = 1) { std::cout << "A->" << val << std::endl; }//this->func()
    virtual void test(){ func(); }//父类指针A*(切片),三同,满足虚函数重写,但虚函数重写的是实现,声明依旧是父类的
};

class B : public A
{
public:
    void func(int val = 0) { std::cout << "B->" << val << std::endl; }//输出 B->1
};

int main(int argc, char* argv[])
{
    B* p = new B;//派生类指针
    p->test();//调父类函数
    return 0;
}

• 问:为什么不能子类指针或者引用?
答:因为只有父类指针/引用既能调用父类,又能调用子类对象。

• 问:为什么不能父类对象?
答:如果父类对象可以,那必须拷贝。但子类赋值给父类对象切片,不会拷贝虚表。如果拷贝虚表,那么父类对象虚表中调用的是父类虚函数,还是子类虚函数就不确定了

• 问:虚基表和虚函数表的区别?
虚基表是存偏移量的
虚函数表,简称虚表(本质是函数指针数组),,虚表一般在结尾位置会给一个nullptr

• 虚表存在常量区(不能被修改):(实验讲解)

b9c5c3622674495e800ee576d43c4ba7.png

 • 派生类成员函数有时在监视窗口不显示(底层原理讲解)

032b72d0662b4cc9a1658550fb2ad21e.png

 

• 静态(编译时)的多态函数调用:静态绑定:编译时候就确定
  动态(运行时)的多态函数调用:继承、虚函数、重写实现的多态

 7、多继承的虚表(底层原理讲解)

• 多继承有多个虚表,派生类不用自己生成虚表

• 多继承派生类的未重写虚函数放在第一个继承虚类部分的虚函数表中

0b79d2d9d562407b95b2fb7251515442.png

 • 菱形虚拟继承虚表与虚基表的位置

a.菱形虚拟继承(只有重写A的虚函数)

//菱形虚拟继承(只有重写A的虚函数)
#include<iostream>
using namespace std;

class A
{
public:
	virtual void func1()
	{
		cout << "A::func1" << endl;
	}
public:
	int _a;
};

class B : virtual public A
{
public:
	virtual void func1()
	{
		cout << "B::func1" << endl;
	}
public:
	int _b;
};

class C : virtual public A
{
public:
	virtual void func1()
	{
		cout << "C::func1" << endl;
	}
public:
	int _c;
};

class D : public B, public C
{
public:
	virtual void func1()
	{
		cout << "D::func1" << endl;
	}
public:
	int _d;
};

int main()
{
	D d;
	d.B::_a = 1;
	d.C::_a = 2;
	d._b = 3;
	d._c = 4;
	d._d = 5;

	return 0;
}

ee0f576eafe74d3ba16450c4bc58fb48.png

 

b.菱形虚拟继承(重写A的虚函数+B和C单独有的虚函数)

//菱形虚拟继承(重写A的虚函数+B和C单独有的虚函数)
#include<iostream>
using namespace std;

class A
{
public:
	virtual void func1()
	{
		cout << "A::func1" << endl;
	}
public:
	int _a;
};

class B : virtual public A
{
public:
	virtual void func1()
	{
		cout << "B::func1" << endl;
	}

	virtual void func2()
	{
		cout << "B::func2" << endl;
	}
public:
	int _b;
};

class C : virtual public A
{
public:
	virtual void func1()
	{
		cout << "C::func1" << endl;
	}

	virtual void func2()
	{
		cout << "C::func2" << endl;
	}
public:
	int _c;
};

class D : public B, public C
{
public:
	virtual void func1()
	{
		cout << "D::func1" << endl;
	}

	virtual void func3()
	{
		cout << "D::func3" << endl;
	}
public:
	int _d;
};

int main()
{
	D d;
	d.B::_a = 1;
	d.C::_a = 2;
	d._b = 3;
	d._c = 4;
	d._d = 5;

	return 0;
}

 085b3a58c8894f22b9e094bb6e9c2f40.png

 

 三、抽象类:

1、在虚函数的后面写上=0,则这个函数为纯虚函数包含纯虚函数的类叫做抽象类(也叫接口类),抽象类,不能实例出对象。派生类继承后也不能实例化出对象,只有重写纯虚函数,派生类才能实例化出对象。纯虚函数规范了派生类必须重写,另外纯虚函数更体现出了接口继承。
2、没有对象,就没有虚表。所以抽象类没有虚表。

 

 四、常见问答题:(以下内容非原创)

1.问:什么是多态?
答:静态的多态:运算符重载。
动态的多态:继承中的重写+父类指针调用。
两者本质:更方便和灵活运用多种形态的调用。
2. 问:什么是重载,重写(覆盖),重定义(隐藏)?答:(见上述详解)
3. 问:多态实现原理?答:函数名修饰规则、虚函数表。(见上述详解)
4. 问:Inline函数可以是虚函数吗?答,可以。不过编译器就忽略inline属性,这个函数就不再是inline,因为虚函数要放到序表中去。
5. 问:静态成员可以是虚函数吗?答,不能因为静态成员函数没有this指针使用类型成员函数的调用方式无法访问虚函数表,所以静态成员函数无法放进虚函数表,无法实现出多态,也就没有意义,所以语法会强制检查。
6. 问:析构函数可以是虚函数,那构造函数呢?答:因为虚表是在编译时生成,虚表指针在走初始化列表阶段初始化。
7. 问:析构函数可以是虚函数吗?什么场景下,虚构函数是虚函数?答:可以,并且最好把基类的虚构函数定义成虚函数。(见上述详解)
8. 问:对象访问普通函数快还是虚函数更快?答:首先,如果是普通对象是一样快的,如果是指针对象或者引用对象,则调用的普通函数快,因为构成多态运行时调用虚函数,需要到虚函数表中去查找。
9. 问:虚函数表是在什么阶段生成的?存在哪的?答:虚函数表是在编译阶段就生成的,一般情况下存在代码段(常量区)的。(见上述底层详解)
10. 问:C++菱形继承的问题?虚继承的原理?答:数据冗余和二义性。(见上述详解)
11. 问:什么是抽象类,抽象类的作用?答:抽象类强制重写了虚函数,另外,抽象类体现出接口继承关系。

 

评论 5
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值