c++继承详解

news2024/11/9 0:45:17

前言

        继承是类复用的重要方式,学习面向对象语言时学习继承是必不可少的,在c++中继承机制一种较为复杂的机制,下面让我们一起来认识一下c++中的继承。

目录

1.继承的概念和定义

        1.1继承的概念

        1.2 继承的定义

2.基类和派生类之间的转换

3.继承中的作用域

4.派生类的默认成员操作

5.继承与友元,继承与静态成员之间的关系

         5.1继承与友元友元

         5.2继承与静态成员

6.多继承和菱形继承

        6.1单继承

        6.2多继承  

        6.3菱形继承 

        6.4虚拟解决数据冗余的和二义性的原理:

7.总结 


1.继承的概念和定义

        1.1继承的概念

        实际上继承是一种类之间代码复用的方式,继承这种机制,它允许程序员在保持原有类特性不变的情况下,扩展新类的特性。增加新类的功能。这样产生的类叫做派生类。继承程序面向对象程序设计的层次,由简单到复杂的认知过程。继承是类设计层次的复用。 

        例如:

#include<iostream>
using namespace std;
class person
{
public:
	void print()
	{
		cout << "person" << endl;
	}
	int _a;
};
class student :public person//通过public的方式继承基类person
{

private:
	int _b;
};
int main()
{
	student s;
	s.print();
	return 0;
}

         student类继承基类person,实际上student派生类继承了基类person的成员变量和成员函数。运行上面的程序我们可以看到继承的成员函数,打开监视窗口我们可以看到student继承的成员变量,如下图:

 

        1.2 继承的定义

        下面的base是基类,也叫作父类,derived 是派生类,也叫作子类。

         继承关系和访问限定符:

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

继承基类成员访问方式的变化
类成员/继承方式public继承protected private
基类的public成员派生类的public成员派生类的protected成员派生类的private成员
基类的protected成员派生类的protected成员派生类的protected成员派生类的private成员
基类的private成员在派生类不可访问不可见在派生类不可访问不可见在派生类不可访问不可见

                 总结:

       1. 如果基类的成员是private的,无论以何种方式继承都是不可见的,这里的不可见是指基类的private成员在派生类中无论是类里面还是类外面都是不可见的。

        2.基类的中private成员在派生类中不可见,如果想要在派生类里面被访问,但是在派生类外面不可见,需要将基类的成员定义为protected。例如:

class base
{
protected:
	int _a = 1;
	int _b = 2;
	void print()
	{
		cout << "base" << endl;
	}
};
class derived :public base
{
public:
	void print()
	{
		_a = 90;//改变从基类继承的成员变量
		_b = 30;
		cout << _a << endl;
		cout << _b << endl;
		print();//调用继承基类的成员函数
	}
private:
	int _num = 0;
	int _a = 0;
};

        3.基类的私有成员在派生类中都是不可见的,而其他成员则是由基类成员的访问限定符和继承方式决定的,取决于访问限定符小的那个,public>protected>private。

        4.建议使用public继承,且基类成员最好为public。因为protected继承和private继承的可维护性不强。

        5.使用class关键字默认继承方式为private,使用struct关键字默认继承方式为public。建议显示的写出继承方式。

2.基类和派生类之间的转换

        派生类对象可以赋值给基类的对象/指针/引用。 这里有个形象的叫法叫做切片。就是将父类原有的那部分切出来赋值给父类。例如:

        基类对象不可以赋值给派生类的对象。但是基类的指针或者引用可以通过强制转换类型赋值给派生类的指针或者引用 ,但是前提是基类的指针是指向派生类才是安全的。这里的基类如果是多态类型,可以使用RTTI的dynamic cast进行识别后,进行安全转换。

        例如:

class base
{
protected:
	int _a = 1;
	int _b = 2;
	void print()
	{
		cout << "base" << endl;
	}
};
class derived :public base
{
public:
	void print1()
	{
		_a = 90;//改变从基类继承的成员变量
		_b = 30;
		cout << _a << endl;
		cout << _b << endl;
		print();//调用继承基类的成员函数
	}
private:
	int _num = 0;
	int _c = 0;
};
int main()
{
	derived d1;
	base b1;
	//下面这种情况的转换是可行的
	b1  = d1;//将派生类赋值给基类
	derived *d2 ;
	//d2 = b1;//errror
	base* b2 = &d1;
	d2 = (derived*)b2;//基类的指针可以强制转换类型赋值给派生类的指针

	//下面这种类型的转换,程序运行是指针会越界
	base d3;
	base* d4 = &d3;
	derived* d3 = (derived*)d4;
	return 0;
}

3.继承中的作用域

        1.在继承体系中,派生类和基类都有自己独立的作用域。

        2.派生类和基类有同名成员时,派生类会屏蔽基类,对同名成员进行直接访问,这种情况叫做隐藏,也就重定义。在子类成员函数中可以使用基类类名+::来显示访问基类成员(如果是成员函数的隐藏,在派生类中只要成员函数的函数名相同就构成隐藏)。

        3.在实际使用中不建议派生类和基类具有同名成员。

        如下:

class person
{
protected:
	int _a = 0;
	string _s1 = "dadadadada";
    int print()
	{
		cout << _s1 << endl;
		cout << _a << endl;
	}
};
class student :public person
{
public:
	void print()
	{
		cout << _a << endl;//当派生类和基类有同名成员时,派生类成员会屏蔽基类,对同名成员进行访问
		cout << _s1 << endl;
	
	}
private:
	int _a = 90;
};
int main()
{
	student s1;
	s1.print();//当派生类和基类有同名函数时,派生类成员会屏蔽基类,对同名函数进行调用
	return 0;
}

         注意:person中的print函数和student中的print函数不够成函数重载,因为函数重载是在同一个作用域中的同名但是参数不同的函数才构成函数重载。

        如果想要访问基类中的同名成员要基类名::的方式访问如下:

        

class student :public person
{
public:
	void print()
	{
		person::print();//访问基类中的同名函数
		cout << _a << endl;//当派生类和基类有同名成员时,派生类成员会屏蔽基类,对同名成员进行访问
		cout << _s1 << endl;
		cout << person::_a << endl;//访问基类中的同名变量
	}
private:
	int _a = 90;
};

4.派生类的默认成员操作

        类中的六个默认成员函数如果我们不写编译器会自动生成默认的,那么在派生类中是怎么样的呢?

        1.在派生类中,实例化对象的时候,系统会自动调基类的默认构造函数,用来初始化继承基类的成员变量,而其他的成员变量要调用派生类自己的构造函数进行初始化,如果继承的基类没有默认的构造函数就必须在派生类的构造函数中显示的调用基类的构造函数对成员进行初始化。例如:

class person
{
protected:
	person()//默认构造函数
		:_a(0)
		,_s1("hahaha")
	{ }
	int _a = 0;
	string _s1 = "dadadadada";
	
};
class student :public person
{
public:
	student()
		:_a(20)
	{ }
private:
	int _a = 90;
};
int main()
{
	student s1;//类实例化处对象——调用它自己的构造函数去初始化成员变量,同时会自动调用基类的构造函数初始化继承基类的成员变量
	return 0;
}

        如果基类没有默认的构造函数就要显示调用基类的构造函数:

class student :public person
{
public:
	student(const char*name = "")
		:_a(20)
		,person(name)//显示调用基类的构造函数
	{ 
	}
private:
	int _a = 90;
};

        2.在派生类中的拷贝构造函数必须调用基类的拷贝构造函数来完成基类的拷贝构造初始化。例如:

         3.派生类中的operator=必须调用基类的operator=完成基本的赋值。

例如:

	student& operator=(const student& s)
	{
		operator=(s);//调用基类的operator=
		_a = s._a;
		return *this;
	}

        注意要是直接这样写程序会死循环,因为这里有函数的隐藏,子类和父类中都有一个operator=函数,需要指定函数的作用域。

        4.派生类对象实例化的时候先调用基类的构造函数,再调用派生类的构造函数。

        例如:

class person
{
protected:
	person(const char*ch = "")//默认构造函数
		:_a(0)
		,_s1(ch)
	{ 
		cout << "person" << endl;
	}
	int _a = 0;
	string _s1 = "dadadadada";
	
};
class student :public person
{
public:
	student(const char*name = "")
		:_a(20)
		
	{ 
		cout << "student" << endl;
	}
	
private:
	int _a = 90;
};
int main()
{
	student s1;//类实例化处对象——调用它自己的构造函数去初始化成员变量,同时会自动调用基类的构造函数初始化继承基类的成员变量
	return 0;
}

       

        5.派生类对象的销毁需要显示的调用基类的析构函数,再去调用自己的析构函数吗?我们可以试一试:

class person
{
protected:
	~person()
	{
		_a = 0;
		cout << "~person" << endl;
	}
};
class student :public person
{
public:

	~student()
	{
		person::~person();//显示的调用基类的析构函数
		cout << "~student" << endl;
	}
private:
	int _a = 90;
};
int main()
{
	student s1;
	return 0;
}

        如果我们显示的调用基类的析构函数,

 我们发现基类的析构函数被调用了两次,这样显然是不对的,那么析构的顺序应该是怎么样的呢?我们知道先定义的对象会后析构,因为对象是在栈上的这与栈后进先出的特性有关,其实,派生类也满足后定义的先析构,所以会先析构派生类自己的成员,然后再去调用基类的析构函数进行析构,所以派生类的对象析构的时候不需要显示调用基类的析构函数,编译器会自动调用。

        注意:虽然基类和派生类的析构函数,函数名不同,但是派生类的析构函数会隐藏基类的析构函数,因为对于析构函数编译器会做特殊的处理,将析构函数名处理为:destrutor().所以它们构成隐藏。

        

5.继承与友元,继承与静态成员之间的关系

       5.1继承与友元友元

        友元分为友元函数和友元类,详见: 友元函数详解

        这里主要是要说明友元关系是不能继承的,也就是说基类友元不能访问派生类private和protected成员例如:

class student;
class person
{
	friend void print(const person& p, const student& s);
private:
	int _id ;
	int _telephone;
};

class student:public person
{
private:
	char _addr[10];
};
void print(const person& p, const student& s)
{
	cout << p._id << "    " << p._telephone << endl;
	cout << s._addr << endl;
}
int main()
{
	person p;
	student s;
	print(p,s);
	return 0;
}

        如果想要访问的话就要在基类中重新声明友元。

         5.2继承与静态成员

        基类的定义的静态成员,则在整个体系中只有一个这样的成员,无论派生出多少个子类。都只有一个static成员。类中的静态成员详见:类中的静态成员

        例如: 

class person
{
public:
	int _id ;
	int _telephone;
	static int _stc;
};
int person:: _stc = 0;//初始化静态成员变量_stc
class student:public person
{
private:
	char _addr[10];
};
int main()
{
	person p;
	student s;
	student s1;
	//给不同对象的静态成员赋不同的值
	person::_stc = 2;
	p._stc = 3;
	s._stc++;
	s1._stc = 10;
	//打印不同对象中的stc
	cout << person::_stc << endl;;
	cout << p._stc << endl;
	cout << s._stc << endl;
	cout << s1._stc << endl;

	return 0;
}

        你会发现即使给不同对象的静态成员赋不同的值 ,最后输出的结果还是一样的。

        打开监视也可以看见不同对象中存放的成员变量_stc的地址也是相同的,不管是基类对象还是派生类对象。如图:

6.多继承和菱形继承

        6.1单继承

        一个子类只有一个父类,这种继承关系叫做单继承。如图:     

          6.2多继承  

        一个子类有多个父类的继承方式叫做多继承,如图:

        6.3菱形继承 

        菱形继承其实是多继承的一种特殊情况,如果子类继承的父类,它们也继承过相同的基类,那么这种继承方式就叫做菱形继承,如图:         注意菱形继承不只有上图所示的这种情况。

         请想想这种菱形继承有什么坏处吗?答案是显而易见的,它会造成数据冗余和二义性的问题。

        例如:

class people
{
public:
	int _age;
	int _sex;
};
class person :public people//继承people类
{
public:
	int _id;
	int _telephone;
};
class student :public people//继承people类
{
private:
	char _addr[10];
};
class super:public student,public person//继承student类和person类
{
private:
	int _value;
};
int main()
{
	super s;
	return 0;
}

        上面这种情况就属于菱形继承,如果打开监视我们会看到:s中有两份people类的成员(一份来自于student类的继承,一份来自person类的继承)。如图: 

        如何解决这个问题呢?这就要需要在上面student类和person类在继承people类时采用虚继承,即可解决这个问题,关键字是virtual。需要注意的是虚继承不要在其他地方使用。例如:

class people
{
public:
	int _age;
	int _sex;
};
class person :virtual public people//继承people类
{
public:
	int _id;
	int _telephone;
};
class student :virtual public people//继承people类
{
private:
	char _addr[10];
};
class super:public student,public person//继承student类和person类
{
private:
	int _value;
};
int main()
{
	super s;
	return 0;
}

         6.4虚拟解决数据冗余的和二义性的原理:

        为了研究虚拟继承的原理,我们给出一个简单的菱形继承的继承体系,在借助内存窗口观察对象成员的模型。如下: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
{
private:
    int _d;
};
int main()
{
    D d1;
    return 0;
}  

        运行这个程序,打开内存,借助内存窗口来观察对象模型,下面是菱形继承的派生类对象在内存中的模型: 

        从这里可以看出d对象中保存着两个_a,数据冗余,且在d1对象访问_a 时有二义性。

        下面是菱形虚拟继承的派生类对象在内存中的模型: 

        

        可以看到原来存放继承B,C类中成员_a的位置都存着一个地址,当我们去这个地址()中找,会发现这段地址所存的数据是,从此原来B中的成员变量_a到现在D中的新的成员变量_a,地址的偏移量。同理C的原来的成员_a所保存的地址处也被替换成一个地址,这个地址所对应的空间存放着原来的C类继承的成员变量_a到现在D中的新的成员变量_a,地址的偏移量。为什么要在B类和C类存放成员变量_a的地址处,存放指针保存到成员_a地址处的偏移量呢?

        因为这样如果就可以保证将d类的对象赋值给B,C类对象时也可以访问到成员_a,如:

        原理图:

        建议:不到万不得已的情况下不要使用菱形继承。 

7.总结 

        很多人都说c++语法复杂,其实多继承就是一个体现,有了多继承就有菱形继承,有了菱形继承,就需要菱形虚拟继承来,底层实现就会很复杂。所以一般不建议设计多继承,一定不要设计菱形继承,否则在复杂度和效率上就会有问题。

        多继承可以认为是c++的缺陷之一,后来的很多面向对象的语言都没有多继承,如Java。

        组合和继承

        public继承是一种is-a的关系。也就是说每一个子类对象都是一个基类对象。

        组合是一种has-a的关系,假设B组合了A,每个B对象中就有一个A对象。

        继承允许你根据基类的实现来定义派生类的实现,这种通过生成派生类的复用通常被称为白箱复用   。术语“白箱”是相对可视性而言的:在继承方式中,基类的内部细节对子类可见,继承在一定程度上是通过破坏基类的封装,基类的改变,对派生类有很大影响。派生类和基类之间的耦合度很高。

        组合对象是类继承之外的一种复用方式的选择,新的更复杂的功能可以通过组装或者组合对象来实现。对象组合要求被组合的对象具有良好的定义接口。这种复用方式叫做黑箱复用,因为对象内部的细节不可见的,对象只以黑箱的形式出现,组合之间没有很强的关联关系,耦合度低,内聚度高。优先使用组合可以保持每个类封装的完整性。

        到底是选择继承来复用代码还是选择组合来复用代码,是要根据实际情况来决定的,如果对象之间更符合is-a关系,优先使用继承,如果对象间更符合has-a关系有优先使用组合,如果对象间没有很强的关系,优先使用组合

        组合的耦合度低,代码易于维护,不过继承也不是一无是处,有些关系更适合继承那就用继承。

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

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

基于html+css的图展示106

准备项目 项目开发工具 Visual Studio Code 1.44.2 版本: 1.44.2 提交: ff915844119ce9485abfe8aa9076ec76b5300ddd 日期: 2020-04-16T16:36:23.138Z Electron: 7.1.11 Chrome: 78.0.3904.130 Node.js: 12.8.1 V8: 7.8.279.23-electron.0 OS: Windows_NT x64 10.0.19044 项目…

PCL点云处理之分层切片法计算树冠投影面积 (一百七十四)

PCL点云处理之分层切片法计算树冠投影面积 (一百七十四) 一、算法介绍二、方法流程三、具体实验1.代码2.效果四、算法总结一、算法介绍 在上一节中,通过树冠整体投影到同一水平面后,计算凸包面积,粗略估计了树冠投影面积,但在通常的研究学习中,这种方法较为笼统,大部分…

springboot配置使用redis序列化时报错“无法自动装配。找不到 ‘RedisConnectionFactory‘ 类型的 Bean”

今天在使用springboot操作redis时出现乱码的问题 像这样&#xff0c;这里对应的key明实际上时springboot:string 对应的值时徐浩的redis 但是当向redis推送数据时就乱码了&#xff0c;后面一查是因为在springboot-redis中&#xff0c;默认配置没有序列化&#xff0c;直接将str…

Python词云绘制

Python词云绘制 效果展示以及准备工作&#xff1a;进入代码书写 效果展示以及准备工作&#xff1a; 效果展示图&#xff1a; 准备工作 pycharm安装第三方库numpy,jieba,wordcloud词云文本的准备&#xff08;.txt&#xff09;背景图的准备&#xff08;我是用的PS&#xff09;…

[激光原理与应用-69]:激光焊接的10大常见缺陷及解决方法

激光焊接是一种以高能量密度的激光束作为热源的高效精密焊接方法。如今&#xff0c;激光焊接已广泛应用于各个行业&#xff0c;如&#xff1a;电子零件、汽车制造、航空航天等工业制造领域。但是&#xff0c;在激光焊接的过程中&#xff0c;难免会出现一些缺陷或次品。只有充分…

[架构之路-203] - 对系统需求类型的进一步澄清

目录 业务/商业需求&#xff1a; 用户/客户需求&#xff1a; 功能性需求&#xff1a; 非功能性需求&#xff1a; 系统需求&#xff1a; 约束条件&#xff1a; 软件需求说明书&#xff1a; 软件质量&#xff1a; 业务/商业需求&#xff1a; 是自顶向下的需求&#xff0…

pytorch卷积神经网络CNN 手写数字识别 MNIST数据集

模型结构和训练代码来自这里 https://blog.csdn.net/weixin_41477928/article/details/123385000 俺又加了离线测试的代码: 第一次运行此代码&#xff0c;需有网络&#xff0c;会下载开源数据集MNIST训练的过程中会把10个epoch的模型均保存到./models下&#xff0c;可能需要你…

2023年第三届陕西省大学生网络安全技能大赛--本科高校组 Reverse题解

文章目录 一. 我的upx -d怎么坏了1. 查看节区信息2. 动态调试脱壳3.输出迷宫图4.走迷宫 二. babypython1.字节码简单分析2. gpt分析3. 程序逻辑4.解题脚本 三. BadCoffee1. 相关文章2.解混淆3.解题脚本 四. Web&Assembly(暂时没复现出来,提供一些相关文章)总结 这次比赛做出…

冈萨雷斯DIP第5章知识点

图像增强&#xff1a;主要是一种 主观处理&#xff0c;而图像复原很大程度上是一种 客观处理。 5.1 图像退化/复原处理的一个模型 如图5.1 本章把图像退化建模为一个算子 H \mathcal{H} H 该算子 与一个加性噪声项 η ( x , y ) η(x,y) η(x,y) 共同对输入图像 f ( x , y…

Rust每日一练(Leetday0013) 解数独、外观数列、组合总和

目录 37. 解数独 Sudoku Solver &#x1f31f;&#x1f31f;&#x1f31f; 38. 外观数列 Count and Say &#x1f31f;&#x1f31f; 39. 组合总和 Combination Sum &#x1f31f;&#x1f31f; &#x1f31f; 每日一练刷题专栏 &#x1f31f; Rust每日一练 专栏 Gola…

常微分方程(ODE)求解方法总结

常微分&#xff08;ODE&#xff09;方程求解方法总结 1 常微分方程&#xff08;ODE&#xff09;介绍1.1 微分方程介绍和分类1.2 常微分方程得计算方法1.3 线性微分方程求解的推导过程 2 一阶常微分方程&#xff08;ODE&#xff09;求解方法2.1 欧拉方法2.1.1 欧拉方法的改进思路…

逻辑推理——弟弟的编程课

前言 这篇文章不写代码&#xff0c;不科普知识。而是推理&#xff01; 这是我弟弟编程课上的一个同学&#xff1b;不是我的同学&#xff0c;我都成年了&#xff0c;这还是个小毛孩&#xff01; 这是他们学的&#xff1a; 乍一看这没任何问题&#xff0c;还有人会说&#xff…

谷歌地图模型自动下载

本工具是收费软件&#xff0c;学生党勿扰&#xff0c;闹眼子党勿扰 本工具收费1000元 视频教程 1 概述 记得去年写过一篇关于谷歌地图模型提取的博客&#xff0c;得到了广泛好评。有很多同学提出&#xff0c;能不能自动下载谷歌地图模型&#xff0c;由于提出此需求的人太多了…

【起点到终点 走哪条路径使得(路径长度排序从大到小后) 第k+1条边最小】通信线路

专注 效率 记忆 预习 笔记 复习 做题 欢迎观看我的博客&#xff0c;如有问题交流&#xff0c;欢迎评论区留言&#xff0c;一定尽快回复&#xff01;&#xff08;大家可以去看我的专栏&#xff0c;是所有文章的目录&#xff09;   文章字体风格&#xff1a; 红色文字表示&#…

单片机GD32F303RCT6 (Macos环境)开发 (三十四)—— 数字加速度计 (ADXL345)

数字加速度计 &#xff08;ADXL345&#xff09;- 计算xyz轴重力值 1、i2c总线读取 1、接线 上一节的软件模式i2c的方式&#xff0c;选择PB10(SCL) PB11(SDA)。 GD32 ADXL345PB10 --------------- SCLPB11 --------------- SDA3.3 --…

Eclipse 教程 完结

Eclipse 快捷键 关于快捷键 Eclipse 的很多操作都提供了快捷键功能&#xff0c;我们可以通过键盘就能很好的控制 Eclipse 各个功能&#xff1a; 使用快捷键关联菜单或菜单项使用快捷键关联对话窗口或视图或编辑器使用快捷键关联工具条上的功能按钮 Eclipse 快捷键列表可通过…

《crossfire》游戏分析

文章目录 一、 穿越火线简介和定位二、 游戏发行三、 用户基础四、 游戏玩法枪王排位团队竞技爆破模式歼灭模式突围模式幽灵模式生化模式个人竞技挑战模式跳跳乐地图工坊 五、 游戏竞技公平性cf竞技公平性 六、CF火热到现在的原因分析1.时代、空间背景2.用户基础3.丰富的游戏模…

【iOS】—— nil、Nil、NULL和NSNull学习

nil、Nil、NULL和NSNull 文章目录 nil、Nil、NULL和NSNullnilNSNullNilNULL总结&#xff1a; 我们先来看看这几个苹果官方文档的解释&#xff1a; nil&#xff1a;Defines the id of a null instance.&#xff08;定义空实例的id&#xff09;Nil&#xff1a;Defines the id of…

给编程初学者的一封信

提醒&#xff1a;以下内容仅做参考&#xff0c;具体请自行设计。 随着信息技术的快速发展&#xff0c;编程已经成为一个越来越重要的技能。那么&#xff0c;我们该如何入门编程呢&#xff1f;欢迎大家积极讨论 一、自学编程需要注意什么&#xff1f; 要有足够的时间、精力等…

大数据治理入门系列:数据目录

在元数据管理一文中&#xff0c;我们曾将数据比喻为一本本的书&#xff0c;将书的作者、出版时间等信息比喻为元数据。试想一下&#xff0c;假如你是一名新任的图书管理员&#xff0c;如何快速掌握图书馆的馆藏情况呢&#xff1f;假如你是一名读者&#xff0c;如何快速找到你需…