详解C++继承(普通继承,菱形继承与虚拟继承)

继承的概念

假如现在我们有三个类,person类,student类和teacher类,很明显,这三个类分别描述,人,学生和老师。每个人有自己的名字,性别,身高等属性。学生有学号的属性,老师有工号的属性。那我们在定义学生与老师这两个类得时候还要把人的属性添加进来,要是再写一遍相同的代码肯定会造成冗余,所以C++有了继承的概念。

继承的具体操作

先来看一个例子

#include <iostream>
using namespace std;

class Person {
public:
	Person(string& name = "Tom", int age = 20, int sex = 1)
		:_name(name)
		, _age(age)
		, _sex(sex) 
	{}
	string _name;
	int _age;
	int _sex;
};

class Student {
public:
	Student(string& name, int age, int sex, int stuNum)
		:_name(name)
		, _age(age)
		, _sex(sex)
		,_stuNum(stuNum)
	{}

	string _name;
	int _age;
	int _sex;
	int _stuNum;
};

class Teacher {
public:
	Teacher(string& name, int age, int sex, int workNum)
		:_name(name)
		, _age(age)
		, _sex(sex)
		, _workNum(workNum)
	{}

	string _name;
	int _age;
	int _sex;
	int _workNum;
};

不继承,代码非常冗余。

#include <iostream>
using namespace std;

class Person {
public:
	Person(string name = "Tom", int age = 20, int sex = 1)
		:_name(name)
		, _age(age)
		, _sex(sex) 
	{}
	string _name;
	int _age;
	int _sex;
};

class Student :public Person {
public:
	Student(int stuNum)
		:_stuNum(stuNum)
	{}

private:
	int _stuNum;
};

class Teacher :public Person{
public:
	Teacher(int workNum)
		:_workNum(workNum)
	{}

private:
	int _workNum;
};

继承后,代码精简。
在这个例子中。Student类与Teacher类都继承了Person类,所以这两个类都有Person类的属性(成员变量即函数)

继承的格式

在这里插入图片描述

定义一个新的类,冒号后面先写继承方式,然后是具体继承哪一个类。

这里继承方式一共有三种,继承下来的父类成员能全部访问与继承方式有关,也与父类中成员的属性有关。具体看下面的表格
在这里插入图片描述
具体可以总结为两点:
父类如果成员为private的,则子类无法访问该类型成员(子类中有该成员,但是基于语法角度不可访问)

父类中成员不为private的,成员在子类中的类型属于 Min(该成员在父类中的类型,子类的继承方式)。即在两者中取权限较小的那一个。

这里新增了一个成员的类型:protected。
有时候需要一些变量时私有的,不可被外界访问,但是希望继承的子类可以访问。由此就产生了protected类型。该类型被子类继承后(public继承或protected继承)还是protected,只可以在类中访问,而不能在类外访问。

子类继承了父类,则子类中拥有父类中所有的成员,所以也可以用子类直接给父类赋值。

#include <iostream>
using namespace std;

class Person {
public:
	Person(string name = "Tom", int age = 20, int sex = 1)
		:_name(name)
		, _age(age)
		, _sex(sex) 
	{}
private:
protected:
	string _name;
	int _age;
	int _sex;
};

class Student :public Person {
public:
	Student(int stuNum)
		:_stuNum(stuNum)
	{}

	int _stuNum;
};

class Teacher :public Person{
public:
	Teacher(int workNum)
		:_workNum(workNum)
	{}

private:
	int _workNum;
};


int main() {
	Person p("Jack", 22, 1);
	Student s(123456);
	Teacher t(147258);

	p = s;//子类赋值给父类
	Person& tmp = s;
	Person* pp = &s;
	return 0;
}

引用,指针也都是可以的。

在实际进行赋值是,会对子类进行类似“切片”一样的操作,把父类中没有的成员直接切掉,就不会发生错误。

注意:但是反过来,父类不可以直接赋值给子类,因为子类会有一些成员是父类没有的。所以不能赋值。

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

  1. 派生类的构造函数必须调用基类的构造函数初始化基类的那一部分成员。如果基类没有默认的构造函
    数,则必须在派生类构造函数的初始化列表阶段显示调用。
  2. 派生类的拷贝构造函数必须调用基类的拷贝构造完成基类的拷贝初始化。
  3. 派生类的operator=必须要调用基类的operator=完成基类的复制。
  4. 派生类的析构函数会在被调用完成后自动调用基类的析构函数清理基类成员。因为这样才能保证派生类对象先清理派生类成员再清理基类成员的顺序。
  5. 派生类对象初始化先调用基类构造再调派生类构造。
  6. 派生类对象析构清理先调用派生类析构再调基类的析构
class Person {
public:
	Person(string name, int age, int sex)
		:_name(name)
		, _age(age)
		, _sex(sex)
	{}
private:
protected:
	string _name;
	int _age;
	int _sex;
};

class Student :public Person {
public:
	Student(int stuNum)
		:_stuNum(stuNum)
		, Person("Tom", 20, 1)//父类没有默认构造函数,子类显示调用
	{}

	int _stuNum;
};

友元与静态成员的继承

友元函数是不可以被继承的。因为友元函数相当于只是声明了一中关系,该函数并不属于父类,所以子类当然也继承不到。

静态成员可以被继承吗?
当然是可以的,静态成员也是属于父类的一份子,子类在继承时是可以继承到的。要注意的是,静态成员终归是静态的,他不可以被复制,就是说子类虽然继承了该成员,但与父类使用的还是同一个成员。不会像其他成员变量一样在创建一个属于自己的变量。整个继承体系中只有一个静态成员。

菱形继承与虚拟继承

继承的多了,当然就会出现问题了。来看下面这个例子。
在这里插入图片描述

学生和老师都是人,所以他们继承了Person类,都有自己的名字,性别等,然后助教既是学生,也是老师,所以他继承了Student类和Teacher类,那么问题来了,Student类和Teacher类都有名字,性别等成员,那么Assisant类到底该使用哪个呢?这就产生了数据冗余与二义性的问题
直接使用name会报错,编译器也不清楚你要使用哪一个,只能分开使用,但要加作用域限定符。

#include <iostream>
using namespace std;

class Person {
public:
	Person(string name = "Tom", int age = 20, int sex = 1)
		:_name(name)
		, _age(age)
		, _sex(sex) 
	{}
	string _name;
	int _age;
	int _sex;
};

class Student :public Person {
public:
	Student(int stuNum = 1)
		:_stuNum(stuNum)
	{}

	int _stuNum;
};

class Teacher :public Person{
public:
	Teacher(int workNum = 1)
		:_workNum(workNum)
	{}

private:
	int _workNum;
};

class Assisant :public Student, public Teacher {

};


int main() {
	Person p("Jack", 22, 1);
	Student s(123456);
	Teacher t(147258);

	s._name = "Mike";//可以直接使用
	Assisant a;
	//a._name = "Jack";//编译器报错
	a.Student::_name = "Jack";//指定使用哪一个父类的name
	a.Teacher::_name = "Jack";

	return 0;
}

但是这样又有问题,只解决了二义性的问题,数据冗余还是没有解决,一个助教不能同时拥有两个名字。
为了解决这个问题,C++引入了虚拟继承的概念。

虚继承有一个关键字:virtual
为了计算方便,我们不使用上面的例子

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 = 16;
	d._b = 48;
	d.C::_a = 32;
	d._c = 48;
	d._d = 48;
	return 0;
}

在这里插入图片描述

上面的例子中,D类继承了B类和C类,B类和C类又都继承了A类,所以在D类中,有两个_a变量,一个_b变量,一个_c变量,一个_d变量。从内存角度看就是如此,这也符合我们的认知,那么加了虚拟继承之后呢?

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 = 16;
	d._b = 48;
	d.C::_a = 32;
	d._c = 48;
	d._d = 48;
	d._a = 64;
	return 0;
}

在这里插入图片描述
注意添加virtual的位置,第一次继承时就要添加,而不是后面继承时添加。
在这里插入图片描述
我们可以看到,在有了虚拟继承之后,d类多出了一个空间来存放_a,所以可以直接访问d._a,那本来应该要存储B::_a与C::_a的位置没有存储数据而是存了一个地址,我们通过该地址找到了一个数据,B::_a的地址里面存的是20(十六进制转为十进制,便于理解),C::_a的地址里面存的是12。这又是什么意思呢?

实际上B::_a存放的地址叫做虚表指针,指向的位置叫做虚表,虚表里面存储的是从原位置(B::_a)到实际存储d._a的位置的偏移量。

比较内存一,从B::_a到d._a的位置就是差了20个字节(每一行是4个字节),从C::_a到d._a的位置差了12个字节。

所以加了虚拟继承之后,B::_a与C::_a可以说已经不复存在了,d中重新开辟了一段空间来保存_a,而B::_a与C::_a的位置都存储的是指针,要用这两个位置的话,就先找到偏移量,然后通过计算再找到实际存储的_a。

由此看到,虚拟继承解决了数据二义性与数据冗余的问题,但实际上又产生了一些新的问题,如虚拟继承后的的类要比未虚拟继承的类空间大,因为要开辟新的空间,并且在访问B::_a时要先找到偏移量,在通过偏移量找到实际空间,效率有所下降,所以菱形继承是一个不好的东西,尽量避免使用!

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值