继承
继承定义和概念
- 我们直接用一段程序来理解我们的继承
cpp
class Person
{
public:
// 进⼊校园/图书馆/实验室刷⼆维码等⾝份认证
void identity()
{
cout << "void identity()" << _name << endl;
}
protected:
string _name = "张三"; // 姓名
string _address; // 地址
string _tel; // 电话
int _age = 18; // 年龄
};
class Student : public Person
{
public:
// 学习
void study()
{
// ...
}
protected:
int _stuid; // 学号
};
class Teacher : public Person
{
public:
// 授课
void teaching()
{
//...
}
protected:
string title; // 职称
};
int main()
{
Student s;
Teacher t;
s.identity();
t.identity();
return 0;
}
上述代码的父类就是我们的Person类student类和teacher类都是父类的子类
因此我们就可以复用父类的成员变量和成员函数
注意:子类继承的成员都是独立的
我们可以看到同一个变量的地址是不一样的因此继承的成员都是独立的
继承的继承方式
- 基类private成员在派⽣类中⽆论以什么⽅式继承都是不可⻅的。这⾥的不可⻅是指基类的私有成员还是被继承到了派⽣类对象中,但是语法上限制派⽣类对象不管在类⾥⾯还是类外⾯都不能去访问它。这是因为被private修饰的不能被外部访问就算是子类也是外部无法访问
- 基类private成员在派⽣类中是不能被访问,如果基类成员不想在类外直接被访问,但需要在派⽣类中能访问,就定义为protected。可以看出保护成员限定符是因继承才出现的。protect就是为了我们在派生类中可以访问,但外部无法访问,方便了派生类的访问。
- 实际上⾯的表格我们进⾏⼀下总结会发现,基类的私有成员在派⽣类都是不可⻅。基类的其他成员在派⽣类的访问⽅式 == Min(成员在基类的访问限定符,继承⽅式),public > protected >
private。 - 使⽤关键字class时默认的继承⽅式是private,使⽤struct时默认的继承⽅式是public,不过最好显⽰的写出继承⽅式。
- 在实际运⽤中⼀般使⽤都是public继承,⼏乎很少使⽤protetced/private继承,也不提倡使⽤protetced/private继承,因为protetced/private继承下来的成员都只能在派⽣类的类⾥⾯使⽤,实际中扩展维护性不强。
基类和派生类的转换关系
- public继承的派⽣类对象可以赋值给 基类的指针 / 基类的引⽤。这⾥有个形象的说法叫切⽚或者切割。寓意把派⽣类中基类那部分切出来,基类指针或引⽤指向的是派⽣类中切出来的基类那部分。
- 基类对象不能赋值给派⽣类对象。
- 基类的指针或者引⽤可以通过强制类型转换赋值给派⽣类的指针或者引⽤。但是必须是基类的指针
是指向派⽣类对象时才是安全的。这⾥基类如果是多态类型,可以使⽤RTTI(Run-Time TypeInformation)的dynamic_cast 来进⾏识别后进⾏安全转换。该知识点会在后续进行详细讲解
继承的作用域
隐藏规则
- 在继承体系中基类和派⽣类都有独⽴的作⽤域。
- 派⽣类和基类中有同名成员,派⽣类成员将屏蔽基类对同名成员的直接访问,这种情况叫隐藏。
(在派⽣类成员函数中,可以使⽤ 基类::基类成员 显⽰访问)- 需要注意的是如果是成员函数的隐藏,只需要函数名相同就构成隐藏。
- 注意在实际中在继承体系⾥⾯最好不要定义同名的成员。
cpp
class Person
{
protected:
string _name = "⼩李⼦"; // 姓名
int _num = 111; // ⾝份证号
};
class Student : public Person
{
public:
void Print()
{
cout << " 姓名:" << _name << endl;
cout << " ⾝份证号:" << Person::_num << endl;
cout << " 学号:" << _num << endl;
}
protected:
int _num = 999; // 学号
};
int main()
{
Student s1;
s1.Print();
return 0;
};
上述代码可以看出如果想要构成隐藏的成员我们应该明确类作用访问域来确定具体访问哪一个类的成员,不明确的话默认访问的是该类的成员
问题
下面我们来看一题
下⾯程序的编译运⾏结果是什么()
A. 编译报错 B. 运⾏报错 C. 正常运⾏
class A
{
public:
void fun()
{
cout << "func()" << endl;
}
};
class B : public A
{
public:
void fun(int i)
{
cout << "func(int i)" << i << endl;
}
};
int main()
{
B b;
b.fun(10);
b.fun();
return 0;
};
- A类和B类里面的func构成隐藏关系因为重载的条件之一要在同一作用域下面
- 这里的会编译报错因为b.func();该语句根本找不到func的定义因此无法通过编译,如果一定要调用需要指定作用域b.A::fun();这用就可以完成调用
继承下的默认成员函数
6个默认成语函数如下:
默认构造函数
- 派⽣类的构造函数必须调⽤基类的构造函数初始化基类的那⼀部分成员。如果基类没有默认的构造函数,则必须在派⽣类构造函数的初始化列表阶段显⽰调⽤。
解释如下:
如果基类没有构造函数我们就要在参数化列表去初始化该基类对象,类似于匿名对象的创建如下所示
拷贝构造函数
- 派⽣类的拷⻉构造函数必须调⽤基类的拷⻉构造完成基类的拷⻉初始化。通常子类的拷贝构造不需要自己实现,但如果该子类定义类一个有资源的变量我们就需要自己来实现
赋值重载
- 派⽣类的operator=必须要调⽤基类的operator=完成基类的复制。需要注意的是派⽣类的operator=隐藏了基类的operator=,所以显⽰调⽤基类的operator=,需要指定基类作⽤域,传入子类进行切片从而调到基类的赋值重载
析构函数
- 派⽣类的析构函数会在被调⽤完成后⾃动调⽤基类的析构函数清理基类成员。因为这样才能保证派⽣类对象先清理派⽣类成员再清理基类成员的顺序。
- 派⽣类对象初始化先调⽤基类构造再调派⽣类构造。
- 派⽣类对象析构清理先调⽤派⽣类析构再调基类的析构。
- 因为多态中⼀些场景析构函数需要构成重写,重写的条件之⼀是函数名相同那么编译器会对析构函数名进⾏特殊处理,处理成destructor(),所以基类析构函数不加virtual的情况下,派⽣类析构函数和基类析构函数构成隐藏关系。
总结:
子类的析构函数当我们显示去实现时应该先去释放子类的资源,再去调用基类的析构函数,当然子类的析构不需要我们显示实现,因为编译器会在释放子类资源后自动调用基类的析构;这样就保证先定义的后释放。任何类的析构函数都会被进行特殊处理成destructor()为了方便后序实现多态来完成对子类父类析构函数精确调用
final关键字
一个问题我们怎么去实现一个无法被继承的类呢?
- 基类的构造函数私有,派⽣类的构成必须调⽤基类的构造函数,但是基类的构成函数私有化以后,派⽣类看不⻅就不能调⽤了,那么派⽣类就⽆法实例化出对象。下面是例子
当基类的构造函数私有时,我们虽然继承下来了但是无法在外部直接访问,而我们在创建派生类对象时一定会调用基类的构造函数,而我们在外部无法访问因此就无法继承了- C++11新增了⼀个final关键字,final修改基类,派⽣类就不能继承了。
继承与友元
友元关系不能继承,也就是说基类友元不能访问派⽣类私有和保护成员友元关系无法被继承
继承与静态成员
- 基类定义了static静态成员,则整个继承体系⾥⾯只有⼀个这样的成员。⽆论派⽣出多少个派⽣类,都只有⼀个static成员实例。
- 因为静态成员是属于整个类不属于某个对象因此我们可以通过类名直接访问到静态成员因此继承下来的静态成员只有一个也就是子类继承下来与父类的静态成员是一样的
多继承和菱形继承
继承模型
- 单继承:⼀个派⽣类只有⼀个直接基类时称这个继承关系为单继承
- 多继承:⼀个派⽣类有两个或以上直接基类时称这个继承关系为多继承,多继承对象在内存中的模型是,先继承的基类在前⾯,后⾯继承的基类在后⾯,派⽣类成员在放到最后⾯。
多继承时的写法:继承多个类时用逗号隔开- 菱形继承:菱形继承是多继承的⼀种特殊情况。菱形继承的问题,从下⾯的对象成员模型构造,可以看出菱形继承有数据冗余和⼆义性的问题,Assistant的对象中Person成员会有两份。⽀持多继承就⼀定会有菱形继承,像Java就直接不⽀持多继承,规避掉了这⾥的问题,所以实践中我们也是不建议设计出菱形继承这样的模型的。
虚继承
虚继承就是为了解决菱形继承的数据冗余;
virtual加的位置:那一个类产生数据冗余就在继承该类时加上virtual具体的就不说了加完之后会把产生数据冗余的类单独拿出来具体我还没学会呢~~
继承和组合
组合:是has-a的关系我们实现queue时我们会在里面维护链表的头尾节点
继承:就是is-a的关系