继承中的 is-a 关系
派生类和基类之间的特殊关系式基于C++继承的底层模型的。实际上,C++有三种继承方式:公有继承、保护继承、私有继承。公有继承是最常用的一种方式,它建立一种is-a关系,即派生类对象也是一个基类对象,可以对基类对象执行的任何操作,也可以对派生类对象执行。用 is-a-kind-of 描述更为准确,即派生类 is-a-kind-of 基类。
多态公有继承
有时我们会希望同一个方法在派生类和基类中的行为是不同的,也就是方法的行为取决于调用该方法的对象,这叫多态。实现多态公有继承有两种机制:1一种是在派生类中重新定义基类的方法,另一种是使用虚方法。
class Brass
{
private:
std::string fullName; long acctNum; double balance;
public:
Brass(const std::string & s = "Nullbody", long an = -1, double bal = 0.0);
void Deposit(double amt);
virtual void Withdraw(double amt);
double Balance()const;
virtual void ViewAcct()const;
virtual ~Brass() {}
};
class BrassPlus :public Brass
{
private:
double maxLoan; double rate; double owesBank;
public:
BrassPlus(const std::string & s = "Nullbody", long an = -1, double bal = 0.0, double ml = 500, double r = 0.11125);
BrassPlus(const Brass & ba, double ml = 500, double r = 0.11125);
virtual void ViewAcct()const;
virtual void Withdraw(double amt);
void ResetMax(double m) { maxLoan = m; };
void ResetRate(double r) { rate = r; };
void ResetOwes() { owesBank = 0; }
};
如果在派生类中重新定义基类的方法,通常应将基类方法声明为虚方法,采用virtual关键字,这样程序将根据对象类型而不是引用或指针的类型来选择方法版本。为基类声明一个虚析构函数也是一种惯例。
如果上述声明中ViewAcct()不是虚的,则程序将根据引用或指针的类型选择方法版本。
Brass Piggy("Porcelot Pigg", 381299, 4000.00);
BrassPlus Hoggy("Horatio Hogg", 382288, 3000.00);
Brass & b1_res = Piggy;
Brass & b2_res = Hoggy;
b1_ref.ViewAcct(); //use Brass::ViewAcct()
b2_ref.ViewAcct(); //use Brass::ViewAcct()
而采用virtual关键字,程序将根据引用或指针指向的对象类型选择方法版本。
Brass Piggy("Porcelot Pigg", 381299, 4000.00);
BrassPlus Hoggy("Horatio Hogg", 382288, 3000.00);
Brass & b1_res = Piggy;
Brass & b2_res = Hoggy;
b1_ref.ViewAcct(); //use Brass::ViewAcct()
b2_ref.ViewAcct(); //use BrassPlus::ViewAcct()
派生类并不能直接访问基类的私有数据,而必须使用基类的公有方法才能访问这些数据。访问的方式取决于方法。构造函数使用一种技术,而其他成员使用另一种技术。
派生类构造函数在初始化基类私有数据时,采用的是成员初始化列表语法。
BrassPlus::BrassPlus(const string & s, long an, double bal, double ml, double r) :Brass(s, an, bal)
{
maxLoan = ml;
owesBank = 0.0;
rate = r;
}
BrassPlus::BrassPlus(const Brass & ba, double ml, double r) :Brass(ba)
{
maxLoan = ml;
owesBank = 0.0;
rate = r;
}
非构造函数不能使用成员初始化列表语法,但派生类方法可以调用公有的基类方法。如下
void BrassPlus::ViewAcct()const
{
Brass::ViewAcct();
cout << "Maximum loan: $" << maxLoan << endl;
cout << "Owed to bank: $" << owesBank << endl;
cout << "Loan Rate: " << 100 * rate << "%\n";
}
void BrassPlus::Withdraw(double amt)
{
double bal = Balance();
if (amt < bal)
Brass::Withdraw(amt);
}
上面几句,由于BrassPlus类并没有重新定义Balance()这个函数,因此代码不必对Balance()使用作用域解析符。
为何需要虚析构函数?
如果析构函数不是虚的,则将只调用对应于指针类型的析构函数。如果析构函数是虚的,将调用相应对象类型的析构函数。因此,如果指针指向的是BrassPlus对象,将调用BrassPlus的析构函数,然后自动调用基类的析构函数。因此,使用虚析构函数可以确保正确的析构函数序列被调用。
静态联编与动态联编
将源代码中的函数调用解释为执行特定的函数代码块被称为函数名联编。在C语言中这很简单,因为每个函数名都对象一个不同的函数。在C++中,由于函数重载,编译器必须查看函数参数以及函数名才能确定使用哪个函数。然而,C/C++编译器可以在编译过程中完成这种联编。在编译过程中进行联编称为静态联编、早期联编。然而,虚函数使得在编译时不能确定使用哪一个函数,因为编译器不知道用户将选择哪种类型的对象。所以编译器必须生成能够在程序运行时选择正确的虚方法的代码,这就是动态联编,也称为晚期联编。
有关虚函数应注意的事项
1、构造函数不能是虚函数。因为派生类不继承基类的构造函数,所以将类构造函数声明为虚的没什么意义
2、通常应给基类提供一个虚析构函数,即使它并不需要析构函数。
3、友元不能是虚函数,因为友元不是类成员,而只有成员才能是虚函数。
4、如果派生类没有重新定义函数,将使用该函数的基类版本。如果派生类位于派生链中,则将使用最新的虚函数版本,例外的情况是基类版本是隐藏的。
5、重新定义将隐藏方法。重新定义继承的方法并不是重载。如果重新定义派生类中的函数,无论参数列表是否相同,该操作都将隐藏所有的同名基类方法。因此,如果重新定义继承的方法,应确保与原来的原型完全相同,但如果返回类型是基类引用或指针,则可以修改为指向派生类的引用或指针;如果基类声明被重载了,则应在派生类中重新定义所有的基类版本。