当前位置: 首页 > article >正文

继承(C++)

继承

  • 继承的概念及定义
    • 继承的概念
    • 继承的定义
      • 定义格式
      • 继承关系和访问限定符
      • 继承基类成员访问方式的变化
  • 基类和派生类对象赋值转换
  • 继承中的作用域
  • 派生类的默认成员函数
  • 继承与友元
  • 继承与静态成员
  • 复杂的菱形继承及菱形虚拟继承
    • 虚拟继承的原理

继承的概念及定义

继承的概念

继承是面向对象程序设计使代码可以复用的重要手段,允许程序在保持原有类特性的基础上进行扩展,增加功能,所产生的新类,称作派生类。继承呈现了面向对象程序设计的层次结构,体现了有简单到复杂的过程,继承是类设计层次的复用

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

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

class Teacher :public Person
{
protected:
	int _jobid;//工号
};

int main()
{
	Student s;
	Teacher t;
	s.Print();
	t.Print();
	return 0;
}

在这里插入图片描述

继承后父类的 Person的成员,都会变成子类的一部分,在 Print的打印中体验了复用的效果

继承的定义

定义格式

Person称作父类,也是基类;Teacher称作子类,也是派生类
在这里插入图片描述

继承关系和访问限定符

在这里插入图片描述

继承基类成员访问方式的变化

类成员/继承方式public继承
基类的public成员派生类的public成员
基类的protected成员派生类的protected成员
基类的private成员派生类的private成员

最终的继承成员:类成员和继承方式中权限较低的那个

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

派生类对象可以赋值给基类的对象/指针/引用,与以往所学习的有所不同,子类可以赋值给父类

观察下列代码

int main()
{
	int i = 1;
	double d = 2.2;
	i = d;
	const int& ri = d;
	return 0;
}

i=d时,并不是直接赋值,而是先创建一个临时变量,临时变量中的值为2,临时变量具有常性,所以在下面引用时,必须加上 const否则程序便会崩溃

在这里插入图片描述

上面的问题在继承中就不会出现,因为赋值的原理都不同;继承中的赋值并不会产生临时变量,而是直接将子类赋值给父类,简单来说是子类中与父类相同类型的数据赋值给父类,也称切片

int main()
{
	Person pn;
	Teacher t;
	pn = t;
	return 0;
}

在这里插入图片描述

继承中的作用域

  1. 在继承中基类和派生类都有独立的作用域
  2. 当子类和父类中有同名成员时,子类成员将屏蔽父类对同名成员的直接访问,此情况称作隐藏,也称作重定义
  3. 如果成员函数也同名,也构成隐藏
class Person
{
protected:
	string _name = "zhangsan";//姓名
	int _age = 18;//年龄
};

class Student :public Person
{
public:
	void Print()
	{
		cout << "姓名:" << _name << endl;
		cout << "年龄:" << _age << endl;
	}
protected:
	int _age=20;//年龄
};

void test()
{
	Student s;
	s.Print();
}

在这里插入图片描述

Student,Person中的_age学号构成隐藏,程序运行之后打印的是子类中的_age,将父类中的_age隐藏了起来

如果想访问父类中的_age,可以做如下操作

class Student :public Person
{
public:
	void Print()
	{
		cout << "姓名:" << _name << endl;
		cout << "年龄:" << Person::_age << endl;
	}
protected:
	int _age=20;//年龄
};

在这里插入图片描述

派生类的默认成员函数

普通类的成员

  1. 内置类型
  2. 自定义类型

在这里插入图片描述

派生类的成员

  1. 基类对象
  2. 派生类的内置类型
  3. 派生类的自定义类型

派生类中基类对象调用基类对应的函数完成初始化/清理/拷贝,内置类型/自定义类型的处理与普通类一致

class Person
{
public:
	Person(const char* name = "zhangsan")
		:_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;
	}

protected:
	string _name;
};


class Student:public Person
{
public:
	Student(const char* name)
		:Person(name)
		,_stuid(210202)
	{
		cout << "Student()" << endl;
	}

	Student(const Student& st)
		:Person(st)
		,_stuid(st._stuid)
	{
		cout << "Student(const Student& st)" << endl;
	}

	Student& operator=(const Student& st)
	{
		cout << "Student& operator=(const Student& st)" << endl;
		if (this != &st)
		{
			Person::operator = (st);
			_stuid = st._stuid;
		}
		return *this;
	}

	~Student()
	{
		Person::~Person();
		cout << "~Student()" << endl;
	}
protected:
	int _stuid;
};

int main()
{
	Student st("zhangsan");
	return 0;
}

在这里插入图片描述

析构函数有所区别,子类析构函数和父类析构函数构成隐藏关系,由于多态的要求,所有的析构函数都会特殊处理成destructor函数名,所以在子类的析构函数中需要加上访问限定符Person::才能调用父类的析构函数;从上面的打印结果来看,父类的析构多调用了一次,只是为什么呢???

在这里插入图片描述

通过查看汇编,我们发现,在子类的析构函数执行结束后,编译器会自动调用父类的析构函数进行资源清理工作;也就是说,我们不需要在子类中调用父类的析构函数

删除子类中调用的父类析构函数运行结果如下

在这里插入图片描述

仔细观察发现其中也有些不寻常,子类后创建为什么先析构呢?
其实这也是析构函数特殊的地方:子类先析构,父类再析构,子类析构函数中不需要显示调用父类析构,子类析构结束后编译器会自动调用父类析构

继承与友元

友元关系不能继承,基类友元不能访问派生类私有和保护成员

class Person
{
public:
	friend void Display(const Person& p,const Student s);
	Person(const char* name = "zhangsan")
		:_name(name)
	{
		cout << "Person()" << endl;
	}

protected:
	string _name;
};


class Student :public Person
{
public:
	Student(const char* name)
		:Person(name)
		, _stuid(210202)
	{
		cout << "Student()" << endl;
	}
protected:
	int _stuid;
};

void Display(const Person& p,const Student s)
{
	cout << p._name << endl;
	cout << s._stuid << endl;
}

int main()
{
	Person p("zhangsan");
	Student s("lisi");
	Display(p,s);
	return 0;
}

在这里插入图片描述

继承与静态成员

如果基类定义了static静态成员,则整个继承体系里面只有一个这样的成员

class Person
{
public:
	Person()
	{
		++_count;
	}

protected:
	string _name;

public:
	static int _count;//统计人数
};

int Person::_count = 0;

class Student :public Person
{
protected:
	int _stuid;
};

int main()
{
	Person p;
	Student s;

	p._count++;
	cout << p._count << endl;
	cout << &p._count << endl;

	s._count++;
	cout << s._count << endl;
	cout << &s._count << endl;

	return 0;
}

在这里插入图片描述

打印的地址都一样,说明static成员,并不在类中,而是存储在静态区中的;静态成员属于整个类,所有对象,同时也属于所有派生类及对象

复杂的菱形继承及菱形虚拟继承

单继承:一个子类只有一个直接父类的继承关系

在这里插入图片描述

多继承:一个子类有两个或者以上直接父类的继承关系

在这里插入图片描述

菱形继承:多继承的一种

在这里插入图片描述

菱形继承存在着某些问题,观察下列代码

class Person
{
public:
	string _name;
};

class Student :public Person
{
protected:
	int _stuid;
};

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

class Assistant :public Teacher, public Student
{
protected:
	string _majorcourse;
};

int main()
{
	Assistant a;
	a._name = "zhangsan";
	return 0;
}

在这里插入图片描述

程序运行之后便会报错,因为Assistant a中有两份_name,一份是Student,一份是Teacher的,所以使用时就造成了二义性;解决方式:使用时加上访问限定符

int main()
{
	Assistant a;
	a.Student::_name = "zhangsan";
	a.Teacher::_name = "lisi";
	return 0;
}

指定作用域并没有彻底地解决菱形继承所造成的问题,接下来介绍虚拟继承来彻底地解决问题

StudentTeacher中继承 Person时使用虚拟继承

在这里插入图片描述

具体操作如下

class Person
{
public:
	string _name;
};


class Student :virtual public Person
{
protected:
	int _stuid;
};

class Teacher :virtual public Person
{
protected:
	int _jodid;
};

class Assistant :public Teacher, public Student
{
protected:
	string _majorcourse;
};

int main()
{
	Assistant a;
	a._name = "zhangsan";
	return 0;
}

在这里插入图片描述

既然已经知道虚拟继承可以解决菱形继承存在的问题,那么原理是什么呢?要做到知其然知其所以然,接下来就深入学习虚拟继承的原理

虚拟继承的原理

观察下列代码
菱形继承:

class A
{
public:
	int _a;
};

class B :public A
{
public:
	int _b;
};

class C :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;
}

通过查看内存,可以很清楚地发现数据冗余

在这里插入图片描述

菱形虚拟继承:

class A
{
public:
	int _a;
};

class B :virtual public A
{
public:
	int _b;
};

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

在这里插入图片描述

通过查看内存,BC中都存在一个指针,分别对指针进行取地址发现,指针所指向的是虚基表,其中保存着距离虚基类对象_a的偏移量(第二行),通过偏移量可以计算出BC中虚拟继承的_a的位置,在上面已经标注出来。

虚拟继承原理解释如下

在这里插入图片描述

继承和组合

  1. public继承是一种is a的关系,每个派生类对象中都是一个基类对象
  2. 组合是一种has a的关系,比如B组合了A,则每个B对象中都有一个A对象

http://www.kler.cn/a/8135.html

相关文章:

  • PHP API如何使用access_token开放接口有效期
  • 超子物联网HAL库笔记:定时器[外部模式]篇
  • C# 集合与泛型
  • 微服务架构面试内容整理-SpringCloud Netflix‌与Spring Cloud Alibaba比较
  • 论文阅读《机器人状态估计中的李群》
  • 【C++】详解RAII思想与智能指针
  • Spring-aop面向切面
  • Tomcat使用https配置实战
  • chatGPT中国入口-ChatGPT评论文章-ChatGPT怎么用
  • 多线程冲突处理方法,锁
  • 首届“兴智杯”产业赛收官,文心大模型助推产业创新
  • 量化注意事项和模型设计思想
  • rsync远程同步实现快速、安全、高效的异地备份
  • Spring 之循环依赖
  • 【计算机网络-网络层】路由选择协议
  • Redission分布式锁
  • vue3脚手架Vite
  • ChatGPT常用prompts汇总
  • springboot感受优化06
  • 使用for循环对ArrayList在遍历时删除存在的问题
  • Linux如何在Ubuntu系统服务器上安装 Jenkins?【详细教程】
  • 初识设计模式 - 策略模式
  • 基于SpringBoot+Vue的家政平台
  • 命名空间和程序集
  • Mysql 的B+树索引 和HASH索引
  • 160. 相交链表 ——【Leetcode每日一题】