面向对象程序设计——Visual C++ 第12章 虚函数和多态性

思考题

1、C++中的多态性有几种表现形式?它们分别如何实现?

C++中的多态性有两种表现形式:静态多态性和动态多态性
静态多态性是通过函数重载和运算符重载实现的,它在编译时就确定了函数或运算符的具体实现。
动态多态性是通过虚函数和继承实现的,它在运行时根据对象的实际类型确定函数的具体实现。在基类中声明虚函数,在派生类中重写虚函数,通过基类指针或引用调用虚函数时,会根据对象的实际类型调用相应的函数实现。

2、引入虚函数的目的如何?它是如何实现动态多态的?

引入虚函数的目的是为了实现动态多态性,即在运行时根据对象的实际类型确定函数的具体实现。
通过在基类中声明虚函数,在派生类中重写虚函数,可以实现动态绑定,即在运行时根据对象的实际类型调用相应的函数实现。当通过基类指针或引用调用虚函数时,会根据对象的实际类型调用相应的函数实现,从而实现动态多态性。

3、从运行效果上看,虚函数和普通的成员函数各有什么特点?在什么情况下必须使用虚函数?

虚函数和普通成员函数的主要区别在于多态性。虚函数可以实现运行时多态性,即在运行时根据对象的实际类型来调用相应的函数,而普通成员函数则只能实现编译时多态性,即在编译时就确定调用哪个函数。
必须使用虚函数的情况包括:

  1. 当需要在基类中定义一个函数,但是该函数的具体实现需要在派生类中进行,此时可以将该函数声明为虚函数。
  2. 当需要在派生类中重写基类中的函数时,该函数也必须是虚函数。
  3. 当需要将派生类的对象赋值给基类指针或引用时,如果基类中的函数是虚函数,则可以实现运行时多态性,调用派生类中的函数。如果基类中的函数不是虚函数,则只能调用基类中的函数。

4、虚析构函数有什么作用?在什么情况需要定义虚析构函数?

虚析构函数的作用是确保在删除一个指向派生类对象的基类指针时,能够正确地调用派生类的析构函数,从而释放派生类对象的资源。如果基类的析构函数不是虚函数,那么在删除基类指针时,只会调用基类的析构函数,而不会调用派生类的析构函数,从而导致派生类对象的资源无法释放,造成内存泄漏。
需要定义虚析构函数的情况包括:当一个类中含有虚函数时,通常也需要将其析构函数声明为虚函数,以确保在删除指向派生类对象的基类指针时能够正确地调用派生类的析构函数。此外,如果一个类中含有纯虚函数,那么该类必须定义虚析构函数,以便派生类能够正确地实现析构函数。

5、什么是抽象类?如何定义抽象类?

抽象类是一种不能被实例化的类,它只能作为其他类的基类使用。抽象类中包含至少一个纯虚函数,即没有实现的函数,只有函数声明,派生类必须实现这些函数才能被实例化。抽象类的定义方式如下:

class AbstractClass {
public:
    virtual void pureVirtualFunction() = 0; // 纯虚函数
    virtual void virtualFunction() { // 虚函数
        // 函数实现
    }
    void normalFunction() { // 普通函数
        // 函数实现
    }
};

在抽象类中,纯虚函数必须以“= 0”结尾,而且不能有函数体。抽象类不能被实例化,只能作为其他类的基类使用。派生类必须实现抽象类中的纯虚函数才能被实例化。抽象类中也可以包含虚函数和普通函数,它们可以有函数体,也可以被派生类继承和重写。

习题

1、编写一个程序计算三角形、正方形和圆形的面积。

分析:依题意,可以抽象出一个基类CBase,在其中说明一个虚函数,用来求面积,并利用单接口、多实现版本设计各个图形求面积的方法。

#include <iostream>
using namespace std;

class CBase {
public:
    virtual double GetArea() = 0; // 声明一个纯虚函数,用来求面积
};

class CTriangle : public CBase {
public:
    CTriangle(double b, double h) : base(b), height(h) {}
    virtual double GetArea() { return 0.5 * base * height; }
private:
    double base;
    double height;
};

class CSquare : public CBase {
public:
    CSquare(double s) : side(s) {}
    virtual double GetArea() { return side * side; }
private:
    double side;
};

class CCircle : public CBase {
public:
    CCircle(double r) : radius(r) {}
    virtual double GetArea() { return 3.14159 * radius * radius; }
private:
    double radius;
};

int main() {
    CBase* shapes[3]; // 一个基类指针数组,存储不同的派生类对象
    shapes[0] = new CTriangle(3.0, 4.0);
    shapes[1] = new CSquare(5.0);
    shapes[2] = new CCircle(2.5);

    for (int i = 0; i < 3; i++) {
        cout << "Area of shape " << i+1 << ": " << shapes[i]->GetArea() << endl;
        delete shapes[i]; // 删除创建的对象
    }

    return 0;
}

输出结果:

Area of shape 1: 6
Area of shape 2: 25
Area of shape 3: 19.6349

可以看到,程序正确地计算出了三角形、正方形和圆形的面积。使用基类和虚函数实现了多态性,使得不同的派生类对象能够调用相同的函数名,实现了统一的接口。

2、编写一个程序计算正方体、球体和圆柱体的表面积和体积。

分析:依题意,抽象出一个公共基类CContainer为抽象类,在其中定义求表面积和体积的纯虚函数(该抽象类本身试没有表面积和体积可言的)。抽象类中定义一个公共的数据成员radius,此数据可作为球体的半径、正方体的边长、圆柱体底面圆半径。由此抽象类派生出要描述的三个类,在这三个类中都具有求表面积和体积的重定义版本。

#include <iostream>
#include <cmath>
using namespace std;

class CContainer { // 容器基类
public:
    virtual double GetSurfaceArea() = 0; // 获取表面积的纯虚函数
    virtual double GetVolume() = 0; // 获取体积的纯虚函数
    double radius; // 公共数据成员
};

class CCube : public CContainer { // 正方体类
public:
    double GetSurfaceArea() { return 6 * pow(radius, 2); } // 重定义获取表面积的函数
    double GetVolume() { return pow(radius, 3); } // 重定义获取体积的函数
};

class CSphere : public CContainer { // 球体类
public:
    double GetSurfaceArea() { return 4 * M_PI * pow(radius, 2); } // 重定义获取表面积的函数
    double GetVolume() { return 4.0 / 3 * M_PI * pow(radius, 3); } // 重定义获取体积的函数
};

class CCylinder : public CContainer { // 圆柱体类
public:
    double height; // 圆柱体高度
    double GetSurfaceArea() { return 2 * M_PI * radius * (radius + height); } // 重定义获取表面积的函数
    double GetVolume() { return M_PI * pow(radius, 2) * height; } // 重定义获取体积的函数
};

int main() {
    CCube cube;
    cube.radius = 5;
    cout << "正方体的表面积是:" << cube.GetSurfaceArea() << endl;
    cout << "正方体的体积是:" << cube.GetVolume() << endl;

    CSphere sphere;
    sphere.radius = 5;
    cout << "球体的表面积是:" << sphere.GetSurfaceArea() << endl;
    cout << "球体的体积是:" << sphere.GetVolume() << endl;

    CCylinder cylinder;
    cylinder.radius = 5;
    cylinder.height = 10;
    cout << "圆柱体的表面积是:" << cylinder.GetSurfaceArea() << endl;
    cout << "圆柱体的体积是:" << cylinder.GetVolume() << endl;

    return 0;
}

输出结果:

正方体的表面积是:150
正方体的体积是:125
球体的表面积是:314.159
球体的体积是:523.599
圆柱体的表面积是:471.239
圆柱体的体积是:785.398

3、编写一个程序,先设计一个整数链表CList类,然后从此链表派生出一个整数集合类CSet,在集合类中增加一个元素个数的数据项。集合类的插入操作与链表相似,只是不插入重复元素,并且插入后,元素个数的数据成员需增值。集合类的删除操作是在链表删除操作的基础上对元素个数减1操作。而查找和显示操作是相同的。

#include <iostream>
using namespace std;

class CList {
public:
    int value;
    CList* next;

    CList() {
        next = NULL;
    }

    virtual void insert(int val) {
        CList* p = this;
        while (p->next != NULL) {
            if (p->next->value == val) {
                return; // 如果插入的元素已经存在于链表中,则不插入
            }
            p = p->next;
        }
        CList* node = new CList();
        node->value = val;
        p->next = node;
    }

    virtual void remove(int val) {
        CList* p = this;
        while (p->next != NULL) {
            if (p->next->value == val) {
                CList* node = p->next;
                p->next = p->next->next;
                delete node;
                return;
            }
            p = p->next;
        }
    }

    virtual bool find(int val) {
        CList* p = this;
        while (p->next != NULL) {
            if (p->next->value == val) {
                return true;
            }
            p = p->next;
        }
        return false;
    }

    virtual void display() {
        CList* p = this->next;
        while (p != NULL) {
            cout << p->value << " ";
            p = p->next;
        }
        cout << endl;
    }
};

class CSet : public CList {
public:
    int count;

    CSet() {
        count = 0;
    }

    void insert(int val) {
        if (!find(val)) {
            CList::insert(val);
            count++;
        }
    }

    void remove(int val) {
        if (find(val)) {
            CList::remove(val);
            count--;
        }
    }

    void display() {
        CList::display();
        cout << "The count of elements is " << count << endl;
    }
};

int main() {
    CSet set;
    set.insert(1);
    set.insert(2);
    set.insert(3);
    set.insert(2);
    set.display(); // 1 2 3; The count of elements is 3

    set.remove(2);
    set.display(); // 1 3; The count of elements is 2

    return 0;
}

说明:定义一个CList类,用于表示链表,有插入、删除、查找、显示等操作。定义一个CSet类,继承自CList类,表示集合,增加了元素个数的计数。在CSet类中重定义了插入和删除操作,对于插入操作,如果元素已经存在于集合中,则不插入;对于删除操作,如果元素不存在于集合中,则不进行操作。在CSet类中重定义了显示操作,将元素列表输出后,再输出元素的个数。最后在主函数中创建一个CSet对象,测试各个操作的正确性。

4、编写一个程序实现图书和杂志销售管理。当输入一系列图书和杂志销售记录后,将销售记录良好(图书每月售500本以上,杂志每月2500本以上)的图书和杂志名称显示出来。

分析:依题意,设计一个基类CBase为抽象类,其中包括GetTitle()和PrintTitle()两个成员函数,另有一个纯虚函数IsGood()。由该类派生CBook和CJournal两个类,分别实现纯虚函数IsGood(),对于前者版本,如果每月图书销售量超过500,则返回true,对于后者版本,如果每月杂志销售量超过2500,则返回true。

#include <iostream>
#include <string>
#include <vector>

using namespace std;

// 抽象基类
class CBase {
public:
    virtual string GetTitle() const = 0;
    virtual void PrintTitle() const = 0;
    virtual bool IsGood() const = 0;
};

// 图书类
class CBook : public CBase {
public:
    CBook(const string& title, int sales) : m_title(title), m_sales(sales) {}
    string GetTitle() const override { return m_title; }
    void PrintTitle() const override { cout << m_title << " (Book)" << endl; }
    bool IsGood() const override { return m_sales >= 500; }
private:
    string m_title;
    int m_sales;
};

// 杂志类
class CJournal : public CBase {
public:
    CJournal(const string& title, int sales) : m_title(title), m_sales(sales) {}
    string GetTitle() const override { return m_title; }
    void PrintTitle() const override { cout << m_title << " (Journal)" << endl; }
    bool IsGood() const override { return m_sales >= 2500; }
private:
    string m_title;
    int m_sales;
};

// 程序主函数
int main()
{
    // 创建销售记录
    vector<CBase*> records;
    records.push_back(new CBook("C++ Primer", 1000));
    records.push_back(new CBook("Effective C++", 700));
    records.push_back(new CJournal("Nature", 3000));
    records.push_back(new CJournal("Science", 2000));
    records.push_back(new CJournal("National Geographic", 4000));

    // 输出销售良好的图书和杂志
    cout << "Good books:" << endl;
    for (auto p : records) {
        if (dynamic_cast<CBook*>(p) && p->IsGood())
            p->PrintTitle();
    }

    cout << "Good journals:" << endl;
    for (auto p : records) {
        if (dynamic_cast<CJournal*>(p) && p->IsGood())
            p->PrintTitle();
    }

    // 释放资源
    for (auto p : records)
        delete p;
    return 0;
}

这里用了一个动态转型的技巧,即使用dynamic_cast将指针转换为CBook或CJournal类型,如果转换成功并且该销售记录的销售量达到了要求,则输出其标题。