万字重谈C++——继承篇

继承的概念及定义

继承的概念

继承(Inheritance)机制作为面向对象程序设计中最核心的代码复用方式,它不仅允许开发人员在保留基础类特性的前提下进行功能扩展(从而创建新的派生类),更重要的是体现了面向对象程序设计的分层架构理念,这种架构完美地映射了从简单到复杂的认知过程。与传统函数级别的复用相比,继承提升到了类设计层次的复用,为软件系统的可扩展性和可维护性提供了更强大的支持。

cpp 复制代码
class Person
{
public:
    void Print()
    {
        cout << "name:" << _name << endl;
        cout << "age:" << _age << endl;
    }
protected:
    string _name = "peter";  // 存储姓名信息
    int _age = 18;           // 存储年龄信息
};

/*
通过继承机制,基类 Person 的所有成员(包括数据成员和成员函数)
都会成为子类的组成部分。以下示例展示了 Student 和 Teacher 类
如何有效地复用 Person 类的成员。开发者可以通过调试工具观察
Student 和 Teacher 对象,直观地验证数据成员的复用情况。
*/

class Student : public Person
{
protected:
    int _stuid;  // 存储学号信息
};

class Teacher : public Person
{
protected:
    int _jobid;  // 存储工号信息
};

int main()
{
    Student s;
    Teacher t;
    s.Print();
    t.Print();
    return 0;
}

继承定义

定义格式

在面向对象系统中,Person 作为父类(也称为基类),而 Student 扮演子类(也称为派生类)的角色,这种层级关系构成了面向对象程序设计的基础架构。

继承关系和访问限定符

继承基类成员访问方式的变化

  1. 基类的 private 成员在派生类中具有不可访问性(无论采用何种继承方式)。这种不可见性并不意味着派生类对象没有继承这些成员,而是从语法层面禁止了派生类对象(无论是在类内部还是外部)的访问权限。

  2. 基类的 private 成员在派生类中无法直接访问。若希望基类成员在派生类中可访问,同时避免类外访问,应当使用 protected 访问限定符。由此可见,protected 限定符的出现正是为了满足继承机制的特殊需求。

  3. 总结访问规则可以发现:基类的私有成员在子类中始终不可见,而基类其他成员在子类中的访问权限取决于

    复制代码
    Min(成员在基类的访问限定符,继承方式)

    其中 public > protected > private

  4. 在 C++ 中,使用 class 关键字定义类时,默认继承方式是 private;使用 struct 时,默认继承方式则为 public。为了代码可读性和维护性,强烈建议显式声明继承方式。

  5. 在工程实践中,public 继承是最常用的继承方式,而 protected/private 继承的应用场景相对较少。这主要是因为 protected/private 继承限制了派生类外部的可访问性,降低了代码的维护性和扩展性。

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

cpp 复制代码
class Person
{
public:
    void Print()
    {
        cout << _name << endl;
    }
protected:
    string _name;  // 存储姓名
private:
    int _age;      // 存储年龄
};

// class Student : protected Person
// class Student : private Person

class Student : public Person
{
protected:
    int _stunum;   // 存储学号
};

/*
在面向对象系统中,派生类对象可以赋值给基类对象、基类指针或基类引用,
这一特征常被形象地称为"切片"或"切割",比喻将派生类中属于父类的部分
"切出"进行赋值。

反之,基类对象不能直接赋值给派生类对象。

基类指针或引用可以通过强制类型转换赋值给派生类指针或引用,但需要注意:
只有当基类指针确实指向派生类对象时,这种转换才是安全的。在多态类型中,
可以使用 RTTI (Run-Time Type Information) 的 dynamic_cast 进行安全转换
(具体实现将在后续章节讲解)。
*/

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. 实际工程中,建议避免在继承体系中定义同名成员,避免带来混淆。例如:

cpp 复制代码
class Person
{
protected:
    string _name = "小李子";  // 姓名
    int _num = 111;           // 身份证号
};

class Student : public Person
{
public:
    void Print()
    {
        cout << "姓名:" << _name << endl;
        cout << "身份证号:" << Person::_num << endl;  // 显式访问基类成员
        cout << "学号:" << _num << endl;
    }
protected:
    int _num = 999;  // 学号,隐藏了基类的_num
};

void Test()
{
    Student s1;
    s1.Print();
}
cpp 复制代码
class A
{
public:
    void fun()
    {
        cout << "func()" << endl;
    }
};

class B : public A
{
public:
    void fun(int i)
    {
        A::fun();  // 显式调用基类函数
        cout << "func(int i)->" << i << endl;
    }
};

void Test()
{
    B b;
    b.fun(10);
}

派生类的默认成员函数

C++中的6个默认成员函数指的是编译器在无显式定义时自动生成的函数。派生类中这些函数的生成及行为:

  1. 构造函数

    派生类构造函数必须调用基类的构造函数来初始化基类部分。如果基类没有默认构造函数,则派生类构造函数须在初始化列表中显式调用相应基类构造函数。

  2. 拷贝构造函数

    派生类的拷贝构造函数必须调用基类的拷贝构造函数来完成基类部分的复制初始化。

  3. 赋值运算符(operator=)

    派生类的赋值运算符必须调用基类的赋值运算符完成基类成员的复制。

  4. 析构函数

    派生类析构函数执行完毕后,自动调用基类的析构函数,保证销毁顺序从派生部分到基类部分。

  5. 对象初始化顺序

    实例化派生类对象时,先调用基类构造,再调用派生类构造。

  6. 对象销毁顺序

    销毁派生类对象时,先调用派生类析构,再调用基类析构。

  7. 析构函数重写注意

    因后续实现多态常重写析构函数,编译器会对析构函数名做特殊处理,形成隐藏关系。若基类析构函数未标记virtual,则子类析构函数和基类析构函数构成隐藏关系,可能导致析构不完全。

cpp 复制代码
class Person
{
public:
    Person(const char* name = "peter")
        : _name(name) 
    {
        cout << "Person()" << endl;
    }
    
    Person(const Person& p)
        : _name(p._name)
    {
        cout << "Person(const Person& p)" << endl;
    }
    
    Person& operator=(const Person& p)
    {
        cout << "Person operator=(const Person& p)" << endl;
        if (this != &p)
            _name = p._name;
        return *this;
    }
    
    ~Person()
    {
        cout << "~Person()" << endl;
    }
protected:
    string _name;  // 姓名
};

class Student : public Person
{
public:
    Student(const char* name, int num)
        : Person(name)
        , _num(num)
    {
        cout << "Student()" << endl;
    }
    
    Student(const Student& s)
        : Person(s)
        , _num(s._num)
    {
        cout << "Student(const Student& s)" << endl;
    }
    
    Student& operator=(const Student& s)
    {
        cout << "Student& operator=(const Student& s)" << endl;
        if (this != &s)
        {
            Person::operator=(s);
            _num = s._num;
        }
        return *this;
    }
    
    ~Student()
    {
        cout << "~Student()" << endl;
    }
protected:
    int _num;  // 学号
};

void Test()
{
    Student s1("jack", 18);
}

继承与友元

友元关系不能继承,即基类中声明为友元的类或函数,不能访问子类的私有或保护成员。

cpp 复制代码
class Student;
class Person
{
public:
 friend void Display(const Person& p, const Student& s);
protected:
 string _name; // 姓名
};
class Student : public Person
{
protected:
 int _stuNum; // 学号
};
void Display(const Person& p, const Student& s)
{
 cout << p._name << endl;
 cout << s._stuNum << endl;
}
void main()
{
 Person p;
 Student s;
 Display(p, s);
}

继承与静态成员

基类定义的 static 静态成员变量在整个继承体系中只有一份实例。无论有多少派生类对象,都共享同一个静态成员。

cpp 复制代码
class Person
{
public :
 Person () {++ _count ;}
protected :
 string _name ; // 姓名
public :
 static int _count; // 统计人的个数。
};
int Person :: _count = 0;
class Student : public Person
{
protected :
 int _stuNum ; // 学号
};
class Graduate : public Student
{
protected :
 string _seminarCourse ; // 研究科目
};
void TestPerson()
{
 Student s1 ;
 Student s2 ;
 Student s3 ;
 Graduate s4 ;
 cout <<" 人数 :"<< Person ::_count << endl;
 Student ::_count = 0;
 cout <<" 人数 :"<< Person ::_count << endl;
}

菱形继承的问题

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

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

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

菱形继承的问题:从下面的对象成员模型构造,可以看出菱形继承有数据冗余和二义性的问题。 在Assistant的对象中Person成员会有两份。

示例模型(对象成员)揭示菱形继承会引发二义性和数据冗余问题:

cpp 复制代码
class Person
{
public:
    string _name;  // 姓名
};

class Student : public Person
{
protected:
    int _num;  // 学号
};

class Teacher : public Person
{
protected:
    int _id;   // 职工编号
};

class Assistant : public Student, public Teacher
{
protected:
    string _majorCourse;  // 主修课程
};
cpp 复制代码
void Test()
{
    Assistant a;
    a._name = "peter";  // 二义性,编译器无法确认访问哪个基类的_name

    // 需明确指定访问路径解决二义性,但数据冗余依然存在
    a.Student::_name = "xxx";
    a.Teacher::_name = "yyy";
}

虚拟继承

为解决菱形继承中数据冗余和二义性问题,可采用虚拟继承

cpp 复制代码
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";  // 无二义性,只有一份 Person 成员
}

继承的总结和反思

  1. C++语法的复杂性,部分来源于多继承及菱形继承的底层实现。虚拟继承虽能解决菱形继承问题,但底层实现复杂且有一定性能开销,因此一般不建议设计多继承结构,更不要设计菱形继承。

  2. 多继承被视为 C++ 的缺陷之一,很多后续面向对象语言(如 Java)均不支持多继承。

  3. 继承与组合关系 :公有继承(public inheritance)是is-a 关系,即每个派生类对象都是一个基类对象。组合是has-a关系,表示一个类内部包含另一个类的对象。例如:

cpp 复制代码
// is-a 关系
class Car
{
protected:
    string _colour = "白色";  // 颜色
    string _num = "陕ABIT00";  // 车牌号
};

class BMW : public Car
{
public:
    void Drive() { cout << "好开-操控" << endl; }
};

class Benz : public Car
{
public:
    void Drive() { cout << "好坐-舒适" << endl; }
};

// has-a 关系
class Tire
{
protected:
    string _brand = "Michelin";  // 品牌
    size_t _size = 17;            // 尺寸
};

class Car
{
protected:
    string _colour = "白色";  // 颜色
    string _num = "陕ABIT00";  // 车牌号
    Tire _t;                   // 轮胎,组合关系
};
  • 建议优先使用组合以降低耦合度,提高代码维护性。继承破坏了基类封装,变化基类实现对派生类有较大影响,导致耦合度高。

  • 继承适用于表达"是一个"关系,且需要实现多态时;组合适用于"拥有一个"关系,是更安全、更灵活的复用手段。

笔试面试题举例

什么是菱形继承?其问题有哪些?

菱形继承(Diamond Inheritance) 是指在多继承中出现的一种特殊继承结构,其中一个派生类同时继承自两个有共同基类的父类,构成菱形结构。具体表现为:

cpp 复制代码
      A
     / \
    B   C
     \ /
      D

派生类 D 继承自 BC,而 BC 又都继承自同一个基类 A

问题:

  • 数据冗余 :派生类 D 会拥有两个独立的基类 A 子对象,导致内存中有两个 A 成员变量,相当于数据重复。
  • 二义性 :在访问基类 A 的成员时,如 D 中调用 A 的成员时是通过 B 继承得到的,还是通过 C 继承得到的?编译器无法确定,导致访问冲突。

什么是菱形虚拟继承?如何解决数据冗余和二义性?

菱形虚拟继承(Virtual Diamond Inheritance) 是解决菱形继承问题的技术手段。通过在所有继承公共基类的路径上使用virtual关键字,确保派生类沿各条路径共享同一个基类子对象,而不是创建多个独立副本。

例如:

复制代码
class A { ... };

class B : virtual public A { ... };

class C : virtual public A { ... };

class D : public B, public C { ... };

解决方案:

  • 数据冗余解决 :通过虚拟继承,派生类 D 只保留一个共享的基类 A 实例,消除多份数据冗余。
  • 二义性解决 :访问基类 A 的成员不再因为多继承路径而产生歧义,编译器明确且唯一地解析其位置,无需显式指定路径,避免访问冲突。

虚拟继承底层通过虚基表(VBT)和虚基表指针(VBPtr)实现,维护偏移量以正确定位唯一基类子对象。

继承与组合的区别?何时使用继承,何时使用组合?

特性 继承(is-a 关系) 组合(has-a 关系)
关系语义 "是一个"关系,例如学生是人 "拥有一个"关系,例如车有轮胎
封装性 破坏部分封装,派生类依赖基类实现 封装良好,只依赖公开接口
耦合度 高,基类变化影响派生类 低,修改部件不影响整体
灵活性 较低,类型固定 高,可动态组合不同部件
多态支持 支持多态,允许重写基类接口 一般不支持多态,但可通过接口实现类似效果
使用场景 需表达"是一个"的类型继承关系,且关注行为重用 组合复杂功能,灵活构建系统,关注模块化和扩展性

何时使用继承?

  • 当类之间存在明确的"是一个"关系。
  • 需要通过多态达到动态绑定和接口统一。
  • 想重用或扩展基类行为。

何时使用组合?

  • 当组件之间是"拥有"的关系。
  • 需降低耦合,提高代码灵活性和可维护性。
  • 希望功能通过组合多个对象实现,便于扩展和替换。

总结:

优先推荐使用组合来实现代码复用,只有在合理且明确的"是一个"关系且多态需求明确时,才采用继承。

相关推荐
blueshaw20 分钟前
CMake中的“包管理“模块FetchContent
c++·cmake
向日葵xyz1 小时前
Qt5与现代OpenGL学习(二)画一个彩色三角形
开发语言·qt·学习
LILI000001 小时前
C++静态编译标准库(libgcc、libstdc++)
开发语言·c++
小白学大数据1 小时前
基于Python的携程国际机票价格抓取与分析
开发语言·爬虫·python
碎梦归途1 小时前
23种设计模式-行为型模式之访问者模式(Java版本)
java·开发语言·jvm·设计模式·软考·软件设计师·行为型模式
孞㐑¥2 小时前
C++之特殊类设计及类型转换
开发语言·c++·经验分享·笔记
毛茸茸斑斑点点3 小时前
补题 (Multiples of 5)
c++
黄雪超4 小时前
JVM——Java的基本类型的实现
java·开发语言·jvm
VBA63374 小时前
VBA代码解决方案第二十四讲:EXCEL中,如何删除重复数据行
开发语言
CodeWithMe4 小时前
【中间件】bthread_基础_TaskControl
c++·中间件