C plus plus ——【继承与派生】

系列文章目录

C plud plus ——【面向对象编程】
C plus plus ——【继承与派生】


文章目录

  • 系列文章目录
  • 前言
  • 一、继承
    • 1.1 类的继承
    • 1.2 继承后可访问性
    • 1.3 构造函数访问顺序
    • 1.4 子类隐藏父类的成员函数
  • 二、重载运算符
    • 2.1重载运算符的必要性
    • 2.2重载运算的形式与规则
    • 2.3 转换运算符
  • 三、多重继承
    • 3.1 多重继承定义
    • 3.2 二义性
    • 3.3 多重继承的构造顺序
  • 四、多态
    • 4.1 虚函数概述
    • 4.2 利用虚函数实现动态绑定
    • 4.3 虚继承
  • 五、抽象类
    • 5.1 纯虚函数
    • 5.2 实现抽象类中的成员函数:
  • 六、总结


前言

继承与派生是面向对象程序设计的两个重要特性,继承是从已有的类哪里得到已有的特性,已有的类为基类或父类,新类被称为派生类或子类。继承与派生是从不同角度说明类之间的关系,这种关系包含了访问机制,多态和重载等。


一、继承

继承(inheritance)是面向对象的主要特征(此外还有封装和多态)之一,它使得一个类可以从现有类中派生,而不必重新定义一个新类。继承的实质就是用已有的数据类型创建新的数据类型,并保留已有数据类型的特点,以旧类为基础创建新类,新类包含了旧类的数据成员和成员函数,并且可以在新类中添加新的数据成员和成员函数。旧类被称为基类或父类,新类被称为派生类或子类。

1.1 类的继承

类的继承形式如下:
class 派生类名标识符:[继承方式] 基类名标识符
{
[访问控制修饰符:]
[成员声明列表]
}
继承方式有3种派生类型,分别为共有型(public)、保护型(protected)和私有型(private);访问控制修饰符也是public protected、private 3种类型:
成员声明列表中包含类的成员及成员函数,是派生类新增的成员。“:” 是一个运算符,表示基类和派生类之间的继承关系。
例如,定义一个继承员工类的操作员类。首先定义一个员工类,它包含员工ID、员工姓名、所属部门等信息。

class  CEmployee           //定义员工类
{
public:
	int m_ID;              //定义员工ID
	char m_Name[128];      //定义员工姓名
	char m_Depar[128];     //定义所属部门
};

然后定义一个操作员类,通常操作员属于公司的员工,它包含ID、员工姓名、所属部门等信息,此外还包含密码信息、登陆方法等。

class  COperator : public CEmployee   //定义一个操作员类,从CEmployee类派生而来
{
public:
	char  m_Password[128];       //定义密码
	bool  Login();      
}

操作员类是从员工类派生的一个新类,新类中增加密码信息、登陆方法等信息,员工ID、员工姓名等信息直接从员工类中继承得到。
类的继承实例:

#include
using  namespace std;
class  CEmployee        //定义员工类
{
public:
	int m_ID;             //定义员工ID
	char m_Name[128];     //定义员工姓名
	char m_Depart[128];   //定义所属部门
	CEmployee()           //定义默认构造函数
	{
		memset(m_Name,0,128);        //初始化m_Name
		memset(m_Depart,0,128);      //初始化m_Depart 
	}
void OutputName()
{
	cout<<"Employee name"<<m_Name<<endl;   //输出员工姓名
}
	
};

class COperator:public CEmployee     //定义一个操作员类,从CEmployee类派生而来
{

public:
	char m_Password[128];            //定义密码
	bool Login()                     //定义登陆成员函数
	{
		if(strcmp(m_Name,"MR")==0&&strcmp(m_Password,"KJ")==0)
		{
			cout<<"Login successful "<<endl;         //输出信息
			return true;                     //设置返回值
		}else
		{
			cout<<"Login failed  "<<endl;       //输出信息
			return false;                   //设置返回值
		}
	}

};
int main(int argc,char* argv[])
{
	COperator  optr;                //定义一个COperator 类对象
	strcpy(optr.m_Name," MR");       //访问基类的m_Name成员
	strcpy(optr.m_Password," KJ");   //访问m_Password成员
	optr.Login();                   //调用COperator类的Login成员函数
	optr.OutputName();              //调用基类CEmployee的OutputName成员函数
	return 0;
}

程序中CEmployee 类是COperator类的基类,也就是父类。COperator 类将继承CEmployee类的所有非私有成员(private类型成员不能被继承)。optr对象初始化m_Name和m_Password 成员后,调用了Login成员函数,程序允许结果如下图所示:
在这里插入图片描述
用户在父类中派生子类时,可能存在一种情况,即在子类中定义了一个与父类同名的成员函数,此时称为子类隐藏了父类的成员函数。
例如,重新定义COperator类,添加一个OutputName成员函数。

1.2 继承后可访问性

继承方式有public、private、protected 3种,其说明如下:

  1. public(共有型派生)

共用型派生表示对于基类中的public数据成员和成员函数,在派生类中仍然是public,对于基类中的private数据成员和成员函数,在派生类中仍然是private。
例如:

class CEmployee
{
public:
	void Output()
	{
		cout<<m_ID<<endl;
		cout<<m_Name<<endl;
		cout<<m_Depart<<endl;
	}
private:
	int  m_ID;
	char m_Name[128];
	char m_Depart[128];
	
};
class COperator:public CEmployee
{
public:
	void Output()
	{
		cout<<m_ID<<endl;        //引用基类的私有成员,错误
		cout<<m_Name<<endl;      //引用基类的私有成员,错误
		cout<<m_Depart<<endl;    //引用基类的私有成员,错误
		cout<<m_Password<<endl;  //正确
	}
private:
	char  m_Password[128];
	bool  Login();
};

COperator 类无法访问CEmployee类中的private数据成员m_ID、m_Name和m_Depart ,如果将CEmployee类中的所有程序都设置为public后,COperator类才能
访问CEmployee类中的所有成员。例如:

class CEmployee
{
public:
	void Output()
	{
		cout<<m_ID<<endl;
		cout<<m_Name<<endl;
		cout<<m_Depart<<endl;
	}
//private:
	int  m_ID;
	char m_Name[128];
	char m_Depart[128];
	
};
class COperator:public CEmployee
{
public:
	void Output()
	{
		cout<<m_ID<<endl;        //正确
		cout<<m_Name<<endl;      //正确
		cout<<m_Depart<<endl;    //正确
		cout<<m_Password<<endl;  //正确
	}
private:
	char  m_Password[128];
	bool  Login();
}

  1. private(私有型派生)
    私有型派生表示对于基类中的public、protected数据成员和成员函数,在派生类中可以访问。基类中的private数据成员,在派生类中不可以
    访问。例如:
class CEmployee
{
public:
	void Output()
	{
		cout<<m_ID<<endl;
		cout<<m_Name<<endl;
		cout<<m_Depart<<endl;		
	}
	int m_ID;
protected:
	char m_Name[128];
private:
	char m_Depart[128];	
};

class  COperator:private CEmployee
{
public:
	void Output()
	{
		cout<<m_ID<<endl;        //正确
		cout<<m_Name<<endl;      //正确
		cout<<m_Depart<<endl;    //错误
		cout<<m_Password<<endl;  //正确
	}
private:
	char m_Password[128];
	bool Login();
}

  1. protected (保护型派生)
    保护型派生表示对于基类中的public、protected数据成员和成员函数,在派生类中均为protected。protected 类型在派生类定义是可以访问,用派生类声明的对象不可以访问,也就是说在类体外不可以访问。protected 成员可以被基类的所有派生类使用。这一性质可以沿继承树无限向下传播。
    因为保护类的内部数据不能被随意更改,实例类本身负责维护,这就起到很好的封装性作用。把一个类分作两部分,一部分是公共的,另一部分是保护的,保护成员对于使用者来说是不可见的,也是不需要了解的,这就减少了类与其他代码的关联程度。类的功能是独立的,它不依赖于应用程序的运行环境,既可以放到这个程序中使用,也可以放到那个程序中使用。
    这就能够非常容易地用一个类替换另一个类。类访问限制的保护机制使人们编制的应用程序更加可靠和易维护。

1.3 构造函数访问顺序

由于父类和子类中都有构造函数和析构函数,那么子类对象在创建时是父类先进行构造,还是子类先进行构造呢?同样在子类对象释放时,是父类先进行释放,还是子类先进行释放?这些都有个先后顺序问题。答案是当从父类派生一个子类并声明一个子类的对象时,它将先调用父类的构造函数,然后调用当前类的构造函数来创建对象;在释放子类对象时,先调用的是当前类的析构函数,然后是父类的析构函数。
实例,构造函数访问顺序:

#include
using namespace std;
class CEmployee   //定义CEmployee类
{
public: int m_ID;   //定义数据成员
	char m_Name[128];  //定义数据成员
	char m_Depart[128];  //定义数据成员
	CEmployee()    //定义构造函数
	{
		cout<<"CEmployee The class constructor is called"<<endl;  //输出信息
	}
	~CEmployee()  //析构函数 
	{
		cout<<"CEmployee The class destructor is called"<<endl;  //输出信息
 	}
};
class COperator:public CEmployee  //从CEmployee 类派生一个子类
{
public:
	char m_Password[128];    //定义数据成员 
	COperator()  //定义构造函数
 	{
		strcpy(m_Name,"MR");  //设置数据成员
		cout<<"COperator The class constructor is called"<<endl; //输出信息 
	}
	~COperator()  //析构函数
	{
		cout<<"COperator The class destructor is called"<<endl; //输出信息
	}
};
int main(int argc,char* argv[]) //主函数
{
	COperator optr; //定义一个COperator 对象
 	return 0;
}

程序运行结果如下图所示:
在这里插入图片描述
从图中可以发现,在定义COperator类对象时,首先调用的是父类CEmployee的构造函数,然后是COperator 类的 构造函数。子类对象的释放过程则与其构造过程恰恰相反,先调用自身的析构函数,然后再调用父类的析构函数。
在分析完对象的构建、释放过程后,会考虑这样一种情况:定义一个基类类型的指针,调用子类的构造函数为其构建对象,当对象释放时,如果析构函数是虚函数,则先调用子类的析构函数,然后再调用父类的析构函数;如果析构函数不是虚函数,则只调用父类的析构函数。可以想象,如果在子类中为某个数据成员在堆中分配了空间,父类中的析构函数不是虚成员函数,将使子类的析构函数不被调用,其结果是对象不能被正确地释放,导致内存泄漏的产生。因此,在编写类的析构函数时,析构函数通常是虚函数。构造函数调用顺序不受基类在成员初始化表中是否存在以及被列出的顺序的影响。

1.4 子类隐藏父类的成员函数

之类隐藏父类的成员函数实例:

#include
using namespace std;
class CEmployee  //定义CEmployee类
{
public:
	int m_ID;
	char m_Name[128];   //定义数据成员					
	char m_Depart[128]; //定义数据成员
	CEmployee()  //定义构造函数
	{	
	}
	~CEmployee() //析构函数
	{		
	}
	void OutputName()  //定义OutputName成员函数
	{
		cout<<"Call the OutputName member function of the CEmployee class:"<<endl; 	//输出操作员姓名
	}
};
class COperator:public CEmployee    //定义COperator类
{
public:
	char m_Password[128];  //定义数据成员
	void OutputName()  //定义OutputName成员函数,隐藏基类的成员函数
	{	
		cout<<"Call the OutputName member function of the COperator class:"<<endl; 	//输出操作员姓名
	}
};
int main(int argc,char* argv[])   // 主成员函数
{
	COperator optr;        //定义COperator 对象
	optr.OutputName();     //调用COperator 类的OutputName成员函数
	return 0;
}   

程序运行结果如下图所示:
在这里插入图片描述
从图中可以发现,语句“optr.OutputName()”调用的是COperator类的OutputName成员函数,而不是
CEmployee类的OutputName成员函数。如果用户想要访问父类的OutputName成员函数,需要显式使用父类名。
例如:

COperator optr; //定义一个COperator类
strcpy(optr.m_Name,"MR"); //赋值字符串
optr.OutputName(); //调用COperator类的OutputName成员函数
optr.CEmployee::OutputName(); //调用CEmployee类的OutputName成员函数

如果子类中隐藏了父类的成员函数,则父类中所有同名的成员函数(重载的函数)均被隐藏。

二、重载运算符

运算符实际上是一个函数,所以运算符的重载实际上是函数的重载。编译程序对运算符重载的选择,遵循函数重载的选择原则。当遇到不很明显的运算时,编译程序会去寻找与参数相匹配的运算符函数。

2.1重载运算符的必要性

C++ 语言中的数据类型分为基础数据类型和构造数据类型,基础数据类型可以直接完成算术运算。
例如:

#include 
using namespace std;
int main(int argc,char*argv[])
{
	int a=10;
	int b=20;
	cout<<a+b<<endl;      //两个整型变量相加
}

程序中实现了两个整型变量的相加,可以正确输出运行结果30,通过两个浮点变量,两个双精度变量都可以直接运用加法运算符 “+”求和。但是类属于新构造的数据类型,类的两个对就无法通过加法运算符来求和。例如:

#include
using namespace std;
class CBook
{
public:
	CBook(int iPage)
	{
		m_iPage = iPage;
	}
	void display()
	{
		cout<<m_iPage<<endl;
	}
protected:
	int m_iPage;
};
int  main()
{
	CBook bk1(10);
	CBook bk2(20);
	CBook tmp(0);
	tmp = bk1 +bk2;  //错误
	tmp.display();
	return 0;
}

当编译器编译到语句“bk1 + bk2 ”时就会报错,因为编译器不知道如何进行两个对象的相加。要实现两个类对象加法运算有两种,一种是通过成员函数,另一种是重载操符。
下面给出通过成员函数的方法实现求和的实例。

#include
using namespace std;
class CBook
{
public:
	CBook(int iPage)
	{
		m_iPage = iPage;
	}
	int add(CBook a)
	{
		return m_iPage +a.m_iPage;
	}
protected:
	int m_iPage;
}
int main()
{
	CBook bk1(10);
	CBook bk2(20);
	cout<<bk1.add(bk2)<<endl;
}

程序可以正确输出运行结果30。使用成员函数实现求和形式比较单一,并且不利于代码复用。如果要实现多个对象的累加,其代码的可读性会大大降低。使用重载操作方法就可以解决这些问题。

2.2重载运算的形式与规则

重载运算符的声明形式如下:

operator 类型名();

operator 是需要重载的运算符,整个语句没有返回类型,因为类型名就代表了它的返回类型。重载运算符将对象转换成类型名规定的类型,转换时的形式就像强制转换一样,但如果没有重载运算符定义,直接用强制转换编译器将无法通过编译。
重载运算符时不能改变运算符操作数的个数、运算符原有的优先级、运算符原有的结合性及运算符原有的语法结构,即单目运算符只能重载单目运算符,双目运算符只能重载双目运算符。重载运算符含义必须清楚,不能有二义性。
通过重载运算符实现求和实例:

#include 
using namespace std;
class CBook
{
public:
	CBook(int iPage)
	{
		m_iPage = iPage;
	}
	CBook operator+(CBook b)
	{
		return CBook(m_iPage+b.m_iPage);
	}
	void display()
	{
		cout<<m_iPage<<endl;
	}
protected:
	int m_iPage;	
};
int main(int argc,char* argv[])
{
	CBook bk1(10);
	CBook bk2(20);
	CBook tmp(0);
	tmp = bk1 + bk2;
	tmp.display();
	return 0;
}

类CBook重载了求和运算符后,由它声明的两个对象bk1和bk2可以像两个整型变量一样相加。

2.3 转换运算符

C++语言中普通的数据类型可以进行强制类型转换,例如:

int i=10;
double d;
d = (double)i;

程序中将整型数i强制转换双精度型语句。

d = (double)i; 

等同于

d = double(i);

double()在C++语言中被称为转换运算符。通过重载运算符可以将类转换成想要的数据。

#include 
using namespace std;
class CBook
{
public:
	CBook(double iPage=0);
	operator double()
	{
		return m_iPage;
	}
protected:
	int m_iPage;	
};
CBook::CBook(double iPage)
{
	m_iPage = iPage;
}
int main(int argc ,char* argv[])
{
	CBook bk1(10.0);
	CBook bk2(20.00);
	cout<<"bk1+bk2="<<double(bk1)+double(bk2)<<endl;
	return 0;
}

程序运行结果如下图所示:
在这里插入图片描述
程序重载了转换运算符double(),然后将类CBook的两个对象强制转换为double 类型后再进行求和,最后输出求和结果。

三、多重继承

前文介绍的继承方式属于单继承,即子类只从一个父类继承共有的和受保护的成员。与其他面向对象语句不同,C++语言允许子类从多个父类继承共用的和受保护的成员,这被称为多重继承。

3.1 多重继承定义

多重继承是指有多个基类名标识符,其声明形式如下:
class 派生类名标识符:[继承方式]基类名标识符1,基类名标识符2,…,基类名标识符n。
{
[访问控制修饰符:]
[成员声明列表]
};
声明形式中有“:”运算符,基类名标识符之间用“,”运算符分开。
多重继承实例:

#include 
using namespace std;
class CBird        //定义鸟类
{
public:
	void FLylnSky()                //定义成员
	{
		cout<<"Birds can fly in the sky again"<<endl;	//输出信息
	}
	void Breath()                 //定义成员函数
	{
		cout<<"Birds are able to breathe"<<endl;   //输出信息  
	}
};
class CFish       //定义鱼类
{
public:
	void SwimlnWater()     //定义成员函数
	{
		cout<<"Fish can swim in the water again"<<endl;    //输出信息
	}
	void Breath()                 //定义成员函数
	{
		cout<<"Birdsare able to breathe"<<endl;   //输出信息  
	}
};
class CWaterBird:public CBird,public CFish   //定义水鸟类,从鸟类和鱼类派生
{
public:
	void Action()
	{
		cout<<"water birds can both fly and swim"<<endl;
	}
};
int main(int argc,char* argv[])
{
	CWaterBird waterbird;       //定义水鸟
	waterbird.FLylnSky();       //调用从鸟类继承而来的FlySky成员函数
	waterbird.SwimlnWater();    //调用从鱼类继承而来的SwimlnWater成员函数
	return 0;
}

程序运行结果如下图所示:
在这里插入图片描述

3.2 二义性

派生类在调用成员函数时,先在自身的作用域内寻找,如果找不到,会到基类中寻找,但当派生类继承的基类中有同名成员时,派生类中就会出现来自不同基类的同名成员。例如:

class CBaseA
{
public:
	void function();
};
class CBaseB
{
public:
	void function();
};
class CDeriveC:public CBaseA,CBaseB
{
public:
	void function();
};

CBaseA和CBaseB 都是CDeriveC的父类,并且两个父类中都含有function成员函数,CDeriveC将不知道调用那个基类的function成员函数,这就产生了二义性。

3.3 多重继承的构造顺序

多重继承中的基类构造函数被调用的顺序以类派生表中的顺序为准。派生表就是多重继承定义中继承方式后面的内容,调用顺序就是按照基类名标识符的前后顺序进行的。
多重继承的构造顺序:

#include 
using namespace std;
class CBicycle
{
public:
	CBicycle()
	{
		cout<<"Bicycle Construct"<<endl;
	}
	CBicycle(int iWeight)
	{
		m_iWeight = iWeight;
	}
	void Run()
	{
		cout<<"Bicycle Run"<<endl;
	}
	
protected:
	int m_iWeight;
};

class CAirplane
{
public:
	CAirplane()
	{
		cout<<"Airplance Construct"<<endl;
	}
	CAirplane(int iWeight)
	{
		m_iWeight = iWeight;
	}
	void Fly()
	{
		cout<<"Airplane Fly"<<endl;
	}
protected:
	int m_iWeight;
		
};

class CAirBicycle:public CBicycle,public CAirplane
{
public:
	CAirBicycle()
	{
		cout<<"CAirBicycle Construct"<<endl;
	}
	void RunFly()
	{
		cout<<"Run and Fly"<<endl;
	}
};
int main(int argc,char* argv[])
{
	CAirBicycle ab;
	ab.RunFly();
	return 0;
}

程序运行结果如下图所示:
在这里插入图片描述
程序中基类的声明顺序是先CBicycle类后CAirplane类,所以对象的构造顺序就是先CBicycle类后CAirplane类,最后是CAirBicycle类。

四、多态

4.1 虚函数概述

在类的继承层次结构中,在不同的层次中可以出现名字、参数个数和类型都相同而功能不同的函数。编译器按照先自己后父类的顺序进行查找覆盖,如果子类有与父类相同原型的成员函数时,要想调用父类的成员函数,需要对父类重新引用调用。虚函数则可以解决子类和父类相同原型成员函数的函数调用问题。虚函数允许在派生类中重新定义与基类同名的函数,并且可以通过基类指针或引用来访问基类和派生类中的同名函数。
在基类中用virtual声明成员函数为虚函数,在派生类中重新定义此函数,改变该函数的功能。在C++语言中虚函数可以继承,当一个成员函数被声明为虚函数后,其派生类中的同名函数都自动成为虚函数,但如果派生类没有覆盖基类的虚函数,则调用时调用基类的函数定义。
覆盖和重载的区别是,重载是同一层次函数名相同,覆盖是在继承层次中成员函数的函数原型完全相同。

4.2 利用虚函数实现动态绑定

多态主要体现在虚函数上,只要有虚函数存在,对象类型就会在程序运行时动态绑定。动态绑定的实现方法是定义一个指向基类对象的指针变量,并使它指向同一类族中需要调用该函数的对象,通过该指针变量调用此虚函数。
虚拟函数实现动态绑定实例:

#include 
using namespace std;
class CEmployee         //定义CEmployee类
{
public:
	int m_ID;           //定义数据成员
	char m_Name[128];   //定义数据成员
	char m_Depart[128];   //定义数据成员
	CEmployee()
	{
		memset(m_Name,0,128);     //初始化数据成员
		memset(m_Depart,0,128);   //定义数据成员
	}
	virtual void OutputName()  //定义一个虚成员函数
	{
		cout<<"Employee Name: "<<m_Name<<endl;  //输出信息
	}
};
class COperator:public CEmployee     //从CEmployee类派生一个子类
{
public:
	char m_Password[128];       //定义数据成员
	void OutputName()          //定义OutputName虚函数
	{
		cout<<"Operator Name: "<<m_Name<<endl;   //输出信息
	}
  	
};

int main(int argc,char* argv[])
{

	CEmployee *pWorker = new COperator();    //定义CEmployee类型指针 ,调用COperator 类构造函数
	strcpy(pWorker->m_Name,"MR");       //设置m_Name数据成员信息
	pWorker->OutputName();            //调用COperator 类的OutputName成员函数
	delete pWorker;               //释放对象
	return 0;
}

程序编译运行如下图所示:
在这里插入图片描述
虚函数有以下几方面限制。

  1. 只有类的成员函数才能为虚函数。
  2. 静态成员函数不能是虚函数,因为静态成员函数不受限于某个对象。
  3. 内联函数不能是虚函数,因为内联函数是不能在运行中动态确定其位置的。
  4. 构造函数不能是虚函数,析构函数通常是虚函数。

4.3 虚继承

C++语言提供了虚继承机制
虚继承实例:

#include 
using namespace std;
class CAnimal                        //定义一个动物类
{
public:
	CAnimal()                        //定义成员函数
	{
		cout<<"Animal classes are constructed "<<endl;      //输出信息
	}
	void Move()                 //定义成员函数
	{
		cout<<"Animals are able to move "<<endl;     //输出信息
	}
};
class CBird:virtual public CAnimal    //从CAnimal类虚继承CBird类
{
public: 
	CBird()                        //定义成员函数
	{
		cout<<"Birds are constructed "<<endl;   //输出信息
	}
	void FlylnSky()                     //定义成员函数
	{
		cout<<"Birds are able to fly in the sky "<<endl;  //输出信息
	}
	void Breath()               //定义成员函数
	{
		cout<<"Birds are able to breathe "<<endl; //输出信息
	}
};
class CFish:virtual public CAnimal   //从CAnimal类虚继承CFish类
{
public:
	CFish()                          //定义成员函数
	{
		cout<<"Fish are constructed "<<endl;  //输出信息
	}
	void SwimlnWater()               //定义成员函数
	{
		cout<<"Fish are able to swim in the water "<<endl;  //输出信息
	}
	void Breath()                  //定义成员函数
	{
		cout<<"Fish are able to breathe "<<endl;       //输出信息
	}
};
class CWaterBird:public CBird ,public CFish    //从CBird 类和CFish类派生子类CWaterBird
{
public:
		CWaterBird()                       //定义成员函数
		{
			cout<<"Water birds are constructed "<<endl;        //输出信息
		}
		void Action()                           //定义成员函数
		{
			cout<<"Water birds can both fly and swim "<<endl;     //输出信息
		}
};
int main(int argc,char* argv[])   //主函数
{
	CWaterBird waterbird;     //定义水鸟对象
	return 0;
}

程序编译运行如下图所示:
在这里插入图片描述
通常,在定义一个对象时,先依次调用基类的构造函数,最后才调用自身的构造函数。但是对于虚继承来说,情况有些不同。在定义CWaterBird类对象时,先调用基类CAnimal的构造函数,然后调用CBird类的构造函数,这里CBird类虽然为CAnimal的子类,但是在调用CBird类的构造函数时将不再调用CAnimal类的构造函数。对于CFish类也是同样的道理。
在程序开发过程中,多继承虽然带来了很多方便,但是很少有人愿意使用它,因为多继承会带来很多复杂的问题,并且它能够完成的功能通过单继承同样可以实现。如今流行的C#、Delphi、Java等面向对象语言没有提供多继承的功能,而是只采用单继承是经过设计者充分考虑的。因此,读者在开发应用程序时,如果能够使用单继承实现,尽量不要使用多继承。

五、抽象类

包含有纯虚函数的类称为抽象类,一个抽象类至少具有一个纯虚函数。抽象类只能作为基类派生出的新的子类,而不能在程序中被实例化(即不能说明抽象类的对象),但是可以使用指向抽象类的指针。在开发程序过程中并不是所有代码都是由软件构造师自己写的,有时需要调用库函数,有时分给别人写。一名软件构造师可以通过纯虚函数建立接口,然后让程序员填写代码实现接口,而自己主要负责建立抽象类。

5.1 纯虚函数

纯虚函数(Pure Virtual Function)是指被标明为不具体实现的虚成员函数,它不具备函数的功能。
许多情况下,在基类中不能给虚函数一个有意义的定义,这时可以在基类中将它说明为纯虚函数,而其实现留给派生类去做。纯虚函数不能被直接调用,仅起到提供一个与派生类相一致的接口的作用。
声明纯虚函数的形式为:

virtual 类型  函数名(参数表列) = 0

纯虚函数不可以被继承。当基类是抽象类时,在派生类中必须给出基类中纯虚函数的定义,或在该类中在声明其为纯虚函数。只有在派生类中给出了基类中所有纯虚函数的实现时,该派生类才不再成为抽象类。
创建纯虚函数实例:

#include 
using namespace std;
class CFigure
{
public:
	virtual double getArea() = 0;	
};
const double PI = 3.14;
class CCircle:public CFigure
{
private:
	double m_dRadius;
public:
	CCircle(double dR)
	{
		m_dRadius = dR;
	}
	double getArea()
	{
		return m_dRadius*m_dRadius*PI;
	}
};
class CRectangle:public CFigure
{
protected:
	double m_dHeight,m_dWidth;
public:
	CRectangle(double dHeight,double dWidth)
	{
		m_dHeight = dHeight;
		m_dWidth = dWidth;
	}
	double getArea()
	{
		return m_dHeight*m_dWidth;
	}
};
int main(int argc,char* argv[])   //主函数
{
	CFigure *fg1;
	fg1 = new CRectangle(4.0,5.0);
	cout<<fg1->getArea()<<endl;
	delete fg1;
	CFigure *fg2;
	fg2 = new CCircle(4.0);
	cout<<fg2->getArea()<<endl;
	delete fg2;
	return 0;
}

程序编译运行如下图所示:
在这里插入图片描述

5.2 实现抽象类中的成员函数:

抽象类通常用于作为其他类的父类,从抽象类派生的子类如果是抽象类,则子类必须实现父类中的所有纯虚函数。

#include 
using namespace std;
class CEmployee       //定义CEmployee类
{
public:
	int m_ID;                   //定义数据成员
	char m_Name[128];           //定义数据成员
	char m_Depart[128];         //定义数据成员
	virtual  void OutputName() = 0;  //定义抽象成员函数
};

class COperator : public CEmployee   //定义COperator 类,派生于CEmployee类
{
public:
	char m_Password[128];         //定义数据
	void OutputName()
	{
		cout<<"Operator Name: "<<m_Name<<endl;      //输出信息
	}
	COperator()
	{
		strcpy(m_Name,"MR");         //设置数据成员m_Name信息
	}
};
class CSystemManager:public CEmployee   //定义CSystemManager类
{
public:
	char m_Password[128];           //定义数据成员
	void OutputName()             //实现父类中的纯虚成员函数
	{
		cout<<"System Administrator Name:"<<m_Name<<endl;    //输出信息
	}
	CSystemManager()              
	{
		strcpy(m_Name,"SK");     //设置数据成员m_Name信息		
	}
};
int main(int argc,char* argv[])
{
	CEmployee  *pWorker;      //定义CEmployee 类型指针对象
	pWorker = new COperator();     //调用COperator 类的构造函数,为pWorker赋值
	pWorker->OutputName();      //调用COperator类的OutputName成员函数
	delete pWorker;            //释放pWorker 对象	
	pWorker = new CSystemManager(); //调用CSystemManager类的构造函数,为pWorker赋值
	pWorker->OutputName();         //调用CSystemManager 类的OutputName成员函数
	delete pWorker;              //释放pWorker对象
	return 0;
   	
}

程序中从CEmployee类派生了两个子类,分别为COperator和CSystemManager。这两个类分别实现了父类的纯虚成员函数OutputName。同样的一条语句“pWorker->OutputName(;”,由于pWorker指向的对象不同,其行为也不同。程序运行结果如下图所示。
在这里插入图片描述

六、总结

面向对象程序设计中的关键技术一继承与派生,继承和派生在使用上还涉及二义性、访问顺序、运算符重载等许多技术问题,正确理解和处理这些技术有利于掌握继承的使用方法。继承中还涉及多重继承,这增加了面向对象开发的灵活性。面向对象可以建立抽象类,由抽象类派生新类,可以形成对类的一定管理。

你可能感兴趣的:(C,plus,plus,c语言,开发语言,c++,数据结构,编辑器)