<C++> 继承

目录

前言

一、继承概念

1. 继承概念

2. 继承定义格式

3. 继承关系和访问限定符 

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

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

三、继承中的作用域

四、派生类的默认成员函数

五、继承与友元

六、继承与静态成员

七、菱形继承及菱形虚拟继承

1. 菱形继承

2. 虚继承

总结

前言

        在代码编写中,如果一段代码重复多次 被调用,那么我们会将其封装为一个函数,提高代码复用性,例如交换函数swap;同样的,对于类的成员函数或成员变量,如果在多个类中重复出现,那么我们可以提取公共数据,封装为一个基类,使其它类来继承基类。 


一、继承概念

1. 继承概念

        继承(inheritance)机制是面向对象程序设计使代码可以复用的最重要的手段,它允许程序员在保持原有类特性的基础上进行扩展,增加功能,这样产生新的类,称派生类。继承呈现了面向对象程序设计的层次结构,体现了由简单到复杂的认知过程。以前我们接触的复用都是函数复用,继承是类设计层次的复用

2. 继承定义格式

例: 

class Person
{
public:
	void Print()
	{
		cout << "name:" << _name << endl;
		cout << "age:" << _age << endl;
	}
protected:
	string _name = "peter"; // 姓名
	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;
}

3. 继承关系和访问限定符 

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

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

 不可见:在语法上限制访问,类里面和类外面都不能使用 (父类的私有成员不管什么继承都不可以使用) 。它跟private不同,private在类外不能使用,类里面可以使用。

子类继承父类的成员变量和成员函数,但是因为成员函数不在类内部,这似乎也叫不了继承

总结:

  1. 基类private成员在派生类中无论以什么方式继承都是不可见的。这里的不可见是指基类的私有成员还是被继承到了派生类对象中,但是语法上限制派生类对象不管在类里面还是类外面都不能去访问它
  2. 基类private成员在派生类中是不能被访问,如果基类成员不想在类外直接被访问,但需要在派生类中能访问,就定义为protected可以看出保护成员限定符是因继承才出现的
  3. 实际上面的表格我们进行一下总结会发现,基类的私有成员在子类都是不可见基类的其他成员在子类的访问方式 == Min(成员在基类的访问限定符,继承方式)public > protected > private
  4. 使用关键字class时默认的继承方式是private,使用struct时默认的继承方式是public不过最好显示的写出继承方式
  5. 在实际运用中一般使用都是public继承,几乎很少使用protetced/private继承,也不提倡使用protetced/private继承,因为protetced/private继承下来的成员都只能在派生类的类里面使用,实际中扩展维护性不强。

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

        我们知道,不同类型的变量直接进行赋值会发生类型转换,类型转换有强制类型转换和隐式类型转换。

        同理,父类和子类之间是不是也可以进行相互转换呢?

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

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

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

int main()
{
	int i = 0;
	double d = i;

	Person p;
	Student s;

	p = s;
	//s = p;  父不能给子,因为子有的变量可能多于父,变量数量都不一致,不能赋值
	//语法方面禁止了父向子传递
}

        父类不能类型转换赋值给子类(称为向下转换),因为子类有的变量可能多于父类,变量数量不一致,不能完成赋值。如果显示的强制类型转换也不可以,在这里C++语法方面直接禁止了父向子的传递 ,只允许子

        对于内置类型,类型转换时会产生临时变量而对于父类与子类之间,它们的类型转换不产生临时变量,这种类型转换被称为赋值兼容(切片,切割),因为子一定含有父的特征,将子类中父类的那一部分切下拷贝赋值给父类变量即可

问:如何证明不产生中间变量?

        用引用!如果有临时变量,那么需要使用const修饰的引用

	int i = 0;
	double& d = i;    错误

	Student s;
	Person& p = s;    正确

        此时父类p是子类s中父类那一部分切片的别名

  • 派生类对象 可以赋值给 基类的对象 / 基类的指针 / 基类的引用。这里有个形象的说法叫切片或者切割。寓意把派生类中父类那部分切来赋值过去。
  • 基类对象不能赋值给派生类对象
  • 基类的指针可以通过强制类型转换赋值给派生类的指针。但是必须是基类的指针是指向派生类对象时才是安全的。这里基类如果是多态类型,可以使用RTTI(Run-Time Type Information)dynamic_cast 来进行识别后进行安全转换。
	Student sobj;
	// 1.子类对象可以赋值给父类对象/指针/引用
	Person pobj = sobj;
	Person* pp = &sobj;
	Person& rp = sobj;

	//2.基类对象不能赋值给派生类对象
	sobj = pobj;

	// 3.基类的指针可以通过强制类型转换赋值给派生类的指针
	pp = &sobj;
	Student * ps1 = (Student*)pp; // 这种情况转换时可以的。
	ps1->_No = 10;

	pp = &pobj;
	Student* ps2 = (Student*)pp; // 这种情况转换时虽然可以,但是会存在越界访问的问题
	ps2->_No = 10;

三、继承中的作用域

  • 在继承体系中基类派生类都有独立的作用域
  • 子类和父类中有同名成员,子类成员将屏蔽父类对同名成员的直接访问,这种情况叫隐藏,也叫重定义(在子类成员函数中,可以使用 基类::基类成员 显示访问
  • 需要注意的是如果是成员函数的隐藏,只需要函数名相同就构成隐藏
  • 注意在实际中继承体系里最好不要定义同名的成员
class Person
{
public:
	void fun()
	{
		cout << "Person::func()" << endl;
	}

protected:
	string _name = "小李子"; // 姓名
	int _num = 111; 	   // 身份证号
};

// 隐藏/重定义:子类和父类有同名成员,子类的成员隐藏了父类的成员
class Student : public Person
{
public:
	void fun()
	{
		cout << "Student::func()" << endl;
	}

	void Print()
	{
		cout << " 姓名:" << _name << endl;
		cout << _num << endl;
        如果要使用父类变量,就指定类域
		cout << Person::_num << endl;
	}
protected:
	int _num = 999; // 学号
};

        若在函数内输出变量,编译器优先在函数内寻找、其次是类成员变量、如果有继承就在父类成员找、最后是全局

        重载要在同一个作用域,底层使用了函数名修饰规则,不然找地址的时候区分不开函数

隐藏是在父子类域中,只要函数名相同就形成隐藏

四、派生类的默认成员函数

class Person
{
public:
	//Person(const char* name = "peter")
	Person(const char* 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 operator=(const Person& p)" << endl;
		if (this != &p)
			_name = p._name;

		return *this;
	}

	~Person()
	{
		cout << "~Person()" << endl;
		delete _pstr;
	}
protected:
	string _name; // 姓名

};

class Student : public Person
{
public:
	// 先父后子
	Student(const char* name = "张三", int id = 0)
		:_name(name)    报错
		,_id(0)
	{}

protected:
	int _id;
};
//输出
Person()
~Person()

语法规定:

  • 派生类不能在初始化列表初始化从基类继承的成员变量(初始化列表初始化顺序和编写顺序无关,只和成员变量声明顺序有关,由于继承的变量在子类成员变量之前,所以先初始化继承的变量)
  • 派生类会在初始化列表自动调用基类的默认构造函数,如果基类没有默认构造,那么就会报错,我们可以显示调用基类的构造函数解决问题,编写语规则就像定义了匿名对象 
	Student(const char* name = "张三", int id = 0)
		:Person(name)    //最好写前面
		,_id(0)
	{}
  • 对于派生类的拷贝构造,如果不写父类的拷贝构造,会默认调用父类的默认构造,如果没有默认构造就会报错,所以需要显示的调用
  • 析构函数特殊,由于多态的原因,析构函数的函数名被特殊处理了,统一处理成destructor,所以派生类的析构隐藏了基类的析构,所以在调用父类的析构时还需要加上类名::
  • 由于子类实例化对象时,先调用父类的构造函数,再调用子类的构造,那么在析构时,要先析构子类,再析构父类,因为子类可能会用到父类。 显示调用父类析构,无法保证先子后父,所以子类析构函数完成后,自动调用父类析构,这样就保证了析构先子后父
例如此情况,先析构父再析构子就发生错误了,因为_pstr是父类的
	~Student()
	{
		Person::~Person();
		cout << *_pstr << endl;
		delete _ptr;
	}
class Person
{
public:
	//Person(const char* name = "peter")
	Person(const char* 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 operator=(const Person& p)" << endl;
		if (this != &p)
			_name = p._name;

		return *this;
	}

	~Person()
	{
		cout << "~Person()" << endl;
		delete _pstr;
	}
protected:
	string _name; // 姓名

	string* _pstr = new string("111111111");
};

class Student : public Person
{
public:
	// 先父后子
	Student(const char* name = "张三", int id = 0)
		:Person(name)
		,_id(0)
	{}

	Student(const Student& s)
		:Person(s)    这里传子类s是可以的,上转型为p
		,_id(s._id)
	{}

	// 10:45继续
	Student& operator=(const Student& s)
	{
		if (this != &s)
		{
            这里如果写为operator=会发生隐藏,造成死循环
			Person::operator=(s);    
			_id = s._id;
		}

		return *this;
	}

	~Student()
	{

		//Person::~Person();

		cout << *_pstr << endl;
		delete _ptr;
	}
protected:
	int _id;

	int* _ptr = new int;
};

总结:

1. 派生类的构造函数必须调用基类的构造函数初始化基类的那一部分成员。如果基类没有默认的构造函

数,则必须在派生类构造函数的初始化列表阶段显示调用。

2. 派生类的拷贝构造函数必须调用基类的拷贝构造完成基类的拷贝初始化。

3. 派生类的operator=必须要调用基类的operator=完成基类的复制。

4. 派生类的析构函数会在被调用完成后自动调用基类的析构函数清理基类成员。因为这样才能保证派生类

对象先清理派生类成员再清理基类成员的顺序。

5. 派生类对象初始化先调用基类构造再调派生类构造。

6. 派生类对象析构清理先调用派生类析构再调基类的析构

五、继承与友元

        友元关系不能继承,即父类的友元不能被子类继承

        如果也想使用父类声明的友元,那么再子类也声明以此即可

六、继承与静态成员

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

        静态成员属于父类和派生类,在派生类中不会单独拷贝一份,派生类继承的是使用权


class Person
{
public:
	Person() 
{}
//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; // 研究科目
};

int main()
{
	Person p;
	Student s;

	cout << &p._name << endl;
	cout << &s._name << endl;

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

	cout << &Person::_count << endl;
	cout << &Student::_count << endl;

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

七、菱形继承及菱形虚拟继承

1. 菱形继承

        多继承:一个子类有两个或以上直接父类时称这个继承关系为多继承,用 ',' 分割

         有多继承就会出现菱形继承,菱形继承是多继承的一种特殊情况(不规则也属于菱形继承,只要有公共的父类,三角形、五边形等等)

        继承的变量所在空间地址是相邻的

         那么菱形继承就会引起一些问题,即数据冗余,例如Student类继承了Person的_name,而Teacher也继承了Person的_name,最终Assistant继承了两类的_name,这不仅会造成数据冗余,还会造成二义性(可以指定类域访问,但是数据冗余问题无法解决)

class Person
{
public:
	string _name; // 姓名
	int _age;
};

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

class Teacher : public Person
{
protected:
	int _id; // 职工编号
};

class Assistant : public Student, public Teacher
{
protected:
	string _majorCourse; // 主修课程
};

int main()
{
	Assistant as;
	as.Student::_age = 18;
	as.Teacher::_age = 30;
	as._age = 19;	错误,因为二义性无法明确知道访问的是哪一个

	return 0;
}

2. 虚继承

        C++3.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;
}

        在继承了B、C类的D类内部增加一个变量空间用来专门存储_a,原本的B、C类存_a的空间改为存储一个指针信息,该指针指向一个表,称为虚基表,表的内容是单个或多个偏移量,是存放指针空间地址与D内部新增的_a空间地址之间的便宜量。

        虚基表可以减小D类对象所占的内存空间,且可以存储多个偏移量信息。

问:有的同学可能认为直接在存指针的地方存偏移量不就好了吗?

答:其实这是格局小了,如果需要存两个偏移量,那么B、C类每一处都要写两个偏移量,而如果我们将偏移量写进表内,那么当D实例化多个对象,我们只需要使每个对象的指针指向的虚基表地址相同,因为类相同,那么偏移量也是相同的,所以可以公用虚基表,这就高效的利用了空间

问:为什么要有偏移量,不能直接到D类内存最后一块直接访问吗?

答:是为了统一上转型对象以及本类对象访问_a的方式,这就是都存偏移量的意义

        首先,B、C类在虚继承之后内存结构也会发生改变,内存结构与D类一致,即首地址存虚基表地址,在B类内存最后存放_a

        这种情况是为了保障上转型对象能够访问_a的情况

B类指针
B* ptr = &b;
ptr->_a++;

上转型指针
ptr = &d;
ptr->_a++;

        在这种情况编译器区分不了ptr是什么类的指针,编译器做的是根据首地址处存储的地址找到偏移量,再根据当前位置的地址加上偏移量去找_a

汇编指令:

根据当前位置加上偏移量,取出值进行++,再放回去

总结

        面向对象三大特性之一的继承内容基本不难,依赖类和对象阶段基本知识(如六大默认构造函数),下节我们学习面向对象三大特性的最后一个——多态。

        最后,如果小帅的本文哪里有错误,还请大家指出,请在评论区留言(ps:抱大佬的腿),新手创作,实属不易,如果满意,还请给个免费的赞,三连也不是不可以(流口水幻想)嘿!那我们下期再见喽,拜拜!

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:/a/185775.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

高性能计算HPC所面临的问题

一、电力墙问题 能源动力领域关注高性能计算主要关注其能效和功耗等问题&#xff0c;也就是在高性能计算&#xff08;High-Performance Computing, HPC&#xff09;领域中&#xff0c;所谓的"电力墙"&#xff08;Power Wall&#xff09;&#xff0c;电力墙是一个描述…

Windows power shell for循环

有时候需要重复执行某个shell命令 for($i1;$i -lt 10;$i$i1){echo $i}如果是cmd for /l %i in (1,1,5) do echo %i

气膜厂家如何确保质量?

气膜厂家是专门生产和销售气膜产品的企业&#xff0c;需要对产品的质量进行有效管理和控制&#xff0c;以确保产品能够满足客户的需求和期望。下面将从生产过程、质量控制手段和售后服务等方面介绍气膜厂家如何确保产品质量。 起初&#xff0c;气膜厂家需要建立完善的质量管理…

python pdf转txt文本、pdf转json

文章目录 一、前言二、实现方法1. 目录结构2. 代码 一、前言 此方法只能转文本格式的pdf&#xff0c;如果是图片格式的pdf需要用到ocr包&#xff0c;以后如果有这方面需求再加这个方法 二、实现方法 1. 目录结构 2. 代码 pdf2txt.py 代码如下 #!/usr/bin/env python # -*- …

基于法医调查算法优化概率神经网络PNN的分类预测 - 附代码

基于法医调查算法优化概率神经网络PNN的分类预测 - 附代码 文章目录 基于法医调查算法优化概率神经网络PNN的分类预测 - 附代码1.PNN网络概述2.变压器故障诊街系统相关背景2.1 模型建立 3.基于法医调查优化的PNN网络5.测试结果6.参考文献7.Matlab代码 摘要&#xff1a;针对PNN神…

JS 中的随机数方法 Math.random()

有时候项目中遇到一个功能需要随机返回多条不重复的数据&#xff0c;也可以是拿了就用&#xff0c;下次再需要时已经忘记如何使用了。 js中的生成随机数操作是基于 Math 方法下的 random() 方法 Math.random() &#xff1a; 随机获取范围内的一个数 &#xff08; 精确到小数点…

NLP中 大语言模型LLM中的思维链 Chain-of-Thought(CoT) GoT

文章目录 介绍思路CoT方法Few-shot CoTCoT Prompt设计CoT投票式CoT-自洽性&#xff08;Self-consistency&#xff09;使用复杂的CoT自动构建CoTCoT中示例顺序的影响Zero-shot CoT 零样本思维链 GoT,Graph of Thoughts总结 介绍 在过去几年的探索中&#xff0c;业界发现了一个现…

Halcon Solution Guide I basics(4): Blob Analysis(连通性解析)

文章目录 文章专栏前言文章解析开头步骤分析简单案例进阶方案 进阶代码案例crystal&#xff0c;结晶匹配需求分析 文章专栏 Halcon开发 Halcon学习 练习项目gitee仓库 CSDN Major 博主Halcon文章推荐 前言 今天来看第三章内容&#xff0c;既然是零基础&#xff0c;而且我还有大…

科荣 AIO 管理系统任意文件读取

声明 本文仅用于技术交流&#xff0c;请勿用于非法用途 由于传播、利用此文所提供的信息而造成的任何直接或者间接的后果及损失&#xff0c;均由使用者本人负责&#xff0c;文章作者不为此承担任何责任。 一、产品介绍 科荣AIO公司服务软件企业一体化管理解决方案,通过ERP&am…

Linux:gdb调试器的解析+使用(超详细版)

Linux调试器-gdb 背景&#xff1a; 程序的发布方式有两种&#xff0c;debug模式和release模式 debug模式&#xff1a;可以被调试&#xff1b; release模式&#xff1a;不可以被调试。 为什么需要debuy和release这两个模式呢&#xff1f; 答&#xff1a;程序员在开发的时候需要…

项目总结报告(案例模板)

软件项目总结报告模板套用&#xff1a; 项目概要项目工作分析经验与教训改进建议可纳入的项目过程资产 --------进主页获取更多资料-------

最新AIGC创作系统ChatGPT网站源码,Midjourney绘画系统,支持GPT-4图片对话能力(上传图片并识图理解对话),支持DALL-E3文生图

一、AI创作系统 SparkAi创作系统是基于OpenAI很火的ChatGPT进行开发的Ai智能问答系统和Midjourney绘画系统&#xff0c;支持OpenAI-GPT全模型国内AI全模型。本期针对源码系统整体测试下来非常完美&#xff0c;可以说SparkAi是目前国内一款的ChatGPT对接OpenAI软件系统。那么如…

【腾讯云云上实验室】向量数据库与数据挖掘分析的黄金组合指南

前言&#xff1a; 在当今信息化时代&#xff0c;掌握对数据进行挖掘和分析的能力变得愈发关键。根据需求精准处理数据不仅仅是一项技能&#xff0c;更是对未来决策和操作的至关重要的支持。除了熟练运用适当的算法模型对大数据进行挖掘和分析外&#xff0c;合理高效存储和处理大…

[原创](免改BIOS)使用Clover升级旧电脑-(高阶玩法)让固态硬盘内置Win11 PE启动系统

[简介] 常用网名: 猪头三 出生日期: 1981.XX.XXQQ: 643439947 个人网站: 80x86汇编小站 https://www.x86asm.org 编程生涯: 2001年~至今[共22年] 职业生涯: 20年 开发语言: C/C、80x86ASM、PHP、Perl、Objective-C、Object Pascal、C#、Python 开发工具: Visual Studio、Delphi…

Qt项目打包发布超详细教程

https://blog.csdn.net/qq_45491628/article/details/129091320

定制手机套餐---python序列

if __name__ __main__:print("定制手机套餐")print("")#定义电话时长&#xff1a;字典callTimeOptions{1:0分钟,2:50分钟,3:100分钟,4:300分钟,5:不限量}keyinput("请输入电话时长的选择编号&#xff1a;")valuecallTimeOptions.get(key)if val…

必看!精品小程序UI设计模板,6款一网打尽!

身处于网络世界日新月异的变革中&#xff0c;智能手机已然成为我们日常生活、学习和工作的必不可少的伙伴。而小程序&#xff0c;这种无需额外下载和安装&#xff0c;随时随地都能用上的应用&#xff0c;因其便捷快速&#xff0c;功能丰富的特色&#xff0c;赢得了广大用户的喜…

SpringBoot3核心原理

SpringBoot3核心原理 事件和监听器 生命周期监听 场景&#xff1a;监听应用的生命周期 可以通过下面步骤自定义SpringApplicationRunListener来监听事件。 ①、编写SpringApplicationRunListener实现类 ②、在META-INF/spring.factories中配置org.springframework.boot.Sprin…

11-23 SSM4

Ajax 同步请求 &#xff1a;全局刷新的方式 -> synchronous请求 客户端发一个请求&#xff0c;服务器响应之后你客户端才能继续后续操作&#xff0c;请求二响应完之后才能发送后续的请求&#xff0c;依次类推 有点&#xff1a;服务器负载较小&#xff0c;但是由于服务器相应…

Python大语言模型实战-记录一次用ChatDev框架实现爬虫任务的完整过程

1、模型选择&#xff1a;GPT4 2、需求&#xff1a;在win10操作系统环境下&#xff0c;基于python3.10解释器&#xff0c;爬取豆瓣电影Top250的相关信息&#xff0c;包括电影详情链接&#xff0c;图片链接&#xff0c;影片中文名&#xff0c;影片外国名&#xff0c;评分&#x…