恒生电子 C++开发 一面
1. 讲一下虚函数表,以及虚函数什么时候生成的?构造函数可以被定义为虚函数吗?
答案:C++ 里带虚函数的类,编译器通常会为它生成一张虚函数表,表里存放的是该类各个虚函数最终对应的函数入口地址。对象实例里一般还会有一个隐藏的虚表指针,用来在运行时找到这张表。通过基类指针或引用调用虚函数时,程序会先取出对象里的虚表指针,再间接找到真正要执行的函数,这就是运行时多态的基础。虚函数表是在编译期由编译器根据类定义生成的,不是运行时动态“创建”的。真正到运行时参与的是对象里的虚表指针初始化过程。对象构造时,编译器会在不同构造阶段把 vptr 指向当前构造层级对应的虚表,所以构造和析构期间的虚函数行为要特别小心。构造函数不能声明为虚函数。因为调用构造函数的前提恰恰是对象还没有构造完成,这时对象的多态基础都还没稳定建立,拿“虚调用”去决定怎么构造本身在语义上就是矛盾的。析构函数则经常要声明为虚函数,尤其是当类要作为基类被多态删除时。
代码:
#include <iostream>
using namespace std;
class Base {
public:
virtual void f() { cout << "Base::f\n"; }
virtual ~Base() = default;
};
class Derived : public Base {
public:
void f() override { cout << "Derived::f\n"; }
};
int main() {
Base* p = new Derived();
p->f();
delete p;
return 0;
}
2. protected 的作用是什么
protected 主要用于控制继承体系内的访问边界。一个成员如果是 protected,那么类外部不能直接访问,但派生类内部可以访问。它比 private 更开放一些,比 public 又更收敛一些。它常见的用途不是“偷懒不给封装”,而是当基类希望给子类暴露一部分实现细节或扩展点,但又不希望这部分能力直接暴露给所有外部调用方。实际工程里如果一个成员只想让子类感知,protected 是合理的;但如果会破坏基类不变量,通常还是应该优先通过 protected 的辅助函数或者纯虚接口来暴露,而不是直接把数据成员裸露出来。
代码:
#include <iostream>
using namespace std;
class Base {
protected:
int value = 42;
};
class Derived : public Base {
public:
void print() { cout << value << endl; }
};
int main() {
Derived d;
d.print();
return 0;
}
3. 函数传参和返回值一般走哪些寄存器,参数是怎么压栈的?
在 Linux x86-64 下,常见的 System V ABI 规定,整数或指针类型的前六个参数通常通过寄存器传递,分别是 rdi、rsi、rdx、rcx、r8、r9,返回值一般走 rax。如果参数更多,超出的部分才会放到栈上。浮点参数则会优先走 xmm 系列寄存器。在 Windows x64 下,前四个整型或指针参数通常放在 rcx、rdx、r8、r9。所以面试里如果被问到“哪个寄存器传参”,最好先说明你讨论的是哪种平台和 ABI。至于“如何压栈”,现代编译器不一定真的是传统意义上一条条 push,很多时候会在函数序言中一次性调整 rsp,然后把参数或临时变量放到预留好的栈帧位置。编译优化打开后,很多小函数甚至可能完全不建传统栈帧。
代码:
#include <iostream>
using namespace std;
long add(long a, long b, long c, long d, long e, long f, long g) {
return a + b + c + d + e + f + g;
}
int main() {
cout << add(1,2,3,4,5,6,7) << endl;
return 0;
}
4. 讲一下进程地址空间
答案:进程地址空间是操作系统给每个进程提供的一套独立虚拟地址视图。程序看到的是一段看起来连续的虚拟地址,而不是直接操作物理内存。典型的进程地址空间里会包括代码段、只读常量区、全局/静态区、堆、共享库映射区、文件映射区、栈,以及内核为线程和运行时保留的一些特殊区域。不同平台和运行时布局细节不完全一样,但核心思想都是一致的。有了地址空间隔离,不同进程之间默认不能随便互相读写,这既提高了安全性,也降低了错误传播范围。与此同时,虚拟内存还支持按需分页、共享库映射、写时复制、内存映射文件等机制。所以“进程地址空间”不是简单的一块内存,而是操作系统、MMU、页表、权限位共同提供的一层抽象。
5. C++ 函数调用的几种方式有哪些
答案:如果从“调用形态”来看,常见有普通函数调用、成员函数调用、虚函数调用、函数指针调用、仿函数调用、lambda 调用。如果从更底层的“调用约定”来看,常见会提到 cdecl、stdcall、fastcall、thiscall 等,不过在 64 位平台上很多历史差异被统一 ABI 吸收掉了。成员函数本质上会多一个隐式的 this 指针;虚函数调用会多一层经由虚表的间接跳转;函数对象和 lambda 通常会转化成对 operator() 的调用。所以这题如果面试官问得偏底层,你就往 ABI、寄存器、栈帧、this 指针说;如果问得偏语言特性,就往普通函数、成员函数、虚函数、可调用对象这几种形式说。
代码:
#include <iostream>
using namespace std;
void normalFunc() { cout << "normal\n"; }
class A {
public:
void memberFunc() { cout << "member\n"; }
virtual void virtualFunc() { cout << "virtual\n"; }
};
struct Functor {
void operator()() { cout << "functor\n"; }
};
int main() {
normalFunc();
A a;
a.memberFunc();
a.v
剩余60%内容,订阅专栏后可继续查看/也可单篇购买
本专栏系统梳理C++方向, 大中厂高频高频面试考点 , 内容皆来自真实面试经历,从基础语法、内存管理、STL与设计模式,到操作系统与项目实战,结合真实面试题深度解析,帮助开发者高效查漏补缺,提升技术理解与面试通过率,打造扎实的C++工程能力.
