专业编程基础技术教程

网站首页 > 基础教程 正文

C++|实例了解类的继承(Inheritance)与组合(Composition)

ccvgpt 2024-10-12 13:51:19 基础教程 7 ℃

类及对象之间有两种关系:继承或者包含。

组合(Composition)关系,表示has-a;
继承(Inheritance),表示 is-a;

1 继承

类的继承就是新类由已经存在的类获得已有特性,类的派生则是由已经存在的类产生新类的过程。这两个概念是两个相对的方向上的。

C++|实例了解类的继承(Inheritance)与组合(Composition)

由已有类产生新类时,新类会拥有已有类的所有特性,然后又加入了自己独有的新特性。已有类叫做基类或者父类,产生的新类叫做派生类或者子类。派生类同样又可以作为基类派生新的子类,这样就形成了类的层次结构。

继承是Is a 的关系,比如说Student继承Person,则说明Student is a Person。

继承的优点是子类可以拥有并重写父类的方法来方便地实现对父类的扩展。

继承的缺点有以下几点:

① 父类的内部细节对子类是可见的。
② 子类从父类继承的方法在编译时就确定下来了,所以无法在运行期间改变从父类继承的方法的行为。
③ 如果对父类的方法做了修改的话(比如增加了一个参数),则子类的方法必须做出相应的修改。所以说子类与父类是一种高耦合,违背了面向对象思想。

派生类声明的语法形式为:

 class 派生类名 : 继承方式1 基类名1, 继承方式2 基类名2, ... 继承方式n 基类名n
 {
 派生类成员的声明;
 }

派生类从基类继承的过程可以分为三个步骤:吸收基类成员、修改基类成员和添加新成员。吸收基类成员就是代码复用的过程,修改基类成员和添加新成员实现的是对原有代码的扩展,而代码的复用和扩展是继承与派生的主要目的。

通过对象访问类的成员属于外部访问,只能访问类的公有成员。

派生类的继承方式为public,即公有继承时,对基类中的公有成员和保护成员的访问属性都不变,而对基类的私有成员则不能访问。(基类的构造函数和析构函数派生类是不能继承的,对派生类初始化时就需要对基类的数据成员、派生类新增数据成员和内嵌的其他类对象的数据成员进行初始化。基类的构造函数若有参数,则派生类必须定义构造函数,将传入的参数再传递给基类的构造函数,对基类进行初始化。)

以下实例定义了一个父类:Shape,从中派生出了两个子类:Rectangle、Circle:

// 虚函数的定义,其中的area和display函数都是虚函数
#include<iostream>
using namespace std;
class Shape{
protected: 
 double x, y; // x、y是图形的位置
public: 
 Shape(double xx, double yy) {x = xx; y = yy;}
	virtual double area() const {return 0.0;}
 virtual void display() const
	{cout <<"This is a shape. The position is ("<< x <<", "<< y <<")\n";}
};
class Rectangle:public Shape {
protected:
 double w, h; // w、h是矩形的宽和高
public: 
 Rectangle(double xx, double yy, double ww, double hh): Shape(xx,yy),w(ww),h(hh){}
 double area() const {return w * h;} //重定义虚函数area
 void display() const //重定义虚函数display
 { 
		cout <<"This is a rectangle. The position is ("<< x <<", "<< y <<")\t"<<endl;
		cout <<"The width is "<< w <<". The height is "<< h << endl;
		cout <<"The area is "<< area() << endl;
 }
};
class Circle:public Shape {
protected:
 double r; // r是圆的半径
public: 
 Circle(double xx, double yy, double rr): Shape(xx,yy),r(rr){}
 double area() const {return 3.14 * r * r;}
 void display() const
 { 
		cout <<"This is a Circle. The position is ("<< x <<", "<< y <<")\t"<<endl;
		cout <<"The radius is "<< r << endl;
		cout <<"The area is "<< area() << endl;
 }
};
void main()
{
	Shape* sp;
	Rectangle rec(1,2,3,4);
	sp = &rec;
	sp->area();
	sp->display();
	Circle cir(1,2,5);
	sp = &cir;
	sp->area();
	sp->display();
	system("pause");
}
/*
This is a rectangle. The position is (1, 2)
The width is 3. The height is 4
The area is 12
This is a Circle. The position is (1, 2)
The radius is 5
The area is 78.5
*/

在保护继承方式中,基类的公有成员和保护成员被派生类继承后变成派生类的保护成员,而基类的私有成员在派生类中不能访问。因为基类的公有成员和保护成员在派生类中都成了保护成员,所以派生类的新增成员可以直接访问基类的公有成员和保护成员,而派生类的对象不能访问它们,类的对象也是处于类外的,不能访问类的保护成员。对基类的私有成员,派生类的新增成员函数和派生类对象都不能访问。

在私有继承方式中,基类的公有成员和保护成员被派生类继承后变成派生类的私有成员,而基类的私有成员在派生类中不能访问。派生类的新增成员可以直接访问基类的公有成员和保护成员,但是在类的外部通过派生类的对象不能访问它们。而派生类的成员和派生类的对象都不能访问基类的私有成员。

2 组合

在我们对现实中的某些事物抽象成类时,可能会形成很复杂的类,为了更简洁的进行软件开发,我们经常把其中相对比较独立的部分拿出来定义成一个个简单的类,这些比较简单的类又可以分出更简单的类,最后由这些简单的类再组成我们想要的类。比如,我们想要创建一个计算机系统的类,首先计算机由硬件和软件组成,硬件又分为CPU、存储器等,软件分为系统软件和应用软件,如果我们直接创建这个类是不是很复杂?这时候我们就可以将CPU写一个类,存储器写一个类,其他硬件每个都写一个类,硬件类就是所有这些类的组合,软件也是一样,也能做成一个类的组合。计算机类又是硬件类和软件类的组合。

类的组合其实描述的就是在一个类里内嵌了其他类的对象作为成员的情况,它们之间的关系是一种包含与被包含的关系。简单说,一个类中有若干数据成员是其他类的对象。以前的教程中我们看到的类的数据成员都是基本数据类型的或自定义数据类型的,比如int、float类型的或结构体类型的,现在我们知道了,数据成员也可以是类类型的。

组合也就是设计类的时候把要组合的类的对象加入到该类中作为自己的成员变量。

组合的优点:

① 当前对象只能通过所包含的那个对象去调用其方法,所以所包含的对象的内部细节对当前对象时不可见的。
② 当前对象与包含的对象是一个低耦合关系,如果修改包含对象的类中代码不需要修改当前对象类的代码。
③ 当前对象可以在运行时动态的绑定所包含的对象。可以通过set方法给所包含对象赋值。

组合的缺点:

① 容易产生过多的对象。
② 为了能组合多个对象,必须仔细对接口进行定义。

由此可见,组合比继承更具灵活性和稳定性,所以在设计的时候优先使用组合。只有当下列条件满足时才考虑使用继承:

子类是一种特殊的类型,而不只是父类的一个角色
子类的实例不需要变成另一个类的对象
子类扩展,而不是覆盖或者使父类的功能失效

如果在一个类中内嵌了其他类的对象,那么创建这个类的对象时,其中的内嵌对象也会被自动创建。因为内嵌对象是组合类的对象的一部分,所以在构造组合类的对象时不但要对基本数据类型的成员进行初始化,还要对内嵌对象成员进行初始化。

组合类构造函数定义(注意不是声明)的一般形式为:

	类名::类名(形参表):内嵌对象1(形参表),内嵌对象2(形参表),...
	{
	 类的初始化
	}

以下是一个类的组合的例子,其中,Distance类就是组合类,可以计算两个点的距离,它包含了Point类的两个对象p1和p2:

#include <iostream>
#include <cmath>
using namespace std;
class Point
{ 
public:
	Point(int xx,int yy) { X=xx; Y=yy; } //构造函数
	Point(Point &p);
	int GetX(void) { return X; } //取X坐标
	int GetY(void) { return Y; } //取Y坐标
private:
	int X,Y; //点的坐标
};
Point::Point(Point &p)
{
	X = p.X;
	Y = p.Y;
	cout << "Point拷贝构造函数被调用" << endl;
}
class Distance
{
public:
	Distance(Point a,Point b); //构造函数
	double GetDis() { return dist; }
private:
	Point p1,p2;
	double dist; // 距离
};
// 组合类的构造函数
Distance::Distance(Point a, Point b):p1(a),p2(b)
{
	cout << "Distance构造函数被调用" << endl;
	double x = double(p1.GetX() - p2.GetX());
	double y = double(p1.GetY() - p2.GetY());
	dist = sqrt(x*x + y*y);
	return;
}
int main()
{
	Point myp1(1,1), myp2(4,5);
	Distance myd(myp1, myp2);
	cout << "The distance is:";
	cout << myd.GetDis() << endl;
	system("pause");
	return 0;
}
/*
Point拷贝构造函数被调用
Point拷贝构造函数被调用
Point拷贝构造函数被调用
Point拷贝构造函数被调用
Distance构造函数被调用
The distance is:5
*/

类组合时有一种特殊情况,就是两个类可能相互包含,即类A中有类B类型的内嵌对象,类B中也有A类型的内嵌对象。我们知道,C++中,要使用一个类必须在使用前已经声明了该类,但是两个类互相包含时就肯定有一个类在定义之前就被引用了,这时候怎么办呢?就要用到前向引用声明了。前向引用声明是在引用没有定义的类之前对该类进行声明,这只是为程序声明一个代表该类的标识符,类的具体定义可以在程序的其他地方,简单说,就是声明下这个标识符是个类,它的定义你可以在别的地方找到。

比如,类A的公有成员函数f的形参是类B的对象,同时类B的公有成员函数g的形参是类A的对象,这时就必须使用前向引用声明:

 class B; //前向引用声明
 class A
 { 
 public:
 void f(B b);
 };
 class B
 { 
 public:
 void g(A a);
 };

这段程序的第一行给出了类B的前向引用声明,说明B是一个类,它具有类的所有属性,具体的定义在其他地方。

-End-

最近发表
标签列表