继承
继承顾名思义那就是孩子继承了父辈的某些东西并且保存父辈的性质,比如我的妈妈有节约的习惯,我继承了我也有节约的习惯。被继承者我们称为基类,继承者称为派生类,这是我的简单理解哈
继承 (inheritance) 机制是面向对象程序设计 使代码可以复用 的最重要的手段,它允许程序员在 保持原有类特性的基础上进行扩展 ,增加功能,这样产生新的类,称派生类。继承 呈现了面向对象程序设计的层次结构 , 体现了由简单到复杂的认知过程。以前我们接触的复用都是函数复用, 继承是类设计层次的复用 。
一、如何完成继承呢?
首先我们需要一个基类
class Person
{
protected:
string _name;
public:
Person(string name="jwt") :_name(name) {}
};
其次在定义派生类(另一个类)的时候加上继承方式和继承类的名字,一个继承完成
class Student: public Person
{
int _num;
public:
Student(string name, int num) :_num(num), Person(name)
{
}
void print()
{
cout << Person::_name;
cout << _num;
}
};
我们发现确实我们可以调用父类中的成员,有很多问题,我知道你很急但是你先别急
1.访问限定符
访问限定符总共有三个:public、protected、private,下面这样使用
public:随便访问,没有限制,拿来主义就完了
protected和private:翻译一下就是保护的和私有的意思,顾名思义其他人想访问没门,不让访问。他们有什么区别呢?派生类如果继承了基类,如果基类描述成员的访问限定符是protected,那么派生类可以访问基类成员,private描述的话就不能访问了,所以记住private我自己私有的谁都不行,我儿子想要我都不给他,保护的东西就没那么严重,他要就给他吧,但是protected和private成员都不能被类外的访问
2.继承方式
也有三种:public继承、private继承、protected继承
你没看错还是这三个单词,好记多了是吧。
那这几种继承方式有什么区别?这三种继承方式需要配合不同的访问修饰符,来达到不同的继承效果,匹配机制如下:
是不是觉得很难记?确实难记
只要记住,private成员不管你怎么继承,你都不能访问我,我自己私有的东西就是私有的
那么其他的继承,只要取继承方式和基类成员中权限最小的作为派生类继承下来的成员,比如父类中一成员是protected成员,继承方式是private,那么该成员在派生类中就是private的,好记吧
3.如何访问基类成员
基类名::成员名,只能访问protected和public
二、基类和派生类对象赋值转换——切片
如果我们用派生类去初始化基类,会发生什么问题?会成功吗?
1.派生类对象 可以赋值给 基类的对象 / 基类的指针 / 基类的引用 。这里有个形象的说法叫切片或者切割。寓意把派生类中父类那部分切来赋值过去。2.基类对象不能赋值给派生类对象3.基类的指针可以通过强制类型转换赋值给派生类的指针。但是必须是基类的指针是指向派生类对象时才 是安全的。 这里基类如果是多态类型,可以使用 RTTI(Run-Time Type Information) 的 dynamic_cast 来 进行识别后进行安全转换。
原理:一个指针指向派生类的起始地址,计算基类的大小,然后派生类也计算到同样的大小赋予给基类,完成了切片,派生类是先继承后再创建自己的成员的,所以不会发生切错的情况
三、继承的问题
1.如果基类定义了一个num成员,派生类也定义了一个num成员,会不会发生冲突?到底访问谁呢?
直接写个程序验证一下
所以派生类可以和父类定义同名的类的,访问基类则需要加上类名来显示访问了
一般我们不建议和写同名的成员的,不好分辨,还是要保证代码的可读性的
2.那么同名的成员函数呢?会出现隐藏
会出现隐藏,只会执行派生类的函数,想要访问基类的也需要显示访问
3.继承中的作用域
1. 在继承体系中 基类 和 派生类 都有 独立的作用域 。2. 子类和父类中有同名成员, 子类成员将屏蔽父类对同名成员的直接访问,这种情况叫隐藏,也叫重定义。 (在子类成员函数中,可以 使用 基类 :: 基类成员 显示访问 )3. 需要注意的是如果是成员函数的隐藏,只需要函数名相同就构成隐藏。4. 注意在实际中在 继承体系里 面最好 不要定义同名的成员 。
4.派生类的默认成员函数
1. 派生类的构造函数必须调用基类的构造函数初始化基类的那一部分成员。如果基类没有默认的构造函 数,则必须在派生类构造函数的初始化列表阶段显示调用。2. 派生类的拷贝构造函数必须调用基类的拷贝构造完成基类的拷贝初始化。3. 派生类的 operator= 必须要调用基类的 operator= 完成基类的复制。4. 派生类的析构函数会在被调用完成后自动调用基类的析构函数清理基类成员。因为这样才能保证派生类对象先清理派生类成员再清理基类成员的顺序。5. 派生类对象初始化先调用基类构造再调派生类构造。6. 派生类对象析构清理先调用派生类析构再调基类的析构
简单理解一下
拷贝构造和operater=都需要让基类调用他自己的拷贝构造和operater=,要不谁给他处理呢是吧
构造和析构函数,就像我们定义两个对象,在return后的顺寻
class Person
{
private:
string _name;
public:
Person(string name = "jwt"):_name(name)
{
cout << "Person()"<<endl;
}
~Person()
{
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 != nullptr)
_name = p._name;
return *this;
}
};
class Student : public Person
{
public:
Student(string n,int num = 9):_num(num), Person(n)
{
cout << "Student()" << endl;
}
~Student()
{
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)
{
_num = s._num;
Person::operator=(s);
}
return *this;
}
private:
int _num;//学号
};
void Test2()
{
Student s1("jwt",3);
Student s2(s1);
Student s3("jqp", 26);
s1 = s3;
}
运行结果说明,派生类除了析构函数外其他的函数需要先对基类完成对于的默认函数操作,最后析构派生类先析构,基类后析构
5.继承和友元
基类的友元并不能继承给派生类,也就无法访问派生类成员
6.继承和静态成员
基类定义了 static 静态成员,则整个继承体系里面只有一个这样的成员 。无论派生出多少个子类,都只有一个 static 成员实例 。
7.复杂的多继承和虚拟继承
继承形式大致分以下三种
一般单继承和多继承是没有问题的,但是菱形继承就有很大的问题了
学生类和老师类都包含了人类,最后的助理中相当于继承了两次人类,那么就会出现数据冗余和二义性的问题
二义性:就是一个变量有两个意思,老师类赋予一遍,学生类赋予一遍意义,那助理应该继承哪边的意思呢?所以要避免二义性
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 test3()
{
Assistant a;
a._name = "jw";
a.Student::_name = "dada";
a.Teacher::_name = "dasda";
}
不明确你看看
8.如何解决二义性和数据冗余呢?
需要用到虚拟继承了,写法如下
就是在要继承的类前面加一个virtual,虚拟继承不要在其他地方去使用。
9.虚拟继承原理——虚基表
可以从图中看出同一个_a被赋值了两次,数据冗余,A类同时属于B、C类
虚拟继承原理如下:
搞一个指针,指向一个表,这个表记录这基类成员的地址的偏移量,相加就能找到基类的成员并可以改变,这里d.B::_a第一次被赋值成1,d.C::_a被赋值成2,解决了二义性和数据冗余的问题