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

news2024/11/24 12:45:18

系列文章目录

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<iostream>
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<iostream>
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<iostream>
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 <iostream>
using namespace std;
int main(int argc,char*argv[])
{
	int a=10;
	int b=20;
	cout<<a+b<<endl;      //两个整型变量相加
}

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

#include<iostream>
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<iostream>
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 <iostream>
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 <iostream>
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 <iostream>
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 <iostream>
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 <iostream>
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 <iostream>
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 <iostream>
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 <iostream>
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指向的对象不同,其行为也不同。程序运行结果如下图所示。
在这里插入图片描述

六、总结

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

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/489646.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

数字信号处理学习1

基本上算是没怎么学过数字信号处理这门课&#xff0c;因为本科的时候&#xff0c;专业方向用不上&#xff0c;现在没法子了&#xff0c;专业使然&#xff0c;只能自己自学了&#xff0c;但是我又不知道该从何学起&#xff0c;就买了一本现代数字信号处理&#xff0c;结果发现人…

数据结构/队列实现栈

前言 在学习数据结构的过程当中&#xff0c;我们会学到栈和队列&#xff0c;在本篇文章中&#xff0c;重点讲解的是队列实现栈&#xff0c;在上篇文章中已经简单介绍过栈和队列的使用说明&#xff0c;以及栈实现队列。(2条消息) 数据结构/栈实现队列_Y君的进化史的博客-CSDN博客…

吴恩达ChatGPT网课笔记Prompt Engineering——训练ChatGPT前请先训练自己

吴恩达ChatGPT网课笔记Prompt Engineering——训练ChatGPT前请先训练自己 主要是吴恩达的网课&#xff0c;还有部分github的prompt-engineering-for-developers项目&#xff0c;以及部分自己的经验。 一、常用使用技巧 prompt最好是英文的&#xff0c;如果是中文的prompt&am…

Day961.老城区前端改造 -遗留系统现代化实战

老城区前端改造 Hi&#xff0c;我是阿昌&#xff0c;今天学习记录的是关于老城区前端改造的内容。 什么是“改造老城区”。改造老城区模式 是指对遗留系统内部的模块进行治理&#xff0c;让模块内部结构合理、模块之间职责清晰的一系列模式。 也就是说&#xff0c;在遗留系统…

【Redis】聊一下Redis基础架构

我们知道学习一个技术&#xff0c;最好的方式就是从全局观出发&#xff0c;然后针对不同的点进行拆分&#xff0c;一个个破解。既可以将学到的和已有的知识联系起来&#xff0c;又可以有一定的深度和目的性。 Redis基础架构 对于一个中间件来说&#xff0c;一个是使用层面&…

GO数组切片-线性数据结构

数据结构 类型 什么是类型 &#xff1f; 内存中的二进制数据本身没有什么区别&#xff0c;就是一串0或1的组合。 内存中有一个字节内容是0x63&#xff0c;他究竟是深恶 字符串?字符&#xff1f;还是整数&#xff1f; 本来0x63表示数字 但是文字必须编码成为0和1的组合 才能记…

【C++】红黑树源码剖析

目录 概述 算法 调整策略 源码 RBTree.h test.cpp 概述 红黑树&#xff0c;是一种二叉搜索树&#xff0c;每一个节点上有一个存储位表示节点的颜色&#xff0c;可以是Red或Black。 通过对任何一条从根到叶子的路径上各个节点着色方式的限制&#xff0c;红黑树确保没有一…

ChatGPT、文心一言、New Bing到底哪个更AI?

Hello 各位小伙伴&#xff0c;要说今年最火爆的 IP 有哪些&#xff0c;那一定少不了人工智能&#xff08;AI&#xff09;&#xff0c;市面上已经相继出现了我们熟知的 ChatGPT&#xff08;OpenAI&#xff09;、ChatGPT 国外镜像网站、文心一言&#xff08;百度&#xff09;、Ne…

MySQL --- DDL图形化工具表结构操作

一. 图形化工具 1. 介绍 前面我们讲解了DDL中关于数据库操作的SQL语句&#xff0c;在我们编写这些SQL时&#xff0c;都是在命令行当中完成的。大家在练习的时候应该也感受到了&#xff0c;在命令行当中来敲这些SQL语句很不方便&#xff0c;主要的原因有以下 3 点&#xff1a;…

redis的介绍和安装

文章目录 一、redis的介绍和安装1.1 初识redis1.1.1 redis是什么&#xff1f;1.1.2 redis能做什么&#xff1f; 1.2 redis的安装配置 一、redis的介绍和安装 1.1 初识redis 1.1.1 redis是什么&#xff1f; Redis是一个开源的内存数据存储系统&#xff0c;也可以被用作数据库、…

阿里云服务器vCPU和CPU有区别吗?

阿里云服务器vCPU是什么&#xff1f;vCPU和CPU有什么区别&#xff1f;CPU是指云服务器的中央处理器&#xff0c;一个CPU可以包含若干个物理核&#xff0c;一台云服务器ECS实例的CPU选项由CPU物理核心数和每核线程数决定&#xff0c;通过超线程HT&#xff08;Hyper-Threading&am…

探索三维世界【3】:Three.js 的 Geometry 几何体 与 Material 材质

探索三维世界【3】&#xff1a;Three.js 的 Material 材质 1、Geometry几何体2、Material 材质3、创建平面与材质4、多平面渲染 1、Geometry几何体 Three.js中的几何体Geometry是构成3D模型的基本单元之一&#xff0c;它定义了一个物体的形状和大小。Geometry包含了物体的顶点…

RK3568平台开发系列讲解(网络篇)网络包的接收过程

🚀返回专栏总目录 文章目录 一、内核接收网络包过程二、用户态读取网络包过程沉淀、分享、成长,让自己和他人都能有所收获!😄 📢本篇我们一起来梳理下网络包的接收过程。 一、内核接收网络包过程 硬件网卡接收到网络包之后,通过 DMA 技术,将网络包放入 Ring Buffer;…

PyTorch数据加载工具:高效处理常见数据集的利器

❤️觉得内容不错的话&#xff0c;欢迎点赞收藏加关注&#x1f60a;&#x1f60a;&#x1f60a;&#xff0c;后续会继续输入更多优质内容❤️ &#x1f449;有问题欢迎大家加关注私戳或者评论&#xff08;包括但不限于NLP算法相关&#xff0c;linux学习相关&#xff0c;读研读博…

HTTP 缓存新鲜度 max-age

新鲜度 理论上来讲&#xff0c;当一个资源被缓存存储后&#xff0c;该资源应该可以被永久存储在缓存中。由于缓存只有有限的空间用于存储资源副本&#xff0c;所以缓存会定期地将一些副本删除&#xff0c;这个过程叫做缓存驱逐。另一方面&#xff0c;当服务器上面的资源进行了更…

使用ControlNet控制Stable-Diffusion出图人物的姿势

概述 在Stable-Diffusion&#xff08;以下简称SD&#xff09;出图中&#xff0c;我们往往需要对出图人物的姿势进行控制&#xff0c;这里我使用一个比较简单上手的方法&#xff0c;通过ControlNet可以很方便地对画面风格&#xff0c;人物姿势进行控制&#xff0c;从而生成更加…

Python —— Windows10下训练Yolov5分割模型并测试

附:Python —— Windows10下配置Pytorch环境、进行训练模型并测试(完整流程,附有视频)   效果 手机拍摄一段工位视频,上传到win10训练了yolov5分割鼠标的样本后推理效果截图。 训练准备 1、查看自己下载的Yolov5源码是否存在"segment"文件夹,该文件夹下存在分…

【Python入门篇】——Python基础语法(字面量注释与变量)

作者简介&#xff1a; 辭七七&#xff0c;目前大一&#xff0c;正在学习C/C&#xff0c;Java&#xff0c;Python等 作者主页&#xff1a; 七七的个人主页 文章收录专栏&#xff1a; Python入门&#xff0c;本专栏主要内容为Python的基础语法&#xff0c;Python中的选择循环语句…

有限等待忙等、让权等待死等、互斥遵循的几大原则——参考《天勤操作系统》,柳婼的博客

参考柳婼的博客 一、 有限等待&&死等 有限等待: 对请求访问的临界资源的进程&#xff0c;应该保证有限的时间进入临界区&#xff0c;以免陷入死等状态。受惠的是进程自己 死等: 进程在有限时间内根本不能进入临界区&#xff0c;而一直尝试进入陷入一种无结果的等待状…

在字节跳动做了6年软件测试,4月无情被辞,想给划水的兄弟提个醒

先简单交代一下背景吧&#xff0c;某不知名 985 的本硕&#xff0c;17 年毕业加入字节&#xff0c;以“人员优化”的名义无情被裁员&#xff0c;之后跳槽到了有赞&#xff0c;一直从事软件测试的工作。之前没有实习经历&#xff0c;算是6年的工作经验吧。 这6年之间完成了一次…