组合与继承
代码重用是面向对象最引人注目的功能之一:
- 可以通过创建新类来复用代码,而不必再重头开始编写。
- 可以使用别人已经开发并调试好的类。
类的重用
- 在新类中使用其他类的对象。即新类由多种类的对象组成,这种方法称为组合。
- 在现有类的基础上创建新类,在其中添加新代码,这种方法称为继承。
类的组合:在定义一个类时,若类的数据成员或者成员函数的参数是另一个类的对象。
- 适用两个对象之间是has-a的关系。
- 类组合语法很简单,只要把已存在类的对象放到新类中即可。
- “has a”关系在UML图中表示聚合,体现了类之间的整体和局部的关系,并且没有了整体,局部也可单独存在。
在类图使用空心的菱形表示聚合,菱形从局部指向整体。
“contains-a”关系在UML图中表示组合(Composition) ,是一种强烈的包含关系。组合类负责被组合类的生命周期。是一种更强的聚合关系。部分不能脱离整体存在。
在类图使用实心的菱形表示,菱形从局部指向整体。
示例
通过“has a”即可简单地把Point对象放在类Line中。
Line类的UML图中还给出关联时的数量关系。两个Point对象决定一条Line,所以用“1”和“2”表示。如果将Line换成多边形,多边形应该包含大于等于3个的Point,则应将“2”换成“3…*”。
Line的对象不能直接存取Point类的对象的私有数据成员,但可以通过Point类的对象成员pt1和pt2存取(对象名.成员名,public访问权限)。
类的组合的构造函数
设计
原则:不仅要负责对本类中的基本类型成员数据赋初值,也要对对象成员初始化。
声明形式:类名::类名(对象成员所需的形参,本类成员形参) :对象1(参数),对象2(参数),...... { 本类初始化 }
组合的析构函数声明方法与一般(无继承关系时)类的析构函数相同。当不需要显式地调用析构函数时,系统会自动隐式调用。组合的析构函数的调用次序与组合的构造函数调用次序相反。
调用
- 构造函数调用顺序:
- 先调用内嵌对象的构造函数(按内嵌时的声明顺序,先声明者先构造)。
然后调用本类的构造函数。 - 若调用缺省构造函数(即无形参的),则内嵌对象的初始化也将调用相应的缺省构造函数。
- 当需要执行类中带形参的构造函数来初始化数据时,组合对象的构造函数应在初始化列表中为组合对象的构造函数提供参数。
示例
例8.1:要求用类的组合方法输出圆信息,包括半径和圆心。
#include <iostream>
using namespace std;
class Point
{private:
double x,y;
public:
Point(double a=0.0,double b=0.0)
{ x=a;y=b; }
void Show()
{ cout<<x<<","<<y<<endl; }
double GetX()
{return x;}
double GetY()
{return y;}
};
class Circle{
public:
Circle(double a=0.0,double b=0.0,double c=0.0):p(a,b)
{ r=c; } //还要负责对本类中的成员r赋初值
Circle(Point &a,double c=0.0):p(a) //要对对象成员初始化
{ r=c; }
void Show();
private:
double r;
Point p;//定义一个Point对象
};
void Circle::Show()
{ cout<<"半径="<<r<<endl<<"圆心=";
cout<<p.GetX()<<","<<p.GetY()<<endl;//等价于p.Show();//通过对象名.公有成员函数
}
int main()
{
Point p1(10,10);//定义点类对象
Circle c1(100,100,10),c2(p1,5);//定义圆类对象
c1.Show();//调用成员函数
c2.Show();//调用成员函数
return 0;
}
Circle类以组合方式重用Point类代码,没有打破Point类对数据的封装保护
类的继承:根据已有类来定义新类,新类拥有已有类的所有功能。
-
基类/父类(super class)是所有派生类/子类(derived class)的公共属性及方法的集合,子类则是父类的特殊化。
-
C++支持类的单继承,也支持类的多继承,本讲重点是单继承,即每个子类有一个直接父类。
-
继承的目的:根据已有类来定义新类,新类拥有已有类的所有功能,实现代码重用。
-
- 基类/父类
被直接或间接继承的类
- 基类/父类
-
派生的目的:当新的问题出现,原有程序无法解决(或不能完全解决)时,需要对原有程序进行改造。
-
- 派生类(derived-class)/子类
继承所有祖先的状态和行为
子类可以增加变量和方法
子类也可以覆盖/重写(override)继承的方法
- 派生类(derived-class)/子类
基类与派生类的对应关系
- 单继承(也叫单一继承)
派生类只从一个基类派生。 - 多继承(也叫多重继承)
派生类从多个基类派生。 - 多重派生
由一个基类派生出多个不同的派生类。 - 多层派生
派生类又作为基类,继续派生新的类。这样就形成类的一个家族—类族。在类族中,直接参与派生出某类的基类称为直接基类,基类的基类甚至更高层的基类也称为间接基类
派生类对象与基类对象存在“is a”(或“is kind of”)的关系
“is a”关系在UML图中表示泛化,体现了类之间的继承关系。使用一条带有空心三角箭头的实线
指向基类表示泛化关系
继承用中空箭头表示,箭头指向父类。
派生类
-
定义格式
class 派生类名: 继承方式 基类名1,…,继承方式 基类名n
//每一个“继承方式”,只用于限制对紧随其后之基类的继承。
{
派生类成员声明;
}; -
继承方式
public:公有继承
protected:保护继承
private:私有继承
注意:如果不显式给出继承方式,系统默认就是私有继承(private)。
//不同继承方式的影响主要体现在:派生类成员对基类成员的访问权限;通过派生类对象对基类成员的访问权限。
继承成员的访问控制
注意:基类的构造函数和析构函数不被继承。
- 公有继承
基类的public和protected成员的访问属性在派生类中保持不变,但基类的private成员不可直接访问。
派生类中的成员函数可以直接访问基类中的public和protected成员,但不能直接访问基类的private成员。
通过派生类的对象只能访问基类的public成员。
示例
例8.2:公有继承示例1,要求用类的公有继承方法输出圆信息,包括半径和圆心。
#include <iostream>
using namespace std;
class Point
{private:
double x,y;
//将Point的数据成员设计为 protected,不必设计返回数据成员的函数。
public:
void SetP(double a,double b)
{x=a;y=b; }
void ShowP()
{cout<<x<<","<<y<<endl; }
double GetX()
{return x;}
double GetY()
{return y;}
};
class Circle:public Point
{
public:
void SetC(double a,double b,double c)
{ SetP(a,b);r=c; } //直接调用继承的公有成员函数
void ShowC();
private:
double r;
};
//Circle类以公有继承方式重用Point类代码,不要忘记Circle类已经继承了Point类的数据成员和成员函数
void Circle::ShowC()
{ cout<<"半径="<<r<<endl<<"圆心=";
cout<<GetX()<<","<<GetY()<<endl;//等价于ShowP();
}
int main()
{Circle c1;//定义圆类对象
c1.SetC(100,100,10);//调用成员函数
c1.ShowC();//调用成员函数
//公有继承,派生类对象.成员方式可以访问基类的公有成员
cout<<c1.GetX()<<","<<c1.GetY()<<endl;//等价于c1.ShowP();
return 0;
}
Circle类的SetC函数不仅要提供数据成员–半径的赋值,还要提供基类Point类的x、y的赋值,即使x、y的是Point类的私有的数据成员,Circle类不能直接访问它们,但是不要忘记Circle类已经继承了Point类的数据成员。
例8.5:公有继承示例2,要求用类的公有继承方法输出圆信息,包括半径和圆心。
#include <iostream>
using namespace std;
class Point
{protected:
//将Point的数据成员设计为 protected,这样就不必设计返回数据成员的函数。
double x,y;
public:
void SetP(double a,double b)
{x=a;y=b; }
void ShowP()
{ cout<<x<<","<<y<<endl; }
};
class Circle:public Point
{
public:
void SetC(double a,double b,double c)
{ SetP(a,b);r=c; }
void ShowC();
private:
double r;
};
void Circle::ShowC()
{ cout<<"半径="<<r<<endl<<"圆心=";
cout<<x<<","<<y<<endl;//等价于ShowP();
//类中直接调用继承的保护数据成员
}
int main()
{ Circle c1;//定义圆类对象
c1.SetC(100,100,10);//调用成员函数
c1.ShowC();//调用成员函数
return 0;
}
Circle类以公有继承方式重用Point类代码,不要忘记Circle类已经继承了Point类的数据成员和成员函数
Circle类的SetC函数不仅要提供数据成员–半径的赋值,还要提供基类Point类的x、y的赋值,x、y的是Point类的保护的数据成员,Circle类中可以直接访问它们。
- 私有继承
基类的public和protected成员都以private身份出现在派生类中,但基类的private成员不可直接访问。
派生类中的成员函数可以直接访问基类中的public和protected成员,但不能直接访问基类的private成员。
通过派生类的对象不能直接访问基类中的任何成员。
示例
例8.4:私有继承示例,要求用类的私有继承方法输出圆信息,包括半径和圆心。
#include <iostream>
using namespace std;
class Point
{private:
double x,y;
//将Point的数据成员设计为 protected,不必设计返回数据成员的函数。
public:
void SetP(double a,double b)
{x=a;y=b; }
void ShowP()
{cout<<x<<","<<y<<endl; }
double GetX()
{return x;}
double GetY()
{return y;}
};
class Circle:private Point
{
public:
void SetC(double a,double b,double c)
{ SetP(a,b);r=c; } //直接调用继承的公有成员函数
void ShowC();
private:
double r;
};
void Circle::ShowC()
{cout<<"半径="<<r<<endl<<"圆心=";
cout<<GetX()<<","<<GetY();//等价 ShowP();
}
//私有继承,派生类对象不能访问基类的公有成员,但是派生类内部可以
int main()
{
Circle c1;//定义圆类对象
c1.SetC(100,100,10);//调用成员函数
c1.ShowC();//调用成员函数
//私有继承不能访问,下面语句被注释
//cout<<c1.GetX()<<","<<c1.GetY();//等价 c1.ShowP();×
return 0;
}
- 保护继承
基类的public和protected成员都以protected身份出现在派生类中,但基类的private成员不可直接访问。
派生类中的成员函数可以直接访问基类中的public和protected成员,但不能直接访问基类的private成员。
通过派生类的对象不能直接访问基类中的任何成员。
示例
例8.3:保护继承示例,要求用类的保护继承方法输出圆信息,包括半径和圆心。
#include <iostream>
using namespace std;
class Point
{private:
double x,y;
//将Point的数据成员设计为 protected,不必设计返回数据成员的函数。
public:
void SetP(double a,double b)
{x=a;y=b; }
void ShowP()
{cout<<x<<","<<y<<endl; }
double GetX()
{return x;}
double GetY()
{return y;}
};
class Circle:protected Point
{
public:
void SetC(double a,double b,double c)
{ SetP(a,b);r=c; } //直接调用继承的公有成员函数
void ShowC();
private:
double r;
};
void Circle::ShowC()
{ cout<<"半径="<<r<<endl<<"圆心=";
cout<<GetX()<<","<<GetY()<<endl;//等价 ShowP();
}
//保护继承,派生类对象不能访问基类的公有成员,但是派生类内部可以
int main()
{
Circle c1;//定义圆类对象
c1.SetC(100,100,10);//调用成员函数
c1.ShowC();//调用成员函数
//保护继承不能访问下面语句,因此被注释
//cout<<c1.GetX()<<","<<c1.GetY();//等价 c1.ShowP();×
return 0;
}
结论
protected成员对建立其所在类对象的模块来说,它与 private 成员的性质相同。对于其派生类来说,它与 public 成员的性质相同。既实现了数据隐藏,又方便继承,实现代码重用。
构造函数
派生类名::派生类名(基类所需的形参,本类成员所需的形参):
基类名(参数)
//自动调用基类构造函数完成继承来的成员初始化
{
本类成员初始化赋值语句;
//只需要对本类中新增成员进行初始化
};
- 基类的构造函数不被继承,派生类中需要声明自己的构造函数。
- 派生类的构造函数需要给基类的构造函数传递参数。
- 当基类中声明有缺省构造函数或未声明构造函数时,派生类构造函数可以不向基类构造函数传递参数,也可以不声明,构造派生类的对象时,基类的缺省构造函数将被调用。
- 当需要执行基类中带形参的构造函数来初始化基类数据时,派生类构造函数应在初始化列表中为基类构造函数提供参数。
执行顺序
- 先调用基类构造函数,如果基类有多个,则基类构造函数的调用顺序按照它们被继承时声明的顺序(从左向右)。
- 如果派生类中有组合对象,其次对成员对象进行初始化,初始化顺序按照它们在类中声明的顺序。
- 最后执行派生类的构造函数
例8.6:构造函数示例,要求用类的公有继承方法输出圆信息,包括半径和圆心。
#include <iostream>
using namespace std;
class Point
{protected:
double x,y;
public:
Point(double a=0,double b=0)
{x=a;y=b; }
void ShowP()
{cout<<x<<","<<y<<endl; }
};
class Circle:public Point
{
public:
Circle(double a=0,double b=0,double c=0):Point(a,b) //基类初始化
{ r=c; } //本类初始化
void ShowC();
private:
double r;
};
void Circle::ShowC()
{ cout<<"半径="<<r<<endl<<"圆心=";
cout<<x<<","<<y<<endl;//等价于ShowP();
}
int main()
{Circle c1(100,100,10);//定义圆类对象,带参数
c1.ShowC();//调用成员函数
return 0;
}
Protected用#表示
Circle类以公有继承方式重用Point类代码
Circle类已经继承了Point类的数据成员和成员函数
析构函数
- 派生类的析构函数
- 析构函数也不被继承,派生类自行声明
- 声明方法与一般(无继承关系时)类的析构函数相同。
- 不需要显式地调用基类的析构函数,系统会自动隐式调用。
- 析构函数的调用次序与构造函数相反。
例8.7:析构函数示例,在例8.6基础上修改,增加构造函数和析构函数的调用提示,并关注构造函数与析构函数调用顺序。
#include <iostream>
using namespace std;
class Point
{protected:
double x,y;
public:
Point(double a=0,double b=0)
{x=a;y=b;cout<<"Point类"<<endl; }
void ShowP()
{ cout<<x<<","<<y<<endl; }
~Point(){cout<<"~Point类"<<endl; }
};
class Circle:public Point
{public:
Circle(double a=0,double b=0,double c=0):Point(a,b)
{ r=c; cout<<"Circle类"<<endl; }
void ShowC();
~Circle() {cout<<"~Circle类"<<endl; }
private:
double r;
};
void Circle::ShowC()
{ cout<<"半径="<<r<<endl<<"圆心=";
cout<<x<<","<<y<<endl;//等价于ShowP();
}
int main()
{Circle c1(100,100,10);//定义圆类对象
c1.ShowC();//调用成员函数
return 0;
}
同名覆盖规则:对象一定先调用自己的同名成员,如果自己没有同名成员,则调用直接基类的同名成员,以此类推。
当派生类与基类中有相同成员时:
若未强行指名,则通过派生类对象使用的是派生类中的同名成员。
如要通过派生类对象访问基类中被覆盖的同名成员,应使用基类名限定。
程序设计过程中,一定要注意避免定义的二义性。可以使用作用域分辨符“::”解决二义性问题。
例8.8:同名覆盖函数示例,要求用类的公有继承方法输出圆信息,包括半径和圆心。
#include <iostream>
using namespace std;
class Point
{protected:
double x,y;
public:
Point(double a=0,double b=0)
{x=a;y=b; }
void Show()
{cout<<x<<","<<y<<endl; }
};
class Circle:public Point
{
public:
Circle(double a=0,double b=0,double c=0):Point(a,b) //基类初始化
{ r=c; } //本类初始化
void Show();
private:
double r;
};
void Circle::Show()
{cout<<"半径="<<r<<endl<<"圆心=";
cout<<x<<","<<y<<endl;
//等价于Point::Show();//调用被隐藏的基类同名成员函数
}
int main()
{Circle c1(100,100,10);//定义圆类对象,带参数
c1.Show();//调用成员函数
return 0;
}
Circle类以公有继承方式重用Point类代码
Circle类已经继承了Point类的数据成员和成员函数