您的位置:首页 > 编程语言 > C语言/C++

C++学习笔记之虚函数

2019-04-08 20:35 260 查看

面向对象程序设计的核心思想是数据抽象、继承和动态绑定。
通过使用数据抽象,我们可以将类的接口与实现分离;使用继承可以定义相似的类型并对其相似关系建模;使用动态绑定可以在一定程度上忽略相似类型的区别,而以统一的方式使用它们的对象。

在C++中,通过继承联系在一起的类构成一种层次关系,通常在层次关系的根部有一个基类,其他类直接或间接的从基类继承而来,这些继承来的类称为派生类。基类负责定义在层次关系中所有类共同拥有的成员,而每个派生类定义各自的成员。

基类将和类型相关的函数和不做改变由派生类直接继承的函数分别开。基类将希望派生类有自己自定义的版本的函数,将自己定义的函数进行覆盖时,会将这些函数声明为虚函数,在函数声明前加上virtual关键字。

C++中的多态是通过虚函数来实现的
观察下面的测试代码:

class Food
{
public:
void func()
{
cout<<"I am Food!"<<endl;
}
};

class Apple: public Food
{
public:
void func()
{
cout<<"I am Apple!"<<endl;
}
};

void pirnt(Food & food)
{
food.func();
}

int main()
{
Apple a1;

a1.func();
print(a1);

return 0;

}

将会输出:
I am Apple!
I am Food!

为什么使用Apple对象调用的方法输出了自身方法,而使用pirnt()函数输出的却是调用其基类的方法。
因为这是静态调用,在编译的时候就已经确定了将调用那个函数,在编译的时候,编译器会静态的将food看做就是Food类的对象从而调用Food类的方法。
要实现动态调用,使print函数调用派生类Apple的方法,就要将基类中的func定义为虚函数,实现动态调用。代码改变为:

class Food
{
public:
virtual void func()
{
cout<<"I am Food!"<<endl;
}
};

class Apple: public Food
{
public:
void func()   override
{
cout<<"I am Apple!"<<endl;
}
};

class Beef:public Food
{
public:
void func()  override
{
cout<<"I am Beef!"<<endl;
}
};
void pirnt(Food & food)
{
food.func();
}

int main()
{
Apple a1;
Beef b1;
print(a1)
print(b1);

return 0;

}

运行结果为
I am Apple!
I am Beef!
这就实现了动态的调用
在编译阶段,编译器不知道要调用哪个类的方法,只有在运行的时候才知道,具体food指向的是基类对象还是派生类的对象,从而实现了动态调用,实现了多态。
在实现派生类覆盖基类中的虚函数时,可以在这些覆盖函数前面加上virtual ,但也不是必须的,一旦某个函数定义为虚函数,在所有的派生类中都是虚函数。也可以在函数后面加上override显示表示覆盖基类中的虚函数,这是C++11新标准允许的表示方法。如果写了override而没有实现覆盖基类的虚函数就会包报错,可以防止一些不必要的失误。
virtual关键字只能出现在类内部的声明语句之前,不能出现在类外。

虚函数的基本低层实现

class Base
{
public:
virtual void func1(){}
virtual void func2(){}
virtual void func3(){}
};

class Derived: public Base
{
public :
virtual void func1(){}
virtual void func2(){}

};

在上述代码中,Derived继承了Base,Base定义了3个虚函数,Derived也覆盖了其func1和func2的方法,编译器看到这种情况,会为Base和Derived两个类建立一个虚函数表(Virtual Function Table,VFT),这个表里存的都是虚函数的函数指针。函数指针指向的都是虚函数定义的地方,可以把这个虚函数表看作是一个静态的数组,数组成员是虚函数的函数指针。在创建这两类的对象时,会隐式的生成一个*VFT指针,指向它们各自的虚函数表,如下图所示:

因为没有实现Derived没有实现fuc2的覆盖,为了在运行的时候正常的运行,Derived类的虚函数表会指向其基类的func1的虚函数定义的位置。
正如上面的

void pirnt(Food & food)
{
food.func();
}

int main()
{
Apple a1;
Beef b1;
print(a1)
print(b1);

return 0;

}

这个例子中,虽然Apple类和Beef类的实例都传递给了food被看做是Food类型的,但其底层的VFT指针还是指向自己类的虚函数表,所以在运行的时候,就调用自己函数,实现C++中的多态。

在基类中通常应该定义一个虚析构函数,即使函数体是空的,因为当我们用delete一个动态分配的对象的指针的时候将执行析构函数。如果该指针指向继承体系中的某个类型,就有可能出现指针的静态类型和动态类型不符的情况。所以和其他函数一样,我们将析构函数定义为虚函数,这样就能在运行的时候动态调用析构函数。如果基类的析构函数不是虚析构函数,则在delete一个指向派生类的基类指针的时候,就会未定义的行为。

任何除构造函数之外的非静态函数都可以是虚函数。
因为,虚函数表是在对象构造的时候在生成的,先有构造函数再有虚函数表,且对象的构造函数在对象的生命周期里也执行一次而已,只是简单的进行对象成员变量的初始化,所以如果将构造函数定义为虚函数没有什么太大作用。
而静态函数是属于类的,没有this指针,虚函数依靠vptr和vtable来处理。vptr是一个指针,在类的构造函数中创建生成,并且只能用this指针来访问它,因为它是类的一个成员,并且vptr指向保存虚函数地址的vtable.
对于静态成员函数,它没有this指针,所以无法访问vptr. 这就是为何static函数不能为virtual.
虚函数的调用关系:this -> vptr -> vtable ->virtual function

动态绑定
当我们使用基类的引用或指针来调用一个虚函数时将发生动态绑定。在运行的时候才确定具体要调用那个函数。
动态绑定只有通过指针或引用调用虚函数才会发生,而通过对象调用函数不管是否是虚函数,对象的类型是不会改变的,这在编译阶段就可以确定,所以通过对象调用函数只会有静态绑定。

一个派生类的函数如果覆盖了基类中的继承来的虚函数,则它的形参类型必须与被他覆盖的基类函数完全一致。同样,返回类型也必须一致,但有一种情况除外,就是类的虚函数返回本类的指针或引用的时候,可以不同。如果基类的虚函数返回自身地引用或指针,则派生类的返回类型可以是本身类的指针或引用。因为,派生类可以向基类进行隐式转换,基类的指针或引用可以接受派生类的指针或引用,反过来则不行。

因为,每一个派生类对象都会有基类的那一部分,而基类的引用或指针可以绑定到基类的那个部分。一个基类的对象可以独立的存在,也可以是作为派生类的基类中的那部分而存在。如果基类不是派生类的一部分的话,则它只含有基类定义的成员,而不含有派生类定义的成员。
一个基类对象可能是派生类对象的一部分,也可能不是,所以不存在从基类向派生类的自动类型的转化。

虚函数与默认实参
虚函数也和其他函数一样也可以使用默认实参,但实参的值由调用的静态类型来决定。

class A
{
virtual void func(int a=10, int b=20)
{
cout<<a<<b<<endl;
}

};

class B : public A
{
virtual void func(int a=0, int b=0)
{
cout<<a<<b<<endl;
}
}

int main()
{
B b;
A* a=b;

a->func();

return 0;

}

运行输出
1020

这和我们预想的结果不一样,应该输出00 才对,所以,如果虚函数使用默认实参,则基类和派生类的默认实参最好一致。

可以虚函数的回避机制
可以使用作用域运算符来实现。

class A
{
virtual void func(int a, int b)
{
cout<<"A"<<a<<b<<endl;
}

};

class B : public A
{
virtual void func(int a, int b)
{
cout<<"B"<<a<<b<<endl;
}
}

int main()
{
B b;
A* a=b;

a->A::func(10,10);
a->B::func(10,10);

return 0;

}

运行结果为
A1010
B1010

通过作用域运算符强制调用虚函数,不管对象指针实际指向的对象是什么,都通过作用域来调用,在编译阶段就可以实现。

纯虚函数
一个纯虚函数不需要定义,通过在函数体的位置(声明语句分号之前)书写=0,就行,=0只能出现在类内部的虚函数声明语句处。
含有虚函数的类都是抽象基类,抽象基类负责定义接口,而后续的其他类可覆盖该接口。不能定义抽象基类的对象,但能定义抽象基类的派生类的对象,抽象基类的派生类必须覆盖抽象基类中的虚函数。

内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: