C++继承详解

本文详细阐述了C++中的继承概念,包括继承的原理、基类和派生类的使用、作用域和隐藏、菱形继承与虚拟继承,以及子类的默认成员函数。同时讨论了继承与组合的区别,强调了组合的低耦合优势和优先级。
摘要由CSDN通过智能技术生成


前言

在本篇文章中我们将会学到有关继承方面的知识,其中C++中三大特性分别为:封装,继承多态。由此可见继承在学习中的重要性,接下来我们来一起看一下有关继承方面的知识吧!!!!😘 😚😘 😚😘 😚

一、继承的概念

继承是一种代码复用的重要手段,在保证原有类的基础上,再增加一下新的功能,成为一个新的类。

🌟🌟我们来举个例子,比如人,在学校分为很多类型,比如学生,老师。那他们有什么共同点信息呢?就比如:都具有姓名,年龄,身高,体重等。
但是我们会发现,我们每个学生都具有学号,而老师具有的是教职工号。
前面的大部分属性都相同,唯独学号,教职工号这一点不同,那如果我们写成两个类,就会出现很多重复的数据。
这时,继承就出现了。

继承:将相同的属性放置到父类中,自己独有的放在子类中
其中父类也称为基类,子类也称为派生类

继承呈现出面向对象程序设计的层次结构,继承是类设计层次的复用。

🌟🌟我们来看一下子类是如何定义的呢

class Person
{
public:
void Print()
{
   cout << "name:" << _name << endl;
   cout << "age:" << _age << endl;
}
protected:
   string _name = "peter"; // 姓名
   int _age = 18; // 年龄
};

class Student : public Person
{ }
class/struct 派生类 : 继承方式 基类

我们定义两个看一下


class Teacher :public Person
{
public:

protected:
	int _teaid;//教职工号
};

class Student  :public Person
{
public:

protected:
	int _stuid;//教职工号
};

继承后的子类拥有父类的成员方法和成员变量

🌟🌟我们调用看一下

在这里插入图片描述
print函数是在Person类中实现的,但是Student和Teacher类继承了Person类,拥有了Person类的成员变量和成员方法。

🌟🌟我们在实现子类的方法中,派生类和基类我们都可以理解,但是这个继承方法是什么呢???我们接下来看一张表格理解一下。
在这里插入图片描述
🔱我们先来看一下,最后一行,在派生类中都是不可见的。那不可见是什么意思呢??
父类中的private成员和方法,虽然可以继承给子类,但是在子类中是不可以使用和访问的。
那麽看起来,这个private好像没什么用了,我们都不能使用还定义它干什么。确实是这样,我们今后在学习中,一般都不采用private.
🔱我们不使用private,那父类中的成员变量就全部暴漏给外边了,可以随便访问修改,那不就符合我们封装的意义了。我们引入了一个新的关键字:protected。它允许子类中的成员访问,但不允许外边的进行访问。
🔱class中默认是private的,struct中默认是public的,我们建议都写上。
🔱基类的私有成员在子类都是不可见。基类的其他成员在子类的访问方式 == Min(成员在基类的访问限定符,继承方式),public > protected> private。

我们一般使用:父类中使用public和protected定义变量和方法。子类继承方法采用public继承。

二、基类和派生类对象赋值转换

子类成员可以赋值给父类成员,但是父类成员不可以赋值给子类成员。
父类赋值给子类一般都是这样,但是有特殊情况,基类的指针或引用可以通过强制类型转换赋值给派生类指针或者引用。
但是必须是基类的指针是指向派生类对象才是安全的。这里的基类如果是多态类型,可以使用dynamic_cast识别后进行安全转换。

派生类对象 可以赋值给 基类的对象 / 基类的指针 / 基类的引用。这里有个形象的说法叫切片或者切割。寓意把派生类中父类那部分切来赋值过去

	Student s1;
	//派生类对象赋值给基类对象
	Person p1 = s1;
	//派生类对象赋值给基类引用
	Person &p1 = s1;
	//派生类对象赋值给基类指针
	Person *p1 =& s1;

我们看一下底层逻辑,子类在赋值给父类时,子类的变量多赋值就会发生切片。
这其中不会产生临时变量

在这里插入图片描述

三、继承中的作用域,隐藏

父类和子类拥有不同的作用域

那就这就说明父类和子类可以有相同的成员变量和成员方法,但是我们在日常生活中一般不建议这样使用,但是考试题目中及其喜欢出这样的题目。

🌟🌟我们看一下下面这个例子

class Person
{
protected :
   string _name = "小李子"; // 姓名
   int _num = 111; // 身份证号
};
class Student : public Person
{
public:
   void Print()
   {
     cout<<" 姓名:"<<_name<< endl;
     cout<<" 学号:"<<_num<<endl;
   }
protected:
   int _num = 999; // 学号
};

Student s1;
s1.Print();
结果答案输出999,也就是说明访问子类成员。

当子类和父类有相同的成员变量时,子类成员会隐藏掉对父类成员的访问,这种就叫做隐藏,同时也叫做重定义。

那我们如何访问父类中的成员变量呢?

基类:基类变量
Person::_num;

🌟🌟对于同名的成员方法,子类是如何处理的呢?

class A
{
public:
	void fun()
	{
		cout << "func()" << endl;
	}
};
class B : public A
{
public:
	void fun(int i)
	{
		cout << "func(int i)->" << i << endl;
	}
};

我们看一看这是不是我们之前学过的重载。

重载:同一个作用域,同名不同参的函数

很显然,这并不是重载,虽然有相同的函数名,但是不是同一个作用域。

成员函数隐藏:只要函数名相同就可以,返回值和参数可以不同

很显然,这就是隐藏。
B b;
b.fun(10);
同样,访问子类的方法,想要访问父类:A::fun();

四、菱形继承,菱形虚拟继承

单继承:只有一个直接父类
在这里插入图片描述

多继承:有两个或者以上的直接父类
在这里插入图片描述

菱形继承:多继承的一种特殊情况
在这里插入图片描述
我们来看一下菱形继承,会不会有什么问题

class Person
{
public:
	string _name; // 姓名
};
class Student : public Person
{
protected:
	int _num; //学号
};
class Teacher : public Person
{
protected:
	int _id; // 职工编号
};
class Assistant : public Student, public Teacher
{
protected:
	string _majorCourse; // 主修课程
};

我们会发现Assistant这个类中出出现了两个_name,这就会发生二义性和空间浪费
在这里插入图片描述

当然我们可以指定父类解决这个问题

Assistant a;
a.Student::_name = “peng”;
a.Teacher::_name = “teach”;

在这里插入图片描述

在这里插入图片描述

但是这毕竟解决不了本质,有些数据我们根本不需要存两份,比如:身份证号,年龄等。
我们可以采用虚拟继承的方法来解决这个问题。
我们只需要加上关键字virtual就可以
在这里插入图片描述

class Person
{
public:
	string _name; // 姓名
};
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; // 主修课程
};

在这里插入图片描述
我们来看一下底层是如何实现的呢??

我们为了方便分析,用下面这一段代码来演示

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

我们看一下内存,确实看到了数据的冗余
在这里插入图片描述
我们再来看一下虚拟继承
在这里插入图片描述
我们发现把A对象存在了最后的位置。那B,C是如何找到A的呢,并进行访问??
这里是通过两个指针实现的,这两个指针称作虚基表指针,指向的表叫做虚基表,虚基表里存放的就是偏移量。我们通过这个偏移量去访问到A

在这里插入图片描述

对象中多个成员存储顺序按照声明顺序确定。
根据对象顺序和内存对齐规则,编译时确定好内存位置,进行访问。
虚函数是在运行时确定的,因为我们在编译时不能确定哪个对象调用的。

我们所用的IO流就是用的虚拟继承这种方式
在这里插入图片描述

多继承可以说是c++的一种缺陷,我们一般不建议设计多继承

五、子类的默认成员函数

我们先来想一下我们主要实现哪几个默认成员函数:初始化,析构函数,拷贝构造,复制重载。
对于父类的默认成员函数我们很轻松就可以实现。
对于子类呢??我们知道子类中不仅仅包含自己的那部分成员,还拥有父类的那部分。

对于派生类,父类的那部分默认成员函数需要调用它的,自己的那部分调用自己的。我们可以把父类当成一个自定义类型看待。

根据这个逻辑我们来简单实现一下。

class Person
{
public:
	//初始化
	Person(string name,int age)
		:_name(name)
		,_age(age)
	{
		cout << "Person(string name,int age)" << endl;

	}

	//拷贝构造
	Person(const Person& p)
	{
		cout << "Person(const Person& p)" << endl;

		_name = p._name;
		_age = p._age;
	}
	//赋值
	Person& operator=(const Person& p)
	{
		cout << "Person& operator=(const Person& p)" << endl;
		if (this!=&p)
		{
			_name = p._name;
			_age = p._age;
		}
		return *this;
	}
	//析构
	~Person()
	{
		cout << "~Person()" << endl;
	}

protected:
	string _name;
	int _age;
};

class Student:public Person
{
public:
	//初始化
	Student(string name, int age,int id)
		:Person(name,age)
		,_id(id)
	{
		cout << "	Student(string name, int age,int id)" << endl;
	}
	//拷贝构造
	Student(const Student&stu)
		:Person(stu)
	{
		cout << "	Student(const Student&stu)" << endl;
		_id = stu._id;
	}
	//赋值
	Student& operator=(const Student &stu)
	{
		cout << "Student& operator=(const Student &stu)" << endl;
		if (this != &stu)
		{
			Person::operator=(stu);
			_id = stu._id;
		}
		return *this;
	}

	//析构
	~Student()
	{
		cout << "~Student()" << endl;
	}

protected:
	int _id;

};

我们看一下主要的这几部分
在这里插入图片描述
接下来我们下面这个情况
在这里插入图片描述

初始化先调用父类的构造,在调用子类的构造
析构函数先调用子类的构造。在调用父类的析构
子类析构函数在被调用完之后会自动调用父类的析构函数(我们不需要显示调用父类析构)
这个地方父类和子类的析构函数也构成隐藏,特殊处理,函数名处理为destrutor().父类析构函数不加virtual,子类和父类析构函数构成隐藏关系。

假设先父后子,可能存在安全隐患。可能父类资源已经清理释放了,子类对象可能又去访问,存在野指针等风险。

六 .继承和组合

什么是组合呢??我们通过下面一段代码来看一下。

class Tire{
protected:
   string _brand = "Michelin"; // 品牌
   size_t _size = 17; // 尺寸
};
class Car{
protected:
   string _colour = "白色"; // 颜色
   string _num = "陕ABIT00"; // 车牌号
   Tire _t; // 轮胎
};

Tire _t; 这个地方就是组合。不同的类这在一起。

我们可以发现继承和组合都是一种代码复用,那他们有什么区别呢??

继承实际上是一种白箱复用:继承允许你根据基类的实现来定义派生类的实现。基类的内部细节对子类可见,这一定程度上破坏基类的封装。基类的改变会对派生类产生很大的影响。基类可以访问父类protected的成员。这种依赖关系很强的,我们称为高耦合

组合:新的复杂的功能可以来完成,对象组合要求被组合对象具有良好定义的接口
对象内部的具体细节是不可见的,称为黑箱复用。组合类之间没有很强大依赖关系,耦合度低,优先使用对象组合有助于你保持每个类被封装。

那我们平时选择哪种方式进行代码复用呢??

优先使用组合,组合耦合度低,代码可维护性好,优先使用对象组合有助于你保持每个类被封装。
比如有些地方我们就适合用继承,比如多态等等。

我们一般都说public继承是一种is-a的关系,每个派生类对象都是一个基类对象。就比如Person和Student。
组合是一种has-a的关系,假设B组合了A,每个B对象中都有一个A对象。比如:车子和轮胎

七.继承与友元/静态成员

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

通过一段代码看一下

在这里插入图片描述
这里会报错.。

我们也可以解决,把子类也设计成友元

class Student;
class Person
{
public:
	friend void Display(const Person& p, const Student& s);
protected:
	string _name; // 姓名
};
class Student : public Person
{
public:
	friend void Display(const Person& p, const Student& s);
protected:
	int _stuNum; // 学号
};
void Display(const Person& p, const Student& s)
{
	cout << p._name << endl;
	cout << s._stuNum << endl;
}
int main()
{
	Person p;
	Student s;
	Display(p, s);

	return 0;
}

继承与静态成员:子类和父类共用一个静态成员,不是各一个。静态成员是存放在静态区的

class Person
{
public:
	Person() { ++_count; }
protected:
	string _name; // 姓名
public:
	static int _count; // 统计人的个数。
};
int Person::_count = 0;
class Student : public Person
{
protected:
	int _stuNum; // 学号
};
class Graduate : public Student
{
protected:
	string _seminarCourse; // 研究科目
};
void TestPerson()
{
	Student s1;
	Student s2;
	Student s3;
	Graduate s4;
	cout << " 人数 :" << Person::_count << endl;
	Student::_count = 0;
	cout << " 人数 :" << Person::_count << endl;
}

int main()
{
	TestPerson();
	return 0;
}

子类也可以对这个静态成员进行访问

总结

以上就是今天要讲的内容,本文仅仅详细介绍了C++继承的相关知识。希望对大家的学习有所帮助,仅供参考 如有错误请大佬指点我会尽快去改正 欢迎大家来评论~~ 😘 😘 😘

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

lim 鹏哥

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

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

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

打赏作者

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

抵扣说明:

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

余额充值