【C++之进阶提升】两万字深剖面向对象三大特性之一:继承

前言

在实现类的过程中我们发现很多相近的类是具有一些共同的属性的那么如果我们不讲究任何技巧那么结果就是在每一个类中都实现了同样的代码比如人有的一些属性姓名性别年龄电话号码等学生和老师和其他的身份也具有相同这些属性那么我们是否能够采取一些方法复用这些代码从而减少代码的冗余呢这就是继承需要回答的问题了。

一、继承的概念及定义

  1. 继承的概念
    继承(inheritance)机制面向对象程序设计使代码可以复用的最重要的手段它允许程序员在保持原有类特性的基础上进行扩展加功能产生新的类派生类原有的类称基类。继承呈现了面向对象程序设计的层次结构体现了由简单到复杂的认知过程。以前我们接触的复用都是函数复用继承是类设计层次的复用
  • 代码理解继承
class Person
{
public:
	void Print()
	{
		cout << "name:" << _name << endl;
		cout << "age:" << _age << endl;
	}
protected:
	string _name = "peter"; // 姓名
	int _age = 18; // 年龄
};

class Student : public Person
{
protected:
	int _stuid; // 学号
};
class Teacher : public Person
{
protected:
	int _jobid; // 工号
};
int main()
{
	Student s;
	Teacher t;
	s.Print();
	t.Print();
	return 0;
}
  • 调试
    在这里插入图片描述
    运行结果
    在这里插入图片描述

分析上面这个代码中分别是Student类和Teacher类去继承了Person类Student类和独有的成员是int _stuid;Teacher类中独有的成员是int _jobid;继承之后在Student类和Teacher类中都会再包含一份Person类中的成员包括void Print()string _name = "peter";int _age = 18;其中Person类就称为父类或者基类Student类和Teacher类就称为子类或者派生类。

  1. 继承的定义
    上面的代码中有两份继承的定义如Student类继承Person类和Teacher类继承Person类。显然我们可以知道继承的定义为class 子类名:继承方式 父类名class Student:public Person,或者class Teacher:public Person,Person类就称为父类或者基类Student类和Teacher类就称为子类或者派生类
  2. 继承方式
    在C++的继承体系继承方式有三种公有继承public保护继承(proteceted)私有继承(private)类中的访问限定符也有三种公有(public),保护(protected),私有(private),其中继承方式主要是会和父类中的成员的访问限定进行结合从而决定父类中的成员在子类中的权限
    在这里插入图片描述
    主要的结合就有九种
    在这里插入图片描述
    其中比较常见的就是子类公有继承父类父类中的成员的访问限定符主要是public和protected这种情况下父类中public访问的成员在子类中仍然是public的父类中的protected成员在子类中就是protected。
    其他的继承方式的记忆方法如果继承方式是私有继承也就是子类私有继承父类那么不管父类中的成员是什么访问限定符修饰父类中的成员在子类中都是不可见的如果继承方式是保护继承如果父类中的成员是public访问,那么到子类中就是保护的如果父类中的成员是保护的到子类中也是保护的如果父类中的成员是私有的那么到子类就是私有的。
    其实总结一点就是取继承方式和父类中成员的访问限定权限的最小值。其中publicprotectedprivate三者的权限大小为public>protected>private
    一些特殊的小细节
  • 使用关键字class时默认的继承方式是private使用struct时默认的继承方式是public不过最好显示的写出继承方式

  • 在实际运用中一般使用都是public继承几乎很少使用protetced/private继承也不提倡使用protetced/private继承因为protetced/private继承下来的成员都只能在派生类的类里面使用实际中
    扩展维护性不强

  • 代码1公有继承在访问父类中的public成员和protected成员

class Person
{
public:
	void Print()
	{
		cout << _name << endl;
	}
protected:
	string _name; // 姓名
private:
	int _age = 18; // 年龄
};

class Student : public Person
{
public:
	void Func()
	{
		cout << _name << endl;
		cout << _stunum << endl;
	}
protected:
	int _stunum = 01; // 学号
};

int main()
{
	Student s1;
	s1.Func();
	return 0;
}
  • 编译结果
    在这里插入图片描述
  • 运行结果
    在这里插入图片描述
    分析通过上面的代码可以看出子类公有继承父类时在子类中可以正常访问父类的public成员和protected成员。
  • 代码2公有继承+子类中访问父类中的private成员
class Person
{
public:
	void Print()
	{
		cout << _name << endl;
	}
protected:
	string _name; // 姓名
private:
	int _age = 18; // 年龄
};

class Student : public Person
{
public:
	void Func()
	{
		cout << _age << endl;
	}
protected:
	int _stunum = 01; // 学号
};

int main()
{

	Student s1;
	s1.Func();
	return 0;
}

编译结果
在这里插入图片描述
分析父类中的私有成员在子类中是不可见的不可见的意思就是在子类中和除了父类之外的地方都是不能访问的。

  • 代码2子类保护继承父类在子类中访问父类的public成员和protected成员
class Person
{
public:
	void Print()
	{
		cout << _name << endl;
	}
	int _p = 1;
protected:
	string _name; // 姓名
private:
	int _age = 18; // 年龄
};

class Student : protected Person
{
public:
	void Func()
	{
		cout << _p << endl;
		cout << _name << endl;
	}
protected:
	int _stunum = 01; // 学号
};

int main()
{

	Student s1;
	s1.Func();
	return 0;
}

运行结果
在这里插入图片描述
分析当子类保护继承父类的时候父类中的public成员在子类中会变成proteced成员父类中的protected成员在子类中也是protected成员子类中的保护成员的意思就是在子类中可以访问在子类外不能访问。

  • 代码4子类保护继承父类+在子类中访问父类的private成员
class Person
{
public:
	void Print()
	{
		cout << _name << endl;
	}
	int _p = 1;
protected:
	string _name; // 姓名
private:
	int _age = 18; // 年龄
};

class Student : protected Person
{
public:
	void Func()
	{
		cout << _age << endl;
	}
protected:
	int _stunum = 01; // 学号
};

int main()
{

	Student s1;
	s1.Func();
	return 0;
}

编译结果
在这里插入图片描述
分析当子类保护继承父类时i父类中的private成员在子类是不可见的所以不能在子类中访问父类的私有成员
同样的方法可以证明当子类私有继承父类时那么父类中的所有成员在子类中都是不可见的也就是无法在子类中访问父类发任何成员。

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

当一个类继承另一个类时主动继承的类称为子类被继承的类称为父类C++规定可以将子类的对象赋值给父类的对象或者指针可以将子类的对象的地址赋值给父类对象的地址。派生类对象
可以赋值给 基类的对象 / 基类的指针 /
基类的引用
。这里有个形象的说法叫切片或者切割。寓意把派生类中父类那部分切来赋值过去。注意基类对象不能赋值给派生类对象。想要理解这个内容我们一定要知道子类继承父类之后子类中的结构。
大概如下

在这里插入图片描述

  • 代码1子类对象赋值给父类对象
class Person
{
protected:
	string _name;
	int _age;
};

class Student :public Person
{
public:
	void SetPerson(const char* str = "张三")
	{
		string s(str);
		_name = s;
		_age = 18;

	}
private:
	int _num;
};

int main()
{
	Person p;
	Student s;
	s.SetPerson();
	p = s;
	return 0;
}

调试
子类对象赋值给父类对象前
在这里插入图片描述
子类对象赋值给父类对象后
在这里插入图片描述

分析从上面的现象可以看出在子类对象赋值给父类对象之前因为父类对象对应的类没有自己实现构造函数所以编译器会自动生成一个默认构造函数这个默认会去调用这个类中string成员的默认构造函数完成对其中的_name成员的初始化对于_age成员因为_age成员是内置类型所以不会初始化是随机值。我们在调用子类中的SetPerson函数之后对子类对象中继承自父类对象的成员进行了初始化此时我们将子类对象赋值给父类对象观察到的结果父类对象赋值了子类对象中继承自父类的成员内容。这种情况下子类对象和父类对象是两个不同的对象。

  • 代码2子类对象赋值给父类的引用
// 子类对象赋值给父类的引用
class Person
{
protected:
	string _name;
	int _age;
};

class Student :public Person
{
public:
	void SetPerson(const char* str = "张三")
	{
		string s(str);
		_name = s;
		_age = 18;

	}
private:
	int _num;
};

int main()
{
	Student s;
	s.SetPerson();

	// 将子类对象赋值给父类对象的引用
	Person& rp = s;
	return 0;
}

调试结果
在这里插入图片描述
分析通过调试结果我们可以看到当我们将子类对象赋值给父类对象的引用之后父类对象的引用中的内容和子类对象中继承自父类的成员是一样的并且我们通过观察子类对象的地址和父类对象的引用的地址也是一样的。原因是当将子类对象赋值给父类对象的引用之后其实父类对象就是子类对象中继承自父类的那一部分成员的别名。

  • 代码3子类对象的地址赋值给父类对象的指针
// 子类对象的地址赋值给父类对象的指针
class Person
{
protected:
	string _name;
	int _age;
};

class Student :public Person
{
public:
	void SetPerson(const char* str = "张三")
	{
		string s(str);
		_name = s;
		_age = 18;

	}
private:
	int _num;
};

int main()
{
	Student s;
	s.SetPerson();

	Person* ptrp = &s;

	return 0;
}

调试结果
在这里插入图片描述
分析通过上面的调试结果我们可以看到当将子类对象的地址赋值给父类指针时其实是赋值指针指向了子类中继承自父类的那一部分成员此时这个父类指针能够看到的内容就是子类对象继承自父类的那一部分成员。

三、继承中的作用域

继承中的作用域和前面学习的作用域是一样的也就是说子类继承自父类子类和父类是属于两个不同的作用域那么既然是在两个不同的作用域就允许在子类和父类中存在相同名的函数当子类和父类出现相同名字的成员的时候子类成员会对父类同名成员进行隐藏或者叫重定义

  • 代码1子类中存在和父类同名的成员变量
// 当子类和存在和父类同名的成员变量
class Parent
{
protected:
	int _a = 1;
};

class Child :public Parent
{
public:
	void Print()
	{
		cout << _a << endl;
	}
private:
	int _a = 2;
};

int main()
{
	Child c;
	c.Print();

	return 0;
}

运行结果
在这里插入图片描述

分析当子类中存在和父类中同名的成员变量时子类中的同名成员变量会对父类中同名成员变量进行隐藏此时在子类中访问该名字的成员变量时默认访问的就是子类中的成员变量。

  • 代码2子类中存在和父类同名的成员变量+指定父类域访问父类中的同名成员变量
// 当子类和存在和父类同名的成员变量+指定父类域访问父类中的同名成员变量
class Parent
{
protected:
	int _a = 1;
};

class Child :public Parent
{
public:
	void Print()
	{
		cout << _a << endl;
		cout << Parent::_a << endl;
	}
private:
	int _a = 2;
};

int main()
{
	Child c;
	c.Print();

	return 0;
}

运行结果
在这里插入图片描述

分析当出现子类成员变量和父类成员变量同名时如果不加任何处理访问该同名成员变量时默认访问的是子类的成员变量。如果想要访问父类中的同名成员变量则需要指明父类的类域告诉编译器此时我们想要访问的是父类中同名的成员变量。

  • 代码3子类中存在和父类同名的成员变量
class Parent
{
public:
	void Func()
	{
		cout << "Parent::Func()" << endl;
	}
};

class Child :public Parent
{
public:
	void Func()
	{
		cout << "Child::Func()" << endl;
	}
private:
	int _a = 2;
};

int main()
{
	Child c;
	c.Func();

	return 0;
}

运行结果
在这里插入图片描述
分析
和同名成员变量类似如果不加任何处理默认访问的就是子类中的成员变量

  • 代码4子类中存在和父类同名的成员变量+调用父类中的同名成员函数
class Parent
{
public:
	void Func()
	{
		cout << "Parent::Func()" << endl;
	}
};

class Child :public Parent
{
public:
	void Func()
	{
		cout << "Child::Func()" << endl;
	}
private:
	int _a = 2;
};

int main()
{
	Child c;
	c.Func();
	c.Parent::Func();

	return 0;
}

运行结果
在这里插入图片描述
分析和成员变量类似如果此时想要访问父类中的同名成员函数则此时需要指定父类的类域告诉编译器我们此时想要访问的是父类中同名的成员函数。

  • 练习代码
    父子类
class A
{
public:
	void fun()
	{
		cout << "func()" << endl;
	}
};
class B : public A
{
public:
	void fun(int i)
	{
		A::fun();
		cout << "func(int i)->" << i << endl;
	}
};

调用代码1

void Test()
{
	B b;
	b.fun();
};

int main()
{
	Test();
	return 0;
}

编译结果
在这里插入图片描述
分析
当子类和父类存在同名的成员函数时在调用处如果我们只是使用子类对象+函数名取去调用函数则默认调用的是子类中的函数所以上面代码中调用的是子类中的函数因为子类的函数是需要传参的调用处没有传参所以报错。

调用代码2

void Test()
{
	B b;
	b.fun(10);
};

int main()
{
	Test();
	return 0;
}

运行结果
在这里插入图片描述
分析
当子类和父类存在同名的成员函数时在调用处如果我们只是使用子类对象+函数名取去调用函数则默认调用的是子类中的函数所以上面代码中调用的是子类中的函数因为子类的函数是需要传参的调用处正常传参所以调用成功执行子类的函数逻辑。

四、继承与友元

继承和友元中我们需要一个道理友元关系是不能继承的。比如假如有两个类A类和B类还有一个Func函数其中B类继承A类Func函数是A类的友元函数则不能退出Func函数是B类的友元函数。具体代码如下

  • 代码1友元函数关系不能继承
class B;
class A
{
	friend void Func(const A& a, const B& b);
protected:
	int _a = 1;
};

class B :public A
{
private:
	int _b = 2;
};

void Func(const A& a, const B& b)
{
	cout << a._a << endl;
	cout << b._b << endl;
}

int main()
{
	A a;
	B b;
	Func(a, b);
	return 0;
}

编译结果
在这里插入图片描述

  • 代码2将该函数也设置称B类的友元函数
class B;
class A
{
	friend void Func(const A& a, const B& b);
protected:
	int _a = 1;
};

class B :public A
{
	friend void Func(const A& a, const B& b);
private:
	int _b = 2;
};

void Func(const A& a, const B& b)
{
	cout << a._a << endl;
	cout << b._b << endl;
}

int main()
{
	A a;
	B b;
	Func(a, b);
	return 0;
}

运行结果
在这里插入图片描述

五、继承与静态成员

基类定义了static静态成员则整个继承体系里面只有一个这样的成员。无论派生出多少个子类都只有一个static成员实例

  • 代码1静态成员变量
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;
	cout << " 地址 :" << &Person::_count << endl;

	// 通过派生类访问静态成员
	cout << " 人数 :" << Student::_count << endl;
	cout << " 地址 :" << &Student::_count << endl;

	cout << " 人数 :" << Graduate::_count << endl;
	cout << " 地址 :" << &Graduate::_count << endl;


	// 通过派生类对象访问静态成员
	cout << " 人数 :" << s1._count << endl;
	cout << " 地址 :" << &s1._count << endl;

}

int main()
{
	TestPerson();

	return 0;
}

运行结果
在这里插入图片描述

分析通过上面的代码我们在基类中设置了一个静态成员变量然后在该静态成员变量经过一系列的变化之后我们可以通过基类派生类和派生类的对象去访问该静态成员变量访问的结果是一样的并且通过结果我们可以看到通过不同类型的访问地址是一样的可以得出以上访问的是同一个变量。

  • 代码2静态成员函数
class Person
{
public:
	Person()
	{
		++_count;
	}
	static int GetCount()
	{
		return 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 << Student::GetCount() << endl;
	cout << Graduate::GetCount() << endl;
	cout << s1.GetCount() << endl;
	cout << s4.GetCount() << endl;

}

int main()
{
	TestPerson();
	return 0;
}

运行结果
在这里插入图片描述
分析通过上面运行结果我们可以看到我们在基类中定义了一个静态成员函数最终通过派生类和派生类对象去调用的时候发现结果是一样的说明调用的是同一个函数。

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

基类代码

class Person
{
public:
	// 默认构造函数
	Person(const char* name = "peter")
		: _name(name)
	{
		cout << "Person()" << 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; // 姓名
};
  1. 构造函数派生类的构造函数必须调用基类的构造函数初始化基类的那一部分成员如果基类没有默认的构造函数则必须在派生类构造函数的初始化列表阶段显示调用
Student(const char* name,int age)
		:Person(name)// 调用基类的构造函数完成子类中父类成员的初始化
		,_age(age)
	{
		cout << "Student(const char* name,int age)" << endl;
	}
  1. 析构函数派生类的析构函数会在被调用完成后自动调用基类的析构函数清理基类成员。因为这样才能保证派生类对象先清理派生类成员再清理基类成员的顺序
// 析构函数
	~Student()
	{
		// 因为子类析构函数调用完成之后会自动调用基类的析构函数所以不需要显示调用基类的析构函数
		cout << "~Student()" << endl;
	}

测试构造函数和析构函数子类和基类构造函数和析构函数的调用顺序

  • 代码
int main()
{
	Student s1("张三",18);
	return 0;
}

运行结果
在这里插入图片描述

分析从上面的代码及运行结果可以看出创建子类对象的时候会先调用子类的构造函数调用子类构造的时候会先在初始化列表调用父类的构造函数完成父类成员的初始化再调用子类的构造函数完成子类成员的初始化。当对象的生命周期到的时候调用子类对象的析构函数在调用子类析构函数的时候会先调用子类析构函数完成子类成员的释放再自动调用父类的析构函数完成父类成员的释放。

  1. 拷贝构造函数派生类的拷贝构造函数必须调用基类的拷贝构造完成基类的拷贝初始化
// 拷贝构造函数
	Student(const Student& s)
		:Person(s)// 调用父类的拷贝构造函数完成父类成员的拷贝
		,_age(s._age)
	{
		cout << "Student(const Student& s)" << endl;
	}

测试拷贝构造函数

  • 代码
int main()
{
	Student s1("张三",18);
	Student s2(s1);
	return 0;
}

调试结果
在这里插入图片描述
运行结果
在这里插入图片描述
分析在调用子类拷贝构造函数时会先调用父类的拷贝构造函数完成对子类中的基类成员的拷贝再调用子类自身的拷贝构造函数完成子类成员的拷贝。
4. 赋值运算符重载函数派生类的operator=必须要调用基类的operator=完成基类的复制

  • 代码
// 赋值运算符重载函数
	Student& operator=(const Student& s)
	{
		if (this != &s)
		{
			Person::operator=(s);
			_age = s._age;
			cout << "Student& operator=(const Student& s)" << endl;
		}
		return *this;
	}

测试代码

int main()
{
	Student s1("张三",18);
	Student s2(s1);

	Student s3("lisi", 23);
	s3 = s1;

	return 0;
}

调试结果
赋值前
在这里插入图片描述
赋值后
在这里插入图片描述
运行结果
在这里插入图片描述

分析在调用子类的赋值运算符重载函数时会先调用基类的赋值运算符重载函数对基类成员进行赋值再调用子类自身的赋值运算符重载函数完成子类对象的赋值在实现运算符重载函数的时候显示调用基类的赋值运算符重载函数时需要注意基类的运算符重载函数和子类的运算符重载函数的名字是相同的所以父子类的运算符重载函数是构成隐藏关系的所以再调用基类的运算符重载函数时一定要显示指定基类的类域否则会导致调用子类的运算符重载函数而导致无穷调用最终导致栈溢出。

七、复杂的菱形继承及菱形虚拟继承

  1. 单继承一个子类只有一个直接父类时称这个继承关系为单继承
    在这里插入图片描述
  2. 多继承一个子类有两个或以上直接父类时称这个继承关系为多继承比如下图中的Assistant类就继承了两个类所以有两个父类属于多继承。
    在这里插入图片描述
  3. 菱形继承菱形继承是多继承的一种特殊情况。多继承可能就会导致菱形继承比如有四个类分别为A,B,C,D其中A是原始基类B和C都继承了A那么根据我们之前对继承的理解A和B中都应该各自包含一份A类的成员此时如果D类继承了B类和C类那么对于D类而言D中就包含了A类的两份成员那么A类的成员在D类中就出现数据冗余和二义性
    在这里插入图片描述
    菱形继承的问题菱形继承有数据冗余和二义性的问题。
  • 代码1
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; // 主修课程
};
int main()
{
	// 这样会有二义性无法明确知道访问的是哪一个
	Assistant a;
	a._name = "peter";

	return 0;
}

编译结果
在这里插入图片描述
分析原始基类是Person类其中Student继承了Person类那么Student中就包含Person类中的成员即姓名接下来Teacher继承了Person类所以Teacher类中也会包含Person类中的成员即姓名接下来Assistant类继承了Student和Teacher类所以Assistant中会包含Student类和Person类根据前面的继承显然可以知道Assistant中会包含两份Person的成员一份是继承Student来的一份是继承Teacher来的所以会出现数据的冗余和二义性就是当我们像访问Assistant中的Person的成员的时候如果不显示指定是访问Assistant中的Student还是Teacher的那么就会出现二义性编译器此时不知道要访问哪一份的。

  • 解决代码1显示指定访问的数据是哪一份
int main()
{
	Assistant a;
	a.Student::_name = "zhangsan";
	a.Teacher::_name = "lisi";

	return 0;
}

调试过程
在这里插入图片描述

  • 解决代码2使用虚继承virtual
// 采用虚继承来解决问题
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; // 主修课程
};

int main()
{
	Assistant a;
	a._name = "zhangsan";

	return 0;
}
  • 调试过程
    在这里插入图片描述
    分析通过上面的调试中在监视窗口中看到的仍然在Assistant类中存在两份Person类成员这是因为编译器对监视窗口做过优化实际上我们是需要通过内存窗口进行观察。但是上面这个例子不方便通过内存窗口进行观察下面我们会再举一个例子通过内存窗口来学习菱形虚拟继承的实现原理。

八、虚拟继承解决数据冗余和二义性的原理

  1. 原始多继承模型
  • 代码
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;
}

调试通过内存窗口
在这里插入图片描述
分析通过内存窗口我们可以看出在D实例化出的对象的模型中首先是B类型的成员然后是C类型的成员最后是D本身的成员B和C的先后顺序是由继承的先后顺序来决定的。显然上面的模型中D类型中存在了两份A类型的数据这样会造成数据的冗余和二义性同时存在内存的浪费。
2. 虚继承模型

  • 代码
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;
}

调试通过内存窗口进行观察
在这里插入图片描述

分析从上面的代码来看菱形虚拟继承中A类型的成员并不是放在B类型和C类型成员中的而是独立放在另一个公共的区域然后B类型的成员和C类型的成员有数单独放在D类型中的某一个地方同时还会存放两个偏移量的指针这两个偏移量分别是B类型的成员距离A类型成员的距离和C类型成员距离A类型成员的距离。所以实际在查找A的过程中可以通过B或者C去查找如果通过B类型成员去查找那么就要先找到存放B类型偏移量的指针然后通过这个指针去找到一个虚基表从而找到B类型成员到A类型成员的偏移量。如果通过C类型成员去找那么就需要先找到C类型偏移量的指针然后通过这个指针找到对应的虚基表进而找到C类型成员距离A类型成员的偏移量进而就可以通过偏移量找到A类型成员了。

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

九、继承的总结和反思

  1. C++语法的缺陷其实多继承就是一个体现。有了多继承就存在菱形继承有了菱形继承就有菱形虚拟继承底层实现就很复杂。所以一般不建议设计出多继承一定不要设计出菱形继承。否则在
    复杂度及性能上都有问题
  2. 继承和组合

十、常见笔试面试题

  1. 什么是菱形继承菱形继承的问题是什么
    菱形继承本质是一种多继承是指由多个类继承同一个基类然后这多个类又被同一个类继承就会形成菱形继承。菱形继承的问题原始的基类中的成岩数据会被后面继承的某一些子类继承多次从而出现原始基类的数据在这些子类中存在多份数据从而出现数据的冗余同时在访问这些子类中的原始基类成员的时候由于数据存在多份如果不显示指定访问的是哪一份就会出现数据的二义性导致编译器不知道访问哪一份数据同时也会存在空间的浪费。
  2. 什么是菱形虚拟继承如何解决数据冗余和二义性的
    在这里假设可能存在数据冗余和二义性的基类称为原始基类。
    菱形虚拟继承是指在继承其中一个可能存在数据冗余的基类时不再采用原始的继承方式而是通过一个关键字virtual来实现虚继承。这样做的效果该基类的成员数据不再在后面的子类中存在多份也不会在每一个子类中单独存在一份而是会在子类中的一个公共的区域存放然后该子类的其他有继承这个原始基类的基类的成员单独存放在这个子类的一个区域同时存放这些基类相对于原始基类成员的偏移量的地址编译器就可以通过这个偏移量的地址找到一个存放偏移量的虚基表从而就可以在虚基表上找到这些基类到原始基类成员的偏移量从而就可以找到原始基类成员相对于继承之的子类成员的位置。
  3. 继承和组合的区别什么时候用继承什么时候用组合
阿里云国内75折 回扣 微信号:monov8
阿里云国际,腾讯云国际,低至75折。AWS 93折 免费开户实名账号 代冲值 优惠多多 微信号:monov8 飞机:@monov6
标签: c++

“【C++之进阶提升】两万字深剖面向对象三大特性之一:继承” 的相关文章