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

目录
  • 继承的概念和定义
  • 基类和派生类之间的赋值
  • 继承中的作用域
  • 派生类的默认成员函数
  • 菱形继承
  • 继承和组合的区分与联系
  • 其余注意事项

继承的概念和定义

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

//以下代码就是采用了继承机制的一个场景
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,使用的具体是哪一个类里的?

使用的是派生类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个默认成员函数,这里只说重点的四个默认成员函数:构造函数、析构函数、拷贝构造函数、赋值重载函数

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

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=完成被继承下来的基类成员的赋值。

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;
	}
	~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; //学号
};
class 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;
};
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中的地址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
  • 组合是一种has-a的关系,Person组合了head, hand, 每一个Person对象中都有一个head、hand对象。
  • 如果某种情况既可以使用继承又可以使用组合,那么优先使用对象组合,而不是类继承。

其余注意事项

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

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

(0)

相关推荐

  • 关于C++中菱形继承和虚继承的问题总结

    前言 菱形继承是多重继承中跑不掉的,Java拿掉了多重继承,辅之以接口.C++中虽然没有明确说明接口这种东西,但是只有纯虚函数的类可以看作Java中的接口.在多重继承中建议使用"接口",来避免多重继承中可能出现的各种问题.本文将给大家详细介绍关于C++菱形继承和虚继承的相关内容,分享出来供大家参考学习,话不多说了,来一起看看详细的介绍吧. 继承: 1. 单继承–一个子类只有一个直接父类时称这个继承关系为单继承 2. 多继承–一个子类有两个或以上直接父类时称这个继承关系为多继承 例如下面

  • C++数据结构继承的概念与菱形继承及虚拟继承和组合

    目录 继承的概念 继承的定义 基类和派生类对象之间的赋值转换 继承中的作用域 派生类的默认成员函数 继承中的两个小细节

  • C++中菱形继承的解释与处理详解

    封装,继承,多态.这是C++语言的三大特性,而每次在谈到继承时我们不可避免的要谈到一个很重要的问题——菱形继承. 派生类继承父类,同时也会继承父类中的所有成员副本,但如果在继承时一个基类同时被两个子类继承,然后一个新类又分别由上面的两个子类派生出来.这样从某种程度来说就形成了C++中的菱形继承,也可以叫做钻石继承,具体的继承形式如下图所示: 在上面的类图说,Left和Right分别派生子Top,但是Bottom又分别继承了Left和Right.继承关系也可以画成下面的方式,这样就可以更好的理解设

  • 一文读懂C++中的继承之菱形继承(案例分析)

    前言 我们上一篇说了世间万物都有一个继承体制,或多或少子类继承了父类的某些特征,但大多都是单向继承,但是就有些特例他就是多继承,比如: 我们从图片中就可以看到,两栖动物它既继承了水生动物的一部分特性,也继承了陆地动物的一些特性,那么我们的代码,会不会也会有这种多继承现象呢,我们一起来看一下. 提示:以下是本篇文章正文内容,下面案例可供参考 一.什么是多继承? 1.单继承 我们来看一个图先了解一下单继承,再看有什莫区别 也就是说,一个子类只有一个直接父类时称这个继承关系为单继承 2.多继承 我们把

  • C++中的菱形继承深入分析

    菱形继承 class Person { int _AA; }; class Student:public Person { int _BB; }; class Teacher :public Person { int _CC; }; class Assistant :public Student, public Teacher { int _DD; }; PS: Assistant的对象中存在两份Person成员 菱形继承存在二义性和数据冗余 解决: 使用虚继承 首先不使用虚继承时: #incl

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

    目录 继承的概念和定义 基类和派生类之间的赋值 继承中的作用域 派生类的默认成员函数 菱形继承 继承和组合的区分与联系 其余注意事项 继承的概念和定义 继承机制是面向对象程序设计的一种实现代码复用的重要手段,它允许程序员在保持原有类特性的基础上进行拓展,增加其他的功能,在此基础上也就产生了一个新的类,称为派生类.继承呈现了面向对象程序设计的层次结构,是类设计层次的复用. //以下代码就是采用了继承机制的一个场景 class person { protected: char _name[28];

  • Maven分模块开发与依赖管理和聚合和继承及属性深入详细介绍

    目录 前言 分模块开发 1.1 分模块开发理念 1.按照功能拆分 2.按照模块拆分 1.2 分模块开发实现 2.依赖管理 2.1 依赖传递与冲突问题 2.2 可选依赖和排除依赖 3.聚合和继承 3.1 聚合 3.2 继承 3.3 聚合VS继承 4.属性 4.1 定义父工程属性 4.2 修改依赖的version 5.配置文件加载属性 5.1 父工程定义属性 5.2 jdbc.properties文件中引用属性 5.3 设置maven过滤文件范围 前言 对于复杂庞大的项目,maven的熟练使用可以大

  • C++多继承同名隐藏实例详细介绍

    如果某个派生类的部分或者全部直接基类是从另一个共同的基类派生而来,在这些俄直接基类中, 从上一级基类继承来的成员就拥有相同的名称,因此派生类中就会出现同名现象.对这种类型的同名成员也要使用作用域分辨符来唯一标识,而且必须使用直接基类来进行限定. -------------------------------------------------- /* * File: main.cpp * Author: yubao * * Created on May 31, 2009, 8:54 AM */

  • C++详细讲解继承与虚继承实现

    目录 继承的概念及定义 概念: 定义: 继承关系和访问限定符 总结 基类和派生类对象赋值转换 继承中的作用域 派生类的默认成员函数 继承与友元 继承与静态成员 复杂的菱形继承及菱形虚拟继承 虚继承原理 继承的总结 继承的概念及定义 概念: 继承机制是面向对象程序设计为了提高代码复用率的一种手段,它可以保持原类特性的基础上进行拓展,简单来说继承是类层次的复用. 接下来我们来看一个简单的继承 class Person { public: void Print() { cout<<"nam

  • 详解C++中单继承与多继承的使用

    目录 前言 1.继承的概念和定义 (1)继承的概念 (2)继承的定义方法 (2)继承后子类的成员类型 2.基类与派生类的赋值转换 (1)派生类赋值给基类 (2)基类给派生类 3.继承中的作用域 (1)隐藏的概念 (2)例题 4.派生类的默认成员函数 (1)默认生成的成员函数 (2)自己写 5.友元与静态成员 6.多继承 (1)概念 (2)复杂的菱形继承 (3)虚继承解决菱形继承问题 (4)虚继承的原理 7.继承与组合 (1)两者区别 (2)继承与组合的区别 (3)使用情况 8.总结 前言 C++

  • Python类的继承与多态详细介绍

    目录 概念 类的创建 类的继承 多态的使用 概念 类(Class): 用来描述具有相同的属性和方法的对象的集合. 类变量:类变量在整个实例化的对象中是公用的.类变量定义在类中且在函数体之外.类变量通常不作为实例变量使用. 类有一个名为 __init__() 的特殊方法(构造方法),该方法在类实例化时会自动调用 self:self 代表的是类的实例,代表当前对象的地址,而 self.class 则指向类. 类调用 Car.weight 实例化  car01=Car(5) 实例对象调用  car01

  • C++继承详细介绍

    在我们进行开发的时候,我们经常会遇到抽象出来的类之间具有继承关系. 举个简单的例子,比如我们在设计某游戏,当中需要定义Human也就是人这个类.每个人有名字,以及一定的血量,能够工作.也就是说Human这个类具有名字和血量这两个成员变量,还有一个工作的函数. 现在我们还需要开发一个英雄Hero类,英雄也是人,他应该也有名字和血量,以及也可以工作.但英雄又和普通人不同,他具有一些特殊的属性.比如变异,比如超能力等等.那么我们在开发Hero这个类的时候,绝大多数的功能都和Human一样,但是又需要额

  • Kotlin类的继承实现详细介绍

    1.在kotlin中,默认类都是封闭的closed的.如果要让某个类开放继承,必须用open关键字修饰 类中的方法默认也是关闭的.如果需要子类复写父类的方法,也必须用open修饰. 1)定义父类,用open将类继承打开.用open将函数的复写打开. //父类必须用open修饰,才能够被继承 open class Person(val name:String) { var age = 0 //父类定义的函数,必须有open修饰,子类才能复写 open fun doWork(){ println("

随机推荐