资讯专栏INFORMATION COLUMN

C++重温笔记(四): 继承和派生

DevWiki / 1433人阅读

摘要:继承继承,就是子类继承父亲的特征和行为,使得子类具有父类的成员变量和方法。此时,被继承的类称为父类或基类,而继承的类称为子类或派生类。,如果存在继承关系的时候,和就不一样了基类中的成员可以在派生类中使用,但是基类中的成员不能再派生类中使用。

1. 写在前面

c++在线编译工具,可快速进行实验: https://www.dooccn.com/cpp/

这段时间打算重新把c++捡起来, 实习给我的一个体会就是算法工程师是去解决实际问题的,所以呢,不能被算法或者工程局限住,应时刻提高解决问题的能力,在这个过程中,我发现cpp很重要, 正好这段时间也在接触些c++开发相关的任务,所有想借这个机会把c++重新学习一遍。 在推荐领域, 目前我接触到的算法模型方面主要是基于Python, 而线上的服务全是c++(算法侧, 业务那边基本上用go),我们所谓的模型,也一般是训练好部署上线然后提供接口而已。所以现在也终于知道,为啥只单纯熟悉Python不太行了, cpp,才是yyds。

和python一样, 这个系列是重温,依然不会整理太基础性的东西,更像是查缺补漏, 不过,c++对我来说, 已经5年没有用过了, 这个缺很大, 也差不多相当重学了, 所以接下来的时间, 重温一遍啦 ?

资料参考主要是C语言中文网光城哥写的C++教程,然后再加自己的理解和编程实验作为辅助,加深印象。

今天这篇文章是C++非常重要的一块,关于类的继承和派生,我们知道C++面向对象开发有四大特性: 抽象,封装,继承和多态。 前面发现,通过定义类,把事物的数据和功能进行抽象,而通过隐藏对象的属性和实现细节,对外只提供接口的方式对类的内部成员形成了封装。 这两个前面都已经了解过, 而这篇文章主要是整理继承,即子类继承父类的特征和行为,使得子类具有父类的成员变量和方法, 继承最大的一个好处就是代码复用,两个类有一些相同的属性和方法。

这篇内容会有些偏多,还是各取所需即可 ?

主要内容如下:

  • C++继承和派生初识
  • C++继承的三种方式
  • C++继承时的名字遮蔽问题与作用域嵌套
  • C++继承时的对象内存模型
  • C++基类和派生类的构造函数和析构函数
  • C++的多继承
  • C++虚继承(虚基类,虚继承构造函数,虚继承内存模型)
  • C++向上转型(派生类指针赋值给基类)与过程原理剖析
  • 借助指针突破访问权限的限制

Ok, let’s go!

2. C++继承和派生初识

2.1 C++面向对象开发的四大特性

在聊C++继承和派生之前,先来看看C++面向对象开发的四大特性,这样能先宏观把握一下继承到底位于什么样的位置。

C++面向对象开发有四大特性: 抽象,封装,继承和多态, 正所谓编程语言的背后都非常相似,Java既然也是面向对象的语言,同样也会有这四大特性。

抽象和封装前面其实已经整理过了, 封装主要讲的是信息隐藏,保护数据,而抽象又可以从两个层面来理解。

  • 抽象:
    从现实生活的具体事物到类层面的抽象(包括各个成员),比如人,有姓名,年龄等各个属性,又有学习,运动等各项功能,那么就可以定义people类把这些数据抽象出来,再通过创建对象的方式把具体实体人创建出来,调用相应的方法实现相应的功能。

    宏观上,这是一种大层面的抽象,而这里面其实又可以看成数据抽象(目标的特性信息)和过程抽象(目标的功能是啥,注意不关注具体实现逻辑)
  • 封装
    所谓封装,就是隐藏对象的属性和实现细节,仅仅对外公开接口,控制程序对类属性的读取和修改。在类的内部, 成员函数可以自由修改成员变量,进行精确控制,但是在类的内部,通过良好的封装, 减少耦合,隐藏实现细节。
  • 继承
    继承,就是子类继承父亲的特征和行为,使得子类具有父类的成员变量和方法。 这个和生活中儿子继承他爹的家产差不多是一个道理,更有意思的是继承有两种模式,单继承和多继承,单继承比较好理解,一个子类只继承一个父类, 而多继承是一个子类,继承多个父类,联想到生活中,可能有好几个爸爸。
  • 多态
    同一个行为具有多个不同表现形式或形态的能力,有两种表现形式覆盖和重载,这个到这里不理解也不要紧,下一篇文章会重点整理。
    • 重载: 这个之前学习过,相同作用域中存在多个同名函数,但函数的参数列表会不一样
    • 重写或者叫覆盖: 主要体现在继承关系里面,子类重写了从他爸那里继承过来的函数,如果子类的对象调用成员函数的时候,如果子类的成员函数重写了他爸的,那么就执行子类自己的函数,否则继承他爸的。 这个也比较好理解,比如同样是挣钱,他爸的路子很可能和儿子的不一样,那么儿子在调用挣钱的时候,肯定是先找找儿子有没有独特的挣钱方式,如果没有,就默认和他爸一样,走他爸的挣钱方式。

2.2 再看继承

有了上面的宏观把握,再看继承就比较容易理解, 简单的讲,继承就是一个类从另一个类获取成员变量和成员函数的过程。 此时,被继承的类称为父类或基类,而继承的类称为子类或派生类。

C++中派生和继承是站在不同角度看的同种概念。继承时从儿子的角度看,派生是父亲的角度看,实际说的是一回事。

派生类除了拥有他爹的成员,还可以定义自己的新成员,增强功能,此时的好处就是只需要定义新成员即可,老的成员和功能,直接继承,实现了代码复用。

下面是两种典型使用继承的场景:

  1. 创建的新类与现有类相似,只多出若干个成员变量和成员函数的时候,用继承,减少代码量,且新类会拥有基类的所有功能
  2. 创建多个类, 他们拥有很多相似的成员变量或函数,可以用继承,把这些类共同的成员提取出来,定义为基类,然后从基类继承, 可以减少代码量,也方便后续的修改。

继承的语法:

class 派生类名:[继承方式] 基类名{    派生类新增加的成员};

直接看个栗子:

class People{public:    void setname(string name);    string getname();private:    string m_name;    int m_age;};void People::setname(string name){m_name = name;}   string People::getname(){return m_name;}class Student: public People{public:    void setage(int age);    int getage();private:    int m_age;};void Student::setage(int age){m_age = age;}int Student::getage(){return m_age;}int main(){    Student stu;    stu.setname("zhongqiang");    stu.setage(25);    cout << stu.getname() << "的年龄是" << stu.getage() << endl;    return 0;}

这个例子比较简单,不解释, 这里就会发现, Student继承了People之后,就有他的setname()getname()方法,在子类里面可以直接调用。

上面演示了public的继承方式,但继承方式其实有3种, public, private, protected, 这哥仨不仅可以修饰类的成员,还可以指定继承方式。如果不写,默认是private(成员变量和成员函数默认也是private), 那么这三种继承方式到底有啥区别呢?

3. C++继承的三种方式

3.1 哥仨修饰类成员

public, private, protected这哥仨,可以修饰类成员,之前见识过public和private了, 这里加上protected之后统一整理下访问权限的问题。

类成员的访问权限从高到低依次是public --> protected --> private。 public成员可以通过对象来访问, private成员不能通过对象访问, protected成员和private成员蕾西, 也不能通过对象访问。

But, 如果存在继承关系的时候, protected和private就不一样了: 基类中的protected成员可以在派生类中使用,但是基类中的private成员不能再派生类中使用

3.2 继承方式会影响基类成员在派生类中的访问权限

不同的继承方式使得基类成员在派生类中的访问权限也不一样, 下面这个很重要:

  • public继承方式
    • 基类中所有public成员 -> 继承到派生类 -> public属性
    • 基类中所有protected成员 -> 继承到派生类 -> protected 属性
    • 基类中所有private 成员 -> 继承到派生类 -> 不能使用
  • protect继承方式
    • 基类中所有public成员 -> 继承到派生类 -> protected属性
    • 基类中所有protected成员 -> 继承到派生类 -> protected 属性
    • 基类中所有private 成员 -> 继承到派生类 -> 不能使用
  • private继承方式
    • 基类中所有public成员 -> 继承到派生类 -> private属性
    • 基类中所有protected成员 -> 继承到派生类 -> private 属性
    • 基类中所有private 成员 -> 继承到派生类 -> 不能使用

使用方法:

  1. 基类成员在派生类中的访问权限不得高于继承方式中指定的权限,也就是说**继承方式中的public, protected, private是用来指明基类成员在派生类中最高访问权限的。
  2. 不管继承方式如何, 基类中的private成员在派生类中始终不能使用(不能在派生类的成员函数中访问或者调用)
  3. 如果希望基类的成员能够在派生类继承并且使用, 那么这些成员应该声明public或者protected, 只有那些不希望在派生类中使用的成员声明为private
  4. 如果希望基类的成员既不向外暴露(不能通过对象访问), 还能在派生类中使用, 那么就声明为protected。

下面通过上面的代码例子来演示下, 由于private和protect继承方式会改变基类成员在派生类中的访问权限,导致继承关系复杂, 所以实际开发中一般使用public。

把上面的栗子修改下, 测试下上面的这几种情况,方便理解,这里只看public继承下面的。

class People{public:    void setname(string name);    string getname();    void setage(int age);    int getage();    void setsex(string sex);    string getsex();    void setwork(string work);    string getwork();    // 属性    string m_sex;    protected:    string m_work;    private:    string m_name;    int m_age;};void People::setname(string name){m_name = name;}   string People::getname(){return m_name;}void People::setage(int age){m_age = age;}int People::getage(){return m_age;}void People::setsex(string sex){m_sex=sex;}string People::getsex(){return m_sex;}void People::setwork(string work){m_work=work;}string People::getwork(){return m_work;}class Student: public People{public:        void setscore(float score);    float getscore();        // 定义问候方法,这里面会访问基类的私有属性    string helloname();    string hellowork();    string hellosex();private:    float m_score;};void Student::setscore(float score){m_score = score;}float Student::getscore(){return m_score;}// 访问基类中的公有属性string Student::hellosex(){return "hello, " + m_sex;}// 访问基类中的protect属性string Student::hellowork(){return "hello, " + m_work;}// 访问基类中的私有属性// string Student::helloname(){return "hello, " + m_name;}  error: "std::string People::m_name" is private within this contextint main(){        Student stu;    stu.setname("zhongqiang");    stu.setsex("man");    stu.setwork("student");    stu.setage(25);    stu.setscore(66.6);        cout << stu.getname() << "今年" << stu.getage() << ",性别: " << stu.getsex() << ", 职业: " << stu.getwork() << ", 分数: " << stu.getscore() << endl;        //cout << stu.helloname() << endl;    cout << stu.hellowork() << endl;    cout << stu.hellosex() << endl;        // 直接通过对象访问属性    cout << stu.m_sex << endl;     // 公有属性到子类中依然是公有, 可以被访问    //cout << stu.m_name << endl;  // error "std::string People::m_name" is private within this context    //cout << stu.m_work << endl;   // error "std::string People::m_work" is protected within this context        //cout << stu.m_score << endl;  // error "float Student::m_score" is private within this context        return 0;}

在这里面就可以看出来, 在Student里面的成员函数中,只能访问到他爹的public属性和protect属性,不能访问他爹的private属性。而如果是通过Student的对象, 那么只能访问public属性,protect和private的都访问不到。

在派生类中访问基类的private成员的唯一方法就是借助基类的非private成员函数,如果基类没有非private成员函数,那么该成员在派生类中将无法访问

这里注意一个问题,这里说的是基类的 private 成员不能在派生类中使用,并不是说基类的 private 成员不能被继承。实际上,基类的 private 成员是能够被继承的,并且(成员变量)会占用派生类对象的内存,它只是在派生类中不可见,导致无法使用罢了。private 成员的这种特性,能够很好的对派生类隐藏基类的实现,以体现面向对象的封装性。

3.3 using改变访问权限

using关键字可以改变基类成员在派生类中的访问权限, 比如将public改成private, protected改成public。

注意:using 只能改变基类中 public 和 protected 成员的访问权限,不能改变 private 成员的访问权限,因为基类中 private 成员在派生类中是不可见的,根本不能使用,所以基类中的 private 成员在派生类中无论如何都不能访问

class People{public:    void setname(string name);    string getname();    void setage(int age);    int getage();    void setsex(string sex);    string getsex();    void setwork(string work);    string getwork();    // 属性    string m_sex;    protected:    string m_work;    private:    string m_name;    int m_age;};void People::setname(string name){m_name = name;}   string People::getname(){return m_name;}void People::setage(int age){m_age = age;}int People::getage(){return m_age;}void People::setsex(string sex){m_sex=sex;}string People::getsex(){return m_sex;}void People::setwork(string work){m_work=work;}string People::getwork(){return m_work;}class Student: public People{public:        void setscore(float score);    float getscore();        // 定义问候方法,这里面会访问基类的私有属性    string helloname();    string hellowork();    string hellosex();        using People::m_work;       // 将m_work提升成public权限    private:    float m_score;    using People::m_sex;        // 将m_sex降低为private权限};void Student::setscore(float score){m_score = score;}float Student::getscore(){return m_score;}// 访问基类中的公有属性string Student::hellosex(){return "hello, " + m_sex;}// 访问基类中的protect属性string Student::hellowork(){return "hello, " + m_work;}// 访问基类中的私有属性// string Student::helloname(){return "hello, " + m_name;}  error: "std::string People::m_name" is private within this contextint main(){        Student stu;    stu.setname("zhongqiang");    stu.setsex("man");    stu.setwork("student");    stu.setage(25);    stu.setscore(66.6);        cout << stu.getname() << "今年" << stu.getage() << ",性别: " << stu.getsex() << ", 职业: " << stu.getwork() << ", 分数: " << stu.getscore() << endl;       // 直接通过对象访问属性    //cout << stu.m_sex << endl;     // 这个这时候就会报错了    cout << stu.m_work << endl;   // 这个就可以访问了    return 0;}

注意,using修改的是派生类里面的成员访问权限。并且是只能修改public和protected的访问权限。

4. C++继承时的名字遮蔽问题与作用域嵌套

4.1 名字遮蔽问题

这个说的情况是派生类中的成员(变量和函数),如果和基类中的成员重名,那么在派生类中使用该成员,实际上用的是派生类新增的成员,而不是从基类继承过来的。 即派生类遮蔽掉从基类继承过来的成员。

下面的这个例子,是Student继承了People, 又重写了People的show函数,那么通过Student对象调用show的时候,实际上是用的Student自身的show函数,但People的show函数也被Student继承了过来,如果想用,需要加上类名和域解析符。

class People{public:    void show();protected:    string m_name;    int m_age;};void People::show(){    cout << m_name << " " << m_age << endl; }class Student: public People{public:    Student(string name, int age, string sex);    void show();    // 遮蔽基类的show()    private:    string m_sex;};Student::Student(string name, int age, string sex): m_sex(sex){    m_name = name;    m_age = age;    //m_sex = sex;}void Student::show(){    cout << m_name << " " << m_age << " " << m_sex << endl;}int main(){        Student stu("zhongqiang", 25, "man");        // 派生类新增的成员函数    stu.show();        // zhongqiang 25 man        // 使用从基类继承过来的成员函数    stu.People::show();  // zhongqiang 25        return 0;}

这里我在实验的时候,发现个问题,就是Student的构造函数定义的时候, 本来是想用构造函数初始化列表的方式,一开始写的代码是这样:

Student::Student(string name, int age, string sex): m_name(name), m_age(age){    m_sex = sex;}

此时编译错误, 报错原因class "Student" does not have any field named "m_name", 而如果写成上面那种形式,或者不用参数化列表的方式,就没问题, 所以这里我感觉,参数化列表那个地方的参数,应该是当前类具有的成员变量才行, 继承过来的应该是不能往这里写。

上面的例子,其实就是派生类对基类的函数重写,内部在执行的时候, 先找派生类里面有没有对应的函数,如果有,就先执行派生类里面的重名函数,如果没有,那么再执行基类里面定义的。

那么,如果派生类里面的函数和基类的函数重名,但形参列表不一样的时候,此时会发生重载现象吗? 答: 不会。 一旦派生类中有同名函数,不管他们的参数是否一样,都会把基类中所有的同名函数遮蔽掉。

这个就不用例子演示了,而是整理下背后的所以然吧。

4.2 作用域嵌套

之前整理过,每个类都会有自己的作用域, 在这个作用域内会定义类的成员,那么,当存在继承关系的时候, 派生类的作用域嵌套在基类的作用域之内,如果一个名字在派生类的作用域没有找到,编译器会继续到外层的基类作用域查找该名字的定义。

两条:

  • 一旦在外层作用域中声明或定义了某个名字, 那么它嵌套着的所有内层作用域都能访问这个名字
  • 同时,允许在内层作用域重新定义外层作用域中已经有的名字

看个嵌套作用域的例子:

class A{public:    void func();public:    int n = 500;};void A::func(){ cout<<"hello, changjinhu!!!"<<endl; }class B: public A{public:    int n = 5000;    int m;};class C: public B{public:    int n = 50000;    int x;};int main(){    C obj;    cout << obj.n << endl;    obj.func();    cout<<sizeof(C)<<endl;    return 0;}

这个例子中的继承关系, B继承A, C继承B,那么作用域的嵌套关系如下:

  • obj是C类的对象, 访问成员变量n时,由于C类自己有n, 那么编译器就会直接用,此时不会去B或者A中找,即派生类中的成员变量会遮蔽基类中的成员变量。
  • 访问成员函数func()的时候,编译器没有在C类里面找到func这个名字,会继续到B作用域找,也没有找到,再往外,从A里面找到了,于是,调用A类作用域的func()函数。
  • 对于成员变量,名字查找过程好理解,成员函数要注意,编译器仅仅是根据函数名字查找,不理会函数参数,所以一旦在内层作用域找到同名函数,不管有几个,编译器都不会再到外层作用域查找,编译器仅仅把最内层作用域的这些同名函数作为一组候选, 而也只有这组候选构成一组重载函数。 即只有一个作用域内的同名函数才会有重载关系,不同作用域的同名函数会造成遮蔽,外层函数会被内层遮蔽掉, 这其实也是重载和重写的一个区别了。

有了上面这些,就能回答上面的两点疑问:

  1. 构造函数的参数初始化列表那里, 初始化列表里面要列本类作用域里面的成员,如果是外层作用域,会报错找不到成员
  2. 派生类和基类拥有不同的作用域,所以它们的同名函数不具有重载关系, 而是重写或者覆盖。

5. C++继承时的对象内存模型

没有继承时对象内存的分布情况,成员变量和成员函数分开存储:

  • 对象的内存中只包含成员变量,存储在栈区或者堆区(new创建)
  • 成员函数与对象内存分离,存储在代码区

有继承关系的时候, 派生类的内存模型可以看成是基类成员变量和新增成员变量的总和,所有成员函数仍然存储在另外一个区域–代码区,由所有对象共享。

看个例子:

class A{public:    A(int a, int b);protected:    int m_a;    int m_b;};A::A(int a, int b): m_a(a), m_b(b){}class B
            
                     
             
               

文章版权归作者所有,未经允许请勿转载,若此文章存在违规行为,您可以联系管理员删除。

转载请注明本文地址:https://www.ucloud.cn/yun/122560.html

相关文章

  • Java学习笔记1-开发环境安装

    摘要:注意在完成配置环境变量后测试是否安装成功时键入命令安装出现了这样的问题,需要升级具体安装方法,可以参考该文档教程下载最新的之后,上边的问题就解决了。 由于其他项目中要使用Java的项目,所以,简单的学下,好对项目有个大概的了解。 一、Eclipse 安装 1.下载地址为: https://www.eclipse.org/downl... 2.配置环境 在配置环境变量中:设置JAVA_H...

    SimpleTriangle 评论0 收藏0
  • C++继承

    摘要:基类中的构造函数和析构函数不能被继承,在派生类中需要定义新的构造函数和析构函数,私有成员不能被继承。对象访问在派生类外部,通过派生类的对象对从基类继承来的成员的访问。 ...

    不知名网友 评论0 收藏0
  • C++继承

    摘要:例如,在关键字为的派生类当中,所继承的基类成员的访问方式变为。继承中的作用域在继承体系中的基类和派生类都有独立的作用域。为了避免类似问题,实际在继承体系当中最好不要定义同名的成员。 ...

    URLOS 评论0 收藏0
  • 秋招——语言篇(C++)

    摘要:语言不支持函数重载,编译后的代码只包含函数名。发布程序无需提供静态库,移植方便。全局和静态变量当且仅当对象首次用到时才进行构造。静态全局变量全局作用域文件作用域,无法在其他文件中使用。求数组数组名数据类型。 ...

    LuDongWei 评论0 收藏0
  • Python 3 学习笔记之——面向对象

    摘要:类的介绍类用来描述具有相同的属性和方法的对象的集合。类变量类变量在整个实例化的对象中是公用的。类的定义语法格式如下类有一个名为的特殊方法,也即是构造函数,该方法会在定义对象的时候自动调用,可以通过参数传递来对类的实例进行设定。 1. 类的介绍 类(Class) 用来描述具有相同的属性和方法的对象的集合。它定义了该集合中每个对象所共有的属性和方法。对象是类的实例,类是对象的抽象。 ...

    yzzz 评论0 收藏0

发表评论

0条评论

最新活动
阅读需要支付1元查看
<