首页
学习
活动
专区
圈层
工具
发布
社区首页 >专栏 >【C++面向对象编程】四大基本特性之二:继承

【C++面向对象编程】四大基本特性之二:继承

作者头像
byte轻骑兵
发布2026-01-20 17:02:26
发布2026-01-20 17:02:26
1350
举报

在C++面向对象编程中,继承是四大基本特性(封装、继承、多态、抽象)之一,它允许我们定义一个新类(派生类或子类)来继承另一个类(基类或父类)的属性和方法。这种机制极大地提高了代码的复用性,并使得程序结构更加清晰和易于维护。

一、继承的基本概念

1.1 什么是继承

继承是面向对象编程中的一个核心概念,它表示一种“is-a”关系。例如,狗是一种哺乳动物,哺乳动物是一种动物。在C++中,我们可以通过继承来模拟这种关系,使得子类可以拥有父类的属性和方法,同时还可以定义自己的特有属性和方法。

1.2 继承的意义

  • 代码复用:通过继承,子类可以复用父类的代码,避免重复编写相同的属性和方法。
  • 层次化设计:继承允许我们构建层次化的类结构,使得程序更加模块化和易于扩展。
  • 多态的基础:继承是实现多态的必要条件之一,通过继承和虚函数,我们可以实现运行时多态。

1.3 继承的称呼

  • 基类(父类):被继承的类称为基类或父类。
  • 派生类(子类):继承基类的类称为派生类或子类。

二、继承的基本语法

在C++中,继承的基本语法如下:

代码语言:javascript
复制
class DerivedClass : access-specifier BaseClass {
    // 派生类的成员
};

其中,access-specifier是访问修饰符,可以是publicprotectedprivate,它决定了基类成员在派生类中的访问权限。

2.1 访问修饰符

  • public:基类的公有成员和保护成员在派生类中保持其原有的访问权限,基类的私有成员在派生类中不可见。
  • protected:基类的公有成员和保护成员在派生类中变为保护成员,基类的私有成员在派生类中不可见。
  • private:基类的公有成员和保护成员在派生类中变为私有成员,基类的私有成员在派生类中不可见。

2.2 示例代码

以下是一个简单的单继承示例,展示了如何通过继承来实现属性和方法的复用:

代码语言:javascript
复制
#include <iostream>
#include <string>

using namespace std;

// 基类(父类)
class Animal {
public:
    Animal(string name) : name(name) {}

    void eat() {
        cout << name << " is eating." << endl;
    }

    void sleep() {
        cout << name << " is sleeping." << endl;
    }

protected:
    string name; // 动物的名字
};

// 派生类(子类)
class Dog : public Animal {
public:
    Dog(string name, string breed) : Animal(name), breed(breed) {}

    void bark() {
        cout << "Woof! Woof!" << endl;
    }

    // 重新定义父类方法
    void eat() {
        cout << "The dog is eating dog food." << endl;
    }

private:
    string breed; // 狗的品种
};

int main() {
    // 创建一个 Animal 对象
    Animal animal("Generic Animal");
    animal.eat();
    animal.sleep();
    cout << endl;

    // 创建一个 Dog 对象
    Dog dog("Buddy", "Golden Retriever");
    dog.eat(); // 调用子类重写的方法
    dog.sleep();
    dog.bark(); // 调用子类特有的方法

    return 0;
}

三、继承中的访问控制

3.1 基类成员的访问权限

在继承中,基类成员的访问权限受到继承方式和基类中成员访问修饰符的共同影响。具体规则如下:

基类成员访问修饰符

继承方式

派生类中的访问权限

public

public

public

public

protected

protected

public

private

private

protected

public

protected

protected

protected

protected

protected

private

private

private

任何方式

不可见

  • 示例:定义一个基类Animal和派生类Dog
代码语言:javascript
复制
// 基类:动物
class Animal {
public:
    void speak() { std::cout << "Animal sound" << std::endl; }
    int age;
};

// 派生类:狗(public继承)
class Dog : public Animal {
public:
    void fetch() { std::cout << "Fetching stick!" << std::endl; }
};

3.2 成员访问级别详解

public继承

  • 保持基类成员的访问级别(publicpublicprotectedprotected
  • 适用于 "是一种"(is-a)关系,如Dog是一种Animal
代码语言:javascript
复制
Dog d;
d.speak(); // 合法,基类public成员在派生类中仍为public
d.age = 10; // 合法,基类public成员

protected继承

  • 基类public成员在派生类中变为protected
  • 常用于中间基类,不希望成员在最终派生类外访问
代码语言:javascript
复制
class Intermediate : protected Animal {
    // Intermediate的子类可访问Animal的public成员(现为protected)
};

private继承

  • 基类所有可访问成员在派生类中变为private
  • 实现 "has-a" 关系的一种方式(更推荐组合)
代码语言:javascript
复制
class Robot : private Animal {
    // Robot内部使用Animal的功能,但不对外暴露
};

3.3 不可访问的基类成员

  • 基类的private成员始终无法在派生类中直接访问,只能通过基类的publicprotected接口间接访问
代码语言:javascript
复制
class Animal {
private:
    int secret; // 派生类无法直接访问
public:
    void setSecret(int s) { secret = s; } // 间接访问接口
};

class Dog : public Animal {
    void accessSecret() {
        // secret = 10; // 错误:无法访问基类private成员
        setSecret(10); // 正确:通过基类public方法访问
    }
};

四、继承中的构造函数与析构函数

4.1 派生类构造函数的执行顺序

  1. 基类构造函数:先于派生类构造函数执行
  2. 成员对象构造函数:按声明顺序执行
  3. 派生类构造函数体:最后执行

示例:多层继承的构造顺序

代码语言:javascript
复制
class A {
public:
    A() { std::cout << "A constructor" << std::endl; }
};

class B : public A {
public:
    B() { std::cout << "B constructor" << std::endl; }
};

class C : public B {
public:
    C() { std::cout << "C constructor" << std::endl; }
};

int main() {
    C obj; // 输出:A→B→C的构造顺序
    return 0;
}

4.2 派生类构造函数的初始化列表

  • 派生类必须在构造函数初始化列表中显式调用基类的构造函数
  • 若基类没有默认构造函数,派生类必须显式调用基类的其他构造函数
代码语言:javascript
复制
class Animal {
public:
    Animal(int age) : age(age) { std::cout << "Animal constructor" << std::endl; }
    int age;
};

class Dog : public Animal {
public:
    Dog(int age, std::string name) 
        : Animal(age), name(name) { // 显式调用基类构造函数
        std::cout << "Dog constructor" << std::endl;
    }
    std::string name;
};

4.3 析构函数的执行顺序

  • 与构造函数顺序相反:派生类析构函数先执行,基类析构函数后执行
  • 基类析构函数应声明为虚函数,以确保多态析构的正确性(见多态章节)

五、继承与静态成员

5.1 静态成员的继承特性

  • 基类的静态成员(静态变量、静态函数)会被所有派生类共享
  • 无论继承方式如何,静态成员的访问级别由基类的访问说明符决定

示例:静态成员的继承

代码语言:javascript
复制
class Base {
public:
    static int staticVar;
    static void staticFunc() { std::cout << "Base static func" << std::endl; }
};

int Base::staticVar = 10;

class Derived : public Base {};

int main() {
    std::cout << Derived::staticVar << std::endl; // 输出10
    Derived::staticFunc(); // 输出"Base static func"
    return 0;
}

5.2 静态成员的隐藏规则

  • 派生类不能通过重新定义静态成员来覆盖基类的静态成员(仅名称隐藏)
  • 若需重新定义,需通过作用域运算符明确访问
代码语言:javascript
复制
class Base {
public:
    static int value;
};

int Base::value = 10;

class Derived : public Base {
public:
    static int value; // 隐藏基类的value,但未覆盖
};

int Derived::value = 20;

int main() {
    std::cout << Base::value << std::endl; // 10
    std::cout << Derived::value << std::endl; // 20
    std::cout << Derived::Base::value << std::endl; // 显式访问基类value,输出10
    return 0;
}

六、继承中的名称隐藏与覆盖

6.1 名称隐藏规则

  • 派生类中定义的成员若与基类成员同名,会隐藏基类成员(无论类型和访问级别)
  • 可通过作用域运算符::显式访问基类成员

示例:函数名称隐藏

代码语言:javascript
复制
class Base {
public:
    void func() { std::cout << "Base func" << std::endl; }
};

class Derived : public Base {
public:
    void func(int x) { std::cout << "Derived func with int: " << x << std::endl; }
};

int main() {
    Derived d;
    d.func(); // 错误:Base::func()被隐藏
    d.Base::func(); // 正确:显式调用基类func()
    d.func(10); // 正确:调用派生类func(int)
    return 0;
}

6.2 虚函数覆盖(多态)

  • 当基类函数声明为virtual时,派生类可通过相同签名的函数实现覆盖(Override)
  • 覆盖需满足:函数名、参数列表、返回类型(协变返回类型除外)完全相同
代码语言:javascript
复制
class Animal {
public:
    virtual void speak() { std::cout << "Animal sound" << std::endl; }
};

class Dog : public Animal {
public:
    void speak() override { // C++11的override关键字确保正确覆盖
        std::cout << "Woof!" << std::endl;
    }
};

6.3 协变返回类型(Covariant Return Types)

  • C++ 允许派生类虚函数返回基类虚函数返回类型的派生类指针 / 引用
代码语言:javascript
复制
class Base {
public:
    virtual Base* clone() { return new Base(); }
};

class Derived : public Base {
public:
    Derived* clone() override { // 合法,返回Derived*(Base*的派生类型)
        return new Derived();
    }
};

七、多层继承与菱形继承问题

7.1 多层继承(Multilevel Inheritance)

  • 派生类作为基类继续派生出新的类,形成继承链
代码语言:javascript
复制
class Animal { /* ... */ };
class Mammal : public Animal { /* ... */ };
class Dog : public Mammal { /* ... */ }; // Animal → Mammal → Dog

7.2 菱形继承(Diamond Inheritance)

①问题描述:两个派生类继承同一基类,又共同派生出一个子类,导致基类成员在子类中存在两份副本,引发二义性问题

②示例代码(未解决的菱形继承)

代码语言:javascript
复制
class Base {
public:
    int value;
};

class Derived1 : public Base {};
class Derived2 : public Base {};
class GrandDerived : public Derived1, public Derived2 {};

int main() {
    GrandDerived obj;
    obj.Derived1::value = 10; // 必须显式指定路径
    obj.Derived2::value = 20;
    return 0;
}

③解决方案:虚继承(Virtual Inheritance)

使用virtual关键字声明基类,确保基类在派生类中只有一份共享副本

代码语言:javascript
复制
class Base { /* ... */ };
class Derived1 : virtual public Base {}; // 虚继承
class Derived2 : virtual public Base {}; // 虚继承
class GrandDerived : public Derived1, public Derived2 {};

④虚继承的构造函数

最底层派生类(GrandDerived)负责调用虚基类(Base)的构造函数

代码语言:javascript
复制
GrandDerived() : Base(), Derived1(), Derived2() { /* ... */ }

八、继承与组合:选择合适的复用方式

8.1 组合(Composition)简介

  • 组合是在类中包含其他类的对象作为成员,实现 "has-a" 关系
  • 示例:Car包含Engine对象
代码语言:javascript
复制
class Engine {
public:
    void start() { std::cout << "Engine started" << std::endl; }
};

class Car {
private:
    Engine engine; // 组合
public:
    void startEngine() { engine.start(); }
};

8.2 继承 vs 组合

特性

继承

组合

关系类型

is-a(如 Dog 是 Animal)

has-a(如 Car 有 Engine)

代码耦合度

高(基类变更影响派生类)

低(成员对象可独立变化)

访问控制

通过继承方式控制

通过成员对象的接口控制

多态支持

天然支持

需通过接口类实现

8.3 何时选择继承?

  • 存在明确的 "是一种" 关系
  • 需要通过多态实现动态行为
  • 希望复用基类的实现细节

8.4 何时选择组合?

  • 存在 "拥有" 关系(has-a)
  • 需要更灵活的对象组合
  • 避免继承带来的强耦合

九、继承的最佳实践与设计原则

9.1 里氏替换原则(Liskov Substitution Principle, LSP)

  • 派生类必须能够替换其基类,且程序行为不变
  • 派生类不应削弱基类的前置条件,也不应强化后置条件

9.2 优先使用组合而非继承(Composition Over Inheritance)

  • 组合更灵活,避免继承的 "白箱" 复用问题
  • 示例:用组合实现日志功能,而非继承日志类

9.3 虚析构函数的必要性

  • 基类析构函数应声明为虚函数,确保通过基类指针删除派生类对象时正确调用析构函数
代码语言:javascript
复制
class Base {
public:
    virtual ~Base() { std::cout << "Base destructor" << std::endl; }
};

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

int main() {
    Base* ptr = new Derived();
    delete ptr; // 输出:Derived→Base析构(虚析构函数生效)
    return 0;
}

9.4 避免过度继承

  • 深层继承层次会增加复杂度,建议不超过 3-4 层
  • 使用接口类(纯虚函数)分离行为,减少实现继承

十、继承的高级主题

10.1 抽象基类(Abstract Base Class, ABC)

  • 包含至少一个纯虚函数的类,无法实例化
  • 用于定义接口规范
代码语言:javascript
复制
class Shape {
public:
    virtual double area() const = 0; // 纯虚函数
};

class Circle : public Shape {
public:
    double area() const override { return 3.14 * radius * radius; }
private:
    double radius;
};

10.2 多重继承(Multiple Inheritance, MI)

  • 派生类继承多个基类
  • 语法:class Derived : public Base1, public Base2 { ... }
  • 注意事项:
    • 可能引发名称冲突(通过作用域运算符解决)
    • 菱形继承问题需虚继承解决

示例:多重继承

代码语言:javascript
复制
class Flyable {
public:
    void fly() { std::cout << "Flying" << std::endl; }
};

class Swimable {
public:
    void swim() { std::cout << "Swimming" << std::endl; }
};

class Duck : public Flyable, public Swimable {}; // 鸭子同时会飞和游泳

int main() {
    Duck d;
    d.fly(); // 合法
    d.swim(); // 合法
    return 0;
}

10.3 继承与模板(Template Inheritance)

  • 模板类可以作为基类或派生类
  • 示例:泛型基类
代码语言:javascript
复制
template <typename T>
class Container {
public:
    void add(T item) { items.push_back(item); }
protected:
    std::vector<T> items;
};

class IntContainer : public Container<int> {
public:
    int sum() { 
        int total = 0;
        for (int item : items) total += item;
        return total;
    }
};

十一、完整示例:继承层次的综合应用

代码语言:javascript
复制
#include <iostream>
#include <vector>
#include <string>
#include <cmath>
using namespace std;

// ====================== 抽象基类:图形 ======================
class Shape {
public:
    // 纯虚函数:获取图形名称和面积
    virtual string getName() const = 0;
    virtual double getArea() const = 0;
    
    // 虚析构函数确保多态析构
    virtual ~Shape() {}
};

// ====================== 二维图形基类 ======================
class TwoDShape : virtual public Shape {  // 使用虚继承
public:
    // 构造函数接收宽和高
    TwoDShape(double width, double height) 
        : width_(width), height_(height) {}
    
    // 获取宽和高
    double getWidth() const { return width_; }
    double getHeight() const { return height_; }

protected:
    double width_;  // 宽度
    double height_; // 高度
};

// ====================== 三维图形基类 ======================
class ThreeDShape : virtual public Shape {  // 使用虚继承
public:
    // 构造函数接收体积
    ThreeDShape(double volume) : volume_(volume) {}
    
    // 获取体积
    double getVolume() const { return volume_; }

protected:
    double volume_; // 体积
};

// ====================== 派生类:矩形(二维图形) ======================
class Rectangle : public TwoDShape {
public:
    // 构造函数调用基类构造函数
    Rectangle(double width, double height) 
        : TwoDShape(width, height) {}
    
    // 覆盖基类虚函数
    string getName() const override { return "Rectangle"; }
    double getArea() const override { 
        return width_ * height_; // 矩形面积 = 宽×高
    }
};

// ====================== 派生类:正方形(矩形的特化) ======================
class Square : public Rectangle {
public:
    // 构造函数直接指定边长(宽高相等)
    Square(double side) : Rectangle(side, side) {}
    
    // 重写名称(可选,也可继承Rectangle的名称)
    string getName() const override { return "Square"; }
};

// ====================== 派生类:圆形(二维图形) ======================
class Circle : public TwoDShape {
public:
    // 构造函数接收半径(宽高统一为直径)
    Circle(double radius) : TwoDShape(2*radius, 2*radius), radius_(radius) {}
    
    // 覆盖基类虚函数
    string getName() const override { return "Circle"; }
    double getArea() const override { 
        return M_PI * radius_ * radius_; // 圆形面积 = πr²
    }

private:
    double radius_; // 半径
};

// ====================== 派生类:立方体(三维图形,基于正方形) ======================
class Cube : public ThreeDShape, public Square {
public:
    // 构造函数调用三维基类和正方形基类(多重继承)
    Cube(double side) 
        : ThreeDShape(side*side*side), Square(side) {} // 体积 = 边长³
    
    // 覆盖基类虚函数(从Shape继承)
    string getName() const override { return "Cube"; }
    double getArea() const override { 
        return 6 * Square::getArea(); // 立方体表面积 = 6×一个面的面积
    }
};

// ====================== 多态容器与遍历函数 ======================
void printShapeInfo(const vector<Shape*>& shapes) {
    for (const auto* shape : shapes) {
        cout << "Name: " << shape->getName() 
             << ", Area: " << shape->getArea();
        
        // 若为三维图形,额外输出体积
        if (auto* threeDShape = dynamic_cast<const ThreeDShape*>(shape)) {
            cout << ", Volume: " << threeDShape->getVolume();
        }
        cout << endl;
    }
}

// ====================== 主函数:测试继承层次 ======================
int main() {
    vector<Shape*> shapes;
    
    // 添加二维图形
    shapes.push_back(new Rectangle(4, 5));   // 矩形
    shapes.push_back(new Square(3));          // 正方形
    shapes.push_back(new Circle(2));          // 圆形
    
    // 添加三维图形
    shapes.push_back(new Cube(3));            // 立方体
    
    // 遍历多态容器,输出信息
    printShapeInfo(shapes);
    
    // 释放内存(多态析构)
    for (auto* shape : shapes) {
        delete shape;
    }
    
    return 0;
}

十二、继承的最佳实践

12.1 合理设计继承层次

  • 明确继承关系:确保派生类与基类之间存在合理的“is-a”关系。
  • 避免过度继承:继承层次不宜过深,以免增加代码复杂性和维护难度。
  • 保持一致性:在整个应用程序中保持继承层次的一致性和规范性。

12.2 合理使用访问修饰符

  • public继承:通常用于表示“is-a”关系,保持基类接口的可见性。
  • protectedprivate继承:较少使用,通常用于实现细节或特殊场景。

12.3 避免命名冲突

  • 命名空间隔离:在大型项目中,考虑使用命名空间来隔离不同继承层次中的同名成员。
  • 明确指定类域:在必要时,通过类域来显式指定要访问的基类成员。

12.4 合理使用虚函数和多态

  • 定义虚函数:如果希望派生类能够重写基类的方法,应将其定义为虚函数。
  • 避免滥用多态:多态虽然强大,但也会带来一定的运行时开销,应合理使用。

十三、总结

继承是C++面向对象编程中的核心特性之一,它允许我们通过复用基类的代码来构建更加复杂和功能丰富的程序。在实际编程中,我们应根据具体需求合理设计继承层次,合理使用访问修饰符,避免命名冲突,并合理使用虚函数和多态。通过深入理解和应用继承特性,可以编写出更加清晰、健壮和易于维护的C++代码。


本文参与 腾讯云自媒体同步曝光计划,分享自作者个人站点/博客。
原始发表:2025-05-18,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 作者个人站点/博客 前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 一、继承的基本概念
    • 1.1 什么是继承
    • 1.2 继承的意义
    • 1.3 继承的称呼
  • 二、继承的基本语法
    • 2.1 访问修饰符
    • 2.2 示例代码
  • 三、继承中的访问控制
    • 3.1 基类成员的访问权限
    • 3.2 成员访问级别详解
    • 3.3 不可访问的基类成员
  • 四、继承中的构造函数与析构函数
    • 4.1 派生类构造函数的执行顺序
    • 4.2 派生类构造函数的初始化列表
    • 4.3 析构函数的执行顺序
  • 五、继承与静态成员
    • 5.1 静态成员的继承特性
    • 5.2 静态成员的隐藏规则
  • 六、继承中的名称隐藏与覆盖
    • 6.1 名称隐藏规则
    • 6.2 虚函数覆盖(多态)
    • 6.3 协变返回类型(Covariant Return Types)
  • 七、多层继承与菱形继承问题
    • 7.1 多层继承(Multilevel Inheritance)
    • 7.2 菱形继承(Diamond Inheritance)
  • 八、继承与组合:选择合适的复用方式
    • 8.1 组合(Composition)简介
    • 8.2 继承 vs 组合
    • 8.3 何时选择继承?
    • 8.4 何时选择组合?
  • 九、继承的最佳实践与设计原则
    • 9.1 里氏替换原则(Liskov Substitution Principle, LSP)
    • 9.2 优先使用组合而非继承(Composition Over Inheritance)
    • 9.3 虚析构函数的必要性
    • 9.4 避免过度继承
  • 十、继承的高级主题
    • 10.1 抽象基类(Abstract Base Class, ABC)
    • 10.2 多重继承(Multiple Inheritance, MI)
    • 10.3 继承与模板(Template Inheritance)
  • 十一、完整示例:继承层次的综合应用
  • 十二、继承的最佳实践
    • 12.1 合理设计继承层次
    • 12.2 合理使用访问修饰符
    • 12.3 避免命名冲突
    • 12.4 合理使用虚函数和多态
  • 十三、总结
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档