前言
面向对象的三大特性:
- 封装
- 继承
- 多态
今天我们就讲第二大特性继承.—继承会为下一部分多态设铺垫
继承就是从父类中继承下来他的元素用于子类的建设.
概念
我们之前使用的代码的复用大部分都是通过将实现的类作为成员的方式来使用,但是总有一些时候我们需要将成员变量函数等复用下来,这时我们就可以用到继承了.
格式
首先来看看一个简单的继承的格式是什么样子的.
class Person
{
public:
Person(string name = "a", int age = 12, string sex="女")
:_name(name)
,_age(age)
,_sex(sex)
{
}
void print()
{
cout << "名字" << _name << "年龄" << _age << "性别" << _sex << endl;
}
protected:
string _name;
int _age;
string _sex;
};
class Student:public Person
{
public:
Student(string id)
:Person("张三",14,"男")
,_id(id)
{
}
void print()
{
cout << "名字 " << _name << " 年龄 " << _age << " 性别 " << _sex << " id " << _id << endl;
}
protected:
string _id;
};
这里的Student类就是继承了Person类.
Person类没有什么可说的就是一个普通的类而已,我们来看看Student是如何继承我们的Person类的.
继承的格式非常简单子类:继承方式 父类
格式内容讲解
子类
: 就是我们创建的要继承父类的类就是我们的子类.
:
: 必不可少
父类
: 被子类继承的类.
继承方式 : 和访问限定符对应有三种: public
private
protected
这里我们要讲解一下protected, protected作为访问修饰符情况的时候和private类似,但是当protected在继承的时候会于private有所差距, private修饰的父类是在子类中不可见的, 而protected在子类中是可见的.
每种继承方式和父类的限定符组合对于子类访问父类的元素有着不同的限制.
关系如下标:
类成员/继承方式 | public继承 | protected继承 | private继承 |
---|---|---|---|
基类的public成员 | 派生类的public成员 | 派生类的protected成员 | 派生类的private成员 |
基类的protected成员 | 派生类的protected成员 | 派生类的protected成员 | 派生类的private成员 |
基类的private成员 | 派生类的private成员 | 派生类的private成员 | 派生类的private成员 |
总结
- 上面的表格总结可发现,基类的私有成员在子类都是不可见的. 基类的其他成员在子类的访问方式==Min(基类在成员的访问限定符, 继承方式). 其中public> protected>private.
- 基类的private成员在派生类(子类)中无论用什么继承方式都是不可见的. 值得注意的是这里的不可见是指基类的私有成员还是被继承到了派生类中的(可以通过类的大小验证). 但是语法上限制派生类无论是在类外还是在类内都是无法访问它的.
- 基类private成员在派生类中无法访问但是基类成员需要基类对象不能在类外访问时可以使用protected.这里也可以看出protected是因为继承才出现的
- 使用关键字class时默认的继承方式时private, 使用struct时默认的继承方式时public, 不过最好显示的写出继承方式
- 在实际的运用中一般使用的都是public继承, 几乎很少使用protected/private继承, 也不提倡使用(大家根据实际情况使用即可)
继承内容小讲解
结论: 友元并不能通过继承继承得到.静态函数和普通函数可以通过继承继承得到
当我们实现了继承后就可以享受继承给我们带来的便捷了.
我们都知道子类可以通过继承得到父类的所有成员函数,以及成员变量.
那我们来试试都有什么属于成员函数.
我们测试静态函数
友元函数
普通成员函数
void ATest3()
{
cout <<"friend void ATest3()"<< endl;
}
class A
{
public:
static void ATest1()
{
cout << "static void ATest1()" << endl;
}
void ATest2()
{
cout << "void ATest2()" << endl;
}
friend void ATest3();
protected:
static int _a1;
int _a2;
};
int A:: _a1 = 0;
class B:public A
{
};
从上图我们可以看出我们的友元并不能通过继承继承得到.
静态变量讲解
我们基类中的静态变量,无论继承多少个子类(派生类)都只有这基类一个静态对象.
我们将基类的静态变量变成公用的来实现我们思想的解读.
可以看出基类和子类的静态成员变量是公用的.
基类和派生类对象赋值转换(切片)
派生类(子类)是可以直接赋值给基类类型的指针
引用
正常变量
的其中发生了切片.
演示代码如下:
class A
{
public:
A(int a1 = 0, int a2 = 0)
:_a1(a1)
,_a2(a2)
{
;
}
void PrintA()
{
cout << "_a1:" << _a1 << endl;
cout << "_a2:" << _a2 << endl;
}
public:
int _a1;
int _a2;
};
class B:public A
{
public://注意这里我们的成员变量是public
B(int b1 = 0, int b2 = 0 )
:_b1(b1)
,_b2(b2)
{
;
}
void PrintB()
{
cout << "_b1: " << _b1 << endl;
cout << "_b2: " << _b2 << endl;
}
public://注意这里我们的成员变量是public
int _b1;
int _b2;
};
我们的B类成员有着_a1
_a2
_b1
_b2
一共四个成员变量 其中_a1
_a2
是通过继承的方式得来的.
当我们将子类成员赋值给基类的时候将基类的成员变量切下来赋值给我们的基类成员(或是让我们的基类指针或引用指向该区域).
我们如果使用A的对象来接收B类型其实就是通过切片切下来属于A的部分然后使用A类的拷贝构造函数来进行初始化A类型对象.
下图是证明调用了拷贝构造函数
然后A类所作的任何事情都不会对B类对象产生任何影响了.
如果使用A类指针来指向B类对象的部分,那么我们所使用的就是B类对象的空间区域我们可以对B类的A内容进行修改
下图是为了证明普通的A类无法修改B类内容—指针和引用可以修改这里不演示了
值得注意的是我们如果使用的是protected继承或private等在外部不可见的类型的时候我们的A类是无法进行切片操作的.
下图是证明protected继承或private继承是无法在外部进行切片操作的.
切片操作原因(有问题)
为什么我们的继承可以做出切片这样的操作呢?
其实原因很简单,我们的类里的变量其实是连续的方式保存在一部分区域的.
来让我们证明一下.
我们使用以下两个类来证明
class A
{
public:
A(int a1 = 1, int a2 = 2)
:_a1(a1)
,_a2(a2)
{
;
}
void PrintA()
{
cout << "_a1:" << _a1 << endl;
cout << "_a2:" << _a2 << endl;
}
public:
int _a1;
int _a2;
};
class B:public A
{
public:
B(int b1 = 3, int b2 = 4)
:_b1(b1)
,_b2(b2)
{
;
}
void PrintB()
{
cout << "_b1: " << _b1 << endl;
cout << "_b2: " << _b2 << endl;
}
public:
int _b1;
int _b2;
};
可以从上图看出我们继承中的成员变量的地址是连续的.
问题: 父类成员相比于子类成员的地址为啥是要低的,明明是先先调用的父类构造函数.
继承中的作用域
我们的每个类中都是有他自己的作用域的.
需要注意的有下面四点
- 在继承体系中基类和派生类都有独立的作用域
- 子类和父类中有同名成员, 子类成员中屏蔽父类对同名对象的直接访问, 这种情况叫做隐藏, 也叫重定义(在子类成员函数中,可以使用 基类::基类成员的方式显示访问)
- 需要注意的是如果是成员函数的隐藏, 只需要函数名相同就可以构成隐藏.
- 注意在实际中在继承体系中最好不要定义同名的成员.
实例中我们可以看一下先相同名称的使用
// Student的_num和Person的_num构成隐藏关系,可以看出这样代码虽然能跑,但是非常容易混淆
class Person
{
protected :
string _name = "快乐的人"; // 姓名
int _num = 23333; // 身份证号
};
class Student : public Person
{
public:
void Print()
{
cout<<" 姓名:"<<_name<< endl;
cout<<" 身份证号:"<<Person::_num<< endl;//这里因为子类命名和父类的相冲所以我们要使用显示的方式调用变量
cout<<" 学号:"<<_num<<endl;
}
protected:
int _num = 66666666; // 学号
};
void Test()
{
Student s1;
s1.Print();
};
派生类的默认成员函数
6个默认成员函数, 在派生类是如何生成的呢?
- 派生类的构造函数必须调用基类的构造函数初始化基类的那一部分成员. 不过基类没有默认的构造函数, 则必须在派生类构造函数的初始化列表显示的调用.
- 派生类的拷贝构造函数必须调用基类的拷贝构造完成基类的拷贝初始化
- 派生类的operator=必须调用基类的operator=完成基类复制
- 派生类的析构函数会在被调用完成后自动调用基类的析构函数清理基类成员.因为这样才能保证派生类对象先清理派生类成员再清理基类成员的顺序.
- 派生类对象初始化先调用基类构造再调用派生类构造
- 派生类对象析构先清理派生类析构再调用基类析构
- 因为后续的一些场景析构函数需要构成重写, 重写的条件之一是函数名相同(这个后面讲解). 所以编译器会对析构函数名进行特殊处理, 处理成destrutor(), 所以父类析构函数不加virtual(多态时讲解)的情况下,子类析构函数和父类析构函数构成隐藏关系.
菱形继承
复杂的菱形继承是C++被诟病最多的部分了,这部分内容就比较复杂晦涩了.
在C++中因为我们可以使用多继承的原因, 不免会产生一个菱形继承
如下图就是菱形继承的一个很好的案例:
因为我们的Assistant类中同时继承了Student 和Teacher类而我们的Student 和Teacher类又因为都继承了Person的原因所以我们的Assistant类中的对象会存储两个Person的内容.
我们用下面代码验证
class Person
{
protected:
char P[10];
};
class Student: public Person
{
protected:
char S[10];
};
class Teacher:public Person
{
protected:
char T[10];
};
class Assistant :public Student, public Teacher
{
public:
Assistant()
{
cout << "class Assistant :public Student, public Teacher" << endl;
}
protected:
char A[10];
};
int main()
{
Assistant a;
cout << sizeof(a) << endl;
return 0;
}
可以看见我们的Assistant类的大小达到了50, 正常来讲我们应该只需要保存一个Person类的成员即可, 也就是我们的Assistant的大小做到40就是可以的了.
这里因为我们的Assistant里有两个Person有时候就会产生二义性和冗余性.
如下图
这时我们可以通过虚拟继承来解决此问题.
虚拟继承
虚拟继承可以解决菱形继承的二义性和数据冗余的问题。如上面的继承关系, 在Student和Teacher的继承person是使用虚拟继承, 即可解决问题。 需要注意的是, 虚拟继承不要在其他地方使用。
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";
}
虚拟继承解决问题原理
我们先使用一个简易的菱形继承模型来讲解此问题。
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;
}
我们使用这个来讲解。
下图
可以看出在没有使用虚拟继承的时候有两个a的存在。
当我们使用虚拟继承的时候呢?
我们可以看到B和C中存放的已经不是之前的A值了而是一个像是内存地址的变量。
让我们看看这个地址的位置到底存放了什么
下图是B中存放的地址位置
下图是C中存放的地址位置
可以看到这个地址的位置是空的但是下一个字节的位置就是有数字的
这个数字的意思其实就是我们(以B为例)我们B元素位置离A位置的偏移量。
用A的地址来减去B的位置就可以得到该地址存放的值及00 00 00 14
C则同理。
其实我们B和C中存在的指针我们叫做虚基表指针 指针指向的位置是虚基表
这样我们的D类中就可以只保存一个A变量了。
结尾
继承的缺陷比较大,尤其是我们子类其实是对基类的依赖很大的。所以如果能使用类的组合就不要使用继承了。