【C++】继承

我们在实际中可能遇到这种情况,比如说两个类有相同的成员变量,当然也有不同的(比如说老师和学生都有姓名和年龄等,但是学生有学号,老师有工号等)。在这种情况下,难道我们要重复的去定义吗?对于大佬来说,当然是不允许的。所以,就有了继承这个概念,就是说子类要去继承父类中的东西,子类也叫派生类,父类也叫基类

那么我先写一个简单的父类和子类

class person {
public:
	void func() {
		cout <<"_name "<< _name << endl;
		cout << "_age " << _age << endl;
	}
protected:
	string _name="abc";
	int _age=0;
};
class student :public person {//public就是继承方式

protected:
	int _s_id;
};
class teacher :public person {

protected:
	int _t_id;
};
int main() {
	student s1;
	s1.func();
	return 0;
}

父类的成员函数也是会继承给子类的,那么,不知道你有没有发现,我们这里给成员变量的访问限定符是protected,之前都没有用过这个。如果要讨论这个,那就不得不提及基类成员在派生类中的访问方式

父类成员在子类中的访问方式跟父类成员的访问限定符和继承方式有关,它们有这样的关系

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

首先我们认为public>protected>private,这是它们之间的能够访问的权限的大小的比较。在前两行都是取权限小的那个。

最后一行的不可见是什么意思呢?不可见并不是不存在而是存在却不可在子类中直接用,更不要说在外边了,但是可以间接用,比如说调用个父类的函数是可以访问的到的。

如果不写继承方式的话,class默认是private,struct 默认是public,当然我们肯定最好写上

我们大多数用到的继承方式是public,其他两个用到的很少

我们之前学的函数模板是函数层面上的代码复用,而这里的继承是类层次上的复用

我们知道赋值时如果有类型转换会产生临时变量,比如说

就是因为产生的临时变量具有常属性,所以要用const引用

但是在我们继承这里,子类对象可以赋值给父类对象,当引用或者用指针时父类对象其实就用的是子类的一部分,把这一部分切割出来给父类,所以我们叫切割或者是赋值兼容转换

我们可以看到,它们的地址确实是一样的,它们指的确实是同一块地方

基类和派生类都有各自独立的作用域,这就说明它们可以有同名的成员,我们直接用的话是用的子类的,父类的是隐藏的,要想用的话要指明类域。并且对于成员函数来说,只要父类子类中的函数名相同则构成隐藏或者叫重定义,这就意味着返回值和参数可以不同。并且这不叫函数重载,因为函数重载是要定义在同一作用域的

我们正常的类有六个默认成员函数,基类就和正常的类是一样的,派生类不同,因为他有一部分是继承的父类的。对于派生类来说,我们要自己实现的就四个:构造,析构,拷贝构造,赋值重载,至于取地址重载我们一般不用自己实现

下面我们先来看一下构造函数,其实对于父类的成员就去调用父类的构造函数,我们写子类时不用处理,就算要处理,也是通过父类的构造函数取处理,对于自己的成员你就需要去写上。并且最后初始化的顺序是先父类成员,后子类成员,因为初始化顺序跟声明顺序有关。大概就是这样的

class person {
public:
	person(const char* s="Tony", int n=10)
		:_name(s)
		,_age(n)
	{}
protected:
	string _name;
	int _age;
};

class student :public person {
public:
	student(const char* s = "Tony", int age = 10, int id = 1)
		:_s_id(id)
		, person(s, age)
	{}
protected:
	int _s_id;
};

下面是拷贝构造,它的大致形式就是长这样的

第二个为什么一个student的对象可以传给person呢?因为它们在进行类型转换的时候叫做切割,这就是我们上边说的,下边看一下赋值重载

为什么子类在调用父类的赋值重载时要指明类域呢?因为它们两个函数名相同,构成隐藏关系,只能通过指明才能调出来。下面看一下析构

这里的子类为什么还要指明类域呢?因为由于后面的多态的原因,析构函数会被特殊处理,函数名都会被处理成destructor(),所以子类的析构函数和父类的析构函数会构成隐藏关系,并且为了保证先子后父,父类的析构会在子类析构后自动调用,所以上面那么写也是不对的,应该不用写第一句。为什么要保证先子后父呢?这是由于子类会访问父类的成员,如果先析构父类,则会不安全。

下一个要说的就是友元关系不能继承,这个也好理解,父亲的朋友并不是我们的朋友,要想让它成为我们的朋友我们就要自己声明一下。

如果基类定义了static静态成员,那么则整个继承体系中只有一个这样的成员,无论派生出多少个子类,都只有一个static成员实例

下面我们说一下单继承和多继承,单继承的意思是只有一个直接父类,多继承是指有两个或更多直接父类。

我们之前说的都是单继承,没有什么问题,但是对于多继承来说就有问题了,比如B和C都单继承于A,而D多继承于B和C,这就导致一个问题,就是D这个类中有两份A的成员,这就叫做棱形继承,它会出现数据的冗余和二义性

class A {
public:
//protected:
	int _a;
};
class B :public A{
public:
//protected:
	int _b;
};
class C:public A {
public:
//protected:
	int _c;
};
class D :public B, public C {
public:
//protected:
	int _d;
};

int main() {
	D dd;
	dd._a = 10;//不明确
	return 0;
}

这时如果我们要访问dd的_a就会显示_a不明确

当然我们如果指明类域的话可以的,但是并不是每个成员都有两个意义,我们只需要一个,这时就要用到虚继承,就是给B和C加上virtual

这样_a就只有一份了,下面我们从底层,也就是内存的角度来看看它们是怎么存储的

先看不是虚继承的

再看虚继承的

这两个内存分别是dd和ddd的地址处的内存情况,我们可以看到第一行和第三行是一样的,这其实也是一个地址,我们看看其中存着什么

我们可以看到,一个存的是20,一个是12,这是十进制。它指的就是_a存在后面20和12个字节的位置,这个表也叫虚基表,存的就是偏移量。总之,应该是当初祖师爷没想到多继承后面会出现这样的问题,等到发现之后已经无法删掉多继承了,所以就想到了这样的办法去弥补。

下面我们再来看一下组合和继承,组合就是类A是类B的一个成员,类B还有其他成员。其实组合就像一个黑箱测试,我只管用类A,用类A的函数也是通过它去用的,它里边是怎么实现的我不知道;继承呢就像一个白箱测试,类B是去继承类A的函数,类B自己去用,这就要求类B要知道类A的函数是怎么实现的。从另一个角度来说,组合的耦合度低,而继承的耦合度高,所以我们也优先使用组合。

  • 27
    点赞
  • 27
    收藏
    觉得还不错? 一键收藏
  • 2
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值