【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访问,那么到子类中就是保护的,如果父类中的成员是保护的,到子类中也是保护的,如果父类中的成员是私有的,那么到子类就是私有的。
    其实总结一点就是:取继承方式和父类中成员的访问限定权限的最小值。其中public,protected,private三者的权限大小为: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. 继承和组合的区别?什么时候用继承?什么时候用组合?

  4. 下面关于访问权限与继承权限说法不正确的是( )
    A.访问权限和继承权限是不同的概念
    B.访问权限和继承权限关键字上是一样的,但是出现位置不一样
    C.如果是protected继承方式,基类public的成员变量能通过基类对象在类外直接访问
    D.基类私有的成员变量在子类中都不能直接访问,因为没有被子类继承了

分析:
A: 访问权限是限定类中的成员的权限,继承权限是限定父类成员在子类的权限
B:访问权限和继承权限的关键字都是public,proteceted,private,存在的位置:访问权限存在于类中,继承权限是存在于子类继承父类的时候。
C:基类的public成员在通过类外怎么访问跟继承权限没有任何关系,直接可以通过基类对象在类外进行访问。
D:基类的私有成员在子类中不能被访问,但是基类的私有对象是有被子类继承的 故本题选C

  1. 下面关于继承说法不正确的是( )
    A.继承可以使用现有类的所有功能,并在无需重新编写原来类的情况下对这些功能进行扩展
    B.继承体系中子类必须要体现出与基类的不同
    C.子类对象一定比基类对象大
    D.继承呈现了面相对象程序设计的层次结构,体现了有简单到复杂的认知过程

分析: C:当子类对象没有声明自己的成员变量时,子类对象和父类对象是一样大的 故本题选C

  1. 下面代码输出结果:( )
class A

{

public:

  void f(){ cout<<"A::f()"<<endl; }

  int a;   

};



class B : public A

{

public:

  void f(int a){cout<<"B::f()"<<endl;}

  int a;

};



int main()

{

  B b;

  b.f();

  return 0;

}

分析:
本题考察的是继承体系中父子类成员函数的隐藏关系,首先由子类对象去调用成员,那么默认调用的就是子类自身的成员函数,当子类的成员函数和父类的成员函数构成同名的时候,此时它们构成隐藏关系,此题中,子类的成员函数是有参数的,所以调用的时候必须传实参,否则调用失败。

  1. 下面说法正确的是( )
    A.派生类构造函数初始化列表的位置必须显式调用基类的构造函数,已完成基类部分成员的初始化
    B.派生类构造函数先初始化子类成员,再初始化基类成员
    C.派生类析构函数不会自动析构基类部分成员
    D.子类构造函数的定义有时需要参考基类构造函数

分析:
A:当基类存在默认构造函数时,子类的构造函数初始化列表不需要显示调用基类的构造函数
B:派生类的构造函数对于子类对象的初始化原则:先调用父类构造完成父类成员的初始化,再调用子类构造函数完成子类成员的初始化。
C:派生类的析构函数为了保证先子后父的析构原则,会在调用子类析构函数之后,自动调用父类的析构函数
D:

  1. 关于基类与派生类对象模型说法正确的是()
    A.基类对象中包含了所有基类的成员变量
    B.子类对象中不仅包含了所有基类成员变量,也包含了所有子类成员变量
    C.子类对象中没有包含基类的私有成员
    D.基类的静态成员可以不包含在子类对象中
    E.以上说法都不对

分析: A:基类对象中不包含基类中的静态成员变量 B:与A类似
C:子类对象中虽然无法使用基类的私有成员,但是子类仍然继承了父类的私有成员,所以子类对象中是存在父类的私有成员的
D:基类静态成员变量一定不包含在子类的对象中

  • 17
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 9
    评论
评论 9
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值