面向对象编程的主要目的之一是提供可重用的代码,即实现代码的模块化。C++提供了类(class)的方法将数据表示和类方法组合在一起。然而不同的产品可能有不同的需求,这意味着需要对原先的类进行修改,而C++提供了比修改代码更好的方法来扩展和修改类,它就是继承。
继承能够从已有的类派生出新的类,而这个派生类继承了原有类(基类)的特征,包括方法。这里以一个乒乓球俱乐部跟踪乒乓球会员为例,首先需要设计一个简单的TableTennisPlayer
类。
class TableTennisPlayer{
private:
string firstname;
string lastname;
bool hasTable;
public:
TableTennisPlayer(const string & fn = "none", const string & ln = "none", bool ht = false);
void Name() const;
bool HasTable() const { return hasTable; }
void ResetTable(bool v) {hasTable = v;}
};
TableTennisPlayer::TableTennisPlayer(const string & fn, const string & ln, bool ht) :
firstname(fn), lastname(ln), hasTable(ht){
}
void TableTennisPlayer::Name() const{
std::cout << lastname << ", " << firstname;
}
现在需要一个类来记录成员在比赛中的比分,所以可以将RatedPlayer
类从TableTennisPlayer
类派生而来,语法如下:
class RatedPlayer : public TableTennisPlayer{
...
};
这里的public
表示TableTennisPlayer
是一个公有基类,这被称为公有派生,它有以下特性:
在这个例子中,派生类需要一个变量rating
来存储比分,声明如下:
class RatedPlayer : public TableTennisPlayer{
private:
unsigned int rating;
public:
RatedPlayer (unsigned int r = 0, const string & fn = "none", const string & ln = "none", bool ht = false);
RatedPlayer (unsigned int r, const TableTennisPlayer & tp);
unsigned int Rating () const {return rating;} // add a method
void ResetRating (unsigned int r ) { rating = r; } // add a method
}
如上所示的两个RatedPlayer
所示,派生类的构造函数必须给新成员(如果有)和继承的成员提供数据。
RatedPlayer::RatedPlayer(unsigned int r, const string & fn, const string & ln, bool ht):TableTennisPlayer(fn, ln, ht)
{
rating = r;
}
或
RatedPlayer::RatedPlayer(unsigned int r, const string & fn, const string & ln, bool ht):TableTennisPlayer(fn, ln, ht), rating(r){}
TableTennisPlayer(fn, ln, ht)
,就将调用默认的构造函数。RatedPlayer rplayer1(1140, "Mallory", "Duck", true);
TableTennisPlayer & rt = rplayer;
TableTennisPlayer * pt = & rplayer;
rt.Name(); // invoke Name() with reference
pt->Name(); // invoke Name() with pointer
void Show(const TableTennisPlayer & rt) {
using std::cout;
cout << "Name: " <
RatedPlayer olaf1(1840, "Olaf", 'Loaf", true);
TableTenisPlayer winner;
winner = olaf1; // assign derived to base object
在C++中,protected
是一种访问控制修饰符,用于类的成员变量和成员函数,具有介于 public
和 private
之间的访问级别。protected
成员对于派生类是可见的,但对于外部类或对象不可见。
protected
成员在派生类中可访问,这意味着派生类可以继承和使用基类中的 protected
成员,就像它们是自己的成员一样。protected
成员是受限制的,不能直接访问它们。只有在派生类内部才能访问。protected
成员的使用有助于实现类的封装性,因为它们不会对外部类或对象公开。只有继承了基类的派生类才能访问这些成员。下面看一个例子来理解:
#include
class Base {
protected:
int protectedVar;
public:
Base(int val) : protectedVar(val) {}
};
class Derived : public Base {
public:
Derived(int val) : Base(val) {}
void printProtectedVar() {
std::cout << "Value of protectedVar in Derived: " << protectedVar << std::endl;
}
};
int main() {
Base baseObj(10);
Derived derivedObj(20);
// 在派生类内部可以访问基类的 protected 成员
derivedObj.printProtectedVar();
// 无法在外部访问基类的 protected 成员
// baseObj.protectedVar; // 编译错误
return 0;
}
有时候我们希望在派生类中调用基类的友元函数,这要怎么实现呢?下面来看一个例子,我们将创建一个基类 Shape
和一个派生类 Circle
,它们都有 <<
运算符重载函数。然后,我们将演示如何在派生类中调用基类的 <<
运算符重载函数。
class Shape {
private:
int id;
public:
Shape(int i) : id(i) {}
friend std::ostream& operator<<(std::ostream& os, const Shape& shape) {
os << "Shape ID: " << shape.id;
return os;
}
};
class Circle : public Shape {
private:
double radius;
public:
Circle(int i, double r) : Shape(i), radius(r) {}
friend std::ostream& operator<<(std::ostream& os, const Circle& circle) {
os << static_cast(circle); // 强制类型转换为基类
os << ", Radius: " << circle.radius;
return os;
}
};
int main() {
Circle circle(1, 5.0);
std::cout << circle << std::endl;
return 0;
}
在这个示例中,Shape
类和 Circle
类都定义了 <<
运算符重载函数作为友元函数,以便在输出对象时自定义输出格式。在 Circle
类的运算符重载函数中,我们使用强制类型转换 static_cast
来将 Circle
对象转换为基类 Shape
,以便调用基类的 <<
运算符重载函数。这允许我们在输出 Circle
对象的同时输出基类的信息。
有时我们希望同一个方法在派生类和基类中的行为是不同的,也就是说方法的行为取决于调用该方法的对象。这种行为称为多态,即一个方法具有多种形态。
下面来看一个例子,假设有一个基类Shape
,包含一个虚函数getArea()
,以及一个虚析构函数:
class Shape {
public:
virtual double getArea() const {
return 0.0;
}
virtual ~Shape() {
std::cout << "Base class destructor" << std::endl;
}
};
虚函数是C++中的一种特殊函数,它允许在派生类中重写基类中的同名函数,从而实现多态性。要将一个函数定义为虚函数,您需要在基类中使用virtual
关键字进行声明。这里的getArea()
函数也可以写成virtual double getArea() const = 0;
,表示这是一个纯虚函数。纯虚函数没有默认的实现,它要求派生类必须提供具体的实现。
Circle
和Rectangle
都是Shape
的派生类,它们重写了getArea()
函数,都具有计算面积的功能,并定义了各自的析构函数。
class Circle : public Shape {
private:
double radius;
public:
Circle(double r) : radius(r) {}
double getArea() const override {
return 3.141592 * radius * radius;
}
~Circle() {
std::cout << "Circle class destructor" << std::endl;
}
};
class Rectangle : public Shape {
private:
double width;
double height;
public:
Rectangle(double w, double h) : width(w), height(h) {}
double getArea() const override {
return width * height;
}
~Rectangle() {
std::cout << "Rectangle class destructor" << std::endl;
}
};
override
关键字用于明确指示正在重写基类中的虚函数,可提醒编译器进行检查是否存在虚函数,这个关键字可以省略,但推荐写上。在main
函数中,我们创建了一个Circle
对象和一个Rectangle
对象,并将它们的指针存储在Shape
指针中。通过这种方式,我们可以使用基类指针来调用不同类型对象的相同函数,实现了多态性。
int main() {
Shape* shape1 = new Circle(5.0);
Shape* shape2 = new Rectangle(4.0, 6.0);
// 使用多态性,调用不同类型对象的相同函数
std::cout << "Area of shape1: " << shape1->getArea() << std::endl;
std::cout << "Area of shape2: " << shape2->getArea() << std::endl;
// 释放内存,注意虚析构函数的作用
delete shape1;
delete shape2;
return 0;
}
程序的输出如下:
Area of shape1: 78.5398
Area of shape2: 24
Circle class destructor
Base class destructor
Rectangle class destructor
Base class destructor
因为基类Shape
的析构函数是虚函数,所以在使用基类指针删除派生类对象时,会先调用派生类析构函数,然后再调用基类的析构函数。