多态:
概念:同一操作作用于不同的对象,可以有不同的解释,产生不同的执行结果,这就是多态性
简单概括是:一个接口,多种结果。
c++的多态性是通过虚函数实现的,虚函数是允许子类成员函数重写父类成员函数,当然父类成员函数前要加virtual关键字,子类可以不加,但是为了提高代码的清晰性,建议你最好加上。而子类重新定义父类成员函数的做法叫做覆盖(或者重写)。
只有重写了虚函数才可以体现c++的多态性,
有多态就有非多态,它们的区别是函数地址是晚绑定还是早绑定,那么问题又来了,早绑定和晚绑定是什么?
早绑定就是程序在编译阶段就可以确定函数的地址并生产代码,这是静态的。
晚绑定就是函数地址是在程序运行阶段确定的,这是动态的。
理解到这里我觉的还是不够,早绑定不就像静态联编吗?晚绑定就像动态联编。
下来我们来了解静态联编和动态联编
静态联编:静态联编是指联编工作是在编译阶段进行的,又叫做早期联编,在程序运行之前完成,要实现静态联编,在编译阶段必须确定程序中的函数调用,确定函数调用就必须确定函数地址,这就涉及了早绑定问题。静态联编对函数的选择是看对象的指针或者引用的类型。
eg:
class A
{
public:
void func()
{
cout<<"funcA"<<endl;
}
};
class B : public A
{
public:
void func()
{
cout<<"funcB"<<endl;
}
};
void Fun(A &a)
{
a.func();
}
int main()
{
A a;
B b;
Fun(a);
Fun(b);
system("pause");
}
结果:
从上面代码可以看出,在Fun函数中创建的对象类型是父类,并且通过对象的引用调用普通成员函数,这里仅仅与引用的类型有关,而与此时引用指向的对象无关。所以在main函数中不同的对象调用其实都调用的是父类对象
动态联编:动态联编是程序在运行阶段动态进行的,根据当时情况确定调用那个重名函数,这就是需要虚函数的实现了,又叫做晚期联编。动态联编对成员函数的选择是基于指向对象的类型,针对不同的对象类型会产生不同的结果。多态性和虚函数就体现了动态联编的特性。
那么构成动态联编必须满足下面的条件(也就是构成多态的条件)
- 当父类使用指针或引用指向子类的对象并调用相应的虚函数
- 子类与父类构成重写(即把重名函数定义为虚函数)
这里必须是父类对象的指针,因为虚函数会根据传入的指针或引用的对象类型来调用不同的函数,实现不同的功能,父类指针可以指向子类对象(可以进行切片处理),子类指针不能指向父类对象(因为子类不是完全继承父类,父类中私有成员是不可见的,是不能继承的)
class A
{
public:
virtual void func()
{
cout << "funcA" << endl;
}
};
class B : public A
{
public:
virtual void func()
{
cout << "funcB" << endl;
}
};
void Fun(A &a)
{
a.func();
}
int main()
{
A a;
B b;
Fun(a);
Fun(b);
system("pause");
return 0;
}
这样结果如下:
从上面代码看出,父子类中的重名函数func定义为虚函数时,但引用指向不同的对象,就执行不同的操作。这里既体现了多态性(同一操作作用于不同的对象,可以有不同的解释,产生不同的执行结果)。
总结一下:如果没有多态性,函数调用的地址是一定的,是静态的,静态的地址
调用时会一直调用同一个函数,而具有多态性了,函数调用的地址就是动态的,调用时就可以根据指向对象类型调用不同的函数,这就体现了一个接口,不同结果的目的。
多态和虚函数的总结:
- 子类重写父类的虚函数实现多态时,必须函数名,参数列表,返回值相同(协变除外)
- 父类中定义了虚函数,子类中该函数依然保持虚函数特性(就算子类中不写virtual)
- 只有类的成员函数才可以定义为虚函数
- 静态成员函数不能定义为虚函数
- 如果在类外定义虚函数,只能在声明时加virtual,不能在定义时加virtual
- 构造函数不能定义为虚函数,最好不要将operator=定义为虚函数,因为容易引起误会,并且没有必要,它们不会构成重写,因为返回类型不同。
- 不要在构造函数和析构函数里面调用虚函数,在构造函数和析构函数中,对象是不完整的,可能会发生未定义的行为。
必须把父类的析构函数定义为虚函数
协变是什么:子、父类虚函数返回类型不同,但返回类型都是子、父类的指针这样就构成了协变。
解释总结5:总结3说:只有类的成员函数才可以定义为虚函数,所以在类外不能定义为虚函数,在类外定义虚函数编译器会自动报错的。所以你声明在类里的虚函数,定义在类外的虚函数就会出现总结3说的错误。
解释6:虚函数的指针或地址放在虚表里的,因为对象还没有产生,虚表还没有初始化出来。
解释8:看下图
此时new B动态开辟出来一个子类对象,子类对象B指向父类指针p,因此在释放时,只调用了父类的析构函数~A,对象B没有释放,就存在内存泄漏问题。
而父类析构函数定义为虚函数时,就会将A,B对象都释放。
delete p;在这里它们构成了多态,为什么构成了多态?
因为析构函数在底层被处理成:p->destructor + operator delete,它们的函数名时相同的,并且父类为虚函数,还使用父类指针指向子类对象,满足多态要求,所以构成多态,所以delete指向子类对象,先析构完子类对象,因为子类继承了父类,所以再析构父类对象。所以必须把父类的析构函数定义为虚函数,否则会产生内存泄漏