文章目录
- 继承的概念与定义
- 继承的定义格式
- 父类和子类的对象赋值转换
- 继承中的作用域
- 子类的默认成员函数
- 菱形继承
- 虚拟继承
- 总结
继承的概念与定义
继承是面向对象编程三大特性之一,是一种可以使代码复用最重要的手段,在原有类特性的基础上进行扩展,产生新的类。例如人和学生的关系,学生是一个人,那么人所有的特性学生都有,比如人有姓名、身份证号码、性别等,这些学生也有具备。在此基础上学生还有额外的特性,例如学号,班级
为了方便的去创建类,因此我们可以在人这个类的基础上去创建出新的学生类,当我们使用继承时,那么在创建学生类时就不用再去定义人这个类的所有特性
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; // 学号
};
像上面的代码就是使用了继承去创建学生类,那么当我们实例化对象之后,学生类的对象就也会包含了人类的所有成员
像这种继承我们就可以称人的类为父类或者基类,学生这个类就称为子类或者派生类。
主要注意:友元关系不能继承,当基类定义了静态成员那么整个继承体系里面只有一个这样的成员
继承的定义格式
继承方式可以有三种,也就是我们所知道的三种访问限定方式。那么不同的继承方式也就意味着,派生类能够继承基类的不同特性
公有继承时,派生类可以访问基类除私有成员外的所有成员
保护继承时,只能访问基类中的保护成员
私有继承时,基类中的所有成员都不能访问
基类中的私有成员不论什么继承方式都不可以访问
父类和子类的对象赋值转换
派生类对象 可以赋值给 基类的对象 / 基类的指针 / 基类的引用 ,也就是说可以用基类的指针或者引用去指向派生类的对象,要注意基类指向派生类可以,但是派生类不可以指向基类。这种方式可以形象的称为切片
int main() {Student s;// 1.子类对象可以赋值给父类对象/指针/引用Person p = s;Person* pp = &s;Person& rp = s;//2.基类对象不能赋值给派生类对象//s = p;// 3.基类的指针可以通过强制类型转换赋值给派生类的指针pp = &s;Student* ps1 = (Student*)pp; // 这种情况转换时可以的。ps1->_stuid = 10;pp = &p;Student* ps2 = (Student*)pp; // 这种情况转换时虽然可以,但是会存在越界访问的问题ps2->_stuid = 10;return 0;
}
继承中的作用域
在继承体系中基类和派生类都有独立的作用域 ,如果基类和派生类中都有着一个同名的函数,那么派生类对象就会自动屏蔽对基类同名函数的访问,这种情况称为隐藏/重定义
class Person
{
public:void Print(){cout << "Person:" << _name << endl;}
protected:string _name = "peter"; // 姓名int _age = 18; // 年龄
};class Student : public Person
{
public:void Print(){cout << "Student:" << _name << endl;}protected:int _stuid; // 学号
};int main() {Person p;Student s;s.Print();return 0;
}
可以看到这种情况访问的就是派生类中的函数。还有一种情况需要注意 如果派生类和基类的同名函数在派生类里面带有参数,但我们调用该函数并且不给函数传参的时候程序就无法运行通过。因为基类中的函数已经被隐藏了,所以就找不到对应的函数去调用
子类的默认成员函数
派生类的6个默认函数的生成规则
- 派生类的构造函数必须调用基类的构造函数初始化基类的那一部分成员。如果基类没有默认的构造函数,则必须在派生类构造函数的初始化列表阶段显示调用。
- 派生类的拷贝构造函数必须调用基类的拷贝构造完成基类的拷贝初始化。
- 派生类的operator=必须要调用基类的operator=完成基类的复制。
- 派生类的析构函数会在被调用完成后自动调用基类的析构函数清理基类成员。因为这样才能保证派生类对象先清理派生类成员再清理基类成员的顺序。
- 派生类对象初始化先调用基类构造再调派生类构造。
- 派生类对象析构清理先调用派生类析构再调基类的析构
菱形继承
继承会有单继承和多继承。单继承就是一个类继承一个类,多继承就是多个类继承一个类
那么因为有多继承的存在,则会产生出一种菱形继承的现象
如图所示,它们之间的继承关系形成了一个菱形形状,那么这种菱形继承会导致什么问题呢。
首先,因为学生和老师的类中都继承了一份人的类,接着助理又去继承学生和老师这两个类,那么也就是说助理这个类就会有两份人的类的属性。
class Person
{
public:void Print(){cout << "Person:" << endl;}
public:string _name = "peter"; // 姓名int _age = 18; // 年龄
};class Student : public Person
{
public:void Print(){cout << "Student:" << endl;}public:int _stuid; // 学号
};class Teacher : public Person
{
public:void Print(int n){cout << "Teacher:" << endl;}public:int _teaid; // 学号
};class Assistant : public Student , public Teacher
{
public:void Print(int n){cout << "Assistant:" << endl;}public:int _assid; // 学号
};int main() {Person p;Student s;Teacher t;Assistant a;return 0;
}
可以看到a对象里面有两个 _name , __age,所以如果这时候我们去访问a的name就会出现问题,因为编译器根本就不知道你想要访问的是哪一个。这样就造成了数据的冗余和二义性
虚拟继承
那么为了解决这种菱形继承的问题,我们可以采用一种方法叫做虚拟继承。在继承方式前加上 virtual 就可以定义为虚拟继承
为了研究虚拟继承,首先先建立一个菱形继承体系,运行起来之后利用内存窗口查看数据
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;
}
这里是通过了B和C的两个指针,指向的一张表。这两个指针叫虚基表指针,这两个表叫虚基表。虚基表中存的偏移量。通过偏移量可以找到下面的A
如上图,b的虚基表地址为 内存2窗口的地址,对应内存1的青蓝色框,通过这个地址找到虚基表后可以看到一个数字 28 这个机会表里存的偏移量,就可以通过这个偏移量往下找到A。c的虚拟表也同理,窗口3里可以看出。
通过这个方式,可以通过偏移量去修改数据这样就可以避免了数据的冗余和二义性了。
总结
多继承可以认为是C++的缺陷,因为会导致菱形继承的发生。
继承是一个依赖关系很强,耦合度很高的方法,所以在日常编写程序时,不是那种特别特定的关系能不用继承就不继承,后面的多态要靠继承实现。否则可以多用组合。