多态性
多态性(polymorphism)是考虑不同层次的类中,以及在同一个类中,同名的成员函数之间的关系问题。函数的重载,运算符的重载,属于编译时的多态性(早期绑定:编译期就确定了调用关系)。以虚函数为基础的运行时的多态性(晚期绑定:程序在运行过程中才能确定调用关系 是面向对象程序设计的标志性特征,体现了类推和比喻的思想方法。
编译时多态
编译器编译时就确定了调用关系,就叫做早期绑定,也叫作动态绑定。
虚函数
虚函数是一个类的成员函数,定义格式如下:
virtual 返回类型 函数名(参数列表);
- 关键字virtual指明该成员函数为虚函数,只有类的成员函数才可以定义为虚函数,virtual仅用于类定义中,如果虚函数在在类中声明,类外定义,类外可不加virtual。但在声明时应尽量放在类体内完成。
- 当某一个类的一个类成员函数被定义为虚函数,则由该类派生出的所有派生类中,该函数始终保持虚函数的特征。
C++中的虚函数的作用主要是实现了多态的机制。关于多态,简而言之就是 用父类型别的指针指向其子类的实例,然后通过父类的指针调用实际子类的成员函数。 这种技术可以让父类的指针有“多种形态”,这是一种泛型技术。所谓泛型技术,说白了就是试图使用不变的代码来实现可变的算法。
运行时的多态
它是通过类继承关系public和虚函数来实现,目的是建立一种通用的程序。
注意 : 必须通过引用或者指针调动虚函数时,才能够达到运行时的多态的效果。
总结:运行时多态需要满足以下三个要求:
- 公有继承(是一个)
- 想要虚化的函数加上virtual关键字
- 通过指针或者引用绑定函数
示例:
class Animal
{
private:
string name;
public:
Animal(const string & na):name(na){}
public:
virtual void eat(){}
virtual void walk(){}
virtual void tail(){}
virtual void PrintInfo(){}
string & Get_name(){return name;}
const string & Get_name() const {return name;}
};
class Dog:public Animal
{
private:
string owner;
public:
Dog(const string & ow,const string &na):Animal(na),owner(ow){}
virtual void eat(){cout<<"Dog eat:bone"<<endl;}
virtual void walk(){cout<<"Dog walk:run"<<endl;}
virtual void tail(){cout<<"Dog tail:Wang wang"<<endl;}
virtual void PrintInfo()
{
cout<<"Dog owner"<<owner<<endl;
cout<<"Dog name"<<Get_name()<<endl;
}
};
class Cat:public Animal
{
private:
string owner;
public:
Cat(const string & ow,const string &na):Animal(na),owner(ow){}
virtual void eat(){cout<<"Cat eat:fish"<<endl;}
virtual void walk(){cout<<"Dog walk:silent"<<endl;}
virtual void tail(){cout<<"Dog tail:Miao miao"<<endl;}
virtual void PrintInfo()
{
cout<<"Cat owner"<<owner<<endl;
cout<<"Cat name"<<Get_name()<<endl;
}
};
void fun(Animal & animal)
{
animal.eat();
animal.walk();
animal.tail();
animal.PrintInfo();
}
int main()
{
Dog dog("mk","erha");
Cat cat("bw","persian");
fun(dog);
fun(cat);
return 0;
}
注意:每一个存在虚函数的类实例化出的对象都存在虚表指针,但是虚表只有一份,
编联时需要确定的属性
- 类型class
- 可访问性
- 函数的默认值
虚函数表
虚函数的实现是因为存在一张虚函数表,这一过程中发生了同名覆盖,有虚函数的类在编译过程中才会产生虚表。 下面我们来辨析以下通过指针或引用调动和 对象调动虚函数的编联方案的异同: 比如下面的代码: 再来看下面的示例,更加清楚地了解同名覆盖以及虚表指针的转换过程(构造函数发挥作用): 同名覆盖:在子类中重写的虚函数,在虚表中会被替换 其实,在类中存在虚函数时,构造函数除了构建对象,初始化对象的数据成员以外,还要将虚表地址传递给虚表指针. 在构造函数中也不能完成如下操作:
memset(this,0,sizeof(Base));
这样很危险,因为这样会将对象内的所有成员初始化成0,虚表指针也会被修改。
所以,我们向前面所说的一样,不能将构造函数定义成虚函数,原因是:调动虚函数时,我们需要查表,构造函数是虚表建立的前提,若构造函数为虚函数,那么此时我们调动虚化的构造函数,虚表还没有建立,所以构造函数,拷贝构造函数,移动构造函数都不能被建立成为虚函数。
再来判断下面程序的输出结果:
- t1传入自己的this指针,查看自身的虚表,发现自己没有fun函数,于是调动其上一层base的fun函数
- base传入自己的this指针,查看自身的虚表,发现自己有fun函数,直接调动
- obj传入自己的this指针,查看自身的虚表,发现自有,于是调动自身的fun函数
继续探究:下面程序的执行结果为什么是:Base::print::10 前面我们提到了编联时需要确定的属性,也要注意重写虚方法时只需要保证三同(同函数名,同参数类型,同返回类型),并没有要求函数的默认值必须相同。
- 编译时,我就需要确定可访问性,对于op来说,它是一个Object类型,因此他就可以访问自己的print函数,函数的默认值在编译时也就确定为10,
- 运行时,发现是通过指针访问虚函数,因此采取动态编联方案,通过查表后又调动Base的print函数
那么为什么可以通过op访问Base的私有虚函数呢? 因为,该程序的编译是可以通过的,在运行时编译器不会考虑访问属性的影响。
虚析构函数
先看下面的代码:
class Object
{
private:
int value;
public:
Object(int x = 0):value(x){cout<<"Create Object:"<<this<<endl;}
virtual void add(){cout<<"Object add"<<endl;}
virtual ~Object(){cout<<"Destroy Object"<<this<<endl;}
};
class Base:public Object
{
private:
int num;
public:
Base(int x = 0):num(x){cout<<"Create Base:"<<this<<endl;}
virtual void add(){cout<<"Base add"<<endl;}
~Base(){cout<<"Destroy Base"<<this<<endl;}
};
int main()
{
Object *op = new Base(10);
op->add();
delete op;
op = NULL;
return 0;
}
上述代码的执行结果如下:
我们可以看到,其实它并没有调用Base对象的析构函数,仅仅调用了父类对象的虚构函数,究其原因是因为:二者的析构函数非虚函数,所以编译器在解析时发现,op是一个Object的指针,此时就只会调动Object的析构函数。
为了做到运行时多态的释放,就必须让析构函数变成虚函数
因为,一旦父类的析构函数定义为虚函数,那么所有派生类的析构函数也都会变成虚函数,因此无需在派生类中声明,在调动delete,传入父类指针时,就会将父类对象和子类对象都析构。
修改代码后执行结果如下: 那么为什么我们要将析构函数定义成虚函数呢?
就是因为在上述的应用场景下,当父对象的指针指向子对象时,我们动态开辟空间,在释放父指针时,我们采用连级释放的机制,从而调动子类的析构。
析构函数的另一个作用
reset重置虚表,以上述代码为例,在析构过程中先析构子对象,然后此时整个Base对象就剩下隐藏基对象的资源和空间,然后重置虚表指针指向隐藏基对象的首地址,再对该对象进行析构。
纯虚函数
纯虚函数是指被标明为不具体实现的虚拟成员函数,它用于这种情况:定义一个基类时,会遇到无法定义基类中虚函数的具体实现,其实现依赖于不同的派生类。 定义纯虚函数的一般格式是:
virtual 返回类型 函数名(参数列表) = 0;
"= 0 "表示程序员将不定义该函数,函数声明是为派生类保留一个位置,其本质是将指向函数体的指针定为NULL。
总结
虚函数和纯虚函数的区别
|