目录

继承的概念及定义

概念:

定义:

三种继承方式:

​编辑

基类和派生类对象赋值转换

什么是对象赋值转换?

代码演示:

继承中的作用域

派生类的默认成员函数

构造析构顺序:

继承与友元

为什么友元关系不继承?

继承与静态成员

复杂的菱形继承及菱形虚拟继承

单继承:一个子类只有一个直接父类时称这个继承关系为单继承

多继承:一个子类有两个或以上直接父类时称这个继承关系为多继承

菱形继承:菱形继承是多继承的一种特殊情况

菱形继承的问题:

虚继承解决方案:

虚继承的内存布局

函数重载、隐藏和覆盖

函数重载(Overload)

函数隐藏(Hiding)

函数覆盖/重写(Override)

三者的对比和区别:


继承的概念及定义

概念:

继承(inheritance)机制是面向对象程序设计使代码可以复用的最重要的手段,它允许程序员在保 持原有类特性的基础上进行扩展,增加功能,这样产生新的类,称派生类。继承呈现了面向对象 程序设计的层次结构,体现了由简单到复杂的认知过程。以前我们接触的复用都是函数复用,继 承是类设计层次的复用

定义:

下面Person是父类,也称作基类。Student是子类,也称作派生类

三种继承方式:

class Base {
public:
    int publicVar;
protected:
    int protectedVar;
private:
    int privateVar;
};

// 1. 公有继承(最常用)
class PublicDerived : public Base {
    // publicVar → public
    // protectedVar → protected
    // privateVar → 不可访问
};

// 2. 保护继承
class ProtectedDerived : protected Base {
    // publicVar → protected
    // protectedVar → protected
    // privateVar → 不可访问
};

// 3. 私有继承
class PrivateDerived : private Base {
    // publicVar → private
    // protectedVar → private
    // privateVar → 不可访问
};

简单总结三种继承方式特点:

基类成员访问权限 公有继承(public) 保护继承(protected) 私有继承(private)
public public protected private
protected protected protected private
private 不可访问 不可访问 不可访问

注意

  • 这里的不可访问是指基类的私 有成员还是被继承到了派生类对象中,但是语法上限制派生类对象不管在类里面还是类外面 都不能去访问
  • 使用关键字class时默认的继承方式是private,使用struct时默认的继承方式是public
  • 实际运用中一般使用都是public继承,几乎很少使用protetced/private继承

基类和派生类对象赋值转换

什么是对象赋值转换?

在继承体系中,基类和派生类对象之间可以进行类型转换,这通常称为"向上转型"和"向下转型"。

向上转型(派生类->基类)向下转型(基类->派生类)

我们这里重点介绍向上转型:

  • 派生类对象 可以赋值给 基类的对象 / 基类的指针 / 基类的引用。这里有个形象的说法叫切片 或者切割。寓意把派生类中父类那部分切来赋值过去。
  • 基类对象不能赋值给派生类对象。
  • 基类的指针或者引用可以通过强制类型转换赋值给派生类的指针或者引用。但是必须是基类 的指针是指向派生类对象时才是安全的。这里基类如果是多态类型,可以使用RTTI(RunTime Type Information)的dynamic_cast来进行识别后进行安全转换。(ps:这个不详细介绍,先了解一下,感兴趣可以自行搜索学习)

代码演示:
 

class Person
{
protected :
 string _name; // 姓名
    string _sex;  // 性别
    int _age; // 年龄
};
class Student : public Person
{
public :
 int _No ; // 学号
};
void Test ()
{
 Student sobj ;
 // 1.子类对象可以赋值给父类对象/指针/引用
 Person pobj = sobj ;
 Person* pp = &sobj;
 Person& rp = sobj;
    
 //2.基类对象不能赋值给派生类对象
    sobj = pobj;
    
    // 3.基类的指针可以通过强制类型转换赋值给派生类的指针
    pp = &sobj
    Student* ps1 = (Student*)pp; // 这种情况转换时可以的。
    ps1->_No = 10;
    
    pp = &pobj;
 Student* ps2 = (Student*)pp; // 这种情况转换时虽然可以,但是会存在越界访问的问
题
    ps2->_No = 10;
}

继承中的作用域

  1. 在继承体系中基类派生类都有独立的作用域
  2. 子类和父类中有同名成员,子类成员将屏蔽父类对同名成员的直接访问,这种情况叫隐藏, 也叫重定义。(在子类成员函数中,可以使用 基类::基类成员 显示访问
  3. 需要注意的是如果是成员函数的隐藏,只需要函数名相同就构成隐藏。(无论参数
  4. 注意在实际中在继承体系里面最好不要定义同名的成员
  5. 虚函数特殊:虚函数机制独立于名字隐藏规则
  6. 名字查找顺序:局部作用域 → 类作用域 → 基类作用域(深度优先)

派生类的默认成员函数

  1. 派生类的构造函数必须调用基类的构造函数初始化基类的那一部分成员。如果基类没有默认 的构造函数,则必须在派生类构造函数的初始化列表阶段显示调用
  2. 派生类的拷贝构造函数必须调用基类的拷贝构造完成基类的拷贝初始化
  3. 派生类的operator=必须要调用基类的operator=完成基类的复制
  4. 派生类的析构函数会在被调用完成后自动调用基类的析构函数清理基类成员。因为这样才能 保证派生类对象先清理派生类成员再清理基类成员的顺序
  5. 派生类对象初始化先调用基类构造再调派生类构造
  6. 派生类对象析构清理先调用派生类析构再调基类的析构
  7. 因为后续一些场景析构函数需要构成重写,重写的条件之一是函数名相同(这个我们后面会讲 解)。那么编译器会对析构函数名进行特殊处理,处理成destrutor(),所以父类析构函数不加 virtual的情况下,子类析构函数和父类析构函数构成隐藏关系

构造析构顺序:

继承与友元

友元关系不能继承,也就是说基类友元不能访问子类私有和保护成员。即父亲的朋友不是我的朋友

为什么友元关系不继承?

这是设计选择,为了保持封装性。如果友元继承,那么基类的所有派生类都会暴露给友元,这违反了封装原则

代码演示:

#include <iostream>
using namespace std;

// 基类
class Base {
private:
    int secret = 100;
    
    // 声明友元类
    friend class FriendClass;
};

// 友元类
class FriendClass {
public:
    void peekBase(Base& b) {
        cout << "FriendClass看到Base的秘密: " << b.secret << endl;  // ✅ 可以
    }
    
    // 尝试访问派生类 - 会失败!
    void peekDerived(Derived& d) {
        // ❌ 下面这行编译错误:'int Derived::derivedSecret' is private
        // cout << "尝试看Derived的秘密: " << d.derivedSecret << endl;
    }
};

// 派生类
class Derived : public Base {
private:
    int derivedSecret = 200;
    // 注意:没有声明FriendClass为友元!
};

int main() {
    cout << "=== 演示1:友元不继承 ===" << endl;
    
    Base b;
    Derived d;
    FriendClass fc;
    
    fc.peekBase(b);  // ✅ 成功:FriendClass是Base的友元
    fc.peekBase(d);  // ✅ 成功:向上转型,访问继承的Base部分
    
    // ❌ 下面的调用会编译错误
    // fc.peekDerived(d);  // FriendClass不是Derived的友元
    
    return 0;
}

继承与静态成员

基类定义了static静态成员,则整个继承体系里面只有一个这样的成员。无论派生出多少个子 类,都只有一个static成员实例

复杂的菱形继承及菱形虚拟继承

单继承:一个子类只有一个直接父类时称这个继承关系为单继承

多继承:一个子类有两个或以上直接父类时称这个继承关系为多继承

菱形继承:菱形继承是多继承的一种特殊情况

菱形继承的问题:

  • 数据冗余:基类数据有多份副本
  • 二义性:访问基类成员时需要明确指定路径

虚继承解决方案:

虚拟继承可以解决菱形继承的二义性和数据冗余的问题

代码演示:

class Person
{
public :
 string _name ; // 姓名
};
class Student : virtual public Person
{
protected :
 int _num ; //学号
};
 class Teacher : virtual public Person
{
protected :
 int _id ; // 职工编号
};
class Assistant : public Student, public Teacher
{
protected :
 string _majorCourse ; // 主修课程
};
void Test ()
{
 Assistant a ;
 a._name = "peter";
}

虚继承的内存布局

为了研究虚拟继承原理,这里使用一个简化的菱形继承继承体系,再借助内存窗口观察对象成 员的模型

class A
{
public:
 int _a;
};
// class B : public A
class B : virtual public A
{
public:
 int _b;
};
// class C : public A
class C : virtual public A
{
public:
 int _c;
};
class D : public B, public C
{
public:
 int _d;
};
int main()
{
 D d;
 d.B::_a = 1;
 d.C::_a = 2;
 d._b = 3;
 d._c = 4;
 d._d = 5;
 return 0;
}

下图是菱形继承(没使用虚继承时)的内存对象成员模型:这里可以看到数据冗余

下图是菱形虚拟继承的内存对象成员模型:这里可以分析出D对象中将A放到的了对象组成的最下 面,这个A同时属于B和C,那么B和C如何去找到公共的A呢?这里是通过了B和C的两个指针,指 向的一张表。这两个指针叫虚基表指针,这两个表叫虚基表。虚基表中存的偏移量。通过偏移量 可以找到下面的A

下面是上面的Person关系菱形虚拟继承的原理解释:

函数重载、隐藏和覆盖

函数重载(Overload)

函数重载是在同一个作用域中,多个函数使用相同的名字,但参数列表不同(参数类型、参数个数或参数顺序不同)

// 经典重载示例
class MathOperations {
public:
    // 重载1:整数相加
    int add(int a, int b) {
        cout << "整数相加: ";
        return a + b;
    }
    
    // 重载2:浮点数相加
    double add(double a, double b) {
        cout << "浮点数相加: ";
        return a + b;
    }
    
    // 重载3:三个数相加
    int add(int a, int b, int c) {
        cout << "三个整数相加: ";
        return a + b + c;
    }
    
    // 重载4:整数和浮点数相加
    double add(int a, double b) {
        cout << "整数+浮点数: ";
        return a + b;
    }
    
    // 重载5:浮点数和整数相加(参数顺序不同)
    double add(double a, int b) {
        cout << "浮点数+整数: ";
        return a + b;
    }
};

注意:

  • 仅返回类型不同不能重载
  • 仅const修饰符不同(非成员函数
  • 但对于指针/引用,const可以构成重载
  • 成员函数的const版本可以构成重载

函数隐藏(Hiding)

函数隐藏发生在继承体系中,当派生类定义了与基类同名的函数时,基类的同名函数在派生类作用域中被隐藏。

class Base {
public:
    void func() {
        cout << "Base::func()" << endl;
    }
    
    void func(int x) {
        cout << "Base::func(int): " << x << endl;
    }
    
    void func(double x) {
        cout << "Base::func(double): " << x << endl;
    }
    
    virtual void virtualFunc() {
        cout << "Base::virtualFunc()" << endl;
    }
};

class Derived : public Base {
public:
    // 隐藏了Base中的所有func函数
    void func() {
        cout << "Derived::func()" << endl;
    }
    
    // 即使基类是虚函数,只要签名不同就是隐藏
    void virtualFunc(int x) {
        cout << "Derived::virtualFunc(int): " << x << endl;
    }
};

函数覆盖/重写(Override)

函数覆盖发生在继承体系中,派生类重新定义基类的虚函数,实现运行时多态

class Animal {
public:
    // 必须是虚函数
    virtual void makeSound() {
        cout << "动物发出声音" << endl;
    }
    
    virtual void eat(string food) {
        cout << "动物吃" << food << endl;
    }
    
    // 协变返回类型
    virtual Animal* clone() {
        cout << "克隆动物" << endl;
        return new Animal(*this);
    }
    
    virtual ~Animal() {
        cout << "Animal析构" << endl;
    }
};

class Dog : public Animal {
public:
    // 覆盖基类虚函数
    void makeSound() override {
        cout << "狗叫: 汪汪!" << endl;
    }
    
    // 覆盖带参数的虚函数
    void eat(string food) override {
        cout << "狗吃" << food << ",摇尾巴" << endl;
    }
    
    // 协变返回类型:返回派生类指针
    Dog* clone() override {
        cout << "克隆狗" << endl;
        return new Dog(*this);
    }
    
    // 狗特有的函数
    void guard() {
        cout << "狗在看家" << endl;
    }
    
    ~Dog() {
        cout << "Dog析构" << endl;
    }
};

int main() {
    cout << "\n=== 函数覆盖演示 ===" << endl;
    
    // 多态调用
    Animal* animals[2];
    animals[0] = new Animal();
    animals[1] = new Dog();
    
    for (int i = 0; i < 2; i++) {
        animals[i]->makeSound();  // 运行时多态
        animals[i]->eat("食物");
        cout << endl;
    }
    
    // 协变返回类型演示
    Dog* originalDog = new Dog();
    Dog* clonedDog = originalDog->clone();  // 返回Dog*,不是Animal*
    clonedDog->guard();  // 可以调用Dog特有方法
    
    // 清理
    for (int i = 0; i < 2; i++) {
        delete animals[i];
    }
    delete originalDog;
    delete clonedDog;
    
    return 0;
}

运行结果:

三者的对比和区别:

特性 重载(Overload) 隐藏(Hiding) 覆盖(Override)
作用域 同一个类中 不同类(继承关系) 不同类(继承关系)
函数签名 必须不同 可以相同或不同 必须完全相同
virtual关键字 不需要 不需要 必须要有
override关键字 不需要 不需要 推荐使用
多态性 编译时多态 无多态 运行时多态
调用方式 静态绑定 静态绑定 动态绑定
目的 提供多种接口 派生类提供新接口 实现多态行为

Logo

Agent 垂直技术社区,欢迎活跃、内容共建。

更多推荐