1.多态的概念
可以理解为事物存在的多种体现形态,对各种对象发出同一种指令,各个对象能根据自身的情况作出相应的回应。
2.多态的实现条件
存在继承,父子类之间的关系;
存在重写;
父类指针或引用指向子类对象,且调用重写函数。
3.三个概念
重载:
同一个类中,就是函数的作用域要相同;
函数名字相同,参数不同;
Virtual关键字可有可无,只要满足重载的要求即可了
重写:
只发生在父类和子类之间;
函数的名字和参数必须相同;返回值类型也要相同,如果父类返回的是父类引用,子类就返回子类。
父类函数前必须有virtual关键字修饰;
只有重写才能实现父子类之间的多态。
是指子类中重新编写父类中的虚函数的实现。
隐藏(覆盖):
指子类的函数屏蔽了与其同名的父类中的函数。隐藏的规则如下:
如果派生类中的函数与父类的函数同名,但是参数不同,此时无论有无virtual关键字,父类中的同名函数将被隐藏。就是在子类中不能通过相同的函数名调用父类中的函数。需要调用必须用父类名::函数
如果派生类的函数与基类的函数同名,并且参数也相同,但是基类函数没有virtual关键字。此时,基类的函数被隐藏,注意由于没有virtual修饰,构不成重写。
所以总结一下,如果父类中函数前没有virtual修饰,只要函数同名都是隐藏,如果有virtual修饰要看原型是不是一样,不一样还是隐藏。重写是一种特殊的隐藏。
4.虚函数实现多态
当类中声明虚函数时,编译器会为这个类生成一个虚函数表
虚函数表是一个存储 类中被virtual修饰的虚函数的地址 的数据结构
虚函数表是由编译器自动产生和维护
Virtual成员函数地址会被编译器放入虚函数表中
存在虚函数的类,创建的每个对象都有一个指向此虚函数表的指针,同类对象指针内容相同,都指向这个类的虚函数表。
注意:C++ class中没有声明权限的时候,默认是private,struct中默认是public
下面从内存布局的角度说明一下虚函数的实现:
例子1:
class DemoA
{
public:
int a;
void fun1()
{
cout << "class DemoA.fun1"<<endl;
}
void fun2()
{
cout << "class DemoA.fun2" << endl;
}
virtual void fun3()
{
cout <<"Class DemoA.virtual fun3"<< endl;
}
virtual void fun4()
{
cout <<"class DemoA.virtual fun4" << endl;
}
};
//虚函数表指针 就存放在类创建的对象的起始位置 占四个字节
int main()
{
DemoA Demo;
DemoA Demo1;
//虚函数表指针占4个字节 且在对象的初始成员位置
cout << sizeof(Demo) << endl;
//取的是Demo对象的地址 可以看出虚函数表的指针存放在对象地址的起始位置
cout <<&Demo << endl;
cout <<&Demo.a << endl;
//这出可以看出 同一个类的两个对象里的虚函数表指针里的值是一样的 都指向这个类的虚函数表
cout << *(int*)&Demo << endl;
cout <<*(int*)&Demo1 << endl;
//通过对象的地址调用public成员函数,此时虚函数和一般函数一样调用
(&Demo)->fun3();
(&Demo)->fun4();
(&Demo)->fun2();
return 0;
}
注意:在继承的时候,会继承成员变量和成员函数,但是成员函数是不占这个类对象的内存的,只有成员变量和虚函数表指针占对象的内存。
例子2:
class DemoA
{
//没有写public时默认的权限是private:
public:
int a;
void fun1()
{
cout << "class DemoA.fun1"<<endl;
}
void fun2()
{
cout << "class DemoA.fun2" << endl;
}
//含有一个虚函数
virtual void fun3()
{
cout <<"Class DemoA.virtual fun3"<< endl;
}
};
class DemoB:public DemoA //父类是DemoA
{
public:
//类DemoB 隐藏了类DemoA中的fun()1函数 不是重写 不是虚函数
void fun1()
{
cout << "DemoB fun1()" << endl;
}
/* //没有动fun2() 直接继承
void fun2()
{
cout << "DemoB fun2()" << endl;
}
*/
//重写了虚函数fun3 自身仍然是虚函数
void fun3()
{
cout <<"class DemoB .virtual fun3" << endl;
}
};
class DemoC : public DemoB // 父类有DemoA DemoB
{
//重写了fun3 没有动DemoB的fun1和fun2
public:
void fun3()
{
cout << "class DemoC.virtual fun3" << endl;
}
};
void test_3()
{
DemoA* n[3];
DemoA a;
DemoB b;
DemoC c;
a.fun3();
b.fun3();
c.fun3();
n[0] = &a;
n[1] = &b;
n[2] = &c;
cout << "-----virtual function array test----" << endl;
for(int i =0;i<3;i++)
{
n[i]->fun3();
}
//注意只有 父类引用和指针指向子类时 才会产生多态
cout <<"----------------------------------"<<endl;
cout <<"((classA*)&b).fun3():"<<endl;
((DemoA*)&b)->fun3();
//对象不会产生多态
cout <<"----------------------------------"<<endl;
cout <<"((classA)b).fun3():"<<endl;
((DemoA)b).fun3();
}
//多个类中探索继承 隐藏 重写
void test_2()
{
DemoA a;
DemoB b;
DemoC c;
cout <<"DemoA的对象大小:"<< sizeof(a) << endl;
cout <<"DemoB的对象大小:"<< sizeof(b) << endl;
cout <<"DemoC的对象大小:"<< sizeof(c) << endl;
//fun1被DemoB隐藏了,所以除了直接利用作用域调用fun1外,调用的都是DemoB中的
c.DemoA::fun1();
c.DemoB::fun1();
c.fun1();
//因为fun2这个函数只是被继承 没有隐藏和重写 所以不管怎么调用都是DemoA中的 A B都是C的父类
c.fun2();
c.DemoB::fun2();
c.DemoB::DemoA::fun2();
c.DemoA::fun2();
}
上面的例子中,DemoB DemoC分别重写了虚函数fun3,我们都知道,虚函数可以做到动态绑定,为了实现动态绑定,编译器通过产生一个虚函数表,在运行时,间接的调用实际上绑定的函数来达到动态绑定,虚函数表对程序员是不可见的,是编译器为我们的代码自动加上去的(更准确的讲,并不是为所有的代码都添加一张虚拟函数表,而是只针对那些包括虚函数的代码才加上这张表的)。
某一个类的虚函数表里面存放的是该类的虚函数的地址,在c++中,该表每一行的元素应该就是我们代码中虚拟函数地址了,也就是一个指针。有了这个地址,我们可以调用实际代码中的虚拟函数了。
编译器既然为我们的代码加了一张虚拟函数表,那这张虚拟函数表怎么与我们的代码关联起来呢? 要实现动态绑定,我们应该利用这张虚拟函数表来调用虚拟函数,为了达到目的,编译器又会为我们的代码增加一个成员变量,这个成员变量就是一个指向该虚拟函数表的指针,该成员变量通常被命名为:vptr。
每一个ClassA的实例,都会有一个虚拟函数表vptr,当我们在代码中通过这个实例来调用虚拟函数时,都是通过vptr先找到虚拟函数表,接着在虚拟函数表中再找出指向的某个真正的虚拟函数地址。虚拟函数表中的内容就是类中按顺序声明的虚拟函数组织起来的。
在派生的时候,子类都会继承父类的虚拟函数表vptr,但是虚函数表的地址肯定是每个类都不同的,我们只在把这个vptr成员在继承体系中一般看待就成了。
有一点要说明一下,当子类重写了父类中的虚拟函数时,同时子类的vptr成员也会作修改,此时,子类的vptr成员指向的虚拟函数表中的存放的虚拟函数指针不再是父类的虚拟函数地址了,而是子类所重写父类的虚拟函数地址。理解这一点就很容易想到了:原来多态体现在这里!
注意:多态发生在父子类之间,也就是必须用父类的指针或引用指向之类的时候,才会发生,而使用父类的变量却不行。首先,指针是指向一块内存的,而不管内存的大小,其都可以指向那块内存,所以可以实现父类的指针指向子类的地址处,只不过所容纳的内容有大有小,从子类的内容取出父类的是可以的。
当一个父类指针指向了其子类的对象地址,那么这个指针类型是父类的,所以其能调用的函数必须父类中要包含,因为其作用域就是父类所能操作的大小,一般的成员函数都是直接找到,当调用虚函数的时候,是通过虚函数表指针找到虚函数,因为虽让父类的指针指向了子类,但是虚函数表指针指向的还是子类的虚函数表,所以会出现多态。
而父类的变量是不能够直接用子类赋值的,因为子类占得内容>=父类,不能直接这样。直接赋值的话,会使内存结构破坏。
多态有助于实现拓展性和替换性。
5.纯虚函数
1.虚函数和纯虚函数可以定义在同一个类中,含有纯虚函数的类被称为抽象类,而只含有虚函数的类不能被称为抽象类。
2.虚函数可以被直接使用,也可以被子类重载以后,以多态的形式调用,而纯虚函数必须在子类中实现该函数才可以使用,因为纯虚函数在基类有声明而没有定义。
3.虚函数和纯虚函数都可以在子类中被重载,以多态的形式被调用。
4.虚函数和纯虚函数通常存在于抽象基类之中,被继承的子类重载,目的是提供一个统一的接口。
5.虚函数的定义形式:virtual{};纯虚函数的定义形式:virtual { } = 0;在虚函数和纯虚函数的定义中不能有static标识符,原因很简单,被static修饰的函数在编译时要求前期绑定,然而虚函数却是动态绑定,而且被两者修饰的函数生命周期也不一样。
虚函数充分体现了面向对象思想中的继承和多态性这两大特性,在C++语言里应用极广。比如在微软的MFC类库中,你会发现很多函数都有virtual关键字,也就是说,它们都是虚函数。难怪有人甚至称虚函数是C++语言的精髓。
定义纯虚函数就是为了让基类不可实例化,因为实例化这样的抽象数据结构本身并没有意义或者给出实现也没有意义。
纯虚函数只是一个接口,是个函数的声明而已,它要留到子类里去实现。
虚函数在子类里面也可以不重载的;但纯虚必须在子类去实现,这就像Java的接口一样。通常我们把很多函数加上virtual,是一个好的习惯,虽然牺牲了一些性能,但是增加了面向对象的多态性,因为你很难预料到父类里面的这个函数不在子类里面不去修改它的实现
相当于java中的接口,就是用来被实现的。先定义抽象类,然后用别的类实现这个类
抽象类就是用来搞多态的,不定义对象,定义指针和引用来使用多态,重写就可以实现多态。