> 文章列表 > 继承(C++)

继承(C++)

继承(C++)

继承

  • 继承的概念及定义
    • 继承的概念
    • 继承的定义
      • 定义格式
      • 继承关系和访问限定符
      • 继承基类成员访问方式的变化
  • 基类和派生类对象赋值转换
  • 继承中的作用域
  • 派生类的默认成员函数
  • 继承与友元
  • 继承与静态成员
  • 复杂的菱形继承及菱形虚拟继承
    • 虚拟继承的原理

继承的概念及定义

继承的概念

继承是面向对象程序设计使代码可以复用的重要手段,允许程序在保持原有类特性的基础上进行扩展,增加功能,所产生的新类,称作派生类。继承呈现了面向对象程序设计的层次结构,体现了有简单到复杂的过程,继承是类设计层次的复用

class Person
{
public:void Print(){cout << "name:" << _name << endl;cout << "age:" << _age << endl;}
protected:string _name="zhangsan";//姓名int _age=18;//年龄
};class Student :public Person
{
protected:int _stuid;//学号
};class Teacher :public Person
{
protected:int _jobid;//工号
};int main()
{Student s;Teacher t;s.Print();t.Print();return 0;
}

继承(C++)

继承后父类的 Person的成员,都会变成子类的一部分,在 Print的打印中体验了复用的效果

继承的定义

定义格式

Person称作父类,也是基类;Teacher称作子类,也是派生类
继承(C++)

继承关系和访问限定符

继承(C++)

继承基类成员访问方式的变化

类成员/继承方式 public继承
基类的public成员 派生类的public成员
基类的protected成员 派生类的protected成员
基类的private成员 派生类的private成员

最终的继承成员:类成员和继承方式中权限较低的那个

基类和派生类对象赋值转换

派生类对象可以赋值给基类的对象/指针/引用,与以往所学习的有所不同,子类可以赋值给父类

观察下列代码

int main()
{int i = 1;double d = 2.2;i = d;const int& ri = d;return 0;
}

i=d时,并不是直接赋值,而是先创建一个临时变量,临时变量中的值为2,临时变量具有常性,所以在下面引用时,必须加上 const否则程序便会崩溃

继承(C++)

上面的问题在继承中就不会出现,因为赋值的原理都不同;继承中的赋值并不会产生临时变量,而是直接将子类赋值给父类,简单来说是子类中与父类相同类型的数据赋值给父类,也称切片

int main()
{Person pn;Teacher t;pn = t;return 0;
}

继承(C++)

继承中的作用域

  1. 在继承中基类和派生类都有独立的作用域
  2. 当子类和父类中有同名成员时,子类成员将屏蔽父类对同名成员的直接访问,此情况称作隐藏,也称作重定义
  3. 如果成员函数也同名,也构成隐藏
class Person
{
protected:string _name = "zhangsan";//姓名int _age = 18;//年龄
};class Student :public Person
{
public:void Print(){cout << "姓名:" << _name << endl;cout << "年龄:" << _age << endl;}
protected:int _age=20;//年龄
};void test()
{Student s;s.Print();
}

继承(C++)

Student,Person中的_age学号构成隐藏,程序运行之后打印的是子类中的_age,将父类中的_age隐藏了起来

如果想访问父类中的_age,可以做如下操作

class Student :public Person
{
public:void Print(){cout << "姓名:" << _name << endl;cout << "年龄:" << Person::_age << endl;}
protected:int _age=20;//年龄
};

继承(C++)

派生类的默认成员函数

普通类的成员

  1. 内置类型
  2. 自定义类型

继承(C++)

派生类的成员

  1. 基类对象
  2. 派生类的内置类型
  3. 派生类的自定义类型

派生类中基类对象调用基类对应的函数完成初始化/清理/拷贝,内置类型/自定义类型的处理与普通类一致

class Person
{
public:Person(const char* name = "zhangsan"):_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):Person(name),_stuid(210202){cout << "Student()" << endl;}Student(const Student& st):Person(st),_stuid(st._stuid){cout << "Student(const Student& st)" << endl;}Student& operator=(const Student& st){cout << "Student& operator=(const Student& st)" << endl;if (this != &st){Person::operator = (st);_stuid = st._stuid;}return *this;}~Student(){Person::~Person();cout << "~Student()" << endl;}
protected:int _stuid;
};int main()
{Student st("zhangsan");return 0;
}

继承(C++)

析构函数有所区别,子类析构函数和父类析构函数构成隐藏关系,由于多态的要求,所有的析构函数都会特殊处理成destructor函数名,所以在子类的析构函数中需要加上访问限定符Person::才能调用父类的析构函数;从上面的打印结果来看,父类的析构多调用了一次,只是为什么呢???

继承(C++)

通过查看汇编,我们发现,在子类的析构函数执行结束后,编译器会自动调用父类的析构函数进行资源清理工作;也就是说,我们不需要在子类中调用父类的析构函数

删除子类中调用的父类析构函数运行结果如下

继承(C++)

仔细观察发现其中也有些不寻常,子类后创建为什么先析构呢?
其实这也是析构函数特殊的地方:子类先析构,父类再析构,子类析构函数中不需要显示调用父类析构,子类析构结束后编译器会自动调用父类析构

继承与友元

友元关系不能继承,基类友元不能访问派生类私有和保护成员

class Person
{
public:friend void Display(const Person& p,const Student s);Person(const char* name = "zhangsan"):_name(name){cout << "Person()" << endl;}protected:string _name;
};class Student :public Person
{
public:Student(const char* name):Person(name), _stuid(210202){cout << "Student()" << endl;}
protected:int _stuid;
};void Display(const Person& p,const Student s)
{cout << p._name << endl;cout << s._stuid << endl;
}int main()
{Person p("zhangsan");Student s("lisi");Display(p,s);return 0;
}

继承(C++)

继承与静态成员

如果基类定义了static静态成员,则整个继承体系里面只有一个这样的成员

class Person
{
public:Person(){++_count;}protected:string _name;public:static int _count;//统计人数
};int Person::_count = 0;class Student :public Person
{
protected:int _stuid;
};int main()
{Person p;Student s;p._count++;cout << p._count << endl;cout << &p._count << endl;s._count++;cout << s._count << endl;cout << &s._count << endl;return 0;
}

继承(C++)

打印的地址都一样,说明static成员,并不在类中,而是存储在静态区中的;静态成员属于整个类,所有对象,同时也属于所有派生类及对象

复杂的菱形继承及菱形虚拟继承

单继承:一个子类只有一个直接父类的继承关系

继承(C++)

多继承:一个子类有两个或者以上直接父类的继承关系

继承(C++)

菱形继承:多继承的一种

继承(C++)

菱形继承存在着某些问题,观察下列代码

class Person
{
public:string _name;
};class Student :public Person
{
protected:int _stuid;
};class Teacher :public Person
{
protected:int _jodid;
};class Assistant :public Teacher, public Student
{
protected:string _majorcourse;
};int main()
{Assistant a;a._name = "zhangsan";return 0;
}

继承(C++)

程序运行之后便会报错,因为Assistant a中有两份_name,一份是Student,一份是Teacher的,所以使用时就造成了二义性;解决方式:使用时加上访问限定符

int main()
{Assistant a;a.Student::_name = "zhangsan";a.Teacher::_name = "lisi";return 0;
}

指定作用域并没有彻底地解决菱形继承所造成的问题,接下来介绍虚拟继承来彻底地解决问题

StudentTeacher中继承 Person时使用虚拟继承

继承(C++)

具体操作如下

class Person
{
public:string _name;
};class Student :virtual public Person
{
protected:int _stuid;
};class Teacher :virtual public Person
{
protected:int _jodid;
};class Assistant :public Teacher, public Student
{
protected:string _majorcourse;
};int main()
{Assistant a;a._name = "zhangsan";return 0;
}

继承(C++)

既然已经知道虚拟继承可以解决菱形继承存在的问题,那么原理是什么呢?要做到知其然知其所以然,接下来就深入学习虚拟继承的原理

虚拟继承的原理

观察下列代码
菱形继承:

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;
};int main()
{D d;d.B::_a = 1;d.C::_a = 2;d._b = 3;d._c = 4;d._d = 5;return 0;
}

通过查看内存,可以很清楚地发现数据冗余

继承(C++)

菱形虚拟继承:

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;
}

继承(C++)

通过查看内存,BC中都存在一个指针,分别对指针进行取地址发现,指针所指向的是虚基表,其中保存着距离虚基类对象_a的偏移量(第二行),通过偏移量可以计算出BC中虚拟继承的_a的位置,在上面已经标注出来。

虚拟继承原理解释如下

继承(C++)

继承和组合

  1. public继承是一种is a的关系,每个派生类对象中都是一个基类对象
  2. 组合是一种has a的关系,比如B组合了A,则每个B对象中都有一个A对象