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

C++抽象类的继承与派生规则

2022-02-194.5k 阅读

C++抽象类的继承与派生规则

一、抽象类的定义

在C++中,抽象类是一种特殊的类,它不能被实例化,主要用于为其他类提供一个通用的基类接口。抽象类至少包含一个纯虚函数。纯虚函数是一种特殊的虚函数,在声明时被初始化为0,其语法形式如下:

class AbstractClass {
public:
    virtual void pureVirtualFunction() = 0;
};

在上述代码中,AbstractClass就是一个抽象类,因为它包含了纯虚函数pureVirtualFunction。纯虚函数没有函数体,它只是定义了函数的接口,具体的实现由派生类来完成。

二、继承抽象类

当一个类继承自抽象类时,它必须实现抽象类中的所有纯虚函数,否则这个派生类也会成为抽象类。下面通过一个简单的例子来演示:

class Shape {
public:
    virtual double area() = 0;
    virtual double perimeter() = 0;
};

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

class Rectangle : public Shape {
private:
    double length;
    double width;
public:
    Rectangle(double l, double w) : length(l), width(w) {}
    double area() override {
        return length * width;
    }
    double perimeter() override {
        return 2 * (length + width);
    }
};

在上述代码中,Shape是一个抽象类,包含两个纯虚函数areaperimeterCircleRectangle类继承自Shape,并且分别实现了areaperimeter函数,因此CircleRectangle不是抽象类,可以被实例化。

三、抽象类继承中的访问控制

  1. 公有继承 在公有继承的情况下,派生类会继承抽象类的接口,并且保持其访问权限。例如:
class BaseAbstract {
public:
    virtual void publicFunction() = 0;
protected:
    virtual void protectedFunction() = 0;
private:
    virtual void privateFunction() = 0;
};

class PublicDerived : public BaseAbstract {
public:
    void publicFunction() override {}
    void protectedFunction() override {}
};

在上述代码中,PublicDerived类公有继承自BaseAbstractpublicFunctionBaseAbstract中是公有的,在PublicDerived中依然是公有的。protectedFunctionBaseAbstract中是受保护的,在PublicDerived中也是受保护的。而privateFunction由于是私有的,PublicDerived类无法直接访问,也不能重写。

  1. 保护继承 当使用保护继承时,抽象类的公有成员和保护成员在派生类中都变为保护成员。例如:
class ProtectedDerived : protected BaseAbstract {
public:
    void publicFunction() override {}
    void protectedFunction() override {}
};

此时,PublicDerived类的对象无法访问publicFunction,因为它在ProtectedDerived类中变为保护成员。只有ProtectedDerived类及其派生类的成员函数可以访问publicFunctionprotectedFunction

  1. 私有继承 私有继承会使抽象类的公有成员和保护成员在派生类中变为私有成员。例如:
class PrivateDerived : private BaseAbstract {
public:
    void publicFunction() override {}
    void protectedFunction() override {}
};

PrivateDerived类中,publicFunctionprotectedFunction都变为私有成员,不仅PrivateDerived类的对象无法访问,而且PrivateDerived类的派生类也无法访问这些函数。

四、抽象类的多重继承与虚继承

  1. 多重继承 在C++中,一个类可以从多个抽象类继承。例如:
class Interface1 {
public:
    virtual void method1() = 0;
};

class Interface2 {
public:
    virtual void method2() = 0;
};

class MultipleDerived : public Interface1, public Interface2 {
public:
    void method1() override {}
    void method2() override {}
};

在上述代码中,MultipleDerived类从Interface1Interface2两个抽象类继承,并且实现了它们的纯虚函数。

  1. 虚继承 当存在多重继承的菱形继承结构时,可能会导致数据冗余和歧义问题。虚继承可以解决这个问题。例如:
class AbstractBase {
public:
    virtual void commonFunction() = 0;
};

class Derived1 : virtual public AbstractBase {
public:
    void commonFunction() override {}
};

class Derived2 : virtual public AbstractBase {
public:
    void commonFunction() override {}
};

class FinalDerived : public Derived1, public Derived2 {
public:
    void commonFunction() override {}
};

在上述代码中,Derived1Derived2虚继承自AbstractBase,这样在FinalDerived类中不会出现AbstractBase成员的重复拷贝,避免了数据冗余和歧义。

五、抽象类继承中的构造函数和析构函数

  1. 构造函数 抽象类可以有构造函数,但是抽象类的构造函数不会被用于创建抽象类的对象,因为抽象类不能被实例化。当派生类构造时,会首先调用抽象类的构造函数。例如:
class AbstractParent {
public:
    AbstractParent(int value) : data(value) {}
    virtual void abstractFunction() = 0;
protected:
    int data;
};

class ConcreteChild : public AbstractParent {
public:
    ConcreteChild(int value) : AbstractParent(value) {}
    void abstractFunction() override {}
};

在上述代码中,ConcreteChild类构造时会首先调用AbstractParent的构造函数,初始化data成员。

  1. 析构函数 抽象类应该有虚析构函数。如果抽象类的析构函数不是虚的,当通过基类指针删除派生类对象时,可能不会调用派生类的析构函数,导致内存泄漏。例如:
class AbstractWithDestructor {
public:
    virtual ~AbstractWithDestructor() = default;
    virtual void abstractFunction() = 0;
};

class DerivedWithDestructor : public AbstractWithDestructor {
public:
    ~DerivedWithDestructor() override {
        // 清理派生类特有的资源
    }
    void abstractFunction() override {}
};

在上述代码中,AbstractWithDestructor的析构函数是虚的,这样当通过AbstractWithDestructor指针删除DerivedWithDestructor对象时,会正确调用DerivedWithDestructor的析构函数。

六、抽象类继承的应用场景

  1. 接口定义 抽象类常用于定义接口,不同的派生类可以根据自身的需求实现这些接口。例如在图形绘制系统中,Shape抽象类定义了draw接口,CircleRectangle等派生类实现该接口来完成各自的绘制逻辑。
class Shape {
public:
    virtual void draw() = 0;
};

class Circle : public Shape {
public:
    void draw() override {
        // 绘制圆形的代码
    }
};

class Rectangle : public Shape {
public:
    void draw() override {
        // 绘制矩形的代码
    }
};
  1. 模板方法模式 抽象类可以实现部分通用的算法逻辑,将一些特定的步骤留给派生类去实现。例如:
class DataProcessor {
public:
    void processData() {
        loadData();
        transformData();
        saveData();
    }
protected:
    virtual void loadData() = 0;
    virtual void transformData() = 0;
    virtual void saveData() = 0;
};

class CSVProcessor : public DataProcessor {
protected:
    void loadData() override {
        // 从CSV文件加载数据的代码
    }
    void transformData() override {
        // 对CSV数据进行转换的代码
    }
    void saveData() override {
        // 将处理后的数据保存到CSV文件的代码
    }
};

在上述代码中,DataProcessor抽象类定义了processData的整体流程,而loadDatatransformDatasaveData由派生类CSVProcessor具体实现。

  1. 多态性的实现 通过抽象类的继承,可以实现运行时的多态性。例如:
#include <iostream>
#include <vector>

class Animal {
public:
    virtual void makeSound() = 0;
};

class Dog : public Animal {
public:
    void makeSound() override {
        std::cout << "Woof!" << std::endl;
    }
};

class Cat : public Animal {
public:
    void makeSound() override {
        std::cout << "Meow!" << std::endl;
    }
};

int main() {
    std::vector<Animal*> animals;
    animals.push_back(new Dog());
    animals.push_back(new Cat());

    for (Animal* animal : animals) {
        animal->makeSound();
    }

    for (Animal* animal : animals) {
        delete animal;
    }

    return 0;
}

在上述代码中,通过Animal抽象类的指针数组,实现了不同派生类对象的多态调用,根据对象的实际类型调用相应的makeSound函数。

七、抽象类继承与运行时类型识别(RTTI)

  1. dynamic_cast dynamic_cast运算符用于在运行时进行类型转换,特别是在涉及抽象类继承的多态层次结构中。例如:
class BaseAbstract {
public:
    virtual void abstractFunction() = 0;
};

class DerivedA : public BaseAbstract {
public:
    void abstractFunction() override {}
    void derivedAFunction() {}
};

class DerivedB : public BaseAbstract {
public:
    void abstractFunction() override {}
    void derivedBFunction() {}
};

int main() {
    BaseAbstract* basePtr = new DerivedA();
    DerivedA* derivedAPtr = dynamic_cast<DerivedA*>(basePtr);
    if (derivedAPtr) {
        derivedAPtr->derivedAFunction();
    }

    DerivedB* derivedBPtr = dynamic_cast<DerivedB*>(basePtr);
    if (!derivedBPtr) {
        std::cout << "Type conversion to DerivedB failed." << std::endl;
    }

    delete basePtr;
    return 0;
}

在上述代码中,dynamic_cast尝试将BaseAbstract指针转换为DerivedADerivedB指针。如果转换成功,dynamic_cast返回有效的指针,否则返回nullptr

  1. typeid typeid运算符用于获取对象的实际类型。在抽象类继承的情况下,它可以帮助我们确定对象在运行时的具体类型。例如:
class BaseAbstract {
public:
    virtual void abstractFunction() = 0;
};

class DerivedA : public BaseAbstract {
public:
    void abstractFunction() override {}
};

class DerivedB : public BaseAbstract {
public:
    void abstractFunction() override {}
};

int main() {
    BaseAbstract* basePtr1 = new DerivedA();
    BaseAbstract* basePtr2 = new DerivedB();

    if (typeid(*basePtr1) == typeid(DerivedA)) {
        std::cout << "basePtr1 points to a DerivedA object." << std::endl;
    }

    if (typeid(*basePtr2) == typeid(DerivedB)) {
        std::cout << "basePtr2 points to a DerivedB object." << std::endl;
    }

    delete basePtr1;
    delete basePtr2;
    return 0;
}

在上述代码中,typeid比较对象的实际类型,从而判断指针所指向的对象是哪种派生类类型。

八、抽象类继承中的注意事项

  1. 避免循环继承 在设计抽象类继承体系时,要避免循环继承。例如:
// 错误的循环继承示例
class A : public B {
public:
    virtual void aFunction() = 0;
};

class B : public A {
public:
    virtual void bFunction() = 0;
};

这种循环继承会导致编译错误,因为编译器无法确定类的大小和布局。

  1. 合理设计纯虚函数 纯虚函数的设计要合理,既要保证抽象类提供足够通用的接口,又要避免接口过于宽泛或过于具体。例如,在Shape抽象类中,areaperimeter函数是合理的纯虚函数,因为不同形状的计算方式不同,但都需要这些基本属性。

  2. 考虑抽象类的版本兼容性 当对抽象类进行修改时,要考虑派生类的兼容性。如果在抽象类中添加新的纯虚函数,所有派生类都需要实现该函数,可能会对现有代码造成较大影响。因此,在设计抽象类时要尽量考虑到未来的扩展。

  3. 注意内存管理 在涉及抽象类继承的动态内存分配中,要注意内存管理。如前面提到的,抽象类要有虚析构函数,以确保在通过基类指针删除派生类对象时,能够正确调用派生类的析构函数,避免内存泄漏。

总之,C++抽象类的继承与派生规则是C++面向对象编程的重要组成部分,合理运用这些规则可以设计出灵活、可扩展且健壮的软件系统。通过深入理解抽象类的定义、继承方式、访问控制、构造析构函数以及应用场景等方面,开发者能够更好地利用C++的特性来解决实际问题。在实际编程中,要根据具体的需求和系统架构,精心设计抽象类继承体系,充分发挥C++面向对象编程的优势。同时,注意避免常见的错误和陷阱,如循环继承、内存泄漏等问题,以保证代码的质量和可靠性。