欢迎光临

我们一直在努力
当前位置:首页 > 编程技术 >

C++继承与菱形继承详细介绍

日期:
后台-插件-广告管理-首页/栏目/内容广告位一(PC)
后台-插件-广告管理-首页/栏目/内容广告位一(手机)
目录
  • 继承的概念和定义
  • 基类和派生类之间的赋值
  • 继承中的作用域
  • 派生类的默认成员函数
  • 菱形继承
  • 继承和组合的区分与联系
  • 其余注意事项

继承的概念和定义

继承机制是面向对象程序设计的一种实现代码复用的重要手段,它允许程序员在保持原有类特性的基础上进行拓展,增加其他的功能,在此基础上也就产生了一个新的类,称为派生类。继承呈现了面向对象程序设计的层次结构,是类设计层次的复用。

//以下代码就是采用了继承机制的一个场景
class person
{
protected:
	char _name[28];
	int _age;
	char _id[30];
};
//继承是代码复用的一种重要手段
class student :public person
{
protected:
	char _academy[50]; //学院
};

继承的格式

在前面的例子中,person是基类,student是派生类,继承方式是public. 这是很容易记忆的,person是基础的类,student是在person这个类的基础之上派生出来的。这就非常地像父子关系,所以基类又可以称为父类,派生类又可为子类。子类的后面紧跟着:,是:后面这个类派生出来的。

继承关系和访问限定符

继承的几种方式和访问限定符是相似的。

三种继承方式:public继承、protected继承、private继承。

三种访问限定符:public访问、protected访问、private访问。

基类类成员的访问权限和派生类继承基类的继承方式, 关系到了基类被继承下来的类成员在派生类中的情况。ps:这句话起始很好理解地,就是这句话写起来就变得绕口和复杂了,哈哈哈.

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

这里的不可见指的是:基类中的private成员也是被继承下来了的,只是在语法上,在派生类的类里和类外都不能够访问。

记住这个特殊的点,那么其他的就可理解为“权限问题”,这里“权限只能缩小,不能放大”。例如,基类的public成员以private继承方式继承下来,为“权限小的那个”,也就是继承下来后在派生类中是private成员。

class person
{
protected:
	char _name[28];
	char _id[30];
private:
	int _age;
};
class teacher :public person
{
public:
	teacher()
		:_age(0) //基类的private成员在派生类里不能访问
	{
	}
protected:
	char _jodid[20]; //工号
};
int main(void)
{
	teacher t1;
	t1._age; //基类的private成员在类外不能访问
	return 0;
}

基类和派生类之间的赋值

派生类的对象可以赋值给其基类的对象、基类的指针、基类的引用。

就像上面这样,取基类需要被赋值的值过去即可。

派生类赋值给基类的对象、基类的指针、基类的引用。在派生类中取基类需要的,就像把派生类给切割了一样、所以这里有一个形象的称呼:切割/切片

class Person
{
protected:
	string _name; // 姓名
	string _sex; // 性别
	int _age; // 年龄
};
class Student : public Person
{
public:
	int _id; // 学号
};
int main(void)
{
	//可以将派生类赋值给基类的对象、指针、引用
	Person p;
	Student s;
	p = s;
	Person* Pptr = &s;
	Person& Refp = s;
	//注意不能将将基类对象给派生类对象
	//s = p;
	//允许将基类指针赋值给派生类指针,但是需要强制转换
	Student* sPtr = (Student*)Pptr;
	return 0;
}

【注意】

1、不允许基类对象赋值给派生类对象

2、允许基类指针赋值给派生类指针, 但是需要强制转化。这种转化虽然可以,但是会存在越界访问的问题。

继承中的作用域

基类和派生类都有独立的作用域。继承下来的基类成员在一个作用域,派生类的成员在另一作用域。

//以下代码的运行结果是什么?
class Person
{
protected:
	string _name = "杨XX"; // 姓名
	int _num = 12138; // 身份证号
};
class Student : public Person
{
public:
	void Print()
	{
		cout <<_num << endl;
	}
protected:
	int _num = 52622; // 学号
};
void Test()
{
	Student s1;
	s1.Print();
};

基类中有一个_num 给了缺省值“12138”, 派生类中也有一个_name,给了缺省值“52622”,那么在派生类里直接使用_name,使用的具体是哪一个类里的?[!--empirenews.page--]

使用的是派生类Student里的。

总结:基类和派生类中如果有同名成员,派生类将屏蔽基类对同名成员的直接访问,这种情况称为隐藏 , 或者称为重定义。

如果想要访问,则使用基类::基类成员显示的访问。

class Person
{
protected:
	string _name = "杨XX"; // 姓名
	int _num = 12138; // 身份证号
};
class Student : public Person
{
public:
	void Print()
	{
		cout << "身份证号:" << Person::_num << endl;
		cout << "学号:" << _num << endl;
	}
protected:
	int _num = 52622; // 学号
};
void Test()
{
	Student s1;
	s1.Print();
};
int main(void)
{
	Test();
	return 0;
}

运行结果

我们已经了解了什么是隐藏。那么来看一下下面这些代码。

//以下的两个函数构成隐藏还是重载?
class A
{
public:
	void func()
	{
		cout << "func()" << endl;
	}
};
class B : public A
{
public:
	void func(int num)
	{
		cout << "func(int num)" << endl;
	}
};
void Test()
{
	B b;
	b.func(10);
}

函数重载要求在同一作用域,而被继承下来的基类成员和派生类成员在不同的作用域,所以构成的是隐藏。

```cpp
//以下代码的运行结果是什么?
class A
{
public:
	void func()
	{
		cout << "func()" << endl;
	}
};
class B : public A
{
public:
	void func(int num)
	{
		cout << "func(int num)" << endl;
	}
};
void Test()
{
	B b;
	b.func();
}

因为func()函数隐藏了,在派生类的作用域内没有func()函数,所以会出现编译报错。

派生类的默认成员函数

类有8个默认成员函数,这里只说重点的四个默javascript认成员函数:构造函数、析构函数、拷贝构造函数、赋值重载函数

如果我们不写派生类的构造函数和析构函数,编译器会做如下的事情:

1、基类被继承下来的部分会调用基类的默认构造函数和析构函数

2、派生类自己也会生成默认构造和析构函数,派生类自己的和普通类的处理一样

如果我们不写派生类的赋值构造函数和拷贝构造函数,编译器会做如下的事情

3、基类被继承下来的部分会调用基类的默认拷贝构造函数和赋值构造函数。

4、派生类自己也会生成默认赋值拷贝构造函数和赋值函数,和普通类的处理一样。

什么情况下需要自己写?

1、父类没有合适的默认构造函数,需要自己显示地写

2、如果子类有资源需要释放,就需要自己显示地写析构函数

3、如果子类存在浅拷贝的问题,就需要自己实现拷贝构造和赋值函数解决浅拷贝的问题。

如果需要自己写派生类的这几个重点成员函数,那么该如何写?

//如果需要自己实现派生类的几个四个重点默认成员函数,需要如何实现?该注意什么?
class Person
{
public:
	Person(const char* name)
		:_name(name)
	{
		cout << "Person(const char* name)" << 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
{
protected:
	int _id; //学号
	int* _ptr = new int[10]; //给一个需要自己实现默认成员函数场景用以举例
};

1、实现派生类的构造函数:需要调用基类的构造函数初始化被继承下来的基类部分的成员。如果基类没有合适的默认构造函数,就需要在实现派生类构造函数的初始化列表阶段显示调用。

2、实现派生类的析构函数:派生类的析构函数会在被调用完成后自动调用基类的析构函数清理被继承下来的基类成员。这样可以保证派生类自己的成员的清理先于被继承下来的基类成员。ps:析构函数名字会被统一处理成destructor(),所以被继承下来的基类的析构函数和派生类的析构函数构成隐藏。

3、实现派生类的拷贝构造函数:需要调用基类的拷贝构造函数完成被继承下来的基类成员的拷贝初始化。

4、实现派生类的operator=:需要调用基类的operator=完成被继承下来的基类成员的赋值。[!--empirenews.page--]

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

class Student : public Person
{
public:
	Student(const char* name, int id)
		: Person(name)
		, _id(id)
	{
		cout << "Student()" << endl;
	}
	Student(const Student& s)
		: Person(s)
		, _id(s._id)
	{
		cout << "Student(const Student& s)" << endl;
	}
	Student& operator = (const Student& s)
	{
		cout << "Student& operator= (const Student& s)" << endl;
		if (this != &s)
		{
			Person::operator =(s);
			_id = s._id;
		}
		return *this;js
	}
	~Student()
	{
		cout << "~Student()" << endl;
	}
protected:
	int _id; //学号
};

菱形继承

继承可分为单继承和多继承。

单继承:一个派生类只有一个直接基类

多继承:一个派生类有两个或两个以上的直接基类。

而多继承中又存在着一种特殊的继承关系,菱形继承

它们之间的继承关系逻辑上就类似一个菱形,所以称为菱形继承。菱形继承相对于其他继承关系是复杂的。

B中有一份A的成员,C中也有一份A的成员,D将B和C都继承了,那么D中被继承下来的A的成员不就有两份了吗?不难看出,菱形继承有数据冗余和二义性的问题。

class Person
{
public:
	string _name; // 姓名
};
class Student : public Person
{
public:
	int _num; //学号
};
clas编程s Teacher : public Person
{
public:
	int _id; // 职工编号
};
class Assistant : public Student, public Teacher
{
public:
	string _majorCourse; // 主修课程
};
int main()
{
	// 二义性、数据冗余
	Assistant a;
	a._id = 1;
	a._num = 2;
	// 这样会有二义性无法明确知道访问的是哪一个
	a._name = "peter";
	return 0;
}

上面的继承关系如下:

此时Assitant中有两份_name.存在数据冗余和二义性的问题。

二义性的问题是比较好解决的,使用::指定就可以了,但是并不能解决数据冗余的问题。

int main()
{
	// 二义性、数据冗余
	Assistant a;
	a._id = 1;
	a._num = 2;
	a.Student::_name = "小张";
	a.Teacher::_name = "张老师";
	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;
}

B和C中都有一份A的数据可以看出数据的冗余。

现在增加虚拟继承机制,解决数据冗余和二义性。

class A {
public:
	int _a;
};
clajavascriptss 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中的地址0x00677bdc里有什么?C中的地址0x00677be4里有什么?

从内存窗口可看出,菱形虚拟继承,内存中只在对象组成的最高处地址保存了一份A,A是B、C公共的。而B和C里分别保存了一个指针,该指针指向一张表。这张表称为虚基表,而指向虚基表的指针称虚基指针。虚基表中保存的值,是到A地址的偏移量,通过这个偏移量就能够找到A了。

继承和组合的区分与联系

在没有学习继承之前,我们其实频繁地使用组合。

class head
{
private:
	int _eye;
	int _ear;
	int _mouth;
};
class hand
{
private:
	int _arm;
	int _fingers;
};
class Person
{
	//组合
	//一个人由手、头等组合
	hand _a;
	head _b;
};
  • 继承是一种is-a的关系, 每一个派生类是基类,例如,Student是一个Person, Teacher 是一个Person[!--empirenews.page--]
  • 组合是一种has-a的关系,Person组合了head, hand, 每一个Person对象中都有一个head、hand对象。
  • 如果某种情况既可以使用继承又可以使用组合,那么优先使用对象组合,而不是类继承。

其余注意事项

  • 友元关系不能被继承,好比父亲的朋友不一定是你的朋友。
  • 如果基类中定义了静态成员,当这个基类被实例化后出现了一份,那么整个继承体系中都只有这一份实例。

到此这篇关于C++继承与菱形继承详细介绍的文章就介绍到这了,更多相关C++继承 内容请搜索我们以前的文章或继续浏览下面的相关文章希望大家以后多多支持我们!

后台-插件-广告管理-首页/栏目/内容广告位二(PC)
后台-插件-广告管理-首页/栏目/内容广告位二(手机)
后台-插件-广告管理-内容广告位三(PC)
后台-插件-广告管理-内容广告位三(手机)

相关阅读

  • C++继承的赋值转换与菱形虚拟继承深入详解

  • 目录一、继承的概念及定义1.1、继承的概念1.2、继承的定义二、基类和派生类对象赋值转换三、继承中的作用域3.1、继承同名成员处理方式3.2、继承同名静态成员处理方式3.3、
后台-插件-广告管理-内容广告位四(PC)
后台-插件-广告管理-内容广告位四(手机)

热门文章

后台-插件-广告管理-侧边广告位一(PC)
后台-插件-广告管理-侧边广告位一(手机)
  • HTML 表单组件实例代码

  • HTML 表单用于搜集不同类型的用户输入。下文通过代码给大家分享html 表单组件实例代码,感兴趣的朋友参考下吧 废话不多说了,直接给大家贴代码了,具体代码如下所示: <!DOCTYPE
  • html2canvas 将html代码转为图片的使用方法

  • 转换代码到图片使用 html2canvas,这是一个非常著名的从浏览器网页截图的开源库,使用很方便,功能也很强大。 使用 html2canvas http:// html2canvas 的使用非常简单,简单
  • HTML网页中插入视频的方法小结

  • 现在如果要在页面中使用video标签,需要考虑三种情况,支持Ogg Theora或者VP8(如果这玩意儿没出事的话)的(Opera、Mozilla、Chrome),支持H.264的(Safari、IE 9、Chrome),都不支持的(IE6、
  • HTML实现文本框只读不能修改其中的内容

  • 废话不多说了,直接给大家贴代码了,具体代码如下所示: <!--方法1:>http:// 当鼠标放不上就离开焦点 --> <input type="text" name="input1" value=http://www.cppcns.com/web
  • 移动端专用的meta标签设置大全

  • 前言 之前学习前端中,对meta标签的了解仅仅只是这一句。 <meta charset="UTF-8"> 但是打开任意的网站,其head标签内都有一列的meta标签。比如我们我们网站,但是自己却很不熟
后台-插件-广告管理-侧边广告位二(PC)
后台-插件-广告管理-侧边广告位二(手机)

最新文章

  • 在Asp.net core项目中使用WebSocket

  • 今天小试了一下在ASP.NET core中使用websocket,这里记录一下: 在 Startup 类的 Configure 方法中添加 WebSocket 中间件。 app.UseWebSockets(); 它也可以传入一些参数 app.Us
  • Vue快速理解事件绑定是什么

  • 目录一、监听事件二、事件修饰符1、stop修饰符阻止事件冒泡2、capture修饰符3、self修饰符4、prevent修饰符5、键盘事件修饰符6、鼠标事件修饰符一、监听事件 监听事件一般
  • C#实现模拟ATM自动取款机功能

  • 目录(1)关于用户帐号的类:Account(2)关于银行数据库的类:BankDatabase(3)关于ATM屏幕显示的类:Screen(4)关于ATM键盘的类:Keypad(5)关于进钞、出钞口的类:DepositSlot(6)关于ATM
  • Java设计模式之抽象工厂模式浅析讲解

  • 1.介绍 当系统准备为用户提供一系列相关对象,又不想让用户代码和这些对象形成耦合时,就可以使用抽象工厂模式。 2.如何实现 1)抽象产品--Car 2)具体产品--BYDCar、TSLCar 3)抽象
  • 如何动态替换Spring容器中的Bean

  • 目录动态替换Spring容器中的Bean原因方案实现Spring中的bean替换问题动态替换Spring容器中的Bean 原因 最近在编写单测时,发现使用 Mock 工具预定义 Service 中方法的行为特
  • C#优雅的实现INotifyPropertyChanged接口

  • INotifyPropertyChanged接口在wpF或WinFrom程序中使用还是经常用到,常用于通知界面属性变更。标准写法如下: class NotifandroidyObject : INotifyPropertyChanged {
后台-插件-广告管理-侧边广告位三(PC)
后台-插件-广告管理-侧边广告位三(手机)