继承是面向对象中的重要复用手段。继承是类型间的关系建模,共享公共的东西,实现各自不同的东西。在继承关系中,派生类通过继承基类中的公有成员来达到复用。
通过下面代码先简单的了解一下继承:
class Person
{
public:
Person(const string &name)
:_name(name)
{}
void Display()
{
cout << "void Display()" << endl;
}
protected:
string _name; // 姓名
};
class Student : public Person
{
protected:
string _num; // 学号
};
上面代码中的Person类为基类,Student类为派生类。其中的public Person为继承的关系(上面给出的为公有继承)。
继承关系
在继承中有三种继承关系,分别为public(公有)、private(私有)和protected(保护)。可发现继承关系与成员访问限定符相同,通过下面我们可以发现两者之间的关系:
通过上表可对继承关系与成员访问限定符总结出一下几点:
- 基类的私有成员在派生类中是不能被访问的。若基类成员不想被基类对象直接访问,但能派生类中可访问,就定义为保护成员。保护成员限定符是因继承才出现的。
- public继承是一个接口继承,保持is-a原则,每个父类可用的成员对子类也可用,因为每个子类对象也都是一个父类对象。
- protetced/private继承是一个实现继承,基类的部分成员并未完全成为子类接口的一部分,是has-a 的关系原则。
- 使用关键字class时默认的继承方式是private(其中成员访问限定符也默认为私有),使用struct时默认的继承方式是public(其中成员访问限定符也默认为公有),不过最好显示的写出继承方式。
- 在实际运用中一般使用都是public继承,极少场景下才会使用protetced/private继承。
继承中的赋值规则
关于这一个方面我们通过实现代码来观察:
class Person
{
public:
Person(const string &name)
:_name(name)
{}
void Display()
{
cout << "void Display()" << endl;
}
protected:
string _name; // 姓名
};
class Student : public Person
{
public:
Student(const string &name, const string &num)
: Person(name)
, _num(num)
{}
public:
string _num; // 学号
};
void Test()
{
Person p("tx");
Student s("lz", "12");
// 1.子类对象可以赋值给父类对象(切割/切片)
p = s;
// 2.父类对象不能赋值给子类对象 (程序编译不过去,强制类型转换也不可以)
// s = p;
// 3.父类的指针/引用可以指向子类对象
Person *p1 = &s;
Person &r1 = s;
// 4.子类的指针/引用不能指向父类对象(可以通过强制类型转换完成)
Student *p2 = (Student*)&p;
Student &r2 = (Student&)p;
// 访问越界,程序崩溃
p2->_num = "10";
r2._num = "20";
}
通过监视窗口,我们可以观察以上变量的值:
由此可总结出以下几点:
- 子类对象可以赋值给父类对象(切割/切片)
- 父类对象不能赋值给子类对象
- 父类的指针/引用可以指向子类对象
- 子类的指针/引用不能指向父类对象(可以通过强制类型转换完成)
在这里通过图片简单介绍一下切片是如何进行实现的(使用上面程序中的变量s, p进行介绍):
继承体系中的作用域
- 在继承体系中基类和派生类都有独立的作用域。
- 子类和父类中有同名成员,子类成员将屏蔽父类对成员的直接访问。(在子类成员函数中,可以使用基类::基类成员访问),这种情况称为重定义。
- 注意在实际中在继承体系里面最好不要定义同名的成员。
下面以代码来对此进行观察:
class Person
{
public:
Person(const string name, string id = 0)
: _name(name)
, _num(id)
{}
protected:
string _name; // 姓名
string _num; // 身份证号
};
class Student : public Person
{
public:
Student(const string name, string id, string stuNum)
: Person(name, id)
, _num(stuNum)
{}
void DisplayNum()
{
cout << " 身份证号:" << Person::_num << endl; // 基类的_num被重定义,不能直接被掉到
cout << " 学号" << _num << endl;
}
protected:
string _num; // 学号
};
void Test()
{
Student s1("lz", "110", "12");
s1.DisplayNum();
};
在此,我们可以比较一下重定义和重载:
- 重定义:不同作用域,函数名相同形成隐藏
- 重载:同一作用域,函数名相同、参数不同
派生类的默认成员函数
在继承关系里面,在派生类中如果没有显示定义这六个成员函数,编译系统则会默认合成这六个默认的成员函数。
模拟实现合成的默认成员函数:
class Person
{
public:
Person(const string name)
: _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;
}
void Display()
{
cout << "void Display()" << endl;
}
protected:
string _name; // 姓名
};
class Student : public Person
{
public:
Student(const string name, const string num)
: Person(name) // 调用基类的构造函数初始化基类成员变量
, _num(num)
{
cout << "Student()" << endl;
}
Student(const Student &s)
: Person(s) // 通过切片调用基类的拷贝构造函数
, _num(s._num)
{
cout << "Student(const Student &s)" << endl;
}
Student & operator = (const Student &s)
{
cout << "Student & operator = (const Student &s)" << endl;
if (this != &s)
{
Person::operator = (s); // 通过切片调用基类赋值运算符重载
_num = s._num;
}
return *this;
}
~Student()
{
cout << "~Student()" << endl; // 会在析构派生类中的成员变量后调用基类的析构函数析构基类成员变量
}
public:
string _num; // 学号
};
void Test()
{
Student s1("lz", "12");
Student s2(s1);
Student s3("tt", "21");
s1 = s3;
}
运行结果为:
可分析出以下几点:
- 派生类中调用构造函数时,不能直接构造基类成员变量,需要调用基类的构造函数
- 对派生类对象调用赋值运算符重载时,基类成员变量需使用基类的赋值运算符重载。(注意:此时赋值运算符重载被重定义,调用基类的赋值运算符重载需加上基类的区间)
- 派生类调用析构时,会先析构派生类中的成员变量再自动调用基类的析构函数析构基类的成员变量
通过以上分析,可推测出将一个类中的构造函数私有后,该类不能被继承。
继承关系 :
单继承 & 多继承
单继承:就是一个派生类只直接继承了一个基类,就称为单继承(上面的代码均为单继承)
多继承:就是一个派生类直接继承了多个(两个及以上)基类,就称为多继承
菱形继承
其实单继承和多继承并没有什么复杂的,但当将两种继承关系结合再一起之后会产生一种很尴尬的继承关系——菱形继承
通过上图可以看出Assistant类中的两个基类中都继承了Person类,再通过对象模型仔细观察一下:
通过对象模型发现和猜想的一样Assistant类中含有了两个Person类。故菱形继承会引起数据冗余以及二义性,如果调用 _name还需要指定调用那个基类的 _name,而且两个基类中的 _name还可能不同。这样使用起来会十分的不方便。下面通过虚继承来解决这个问题。
虚继承
虚继承主要为了解决菱形继承中的数据冗余与二义性问题,可以减少空间的浪费。虚继承就是再继承关系前面加上关键字virtual,下面通过代码来分析(其中为方便观察类型均使用int类型):
class Person
{
public:
int _name; // 姓名
};
class Student : virtual public Person
{
public:
int _num; //学号
};
class Teacher : virtual public Person
{
public:
int _id; // 职工编号
};
class Assistant : public Student, public Teacher
{
public:
int _majorCourse; // 主修课程
};
void Test()
{
// 显示指定访问哪个父类的成员
Assistant a;
a.Student::_name = 1;
a.Teacher::_name = 2;
a._num = 3;
a._id = 4;
a._majorCourse = 5;
}
观察代码中对象a在内存是如何存储的:
通过观察内存可以发现:对象d中含有基类B、C,但基类B、C中本应继承基类A的位置放入的为一个指针,指针指向位置我们称之为虚基表。观察内存2、3,我们发现虚基表中存在一个值,该值为偏移量(d对象中基类B、C到基类A的位置的偏移量)。由此可知,d对象中不在含有两个A类,通过使用虚继承很好的解决了菱形继承的数据冗余及二义性问题。其中当A类占用的内存越大时,越能体现出虚继承的优势。
注意:虚继承体系看起来好复杂,在实际应用我们通常不会定义如此复杂的继承体系。一般不到万不得已都不要定义菱形结构的虚继承体系结构,因为使用虚继承解决数据冗余问题也带来了性能上的损耗。