C++进阶——继承
文章目录
前言
继承是C++的核心内容,也是C++实现多态的基础,更是C++为什么是面向对象的语言的原因之一。
一、继承的概念及定义
继承是面向对象程序设计中使代码可以复用的重要手段,就是一段代码其他的类也需要使用,这样我们就没有必要每次都再写一遍;并且我们还可以对其类特性的基础上进行功能上的扩展,增加新功能,这样产生的类,我们称为派生类(子类);继承呈现了面向对象程序设计的层次结构,体现了由简单到复杂的认知过程,继承是类设计层次的复用
继承在我个人的看法中,我更倾向于它是为了我们后面将讲到的多态而服务,代码复用和扩展也只是它顺带的功能。
二、继承的定义
class Person
{
public:
void Print()
{
cout << "name:" << _name << endl;
cout << "age:" << _age << endl;
}
protected:
string _name = "peter"; // 姓名
int _age = 18; // 年龄
};
class Student : public Person
{
protected:
int _stuid; // 学号
};
Person:被继承的基类(父类)
public:继承的方式
Student:派生类(子类)
三、继承基类成员访问方式的变化
这里我们在将上表从类的角度以及对象的角度概括一下:
类的角度
public继承:父类protected成员可以在子类类内访问,父类private成员无法访问
protected继承:父类public成员变成子类protected成员只能子类类内访问,父类private成员无法访问
private继承:父类所有成员变成private成员,private成员无法访问
对象角度:
public继承:父类public成员可以访问,父类protected成员不可以访问,父类private成员无法访问
protected继承:父类public成员变成子类protected成员不能访问,父类private成员无法访问
private继承:父类所有成员变成private成员,父类private成员无法访问
这里需要提一下(特殊关系,特殊成员的继承方式):
1.父类的友元跟子类没有关系,父类的友元不是子类的友元,友元关系不能继承
2.基类定义了static静态成员,则整个继承体系里面只有一个这样的成员。无论派生出多少个子类,都只有一个static成员实例,它不属于任何一个类 。
继承基类成员访问方式的变化总结:
-
基类private成员在派生类中无论以什么方式继承都是不可见的。这里的不可见是指基类的私有成员还是
被继承到了派生类对象中,但是语法上限制派生类对象不管在类里面还是类外面都不能去访问它。 -
基类private成员在派生类中是不能被访问,如果基类成员不想在类外直接被访问,但需要在派生类中能
访问,就定义为protected。可以看出保护成员限定符是因继承才出现的。 -
实际上面的表格我们进行一下总结会发现,基类的私有成员在子类都是不可见。基类的其他成员在子类
的访问方式 == Min(成员在基类的访问限定符,继承方式),public > protected > private。 -
使用关键字class时默认的继承方式是private,使用struct时默认的继承方式是public,不过最好显示的
写出继承方式。 -
在实际运用中一般使用都是public继承,几乎很少使用protetced/private继承,也不提倡使用
protetced/private继承,因为protetced/private继承下来的成员都只能在派生类的类里面使用,实际中
扩展维护性不强。
四、派生类的默认成员函数
6个默认成员函数,“默认”的意思就是指我们不写,编译器会变我们自动生成一个,那么在派生类中,这几个
成员函数是如何生成的呢?
class Person
{
public :
Person(const char* name = "peter")
: _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)
: 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 ; //学号
};
void main()
{
Student s1 ("jack", 18);
Student s2 (s1);
Student s3 ("rose", 17);
s1 = s3 ;
}
-
派生类的构造函数必须调用基类的构造函数初始化基类的那一部分成员。如果基类没有默认的构造函
数,则必须在派生类构造函数的初始化列表阶段显示调用(这里建议不管有没有,都把基类部分成员在派生类构造函数初始化列表初始化)。 -
派生类的拷贝构造函数必须调用基类的拷贝构造完成基类的拷贝初始化。
-
派生类的operator=必须要调用基类的operator=完成基类的复制。
-
派生类的析构函数会在被调用完成后自动调用基类的析构函数清理基类成员。因为这样才能保证派生类
对象先清理派生类成员再清理基类成员的顺序。 -
派生类对象初始化先调用基类构造再调派生类构造。
-
派生类对象析构清理先调用派生类析构再调基类的析构。
这里这几条到底说的时什么意思,这个希望大家上机实验一下,我们在调试的时候可以看到,子类在实例化对象的时候,子类对象内部会有父类对象的部分,就如下所示:
D类公有继承B ,C类,D类在实例化对象d时,会去调用B,C类构造函数去构造d对象中它们的那一部分。接下来进行一下拓展,如果B,C类也继承了同一个类,那么d对象的结构时什么样的呢?这将在后面的虚继承中又很大的作用。下面请看例子:
这里我们可以发现,A类部分被实例化了两次,这就是我们继承里面比较典型的例子,菱形继承,也是我们必须搞明白的一点。这个下面再讲。
现在我们知道了。子类在实例化对象的时候,会先去调用父类的构造函数去构造子类对象中包含的父类的部分,这就是,继承不是没有继承privated成员,而是没有显示而已,这个问题就可以得到很好的解释。
class A
{
public:
A()
{
cout <<"A()"<< endl;
}
private:
int a = 1;
};
class B:public A
{
public:
B()
{
cout << "B()" << endl;
}
private:
int b = 2;
};
class C :public A
{
public:
C()
{
cout << "C()" << endl;
}
private:
int c = 3;
};
class D :public B,public C
{
public:
D()
{
cout << "D()" << endl;
}
private:
int d = 4;
};
int main()
{
D d;
}
其实我们从结构中也可以看出来,它们构造的顺序,ABACD,这是他们的构造顺序,析构就不用多说,那么为什么B在C前呢?
这里就是我们继承时候格式的问题了因我们是class D :public B,public C,所以B在C 前大家只需要记住一句话,从上到下,从左到右的顺序就是构造的顺序。
这一部分知识点希望大家谨记,这是搞懂继承最基本的东西!!!
五、基类和派生类对象赋值转换
1.派生类对象可以赋值给基类的对象 / 基类的指针 / 基类的引用。这里有个形象的说法叫切片或者切
割。寓意把派生类中父类那部分切来赋值过去。
2.基类对象不能赋值给派生类对象
3.基类的指针可以通过强制类型转换赋值给派生类的指针。但是必须是基类的指针是指向派生类对象时才
是安全的。
class Person
{
protected :
string _name; // 姓名
string _sex; // 性别
int _age; // 年龄
};
class Student : public Person
{
public :
int _No ; // 学号
};
void Test ()
{
Student sobj ;
// 1.子类对象可以赋值给父类对象/指针/引用
Person pobj = sobj ;
Person* pp = &sobj;
Person& rp = sobj;
//2.基类对象不能赋值给派生类对象
sobj = pobj;
// 3.基类的指针可以通过强制类型转换赋值给派生类的指针
pp = &sobj
Student* ps1 = (Student*)pp; // 这种情况转换时可以的。
ps1->_No = 10;
pp = &pobj;
Student* ps2 = (Student*)pp; // 这种情况转换时虽然可以,但是会存在越界访问的问题
ps2->_No = 10;
}
什么叫做把父类的部分赋值过去呢?这里就要用到上文所提到的,子类对象中是有父类的数据成员的,只是没有显示,这也就是为什么子类对象可以给父类对象赋值,而父类对象不能给子类对象赋值的原因,就是因为父类对象没有子类部分的数据成员,只有它自己的,那么它拿什么去给子类对象赋值呢,程序中可不存在无中生有这个说法。至于子类能给父类对象,对象指针,对象的引用赋值,这三个希望大家可以记住,因为真的没什么好说的啊!!!!!!!!用多了就会了!!!!!!
这个非常非常重要!!!!!这个将会在多态中大显身手!!!好好学习!!天天向上!!!!!!!!
六、虚拟继承(菱形继承问题)
这个!!!非常重要!!!咱会尽量尽自己的最大努力,让语言条理清晰,让大家容易理解,那么开始!!!
先上代码,还是起那面的例子!
class A
{
public:
A()
{
cout <<"A()"<< endl;
}
public:
int a = 1;
};
class B:virtual public A
{
public:
B()
{
cout << "B()" << endl;
}
public:
int b = 2;
};
class C :virtual public A
{
public:
C()
{
cout << "C()" << endl;
}
public:
int c = 3;
};
class D : public B, public C
{
public:
D()
{
cout << "D()" << endl;
}
private:
int d = 4;
};
int main()
{
D d;
}
6.1为什么要虚拟继承?
我们在上文四、派生类的默认成员函数中提到,子类对象中会有父类的成员,那么在没有虚拟继承的时候,我们可以看到D类对象的内部是 abacd 这样的数据顺序,这里我们看到,a 数据重复冗余,而且 ab 是 父类B的成员,ac是父类C的成员,那再访问d对象中a成员(这里我们把数据成员公有进行讲解,你可能再平时也会用A类对象去访问A成员,还是会造成二义性,应为不知道访问哪个a),这时候就会造成访问时二义性,因为编译器不知道你访问的是父类B中的a成员,还是父类C中的a成员。
所以我们需要虚拟继承来解决这个问题,那么它是如何解决这个问题的,请继续往下看
6.2虚拟继承virtual关键字位置
这里就简而言之,长话短说,记住一点,最上方的父类的派生类使用virtual关键字继承!!
经过上面的介绍,我们知道B类和C类都会包含一个A类的数据成员a,数据的二义性就是这里发生的,这时我们实例化BC类对象我们就会发现,它们各自有一个A类数据成员a并且是不同的地址(说明是不同的a不是一个a)这样D类再继承BC后就会出现访问a成员时的二义性。
这里我们可以看到继承过来的a都是有自己的地址,不是同一个a,但是命名是相同的,访问时就会出现二义性。
也就是说我们再第一次继承的时候,就已经导致代码不是一份了,如果们在D类继承B,C类时才去加关键字,那么我们依旧会创建两个不同的a因为B,C类部分代码中的a不是同一个a,这已经晚了,虚拟继承就失去了意义,这是判断在哪里加virtual关键字的判断依据。不过一般都是第一个父类被继承时添加virtual关键字。
6.3虚拟继承的原理
那么,上文提到的,因为a已经出现了两份,问题就这个两份的问题,我们要让其在整个继承体系中只有一份代码,就需要我们进行虚拟继承,继承的原理主要依赖虚基表指针和虚基表,这两个东西是什么?我们在例子中看
这个图就是上文中我们B,C类继承A时添加了virtual关键字后,B,C类实例化对象的时候的内部结构,我们可以看到到它们本来ab,ac中 a 部分的数据变成了一串地址,这是什么呢?这就是虚基表指针,它指的是什么呢?看例子:
这里我们把地址输入进去后跳转到虚基表,可以看到前四个字节是一个终止符,那么我们看下一行,第一个” 08 “
这是什么?因为是16进制的数据,转换为十进制就是 8,那,这个 8 代表什么呢?这个 8 就是我们找到的a在物理内存中的实际地址的偏移量,我们从下面这张图看
我们看到,它刚好是1,那这个 1 是什么?这个 1 就是我们的 a 数据成员,这里通过虚基表指针,在虚基表中找到物理内存中的偏移量,b对象存储空间开始,偏移八个字节,就是我们的 a 数据成员的地址,这样就可以访问数据成员a。
那么D实例化的对象内部又是什么样子呢这两个就是分别是B类和C类的虚基表指针,我们拿其中一个看看:
大家可以自己数一数,这里偏移量“ 0c” 转换成十进制就是“ 12 ”是指向了哪个数据,大家可以自己数一下,看看是不是指向了,最低端 01 那个数据
由此,我们可以看出来,整个继承家族中,A类中的数据成员 a 在 D类中只有 1份,D类中B,C类部分的a成员变成虚基表指针,来只向1份 a数据,这样就避免了二义性,减少了代码的冗余。这也就是虚继承斗篷下所隐藏的东西。
总结
今天我们所讲的是C++进阶的继承模块,它是C++最重要的部分之一,也是C++代码复用,实现多态的基础,这里希望大家好好看看,虚继承更是继承中的难点,希望大家可以明白其中的原理以及该如何使用。基类和派生类对象赋值的原理,为什么可以赋值这个也是学习中的重点,希望大家可以掌握。