派生类与基类 指针指向

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
来源:简书
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
posted @ 2020-09-28 23:20  clemente  阅读(3266)  评论(1编辑  收藏  举报