1. 多态
1. C++中的虚函数的作用主要是实现了多态的机制。关于多态,简而言之就是用父类型别的指针指向其子类的实例,然后通过父类的指针调用实际子类的成员函数。这种技术可以让父类的指针有“多种形态”,这是一种泛型技术。
2. 所谓泛型技术,说白了就是试图使用不变的代码来实现可变的算法。比如:模板技术,RTTI技术,虚函数技术,要么是试图做到在编译时决议,要么试图做到运行时决议。
2. 多态特点
1. 多态是在不同继承关系的类对象,去调同一函数,产生了不同的行为。
2. 就是说,有一对继承关系的两个类,这两个类里面都有一个函数且名字、参数、返回值均相同,然后我们通过调用函数来实现不同类对象完成不同的事件。
3. 构成条件
- 调用函数的对象必须是指针或者引用。
- 被调用的函数必须是虚函数,且完成了虚函数的重写。
4. 虚函数表
1. 虚函数(Virtual Function)是通过一张虚函数表(Virtual Table)来实现的。简称为V-Table。在这个表中,主是要一个类的虚函数的地址表,这张表解决了继承、覆盖的问题,保证其容真实反应实际的函数。有虚函数的类的实例中这个表被分配在了这个实例的内存中。当我们用父类的指针来操作一个子类的时候,这张虚函数表就显得由为重要了,它就像一个地图一样,指明了实际所应该调用的函数。
2. C++的编译器应该是保证虚函数表的指针存在于对象实例中最前面的位置(这是为了保证取到虚函数表的有最高的性能——如果有多层继承或是多重继承的情况下)。所以假如对象A的类有虚函数,那么对象A首地址“&A”就是虚函数表的地址。
5. 虚函数“地址调用”的分析
解析“(Fun)*(int*)*(int*)(&base27_b_01)”
(&base27_b_01) 是虚函数表地址
(int*)(&base27_b_01) 强转为(int*) 类型
*(int*)(&base27_b_01) 解引用,表示虚函数表(非虚函数表地址),也是虚函数表的第一个函数指向入口地址
(int*)*(int*)(&base27_b_01) 强转为(int*) 类型
*(int*)*(int*)(&base27_b_01) 解引用,表示第一个虚函数(入口地址,函数首地址,真实的函数代码)
(Fun)*(int*)*(int*)(&base27_b_01) 再强转为自定义的函数指针,就可以调用了
总结是:两次解引用,三次强制转换。第一次解引用到真实虚函数表也是函数指向入口地址,
第二次解引用表示真正的函数代码段,函数入口地址,函数首地址
重要区分:表与表地址,函数与函数地址,指向函数入口地址与入口地址
6. 索引
1. 不用虚函数,派生类对基类成员函数重定义,基类指针赋值为子类的对象引用,调用的成员函数是基类成员函数。
2. 采用虚函数,派生类对基类成员函数重定义,基类指针赋值为子类的对象引用,调用的成员函数是子类成员函数。
3. 虚函数表
4. (Fun)*(int*)*(int*)(&base27_b_01) 因为我的系统指针占8位,转成(int*)类型会告警,那么转成(long*)怎么样?
5. gdb查看虚函数入口地址
————————————————————————————————————————-
1.
class base27_A{//基类
public:
void fun27_01(){//非虚函数
printf("我是基类\n");
}
};
class class27_A:public base27_A{//子类1
public:
void fun27_01(){
printf("我是子类1\n");
}
};
class class27_B:public base27_A{//子类2
public:
void fun27_01(){
printf("我是子类2\n");
}
};
void test_27_01(){//测试函数
base27_A *base;
class27_A class27_a_01;
class27_B class27_b_01;
base = &class27_a_01;//赋值子类1的对象,基类指针调用的还是基类成员函数。
base->fun27_01();
base = &class27_b_01;//赋值子类2的对象,基类指针调用的还是基类成员函数。
base->fun27_01();
}
输出结果:
我是基类
我是基类
2. 基类改为 虚函数,子类如上一样,结果就变了。
class base27_A{//基类
public:
virtual void fun27_01(){//添加关键字virtual,变成虚函数,结果就变了
printf("我是基类\n");
}
};
输出结果:
我是子类1
我是子类2
3. 虚函数表
/* 虚函数表 */
class base27_B{//基类
public:
virtual void fun27_01(){
printf("我是基类函数1\n");
}
virtual void fun27_02(){
printf("我是基类函数2\n");
}
virtual void fun27_03(){
printf("我是基类函数3\n");
}
};
class class27_c:public base27_B{//子类
public:
void fun27_01(){
printf("我是子类函数1\n");
}
void fun27_02(){
printf("我是子类函数2\n");
}
void fun27_03(){
printf("我是子类函数3\n");
}
};
void test_27_02(){//测试函数
typedef void(*Fun)(void); //定义一个函数指针别名
base27_B base27_b_01;//基类实例化一个对象
Fun pFun1 = NULL;//定义一个函数指针
printf("base27_b_01对象的虚函数表地址:%p\n",(int*)(&base27_b_01));
/*对象的首地址,“&base27_b_01”,把地址强制转为(int*)类型。(int*)类型好操作,
如要执行下一个虚函数表的下一个,加2就行了((int*)+2);我的linux系统要加2,因为指针占8字节*/
printf("对象base27_b_01第一个虚函数地址:%p\n",(int*)*(int*)(&base27_b_01));
//虚函数表地址,解引用就是第一个虚函数指向入口地址。
printf("对象base27_b_01第二个虚函数地址:%p\n",(int*)*(int*)(&base27_b_01)+2);
//第一个虚函数地址加1就是第二个函数指向入口地址。
pFun1 = (Fun)*(int*)*(int*)(&base27_b_01);//第一个函数入口地址,函数指向入口地址解引用就是入口地址,再强转为函数指针,就可以调用
pFun1();//调用函数
pFun1 = (Fun)*((int*)*(int*)(&base27_b_01)+2);//第二个虚函数函数入口地址
pFun1();//调用函数
pFun1 = (Fun)*((int*)*(int*)(&base27_b_01)+4);//第三个虚函数函数入口地址
pFun1();//调用函数,因为我的系统指针占用8字节是两个int大小,所以要加2。
/*
解析“(Fun)*(int*)*(int*)(&base27_b_01)”
(&base27_b_01) 是虚函数表地址
(int*)(&base27_b_01) 强转为(int*) 类型
*(int*)(&base27_b_01) 解引用,表示虚函数表(非虚函数表地址了),也是虚函数表的第一个函数入口地址
(int*)*(int*)(&base27_b_01) 强转为(int*) 类型
*(int*)*(int*)(&base27_b_01) 解引用,表示第一个虚函数(非入口地址了)
(Fun)*(int*)*(int*)(&base27_b_01) 再强转为自定义的函数指针,就可以调用了
总结是:两次解引用,三次强制转换。第一次解引用到真实虚函数表也是函数入口地址,
第二次解引用表示真正的函数代码段
重要区分:表与表地址,函数与函数地址,函数指向入口地址与入口地址
*/
输出结果:
base27_b_01对象的虚函数表地址:0x7ffcd0764470
对象base27_b_01第一个虚函数地址:0x400cb0
对象base27_b_01第二个虚函数地址:0x400cb8
我是基类函数1
我是基类函数2
我是基类函数3
测试函数地址实现子类成员函数(父类虚函数),在上面的测试函数下面添加如下代码
class27_c class27_c_01;
pFun1 = (Fun)*(int*)*(int*)(&class27_c_01);
pFun1();//执行第一个子类对象的成员函数(父类的虚函数)
pFun1 = (Fun)*((int*)*(int*)(&class27_c_01)+2);
pFun1();//执行第二个子类对象的成员函数(父类的虚函数)
pFun1 = (Fun)*((int*)*(int*)(&class27_c_01)+4);
pFun1();//执行第二个子类对象的成员函数(父类的虚函数)
输出结果:
我是基类函数1
我是基类函数2
我是基类函数3
我是子类函数1
我是子类函数2
我是子类函数3
4. (Fun)*(int*)*(int*)(&base27_b_01) 因为我的系统指针占8位,转成(int*)类型会告警,那么转成(long*)怎么样? 测试发现不再告警。
void test_27_03(){//测试函数
typedef void(*Fun)(void); //定义一个函数指针别名
base27_B base27_b_01;//基类实例化一个对象
Fun pFun1 = NULL;//定义一个函数指针
printf("base27_b_01对象的虚函数表地址:%p\n",(long*)(&base27_b_01));
printf("对象base27_b_01第一个虚函数地址:%p\n",(long*)*(long*)(&base27_b_01));
printf("对象base27_b_01第二个虚函数地址:%p\n",(long*)*(long*)(&base27_b_01)+2);
pFun1 = (Fun)*(long*)*(long*)(&base27_b_01);
pFun1();
pFun1 = (Fun)*((long*)*(long*)(&base27_b_01)+1);
pFun1();
pFun1 = (Fun)*((long*)*(long*)(&base27_b_01)+2);
pFun1();
printf("---------对象class27_c_01-------\n");
class27_c class27_c_01;
pFun1 = (Fun)*(long*)*(long*)(&class27_c_01);
pFun1();//执行第一个子类对象的成员函数(父类的虚函数)
pFun1 = (Fun)*((long*)*(long*)(&class27_c_01)+1);
pFun1();//执行第二个子类对象的成员函数(父类的虚函数)
pFun1 = (Fun)*((long*)*(long*)(&class27_c_01)+2);
pFun1();//执行第二个子类对象的成员函数(父类的虚函数)
}
此时编译没有告警
输出结果:
base27_b_01对象的虚函数表地址:0x7ffd44f29dc0
对象base27_b_01第一个虚函数地址:0x400e10
对象base27_b_01第二个虚函数地址:0x400e20
我是基类函数1
我是基类函数2
我是基类函数3
---------对象class27_c_01-------
我是子类函数1
我是子类函数2
我是子类函数3
5. gdb查看虚函数入口地址。以上代码添加如下代码
printf("---------虚函数入口地址-------\n");
printf("对象base27_b_01第一个虚函数入口地址:%p\n",(long*)*(long*)*(long*)(&base27_b_01));
printf("对象base27_b_01第二个虚函数入口地址:%p\n",(long*)*((long*)*(long*(&base27_b_01)+1));
printf("对象base27_b_01第三个虚函数入口地址:%p\n",(long*)*((long*)*(long*(&base27_b_01)+2));
输出结果:
---------虚函数入口地址-------
对象base27_b_01第一个虚函数入口地址:0x400b26
对象base27_b_01第二个虚函数入口地址:0x400b3e
对象base27_b_01第三个虚函数入口地址:0x400b56
gda查看结果。分析得出,此命令查看的是虚函数入口地址,而不是,虚函数表里的指向入口地址,是指向入口地址再解引用得到函数入口地址。从“地址差”也可以发现不是恒定的指针字节大小(8byte)。