C++: 虚函数,一些可能被忽视的细节
引言:关于C++虚函数,对某些细节的理解不深入,可能导致我们的程序无法按预期结果运行,或是表明我们对其基本原理理解不够透彻。本文详细解答以下几个问题:实现多态,忘记写virtual会怎么样?虚函数的默认参数可以重载吗?纯虚函数真的不能有实现吗?析构函数可以是纯虚函数吗?
1.1 虚函数是什么?
-
虚函数是在基类中使用关键字
virtual
声明的函数,它在派生类中可以被重写,且在运行时根据对象的类型来调用相应的函数。 -
虚函数的作用是实现多态。多态是指同一操作作用于不同的对象上面,可以产生不同的解释和不同的执行结果。多态分为编译时多态和运行时多态,编译时多态是指函数重载,运行时多态是指虚函数。
-
虚函数动态绑定的实现原理:每个含有虚函数的类都有一个虚函数表,虚函数表中存储着虚函数的地址,当基类指针绑定了类对象后,通过类对象虚表指针指向的虚函数表找到虚函数的地址,然后调用对应的虚函数。
1.2 实现多态,忘记写virtual会怎么样?
如果忘记在派生类中写virtual
关键字,那么就不会实现多态,而是静态绑定。因此以下例子中,调用的是基类的函数,而不是派生类的函数。
class Base {
public:
void func() {
std::cout << "Base func" << std::endl;
}
};
class Derived : public Base {
public:
void func() {
std::cout << "Derived func" << std::endl;
}
};
int main() {
Base *b = new Derived();
b->func();
delete b;
return 0;
}
输出:
Base func
为了避免这种情况发生,C++11中引入了override
关键字对需要重写的函数进行声明,这样如果派生类中没有重写基类的函数,编译器就会报错。
1.3 虚函数的默认参数可以重载吗?
虚函数的默认参数不可以重载,因为虚函数的调用是在运行时确定的,而默认参数是在编译时确定的。从设计角度来说,这样做是合理的,如果虚函数的默认参数可以重载,那么在运行时,编译器就需要在运行时选择合适的默认参数,这样就会增加编译器的复杂度。因此,虚函数的默认参数不可以重载。
class Base {
public:
virtual void func(int i = 0) = 0;
};
void Base::func(int i) {
std::cout << "Base func: " << i << endl;
}
class Derived : public Base {
public:
void func(int i = 2) {
std::cout<< "Derived func: " << i << endl;
}
};
int main() {
Base *b = new Derived();
b->func();
delete b;
return 0;
}
输出:
Base func: 0
1.4 纯虚函数是什么?
-
纯虚函数是在基类中声明的虚函数,它在基类中没有定义,但要求任何派生类都要定义自己的实现方法,否则编译失败。
-
纯虚函数的声明格式为:
virtual 函数类型 函数名(参数表) = 0;
,其中“= 0”是纯虚函数的标志,它告诉编译系统,该虚函数没有实现。 -
含有纯虚函数的类是抽象类,抽象类是不能实例化的。
纯虚函数真的不能有实现吗?其实不然,纯虚函数是可以有自己的实现的,但是这个实现是在类外部实现的,而不是在类内部实现的。详见1.5中的代码实例。
1.5 析构函数可以是纯虚函数吗?
我们知道,析构函数是在对象销毁时调用的,而纯虚函数是没有实现的虚函数,含有纯虚函数的类是抽象类,那么,析构函数可以是纯虚函数吗?
程序验证如下:
class Base {
public:
virtual ~Base() = 0;
};
Base::~Base() {
std::cout << "Base destructor" << std::endl;
}
class Derived : public Base {
public:
~Derived() {
std::cout << "Derived destructor" << std::endl;
}
};
int main() {
Base *b = new Derived();
delete b;
return 0;
}
输出:
Derived destructor
Base destructor
结论:析构函数可以是纯虚函数,含有纯虚析构函数的类无法实例化。因为析构函数是在派生类析构函数调用之后才调用基类析构函数,而派生类析构函数在派生类对象销毁时才会调用,而派生类对象的销毁必须要调用基类的析构函数,因此基类析构函数必须要在类外提供定义。
如果不定义纯虚析构函数的实现,则会链接失败,报以下错误。
:(.text$_ZN7DerivedD1Ev[__ZN7DerivedD1Ev]+0x3e): undefined reference to `Base::~Base()'
collect2.exe: error: ld returned 1 exit status
为什么在类外部实现就可以呢?因为含有纯虚函数的类是抽象类,抽象类是不能实例化的,但是抽象类可以有指针和引用,因此,我们可以通过抽象类的指针或引用调用纯虚函数,但是如果纯虚函数没有实现,那么就会出现问题,因此,我们需要在类外部实现纯虚函数。
1.6 纯虚函数可以被显示调用吗?
派生类的成员函数可以通过限定函数id自由调用基类在类外定义的纯虚函数。
class Base {
public:
virtual void func() = 0;
};
void Base::func() {
std::cout << "Base func" << std::endl;
}
class Derived : public Base {
public:
void func() {
Base::func();
std::cout << "Derived func" << std::endl;
}
};
int main() {
Base *b = new Derived();
b->func();
delete b;
return 0;
}
输出:
Base func
Derived func
参考
- https://en.cppreference.com/w/cpp/language/abstract_class
- Effective C++: 55 Specific Ways to Improve Your Programs and Designs.