问小白 wenxiaobai
资讯
历史
科技
环境与自然
成长
游戏
财经
文学与艺术
美食
健康
家居
文化
情感
汽车
三农
军事
旅行
运动
教育
生活
星座命理

C++继承详解(富含代码和通俗示例,小白也能0负担听懂)

创作时间:
作者:
@小白创作中心

C++继承详解(富含代码和通俗示例,小白也能0负担听懂)

引用
CSDN
1.
https://m.blog.csdn.net/TS2230090042/article/details/139156768

C++中的继承机制是面向对象程序设计中非常重要的概念,它允许程序员在保持原有类特性的基础上进行扩展,增加功能。本文将通过通俗易懂的语言和丰富的代码示例,帮助读者全面理解C++中的继承机制。

一、什么是继承?

定义:
继承(inheritance)机制是面向对象程序设计中使代码可以复用的最重要的手段,它允许程序员在保持原有类特性的基础上进行扩展,增加功能。这样产生的新类,称派生类(或子类),被继承的类称基类(或父类)。

继承呈现了面向对象程序设计的层次结构,体现了由简单到复杂的认知过程。之前接触的复用都是函数复用,继承是类设计层次的复用

是不是看的你眼睛都直发晕想睡觉?咱们还是来结合下面的代码说说人话吧哈哈。

简单来说,继承就是一种复用!

假设你是一个程序员,你想用一个类来描述老师这个概念,在你曾经的学习过程中,你已经学会了用一个类去描述人这个概念

可是聪明的你一想,老师也是一种特殊的人,为什么不在人的基础上去添加一些特征,让人变成老师呢?

然后你决定在描述人的那个类里面添加其他的成员,(比方说工号)等来添加更多的特征,把人变成一个老师。

可是,聪明的你发现了,如果我想描述一个医生,那你添加的那些成员,就没用了,你必须再把人的特征复制到医生类里面,再添加医生的专属特征,可是明明都具有人的特征,但是每次都要这样麻烦的改来改去,让你很累。于是你就来到了这里,打开了继承的大门。

#include<iostream>
using namespace std;
class Person
{
public:
    void Print()
    {
        cout << "name:" << name << endl;
        cout << "age:" << age << endl;
    }
protected:
    string name = "a";//姓名
    int age = 18;//年龄
};
class teacher :public Person
{
protected:
    int num=001;//工号
};
int main()
{
    teacher t;
    t.Print();
    return 0;
}  

你仔细看了看上面的代码和运行的结果,你似乎有一点感觉了!你发现teacher类通过下面的写法,好像具有了person的成员,也可以调用person的成员函数了!

class 新类的名字:继承方式 继承类的名字{};

恭喜你!你学会了继承的基本格式!

回到上面的代码,一般来说,person类(被继承的类)称为父类,或者基类。而teacher类,会被称为子类,或者派生类。可能出于中国的宗族观念我觉得父子会更生动,也更好理解。以下我都会采用父子类向你继续介绍。好了,你现在已经有一些基本的理解了!但是你还是有些地方不太明白,比如说继承的方式,继承的方式不同会影响哪些东西?

二、继承的分类

和类成员的访问限定一样,类的继承也分为public,protected,private三种类型。很复杂的是,类的继承方式和父类的成员限定碰撞在一起,决定了子类的成员限定。

9种!你看到这里是不是又开始了头痛了哈哈哈。

但是!其实没有那么复杂!你只用记住两个原则:

1.私有成员无论怎么继承,子类里面都是看不见的。(你想想,私有成员就相当于不希望被别人访问的成员,很像爸爸的私房钱,他巴不得只有自己知道,即使你是他宝贝娃,爸爸也不希望你发现他的私房钱,万一被你知道了告诉母上大人怎么办,所以他会瞒着你,不让你看见)

2.除去原则1,成员的访问限定和继承方式,哪个范围更小选哪个。

这里有一个补充小知识:class类默认私有继承,struct默认公有继承

三、父类和子类二三事

(1)父子之间的赋值问题

大原则:子类对象可以赋值给父类对象,但是父类对象不能赋值给子类对象

很简单,因为子类既然是继承来的,肯定有父类对象的那部分,但是父类大概率没有,是满足不了子类对象的。

那么子类怎么把父类对象那部分给赋值过去呢?

有点粗暴,但是,形象来说答案是:切过去,类似这样(注意!!这个过程有点类似于强制类型转换,但是由于父子类兼容规则,是不产生临时对象的!更准确一点来说其实分为下面三种情况)

可是怎么切呀?有三种切的方法

对象赋值对象切

teacher T;
Person t = T;
这就相当于一个子类对象把从父类继承过来的那部分重新赋值给了我们创建的父类对象,从监视窗口可看出,被赋值过后的person类对象t拥有的成员就是子类teacher类对象从父类继承的那部分

对象赋值给引用

teacher T;
Person& t1 = T;

对象地址赋值给指针

teacher T;
Person* t2 = &T;

(2)父子类里面之间的作用域问题

聪明的你应该已经理解了父子类之间的赋值。但是我相信你应该会有类似的疑问

如果父类和子类都具有同名的成员或者函数,进行访问的时候,我到底访问的是哪个呢?

试试看运行一下下面的代码,你就会知道答案。

#include<iostream>
using namespace std;
class Person
{
public:
    void Print()
    {
        cout << "name:" << name << endl;
        cout << "age:" << age << endl;
    }
protected:
    string name = "a";//姓名
    int age = 18;//年龄
    string sex = "女";
};
class teacher :public Person
{
public:
    void Print()
    {
        cout << "name:" << name << endl;
        cout << "age:" << age << endl;
        cout << "工号:" <<num<< endl;
    }
protected:
    string name = "小a老师";
    int num=001;//工号
};
int main()
{
    teacher T;
    T.Print();
    return 0;
}  

我们可以看到,即使父类和子类都有name变量以及Print这个打印函数,运行结果告诉我们,我们访问的都是子类里面的name和Print,我们一般把这种情况称之为隐藏,也叫做重定义。

那么如果我就是想访问父类的那个成员和成员函数呢?

我们可以这样去写,那么打印出来的就不是teacher类的“小a老师”,而是person类的"a"了

子类和父类中有同名成员,子类成员将屏蔽父类对同名成员的直接访问,这种情况叫隐藏, 也叫重定义。(在子类成员函数中,可以使用 父类::父类类成员 显示访问)

其次,对于成员函数,只要名字相同就构成了隐藏

需要值得注意的是,子类和父类其实是独立且不同的作用域,而且由于上述规则,会出现隐藏。所以在实际里使用继承的时候尽量不要定义同名的成员,容易产生混淆。

函数重载和函数隐藏的区别:

你也许在问,隐藏看起来和函数重载很像,但其实本质来说是不一样的。因为函数重载实际上是发生在同一作用域的,但是父类和子类是两个不同的类域,所以不属于函数重载。

(3)父子类与默认成员函数的二三事

敲一敲下面的代码,观察一下运行结果。

#include<iostream>
using namespace std;
class Person
{
public:
    Person(string Name="bob")
        :name(Name),age(20),sex("男")
    {
        cout << "Person()" << endl;
    }
    
    void Print()
    {
        cout << "name:" << name << endl;
        cout << "age:" << age << endl;
    }
    ~Person()
    {
        cout << "~Person" << endl;
    }
protected:
    string name = "a";//姓名
    int age = 18;//年龄
    string sex = "女";
};
class teacher :public Person
{
public:
    teacher(string Name, int Num)
        : Person(Name)
        , num(Num)
    {
        cout << "teacher()" << endl;
    }
    ~teacher()
    {
        cout << "~teacher()" << endl;
        
    }
    void Print()
    {
        cout << "name:" << Person::name << endl;
        cout << "age:" << age << endl;
        cout << "工号:" <<num<< endl;
    }
protected:
    string name = "小a老师";
    int num=001;//工号
};
int main()
{
    teacher T("小a老师",001);
    T.Print();
    return 0;
}  

构造函数与析构函数

聪明的你应该已经发现了,在继承关系里面构造函数与析构函数的调用有如下几个规则:

构造:
先父后子,即先调用父类的构造函数,再调用子类的构造函数(很好理解,没有父哪来的子呢?)

析构:
先子后父(这是为了防止子类析构函数如果在释放的时候还需要调用父类的成员变量,但是父类已经提前释放了无法访问导致程序出错的情况),理论上是需要调用子类的析构函数,在调用父类的析构函数。

但是你仔细观察应该发现了,我们并没有在子类teacher析构函数里面手动调用父类persond的析构函数呀,为什么还是调用了呢?

这是因为这个地方进行了特殊处理,为了防止某些程序员使用继承的时候忘记对父类进行析构造成内存泄漏,所以一般来说,编译器调用完子类析构函数的时候会自动调用父类的析构函数。所以不需要我们再在子类析构函数里面手动添加父类的析构函数了

你也许在想,照这个意思,父类析构函数与子类析构函数是什么关系呢?

你非常敏锐,其实父类析构函数与子类析构函数虽然名字不同,但是由于多态,编译器处理之后析构函数名字是一样的,都叫distrustor(),所以在一般情况下是构成了我们刚刚提到的隐藏关系

其实其他的拷贝构造,复制拷贝构造等等也是类似的,都是先父后子,我在此就不过多赘述了

(4)友元关系

友元关系是不能继承的哦,就像爸爸的朋友不一定是你的朋友

(5)父子类之间静态成员变量与静态成员函数之间的继承关系

你应该还记得在类里面一种特殊的变量——静态成员变量吧?静态成员变量是被整个类共有的变量,不属于任何一个对象,属于整个类的公共财产。这一点在继承时也不会改变。

无论派生出多少子类,静态变量及其静态成员函数的个数都不会改变,所谓继承静态变量与静态成员函数,继承的只是使用权,不是所有权

总结

好啦,看完这篇博客我觉得你一定对C++里面有关继承的部分有了比较清晰的简单认知,下一篇文章我们就要进化啦!我们将要来讨论一些更难更复杂的问题:有关于多重继承,菱形继承,和虚拟继承!

© 2023 北京元石科技有限公司 ◎ 京公网安备 11010802042949号