学习总结

(1)实例化Worker对象的时候——先基类的构造函数,即要想实例化一个派生类,必须先(隐性)实例化一个基类; 在销毁Worker这个对象的时候,——是先子类的析构函数(和构造函数的顺序相反)。

(2)子类继承了父类后,观察是否继承了父类的数据成员和成员函数。Worker对象,子类可以访问父类的成员函数eat(),也可访问自己的成员函数work()。is-a用了很大篇幅介绍(栗子),public继承必须满足is-a关系,父类指针指向子类对象等;

(3)多继承是有多个基类;而多重继承指有多个连续继承关系。virtual是重点,如虚析构函数,还有菱形继承中,子类继承父类时需要加上virtual;为了保存一份基类数据,在菱形继承中,基类进行宏定义,就不会产生基类的重定义报错。

(4)如果函数的参数是基类的对象,则基类的对象和派生类的对象都可以作为实参数传递进来,并能够正常使用。

(5)9种继承的情况,private私有成员变量或者私有类型函数,只能被本类的成员函数访问,类外部访问不了;C++的结构体的继承默认是public,而类的继承默认是private

  • protected继承将父类中的public成员变为子类的protected成员 ;
  • private继承使得父类所有成员在子类中的访问权限变为private

文章目录

  • 学习总结
  • 一、继承
    • (1)内存中的对象
    • (2)继承代码实践
      • 头文件Person.h
      • 头文件Worker.h
      • Person.cpp
      • Worker.cpp
      • 主函数demo.cpp
      • 目的1 :调用顺序
      • 目的2:是否继承
  • 二、继承方式、隐藏
    • (1)公有继承
    • (2)保护继承
    • (3)私有继承
    • (4)继承中的特殊关系
      • 1)隐藏(父类子类有同名函数)
      • 2) 不好的习惯:
      • 3)隐藏代码实践
        • 头文件
        • 源程序
        • 运行结果&分析
        • 探究是否参数问题
        • 数据成员同名
    • (5)is-a
      • 1)什么是is-a
      • 2)存储结构
      • 3)is-a代码实践(父类指针指向子类对象)
      • 1)头文件
      • 2)源文件
      • 【变式1】调用父类的play
      • 【变式2】分开赋值
      • 【变式3】用指针
      • 【变式4】父类的指针,不能调用子类的成员函数
      • 【变式5】销毁指针时用哪个析构函数
    • (6)虚析构函数
    • (7)三个test函数
      • 1)test1函数
      • 2)test2函数
      • 3)test3函数
  • 三、多继承和多重继承
    • (1)多重继承
    • (2)多继承
    • (3)多重继承代码实践
      • 1)头文件
      • 2)源文件
    • (4)多继承代码实践
      • 1)头文件
      • 2)源程序
  • 四、虚拟继承
    • (1)多继承+多重继承的烦扰
    • (2)虚继承
    • (3)虚继承编码实践
      • 1)头文件
      • 2)源程序
    • (4)宏定义(解决菱形继承中,基类的重定义)
    • (5)菱形继承的构造和析构顺序

一、继承

工人类属于人类,在定义工人类时为了不重复写部分代码,即两个类具包含关系。

class Worker:public Person{public:void work();int m_iSalary;
};

PS :继承

(1)内存中的对象

在定义工人类时,就不需要再定义人类共有的属性。

(2)继承代码实践

/* ************************************************************/
/* 继承
要求:
​ 1.定义Person类
​ 数据成员:姓名(m_strName)和年龄(m_iAge)
​ 成员函数:构造函数、析构函数、eat()函数​ 2.定义Worker类
​ 公有继承Person类,特有数据成员(工资 m_iSalary)
​ 成员函数:构造函数、析构函数、work()函数

​ 目的:
​ 1.实例化Worker对象的时候,到底是先调用谁的构造函数,
​ 在销毁Worker这个对象的时候,又是先调用谁的析构函数

​ 2.子类继承了父类后,观察是否继承了父类的数据成员和成员函数

文件结构为:

头文件Person.h

#include<string>
using namespace std;
class Person{public:Person();~Person();void eat();string m_strName;int m_iAge;
};

头文件Worker.h

#include"Person.h"class Worker:public Person{//Worker共有继承Person类
public:Worker();~Worker();void work();int m_iSalary;
};

Person.cpp

#include"Person.h"
#include<iostream>
using namespace std;Person::Person(){cout<<"Person()"<<endl;
}
Person::~Person(){cout<<"~Person()"<<endl;
}void Person::eat(){cout<<"eat"<<endl;
}

Worker.cpp

构造函数、析构函数、work()。

#include"Worker.h"
#include<iostream>
using namespace std;Worker::Worker(){cout<<"Worker()"<<endl;
}
Worker::~Worker(){cout<<"~Worker()"<<endl;
}
void Worker::work(){cout<<"work()"<<endl;
}

主函数demo.cpp

#include<iostream>
#include<stdlib.h>
#include"Worker.h"
using namespace std;int main(){Worker *p=new Worker();//堆delete p;p=NULL;system("pause");return 0;
}

结果为:

目的1 :调用顺序

实例化Worker对象的时候,到底是先调用谁的构造函数
——先基类的构造函数,即要想实例化一个派生类,必须先(隐性)实例化一个基类。

在销毁Worker这个对象的时候,又是先调用谁的析构函数——是先子类的析构函数(和构造函数的顺序相反)。

目的2:是否继承

2.子类继承了父类后,观察是否继承了父类的数据成员和成员函数。

#include<iostream>
#include<stdlib.h>
#include"Worker.h"using namespace std;int main()
{Worker *p = new Worker();p->m_strName = "Keiven";p->m_iAge = 20;p->eat();p->m_iSalary = 5000;p->work();delete p;p = NULL;system("pause");return 0;
}

从如下结果,看出Worker对象,子类可以访问父类的成员函数eat(),也可访问自己的成员函数work()

二、继承方式、隐藏

(1)公有继承

公有继承的方式时,按下表(注意:Protected成员是按照protected继承方式)

基类成员访问属性 继承方式 派生类成员访问属性
private成员 public 无法访问
protected成员 protected
public成员 public

(2)保护继承

保护继承时,按下表(注意:虽然基类的private成员会被继承,但也会无法访问)

基类成员访问属性 继承方式 派生类成员访问属性
private成员 protected 无法访问
protected成员 protected
public成员 protected

(3)私有继承

私有继承时,按下表(注意:虽然基类的private成员会被继承,但也会无法访问)

基类成员访问属性 继承方式 派生类成员访问属性
private成员 private 无法访问
protected成员 private
public成员 private
class Line{public:Line(int x1,int y1,int x2,int y2);
private:Coordinate m_coorA;Coordinate m_coorB;
};

线段类Line只能访问到A点和B点这两个对象的共有数据成员和共有成员函数——Has a关系,即在线段中有一个坐标点的这种包含关系。

而私有继承即子类的对象只能访问父类的共有数据和共有的成员函数——私有继承也是一种包含关系(Has a关系)。

(4)继承中的特殊关系

1)隐藏(父类子类有同名函数)

如果父类A和子类B都定义了一个成员函数ABC(),由于子类B类继承A后即B中也拥有A中的成员函数ABC()
——此时子类B类中的ABC()函数会隐藏掉父类A中的ABC()函数。

隐藏的特性:父子关系、成员同名、隐藏。
(1)在实例化B的对象时,使用该对象只能够直接访问子类B中的ABC()成员函数,而无法访问A类的ABC()。但是我们能够通过特殊手段访问。

(2)同名的隐藏不仅限于成员函数,还有同名的数据成员——不过父子类的数据成员同名没啥意义,所以少见。

class Person{public:void play();
protected:string m_strName;
};
class Soldier:public Person{public:void play();void work();
protected:int m_iCode;
};

上面例子就是父类和子类都有work成员函数,而真正访问的时候:

int main(){Soldier soldier;soldier.play();soldier.Person::play();//注意return 0;
}

2) 不好的习惯:

让父类的数据成员和子类的数据成员同名,如Person类定义string code,Soldier类定义int code。
——因为2个数据成员都定义在protected下,即实例化的对象无法访问到子类和父类(protected成员是只有子类能访问)——而子类soldier的成员函数去使用code,访问的就是soldier类的code,如code="1234"

如果子类soldier的成员函数要访问父类Person继承下来的code数据成员,就必须使用Person::code="5678"

3)隐藏代码实践

/*******************************/
/* 继承关系中的隐藏
要求:
​ 1. Person类,数据成员:m_strName,成员函数:构造函数、play()
​ 2. Soldier类,数据成员:无,成员函数:构造函数、play()、work()
/*******************************/

头文件
#include<string>
using namespace std;
class Person{public:Person();void play();
protected:string m_strName;
};
#include"Person.h"class Soldier:public Person{public:Soldier();void play();void work();
protected:
};
源程序

关键在于main函数的访问父类和子类的play()的两种写法。

#include"Person.h"
#include<iostream>
using namespace std;Person::Person(){m_strName="Mery";
}
void Person::play(){cout<<"Person---play()"<<endl;cout<<m_strName<<endl;
}
#include"Soldier.h"
#include<iostream>
using namespace std;Soldier::Soldier(){}
void Soldier::play(){cout<<"Soldier---play()"<<endl;
}
void Soldier::work(){cout<<"work()"<<endl;
}
#include<iostream>
#include<stdlib.h>
#include"Soldier.h"using namespace std;
int main(){Soldier Soldier;Soldier.play();//用子类的playSoldier.work();Soldier.Person::play();//用父类的playsystem("pause");
}
运行结果&分析


关键在于main函数的访问父类和子类的play()的两种写法。

探究是否参数问题

上面是父类和子类的同名函数play()的参数一样(都是无参),这次我们试试参数不同,看是否能有【隐藏】效果。
现对子类Soldierplay函数改为传入一个参数的:

void Soldier::play(int x){cout<<"Soldier---play()"<<endl;
}

而在主函数这次我们为了看能否用Soldier.play()的无参形式访问到父类的play(),用下面代码发现报错。

#include<iostream>
#include<stdlib.h>
#include"Soldier.h"using namespace std;
int main(){Soldier Soldier;Soldier.play();//用子类的playSoldier.work();Soldier.Person::play();//用父类的playsystem("pause");
}
1>------ 已启动生成: 项目: person_soldier, 配置: Debug Win32 ------
1>  demo.cpp
1>c:\users\86493\desktop\王道ds\20210420\person_soldier\demo.cpp(8): error C2660: “Soldier::play”: 函数不接受 0 个参数
========== 生成: 成功 0 个,失败 1 个,最新 0 个,跳过 0 个 ==========

由上面报错即知不用Soldier.play()的无参形式访问到父类的play(),要访问父类的play()只能用Soldier.Person::play()

数据成员同名

(5)is-a

1)什么是is-a

is-a字面上理解是一个从属派生关系,如“学生是人”,但反过则不一定成立。

在C++中,public继承必须满足is-a关系,如果子类B和父类A满足is-a关系,则A对象能够派上用场的地方,B对象一定可以用,父类表现的是一般化的形式,而子类表示的是特殊化的概念。

public继承意味着is-a。适用于base classes身上的每一件事情一定适用于derived classes身上,因为每一个derived对象也都是base class对象。

【栗子1】

栗子分析:
(1)第一二行:首先实例化Soldier类的一个对象s1,然后将实例化Person对象p1接收s1(注:语法上是正确的,即一个soldier也是人,可以用soldier初始化人)。

(2)第三行:定义一个的Person指针p2指向Soldier对象s1

(3)第四五行:将人的对象赋值给soldier(s1 = p1),同时用一个soldier的指针指向一个人对象(Soldier *s2 = &p1),这两种写法都有问题。

总结:
(1)派生类的对象可以赋值给基类,基类指针可以指向派生类的对象。
(2)基类的指针 or 基类的对象 or 基类的引用 可以作为函数的参数,来接收传入的子类的对象。

【栗子2】

栗子分析:
(1)fun1的参数是一个Person指针,即可以指向Person对象,也可以指向子类对象Soldier
(2)fun2可以传入p1s1,因为fun2参数是Person的引用,所以不需要加&符号,直接传对象本身就行。

2)存储结构

情况一:将子类的对象赋值给(或者说 初始化)父类的对象。

如果父类中含有m_strNamem_iAge这两个数据成员的时候,那么子类在继承父类的时候一定也含有m_strNamem_iAge这两个数据成员,同时,子类应该还含有其自身的数据成员。当我们用子类的对象向父类的对象赋值或者是用子类的对象初始化父类的一个对象的时候,它的本质就是将子类当中从父类继承下来的数据成员赋值给父类的对象,那么子类中其他的数据成员此时就会被截断。

因为,对于父类来说,它只能接收自己拥有的数据成员的数据,而无法接收其他数据。如果是用父类的指针指向一个子类对象,那么,父类的指针也只能够访问到父类所拥有的数据成员。而无法访问到子类所独有的数据成员。也就是说,如果我们用一个父类指针去指向一个子类对象的话,我们只能够通过这个父类指针去访问父类原有的数据成员和成员函数,而无法访问子类所独有的数据成员和成员函数。

3)is-a代码实践(父类指针指向子类对象)

继承关系中的隐藏,要求:
​ 1. Person类,数据成员:m_strName,成员函数:构造函数、play()
​ 2. Soldier类,数据成员:m_iAge,成员函数:构造函数、析构函数、work()
​ 3. 定义函数test1(Person p)test2(Person &p)test3(Person *p)

1)头文件

头文件Person.h

#include<string>
using namespace std;class Person{public:Person(string name = "Jim");//这里给定默认参数值~Person();void play();
protected:string m_strName;
};

头文件Soldier.h

#include"Person.h"
//这里如果不包含这个头文件,编译时就会出现“Person”未定义基类class Soldier:public Person{public:Soldier(string name = "James", int age = 20);//这里也给定默认参数值~Soldier();void work();
protected:int m_iAge;
};

2)源文件

源文件Person.cpp

#include"Person.h"
#include<iostream>
using namespace std;Person::Person(string name){m_strName = name;cout <<"Person()"<< endl;
}
Person::~Person(){cout <<"~Person()"<< endl;
}
void Person::play(){cout <<"Person---play()"<< endl;cout << m_strName << endl;
}

源文件Soldier.cpp

#include"Soldier.h"
#include<iostream>
using namespace std;Soldier::Soldier(string name, int age){m_strName = name;m_iAge = age;cout <<"Soldier()"<< endl;
}
Soldier::~Soldier(){cout <<"~Soldier()"<< endl;
}
void Soldier::work(){cout << m_strName << endl;cout << m_iAge << endl;cout <<"Soldier--work()"<< endl;
}

源文件demo.cpp:这里实例化的soldier对象,然后用赋值给Person类的对象pPerson是父类,所以可以这样赋值)。

这里可以思考下pm_strName的值:
在下面的demo.cpp中我们调用p.play()得到

#include<iostream>
#include<stdlib.h>
#include"Soldier.h"using namespace std;/*******************************/
/* 继承关系中的隐藏
要求:
​ 1. Person类,数据成员:m_strName,成员函数:构造函数、play()
​ 2. Soldier类,数据成员:m_iAge,成员函数:构造函数、析构函数、work()
​ 3. 定义函数test1(Person p) test2(Person &p) test3(Person *p)
/*******************************/int main(){Soldier soldier;Person p = soldier;p.play();system("pause");return 0;
}

下面结果前两行:
第一行实例化Soldier对象,对象实例化会自动调用默认构造函数,这里会先调用父类的默认构造函数,再调用自身的默认构造函数。

第三行:调用p.play()函数后是调用父类的play()函数;

第四行的m_strNameJames(这个James名字的初始化是在Soldier类的构造函数里初始化列表里的默认参数值),我们使用soldier去初始化Person的对象p,就使得p中的m_strName的值是James。意味着,这里做了截断(前面介绍过,即把soldier中的m_strName赋值给了p中的m_strName)。

【变式1】调用父类的play

因为p有默认构造函数,并且有默认值“Jim”,当调用单纯Person对象的play函数时,一定打印出的是Jim

int main()
{Soldier soldier;Person p;p.play();system("pause");return 0;
}

【变式2】分开赋值

最初栗子我们是用soldier实例化p,现在我们让soldier直接赋值给p对象(即分开写了)。所以无论是用soldier去初始化p,还是将soldier直接赋值给p,那么soldier中的m_strName都可以赋值给其父类中对应的那个数据成员。

int main()
{Soldier soldier;Person p;p = soldier;p.play();system("pause");return 0;
}

【变式3】用指针

结果和上面也是一样的。无论是用对象赋值的方式,还是用指针指向的方式,如果用父类去指向或者接收子类对象的值,打印出的都是子类对象所拥有的那个值。

int main()
{Soldier soldier;Person *p = &soldier;p->play();system("pause");return 0;
}

【变式4】父类的指针,不能调用子类的成员函数

下面这样会报错:Person类对象的指针只能调用自己的数据成员和成员函数,无法调用其子类的成员和成员函数。

int main()
{Soldier soldier;Person *p = &soldier;p->play();p->work();system("pause");return 0;
}

【变式5】销毁指针时用哪个析构函数

如果通过父类的指针指向子类的对象,那么指针销毁的时候,究竟执行的是父类的析构函数还是子类的析构函数呢?——下面栗子看出。只执行了父类的析构函数。即子类的析构函数没有执行,即可能造成内存泄漏。

为了避免这种情况的内存泄漏,下一小节会介绍【虚析构函数】。

int main()
{Person *p = new Soldier;p->play();delete p;p = NULL;system("pause");return 0;
}

(6)虚析构函数

当存在继承关系的时候,我们使用父类的指针去指向堆中的子类的对象,并且我们还想使用父类的指针去释放这块内存,这个时候就需要使用虚析构函数。写法很简单,只需要在析构函数前面加上关键字virtual即可。如果父类的析构函数是虚析构函数,那么其子类的析构函数无论前面加不加关键字virtual,其都是虚析构函数(建议都加上,方便理解)。

现在修改析构函数使其变成虚析构函数,即分别在Person.hSoldier.h中在析构函数的前面加上关键字virtual即可,如下,main函数和变式5的相同:

virtual ~Person();
virtual ~Soldier();


最后两行分别执行的是Soldier类的析构函数和Person类的析构函数,也就是说,它可以将soldier这个对象完全的释放掉。

(7)三个test函数

demo.cpp中定义如下的三个test函数。

void test1(Person p)
{p.play();
}void test2(Person &p)
{p.play();
}
void test3(Person *p)
{p->play();
}

main函数中,分别实例化PersonSoldier类,即两个对象ps,分别传参给三个test函数,注意区别。

1)test1函数

void test1(Person p)
{p.play();
}int main()
{    Person p;Soldier s; test1(p);test1(s);system("pause");return 0;
}

结果为如下,分析:
第一行:实例化Person对象所以打印出Person()

第二三行:实例化子类Soldier对象,所以先调用父类的默认构造函数,再调用自身的构造函数,所以分别打印出Person()Soldier()

第四行:调用test1函数,传参传值时需要实例化一个临时对象p(后面调用play函数),在test1函数执行完毕后,p对象会被销毁,即第6和9行分别两次析构~Person()

第五行:当我们传入Person对象时,打印出是Jim;当我们传入Soldier对象时,打印出是JamesSoldier类的默认初始化列表赋值)。

小结:如果函数的参数是基类的对象,则基类的对象和派生类的对象都可以作为实参数传递进来,并能够正常使用。

2)test2函数

void test2(Person &p)
{p.play();
}int main(){Person p;Soldier s;test2(p);test2(s);system("pause");return 0;
}

前三行和test1的结果一样;
因为test2传参的是一个引用,所以在传参时会起一个别名p(通过该p调用play)。这个过程中,并没有实例化临时对象,所以也没有销毁临时对象的痕迹。其他打印结果和test1相同。
小结:使用基类的引用,也可以接收基类的对象以及派生类的对象。

3)test3函数

void test3(Person *p)
{p->play();
}int main(){Person p;Soldier s;//因为test3要求传入的是一个指针,//所以这里需要传入的是p和s的地址test3(&p);test3(&s);system("pause");return 0;
}

结果和test2相同,因为test3的参数是一个指针(这里是基类Person的指针),当传递基类 or 派生类的对象的地址时,会用指针p分别调用基类和子类对象的play函数,所以分别打印出JimJames(名字不同)。

小结:通过对于test1、test2和test3三个函数的对比调用,可以发现,使用test2和test3并不会产生新的临时变量,所以效率更高。

三、多继承和多重继承

(1)多重继承


如果这三个类在继承的时候,都使用的是public方式,也存在如下关系:

class Person{...
};
class Soldier: public Person{...
};
class Infrantryman: public Soldier{...
};

(2)多继承

多继承:一个派生类同时有两个基类,如农民工类同时继承工人类和农民类。注意多继承 ≠ 多重继承。

在多继承的情况下,如果农民工在继承工人和农民的时候,都是以public方式继承的话,那么它们还存在着这样一种关系:农民工是一个工人,农民工是一个农民,但是,工人和农民这两个类本身是平行的,如下:

class Worker{...};
class Farmer{...};
class MigrantWorker: public Worker, public Farmer{...};

(3)多重继承代码实践

多重继承

要求:1.Person类,数据成员: m_strName,成员函数:构造函数,析构函数,play()

​ 2.Soldier类,数据成员:m_iAge,成员函数:构造函数,析构函数,work()

​ 3.Infantry类,数据成员:无,成员函数:构造函数,析构函数,attack()

​ 4.定义函数test1(Person p) test2(Person &p) test3(Person *p)

1)头文件

回顾上面说的三个类的继承关系如下图:

Person.h文件(基类):

#include<string>
using namespace std;class Person{public:Person(string name = "Jim");virtual ~Person();void play();
protected:string m_strName;
};

Soldier.h头文件:继承人类。

#include<string>
#include"Person.h"
//这里如果不包含这个头文件,编译时就会出现“Person”未定义基类class Soldier:public Person{public:Soldier(string name = "James", int age = 20); //这里也给定默认参数值~Soldier();void work();
protected:int m_iAge;
};

Infantry.h头文件:这里的步兵继承Soldier父类。

//步兵类
#include"Soldier.h"class Infantry:public Soldier
{public:Infantry(string name = "Jack", int age = 30);~Infantry();void attack();
};

2)源文件

Person.cpp文件:

#include"Person.h"
#include<iostream>
using namespace std;Person::Person(string name){m_strName = name;cout <<"Person()"<< endl;
}
Person::~Person(){cout <<"~Person()"<< endl;
}
void Person::play(){cout <<"Person---play()"<< endl;cout << m_strName << endl;
}

Soldier.cpp文件:

#include"Soldier.h"
#include<iostream>
using namespace std;Soldier::Soldier(string name, int age){m_strName = name;m_iAge = age;cout <<"Soldier()"<< endl;
}
Soldier::~Soldier(){cout <<"~Soldier()"<< endl;
}
void Soldier::work(){cout << m_strName << endl;cout << m_iAge << endl;cout <<"Soldier--work()"<< endl;
}

Infantry.cpp文件:

#include<iostream>
#include"Infantry.h"
using namespace std;Infantry::Infantry(string name, int age){m_strName = name;m_iAge = age;cout <<"Infantry()"<< endl;
}
Infantry::~Infantry(){cout <<"~Infantry()"<< endl;
}
void Infantry::attack(){cout << m_strName << endl;cout << m_iAge << endl;cout <<"Infantry--attack()"<< endl;
}

demo.cpp文件:

#include<iostream>
#include<stdlib.h>
#include"Infantry.h"
using namespace std;void test1(Person p){p.play();
}void test2(Person &p){p.play();
}
void test3(Person *p){p->play();
}int main(){Infantry infantry;system("pause");return 0;
}

一开始的main函数中我们只实例化一个步兵对象,打印出3行(分别是Person类、Soldier类、Infantry类的构造函数),二如果是销毁时析构函数则会按照构造函数的逆序执行。

三个test函数:
一个子类的对象可以作为函数参数传入这三个函数当中。这里的三个函数要求的参数是Person的对象、引用、指针。

void test1(Person p){p.play();
}void test2(Person &p){p.play();
}
void test3(Person *p){p->play();
}int main()
{Infantry infantry;test1(infantry);test2(infantry);test3(&infantry);system("pause");return 0;
}


分析上面的结果:
前3行:实例化步兵对象。
三个test函数内都有调用play函数,所以有3个Person---play()打印出来。

小结:无论继承关系有多少层,它们只要保持着直接或者间接的继承关系,那么子类都可以与自己的直接父类或者间接父类称之为is-a的关系,并且能够通过父类的指针对直接子类或间接子类的对象进行相应的操作。

(4)多继承代码实践

多继承

要求:1.Farmer类,数据成员: m_strName【农民姓名】,成员函数:构造函数,析构函数,sow()【播种函数】

​ 2.Worker类,数据成员:m_strCode【工人工号】,成员函数:构造函数,析构函数,carry()【搬运函数】

​ 3.MigrantWorker类,数据成员:无,成员函数:构造函数,析构函数

1)头文件

Farmer.h头文件:

#include<string>
using namespace std;class Farmer{public:Farmer(string name = "Jack");virtual ~Farmer();void sow();
protected:string m_strName;
};

Worker.h头文件:

#include<string>
using namespace std;class Worker{public:Worker(string code = "001");virtual ~Worker(); //虚析构函数void carry();
protected:string m_strCode;
};


MigrantWorker.h头文件,如上图所示,农民工类同时继承2个类:

#include"Worker.h"
#include"Farmer.h"//农民工类继承农民类和工人类
class MigrantWorker:public Farmer, public Worker{public:MigrantWorker(string name,string code);~MigrantWorker();
};

2)源程序

两个基类的构造函数、析构函数和成员函数:
Farmer.cpp文件:

#include"Farmer.h"
#include<iostream>
using namespace std;Farmer::Farmer(string name){m_strName = name;cout <<"Farmer()"<< endl;
}Farmer::~Farmer(){cout <<"~Farmer()"<< endl;
}
void Farmer::sow(){cout << m_strName << endl;cout <<"Farmer---sow()"<< endl;
}

Worker.cpp文件:

#include"Worker.h"
#include<iostream>
using namespace std;Worker::Worker(string code){m_strCode = code;cout <<"Worker()"<< endl;
}
Worker::~Worker(){cout <<"~Worker()"<< endl;
}
void Worker::carry(){cout << m_strCode << endl;cout <<"Worker---carry()"<< endl;
}

MigrantWorker.cpp文件:继承自上面两个基类的农民工类。采用初始化列表的方式将农民工的姓名和工号分别传递给农民的m_strName和工人的m_strCode

#include"MigrantWorker.h"
#include<iostream>
using namespace std;//采用初始化列表的方式将农民工的姓名和工号分别传递给农民的m_strName和工人的m_strCode
MigrantWorker::MigrantWorker(string name, string code):Farmer(name),Worker(code){cout <<"MigrantWorker()"<< endl;
}
MigrantWorker::~MigrantWorker(){cout <<"~MigrantWorker()"<< endl;
}

demo.cpp文件:采用堆的方式实例化农民工对象,其中姓名将来传给农民类,工号传给工人类。

#include<iostream>
#include<stdlib.h>
#include"MigrantWorker.h"
using namespace std;int main(){//采用堆的方式实例化农民工对象MigrantWorker *p = new MigrantWorker("Merry", "100"); //姓名将来传给农民类,工号传给工人类p->carry();p->sow();delete p;p = NULL;system("pause");return 0;
}


分析上面的结果:
(1)前3行:依次3个构造函数。在实例化一个子类的对象时,它会先调用父类的构造函数,如果它有多个父类,那么它会依次调用每一个父类的构造函数,其调用顺序与初始化列表的顺序是一样的。
(2)先是打印出工号100;
(3)p调用carry函数,即打印Worker中的carry函数;
(4)p调用sow函数,因为姓名传给Farmer类了所以打印的姓名是Merry,然后Farmer---sow()
(5)最后3个析构函数打印顺序和构造函数相反。

四、虚拟继承

(1)多继承+多重继承的烦扰

如下图所示,A类是父类,B和C继承了A类,D类同时继承B类和C类,这种继承关系(菱形继承),既有多继承,也有多重继承。

按理说这种情况的继承,D中会有两个一样的A数据,需要解决这种耗内存的问题。再举个之前的栗子:工人、农民和农民工是三个类的继承关系如图:

(2)虚继承

虚继承是继承的一种方式,上面栗子中,工人类称为虚基类,工人类去继承人类时,需要加上关键字virtual。这样农民工类就能继承工人类和农民类了,实例得到的农民工对象也只有一份Person的数据。

(3)虚继承编码实践

虚继承:
说明:通过这个例子学习虚继承的使用方法,及虚继承存在的必要性
要求:
1.Farmer类,数据成员: m_strName【农民姓名】,成员函数:构造函数,析构函数,sow()【播种函数】
​ 2.Worker类,数据成员:m_strCode【工人工号】,成员函数:构造函数,析构函数,carry()【搬运函数】
​ 3.MigrantWorker类,数据成员:无,成员函数:构造函数,析构函数
注:采用初始化列表的方式将农民工的姓名和工号分别传递给农民的m_strName和工人的m_strCode
​ 4.Person类,数据成员:m_strColor【人的肤色】,成员函数:构造函数,析构函数,printColor()

1)头文件

几个类之间的关系:

Farmer.h头文件:

#include<string>
#include"Person.h"
using namespace std;class Farmer:public Person
{public:Farmer(string name = "Jack", string color = "blue");virtual ~Farmer();void sow();
protected:string m_strName;
};

MigrantWorker.h头文件:

#include"Worker.h"
#include"Farmer.h"//农民工类继承农民类和工人类
class MigrantWorker: public Farmer, public Worker{public:MigrantWorker(string name, string code, string color);~MigrantWorker();
};

Person.h头文件:

#include<string>
usingnamespace std;class Person
{public:Person(string color = "blue");virtual ~Person();void printColor();
protected:string m_strColor;
};

Worker.h头文件:

#include<string>
#include"Person.h"
using namespace std;classWorker:publicPerson
{public:Worker(string code = "001", string coloe = "blue");//我们希望Worker可以传入参数“肤色”给Person类virtual ~Worker(); //虚析构函数void carry();
protected:string m_strCode;
};

2)源程序

Person.cpp源程序:

#include<iostream>
#include"Person.h"
using namespace std;Person::Person(string color)
{m_strColor = color;cout <<"Person()"<< endl;
}Person::~Person()
{cout <<"~Person()"<< endl;
}voidPerson::printColor()
{cout << m_strColor << endl;cout <<"Person---printColor()"<< endl;
}

Farmer.cpp源程序:

#include"Farmer.h"
#include<iostream>
using namespace std;Farmer::Farmer(string name, string color):Person(color)
{m_strName = name;cout <<"Farmer()"<< endl;
}Farmer::~Farmer()
{cout <<"~Farmer()"<< endl;
}
voidFarmer::sow()
{cout << m_strName << endl;cout <<"Farmer---sow()"<< endl;
}

Worker.cpp源程序:

#include"Worker.h"
#include<iostream>
usingnamespace std;Worker::Worker(string code, string color):Person(color)
{m_strCode = code;cout <<"Worker()"<< endl;
}
Worker::~Worker()
{cout <<"~Worker()"<< endl;
}
voidWorker::carry()
{cout << m_strCode << endl;cout <<"Worker---carry()"<< endl;
}

MigrantWorker.cpp源程序:

#include"MigrantWorker.h"
#include<iostream>
usingnamespace std;//采用初始化列表的方式将农民工的姓名和工号分别传递给农民的m_strName和工人的m_strCode
MigrantWorker::MigrantWorker(string name, string code, string color):Farmer(name, color),Worker(code, color)
{cout <<"MigrantWorker()"<< endl;
}
MigrantWorker::~MigrantWorker()
{cout <<"~MigrantWorker()"<< endl;
}

demo.cpp源程序:

#include<iostream>
#include<stdlib.h>
#include"MigrantWorker.h"
using namespace std;int main(){MigrantWorker *p = new MigrantWorker("Merry", "200", "Yellow");p->Farmer::printColor();p->Worker::printColor();delete p;p = NULL;system("pause");return 0;
}

会发现如下的报错:

Farmer.hWorker.h中,我们都引用了”Person.h”,这样就对Person这个类进行了重定义,所以会报这个错误。为了解决重定义问题,有一种方法——宏定义。

(4)宏定义(解决菱形继承中,基类的重定义)

首先,我们在公共的被继承的这个类的.h文件当中,加上如下几行代码,注意#endif是写在最后一行的,这样就不会报错重定义了。

#ifndef PERSON_H  //如果没有定义 PERSON_H
#define PERSON_H//那么就定义 PESON_H
#endif//结束定义

即在Person.h文件中加上这三行代码:

#ifndef PERSON_H  //如果没有定义 PERSON_H
#define PERSON_H//那么就定义 PESON_H#include<string>
using namespace std;class Person{public:Person(string color = "blue");virtual ~Person();void printColor();
protected:string m_strColor;
};
#endif//结束定义

(5)菱形继承的构造和析构顺序

我们继续(3)中的虚继承实践,在main函数中实例化一个农民工对象(堆中实例化),传入3个参数(姓名=”Merry”,工号=”200”,肤色=”Yellow”),然后删除该对象,观察菱形继承的构造和析构函数执行顺序。

#include<iostream>
#include<stdlib.h>
#include"MigrantWorker.h"
using namespace std;int main(){MigrantWorker *p = new MigrantWorker("Merry", "200", "Yellow");delete p;p = NULL;system("pause");return 0;
}


分析上面的结果:
因为Farmer类和Worker类的父类都是Person类,所以这里会实例化2次Person类。同样构造函数和析构函数的执行顺序相反。

回顾我们这里的菱形继承关系:

在农民工类中,已经存了两份Person对象数据。我们也可以用调用的方式来证明。

(1)Worker.cpp文件中构造函数:如果传入参数color,则会直接传递给Person如果我们希望在这个过程中,打上Worker的印记,则可以改造构造函数:将Worker作为字符串的一部分也传递给Person(字符串之间是可以进行拼接的)。

Worker::Worker(string code, string color):Person("Worker" + color)
{m_strCode = code;cout <<"Worker()"<< endl;
}

同样的改下Farmer.cpp的构造函数:

Farmer::Farmer(string name, string color):Person("Farmer" + color)
{m_strName = name;cout <<"Farmer()"<< endl;
}

(2)在demo.cpp文件中,为了证明Person在农民工对象中存在两份相同的数据成员,我们可以通过农民工的对象指针,打印两份数据成员值:

#include<iostream>
#include<stdlib.h>
#include"MigrantWorker.h"
using namespace std;int main(){MigrantWorker *p = new MigrantWorker("Merry", "200", "Yellow");p->Farmer::printColor();p->Worker::printColor();delete p;p = NULL;system("pause");return 0;
}


从上面的运行结果,我们看到了“FarmerYellow”和“WorkerYellow”,可见,在农民工这个对象当中,实际上是存在着两个m_strColor这样的数据成员的。当然我们为了节省空间,只需要一份,这里就要用到虚继承,即将Worker类和Farmer类分别采用虚继承的方式继承Person类。

class Worker:virtual public Person{……}
class Farmer:virtual public Person{……}


分析上面的结果:
(1)前3行和后3行:在两个子类继承的语句中,在父类前加上virtual后,会发现Person类的构造函数和析构函数就执行了一次。即虚继承使得农民工这个类所实例化的对象当中只有一份Person的数据。

(2)原来是“FarmerYellow”和“WorkerYellow”的地方,现在变成了只是“blue”,这就说明,在虚继承的情况下,作为菱形继承最顶层的父类并没有进行参数的传递,也就是说,参数只使用了顶层父类的默认参数,而无法从子类当中获得传入的参数。

【C++】面向对象之继承篇相关推荐

  1. python 面向对象(进阶篇)

    上一篇<Python 面向对象(初级篇)>文章介绍了面向对象基本知识: 面向对象是一种编程方式,此编程方式的实现是基于对 类 和 对象 的使用 类 是一个模板,模板中包装了多个" ...

  2. 子类重载父类函数_Python面向对象之继承、重写与重载

    回顾 在Python进阶记录之基础篇(十六)中,我们介绍了Python面向对象中对属性的访问限制,需要重点掌握私有变量和公有变量的区别和用法,牢记面向对象的编程规则.今天我们讲一下Python面向对象 ...

  3. 面向对象编程-面试篇

    面向对象编程-面试篇 什么是面向对象思想? 面向对象是一种思想,是基于面向过程而言的,就是说面向对象是将功能等通过对象来实现,将功能封装到对象之中,让对象去实现具体功能细节. 特点: 1.将复杂的事情 ...

  4. Python 面向对象(初级篇) 2015/09/04 · 基础知识 · 2 评论 · 面向对象 分享到: 24 原文出处: 武沛齐 cnblog Python 面向对象(初级篇) 概述

    概述 面向过程:根据业务逻辑从上到下写垒代码 函数式:将某功能代码封装到函数中,日后便无需重复编写,仅调用函数即可 面向对象:对函数进行分类和封装,让开发"更快更好更强-" 面向过 ...

  5. Python面向对象简单继承

    Python面向对象简单继承 python是面向对象的语言,它支持继承,即一个类可以继承父类那里属性和方法.本书代码源自<<Python 3 Object-Oriented Program ...

  6. Python 面向对象(初级篇)

    概述 面向过程:根据业务逻辑从上到下写垒代码 函数式:将某功能代码封装到函数中,日后便无需重复编写,仅调用函数即可 面向对象:对函数进行分类和封装,让开发"更快更好更强..." 面 ...

  7. 面向对象编程其实很简单——Python 面向对象(初级篇)

    在Python教学中发现,很多同学在走到面向对象编程这块就开始蒙圈了,为了帮助大家更好的理解面向对象编程并其能将其用到自己的开发过程中,特写此文. 概述 面向过程:根据业务逻辑从上到下写垒代码 函数式 ...

  8. python继承语法_python语法学习面向对象之继承

    python语法学习面向对象之继承 只要涉及到面向对象,"类"是必须出现的一个代名词. 类和对象是面向对象编程的两个主要方面.类创建一个新类型,而对象是这个类的实例. 类的一些概念 ...

  9. python中继承和组合的区别_Py修行路 python基础 (十五)面向对象编程 继承 组合 接口和抽象类...

    一.前提回忆: 1.类是用来描述某一类的事物,类的对象就是这一类事物中的一个个体.是事物就要有属性,属性分为 1:数据属性:就是变量 2:函数属性:就是函数,在面向对象里通常称为方法 注意:类和对象均 ...

最新文章

  1. set class_ x set fid_ x
  2. 安卓学习之--如何关闭所有的activity
  3. Android系统介绍
  4. Visual Studio 2010 中的 SharePoint 开发
  5. 数字滤波器的matlab 与fpga实现,1 数字滤波器的MATLAB与FPGA实现——杜勇(配套光盘) 程序源码 - 下载 - 搜珍网...
  6. python中mysqldb模块_python中MySQLdb模块用法实例
  7. attention :为什么要用attention机制
  8. C Primer Plus 第9章 函数 9.7 指针简介
  9. java word转pdf_Java中Word转PDF解决方案
  10. Win制作苹果IOS证书
  11. thinkphp5.1合成带二维码海报图片
  12. 坚果云企业版服务器端,坚果云企业版常见问题解答
  13. 【生信可视化】ChemDraw基础操作教程
  14. PhraseQuery slop
  15. 十年终于读懂你——你从没见过的算法之美
  16. 微信提示:绑定非国内手机号的账户将迁移至 Wechat,或者换绑手机号
  17. DataStream API【1】
  18. fenix3 hr 中文说明书_Fenix3HR中英文菜单对照.pdf
  19. 常用电子元器件应用要点及识别方法
  20. 软件外包项目管理5 - 项目实施

热门文章

  1. python解释器环境中用于表示上一次运算结果的特殊变量_Python语句print(type(1//2))的输出结果是...
  2. redisson究极爽文-手把手带你实现redisson的发布订阅,消息队列,延迟队列(死信队列),(模仿)分布式线程池
  3. android sim卡 信息,android-如何使用SIM2或双SIM卡提交短信
  4. Linux用户获得超级管理员权限
  5. 欧格电商:商家延迟发货有什么影响
  6. 用计算机解题前 需要将解题方法,算法及其表示方法
  7. 下载erlang的.rpm文件 erlang下载 centos安装rabbitmq
  8. 项目管理的五个典型工具
  9. 白平衡之灰度世界算法
  10. 响应式Web程序设计【15】