组合与继承
组合-复用类的实现
//patrs.h
class Eye
{
public:
void see(void){
...}
};
class Nose
{
public:
void smell(void){
...}
};
class Mouth
{
public:
void speak(void){
...}
};
class Ear
{
public:
void listen(void){
...}
};
//head.h
//组合,复用已有类实现的代码
#include"parts.h"
class Head
{
public:
Eye leftEye,rightEye;
Nose nose;
Mouth mouth;
Ear leftEar,rightEar;
void turn(){
...}
};
//test.cpp
#include"head.h"
int main()
{
Head h;
h.turn();
h.nose.smell();
}
常用的方式是将嵌入对象作为新类的私有成员, 这时它是新类内部实现的一部分。新类中的方法可以使用成员对象提供的功能, 但新类只向外部展现自己的接口, 隐藏了包含的成员对象。
类中如果包含多个对象成员, 在初始化列表中将它们用逗号隔开。成员初始化的次序和成员声明的次序相同, 并不考虑它们在初始化列表中的排列顺序。建议初始化成员的顺序和声明顺序一致。
当组合对象被撤销时, 会执行其析构函数, 成员对象的析构函数也会被执行。执行次序和构造函数相反, 即先执行组合对象的析构函数, 再执行成员对象的析构函数。
指针成员与聚合关系
聚合关系的特点是成员对象可以独立于聚合对象而存在。当聚合对象被创建或撤销时, 其成员对象可以不受影响, 而只是它们之间的关系受到影响。
聚合关系在C++中使用按指针组合的语法实现: 聚合对象中包含成员类对象的指针。
class MailBody{
...};
class Attachment
{
string filename;
//其他成员...
};
class Email
{
string title;
MailBodey body;
vector<Attachment*> attch //聚合,指向多个附件对象的指针数组
public:
void edit(){
...};
void save(){
...};
void send(string recieverAddr){
...};
void addAttch(Attachment* a){
attch.push_back(a); ... };
};
聚合关系的聚集对象中如果包含多个某类的成员,实现时一般借助数组或vector之类的标准容器。
class Coach {
...};
class Player {
...};
class Team
{
private:
Coach* chiefCoach;
vector<Player*>players;
public:
Team(Coach* pc)
{
chiefCoach=pc;
}
void changeCoach(Coach* pnc)
{
chiefCoach=pnc;
}
void employPlayer(Player* player)
{
players.push_back(player);
}
void firePlayer(Player* player)
{
//查找指定球员,返回指定球员的迭代器it
if(it!=players.end())
players.erase(it);
}
};
指针成员与关联关系
组合是一种更强的聚合,聚合是一种特殊的关联。
关联关系再C++中也用按指针组合的语法实现。
组合关系的语义可以用按值组合的对象成员语法实现,聚合或关联都使用按指针组合的语法实现。
class BankAccount
{
long accountNo;
double balance;
string clientName;
public:
BankAccount(long aNo,string name,double bal) {
...}
};
class Client
{
string name;
string address;
BankAccount* acc;//也可以如下
//BankAccount* accounts[5]; //一个客户最多5个账户
//vector<BankAccount*>accounts; //一个客户可以有多个账户
public:
Client(string nm,BankAccount* a):name(nm)
{
acc=a;
}
};
BankAccount ba(18024,"Hive",5000); //建立新帐户
Client Hive("Hive",&ba);
继承-复用类的接口
被继承的类称为基类,继承得到的新类称为派生类
class student
{
string name;
int stu_id;
string department;
public:
student(string nm,int id,string dp);
void print()const;
};
class grad_student:public student
{
string thesis;
//其他成员继承得到,不用再重复声明
public:
grad_student(string nm,int id,string dp,string th);
void print()const; //重复声明表示要重新实现这个操作
}
派生类成员的访问控制
派生类继承了基类的成员, 但是基类成员在派生类中的可见性由两个因素决定: 成员在基类中的访问限定和继承时使用的访问限定符。
一个类的public成员在任何类和函数中都是可以访问的
private成员只有本类或本类的友元可以访问, 在其他类和函数中不可访问, 在自己的派生类中也同样不可访问。
一个类的protected成员的访问权限介于public 和private之间: 对它的派生类来说,protected 成员和public 成员一样是可访问的, 但对其他类和函数而言, protected 成员就如同private 成员一样不可访问。
在公有派生类中, 基类的public成员和protected成员被继承,分别作为派生类的public成员和protected成员。基类的private成员虽然也被继承了, 但在派生类中是不可见的。
在私有派生类中, 基类的public成员和protected成员被派生类作为自己的private成员继承下来。基类的private成员虽然也被继承了, 但在派生类中是不可见的。
继承时也可以使用protected限定符, 这时基类的public成员和protected成员都被派生类作为自己的protected成员继承下来。基类的private成员在protected派生类中仍是不可见的。
如果担心基类的封装性会因此被破坏, 可以将基类中的所有数据成员都声明为private,而不是protected。如果派生类真的需要访问基类的属性, 就在基类中为其提供相应的protected访问器函数。
//基类
class Base
{
int attr; //私有
protected: //设置protected访问器供派生类使用
int getAttr();
void setAttr(int);
};
如果要在派生类中对继承的基类成员的可见性进行调整,可以使用using声明,语法形式为:
class Derived:(public|private|protected)Base
{
public:(|private: |protected:)
using Base::成员名; //将继承的基类成员声明为public(private或protected)
};
using声明的作用是在派生类中调整个别基类成员的访问限制。派生类只能为它可以访问的名字提供using 声明, 不能改变基类private 成员的访问限制。
#include<iostream>
using namespace std;
class Base
{
public:
void f() {
cout<<"public: Base::f()"<<endl;}
void f(int) {
cout<<"public: Base::f(int)"<<endl;}
void g() {
cout<<"public: Base::g()"<<endl;}
protected:
void h() {
cout<<"protected: Base::h()"<<endl;}
private:
void k() {
}
};
class Derived1:public Base
{
protected:
using Base::g;
private:
using Base::f;
public:
//using Base::k;
using Base::h;
};
class Derived2:private Base
{
public:
using Base::g;
protected:
using Base::f;
};
int main()
{
Base b; b.f(); b.f(1); b.g(); //b.h();
Derived1 d1; d1.h(); //d1.g(); d1.f();
Derived2 d2; d2.g(); //d2.f();
cin.get();
}
/*Output*/
/*
public: Base::f()
public: Base::f(int)
public: Base::g()
protected: Base::h()
public: Base::g()
*/
派生类对象的创建和撤销
class Point2d
{
public:
Point2d(double x=0.0,double y=0.0):_x(x),_y(y){
};
double x() {
return _x;}
double y() {
return _y;}
void x(double newX) {
_x=newX;}
void y(double newY) {
_y=newY;}
protected:
double _x,_y;
};
class Point3d:public Point2d
{
public:
Point3d(double x=0.0,double y=0.0,double z=0.0)
:Point2d(x,y), _z(z) {
}
double z() {
return _z;}
void z(double newZ) {
_z=newZ;}
void print()
{
cout<<'('<<x()<<','<<y()<<','<<z()<<')';
}
protected:
double _z;
};
在撤销一个派生类对象时, 基类了对象也被撤销。析构函数的执行次序和构造函数的执行次序相反, 即先执行派生类的析构函数, 再执行基类的析构函数。
继承与特殊成员
1.禁止继承的类
如果不希望一个类被其他类继承,可以在类名后跟一个关键字final
2.不能自动继承的成员
并不是所有的基类成员都能被派生类继承, 下列成员函数是不能继承的:
- 构造函数
- 析构函数
- 赋值运算符函数
如果在派生类中没有定义这些函数, 编译器在必要时会自动生成派生类的默认构造函数、拷贝/ 移动构造函数、拷贝/ 移动赋值运算符和析构函数。在编译器自动生成的构造函数中会调用相应的基类构造函数来完成基类子对象的初始化。编译器自动生成的赋值运算符函数只能用于同类型对象之间的赋值, 其行为是按成员赋值, 如果要对不同类型的对象赋值, 需要自己定义赋值运算符。
3.复用基类的构造函数
派生类复用基类构造函数的方式是在派生类定义中提供一条using声明:
class Derived:public Base
{
using Base::Base;
}
using声明不改变构造函数的访问级别,无论出现在哪里,基类的构造函数在派生类中仍然是原来的访问权限。
派生类可以复用基类的构造函数, 同时定义自己的。一部分构造函数。如果派生类定义的构造函数与基类的构造承数有相同的参数列表, 则不会继承基类的这个构造函数, 在派生类中定义的构造函数将替换继承到的基类构造函数。
派生类不能复用默认、拷贝和移动构造函数, 如果没有直接定义这些构造函数, 编译器将按照正常规则为派生类自动生成。
继承的基类构造函数不会被作为类中定义的构造函数, 也就是说, 如果一个类只含有继承的基类构造函数, 那么编译器认为它没有定义任何构造函数, 会自动生成默认构造函数。
4.静态成员的继承
如果基类定义了一个static成员, 则在整个继承层次中只存在该成员的唯一定义。不论从基类派生出来多少个派生类, 对于每个静态成员来说都只存在唯一的实例。
class Base
{
public:
static void statmem(); //基类static成员
};
void Base::statmem() {
}
class Derived:public Base
{
public:
void f(const Derived& d);
};
void Derived::f(const Derived& d)
{
Base::statmem(); //正确,Base类中定义了statmem()
Derived::statmem(); //正确,Derived类中继承了statmem()
d.statmem(); //正确,通过派生类对象访问基类static成员
statmem(); //正确,通过this指向对象访问static成员
}
派生类与基类的不同
在派生类中修改基类的方式有如下两种。
( 1 ) 覆盖或隐藏基类的操作: 重新定义基类接口中已经存在的操作, 从而改变继承到的行为, 使得派生类对象在接收到同样的消息时其行为不同于基类对象。
( 2 ) 扩充接口: 向派生类的接口中添加新操作,使得派生类对象能够接收更多的消息。
覆盖与同名隐藏
class Point2d
{
public:
Point2d(double x=0.0,double y=0.0):_x(x),_y(y){
};
double x() {
return _x;}
double y() {
return _y;}
void x(double newX) {
_x=newX;}
void y(double newY) {
_y=newY;}
void moveto(double x,double y) {
_x=x;_y=y;}
void f(int) {
}
void f() {
}
void g(char) {
}
protected:
double _x,_y;
};
class Point3d:public Point2d
{
public:
Point3d(double x=0.0,double y=0.0,double z=0.0)
:Point2d(x,y), _z(z) {
}
double z() {
return _z;}
void z(double newZ) {
_z=newZ;}
void moveto(double x,double y,double z) //覆盖
{
Point2d::moveto(x,y);
_z=z;
}
void f() {
} //覆盖了Point2d中的f(),同时隐藏了f(int)
void g() {
} //隐藏了Point2d中的 void g
protected:
double _z;
};
覆盖:在派生类中重定义基类接口中的成员函数, 参数表和返回类型保持与基类中一致
隐藏:在派生类中重定义基类接口中的成员函数, 并改变了函数的参数表或返回类型
即使派生类中覆盖了基类的同名承数, 但是如果设置了不同的访问限制, 那么也会引起派生类和基类接口的差异。例如:
class Base
{
public: //公有接口中有两个操作f和g
void f() {
}
void g() {
}
};
class Derived:public Base
{
public: //公有接口中只有一个操作f
void f() {
}
private: //私有的g覆盖了Base中公有的g
void g() {
}
};
派生类向基类类型的转换
继承最重要的特性之一是替代原则: 在任何需要基类对象( 或地址) 的地方, 都可以由其公有派生类的对象( 或地址) 代替。替代原则有时也被称为赋值兼容规则。
在C++ 语言中,公有派生类就是基类的子类型, 公有派生类的对象可以自动转换为基类类型, 基类的指针和引用可以指向派生类的对象。例如:
class Base {
};
class Derived:public Base {
};
int main()
{
//派生类对象代替基类对象
Base b;
Derived d;
b=d; //正确:对象类型转换,派生类左值代替基类左值
Base& rb=d; //正确,引用类型转换
Base* pb;
pb=&d;
pb=new Derived;
delete pb;
}
因为是从更特殊的类型转换到更一般的类型, 所以派生类向基类类型转换总是安全的。
//派生类向基类的隐式类型转换
class Point2d
{
public:
Point2d(double x=0.0,double y=0.0):_x(x),_y(y){
};
double x() {
return _x;}
double y() {
return _y;}
void x(double newX) {
_x=newX;}
void y(double newY) {
_y=newY;}
void print()
{
cout<<'('<<x()<<','<<y()<<')'<<endl;
}
protected:
double _x,_y;
};
class Point3d:public Point2d
{
public:
Point3d(double x=0.0,double y=0.0,double z=0.0)
:Point2d(x,y), _z(z) {
}
double z() {
return _z;}
void z(double newZ) {
_z=newZ;}
void print()
{
cout<<'('<<x()<<','<<y()<<','<<z()<<')'<<endl;
}
protected:
double _z;
};
int main()
{
Point2d p2(1,2), *pt2=&p2;
Point3d p3(4,5,6), *pt3=&p3;
pt2->print(); //Point2d::print()
pt3->print(); //Point3d::print()
p2=p3; //向上类型转换-对象切片
p2.print(); //Point2d::print()
Point2d& r2=p3; //向上类型转换-引用
r2.print(); //Point2d::print()
pt2=&p3; //向上类型转换-指针
pt2->print(); //Point2d::print()
}
/*Output*/
/*
(1,2)
(4,5,6)
(4,5) //对象切片现象
(4,5)
(4,5)
*/
对象切片只是派生类向基类转换过程中改变地址的类型,用不同的方式解读同一段内存空间中的内存,并不会真正切除派生类对象多余的部分
组合与继承的选择
通过组合语法创建新类型时, 通常将己有类型的对象作为私有成员, 这使得被嵌入的成员成为了新类型的内部实现, 新类型可以不受其成员的约束, 向外提供完全不同的接口。即使其内部成员或实现方式发生改变, 也不会影响外部客户代码。组合具有很大的灵活性,是一种简单有效的代码复用方法, 在面向对象的设计模式中得到了大量应用。
继承是面向对象技术中另一种复用代码的重要机制。继承使得派生类与基类之间具有接口的相似性,派生类可以看作是基类的特殊子类型, 派生类对象可以替代基类对象。是否使用继承的一个重要依据便是考察类之间是否存在这种关系, 是否需要由基类提供公共接口, 是否需要派生类向基类的类型转换。
总结:
1.如果多个类共享数据而非行为, 应该创建这些类可以包含的共用对象。
2.如果多个类共享行为而非数据, 应该让它们从共同的基类继承而来, 并在基类里定义共用的操作。
3.如果多个类既共享数据也共享行为, 应该让它们从一个共同的基类继承而来, 并在基类里定义共用的数据和操作。
4.如果想由基类控制接口,使用继承; 如果想自己控制接口, 使用组合。
一个组合的例子:学生成绩单
//score.h
#ifndef SCORE_H
#define SCORE_H
#include<iostream>
#include<string>
using std::string;
using std::istream;
using std::ostream;
class Score
{
public:
Score(unsigned long id,string name,int p,int m,int f)
:sid(id),sname(name),project(

897

被折叠的 条评论
为什么被折叠?



