品峰医疗 客户端开发-C++ 一面
1. C++ 多态的实现原理是什么,静态多态和动态多态有什么区别
C++ 多态分成静态多态和动态多态。静态多态一般发生在编译期,比如函数重载、模板实例化;动态多态一般依赖继承和虚函数,在运行期决定调用哪个函数。动态多态的核心是虚函数表。一个含有虚函数的类,对象内部通常会维护一个虚表指针,指向该类型对应的虚函数表。通过基类指针或引用调用虚函数时,会根据对象真实类型找到实际要执行的函数。静态多态没有运行时分派开销,性能更直接;动态多态扩展性更好,适合接口抽象和运行时替换。工程上如果类型关系在编译期就确定,模板往往更高效;如果要面向接口编程,动态多态更常见。
代码:
#include <iostream>
using namespace std;
class Base {
public:
virtual void run() { cout << "Base\n"; }
virtual ~Base() = default;
};
class Derived : public Base {
public:
void run() override { cout << "Derived\n"; }
};
int main() {
Base* p = new Derived();
p->run();
delete p;
return 0;
}
2. 计算机里整数、负数和浮点数分别是怎么存的
无符号整数通常直接按二进制存储。带符号整数主流机器上一般采用补码表示,这样加减法可以统一交给硬件处理。补码里,正数的补码和原码一致,负数的补码等于其绝对值按位取反再加一,所以 -1 在 32 位机器上通常是所有位都为 1。浮点数通常采用 IEEE 754 标准表示,分成符号位、指数位和尾数位。以 float 为例,一般是 1 位符号位、8 位指数位、23 位尾数位。浮点数不是精确表示所有十进制小数,因此会存在精度误差。面试里如果被继续追问,可以进一步说浮点数为什么不能直接比较相等,以及 NaN、无穷大、非规格化数这些特殊值。
代码:
#include <iostream>
using namespace std;
int main() {
int x = -1;
float y = 0.1f;
cout << x << endl;
cout << y << endl;
return 0;
}
3. 具体讲一下虚函数和虚函数表,虚表一般放在哪里
虚函数是为了支持运行时多态。编译器通常会给含有虚函数的类生成一张虚函数表,表中存放该类各个虚函数的入口地址。对象里会额外有一个隐藏的虚表指针,构造对象时由编译器负责初始化。当通过基类指针调用虚函数时,程序会先找到对象中的虚表指针,再到虚表中取出对应函数地址进行调用。虚函数代码本身在代码段,虚表通常放在只读数据区或者类似区域,对象实例中通常只存一个指针。如果涉及多继承、虚继承,对象布局会更复杂,因为编译器还要处理基类偏移和 this 指针调整。
4. 智能指针有哪些,什么情况下不能随便用智能指针
答案:常见智能指针有 unique_ptr、shared_ptr 和 weak_ptr。unique_ptr 表示独占所有权,开销小;shared_ptr 表示共享所有权,内部通常维护引用计数;weak_ptr 不拥有对象,主要用于观察和打破循环引用。并不是任何地方都适合无脑上智能指针。比如对象所有权很明确、生命周期跟栈绑定时,直接用局部对象更简单;性能敏感路径上大量使用 shared_ptr,可能引入额外计数开销;跨模块边界乱传 shared_ptr,容易让生命周期变得模糊。还有一种典型情况是“对象并不真正拥有资源”,这时用智能指针管理反而会制造错误的释放语义。所以本质上不是“能不能用智能指针”,而是要先想清楚所有权。
代码:
#include <iostream>
#include <memory>
using namespace std;
class A {
public:
A() { cout << "ctor\n"; }
~A() { cout << "dtor\n"; }
};
int main() {
unique_ptr<A> p1 = make_unique<A>();
shared_ptr<A> p2 = make_shared<A>();
weak_ptr<A> p3 = p2;
return 0;
}
5. vector 的扩容机制是什么,为什么频繁 push_back 可能带来性能问题
答案:vector 底层是连续内存,当容量不足时会申请一块更大的连续空间,把旧元素搬过去,再释放旧空间。扩容倍数不是标准强制规定,常见实现可能是 1.5 倍或 2 倍。由于扩容时要重新分配内存并拷贝或移动已有元素,所以在元素很多、对象很重时,扩容开销会比较明显。频繁 push_back 的问题主要在于如果提前不知道数据量,就可能触发多次扩容;而且连续内存要求较高,大对象或者内存碎片严重时也可能影响分配效率。工程里如果能预估元素数量,通常会先 reserve,这样可以显著减少重分配次数。
代码:
#include <vector>
using namespace std;
int main() {
vector<int> v;
v.reserve(1000);
for (int i = 0; i < 1000; ++i) v.push_back(i);
return 0;
}
6. 快速排序什么时候会退化成 O(n^2),常见优化手段有哪些
答案:快速排序在每次划分都极不均衡时会退化成 O(n^2),典型情况是数组本身接近有序,而每次又总是选第一个或最后一个元素作为 pivot。常见优化方式包括随机选 pivot、三数取中、当分区规模较小时切换为插入排序,以及递归时优先处理较小区间来降低栈深度。如果要进一步控制最坏情况,一些标准库实现会采用 introsort,也就是先用快排,当递归深度过深时切到堆排序,从而把最坏情况控制住。所以面试里只说“快排平均 O(nlogn)”是不够的,更重要的是你知不知道它为什么会退化、怎么做工程优化。
代码:
#include <cstdlib>
int partition(int a[], int l, int r) {
int idx = l + rand() % (r - l + 1);
int pivot = a[idx];
剩余60%内容,订阅专栏后可继续查看/也可单篇购买
本专栏系统梳理C++方向, 大中厂高频高频面试考点 , 内容皆来自真实面试经历,从基础语法、内存管理、STL与设计模式,到操作系统与项目实战,结合真实面试题深度解析,帮助开发者高效查漏补缺,提升技术理解与面试通过率,打造扎实的C++工程能力.
