【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,避免子类析构函数不被调用。
简单记忆:
封装:隐藏数据,提供接口。
继承:复用代码,扩展功能。
多态:同一接口,不同表现。
虚函数:实现运行时多态。
虚析构函数:保证通过父类指针删除子类对象时析构完整。
纯虚函数:定义接口,形成抽象类。
面试中回答这类问题时,最好不要只背概念,要结合代码说明父类指针指向子类对象、虚函数调用、虚析构函数释放资源这些典型场景。
更多推荐



所有评论(0)