【C++ 面试高频:面向对象、虚函数、多态和虚析构函数】
2026/6/16 18:46:59 网站建设 项目流程

一、面向对象三大特性

C++ 是一门支持面向对象编程的语言。面向对象主要有三大特性:

1. 封装 2. 继承 3. 多态

这三个概念是 C++ 面试中非常高频的基础问题。


二、封装

封装就是把数据和操作数据的函数放在一个类里面,并通过访问权限控制外部是否可以直接访问这些数据。

C++ 中常见的访问权限有:

public:公有成员,类外可以访问 protected:保护成员,类外不能访问,子类可以访问 private:私有成员,类外不能访问,子类也不能直接访问

1. 封装示例

#include <iostream> #include <string> using namespace std; class Student { private: // 私有成员变量,类外不能直接访问 string name; int age; public: // 设置姓名 void setName(string n) { name = n; } // 获取姓名 string getName() { return name; } // 设置年龄 void setAge(int a) { // 可以在函数中加入限制条件,保证数据合理 if (a > 0 && a < 150) { age = a; } else { cout << "年龄不合法" << endl; } } // 获取年龄 int getAge() { return age; } }; int main() { Student stu; // 不能直接访问 private 成员 // stu.name = "Tom"; // 错误 // 通过 public 函数间接访问和修改数据 stu.setName("Tom"); stu.setAge(20); cout << stu.getName() << endl; cout << stu.getAge() << endl; return 0; }

2. 封装面试总结

面试时可以这样回答:

封装就是把数据和操作数据的方法放到一个类中,并通过访问权限控制外部访问。封装可以隐藏内部实现细节,提高代码安全性和可维护性。比如把成员变量设置为 private,外部只能通过 public 方法访问,这样可以在方法中加入数据检查,避免非法赋值。


三、继承

继承表示一个类可以复用另一个类已有的成员。被继承的类叫父类或基类,继承得到的新类叫子类或派生类。

1. 继承示例

#include <iostream> using namespace std; // 父类 class Animal { public: void eat() { cout << "动物会吃东西" << endl; } }; // 子类继承父类 class Dog : public Animal { public: void bark() { cout << "狗会叫" << endl; } }; int main() { Dog dog; // 子类可以使用自己的成员函数 dog.bark(); // 子类也可以使用从父类继承来的成员函数 dog.eat(); return 0; }

在这段代码中,Dog继承了Animal,所以Dog对象可以调用Animal中的eat()函数。

2. 继承的作用

继承的主要作用是代码复用和扩展。

例如:

Animal 表示通用动物 Dog 表示狗 Cat 表示猫

狗和猫都有动物的共同特征,所以可以把共同部分放到Animal中,然后让DogCat去继承。

3. 继承面试总结

面试时可以这样回答:

继承是面向对象的重要特性,子类可以复用父类已有的成员,也可以在父类基础上扩展新的功能。继承可以减少重复代码,提高代码复用性。但是继承关系不能乱用,只有当两个类之间确实存在“is-a”的关系时,才适合使用继承。


四、多态

多态的意思是“同一个接口,在不同对象上表现出不同的行为”。

C++ 中多态主要分为两种:

1. 编译时多态:函数重载、模板 2. 运行时多态:虚函数

面试中问到的多态,通常重点指运行时多态,也就是虚函数实现的多态。

1. 没有 virtual 的情况

#include <iostream> using namespace std; class Animal { public: void speak() { cout << "动物在叫" << endl; } }; class Dog : public Animal { public: void speak() { cout << "狗在汪汪叫" << endl; } }; int main() { Animal* animal = new Dog(); // 没有 virtual,调用的是父类的 speak animal->speak(); delete animal; return 0; }

输出结果:

动物在叫

虽然animal指向的是Dog对象,但是因为speak()不是虚函数,所以调用的是父类的speak()


五、虚函数

虚函数就是在成员函数前面加上virtual关键字。

有了虚函数之后,父类指针或引用指向子类对象时,调用同名函数会根据对象的真实类型决定调用哪个函数。

1. 虚函数示例

#include <iostream> using namespace std; class Animal { public: // virtual 表示这是一个虚函数 virtual void speak() { cout << "动物在叫" << endl; } }; class Dog : public Animal { public: // 子类重写父类的虚函数 void speak() override { cout << "狗在汪汪叫" << endl; } }; class Cat : public Animal { public: // 子类重写父类的虚函数 void speak() override { cout << "猫在喵喵叫" << endl; } }; int main() { Animal* animal1 = new Dog(); Animal* animal2 = new Cat(); // 父类指针指向 Dog 对象,调用 Dog 的 speak animal1->speak(); // 父类指针指向 Cat 对象,调用 Cat 的 speak animal2->speak(); delete animal1; delete animal2; return 0; }

输出结果:

狗在汪汪叫 猫在喵喵叫

这就是运行时多态。

2. override 的作用

在子类重写父类虚函数时,建议加上override

void speak() override { cout << "狗在汪汪叫" << endl; }

override的作用是告诉编译器:这个函数是重写父类的虚函数。

如果函数名、参数列表写错了,编译器会报错,帮助我们提前发现问题。

3. 虚函数面试总结

面试时可以这样回答:

虚函数是实现 C++ 运行时多态的基础。在父类函数前加上 virtual 后,如果子类重写该函数,那么通过父类指针或引用调用该函数时,会根据对象的真实类型调用对应的子类函数。这样可以实现同一个接口,不同对象有不同表现。


六、虚函数表和虚函数指针

面试中有时会继续问:虚函数的底层原理是什么?

简单来说:

有虚函数的类,编译器通常会为它生成一张虚函数表。 对象内部通常会有一个虚函数指针,指向对应类的虚函数表。 调用虚函数时,会通过虚函数指针找到虚函数表,再找到真正要调用的函数。

1. 简单理解

例如:

class Animal { public: virtual void speak() { cout << "动物在叫" << endl; } }; class Dog : public Animal { public: void speak() override { cout << "狗在汪汪叫" << endl; } };

可以简单理解为:

Animal 类有自己的虚函数表,里面存 Animal::speak 的地址。 Dog 类也有自己的虚函数表,里面存 Dog::speak 的地址。

当父类指针指向子类对象时:

Animal* animal = new Dog(); animal->speak();

程序会根据对象内部的虚函数指针,找到Dog的虚函数表,然后调用Dog::speak()

2. 虚函数表面试总结

面试时可以这样回答:

C++ 的虚函数通常通过虚函数表和虚函数指针实现。含有虚函数的类会有虚函数表,对象中通常会有虚函数指针。调用虚函数时,会通过对象的虚函数指针找到对应的虚函数表,再根据函数位置找到真正要调用的函数。所以虚函数可以在运行时决定调用父类函数还是子类函数。


七、虚析构函数

虚析构函数是 C++ 面试中非常高频的考点。

如果一个类要作为基类,并且可能通过父类指针删除子类对象,那么父类析构函数应该写成虚函数。

1. 没有虚析构函数的问题

#include <iostream> using namespace std; class Base { public: Base() { cout << "Base 构造函数" << endl; } // 普通析构函数,不是虚函数 ~Base() { cout << "Base 析构函数" << endl; } }; class Derived : public Base { public: Derived() { cout << "Derived 构造函数" << endl; } ~Derived() { cout << "Derived 析构函数" << endl; } }; int main() { Base* p = new Derived(); // 通过父类指针删除子类对象 // 如果父类析构函数不是 virtual,可能只调用 Base 析构函数 delete p; return 0; }

这种情况下,可能只调用父类析构函数,而子类析构函数没有被正确调用。如果子类中申请了资源,就可能导致资源泄漏。

2. 正确写法:父类析构函数加 virtual

#include <iostream> using namespace std; class Base { public: Base() { cout << "Base 构造函数" << endl; } // 父类析构函数写成虚函数 virtual ~Base() { cout << "Base 析构函数" << endl; } }; class Derived : public Base { public: Derived() { cout << "Derived 构造函数" << endl; } ~Derived() { cout << "Derived 析构函数" << endl; } }; int main() { Base* p = new Derived(); // 父类析构函数是虚函数时,会先调用子类析构,再调用父类析构 delete p; return 0; }

正常析构顺序是:

Derived 析构函数 Base 析构函数

3. 为什么析构顺序是先子类后父类?

构造对象时,先构造父类部分,再构造子类部分。

析构对象时,顺序正好相反,先析构子类部分,再析构父类部分。

可以这样记:

构造:先父后子 析构:先子后父

4. 虚析构函数面试总结

面试时可以这样回答:

如果一个类要作为基类使用,并且可能通过父类指针删除子类对象,那么父类析构函数必须写成 virtual。否则 delete 父类指针时,可能只调用父类析构函数,而不调用子类析构函数,导致子类资源没有释放,出现内存泄漏。虚析构函数可以保证先调用子类析构函数,再调用父类析构函数。


八、纯虚函数和抽象类

纯虚函数是在虚函数后面加= 0

含有纯虚函数的类叫抽象类。

抽象类不能直接创建对象,只能被子类继承,并要求子类实现纯虚函数。

1. 纯虚函数示例

#include <iostream> using namespace std; class Shape { public: // 纯虚函数 // 表示不同图形都应该有 area 方法,但具体怎么算由子类决定 virtual double area() = 0; // 抽象类作为基类时,析构函数建议写成 virtual virtual ~Shape() {} }; class Circle : public Shape { private: double radius; public: Circle(double r) { radius = r; } // 实现父类的纯虚函数 double area() override { return 3.14 * radius * radius; } }; class Rectangle : public Shape { private: double width; double height; public: Rectangle(double w, double h) { width = w; height = h; } // 实现父类的纯虚函数 double area() override { return width * height; } }; int main() { Shape* s1 = new Circle(2.0); Shape* s2 = new Rectangle(3.0, 4.0); cout << "圆的面积:" << s1->area() << endl; cout << "矩形的面积:" << s2->area() << endl; delete s1; delete s2; return 0; }

2. 抽象类的作用

抽象类主要用来定义统一接口。

例如所有图形都有面积,但是不同图形面积计算方式不同,所以可以把area()定义为纯虚函数,让具体子类去实现。

3. 纯虚函数面试总结

面试时可以这样回答:

纯虚函数是在虚函数后面加= 0,含有纯虚函数的类叫抽象类。抽象类不能直接实例化,主要用于定义接口。子类继承抽象类后,必须实现纯虚函数,否则子类仍然是抽象类。


九、函数重载、重写和隐藏

这三个概念也经常和虚函数一起考。

1. 函数重载

函数重载发生在同一个作用域中,函数名相同,但是参数列表不同。

#include <iostream> using namespace std; class Printer { public: void print(int x) { cout << "打印整数:" << x << endl; } void print(string s) { cout << "打印字符串:" << s << endl; } }; int main() { Printer p; p.print(10); p.print("hello"); return 0; }

2. 函数重写

函数重写发生在父类和子类之间,要求父类函数是虚函数,子类函数的函数名、参数列表、返回值类型要匹配。

class Base { public: virtual void show() { cout << "Base show" << endl; } }; class Derived : public Base { public: void show() override { cout << "Derived show" << endl; } };

3. 函数隐藏

函数隐藏也发生在父类和子类之间。如果子类定义了和父类同名的函数,即使参数不同,也可能隐藏父类同名函数。

#include <iostream> using namespace std; class Base { public: void show(int x) { cout << "Base show int: " << x << endl; } }; class Derived : public Base { public: // 子类定义了同名函数 show // 会隐藏父类中的 show(int) void show() { cout << "Derived show" << endl; } }; int main() { Derived d; d.show(); // d.show(10); // 错误,父类 show(int) 被隐藏了 return 0; }

4. 三者区别总结

重载:同一作用域,函数名相同,参数不同。 重写:父子类之间,父类是虚函数,子类重新实现。 隐藏:父子类之间,子类同名函数隐藏父类同名函数。

十、面试高频问题整理

1. 面向对象三大特性是什么?

面向对象三大特性是封装、继承和多态。

封装是隐藏内部实现,通过接口访问数据。

继承是子类复用父类已有功能,并在此基础上扩展。

多态是同一个接口在不同对象上表现出不同的行为,C++ 中运行时多态主要通过虚函数实现。


2. 什么是虚函数?

虚函数是在成员函数前加virtual的函数。它可以被子类重写,并且通过父类指针或引用调用时,会根据对象的真实类型决定调用父类函数还是子类函数。


3. C++ 多态如何实现?

C++ 运行时多态主要通过虚函数实现。底层通常依靠虚函数表和虚函数指针。含有虚函数的类会有虚函数表,对象中会有虚函数指针。调用虚函数时,会通过虚函数指针找到对应的虚函数表,再调用实际对象对应的函数。


4. 为什么基类析构函数要写成 virtual?

如果通过父类指针删除子类对象,而父类析构函数不是虚函数,可能只调用父类析构函数,不调用子类析构函数,导致子类资源没有释放。

所以作为基类使用的类,析构函数通常要写成虚函数。


5. 构造函数和析构函数的调用顺序是什么?

构造时,先调用父类构造函数,再调用子类构造函数。

析构时,先调用子类析构函数,再调用父类析构函数。

简单记忆:

构造:先父后子 析构:先子后父

6. 构造函数可以是虚函数吗?

构造函数不能是虚函数。

因为对象在构造过程中,虚函数机制还没有完整建立,对象类型还没有完全形成,所以构造函数不能声明为虚函数。

但是析构函数可以是虚函数,并且基类析构函数经常需要写成虚函数。


7. 什么是纯虚函数和抽象类?

纯虚函数是在虚函数后面加= 0。含有纯虚函数的类叫抽象类。

抽象类不能直接创建对象,主要用于定义接口。子类继承抽象类后,需要实现纯虚函数,否则子类仍然是抽象类。


十一、总结

本文主要整理了 C++ 面试中面向对象相关的高频知识点,包括封装、继承、多态、虚函数、虚函数表、虚析构函数、纯虚函数和抽象类。

封装主要是隐藏内部数据,通过公开接口访问,提高代码安全性和可维护性。

继承主要用于代码复用和功能扩展,子类可以继承父类已有成员。

多态表示同一个接口在不同对象上有不同表现,C++ 中运行时多态主要通过虚函数实现。

虚函数的底层通常通过虚函数表和虚函数指针实现。父类指针或引用调用虚函数时,会根据对象真实类型调用对应函数。

虚析构函数是面试重点。如果一个类作为基类使用,并且可能通过父类指针删除子类对象,那么父类析构函数应该声明为 virtual,避免子类析构函数不被调用。

简单记忆:

封装:隐藏数据,提供接口。 继承:复用代码,扩展功能。 多态:同一接口,不同表现。 虚函数:实现运行时多态。 虚析构函数:保证通过父类指针删除子类对象时析构完整。 纯虚函数:定义接口,形成抽象类。

面试中回答这类问题时,最好不要只背概念,要结合代码说明父类指针指向子类对象、虚函数调用、虚析构函数释放资源这些典型场景。

0voice · GitHub

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询