多态的C++实现

多态的C++实现

1 多态的原理

什么是多态?多态是面向对象的特性之一,只用父类指针指向子类的对象。

1.1 多态实现的三个条件

  • 存在继承
  • 虚函数重写
  • 父类指针指向子类对象

如下代码,如果并没有增加virtual关键字,并不会发生多态现象。


#include <iostream>
using namespace std;

class Parent {
    
    
public:
    
    Parent(int a, int b)
    {
        this->p1 = a;
        this->p2 = b;
    }
    
    //多态的重点:需要加virtual关键字,否则不会发生多态
    virtual void add()
    {
        cout<<"Parent: p1 + p2 "<<endl;
    }
    
private:
    int p1;
    int p2;
    
};

class Child :public Parent {
    
    
public:
    
    Child(int a, int b, int c, int d):Parent(c,d)
    {
        this->c1 = a;
        this->c2 = b;
    }
    
    void add()
    {
        cout<<"Child: c1 + c2 "<<endl;
    }
    
private:
    int c1;
    int c2;
    
    
};


void play(Parent *base)
{
    base->add();
}


int main(int argc, const char * argv[]) {
    
    Child c(1,2,3,4);
    
    Parent *pP = &c;
    Child  *pC = &c;
    
    play(pP);
    play(pC);
    
    return 0;
}


1.2 多态的实现原理

上面的例子中,在基类的函数增加了virtual关键字后,编译器会自动为子类对应的方法也会增加virtual关键字

1.2.1 虚函数表
  • 当类中声明了虚函数时,编译器会自动为类生成一张虚函数表。
  • 虚函数表一个存储类成员函数指针的数据结构
  • 虚函数表是有编译器自动生成与维护的
1.2.2 vptr指针

如果存在virtual关键字,编译器在运行时的时候(动态联编)会自动为当前对象增加vptr指针,这个vptr指针指向了当前类的虚函数表。

判断vptr指针是否存在

 //如果vptr指针存在,则对象sizeof()之后,大小会发生变化
 #include <iostream>
 using namespace std;

 class Parent {
    
    
 public:
    
    Parent(int a, int b)
    {
        this->p1 = a;
        this->p2 = b;
    }
    
    virtual void add()
    {
        cout<<"Parent: p1 + p2 "<<endl;
    }
    
 private:
    int p1;
    int p2;
    
 };
 
 int main(int argc, const char * argv[]) {
    
    Parent p(1,2);
    //可以分别测试下,添加virtual关键字和不加的内存空间大小
    cout<<sizeof(p)<<endl;
 }

1.2.2 多态实现原理
  1. 编译器发现存在virtual关键字,则会为类生成一张虚函数表
  2. 编译器会在动态联编(运行时)时为对象添加一个vptr指针
  3. 有父类对象指向子类对象存在,且执行了父类方法
  4. 如果当前对象有vptr指针存在,则会通过vptr指针找到对应的虚函数表,在虚函数表中查找对应的方法地址,执行。

2 vptr指针的分步初始化

2.1 父类构造函数中调用父类的方法,会产生多态吗?

如果再父类的构造函数中,调用父类的虚函数。那么在子类对象初始化的时候,会不会产生多态现象呢,还是仍然调用父类的虚函数呢?

答案是:否。不会产生多态。因为vptr指针是分步初始化的

#include <iostream>
using namespace std;

class Parent {
    
    
public:
    
    Parent(int a, int b)
    {
        this->p1 = a;
        this->p2 = b;
        
        this->add();    //调用父类的虚函数,这个地方不会产生多态现象。
    }
    
    virtual void add()
    {
        cout<<"Parent: p1 + p2 "<<endl;
    }
    
private:
    int p1;
    int p2;
    
};

class Child :public Parent {
    
    
public:
    
    Child(int a, int b, int c, int d):Parent(c,d)
    {
        this->c1 = a;
        this->c2 = b;
    }
    
    virtual void add()
    {
        cout<<"Child: c1 + c2 "<<endl;
    }
    
private:
    int c1;
    int c2;
    
    
};


void play(Parent *base)
{
    base->add();
}


int main(int argc, const char * argv[]) {
    
    //虽然会调用父类的构造函数,但是仍然是调用父类的add方法
    Child c(1,2,3,4);
    
    
    return 0;
}

2.2 vptr指针的分步初始化

那么vptr指针是如何初始化的呢?

  1. 编译器编译时,基类会产生虚函数表,子类也会产生虚函数表
  2. 当初始化子类对象的时候,先调用父类的构造函数,同时将子类对象的vptr指针指向父类的虚函数表
  3. 接下来,调用自己的构造函数,同时将vptr指针指向子类的虚函数表

上面的例子中,当调用父类构造函数时,当前vptr指针仍然指向父类的虚函数表,调用的仍然是父类的add方法,不会产生多态。

3 多态带来的问题

可以使用父类指针指向子类的对象,但是指针的类型却改变了,最指针进行++或者--操作时,可能带来意向不到的后果。

#include <iostream>
using namespace std;

class Parent {
    
    
public:
    
    Parent(int a, int b)
    {
        this->p1 = a;
        this->p2 = b;
    }
    
    virtual void print()
    {
        cout<<"p1 = "<<p1<<"; p2 = "<<p2<<endl;
    }

    
private:
    int p1;
    int p2;
    
};

class Child :public Parent {
    
    
public:
    
    Child(int a, int b, int c, int d):Parent(c,d)
    {
        this->c1 = a;
        this->c2 = b;
    }
    
    virtual void print()
    {
        cout<<"c1 = "<<c1<<"; c2 = "<<c2<<endl;
    }
    
private:
    int c1;
    int c2;
    
    
};


int main(int argc, const char * argv[]) {
    
    
    
    Child array[] = {Child(1,2,3,4),Child(5,6,7,8),Child(9,10,11,12),Child(13,14,15,17),Child(18,19,20,21)};
    
    Child *c = array;
    c->print();
    c++;
    c->print();
    
    Parent *p = array;
    p->print(); //仍然打印的是子类的(没问题)
    p++;        //执行++操作
    p->print(); //执行之后,崩溃(因为p++和c++的步长不一样导致错误)
    
    
    return 0;
}

4 纯虚函数和虚基类(抽象类)

  • 纯虚函数是一个在基类中说明的虚函数,在基类中没有定义,要求任何派生类都定义自己的版本
  • 纯虚函数为各派生类提供了一个公共界面(接口的封装和设计、软件的模块功能划分)
  • 纯虚函数的声明virtual void print() = 0;
  • 一个具有纯虚函数的基类成为抽象类

注意:

  • 抽象类不能建立对象,但可以声明抽象类的指针
  • 抽象类不能作为返回来兴,也不能作为参数类型
  • 抽象类可以声明抽象类的引用
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,772评论 6 477
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,458评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,610评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,640评论 1 276
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,657评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,590评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,962评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,631评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,870评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,611评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,704评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,386评论 4 319
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,969评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,944评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,179评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 44,742评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,440评论 2 342

推荐阅读更多精彩内容