【CPP】继承语法详解与菱形继承

news2024/12/26 20:47:14

关于我:在这里插入图片描述


睡觉待开机:个人主页

个人专栏: 《优选算法》《C语言》《CPP》
生活的理想,就是为了理想的生活!
作者留言

PDF版免费提供倘若有需要,想拿我写的博客进行学习和交流,可以私信我将免费提供PDF版。
留下你的建议倘若你发现本文中的内容和配图有任何错误或改进建议,请直接评论或者私信。
倡导提问与交流关于本文任何不明之处,请及时评论和私信,看到即回复。


参考目录

  • 1.前言
  • 2.回顾封装的概念
  • 3.继承的概念
  • 4.继承的定义
    • 4.1继承的定义格式
    • 4.2继承权限
  • 5.父类和子类对象赋值转换
  • 6.继承种的作用域
  • 7.子类的默认成员函数
    • 7.1构成函数
    • 7.2拷贝构造函数
    • 7.3赋值运算符重载函数
    • 7.4析构函数
  • 8.继承与友元函数
  • 9.继承与静态成员
  • 10.菱形继承
    • 10.1单继承与多继承
    • 10.2多继承中的菱形继承问题及虚拟继承
    • 10.3菱形继承中虚拟继承的原理
    • 10.4在继承公共基类的时候使用虚继承
    • 10.5腰部父类也使用虚继承模型
    • 10.6菱形继承的实例
  • 11.组合与继承


1.前言

我们知道,CPP有三大特性:封装、继承和多态。在本文中,我们将简单回顾一下封装的理解,由浅到深的去了解继承的相关语法和一些高级语法。

好,那话不多说,让我们开始吧。

2.回顾封装的概念

什么是封装呢?
实际上,这是一个CPP概念理解中一个挺重要的概念之一,请你简谈一下对CPP封装语法的理解。

我是这样理解的,以下回答仅供参考:
从语法的角度:就是把数据和函数方法进行合并,并用访问限定符修饰加以限定。
从上下层角度:就是把一个类放到另一个类里面,通过typedef的方式,封装成为一个全新的类型。
这个类型从上层使用来说,可能会与一些其他类型使用保持一致,但是底层差异很大,比如我们之前接触过的deque和vector,deque的底层完全不是一个连续的空间,但是通过封装的方式,使得deque得使用与vector差别不大。再比如说,我们之前使用正向迭代器封装成为反向迭代器,反向迭代器虽然在使用上与与正向迭代器一致,实际上就是给正向迭代器套了一层壳。

好的,以上便是我对封装得一点简单理解。下面我们开始谈继承得相关概念~我先说继承是一种特殊得代码复用得一种形式。
谈到代码复用,我们前面学过一些代码复用简单来回顾一下:

  • 函数逻辑的代码复用
  • 针对类/函数大体逻辑相似,只有类型不同的模板代码复用
  • 继承,是一种类层次设计上的代码复用
    在这里插入图片描述

那我们现在正式进入介绍继承语法这一阶段。

3.继承的概念

继承:类层次设计的一种代码复用。
在这里插入图片描述

如何理解继承的概念呢?举个比较形象的例子,你父亲的东西你可以拿过来用,这就是你继承了你父亲的一些东西。比如说,你长大了,是不是可以继承一些你父亲的财产?你父亲的技术?哈哈(仅仅是举个例子)。代码也是同理,我们可以定义一个父类,然后去让别的类继承,比如说上图中我们下面定义的学生类、老师类、导员类都是可以去继承人类这个类的,继承了有什么好处呢?很多重复的类代码就不用自己去写了呗,本质就是一种代码复用。比如说学生、老师、导员都可以继承人类的名字,这样就不用再在学生类、老师类、导员类中每个中都去定义一个名字变量了对不对。

如此好用的继承,那该如何定义呢?下面来简单进行介绍。

4.继承的定义

4.1继承的定义格式

先来说一下继承的定义格式:
在这里插入图片描述
主要是在一个一般的类声明的基础上在后面跟个冒号,然后写继承方式(public/protected/private),然后再去写明继承父类的名字就好了。

总结下来就是:class 继承子类名称 : 继承方式 继承父类名称

是不是语法设置很简单呢?但是在这里我们提到了继承方式,那啥是继承方式呢?
继承方式指的是继承子类打算以什么方式去继承这个父类的一些成员,继承方式有下面三种:public、private、protected三种继承方式。
除此之外,父类中每个成员都会访问限定符进行修饰。
继承方式和每个成员的访问限定符共同决定了子类中到底继承到的成员具有什么权限。

在这里插入图片描述
继承方式有三种,每个父类成员的访问限定符又有三种,所以组合起来一共有9种情况。情况比较多,待我一一道来。

4.2继承权限

前面提过,子类成员继承成员权限 = 父类成员修饰限定符 + 继承方式共同决定。

我总结了下面表格:
在这里插入图片描述
可能有些同学会对这个表格的一些内容感到不太理解,没关系,下面我挨个说明,挨个去举例。

class F
{
private:
	void PriTest()
	{
		cout << "F:PriTest()" << endl;
	}
protected:
	void ProTest()
	{
		cout << "F:ProTest()" << endl;
	}
public:
	void PubTest()
	{
		cout << "F:PubTest()" << endl;
	}
};

class PriS : private F
{
public:
	void PriSTest_Pri()
	{
		//PriTest();//父类私有成员,私有继承,类内不能访问
	}

	void PriSTest_Pro()
	{
		ProTest();//父类保护成员,私有继承,类内可以访问
	}
	

	void PriSTest_Pub()
	{
		PubTest();//父类公共成员,私有继承,类内可以访问
	}
};
void PriSTest()
{
	PriS pris;//对于私有继承,所有父类成员均不可在类外访问
}


class ProS : protected F
{
public:
	void ProSTest_Pri()
	{
		//PriTest();//父类私有成员,保护继承,类内不能访问
	}

	void ProSTest_Pro()
	{
		ProTest();//父类保护成员,保护继承,类内可以访问
	}

	void ProSTest_Pub()
	{
		PubTest();//父类公共成员,保护继承,类内可以访问
	}
};
void ProSTest()
{
	ProS pros;
	//对于保护继承,所有父类成员均不可在类外访问
}

class PubS : public F
{
public:
	void PubSTest_Pri()
	{
		//PriTest();//父类私有成员,保护继承,类内不能访问
	}

	void PubSTest_Pro()
	{
		ProTest();//父类保护成员,保护继承,类内可以访问
	}

	void PubSTest_Pub()
	{
		PubTest();//父类公共成员,保护继承,类内可以访问
	}
};
void PubSTest()
{
	PubS pubs;
	pubs.PubTest();
	//对于公共继承,所有父类成员种只有公共成员才可在类外访问
}
void test1()
{
	PriSTest();
	ProSTest();
	PubSTest();
}

可能有些同学还是不太能理解,虽然上面附了一些代码…
那我直接总结了一些规律来供大家快速理解上面表格。

  • 对于保护访问限定符的理解
    protected是针对于CPP继承语法而诞生的。
    protected所修饰的父类成员,允许在子类中使用,但是不允许在子类类外使用。
  • 私有继承和私有成员的理解
    私有继承:继承方式是private的继承,私有成员:被private修饰符所修饰的类成员。
    私有继承对父类的public、protected修饰的成员是可见的。但是任何继承方式对于父类种private修饰的成员是不可见的。
  • 继承访问限定的确定
    对于不是父类私有的成员,我们可以取其继承方式和权限修饰限定符的权限较小者。比如说,继承方式是protected,对于父类中的public成员,那么继承下来的就是protected权限。
  • struct和class默认继承
    其实针对于struct和class继承,是可以进行默认继承的,就是写继承定义语法的时候可以省略继承方式。对于struct,默认继承方式是public继承,对于class,默认就是private继承。

这里我们不妨来做个引申:CPP中struct与class的区别是什么?
struct、class做类,默认是public公开成员的,而class是默认private成员的。
struct、class对于继承来说,struct默认继承是公开继承方式,而class默认继承是私有继承方式。
在这里插入图片描述

5.父类和子类对象赋值转换

CPP中支持把子类对象赋值给父类对象,有个专属的名词叫做切片切割
很新奇吧?为啥其这么个名字呢?
在这里插入图片描述

class Father
{
private:
	int f_a;
protected:
	int f_b;
public:
	int f_c;
};

class Son : public Father
{
private:
	int s_a = 1;
protected:
	int s_b = 2;
public:
	int s_c = 3;
};

void test2()
{
	Son s;
	Father f = s;//代码为 0。
}

在上图种,父类有name、sex、age三个成员变量,子类呢比父类多个_no的变量,
你想,要把一个子类对象强行放到一个父类类型里面,那是不是_no变量会被扔掉?所以十分切合我们所说的这种意思,CPP就形象的称此为“切片”/“切割”啦。

实际上,除了子类对象可以赋给父类对象之外,自然也支持把子类指针给到父类指针,把子类引用给到父类引用啦(请参见下图)。
在这里插入图片描述

void test2()
{
	Son s;
	Father f = s;//代码为 0。
	Father* pf = &s;//代码为 0。
	Father& qf = s;//代码为 0。
}

除此之外,我还需要介绍:子类给父类对象的时候是没有中间变量产生的
我们都知道,隐式类型转换、强制类型转换都会在赋值中间产生一个临时对象,而子类和福哦类的复制转换是没有临时对象产生的。
这是为什么呢?编译器做了特殊处理。其中的道理我也不太懂,暂且留到以后有机会再说吧哈哈。

之后,我还要去强调另外一点:父类对象不能给到子类类型变量哈。

6.继承种的作用域

两个类构成继承,那么对于作用域而言两者也是相互独立的。
子类和父类中有同名成员不会报错,此时会构成 隐藏
需要主要的是成员函数的隐藏构成条件是函数名一致即可,不需要参数进行比较,两个不同类中的函数不会构成重载哈!只有在同一个作用域的函数才会有重载这一说,我们刚开始就说了两个类有着相互独立的作用域。
我个人建议大家在继承体系定义的时候尽量不要定义重名的成员,因为容易进坑。

class Father2
{
public:
	int a = 1;
	void func()
	{
		cout << " father " << endl;
	}
};

class Son2 : public Father2
{
public:
	int a = 2;
	void func()
	{
		cout << " son " << endl;
	}
};

void test3()
{
	Son2 son2;
	cout << son2.a << endl;//访问的是son中的变量
	son2.func();//访问的是son中的函数
	cout << son2.Father2::a << endl;//访问的是father中的变量
	son2.Father2::func();//访问的是father中的函数
}

7.子类的默认成员函数

对于子类的默认成员函数认识比较复杂,首先需要对子类的默认成员函数有三个方面进行认识:一整个父类 + 子类中的内置类型 + 子类中的自定义类型
在这里插入图片描述

7.1构成函数

子类构造的逻辑:
如果你不写子类的构造函数,那么编译器帮你自动生成一个默认构造函数,这个默认构造函数会忽略子类中的内置类型,会去自动调用子类中的自定义类型,会去自动调用父类的默认构造函数,如果此时父类没有默认构造函数就会报错哈!

class Fa
{
public:
	int _fa;
};
class So: public Fa
{
public:
	int _so;
};

void test4()
{
	So s;
	//在父类和子类都不写构造的情况下,子类会生成默认构造
	//子类默认构造里会去调用父类的默认构造
}
class Fa
{
public:
	int _fa;

	Fa(int f, char c)//此时Fa没有默认构造函数
	{

	}
};
class So : public Fa
{
public:
	int _so;

};

void test4()
{
	So s;//So::So(void)”: 由于 基类“Fa”不具备相应的 默认构造函数 或重载解决不明确,因此已隐式删除函数
	//此时So不写默认构造,编译器会自动生成子类默认构造函数,并去调用父类的默认构造函数、
	//但是父类没有默认构造,因而报错
}

什么是默认构造函数?
全缺省的,编译器默认生成的,你显示写的无参的构造函数我们都叫做默认构造函数。

如果你显示写了子类的构造函数,并且都正常去对子类中的内置类型做了处理,也调用了子类中自定义类型的构造函数,指明调用了父类中的构造函数,那么编译器就会按照你写的去走。
但是如果你显示写了子类的构造函数,但是里面什么都没写,那么编译器怎么做呢?此时请注意:编译器依然会对子类内置类型忽略,对子类中的自定义类型去调用对应的构造函数,仍然会调用父类的默认构造。为什么?明明我什么都没有写啊!因为编译器会自动走构造函数的初始化列表!

class Fa
{
public:
	int _fa;

	//此时_fa存在默认构造函数
};
class So : public Fa
{
public:
	int _so;

	So()
		:_so(1)
	{}
};

void test4()
{
	So s;
	//此时so写了子类构造函数,会去调用父类默认构造函数。
}
class Fa
{
public:
	int _fa;

	Fa(int c)
		:_fa(1)
	{
		//此时Fa没有默认构造函数
	}
};
class So : public Fa
{
public:
	int _so;

	So()
		:Fa(1)//明确写要调用父类的非默认构造函数
		,_so(1)
	{}
};

void test4()
{
	So s;
	//So明确写了构造函数,虽然父类中没有默认构造,但是子类构造明确调用父类有参构造,所以也可以正常运行
}

7.2拷贝构造函数

拷贝构造的逻辑基本与构造函数是一样的,依然编译器会自动给你生成一个。这里就不再多介绍了。

不过有一点我需要强调哈:就是拷贝构造函数与构造函数是并列关系,显示写有参构造不会影响编译器生成拷贝构造函数。但是我写一个拷贝构造函数编译器不再生成默认构造函数了哈。

这个地方比较奇怪,这都怪CPP的老古董语法了~

class father
{
public:
	int _f = 1;
};

class son : public father
{
public:
	int _s = 1;
};

void test5()
{
	son s;

	son s2(s);
	//子类有默认拷贝构造,父类也有,所以这时候是没有问题的
}
class father
{
public:
	int _f = 1;

	father() = default;//强制生成默认构造函数
	
	//拷贝构造
	father(father& f)
		:_f(f._f)
	{}
};

class son : public father
{
public:
	int _s = 1;

	son() = default;//强制生成默认构造函数

	son(son& s)
		:_s(s._s)
	{
		cout << " father " << endl;
	}
};

void test5()
{
	son s2;

	son s(s2);//father
	//子类拷贝构造即使不写调用父类拷贝构造,也会去默认调用
}
class son : public father
{
public:
	int _s = 1;

	son() = default;//强制生成默认构造函数

	son(son& s)
		:father(s)//明确写调用父类的拷贝构造,注意这个地方会发生切片
		,_s(s._s)
	{
		cout << " father " << endl;
	}
};

void test5()
{
	son s2;

	son s(s2);//father
	//子类拷贝构造写调用父类拷贝构造,那么也会去调用父类的拷贝构造函数
}

7.3赋值运算符重载函数

这个跟上面的构造函数还是不太一样的,需要着重说一下。

如果子类和父类的赋值运算符重载函数自己都不写,编译器都会默认进行生成,对于子类的内置类型,直接浅拷贝(值拷贝),对于自定义类型,那么就直接调用对应的拷贝构造函数,同样对于父类的赋值也自然会去调用。

如果子类中明确写了赋值,但是子类赋值没有写要访问父类赋值,此时并不会去调用父类赋值。为什么跟前面两个拷贝构造、构造不一样呢?因为前两个构造都要走初始化列表,但是赋值函数没有初始化列表这一说。

class F1
{
public:
	int _f;

	F1()
	{
		_f = 2;
		cout << "F1()" << endl;
	}

	F1& operator=(const F1& f)
	{
		if (this != &f)//排除自己给自己赋值的情况
		{
			cout << "F1& operator=(const F1& f)" << endl;

			_f = f._f;
		}

		return *this;
	}
};

class S1 : public F1
{
public:
	int _s;

	S1()
	{
		_s = 1;
		cout << "S1()" << endl;
	}

	S1& operator=(const S1& s)
	{
		if (&s != this)
		{
			//不写,不去默认调用父类的赋值函数。
			cout << "S1& operator=(const S1& s)" << endl;
			_s = s._s;
		}

		return *this;
	}
};

void test6()
{
	S1 s1;//F1() S1()
	S1 s2;//F1() S1()

	s2 = s1;//S1& operator=(const S1& s)
}

要显示写调用的话怎么写?前面要加类名限定符。不写的后果就是死递归,然后程序挂掉。

class F1
{
public:
	int _f;

	F1()
	{
		_f = 2;
		cout << "F1()" << endl;
	}

	F1& operator=(const F1& f)
	{
		if (this != &f)//排除自己给自己赋值的情况
		{
			cout << "F1& operator=(const F1& f)" << endl;

			_f = f._f;
		}

		return *this;
	}
};

class S1 : public F1
{
public:
	int _s;

	S1()
	{
		_s = 1;
		cout << "S1()" << endl;
	}

	S1& operator=(const S1& s)
	{
		if (&s != this)
		{
			F1::operator=(s);//这个地方前面一定要写F1,不然就是死递归

			cout << "S1& operator=(const S1& s)" << endl;
			_s = s._s;
		}

		return *this;
	}
};

void test6()
{
	S1 s1;//F1() S1()
	S1 s2;//F1() S1()

	s2 = s1;//F1& operator=(const F1& f) S1& operator=(const S1& s)
}

7.4析构函数

子类的析构函数调用结束后会自动调用父类的析构函数。->原因在于要保证先析构子类后析构父类,因为子类是可以访问父类的,如果先析构父类,那么再访问父类的成员会出现意想不到的结果。

class Fa
{
public:
	int _fa;

	~Fa()
	{
		cout << "~Fa()" << endl;
	}

};
class So : public Fa
{
public:
	int _so;

	~So()
	{
		cout << "~So()" << endl;
	}
};

void test7()
{
	So s;

	//~So()
	//~Fa()
}

子类和父类的析构函数在子类函数中也会发生隐藏/重定义,写的时候也要前面加上类名->这是因为后面多态的缘故,编译器对析构底层做了特殊处理,使得子类和父类的析构函数产生了隐藏/重定义。

class Fa
{
public:
	int _fa;

	~Fa()
	{
		cout << "~Fa()" << endl;
	}

};
class So : public Fa
{
public:
	int _so;

	~So()
	{
		Fa::~Fa();//这个地方前面也得指明类域

		cout << "~So()" << endl;
	}
};

void test7()
{
	So s;

	//~Fa()
	//~So()
	//~Fa()
}

8.继承与友元函数

把继承这个新语法加入与友元函数又有什么火花呢?

继承对于友元函数是没什么关系哈,我们如果把友元函数比作是朋友,那么继承就类似于父子之间的关系,你父亲的朋友跟你没啥关系,你的朋友也跟你父亲没啥关系。

class Student;//类的声明
class Person
{
public:
	friend void Display(const Person& p, const Student& s);

protected:
	string _name = "1"; // 姓名
};

class Student : public Person
{
protected:
	string _s = "2";
};

void Display(const Person& p, const Student& s)
{
	cout << p._name << endl;//友元函数仅可以访问父类的东西
	//cout << s._stuNum << endl;//报错
}

void test81()
{
	Person p;
	Student s;

	Display(p, s);
}

不过需要注意的是,因为你继承了你父亲的一些成员,所以友元函数是可以访问你继承了你父亲这一部分的成员的,因为这些成员是属于你的(加入说函数在该类友元的话)。

class Student;//类的声明
class Person
{
public:
protected:
	string _name = "1"; // 姓名
};

class Student : public Person
{
	friend void Display(const Person& p, const Student& s);

protected:
	string _s = "2";
};

void Display(const Person& p, const Student& s)
{
	cout << s._name << endl; //友元函数可以访问子类继承父类的东西
	cout << s._s << endl; //友元函数仅可以访问子类的东西
	//cout << p._name << endl;//此时去访问父类的东西会报错
}

void test81()
{
	Person p;
	Student s;

	Display(p, s);
}

9.继承与静态成员

对于一般的变量,父类对象有一份,继承他的子类对象也有一份(前提是父类变量不是私有的哈)。
对于静态变量比较特殊,CPP规定只有一份,既属于父类,也属于子类。请注意,整个父类无论有多少对象,都只有一个static变量!

基类定义了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 _seminarCourse; // 研究科目
};
void TestPerson()
{
	Student s1;
	Student s2;
	Student s3;
	Graduate s4;
	cout << " 人数 :" << Person::_count << endl;
	Student::_count = 0;
	cout << " 人数 :" << Person::_count << endl;
}

10.菱形继承

10.1单继承与多继承

在介绍什么是菱形继承之前,我先来说一下什么是单继承与多继承的概念。
继承按照可以继承父类的数量可以分为单继承多继承
单继承:
在这里插入图片描述
多继承:
在这里插入图片描述
前面讲的都是单继承,CPP中也有多继承机制,在多继承机制下,CPP为多种场景提供了更好的支持,但是,多继承中的菱形继承存在一定的小问题!
在这里插入图片描述

10.2多继承中的菱形继承问题及虚拟继承

在这里插入图片描述
在上面菱形继承中,我们发现同一份变量会继承两份。这样会造成数据冗余和二义性问题。

class Person
{
public:
	string _name; // 姓名
};
class Student : public Person
{
protected:
	int _num; //学号
};
class Teacher : public Person
{
protected:
	int _id; // 职工编号
};
class Assistant : public Student, public Teacher
{
protected:
	string _majorCourse; // 主修课程
};
void test10()
{
	// 这样会有二义性无法明确知道访问的是哪一个
	Assistant a;
	//a._name = "peter";
	// 需要显示指定访问哪个父类的成员可以解决二义性问题,但是数据冗余问题无法解决
	a.Student::_name = "xxx";
	a.Teacher::_name = "yyy";
}

虚拟继承可以解决菱形继承的二义性和数据冗余的问题。如上面的继承关系,在Student和Teacher的继承Person时使用虚拟继承,即可解决问题。需要注意的是,虚拟继承不要在其他地方去使用。

class Person
{
public:
	string _name; // 姓名
};
class Student : virtual public Person
{
protected:
	int _num; //学号
};
class Teacher : virtual public Person
{
protected:
	int _id; // 职工编号
};
class Assistant : public Student, public Teacher
{
protected:
	string _majorCourse; // 主修课程
};
void test10()
{
	// 这样会有二义性无法明确知道访问的是哪一个
	Assistant a;
	a._name = "peter";
	// 需要显示指定访问哪个父类的成员可以解决二义性问题,但是数据冗余问题无法解决
	a.Student::_name = "xxx";
	a.Teacher::_name = "yyy";

	cout << a._name << endl;//yyy
	cout << a.Student::_name << endl;//yyy
	cout << a.Teacher::_name << endl;//yyy

	cout << &a._name << endl;//000000860A0FFAA8
	cout << &a.Student::_name << endl;//000000860A0FFAA8
	cout << &a.Teacher::_name << endl;//000000860A0FFAA8
}

10.3菱形继承中虚拟继承的原理

虚拟继承是如何解决菱形继承二义性、数据冗余的问题的呢?
为了研究虚拟继承原理,我们给出了一个简化的菱形继承继承体系,再借助内存窗口观察对象成员的模型。

//模型代码
class A
{
public:
	int _a;
};

class B : public A
//class B : virtual public A
{
public:
	int _b;
};

class C : public A
//class C : virtual public A
{
public:
	int _c;
};
class D : public B, public C
{
public:
	int _d;
};
void test11()
{
	//这是在没有使用虚拟继承情况下的菱形继承,看d的内存空间
	D d;
	d.B::_a = 1;
	d.C::_a = 2;
	d._b = 3;
	d._c = 4;
	d._d = 5;
}

在这里插入图片描述

class A
{
public:
	int _a;
};

//class B : public A
class B : virtual public A
{
public:
	int _b;
};

//class C : public A
class C : virtual public A
{
public:
	int _c;
};
class D : public B, public C
{
public:
	int _d;
};
void test11()
{
	//这是在使用虚拟继承情况下的菱形继承,看d的内存空间
	D d;
	d.B::_a = 1;
	d.C::_a = 2;
	d._b = 3;
	d._c = 4;
	d._d = 5;
}

在这里插入图片描述
在这里插入图片描述
我们发现在d的存储中的确是只有一个_a了,然后多了两个指针,一个是黄色曲线的,一个是蓝色曲线的。
黄色曲线的指针指向了一个00,后面紧跟着一个数字20,这个20代表在d内存中_b到_a之间的偏移量,蓝色同理,代表的是在d内存中_c到_a之间的偏移量。
B区域的开始,0x63C+0x14=0x650,C区域的开始0x644+0x0C=0x650
其中,_a我们称之为虚基类,一般放在最下面,用偏移量进行访问,主要用于切片时候。

菱形继承中的指针:

void test11()
{
	//这是在使用虚拟继承情况下的菱形继承,看d的内存空间
	D d;
	d.B::_a = 1;
	d.C::_a = 2;
	d._b = 3;
	d._c = 4;
	d._d = 5;

	B* pb = &d;
	C* pc = &d;
	cout << pb << endl;//00AFF658
	cout << pc << endl;//00AFF660
}

在这里插入图片描述

需要虚继承的话,那在什么地方加上关键字virtual呢?

10.4在继承公共基类的时候使用虚继承

在这里插入图片描述
在这种情况下,也属于菱形继承,在使用虚拟继承的时候,应该把关键字virtual加到Student、Teacher类上。因为他俩有公共的基类。
在这里插入图片描述

10.5腰部父类也使用虚继承模型

之所以这样,是因为方便指针进行访问。使用了菱形虚拟继承之后,定义一个中间父类的指针,我们发现既可以是子类做切片进行访问,又可以是访问它本身,为了统一处理,CPP干脆把中间的父类模型结构也换成了与子类大体一致的模型。

10.6菱形继承的实例

菱形继承在库中有一个案例,就是iostream,这个地方用到的就是菱形虚拟继承的方式进行处理的。
在这里插入图片描述

11.组合与继承

与继承相似的一种代码复用方式叫做组合。
组合的概念:一个类把另一个类作为他的成员变量。类似于一种包含关系。

class A
{
public:
	int _a;
};

class B
{
public:
	A _aa;//组合
	int _b;
};

void test13()
{
	B b;
}

在这里插入图片描述

在这里插入图片描述



好的,如果本篇文章对你有帮助,不妨点个赞~谢谢。
在这里插入图片描述


EOF

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

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

相关文章

iOS更新后在IPhone上恢复丢失的文本消息的4种方法

您是否在更新 iPhone 软件后丢失了重要的短信&#xff1f;丢失数据可能会令人沮丧&#xff0c;尤其是当它包含有价值的信息或感性信息时。幸运的是&#xff0c;有一些方法可以在iOS更新后恢复iPhone上丢失的短信。 在这篇博文中&#xff0c;我们将讨论可用于恢复丢失的短信的不…

【19. 删除链表的倒数第 N 个结点 中等】

题目&#xff1a; 给你一个链表&#xff0c;删除链表的倒数第 n 个结点&#xff0c;并且返回链表的头结点。 示例 1&#xff1a; 输入&#xff1a;head [1,2,3,4,5], n 2 输出&#xff1a;[1,2,3,5] 示例 2&#xff1a; 输入&#xff1a;head [1], n 1 输出&#xff1a;…

Vue学习 Day03 (hooks和路由)

自定义hooks 准备工作&#xff1a;首先导入axios的依赖 案例 首先写一个子组件&#xff0c;如下 <template> <h2>{{ sum }}</h2> <button click"addSum">点我sum1</button> <hr><img v-for"(dog,index) in dogs&qu…

Bugku-web-你必须让他停下来

打开环境&#xff0c;先分析出现的提示是让这个页面停下来 我们尝试关闭JS&#xff0c;发现确实停下来了-----》F12查看页面源代码但是flag还是没有出现&#xff0c;----》尝试刷新jpg随着刷新而变化&#xff0c;当刷新到10.jpg时flag出现 最后我尝试不关js刷新打开源代码刷新也…

展厅中控需要配套些什么设备

展厅中控系统需要配套的设备主要包括以下几个方面&#xff0c;以确保展厅内各种设备的高效管理和控制&#xff1a; 一、核心控制设备 中控主机&#xff1a; 功能&#xff1a;中控主机是展厅中控系统的核心&#xff0c;能够全面管理和控制展厅内的所有声光电设备。重要性&…

Jetpack 各种框架简介

Jetpack是Google推出的一套为Android开发提供极大便利的组件、工具和指导集&#xff0c;旨在帮助开发者快速构建高质量的应用&#xff0c;并遵循最佳实践。 Jetpack不仅是一个提高开发效率的工具集&#xff0c;还是Android开发的未来方向。它通过整合各种组件和工具&#xff0…

Easy SSSP(sssp)

目录 题目描述 输入 输出 样例输入 样例输出 提示 代码 今天的特邀网站&#xff1a; 和 题目描述 输入数据给出一个有 N 个节点&#xff0c;M 条边的带权有向图。要求你写一个程序&#xff0c;判断这个有向图中是否存在负权回路。如果从一个点沿着某条路径出发&#xff0c;又回…

【文献阅读】COUNTGD 模型结构

提出什么模块 解决什么问题 图、贡献&#xff0c;模型架构 图1 图1&#xff1a;COUNTGD能够同时使用视觉示例和文本提示来产生高度准确的对象计数&#xff08;a&#xff09;&#xff0c;但也无缝支持仅使用文本查询或仅使用视觉示例进行计数&#xff08;b&#xff09;。多模态视…

Jenkins入门以及安装

本文主要讲解&#xff0c;什么是Jenkins&#xff0c;Jenkins在Linux上如何安装。因为最近在公司&#xff0c;需要安装一套测试环境&#xff0c;便想着写了此篇文章。 什么是Jenkins Jenkins是一个开源的自动化部署工具&#xff0c;它能够持续地自动化构建、测试和部署软件项目…

尚品汇-前端(三十三)

目录&#xff1a; &#xff08;1&#xff09;面包屑处理平台属性 &#xff08;2&#xff09;排序处理 &#xff08;2&#xff09;单点登录业务介绍 &#xff08;1&#xff09;面包屑处理平台属性 前端显示&#xff1a;面包屑显示效果 搜list搜索方法继续添加返回的平台属性…

零基础5分钟上手亚马逊云科技核心云架构知识 - 权限管理最佳实践

简介&#xff1a; 欢迎来到小李哥全新亚马逊云科技AWS云计算知识学习系列&#xff0c;适用于任何无云计算或者亚马逊云科技技术背景的开发者&#xff0c;通过这篇文章大家零基础5分钟就能完全学会亚马逊云科技一个经典的服务开发架构方案。 我会每天介绍一个基于亚马逊云科技…

信息学奥赛初赛天天练-67-NOIP2018普及组-完善程序2-计数排序、排列、链表、单向链表、双向链表

PDF文档公众号回复关键字:20240817 1 完善程序 (单选题 &#xff0c;每小题3分&#xff0c;共30分) 最大公约数之和 对于一个 1到 n的排列 P&#xff08;即 1 到 n 中每一个数在 P中出现了恰好一次&#xff09;&#xff0c;令 q[i] 为第 i个位置之后第一个比 P[i] 值更大的位…

【数学建模】趣味数模问题-状态转移模型的应用

概述 状态转移模型结合图论&#xff0c;通过经典的智力问题展示了其在求解实际问题中的巧妙应用。虽然这些问题不需要复杂的数学知识&#xff0c;但通过建立数学模型&#xff0c;可以将其转化为标准的图论模型来解决。 问题1&#xff1a;人、狼、羊、菜渡河问题 问题描述&…

CUDA Out of Memory :CUDA内存不足的完美解决方法

CUDA Out of Memory &#x1f6d1;&#xff1a;CUDA内存不足的完美解决方法 CUDA Out of Memory &#x1f6d1;&#xff1a;CUDA内存不足的完美解决方法摘要 &#x1f4dd;引言 &#x1f31f; 什么是 CUDA Out of Memory 错误&#xff1f; &#x1f914;基本定义常见场景 常见的…

基于spring boot的小型诊疗预约平台的设计与开发

TOC springboot262基于spring boot的小型诊疗预约平台的设计与开发 绪论 1.1 研究背景 当前社会各行业领域竞争压力非常大&#xff0c;随着当前时代的信息化&#xff0c;科学化发展&#xff0c;让社会各行业领域都争相使用新的信息技术&#xff0c;对行业内的各种相关数据进…

canal数据同步工具介绍与应用

canal服务 canal介绍canal版本与环境canal 服务集canal应用场景&#xff1a; canal常见问题xml配置问题连接认证问题jar版本问题连接问题 canal介绍 ‌1、Canal是‌阿里巴巴开源的‌MySQL增量数据订阅和消费工具&#xff0c;通过模拟MySQL的‌slave与‌master交互&#xff0c;捕…

XSS- DOMclobbering与svg深度利用

目录 源码展示 解法一&#xff1a;绕过过滤-DOM clobbering 什么是DOM clobbering DOM clobbering原理 全局变量自动创建 属性名冲突 影响脚本执行 逐过程分析 源码展示 <script>const data decodeURIComponent(location.hash.substr(1));;const root documen…

深度学习------------池化层

目录 池化层二维最大池化填充、步幅和多个通道平均池化层 总结池化层的代码部分实现池化层的正向传播验证二维最大池化层的输出该部分总代码 验证平均池化层该部分总代码 填充和步幅深度学习框架中的步幅与池化窗口的大小相同该部分总代码 填充和步幅可以手动设定该部分总代码 …

adb查看当前运行的应用的包名和Activity(模拟器也可以)

adb查看当前运行的应用的包名和Activity(模拟器也可以) 在adb 中&#xff0c;输入命令&#xff1a;adb shell 进入adb模式 adb shell dumpsys window w |findstr / |findstr name 输入完成后会显示当前运行的应用的包名和Activity(模拟器也可以) 例如抖音 ** **

布隆过滤器--极致的速度

前言 上一篇博客提到了位图&#xff0c;位图是十分高效的数据结构&#xff0c;但可惜的是只支持整型&#xff0c;今天这篇博客的主角是布隆过滤器&#xff0c;他与位图有异曲同工之妙。&#xff08;不了解位图可以点击下面这篇博客快速了解&#xff09;位图&#xff08;bitse…