1.虚函数是怎么实现的
简单的说,虚函数是通过虚函数表实现的。那么,什么是虚函数表呢?
事实上,如果一个类中含有虚函数,则系统会为这个类分配一个指针成员指向一个虚函数表(vtbl),表中每一项指向一个虚函数的地址,实现上就是一个函数指针的数组。为了说明虚函数表,请看下面的程序用例:
class Parent
{
public:
virtual void fool1(){}
virtual void fool2(){}
void fool3();
};
class Child1
{
public:
void fool1(){};
void fool3();
};
class Child2
{
public:
void fool1(){}
void fool2(){}
void fool3();
};
下面列出了各个类的虚函数表(vtbl)的内容。
Parent类的vbtl:Parent::fool()的地址,Parent::fool();
Child1类的vbtl:Child1::fool()的地址,Parent::fool();
Child2类的vbtl:Child1::fool()的地址,Child2::fool();
可以看出,虚函数表既有继承性,又有多态性。每个派生类的vtbl继承了它各个基类的vbtl,如果基类vbtl中包含某一项,则其派生类的vbtl中也将包含同样的一项,但是两项的值可能不同。如果派生类覆盖(override)了该项对象的虚函数,则派生类vtbl的该项指向重载后的虚函数,没有重载的话,则沿用基类的值。
在类对象的内存布局中,首先是该类的vbtl指针,然后才是对象数据。在通过对象指针调用一个虚函数时,编译器生成的代码将先获取对象类的vbtl指针,然后调用vbtl中对应的项。对于通过对象指针调用的情况,在编译期间无法确定指针指向的是基类对象还是派生类对象,或者是哪个派生类对象。但是在运行期间执行到调用语句时,这一点已经确定,编译后的调用代码能够根据具体对象获取正确的vtbl,调用正确的虚函数,从而实现多态性。
分析一下这里的思想所在,问题的实质是这样,对于发出虚函数调用的这个对象指针,在编译期间缺乏更多的信息,而在运行期间具备足够的信息,但那时已不再进行绑定了,怎么在二者之间做一个过渡呢?把绑定所需的信息用一种通用的数据结构记录下来,该数据结构可以同对象指针相联系,在编译时只需要使用这个数据结构进行抽象的绑定,而在运行期间将会得到正真的绑定。这个数据结构就是vtbl。可以看到,实现用户所需的抽象和多态需要进行后绑定,而编译器又是通过抽象和多态实现后绑定的。
2.构造函数调用虚函数
(C++虚拟机制的理解)
#include<stdio.h>
class A
{
public:
A(){doSth();}//构造函数调用虚函数
virtual void doSth(){printf("I am A");}
};
class B:public A
{
public:
virtual void doSth(){printf("I am B");}
};
int main()
{
B b;
return 0;
}
执行结果是什么?为什么?
解析如下:在构造函数中,虚拟机制不会发生作用,因为基类的构造函数在派生类构造函数之前执行,当基类构造函数运行时,派生类数据成员还没有被初始化。如果基类构造期间调用的虚函数向下匹配到派生类,派生类的函数理所当然会涉及本地数据成员,但是那些数据成员还没有被初始化,而调用涉及一个对象还没有被初始化的部分自然是危险的,所以C++会提示此路不通。因此,虚函数不会向下匹配到派生类,而是直接执行基类的函数。
所以,执行结果如下:
I am A
3.看代码写结果——虚函数的作用
#include<iostream>
using namespace std;
class A
{
public:
virtual void print(void)
{
cout<<"A::print()"<<endl;
}
};
class B:public A
{
pubic:
virtual void print(void)
{
cout<<"B:print()"<<endl;
}
};
class C:public A
{
public:
void print(void)
{
cout<<"C::print()"<<endl;
}
};
void print(A a)
{
a.print();
}
void main(void)
{
A a,*pa,*pb,*pc;
B b;
C c;
pa=&a;
pb=&b;
pc=&c;
41 a.print();
b.print();
c.print();
45 pa->print();
pb->print();
pc->print();
49 print(a);
print(b);
print(c);
}
解析:
代码第41-43行,分别使用类A,类B和类C的各个对象来调用其print()成员函数,因此执行的是各个类的print()成员函数。
代码第45-47行,使用3个类A的指针来调用print()成员函数,而这3个指针分别指向类A,类B和类C三个对象。由于print()函数时虚函数,因此这里有多态,执行的是各个类的print()成员函数。
代码第49-51行,全局的print()函数的参数使用传值得方式(注意与传引用的区别,如果是引用,则又是多态),在对象a,b,c分别传入时,在函数栈中会分别生成类A的临时对象,因此执行的都是类A的print()成员函数。
4.看代码写结构——虚函数
#include<iostream>
#include<string>
using namespace std;
void println(const std::string& msg)
{
cout<<msg<<"\n";
}
class Base
{
public:
Base()
{
println("Base::Base()");
virt();
}
void f()
{
println("Base::f()");
virt();
}
virtual void virt()
{
println("Base::virt()");
}
};
class derived:public Base
{
public:
Derived()
{
println("Derived::Derived()");
virt();
}
virtual void virt()
{
println("Derived::virt()");
}
};
int main(int argc,char *argv[])
{
Derived d;
Base *pB=&d;
pB->f();
return 0;
}
代码第54行,构造Derived对象d。首先调用Base的构造函数,然后调用Derived的构造函数。在Base类的构造函数中,又调用了虚函数virt(),此时虚拟机制还没有开始作用(因为在构造函数中),所以执行的是Base类的virt()函数。同样,在Derived类的构造函数中,执行的是Derived类的virt()函数。
代码第47行,通过Base类的指针pB访问Base类的公共成员函数f()。f()函数又调用了虚函数virt(),这里出现多态。由于指针pB是指向Derived类对象的,因此实际执行的是Derived类中的virt()成员。
5.虚函数相关的选择题
#include<iostream>
#include<complex>
using namespace std;
class Base
{
public:
Base(){cout<<"Base-ctor"<<endl;}
~Base(){cout<<"Base-dtor"<<endl;}
virtual void f(int){cout<<"Base::f(int)"<<endl;}
virtual void f(double){cout<<"Base::f(double)"<<endl;}
virtual void g(int i=10){cout<<"Base::g()"<<i<<endl;}
};
class Derived:public Base
{
public:
Derived(){cout<<"Derived-ctor"<<endl;}
~Derived(){cout<<"Derived-dtor"<<endl;}
void f(complex<double> c){cout<<"Derived::f(complex)"<<endl;}
virtual void g(int i=20){cout<<"Derived::g()"<<i<<endl;}
};
Base b;
Derived d;
Base* pb=new Derived;
从4个选项中选择正确的一个:
1)cout《sizeof(Base)《endl;
2)cout《sizeof(Derived)《endl;
3)pb->f(1.0);
4)pb->g();
解析:
题(1),Base类没有任何数据成员,并且含有虚函数,所以系统会为它分配一个指针指向虚函数表。指针的大小是4个字节。
题(2),Derived类没有任何数据成员,它是Base类的派生类,因此它继承了Base的虚函数表。系统也会为它分配一个指针指向这张虚函数表。
题(3),Base类中定义了两个f()的重载函数,Derived只有一个f(),其参数类型为complex,因此Derived并没有Base的f()进行覆盖。由于参数1.0默认是double类型的,因此调用的是Base::f(double)。
题(4),Base和Derived都定义了含有相同参数列表的g(),因此这里发生多态了。pb指针指向的是Derived类的对象,因此调用的是Derived类的g()。这里要注意,由于参数的值在编译期就已经决定的,而不是在运行期,因此参数i应该取Base类的默认值,即10.
6.为什么需要多重继承?它的优缺点是什么?
实际生活中,一些事物往往会拥有两个或两个以上事物的属性,为了解决这个问题,C++中引入了多重继承的概念。
多重继承的优点是对象可以调用多个基类中的接口。
多重继承的缺点是容易出现继承向上的二义性。解决措施:1)加上全局符确定调用哪一份拷贝;2)使用虚拟继承。使得多重继承只拥有父类的一份拷贝。
7.多重继承中的二义性?
#include<iostream>
class cat
{
public:
void show()
{
cout<<"cat"<<endl;
}
};
class fish
{
public:
void show()
{
cout<<"fish"<<endl;
}
};
class catfish:public cat,public fish
{
};
int main()
{
catfish obj;
obj.show();
return 0;
}
程序中catfish类多重继承cat类和fish类,因此继承了cat的show()方法和fish的show()方法。由于这两个方法同名,代码第27行直接用obj.show()时,无法区分应该执行哪个基本类的show方法,因此会出现编译错误。
可以改成obj.cat::show()访问cat中的show()成员。
8.多重继承二义性的消除
类A派生B和C,类D从B,C派生,如何将一个类A的指针指向一个类D的实例?
代码如下:
class A{};
class B:public A{};
class C:public A{};
class D:public B,public C{};
int main()
{
D d;
A *pd=&d;//编译错误
return 0;
}
由于B,C继承自A,B,C都拥有A的一份拷贝,类D多重继承自B,C,因此拥有A的两份拷贝。如果此时一个类A的指针指向一个类D的实例,会出现“模糊的转换”之类的编译错误。解决办法如下。
class A{}
class B:virtual public A {} //B虚拟继承自A
class C:virtual public A {} //C虚拟继承自A
class D:public B,public C {};
int main()
{
D d;
A *pd=&d;//成功转换
return 0;
}
将B,C都改为虚拟继承自A,则类D多重继承自B,C时,就不会重复拥有A的拷贝了,因此也就不会出现转换错误了。消除了继承的二义性。