• 派生类与基类 指针指向


    https://juejin.im/post/6844904054930292749

    派生类和基类的关系并不是两个独立的类型,在派生关系中,派生类型“是一个”基类类型(Derived class is a base class)。在C++语法里规定:基类指针可以指向一个派生类对象,但派生类指针不能指向基类对象。

    用问题里的例子来说
    DerivedClass is a BaseClass

    派生类型之间的数据结构类似于这样:

    BaseClass : [Base Data]
    DerivedClass : [Base Data][Derived Data]

    派生类型的数据附加在其父类之后,这意味着当使用一个父类型指针指向其派生类的时候,父类访问到的数据是派生类当中由父类继承下来的这部分数据


    对比起见,我们再定义一个派生类的派生类

    class  DerivedDerivedClass : public DerivedClass
    
    它的数据结构如下:
    DerivedDerivedClass : [Base Data][Derived Data][DerivedDerived Data]

    而通过基类指针
    BaseClass *pbase
    访问每一个类型的数据部分为:
    [Base Data]
    [Base Data][Derived Data]
    [Base Data][Derived Data][DerivedDerived Data]

    通过派生类指针
    DerivedClass *pderived
    访问每一个类型的数据部分为:
    [Base Data] 不能访问,派生类型指针不能指向基类对象(因为数据内容不够大,通过派生类指针访问基类对象会导致越界)
    [Base Data][Derived Data]
    [Base Data][Derived Data][DerivedDerived Data]
     
     

    函数重载、函数隐藏、函数覆盖

    函数重载只会发生在同作用域中(或同一个类中),函数名称相同,但参数类型或参数个数不同。 函数重载不能通过函数的返回类型来区分,因为在函数返回之前我们并不知道函数的返回类型。

    函数隐藏和函数覆盖只会发生在基类和派生类之间。

    函数隐藏是指派生类中函数与基类中的函数同名,但是这个函数在基类中并没有被定义为虚函数,这种情况就是函数的隐藏。
    所谓隐藏是指使用常规的调用方法,派生类对象访问这个函数时,会优先访问派生类中的这个函数,基类中的这个函数对派生类对象来说是隐藏起来的。 但是隐藏并不意味这不存在或完全不可访问。通过 b->Base::func()访问基类中被隐藏的函数。

    函数覆盖特指由基类中定义的虚函数引发的一种多态现象。在某基类中声明为 virtual 并在一个或多个派生类中被重新定义的成员函数,用法格式为:virtual 函数返回类型 函数名(参数表) {函数体};实现多态性,通过指向派生类的基类指针或引用,访问派生类中同名覆盖成员函数。

    虚函数 是在基类中使用关键字 virtual 声明的函数。在派生类中重新定义基类中定义的虚函数时,会告诉编译器不要静态链接到该函数。

    函数覆盖(多态)的条件:

    • 1: 基类中的成员函数被virtual关键字声明为虚函数;
    • 2:派生类中该函数必须和基类中函数的名称、参数类型和个数等完全一致;
    • 3:将派生类的对象赋给基类指针或者引用,实现多态。

    函数覆盖(多态)实现了一种基类访问(不同)派生类的方法。我们把它称为基类的逆袭。

    基类指针和派生类指针之间的转换

    1. 基类指针指向基类对象、派生类指针指向派生类对象
    这种情况是常用的,只需要通过对应类的指针直接调用对应类的功能就可以了。

    #include<iostream>
    using namespace std;
     
    class Father{
    public:    
        void print()
        {
            printf("Father's function!");
        }
    };
     
    class Son:public Father
    {
    public:
        void print()
        {
            printf("Son's function!");
        }
    };
     
    int main()
    {
        Father f1;
        Son s1;
     
        Father* f = &f1;
        Son* s = &s1;
     
        f->print();
        cout<<endl<<endl;
        s->print();
    }

    2. 基类指针指向派生类对象

    这种情况是允许的,通过定义一个基类指针和一个派生类对象,把基类指针指向派生类对象,但是需要注意,通常情况这时的指针调用的是基类的成员函数。分四种情况:

        一、 函数在基类和派生类中都存在

    这时通过“指向派生类对象的基类指针”调用成员函数,调用的是基类的成员函数。

        Father f1;
        Son s1;

        Father* f = &s1;
        f->print();  //调用的是基类成员函数

        二、函数在基类中不存在,在派生类中存在

    由于调用的还是基类中的成员函数,试图通过基类指针调用派生类才有的成员函数,则编译器会报错。

    error C2039: “xxx”: 不是“Father”的成员

          三、 将基类指针强制转换为派生类指针

    这种是向下的强制类型转换,转换之后“指向派生类的基类指针”就可以访问派生类的成员函数:

        Son s1;
        Father* f = &s1;
        Son *s = (Son*)f;
        s->print1(); //调用派生类成员函数

    但是这种强制转换操作是一种潜在的危险操作。

          四、基类中存在虚函数的情况

    如果基类中的成员函数被定义为虚函数,并且在派生类中也实现了该函数,则通过“指向派生类的基类指针” 访问虚函数,访问的是派生类中的实现。允许“基类指针指向派生类”这个操作,最大的意义也就在此,通过虚函数和函数覆盖,实现了“多态”(指向不同的派生类,实现不同功能)。

        Father f1;
        Son s1;

        Father* f = &s1;
        f->print();   //调用派生类成员函数

    3. 派生类指针指向基类对象

    会产生编译错误。基类对象无法被当作派生类对象,派生类中可能具有只有派生类才有的成员或成员函数。
    即便是使用强制转换,将派生类指针强制转换成基类指针,通过这个“强制指向基类的派生类指针”访问的函数依然是派生类的成员函数。

        Father f1;
        Son s1;

        Son* s=&s1;
        Father* f = (Father*) s;

        f->print();  //调用派生类成员函数


    综上,可以通过基类指针访问派生类方法(强制转换和虚函数),不存在通过派生类指针调用基类成员函数的方法(即便是强制转换)。

    参考 

    https://www.jianshu.com/p/a75b267325c2

    https://juejin.im/post/6844904054930292749

    有下面的一个CPerson类:

    class CPerson
    {
    public:
        void show()
        {
            cout<<"I am a people"<<endl;
        }
    };
    

    在实际编程中,我们经常会看到一个类型有下面两种不同的使用方式:

    CPerson s1;
    CPerson *s2 = NULL;
    s2 = new CPerson();
    s1.show();
    s2->show();
    delete s2;
    

    那么这两者在实际的使用中到底有何差别呢,下面从不同的方面来剖析一下。

    调用方式上的不同

    首先二者最明显的区别就是调用方式上的不同,对象使用" . "操作符调用,而指针使用" -> "操作符调用,且指针在调用时需要先用new来分配空间,且用完后必须手动delete掉。如不想手动delete也可使用智能指针。

    内存空间上的不同

    二者的类型决定了它们在内存上的分布不同,一个是对象类型,一个是指针类型。对象类型在创建时就已为对象分配好内存空间,用的是内存栈,是个局部的临时变量,作用域在该函数体内,随函数的结束被释放。

    指针变量在创建时也是在内存栈,里面的值是对象的地址,当用new操作符时会在内存堆上分配一个空间,即存储实际的对象内容,此时的指针变量里的值即为刚刚分配的内存地址。所以为什么要用delete释放呢?这是因为内存栈里的变量会随着函数的结束而释放,内存堆里的内容需要用户手动释放,所以当函数调用结束时,指针变量会被释放,如果不先delete的话,内存堆里的内容就会找不到地址,也就"无人看管"了。所以在实际使用中一定要记得用完后delete,若是数组,则是delete[]。

    作为函数参数的不同

    类的对象和指针都可作为函数参数传递,这其中还可以有一个引用,代码如下:

    void func(CPerson object)
    void func(CPerson* object)
    void func(CPerson &object)
    

    那么这几种方式有何区别呢?下面来一一分析一下。
    1: void func(CPerson object)
    这种函数是非常不建议的,因为函数参数压栈时,对object进行了复制(还记得拷贝构造函数吗),所以函数对object的操作实际上是对新的对象空间进行的操作,不会影响原对象空间。由于不必要的拷贝对象是十分浪费时间的,也没有意义,我们完全可以用函数func(const Cobject& object);来代替,同样能保护对象的只读性质。
    2:void func(CPerson object)
    这种方式是将类指针作为参数,函数压入参数时实际上复制了指针的值(其实指针可以看作一个存放地址值的整形),实际复制的指针仍指向原对象的空间,所以func函数对该指针的操作是对原对象空间的操作。
    3: void func(CPerson &object)
    这种方式和传指针类似,但函数压入参数时实际上复制了object对象的this指针,其实this指针不是一个真正存在的指针,可以看作是每个对象的数据空间起始地址。func函数中对this指针的操作,实际上也是对原对象空间的操作。

    不管值传递,引用传递啊乱七八遭的什么东西,反正调用函数时都是会复制参数的,只是不同的是复制的是地址,还是整个对象空间的区别而已。
    相同的,函数CPerson func(CPerson& object);在return CPerson对象时,同样会进行构贝构造。这些隐藏的对象复制都是不需要的,我们可以改为CPerson& func(CPerson& object);或是CPerson* func(CPerson& object);这样,在return时,就只是对指针(地址)的复制而已。在不是特殊的情况下,不要将整个对象作为参数,也不要返回整个对象。

    类指针实现多态

    还有一个显著区别就是类指针可以实现多态,通过分类指针调用子类对象,下面详细说明。
    C++的多态性用一句话概括就是:在基类的函数前加上virtual关键字,在派生类中重写该函数,运行时将会根据对象的实际类型来调用相应的函数。如果对象类型是派生类,就调用派生类的函数;如果对象类型是基类,就调用基类的函数。

    1:用virtual关键字申明的函数叫做虚函数,虚函数肯定是类的成员函数。
    2:存在虚函数的类都有一个一维的虚函数表叫做虚表,类的对象有一个指向虚表开始的虚指针。虚表是和类对应的,虚表指针是和对象对应的。
    3:多态性是一个接口多种实现,是面向对象的核心,分为类的多态性和函数的多态性。
    4:多态用虚函数来实现,结合动态绑定.
    5:纯虚函数是虚函数再加上 = 0;
    6:抽象类是指包括至少一个纯虚函数的类。纯虚函数:virtual void fun()=0;即抽象类!必须在子类实现这个函数,即先有名称,没有内容,在派生类实现内容。

    下面看一个具体例子:

    class CPerson
    {
    public:
        virtual void show()
        {
            cout<<"I am a people"<<endl;
        }
    };
    
    class CStudent:public CPerson
    {
    public:
        void show()
        {
            cout<<"I am a student"<<endl;
        }
    };
    
    int main()
    {
        CStudent stu;
        CPerson *per = &stu;
        per->show();
        system("pause");
        return 0;
    }
    

    输出的结果为:


     
    运行jie

    可以看到父类指针指向子类对象后,调用的是子类的show函数,注意多态的实现需要在函数前加virtual修饰,使其为虚函数,否则调用的还是父类的,总之要实现多态,类指针和虚函数缺一不可。关于多态的详细分析具体可以参考http://www.cnblogs.com/cxq0017/p/6074247.html

    通过以上对类对象和类指针不同方面的分析,我们在知道在什么时候应该使用类对象,什么时候使用类指针比较好。



    作者:litexy
    链接:https://www.jianshu.com/p/a75b267325c2
    来源:简书
    著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
  • 相关阅读:
    星空雅梦
    星空雅梦
    Navicat permium快捷键
    Mysql建表+创建索引
    mysql 常见ALTER TABLE操作
    mysql常用的索引种类
    xShell终端中文乱码-解决方法
    git删除本地分支和远程分支
    git版本回退
    log4j2配置文件log4j2.xml详解
  • 原文地址:https://www.cnblogs.com/clemente/p/13747491.html
Copyright © 2020-2023  润新知