C++继承(最详细)

目录

1.继承的概念以及定义

1.1 继承的概念

1.2 继承的定义

​编辑

2.继承中的作用域

 3.基类和派生类间的转换

4.派生类的默认成员函数

5.实现不被继承的类

6.継承与友元

​编辑 

7.继承与静态成员

8.多继承及其菱形继承问题

8.2 虚继承

8.3 来看一个小题

9.继承和组合


学完STL后,咱们再转回来看语法,今天要讲的语法是继承,这也是一个算是比较难的语法,做好了?发车喽,来跟博主一起学习吧!

1.继承的概念以及定义

1.1 继承的概念

什么叫继承呢?继承算是属于对代码的复用。即在保持原有的特性上面,增加一些新的方法以及变量,就叫做继承。那么原有的类叫做基类(父类),增加新的方法以及变量之后的类叫做派生类(子类)。继承 呈现了⾯向对象程序设计的层次结构,体现了由简单到复杂的认知过程。以前我们接触的函数层次的 复用,继承是类设计层次的复用

来看这一段代码,那么观察可以发现,Student与Teacher这两个类都是属于派生类(因为它们都继承了基类Person的特性),而Person类属于基类。并且,派生类定义的对象也可以调用基类中的成员函数(这个待会讲)。OK,接下来看定义:

1.2 继承的定义

如上图,Person是基类,也称作父类。Student是派生类,也称作子类。

那么到这就会有人问了:限定符有三种,那么继承方式是不是也有三种?没错,很聪明,确实有三种。

那么访问限定符加上继承方式一组合,是不是有9种组合方式呀。

 当然,这里,基类的private成员,基本都是不可见的,(是不可见,不是没继承下来,别搞错了)。那么继承方式怎么看呢?

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

所以通过这个,咱们之前设计一个类,都是不知道啥设计成public,啥设计成protected,现在可能有点眉目了。

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

但这里涉及到一个问题,既然private,都不能访问,那我要它做什么?或者说我要private継承有什么用吗?所以说,在这,真正用到的也就两个,即public继承下的两个(基类public成员与基类protected成员)。

3.在实际运用中⼀般使用都是public继承,几乎很少使用protetced/private继承,也不提倡使用 protetced/private继承,因为protetced/private继承下来的成员都只能在派生类的类里面使用,实 际中扩展维护性不强。

4.使用关键字class时默认的继承方式是private,使用struct时默认的继承方式是public,不过最好显 示的写出继承方式。

2.继承中的作用域

隐藏规则:

1. 在继承体系中基类和派生类都有独立的作用域。

2. 派生类和基类中有同名成员,派生类成员将屏蔽基类对同名成员的直接访问,这种情况叫隐藏。 (在派生类成员函数中,可以使用基类::基类成员显示访问)

3. 需要注意的是如果是成员函数的隐藏,只需要函数名相同就构成隐藏。

4. 注意在实际中在继承体系里面最好不要定义同名的成员。

 看上面的代码图片,可以看出,如果只定义派生类的对象,用派生类的对象调用_num,那么默认调用的是派生类中的_num(就近原则)。若是想调用基类中的_num,必须得在调用的那个函数里指定作用域。还有一些注意事项,都在上面的代码图片中。反正如果说基类与派生类中有同名的成员函数或者成员变量,优先调用派生类中的(就近原则)。

在这里需要强调一个东西就是:重载:重载是函数名相同,参数类型或者参数个数不同,并且构成重载的函数必须在同一个作用域中。比如:基类跟派生类是两个不同的作用域,所以不构成重载。

隐藏:函数名相同并且作用域不在同一个作用域就构成了隐藏。

class A

{

        public:

        void fun()

        {

                cout << "func()" << endl;

        }

};

class B : public A

{

        public:

        void fun(int i)

        {

                cout << "func(int i)" <<i<<endl;

        }

};

int main()

{

        B b;

        b.fun(10);

        b.fun();

        return 0;

}

那么看上面的代码,A类与B类中的fun()函数构成了隐藏(注意不是重载)。并且这个程序执行不了,因为虽然第一个函数可以调用,调用了派生类中的fun函数,但是第二个函数在派生类中找不到,那么找不到就无法调用,程序就无法运行。

 3.基类和派生类间的转换

1.public继承的派生类对象可以赋值给基类的指针/基类的引用。这里有个形象的说法叫切片或者切 割。寓意把派生类中基类那部分切出来,基类指针或引用指向的是派生类中切出来的基类那部分。

2.基类对象不能赋值给派生类对象。

3.基类的指针或者引用可以通过强制类型转换赋值给派生类的指针或者引用。但是必须是基类的指针 是指向派生类对象时才是安全的。

废话少说,上代码:

class Person

{

protected :

string _name; // 姓名

string _sex; // 性别

int _age; // 年龄

};

class Student : public Person

{

public :

int _No ; // 学号

};

int main()

{

Student sobj ; // 1. 派生类对象可以赋值给基类的指针 / 引用

Person* pp = &sobj;

Person& rp = sobj;

// 派生类对象可以赋值给基类的对象是通过调用后面会讲解的基类的拷贝构造完成的

Person pobj = sobj;

//2. 基类对象不能赋值给派生类对象,这里会编译报错

sobj = pobj;

return 0;

}

这个切片的作用就是将派生类中属于基类的那一部分给切出来,怎么切呢,就通过上面所说的方法即可。 这个切片后面代码有作用,再带大家来进一步的理解。

4.派生类的默认成员函数

这个默认成员函数,还是类的那六个,因为派生类也属于类嘛。

先来看一段代码吧:

class Person
{
public:
    Person(const char*name="peter",const char *telephone="1234")
        : _name(name)
        ,_telephone(telephone)
    {
        cout << "Person()" << endl;
    }

    Person(const Person& p)
        : _name(p._name)
        ,_telephone(p._telephone)
    {
        cout << "Person(const Person& p)" << endl;
    }

    Person& operator=(const Person& p)
    {
        cout << "Person operator=(const Person& p)" << endl;
        if (this != &p)
            _name = p._name;
        _telephone = p._telephone;

        return *this;
    }

    ~Person()
    {
        cout << "~Person()" << endl;
    }
protected:
    string _name="kate"; // 姓名//有初始化列表先走初始化列表,无初始化列表就用
    //缺省值,反正,最后都类似的要走初始化列表,并且,若派生类中有对基类的初始化
    //还必须得看派生类中的。
    string _telephone="0000";
};
class Student : public Person
{
public:
    Student(const char* name, const char* telephone, int num)
        : Person(name,telephone)//如将基类中的初始化列表屏蔽了再去执行这个
        //会发现执行不通,是因为这个就是得调用基类的初始化列表,但是基类的初始化
        //列表被咱屏蔽了呀。那肯定会报错的
         ,_num(num)
    {
        cout << "Student()" << endl;
    }

    Student(const Student& s)
        : Person(s)//这个地方要把派生类中的基类那一部分给拿出来
        //那么切片,就是将派生类对象赋值给基类的引用或指针,叫切片
        //并且这里只需要这么写person(s)。因为上面的基类中是&p,
        //所以说意思就是将派生类中的基类的那一部分给切下来给基类,交给他们进行
        //初始化即可。当然,若基类中只有一个成员变量,可能你说有点多余,但是基类中
        //有好多个成员变量呢?直接把他们切下来就非常好用了。
        , _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; //学号

};
int main()
{
    Student s1("jack","23456", 18);
    Student s2(s1);
    Student s3("rose","56789" ,17);
    s1 = s3;//按理说派生类的拷贝构造与析构都是不需要自己去写的,只有一个构造需要自己写
    //一般写一个类后,最好还是自己实现一下构造函数,即初始化
    //若其默认构造函数的行为不符合需求(例如需要特定参数),需显式调用其他构造函数
    //但一般默认构造函数也就会将它们初始化为空(string)
    //接下来,我需要确认std::vector的默认构造函数的定义。根据C++标准,
    // 默认构造函数会创建一个空的vector,没有元素,容量为0。
    // 这意味着它不会分配任何内存,只是初始化内部指针为nullptr,大小和容量为0。
    //cplusplus网站上的构造的第一个就是默认构造函数的行为
    return 0;
}

 

1.关于构造函数部分:由于派生类中有基类的那一部分,子类继承父类,创建一个子类对象的时候,这个子类对象中有父类的部分的,是需要调用父类的构造来构造这部分内容的。所以说要先调用基类的构造函数,先完成对基类的构造,之后再构造派生类。

2.派生类的拷贝构造函数必须调用基类的拷贝构造完成基类的拷贝初始化。

3.派生类的operator=必须要调用基类的operator=完成基类的复制。需要注意的是派生类的 operator=隐藏了基类的operator=,所以显示调用基类的operator=,需要指定基类作用域。

4.来看它的析构函数:

这是上面那个代码的执行结果,观察这个我们发现,有三个一样的~student(),~person(),这个是因为有三个对象,需要析构三次。关键的问题是不是只析构一次吗?为什么会出现两个内容?(~student()与~person())原因是:

这里先调用了派生类的析构函数,之后调用了基类的析构函数,那么为什么先调用派生类的析构函数呢?因为,派生类是依靠基类来实现的吧,所以说,如果先析构了基类,那么依靠基类的派生类中的资源是不是就是不安全的了(因为基类都被析构了),所以为了确保派生类的安全,就先析构派生类,这样可以避免资源安全的问题。

还有一个问题就是,我能不能在这里显示的写一个基类的析构函数,不可以的,原因是,你只要显示写了,大概率会先调用基类的析构函数,那么经过咱们上面的解释,是不是先调用基类的析构函数不安全。并且,子类的析构函数是比较特殊的一个函数,我们不需要显示调用父类析构函数,每个子类析构函数后面,会自动调用父类析构函数。所以说析构函数不需要咱们去管就行了。

还有一个问题:基类的析构函数要与派生类的析构函数名字保持一致。因为多态中⼀些场景析构函数需要构成重写,重写的条件之⼀是函数名相同(这个多态章节会讲 解)。那么编译器会对析构函数名进行特殊处理,处理成destructor(),所以基类析构函数不加 virtual的情况下,派生类析构函数和基类析构函数构成隐藏关系。 

5.实现不被继承的类

这里咱们讲C++11的用法,因为C++11的用法较C++98更简单。

在基类后面加上final关键字,那么这个基类就不可以被继承了

6.継承与友元

 

基类的友元是不可以被继承到派生类中的(就像父亲的朋友不是你的朋友)

这里有两个问题:

1.基类中的友元声明,并不知道student类在哪。那有同学又说了要是把派生类放基类前面不就可以了吗?那请问你这样的话,这个継承还怎么继承呢?所以最好的办法就是在开头加上class student,让编译器知道有这个类。

2.如果不在student类中加上友元声明,那么下面的友元函数中的第二个就无法打印,因为类外无法访问类中的protected限定的成员变量,除非也在student类中加上友元声明 。

7.继承与静态成员

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

8.多继承及其菱形继承问题

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

多继承:⼀个派生类有两个或以上直接基类时称这个继承关系为多继承,多继承对象在内存中的模型 是,先继承的基类在前⾯,后⾯继承的基类在后⾯,派生类成员在放到最后⾯。

菱形继承:菱形继承是多继承的⼀种特殊情况。菱形继承的问题,从下⾯的对象成员模型构造,可以 看出菱形继承有数据冗余和二义性的问题,在Assistant的对象中Person成员会有两份。支持多继承就 ⼀定会有菱形继承,像Java就直接不支持多继承,规避掉了这里的问题,所以实践中我们也是不建议 设计出菱形继承这样的模型的。 

这种就是菱形继承问题,那么就会导致Assistant中有两个person中的成员变量,会冗余。并且,你要是调用比如student类继承的person变量,不可以,因为编译器不知道你要调用哪个(因为teacher中,person中都有这个成员变量)。

其实这种也是菱形继承:B继承了A,C继承了B,D继承了A,E继承了C和D,那么E就有了两份A

只要派生类中有两份相同的变量时,都叫做菱形继承,那么在B和D的位置放virtual,原因下面有。

 

解决办法就是:

1.指定作用域是哪个类。

2.采用虚继承的方式。

 

8.2 虚继承

在继承那个双份的成员变量的第一个派生类中加上virtual即可。virtual加在:后,继承方式前。

那么加上这个后,你再去访问那个双份的成员变量,访问到的就只有最一开始的基类中的成员变量。要是还想访问类中的那个成员变量,加上作用域即可。

8.3 来看一个小题

class Base1

{

        public:

        int _b1;

};

class Base2

{

        public:

        int _b2;

};

class Derive : public Base1, public Base2

{

        public: int _d;

};

int main()

{

        Derive d;

        Base1* p1 = &d;

        Base2* p2 = &d;

        Derive* p3 = &d;

        return 0; 

}

看这个题,main函数里的看着熟悉不,没错,就是咱们说的切片,那么这个问题的本质就是,这个切片在内存中是怎么存储的,就是先先继承的内存更靠上 。

 

p3是指向派生类的指针,自然指向最上面,地址的起始处。而p1是最先继承的,所以也在地址最高处,而p3与p1指向同一个地方,纯属巧合。自然p2的位置就相对于靠后点。

所以说它们几个的关系是: p3==p1!=p2

9.继承和组合

1.public继承是⼀种is-a的关系。也就是说每个派生类对象都是⼀个基类对象。

 2.组合是⼀种has-a的关系。假设B组合了A,每个B对象中都有⼀个A对象。

 3.继承允许你根据基类的实现来定义派生类的实现。这种通过生成派生类的复用通常被称为白箱复⽤ (white-box reuse)。术语“白箱”是相对可视性而言:在继承方式中,基类的内部细节对派生类可 见。继承⼀定程度破坏了基类的封装,基类的改变,对派生类有很大的影响。派生类和基类间的依 赖关系很强,耦合度高

 4.对象组合是类继承之外的另⼀种复⽤选择。新的更复杂的功能可以通过组装或组合对象来获得。对 象组合要求被组合的对象具有良好定义的接口。这种复⽤风格被称为黑箱复用(black-boxreuse), 因为对象的内部细节是不可见的。对象只以“黑箱”的形式出现。组合类之间没有很强的依赖关 系,耦合度低。优先使用对象组合有助于你保持每个类被封装。

 5.优先使用组合,而不是继承。实际尽量多去用组合,组合的耦合度低,代码维护性好。不过也不太 那么绝对,类之间的关系就适合继承(is-a)那就⽤继承,另外要实现多态,也必须要继承。类之间的 关系既适合用继承(is-a)也适合组合(has-a),就用组合。

继承的写法

template<class T>

class stack :public vector<T>

{};

组合的写法

template<class T>

class stack

{

public:

vector<T> _v;

};

 

 

这里有个耦合度的概念:可以把耦合度理解为联系,耦合度高就是联系紧密,耦合度低就是联系松散。那么咱们写一个项目,有几百个类,那么使用耦合度低的,还是耦合度高的?肯定是耦合度低的,你想想,如果说,你在写项目的时候,一个类写错了,那么入过这个项目的耦合度高,会牵连到其他的几个类,甚至说整个项目,导致整个项目会出现修不完的bug,严重的时候,可能要重写。这个代价成本可是非常大的。所以说有耦合度低的肯定都会选择耦合度低的。

而上文提到的白箱与黑箱,就是白箱就是可视化程度很高,即 继承,因为继承的话,基类中的所有派生类都可以看到,也就是耦合度高。而黑箱,就是可视化很低,也就说明每个类之间耦合度低,所以说,没有啥联系,互相之间可看到的代码也就很少,符合类的封装。

OK,可算是讲完了,大家若是发现错误,还请指出,谢谢啦!

本篇完.........................

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/902484.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

day35图像处理OpenCV

文章目录 一、图像预处理17 直方图均衡化17.1绘制直方图17.2直方图均衡化1. 自适应直方图均衡化2. 对比度受限的自适应直方图均衡化3. 示例 19 模板匹配 一、图像预处理 17 直方图均衡化 直方图&#xff1a;反映图像像素分布的统计图&#xff0c;横坐标就是图像像素的取值&…

【音视频】FFmpeg内存模型

FFmpeg内存模型 从现有的Packet拷贝一个新Packet的时候&#xff0c;有两种情况&#xff1a; 两个Packet的buf引用的是同一数据缓存空间&#xff0c;这时候要注意数据缓存空间的释放问题&#xff1b;两个Packet的buf引用不同的数据缓存空间&#xff0c;每个Packet都有数据缓存…

1.2软考系统架构设计师:系统架构的定义与作用 - 练习题附答案及超详细解析

系统架构定义与作用综合知识单选题 题目覆盖核心概念、发展历程、设计原则、评估标准及易混淆点&#xff0c;附答案解析&#xff1a; 1. 系统架构的标准定义源自于以下哪个标准&#xff1f; A. ISO/IEC 9126 B. IEEE 1471-2000 C. TOGAF 9.2 D. ITIL v4 答案&#xff1a;B 简…

go语言对http协议的支持

http&#xff1a;无状态协议&#xff0c;是互联网中使用http使用http实现计算机和计算机之间的请求和响应 使用纯文本方式发送和接受协议数据&#xff0c;不需要借助专门工具进行分析就知道协议中的数据 服务器端的几个概念 Request&#xff1a;用户请求的信息&#xff0c;用…

iscsi服务端安装及配置

1. 安装targetcli软件包 yum install -y targetcli 2. 启动target服务 systemctl start target systemctl enable target 3. 配置防火墙 firewall-cmd --add-port"3260/tcp" 3. 准备一个物理分区&#xff08;或者逻辑分区&#xff09;…

解决 MongoDB 查询中的 `InvalidMongoDbApiUsageException` 错误

您在使用 Spring Data MongoDB 时遇到了 InvalidMongoDbApiUsageException 异常&#xff0c;错误信息如下&#xff1a; “由于 com.mongodb.BasicDocument 的限制&#xff0c;您无法添加第二个 ‘null’ 条件。查询已经包含 ‘{ “KaTeX parse error: Expected }, got EOF at e…

一个关于相对速度的假想的故事-4

回到公式&#xff0c; 正写速度叠加和倒写速度叠加的倒写相等&#xff0c;这就是这个表达式所要表达的意思。但倒写叠加用的是减法&#xff0c;而正写叠加用的是加法。当然是这样&#xff0c;因为正写叠加要的是单位时间上完成更远的距离&#xff0c;而倒写叠加说的是单位距离需…

重学React(一):描述UI

背景&#xff1a;React现在已经更新到19了&#xff0c;文档地址也做了全面的更新&#xff0c;上一次系统性的学习还是在16-17的大版本更新。所以&#xff0c;现在就开始重新学习吧&#xff5e; 学习内容&#xff1a; React官网教程&#xff1a;https://zh-hans.react.dev/lea…

AI大模型:(二)2.3 预训练自己的模型

目录 1.预训练原理 2.预训练范式 1.未标注数据 2.标注数据 3.有正确答案、也有错误答案 3.手撕transform模型 3.1.transform模型代码 3.2.训练数据集 3.3.预训练 3.4.推理 4.如何选择模型

gradle可用的下载地址(免费)

这几天接手一个老项目&#xff0c;想找gradle老版本的&#xff0c;但一搜&#xff0c;虽然在CSDN上搜索出来一堆&#xff0c;但都是收费&#xff0c;有些甚至要几十积分(吃相有点难看了)。 我找了一个能访问的地址&#xff0c;特地分享出来&#xff0c;有需要的自取&#xff01…

vue3新增特性

一、Vue 3 新增特性 1. Composition API 概述: Composition API 提供了一种更灵活和强大的方式来组织和复用逻辑。适用于复杂组件和逻辑复用场景。主要功能: setup 函数:组件的入口点,用于定义响应式数据、方法、生命周期钩子等。响应式 API:ref 和 reactive 提供更细粒度…

前端性能优化全攻略:JavaScript 优化、DOM 操作、内存管理、资源压缩与合并、构建工具及性能监控

1 为什么需要性能优化&#xff1f; 1.1 性能优化的核心价值&#xff1a;用户体验与业务指标 性能优化不仅是技术层面的追求&#xff0c;更是直接影响用户体验和业务成败的关键因素。 用户体验&#xff08;UX&#xff09;&#xff1a; 响应速度&#xff1a;用户期望页面加载时…

【Unity笔记】Unity + OpenXR项目无法启动SteamVR的排查与解决全指南

图片为AI生成 一、前言 随着Unity在XR领域全面转向OpenXR标准&#xff0c;越来越多的开发者选择使用OpenXR来构建跨平台的VR应用。但在项目实际部署中发现&#xff1a;打包成的EXE程序无法正常启动SteamVR&#xff0c;或者SteamVR未能识别到该应用。本文将以“Unity OpenXR …

Curl用法解析

Curl 用法解析 简介 Curl 是一个强大的命令行工具&#xff0c;主要用于从服务器发送 HTTP 请求并获取数据。它广泛应用于调试 RESTful API、文件上传下载、模拟用户交互等多种场景。下面是一些基本用法及常见参数的分析&#xff1a; 基础用法 curl [options] [URL]其中最基…

C语言教程(十一):C 语言中四种主要作用域及作用域嵌套遮蔽

一、引言 在 C 语言里&#xff0c;作用域指的是程序中变量、函数、类型等标识符能够被使用的范围。C 语言里有四种主要的作用域&#xff1a;块作用域、函数作用域、文件作用域和原型作用域&#xff0c;下面为你展开介绍&#xff1a; 二、块作用域 定义&#xff1a;块作用域是 C…

初次尝试Ghidra

最近看京东读书上有本书叫《Ghidra权威指南》&#xff0c;竟然是美国国家安全局出品的逆向工具&#xff0c;我真是孤陋寡闻&#xff0c;第一次听说。赶紧试试。 Release Ghidra 11.3.2 NationalSecurityAgency/ghidra GitHub 最新版本竟然是上周发布的&#xff0c;看来很活…

乐视系列玩机---乐视2 x620 x628等系列线刷救砖以及刷写第三方twrp 卡刷第三方固件步骤解析

乐视2 x620 x628 x626等,搭载了Helio X20处理器,mtk6797芯片。 通过博文了解💝💝💝 1💝💝💝-----详细解析乐视2 x620系列黑砖线刷救砖的步骤 2💝💝💝----官方两种更新卡刷步骤以及刷写第三方twrp过程与资源 3💝💝💝----乐视2 mtk系列机型救砖 刷…

web原生API AbortController网络请求取消方法使用介绍:防止按钮重复点击提交得最佳方案

在前端开发中&#xff0c;取消网络请求是一个常见的需求&#xff0c;尤其是在用户频繁操作或需要中断长时间请求的场景下。 AbortController 主要用于 ​优雅地管理和取消异步操作&#xff1a; 浏览器原生 API 一、代码解析 1. ​创建 AbortController 实例 const controlle…

2025智能驾驶趋势评估

以下是对2025年智能驾驶趋势的评估&#xff1a; 技术发展 • 自动驾驶级别提升&#xff1a;2025年有望成为L3级自动驾驶的商用元年。L3级自动驾驶技术开始从高端车型向20万元以下价格带下沉&#xff0c;部分车企如江淮和华为合作的尊界S800、小鹏汽车等都在积极推进L3级自动驾驶…

Spring MVC DispatcherServlet 的作用是什么? 它在整个请求处理流程中扮演了什么角色?为什么它是核心?

DispatcherServlet 是 Spring MVC 框架的绝对核心和灵魂。它扮演着前端控制器&#xff08;Front Controller&#xff09;的角色&#xff0c;是所有进入 Spring MVC 应用程序的 HTTP 请求的统一入口点和中央调度枢纽。 一、 DispatcherServlet 的核心作用和职责&#xff1a; 请…