2 实现继承
面向对象编程基于四个重要方面:封装、抽象、继承和多态。继承是一种强大的属性重用方式,是通向多态的跳板。
这章中将学习:
- 编程意义上的继承;
- C++继承语法;
- 公有继承、私有继承和保护继承;
- 多继承;
- 隐藏基类方法和切除(slicing)导致的问题。
2.1 继承基础
在编程领域,经常会遇到具有类似属性,但细节或行为存在细微差异的组件。在这中情况下,一种解决之道是将每个组件声明为一个类,并在每个类中实现所有属性,这将重复实现相同的属性。另一种解决办法是继承,从同一个基类派生出类似的类,在基类实现所有通用的功能,并在派生类中覆盖基本功能,以实现让每个类都独一无二。第二种方法更好。面向对象编程支持继承。看下图:
2.1.1 继承和派生
上面的图说明了基类与派生之间的关系。派生类继承了基类。
2.1.2 C++派生语法
如何从Fish类派生出Carp类呢?C++派生语法如下:
//declaring a super class
class Base
{
//...base class member
};
//declaring a sub-class
class Derived:access-specifer Base
{
//...derived class members
} ;
其中access-specifier可以使public(这是最常见的,表示派生类是一个基类)、private或protected(表示派生类有一个基类)。
下面的继承结构表明,Carp类是从Fish类派生而来的:
class Fish
{
//...Fish's members
};
class Carp:public Fish
{
//...Carp's members
};
基类也称为超类,从基类派生而来的类称为派生类,也叫子类。
#include <iostream>
using namspace std;
class Fish
{
public:
bool FreshWaterFish;
void Swim()
{
if (FreshWaterFish)
cout<<"Swim in lake"<<endl;
else
cout<<"Swim in sea"<<endl;
}
};
class Tuna:public Fish
{
public:
Tuna()
{
FreshWaterFish = False;
}
};
class Carp:public Fish
{
public:
Carp()
{
FreshWaterFish = true;
}
};
int main()
{
Carp myLunch;
Tuna myDinner;
cout<<"Getting my food to swim"<<endl;
cout<<"Lunch: ";
myLaunch.Swim();
cout<<"Dinner: ";
myDinner.Swim();
return 0;
}
2.1.3 访问限定符protected
避免某些篡改,可以让基类的某些属性能在派生中访问,但不能在继承结构层次之外部访问。这意味着,Fish类的布尔标记FreshWaterFish可在派生类Tuna和Carp中访问,但不能在实例化Tuna和Carp的main()中访问。为此,可以使用关键字protected。
myDinner.FreshWaterFish = true;
与public和private一样,protected也是一个访问限定符。将属性声明为protected是,相当于允许派生类和友元访问它,但是禁止在继承层次结构外部(包括main())访问它。
总结一下:要让派生类能访问基类的某个属性,可使用访问限定符protected。
#include <iostream>
using namespace std;
class Fish
{
protected:
bool FreshWaterFish;
public:
void Swim()
{
if(FreshWaterFish)
cout<<"Swims in lake"<<endl;
else
cout<<"Swims in sea"<<endl;
}
};
class Tuna:public Fish
{
public:
Tuna()
{
FreshWaterFish = false;
}
};
class Carp:public Fish
{
public:
Carp()
{
FreshWaterFish = true;
}
};
int main()
{
Carp myLaunch;
Tuna myDinner;
cout<<"Getting my food to swim"<<endl;
cout<<"Launch: ";
myLaunch.Swim();
cout<<"Dinner: "
myDinner.Swim();
return 0;
}
这是面向对象编程的一个非常重要的的方面,它与数据抽象和继承一起确保派生类可安全地继承基类的属性,同时禁止在继承层次结构外部对其进行修改。
1.1.4 基类初始化——向基类传递参数
如果基类包含重载的构造函数,需要在实例化时给它提供实参。
采用初始化列表即可,并通过派生类的构造函数调用合适的基类构造函数:
class Base
{
public:
Base(int SomeNumber) //Overloaded constructor
{
//Do something with SomeNumber
}
};
class Derived:public Base
{
public:
Derived():Base(25) //instantiate class Base with argument 25
{
//derived class constructor code
}
};
对于Fish类来说,这种机制很有用。通过给Fish的构造函数提供一个布尔参数,以初始化Fish::REshWaterFish,可以强制每个派生类都指出它自己是淡水鱼还是海水鱼,代码如下:
#include <iostream>
using namespace std;
class Fish
{
protected:
bool FreshWaterFish;
public:
Fish(bool IsFreshWaterFish):FreshWaterFish(IsFreshWaterFish){}
void Swim()
{
if (FreshWaterFish)
cout<<"Swim in Lake."<<endl;
else
cout<<"Swim in Sea."<<endl;
}
};
class Tuna:public Fish
{
public:
Tuna():Fish(false){}
};
class Carp:public Fish
{
public:
Carp():Fish(true){}
};
int main()
{
Carp myLaunch;
Tuna myDinner;
cout<<"Getting my food to swim."<<endl;
cout<<"Launch: ";
myLaunch.Swim();
cout<<"Dinner: ";
myDinner.Swim();
return 0 ;
}
现在,Fish有一个构造函数,它接受一个默认参数,用于初始化Fish::FreshWaterFish。因此要创建Fish对象,必须提供一个用于初始化该保护成员的参数。这样,Fish类便避免了保护成员包含随机值的情况,尤其是派生类忘记设置它是。派生类Tuna和Carp被迫定义这样一个构造函数,即使用何时的参数(true或者false,表示是否是淡水鱼)来实例化Fish。
注意:0派生类没有直接访问布尔成员变量Fish::FreshWaterFish,虽然这是一个派生类可以访问的保护成员。这是因为这个变量是通过Fish的构造函数设置的。为最大限度地体改安全性,对于派生类不需要访问的基类属性,别忘了将其声明为私有的。
2.1.5 在派生类中覆盖基类的方法
如果派生类实现了从基类继承的函数,且返回值和特征标相同,就相当于覆盖了基类的这个方法,如下面的代码
class Base
{
public:
void DoSomething()
{
//implementation code ... Dose something
}
};
class Derived:public Base
{
public:
void DoSomething()
{
//implementation code ... Dose something else
}
};
因此,如果使用Derived类的实例调用方法DoSomething(),调用的将不是Base类中的这个方法。
如果Tuna和Carp中实现了自己的Swim()方法,则相当于覆盖了基类中的Swim()方法。看下面的程序:
#include <iostream>
using namespace std;
class Fish
{
private:
bool FreshWaterFish;
public:
Fish(bool IsFreshWaterFish):FreshWaterFish(IsFreshWaterFish){}
void Swim()
{
if (FreshWaterFish)
cout<<"Swim in Lake."<<endl;
else
cout<<"Swim in Sea."<<endl;
}
};
class Tuna:public Fish
{
public:
Tuna():Fish(false){}
void Swim()
{
cout<<"Tuna swims real fast."<<endl;
}
};
class Carp:public Fish
{
public:
Carp():Fish(true){}
void Swim()
{
cout<<"Carp swims real slow."<<endl;
}
};
int main()
{
Carp myLunch;
Tuna myDinner;
cout<<"Getting my food swim."<<endl;
cout<<"Lunch: ";
myLunch.Swim();
cout<<"Dinner: ";
myDinner.Swim();
return 0;
}
运行下代码可以看出,Tuna和Carp的类中的Swim()覆盖了Fish中的Swim()。
如果想调用Fish::Swim(),要么让派生类在其成员中显式地使用它,要么在main()中使用作用域解析符显式地调用它。
这里具体怎么做?看之后内容。
1.1.6 调用基类中被覆盖的方法
如果要在main()中调用Fish::Swim(),需要使用作用域解析运算符(::),看下面的代码:
myDinner.Fish::Swim();
1.1.7 在派生类中调用基类方法
通常,Fish::Swim()包含适用于所有鱼类的通用实现。如果要在Tuna::Swim()和Carp::Swim()中重用Fish::Swim()的通用实现,可以使用作用域解析运算符(::),代码如下:
class Carp:public Fish
{
public:
Carp():Fish(true){}
void Swim()
{
cout<<"Carp swims real slow."<<endl;
Fish::Swim();
}
};
看下面的实践代码:
#include <iostream>
using namespace std;
class Fish
{
private:
bool FreshWaterFish;
public:
Fish(bool IsFreshWaterFish):FreshWaterFish(IsFreshWaterFish){}
Void Swim()
{
if (FreshWaterFish)
cout<<"Swim in Lake."<<endl;
else
cout<<"Swim in Sea."<<endl;
}
};
class Tuna:public Fish
{
public:
Tuna():Fish(false){}
void Swim()
{
cout<<"Tuan swims real fast."<<endl;
}
};
class Carp:public Fish
{
public:
Carp():Fish(true){}
void Swim()
{
cout<<"Carp swims real slow."<<endl;
Fish::Swim();
}
};
int main()
{
Carp myLunch;
Tuna myDinner;
cout<<"Getting my food to swim."<<endl;
cout<<"Lunch: ";
myLunch.Swim();
cout<<"Dinner: ";
myDinner.Swim();
return 0;
}
1.1.8 在派生类中隐藏基类的方法
覆盖的一种极端情况,Tuna::Swim()可能隐藏Fish::Swim()的所有重载版本,使得调用这些重载版本会导致编译错误(因此称为被隐藏),看下面的程序:
#include <iostream>
using namespace std;
class Fish
{
public:
void Swim()
{
cout<<"Fish swims...!"<<endl;
}
void Swim(bool FreshWaterFish)
{
if(FreshWaterFish)
{
cout<<"Swims in lake."<<endl;
}
else
{
cout<<"Swims in sea."<<endl;
}
}
void Swim(bool FreshWaterFish)
{
if(FreshWaterFish)
cout<<"Swims in lake."<<endl;
else
cout<<"Swims in Sea."<<endl;
}
};
class Tuna:public Fish
{
public:
void Swim()
{
cout<<"Tuna swims real fast."<<endl;
}
};
int main()
{
Tuna myDinner;
cout<<"Getting my food to swim."<<endl;
//myDinner.Swim(false); //compile failure: Fish::Swim(bool) is hiden by //Tuna::Swim()
myDinner.Swim();
return 0;
]
这个Fish类与之前的Fish有所不同。除尽可能简单的版本诠释当前问题之外,这个Fish版本还包含两个重载的Swim()方法:一个不接受任何参数,另一个接受一个bool参数。鉴于Tuna以公有方式继承了Fish,理所当然以为可以通过Tuna实例可调用这两个版本的Fish::Swim()。然而,由于Tuna实现了自己的Tuna::Swim(),这对编译器隐藏了Fish::Swim(bool)。如果取消注释,将出现编译错误。
要通过Tuna实例调用Fish::Swim(bool),可采用如下解决方案:
- 解决方案一:在main()中使用作用域解析运算符(::) myDinner.Fish::Swim();
- 解决方案二:在Tuna类中,使用关键字using 解除对Fish::Swim()的隐藏
class Tuna:public Fish { using Fish::Swim(); void Swim() { cout<<"Tuna swims real fast."<<endl; } };
3.解决方案三,覆盖Fish::Swim()所有重构版本(如果需要,可以通过Tuna::Fish(...)调用方法Fish::Swim())
class Tuna:public Fish { public: void Swim(bool FreshWaterFish) { Fish::Swim(FreshWaterFIsh); } void Swim() { cout<<"Tuna swims real fast."<<endl; } };
2.1.9 顺序构造
如果Tuna是从Fish派生而来的,创建Tuna对象时,先调用Tuna的构造函数还是Fish的构造函数?另外,实例化对象时,成员属性(如Fish::FreshWaterFish)是调用构造函数之前还是之后之后实例化?基类对象在派生类对象之前被实例化,因此,首先构造Tuna对象的Fish部分,这样,实例化Tuna部分时,成员属性(具体地说应该是Fish的保护和公有属性)已经准备就绪,可以使用了。实例化Fish部分和Tuna部分时,先实例化成员属性(如Fish::FreshWaterFish),再调用构造函数,确保成员属性准备就绪,可供构造函数使用。这也适用于Tuna::Tuna()。
2.1.10 析构顺序
Tuna实例不再在作用域内时,析构顺序与构造顺序。看如下代码:
#include <iostream>
using namespace std;
class FishDummyMember
{
public:
FishDummyMember()
{
cout<<"FishDummyMember constructor"<<endl;
}
~FishDummyMember()
{
cout<<"FishDummyMember destructor"<<endl;
}
};
class Fish
{
protected:
FishDummyMember dummy;
public:
//Fish constructor
Fish()
{
cout<<"Fish constructor"<<endl;
}
~Fish()
{
cout<<"Fish destructor"<<endl;
}
};
class TunaDummyMember
{
public:
TunaDummyMember()
{
cout<<"TunaDummyMember constructor"<<endl;
}
~TunaDummyMember()
{
cout<<"TunaDummyMember destructor"<<endl;
}
};
class Tuna: public Fish
{
private:
TunaDummyMember dummy;
public:
Tuna()
{
cout<<"Tuna constructor"<<endl;
}
~Tuna()
{
cout<<"Tuna destructor"<<endl;
}
};
int main()
{
Tuna myDinner;
}
虽然主函数里面就一行代码,但是输出的东西还是比较多的。实例化一个Tuna对象就产生了这些输出,这是因为构造函数和析构函数里面包含了cout语句。为了帮助理解成员变量是如何被实例化和销毁的,定义了两个毫无用途的类———FishDummyMember和TunaDummyMember,并在其构造函数和析构函数中包含了cout语句。Fish和Tuna类分别将这些类的对象作为成员。输出表明,实例化Tuna对象时,将从继承层次结构底部开始,因此首先实例化Tuna对象的Fish部分。为此实例化Fish的成员属性,即Fish::dummy。构造好成员属性(如dummy)后,将调用Fish的构造函数。构造好基类部分后,将实例化Tuna部分——首先实例化成员Tuna::dummy,再执行构造函数Tuna::Tuna()的代码。输出表明,析构顺序正好相反。
2.2 私有继承
前面介绍的都是公有继承,私有继承与公有继承的不同之处在于,指定派生类的基类时使用关键字private:
class Base
{
//...base class member and methods
};
class Derived:private Base
{
//...derived class members and methods
};
私有继承意味着在派生类的实例中,基类的所有成员和方法都是私有的——不能从外部访问。换句话说,即便是Base类的公有成员和方法,也只能被Derived类使用,无法通过Derived实例来使用它们。
私有继承使得只有子类才能使用基类的属性和方法。
#include <iostream>
using namespace std;
class Motor
{
public:
void SwitchIgnition()
{
cout<<"Ignition On"<<endl;
}
void PumpFuel()
{
cout<<"Fuel in cylinders"<<endl;
}
void FireCylinders()
{
cout<<"Vroooom"<<endl;
}
};
class Car:private Motor
{
public:
void Move()
{
SwitchIgnition();
PumpFuel();
FireCylinders();
}
};
int main()
{
Car myDreamCar;
myDreamCar.Move();
return 0;
}
Motor类非常简单,包含三个公有的成员函数。Car类使用关键字private集成了Motor类。公有函数Car::Move()调用了基类Motor的成员函数。如果在上面的main()函数中插入:
myDreamCar.PumpFuel();
这将无法通过编译。
注意:如果有一个SuperCar,它继承了Car类,则不管SuperCar和Car之间的继承关系是什么样的,SuperCar都不能访问基类Motor的公有成员和方法,这是因为Car和Motor之间是私有继承关系,这意味着除了Car之外,其他所有实体都不能访问Motor的公有成员。换句话说,编译器在确定派生类能否访问基类的公有或保护成员时,考虑的是继承层次中最严格的访问限定符。
2.3 保护继承
保护继承不同于公有继承在于,声明派生类继承基类时的关键字protected:
class Base
{
//... base class members and methods
};
class Derived: protected Base
{
//... derived class members and methods
};
保护继承与私有继承的类似之处如下:
- 它也表示has-a关系;
- 它也让派生类能够访问基类的所有公有和保护成员;
- 在继承层次结构外面,也不能通过派生类实例访问基类的公有成员。
随着继承层次结构的加深,保护继承与私有继承有些不同:
class Derived2:protected Derived
{
//can access members of Base
};
在保护继承层次中,子类的子类(即Derived2)能够访问Base类的公有成员。但如果Derived和Base之间的继承关系是私有的,就不能这样做。
#include <iostream>
using namespace std;
class Motor
{
public:
void SwitchIgnition()
{
cout<<"Ignition On"<<endl;
}
void PumpFuel()
{
cout<<"Fuel in cylinders"<<endl;
}
void FireCyliders()
{
cout<<"Vroooom"<<endl;
}
};
class Car:protected Motor
{
public:
void Move()
{
SwitchIgnition();
PumpFuel();
FireCylinders();
}
};
class SuperCar:protected Car
{
public:
void Move()
{
SwitchIgnition();
PumpFuel();
FireCylinders();
FireCylinders();
FireCylinders();
}
};
int main()
{
SuperCar myDreamCar;
myDreamCar.Move();
return 0;
}
Car类以保护方式继承了Motor类,而SuperCar类以保护方式继承了Car类。正如所看到的,SuperCar::Move()实现使用了基类Motor中定义的方法。
能否经由中间基类Car访问终极基类Motor呢?这取决于Car和Motor之间的继承关系。如果继承关系是私有的,而不是保护的,SuperCar将不能访问Motor类的公有成员,因为编译器根据最严格的访问限定符来确定访问权限。
警告:仅当必要时才使用私有或保护继承。
对于大多数使用私有继承的情形(如Car和Motor之间的私有继承),更好的选择是,将基类对象作为一个成员属性,通过继承Motor类,相当于对Car进行了限制,使其只能有一台发动机。
将Motor对象作为Car类的私有成员被称为组合或者聚合,这样的Car类类似于下面这样:
class Car
{
private:
Motor heartOfCar;
public:
void Move()
{
heartOfCar.SwitchIgnition();
heartOfCar.PumpFuel();
heartOfCar.FireCylinders();
}
};
这是一种不错的设计,可以轻松地在Car类中添加Motor成员,而无需改变继承层次结构,也不用修改客户看到的设计。
2.4 切除问题
如果这样做,结果会如何?
Derived objectDerived;
Base objectBase = objectDerived;
如果这样做,结果又会如何?
void FuncUseBase(Base Input);
...
Derived objectDerived;
FuncUseBase(objectDerived);
它们都将Derived对象复制给Base对象,一个是通过显式复制,另一个是通过传递参数。在这些情境下,编译器将只复制objectDerived的Base部分,即不是整个对象,而是Base容纳的部分,这通常不是程序员本意,这种无意间的剪裁数据,导致Derived变成Base的行为称为切除。
警告;要避免切除问题,不要按值传递参数,而硬以指向基类的指针或const引用的方式传递。
2.5 多传递
C++允许继承多个类
class Derived:access-specifier Base1,access-specifier Base2
{
//class members
};
我们直接看代码吧
#include <iostream>
using namespace std;
class Mammal
{
public:
void FeedBabyMilk()
{
cout<<"Mammal: Baby says glug!"<<endl;
}
};
class Reptile
{
public:
void SpitVenom()
{
cout<<"Reptile: Shoo enemy! Spits venom!"<<endl;
}
};
class Bird
{
public:
void LayEggs()
{
cout<<"Bird: Laid my eggs, am lighter now!"<<endl;
}
};
class Platypus: public Mammal, public Bird, public Reptile
{
public:
void Swim()
{
cout<<"Platypus: Voila, I can swim!"<<endl;
}
};
int main()
{
Platypus realFreak;
realFreak.LayEggs();
realFreak.FeedBabyMilk();
realFreak.SpitVenom();
realFreak.swim();
return 0;
}
务必牢记:公有继承意味着继承派生类的类能够访问基类的公有和保护成员。
务必牢记,私有继承意味着继承派生类的类也不能访问基类的成员。
务必牢记,保护继承意味着派生继承类的类能够访问基类的公有和保护方法。
务必牢记,无论继承关系是什么,派生类都不能访问基类的私有成员。