文章目录
- 继承的概念及定义
- 继承的概念
- 继承的定义
- 基类和派生类的赋值问题(切片)
- 继承的作用域
- 继承中的默认成员函数
- 继承中的友元函数和静态成员
- 继承的方式
- 菱形继承
- 虚拟菱形继承及原理
- 继承(白箱复用)和组合(黑箱复用)
- 菱形继承的面试题
继承的概念及定义
继承的概念
继承其实就是对原本已经存在的类进行复用的过程,和函数的复用类似,同时也体现了面向对象编程效率高的方面之一。
继承的定义
上面的例子中就是把:
preson作为了基类以public的继承方法继承给了student派生类
继承方式和访问限定符
访问限定符有以下三种:
public访问
protected访问
private访问
而继承的方式也有类似的三种:
public继承
protected继承
private继承
类成员/继承方式 | public继承 | protected继承 | private继承 |
---|---|---|---|
public基类成员 | 派生类的public的成员 | 派生类的protected的成员 | 派生类的 private的成员 |
protected基类成员 | 派生类的protected的成员 | 派生类的protected的成员 | 派生类的protected的成员 |
private基类成员 | 派生类不可见 | 派生类不可见 | 派生类不可见 |
protected限定符由此可见是为了继承而出现的,要是基类不想被外界直接访问,但是又需要被派生类访问,那就需要把基类的成员用protected修饰,并且派生类用protected的方式继承就可以解决这个问题。
注意一个问题就是,默认的继承方式是private
基类和派生类的赋值问题(切片)
在继承中,派生类是可以直接赋值给基类的指针或者基类的引用,为这个行为就叫做切片,寓意把派生类中基类那部分切来赋值过去。
#include<string>
class person
{
public:string _name;//姓名string _sex;//性别size_t _age;//年龄
};
class student :public person
{
private:int _stuid;//学号
};int main()
{student s1;person &ref=s1;//赋值给基类的引用person *ptr=&s1;//赋值给基类的指针return 0;
}
派生类给基类指针赋值:
派生类给基类应用赋值:
**注意:**基类则不可以给赋值派生类
class person
{
public:string _name = "person";
};class student :public person
{
public:void show(){cout << _name << endl;}
private:string _name = "student";
};int main()
{student s1; s1.show();return 0;
}
此时基类中有一个_name派生类中有一个_name,两个成员同名构成了重写,就会优先调用派生类的成员,如果先要看到父类的成员就需要加访问限定符::,如person::_name
class person
{
public:string _name = "person";
};class student :public person
{
public:void show(){cout << person::_name << endl;//显示person的作用域去访问cout << _name << endl;//此时基类中有一个_name派生类中有一个_name,两个成员同名构成了重写,就会优先调用派生类的成员}//如是构成重写默认先访问派生类的成员
private:string _name = "student";
};int main()
{student s1; s1.show();return 0;
}
继承的作用域
在继承体系中的基类和派生类都有独立的作用域。若子类和父类中有同名成员,子类成员将屏蔽父类对同名成员的直接访问,这种情况叫隐藏,也叫重定义。
继承中的默认成员函数
派生类与普通类的默认成员函数的不同之处概括为以下几点:
- 派生类的构造函数被调用时,会自动调用基类的构造函数初始化基类的那一部分成员,如果基类当中没有默认的构造函数,则必须在派生类构造函数的初始化列表当中显示调用基类的构造函数。
- 派生类的拷贝构造函数必须调用基类的拷贝构造函数完成基类成员的拷贝构造。
- 派生类的赋值运算符重载函数必须调用基类的赋值运算符重载函数完成基类成员的赋值。
- 派生类的析构函数会在被调用完成后自动调用基类的析构函数清理基类成员。
- 派生类对象初始化时,会先调用基类的构造函数再调用派生类的构造函数。
- 派生类对象在析构时,会先调用派生类的析构函数再调用基类的析构函数。
在编写派生类的默认成员函数时,需要注意以下几点:
1、派生类和基类的赋值运算符重载函数因为函数名相同构成隐藏,因此在派生类当中调用基类的赋值运算符重载函数时,需要使用作用域限定符进行指定调用。
2、由于多态的某些原因,任何类的析构函数名都会被统一处理为destructor();。因此,派生类和基类的析构函数也会因为函数名相同构成隐藏,若是我们需要在某处调用基类的析构函数,那么就要使用作用域限定符进行指定调用。
3、在派生类的拷贝构造函数和operator=当中调用基类的拷贝构造函数和operator=的传参方式是一个切片行为,都是将派生类对象直接赋值给基类的引用。
说明一下:
1、基类的构造函数、拷贝构造函数、赋值运算符重载函数可以在派生类当中自行进行调用,而基类的析构函数是当派生类的析构函数被调用后由编译器自动调用的,我们若是自行调用基类的构造函数就会导致基类被析构多次的问题。
2、创建派生类对象时是先创建的基类成员再创建的派生类成员,编译器为了保证析构时先析构派生类成员再析构基类成员的顺序析构,所以编译器会在派生类的析构函数被调用后自动调用基类的析构函数。
继承中的友元函数和静态成员
友元关系是不可以被继承的,基类中的友元函数是可以访问基类的成员的,但是不可以访问派生类中的成员;如想要访问派生类的成员,这个友元函数就需要写在派生类中。
#include<string>
class person
{friend void show();
public:string _name="张三";//姓名string _sex="男";size_t _age=21;
};
class student :public person
{
private:size_t _stuid=999;//学号
};
void show(const person& p,const student& s)
{cout << p._name << " " << p._sex << " " << p._age << endl;cout << s.stuid << endl;//友元不能被继承,若要继承就把show(),在student类中也友元申明
}
int main()
{person p1;student s1;show(p1, s1);
}
若基类当中定义了一个static静态成员变量,则在整个继承体系里面只有一个该静态成员。无论派生出多少个子类,都只有一个static成员实例。
#include<string>
class person
{
public:person(){++_count;}person(const person& p){++_count;}
public:string _name = "张三";//姓名string _sex = "男";size_t _age = 21;static int _count;//可以看有多少派生类继承了此基类
};
int person::_count = 0;//静态成员需要在类外初始化
class student :public person
{private:size_t _stuid = 999;//学号
};int main()
{ student s1;cout << s1._count << endl;//1student s2(s1);cout << s2._count << endl;//2
}
继承的方式
继承的方式有两种,
一种是单继承:就是一个派生类继承基类
另外一种是多继承:就是一个派生类继承多个基类
菱形继承
多继承的特殊继承:菱形继承
菱形继承是,一个派生类继承了多个基类,然后这个基类又同时继承了相同的基类所造成的一种缺陷,菱形继承会带来数据冗余和二义性。
#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; //主修课程
};
int main()
{Assistant a;a._name = "peter"; //二义性:无法明确知道要访问哪一个_namereturn 0;
}
上面的代码中student也有name,thcher也有name,assistant也有name,到最后不知道访问的是哪个name,这就导致了二义性。
上面的代码中Student类继承了person,Teacher类也继承了,Assistant也继承了,这就导致出现了三份_name造成数据冗余。
虚拟菱形继承及原理
菱形虚拟继承 |
---|
为了解决菱形继承带来的缺陷,就出现了虚拟的菱形继承来解决菱形继承带来的数据冗余和二义性问题。
#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; //主修课程
};
int main()
{Assistant a;a._name = "peter"; //无二义性return 0;
}
int main()
{
Assistant a;
a._name = "张三";
//解决了二义性
cout << a.Student::_name << endl;
cout << a.Teacher::_name << endl;
//解决了数据冗余
cout << &a.Student::_name << endl;
cout << &a.Teacher::_name << endl;
return 0;
}
virtual继承的细节: 虚拟继承的关键字virtual,和虚函数的virtual的作用是完全不一样的。继承的virtual用在有重复成员变量的基类中的。
菱形虚拟继承的原理 |
---|
无实现虚拟继承的测试代码:
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;
};
D 类的模型大致如下:
这里就可以看出为什么菱形继承导致了数据冗余和二义性,根本原因就是D类对象当中含有两个_a成员。
实现虚拟继承的测试代码:
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;d.B::_a = 1;d.C::_a = 2;d._b = 3;d._c = 4;d._d = 5;return 0;
}
这里发现,原先B::_a的位置和A::_a的位置变成了两个指针,其实这个就是虚基表的指针,分别指向的是一张虚基表(蓝色部分),而虚基表有两个数据:
第一个数据是为多态的虚表预留的存偏移量的位置;
第二个数据就是当前类对象位置距离公共虚基类的偏移量。
那这样虚基表指针就可以通过虚基表的第二个数据计算出偏移量找到_a。
大致模型如下:
在虚拟菱形继承中切片的的原理也是需要用到虚基表指针指向的虚基表来进行赋值的。
继承(白箱复用)和组合(黑箱复用)
继承因为派生类不可以脱离基类,那么就注定了继承的耦合度是比较高的,维护起来是比较困难的,但是由于多态的实现是脱离不了继承的,所以继承有继承的好处。
而反观组合,组合就是说在一个类中定义一个别的类的成员,那么这样的耦合度就低了,代码维护起来就会比较轻松,但是组合的缺点在于无法实现多态,所以对于继承和组合的选择是需要根据不同的情况下选择的。
菱形继承的面试题
什么是菱形继承?菱形继承的问题?
菱形继承是多继承的一种特殊情况,两个子类继承了相同的父类,而子类有继承了这两份子类就造成了菱形继承。
因为菱形继承中的子类继承了两份相同的父类,所以就造成了数据冗余和二义性。
什么是虚拟菱形继承?怎么解决菱形继承的问题?
虚拟菱形继承是在两份继承了相同父类的继承方式除加上virtual,形成菱形虚拟继承。
当实现菱形虚拟继承的时候,相同的数据就会被放到子类内存地址的最高处,而原先重复数据得位置就会变成虚基表指针,然后通过虚基表指针指向的虚基表通过计算偏移量定位到重复的数据位置,这就解决了数据冗余和二义性的问题。