东软集团 C++开发 二面
1. C++ 中虚函数的作用是什么,虚函数表是怎么工作的
答案:虚函数的核心作用是支持运行时多态。当父类指针或引用指向子类对象时,调用虚函数可以在运行时决定具体执行哪个版本,而不是在编译期静态绑定。
底层上,编译器通常会为包含虚函数的类生成一张虚函数表,表里保存对应虚函数地址;对象内部会有一个隐藏的虚表指针,指向当前对象实际类型对应的虚表。调用虚函数时,大致过程是:
- 先通过对象拿到虚表指针
- 再从虚表中找到对应函数地址
- 最终调用实际类型的实现
如果析构函数涉及多态删除,基类析构函数一般也要声明为虚函数,否则通过基类指针删除派生类对象会出现未定义行为。
代码:
#include <iostream>
using namespace std;
class Base {
public:
virtual void run() {
cout << "Base::run" << endl;
}
virtual ~Base() = default;
};
class Derived : public Base {
public:
void run() override {
cout << "Derived::run" << endl;
}
};
int main() {
Base* p = new Derived();
p->run();
delete p;
return 0;
}
2. new 和 malloc 的区别是什么
答案:new 和 malloc 都可以申请内存,但它们不属于同一套机制。
malloc 是 C 语言库函数,做的事情主要是:
- 按字节申请一段原始内存
- 返回
void* - 不会调用构造函数
- 释放时使用
free
new 是 C++ 运算符,除了分配内存外,还会:
- 根据类型返回对应指针
- 调用构造函数完成对象初始化
- 失败时默认抛出异常
- 释放时使用
delete new[]/delete[]成对使用
实际开发里,如果是在 C++ 里管理对象,通常优先使用 new,更进一步则优先使用智能指针和 RAII,尽量减少手动管理内存。
代码:
#include <iostream>
#include <cstdlib>
using namespace std;
class Test {
public:
Test() { cout << "constructor" << endl; }
~Test() { cout << "destructor" << endl; }
};
int main() {
Test* p1 = new Test();
delete p1;
Test* p2 = (Test*)malloc(sizeof(Test));
free(p2);
return 0;
}
3. 引用和指针的区别是什么
答案:引用可以理解为变量的别名,指针本质上是一个保存地址的变量。它们都能间接操作对象,但语义和使用方式不同。
主要区别有:
- 引用定义时必须初始化,指针可以先不初始化
- 引用一旦绑定对象后不能再改绑,指针可以修改指向
- 引用使用时更像原变量,不需要显式解引用
- 指针可以为空,引用通常不能为空
- 指针更适合表达“可能没有对象”,引用更适合表达“必须有对象”
参数传递时:
- 值传递会拷贝对象
- 指针传递和引用传递都可以修改外部对象
- 如果只读且避免拷贝,常用
const T&
代码:
#include <iostream>
using namespace std;
void change1(int x) {
x = 100;
}
void change2(int* x) {
if (x) *x = 200;
}
void change3(int& x) {
x = 300;
}
int main() {
int a = 10;
change1(a);
cout << a << endl;
change2(&a);
cout << a << endl;
change3(a);
cout << a << endl;
return 0;
}
4. vector 和 list 的区别是什么,实际开发中怎么选
答案:vector 底层是连续内存,list 底层是双向链表。它们最本质的差异是内存布局不同,这会直接影响访问性能和操作特性。
vector 的特点:
- 连续内存,支持随机访问
- 遍历性能好,cache 友好
- 尾部插入效率高
- 扩容时可能触发整体搬迁
- 中间位置插入删除代价较高
list 的特点:
- 节点离散分布,不支持随机访问
- 插入删除方便
- 每个节点额外维护前后指针
- 内存开销更大
- 遍历局部性差
工程里 vector 使用得通常更多,因为:
- 连续内存对现代 CPU 更友好
- 遍历效率通常明显更高
- 很多业务场景读多写少
5. map 和 unordered_map 的区别是什么
答案:map 底层通常是红黑树,unordered_map 底层通常是哈希表。它们都可以保存 key-value,但在有序性、复杂度和使用场景上差异较大。
map 的特点:
- 按 key 有序
- 查找、插入、删除复杂度一般是
O(log n) - 支持范围查询、上下界查找
- 迭代输出有序
unordered_map 的特点:
- 无序
- 平均查找、插入、删除是
O(1) - 冲突严重时可能退化
- 不适合需要有序遍历和范围操作的场景
如果需求是:
- 需要顺序、范围查询:优先
map - 更关注单点查找性能:优先
unordered_map
代码:
#include <iostream>
#include <map>
#include <unordered_map>
using namespace std;
int main() {
map<int, int> mp1;
unordered_map<int, int> mp2;
mp1[3] = 30;
mp1[1] = 10;
mp1[2] = 20;
mp2[3] = 30;
mp2[1] = 10;
mp2[2] = 20;
for (auto& [k, v] : mp1) {
cout << k << " " << v << endl;
}
return 0;
}
6. 模板和模板特化的作用是什么
答案:模板的作用是把类型参数化,让一套代码可以适配多种类型。这是 C++ 泛型编程的基础,常见有函数模板和类模板。
模板特化是在通用模板基础上,为某些特定类型提供专门实现。这样可以做到:
- 针对特殊类型优化行为
- 对某些类型做定制逻辑
- 在通用性和特殊性之间做平衡
模板特化常见分为:
- 全特化
- 偏特化
代码:
#include <iostream>
using namespace std;
template <typename T>
class Printer {
public:
void print() {
cout << "generic" << endl;
}
};
template <>
class Printer<int> {
public:
void print() {
cout << "int specialization" << endl;
}
};
int main() {
Printer<double> p1;
p1.print();
Printer<int> p2;
p2.print();
return 0;
}
7. 什么是右值引用,移动语义解决了什么问题
答案:右值引用是 C++11 引入的特性,形式是 T&&。它主要是为了支持移动语义和完美转发。
移动语义解决的问题是:很多对象在临时对象、返回值传递、容器扩容等场景下,本来只需要“转移资源所有权”,但如果按拷贝语义处理,就会产生额外的深拷贝开销。
典型收益有:
- 减少不必要的对象拷贝
- 提升容器操作效率
- 提高返回对象时的性能
一个类如果自己管理资源,比如堆内存、文件句柄、socket 等,通常要考虑:
- 拷贝构造
- 拷贝赋值
- 移动构造
- 移动赋值
- 析构函数
代码:
#include <iostream>
#include <utility>
using namespace std;
class Buffer {
public:
Buffer(size_t n) : size_(n), data_(new int[n]) {
cout << "construct" << endl;
}
~Buffer() {
delete[] data_;
}
Buffer(const Buffer& other) : size_(other.size_), data_(new int[other.size_]) {
cout << "copy construct" << endl
剩余60%内容,订阅专栏后可继续查看/也可单篇购买
本专栏系统梳理C++方向, 大中厂高频高频面试考点 , 内容皆来自真实面试经历,从基础语法、内存管理、STL与设计模式,到操作系统与项目实战,结合真实面试题深度解析,帮助开发者高效查漏补缺,提升技术理解与面试通过率,打造扎实的C++工程能力.