99网
您的当前位置:首页详解C++继承(普通继承,菱形继承与虚拟继承)

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

来源:99网

继承的概念

假如现在我们有三个类,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的,成员在子类中的类型属于 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个默认成员函数,“默认”的意思就是指我们不写,编译器会变我们自动生成一个,那么在派生类中,这几个成员函数是如何生成的呢?

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 = ;
	return 0;
}

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

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

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

因篇幅问题不能全部显示,请点此查看更多更全内容