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

目录
  • 一、继承的概念及定义
    • 1.1、继承的概念
    • 1.2、继承的定义
  • 二、基类和派生类对象赋值转换
  • 三、继承中的作用域
    • 3.1、继承同名成员处理方式
    • 3.2、继承同名静态成员处理方式
    • 3.3、继承与友元
    • 3.4、继承与静态成员
  • 四、派生类的默认成员函数
  • 五、复杂菱形继承及菱形虚拟继承
    • 5.1、继承分类
    • 5.2、虚拟继承解决菱形继承问题原理

一、继承的概念及定义

继承是面向对象三大特性之一。

1.1、继承的概念

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

1.2、继承的定义

继承的语法:class 子类 : 继承方式 父类

继承方式:

  • 共有继承
  • 私有继承
  • 保护继承

基类private成员在派生类中无论以什么方式继承都是不可见的。这里的不可见是指基类的私有成员还是被继承到了派生类对象中,但是语法上限制派生类对象不管在类里面还是类外面都不能去访问它。

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

  • 派生类对象可以赋值给基类的对象/基类的指针/基类的引用。这里有个形象的说法叫切片或者切割。寓意把派生类中父类那部分切来赋值过去。
  • 基类对象不能赋值给派生类对象。
  • 基类的指针或者引用可以通过强制类型转换赋值给派生类的指针或者引用。但是必须是基类的指针是指向派生类对象时才是安全的。

class person{
protected:
	string _name;
	int _age;
};
class student :public person
{
public:
	int _No;
};
void test01()
{
	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;
}

三、继承中的作用域

3.1、继承同名成员处理方式

️问题:当子类与父类出现同名的成员,如何通过子类对象,访问到子类或父类中同名的数据呢?

class Base {
public:
	Base(){
		m_A = 100;
	}
	void func(){
		cout << "Base - func()调用" << endl;
	}
	void func(int a){
		cout << "Base - func(int a)调用" << endl;
	}
public:
	int m_A;
};
class Son : public Base {
public:
	Son(){
		m_A = 200;
	}
	//当子类与父类拥有同名的成员函数,子类会隐藏父类中所有版本的同名成员函数
	//如果想访问父类中被隐藏的同名成员函数,需要加父类的作用域
	void func()
	{
		cout << "Son - func()调用" << endl;
	}
public:
	int m_A;
};
void test01()
{
	Son s;
	cout << "Son下的m_A = " << s.m_A << endl;
	cout << "Base下的m_A = " << s.Base::m_A << endl;
	s.func();
	s.Base::func();
	s.Base::func(10);
}

️️️总结:

  • 子类对象可以直接访问到子类中同名成员
  • 子类对象加作用域可以访问到父类同名成员
  • 当子类与父类拥有同名的成员函数,子类会隐藏父类中同名成员函数,加作用域可以访问到父类中同名函数。
  • 当然父类对象随便调用父类成员。

注:子类和父类中有同名成员时构成隐藏关系,也叫重定义。需要注意的是,如果是成员函数的隐藏,只需要函数名相同就构成隐藏。

3.2、继承同名静态成员处理方式

️:问题:继承中同名的静态成员在子类对象上如何进行访问?

静态成员和非静态成员出现同名,处理方式一致:

  • 子类对象访问子类同名成员 直接访问即可
  • 子类对象访问父类同名成员 需要加作用域
class Base {
public:
	static void func()
	{
		cout << "Base - static void func()" << endl;
	}
	static void func(int a)
	{
		cout << "Base - static void func(int a)" << endl;
	}
	static int m_A;
};
int Base::m_A = 100;
class Son : public Base {
public:
	static void func()
	{
		cout << "Son - static void func()" << endl;
	}
	static int m_A;
};
int Son::m_A = 200;
//同名成员属性
void test01()
{
	//通过对象访问
	cout << "通过对象访问: " << endl;
	Son s;
	cout << "Son  下 m_A = " << s.m_A << endl;
	cout << "Base 下 m_A = " << s.Base::m_A << endl;
	//通过类名访问
	cout << "通过类名访问: " << endl;
	cout << "Son  下 m_A = " << Son::m_A << endl;
	cout << "Base 下 m_A = " << Son::Base::m_A << endl;
}
//同名成员函数
void test02()
{
	//通过对象访问
	cout << "通过对象访问: " << endl;
	Son s;
	s.func();
	s.Base::func();
	cout << "通过类名访问: " << endl;
	Son::func();
	Son::Base::func();
	//出现同名,子类会隐藏掉父类中所有同名成员函数,需要加作作用域访问
	Son::Base::func(100);
}

总结:同名静态成员处理方式和非静态处理方式一样,只不过有两种访问的方式(通过对象 和 通过类名)

3.3、继承与友元

友元关系不可以继承,也就是说基类的友元不要可以访问子类的私有成员和保护成员。

(就好比说爸爸的朋友不一定是我的朋友)

3.4、继承与静态成员

基类定义了static静态成员,则整个继承体系只有这一个成员(我们知道静态成员是整个类共享的),无论派生出多少个子类,都只有这么一个static成员。

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 course;
};
void test()
{
	student s1;
	student s2;
	student s3;
	graduate s4;
	cout << "人数" << person::_count << endl;
	student::_count = 0;
	cout << "人数" << person::_count << endl;
}

人数4
人数0
请按任意键继续. .

代码解释:因为子类对象构造是会调用基类的构造函数,所以每实例化一个子类对象都会调用一次基类构造,从而_count++,并且静态成员是整个类共享的,所以无论哪个子类都可修改!!!

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

6个默认成员函数,“默认"的意思就是指我们不写,编译器会变我们自动生成一个,那么在派生类中,这几个成员函数是如何生成的呢?

  1. 派生类的构造函数必须调用基类的构造函数初始化基类的那一部分成员。如果基类没有默认的构造函数,则必须在派生类构造函数的初始化列表阶段显示调用。
  2. 派生类的拷贝构造函数必须调用基类的拷贝构造完成基类的拷贝初始化。
  3. 派生类的operator=必须要调用基类的operator=完成基类的复制。
  4. 派生类的析构函数会在被调用完成后自动调用基类的析构函数清理基类成员。因为这样才能保证派生类对象先清理派生类成员再清理基类成员的顺序。
  5. 派生类对象初始化先调用基类构造再调派生类构造。
  6. 派生类对象析构清理先调用派生类析构再调基类的析构。
class person
{
public:
	person(const char* name = "pxl")
		:_name(name)
	{}
	person(const person& p)
		:_name(p._name)
	{}
	person& operator=(const person& p)
	{
		if (this != *p){
			_name = p._name;
		}
		return *this;
	}
	~person()
	{}
protected:
	string _name;
};
class student :public person
{
public:
	student(const char* name, int num)
		:person(name)//显示调用基类的构造函数初始化基类成员
		, _num(num)
	{}
	student(const student& s)
		:person(s)//注意这里有个隐式的切片操作 person& p = s;
		, _num(s._num)
	{}
	student& operator=(const student& s)
	{
		if (this != &s){
			person::operator=(s);//调用基类的operator=完成基类的赋值
			_num = s._num;
		}
		return *this;
	}
	~student()
	{
		cout << "~student()" << endl;
		//注意这里会自动调用父类析构
	}
protected:
	int _num;
};
void test()
{
	student s1("ppp", 20);
	student s2(s1);
	student s3("xxx", 30);
	s1 = s3;
}

️留意代码中注释部分!

五、复杂菱形继承及菱形虚拟继承

5.1、继承分类

单继承:一个子类只有一个直接父类时称为单继承

多继承:一个子类有两个或者两个以上直接父类时称这个继承关系为多继承

菱形继承:两个派生类继承同一个基类,又有某个类同时继承者两个派生类。菱形继承带来的主要问题是子类继承两份相同的数据,导致资源浪费以及毫无意义。

利用虚继承可以解决菱形继承问题

️️️️️️️️️️️️️️️️️️️️️️️️️️️️

对于菱形继承的二义性问题,我们可以在访问的时候加上类域,这样是可以解决的,但是数据冗余无法解决。所以下面引入虚拟继承!

5.2、虚拟继承解决菱形继承问题原理

为了研究虚拟继承原理,我们给出一个简单的菱形继承体系,再借助内存窗口观察对象成员模型。

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;
	system("pause");
	return 0;
}

如图是菱形继承的内存对象成员模型,可以看出来数据冗余!!!

下面是菱形虚拟继承的内存对象成员模型:

这里可以分析出D对象将A放在了对象组成的最下面,这个A同时属于B和C,那么B和C如何去找到公共的A呢?

这里通过B和C的两个指针,指向一张表。这两个指针叫虚基表指针,这两个表叫虚基表,虚基表中存的是偏移量。通过偏移量可以找到下面的A。

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

(0)

相关推荐

  • C++私有继承与EBO深入分析讲解

    目录 私有继承本质不是继承 空类大小 空基类成员压缩 总结 Hello!大家好呀,近期逗比老师的一个学生问了我这样一个问题:“C++里的私有继承到底有什么意义?” 不知道你有没有跟他一样的困惑.的确,我们在编写C++项目中,几乎是没有用过私有继承(这里包括protected继承和private继承),都是清一色的public继承.有的老师干脆直接告诉学生,你见到继承就是public,其他那俩是历史原因,当它不存在就好了. 这种说法呢,其实也有一定道理,但也不全对.对的部分在于:C++中,确实只有

  • C++ 继承的范例讲解

    目录 1.继承的概念 2.继承方式 3.基类与派生类的赋值转换 4.作用域与隐藏 5.派生类的默认成员函数 6.友元与静态成员 7.菱形继承与虚继承 8.继承和组合 1.继承的概念 继承,是面向对象的三大特性之一.继承可以理解成是类级别的一个复用,它允许我们在原有类的基础上进行扩展,增加新的功能. 当创建一个类时,我们可以继承一个已有类的成员和方法,并且在原有的基础上进行提升,这个被继承的类叫做基类,而这个继承后新建的类叫做派生类. 用法如下: class [派生类名] : [继承类型] [基类

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

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

  • 一文搞懂C++中继承的概念与使用

    目录 前言 继承概念及定义 继承概念 继承定义 继承方式 父类和子类对象赋值转换 继承中的作用域 派生类的默认成员函数 派生类的友元与静态成员 继承关系 单继承 多继承 菱形继承 前言 我们都知道面向对象语言的三大特点是:**封装,继承,多态.**之前在类和对象部分,我们提到了C++中的封装,那么今天呢,我们来学习一下C++中的继承. 继承概念及定义 继承概念 继承(inheritance)机制是面向对象程序设计使代码可以复用的最重要的手段,它允许程序员在保持原有类特性的基础上进行扩展,增加功能

  • C++深入探究继承的概念与使用

    目录 1.概念及定义 1.1 概念 1.2 定义 2.class与struct的区别 3.赋值兼容规则 4.继承中的作用域问题 5.派生类(子类)的默认成员函数 5.1 构造函数 5.2 拷贝构造函数 5.3 赋值运算符重载 5.4 析构函数 6.基类中哪些成员被子类继承了 6.1 成员变量 6.2 成员方法 7.友元函数被继承了吗 1.概念及定义 1.1 概念 继承主要的工作就是-----共性抽取 具体地讲: ①继承机制是面向对象程序设计使代码可以复用的最重要的手段; ②允许程序员在保持原有类

  • C++图文并茂讲解继承

    目录 一.生活中的例子 二.惊艳的继承 三.继承的意义 四.小结 一.生活中的例子 组合关系∶整体与部分的关系 下面看一个组合关系的描述代码: #include <iostream> #include <string> using namespace std; class Memory { public: Memory() { cout << "Memory()" << endl; } ~Memory() { cout <<

  • C++虚继承的实现原理由内存布局开始讲起

    目录 准备工作 虚继承的内存分布情况 准备工作 1.VS2012使用命令行选项查看对象的内存布局 微软的Visual Studio提供给用户显示C++对象在内存中的布局的选项:/d1reportSingleClassLayout.使用方法很简单,直接在[工具(T)]选项下找到“Visual Studio命令提示(C)”后点击即可.切换到cpp文件所在目录下输入如下的命令即可 c1 [filename].cpp /d1reportSingleClassLayout[className] 其中[fi

  • C++深入探究不同的继承体系

    目录 单继承 多继承 菱形继承 概念 存在的问题 解决方案 菱形虚拟继承 虚拟继承 什么是虚拟继承 内存层面理解虚拟继承 虚拟继承和普通单继承的区别 虚拟继承+菱形继承 声明: 本文的测试环境为Windows平台下的VS2013环境 单继承 一个子类只有一个父类.这类继承方式是最为简单的.具体可以参考继承 上篇中的例子.这里给出简单的图示即可: 多继承 一个子类有两个或两个以上的直接父类,这样的继承关系称之为多继承. 图解: 注意事项:  ①在多继承的场景下,建议在继承的每一个基类名称前都加上继

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

    目录 一.继承的概念及定义 1.1.继承的概念 1.2.继承的定义 二.基类和派生类对象赋值转换 三.继承中的作用域 3.1.继承同名成员处理方式 3.2.继承同名静态成员处理方式 3.3.继承与友元 3.4.继承与静态成员 四.派生类的默认成员函数 五.复杂菱形继承及菱形虚拟继承 5.1.继承分类 5.2.虚拟继承解决菱形继承问题原理 一.继承的概念及定义 继承是面向对象三大特性之一. 1.1.继承的概念 继承(inheritance)机制是面向对象程序设计使代码可以复用的最重要的手段,它允许

  • C++类继承之子类调用父类的构造函数的实例详解

    C++类继承之子类调用父类的构造函数的实例详解 父类HttpUtil: #pragma once #include <windows.h> #include <string> using namespace std; class HttpUtil { private: LPVOID hInternet; LPVOID hConnect; LPVOID hRequest; protected: wchar_t * mHostName; short mPort; string send

  • 继承行为在 ES5 与 ES6 中的区别详解

    笔者注:一句话引发的基础知识回炉,基础不扎实,还要什么自行车 最近在看 React 方面的一些文章时,看到了这样一个问题,「为什么每个 class 中都要写 super, super 是做什么的?」, 刚看到这个问题时,直接就想到了继承行为在 javascript 中的表现.后面作者的一句话「super 不可以省略,省略的话会报错」.当时脑海中蹦出来一个念头,这个同学是不是写错了,super 不就是用来完成调用父类构造函数,将父类的实例属性挂在到 this 上吗?为什么不写还会报错? 后来自己亲

  • 基于JavaScript实现继承机制之调用call()与apply()的方法详解

    call() 方法call() 方法是与经典的对象冒充方法最相似的方法.它的第一个参数用作 this 的对象.其他参数都直接传递给函数自身.例如: 复制代码 代码如下: function sayHello(sPrefix,sSuffix) {    alert(this.name + "says" + sPrefix + sSuffix);}; var obj = new Object();obj.name = "Tom"; sayHello.call(obj, &

  • 基于JavaScript实现继承机制之构造函数方法对象冒充的使用详解

    继承的方式 ECMAScript 实现继承的方式不止一种.这是因为 JavaScript 中的继承机制并不是明确规定的,而是通过模仿实现的.这意味着所有的继承细节并非完全由解释程序处理.作为开发者,你有权决定最适用的继承方式.最原始的继承实现方式就是对象冒充,下面着重介绍该方法. 对象冒充 对象冒充实现继承的核心其实依赖于在函数环境中使用 this 关键字.其原理如下:构造函数使用 this 关键字给所有属性和方法赋值(即采用类声明的构造函数方式).因为构造函数只是一个函数,所以可使 Class

  • Python 日期的转换及计算的具体使用详解

    日期的转换及计算 对于日期,有时需执行不同时间单位的转换,或者接受字符串格式的日期,转换为 datetime 对象.有时需计算日期的范围,以及特定某个星期几的日期.这里更多用到的是 Python 提供的 datetime 模块. datetime 模块 日期与时间的简单转换 datetime 模块中可以通过创建 timedelta 对象表示一个时间段.如下示例: >>> from datetime import timedelta >>> a = timedelta(d

  • JavaScript转换与解析JSON方法实例详解

    本文实例讲述了JavaScript转换与解析JSON方法.分享给大家供大家参考,具体如下: json格式数据如下: var json = { 'jquery': [{ "id": "1", "type": "ASP.NET", "title": "JSON全解析"}] } alert(json.jquery[0].id); alert(json.jquery[0].type); aler

  • 解析在.net中使用XSLT转换xml文档的示例详解

    XSL即可扩展的样式表文件. 可以格式化xml的显示,也可以将xml转换成需要的另一种格式.学习XSL必须熟悉XPath.XSL和XPath一样简单强大,容易学习.1. XSL既然可以格式化xml的显示样式,我们先来看如何在xml中引用xsl文件如下代码示例:<?xml version="1.0" encoding="utf-8"?><?xml-stylesheet type="text/xsl" href="url.

  • nodejs中转换URL字符串与查询字符串详解

    一个完整的URL字符串中,从"?"(不包括?)到"#"(如果存在#)或者到该URL字符串结束(如果不存在#)的这一部分称为查询字符串. 可以使用Query String模块中的parse方法将该字符串转换为一个对象,parse方法的使用方式如下所示: querystring.parse(str,[sep],[eq],[options]); str表示被转换的查询字符串, sep.字符串中的分隔符,默认是& eq.该字符串中的分配符,默认为=."=&

  • 基于JavaScript实现继承机制之构造函数+原型链混合方式的使用详解

    构造函数.原型实现继承的缺陷 首先来分析构造函数和原型链两种实现继承方式的缺陷: 构造函数(对象冒充)的主要问题是必须使用构造函数方式,且无法继承通过原型定义的方法,这不是最好的选择.不过如果使用原型链,就无法使用带参数的构造函数了.开发者如何选择呢?答案很简单,两者都用. 构造函数+原型混合方式 这种继承方式使用构造函数定义类,并非使用任何原型.创建类的最好方式是用构造函数定义属性,用原型定义方法.这种方式同样适用于继承机制,用对象冒充继承构造函数的属性,用原型链继承 prototype 对象

随机推荐