MK
摩柯社区 - 一个极简的技术知识社区
AI 面试

C++类抽象类的派生类实现

2024-02-055.5k 阅读

C++类抽象类的派生类实现

1. 抽象类基础回顾

在C++ 中,抽象类是一种特殊的类,它为一组相关的类提供了一个通用的接口和基本框架。抽象类至少包含一个纯虚函数。纯虚函数是在声明时被初始化为 = 0 的虚函数,例如:

class Shape {
public:
    virtual double area() const = 0; // 纯虚函数
    virtual double perimeter() const = 0; // 纯虚函数
};

这里的 Shape 类就是一个抽象类,因为它包含了纯虚函数 areaperimeter。抽象类不能被实例化,即不能直接创建抽象类的对象。它的存在主要是为了被其他类继承,为派生类提供一个公共的接口规范。

2. 派生类的概念及与抽象类的关系

当一个类从抽象类继承时,这个类就成为了抽象类的派生类。派生类继承了抽象类的成员(包括数据成员和函数成员),但需要根据自身的特性来实现抽象类中的纯虚函数,否则该派生类也会成为抽象类。

例如,我们有一个 Circle 类从 Shape 抽象类派生:

class Circle : public Shape {
private:
    double radius;
public:
    Circle(double r) : radius(r) {}
    double area() const override {
        return 3.14159 * radius * radius;
    }
    double perimeter() const override {
        return 2 * 3.14159 * radius;
    }
};

在这个例子中,Circle 类通过 public 继承方式从 Shape 类派生。它实现了 Shape 类中的纯虚函数 areaperimeter,因此 Circle 类不再是抽象类,可以创建 Circle 类的对象。

3. 派生类实现抽象类函数的规则

3.1 函数签名必须一致

派生类实现抽象类的纯虚函数时,函数的签名(包括函数名、参数列表和返回类型)必须与抽象类中纯虚函数的签名完全一致。例如在上述 Circle 类中,areaperimeter 函数的签名与 Shape 类中对应的纯虚函数签名一致。

3.2 访问控制

派生类中实现的函数访问控制级别不能比抽象类中纯虚函数的访问控制级别更严格。在抽象类中,如果纯虚函数是 public 的,那么在派生类中实现的函数也必须是 public 的。如果在抽象类中纯虚函数是 protected 的,派生类中实现的函数访问控制级别可以是 protected 或者 public

例如:

class Base {
protected:
    virtual void protectedFunction() = 0;
};

class Derived : public Base {
public:
    void protectedFunction() override {
        // 实现代码
    }
};

这里 Base 类中的 protectedFunctionprotected 的,Derived 类中实现时可以将其设置为 public,这是符合规则的。

3.3 重写说明符 override

在C++ 11 及以后的标准中,强烈建议在派生类实现重写基类虚函数(包括抽象类中的纯虚函数)时使用 override 关键字。这样做有两个好处:一是提高代码的可读性,让阅读代码的人清楚知道这个函数是重写基类的函数;二是编译器可以进行更严格的检查,如果函数签名与基类中虚函数不一致,编译器会报错。

例如,如果我们在 Circle 类的 area 函数中误写了函数签名:

class Circle : public Shape {
private:
    double radius;
public:
    Circle(double r) : radius(r) {}
    double area(int wrongParam) const override { // 错误的签名,编译器会报错
        return 3.14159 * radius * radius;
    }
    double perimeter() const override {
        return 2 * 3.14159 * radius;
    }
};

编译器会因为 area 函数签名与 Shape 类中 area 纯虚函数不一致而报错,这样可以避免一些不易察觉的逻辑错误。

4. 多重继承下的派生类与抽象类

在C++ 中,一个类可以从多个基类继承,这种情况被称为多重继承。当涉及到抽象类时,多重继承会带来一些复杂的情况。

例如,假设有两个抽象类 DrawableSelectable

class Drawable {
public:
    virtual void draw() = 0;
};

class Selectable {
public:
    virtual void select() = 0;
};

现在有一个 Button 类需要从这两个抽象类继承:

class Button : public Drawable, public Selectable {
private:
    std::string label;
public:
    Button(const std::string& lbl) : label(lbl) {}
    void draw() override {
        std::cout << "Drawing button with label: " << label << std::endl;
    }
    void select() override {
        std::cout << "Selecting button with label: " << label << std::endl;
    }
};

在这个例子中,Button 类从 DrawableSelectable 两个抽象类继承,并实现了它们各自的纯虚函数。多重继承使得 Button 类同时具备了绘制和选择的功能。

然而,多重继承也可能带来一些问题,比如菱形继承问题。假设 DrawableSelectable 都从一个共同的基类 Component 继承:

class Component {
public:
    int id;
};

class Drawable : public Component {
public:
    virtual void draw() = 0;
};

class Selectable : public Component {
public:
    virtual void select() = 0;
};

class Button : public Drawable, public Selectable {
private:
    std::string label;
public:
    Button(const std::string& lbl) : label(lbl) {}
    void draw() override {
        std::cout << "Drawing button with label: " << label << std::endl;
    }
    void select() override {
        std::cout << "Selecting button with label: " << label << std::endl;
    }
};

在这种情况下,Button 类会继承两份 Component 类的成员,这可能会导致数据冗余和命名冲突等问题。为了解决菱形继承问题,可以使用虚继承:

class Component {
public:
    int id;
};

class Drawable : virtual public Component {
public:
    virtual void draw() = 0;
};

class Selectable : virtual public Component {
public:
    virtual void select() = 0;
};

class Button : public Drawable, public Selectable {
private:
    std::string label;
public:
    Button(const std::string& lbl) : label(lbl) {}
    void draw() override {
        std::cout << "Drawing button with label: " << label << std::endl;
    }
    void select() override {
        std::cout << "Selecting button with label: " << label << std::endl;
    }
};

通过 virtual 关键字,Button 类只会继承一份 Component 类的成员,避免了数据冗余和命名冲突问题。

5. 抽象类派生类的多态性

多态性是C++ 面向对象编程的重要特性之一。当使用抽象类的派生类时,多态性可以通过虚函数和指针或引用来实现。

例如,我们有一个函数 printShapeInfo,它接受一个 Shape 类的指针,并调用 areaperimeter 函数:

void printShapeInfo(const Shape* shape) {
    std::cout << "Area: " << shape->area() << ", Perimeter: " << shape->perimeter() << std::endl;
}

我们可以这样使用这个函数:

int main() {
    Circle circle(5.0);
    printShapeInfo(&circle);

    return 0;
}

printShapeInfo 函数中,虽然参数类型是 Shape*,但实际调用的是 Circle 类中重写的 areaperimeter 函数,这就是多态性的体现。编译器会在运行时根据指针实际指向的对象类型来决定调用哪个类的虚函数。

6. 抽象类派生类的内存管理

在涉及抽象类的派生类时,内存管理需要特别注意。如果使用 new 来动态分配派生类对象的内存,并通过抽象类指针来操作,在释放内存时需要确保调用正确的析构函数。

例如:

class Shape {
public:
    virtual double area() const = 0;
    virtual double perimeter() const = 0;
    virtual ~Shape() = default; // 建议将抽象类析构函数声明为虚函数
};

class Circle : public Shape {
private:
    double radius;
public:
    Circle(double r) : radius(r) {}
    double area() const override {
        return 3.14159 * radius * radius;
    }
    double perimeter() const override {
        return 2 * 3.14159 * radius;
    }
    ~Circle() {
        std::cout << "Circle destructor called" << std::endl;
    }
};

如果在 main 函数中这样操作:

int main() {
    Shape* shape = new Circle(5.0);
    delete shape;

    return 0;
}

如果 Shape 类的析构函数不是虚函数,那么在 delete shape 时,只会调用 Shape 类的析构函数,而不会调用 Circle 类的析构函数,这可能会导致内存泄漏。因此,为了确保在通过抽象类指针释放派生类对象时能正确调用派生类的析构函数,应将抽象类的析构函数声明为虚函数。

7. 模板与抽象类派生类

模板是C++ 中强大的泛型编程工具,它可以与抽象类及其派生类结合使用,带来更多的灵活性和复用性。

例如,我们可以定义一个模板类 Container,它可以容纳不同类型的从抽象类 Shape 派生的对象:

#include <vector>

template<typename T>
class Container {
private:
    std::vector<T*> items;
public:
    void addItem(T* item) {
        items.push_back(item);
    }
    void printAllAreas() {
        for (const auto& item : items) {
            std::cout << "Area: " << item->area() << std::endl;
        }
    }
};

然后可以这样使用:

int main() {
    Container<Shape> container;
    Circle circle(5.0);
    // 假设还有一个Rectangle类从Shape派生
    // Rectangle rectangle(4.0, 6.0);
    container.addItem(&circle);
    // container.addItem(&rectangle);
    container.printAllAreas();

    return 0;
}

在这个例子中,Container 模板类可以容纳任何从 Shape 类派生的对象,并通过调用 area 函数来打印它们的面积。模板与抽象类派生类的结合,使得代码可以在不同类型的派生类对象上实现通用的操作,提高了代码的复用性。

8. 抽象类派生类在实际项目中的应用

在实际项目开发中,抽象类及其派生类有广泛的应用场景。例如在图形用户界面(GUI)开发中,可能会有一个抽象类 Widget,它定义了一些通用的属性和方法,如绘制、事件处理等。然后有各种具体的派生类,如 ButtonTextBoxLabel 等,它们根据自身特点实现 Widget 抽象类中的纯虚函数。

又如在游戏开发中,可能会有一个抽象类 GameObject,包含位置、速度等基本属性和一些通用的行为方法,如更新、渲染等。不同类型的游戏对象,如 PlayerEnemyBullet 等从 GameObject 派生,并实现各自特定的行为。

再比如在数据库访问层,可能有一个抽象类 DatabaseAccess,定义了连接数据库、执行查询等抽象方法。然后不同的数据库类型,如 MySQLAccessOracleAccessSQLiteAccess 等从 DatabaseAccess 派生,根据各自数据库的特点实现这些抽象方法。

9. 总结派生类实现抽象类的要点

  • 纯虚函数实现:派生类必须实现抽象类中的纯虚函数,否则该派生类也会成为抽象类。实现时函数签名要与抽象类中纯虚函数一致,且访问控制级别不能更严格。
  • 多态性运用:通过抽象类指针或引用,可以实现运行时的多态性,根据实际对象类型调用相应派生类的虚函数。
  • 内存管理:将抽象类的析构函数声明为虚函数,以确保在通过抽象类指针释放派生类对象时能正确调用派生类的析构函数,避免内存泄漏。
  • 多重继承注意事项:在多重继承情况下,要注意菱形继承问题,可以使用虚继承来解决。
  • 模板结合:模板与抽象类派生类结合,可以实现更通用的代码,提高复用性。

通过深入理解和掌握这些要点,开发者能够在C++ 编程中更好地利用抽象类及其派生类,构建出灵活、可维护且高效的软件系统。无论是小型项目还是大型企业级应用,合理运用抽象类派生类的特性都能为项目的架构设计和功能实现带来很大的优势。