【C++ 面试高频:面向对象、虚函数、多态和虚析构函数】
一、面向对象三大特性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中然后让Dog和Cat去继承。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