什么是多态
- 多态是指调用同一个函数时,出现不同的效果。
多态分类
- 静态的多态:函数重载就是静态的多态
- 动态的多态:用父类的指针或引用调用虚函数
虚函数
- 虚函数是指:类的成员函数被 virtual 修饰的函数(普通函数不能用virtual修饰)
注意:
- 内联函数实际是没有地址的,但是如果内联函数被virtual修饰,则编译器会忽略函数的内联属性,把它当做正常函数使用。
- 静态成员函数是不能作为虚函数的,因为静态函数调用时不会隐式传入this指针,所以静态成员函数无法放入虚函数表。
- 构造函数不能是虚函数,因为构造函数阶段,虚表还没有初始化,所以构造函数不能是虚函数。
有虚函数的类,编译器会自动增加一个成员变量(虚表指针,指向虚表(函数指针数组))
- 例如下列类,sizeof(A) == 12
- 实际的成员变量为:1个指针类型,一个int类型,一个char类型
- 根据对齐规则,所以类的大小为 12 字节。
class A
{
public:
virtual void test() //这个就是虚函数
{}
int a;
char b;
}
继承中构成多态的条件
- 必须通过父类的指针或引用调用虚函数
被调用的函数必须是虚函数,且子类必须对虚函数进行覆盖。
- 注意:虚函数的覆盖,必须是返回类型、函数名、参数列表都相同才行。
- 如果返回值是父类、子类的指针或引用时,返回值不同也可以构成虚函数覆盖(协变)
- 如果父类的析构函数为虚函数,则子类的析构函数只要定义(无论是否virtual修饰),都与父类的虚析构函数构成覆盖。(析构函数本身函数名就不相同)
class A
{
public:
virtual void test() //这个就是虚函数
{}
}
class B
{
public:
virtual void test() //覆盖父类的虚函数
{}
}
虚函数的重写(覆盖)
- 子类会继承父类的虚函数,如果子类有父类虚函数的重写时,就会将重写的虚函数地址覆盖父类虚函数的地址。
- 注意:虚函数的重写,是重新实现父类的虚函数,所以重写的函数参数要使用父类的参数。
- 下列代码运行结果为:B->0
class A
{
public:
virtual void test1(int a = 0)
{
cout << "A->" << a << endl;
}
virtual void test() //父类函数被子类调用
{
test1(); //这里其实隐含了一个this,即this->test1()
//子类调用test,传递指针p,所以这里是 p->test1()
}
};
class B :public A
{
public:
virtual void test1(int a = 1)
{
cout << "B->" << a << endl;
//理论上这里应该是输出B->1
//但是这个函数重写父类的函数,是实现父类的函数
//所以a在这里要使用父类的a = 0
}
};
int main()
{
B* p = new B; //创建子类
p->test(); //子类调用父类成员
return 0;
}
虚析构函数的覆盖
- 下列 类A 的析构函数被声明为虚函数,子类的析构函数与父类构成覆盖
class A
{
public:
virtual ~A() //父类的析构函数声明为虚函数
{
cout << "~A" << endl;
}
};
class B :public A
{
public:
~B() //与父类的虚析构函数构成覆盖
{
cout << "~B" << endl;
}
};
int main()
{
A* p1 = new A;
A* p2 = new B;
delete p1;
delete p2;
return 0;
}
- 构成覆盖后,delete p2时,会先调用子类的析构函数,再调用父类的析构函数。
- 如果没有构成覆盖,则delete p2时只会调用父类的析构函数。
关键字final和override
final:修饰虚函数,使虚函数不能被覆盖。(加在父类中)
- final修饰类时,表示这个类不能被继承。
- override:修饰虚函数,检测是否正确覆盖。(加在子类中)
class A
{
public:
virtual void test()final //使该虚函数不能覆盖
{
}
}
class B: public A
{
public:
virtual void test()override //检查虚函数是否正确覆盖
{}
}
重载、覆盖、隐藏的对比
重载:
- 两个函数在同一作用域
- 函数名相同,参数不同
覆盖:
- 两个函数分别在父类和子类中
- 函数名、参数、返回值必须相同(协变除外)
- 两个函数必须是虚函数
隐藏:
- 父类和子类拥有同名成员,当调用这些同名成员时,无法确定调用的是父类还是子类
- 所以子类不能直接调用父类的成员,必须指明这个成员属于哪个类。
- 即子类成员屏蔽了父类成员的直接访问(这就是隐藏)
纯虚函数(抽象类)
- 在虚函数后面加上 =0,则这个函数为纯虚函数。
- 包含纯虚函数的类叫做抽象类(接口类),抽象类不能实例化出对象。
- 子类继承抽象类后也不能实例化出对象,只有覆盖(重写)纯虚函数,子类才能实例化出对象。
- 纯虚函数强制子类必须覆盖函数,更加体现出接口继承。
- 纯虚函数不需要实现功能,只需要声明(重写时再实现功能)
class A //抽象类
{
public:
virtual void test() = 0; //纯虚函数
}
虚表与虚表指针
- 虚表全称虚函数表,用来存放类中所有虚函数的指针,一个类只会有一份虚表(无论创建多少个对象)
- 虚表会在编译阶段生成,在运行阶段使用。
- 虚表指针全称虚函数表指针,指向虚函数表。
- 虚表指针是编译器自动创建的,在程序运行时,会根据对象类型去初始化虚表指针,从而让虚表指针指向所属类的虚表。
- 注意:对象的前四个字节就是虚表的地址,虚表存放在常量区(虚表是不能人为更改的)
多态的示例
class A
{
public:
virtual void test() //父类虚函数
{
cout << "A的类" << endl;
}
};
class B :public A
{
public:
virtual void test() //虚函数的覆盖
{
cout << "B的类" << endl;
}
};
void f1(A& p) //使用引用
{
p.test();
}
void f2(A p)
{
p.test();
}
int main()
{
A aa;
B bb;
f1(aa); //输出 A的类
f1(bb); //输出 B的类
f2(aa); //输出 A的类
f2(bb); //输出 A的类
return 0;
}
- void f1(A& p) 使用父类的引用,引用无需拷贝,直接将传递的参数拿来使用,所以可以分别调用父类的 test() 和子类的 test() 。
- void f2(A p) 使用父类的对象,相当于值传递,需要拷贝出一个临时变量,而拷贝时是不会拷贝虚表指针,虚表指针在程序运行时根据对象类型去初始化(对象p的类型是 A ,所以虚表指针指向 A的虚表)
- 所以 void f2(A p) 函数中,无论传递哪种子类的对象,最终调用的虚函数都是会是 A 父类的虚函数。
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。