1.继承的概念
继承,如同字面意思,就是继承人将拥有被继承人的某些东西,比如父子之间是有继承关系的,父亲在临走前会将自己的财产交给自己的子女保管,此概念用于C++也是互通的,只不过这里的父亲不会真的走。
继承机制是面向对象程序设计是代码可以复用的重要手段,它允许程序员在保持父类原有功能和属性的机制下进行功能扩充,这样产生的新类,俗称派生类,也称作子类。继承呈现了面向对象程序设计的层次结构。
一个简单的继承结构:
class Person
{
public:
void Print()
{
cout << "name:" << _name << endl;
cout << "age:" << _age << endl;
}
protected:
string _name = "peter";
int _age = 18;
private:
int _aa;
};
class Student : public Person
{
public:
void Print()
{
//_aa = 1; // 不可见
}
protected:
int _stuid;
};
我们知道C++中访问限定符有三个,分别是public、private、protected,分别代表了共有、私有和保护,那么对于继承也是同样有这三种访问限定符,成为public继承、private继承和protected继承。
三种不同的访问限定符下的继承关系,对于成员变量和成员函数的访问权限肯定是不同的,C++在继承中使用这三种访问限定符的目的也是为了限定和保护父类的成员变量和成员函数。
当父类成员变量为public时,子类使用public继承,那么在子类中此成员也是public的;子类使用protected继承,那么在子类中此成员是protected,子类使用private继承,那么在子类中此成员是private。
当父类成员变量为protected时,子类使用public继承,那么在子类中此成员也是protected的;子类使用protected继承,那么在子类中此成员也是protected的;子类使用private继承,那么在子类中此成员也是private的。
当父类成员变量为private时,子类使用public继承,那么在子类成员中是不可见的;子类使用protected继承,那么在子类中此成员也是不可见的;子类使用private继承,那么在子类中也是不可见的。可以发现父子类的继承关系采取的是“向下兼容”,也就是说每次都取权限更小的权限,用一张图片来表示:
总结:子类无论采取什么继承方式去继承父类中的private成员,那么在子类中都是不可见的, 但虽然是不可见,但子类依然将它们继承到了自己的类中,只不过无论是在类内还是类外都无法查看。如果父类private成员被派生类所继承,只想其成员变量在派生类中被访问,不想它们在除子类之外的域被访问,就定义为protected,所以可以看出保护成员限定符是因为继承才出现的。那么在实际中一般都是用public继承方式,几乎很少使用private或protected,因为 private/protected继承下来的成员都只能在派生类的类里面使用,实际中扩展维护性不强。
3.基类和派生类对象赋值转换
class Person
{
protected:
string _name;
string _sex;
int _age;
};
class Student :public Person
{
public:
int _No;
};
int main()
{
Person p;
Student s;
//父类 = 子类赋值兼容 - 切片
//这里不存在类型转换,语法天然支持的行为
p = s;
Person* pter = &s;
Person& ref = s;
//子类 = 父类
//s = p; // error
Student* pptr = (Student*)&p;
Student& pptr = (Student&)p; // 如果子类比父类大,会有越界的风险
//类型转换,中间会产生临时变量,临时变量具有常性
int i = 1;
double d = 2.2;
i = d;
const int& ri = d;
return 0;
}
当一个子类继承了父类,那么子类也就拥有父类的成员方法和成员变量,也就是说子类此时是大于父类的,当我们用父类的指针或者引用去指向一个子类变量时,此时父类能看到的也只有子类的内容,这个在语法层面叫做“切片”,相当于从子类中将父类的切出来给父类指针看。
值得注意的是,这里的切片和强制类型转换不一样,虽然Person类和Student类是两个不同的类型,但是这里并没有发生强制类型转换,只是将父类的内容切了出去。
4.隐藏
当派生类继承了父类,并且派生类和父类都出现了同样的成员变量,那么这种情况就构成了隐藏,也叫做重定义。那么究竟该如何访问呢?子类父类都出现了相同的变量,该如何准确找到我们要的那个变量呢?可以使用【类型::成员】显示访问,我们都知道::是域访问限定符,当只使用::时代表访问全局变量。
需要注意的是如果是成员函数的隐藏,只需要函数名相同就构成隐藏。所以要注意在继承体系中最好不要定义同名的成员。
5.派生类的默认成员函数
在C++的类中,当我们只写了class A{int a;};再使用A类定义了一个变量,那么此时a变量是否会被初始换呢?那么在C++中,当我们不写,编译器会为我们自动生成6个默认的成员函数,也就是说虽然我们没有为A类编写任何成员函数,但是实际上它已经有默认6个成员函数了,它们分别是:
①无参的构造函数:主要完成初始化工作
②析构函数:主要完成清理工作
③拷贝构造函数:利用同类的对象初始化创建对象
④复制重载函数:重载=,把一个对象赋值给另一个对象
⑤取地址重载函数:普通对象取地址
⑥const取地址重载函数:const对象取地址
那么在这6个默认生成的成员函数中,只有前四个是会经常使用的,当我们不写时,会发生什么?如果在派生类针对自己类的成员变量和父类的成员变量中会作什么处理?
当我们不写时,派生类针对自己类型的成员变量会调用自身的构造函数,针对继承下来的父类成员变量会调用父类的构造函数,销毁时也同样调用父类的析构函数。
class Person
{
public:
~Person()
{
cout << "~Person()" << endl;
}
protected:
string _name;
};
class Student :public Person
{
public:
Student(const char* name, int num)
:Person(name)
,_num(num)
{
}
Student(const Student& s)
:Person(s)
,_num(s._num)
{
}
Student& operator=(const Student& s)
{
if (this != & s)
{
Person::operator=(s);
_num = s._num;
}
return *this;
}
//子类析构函数名字会被统一处理成destructor()
//子类的析构函数和父类的析构函数会构成隐藏
~Student()
{
//Person::~Person();
}
//子类析构函数结束时,会自动调用父类的析构函数
protected:
int _num = 1;//学号
//string _s = "hello world";
//int* p = new int[10];
};
//派生类重点的四个默认成员函数,我们不写,编译器默认生成的会做些什么事?
//如果自己写,要做些什么事?
//我们不写默认生成的派生的构造和析构
//a.父类继承下来的 - 调用父类的默认构造和析构处理父类的成员
//b.自己的(内置类型和自定义类型成员) - 跟普通类一样
//我们不写默认生成的operator=
//a.父类继承下来的 - 调用父类的默认operator=
//b.自己的(内置类型和自定义类型成员) - 跟普通类一样
//总结:原则是:继承下来的调用父类的处理,自己的按普通类基本规则处理
//什么情况下必须自己写?a.父类没有默认构造函数 b.如果子类有资源需要释放,就需要自己显示些析构 c.如果子类存在浅拷贝问题,就需要自己实现拷贝构造和赋值
//如果要自己如何写?父类成员调用父类的对应构造、拷贝构造、operator=和析构处理
//自己成员按普通类处理
int main()
{
Student s1;
Student s2(s1);
return 0;
}
6.菱形继承
当一个父类被一个派生类继承时,在逻辑上可以理解成为线性的继承,成为单继承。 但是当一个父类同时被两个派生类继承,并且这两个派生类都同时被一个孙子类继承,此时在逻辑层面就形成了菱形继承。
那么根据继承的特性来说,派生类会拥有父类的成员函数和成员变量,但是当在菱形继承中,孙子类将拥有两个派生类+一个父类的所有public成员函数和成员变量,那么虽然看起来没什么问题,但是当中间的两个派生类中的成员变量和成员函数都相同时,这时在孙子类中就会有大量的重复成员出现,会有数据冗余和二义性的问题出现。
//菱形继承 - 多继承的一种特殊情况
// class Person
//
//class Student:public Person class Teacher :public Person
//
// class Assistant:public Student, public Teacher
//存在数据冗余性和二义性
class Person
{
public:
string _name;
int a[1000];
};
class Student :virtual public Person // 虚继承解决冗余和二义性
{
public:
int _num;
};
class Teacher :virtual public Person // 虚继承解决冗余和二义性
{
public:
int _id;
};
class Assistant :public Student, public Teacher
{
public:
string _majorCourse;
};
int main()
{
Assistant at;
at._id = 1;
at._num = 2;
at.Person::_name = "人类";
at.Student::_name = "学生";
at.Teacher::_name = "老师";
cout << sizeof(at) << endl;
at._name = "张三";
return 0;
}
所以菱形继承在编程中是一定要注意避免出现的问题,一旦出现可能会对程序的内存大量的占用,从而影响性能。
解决方案:使用虚继承。
在public继承前面加上virtual,在C++语法中就代表使用了虚继承,使得在派生类中只保留一份间接基类的成员。
所以在Assistant中针对父类的成员变量_name只保留了一份,而并非三份,从而解决了数据的冗余问题。