关于cpp多态的一些随笔

多态实现的基础是虚函数。

首先最常见的一个问题就是:一个类的构造函数可不可以是虚函数?

嗯,这个答案当然是否定的,一个类的构造函数它不可能是虚函数。

image.png

我们知道虚函数是多态的基础,谈到多态就必须要讲到虚函数表(vtable)
子类和父类通过调用虚函数表不同位置的函数指针,来实现对同一个同名函数的不同调用。
那么一个对象如何去找到虚表呢?
就是通过对象中的虚函数表指针
虚函数表指针是位于对象内存空间首位的一个指针,它指向虚函数表的位置。
虚函数表指针只有在对象被构造之后,也就是对象的构造函数被调用之后,它才会被生成。如果构造函数被设置为了虚函数,那么就进入了一个死锁

我们来看一个例子来证明虚函数表指针的存在:

class Person {
public:
    Person();
    int water;

    void flow();
    void drink();
};

sizeof(person) 为 4

将flow和drink函数变为虚函数后:

class Person {
public:
    Person();
    int water;

    virtual void flow();
    virtual void drink();
};

sizeof(person) 为 16

由于函数被设置为了虚函数,所以这个对象内存头部会增加一个虚函数表指针。
虚函数表指针的大小为8个字节,但为什么这个对象的总大小由4个字节变成了16个字节呢?
这里面涉及到了内存对齐的问题,整个对象的占用大小必须是对象内部最大成员的整数倍。
64位系统的指针占用8个字节,int变量占用4个字节,4 + 8 = 12,整体占用必须是8的倍数,所以为16字节。
这里附上内存对齐的二原则:

  • 前面的地址必须是后面的地址正数倍,不是就补齐。
  • 整个对象占用空间必须是最大字节的整数倍。

这里拓展一下虚函数表

虚函数表是装着函数指针的数组,每一个类都会有自己的虚函数表,注意这里说的是每一个类。
子类和父类都会有一张虚函数表,子类会继承父类的虚函数表,然后将父类中的虚函数覆盖重写为自己所实现的函数,这样去调用的时候就会找到自己想要实现的函数,也就实现了多态。
这里生成三个类,有一个线性的继承关系:

using namespace std;
class Person {
public:
    Person();
    int gender;

    virtual void flow() ;
    virtual void drink();

    virtual ~ Person();
};

class Male : public Person {
public:
    Male(){
        height = 175;
    }
    int height;
    int hair;
    ~ Male() {
        cout << "析构Male" << endl;
    }
};

class Child : public Male {
public:
    ~Child() {
        cout << "析构child" << endl;
    }
};

PS:可以打印出虚表的内存地址:

using namespace std;

int main()
{
    Person* person = new Person;
    Male* male = new Male;
    Child* child = new Child;

    std::cout << "person虚表地址: " << (int *) * (int *)(&person) << std::endl;
    std::cout << "male虚表地址: " << (int *) * (int *)(&male) << std::endl;
    std::cout << "child虚表地址: " << (int *) * (int *)(&child) << std::endl;

    return 0;
}

结果:
person虚表地址: 0x6bc01700
male虚表地址: 0x6bc01710
child虚表地址: 0x6bc01730
平台:macos clang

三个虚表的内存地址不同,说明是不同的虚表,注意虚表的位置一般位于.data段,是一个全局变量,不同平台可能位置不同,但一般都是一个全局变量。

析构函数可以是虚函数吗?

那么析构函数可不可以是虚函数呢?这个答案是肯定的,而且在有继承的情况下,父类析构函数必须要被设置为虚函数,要么可能就会存在内存泄漏的问题。
这是为什么呢?因为子类对象是包含有父类对象的全部信息的,在析构子类对象的时候,需要将父类部分占用内存一并释放掉。
调用子类的析构函数会同时虚构其父类对象,这是一种编译器决定的析构顺序。析构函数的调用次序是先析构子类的对象,然后再去虚构父类的对象。
如果父类的析构函数不被设置为虚函数的话,那么如果有继承发生,析构时就只会析构父类的对象。

int main()
{
    Person * person = new Child();
    delete person;
    return 0;
}

结果:
析构child
析构Male
析构Person

那么其实还有一个问题,为什么只能用父类的指针去指向子类的对象而不能反过来?

如果从感性的角度去解释的话,那么就是子类对象也是父类对象,但是父类对象并不是子类对象。
男人都是是人,但不能说人都是男人。
如果在代码中强制定义一个子类的指针去指向父类的对象,那么就会报错,但是我们可以通过强制类型转换的方式来通过编译,但需要注意的是这种行为是非常危险的,同时它会带来很多困惑。

image.png

我们用强制类型转换进行一个小测验:
其中flow是虚函数

class Person {
public:
    Person();
    int gender;

    virtual void flow();
    virtual void drink();

    virtual ~ Person();
};

class Male : public Person {
public:
    Male(){
        height = 175;
    }
    int height;
    int hair;
    ~ Male() {
        cout << "析构Male" << endl;
    }
};
class Child : public Male {
public:
    Child() {
        cout << "构造child" << endl;
        height = 130;
    }
    ~Child() {
        cout << "析构child" << endl;
    }

    void flow() {
        cout << "child flow" << endl;
    }
};
int main()
{
    Child* child = (Child *)(new Person);
    child->flow();
    cout << "height值为 " << child->height;

    return 0;
}

结果:
构造person
person flow
height值为 0

结果说明调用的是person的构造函数,并没有调用child的构造函数,flow函数调用的也是person的虚函数。child指针中的height值为0,其值也并不是父类中的height值,而是一个未经构造过的默认值。

结论:
定义一个子类指针,指向一个被强制类型转换成子类的父类对象,其虚函数表仍为父类的,其this指针仍然是指向子类的指针,而且这个this指针指向的是未经构造函数构造过的,采用默认值的对象。
这种操作容易带来很多困扰,所以不要这样搞。

纯虚函数

其实写到这里就剩下最后一个纯虚函数没有涉及到了,在类中把一个函数定义为纯虚函数,那么这个类是不能被实体化的。
感性来讲就是如果说类为是"人类"的话,它没有血肉,只是一个概念,所以不会形成实体。
对于父类中的纯虚函数,子类必须重新实现,这样才能够被实例化。一个没有实现父类纯虚函数的类是不能够被实例化的。但是同样可以用父类的指针指向子类对象。同时,父类的构造函数还会被调用,很神奇。

class Male : public Person {
public:
    Male(){
        cout << "构造person" << endl;
    }
    int height;
    int hair;
    ~ Male() {
        cout << "析构Male" << endl;
    }
};

class Child : public Male {
public:
    Child() {
        cout << "构造child" << endl;
        height = 130;
    }
    ~Child() {
        cout << "析构child" << endl;
    }

    void flow() {
        cout << "child flow" << endl;
    }
};

using namespace std;


int main()
{
    Person* peron = new Child;
    peron->flow();
    return 0;
}

结果:
构造person
构造child
child flow

假如直接构造Person对象会发生什么?

image.png

假如子类没有实现纯虚函数会发生什么?

image.png

你可能感兴趣的:(关于cpp多态的一些随笔)