继承是基于一个类(称为基类)创建新类(称为派生类)的过程。派生类自动拥有基类的所有成员变量和函数,并可根据需要添加更多的成员函数和/或成员变量。
下面的代码演示了派生类B继承基类A。
#include
using namespace std;
class A
{
public:
A(): x(0),y(0) {}
A(int _x, int _y): x(_x), y(_y) {}
int getSum()
{
return x+y;
}
protected:
int x,y;
};
class B : public A
{
public:
B(): A(), z(0) {}
B(int _x, int _y, int _z): A(_x, _y), z(_z) {}
int getSum()
{
return x+y+z;
}
protected:
int z;
};
int main()
{
A a(1,2);
B b(1,2,3);
cout<<a.getSum()<<endl;
cout<<b.getSum()<<endl;
return 0;
}
这里面有几个需要注意的部分。
派生类不从基类继承构造函数。但为派生类定义构造函数时,可以(而且应该)包含对某个基类构造函数的调用,并将该调用放到构造函数定义的初始化区域。比如上面代码中,派生类B的构造函数就调用了基类A的构造函数。
不包含对任何基类构造函数的调用,在调用派生类构造函数时,会自动调用基类的默认(无参)构造函数。
需要注意的是,基类的私有成员函数不会被继承且私有变量也不能直接通过变量名访问,必须通过基类的公有方法取值和赋值函数进行访问。
但我们可以在变量或成员函数前使用限定符protected,对于派生类(或派生类的派生类)之外的其他任何类或函数,它等同于private来标记。但在派生类中,可通过名称直接访问这种变量或成员函数。
派生类继承的protected成员仍然是protected的。换言之,只要在基类中将成员标记为protected,在所有后辈类中(而非仅仅从基类直接派生的类),都可以通过成员的名称直接访问。
派生类继承基类的所有成员函数(和成员变量)。但是,如果派生类要以不同方式实现继承的成员函数,可以在派生类中重定义该函数。要重定义成员函数,必须在派生类定义中列出它的声明,即使该声明与基类中的声明完全相同。不想重定义从基类继承的成员函数,就不要列出。比如上面代码中的 getSum()
方法,就在派生类B中进行了重定义。
不要混淆在派生类中对函数的重定义(redefining)和对函数名的重载(overloading)。重定义时,派生类给出的新定义具有相同的参数数量和类型。另一方面,和基类的函数定义相比,如派生类的函数使用了数量不同的参数,或某个参数具有不同类型,那么派生类中实际会同时存在两个函数,这称为重载而不是重定义。
假设重定义了函数,使其在派生类中的定义有别于基类中的定义。这种情况下,并不是说基类中的定义再也不能由派生类的对象使用了。要为派生类的一个对象调用函数的基类版本,需要使用作用域解析操作符,并指定基类名称。下面举例说明:
假设有基类 Employee
和 派生类 HourlyEmployee
。两个类都定义了 printCheck()
函数。现在,假定每个类都有一个对象:
Employee janeE;
HourlyEmployee sallyH;
那么以下语句使用 Employee
类的 printCheck
定义:
janeE.printCheck();
而以下语句使用 HourlyEmployee
类的 printCheck
定义:
sally.printCheck();
在派生类对象 sallyH
上调用基类版本的 printCheck
,应使用以下语句:
sallyH.Employee::printCheck();
重载的赋值操作符和拷贝构造函数不会被继承,但它们可以(且通常必须)在派生类的重载赋值操作符及拷贝构造函数的定义中使用。
假定 Derived
是 Base
的派生类,在 Derived
类中,重载的赋值操作符的定义通常像下面这样开始:
Derived& Derived::operator = (const Derived& rightSide)
{
Base::operator =(rightSide)
定义主体的第一行调用 Base
类重载的赋值操作符,这就照顾到了继承的成员变量及其数据,接着,重载的赋值操作符的定义可开始设置在 Derived
类定义中新引入的成员变量。
在派生类中定义拷贝构造函数的情况相似。代码通常像下面这样开始:
Derived::Derived(const Derived& object)
: Base(object), <可能还有更多的初始化>
{
调用基类拷贝函数 Base(object)
,可为准备创建的 Derived
对象设置继承的成员变量。注意,由于 object
是 Derived
类型;因此,object
是传给 Base
类拷贝构造函数的合法实参。
当然,除非基类中已经有了一个能正确工作的赋值操作符和拷贝构造函数,否则这些都是无用功。也就是说,在基类定义中,必须包括拷贝构造函数和要么自动创建、要么重载的赋值操作符,它们在基类必须能正常工作。
调用派生类的析构函数时,她会自动调用基类析构函数,所以不必显式调用;它肯定是自动发生的。因此,在派生类的析构函数中,只需要 delete
销毁派生类新增的成员变量(以及它们指向的任何数据)。与此同时,基类析构函数会赋值为继承的成员变量调用 delete
。
假定类B派生自类A,类C派生自类B,那么一旦类C的某个对象离开作用域,首先会调用类C的析构函数,再调用类B的析构函数,最后调用类A的析构函数。注意析构函数的调用顺序刚好与构造函数相反。
多态性(Polymorphism) 是指为一个函数名关联多种含义的能力。具体地说,多态性指通过名为 “晚期绑定” 的特殊机制为函数名关联多个含义。多态性是面向对象编程的核心概念。 而虚函数是 C++ 提供晚期绑定的一种具体手段。将函数指定为 virtual
,相当于告诉编译器:“我不知道这个函数如何实现。等它在程序中使用时,再从对象实例中获取它的实现”。下面以代码为例进一步说明。
基类Sale的接口
// 头文件sale.h,它是Sale类的接口。Sale是代表简单销售的一个类
#ifndef SALE_H
#define SALE_H
#include
using namespace std;
namespace salesavitch
{
class Sale
{
public:
Sale();
Sale(double thePrice);
virtual double bill() const;
double savings(const Sale& other) const;
// 如果购买other代表的商品,而非购买调用对象代表的商品,返回能省多少钱
protected:
double price;
};
bool operator <(const Sale& first, const Sale& second);
// 比较两个销售,看哪个较大
} // salesavitch
#endif // SALE_H
基类Sale的实现
// 这是实现文件sale.cpp,它实现了Sale类
// Sale类的接口在头文件sale.h中
#include "sale.h"
namespace salesavitch
{
Sale::Sale() : price(0)
{}
Sale::Sale(double thePrice) : price(thePrice)
{}
double Sale::bill() const
{
return price;
}
double Sale::savings(const Sale& other) const
{
return ( bill() - other.bill() );
}
bool operator < (const Sale& first, const Sale& second)
{
return (first.bill() < second.bill());
}
} // salesavitch
派生类DiscountSale的接口
#ifndef DISCOUNTSALE_H
#define DISCOUNTSALE_H
#include "sale.h"
namespace salesavitch
{
class DiscountSale : public Sale
{
public:
DiscountSale();
DiscountSale(double thePrice, double theDiscount);
//Discount is expressed as a percent of the price.
virtual double bill() const;
protected:
double discount;
};
}//salesavitch
#endif // DISCOUNTSALE_H
派生类DiscountSale的实现
// 这是DiscountSale类的实现
#include "discountsale.h"
namespace salesavitch
{
DiscountSale::DiscountSale() : Sale(), discount(0)
{}
DiscountSale::DiscountSale(double thePrice, double theDiscount)
: Sale(thePrice), discount(theDiscount)
{}
double DiscountSale::bill() const
{
double fraction = discount/100;
return (1 - fraction)*price;
}
} // salesavitch
使用虚函数
// 该程序用于演示虚函数bill的使用
#include
#include "sale.h"
#include "discountsale.h"
using namespace std;
using namespace salesavitch;
int main()
{
Sale simple(10.00);// 一件价格为$10.00的商品
DiscountSale discount(11.00, 10); // 一件价格为$11.00,折扣为10%的商品
cout.setf(ios::fixed);
cout.setf(ios::showpoint);
cout.precision(2);
if (discount < simple)
{
cout << "Discounted item is cheaper.\n";
cout << "Savings is $" << simple.savings(discount) << endl;
}
else
cout << "Discounted item is not cheaper.\n";
return 0;
}
savings
函数的定义(即使它用于DiscountSale
类的对象)是在基类 Sale
的实现文件中给出。它在 DiscountSale
类出现之前就已经编译好。但在函数调用 d1.savings(d2)
中,调用 bill
函数的那一行知道自己应该使用 DiscountSale
类给出的 bill
函数定义。
要在C++中顺利使用虚函数,需掌握如下所示的技术细节。
virtual
。在派生类的函数声明中,则可以不添加virtual
。函数在基类中virtual
,在派生类自动 virtual
(但为了澄清,最好派生类中也将函数声明标记为virtual
,尽管这并非必须)。virtual
在函数声明中添加,不要在函数定义中添加。virtual
,否则不能获得虚函数,也不能虚函数的任何好处。需要注意的是,编译器和"运行时"环境要为虚函数做多得多的工作。所以,无谓地将成员函数标记为 virtual
会影响程序执行效率。
虚函数定义在派生类中发生改变时,我们说函数定义被重写。一些 C++ 书籍区分了重定义(redefine)和重写(override)。两者都是在派生类中更改函数定义。函数是虚函数,就称为重写。如果不是,就称为重定义。
假定定义类 A 和类 B,并定义类 A 和 类 B 的对象,那么并非肯定能在这些类型的对象之间赋值。例如,假定某个程序或程序单元包含以下类型声明:
class Pet
{
public:
virtual void print();
string name;
};
class Dog : public Pet // 狗屎宠物
{
public:
virtual void print();
string breed; // 代表狗的品种
];
Dog vDog;
Pet vPet;
以下赋值语句是允许的:
vDog.name = "Tiny";
vDog.breed = "Great Dane";
vPet = vDog;
但反过来将 vPet
赋值给 vDog
则不成立。虽然上述赋值是允许的,但赋给 vPet
的值会丢失其 breed
字段。这称为 切割问题(slicing problem)。例如,以下输出语句会报错:
cout << vPet.breed; // 非法:Pet类没有名为breed的成员
但 C++ 提供了一种方式,允许在将一个 Dog
视为 Pet
的同时不丢失品种名称。为此,需要使用指向动态对象实例的指针。假定添加以下声明:
Pet *pPet;
Dog *pDog;
使用指针和动态变量,就可将 Tiny
视为 Pet
而不丢失其品种名称。以下语句是允许的:
pDog = new Dog;
pDog->name = "Tiny";
pDog->breed = "Great Dane";
pPet = pDog;
此外,仍然能访问 pPet
所指向那个节点的 breed
字段。假定像下面这样定义虚函数 Dog:::print()
:
void Dog::print()
{
cout << "name: " << name << endl;
cout << "breed: " << breed << endl;
}
那么以下语句:
pPet->print();
会导致在屏幕上打印以下内容:
name: Tiny
breed: Great Dane
如果将 pPet->print();
替换成以下代码会报错:
cout << "name: " << pPet->name << endl;
cout << "breed: " << pPet->breed << endl;
这是由于对于以下表达式:
*pPet
它的类型由 pPet
的指针类型决定,其没有 breed
字段。但是,基类 Pet
将 print()
声明为 virtual
。所以,一旦编译器看到以下调用就会检查 Pet
和 Dog
的 virtual
表,判断 pPet
指向的是 Dog
类型的对象。
析构函数最好都是虚函数,例如以下代码,其中 SomeClass
是含有非虚析构函数的类:
SomeClass *p = new SomeClass;
...
delete p;
为 p
调用 delete
,会自动调用 SomeClass
类的析构函数。下面看看将析构函数标记为 virtual
后会发生什么。
假定 Derived
类是 Base
类的派生类,并假定 Base
类的析构函数标记为 virtual
。现在分析以下代码:
Base *pBase = new Derived;
...
delete pBase;
为 pBase
调用 delete
时,会调用一个析构函数。由于 Base
类中的析构函数标记为 virtual
,而且指向的对象时 Derived
类型,所以会调用 Derived
的析构函数(它进而调用Base
类的析构函数)。如果Base
类的析构函数没有标记为 virtual
,则只调用 Base
类的析构函数。
还要注意一点,将析构函数标记为 virtual
后,派生类的所有析构函数都自动成为 virtual
的(不管是否用virtual
标记)。同样,这种行为就好比所有析构函数具有相同的名称(即使事实上不同名)。