一、概念
继承(inheritance)机制是面向对象程序设计使代码可以复用的最重要的手段,它允许我们在保持原有类特性的基础上进行扩展,增加方法(成员函数)和属性(成员变量),这样产生新的类,称派生类。继承呈现了面向对象程序设计的层次结构,体现了由简单到复杂的认知过程。以前我们接触的函数层次的复用,继承是类设计层次的复用。
大白话说继承:继承就是我们生活中说的继承家产,就是家低很厚,我就不用努力了,等着继承家产就行,代码的话就是说用类来描述世界,例如:描述人,人人都有手脚,那么我们用类来描述人的话每个人都要描述手脚太麻烦了,既然大家都有,那么假设2类来继承1类(这个类包含手脚的描述),2类就不用描述人的手脚了。
总结:大家都有的放到一个基类/父类里面,其他类继承父类就得了。
二、语法形式/书写方式
代码示例:
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; // 职称//老师特有
};
规律:
1、父类的私有成员,派生类不管是什么继承都不能访问(派生类外面和里面都不能访问,基类可自己以访问)但是会继承。
class Person//公共的叫基类/父类
{
public:
// 进⼊校园/图书馆/实验室刷⼆维码等⾝份认证
void identity()const
{
cout << "void identity()" << _name << endl;
}
private:
string _name = "张三"; // 姓名
string _address="xxxx"; // 地址
string _tel="1008711"; // 电话
int _age = 18; // 年龄
};
//继承的书写方式
class Student : private Person//学生也是人,具有人的特征//继承父类的叫子类/派生类
{
public:
// 学习
void study()
{
_name = "李四";//不能访问
// ...
}
protected:
int _stuid=2023; // 学号//学生特有
};
int main()
{
Student s;
//s.identity();
//s.identity();
return 0;
}
2、派生类只要是pubild继承,派生类成员(pubilc、private、protected)可以访问父类成员(pubilc、protected),类外面(派生类实例化出来的对象)能访问基类的puclic成员。如果是private、protected 继承,派生类成员(pubilc、private、protected)也可以访问父类成员(pubilc、protected)但是类外面(派生类实例化出来的对象)不能访问基类的public成员。
总结:不管是什么继承,派生类所有的成员(public、protected、private)都能访问父类的 public和 protected 成员,只有 public 继承派生类实例化出来的对象只能能访问父类的public成员,其他继承派生类实例化出来的对象都不能访问父类所有的成员。
3、一般是pubilc继承,父类成员不包含private。父类实例化出来的对象可以访问pubilc成员不能访问 protected 成员,但是派生类所有成员可以访问父类 protectecd、public 成员实例化出来的对象只能访问父类的 public 成员。
4、如果不写继承方式,class 默认private 继承,struct 默认为 public 继承,建议最好写出来。
特殊情况:如果想访问父类的私有成员,可以在父类里面的定义一个公有成员函数访问,这样派生类就可以访问私有成员。
三、继承类模板
代码示例:
namespace bit
{
//template<class T>
//class vector
//{};
// stack和vector的关系,既符合is-a,也符合has-a
template<class T>
class stack : public std::vector<T>
{
public:
void push(const T& x)
{
// 基类是类模板时,需要指定⼀下类域,
// 否则编译报错:error C3861: “push_back”: 找不到标识符
// 因为stack<int>实例化时,也实例化vector<int>了
// 但是模版是按需实例化,push_back等成员函数未实例化,所以找不到
vector<T>::push_back(x);
//push_back(x);
}
void pop()
{
vector<T>::pop_back();
}
const T& top()
{
return vector<T>::back();
}
bool empty()
{
return vector<T>::empty();
}
};
}
注意:这种继承是不太建议的,原因:换一种容器呢?继承也要改一改,麻烦。
四、基类和派生类之间的转换(PS:只有公有继承有)
基类和派生类之间的转换也叫赋值兼容转换(不产生临时对象),是一种特殊处理。
public继承的派生类对象可以赋值给基类的指针/基类的引用。这里有个形象的说法叫切片或者切割。寓意把派生类中基类那部分切出来,基类指针或引用指向的是派生类中切出来的基类那部分。
基类对象不能赋值给派生类对象。
基类的指针或者引用可以通过强制类型转换赋值给派生类的指针或者引用。但是必须是基类的指针是指向派生类对象时才是安全的。这里基类如果是多态类型,可以使用 RTTI ( Run-Time TypeInformation ) 的 dynamic_cast 来进行识别后进行安全转换。 (ps:这个我们后面类型转换章节再单独专门讲解,这里先提一下)
代码示例:
class Person
{
protected:
string _name; // 姓名
string _sex; // 性别
int _age; // 年龄
};
class Student : public Person
{
public:
int _No; // 学号
};
int main()
{
Student sobj;
// 1.派⽣类对象可以赋值给基类的指针/引⽤
Person* pp = &sobj;
Person& rp = sobj;
return 0;
}
注意: 一般来说类型转换是会产生临时对象的,但是赋值兼容转换没有。基类对象不能赋值给派生类对象(强转也不行),例如:
Student sobj;
Person oob;
sobj = oob;
图片解疑问:
五、继承中的作用域
隐藏规则:
1.在继承体系中基类和派生类都有独立的作用域。
2.派生类和基类中有同名成员,派生类成员将屏蔽基类对同名成员的直接访问,这种情况叫隐藏。(在派生类成员函数中,可以使用基类:基类成员显示访问)
3.需要注意的是如果是成员函数的隐藏,只需要函数名相同就构成隐藏。
4.注意在实际中在继承体系里面最好不要定义同名的成员。
代码示例:
class Person
{
protected:
int _age=18; // 年龄
};
class Student : public Person
{
public:
void retage()
{
cout << "_age:" << _age << endl;//不会访问到基类的_age——隐藏
}
private:
int _age=99;
};
int main()
{
Student s;
s.retage();
return 0;
}
运行结果:
如果想访问基类的_age可以指定作用域。如:
cout << "_age:" << Person::_age << endl;//不会访问到基类的_age——隐藏
注意:工作中不建议弄个隐藏,为什么?代码维护太难、成本上升。
六、派生类的默认成员函数
1、4个常见的默认成员函数
实际上是6个默认成员函数,默认的意思就是指我们不写,编译器会变我们自动生成一个,那么在派生类中,这几个成员函数是如何生成的呢?
1)派生类的构造函数必须调用基类的构造函数初始化基类的那一部分成员。如果基类没有默认的构造函数,则必须在派生类构造函数的初始化列表阶段显示调用。
基类的默认构造跟之前类和对象说的一样,派生类的默认构造对内置类型有的编译器会给初始化为0,有的会给个随机值。我们实例化一个派生类的对象之后,会调用基类的默认构造,因为派生类是继承基类的,所以会自动调用基类的默认构造。
代码实例1:
class Person
{
public:
Person()
{
_age = 18;
cout << "调用Person的默认构造" << endl;
}
protected:
int _age; // 年龄
};
class Student : public Person
{
public:
void retage()
{
cout << "_age:" << Person::_age << endl;//不会访问到基类的_age——隐藏
}
private:
int _age=99;
};
int main()
{
Student s;
return 0;
}
运行结果:
代码示例2:
class Person
{
public:
Person(const char* name)
{
_name = name;
}
protected:
int _age; // 年龄
string _name;
};
class Student : public Person
{
public:
Student(const char* name,const char* address,int num)
:Person(name)//把Person看做一个整体,不能这么写_name(name)
,_address(address)
,_num(num)
{
}
private:
string _address="李四";
int _num = 22211;
};
int main()
{
Student s("小龙","街上",78);
return 0;
}
注意: 初始化列表是按照声明的顺序来走的,派生类初始化列表默认先走基类的初始化(例如:上面的Person(name)),再按照自己声明的顺序来初始化。
监视查看:
代码示例3:
class Person
{
public:
Person(const char* name)
{
_name = name;
_age = 20;
}
Person()
{
_age = 19;
_name = "小三";
cout << "调用Person的默认构造" << endl;
}
protected:
int _age=18; // 年龄
string _name;
};
class Student : public Person
{
public:
Student(const char* name,const char* address,int num)
:_address(address)
,_num(num)
{
}
private:
string _address="李四";
int _num = 22211;
};
int main()
{
Student s("小龙","街上",78);
return 0;
}
注意:不写Person(name),导致接收不到传过来的值,但是会调用Person默认构造,默认构造是不会接收到name值。
2)派生类的拷贝构造函数必须调用基类的拷贝构造完成基类的拷贝初始化,有资源的要进行深拷贝。
代码示例1:
class Person
{
public:
Person(const char* name)
{
_name = name;
_age = 20;
}
Person()
{
_age = 19;
_name = "小三";
cout << "调用Person的默认构造" << endl;
}
Person(Person& p)
{
_age = p._age;
_name = p._name;
cout << "Person(person& p)" << endl;
}
protected:
int _age=18; // 年龄
string _name;
};
class Student : public Person
{
public:
Student(const char* name,const char* address,int num)
:_address(address)
,_num(num)
{
}
private:
string _address="李四";
int _num = 22211;
};
int main()
{
Student s("小龙","街上",78);
Student s1(s);//浅拷贝
return 0;
}
代码示例2:
class Person
{
public:
Person(const char* name)
{
_name = name;
_age = 20;
}
Person()
{
_age = 19;
_name = "小三";
cout << "调用Person的默认构造" << endl;
}
Person(Person& p)
{
_age = p._age;
_name = p._name;
cout << "Person(person& p)" << endl;
}
protected:
int _age=18; // 年龄
string _name;
};
class Student : public Person
{
public:
Student(const char* name,const char* address,int num)
:_address(address)
,_num(num)
,Person(name)
{
}
private:
string _address="李四";
int _num = 22211;
};
int main()
{
Student s("小龙","街上",78);
Student s1("小王","大门",90);
s = s1;//赋值重载,没有资源调用默认的
return 0;
}
3)派生类的operator=必须要调用基类的operator=完成基类的复制。需要注意的是派生类的operator=隐藏了基类的operator=,所以显示调用基类的operator=,需要指定基类作用域。
代码示例:
class Person
{
public:
Person(const char* name)
{
_name = name;
_age = 20;
}
Person()
{
_age = 19;
_name = "小三";
cout << "调用Person的默认构造" << endl;
}
Person(Person& p)
{
_age = p._age;
_name = p._name;
cout << "Person(person& p)" << endl;
}
const Person& operator=(const Person& s)
{
if (this != &s)
{
_age = s._age;
_name = s._name;
}
return s;
}
protected:
int _age=18; // 年龄
string _name;
};
class Student : public Person
{
public:
Student(const char* name,const char* address,int num)
:_address(address)
,_num(num)
,Person(name)
{
}
Student(const char* address, int num)
:_address(address)
,_num(num)
{
}
const Student& operator=(const Student& s)//假设有资源
{
if (this!=&s)
{
Person::operator=(s);//一但跟基类牵扯到,必须调用基类的赋值重载
_address = s._address;
_num = s._num;
}
return s;
}
private:
string _address="李四";
int _num = 22211;
};
int main()
{
Student s("小龙","街上",78);
Student s1("小王","大门",90);
s = s1;//赋值重载,没有资源调用默认的
return 0;
}
4)派生类的析构函数会在被调用完成后自动调用基类的析构函数清理基类成员。因为这样才能保证派生类对象先清理派生类成员再清理基类成员的顺序。
派生类析构顺序:先析构自己,再析构基类。原因:派生类有时候会用到基类的东西,先析构基类的话,派生类无法析构基类的东西。所以我们在派生类不用显示调用析构基类的函数,正确写法直接析构派生类的资源,最后它会默认调用基类的析构函数。
代码示例:
class Person
{
public:
Person(const char* name)
{
_name = name;
_age = 20;
}
Person()
{
_age = 19;
_name = "小三";
cout << "调用Person的默认构造" << endl;
}
Person(Person& p)
{
_age = p._age;
_name = p._name;
cout << "Person(person& p)" << endl;
}
const Person& operator=(const Person& s)
{
if (this != &s)
{
_age = s._age;
_name = s._name;
}
return s;
}
~Person()
{
cout << "~Person()" << endl;
}
protected:
int _age=18; // 年龄
string _name;
};
class Student : public Person
{
public:
Student(const char* name,const char* address,int num)
:_address(address)
,_num(num)
,Person(name)
{
}
Student(const char* address, int num)
:_address(address)
,_num(num)
{
}
const Student& operator=(const Student& s)//假设有资源
{
if (this!=&s)
{
Person::operator=(s);//一但跟基类牵扯到,必须调用基类的赋值重载
_address = s._address;
_num = s._num;
}
return s;
}
~Student()
{
cout << "~Student()" << endl;
}
private:
string _address="李四";
int _num = 22211;
};
int main()
{
Student s("小龙","街上",78);
return 0;
}
运行结果:
5)派生类对象初始化先调用基类构造再调派生类构造。
6)派生类对象析构清理先调用派生类析构再调基类的析构。
7)因为多态中一些场景析构函数需要构成重写,重写的条件之一是函数名相同(这个我们多态章节会讲解)。那么编译器会对析构函数名进行特殊处理,处理成destructor(),所以基类析构函数不加virtual的情况下,派生类析构函数和基类析构函数构成隐藏关系。
2、不能被继承的类
方法一、C++98把构造函数私有化,这样派生类一旦示例化出对象,就会编译报错。
代码示例:
class Person
{
private://构造函数私有化
Person(const char* name)
{
_name = name;
_age = 20;
}
Person()
{
_age = 19;
_name = "小三";
cout << "调用Person的默认构造" << endl;
}
Person(Person& p)
{
_age = p._age;
_name = p._name;
cout << "Person(person& p)" << endl;
}
const Person& operator=(const Person& s)
{
if (this != &s)
{
_age = s._age;
_name = s._name;
}
return s;
}
~Person()
{
cout << "~Person()" << endl;
}
protected:
int _age=18; // 年龄
string _name;
};
class Student : public Person
{
public:
Student(const char* name,const char* address,int num)
:_address(address)
,_num(num)
,Person(name)
{
}
Student(const char* address, int num)
:_address(address)
,_num(num)
{
}
const Student& operator=(const Student& s)//假设有资源
{
if (this!=&s)
{
Person::operator=(s);//一但跟基类牵扯到,必须调用基类的赋值重载
_address = s._address;
_num = s._num;
}
return s;
}
~Student()
{
cout << "~Student()" << endl;
}
private:
string _address="李四";
int _num = 22211;
};
int main()
{
Student s("小龙","街上",78);
return 0;
}
方法二、C++11新增加一个关键字 final 表示这个类不能被继承。
代码示例:
//C++11增加一个final关键字,表示这个类不能被继承,final这个关键字是抄袭其他语言的
class Person final
{
public:
Person(const char* name)
{
_name = name;
_age = 20;
}
Person()
{
_age = 19;
_name = "小三";
cout << "调用Person的默认构造" << endl;
}
Person(Person& p)
{
_age = p._age;
_name = p._name;
cout << "Person(person& p)" << endl;
}
const Person& operator=(const Person& s)
{
if (this != &s)
{
_age = s._age;
_name = s._name;
}
return s;
}
~Person()
{
cout << "~Person()" << endl;
}
protected:
int _age=18; // 年龄
string _name;
};
class Student : public Person
{
public:
Student(const char* name,const char* address,int num)
:_address(address)
,_num(num)
,Person(name)
{
}
Student(const char* address, int num)
:_address(address)
,_num(num)
{
}
const Student& operator=(const Student& s)//假设有资源
{
if (this!=&s)
{
Person::operator=(s);//一但跟基类牵扯到,必须调用基类的赋值重载
_address = s._address;
_num = s._num;
}
return s;
}
~Student()
{
cout << "~Student()" << endl;
}
private:
string _address="李四";
int _num = 22211;
};
int main()
{
Student s("小龙","街上",78);
return 0;
}
七、继承和友元
友元关系不被继承。
代码示例:
class Student;
class Person
{
public:
friend void Print(const Person& p,const Student& s);
private:
int _age = 19;
string _name = "小三";
};
class Student:public Person
{
public:
private:
int _num = 20211;
};
void Print(const Person& p,const Student& s)
{
cout << p._age << endl;
cout << s._num << endl;//友元关系不能被继承,所以不能访_num;
}
int main()
{
Person p;
Student s;
Print(p, s);
return 0;
}
运行结果:
解决办法:既然不能被继承,那么我自己再声明我是你的友元以下不就行了。
class Student :public Person
{
public:
friend void Print(const Person& p, const Student& s);
private:
int _num = 20211;
};
八、继承和静态成员
class Student;
class Person
{
public:
int _age = 19;
string _name = "小三";
static int _i;
};
int Person::_i = 100;
class Student:public Person
{
public:
private:
int _num = 20211;
};
int main()
{
Person p;
Student s;
//他们不是同一个_name
cout << "p._name:" << &p._name << endl;
cout << "s._name:" << &s._name << endl;
//同一个_i
cout << "p._i:" << &p._i << endl;
cout << "s._i:" << &s._i << endl;
return 0;
}
运行结果:
九、单继承和多继承问题
单继承:一个派生类只有一个直接基类时称这个继承关系为单继承。
多继承:一个派生类有两个或以上直接基类时称这个继承关系为多继承,多继承对象在内存中的模型是,先继承的基类在前面,后面继承的基类在后面,派生类成员在放到最后面。
总结:派生类后的继承方式只有一个的就是单继承,继承方式有多个的就是多继承。
菱形继承:菱形继承是多继承的一种特殊情况。菱形继承的问题,从下面的对象成员模型构造,可以看出菱形继承有数据冗余和二义性的问题。支持多继承就一定会有菱形继承,像Java就直接不支持多继承,规避掉了这里的问题,所以实践中我们也是不建议设计出菱形继承这样的模型的。
菱形继承不规定是是一个标准的菱形,只要形成一个闭环就是菱形继承。
如图:
数据冗余和二义性:如上图,多继承哪个类,它有多个Person,每个Person包含名字和年龄等等数据,就会会造成数据冗余。二义性:多继承那个类访问Person时候不知道访问那个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 _majorCourse; // 主修课程
};
int main()
{
Assistant A;
cout << A._name << endl;//多个_name,不知道访问那个编译报错
return 0;
}
运行结果:
解决方法:指定类域,如:
cout << A.Teacher::_name << endl;
注意:这种办法没有解决数据冗余问题。
十、虚继承
那为了解决菱形继承问题,C++提出了虚继承,新增加了一个关键字:virtual
那么这个关键字加到菱形继承的那个部分呢?
如图:
代码示例:
//菱形继承
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; // 主修课程
};
int main()
{
Assistant A;
cout << A._name << endl;//底层只有一个_name
return 0;
}
注意:关于虚继承是怎么实现或者底层原理是什么,这里太复杂了,作者(我本人)还不懂。
十一、继承和组合
public继承是一种is-a的关系。也就是说每个派生类对象都是一个基类对象。
组合是一种has-a的关系。假设B组合了A,每个B对象中都有一个A对象。
继承允许你根据基类的实现来定义派生类的实现。这种通过生成派生类的复用通常被称为白箱复用(white-boxreuse)。术语“白箱”是相对可视性而言:在继承方式中,基类的内部细节对派生类可见。继承一定程度破坏了基类的封装,基类的改变,对派生类有很大的影响。派生类和基类间的依赖关系很强,耦合度高。
对象组合是类继承之外的另一种复用选择。新的更复杂的功能可以通过组装或组合对象来获得。对象组合要求被组合的对象具有良好定义的接口。这种复用风格被称为黑箱复用(black-boxreuse),因为对象的内部细节是不可见的。对象只以“黑箱”的形式出现。组合类之间没有很强的依赖关系,耦合度低。优先使用对象组合有助于你保持每个类被封装。
注意:白箱指的是底层代码的可见度高,反之黑箱的底层代码的可见度低。
优先使用组合,而不是继承。实际尽量多去用组合,组合的耦合度低,代码维护性好。不过也不太那么绝对,类之间的关系就适合继承(is-a)那就用继承,另外要实现多态,也必须要继承。类之间的关系既适合用继承(is-a)也适合组合(has-a),就用组合。
组合代码示例:
// Tire(轮胎)和Car(⻋)更符合has-a的关系,组合
class Tire {
protected:
string _brand = "Michelin"; // 品牌
size_t _size = 17; // 尺⼨
};
class Car {
protected:
string _colour = "⽩⾊"; // 颜⾊
string _num = "陕ABIT00"; // ⻋牌号
Tire _t1; // 轮胎
Tire _t2; // 轮胎
Tire _t3; // 轮胎
Tire _t4; // 轮胎
};
总结:继承的耦合度高,组合的耦合度低。能用组合就用组合。
完!!