多态概念
什么是多态:为了完成某个功能,调用不同的对象去完成时会产生不同结果,满足对象类型是父类就调用父类的函数,是子类就调用子类的函数,完成正确调用
如何构成多态(条件1,2同时满足):
1.通过父类指针或引用调用虚函数
2.被调用的函数为虚函数,且子类对父类的虚函数进行重写,虚函数的覆盖(覆盖指的是虚表中虚函数的覆盖,覆盖是原理层的叫法,在语法上叫做重写)
class A
{
public:
virtual void func()
{
cout << "A" << endl;
}
};
class B : public A
{
public:
virtual void func()
{
cout << "B" << endl;
}
};
void func(A &temp)
{
temp.func();
}
int main()
{
A t1;
func(t1); //A
B t2;
func(t2); //B
}
函数重写的两种特殊情况:
1.协变:父类的虚函数返回类型与子类重写的虚函数返回类型不同,即父类/子类使用指针或引用作为返回值时
class A{};
class B : public A {};
class Person
{
public:
virtual A* f() {
return new A;
}
};
class Student : public Person
{
public:
virtual B* f()
{
return new B;
}
};
2.析构函数的重写: 若父类的析构函数为虚函数,此时子类的析构函数只要定义,无论有无virtual关键字,都与父类的析构函数构成重载
class A
{
public:
virtual void ~A()
{
cout<<"~A"<<endl;
}
};
class B
{
public:
virtual ~B()
{
cout<<"~B"<<endl;
}
};
int main()
{
A* a = new A;
B* b = new B;
//只有子类对父类的析构函数进行重写,才会保证对象析构时,会正确调用各自的析构函数
delete a;
delete b;
}
override和final关键字
C++提供了override和final关键字可以帮助用户检查是否重写
1.override:检查子类虚函数是否已经重写了父类的某个虚函数,若没有完成重写则报错
class A
{
public:
virtual void func_1() {}
};
class B : public A
{
public:
virtual void func_1() override
{
cout << "func_2" << endl;
}
};
2.final: 修饰虚函数,表示该虚函数不能再被继承,也可以修饰类,使类不能被继承
class A
{
public:
virtual void func() final {}
};
class final B : public A
{
public:
//virtual void func(){}
};
class C : public B //报错,无法重写
{
public:
void func_1() {}
};
抽象类
抽象类概念:包含纯虚函数(在虚函数的后面加上 =0 )的类叫做抽象类
抽象类作用: 抽象类不能实例化对象,子类继承后也不能实例化出对象,只能通过重写纯虚函数来实
例化出对象,规范了子类必须重写
纯虚函数:在虚函数后面加上 =0 的虚函数叫做纯虚函数,纯虚函数体现出了接口继承
纯虚函数作用:
- 强制子类完成重写
- 表示抽象类
class A
{
public:
virtual void func() = 0; //不需要实现,纯虚函数
}
class B : public A
{
public:
virtual void func()
{
cout<<"func_1()"<<endl;
}
}
class C : public B
{
public:
virtual void func()
{
cout<<"func()_2"<<endl;
}
}
void Test()
{
A* p1 = new B;
p1.func();
A* p2 = new C;
p2.func();
}
多态实现原理
在说原理之前我们先来了解两个概念:虚函数表与虚表指针
虚函数表:含有虚函数的类都至少有一个虚函数表指针,因为虚函数地址要存放到虚函数表中所以虚函数表的本质就是一个存放虚函数指针的指针数组,并在数组的最后面放了一个nullptr
虚表指针:在包含虚函数的类中,类的对象内部包含一个虚表指针,用来指向属于自己类的虚表。为了使每个包含虚表的类的对象都拥有一个虚表指针,编译器在类中添加了一个指针 _vptr ,用来指向虚表。当类的对象在创建时就包含了这个指针,而且这个指针的值会自动指向类的虚表
子类的虚表如何生成?
- 1.先将父类中的虚表内容拷贝一份到子类虚表中
- 2.若子类重写了某个父类中的虚函数,则将子类中的虚函数覆盖掉虚表中父类的虚函数
- 3.子类新增加的虚函数按照在子类中的声明顺序增加到父类虚表的最后、
注意: - 1.父类和子类的虚表是不一样的,所以覆盖指的就是虚表中虚函数的覆盖
- 2.虚函数的指针赋值发生在编译器的编译阶段
- 3.虚表是属于类的,不属于某个具体的对象,一个类只需要一个虚表用来存放虚函数指针就够了
在了解了这两个概念之后,是不是对如何实现多态有了一些思考呢?
多态原理:满足多态之后的函数调用,不是在编译时就确定的,而是在运行之后到对象中去寻找,不满足多态的函数调用是编译器就确认的
静态绑定和动态绑定
- 1.静态绑定又称为前期绑定,在程序编译期间就确定了程序的行为,所以也被称为静态多态,如函数重载
- 2.动态绑定又称为后期绑定,在程序运行期间,根据不同类型的程序行为来调用具体的函数,所以也被称为动态多态
单继承和多继承关系的虚函数表
单继承的虚函数表
多继承的虚函数表
多继承子类中未重写的虚函数放在第一个继承的父类的虚函数表中
virtual 函数是动态绑定,而缺省参数值却是静态绑定。 意思是你可能会 在“调用一个定义于子类内的virtual函数”的同时,却使用父类为它所指定的缺省参数值。
所以绝不要重新定义继承而来的缺省参数值!
class A
{
public:
virtual void func(int val = 1){ std::cout<<"A->"<< val <<std::endl;}
virtual void test(){ func();}
};
class B : public A
{
public:
void func(int val=0){ std::cout<<"B->"<< val <<std::endl; }
};
int main(int argc ,char* argv[])
{
B*p = new B;
p->test(); //B->1
return 0;
}
关于虚表和虚函数指针的一些问题
虚函数和虚表存在哪里?
虚函数和普通函数相同都是存放在代码段,但是虚函数指针存放在了虚表中
虚表在vs下存放在了代码段(常量区)
虚表指针什么时候初始化?
执行父类或子类的构造函数时,父类或子类对象的虚表指针被初始化,指向自身的虚表
虚表指针在哪初始化?
在构造函数中进行虚表的创建和虚表指针的初始化
编译时多态与运行时多态
编译时多态:通过函数的参数列表的不同来区分不同函数,这些函数在编译后就会成为两个不同的函数名,比如函数重载
int add(int a,int b)
{
return a+b;
}
double add(double a,double b)//返回值,参数类型不同
{
return a+b;
}
int add(int a,int b,int c)//参数个数不同
{
return a+b+c;
}
运行时多态:在程序运行前,编译器不知到调用那个方法,而是运行时通过运算程序,动态计算出被调用的函数地址,如虚函数调用
class A
{
public:
virtual void func()
{
cout << "A" << endl;
}
};
class B : public A
{
public:
virtual void func()
{
cout << "B" << endl;
}
};
void func(A &temp)
{
temp.func();
}
int main()
{
A t1;
func(t1); //A
B t2;
func(t2); //B
}