C++八股题整理 - 虚函数
- 虚函数
- 虚函数的定义?
- C++11引入的override和final关键字的作用?
- 虚函数的实现原理?虚函数表(vbtl)和虚函数表指针(vptr)
- 虚函数表、虚函数表指针的生成时期及存储位置?
- 含有虚函数的类的对象的大小?
- 构造函数和析构函数可以是虚函数吗?
- 构造函数和析构函数中能否调用虚函数?
- 哪些函数不能是虚函数?
- 虚函数和纯虚函数的区别?
- 虚函数和模板的区别?
虚函数
虚函数的定义?
虚函数是在基类中使用关键字 virtual 声明的成员函数,它允许派生类对其进行重写(Override),实现运行时多态。当通过基类指针或引用调用虚函数时,实际调用的是对象类型对应的派生类中的函数,这个过程称为动态绑定(Dynamic Binding)
#include<iostream>
using namespace std;
class A {
public:
void foo() { printf("1\n"); }
virtual void fun() { printf("2\n"); } // 虚函数
};
class B : public A {
public:
void foo() { printf("3\n"); } // 派生类的函数屏蔽了与其同名的基类函数
void fun() { printf("4\n"); } // 重写虚函数
};
int main(void) {
A a;
B b;
A *p = &a;
p->foo(); // 1
p->fun(); // 2
p = &b;
p->foo(); // 取决于指针类型,输出1
p->fun(); // 取决于对象类型,输出4,体现了多态
return 0;
}
派生类B重写了A中的虚函数foo(),B中重写后的foo()同样是一个虚函数(不需要virtual显式标注)。如果B被继承,可以在子类中继续重写。
C++11引入的override和final关键字的作用?
- override:保证在派生类中声明的重载函数,与基类的虚函数有相同的签名
- 函数签名不一致:不加override,会视为派生类中新定义的函数;加了override,会报错
virtual void fun() override;
- final:阻止类的进一步派生 和 虚函数的进一步重写
- 一个虚函数被定义为final,则派生类中不能再重写它
virtual void fun() final;
虚函数的实现原理?虚函数表(vbtl)和虚函数表指针(vptr)
- 类 的 虚函数表(vbtl)
- 当一个类中包含虚函数时,编译器会为该类生成虚函数表,表中保存着该类包含的虚函数的地址。“包含”的意思是继承的+自己新定义的
- 如果在该类中重写了父类的虚函数A,那就在虚函数表中将A对应的地方,替换成重写后的虚函数的地址
- 类自己新定义的虚函数,也要将其追加到某一张虚函数表上
- 一个包含虚函数的类,至少有1张虚函数表,即使该类不重写任何虚函数
- 一个类继承了n个有虚函数的基类,就有n张虚函数表
- 对象 的 虚函数表指针(vptr)
- 当一个类中包含虚函数时,该类的对象将会拥有虚函数表指针(vptr),指向该类的虚函数表。虚函数表指针也称虚指针、虚表指针
- 类有n张虚函数表,类的对象就有n个虚指针,每个指针指向1张虚函数表
- 虚函数的实现原理
在程序运行时,找到动态绑定到基类指针上的对象,然后根据该对象的虚函数表指针找到对应的虚函数表,从而确定调用哪个版本的虚函数。
虚函数表、虚函数表指针的生成时期及存储位置?
- 虚函数表:在编译时生成,存储在只读数据段
- 虚函数表指针:在对象创建时生成,位置在对象的头部,根据对象创建方式存储在堆或栈上
含有虚函数的类的对象的大小?
前置知识:C++类对象大小的计算(一)常规类大小计算
含有虚函数的类的对象的大小 = 虚函数表指针(vptr)个数 x 指针大小 + 内存对齐后,对象拥有的非静态成员变量的大小
32位系统下,指针大小为4;64位系统下,指针大小为8。
在64位系统下考虑如下代码:
class Base1 {
public:
int a; // size: 4, 内存对齐后为 8
static int b; // 静态成员属于类,不计入大小
virtual void func1() {}
};
class Base2 {
public:
double c; // size: 8
virtual void func2() {}
};
class Derived : public Base1, public Base2 {
public:
char d; // size: 1, 内存对齐后为8
virtual void func3() {}
};
Derived类的对象,共拥有a、c、d三个非静态成员变量,内存对齐后的总大小为8+8+8=24;Derived类的对象还拥有2个虚函数表指针,每个指针的大小为8,因此总的大小为24 + 2 x 8 = 40字节。
构造函数和析构函数可以是虚函数吗?
- 构造函数不能是虚函数
- vptr是在构造函数中初始化的,如果将构造函数定义为虚函数,那么在调用构造函数前vptr还未生成,因此无法调用到该构造函数
- 析构函数应该为虚函数
- 当基类的指针指向子类的对象时,如果基类的析构函数不为虚函数,那么销毁基类指针时,只会调用基类的析构函数,子类的对象无法被析构,造成内存泄漏
构造函数和析构函数中能否调用虚函数?
在构造函数和析构函数中调用虚函数,不会起到想要的结果。比较下面两段代码:
#include <iostream>
class Base {
public:
virtual void show() {
std::cout << "Base show()" << std::endl;
}
void callShow() {
std::cout << "Base callShow()" << std::endl;
show(); // 调用虚函数
}
virtual ~Base() = default;
};
class Derived : public Base {
public:
void show() override {
std::cout << "Derived show()" << std::endl;
}
};
int main() {
Derived d;
d.callShow(); // 调用基类的成员函数,但期望调用派生类的虚函数
return 0;
}
// Base callShow()
// Derived show()
这段代码中,虚函数show正确地表现出了多态性。而在构造函数和析构函数中调用,不能表现多态性。
#include <iostream>
class Base {
public:
Base() {
std::cout << "Base constructor" << std::endl;
show(); // 调用虚函数
}
virtual void show() {
std::cout << "Base show()" << std::endl;
}
virtual ~Base() {
std::cout << "Base destructor" << std::endl;
show(); // 再次调用虚函数
}
};
class Derived : public Base {
public:
Derived() {
std::cout << "Derived constructor" << std::endl;
}
void show() override {
std::cout << "Derived show()" << std::endl;
}
~Derived() {
std::cout << "Derived destructor" << std::endl;
}
};
int main() {
Derived d;
return 0;
}
// Base constructor
// Base show()
// Derived constructor
// Derived destructor
// Base destructor
// Base show()
在构造函数和析构函数中调用show(),show采用的是基类中的实现。这是因为,在调用到基类的构造函数和析构函数时,派生类中的内容尚没有被创建、或者已经被销毁了。
哪些函数不能是虚函数?
- 构造函数:执行构造函数前虚表指针尚未初始化,无法正确调用构造函数
- 内联函数:内联函数在编译阶段进行函数体的替换操作,而虚函数意味着在运行期间进行类型确定,所以内联函数不能是虚函数
- 静态函数:静态函数不属于对象属于类,静态成员函数没有this指针,因此静态函数设置为虚函数没有任何意义
- 友元函数,友元函数不属于类的成员函数,不能被继承。对于没有继承特性的函数没有虚函数的说法
- 普通函数,普通函数不属于类的成员函数,不具有继承特性,因此普通函数没有虚函数
总结:不能被继承的函数 和 不能被重写的函数 不能是虚函数
虚函数和纯虚函数的区别?
class A {
virtual void example() = 0; // 纯虚函数
}
纯虚函数是虚函数的一种特殊形式,它的语法是在函数声明后加上’=0’。纯虚函数只有声明没有实现,含有纯虚函数的类称为抽象类,不能被实例化。它的派生类如果想被实例化,就必须实现所有的纯虚函数。
- 虚函数和纯虚函数都是实现多态性的工具。通过将基类的指针或引用指向派生类对象,可以在运行时调用派生类的重写方法。
- 虚函数提供了一个默认实现,但派生类可以选择重写它。纯虚函数则强制要求派生类必须提供自己的实现。
虚函数和模板的区别?
模版是一种编译时多态性技术,通过在编译时确定类型来生成特定的代码。
虚函数是运行时多态,在运行时根据对象的实际类型来调用相应的方法,从而实现多态性。
特性 | 模板(Templates) | 虚函数(Virtual Functions) |
---|---|---|
决策时间 | 编译时 | 运行时 |
实现机制 | 编译时生成特定类型代码 | 通过虚函数表动态绑定 |
类型检查 | 编译时 | 运行时 |
运行时开销 | 无 | 有虚函数表查找开销 |
使用场景 | 泛型编程,STL容器 | 面向对象编程的多态行为 |