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

C++类单继承的特性分析

2021-08-241.3k 阅读

C++类单继承的基本概念

在C++编程中,继承是面向对象编程的重要特性之一,它允许一个类从另一个类获取属性和行为。单继承意味着一个派生类(子类)只能有一个直接基类(父类)。这种设计使得代码结构更加清晰和易于管理,避免了多重继承可能带来的复杂性和歧义性。

例如,我们定义一个基类 Animal,它有一些基本的属性和方法:

class Animal {
public:
    void eat() {
        std::cout << "Animal is eating." << std::endl;
    }
    void sleep() {
        std::cout << "Animal is sleeping." << std::endl;
    }
};

然后,我们可以定义一个派生类 Dog 继承自 Animal

class Dog : public Animal {
public:
    void bark() {
        std::cout << "Dog is barking." << std::endl;
    }
};

在上述代码中,Dog 类继承了 Animal 类的 eatsleep 方法,同时它还拥有自己特有的 bark 方法。这就是单继承的基本体现,Dog 类只有一个直接的父类 Animal

单继承的访问控制

公有继承(public inheritance)

当使用公有继承时,基类的公有成员在派生类中仍然是公有的,基类的保护成员在派生类中仍然是保护的,而基类的私有成员在派生类中是不可访问的。

class Base {
public:
    int publicData;
protected:
    int protectedData;
private:
    int privateData;
};

class Derived : public Base {
public:
    void accessMembers() {
        publicData = 10; // 合法,publicData 在派生类中仍然是公有成员
        protectedData = 20; // 合法,protectedData 在派生类中是保护成员
        // privateData = 30; // 非法,privateData 在派生类中不可访问
    }
};

在这个例子中,Derived 类通过公有继承从 Base 类获取成员。publicData 可以在 Derived 类的成员函数中直接访问,因为它在派生类中仍然是公有的。protectedData 也可以在 Derived 类的成员函数中访问,因为它在派生类中是保护的。而 privateData 则无法在 Derived 类的成员函数中访问。

保护继承(protected inheritance)

在保护继承中,基类的公有成员和保护成员在派生类中都变成保护成员,基类的私有成员在派生类中仍然不可访问。

class Base {
public:
    int publicData;
protected:
    int protectedData;
private:
    int privateData;
};

class Derived : protected Base {
public:
    void accessMembers() {
        publicData = 10; // 合法,publicData 在派生类中变成保护成员,可在成员函数中访问
        protectedData = 20; // 合法,protectedData 在派生类中仍然是保护成员
        // privateData = 30; // 非法,privateData 在派生类中不可访问
    }
};

class FurtherDerived : public Derived {
public:
    void accessMembers() {
        publicData = 10; // 合法,publicData 在 FurtherDerived 中是保护成员,可在成员函数中访问
        protectedData = 20; // 合法,protectedData 在 FurtherDerived 中是保护成员
    }
};

这里,Derived 类通过保护继承从 Base 类获取成员。publicDataDerived 类中变成了保护成员,所以在 Derived 类的成员函数中可以访问。FurtherDerived 类又从 Derived 类公有继承,由于 Derived 类的成员在 FurtherDerived 类中的访问权限不变(因为是公有继承),所以 FurtherDerived 类的成员函数也可以访问 publicDataprotectedData

私有继承(private inheritance)

私有继承时,基类的公有成员和保护成员在派生类中都变成私有成员,基类的私有成员在派生类中同样不可访问。

class Base {
public:
    int publicData;
protected:
    int protectedData;
private:
    int privateData;
};

class Derived : private Base {
public:
    void accessMembers() {
        publicData = 10; // 合法,publicData 在派生类中变成私有成员,可在成员函数中访问
        protectedData = 20; // 合法,protectedData 在派生类中变成私有成员,可在成员函数中访问
        // privateData = 30; // 非法,privateData 在派生类中不可访问
    }
};

class FurtherDerived : public Derived {
public:
    void accessMembers() {
        // publicData = 10; // 非法,publicData 在 FurtherDerived 中不可访问,因为在 Derived 中是私有成员
        // protectedData = 20; // 非法,protectedData 在 FurtherDerived 中不可访问,因为在 Derived 中是私有成员
    }
};

在这个例子中,Derived 类通过私有继承从 Base 类获取成员。publicDataprotectedDataDerived 类中都变成了私有成员,所以在 Derived 类的成员函数中可以访问。但是,FurtherDerived 类从 Derived 类公有继承时,由于 publicDataprotectedDataDerived 类中是私有成员,所以 FurtherDerived 类的成员函数无法访问它们。

单继承中的构造函数和析构函数

基类构造函数的调用

当创建一个派生类对象时,首先会调用基类的构造函数,然后再调用派生类的构造函数。这是因为派生类对象包含了基类对象的部分,需要先初始化基类部分。

class Base {
public:
    Base() {
        std::cout << "Base constructor called." << std::endl;
    }
    ~Base() {
        std::cout << "Base destructor called." << std::endl;
    }
};

class Derived : public Base {
public:
    Derived() {
        std::cout << "Derived constructor called." << std::endl;
    }
    ~Derived() {
        std::cout << "Derived destructor called." << std::endl;
    }
};

当我们创建一个 Derived 类对象时:

int main() {
    Derived d;
    return 0;
}

输出结果为:

Base constructor called.
Derived constructor called.
Derived destructor called.
Base destructor called.

可以看到,先调用了基类的构造函数,然后调用派生类的构造函数。在对象销毁时,先调用派生类的析构函数,然后调用基类的析构函数。

带参数的基类构造函数

如果基类有带参数的构造函数,派生类需要在其构造函数的初始化列表中显式调用基类的构造函数,并传递相应的参数。

class Base {
public:
    int value;
    Base(int v) : value(v) {
        std::cout << "Base constructor with value " << value << " called." << std::endl;
    }
    ~Base() {
        std::cout << "Base destructor called." << std::endl;
    }
};

class Derived : public Base {
public:
    Derived(int v) : Base(v) {
        std::cout << "Derived constructor called." << std::endl;
    }
    ~Derived() {
        std::cout << "Derived destructor called." << std::endl;
    }
};

Derived 类的构造函数初始化列表中,通过 Base(v) 调用了基类的带参数构造函数。当创建 Derived 类对象时:

int main() {
    Derived d(10);
    return 0;
}

输出结果为:

Base constructor with value 10 called.
Derived constructor called.
Derived destructor called.
Base destructor called.

单继承中的函数重写

虚函数和函数重写的概念

在C++中,当基类中的函数被声明为虚函数(使用 virtual 关键字)时,派生类可以重写这个函数,以提供自己的实现。函数重写发生在派生类定义了与基类虚函数具有相同签名(函数名、参数列表和返回类型)的函数时。

class Shape {
public:
    virtual double area() {
        return 0.0;
    }
};

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

class Rectangle : public Shape {
public:
    double width;
    double height;
    Rectangle(double w, double h) : width(w), height(h) {}
    double area() override {
        return width * height;
    }
};

在这个例子中,Shape 类的 area 函数被声明为虚函数。Circle 类和 Rectangle 类都重写了 area 函数,提供了各自形状面积的计算方法。这里使用 override 关键字来明确表示该函数是对基类虚函数的重写,这有助于编译器检查函数签名是否正确。

动态绑定与多态性

通过基类指针或引用调用虚函数时,会发生动态绑定,即根据对象的实际类型来决定调用哪个版本的虚函数,这就是C++的多态性。

int main() {
    Shape* shapes[2];
    shapes[0] = new Circle(5.0);
    shapes[1] = new Rectangle(4.0, 6.0);

    for (int i = 0; i < 2; ++i) {
        std::cout << "Area of shape " << i << " is " << shapes[i]->area() << std::endl;
    }

    for (int i = 0; i < 2; ++i) {
        delete shapes[i];
    }

    return 0;
}

在上述代码中,shapes 数组是一个 Shape 指针数组,它可以指向 CircleRectangle 对象。当通过 shapes[i]->area() 调用 area 函数时,根据指针实际指向的对象类型(CircleRectangle),会调用相应的 area 函数实现,从而体现了多态性。输出结果为:

Area of shape 0 is 78.5398
Area of shape 1 is 24

单继承与代码复用和扩展

代码复用

单继承使得代码复用变得非常容易。派生类可以直接使用基类的属性和方法,避免了重复编写相同的代码。例如,在前面的 AnimalDog 的例子中,Dog 类继承了 Animal 类的 eatsleep 方法,无需在 Dog 类中重新实现这些方法。这不仅减少了代码量,还提高了代码的维护性。如果 Animal 类的 eat 方法需要修改,只需要在 Animal 类中修改一次,所有继承自 Animal 的派生类都会自动应用这个修改。

代码扩展

派生类可以在继承基类的基础上进行功能扩展。通过添加新的属性和方法,派生类可以满足特定的需求。比如,Dog 类添加了 bark 方法,这是 Animal 类所没有的,使得 Dog 类具有了独特的行为。此外,派生类还可以重写基类的虚函数,以提供更适合自身的实现,进一步扩展了功能。

单继承的内存布局

派生类对象的内存结构

在单继承中,派生类对象的内存布局是基类对象在前,派生类新增的成员在后。以 BaseDerived 类为例:

class Base {
public:
    int baseData;
};

class Derived : public Base {
public:
    int derivedData;
};

当创建一个 Derived 类对象时,baseData 会先存储在内存中,然后是 derivedData。这种内存布局保证了派生类对象包含了基类对象的所有成员,并且可以按照预期的方式访问和操作。

基类指针和派生类指针

基类指针可以指向派生类对象,因为派生类对象包含了基类对象的部分。例如:

Base* basePtr;
Derived d;
basePtr = &d;

在这个例子中,basePtr 可以指向 d,因为 d 包含了 Base 类的部分。但是,通过基类指针只能访问基类中定义的成员。如果要访问派生类特有的成员,需要进行类型转换。不过,这种类型转换需要谨慎使用,因为如果转换不当,可能会导致运行时错误。

单继承在实际项目中的应用场景

图形绘制系统

在一个图形绘制系统中,可以定义一个基类 Shape,包含一些通用的属性和方法,如颜色、位置等。然后通过单继承派生出不同的形状类,如 CircleRectangleTriangle 等。每个派生类可以重写 draw 方法来实现自己的绘制逻辑。这样的设计使得代码结构清晰,易于维护和扩展。例如,如果需要添加一个新的形状,只需要派生一个新的类并实现相应的绘制方法即可。

游戏角色系统

在游戏开发中,可以定义一个基类 Character,包含角色的基本属性,如生命值、攻击力、防御力等,以及一些基本的行为,如移动、攻击等。然后通过单继承派生出不同类型的角色类,如 WarriorMageArcher 等。每个派生类可以根据自身特点重写一些方法,如 Warrior 可能有更强的近战攻击能力,Mage 可能有强大的魔法攻击技能。这种设计可以有效地复用代码,同时为不同类型的角色提供个性化的实现。

数据库访问层

在数据库访问层的设计中,可以定义一个基类 DatabaseAccess,包含一些通用的数据库操作方法,如连接数据库、执行SQL语句等。然后通过单继承派生出不同的数据库类型的访问类,如 MySQLAccessOracleAccessSQLiteAccess 等。每个派生类可以根据具体数据库的特点重写一些方法,以优化数据库操作。例如,MySQLAccess 可以针对MySQL数据库的特性实现更高效的查询方法。

单继承可能带来的问题及解决方案

类层次结构过深

随着项目的发展,可能会出现类层次结构过深的问题。过多的继承层次可能导致代码理解和维护困难。例如,一个派生类可能从多层基类继承了大量的属性和方法,使得其功能难以清晰界定。 解决方案是尽量保持类层次结构的简洁,避免不必要的继承。可以考虑使用组合(composition)来代替继承,将一个类作为另一个类的成员变量,这样可以更灵活地控制代码的复用和组合关系。

基类修改影响派生类

如果基类的接口或实现发生修改,可能会对所有的派生类产生影响。例如,基类的某个虚函数签名发生改变,那么所有重写该函数的派生类都需要相应修改。 为了减少这种影响,在设计基类时应该尽量保持接口的稳定性。可以使用抽象基类(abstract base class)和纯虚函数来定义接口,这样派生类只需要关注接口的实现,而基类的内部实现细节修改对派生类的影响较小。同时,在修改基类时,要进行充分的测试,确保所有派生类仍然能够正常工作。

多重继承的替代

在某些情况下,可能会有使用多重继承的需求,但多重继承会带来复杂性和菱形继承问题。在C++中,可以通过单继承结合接口类(纯虚函数组成的类)来模拟多重继承的效果。例如,定义多个接口类,然后让一个派生类继承自一个基类并实现多个接口类,这样既可以获得单继承的简单性,又能实现类似多重继承的功能。

通过对C++类单继承特性的深入分析,我们了解了它的基本概念、访问控制、构造函数和析构函数、函数重写、内存布局、应用场景以及可能带来的问题和解决方案。单继承作为C++面向对象编程的重要特性,在代码复用、扩展和设计清晰的软件架构方面发挥着重要作用。在实际编程中,合理运用单继承可以提高代码的质量和可维护性。