继承的概念和定义
继承的概念
-
继承是一种机制,该机制是面向对象程序设计中使代码可以复用的最重要的手段。
-
有了该机制,让我们能够在保持原有类特性(基类)的基础上进行扩展,扩展产生的类,我们称作为派生类
-
继承呈现了面向对象程序设计中的层次结构,体现了由简单到复杂的认知过程
-
我们之前接触到的复用都是函数复用(将要多次使用的功能写成一个函数),而继承是类设计层次的复用(将一些共同拥有的特性抽象出来,实现在原有类的基础上进行扩展)
-
注意:继承之后,基类的成员函数和成员变量都会编程派生类的一部分。这意味着只要权限允许子类能够调用父类的成员函数
代码示例1
-
以学生、老师和人的关系举例,学生和老师都是人,这时我们就可以将人作为基类,学生和老师都作为派生类
-
为了方便讲述,我将人的行为指定说话,属性为姓名和年龄,再定义两个派生类学生和老师,学生类的私有成员为年级,老师类的私有成员为职称
-
测试程序:
#include<iostream>
#include<string>
using namespace std;
class Person
{
public:
Person()
:_name("姓名")
, _age("年龄")
{}
~Person()
{}
void spack()
{
cout << "我叫:" << _name << ' ' << "我的年龄是:" << _age << endl;
}
private:
string _name;
string _age;
};
class Student :public Person
{
public:
Student()
:_grade("一年级")
{}
~Student()
{}
void printGrade()
{
cout << "我的年级是:" << _grade << endl;
}
private:
string _grade;
};
class Teacher : public Person
{
public:
Teacher()
:_lever("初级教师")
{}
~Teacher()
{}
void printLever()
{
cout << "我的职称是:" << _lever << endl;
}
private:
string _lever;
};
int main()
{
cout << "人类:" << endl;
Person per;
per.spack();
cout << "学生类:" << endl;
Student stu;
stu.spack();
stu.printGrade();
cout << "老师类:" << endl;
Teacher tea;
tea.spack();
tea.printLever();
return 0;
}
-
运行结果
-
我们可以发现,两个派生类(学生和老师都能够调用基类的成员函数),那么基类能不能调用子类的成员函数呢?
-
程序直接报错了,说明基类之中,没有子类成员函数
继承的定义
继承定义的格式
class 派生类名 : 继承方式 基类名
-
以学生类继承人类举例
class Student :public Person
{
public:
Student()
:_grade("一年级")
{}
~Student()
{}
void printGrade()
{
cout << "我的年级是:" << _grade << endl;
}
private:
string _grade;
};
继承关系和访问限定符
不同的访问方式所带来的效果
-
基类的private成员在子类中都不可见,但不代表没被继承下来,只是语法上限制派生类对象不管在类里面还是类外面都不能访问它。(好比老爹给你送了个音乐盒,夹层里放着他的私房钱,你不知道,所以你也没办法花,但它就存在在那里QVQ)
-
如果我们想要基类的成员能被子类访问且不想在类外被访问,应当用protected关键字,可以看出protected访问限定符就是应继承而生
-
基类成员在子类中的访问方式是取访问限定符和继承方式中权限更小的那个,public > protected > private
-
使用class继承时,默认的继承方式是private;使用struct继承时,默认的继承方式是public,不过继承方式最好显式写出来
-
在实际应用中,public继承方式占绝大多数,很少会用到其他两个继承方式
代码示例
-
加上protected后,基类的成员变量能在子类被访问了。而是由private访问限定符修饰时不行
class Person
{
public:
Person()
:_name("姓名")
, _age("年龄")
{}
~Person()
{}
void spack()
{
cout << "我叫:" << _name << ' ' << "我的年龄是:" << _age << endl;
}
/*private*/
protected:
string _name;
string _age;
};
class Student :public Person
{
public:
Student()
:_grade("一年级")
{}
~Student()
{}
void printGrade()
{
cout << "我的年级是:" << _grade << endl;
}
void printFather()
{
cout << "基类的姓名:"<<Person::_name<<"基类的年龄"<<Person::_age << endl;
}
private:
string _grade;
};
int main()
{
cout << "学生类:" << endl;
Student stu;
stu.printFather();
return 0;
}
-
运行结果
基类和派生类对象赋值转换
-
派生类对象可以赋值给基类的对象、基类的指针、基类的引用。我将这种玩法叫作切片、切割,指的是将派生类中的基类部分拿出来赋值过去给基类的对象、基类的指针、基类的引用
-
基类对象不能赋值给派生类对象
-
若基类指针指向子类对象,可以通过强制类型转换的方式来赋值给派生类指针
-
子类赋值给父类对象,我们认为是天然的,中间并不会产生临时对象,这个就是父子类赋值兼容规则(切割、切片)
-
代码示例
class Person
{
protected :
string _name; // 姓名
string _sex; // 性别
int _age; // 年龄
};
class Student : public Person
{
public :
int _No ; // 学号
};
void Test ()
{
Student sobj ;
// 1.子类对象可以赋值给父类对象/指针/引用
Person pobj = sobj ;
Person* pp = &sobj;
Person& rp = sobj;
//2.基类对象不能赋值给派生类对象
sobj = pobj;
// 3.基类的指针可以通过强制类型转换赋值给派生类的指针
pp = &sobj
Student* ps1 = (Student*)pp; // 这种情况转换时可以的。
ps1->_No = 10;
pp = &pobj;
Student* ps2 = (Student*)pp; // 这种情况转换时虽然可以,但是会存在越界访问的问
题
ps2->_No = 10;
}
继承中的作用域
-
在继承体系中基类和派生类都有独立的作用域。
-
子类和父类中有同名成员,子类成员将屏蔽父类对同名成员的直接访问,这种情况叫隐藏,也叫重定义。(在子类成员函数中,可以使用 基类::基类成员 显示访问)
-
需要注意的是如果是成员函数的隐藏,只需要函数名相同就构成隐藏。
-
注意在实际中在继承体系里面最好不要定义同名的成员。
-
当基类的成员名与派生类相同时,派生优先(派生类同名成员会将基类的同名成员隐藏,想要使用基类的同名成员需要显示调用)
-
注意:函数的重载要在同一作用域,继承中只要派生类和基类的名字相同就会构成隐藏
class Person
{
public:
Person()
:_name("姓名")
, _age("年龄")
{}
~Person()
{}
void speak()
{
cout << "基类:我叫:" << _name << ' ' << "基类:我的年龄是:" << _age << endl;
}
/*private*/
protected:
string _name;
string _age;
};
class Student :public Person
{
public:
void speak()
{
cout << "派生类:我叫:" << _name << ' ' << "派生类:我的年龄是:" << _age << endl;
}
protected:
string _grade;
};
Student stu;
stu.speak();
stu.Person::speak();
派生类的默认成员函数
-
6个默认成员函数,“默认”的意思就是指我们不写,编译器会变我们自动生成一个,那么在派生类中,这几个成员函数是如何生成的呢?
-
派生类的构造函数必须调用基类的构造函数初始化基类的那一部分成员。如果基类没有默认的构造函数(无参构造函数、全缺省构造函数、编译器默认生成的构造函数),则必须在派生类构造函数的初始化列表阶段显示调用。
-
派生类的拷贝构造函数必须调用基类的拷贝构造完成基类的拷贝初始化。
-
派生类的operator=必须要调用基类的operator=完成基类的复制。
-
派生类的析构函数会在被调用完成后自动调用基类的析构函数清理基类成员。因为这样才能保证派生类对象先清理派生类成员再清理基类成员的顺序。
-
派生类对象初始化先调用基类构造再调派生类构造。
-
派生类对象析构清理先调用派生类析构再调基类的析构。
-
回顾六个默认成员函数:
-
上面要点的流程图
-
额外冷知识:要想使类不能被继承,只需要私有化构造函数(c++98标准的玩法),在类后面加final(c++11标准的玩法)
继承和友元
-
友元关系不能被继承(父亲的朋友不是你的朋友)
继承和静态成员
-
基类定义了一个static静态成员,则整个继承体系里面只有一个这样的成员,不管派生出多少子类,都只有一个static实例
-
继承static成员继承的是使用权
复杂的菱形继承和菱形虚拟继承
-
单继承:一个派生类只有一个直接基类时,称这个继承关系为单继承
-
多继承:一个派生类有两个或以上直接基类时,称这个继承为就是多继承
-
菱形继承:菱形继承是多继承的一个特殊情况
菱形继承的问题
-
二义性: Assistant中有两份Person,直接调用编译器会不知道该调哪个
-
代码冗余:Assistant中有两份Person
#include<iostream>
using namespace std;
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; // 主修课程
};
void Test()
{
// 这样会有二义性无法明确知道访问的是哪一个
Assistant a;
a._name = "peter";
// 需要显示指定访问哪个父类的成员可以解决二义性问题,但是数据冗余问题无法解决
a.Student::_name = "xxx";
a.Teacher::_name = "yyy";
}
菱形虚拟继承
-
菱形虚拟继承能够解决菱形继承的二义性和数据冗余问题
{
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 ; // 主修课程
};
void Test ()
{
Assistant a ;
a._name = "peter";
}
虚拟机城解决数据冗余和二义性的原理
-
我们通过内存窗口来研究这个原理
-
未使用菱形虚拟继承:
-
我们可以发现,未使用虚拟菱形继承的话,d对象中是继承了两份A,一份是b中的,一份是c中的。
-
使用菱形虚拟继承
-
下图是菱形虚拟继承的内存对象成员模型:这里可以分析出D对象中将A放到的了对象组成的最下面,这个A同时属于B和C,那么B和C如何去找到公共的A呢?这里是通过了B和C的两个指针,指向的一张表。这两个指针叫虚基表指针,这两个表叫虚基表。虚基表中存的偏移量。通过偏移量可以找到下面的A。
-
原理:使用菱形虚拟继承之后,b、c对象类存的并不是整个a,而是存入其距离a对象的偏移量值得地址(距离)。如此,a对象对象只需要在d中存一份,b、c对象根据所存偏移量的地址找到偏移量,随后便可以找到a对象
关于继承的总结
-
很多人说C++语法复杂,其实多继承就是一个体现。有了多继承,就存在菱形继承,有了菱形继承就有菱形虚拟继承,底层实现就很复杂。所以一般不建议设计出多继承,一定不要设计出菱形继承。否则在复杂度及性能上都有问题。
-
多继承可以认为是C++的缺陷之一,很多后来的OO语言都没有多继承,如Java。
-
继承和组合public继承是一种is-a的关系。也就是说每个派生类对象都是一个基类对象。组合是一种has-a的关系。假设B组合了A,每个B对象中都有一个A对象。
-
继承允许你根据基类的实现来定义派生类的实现。这种通过生成派生类的复用通常被称为白箱复用(white-box reuse)。术语“白箱”是相对可视性而言:在继承方式中,基类的内部细节对子类可见 。继承一定程度破坏了基类的封装,基类的改变,对派生类有很大的影响。派生类和基类间的依赖关系很强,耦合度高。
-
对象组合是类继承之外的另一种复用选择。新的更复杂的功能可以通过组装或组合对象来获得。对象组合要求被组合的对象具有良好定义的接口。这种复用风格被称为黑箱复用(black-box reuse),因为对象的内部细节是不可见的。对象只以“黑箱”的形式出现。组合类之间没有很强的依赖关系,耦合度低。优先使用对象组合有助于你保持每个类被封装。
-
实际尽量多去用组合。组合的耦合度低,代码维护性好。不过继承也有用武之地的,有些关系就适合继承那就用继承,另外要实现多态,也必须要继承。类之间的关系可以用继承,可以用组合,就用组合。