文章目录
继承
概念
-
继承(inheritance)是使代码可以复用的最重要的手段
-
它允许程序员在保持原有类(基类或父类)特性的基础上进行扩展和增加功能,从而产生新的类(派生类或子类)
-
以前我们接触的代码复用都是函数的复用,继承是类设计层次的复用
继承的使用
🍪继承的语法:
子类 : 继承方式 父类
class base {};
class son1 : public base
{};
class son2 : protected base
{};
class son3 : private base
{};
⭕️注意:继承可以不用指明继承方式。如果不加继承方式,那么:
-
如果子类用 class 定义,则默认继承方式为 private
-
如果子类用 struct 定义,则默认继承方式为 public
class base {};
class son1 : base //private
{};
struct son2 : base //public
{};
🍪举个栗子:不同的动物都有共性,我们将共性定义为 animal 类,不同的动物都继承这个类以实现这个共性,代码如下:
//共性
class animal {
public:
string name;
int age;
int gender;
string type;
//else...
};
class cat : public animal {
private:
bool is_pet; //是否为宠物猫
//else...
};
class dog : public animal {
private:
bool tail; //是否有尾巴
};
🍪根据下面调试,可以更能理解继承:
继承方式
🍪继承方式由三种:public、private、protected
🍪说明:
- 基类 private 成员在派生类中无论以什么方式继承都是不可见的
⭕️注意:这里的不可见是指,基类的 private 成员虽然还是被继承到了派生类对象中,但是语法上限制了派生类不管在类内还是类外都不能访问该成员
class base {
public:
int get_val() {
return val;
}
private:
int val;
}
class son : private base {
public:
int son_get_val() {
return val;
}
}
void test() {
son s;
//s.val; //error
//s.son_get_val(); //error
//派生类可以通过调用基类的成员函数访问
s.get_val();
}
🍪由上面的例子我们可知,基类的 private 不一定在 派生类中不一定是不可访问的
-
如果基类成员不想在类外直接被访问,但需要在子类中能访问,就定义为 protected(protected 成员限定符是因继承才出现的)
-
在实际项目中一般使用 public 继承。因为 protetced 或 private 继承下来的基类成员都只能在派生类的类内使用,扩展性和维护性不强
-
被继承的 public 和 protected 成员在派生类中是可以被访问和修改的
class base {
public:
int _public;
private:
int _private;
protected:
int _protected;
};
class son :public base
{
public:
int a;
void func(){
son s;
s._protected = 10; //可以访问
//s._private = 10; //error,private 成员不能访问
}
private:
int b;
protected:
int c;
};
🍪下面的例子也能说明基类的 private 成员变量确实被派生类继承了
//基类
class Base {
public:
int a;
private:
int b;
protected:
char c;
};
//派生类
class Son : Base
{
public:
int m_a;
};
void test()
{
cout << sizeof(Son); //16
//说明父类中的所有属性,子类中都会保留一份,即使是 private
}
基类和派生类对象赋值转换
-
派生类对象可以赋值给基类的对象
-
派生类指针可以赋值给基类的指针
-
派生类引用可以赋值给基类的引用
//基类
class Person {
protected:
string _name;
int _sex;
int _age;
};
//派生类
class Student : public Person {
public:
string _num;
};
void test() {
//对象赋值
Student stu;
Person per = stu;
//指针赋值
Student stu_ptr = new Student;
Person per_ptr = stu_ptr;
//引用赋值
Student stu_ref;
Person& per_ref = stu_ref;
}
🍪上面的赋值操作就叫做切片或切割,表示把派生类中基类那部分成员切来赋值给基类
⭕️注意:
-
基类对象不能赋值给派生类对象或引用
-
基类的指针可以通过强制类型转换赋值给派生类的指针,但是这种做法是有风险的,访问派生类类内自己定义的成员变量或函数会越界报错
//基类
class Person {
protected:
string _name;
int _sex;
int _age;
};
//派生类
class Student : public Person {
public:
string _num;
};
void test() {
Person per;
//Student stu = per; //error
//Student& stu = per; //error
Person* per_ptr = new Person;
//Student* stu = per_ptr //error
Student* stu = (Student*)per_ptr; //不会报错
//cout << stu._num; //error,越界
}
继承中的作用域
🍪在继承体系中基类和派生类都有独立的作用域
🍪提问:如果基类和派生类中有一个名字相同的成员变量,那么在派生类中打印这个变量,会打印哪个作用域中的值呢?
class base {
protected:
int _age = 10;
}
class son : public base {
public:
int _age = 20;
void print() {
cout << _age;
}
};
int main() {
son s;
s.print(); //20
return 0;
}
🍪分析:
-
我们发现打印的是派生类作用域中的成员变量值
-
这是因为就近原则,当前作用域中的变量优先
🍪提问:那么我们如何打印基类的成员变量值呢?
答:指明类域就可以了
class base {
protected:
int _age = 10;
}
class son : public base {
public:
int _age = 20;
void print() {
//指明类域
cout << Person::_age << " ";
cout << Son::_age;
}
};
int main() {
son s;
s.print(); //10 20
return 0;
}
🍪总结:
-
基类和派生类中有同名成员时,派生类访问同名成员时会屏蔽基类成员,直接访问派生类成员。这种情况叫重定义,也叫隐藏
-
在派生类成员函数中,可以使用 " 基类::基类成员 " 显示访问基类成员,从而解决上面的问题(如上面的例子)
-
我们也可以在类外指定类域调用基类成员
class base {
protected:
void print() {
cout << "base";
}
}
class son : public base {
public:
void print() {
cout << "son";
}
};
class son_son : public son {
public:
void print() {
cout << "son_son";
}
}
int main() {
son s;
s.base::print(); //base
//嵌套
son_son ss;
ss.son::base::print(); //base
return 0;
}
⭕️注意:
-
在继承体系里,对于成员函数,重定义(隐藏)不同于重载:重定义只要函数名相同就满足,而重载不是
-
在实际中在继承体系里面最好不要定义同名的成员
-
如果基类和派生类成员名字没有冲突,派生类可以直接访问基类成员
class base {
public:
//else...
int base_val;
};
class son : public base {
//else..
}
void test() {
son s;
cout << s.base_val;
cout << s.base::base_val;
}
派生类的六个默认成员函数
构造函数
- 派生类的构造函数必须调用基类的构造函数初始化基类的那一部分成员
//基类
class Person {
public:
//默认构造函数
Person(const char* name = "")
:_name(name)
{
cout << "基类构造函数";
}
protected:
string _name;
};
//派生类
class Student : public Person {
public:
//构造函数
//因为基类有默认构造函数,所以不用显示调用基类的构造函数
Student(string name = "张三", int num = 1)
:_num(num)
{
cout << "派生类构造函数";
}
private:
int _num;
}
- 如果基类没有默认的构造函数,则必须在派生类构造函数的初始化列表阶段显示调用
//基类
class Person {
public:
//不存在默认构造函数
Person(const char* name)
:_name(name)
{
cout << "基类构造函数";
}
//else...
protected:
string _name;
};
//派生类
class Student : public Person {
public:
//构造函数(显示调用基类构造函数)
Student(string name = "张三", int num = 1)
:Person(name), _num(num)
{
cout << "派生类构造函数";
}
//else...
private:
int _num;
}
拷贝构造函数
🍪派生类的拷贝构造函数必须调用基类的拷贝构造完成基类的拷贝初始化(切片传参)
//基类
class Person {
public:
//拷贝构造函数
Person(const Person& per)
:_name(name)
{
cout << "基类拷贝构造函数";
}
//else...
protected:
string _name;
};
//派生类
class Student : public Person {
public:
//拷贝构造函数
Student(const Student& stu)
:Person(stu) //调用基类拷贝构造函数,切片传参
, _num(num)
{
cout << "派生类拷贝构造函数";
}
//else...
private:
int _num;
}
operator=() 赋值重载函数
🍪派生类的赋值重载函数 operator=() 必须要调用基类的赋值重载完成基类的复制
//基类
class Person {
public:
//赋值重载函数
Person& operator=(const Person& per)
:_name(per._name)
{
cout << "基类赋值重载函数"
}
//else...
protected:
string _name;
};
//派生类
class Student : public Person {
public:
//赋值重载函数
Student& operator=(const Student& stu) {
if(this != &stu){
//*this = stu; //就近原则,会导致栈溢出
//operator=(stu); //同上
Person::operator=(stu); //调用基类的赋值重载函数
_num = stu._num;
}
cout << "派生类赋值重载函数";
}
//else...
private:
int _num;
}
析构函数
🍪派生类的析构函数会在被调用完成后自动调用基类的析构函数清理基类成员。这样才能保证先清理派生类成员再清理基类成员的顺序
⭕️注意:
-
派生类对象初始化先调用基类构造再调派生类构造
-
派生类对象销毁先调用派生类析构再调基类的析构(类似栈)
-
事实上,对于所有类,析构函数的名字都会被同一处理成 destructor(),那么这就会造成重定义(后面多态会讲)
取地址重载函数 operator&()
🍪包括普通和 const 两个重载,一般不用自己实现
一些问题
🍪我们不写默认生成的派生类的构造函数和析构函数会做些什么?
-
基类继承来的成员,调用基类的默认构造函数和析构函数处理
-
派生类自己的成员(内置类型和自定义类型)和普通类一样的实现方法
🍪我们不写默认生成的派生类拷贝构造函数和 operator=() 会做些什么?
-
基类继承来的,调用基类的拷贝构造函数和 operator=()
-
派生类自己的成员(内置类型和自定义类型)和普通类一样的实现方法
🍪什么情况下需要我们自己写派生类的默认成员函数?
-
父类没有默认构造函数,需要我们显示写构造函数
-
如果派生类有需要清理的资源,需要我们显示写派生类的析构函数
-
如果派生类存在深浅拷贝问题,就需要实现拷贝构造和 operator=() 解决深浅拷贝问题
🍪总结:
-
原则,继承下来的调用父类的进行处理,子类自己的按照普通类基本规则进行处理
-
自己实现派生类默认成员函数的步骤:基类成员调用基类的对应默认成员函数(构造、析构和拷贝构造等)进行处理,子类自己的成员按照普通类规则处理
继承和友元
🍪友元关系不能继承,也就是说,如果某个函数或类是基类的友元,那么它不能访问派生类的 private 成员和 protected 成员
//基类
class base {
public:
friend void print();
protected:
int _pro_base;
private:
int _pri_base;
}
//派生类
class son : public base {
public:
int _pub_son;
protected:
int _pro_son;
private:
int _pri_son;
}
//基类的友元函数
void print() {
son s;
cout << s._pro_base << s._pri_base; //可以访问
cout << s._pub_son; //可以访问
//cout << s._pro_son << s._pri_son; //error
}
继承与静态成员
🍪基类定义了一个 static 成员,则整个继承体系里面只有一个这样的成员,无论有多少个派生类继承该基类,都只有一个 static 成员实例
//基类
class Person {
public:
Person() {
count++;
}
static int count; //统计对象个数
};
int Person::count = 0; //静态成员变量初始化
//派生类 1
class Student : public Person {
protected:
int num;
};
//派生类 2
class Postgraduate : public Student {
protected:
string _major;
};
//count 用于统计人数,无论创建基类还是派生类对象,count 都会加 1
int main() {
Student stu1;
Student stu2;
Postgraduate psg1;
Postgraduate psg2;
cout << Student::count; //4
Student::count = 0;
cout << Postgraduate.Student::Person.count; //0
return 0;
}
菱形继承
- 单继承:一个派生类只有一个直接基类
- 多继承:一个派生类有两个或以上的直接基类
🍪由于 C++ 支持多继承,所以就会衍生出一个很复杂的继承关系,菱形继承
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 _major;
};
🍪对象成员模型构造如下:
🍪从上面的模型可以看出,菱形继承存在两个问题:
-
数据冗余,Assistant 对象中 Person 成员有两份
-
二义性,原因同上
🍪二义性解决办法:指定类域
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 _major;
};
void test() {
Assistant ast;
//cout << ast._name; //error,不知道访问的是哪一个
//指定类域
cout << ast.Student::_name;
cout << ast.Teacher::_name;
}
🍪数据冗余和二义性的解决办法:虚拟继承,在菱形继承的腰部加个 virtual 关键字
举个例子:
🍪虚拟继承解决数据冗余和二义性的原理:
🍪根据上面一个简单的菱形虚拟继承体系,并观察内存窗口,我们可知:
-
对象 A 同时 被 B 和 C 继承,A 同时属于 B 和 C
-
在虚拟继承中,B 和 C 为了找到公共的 A,其都会保存一张虚基表的地址
-
虚基表中存的偏移量,我们可以通过将对象 B 或 C 的地址偏移从而得到 A 的地址,从而找到 A
-
用一张图表示如下:
继承的反思和总结
🍪一般不建议设计出多继承,一定不要设计出菱形继承
继承和组合
-
public 继承是一种 is - a (是)的关系,每个派生类对象都是一个基类对象
-
组合是一种 has - a (有)的关系,如果 B 组合了 A,那么每个 B 对象中都有一个 A 对象
-
优先使用组合,而不是继承
//继承
class animal {}; //动物
class fish : public base {}; //鱼
class wheel {}; //轮子
class car { //车
private:
whell wel; //组合
};
🍪拓展:
- 继承允许你根据基类的实现来定义派生类的实现。这种通过生成派生类的复用通常被称为白箱复用(white-box reuse)。术语 " 白箱 " 是相对可视性而言:在继承方式中,基类的内部细节对子类可见
- 继承一定程度破坏了基类的封装,基类的改变,对派生类有很大的影响。派生类和基类间的依赖关系很强,耦合度高
- 对象组合是类继承之外的另一种复用选择。新的更复杂的功能可以通过组装或组合对象来获得。对象组合要求被组合的对象具有良好定义的接口。这种复用风格被称为黑箱复用(black-box reuse),因为对象的内部细节是不可见的。对象只以 " 黑箱 " 的形式出现。组合类之间没有很强的依赖关系,耦合度低
- 优先使用对象组合有助于你保持每个类被封装。实际尽量多去用组合。组合的耦合度低,代码维护性好。不过继承也有用武之地的,有些关系就适合继承那就用继承,另外要实现多态,也必须要继承。类之间的关系可以用继承,可以用组合,就用组合
本篇文章到这里就结束啦,欢迎批评指正!