【C++闯关笔记】深究继承

系列文章目录

上一篇:【C++闯关笔记】模板的特化-CSDN博客


文章目录

目录

系列文章目录

文章目录

前言

一、继承是什么?

1.继承的概念

2.继承的定义方法

继承方式与访问限定

二、继承相关知识

1.切割

2.隐藏

3.派生类中的默认成员函数

4.友元与静态成员

三、虚继承

1.菱形继承

2.虚继承

总结



前言

为什么C++要引入继承?看下面两个类。

我们观察到下面的Student类,与Teacher类有很多重复的代码,造成了代码冗余有没有什么方法像模板一样能够使代码可以复⽤呢?------继承应运而出。

cpp 复制代码
class Student 
{
public:
	void Learn()
    {
        ......;
    }
private:
	int _ID;
	string _name;
    char _sex;
    string _addr;
};

class Teachr
{
public:
	void Teach()
    {
        ......;
    }
private:
	int _ID;
	string _name;
    char _sex;
    string _title; 
};

一、继承是什么?

继承,是C++⾯向对象设计中使代码可以复⽤的最重要的⼿段之一,它允许我们在保持原有类特性的基础上进⾏扩展,增加方法(成员函数)和属性(成员变量),这样产⽣新的类,称派⽣类。

1.继承的概念

我们将上述的Student类与Teacher类中重复的代码提炼出,设计出一个新的Person类:

cpp 复制代码
class Person
{
private:
	int _ID;
	string _name;
    char _sex;
};

将公共的成员都放到Person类中,Student和teacher都继承Person,这样就可以复⽤这些成员,就不需要重复定义了,这样既整洁又省去了很多⿇烦。

Student类与Teacher类继承于Person:

cpp 复制代码
class Student : public Person
{
public:
	void Learn()
    {}
private:
   int _major;//专业
};

class Teachr: public Person
{
public:
	void Teach()
    {}
private:
    string _title; //职称
};

其中Person是基类 ,也称作**⽗类** 。Student是派生类 ,也称作**⼦类**。

当用Student 创建对象后,对象中除了本身的 _major外还有从Person中继承的:_ID _name _sex。

换句话说,派生类对象由基类子对象(即从基类继承的成员)和派生类自己的成员组成。

2.继承的定义方法

如上述的Student类继承于Person类,继承的定义方法为:

在子类(派生类)之后加一个 ' : ' 与继承方式,再跟上父类(基类)的类名。

继承语法的核心在于继承的方式。

继承方式与访问限定

子类继承父类的方式有三类:public、protected、private,它们分别对应子类对父类的三种访问限定。

访问限定解释

已知父类中的类成员有三种类型:

public:外界可访问;

protected:外界不可访问,子类可访问;

private:外界与子类均不可访问。

继承方式也有三种,那么关于继承基类成员访问方式就有3 * 3 = 9形态,如下图所示。

助记总结

①基类private成员在派⽣类中⽆论以什么方式继承都是不可见的。

**②如果基类成员不想在类外直接被访问,但需要在派生类中能访问,就定义为protected。**可以看出保护成员限定符是因继承才出现的。

③除去基类的private成员,其他成员在派生类的访问方式 == Min(成员在基类的访问限定符,继承方式),public >protected> private

细节提醒

①即使基类中private成员不可被子类访问,但它们还是被继承到了派⽣类对象中,只是语法上限制派⽣类对象不管在类⾥⾯还是类外⾯都不能去访问它们。

②若子类使用关键字class继承时默认的继承⽅式是private,使⽤struct时默认的继承⽅式是public,不过最好显示的写出继承⽅式。

二、继承相关知识

1.切割

如果是public继承的派生类,那么它的对象可以赋值给基类的指针/基类的引用/基类对象。这⾥有个形象的说法叫切割:寓意把派⽣类中基类那部分切出来,基类指针或引⽤指向的是派⽣类中切出来的基类那部分。

但基类对象不能赋值给派⽣类对象。

派生类对象赋值给基类对象过程解析

cpp 复制代码
Person person = student;  // 实际发生的过程:
  • 编译器发现studentStudent类型,但personPerson类型

  • 它知道Student继承自Person,所以student对象中包含Person部分

  • 编译器提取 student对象中的Person子对象

  • 然后调用Person类的拷贝构造函数,用提取的Person子对象来初始化person

实际上等价于:

复制代码
Person person(static_cast<Person>(student));  // 提取基类部分,然后拷贝构造

代码示例

cpp 复制代码
int main()
{
     Student s ;
     // 1.派⽣类对象可以赋值给基类的指针/引⽤
     Person* pp = &s;
     Person& rp = s;

     //2.基类对象不能赋值给派生类指针/引用
     Person p;
     //这里会报错
     Student* sp = &p;
     Student& rp = p;

     //3.派生类对象赋值给基类对象
     p = s;
}

2.隐藏

①如果派⽣类和基类中有同名成员,派生类成员将屏蔽基类同名成员,这种情况叫隐藏

②如果是成员函数的隐藏,则只需要函数名相同就构成隐藏。

③在派⽣类成员函数中,可以使⽤基类::基类成员显示访问。

比如Person中有个_ID成员,如果Student在继承Person后再定义一个_ID成员,这就构成了隐藏,如:

cpp 复制代码
class Person
{
public:
    void func(){}
private:
	int _ID;
};

class Student
{
public:
    void func(int x){}
    int getID(){return _ID;}
private:
    int _ID;
}

上述Student中的_ID隐藏了从Person中继承下来的_ID,当调用getID函数返回的是Student类中的ID;Student中的func函数隐藏了从Person中继承下来的func函数。

3.派生类中的默认成员函数

默认成员函数,意思是指我们不写,编译器会变我们⾃动⽣成。常用到的包括:默认构造函数、默认拷贝构造函数、默认赋值运算符函数、默认析构函数。

默认构造函数派⽣类的构造函数必须调⽤基类的构造函数初始化基类的那⼀部分成员。如果基类中没有默认的构造函数,则必须在派⽣类构造函数的初始化列表阶段显⽰调⽤基类的构造函数初始化派生类中继承的基类对象。

默认拷贝构造函数派⽣类的拷⻉构造函数必须调⽤基类的拷⻉构造,以完成派生类对象中继承的基类成员的拷⻉初始化。

默认赋值运算符函数:派⽣类的operator=必须要调⽤基类的operator=完成基类的复制。

默认析构函数:派生类的析构函数会在被调用完成后自动调⽤基类的析构函数清理基类成员。

细节注意:

①派⽣类的 operator=隐藏了基类的operator=,所以显⽰调⽤基类的operator=需要指定基类作⽤域。

②派⽣类对象初始化先调⽤基类构造再调派⽣类构造。

③派⽣类对象析构清理会先调⽤派⽣类析构再调基类的析构,以保证派生类对象先清理派⽣类成员再清理基类成员的顺序。

什么时候需要自实现上述默认构造函数?

①当子类没有动态资源管理需求时,通常只需要关注构造函数,编译器会自动生成其他特殊成员函数。

②如果需要自定义析构函数(有动态资源管理需求时)、拷贝构造函数或拷贝赋值运算符中的任何一个,那么这三个通常都需要显示实现。

4.友元与静态成员

友元关系不能继承,也就是说基类友元不能访问派⽣类私有和保护成员 。

基类定义了static静态成员,则整个继承体系⾥⾯只有⼀个这样的成员。⽆论派⽣出多少个派⽣类,都 只有⼀个static成员实例。

三、虚继承

1.菱形继承

单继承:⼀个派⽣类只有⼀个直接基类时称这个继承关系为单继承。

多继承:⼀个派⽣类有两个或以上直接基类时称这个继承关系为多继承。

什么是菱形继承?

菱形继承:菱形继承是多继承的⼀种特殊情况。如定义一个Doctor(博士类),继承于Student与Teacher类(博士既是学生也可能是助教),但Student与Teacher又同时继承于Person类,这就是菱形继承。

菱形继承有数据冗余和⼆义性的问题 ,在Doctor实例化的对象中Person成员会有两份。

2.虚继承

虚继承的概念

继承是C++中用于解决多重继承中菱形继承问题的一种机制 。在菱形继承结构中,一个类(派生类)从两个类继承,而这两个类又共同继承自同一个基类,这样就会导致派生类中包含两份基类的成员,从而产生二义性。虚继承可以确保在菱形继承中,最终派生类中只包含一份基类子对象。

虚继承语法

在定派生类时,若意识到形成了菱形继承而产生了数据冗余和二义性时,**需要在继承语法前加上virtual关键字。**如

cpp 复制代码
class Person
{}
class Student : virtual public Person
{};
class Teachr: virtual public Person
{}

class Doctor:public Student ,public Teacher
{

}

注意:产生的冗余数据属于哪个类,就在那个类的子类继承它时,加上virtual。

尽管有虚继承解决菱形继承问题,但同时也带来了额外的复杂性和性能开销。

多继承被公认为C++的缺陷之⼀,后来的⼀些编程语⾔(如Java)都没有多继承,故在实际编程中尽量不要设计出使用菱形继承的类。


总结

本文主要介绍了C++三驾马车之一的继承机制。

首先,介绍了继承的概念与定义方法,并额外介绍继承方式与访问限定的关系,总结了基类成员在派生类的访问方式 == Min(成员在基类的访问限定符,继承方式),public >protected> private;

之后,介绍了继承相关的知识,如基类与派生类之间的切割问题、隐藏问题、默认成员函数行为以及友元与静态成员;

最后,介绍了单继承、多继承,并着重介绍虚拟继承与菱形继承问题。

相关推荐
又是忙碌的一天1 天前
Java IO流
java·开发语言
程序员buddha1 天前
springboot-mvc项目示例代码
java·spring boot·mvc
不懂英语的程序猿1 天前
【Java 工具类】Java通过 TCP/IP 调用斑马打印机(完整实现)
java
卡提西亚1 天前
C++笔记-25-函数模板
c++·笔记·算法
多多*1 天前
分布式系统中的CAP理论和BASE理论
java·数据结构·算法·log4j·maven
sg_knight1 天前
Docker 实战:如何限制容器的内存使用大小
java·spring boot·spring·spring cloud·docker·容器·eureka
合作小小程序员小小店1 天前
web网页开发,在线考勤管理系统,基于Idea,html,css,vue,java,springboot,mysql
java·前端·vue.js·后端·intellij-idea·springboot
R&L_201810011 天前
C++之内联变量(Inline Variables)
c++·c++新特性
小白程序员成长日记1 天前
2025.11.10 力扣每日一题
数据结构·算法·leetcode
IT阳晨。1 天前
【QT开发】交叉编译QT程序在ARMLinux平台上运行
c++·qt·交叉编译·armlinux·代码移植