C++总结(4):继承和多态

面向对象编程的主要目的之一是提供可重用的代码,即实现代码的模块化。C++提供了类(class)的方法将数据表示和类方法组合在一起。然而不同的产品可能有不同的需求,这意味着需要对原先的类进行修改,而C++提供了比修改代码更好的方法来扩展和修改类,它就是继承。

文章目录

  • 1 继承
    • 1.1 派生类
    • 1.2 派生类的特性和使用
    • 1.3 protected关键字
    • 1.4 友元的继承
  • 2 多态

1 继承

1.1 派生类

继承能够从已有的类派生出新的类,而这个派生类继承了原有类(基类)的特征,包括方法。这里以一个乒乓球俱乐部跟踪乒乓球会员为例,首先需要设计一个简单的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是一个公有基类,这被称为公有派生,它有以下特性:

  • 派生类对象存储了基类的数据成员,基类的公有成员将成为派生类的公有成员
  • 基类的私有部分也将成为派生类的一部分,但只能通过基类的public或protected的方法访问
  • 派生类对象可以使用基类的方法

在这个例子中,派生类需要一个变量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),就将调用默认的构造函数。

1.2 派生类的特性和使用

  • 基类对象应当在程序进入派生类构造函数之前被创建。
  • 创建派生类对象时,程序首先调用基类构造函数,然后再调用派生类构造函数。
  • 派生类对象销毁时,程序将首先调用派生类析构函数,然后再调用基类析构函数。
  • 派生类可以使用基类的公有方法
  • 基类指针可以在不进行显式类型转换的情况下指向派生类对象;基类引用可以在不进行显式类型转换的情况下引用派生类对象。但这样只能调用基类的方法。
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

1.3 protected关键字

在C++中,protected 是一种访问控制修饰符,用于类的成员变量和成员函数,具有介于 publicprivate 之间的访问级别。protected 成员对于派生类是可见的,但对于外部类或对象不可见。

  1. 继承和派生类访问protected 成员在派生类中可访问,这意味着派生类可以继承和使用基类中的 protected 成员,就像它们是自己的成员一样。
  2. 外部访问受限:对于外部类或对象,protected 成员是受限制的,不能直接访问它们。只有在派生类内部才能访问。
  3. 类的封装性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;
}

1.4 友元的继承

有时候我们希望在派生类中调用基类的友元函数,这要怎么实现呢?下面来看一个例子,我们将创建一个基类 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 对象的同时输出基类的信息。

2 多态

有时我们希望同一个方法在派生类和基类中的行为是不同的,也就是说方法的行为取决于调用该方法的对象。这种行为称为多态,即一个方法具有多种形态。

下面来看一个例子,假设有一个基类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;,表示这是一个纯虚函数。纯虚函数没有默认的实现,它要求派生类必须提供具体的实现。

CircleRectangle都是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的析构函数是虚函数,所以在使用基类指针删除派生类对象时,会先调用派生类析构函数,然后再调用基类的析构函数

你可能感兴趣的:(C++,c++)