1、多态
多态是在不同继承关系的类对象,去调用同一函数,产生了不同的行为。简而言之就是用父类型别的指针指向其子类的实例,然后通过父类的指针调用实际子类的成员函数。
那么在继承中要构成多态还有两个条件:
1、 必须通过基类的指针或者引用调用虚函数
2、 被调用的函数必须是虚函数,且派生类必须对基类的虚函数进行重写。
例如以下代码:
class Person {
public:
virtual void BuyTicket() { cout << "买票-全价" << endl; }
};
class Student : public Person {
public:
//virtual void BuyTicket() { cout << "买票-半价" << endl; }
//注意:在重写基类虚函数时,派生类的虚函数在不加virtual关键字时,虽然也可以构成重写(因为继
//承后基类的虚函数被继承下来了在派生类依旧保持虚函数属性),但是该种写法不是很规范,不建议这样使用
};
void Func(Person& p)
{
p.BuyTicket();
}
int main()
{
Person ps;
Student st;
Func(ps); // 输出"买票-全价
Func(st); // 输出"买票-半价
return 0;
我们知道,派生类的对象,指针,引用都能赋值给相应的基类,在我们调用Func(st),st赋值给p,如果不关键字virtual,调用的都是person中的BuyTicket()方法,而如果使用的虚方法,则调用的是Student 中的BuyTicket()方法。这是因为在有虚方法的类中实例的对象中存在虚函数表,派生类的虚函数表会对子类中的虚函数表进行重写(覆盖),所以在经过“切片”将派生类的对象赋值给基类的对象后,实际上基类的对象的虚函数表已经发生了改变。虚函数表的指针存在于对象实例中最前面的位置(这是为了保证取到虚函数表的有最高的性能——如果有多层继承或是多重继承的情况下)。 这意味着我们通过对象实例的地址得到这张虚函数表,然后就可以遍历其中函数指针,并调用相应的函数。
2、虚函数表
虚函数的重写(覆盖):派生类中有一个跟基类完全相同的虚函数(即派生类虚函数与基类虚函数的返回值类型、函数名字、参数列表完全相同),称子类的虚函数重写了基类的虚函数。虚函数重写的两个例外:1、 协变(基类与派生类虚函数返回值类型不同)
派生类重写基类虚函数时,与基类虚函数返回值类型不同。即基类虚函数返回基类对象的指针或者引用,派生类虚函数返回派生类对象的指针或者引用时,称为协变。2. 析构函数的重写(基类与派生类析构函数的名字不同)如果基类的析构函数为虚函数,此时派生类析构函数只要定义,无论是否加virtual关键字,都与基类的析构函数构成重写,虽然基类与派生类析构函数名字不同。虽然函数名不相同,看起来违背了重写的规则,其实不然,这里可以理解为编译器对析构函数的名称做了特殊处理,编译后析构函数的名称统一处理成destructor。
class Base
{
public:
virtual void Func1()
{
cout << "Base::Func1()" << endl;
}
virtual void Func2()
{
cout << "Base::Func2()" << endl;
}
void Func3()
{
cout << "Base::Func3()" << endl;
}
private:
int _b = 1;
};
class Derive : public Base
{
public:
virtual void Func1()
{
cout << "Derive::Func1()" << endl;
}
private:
int _d = 2;
};
int main()
{
Base b;
Derive d;
return 0;
}
Base类的对象b和Derive 类的对象d都有一个虚函数表(这里显示的是虚函数表指针,是一个数组指针),由于d对虚函数进行了重写,所以d中的虚函数表fun1的地址和fun2的地址是不同的。
2.1 单继承中的虚函数表
如果子类没有重载任何父类的函数。那么,在派生类的实例中,其虚函数表如下所示:1)虚函数按照其声明顺序放于表中。
2)父类的虚函数在子类的虚函数前面。
class Base
{
public:
virtual void Func1()
{
cout << "Base::Func1()" << endl;
}
virtual void Func2()
{
cout << "Base::Func2()" << endl;
}
void Func3()
{
cout << "Base::Func3()" << endl;
}
};
class Derive : public Base
{
public:
virtual void Func3()
{
cout << "Derive::Func3()" << endl;
}
virtual void Func4()
{
cout << "Derive::Func4()" << endl;
}
};
class people : public Derive
{
public:
virtual void Func5()
{
cout << "Derive::Func3()" << endl;
}
};
int main()
{
Base b;
Derive d;
people p;
return 0;
}
但是,我们发现,不论是子类Derive的对象d,还是子类people的对象p,显示的虚函表中只有fun1和fun2,这个其实可以理解为编译器的bug,其实在内部是存在其他的虚函数,我们可以将其打印出来。
class Base
{
public:
virtual void Func1()
{
cout << "Base::Func1()" << endl;
}
virtual void Func2()
{
cout << "Base::Func2()" << endl;
}
void Func3()
{
cout << "Base::Func3()" << endl;
}
};
class Derive : public Base
{
public:
virtual void Func3()
{
cout << "Derive::Func3()" << endl;
}
virtual void Func4()
{
cout << "Derive::Func4()" << endl;
}
};
class people : public Derive
{
public:
virtual void Func5()
{
cout << "people::Func5()" << endl;
}
};
typedef void(*VFPTR) ();
void PrintVTable(VFPTR vTable[])
{
// 依次取虚表中的虚函数指针打印并调用。调用就可以看出存的是哪个函数
cout << " 虚表地址>" << vTable << endl;
for (int i = 0; vTable[i] != nullptr; ++i)
{
printf(" 第%d个虚函数地址 :0X%x,->", i, vTable[i]);
VFPTR f = vTable[i];
f();
}
cout << endl;
}
int main()
{
Base b;
VFPTR* vTableb = (VFPTR*)(*(int*)&b);
PrintVTable(vTableb);
Derive d;
VFPTR* vTableb2 = (VFPTR*)(*(int*)&d);
PrintVTable(vTableb2);
people p;
VFPTR* vTableb3 = (VFPTR*)(*(int*)&p);
PrintVTable(vTableb3);
return 0;
}
可见,父类的虚函数在子类的虚函数前面。
当子类的虚函数重写后,1)覆盖的fun1函数被放到了虚表中原来父类虚函数的位置。2)没有被覆盖的函数依旧。
class Base
{
public:
virtual void Func1()
{
cout << "Base::Func1()" << endl;
}
virtual void Func2()
{
cout << "Base::Func2()" << endl;
}
void Func3()
{
cout << "Base::Func3()" << endl;
}
};
class Derive : public Base
{
public:
virtual void Func1()
{
cout << "Derive::Func3()" << endl;
}
virtual void Func4()
{
cout << "Derive::Func4()" << endl;
}
};
typedef void(*VFPTR) ();
void PrintVTable(VFPTR vTable[])
{
// 依次取虚表中的虚函数指针打印并调用。调用就可以看出存的是哪个函数
cout << " 虚表地址>" << vTable << endl;
for (int i = 0; vTable[i] != nullptr; ++i)
{
printf(" 第%d个虚函数地址 :0X%x,->", i, vTable[i]);
VFPTR f = vTable[i];
f();
}
cout << endl;
}
int main()
{
Base b;
VFPTR* vTableb = (VFPTR*)(*(int*)&b);
PrintVTable(vTableb);
Derive d;
VFPTR* vTableb2 = (VFPTR*)(*(int*)&d);
PrintVTable(vTableb2);
return 0;
}
2.2 函数指针和虚函数表
对上述求虚函数表地址的方法,使用了函数指针,这里再总结一下。
void (*fun)(char* str),很明显,这就是一个函数指针的表示方法,表示一个指针指向一个参数是char*,返回值是void的函数。
函数指针可以直接等于一个函数。
void Print(const char* str)
{
cout << str << endl;
}
void main()
{
void(*fun)(const char* str);
const char* str = "1234";
fun = Print;
//fun = Print(str);不允许
fun(str);//输出1234
}
如果我们需要多次使用这个函数,可以typedef,这样再需要使用是只需定义一个函数指针,然后对其进行初始化,最后运行。
void Print(const char* str)
{
cout << str << endl;
}
typedef void(*Fun)(const char* str) ;
void main()
{
const char* str = "1234";
Fun fun1 = NULL;//定义一个函数指针
fun1 = Print;//进行初始化
fun1(str);//运行
}
当我们需要打印一个数组是,可以这样传参。
void Print(int arr[])
{
for (int i = 0; i < 3; i++)
{
cout << arr[i] << " ";
}
cout << endl;
}
void main()
{
int arr[3] = { 1,2,3 };
Print(arr);
}
这时,在理解我们之前的求虚函数地址的代码就好理解了。定义一个函数指针类型VFPTR,由于对象的虚函数表指针都是存在对象的前四个字节,所以需要取出来。
(
V
F
P
T
R
∗
)
(
∗
(
i
n
t
∗
)
&
b
)
(VFPTR*)(*(int*)\&b)
(VFPTR∗)(∗(int∗)&b),
1、对b取地址,然后强转为int类型
2、再解引用取值,就取到了b对象头4bytes的值,这个值就是指向虚表的指针
3、再强转成VFPTR,因为虚表就是一个存VFPTR类型(虚函数指针类型)的数组。
4、虚表指针传递给PrintVTable进行打印虚表,因为虚表指针是一个函数指针数组,所以接受也需要是相同类型VFPTR vTable[]。
5、打印的时候根据我们之前演示的,定义一个函数指针,初始化,运行即可。
6、注意,在vs中,虚表数组的最后一个元素是NULL,而在Linux中,如果这个虚表结束,最后一个元素是0,如果这个虚表之后还有虚表,最后一个元素是1(多继承)
typedef void(*VFPTR) ();
void PrintVTable(VFPTR vTable[])
{
// 依次取虚表中的虚函数指针打印并调用。调用就可以看出存的是哪个函数
cout << " 虚表地址>" << vTable << endl;
for (int i = 0; vTable[i] != nullptr; ++i)
{
printf(" 第%d个虚函数地址 :0X%x,->", i, vTable[i]);
VFPTR f = vTable[i];
f();
}
cout << endl;
}
int main()
{
Base b;
VFPTR* vTableb = (VFPTR*)(*(int*)&b);
PrintVTable(vTableb);
Derive d;
VFPTR* vTableb2 = (VFPTR*)(*(int*)&d);
PrintVTable(vTableb2);
return 0;
}
2.3 多继承中的虚函数表
有虚函数覆盖时,1) 每个父类都有自己的虚表。2) 子类的成员函数被放到了第一个父类的表中。(所谓的第一个父类是按照声明顺序来判断的)这样做就是为了解决不同的父类类型的指针指向同一个子类实例,而能够调用到实际的函数。原理如下:
代码演示:
class Base1 {
public:
virtual void func1() { cout << "Base1::func1" << endl; }
virtual void func2() { cout << "Base1::func2" << endl; }
};
class Base2 {
public:
virtual void func1() { cout << "Base2::func1" << endl; }
virtual void func2() { cout << "Base2::func2" << endl; }
};
class Derive : public Base1, public Base2 {
public:
virtual void func1() { cout << "Derive::func1" << endl; }
virtual void func3() { cout << "Derive::func3" << endl; }
};
typedef void(*VFPTR) ();
void PrintVTable(VFPTR vTable[])
{
cout << " 虚表地址>" << vTable << endl;
for (int i = 0; vTable[i] != nullptr; ++i)
{
printf(" 第%d个虚函数地址 :0X%x,->", i, vTable[i]);
VFPTR f = vTable[i];
f();
}
cout << endl;
}
int main()
{
Derive d;
VFPTR* vTableb1 = (VFPTR*)(*(int*)&d);
PrintVTable(vTableb1);
VFPTR* vTableb2 = (VFPTR*)(*(int*)((char*)&d + sizeof(Base1)));
PrintVTable(vTableb2);
return 0;
}
2.4虚函数表的不安全性
1、在上面的图中我们可以看到Base1的虚表中有Derive的虚函数,所以在运行时,可以使用父类指针调用子类中的未覆盖父类的成员函数,我们可以通过指针的方式访问虚函数表来达到违反C++语义的行为。
2、如果父类的虚函数是private或是protected的,但这些非public的虚函数同样会存在于虚函数表中,所以,我们同样可以使用访问虚函数表的方式来访问这些non-public的虚函数,这是很容易做到的。
以上两点参考酷壳