【C++】继承总结

news2024/11/29 12:36:47

一、前言

        我们众所周知的C++三大特性分别为:封装、继承、多态。

        封装就是将接口实现统一化,隐藏那些不同的地方,在上层函数调用体现的方式一样,如各种容器的迭代器iterator,尽管底层实现的方式不同,但是在使用时都是一样的,这就是封装的魅力。而今天我们接触的继承,也有别样的魅力!

二、继承的概念

        比如孩子继承了父亲的家业家产,这就属于从父亲那边得到了父亲的物品归自己所用,那也就是孩子可以复用父亲的家业和家产,所以继承的概念很简单,就是复用

在C++中,关于继承是这样定义的:

        继承机制是面向对象程序设计使代码可以复用的最重要的手段,它允许程序员在保持原有类特性的基础上进行扩展,增加功能,产生新的类;

        新类可以使用原有类的成员函数和成员变量,就相当于新的类继承了原有类的成员。

        以前我们接触的复用都是函数复用,本篇博客的继承是类设计层次的复用。

被继承的类: 

基类 / 父类;

继承  基类/  父类 的类: 

派生类 / 子类

继承的示例:

#include <iostream>
using namespace std;
//基类
class Person
{
public:
    void Print()
    {
        cout << "name:" << _name << endl;
        cout << "age:" << _age << endl;
    }
protected:
    string _name = "peter"; // 姓名
    int _age = 18; // 年龄
};
//派生类
class Student : public Person //继承方式
{
private:
    int _stuid; // 学号
};
class Teacher : public Person //继承方式
{
private:
    int _jobid; // 工号
};
int main()
{
    Student s;
    Teacher t;
    s.Print();
    t.Print();
    return 0;
}

运行结果:

结果分析:

        上述代码就是一个继承的样例,Student和Teacher都继承的是Person,那Student和Teacher为派生类,Person是基类,我们可以看到Stdent和Teacher都使用了Person的Print函数,也就是说明我们成功继承了Person的类里面的内容。

        但是具体继承的规则是什么样的呢?

三、继承的规则

        首先我们来回顾一下访问限定符,分别为public、protected、private

        它们控制的是我们是否可以在类外面来使用类里面的成员函数或成员变量。众所周知,只有public允许我们来使用类里面的成员,而private和protected是不允许的。

当派生类继承基类的成员时,是否要遵守基类的访问限定符呢?

而派生类的继承方式,是否会控制基类的访问限定符呢?

下面是继承方式与访问限定符的权限统一表格:(权限依旧是只可缩小,不可放大

                                                     public > protected > private

类成员  \  继承方式public继承protected继承private继承
基类的public成员变成 派生类的public成员派生类的protected成员派生类的private成员
基类的protected成员变成 派生类的protected成员派生类的protected成员派生类的private成员
基类的private成员在派生类中不可见在派生类中不可见在派生类中不可见

注意事项:

  • 基类private成员被派生类继承了,但是派生类没有权限访问
  • 基类private成员在派生类中是不能被访问,如果基类成员不想在类外直接被访问,但需要在派生类中能访问,就定义为protected,所以保护成员限定符是因继承才出现的。
  • 使用关键字class时默认的继承方式是private,使用struct时默认的继承方式是public,不过最好显示的写出继承方式。
  • 在实际运用中一般使用都是public继承,几乎很少使用protetced/private继承,也不提倡
  • 使用protetced/private继承,因为protetced/private继承下来的成员都只能在派生类的类里面使用,实际中扩展维护性不强。

四、基类和派生类对象的赋值转换

基类对象/指针/引用 = 派生类对象

        基类的对象、指针、引用都可以被派生类对象赋值,因为派生类是继承基类的,也就是说派生类既包含了基类的成员,也拥有自己的成员。所以在派生类赋值给基类时,仅仅会赋值基类那部分,形象来说就是把派生类中基类的那部分切割给了基类。反之基类对象不能赋值给派生类对象,可以大变小,不能小变大。

注意:

  • 基类对象 不能赋值给 派生类对象,但是 基类的指针或引用 可以通过强制类型转换赋值给    派生类的指针或引用,但必须是基类的指针指向派生类时才安全,否则会存在越界访问。
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;
}

存在越界访问的原因:

        首先明确一点,pp指向的是Person的对象,所以无论怎么对其类型转换,都是指向的Person的对象,pp开始可以去访问Person里的成员,因为这是它自身的权限,但是当强制类型转换为Student*,就将其访问权限扩大了,因为在Student中有_No成员,所以强制类型转换后理应可以去访问,但是别忘了我们pp指向的是Person 的对象,而Person对象中压根就没有_No成员,所以访问就会越界。

五、继承中的作用域

        我们知道作用域其实就是花括号{ },一个花括号代表了一个作用域,所以在继承体系中,基类和派生类都有自己的花括号,也就是说明,基类和派生类都有自己独立的作用域

当基类和派生类中有同名的成员,我们访问该成员,是访问的基类,还是派生类?

        当基类和派生类中有同名成员,派生类成员将屏蔽基类对同名成员的直接访问,这种情况叫隐藏,也叫重定义。

        说白了就是有同名成员,基类的成员被隐藏,只能访问到派生类的成员,若想访问基类的同名成员,需要显示调用:基类::基类成员

        成员变量构成重定义,只需变量名相同

        成员函数构成重定义,只需函数名相同

class A
{
public:
    int Print()
    {
        cout << "A" << endl;
        return 0;
    }
    int _a;

};
class B:public A
{
public:
    void Print() //成员函数,只需要函数名相同就构成隐藏
    {
        cout << "B" << endl;
    }
public:
    int _a;
};
void Test()
{
    B b;
    //访问同名成员,只会访问到派生类的
    b.Print();
    b._a = 3;
    //显示访问
    cout << b.B::_a << endl;
    cout << b.A::_a << endl;
}
int main()
{
    Test();
    return 0;
}

注意:

不要给自己找麻烦,最好不要定义同名成员

为什么是构成重定义,而不是重载?

        因为构成重载的前提条件是在同一个作用域中,而基类和派生类属于不同的作用域

六、派生类的默认成员函数

1. 构造函数

派生类对象初始化:   先调用  基类构造   再调  派生类构造

        派生类的构造函数会自动调用基类的默认构造函数;

        如果基类没有默认的构造函数,则必须在派生类构造函数的初始化列表阶段显示调用。

2. 拷贝构造函数

        派生类的默认的拷贝构造函数会自动调用基类的默认的拷贝构造;

        如果基类没有默认的拷贝构造,则必须在派生类构造函数的初始化列表阶段显示调用。

3. 赋值运算符重载

        派生类的operator=必须要调用基类的operator=完成基类的复制。

4. 析构函数

派生类对象析构清理:   先调用  派生类析构   再调  基类的析构

        派生类的析构函数会在被调用完成后自动调用基类的析构函数清理基类成员。因为这样才能保证派生类对象先清理派生类成员再清理基类成员的顺序。

5. 代码

class Person
{
public:
    //构造函数
    Person()
    {
        cout << "Person()" << endl;
    }

    Person(const string& name, const int& age)
        : _name(name)
        , _age(age)
    {
        cout << "Person(const string& name, const int& age)" << endl;
    }

    //拷贝构造
    Person(const Person& p)
        : _name(p._name)
        , _age(p._age)
    {
        cout << "Person(const Person& p)" << endl;
    }

    //赋值运算符重载
    Person& operator=(const Person& p)
    {
        cout << "Person& operator=(const Person* p)" << endl;
        if(this != &p)
        {
            _name = p._name;
            _age = p._age;
        }
        return *this;
    }

    //析构函数
    ~Person()
    {
        cout << "~Person()" << endl;
    }
protected:
    string _name;
    int _age;
};
class Student:public Person
{
public:

    Student()
    {
        cout << "Student()" << endl;
    }

    Student(const string& name, const int& age, const int& id)
        : Person(name, age)
        , _id(id)
    {
        cout << "Student(const string& name, const int& age, const int& id)" << endl;
    }

    Student(const Student& s)
        : Person(s)
        , _id(s._id)
    {
        cout << "Student(const Student& s)" << endl;
    }

    Student& operator=(const Student& s)
    {
        if(this != &s)
        {
            Person::operator=((Person&)s);
            cout << "Student& operator=(const Student* s)" << endl;
            _id = s._id;
        }
        return *this;
    }

    ~Student()
    {
        cout << "~Student()" << endl;
    }

    void Print()
    {
        cout << _name << endl;
        cout << _age << endl;
        cout << _id << endl;
    }
private:
    int _id;
};

七、继承与友元的关系

       友元函数可以访问类的私有和保护成员,但是继承关系中,友元不会被继承。所以基类友元不可以使用派生类的私有或保护成员,因为爸爸的朋友,不是我的朋友。

        结论:友元函数只能访问被友元的那个类的成员,不会被继承

class B;
class A
{
public:
    friend void Print(const A& a, const B& b);
private:
    int _a;
};

class B:public A
{
public:
private:
    int _b;
};
void Print(const A& a, const B& b)
{
    cout << a._a << endl;
    cout << b._b << endl; //无法访问
}

八、继承与静态成员

        首先回顾一下类的静态成员:

        类中定义的静态成员,是属于整个类的,并不属于任何一个实例化对象,所以无论实例化多少个对象,静态成员只有一个。

        回到继承,我们知道派生类会继承基类的成员,那就也一定会继承静态成员,由于静态成员在基类只有一份,所以在继承之后,基类和派生类共用这一份静态成员,所以无论派生出多少个派生类,也只有这一份静态成员。

        结论:静态成员可以被派生类继承,但是整个继承体系中只有一份!

class A
{
public:
    static void Print()
    {
        cout << "_a = " << _a << endl;
    }
protected:
    static int _a;
    int _aa;
};
int A::_a = 2;  //静态成员变量要在类外面初始化,并表明作用域

class B:public A
{
public:
    static void Add()  //静态成员函数,只能使用静态成员
    {                  //因为静态成员函数属于整个类,不属于哪个实例化对象
        ++_a;          //所以静态成员函数里没有this指针,就无法使用非静态成员
    }
private:
    int _b;
};

九、单继承、多继承、菱形继承、菱形虚拟继承

1. 单继承

        单继承:一对一的继承

class A
{
public:
    void APrint()
    {
        cout << "_a = " << _a << endl;
    }
protected:
    int _a = 1;
};
class B:public A
{
public:
    void BPrint()
    {
        APrint();
        cout << "_b = " << _b << endl;
    }
protected:
    int _b = 2;
};
class C:public B
{
public:
    void CPrint()
    {
        BPrint();
        cout << "_c = " << _c << endl;
    }
private:
    int _c = 3;
};

2. 多继承

        多继承:一个派生类有两个或以上的直接基类

class A
{
public:
    void APrint()
    {
        cout << "_a = " << _a << endl;
    }
protected:
    int _a = 1;
};
class B
{
public:
    void BPrint()
    {
        cout << "_b = " << _b << endl;
    }
protected:
    int _b = 2;
};
class C:public A , public B
{
public:
    void CPrint()
    {
        APrint();
        BPrint();
        cout << "_c = " << _c << endl;
    }
private:
    int _c = 3;
};

3. 菱形继承

        菱形继承:如下图

class P
{
public:
    void PPrint()
    {
        cout << "_p = " << _p << endl;
    }
protected:
    int _p = 0;
};
class A:public P
{
public:
    void APrint()
    {
        cout << "_a = " << _a << endl;
        ++_p;
        PPrint();
    }
protected:
    int _a = 1;
};
class B:public P
{
public:
    void BPrint()
    {
        cout << "_b = " << _b << endl;
        ++_p;
        PPrint();
    }
protected:
    int _b = 2;
};
class C:public A , public B
{
public:
    void CPrint()
    {
        APrint();
        BPrint();
        cout << "_c = " << _c << endl;
    }
private:
    int _c = 3;
};

运行结果:

        首先继承方法是C继承A,B;A继承P,B继承P;从运行结果来看会发现P中的成员在A和B中分别拷贝了一份,A和B中的P是独立的,不同的。这就会使数据产生二义性和冗余

当我们去访问这个_p时,就不知道是访问的哪个_p,所以下面的代码就会有问题,在函数Add_P中,就不知道是访问哪个_p。

class P
{
public:
protected:
    int _p = 0;
};
class A:public P
{
public:
protected:
    int _a = 1;
};
class B:public P
{
public:
protected:
    int _b = 2;
};
class C:public A , public B
{
public:
    void Add_P()
    {
        _p = _p + 6;    //不知道访问的哪个_p
    }
private:
    int _c = 3;
};

4. 菱形虚拟继承

        菱形虚拟继承就是用来解决菱形继承问题,防止数据二义性和冗余。

class P
{
public:
protected:
    int _p = 0;
};

class A:virtual public P
{
public:
protected:
    int _a = 1;
};

class B:virtual public P
{
public:
protected:
    int _b = 2;
};
class C:public A , public B
{
public:
    void Add_P()
    {
        _p = _p + 6;   
    }
private:
    int _c = 3;
};

十、继承与组合

  • 继承是一种is-a的关系。也就是说每个派生类对象都是一个基类对象
  • 组合是一种has-a的关系。假设B组合了A,每个B对象中都有一个A对象
  • 优先使用对象组合,而不是类继承 

        继承允许你根据基类的实现来定义派生类的实现。这种通过生成派生类的复用通常被称为白箱复用。

        术语“白箱”是相对可视性而言:在继承方式中,基类的内部细节对子类可见。继承一定程度破坏了基类的封装基类的改变,对派生类有很大的影响派生类和基类间的依赖关系很强,耦合度高
        对象组合是类继承之外的另一种复用选择。新的更复杂的功能可以通过组装或组合对象来获得。对象组合要求被组合的对象具有良好定义的接口。这种复用风格被称为黑箱复用,因为对象的内部细节是不可见的。对象只以“黑箱”的形式出现。组合类之间没有很强的依赖关系,耦合度低。优先使用对象组合有助于你保持每个类被封装。
        实际尽量多去用组合。组合的耦合度低,代码维护性好。不过继承也有用武之地的,有些关系就适合继承那就用继承,另外要实现多态,也必须要继承。类之间的关系可以用继承,可以用组合,就用组合。

十一、继承的反思

  • 最好不要实现多继承,因为会引发菱形继承
  • 能用组合用组合,避免使用继承,因为继承会破坏封装,提高耦合度

十二、继承的笔试面试题

1.  什么是菱形继承?菱形继承的问题是什么?

        菱形继承就是一个派生类继承两个类,而这两个类继承同一个基类

        问题:在初始化对象的时候会产生数据冗余和二义性

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

        某几个类同时继承同一个类,在这几个类的继承方式前加virtual关键字,形成菱形继承

        解决方法:通过虚基表指针,找到虚基表中的偏移量,再通过地址的偏移量来找到共有的数据地址。

3. 继承和组合的区别?什么时候用继承?什么时候用组合?

        继承和组合的最大区别就是 数据的耦合度

        继承的数据耦合度高,当改变基类的数据时,会影响派生类的成员;

        但组合的耦合度低,因为基类的成员变量对派生类是有访问权限的,所以基类的成员变量改变,不会影响到派生类。

        当我们要实现多态的时候,必须用继承;剩下的情况能用组合就用组合

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

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

相关文章

Java学习笔记23(面向对象三大特征)

1.5 多态 ​ *多态(polymorphic) ​ *方法或对象具有多种形态&#xff0c;是面向对象的第三大特征&#xff0c;多态是建立在封装和继承基础之上的 1.多态的具体体现 1.方法的多态 &#xff08;重写和重载体现了多态&#xff09; 2.对象的多态 ​ 1.一个对象的编译类型和…

亮数据----教你轻松获取数据

文章目录 1. 数据采集遇到的瓶颈1.1 不会造数据&#xff1f;1.2 不会写爬虫代码&#xff1f; 2.IP 代理基础知识2.1 基本概念2.2 作用和分类2.3 IP 代理的配置和使用2.4 安全和合规 3. 为何使用亮数据 IP 代理3.1 拥有丰富的代理网络服务3.2 简单易操作的采集工具3.3 拥有各平台…

爬虫入狱笔记——xx政府网站公开政策数据

最近在学习爬虫&#xff0c;做个笔记吧 今天爬xx政府网站-政策法规栏目的数据 咱们首先需要找到数据从哪里来&#xff0c;鼠标右键->检查&#xff08;或者快捷键一般为F12&#xff09;检查元素&#xff0c;搜索关键词 eg.【违法案例】 回车&#xff0c; 如果没有的话&am…

「媒体邀约」天津媒体邀约资源有哪些?媒体宣传现场报道

传媒如春雨&#xff0c;润物细无声&#xff0c;大家好&#xff0c;我是51媒体网胡老师。 天津作为中国北方的重要城市&#xff0c;拥有丰富的媒体资源&#xff0c;可以为各类活动提供全面的媒体宣传和现场报道。以下是天津地区的媒体邀约资源&#xff1a; 1. 报纸媒体 - 《天…

「38」LUT在直播间调色的高级应用……

「38」LUT滤镜 对人或物进行精进调色 OBS软件中的LUT&#xff08;Look Up Table&#xff09;滤镜&#xff0c;可用于调整视频、图像的色彩和对比度等效果&#xff0c;以实现专业级的颜色调整需求。 左图为原图 右图为LUT 先对比上面的两张图&#xff0c;用了滤镜效果的变化…

【问题处理】银河麒麟操作系统实例分享,银河麒麟高级服务器操作系统mellanox 网卡驱动编译

1.Mellanox 网卡源码驱动下载链接&#xff1a; https://www.mellanox.com/downloads/ofed/MLNX_EN-5.7-1.0.2.0/MLNX_EN_SRC-5.7-1.0.2.0.tgz 2.系统及内核版本如下截图&#xff1a; 3.未升级前 mellanox 网卡驱动版本如下&#xff1a; 4.解压 “MLNX_EN_SRC-5.7-1.0.2.0.tg…

基于单片机电动自行车太阳能充电系统设计

**单片机设计介绍&#xff0c;基于单片机电动自行车太阳能充电系统设计 文章目录 一 概要二、功能设计设计思路 三、 软件设计原理图 五、 程序六、 文章目录 一 概要 基于单片机的电动自行车太阳能充电系统设计&#xff0c;是一个将太阳能技术与电动自行车充电技术相结合的创…

【RealSense】Ubuntu20.04 安装 Intel RealSense ROS 并使用 D435i 测试

【RealSense】Ubuntu20.04 安装 Intel RealSense ROS 并使用 D435i 测试 1 本机环境2 安装流程3 存在的 bug3.1 Resource not found: rgbd_launch 1 本机环境 Ubuntu20.04ROS Noetic 2 安装流程 参考文档: Link 安装 Intel RealSense™ SDK 2.0&#xff0c;参考上一篇文章: L…

微信小程序实现滚动标签

使用scroll-view标签可实现组件滚动标签 1、list中 list.wxml代码如下: <!--pages/list/list.wxml--> <navigation-bartitle"小程序" back"{{false}}"color"black" background"#FFF"></navigation-bar><scroll-…

「媒体宣传」科技IT行业有哪些媒体邀约资源-51媒体网

传媒如春雨&#xff0c;润物细无声&#xff0c;大家好&#xff0c;我是51媒体网胡老师。 科技IT行业的媒体邀约资源非常丰富&#xff0c;包括了各种类型的传统媒体和新兴的网络媒体平台。以下是一些主要的媒体邀约资源&#xff1a; 除此之外&#xff0c;还有一些其他科技类网络…

finebi6.0中我的分析中...中加自己的菜单

js的两个扩展点是&#xff1a; BI.config("bi.result_wrapper", function (e) {return e.showMerge !0, e}),BI.config("bi.analysis.admin_list", function (e) {return e.showMergeUser !0, e}) 对应的组件在conf.min.js中的 bi.search_sort 点击事件…

【多线程】Callable详解

Callable接口 先看看Callable接口的源码: Callable是一个函数式接口&#xff0c;此时就可以用lambda表达式更简洁地使用它。Callable是个泛型接口&#xff0c;只有一个方法call&#xff0c;该方法返回类型就是传递进来的V类型。call方法还支持抛出异常. 与Callable对应的是Ru…

「媒体宣传」产业金融财经媒体邀约资源有哪些-51媒体

传媒如春雨&#xff0c;润物细无声&#xff0c;大家好&#xff0c;我是51媒体网胡老师。 产业金融财经领域的媒体邀约资源涵盖了多种平台和形式&#xff0c;主要包括以下几类&#xff1a; 杂志和报纸&#xff1a;如《中国经济周刊》和《中国证券报》&#xff0c;它们通常报道财…

从零开始:构建、打包并上传个人前端组件库至私有npm仓库的完整指南

文章目录 一、写组件1、注册全局组件方法2、组件13、组件2 二、测试三、发布1、配置package.json2、生成库包3、配置发布信息4、发布 四、使用1、安装2、使用 一、写组件 1、注册全局组件方法 plugins/index.js const requireComponent require.context(./, true, /\.vue$/…

idea常用配置

IDEA设置全局配置 参考&#xff1a;IDEA设置全局配置_idea如何打开一个项目,全局设置-CSDN博客 idea提交代码到git或svn上时&#xff0c;怎么忽略.class、.iml文件和文件夹等不必要的文件 参考&#xff1a;idea提交代码到git或svn上时&#xff0c;怎么忽略.class、.iml文件和文…

学浪app中的视频怎么缓存

现在越来越多人在学浪app里面购买课程&#xff0c;有的课程有时间限制&#xff0c;想要下载下来&#xff0c;如果你还不知道下载的方法&#xff0c;可以看看我这篇文章&#xff0c;专门讲解如何缓存学浪app里面的课程 讲技术方法很多人可能听不懂&#xff0c;所以我就将技术融…

室友打团太吵?一条命令让它卡死

「作者主页」&#xff1a;士别三日wyx 「作者简介」&#xff1a;CSDN top100、阿里云博客专家、华为云享专家、网络安全领域优质创作者 「推荐专栏」&#xff1a;更多干货&#xff0c;请关注专栏《网络安全自学教程》 SYN Flood 1、hping3实现SYN Flood1.1、主机探测1.2、扫描端…

BugKu:Flask_FileUpload

1.打开此题 通过题目知道这个是一个关于Flask的文件上传的漏洞题目 2.查看网页源代码 Flask是一个使用Python编写的轻量级Web应用框架。 这里又提示说用python来运行结果&#xff0c;那很有可能就是要通过python脚本来抓取flag 3.编辑Python脚本 工具&#xff1a;pycharm 文件…

C++从入门到精通——this指针

this指针 前言一、this指针的引出问题 二、this指针的特性三、例题什么时候会出现编译报错什么时候会出现运行崩溃this指针存在哪里this指针可以为空吗 四、C语言和C实现Stack的对比C语言实现C实现 前言 this指针是一个特殊的指针&#xff0c;在C类的成员函数中使用。它指向调…

高校本科毕业论文word 自动化模板

高校本科毕业论文word 自动化模板 “一份好的论文模板能真正能够让作者专注于内容&#xff0c;而不用关注令人烦恼的排版细节问题, 例如插入页眉页码、插入目录、编号、交叉引用、参考文献等等。本文提供中山大学本科毕业论文Word模板&#xff0c;已经按照学校最新格式规范设置…