C++继承
继承引出
继承主要是为了减少代码的重复内容,解决代码复用问题。通过抽象出一个基类(父类),将重复代码写到基类中,在派生类(子类)中实现不同的方法。
语法: class 子类:继承方式 父类
1 class News{ 2 public: 3 void header(){ 4 cout << "头部" << endl; 5 } 6 7 void footer(){ 8 cout << "底部" << endl; 9 } 10 11 void mynew(){ 12 cout << "今日新闻" << endl; 13 } 14 }; 15 16 class Entertain{ 17 public: 18 void header(){ 19 cout << "头部" << endl; 20 } 21 22 void footer(){ 23 cout << "底部" << endl; 24 } 25 26 void mynEntertain(){ 27 cout << "娱乐" << endl; 28 } 29 }; 30 31 //其中两个类中的代码有重复 32 void test00(){ 33 News mynews; 34 Entertain myEnters; 35 } 36 37 //使用继承 38 class BasePage{ 39 public: 40 void header(){ 41 cout << "头部" << endl; 42 } 43 44 void footer(){ 45 cout << "底部" << endl; 46 } 47 }; 48 49 class SonNew:public BasePage{ 50 public: 51 void mynew(){ 52 cout << "今日新闻" << endl; 53 } 54 }; 55 56 class SonEntertain :public BasePage{ 57 public: 58 void mynEntertain(){ 59 cout << "娱乐" << endl; 60 } 61 }; 62 63 void test01(){ 64 SonNew mynews; 65 SonEntertain myEnters; 66 }
继承方式
继承方式有公有继承,私有继承,保护继承。
(1)公有继承:保持父类中的访问属性;
(2)私有继承:将父类中的所有访问属性改为private;
(3)保护继承:除父类中的私有属性,其他改为保护属性。
继承中的对象模型
子类中会继承父类的私有成员,只是被编译器隐藏起来了,无法访问到,通过sizeof(子类class)可以检查出。
1 class Base{ 2 public: 3 int m_A; 4 float m_B; 5 6 private: 7 double m_C; 8 }; 9 10 class Son:public Base{ 11 public: 12 int m_D; 13 }; 14 15 void test01(){ 16 cout << sizeof(Son) << endl; //24,由于字节对齐 4*6 17 }
另外也可以通过开发人员命令提示工具进行查看类布局。
1 cl /d1 reportSingleClassLayoutSon heap.cpp
继承中的构造和析构
(1)子类创建对象时,先调用父类的构造函数,然后再调用自身的构造,析构顺序与构造顺序相反;
(2)子类会继承父类的成员属性和成员函数,但子类不会继承父类构造函数和析构函数;
注意:由于继承中父类和子类的构造、析构顺序原因,当父类中只提供了有参构造时(默认构造等函数会被隐藏),子类仅仅调用默认构造时,会因为子类创建对象时无法调用父类构造函数而报错,这里可以让子类利用初始化列表来显式调用父类有参构造函数来进行父类构造,然后进行子类构造。
1 class Base{ 2 public: 3 Base(){ 4 cout << "默认构造函数" << endl; 5 } 6 ~Base(){ 7 cout << "默认析构函数" << endl; 8 } 9 }; 10 class Son :public Base{ 11 public: 12 Son(){ 13 cout << "子类默认构造函数" << endl; 14 } 15 16 ~Son(){ 17 cout << "子类默认析构函数" << endl; 18 } 19 }; 20 21 class Base1{ 22 public: 23 Base1(int a){ 24 cout << "默认构造函数" << endl; 25 } 26 ~Base1(){ 27 cout << "默认析构函数" << endl; 28 } 29 }; 30 31 class Son1 :public Base1{ 32 public: 33 34 //会报错,Base1不存在默认构造函数 35 //Son1(){ 36 // cout << "子类默认构造函数" << endl; 37 //} 38 39 //初始化列表显式调用父类有参构造函数 40 Son1(int a):Base1(a){ 41 cout << "子类默认构造函数" << endl; 42 } 43 44 ~Son1(){ 45 cout << "子类默认析构函数" << endl; 46 } 47 }; 48 49 void test01(){ 50 Son son; 51 }
继承中的同名处理
(1)父类和子类成员属性同名,用子类声明对象调用子类属性,若想调用父类成员,则加上父类的作用域;
(2)父类和子类成员函数同名,子类函数不会覆盖父类的成员,只是隐藏起来,用子类声明对象调用子类成员函数,若想调用父类函数(包括重载),则加上父类的作用域;
(3)若子类中没有与父类同名的成员函数,子类声明对象后,可以直接调用父类成员函数。
1 class Base{ 2 public: 3 4 void myfun(){ 5 cout << "父类成员函数" << endl; 6 } 7 void myfun(int a){ 8 cout << "父类成员函数" << endl; 9 } 10 }; 11 12 class Son :public Base{ 13 public: 14 15 void myfun(){ 16 cout << "子类成员函数" << endl; 17 } 18 }; 19 20 21 void test01(){ 22 Son son; 23 son.myfun(); 24 son.Base::myfun(); 25 26 son.myfun(10); //报错 27 son.Base::myfun(10); //必须加作用域 28 }
继承中静态成员处理
同名成员和非静态成员基本一致,同样可以被子类继承,但静态成员可以通过类名直接访问,不需要创建对象。
多继承
多继承会产生二义性的问题。如果继承的多个父类中有同名的成员属性和成员函数,在子类调用时,需要指定作用域从而确定父类。
1 class Base{ 2 public: 3 4 void myfun(){ 5 cout << "父类成员函数" << endl; 6 } 7 }; 8 9 class Base1{ 10 public: 11 12 void myfun(){ 13 cout << "父类成员函数" << endl; 14 } 15 }; 16 17 18 19 class Son :public Base, private Base1{ 20 public: 21 22 void myfun1(){ 23 cout << "子类成员函数" << endl; 24 } 25 }; 26 27 28 void test01(){ 29 Son son; 30 31 //son.myfun(); //指定不明确 32 33 //son.Base1::myfun();//Base1为私有成员变量,无法访问 34 son.Base::myfun(); 35 }
菱形继承
两个子类继承于同一个父类,同时又有另外一个类多继承于两个子类,这种继承称为菱形继承。比如羊和驼继承于动物类,同时羊驼继承于羊和驼。
菱形继承会产生两个问题:
(1)羊和驼都继承了动物类的数据和函数,因此羊驼调用数据和函数时,会出现二义性。虽然可以通过作用域确定父类,但我们想要一个数据;
(2)羊驼继承了两份动物类中的某些数据和函数,但只需要一份即可。
1 class Animal{ 2 public: 3 int m_Age; 4 }; 5 6 class Sheep:public Animal{ 7 8 }; 9 10 class Camel :public Animal{ 11 12 }; 13 14 15 class Son :public Sheep, public Camel{ 16 17 }; 18 19 20 void test01(){ 21 22 //子类会继承父类的成员属性,但两者之间不会相互修改 23 //比如修改sheep不会修改到animal的age属性 24 Animal animal; 25 animal.m_Age = 30; 26 27 Sheep sheep; 28 sheep.m_Age = 40; 29 30 //Son为多继承,多继承会产生二义性 31 //虽然可以通过作用域来确定父类,但我们想要的是一份数据 32 Son son; 33 son.Sheep::m_Age = 10; 34 son.Camel::m_Age = 20; 35 36 cout << animal.m_Age << endl; 37 cout << son.Sheep::m_Age << endl; 38 cout << son.Camel::m_Age << endl; 39 }
对上述代码通过工具查看Son类布局,如下所示:
布局显示Son继承Sheep父类,父类继承祖父类,祖父类中有成员属性m_Age;同样Son继承Camel父类,父类继承祖父类,祖父类中有成员属性m_Age,左侧的0和4表示类Son相对地址,即Son的起始地址和Sheep中的m_Age地址为0,加上偏移量4,指向了类内地址为4的Camle的m_Age,Son中包含了两个int类型,大小为8。
菱形继承问题解决:使用虚继承,这样操作的是共享的一份数据,在继承方式前加virtual,这样的话Son类可以直接访问m_Age,不用添加作用域。
1 class Animal{ 2 public: 3 int m_Age; 4 }; 5 6 class Sheep:virtual public Animal{ 7 8 }; 9 10 class Camel :virtual public Animal{ 11 12 }; 13 14 15 class Son :public Sheep, public Camel{ 16 17 }; 18 19 void test01(){ 20 Son son; 21 22 son.m_Age = 10; 23 }
利用开发者工具对虚继承的代码查看Son类布局,如下所示:
布局显示Son继承Sheep父类,父类中有虚指针vbptr(virtual base pointer),对象结构类似结构体,首元素是虚基类指针,其余为自身数据(不包括静态成员和成员函数)(具体详见C++多态之内存中对象的数据结构);同样Son继承Camel父类,父类中有虚指针vbptr(virtual base pointer),左侧的0、4、8同样表示类内相对地址,Son的起始地址和Sheep的虚指针地址为0,Camel的虚指针地址为4,Animal中的m_Age为8。
Sheep父类的虚指针指向下面Sheep的虚基类表vbtale@Sheep(virtual base table),虚基类表是一个整型数组,数组第二个元素值为8,即Sheep的虚指针地址偏移8指向Animal的m_Age地址。Camel父类同理,因此,类中只有一个m_Age元素。
Son中包含了两个指针和一个int类型,所以大小为12。
虚继承工作原理
通过虚基类表的地址索引输出虚基类指针的偏移量及虚基类数据,对照上图虚基类指针和虚基类表,访问虚基类的偏移量:
(1) &son 表示对son取地址,也表示父类Sheep虚基类指针的地址,也表示Sheep虚基类表(整型数组)的地址
(2) (int*)&son 对地址进行强转,&son的类型为class son*,转为整型指针
(3) *(int*)&son 对地址解引用,返回整型数组的首地址,也是第一个元素的地址,为整型类型
(4) *(int*)&son + 1 为第二个元素的地址,偏移量为一个int类型
(5) (int*)*(int*)&son + 1对一个地址进行解引用,需要先将其转化为对应元素的指针类型,这里为整型指针类型
(6) 对上述地址进行解引用,访问偏移量
(7) (int*)&son + 1 访问tuo的偏移量时,需要先指向tuo的虚基类指针,因此需要加偏移量,这里的1表示偏移一个整型字节
补充(7),如果m_Age不是int类型,而是double类型,此时偏移量为一个double
1 class Animal{ 2 public: 3 int m_Age; 4 //double m_Age; 5 }; 6 7 class Sheep:virtual public Animal{ 8 9 }; 10 11 class Camel :virtual public Animal{ 12 13 }; 14 15 16 class Son :public Sheep, public Camel{ 17 18 }; 19 20 void test01(){ 21 Son son; 22 int *a; 23 son.m_Age = 10; 24 25 //----------------------访问虚基类的偏移量---------------------- 26 //(1) &son 表示对son取地址,也表示父类Sheep虚基类指针的地址,也表示Sheep虚基类表(整型数组)的地址 27 //(2) (int*)&son 对地址进行强转,&son的类型为class son*,转为整型指针 28 //(3) *(int*)&son 对地址解引用,返回整型数组的首地址,也是第一个元素的地址,为整型类型 29 //(4) *(int*)&son + 1 为第二个元素的地址,偏移量为一个int类型 30 //(5) (int*)*(int*)&son + 1对一个地址进行解引用,需要先将其转化为对应元素的指针类型,这里为整型指针类型 31 //(6) 对上述地址进行解引用,访问偏移量 32 //(7) (int*)&son + 1 访问tuo的偏移量时,需要先指向tuo的虚基类指针,因此需要加偏移量,这里的1表示偏移一个整型字节 33 //补充(7),如果m_Age不是int类型,而是double类型,此时偏移量为一个double, 34 35 36 cout << typeid(son.m_Age).name() << endl; 37 cout << typeid(&son).name() << endl; 38 cout << typeid(*(int*)&son).name() << endl; 39 cout << typeid((*(int*)&son) + 1).name() << endl; 40 cout << typeid(*((int*)&son + 2)).name() << endl; 41 cout << "--------------------" << endl; 42 cout << *(int*)((int*)*(int*)&son + 1) << endl; //访问sheep的偏移量 43 cout << *((int*)*(int*)&son + 1) << endl; //访问sheep的偏移量 44 45 cout << *((int*)*((int*)&son + 2) + 1) << endl;//访问tuo的偏移量 46 cout << *(int*)((int*)*((int*)&son + 2) + 1) << endl;//访问tuo的偏移量 47 48 49 //----------------------访问m_Age---------------------- 50 //&son 表示对son取地址,将其转为char型,加8表示要偏移8个字节,若为int型,加8表示偏移32字节 51 //取son的地址,加上偏移量,转到animal的m_Age地址处,需要强转为Animal * 52 //取->进行间接访问 53 cout << ((Animal*)((char*)&son + *(int*)((int*)*(int*)&son + 1)))->m_Age << endl; 54 }