虚函数
虚函数是为了实现多态而设定的。而且只能在继承体系中成员函数可以设置为虚函数,虚函数是什么样子的呢?
如上图,我们fun()
函数即为虚函数。同时只有我们的成员数为虚函数才能够形成多态,在继承章节我们提到一个隐藏
的概念,那么在多态中我们还有一个重写(重写虚函数的实现)
的概念。
隐藏只要求我们基类和派生的函数名相同即可。
重写则需要函数返回值,函数名,函数参数类型(只是类型相同即可都要相同才可以构成多态。而重写有什么用呢?也是为了形成多态
多态
什么是多态呢?本质就是:不同人干同一件事,具有不同的效果
。如下段代码:
class Person
{
public:
virtual void fun()
{
cout << "买票-全价" << endl;
}
};
class Adult : public Person
{
public:
virtual void fun()
{
cout << "买票-全价" << endl;
}
};
class Child : public Person
{
public:
virtual void fun()
{
cout << "买票-半价" << endl;
}
};
class Soldier : public Person
{
public:
virtual void fun()
{
cout << "买票-优先" << endl;
}
};
void Buy_tickets(Person& p)
{
p.fun();
}
int main()
{
Adult _adu;
Child _chl;
Soldier _sol;
//调用买票这个函数
Buy_tickets(_adu);
Buy_tickets(_chl);
Buy_tickets(_sol);
return 0;
}
上段代码都是成员函数为虚函数的情况下,先来看一下我们成员函数不是虚函数的运行结果:
再来看一下类成员函数为虚函数的运行结果:
如上图我们可以总结多态的形成条件:
- 虚函数重写
- 父类的指针或引用调用
这就是多态,根据指针指向的内容的类型来决定调用谁的函数,就是不同人干一件事有不同的效果。同时上面两个条件必须严格遵守,缺一个都不能构成多态。
协变
上文提到,我们的重写必须返回值,函数名,参数类型都要相等,但是这里有一个例外,就是我们的返回值如果是继承关系的就可以不一样也可以形成多态。如下图:
这里的返回值是自己继承体系的也可以。
多态原理
那么上述指向谁调用谁是如何实现的呢来看一下代码:
class Base
{
public:
virtual void fun1() { cout << "virtual void fun1() - Base" << endl; }
virtual void fun2() { cout << "virtual void fun2() - Base" << endl; }
void fun3() { cout << "void fun3() - Base" << endl; }
};
class Derive : public Base
{
public:
virtual void fun1() { cout << "virtual void fun1() - Derive" << endl; }
virtual void fun2() { cout << "virtual void fun2() - Derive" << endl; }
void fun3() { cout << "void fun3() - Derive" << endl; }
};
void Func(Base& b)
{
b.fun1();
b.fun2();
b.fun3();
}
int main()
{
Base b;
Derive d;
Func(b);
cout << endl << endl;
Func(d);
return 0;
}
虚表
当我们有定义虚函数的时候,我们在基类或者派生类对象中会默认生成一个虚表。它存储着我们虚函数编译后的指令,当我们使用对应的虚函数的时候回去虚表中调用对应的虚函数。
如上图,我们虚表存储在对象的头四个节点上,它是一个函数指针数组的地址,所以我们的虚表本质是一个函数指针数组。而我们的虚函数和成员函数在同一个地方(代码段),编译器会把函数编译好之后的指令放入虚表中以供调用。
如上图,当我们派生类中重写的对应的虚函数的时候,派生类当中的虚存储的是派生类自己的虚函数,如果没有重写时则是基类的虚函数所以重写其实是一种特殊的隐藏,只不过重写的要求更加严格了。
动态绑定和静态绑定
函数调用的时候分为两种绑定方式:
- 静态绑定(编译时绑定)
普通成员函数
- 动态绑定(运行时绑定)
虚函数
如上图,对于我们的fun2()
是我们实现的虚函数,我们的fun3()
则不是虚函数,可以看到,fun2()的调用是在运行时才确定的,而我们fun3()则是直接就是call对应的地址。所以这也就是为什么我们多态可以实现指向谁调用谁的原因,因为虚函数的调用是运行时才确定的。
那么虚函数和成员函数是在代码段存着的,那虚表是在哪存储的呢?
如上图,我们虚表其实是存在代码段的。
析构函数
在我们继承章节的时候,我们在派生类构造的时候需要手动调用父类的构造函数,而析构的时候并不让我们去调用父类的析构,因为我们要先析构子类,最后子类自动调用父类的析构函数,而析构函数会被编译器默认统一命名为destructor()。
class Person
{
public:
Person()
{
cout << "Person()" << endl;
}
~Person()
{
cout << "~Person()" << endl;
}
};
class Student : public Person
{
public:
Student()
{
cout << "Student()" << endl;
}
~Student()
{
cout << "~Student()" << endl;
}
};
int main()
{
Student st;
return 0;
}
如上段代码,先来看一下直接创建对象的时候的运行结果:
如上图,我们析构和构造的调用次数是正确的,来看如下情况:
看如上图的情况,发生了经典的内存泄漏问题。
为什么会发生这样的问题呢?
我们的p是一个子类对象的指针,那么析构的时候应该把子类也析构了,但是此时我们的析构函数并不是虚函数,那么delete p
的本质是:析构 + operator delete。前面说了,析构函数会被默认转化为destructor(),但是由于这里析构函数并不是虚函数,所以调用析构的时候就是**什么类型的指针调用对应的析构函数,所以这里我们调用的是基类的析构函数。**所以这里我们只需要把析构函数加上我们的virtual即可。
所以这就是多态的好处,由于我们析构函数会被转化为destructor(),所以天生就是重写,当我们使用父类指针调用的时候会去执行指针指向对象的析构函数。
虚函数重写
如下段代码,它的运行结果是什么呢?
如上图,我们发现输出结果为: D e r i v e : − > 1 Derive:->1 Derive:−>1,所以虚函数重写只是重写我们函数的实现。而函数的声明使用的是父类的,这里要跟直接使用Derive对象调用fun()函数区分开,当我们直接是用Derive对象调用的时候输出的是: D e r i v e : − > 0 Derive:->0 Derive:−>0,他直接去对象的虚表中查找调用的。所以可以说当我们使用父类指针或引用调用的时候,虚函数的声明始终是我们父类的,但是函数的实现是由指针指向的对象决定的。
那么再来看一下下面的代码,运行结果是什么呢?
class A
{
public:
A()
:m_iVal(0)
{
cout << "A() " << endl;
test();
}
virtual ~A()
{
cout << "~A()" << endl;
}
virtual void func()
{
std::cout << m_iVal << " ";
}
void test()
{
func();
}
public:
int m_iVal;
};
class B : public A
{
public:
B()
:A()
{
cout << "B() " << endl;
test();
}
~B()
{
delete[] ptr;
cout << "~B()" << endl;
}
virtual void func()
{
++m_iVal;
std::cout << m_iVal << " ";
}
private:
int* ptr = new int[2];
};
int main()
{
A* p = new B;
p->test();
return 0;
}
这里需要注意的是,在我们B对象构造的过程会先去调用A的构造,那么在A构造的过程中,编译器会认为此时的类型是A类型,所以调用虚函数的时候,也是调用A的虚函数。
小知识
-
virtual
关键字只在声明时加上,在类外实现时不能加 -
.
static
和virtual
是不能同时使用的