多态的概念
不同类型的对象 ( 这些类型具有继承关系 ),去做同一件事情时,会产生不同的结果.
以人买票为例:
代码实现多态
( 1 ) 虚函数
被virtual修饰的 类成员函数 叫虚函数.
( 2 ) 虚函数的重写
子类有一个和父类完全相同的虚函数(返回类型、函数名、参数类型列表完全相同),
则子类重写了父类的虚函数
class Person
{
public:
//父类的虚函数
virtual void buy_ticket()
{
cout << "全价买票" << endl;
}
};
class Student:public Person
{
public:
//子类重写了父类的虚函数
virtual void buy_ticket()
{
cout << "半价买票" << endl;
}
};
( 3 ) 父类的指针或引用去调用虚函数
父类的指针/引用可以指向父类对象,也可以指向子类对象(切片).
父类指针 指向 父类对象,调用父类的虚函数;
父类指针 指向 子类对象,调用子类的虚函数.
void test1()
{
//父类指针指向父类对象
Person* p1 = new Person;
//父类指针指向子类对象(切片现象)
Person* p2 = new Student;
p1->buy_ticket(); //调用父类的虚函数
p2->buy_ticket(); //调用子类的虚函数
}
--特例
( 1 ) 子类要重写虚函数,可以不加virtual
class Person
{
public:
//父类的虚函数
virtual void buy_ticket()
{
cout << "全价买票" << endl;
}
};
class Student:public Person
{
public:
//子类不加virtual,仍然重写了父类的虚函数
void buy_ticket()
{
cout << "半价买票" << endl;
}
};
( 2 ) 协变:返回值可以不同,但必须是具有父子关系的指针/引用.
父类的虚函数返回父类指针或引用;
子类的虚函数返回子类指针或引用.
只要是具有父子关系的指针/引用都可以,不限类型
class Person
{
public:
//协变
//返回类型是父子关系的指针/引用
virtual Person* buy_ticket()
{
cout << "全价买票" << endl;
return nullptr;
}
};
class Student:public Person
{
public:
Student* buy_ticket()
{
cout << "半价买票" << endl;
return nullptr;
}
};
--接口继承和实现继承
( 1 ) 普通函数的继承是实现继承,把父类整个函数全部继承下来,包括函数接口和具体实现.
class A
{
public:
void ordinary_func()
{
cout << "普通函数继承:" << "this指针类型:" << typeid(this).name() << endl;
}
};
class B:public A
{public:};
void test1()
{
B b;
b.ordinary_func();
}
b.ordinary_func()--编译器处理为b.ordinary_func(&b),
将 B* 传递给this指针,由于接口使用的是父类,
父类的this指针是A* const类型,中间会发生切片.
( 2 ) 构成多态是接口继承,把父类的虚函数接口声明拿下来
class A
{
public:
virtual void virtual_func(int a = 10)
{
cout << "参数a= " << a << endl;
}
};
class B:public A
{
public:
void virtual_func(int a = 5)
{
cout << "参数a= " << a << endl;
}
};
void test2()
{
A* ptr = new B;
ptr->virtual_func();//构成多态,接口继承 a = 10
B b;
b.virtual_func();//不构成多态,普通继承 a = 5
}
多态的原理
为什么能实现:
父类指针/引用 指向 父类对象,调用父类的虚函数
父类指针/引用 指向 子类对象,调用子类的虚函数
--虚函数表
( 1 ) 对于一个类,编译器会把虚函数的地址,放进虚函数表中
一般而言,每个类都有各自的虚函数表.
( 2 ) 类对象,会多一个成员变量,保存虚函数表的地址
同类对象会共享一个虚函数表.
class A
{
public:
virtual void func1(){}
virtual void func2(){}
};
class B
{
public:
virtual void func3(){}
virtual void func4(){}
};
void test2()
{
A a;
cout << sizeof(A);
B b;
}
( 3 ) 在单继承中,若父类有虚函数,例:
class A
{
public:
virtual void func1(){}
virtual void func2(){}
public:
int _a;
};
class B:public A
{
public:
int _b;
};
B类对象和A类对象都只有一个虚表指针,但指向的虚表是不一样的.
( 4 ) 若子类没有完成虚函数的重写,
那 子类虚表的对应虚函数地址 和 父类虚表的对应虚函数地址 相同;
若子类完成虚函数的重写,
那 子类虚表的对应虚函数地址 和 父类虚表的对应虚函数地址 不同
class A
{
public:
virtual void func1(){}
virtual void func2(){}
public:
int _a;
};
class B:public A
{
public:
//重写func1(),不重写func2()
virtual void func1(){}
public:
int _b;
};
完整结论
编译器检查构成多态,运行时就会到 父类指针/引用 指向的对象中,
找到对象的虚函数表指针 -> 去对象的虚函数表中找到对应虚函数地址
( 5 ) 虚函数表存在哪里?
一个类的虚函数表是存在常量区/代码段的,
在编译阶段就生成.
--多态调用特例
若父类有虚函数,没有完成重写,此时用父类指针/引用去调用该虚函数时,
仍然会到虚表中找虚函数地址.
class A
{
public:
virtual void func1(){}
};
class B:public A
{public:};
void test4()
{
A* a = new B;
a->func1();
}
编译器不会具体看有没有完成重写,
调用函数在父类中是否是虚函数?是否是父类指针或引用去调用?
只要满足如上两个条件,就去指向对象的虚表中找到虚函数地址.
但是没完成重写,父类和子类的虚表里,存的都是父类的虚函数地址.
--模拟调用虚函数
以32位平台为例,因为指针长度较短.
以下面的代码为例:
class A
{
public:
virtual void func1()
{
cout << "A::func1()" << endl;
}
virtual void func2()
{
cout << "A::func2()" << endl;
}
public:
int _a;
};
class B:public A
{
public:
virtual void func1()
{
cout << "B::func1()" << endl;
}
virtual void func2()
{
cout << "B::func2()" << endl;
}
public:
int _b;
};
虚函数表本质是一个函数指针数组,
即一个数组,每个元素都是函数地址,每个元素的类型都是函数指针类型.
typedef void(*VFPTR)();//将【无返回值无参的函数】指针类型 typedef 成 VFPTR
具体步骤:
( 1 ) 拿到对象中,虚函数表的指针(一般在前4/8byte).
void test5()
{
A a;
*(int*)&a;//拿到虚函数表的地址,但是用int表示
(VFPTR*)*(int*)&a;//该虚函数表存的都是VFPTR类型元素,强转成VFPTR*
}
( 2 ) 用虚函数表指针找到虚函数表.
( 3 ) 打印虚函数表的内容,并调用虚函数表里的虚函数.
//显示传虚函数数目
void printVFTable(VFPTR vftable[], int num)
{
for (int i = 0; i < num; ++i)
{
cout << vftable[i] << endl;
vftable[i]();//函数地址+()可以直接调用,已经越过了正常的调用方式
}
}
void test5()
{
A a;
B b;
printVFTable((VFPTR*)*(int*)&a, 2);
cout << endl;
printVFTable((VFPTR*)*(int*)&b, 2);
}
多继承里的多态
--对象模型
会出现多个虚表的情况.
例:
class A1
{
public:
virtual void func1(){}
virtual void func2(){}
public:
int _a1 = 1;
};
class A2
{
public:
virtual void func3(){}
virtual void func4(){}
public:
int _a2 = 2;
};
class B:public A1,public A2
{
public:
int _b = 3;
};
--特殊情况
若子类单独增加一个虚函数,只会被放进第一张虚表中.
class B:public A1,public A2
{
virtual void func5(){}
public:
int _b = 3;
};
void test7()
{
B b;
//打印第一张虚表
printVFTable((VFPTR*)*(int*)&b, 3);
cout << endl;
//打印第二张虚表
//切片
A2* a2 = &b;
printVFTable((VFPTR*)*(int*)a2, 3);//在某些编译器会崩溃,因为第二张虚表里只有2个虚函数地址
}
构造函数/析构函数与虚函数
--构造函数不能是虚函数
对象的虚函数表指针,是在构造函数里初始化列表阶段,进行初始化的.
( 1 ) 若虚函数表指针没有初始化,无法找到对象的虚函数表
( 2 ) 构造函数若作为虚函数,多态调用时,就会先找虚函数表,再调用构造函数.
但此时虚函数表指针没有初始化,无法找到虚函数表,也无法调用构造函数.
--析构函数建议为虚函数
( 1 ) 析构函数名会被编译器统一处理为destructor,
因此父类析构函数加了virtual,子类都完成了虚函数重写.
( 2 ) delete释放空间时,会先调用析构函数清理对象,再调用operator delete()释放堆区空间.
( 3 ) 父类指针可以指向 new出来的父类对象,也可以指向 new出来的子类对象.
在delete时,只有把父类的析构函数设为虚函数,才能正确调用析构函数
【即去指针指向的对象中,找到虚函数表指针,找到对应虚函数进行调用】
否则,一律只调用父类的析构函数,因为指针类型是父类的指针.
class A
{
public:
~A() { cout << "~A()" << endl; }
};
class B:public A
{
public:
~B() { cout << "~B()" << endl; }
};
void test8()
{
A* a = new A;
A* b = new B;
delete a;
delete b;
}
抽象类
( 1 ) 在虚函数声明后加上=0,则该虚函数为纯虚函数.
( 2 ) 包含纯虚函数的类叫抽象类,抽象类无法实例化出对象.
子类继承后,也不能实例化出对象,只有重写纯虚函数,子类才能实例化出对象.
class A
{
public:
virtual void func1() = 0;
};
class B:public A
{
public:
virtual void func1(){}
public:
int _b = 3;
};
void test8()
{
B b;
}
间接要求抽象类的子类必须重写纯虚函数.