【C++】继承

前言

本章我们要学习,面向对象编程的三大特性之一的继承。
我们简要阐述继承的使用场景

多个类中存在相同的属性和行为时,将这些内容抽取到单独一个类中,那么多个类无需再定义这些属性和行为,只需要继承那个类即可。
多个类可以称为子类,单独这个类称为父类或者超类,基类等。

一. 继承基本语法

在我们日常生活中,人是我们的共性,但是在人之下,我们可以有很多的身份,一个人可以是学生,老师,工程师…所以,人就可以作为一个基类,而细化的学生,老师,工程师则是人的子类

代码如下

class Person
{
public:
	void Print()
	{
		cout << "name:" << _name << endl;
		cout << "age:" << _age << endl;
	}

protected:
	string _name = "张三";
	int _age = 18;
};

而细化的学生,老师,可以是这样的。因为身份的细化,可能会有新的属性,这里我们仅举例一两个

class Student : public Person
{

protected:
	int _stuid;
};

class Tercher :public Person
{
protected:
	int _jodid;
};

我们先实例化一个学生,这里看到,我们在学生类里并没有定义_name,_age,Print函数。但是,因为是继承关系,子类将会拥有父类的一切属性

int main()
{
	Student s1;
	s1.Print();

	return 0;
}

在这里插入图片描述
这里Student类成功继承了Person的_name,_age和Print函数。

二. 继承基类访问限制

其实,子类继承基类属性有三种方式,以上我们使用的public公有继承,其实还有protected保护继承private私有继承
以下是继承后访问限制规则
在这里插入图片描述

访问限定符的访问权限:public>protected>private

  1. 公有继承,子类继承的父类的属性访问限定都不变
  2. 保护继承,子类继承的父类属性,在保护以上的会发生降级,降为保护。
  3. 私有继承,子类继承的父类属性,在私有以上的会发生降级,降为私有。
  4. 基类的私有成员,子类不管已任何方式继承,都不可访问。
  5. protected和private的属性,类外都不可访问。区别是父类的protected,子类可以访问,父类的private子类也不可访问
    6.子类继承如果不显示写继承方式,class默认是private继承struct默认是public继承

但在实际运用中,一般都是public继承几乎很少使用protected/private继承,也不提倡使用,因为protected/private继承的成员只能在子类中使用,实际运用中扩展维护性不强

三. 基类和子类对象赋值转换

我们在赋值时,如果是内置类型,相近的类型赋值会发生隐式类型转换。
比如,我们将double赋值给int,会产生临时对象。
在这里插入图片描述

那我们能否将子类对象赋值给父类呢?会不会发生隐式类型转换,产生临时对象呢?
答案是:不会发生隐式类型转换,也不会产生临时对象
但是会发生一个被称为切片或者切割的现象

切片/切割:派生类对象可以赋值给基类的对象 / 基类的指针 / 基类的引用。过程发生切片 / 切割。寓意把子类中父类部分的那部分切回给父类
基类对象不能赋值给派生类对象
在这里插入图片描述


我们再看一个现象
在这里插入图片描述
这里可以证明,子类给父类赋值没有产生临时对象。
同时,对于引用和指针的理解,可以如下图
在这里插入图片描述
指针和引用都是指向子类中父类的那一部分。

四. 继承中的作用域

基类和子类都有其自己的作用域。
在子类中,我们可以定义和父类一样的变量或者函数呢?
答案是可以的。因为子类和父类是不同的作用域,并且如果不显示调用,则会遵循就近原则。
测试代码如下;

//父类
class Person
{
public:
protected:
	string _name = "张三";
	int _age = 30;
};

//子类
class Student : public Person
{
public:
	void Print()
	{
		cout << "name:" << _name << endl;
		cout << "age:" << _age << endl;
	}
protected:
	int _age = 18;
};

int main()
{
	Student s1;
	s1.Print();

	return 0;
}

在这里插入图片描述

如果要使用父类的同名变量,需要指定作用域

void Print()
{
	cout << "name:" << _name << endl;
	cout << "age" << Person::_age << endl;
	cout << "age:" << _age << endl;
}

在这里插入图片描述

  1. 在继承体系中,父类和子类都有独立的作用域
  2. 子类和父类中有同名成员,子类成员将屏蔽父类对同名成员的直接访问,这种情况叫隐藏,也就重定义。(在子类成员函数中,可以使用 父类::父类成员 访问)
  3. 需要注意的是如果是成员函数的隐藏,只需要函数名相同就构成隐藏
  4. 注意在实际中的继承体系里面,最好不要定义同名成员

五. 子类的默认成员函数

子类会继承父类的所有属性,无论是变量还是函数,那么默认成员函数呢?构造,析构,拷贝,赋值…
我们接下来学习子类继承的默认成员函数
我们先举个例子

//父类
class Person
{
public:
	Person(const string&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 operaot=(const Person&p)" << endl;
		if (this != &p)
		{
			_name = p._name;
		}

		return *this;
	}

	~Person()
	{
		cout << "~Person()" << endl;
	}
protected:
	string _name;
};

//子类
class Student : public Person
{
public:
protected:
	int _num = 18;
};

1.构造函数

我们定义一个Person父类,Student子类。我们在父类中,写好四个默认成员函数,构造,拷贝,赋值,析构。但是在子类中并没有显示写默认成员函数,接下来我们实例化一个Student。
在这里插入图片描述
我们可以看到,实例化的子类Student,其内部不仅有赋值好的_name,还调用了父类的构造和析构函数。
我们再更改一下子类的内容。
我们增加一个子类的默认构造(全缺省也是默认构造函数)

Student(const string&name="张三",const int&num=18)
		:_name(name)
		,_num(num)
	{
		cout << "Student()" << endl;
	}

但是这并不行,会报错
在这里插入图片描述

在继承中规定,父类的成员初始化,必须使用父类的构造函数,子类的构造会自动调用父类的相应的构造函数。
我们可以这么处理

Student(const string&name = "张三", const int&num = 18)
		:Person(name)
		,_num(num)
	{
		cout << "Student()" << endl;
	}

调用父类的构造函数。

在这里插入图片描述

2. 拷贝构造

通过构造函数的学习,我们知道父类部分的成员变量。需要调用父类的相应构造 / 函数初始化。拷贝构造同样如此

Student(const Student&s)
		:Person(s)
		,_num(s._num)
	{}

在这里插入图片描述

而我们在上面讲到的,子类对象赋值给父类发生的切片。实际是调用了父类的拷贝构造,将子类中父类部分的数据切片用拷贝构造实例化
在这里插入图片描述

3. 赋值重载

赋值重载也同样遵循规则

Student& operator = (const Student&s)
	{
		if (this != &s)
		{
			Person::operator=(s);
			_num = s._num;
		}

		return *this;
	}

在这里插入图片描述

4.析构函数

析构函数的规则有些不一样,父类的析构函数不需要显示调用,其会在调用子类的析构函数之前调用。
我们在Student类中补上析构函数,以便观察

~Student()
	{
		cout << "~Student()" << endl;
	}

在这里插入图片描述

我们看到,构造是先构造父类,再构造子类。而析构时先析构子类,再析构父类
编译器在编译子类析构函数时,会在子类析构函数最后一条语句之后,插入调用基类析构函数的语句

比如我们在Student析构内部什么都不写
在这里插入图片描述
在这里插入图片描述
通过反汇编,我们可以看到,在Student析构的最后,有call Person父类析构。

六. 继承的小知识点

  1. 父类的友元函数不可继承,父类的友元函数,在子类中没有继承,如果需要,则还需要将该函数设置为子类的友元函数
  2. 父类的静态成员,共同属于父类和子类,且无论是子类访问,还是父类访问,都是同一个
  3. 父类的构造函数或者析构函数私有化,就实现了一个不可以被继承的类。因为子类构造需要调用父类的构造,子类的析构也需要调用父类的析构,如果二者其一私有化,则子类也无法调用,故不可以继承

七. 菱形继承

现实生活中,一个事物具有多种特征,抽象过来,就是一个类可以有多个父类。这就可能会出现菱形继承的问题。

在这里插入图片描述
因为Assistant的两个父类有相同的成员_name。而隐藏是发生在父子类中的,所以这边不会发生隐藏,但直接指定_name是不可以的,因为不知道指定的是哪个作用域的_name,发生二义性
如果要访问,需要指明作用域。但成员变量一般不需要同名。
就像记录我们信息时,其实是不需要两个名字的。即使我们在生活中有很多小名,外号啥的,但是信息记录只需要记录一个就好
在这里插入图片描述

菱形继承会造成数据冗余和二义性

结束语

本章是对继承的初次学习,感谢您的阅读
如果觉得本篇文章对你有所帮助的话,不妨点个赞支持一下博主,拜托啦,这对我真的很重要。
在这里插入图片描述

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值