C++面向对象之-----继承(二)

继承是面向对象编程语言中代码复用的一种形式,是子类与父类之间的一种关系。

既然是类,那么类的默认成员函数会不会也被继承过去呢?

子类的默认成员函数

了解 类的默认成员函数

在这里插入图片描述

构造函数与析构函数

先对之前的People类和Student类加上构造函数和析构函数

class People
{
public:
	People(string name = "Windy_X",int age = 20)
		:_name(name)
		, _age(age)
	{
		cout << "People 构造函数" << endl;
	}

	~People()
	{
		cout << "People 析构函数" << endl;
	}
	void ShowInfo()
	{
		cout << "People ShowInfo" << endl;
	}
protected:
	string _name;//姓名
	int _age;	//年龄
};

class Student:public People
{
public:
	Student(int Sid = 419)
		:_Sid(Sid)
	{
		cout << "Student 构造函数" << endl;
	}
	~Student()
	{
		cout << "Student 析构函数" << endl;
	}
	void ShowInfo()
	{
		cout << "Student ShowInfo" << endl;
	}
public:
	int _Sid;	//学号
};

在这里插入图片描述
可以推导出在我们在子类中没有重写父类的析构与构造函数的时候,这两函数的执行逻辑是这样的:
在这里插入图片描述
子类的析构函数在被调用完之后再调用父类的析构函数清理父类的成员。这样就可以保证子类对象先被清理,再清理父类成员的顺序,这也是在函数执行时在栈帧中后进先出的一个顺序。

当我们在子类中想要调用父类的析构函数的时候:
在这里插入图片描述
这里需要注意,一个类中只能有一个析构函数,所以我们在子类中调用父类的析构函数时,需要在子类的析构函数中调用,或者使用多态中的虚函数

我们看到,当子类中调用父类的析构函数时,等到函数执行完毕,父类的析构函数一共被调用了两次。这里因为没有写析构函数具体的执行逻辑,所以说没有报错,但是当涉及到深拷贝的时候,这样析构两次,会搞事情的。

拷贝构造

在子类中对子类对象进行拷贝构造的时候,必须调用父类的构造函数先对父类成员进行初始化。

如果父类没有默认构造函数,那么必须在子类构造函数的初始化列表阶段显示调用。

  • 子类显示调用父类的拷贝构造函数时

在这里插入图片描述

	People(const People& P)
	{
		cout << "People 拷贝构造函数" << endl;
	}

其中在People::People(S);这一步,是发生了从子类到父类的强制类型转换,发生了切片。

  • 子类隐式调用父类默认构造函数

在这里插入图片描述
在这里插入图片描述
通过调试,就会发现,编译器在进行子类拷贝构造的时候,自动调用了父类的默认构造函数。所以构造函数的参数还是写成全缺省为好。

如果父类中没有默认构造函数,或者默认构造函数需要传参,编译器也是会报错的
在这里插入图片描述

赋值运算符重载

子类的operator=必须调用父类的operator=先完成父类的复制。

	Student& operator=(const Student& S)
	{
		if (this != &S)
		{
			People::operator=(S);
			_Sid = S._Sid;
		}

		cout << "Student 拷贝构造函数" << endl;
		return *this;
	}

赋值运算符重载中,编译器并不会自动调用父类的运算符重载函数,这需要我们在程序中书写。

注意:
在这些默认成员函数中,当牵扯到初始化,赋值,拷贝这样的操作时,一定要先对父类进行操作,因为子类很有可能会隐藏父类的某些成员变量。

这时候,如果先给子类赋值或者初始化,就会造成之后调用父类的初始化时,产生一个二义性的结果。

继承与友元

友元的关系不能继承,也就是说父类友元不能访问子类私有和保护的成员

class A
{
public:
	A(int a = 0)
		:_protected(a)
		,_private(a)
	{}
	friend class B;
protected:
	int _protected;
private:
	int _private;
};

class B
{
public:
	void Fun(A& Aobj)
	{
		cout << "B -->  Aobj" << endl;
	}
private:
	int _b;
};

class C:public B
{
public:
	void Fun(A& Aobj)
	{
		cout << Aobj._protected << endl;//会报错,不能访问
		cout << Aobj._private << endl;//会报错,不能访问
		cout << "C -->  Aobj" << endl;
	}
};

在这里插入图片描述
三个类的关系:

  • A类的友元类是B类
  • B类是C类的父类

结论:A类的友元B类的子类,C类不能直接访问A类的私有和保护成员。但是可以通过B类提供的接口间接进行访问

继承与静态成员

父类中定义了一个static静态成员,那么整个继承的关系网中只有一个这样的静态成员。

无论有多少个子类,都只有一个staitc成员实例

class B
{
public:
	static int _sum;
protected:
	int _b;
};

int B::_sum = 0;

class C:public B
{
protected:
	int _c;
};

在这里插入图片描述
我们在子类中并没有把父类的静态成员变量进行隐藏,所以静态成员变量只有一个。

如果想要区分子类的静态成员变量的话,就得要在子类中对父类的成员变量进行隐藏

class C:public B
{
public:
	static int _sum;
protected:
	int _c;
};

int C::_sum = 0;

在这里插入图片描述

菱形继承

继承也分两种,一种是单继承,一种是多继承。(就跟兄弟姐妹之间的关系一样)

  • 单继承
    一个子类只有一个直接父类

在这里插入图片描述

  • 多继承
    一个子类有两个或两个以上的直接父类

在这里插入图片描述
菱形继承呢,他是多继承的一种特殊的情况

在这里插入图片描述
可以看到,菱形继承类似于爷孙的关系,到了Grede类这里,会存在对于People类中的数据冗余的问题
在这里插入图片描述
然后我们调用该类,对People类中的数据(数据变成public公有)进行更改的时候,就会出现访问不明确的现象
在这里插入图片描述
当需要访问的时候,我们只能显示指定我们访问那个父类的People成员,这样就可以解决这个二义性问题。
在这里插入图片描述
但是当这个继承的关系变得特别长的时候呢?。。。难道需要对着族谱把父辈每个类的名字都写出来,然后精确到祖宗???

这个时候,就需要使用虚拟继承

虚拟继承

虚拟继承可以解决菱形继承中关于数据二义性和数据冗余的问题

class People
{
public:
	string _name;//姓名
	int _age;	//年龄
};

//学生类
class Student :virtual public People
{
protected:
	int _Sid;	//学号
};

//继承的教师类
class Teacher :virtual public People
{
protected:
	int _Tid;//教师号
};

//班级类
class Grade :public Student, public Teacher
{
protected:
	int _Gid;
};

在这里插入图片描述

通过内存分析一下虚拟继承解决二义性和数据冗余的方式

测试程序

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

void text8()
{
	D d;
	d.B::_a = 1;
	d.C::_a = 2;
	d._b = 3;
	d._c = 4;
	d._d = 5;
	cout << "d .size() = " << sizeof(d) << endl;
}
  • 不使用虚拟继承

在这里插入图片描述
最后的D类的大小是 d .size() = 24

  • 使用虚拟继承后

在这里插入图片描述
最后类的大小是d .size() = 24

这里可以分析出D对象中将A放到的了对象组成的最下面,这个A
同时属于B和C,那么B和C如何去找到公共的A呢?这里是通过了B和C的两个指针,指向的一张表。

这两个指针叫虚基表指针,这两个表叫虚基表。虚基表中存的偏移量。通过偏移量可以找到下面的A

虽然这个看似类的大小没有什么变化,那是因为我们的成员变量是普通类型,当存在数组类型的时候呢?

  • 直接继承,d .size() = 8012
  • 虚拟继承,d .size() = 4020

相比之下,还是虚拟继承更节省空间,利用了一个段基址 + 偏移量来找到那些冗余的部分

关于多继承,java老师在讲课的时候就说过,java不像C++,他是不支持多继承的。
那个时候不知道为啥,这次明白了

继承与组合

public继承是一种is-a的关系。也就是说每个派生类对象都是一个基类对象。

class A
{
protected:
	int _a;
};
class Bpublic A
{
private:
	int _b;
};

组合是一种has-a的关系。假设B组合了A,每个B对象中都有一个A对象。

class B
{
private:
	int _b;
	A _Aobj;
};

组合就是直接把原本的父类,变成了自己的成员

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值