多态的概念
多态:一词最初来源于希腊语,意思是具有多种形式或形态的情形,在C++语言中多态有着更广泛的含义。
静态多态
编译器在编译期间完成的,编译器根据函数实参的类型(可能会进行隐式类型转换),可推断出要调用那个函数,如果有对应的函数就调用该函数,否则出现编译错误。就像一个同样逻辑的加法函数,但是类型不一样,我们在使用时编译器自己就会识别到底应该使用哪个函数。
例如如下代码:
int Add(int left, int right)
{
return left+right;
}
double Add(double left, double right)
{
return left+right;
}
int main()
{
cout<<Add(1 , 2) << endl;
cout<<Add(12.34f, 56.78f)<< endl;
return 0;
}这样的代码,在编译运行时,编译器就会识别数据的类型,然后查找到合适的函数来进行运行这样的代码。
类似这样的情况就叫做静态多态。
动态多态
动态绑定:在程序执行期间(非编译期)判断所引用对象的实际类型,根据其实际类型调用相应的方法。
所以在这里我们就要使用到”virtual”这个关键字了,这个字加在成员函数的函数原型之前,就表明这个函数是一个虚函数,这样的话,就要在它的派生类中重新实现这个函数,这就叫做”函数的重写”,此时编译器会对这个函数实现动态绑定。注意:这块并不是函数的重载,要分清这两个概念。
函数重写的条件:
1.函数原型相同;
2.函数必须为虚函数。
3.调用虚函数时,要对虚函数进行重写,此时派生了中访问的权限可以与基类里面的权限不同。
动态绑定的条件:
1.该函数必须是虚函数;
2.必须通过基类类型的引用或指针调用虚函数。
但在这里还有一个概念–协变:基类函数返回基类的指针或引用,派生类函数返回派生类的指针或引用。在使用时要注意二者的不同,正确使用。
几个虚函数的条件:
1.构造函数不能成为虚函数,因为虚函数在类里面时,在内存上会在给当前类的变量前分配一个4字节空间,保存类中虚函数的地址,这个过程是在构造函数中完成的,若构造函数设置成虚函数,则没有虚表指针,继而无法调用虚函数;2.静态函数不能成为虚函数,因为静态函数中this无法调用,无法调用虚表;
3.友元函数不能成为虚函数;
4.赋值运算符重载函数可以成为虚函数,但不建议这么做;
5.析构函数要给成虚函数,因为会牵扯到系统空间的释放问题。
纯虚函数
在虚函数的形参列表后面直接加上” = 0 “。注意:有纯虚函数的类成为抽象类,抽象类不能实例化对象,它的实现在抽象类的派生类里面实现,只有抽象类的派生类才可以实力化出对象。
类外定义虚函数
类外定义虚函数时,必须在类内的函数声明前加上”virtual”关键字,而类外定义时不可加”virtual”关键字。虚表指针
在看虚表指针时,我们可以先来看看这样的代码:
在下文中的成员变量,我都是定义“public”的,这样便于操作,在实际操作中我们建议把成员变量定义成“private”型的,便与保护类的封装性。
class Base
{
public:
virtual void FunTest1()
{
cout << "Base::FunTest1()" << endl;
}
int _b;
};
class Derived:public Base
{
public:
virtual void FunTest1()
{
cout << "Derived::FunTest1()" << endl;
}
virtual void FunTest2()
{
cout << "Derived::FunTest2()" << endl;
}
int _d;
};
int main()
{
Derived d;
d._b = 1;
d._d = 2;
system("pasue");
return 0;
}
首先,我们使用sizeof来计算一下这块空间的大小,得出结论是12个字节。
这样的话,我们就可得到这样的内存分布空间
可以发现,在基类的前面有一个地址,我们把这个地址就叫做虚表指针,这块地址里面存放的就是基类里面的和派生类里面的虚函数的地址,那么,为什么不是派生类自己创建一个虚表指针呢?我自己猜想,大概是因为节省空间的原因吧。或许还有人问,派生类的虚函数的地址在这块虚表中是怎么存放的呢,与基类的存放有什么关联吗?
这样,我们通过下面这个代码简单的来看一下:
typedef void(*Fun)();
void Printvpf(Base& b)
{
int* virtable = (int*)*(int*)&b;
// 获得虚表里面的第一个地址
Fun* pFun = (Fun*)virtable;
// 强转为Fun*的函数指针
while (*pFun)
{
(*pFun)(); // 运行该地址保存的函数
pFun = (Fun*)++virtable;
}
}
int main()
{
Derived d;
d._b = 1;
d._d = 2;
Base& b = d;
Printvpf(b)
system("pasue");
return 0;
}
则会出现下面这样的输出:
这个输出充分证明了虚表里面保存的是当前类的虚函数地址。
8.虚函数的多继承
我们可以借鉴之前的菱形继承来看一下具体情况,但是此时菱形继承里面的函数均是虚函数。
class B
{
public:
virtual void FunTest1()
{
cout << "B::FunTest1" << endl;
}
int _b;
};
class C1:public B
{
public:
virtual void FunTest1()
{
cout << "C1::FunTest1" << endl;
}
virtual void FunTest2()
{
cout << "C1::FunTest2" << endl;
}
int _c1;
};
class C2 :public B
{
public:
virtual void FunTest1()
{
cout << "C2::FunTest1" << endl;
}
virtual void FunTest3()
{
cout << "C2::FunTest3" << endl;
}
int _c2;
};
class D :public C1, public C2
{
public:
virtual void FunTest1()
{
cout << "D::FunTest1" << endl;
}
virtual void FunTest2()
{
cout << "D::FunTest2" << endl;
}
virtual void FunTest3()
{
cout << "D::FunTest3" << endl;
}
virtual void FunTest4()
{
cout << "D::FunTest4" << endl;
}
int _d;
};
int main()
{
D temp;
temp.C1::_b = 0;
temp._c1 = 1;
temp.C2::_b = 2;
temp._c2 = 3;
temp._d = 4;
cout << sizeof(temp) << endl; //28
system("pause");
return 0;
}
首先,是C1类的内容,C1类的虚表指针和C1继承B类的_b和他自身的_c1,然后是C2类的内容,C2类的虚表指针和C2继承B类的_b和他自身的_c2,最后是派生类D中自己的元素_d。
上篇博客有写,在菱形继承的虚拟继承中,我们用这样的代码可以得到这样的内存结构:
class B
{
public:
void FunTest1()
{
cout << "B::FunTest1" << endl;
}
int _b;
};
class C1 :virtual public B
{
public:
void FunTest2()
{
cout << "C1::FunTest2" << endl;
}
int _c1;
};
class C2 :virtual public B
{
public:
void FunTest3()
{
cout << "C2::FunTest3" << endl;
}
int _c2;
};
class D :public C1, public C2
{
public:
void FunTest4()
{
cout << "D::FunTest4" << endl;
}
int _d;
};
int main()
{
D d;
d._b = 1;
d._c1 = 2;
d._c2 = 3;
d._d = 4;
cout << sizeof(d) << endl;
system("pause");
return 0;
}
只不过此时的地址空间里面保存的就是偏移量了。试想一下,我们把这个菱形继承里面的函数全部换成虚函数会出现什么情况。我们可以来试试:
class B
{
public:
virtual void FunTest1()
{
cout << "B::FunTest1" << endl;
}
int _b;
};
class C1:virtual public B
{
public:
virtual void FunTest1()
{
cout << "C1::FunTest1" << endl;
}
virtual void FunTest2()
{
cout << "C1::FunTest2" << endl;
}
int _c1;
};
class C2 :virtual public B
{
public:
virtual void FunTest1()
{
cout << "C2::FunTest1" << endl;
}
virtual void FunTest3()
{
cout << "C2::FunTest3" << endl;
}
int _c2;
};
class D :public C1, public C2
{
public:
virtual void FunTest1()
{
cout << "D::FunTest1" << endl;
}
virtual void FunTest2()
{
cout << "D::FunTest2" << endl;
}
virtual void FunTest3()
{
cout << "D::FunTest3" << endl;
}
virtual void FunTest4()
{
cout << "D::FunTest4" << endl;
}
int _d;
};
int main()
{
D temp;
temp._b = 1;
temp._c1 = 2;
temp._c2 = 3;
temp._d = 4;
cout << sizeof(temp) << endl;
system("pause");
return 0;
}
我们可以看到,在temp的空间最前面的就是C1的,这是因为我们在创建D类对象时,先继承的是C1类,继而继承的才是C2类,至于基类B,则是毫无疑问的在最后。在上面代码中,我们可以看到C1,C2都是虚拟继承的B类,我们也发现在内存中C1,C2都有两个地址空间,那到底哪个是偏移量,哪个是虚表指针呢?我们再次验证一番就行了,将两个地址都给入内存中,发现第二个地址保存的是偏移量,而且第一个偏移量还是“-4”,那是因为虚表指针在偏移量之前,相对自己的偏移量就是-4了。还有,D类中也有自己的虚函数,所以D类一定有自己的虚表指针啊,为什么不见了呢?那厮因为编译器在多继承的情况下,将派生类的虚函数的地址保存在第一个继承的类的虚表指针中。
接下来,还有这样一个情况:
class B
{
public:
virtual void FunTest1()
{
cout << "B::FunTest1" << endl;
}
int _b;
};
class D :virtual public B
{
public:
virtual void FunTest1()
{
cout << "D::FunTest1" << endl;
}
/*
virtual void FunTest2()
{
cout << "D::FunTest2" << endl;
}
*/
int _d;
};
int main()
{
D d;
d._b = 1;
d._d = 2;
cout << sizeof(d) << endl;
system("pause");
return 0;
}
大家可以看到,我将派生类中自身的虚函数屏蔽了,此时我么能得到的内存空间的分配是这样的:
一共16个字节,且第一个地址空间中保存的是偏移量,第二个地址空间里面保存的还是一个地址,这个地址里面保存的是什么呢?我们不妨用刚才的Printf函数来看一下,运行之后,发现是D类里面的FunTest函数,而且我们传基类对象,打印的就是基类的FunTest函数,我们传派生类对象,打印的就是派生类中的FunTest函数,所以,我们猜想,虚拟继承的情况下,在基类和派生类中的虚函数一致时,编译器只开辟一个虚表指针保存虚函数的地址,采用什么类型的对象时,编译器就使用什么类型的虚表指针。
但是我们把代码中屏蔽的放开之后,就会发现会多4个字节。如图所示:
那么,现在已经很明显了,多出来的地址肯定是一个虚表指针,这个就告诉我们:如果派生类中还有和基类中恶不同的虚函数,那么编译 器就会给派生类开辟一个4字节空间用作虚表指针的存放,但是如果派生类中的虚函数与基类中的虚函数一致时,编译器则不会多开辟一个4字节空间。
总结
多态里面的内容在了解继承之后会比较容易理解,但是对于虚表指针和虚表指针的位置以及派生类或者基类的虚表指针是否开辟,不开辟的话虚表会存放在哪里这种问题上还是需要深入理解,这对于我们在理解计算机内存和虚标问题上会有很大的帮助。
本文中若有不当、不全面之处,请大家指出,在评论区留言,我会第一时间改正。