1 虚函数
- 虚函数可以实现运行时的多态,在运行时觉得调用哪个函数体;
- 虚函数必须是非静态的成员函数,虚函数是属于对象的,不是属于整个类的,需要在运行时通过指针定位到指向的对象,然后决定调用哪个函数体;
- 虚函数经过派生之后,就可以实现运行过程中的多态;
- 构造函数不能是虚函数,析构函数可以;
- 虚函数virtual只能出现在类定义的函数原型声明中,而不能在成员函数实现的时候;
- 虚函数不能是内联函数,因为对虚函数的调用需要动态绑定,而对内联函数的处理是静态的
#include <iostream>
using namespace std;
class Base1 {
public:
virtual void display() const;
}; //虚函数一般不声明为内联函数;
void Base1::display() const { //实现时不能写virtual
cout << "111" << endl;
}
class Base2 : public Base1{
virtual void display() const;
};
void Base2::display() const{
cout << "222" << endl;
}
class Derived : public Base2 {
virtual void display() const;
};
void Derived::display() const {
cout << "333" << endl;
}
void haha(Base1* ptr) {
//指针类型是指向初始基类的指针
ptr->display();
}
int main()
{
Base1 base1;
Base2 base2;
Derived derived;
Base1* ptr1 = &base1;
haha(ptr1);
haha(&base1);
haha(&base2);
haha(&derived);
}
输出:
111
111
222
333
2 构造函数不可以为虚函数
尽管虚函数表vtable是在编译阶段就已经建立的,但指向虚函数表的指针vptr是在运行阶段实例化对象时才产生的。 如果类含有虚函数,编译器会在构造函数中添加代码来创建vptr。 问题来了,如果构造函数是虚的,那么它需要vptr来访问vtable,可这个时候vptr还没产生。 因此,构造函数不可以为虚函数。
我们之所以使用虚函数,是因为需要在信息不全的情况下进行多态运行。而构造函数是用来初始化实例的,实例的类型必须是明确的。 因此,构造函数没有必要被声明为虚函数。
构造函数不可以声明为虚函数。同时除了inline之外,构造函数不允许使用其它任何关键字。
3 虚析构函数
析构函数可以声明为虚函数。如果我们需要删除一个指向派生类的基类指针时,应该把析构函数声明为虚函数。 事实上,只要一个类有可能会被其它类所继承, 就应该声明虚析构函数(哪怕该析构函数不执行任何操作)。
- 很多情况下要写虚析构函数
- 仔细看注释
- 当类的对象离开了它的作用域或者delete表达式应用到一个类对象的指针上时,析构函数会自动调用。
如果不写virtual,del函数在静态编译时,总是调用Base1的析构函数;
如果加上virtual,编译器会在运行时根据指针ptr所指向的实际对象决定该调用哪个析构函数;
#include <iostream>
using namespace std;
class Base1 {
public:
Base1() {
cout << "Base1构造" << endl;
}
virtual ~Base1();
};
Base1::~Base1() {
cout << "Base1析构" << endl;
}
class Derived : public Base1 {
public:
Derived() {
cout << "Derived构造" << endl;
p = new int(0);
}
virtual ~Derived();
private:
int* p;
};
Derived::~Derived() {
cout << "Derived析构" << endl;
delete p;
}
void del(Base1* ptr) {
//指针类型是指向初始基类的指针
delete ptr;
}
int main()
{
Base1* ptr = new Derived();
//指针类型是指向初始基类的指针
del(ptr);
return 0;
}
输出:
Base1构造
Derived构造
Derived析构
Base1析构
4 虚函数的作用:与指向初始基类的指针结合,实现多态、统一性
上两个例子中,指针ptr的类型是指向初始基类的指针,这是因为所有派生类的对象都是初始基类的对象,指向所有派生类的指针都可以调用初始基类的成员(*this->Base1::fun),所以可以用指向基类的指针来作为下列指针
①指向所有动态构造的派生类对象的指针,即便有无数级派生;
②统一进行内存释放(delete)的函数的参数,无论要删除哪一级派生类的对象占用内存;()
③统一display函数等;
通过使用虚函数与指向基类的指针,可以精简程序结构,统一函数,实现多态性。
PS:②通过虚析构函数实现,基类析构函数必须是虚函数、③通过普通虚函数实现
5 虚函数中默认参数
虚函数是动态绑定的,默认参数是静态绑定的。默认参数的使用需要看指针或者应用本身的类型,而不是对象的类型。
静态编译时将x=10。
#include <iostream>
using namespace std;
class Base
{
public:
virtual void fun(int x = 10)
{
cout << "Base::fun(), x = " << x << endl;
}
};
class Derived : public Base
{
public:
virtual void fun(int x = 20)
{
cout << "Derived::fun(), x = " << x << endl;
}
};
int main()
{
Derived d1;
Base* bp = &d1;
bp->fun(); // 10
return 0;
}
输出:Derived::fun(), x = 10
6 基类虚函数不能是私有函数,派生类虚函数可以是私有函数
- 基类中的虚函数必须是公有的(或将调用函数设为友元),因为一般用基类的指针(Base* prt = new derived;)实现多态,基类虚函数为私有成员时,静态编译时语法检查不能通过;
- 派生类的虚函数的访问权限不影响虚函数的动态联编,也就是多态与成员函数的访问权限并没有什么关系,基类定义了虚函数,并且是 public ,那么派生类只要 override 虚函数无论放在什么样的访问权限下,都以基类的访问权限为主。
#include<iostream>
using namespace std;
class base
{
private:
virtual void func() { cout << "base : func()" << endl; }
};
class derived :public base
{
public:
virtual void func() { cout << "derived :func()" << endl; }
};
int main()
{
derived d;
base* pbase = &d;
pbase->func(); // 编译错误,需要将基类虚函数设为公有
return 0;
}
7 虚函数什么时候可以被内联?
①什么是内联
关键字inline必须与函数定义放在一起才能使函数成为内联,仅仅将inline放在函数声明前是不起任何作用的。
ps:函数的声明与函数的定义形式上十分相似,但是二者有着本质上的不同。声明是不开辟内存的,仅仅告诉编译器,要声明的部分存在,要预留一点空间。定义则需要开辟内存。
声明就是告诉使用者函数名是什么,返回值是什么,参数列表又是什么。
②inline说明对编译器来说只是一种建议, 编译器可以忽略这个建议的,比如,你将一个长达100多行的函数指定为inline,编译器就会自动忽略这个inline,将这个函数还原成普通函数。
③通常类成员函数都会被编译器考虑是否进行内联。 但通过基类指针或者引用调用的虚函数必定不能被内联。 当然,实体对象调用虚函数或者静态调用时可以被内联, 虚析构函数的静态调用也一定会被内联展开。
- 虚函数可以是内联函数,内联是可以修饰虚函数的,但是当虚函数表现多态性的时候不能内联。
- 内联是在编译器建议编译器内联,而虚函数的多态性在运行期,编译器无法知道运行期调用哪个代码,因此虚函数表现为多态性时(运行期)不可以内联。
- inline virtual 唯一可以内联的时候是:编译器知道所调用的对象是哪个类(如 Base::who()),这只有在编译器具有实际对象而不是对象的指针或引用时才会发生。
#include <iostream>
using namespace std;
class Base
{
public:
inline virtual void who()
{
cout << "I am Base\n";
}
virtual ~Base() {}
};
class Derived : public Base
{
public:
inline void who() // 不写inline时隐式内联
{
cout << "I am Derived\n";
}
};
int main()
{
// 此处的虚函数 who(),是通过类(Base)的具体对象(b)来调用的,编译期间就能确定了,所以它可以是内联的,但最终是否内联取决于编译器。
Base b;
b.who();
// 此处的虚函数是通过指针调用的,呈现多态性,需要在运行时期间才能确定,所以不能为内联。
Base *ptr = new Derived();
ptr->who();
// 因为Base有虚析构函数(virtual ~Base() {}),所以 delete 时,会先调用派生类(Derived)析构函数,再调用基类(Base)析构函数,防止内存泄漏。
delete ptr;
return 0;
}