计算机基础知识-编程语言
一、通用编程基础
1.1、内存管理篇
Q1:请简述“堆(Heap)”和“栈(Stack)”的区别?
【参考答案】
堆和栈都是程序运行时使用的内存区域,主要区别如下:
- 管理方式:
- 栈:由编译器自动分配和释放。用于存储局部变量、函数参数、返回地址等。
- 堆:由程序员手动分配和释放(如
malloc/free或new/delete)。若忘记释放,会导致内存泄漏。
- 生长方向:
- 栈:向低地址方向生长(向下)。
- 堆:向高地址方向生长(向上)。
- 大小与效率:
- 栈:空间较小(通常几MB),但存取速度极快(直接通过指针移动),不易产生碎片。递归过深容易导致栈溢出。
- 堆:空间较大(受限于虚拟内存),但分配速度慢(涉及搜索空闲块),容易产生内存碎片。
- 应用场景:
- 栈适合存储生命周期短、大小确定的数据;堆适合存储生命周期长、大小动态变化的数据(如大型数组、对象)。
Q2:什么是内存泄漏(Memory Leak)?如何避免?
【参考答案】
- 定义:指程序在运行过程中动态分配了堆内存,但在使用完毕后没有释放,导致这块内存无法被再次利用。随着程序运行时间增加,可用内存越来越少,最终可能导致系统崩溃或程序终止。
- 常见原因:指针丢失(重新赋值前未释放)、循环引用、异常处理中未释放资源。
- 避免方法:
- 原则:严格遵循“谁申请,谁释放”的原则,成对使用
malloc/free或new/delete。 - 机制:使用智能指针(C++ 中的
std::shared_ptr,std::unique_ptr)利用 RAII 机制自动管理资源。 - 工具:开发阶段使用检测工具(如 Valgrind, Visual Studio 诊断工具)定期扫描。
- 语言特性:在使用 Java/Python 等带垃圾回收(GC)的语言时,虽无需手动释放,但仍需注意消除无用对象的引用以便 GC 回收
- 原则:严格遵循“谁申请,谁释放”的原则,成对使用
Q3:深拷贝(Deep Copy)和浅拷贝(Shallow Copy)的区别?
【参考答案】
- 浅拷贝:只复制对象的基本数据类型成员变量的值。如果成员变量是指针,只复制指针的地址,不复制指向的内容。结果是两个对象共享同一块堆内存,修改一个会影响另一个,析构时可能导致重复释放(Double Free)。
- 深拷贝:不仅复制基本类型,还会为指针成员重新分配一块新的内存,并将原内容复制过去。结果是两个对象完全独立,互不影响。
- 应用:在编写拷贝构造函数或重载赋值运算符时,若类中包含指针成员,必须实现深拷贝(遵循 C++ 的“三法则”或“五法则”)。
1.2、数据结构与算法篇
Q1:数组(Array)和链表(Linked List)的区别及适用场景?
【参考答案】
- 存储结构:
- 数组:内存连续,支持随机访问。
- 链表:内存离散,通过指针链接,不支持随机访问。
- 操作效率:
- 查询:数组 O(1)O(1)O(1)(通过下标直接计算地址);链表 O(n)O(n)O(n) (需遍历)。
- 增删:数组 O(n)O(n)O(n)(需移动大量元素);链表 O(1)O(1)O(1) (已知位置下,仅需修改指针指向)。
- 空间利用:
- 数组需预分配大小,可能造成浪费或溢出;链表动态分配,灵活但每个节点需额外存储指针,空间开销略大。
- 适用场景:
- 数组:读多写少、数据量固定、需要频繁随机访问的场景(如图像像素处理)。
- 链表:写多读少、数据量变化大、频繁插入删除的场景(如文件系统目录、浏览器历史记录)。
Q2:简述哈希表(Hash Map)的工作原理及如何解决冲突?
【参考答案】
- 原理:通过哈希函数将键(Key)映射为数组的下标,从而实现 O(1)O(1)O(1) 平均时间复杂度的查找、插入和删除。
- 哈希冲突:不同的 Key 被映射到了同一个下标。
- 解决方法:
- 链地址法(拉链法):每个数组位置挂一个链表(或红黑树),冲突的元素挂在链表上。这是最常用的方法(如 Java 8+ 的 HashMap)。
- 开放寻址法:发生冲突时,按照某种探测序列(如线性探测、二次探测)寻找下一个空闲位置。
- 优化:当链表过长时(如超过 8 个节点),可转换为红黑树以提高查询效率(从 O(n)O(n)O(n) 提升至 O(logn)O(logn)O(logn) )。
Q3:常见排序算法的时间复杂度及稳定性?
【参考答案】
| 算法 | 平均时间复杂度 | 最坏时间复杂度 | 空间复杂度 | 稳定性 | 备注 |
|---|---|---|---|---|---|
| 快速排序 | $ O(n \log n) $ | $ O(n^2) $ | $ O(\log n) $ | 不稳定 | 实际最快,基于分治,最坏情况发生在数组已有序且基准选择不当时 |
| 归并排序 | $ O(n \log n) $ | $ O(n \log n) $ | O(n)O(n)O(n) | 稳定 | 需要额外空间,适合链表排序或外部排序 |
| 堆排序 | $ O(n \log n) $ | $ O(n \log n) $ | O(1)O(1)O(1) | 不稳定 | 适合 Top-K 问题 |
| 冒泡/插入 | $ O(n^2) $ | $ O(n^2) $ | O(1)O(1)O(1) | 稳定 | 数据量小或基本有序时效率高 |
1.3、编程思想与工程篇
Q1:递归(Recursion)的优缺点?什么情况下不能用递归?
【参考答案】
- 优点:代码简洁、逻辑清晰,非常适合解决具有自相似性的问题(如树的遍历、汉诺塔、分治算法)。
- 缺点:
- 效率低:每次函数调用都需要压栈、保存现场,开销大。
- 栈溢出风险:递归层数过深会耗尽栈空间,导致程序崩溃(Stack Overflow)。
- 不能使用递归的情况:
- 数据规模极大,递归深度不可控时。
- 对性能要求极高,且迭代写法容易实现的场景。
- 嵌入式设备等栈空间受限的环境。
- 替代方案:可以使用**显式栈(Stack 数据结构)**将递归改为迭代,或使用尾递归优化(取决于编译器支持)。
Q2:编译型语言和解释型语言的区别?
【参考答案】
- 编译型(如 C/C++):
- 过程:源代码通过编译器一次性翻译成机器码(可执行文件)。
- 特点:执行速度快(直接运行机器码),跨平台性差(不同系统需重新编译)。
- 应用:操作系统、游戏引擎、高性能计算。
- 解释型(如 Python, JavaScript):
- 过程:源代码由解释器逐行读取并翻译执行,不生成独立的可执行文件。
- 特点:开发灵活、跨平台性好(有解释器即可),执行速度相对较慢。
- 应用:脚本、Web 开发、数据分析、AI 原型。
- 混合型(如 Java):先编译成字节码(.class),再由虚拟机(JVM)解释执行或通过 JIT(即时编译)转为机器码,兼顾了跨平台和效率。
Q3:你在调试程序(Debug)时通常用什么方法?
【参考答案】
- 工具调试:熟练使用 IDE 的断点调试功能(如 VS Code, IntelliJ IDEA, GDB)。
- 断点(Breakpoint):在关键位置暂停程序。
- 单步执行(Step Over/Into):逐行跟踪代码逻辑。
- 查看变量/监视(Watch):实时观察变量值的变化。
- 调用栈(Call Stack):分析函数调用层级,定位错误来源。
- 日志调试:在关键节点打印日志(Log),记录程序运行状态和变量值,适用于生产环境或难以复现的 Bug。
- 常见错误定位:
- 段错误(Segmentation Fault):通常是野指针或数组越界。
- 死循环:检查循环终止条件。
- 空指针异常:检查对象是否初始化。
Q4. 什么是面向对象?三大特性是什么?
参考回答:
- 封装:将数据和行为包装在类中,隐藏内部实现,对外暴露接口,提高安全性和可维护性。
- 继承:子类复用父类的属性和方法,支持代码复用和层次化设计。
- 多态:同一接口,不同实现。运行时通过父类引用调用子类对象的方法,提高扩展性。
加分点:可以提到多态的实现方式(虚函数表、动态绑定),以及继承的缺点(耦合度高,组合优于继承原则)。
二 C/C++ 面试题
2.1、基础语法与特性
Q1. C和C++的区别是什么?
参考回答:
- 编程范式:C是面向过程;C++是面向对象(封装、继承、多态),也支持泛型编程、函数式编程。
- 内存管理:C用
malloc/free;C++用new/delete,且new会调用构造函数。 - 类型安全:C++更强,如
const、引用、强转类型(static_cast等)。 - 标准库:C有标准库;C++有STL(容器、算法、迭代器)。
- 设计思想:C 是面向过程,关注算法和数据结构;C++ 兼容 C,新增面向对象(类、继承、多态)、泛型编程(STL)、异常处理等。
- 内存管理:C 靠
malloc/free;C++ 新增new/delete、智能指针。 - 函数特性:C++ 支持重载、默认参数、虚函数;C 不支持。
- 类型检查:C++ 类型检查更严格(如
void*赋值需强制转换)。
Q2. struct和class的区别?
参考回答:
- C++中两者本质相同,区别仅在于默认访问权限:
struct默认成员是publicclass默认成员是private
- C语言中
struct不能包含成员函数,C++中可以。
Q3. const的作用及用法?
参考回答:
- 修饰变量:值不可修改,必须初始化。
- 修饰指针:
const int *p:指向常量,指针可变,指向内容不可变。int * const p:指针常量,指针不可变,指向内容可变。const int* const p:都不可变。
- 修饰函数参数:防止参数被修改,尤其是引用或指针传递时。
- 修饰成员函数:表示该函数不会修改成员变量(
void func() const)。 - 修饰引用:
const T&,常用于函数参数,避免拷贝且防止修改实参。
Q4:#define 宏定义 与 const 常量 的区别?
【参考答案】
- 处理阶段:
#define:在预处理阶段进行文本替换,不分配内存,无类型检查。const:在编译阶段处理,有具体的数据类型,会分配内存(通常存储在只读数据段),有类型安全检查。
- 安全性:
#define:容易出错(如#define ADD(a,b) a+b,调用ADD(1,2)*3会变成1+2*3=7而不是 9),需加括号。const:更安全,编译器会拦截类型不匹配的错误。
- 调试:
#define:调试器看不到符号名(已被替换)。const:调试器可以看到变量名和值。
- 建议:现代 C++ 编程中优先使用
const(或constexpr),尽量避免宏。
Q5:指针(Pointer)与引用(Reference)的区别?
【参考答案】
- 本质:指针是一个变量,存储地址;引用是原变量的别名(底层通常由指针实现,但语义不同)。
- 初始化:
- 指针:可以不初始化(野指针),可以中途改变指向。
- 引用:必须初始化,且一旦绑定不能改变指向。
- 空值:指针可以为
NULL;引用不能为空(必须绑定有效对象)。 sizeof:- 指针:
sizeof得到的是指针本身的大小(32位系统4字节,64位系统8字节)。 - 引用:
sizeof得到的是所引用对象的大小。
- 指针:
- 自增运算:
p++指针地址移动;ref++是引用对象的值增加。
Q6:虚函数(Virtual Function)的实现原理?(高频难点)
【参考答案】
- 核心机制:虚函数表(vtable) 和 虚表指针(vptr)。
- 实现过程:
- 编译器为每个包含虚函数的类生成一张 vtable,表中存储了该类所有虚函数的地址。
- 该类的每个对象在内存起始位置(通常)隐藏了一个 vptr 指针,指向该类的 vtable。
- 动态绑定:当通过基类指针调用虚函数时,程序通过对象的
vptr找到vtable,再查表找到实际子类的函数地址进行调用。
- 关键点:若子类重写了虚函数,子类的 vtable 中对应项会被替换为子类函数的地址。
Q7:为什么析构函数要设为虚函数?
【参考答案】
- 场景:当通过基类指针删除一个派生类对象时(
delete basePtr)。 - 后果:
- 若基类析构非虚:只会调用基类的析构函数,派生类特有的资源(如派生类中
new的内存)不会被释放,导致内存泄漏。 - 若基类析构为虚:会根据 vtable 动态绑定,先调用派生类析构,再调用基类析构,确保资源完全释放。
- 若基类析构非虚:只会调用基类的析构函数,派生类特有的资源(如派生类中
- 结论:只要类中有虚函数,或者打算作为基类使用,析构函数必须设为
virtual。
Q8:static 关键字的作用?(分场景回答)
【参考答案】
- 修饰局部变量:
- 改变生命周期:变量存储在静态数据区,程序启动时初始化,结束时销毁,而非函数调用结束。
- 作用域不变(仍只在函数内可见)。
- 修饰全局变量/函数:
- 改变作用域:限制在当前文件(
.cpp)内可见,其他文件无法访问(内部链接),避免命名冲突。
- 改变作用域:限制在当前文件(
- 修饰类成员:
- 静态成员变量:属于类而非对象,所有对象共享一份,需在类外初始化。
- 静态成员函数:属于类,没有
this指针,只能访问静态成员变量或其他静态函数。
Q9:sizeof 一个空类是多少?为什么?
【参考答案】
- 结果:1 字节。
- 原因:C++ 标准要求每个对象在内存中必须有唯一的地址。如果大小为 0,那么在数组中两个相邻元素地址将相同,无法区分。因此编译器会隐式插入一个占位字节(padding)。
- 追问:如果有虚函数呢?
- 结果:1 字节(占位) + 指针大小(32位4字节,64位8字节)。因为需要存储
vptr。
- 结果:1 字节(占位) + 指针大小(32位4字节,64位8字节)。因为需要存储
2.2、内存管理
Q1. new/delete和malloc/free的区别?
参考回答:
| 特性 | new/delete | malloc/free |
|---|---|---|
| 本质 | 运算符 | 库函数 |
| 内存分配 | 自动计算大小 | 手动指定大小 |
| 构造函数/析构函数 | 会调用 | 不会调用 |
| 返回值 | 类型安全,直接返回类型指针 | 返回void*,需要强转 |
| 失败处理 | 抛出bad_alloc异常 | 返回NULL |
| 重载 | 可以重载 | 不可以 |
加分点:可以说明new实际上先调用operator new分配内存,再调用构造函数。
| 维度 | malloc/free | new/delete |
|---|---|---|
| 性质 | 库函数(需包含<stdlib.h>) | C++ 运算符 |
| 构造 / 析构 | 仅分配 / 释放内存,不调用 | 分配内存 + 调用构造函数;释放内存 + 调用析构函数 |
| 类型安全 | 返回void*,需强制类型转换 | 直接返回对应类型指针,无需转换 |
| 失败处理 | 返回NULL | 抛出bad_alloc异常 |
| 数组操作 | malloc(n*sizeof(int)) | new int[n]/delete[] |
Q2. 什么是内存泄漏?如何检测和避免?
参考回答:
- 定义:动态分配的内存不再使用但未释放,导致内存占用持续增长。
- 避免方法:
- 使用智能指针(
unique_ptr、shared_ptr、weak_ptr) - 遵循RAII原则(资源获取即初始化)
- 确保每个
new有对应的delete
- 使用智能指针(
- 检测工具:Valgrind、AddressSanitizer、Visual Studio诊断工具
Q3. 什么是野指针?如何避免?
参考回答:
- 野指针:指向已释放内存或未初始化内存的指针。
- 避免方法:
- 指针定义时初始化为
nullptr - 释放内存后将指针置为
nullptr - 避免返回局部变量的地址
- 使用智能指针
- 指针定义时初始化为
Q4. C/C++ 的内存分区?
- 栈:存储局部变量、函数参数、返回值;自动分配 / 释放,大小固定(通常几 M),溢出会栈崩溃。
- 堆:动态内存(
malloc/new);手动分配 / 释放,大小灵活,泄漏会导致内存耗尽。 - 全局 / 静态区:存储全局变量、
static变量;程序启动时分配,结束时释放。 - 常量区:存储字符串常量、
const常量;只读,不可修改。 - 代码区:存储程序指令;只读,受系统保护。
2.3、面向对象与多态
Q1. 虚函数是如何实现的?什么是虚函数表?
参考回答:
- 每个包含虚函数的类有一个虚函数表(vtable),存储虚函数地址。
- 每个对象有一个虚指针(vptr),指向该类的虚函数表。
- 调用虚函数时,通过vptr找到vtable,再调用对应函数,实现动态绑定。
- 构造函数不能是虚函数(虚表未初始化完成),析构函数通常是虚函数(保证正确释放派生类资源)。
Q2. 纯虚函数和抽象类
参考回答:
- 纯虚函数:
virtual void func() = 0;,表示没有实现,强制派生类重写。 - 抽象类:包含至少一个纯虚函数的类,不能实例化,只能作为接口。
- 抽象类可以有成员变量和非纯虚函数,提供默认实现。
Q3. 重载、重写、隐藏的区别?
参考回答:
重载(Overload):
- 同一作用域内,函数名相同,参数列表不同(类型、个数、顺序)。
- 与返回值无关,与
virtual无关。 - 属于静态多态。
重写(Override):
- 子类与父类之间,函数名、参数列表、返回值(协变除外)完全相同。
- 父类函数必须是
virtual。 - 属于动态多态。
隐藏(Hide):
- 子类函数与父类函数同名,但参数不同(无论是否有 virtual),或者父类函数非 virtual 且签名相同。
- 结果:父类同名函数在子类作用域被屏蔽,无法通过子类对象直接调用。
| 概念 | 作用域 | 条件 |
|---|---|---|
| 重载 | 同一作用域 | 函数名相同,参数列表不同(类型/个数/顺序) |
| 重写 | 基类与派生类 | 函数名、参数、返回值完全相同,基类为虚函数 |
| 隐藏 | 基类与派生类 | 派生类函数与基类函数同名但参数不同,或基类非虚函数同名 |
举例:
class Base {
public:
virtual void func() { } // 重写
void foo(int x) { } // 隐藏(如果派生类有foo())
};
class Derived : public Base {
public:
void func() override { } // 重写
void foo() { } // 隐藏Base::foo(int)
};
Q4:面向对象的三大特性及其作用?
【参考答案】
- 封装(Encapsulation):
- 含义:将数据和方法包装在类中,隐藏内部实现细节,仅通过公共接口(public)访问。
- 作用:提高安全性,降低耦合,便于维护。
- 继承(Inheritance):
- 含义:子类继承父类的属性和方法。
- 作用:代码复用,建立类之间的层次关系(Is-a 关系)。
- 多态(Polymorphism):
- 含义:同一接口在不同对象上有不同表现。
- 分类:
- 静态多态:函数重载、模板(编译期确定)。
- 动态多态:虚函数(运行期确定)。
- 作用:提高扩展性,符合“开闭原则”(对扩展开放,对修改关闭)。
Q5. 虚函数和纯虚函数的区别?
- 虚函数:声明加
virtual,有函数体;用于实现运行时多态,子类可重写。 - 纯虚函数:
virtual void func() = 0;,无函数体;包含纯虚函数的类为抽象类,不能实例化,子类必须实现纯虚函数才能实例化。 - 应用:纯虚函数用于定义接口(如基类
Shape的draw()),虚函数用于基类提供默认实现。
Q6. 析构函数为什么要声明为虚函数?
- 场景:父类指针指向子类对象(
Base *p = new Derived;),删除指针时(delete p),若父类析构非虚函数,仅调用父类析构,子类析构不执行,导致内存泄漏。 - 作用:声明为虚函数后,会根据对象实际类型(子类)调用对应析构函数,保证析构完整。
Q7. 拷贝构造函数的作用?什么时候会调用?
- 作用:用已存在的对象初始化新对象,默认浅拷贝;若类有指针成员,需手动实现深拷贝(避免浅拷贝导致的野指针 / 重复释放)。
- 调用场景:
- 用一个对象初始化另一个对象(
A a2 = a1;); - 函数参数按值传递;
- 函数返回值为类对象(编译器可能优化,但逻辑上会调用)。
- 用一个对象初始化另一个对象(
Q8. 友元函数 / 友元类的作用与缺点?
- 作用:突破封装,让外部函数 / 类访问类的私有 / 保护成员(如重载
<<运算符)。 - 缺点:破坏类的封装性,增加耦合度,尽量少用。
Q9. 继承中 public/protected/private 的访问权限?
| 基类权限 | public 继承 | protected 继承 | private 继承 |
|---|---|---|---|
| public | public | protected | private |
| protected | protected | protected | private |
| private | 不可访问 | 不可访问 | 不可访问 |
核心:private 成员在继承中始终不可访问;继承方式决定基类公有 / 保护成员在子类的权限。
2.4、智能指针
Q1. C++11的智能指针有哪些?区别是什么?
参考回答:
unique_ptr:独占所有权,不可拷贝,可移动,轻量高效,用于明确单一所有权场景。shared_ptr:共享所有权,使用引用计数,最后一个shared_ptr销毁时释放内存,有循环引用风险。weak_ptr:配合shared_ptr使用,不增加引用计数,用于解决循环引用,可通过lock()获取shared_ptr。
使用场景:
- 资源独占:
unique_ptr - 资源共享:
shared_ptr - 观察者模式、缓存:
weak_ptr
2.5、STL相关
Q1. vector的底层实现及扩容机制?
参考回答:
- 底层是连续内存数组,维护三个指针:
start、finish、end_of_storage。 - 扩容:当
size() == capacity()时,重新分配新内存(通常是原容量的2倍),将元素拷贝/移动到新空间,释放原内存。 - 扩容代价高,可提前用
reserve()预分配空间避免多次扩容。
Q2. map和unordered_map的区别?
| 特性 | map | unordered_map |
|---|---|---|
| 底层结构 | 红黑树 | 哈希表 |
| 有序性 | 键有序 | 键无序 |
| 时间复杂度 | O(log n) | O(1) 平均,最坏O(n) |
| 内存占用 | 较小 | 较大 |
| 适用场景 | 需要有序遍历 | 快速查找,不关心顺序 |
Q3. 左值和右值的区别?什么是移动语义?
参考回答:
- 左值:有持久地址,可取地址,如变量。
- 右值:临时对象、字面量,不能取地址。
- 移动语义:通过
std::move将左值转为右值引用,避免深拷贝,转移资源所有权。 - 作用:提高性能,尤其对于大对象(如
vector、string),避免不必要的拷贝。
举例:
std::vector<int> v1 = {1,2,3};
std::vector<int> v2 = std::move(v1); // v1资源转移到v2,v1变为空
Q4. 什么是RAII?
参考回答:
- RAII(Resource Acquisition Is Initialization)是C++的核心资源管理思想。
- 核心:资源的获取在对象初始化时完成,资源的释放在对象析构时完成。
- 优点:利用栈对象生命周期自动管理资源,防止资源泄漏。
- 应用:智能指针、锁的自动管理(
std::lock_guard)、文件句柄等。
Q5. 什么是未定义行为?举例说明。
参考回答:
- 未定义行为是指C++标准未规定的结果,编译器可能产生任意行为。
- 常见例子:
- 数组越界访问
- 使用已释放的指针
- 有符号整数溢出
- 除以0
- 修改字符串字面量:
char *p = "hello"; p[0] = 'H';

242

被折叠的 条评论
为什么被折叠?



