您的位置:首页 > 编程语言 > C语言/C++

c++类型兼容规则与虚函数实现多态的实现原理和区别

2017-09-19 21:51 801 查看
大一之后就没有系统的学过c++了,最近为了校园招聘,又把c++知识捡出来学习,弄了2个钟头终于把这里面的原理弄懂了。

背景:

类型兼容规则含义:类型兼容规则指在需要基类对象的任何地方,都可以使用公有派生类的对象来替代。在替代之后,派生类对象就可以作为基类的对象使用,但只能使用从基类继承的成员

类型兼容规则应用场合:

有如下代码:

class B{...}

class D:public B{...}

B b1,* pb1;

D d1;

派生类的对象可以隐含转化为基类对象。eg:b1 = d1
派生类的对象可以初始化基类的引用。eg:B &rb = d1
派生类的指针可以隐含转化为基类的指针。eg:pb1 = &d1
虚函数含义:使用关键字virtual定义的类成员函数。
虚函数实习多态需要满足的条件:

类之间满足类型兼容规则。
要声明虚函数。(自动声明虚函数需要满足派生类函数同基类虚函数同名、同参、同返回或满足赋值兼容规则的指针、引用返回值)
由成员函数调用或者通过指针、引用来访问虚函数。
实践分析:

在分析之前需要知道如下的几个类和对象的内存分配方式:

所有类成员函数,无论静态与否实际上在内存中都只有一个副本,且保存在内存代码段。为对象开辟的内存只保存类中的成员变量。
虚函数是通过,对象中的虚表指针(vptr)访问虚表(vritual table)的方式确定指定虚函数的入口指针,从而实现对应虚函数的访问。
每个对象都有个虚表指针,该指针是在构造函数中被赋值,所有同类对象共享内存中的一个虚表。对象复制时的虚表指针不参与赋值
在虚表中,派生类的虚函数会覆盖基类中的同名同参同返回值的虚函数,并且隐藏基类中同名函数的所有重载。

场景1:派生类的指针可以隐含转化为基类的指针
program1:

#include <iostream>
using namespacestd;
class base{
public:
     void display()const;
    int a=1;
};
void base::display()const{
    cout<<"Base::display"<<endl;
}

class derived:publicbase{
public:
    virtualvoid display()const;
    int b=2;
};
void derived::display()const{
    cout<<"derived::display"<<endl;
}

void fun(base *p){
    p->display();
}
int main(){
    base b1;
    derived d1;
    
    fun(&b1);
    fun(&d1);
    return0;
}
output1:

Base::display
Base::display

program2:

#include <iostream>
using namespacestd;
class base{
public:
    virtualvoid display()const;
    int a=1;
};
void base::display()const{
    cout<<"Base::display"<<endl;
}

class derived:publicbase{
public:
    virtualvoid display()const;
    int b=2;
};
void derived::display()const{
    cout<<"derived::display"<<endl;
}

void fun(base *p){
    p->display();
}
int main(){
    base b1;
    derived d1;
    
    fun(&b1);
    fun(&d1);
    return0;
}
output2:

Base::display
derived::display

分析:
程序1没有使用虚函数,那么程序编译阶段处理语句p->display();的时候根据p的类型base将该函数,编译为base中display的函数体。编译的时候已经决定使用那个函数了。程序2使用了虚函数,编译阶段处理处理语句p->display();知道该语句是虚函数,对虚函数的访问是在运行过程中通过vptr指向的虚表实现动态调用。那么p所指向的对象内存中存放的vptr指向哪个虚表,该次执行便访问该虚表中对应的函数。因为派生类中display同基类中display同名同参同返回,所以在虚表中只保存派生类中的display入口地址。

场景2:派生类的对象可以初始化基类的引用
该过程同场景1一样,结果及原因都相同,即虚函数在该场景下可以重载。故不再赘述。

场景3:派生类的对象可以隐含转化为基类对象
program1:

#include <iostream>
using namespacestd;
class base{
public:
     void display()const;
    int a=1;
};
void base::display()const{
    cout<<"Base::display"<<endl;
}

class derived:publicbase{
public:
    virtualvoid display()const;
    int b=2;
};
void derived::display()const{
    cout<<"derived::display"<<endl;
}

void fun(base b){
    b.display();
}
int main(){
    base b1;
    derived d1;
    
    fun(b1);
    fun(d1);
    return0;
}
output1:

Base::display
Base::display

program2:

#include <iostream>
using namespacestd;
class base{
public:
    virtualvoid display()const;
    int a=1;
};
void base::display()const{
    cout<<"Base::display"<<endl;
}

class derived:publicbase{
public:
    virtualvoid display()const;
    int b=2;
};
void derived::display()const{
    cout<<"derived::display"<<endl;
}

void fun(base b){
    b.display();
}
int main(){
    base b1;
    derived d1;
    
    fun(b1);
    fun(d1);
    return0;
}
output2:

Base::display
Base::display

分析:
可以看出在该场景下b.display();没有虚函数没有实现多态。原因分析可能有两个,具体还有待进一步研究。

在编译阶段,因为虚函数是通过普通(非引用对象)调用的,所以编译阶段认为不符合,多态的条件。就直接根据类型原则,静态编译为base1的display函数了。
根据虚函数的调用原理,访问对象b的内存中vptr指向的虚表由于vptr不参与复制,它由类构造函数设置为指向属于base类所有对象的共公虚表,该虚表中指向dispaly函数的入口指针,只是在该类中定义的display函数的入口指针。故没有实现多态。

总结:

c++语法规则十分复制,而只要把一些机制的内部运行机理搞清楚,不用死记硬背,学起来就快很多了。
该文章中的分析只是,个人学习结果,具体正确性还有待进一步的验证。
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: