继承
文章目录
1.继承的概念及定义
1.1继承的概念
继承机制是面向对象程序设计中使代码可以复用的最重要的手段。它允许程序员在保持原有类特性的基础上进行扩展,增加功能,这样产生的新的类,叫派生类。
举个例子:比如说现在想定义学生管理系统。
- 学生
- 老师
- 舍管阿姨
- 保安大叔
没有继承之前,要定义四个类。但是这四个类有很多重复的东西。
比如说name,age,telephone,address等都是共有的属性。假如这四个类都定义了这些成员变量和成员函数,就重复了。因此可以定义一个公共的Person类,让四个类来继承它。
继承的目的:类之间的复用
之前接触的都是函数的复用,继承是类设计层次的复用。
下面这段自己放vs调试截图贴过来。
class Person{
public:
void Print(){
cout<<"name: "<<_name<<endl;
cout<<"age: "<<_age<<endl;
}
protected:
string _name="xiaoming";//姓名
int _age=10;//年龄
};
//继承后父类的Person的成员(成员函数+成员变量)都会变成子类的一部分。
///使用监控窗口看Student和Teacher对象,可以看到变量的复用。
//调用Print可以看到成员函数的复用
class Student :public Person
{
protected:
int _stuid;//学号
};
class Teacher:public Person
{
protected:
int _jobid;//工号
};
int main(){
Student s;
Teacher t;
s.Print();
t.Print();
}
1.2继承定义
1.2.1继承格式
class Student :public Person
派生类 继承方式 基类
1.2.2继承关系和访问限定符
继承方式有三种:public继承,protected继承,private继承
访问限定符有三种:public访问,protected访问,private访问
1.2.3继承基类成员访问方式的变化
类成员/继承方式 | public继承 | protected继承 | private继承 |
---|---|---|---|
基类的public成员 | 派生类的public成员 | 派生类的protected成员 | 派生类的private成员 |
基类的protected成员 | 派生类的protected成员 | 派生类的protected成员 | 派生类的private成员 |
基类的private成员 | 在派生类中不可见 | 在派生类中不可见 | 在派生类中不可见 |
总结
- 基类private成员在派生类中无论以什么方式继承都是不可见的。这里的不可见是指基类的私有成员还是被继承到了派生类对象中,但是语法上限制派生类对象不管在类里面还是类外面都不能去访问它
- 基类private成员在派生类中是不能被访问,如果基类成员不想在类外直接被访问,但需要在派生类中能访问,就定义为protected。可以看出保护成员限定符是因继承而出现的。
- 基类的私有成员都是不可见的。基类的其他成员在子类的访问方式== m i n ( 成 员 在 基 类 的 访 问 限 定 符 , 继 承 方 式 ) min(成员在基类的访问限定符,继承方式) min(成员在基类的访问限定符,继承方式) [ 其 中 p u b l i c > p r o t e c t e d > p r i v a t e ] [其中public>protected>private] [其中public>protected>private]
- 使用关键字class时的默认继承方式是private,使用struct时默认的继承方式是public。不过最好写出继承方式
- 在实际运用中一般是public继承,几乎很少使用protected/private继承,也不提倡使用protected/private继承,因为protected/private继承下来的成员都只能在派生类的类里面使用。实际中扩展维护性不强。
- 类中的私有和保护在当前类没有差别
- 在继承后的子类中有差别,private的在子类中不可见
- 由前面的知识,成员函数是存在代码段的,不是在对象里的。编译器的语法控制谁可以用,没继承就不能用,继承了就可以用。
class Person{
public:
void Print(){
cout<<"name: "<<_name<<endl;
cout<<"age: "<<_age<<endl;
}
protected:
string _name="xiaoming";//姓名
private:
int _age=10;//年龄
};
class Student :public Person
{
//- 类中的私有和保护在当前类没有差别
//在继承后的子类中有差别,private的在子类中不可见
void f(){
cout<<_age<<endl;//error:"不可见"虽然有,但是不能访问
}
protected:
int _stuid;//学号
};
2.基类和派生类对象赋值转换
- 派生类对象可以赋值给基类的对象/基类的指针/基类的引用。形象地说叫“切片”,意义就是把派生类中父类那部分切过来赋值过去。
- 基类对象不能赋给派生类对象
- 基类的指针可以通过强制类型转化赋值给派生类的指针。但是必须基类的指针式指向派生类对象时才是安全的。这里基类如果是多态类型,可以使用RTT(Run-Time Type Information)的dynamic cast来进行识别后的安全转换。
class Person{
public:
void Print(){
cout<<"name: "<<_name<<endl;
cout<<"age: "<<_age<<endl;
}
protected:
string _name="xiaoming";//姓名
int _age=10;//年龄
};
//继承后父类的Person的成员(成员函数+成员变量)都会变成子类的一部分。
///使用监控窗口看Student和Teacher对象,可以看到变量的复用。
//调用Print可以看到成员函数的复用
class Student :public Person
{
protected:
int _stuid;//学号
};
class Teacher:public Person
{
protected:
int _jobid;//工号
};
int main(){
Person p;
Student s;
//子类和父类之间的赋值兼容规则
//1.子类对象可以赋值给父类的对象,指针或者引用(切片)
p=s;
Person* ptr=&s;
Person& ref=s;
}
现在先记住这个规则,还没到理解的时候。
反过来行不行呢?
s = p; | Student* ptr=&p; | Student& ref=p; |
---|---|---|
不行 | 有时候可以 | 有时候可以 |
缺少的部分没办法 | Student* sprt=(Student*)ptr | |
这个父类的指针本身指向子类对象的[dynamic cast再提及] |
3.继承中的作用域
隐藏(重定义)的定义和特性
在继承体系中的基类和派生类都有独立的作用域。若子类和父类中有同名成员,子类成员将屏蔽父类对同名成员的直接访问,这种情况叫隐藏,也叫重定义。
- 在继承体系中基类和派生类都有独立的作用域
- 子类和父类中有同名成员,子类成员将屏蔽父类对同名成员的直接访问,这种情况叫隐藏,也叫重定义(在子类成员函数中,可以使用基类::基类成员 显示访问)
- 需要注意的是如果是成员函数的隐藏,只需要函数名相同就构成隐藏
- 注意在实际中在继承体系里最好不要定义同名的成员
就如同局部变量碰到全局变量先用局部变量的就近原则。
会优先到自己的类里去找,再到父类里去找。
class Person{
protected:
string _name="123;
int _num=111;
};
class Student :public Person{
public:
void Print(){
cout<<"姓名"<<_name<<endl;
cout<<"学号:"<<_num<<endl;
cout<<"学号:"<<Person::_num<<endl;
}
protected:
//当子类和父类有相同的成员时,子类隐藏了父类的。
int _num=999;
}
int main(){
Student s;
s.Print();
}
这两个类中的函数是什么关系?
A、重载 B、重写 C、重定义 D、以上都不对
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();//error:因为隐藏了所以调不动无参数fun()
b.fun(1);
b.A::fun();
}
4.派生类的4个默认成员函数
在类和对象的基本知识学习中,我们知道一个类有默认的六个成员函数。这里要来学习派生类的4个默认成员函数。
4.1派生类的默认构造函数
- 派生类的默认构造函数
- 分为两个部分,一个父类的,一个自己的。分得很清。
- 父类的部分是调用父类的构造函数,析构函数清理的时候调用父类的。
- 派生类自己的对象和单独一个类的构造函数一样,看内置类型和自定义类型决定
- 不管如何,父类部分的一定调用父类部分去完成。
- 如果自己不写显示,编译器就会调用父类的默认构造函数(无参,全缺省,半缺省)
class Person{
public:
Person(const char* name="1234")
:_name(name)
{ cout<<"Person()"<<endl; }
Person(const Person& p)
:_name(p._name)
{
cout<<"Preson(const Person& p)"<<endl;
}
Person& operator=(const Person& p){
cout<<"Person& operator=(const Person& p)"<<endl;
if(this!=&p){
this->_name=p._name;
}
return *this;
}
~Person(){
cout<<"~Person()"<<endl;
}
protected:
string _name;
};
class Student:public Person
{
public:
Student(const char* name="Z3",int stuid)
:_name(name)//error:应该调父类的,不能自己显示初始化。
//Person(name)自己显示调。但是它自己也会调。但是自己调只会调三种(无参,缺省,全缺省)
,_stuid(stuid)
{}
protected:
int _stuid;
};
int main(){
Student s;
Student d("pER",123);
}
#include<iostream>
#include<string>
using namespace std;
class Person{
public:
Person(const char* name="1234")
:_name(name)
{ cout<<"Person()"<<endl; }
Person(const Person& p)
:_name(p._name)
{
cout<<"Preson(const Person& p)"<<endl;
}
Person& operator=(const Person& p){
cout<<"Person& operator=(const Person& p)"<<endl;
if(this!=&p){
this->_name=p._name;
}
return *this;
}
~Person(){
cout<<"~Person()"<<endl;
}
protected:
string _name;
};
class Student:public Person
{
public:
Student(const char* name="Z3",int stuid = 1)
: Person(name),//自己显示调。但是它自己也会调。但是自己调只会调三种(无参,缺省,全缺省)
_stuid(stuid)
{}
protected:
int _stuid;
};
int main(){
Student s;
Student d("pER",123);
}
4.2派生类的默认拷贝构造函数
-
默认拷贝构造函数
- 父类的部分调用父类的拷贝构造
- 自己的部分遵守之前一个类的默认拷贝构造规则(值拷贝)
- 自己如何写呢?
class Person{ public: Person(const char* name="1234") :_name(name) { cout<<"Person()"<<endl; } Person(const Person& p) :_name(p._name) { cout<<"Preson(const Person& p)"<<endl; } Person& operator=(const Person& p){ cout<<"Person& operator=(const Person& p)"<<endl; if(this!=&p){ this->_name=p._name; } return *this; } ~Person(){ cout<<"~Person()"<<endl; } protected: string _name; }; class Student:public Person { public: Student(const char* name,int stuid) :Person(name) ,_stuid(stuid) {cout<<"Student()"<<endl;} Student(const Student& s) :Person(s)///父类部分应该要父类对象的,但是根据前面所学可以切片,这里语法意义上天然支持 ,_stuid(s._stuid){ cout<<"Student(const Student& s) "<<endl; } protected: int _stuid; }; int main(){ Student s1("pER",123); Student s2(s1); return 0; }
4.3派生类的默认赋值函数
-
默认赋值
- 先调父类的
class Person{ public: Person(const char* name="1234") :_name(name) { cout<<"Person()"<<endl; } Person(const Person& p) :_name(p._name) { cout<<"Preson(const Person& p)"<<endl; } Person& operator=(const Person& p){ cout<<"Person& operator=(const Person& p)"<<endl; if(this!=&p){ this->_name=p._name; } return *this; } ~Person(){ cout<<"~Person()"<<endl; } protected: string _name; }; class Student:public Person { public: Student(const char* name,int stuid) :Person(name) ,_stuid(stuid) {cout<<"Student()"<<endl;} Student(const Student& s) :Person(s) ,_stuid(s._stuid){ cout<<"Student(const Student& s) "<<endl; } Student& operator=(const Student& s){ if(this!=&s){ operator=(s);///隐藏了父类的operator=,两个是同名函数。导致递归 //正解:Person::operator=(s); _stuid=s._stuid; cout<<"Student& operator=(const Student& s)"<<endl; } return *this; } protected: int _stuid; }; int main(){ Student s1("pER",123); Student s2(s1); Student s3("ppp",444); s1=s3; return 0; }
可以使用Coredump查看具体在哪行出现的问题,但是目前服务器和虚拟机都没法成功。
class Person{
public:
Person(const char* name="1234")
:_name(name)
{ cout<<"Person()"<<endl; }
Person(const Person& p)
:_name(p._name)
{
cout<<"Preson(const Person& p)"<<endl;
}
Person& operator=(const Person& p){
cout<<"Person& operator=(const Person& p)"<<endl;
if(this!=&p){
this->_name=p._name;
}
return *this;
}
~Person(){
cout<<"~Person()"<<endl;
}
protected:
string _name;
};
class Student:public Person
{
public:
Student(const char* name,int stuid)
:Person(name)
,_stuid(stuid)
{cout<<"Student()"<<endl;}
Student(const Student& s)
:Person(s)
,_stuid(s._stuid){
cout<<"Student(const Student& s) "<<endl;
}
Student& operator=(const Student& s){
if(this!=&s){
Person::operator=(s);
_stuid=s._stuid;
cout<<"Student& operator=(const Student& s)"<<endl;
}
return *this;
}
protected:
int _stuid;
};
int main(){
Student s1("pER",123);
Student s2(s1);
Student s3("ppp",444);
s1=s3;
return 0;
}
4.4派生类的默认析构函数
-
析构
- 特殊
- 子类的析构函数和父类的析构函数构成隐藏,因为他们的名字会被编译器处统一处理成destructor(和多态有关)
- 结束时会自动调用父类的析构函数,因为这样才能保证先析构子类再析构父类。
class Person{ public: Person(const char* name="1234") :_name(name) { cout<<"Person()"<<endl; } Person(const Person& p) :_name(p._name) { cout<<"Preson(const Person& p)"<<endl; } Person& operator=(const Person& p){ cout<<"Person& operator=(const Person& p)"<<endl; if(this!=&p){ this->_name=p._name; } return *this; } ~Person(){ cout<<"~Person()"<<endl; } protected: string _name; }; class Student:public Person { public: Student(const char* name,int stuid) :Person(name) ,_stuid(stuid) {cout<<"Student()"<<endl;} Student(const Student& s) :Person(s) ,_stuid(s._stuid){ cout<<"Student(const Student& s) "<<endl; } Student& operator=(const Student& s){ if(this!=&s){ operator=(s);///隐藏了父类的operator=,两个是同名函数。导致递归 //正解:Person::operator=(s); _stuid=s._stuid; cout<<"Student& operator=(const Student& s)"<<endl; } return *this; } ~Student()//子类的析构函数和父类的析构函数构成隐藏,因为他们的名字会被编译器处统一处理成destructor(和多态有关) { ~Person();///和~Student构成隐藏,而正确的用法不需要你显示调用,结束后会自动调用父类 //Person::~Person(); cout<<"~Student()"<<endl; //结束时会自动调用父类的析构函数,因为这样才能保证先析构子类再析构父类。 } protected: int _stuid; }; int main(){ Student s1("pER",123); return 0; }
Person();
Student();
~Person(); //应该去掉
~Student();
~Person();
4.5总结
总的来说都是自己管自己的,除了析构函数派生类结束会自动去调用父类,前三个都是要显式调用的。
class Person{
public:
Person(const char* name="1234")
:_name(name)
{ cout<<"Person()"<<endl; }
Person(const Person& p)
:_name(p._name)
{
cout<<"Preson(const Person& p)"<<endl;
}
Person& operator=(const Person& p){
cout<<"Person& operator=(const Person& p)"<<endl;
if(this!=&p){
this->_name=p._name;
}
return *this;
}
~Person(){
cout<<"~Person()"<<endl;
}
protected:
string _name;
};
class Student: public Person
{
public:
Student(const char* name,int num)
:Person(name)
,_num(s._num)
{
cout<<"Student()"<<endl;
}
Student(const Student& s)
:Person(s)
,_num(s._num)
{
cout<<"Student(const Student& s)"<<endl;
}
Student& operator=(const Student& s){
if(this!=&s){
Person::operator=(s);
_num=s._num;
}
return *this;
}
~Student(){
cout<<"~Student()"<<endl;
}
}
-
面试题
如何设计一个不能被继承的类
构造函数私有或者析构私有【实际上这是标准的单例模式,单例模式不应该实例化,只能通过接口访问其单例】
单例模式之后会提饿汉和懒汉两种方式
class A{ private: A(){} }; class B:public A{ }
5.继承与友元
友元关系不能被继承,也就是说基类友元不能访问子类私有和保护成员。
class Student;
class Person{
public:
friend void Display(const Person& p,const Student& s);
protected:
string _name;
};
class Student:public Person
{
protected:
int stuNnum;
}
void Display(const Person& p,const Student& s){
cout<<p._name<<endl;
cout<<p._stuNum<<endl;
}
int main(){
Person p;
Student s;
Display(p,s);
}
6.继承与静态成员
基类定义了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
{
protected:
int _stuNum;
};
int main(){
Person p;
Student s;
p._name="1";
s._name="2";
p._count=1;//&p._count
s._count=2;//&s._count
cout<<Person::_count<<endl;
}
7.复杂的菱形继承及菱形虚拟继承
7.1菱形继承的问题以及解决方案
单继承:一个子类只有一个直接父类时称这个继承关系为单继承
多继承:一个子类有两个或以上直接父类时称这个继承关系为多继承
【早期吃螃蟹的一个坑】
菱形继承:菱形继承是多继承的一种特殊情况。
继承是类的复用,而Assiatant里面有两份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;
a._name="peter";
//需要显示指定访问哪个父类的成员可以解决二义性问题,但是数据冗余问题没法解决
a.Student::_name="xxxx";
a.Student::_name="yyyy";
}
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;
//需要显示指定访问哪个父类的成员可以解决二义性问题,但是数据冗余问题没法解决
a.Student::_name="xxxx";
a.Teacher::_name="yyyy";
}
- c++官方如何解决菱形继承的二义性和数据冗余问题。如上面的继承关系,在Student和Teacher继承Person时使用虚继承,即可解决问题。需要注意的是,虚拟继承不要在其他地方去用
- 注意这里的virtual和多态的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;
a._name="peter"; //
a.Student::_name="xxxx"; //
a.Student::_name="yyyy"; //
//以上引用的是同一个_name
}
7.2虚拟继承的底层机理
由于直接观察调式窗口不合适(经过编译器的优化不方便观察一个数据三个类里都有)所以要通过内存窗口
这里采用内存对象模型(对象在内存中是怎么存的?)
class A{
public:
int _a;
};
class B:public A{
public:
int _b;
};
class C:public A{
public:
int _c;
};
class D:public B,public C{
public:
int _d;
};
int main(){
D d;
cout<<sizeof(d)<<endl;//20
d.B::_a=1;
d.C::_a=2;
d._b=3;
d._c=4;
d._d=5;
}
#include<iostream>
#include<algorithm>
using namespace std;
class A {
public:
int _a;
};
class B :virtual public A {
public:
int _b;
};
class C :virtual public A {
public:
int _c;
};
class D :public B, public C {
public:
int _d;
};
int main() {
D d;
cout << sizeof(d) << endl;//24
d.B::_a = 1;
d.C::_a = 2;
d._b = 3;
d._c = 4;
d._d = 5;
d._a = 6;
}
00667bdc,00667be4,看起来有点像指针。
其中D类对象当中的_a成员被放到了最后,而在原来存放两个_a成员的位置变成了两个指针,这两个指针叫虚基表指针,它们分别指向一个虚基表。
虚基表中包含两个数据,第一个数据是为多态的虚表预留的存偏移量的位置(这里我们不必关心),第二个数据就是当前类对象位置距离公共虚基类的偏移量。
也就是说,这两个指针经过一系列的计算,最终都可以找到成员_a。
-
偏移量在什么时候能用到呢?(比如切片)
-
B b; C c; D d; b=d; c=d; //d对象里除了有_c还有_a.还要给_a的值 //d对象赋值给c,4的值可以直接给过去,但是_a的值需要解引用找到偏移量再给c赋给过去
-
这里后面的空一行0是因为基类对象可以还有很多其他的数据
-
存偏移量的表叫虚基表(这个表就是用来找虚基类的)
-
这里的A通常被叫做虚基类
-
这两个指针也可以叫做虚基表指针
-
目前这个例子虽然少了4个字节却多了8个字节,然而如果虚基类的成员数据是数组的话就划算大了。
- 将虚基类改成数组成员并且可以打印出两个情况下的size
-
这里有一定的效率损失(额外解引用找)但是随着硬件发展是微乎其微的。
-
-
解决原理
- 把虚基类的成员放到派生类的最下面
- 并且存两个指针指向虚基表,虚基表里面存的是偏移量来计算找它的位置
-
实际中不到万不得已不要把类的关系设计成菱形继承
8.继承的总结和反思
- c++语法复杂,多继承就是一个体现,有了多继承,就存在菱形继承,有了菱形继承就需要菱形虚拟继承,底层实现就很复杂。所以一般不建议设计出多继承,一定不要设计出菱形继承。否则性能上都有问题
- 多继承可以认为是c++的缺陷之一,很多后来的面向对象语言没有继承,如Java
9.继承和组合
-
public继承是一种is-a的关系。也就是说每个派生类对象都是一个基类对象.
-
//Car和BMw Car 和Benz构成is-a关系 //宝马是一个车,奔驰也是一个车. class Car{ protected: }; class BMW:public Car{ }; class Benz:public Car{ };
-
-
组合是一种has-a的关系。假设B组合了A,每个B对象中都有一个A对象.
-
//Tire和Car构成has-a的关系 class Tire{ protected: string _brand="xx"; //品牌 size_t _size=17; ///尺寸 }; class Car{ protected: string _color="白色";//颜色 string _num="浙J:xxxx";//车牌 Tire _t; //轮胎 };
-
-
优先使用对象组合,而不是类继承.
-
继承和组合两者都完成了类层次的复用.
- 继承是一种白箱复用.父类对子类基本是透明的.但是它一定程度破坏了父类的封装.
- 父类的protected在类外面不可以用,但是继承后子类可以用.子类继承后是子类的一部分,都在子类里.
- 组合是一种黑箱复用,C对D是不透明的,C保持着他的封装.
- 对比中认为组合更好
- 耦合是一种关联关系
- 组合的类耦合度更低
- 继承的类是一种高耦合,容易互相影响
- 继承是一种白箱复用.父类对子类基本是透明的.但是它一定程度破坏了父类的封装.
//A和B的关系就是继承.
class A{};
class B :publc A{};
//C和D的关系就是组合
class C{};
Class D
{
C c;
}
10.面试题
c++的缺陷有哪些?
- 多继承就是一个问题–>菱形继承–>虚继承–>底层结构的对象模型非常复杂且有一定效率损失。
- 什么是菱形继承?菱形继承的问题是什么?如何解决?虚继承–>底层原理是什么
- 使用继承还是组合呢?
- 看场景,更符合is-a就使用继承
- 更符合has-a就使用组合
- 两者都可以的优先考虑组合