路漫漫其修远兮
吾将上下而求索
目录
契子✨
本期我将带大家一同探究继承的本质
这里就简单的介绍一下什么是继承
继承的概念:继承是面向对象程序设计使代码可以复用的最重要的手段,它允许程序员在保
持原有类特性的基础上进行扩展,增加功能,这样产生新的类,称派生类
通俗一点来讲就是:子承父业,子类能使用父类的公开的资产:比如房、车等等(public、protected),但是未公开的资产不能直接访问:比如一些不为人知的小爱好(private)
继承的概念
我们先来看一下以下的代码:
#include<string>
#include<iostream>
using namespace std;
class person
{
protected:
string _name = "peter";
int _age = 18;
};
class student : public person
{
public:
void Print()
{
cout << "name:" << _name << endl;
cout << "age:" << _age << endl;
cout << "ID:" << _studentID << endl;
}
protected:
int _studentID = 111;
};
class teacher : public person
{
public:
void Print()
{
cout << "name:" << _name << endl;
cout << "age:" << _age << endl;
cout << "ID:" << _teacherID << endl;
}
protected:
int _teacherID = 222;
};
int main()
{
student s;
teacher t;
s.Print();
t.Print();
system("pause");
return 0;
}
根据打印结果来看我们的派生类 studnet、teacher 确实继承了 person 这个基类
因为 studnet、teacher 可以访问 person 的成员函数
所以继承的本质就是复用:你没有进行定义,但是可以拿别人定义过的
继承定义:
设想一下,如果我们设计一个学校的教务系统,那我们的对象就会有学生,老师等,当然我们可以使用类把他们的信息一个一个存起来,像下面这样:
可以发现,学生,老师等,他们都是人,都有一些人的基本信息,比如上图红框中的姓名和年龄,如果每个类都定义上年龄和姓名的话,代码会非常冗余,而且如果以后想要在基本信息中加上一些信息时,我们需要把每个类都添加一遍,这个时候我们的继承就该登场了
创建一个基类(父类)来封装这些基本信息,让派生类来继承,这样以后我们要对基本信息修改的话,就只需在 person类 中修改就行了
一些小细节:
《1》子类继承下来的变量与父类之间的关系
那么有的老铁可能会问,那么学生、老师的类继承了人这个父类,那么他们继承下来的东西是同一份吗?(地址相同)答案肯定是:不是同一份,是同一份的话还不得乱套
我们可以简单的验证一下:
我们先将父类这里修改成公有,这样我们的子类就可以访问父类的成员
我们这里修改一下继承下来的 _name ,如果名字相同便是同一份
所以我们子类继承下来的空间并不是同一份,他们是独立的
《2》父类构造、析构函数和子类构造、析构函数的关系
- 子类中可以定义构造函数
- 子类构造函数
- 必须对继承而来的成员进行初始化
- 直接通过初始化列表或者赋值的方式进行初始化
- 调用父类构造函数进行初始化
- 父类构造函数在子类中的调用方式
- 默认调用
- 适用于无参构造函数和使用默认参数的构造函数
我们可以看看一下代码:
class person
{
public:
person()
{
cout << "person()" << endl;
}
};
class student : public person
{
public:
student()
{
cout << "student()" << endl;
}
};
int main()
{
student s;
system("pause");
return 0;
}
class person
{
public:
person()
{
cout << "person()" << endl;
}
};
class student : public person
{
};
int main()
{
student s;
system("pause");
return 0;
}
构造规则:
<1>先执行父类构造函数再执行子类的构造函数
<2>如果子类没有写构造,会默认调用父类构造
#include<iostream>
#include<string>
using namespace std;
class person
{
public:
person()
{
cout << "person()" << endl;
}
~person()
{
cout << "~person()" << endl;
}
};
class student : public person
{
public:
student()
{
cout << "student()" << endl;
}
~student()
{
cout << "~student()" << endl;
}
};
int main()
{
student s;
return 0;
}
这里说明析构的顺序与构造的顺序是相反的
析构规则:
先执行子类析构函数再执行父类的析构函数
继承方式:
类成员/继承方式 | public继承 | protected继承 | private继承 |
基类的public成员 | 派生类的public成员 | 派生类的protected成员 | 派生类的private成员 |
基类的protected成员 | 派生类的protected成员 | 派生类的protected成员 | 派生类的private成员 |
基类的private成员 | 在派生类中不可见 | 在派生类中不可见 | 在派生类中不可见 |
这个时候很多老铁就会发现我们这里运用了一个很不常见的域运算符:protected(保护)
先说明一下 protected 只有在继承中才有意义,在其他情况与 private 用法一样
根据上面的表格我们来看:
private:
错误示范
#include<string>
#include<iostream>
using namespace std;
class person
{
public:
void Print()
{
cout << "name:" << _name << endl;
cout << "age:" << _age << endl;
}
protected:
string _name = "peter";
int _age = 18;
private:
int _ID = 7;
};
class student : public person
{
public:
void Func()
{
cout << "ID:" << _ID << endl;
}
protected:
int _studentID = 111;
};
int main()
{
student s;
person p;
s.Func();
p.Print();
system("pause");
return 0;
}
因为 private 是私有,所以类里面不能访问,类外面也不能访问
但是我们可以通过父类成员函数间接使用
总结:基类 private 成员在派生类中无论以什么方式继承都是不可见的。这里的不可见是指基类的私有成员还是被继承到了派生类对象中,但是语法上限制派生类对象不管在类里面还是类外面都不能去访问它
注意:这里只是不可见,并不是没有发生继承
protected:
#include<string>
#include<iostream>
using namespace std;
class person
{
public:
void Print()
{
cout << "name:" << _name << endl;
cout << "age:" << _age << endl;
cout << "ID:" << _ID << endl;
}
protected:
string _name = "peter";
int _age = 18;
private:
int _ID = 7;
};
class student : public person
{
public:
void Func()
{
_name = "张三";
_age = 19;
cout << "name:" << _name << endl;
cout << "age:" << _age << endl;
}
protected:
int _studentID = 111;
};
int main()
{
student s;
person p;
s.Func();
p.Print();
system("pause");
return 0;
}
protected 则是子类可以访问父类中的 protected 成员
如果基类成员不想在类外直接被访问,但需要在派生类中能访问,就定义为 protected。可以看出保护成员限定符是因继承才出现的
实际上面的表格我们进行一下总结会发现:
基类的成员在子类的访问方式权限(public > protected > private)
这里有一点小细节,继承方式可以隐式写,也可以显示写(显示写就如上面一样)
我们这里简单提一下:
使用关键字 class 时默认的继承方式是 private,使用 struct 时默认的继承方式是 public
c++在刚创立继承时,为了多方面考虑就把所有的继承方式都组合了,但在实际应用中,我们基本只会使用 public 继承✨
基类和派生类对象赋值转换
派生类向基类的转化
我们可以先来看看这段代码:
#include<string>
#include<iostream>
using namespace std;
class Person
{
public:
void Print()
{
cout << "name:" << _name << endl;
cout << "sex:" << _sex << endl;
cout << "age:" << _age << endl;
}
protected:
string _name;
string _sex;
int _age;
};
class Student : public Person
{
public:
void Func()
{
_name = "张三";
_sex = "男";
_age = 18;
}
protected:
int _No = 7;
};
void Test()
{
Student s;
s.Func();
Person p = s;
p.Print();
}
int main()
{
Test();
return 0;
}
根据以上代码的逻辑我们不难发现:子类对象s 转化为 父类对象 p
转换的过程是什么呢?(我们可以从监视窗口看一下)
我们发现 Student 对象赋值给 Person 对象时,发生了 “切片” 或者 “切片” :把不属于父类的成员截断,将父类有的成员拷贝过去
还有一点需要注意,如果是指针、引用的情况(没错,子类对象可以赋值给父类对象/指针/引用)
#include<iostream>
#include<string>
using namespace std;
class Person
{
public:
string _name;
protected:
string _sex;
int _age;
};
class Student :public Person
{
public:
void Func()
{
_name = "zs";
_sex = "男";
_age = 18;
}
protected:
int _No = 7;
};
int main()
{
Student s;
s.Func();
Person p = s;
Person* ptr = &s;
Person& ret = s;
ptr->_name += 'x';
ret._name += 'y';
return 0;
}
总结:
<1>派生类对象可以赋值给基类的 对象 / 指针 / 引用,这里有个形象的说法叫 切片 / 切割,就是把派生类中父类那部分切来拷贝过去
<2>基类对象不能赋值给派生类对象
<3>基类的指针或者引用可以通过强制类型转换赋值给派生类的指针或者引用。但是必须是基类的指针是指向派生类对象时才是安全的。这里基类如果是多态类型,可以使用 RTTI 的 dynamic_cast 来进行识别后进行安全转换
继承中的作用域(隐藏的概念)
在C++中,当子类继承了父类时,如果子类中出现了与父类同名的成员 函数 / 变量,那么子类的同名成员 函数 / 变量 将隐藏父类的同名成员函数
简单来讲当通过子类对象调用同名成员 函数 / 变量时,会优先调用子类中的成员 函数 / 变量,而无法直接调用到父类中的同名成员函数 / 变量,如果要调用父类还需指定类域
<1> 在继承体系中基类和派生类都有独立的作用域 |
<2> 子类和父类中有同名成员,子类成员将屏蔽父类对同名成员的直接访问,这种情况叫隐藏 |
<3> 需要注意的是如果是成员函数的隐藏,只需要函数名相同就构成隐藏 |
<4> 注意在实际中在继承体系里面最好不要定义同名的成员 |
我们可以先来看一段代码:
#include<iostream>
#include<string>
using namespace std;
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;
};
void Test()
{
Student s1;
s1.Print();
};
int main()
{
Test();
return 0;
}
我们发现在子类 Print 中成员变量 _num 与父类中的成员变量 _num 相冲突了
这里就会发生一个(到底该用谁)的问题
<1>我们先看子类中的第一个 _num ,因为指定了父类的类域,所以直接调用父类中的 _num,不构成隐藏
<2>我们先看子类中的第二个 _num ,因为子类与父类有相同的成员变量,所以隐藏了父类的,构成隐藏
出道题,看看老铁们掌握的怎么样:
//<1>A中的fun与B中的fun构成重载吗?
//<2>A中的fun与B中的fun构成隐藏吗?
class A
{
public:
void fun()
{
cout << "func()" << endl;
}
};
class B : public A
{
public:
void fun(int i)
{
A::fun();
cout << "func(int i)->" << i << endl;
}
};
void Test()
{
B b;
b.fun(10);
};
A 中的 fun 和 B 中的 fun 不是构成重载,因为不是在同一作用域
A 中的 fun 和 B 中的 fun 构成隐藏,成员函数满足函数名相同就构成隐藏
子类中的默认成员函数
之前只是简单的提了一下,接下来将直接进入正题:
构造函数
错误示范
#include<iostream>
#include<string>
using namespace std;
class person
{
public:
person(string name,int id)
:_name(name)
,_id(id)
{}
protected:
string _name;
int _id;
};
class student : public person
{
protected:
int student_id;
};
int main()
{
student s;
system("pause");
return 0;
}
我们来分析它的报错:
因为派生类的构造函数必须调用基类的构造函数初始化基类的那一部分成员
但是这里我们基类的构造并不是默认构造
这里和我们之前的自定义成员很像,都是调用它的默认构造
#include<iostream>
#include<string>
using namespace std;
class person
{
public:
person(string name = "张三", int id = 18)
:_name(name)
,_id(id)
{}
protected:
string _name;
int _id;
};
class student : public person
{
protected:
int student_id;
};
int main()
{
student s;
system("pause");
return 0;
}
我们将基类的默认构造补全即可 ~
#include<iostream>
#include<string>
using namespace std;
class person
{
public:
person(string name = "")
:_name(name)
{
cout << "person()" << endl;
}
protected:
string _name;
};
class student : public person
{
public:
student(string name = "张三")
:student_id(111)
, person(name)
{
cout << "student()" << endl;
}
protected:
int student_id;
};
int main()
{
student s;
system("pause");
return 0;
}
这里提供另外一种写法,我们父类的构造依然是默认构造,但是我们可以在子类的初始化列表中进行初始化
派生类的构造函数必须调用基类的构造函数初始化基类的那一部分成员
如果基类没有默认的构造函数,则必须在派生类构造函数的初始化列表阶段显示调用
拷贝构造
派生类的拷贝构造函数必须调用基类的拷贝构造完成基类的拷贝初始化
我们可以看看以下代码
#include<iostream>
#include<string>
using namespace std;
class Person
{
public:
Person(const char* 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;
}
protected:
string _name;
};
class Student :public Person
{
public:
Student(const char* name = "张三", int num = 111)
: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;
}
protected:
int _num;
};
int main()
{
Student s1("张三", 18);
Student s2 = s1;
return 0;
}
这里其实和内置类型值拷贝差不多,自定义类型调用他的拷贝构造
继承的父类成员,必须调用父类的拷贝构造函数初始化
赋值构造
代码根据上面的模板
int main()
{
Student s1("张三", 18);
Student s2;
s2 = s1;
return 0;
}
派生类的 operator= 必须要调用基类的 operator= 完成基类的复制
还有一点需要注意:
基类的 operator= 与 派生类的 operator= 构成隐藏
析构函数
代码根据上面的模板
int main()
{
Student s1("张三", 18);
return 0;
}
子类的析构函数是比较特殊的一个函数,我们不需要显示调用父类析构函数,每个子类析构函数后面,会自动调用父类析构函数,这样才能保证先析构子类,在析构父类
那如果我们要显示的调用析构函数该怎么办呢?
如果我们之间直接调用父类的析构是不行的,因为我们子类的 ~Person() 与父类的 ~Person() 构成隐藏,然后我们默认使用的是我们子类 ~Person() 但是我们没有写,所以就会报这个错误
我们只需指定类域即可 ~
~Student()
{
cout << "~Student()" << endl;
Person::~Person();
}
但是 ~
注意:析构函数不可以显式调用基类的,是错误的!
<1>这是由于子类继承父类,因为是父类先构造初始化的,子类最后再构造的。所以符合后定义的先析构,先定义的后析构,子类后定义,所以子类先被析构
<2>因为这些变量存储在栈里面,在局部栈帧中,先进后出,后进先出。所以为了保证这个顺序,如果自己显式调用析构,则这个顺序就无法保证,所以系统会自动调用
<3>自己不需要显式调用父类的析构函数,每个子类的析构函数后面会自动调用父类的析构函数。所以显式调用的话会有重复,重复的话,普通的变量没什么,但是如果是一个指针,连续被释放两次,一定会出错的
派生类的析构函数会在被调用完成后自动调用基类的析构函数清理基类成员 |
因为这样才能保证派生类对象先清理派生类成员再清理基类成员的顺序 |
继承与友元
友元关系不能继承,也就是说基类友元不能访问子类私有和保护成员
错误示范
#include<iostream>
#include<string>
using namespace std;
class Student;
class Person
{
public:
friend void Display(const Person& p, const Student& s);
protected:
string _name;
};
class Student : public Person
{
protected:
int _stuNum;
};
void Display(const Person& p, const Student& s)
{
cout << p._name << endl;
cout << s._stuNum << endl;
}
int main()
{
Person p;
Student s;
Display(p, s);
return 0;
}
根据以上代码我们可以知道: Display 是 Person 的友元,Display 可以调用 Person 的成员,但是它们这层关系并没有继承给 Student(所以出现了这个错误)
就比如:你现任女朋友的前男友的联系方式,她是不会告诉你的一样 ~
继承与静态成员
基类定义了 static 静态成员,则整个继承体系里面只有一个这样的成员。无论派生出多少个子类,都只有一个 static 成员实例
#include<iostream>
#include<string>
using namespace std;
class Person
{
public:
Person()
{
++_count;
}
protected:
string _name;
public:
static int _count;
};
int Person::_count = 0;
class Student : public Person
{
public:
void Func()
{
_count++;
}
protected:
int _stuNum;
};
void TestPerson()
{
Student s1;
cout << " Student人数 :" << Person::_count << endl;
cout << " Person人数 :" << Student::_count << endl;
s1.Func();
cout << " Student人数 :" << Person::_count << endl;
cout << " Person人数 :" << Student::_count << endl;
Student::_count = 0;
cout << " Student人数 :" << Person::_count << endl;
cout << " Person人数 :" << Student::_count << endl;
}
int main()
{
TestPerson();
return 0;
}
通过观察我们发现 Student、Person 是同一个 _count
单继承与多继承
单继承:一个子类只有一个直接父类时称这个继承关系为单继承
多继承:一个子类有两个或以上直接父类时称这个继承关系为多继承
多继承看似很有用,考虑到了多种应用场景,实则是个天大的坑!!!
因为有了多继承就可能出现菱形继承
菱形继承:菱形继承是多继承的一种特殊情况
当一个派生类通过两个不同的路径继承自同一个基类时,就会形成菱形继承结构
菱形继承的危害:数据冗余和二义性(下面的例子会详细讲解)
我们简单来看一下菱形继承:
#include<iostream>
#include<string>
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";
}
int main()
{
Test();
return 0;
}
此时实例化 Assistant 类后,就会发现里面有两份 Person 的信息,就会造成数据冗余和二义性
编译器直接报错,因为二义性,它不清楚是应该调用 Teacher 中的 _name 还是调用 Student 中的 _name,可以使用域作用限定符显示调用
这样虽然解决了二义性的问题但数据冗余的问题仍无法解决 ~
因为 Teacher 、Student 继承了 Person,所以在 Assistant 中有两份 _name
可能你会觉得这没什么,但是数据量一大呢?数据冗余造成的影响就会十分严重
虚拟继承(virtual)
虚拟继承(Virtual Inheritance)是C++中一种特殊的继承方式,用于解决多继承中的菱形继承问题。通过使用虚拟继承,可以确保在派生类中只包含一个共享的基类子对象,避免了数据冗余和虚函数二义性的问题
虚拟继承可以解决菱形继承的二义性和数据冗余问题
如上面继承关系在 Student 和 Teacher 的继承 Person 时使用虚拟继承,即可解决问题
需要注意的是,虚拟继承不要在其他地方去使用
#include<iostream>
#include<string>
using namespace std;
class Person
{
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";
a.Student::_name = "xxx";
a.Teacher::_name = "yyy";
}
int main()
{
Test();
return 0;
}
这样就可以正常调用了
为了探究虚继承的本质,我们可以通过调试观察:
#include<iostream>
#include<string>
using namespace std;
class A
{
public:
int _a;
};
// class B : public A
class B : virtual public A
{
public:
int _b;
};
// class C : public A
class C : virtual public A
{
public:
int _c;
};
class D : public B, public C
{
public:
int _d;
};
int main()
{
D d;
d.B::_a = 1;
d.C::_a = 2;
d._b = 3;
d._c = 4;
d._d = 5;
return 0;
}
在未使用虚继承时,这里可以看到数据冗余
以下是使用虚继承后的内存
这里分析出 D 对象中将 A 放到的了对象组成的最下面,这个 A 同时属于 B 和 C ,那么 B 和 C 如何去找到公共的 A 呢?这里是通过了 B 和 C 的两个指针,指向的一张表。这两个指针叫虚基表指针,这两个表叫虚基表。虚基表中存的偏移量。通过偏移量可以找到下面的 A