计算机基础知识-编程语言

计算机基础知识-编程语言

一、通用编程基础

1.1、内存管理篇

Q1:请简述“堆(Heap)”和“栈(Stack)”的区别?

【参考答案】
堆和栈都是程序运行时使用的内存区域,主要区别如下:

  1. 管理方式
    • :由编译器自动分配和释放。用于存储局部变量、函数参数、返回地址等。
    • :由程序员手动分配和释放(如 malloc/freenew/delete)。若忘记释放,会导致内存泄漏
  2. 生长方向
    • :向低地址方向生长(向下)。
    • :向高地址方向生长(向上)。
  3. 大小与效率
    • :空间较小(通常几MB),但存取速度极快(直接通过指针移动),不易产生碎片。递归过深容易导致栈溢出
    • :空间较大(受限于虚拟内存),但分配速度慢(涉及搜索空闲块),容易产生内存碎片
  4. 应用场景
    • 栈适合存储生命周期短、大小确定的数据;堆适合存储生命周期长、大小动态变化的数据(如大型数组、对象)。

Q2:什么是内存泄漏(Memory Leak)?如何避免?

【参考答案】

  • 定义:指程序在运行过程中动态分配了堆内存,但在使用完毕后没有释放,导致这块内存无法被再次利用。随着程序运行时间增加,可用内存越来越少,最终可能导致系统崩溃或程序终止。
  • 常见原因:指针丢失(重新赋值前未释放)、循环引用、异常处理中未释放资源。
  • 避免方法
    1. 原则:严格遵循“谁申请,谁释放”的原则,成对使用 malloc/freenew/delete
    2. 机制:使用智能指针(C++ 中的 std::shared_ptr, std::unique_ptr)利用 RAII 机制自动管理资源。
    3. 工具:开发阶段使用检测工具(如 Valgrind, Visual Studio 诊断工具)定期扫描。
    4. 语言特性:在使用 Java/Python 等带垃圾回收(GC)的语言时,虽无需手动释放,但仍需注意消除无用对象的引用以便 GC 回收

Q3:深拷贝(Deep Copy)和浅拷贝(Shallow Copy)的区别?

【参考答案】

  • 浅拷贝:只复制对象的基本数据类型成员变量的值。如果成员变量是指针,只复制指针的地址,不复制指向的内容。结果是两个对象共享同一块堆内存,修改一个会影响另一个,析构时可能导致重复释放(Double Free)。
  • 深拷贝:不仅复制基本类型,还会为指针成员重新分配一块新的内存,并将原内容复制过去。结果是两个对象完全独立,互不影响。
  • 应用:在编写拷贝构造函数或重载赋值运算符时,若类中包含指针成员,必须实现深拷贝(遵循 C++ 的“三法则”或“五法则”)。

1.2、数据结构与算法篇

Q1:数组(Array)和链表(Linked List)的区别及适用场景?

【参考答案】

  1. 存储结构
    • 数组:内存连续,支持随机访问。
    • 链表:内存离散,通过指针链接,不支持随机访问。
  2. 操作效率
    • 查询:数组 O(1)O(1)O(1)(通过下标直接计算地址);链表 O(n)O(n)O(n) (需遍历)。
    • 增删:数组 O(n)O(n)O(n)(需移动大量元素);链表 O(1)O(1)O(1) (已知位置下,仅需修改指针指向)。
  3. 空间利用
    • 数组需预分配大小,可能造成浪费或溢出;链表动态分配,灵活但每个节点需额外存储指针,空间开销略大。
  4. 适用场景
    • 数组:读多写少、数据量固定、需要频繁随机访问的场景(如图像像素处理)。
    • 链表:写多读少、数据量变化大、频繁插入删除的场景(如文件系统目录、浏览器历史记录)。

Q2:简述哈希表(Hash Map)的工作原理及如何解决冲突?

【参考答案】

  • 原理:通过哈希函数将键(Key)映射为数组的下标,从而实现 O(1)O(1)O(1) 平均时间复杂度的查找、插入和删除。
  • 哈希冲突:不同的 Key 被映射到了同一个下标。
  • 解决方法
    1. 链地址法(拉链法):每个数组位置挂一个链表(或红黑树),冲突的元素挂在链表上。这是最常用的方法(如 Java 8+ 的 HashMap)。
    2. 开放寻址法:发生冲突时,按照某种探测序列(如线性探测、二次探测)寻找下一个空闲位置。
  • 优化:当链表过长时(如超过 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)的优缺点?什么情况下不能用递归?

【参考答案】

  • 优点:代码简洁、逻辑清晰,非常适合解决具有自相似性的问题(如树的遍历、汉诺塔、分治算法)。
  • 缺点
    1. 效率低:每次函数调用都需要压栈、保存现场,开销大。
    2. 栈溢出风险:递归层数过深会耗尽栈空间,导致程序崩溃(Stack Overflow)。
  • 不能使用递归的情况
    1. 数据规模极大,递归深度不可控时。
    2. 对性能要求极高,且迭代写法容易实现的场景。
    3. 嵌入式设备等栈空间受限的环境。
  • 替代方案:可以使用**显式栈(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. structclass的区别?

参考回答:

  • C++中两者本质相同,区别仅在于默认访问权限
    • struct默认成员是public
    • class默认成员是private
  • C语言中struct不能包含成员函数,C++中可以。

Q3. const的作用及用法?

参考回答:

  • 修饰变量:值不可修改,必须初始化。
  • 修饰指针
    • const int *p:指向常量,指针可变,指向内容不可变。
    • int * const p:指针常量,指针不可变,指向内容可变。
    • const int* const p:都不可变。
  • 修饰函数参数:防止参数被修改,尤其是引用或指针传递时。
  • 修饰成员函数:表示该函数不会修改成员变量(void func() const)。
  • 修饰引用const T&,常用于函数参数,避免拷贝且防止修改实参。

Q4:#define 宏定义 与 const 常量 的区别?

【参考答案】

  1. 处理阶段
    • #define:在预处理阶段进行文本替换,不分配内存,无类型检查。
    • const:在编译阶段处理,有具体的数据类型,会分配内存(通常存储在只读数据段),有类型安全检查。
  2. 安全性
    • #define:容易出错(如 #define ADD(a,b) a+b,调用 ADD(1,2)*3 会变成 1+2*3=7 而不是 9),需加括号。
    • const:更安全,编译器会拦截类型不匹配的错误。
  3. 调试
    • #define:调试器看不到符号名(已被替换)。
    • const:调试器可以看到变量名和值。
  4. 建议:现代 C++ 编程中优先使用 const(或 constexpr),尽量避免宏。

Q5:指针(Pointer)与引用(Reference)的区别?

【参考答案】

  1. 本质:指针是一个变量,存储地址;引用是原变量的别名(底层通常由指针实现,但语义不同)。
  2. 初始化
    • 指针:可以不初始化(野指针),可以中途改变指向。
    • 引用:必须初始化,且一旦绑定不能改变指向。
  3. 空值:指针可以为 NULL;引用不能为空(必须绑定有效对象)。
  4. sizeof
    • 指针:sizeof 得到的是指针本身的大小(32位系统4字节,64位系统8字节)。
    • 引用:sizeof 得到的是所引用对象的大小。
  5. 自增运算p++ 指针地址移动;ref++ 是引用对象的值增加。

Q6:虚函数(Virtual Function)的实现原理?(高频难点)

【参考答案】

  • 核心机制虚函数表(vtable)虚表指针(vptr)
  • 实现过程
    1. 编译器为每个包含虚函数的类生成一张 vtable,表中存储了该类所有虚函数的地址。
    2. 该类的每个对象在内存起始位置(通常)隐藏了一个 vptr 指针,指向该类的 vtable。
    3. 动态绑定:当通过基类指针调用虚函数时,程序通过对象的 vptr 找到 vtable,再查表找到实际子类的函数地址进行调用。
  • 关键点:若子类重写了虚函数,子类的 vtable 中对应项会被替换为子类函数的地址。

Q7:为什么析构函数要设为虚函数?

【参考答案】

  • 场景:当通过基类指针删除一个派生类对象时(delete basePtr)。
  • 后果
    • 若基类析构非虚:只会调用基类的析构函数,派生类特有的资源(如派生类中 new 的内存)不会被释放,导致内存泄漏
    • 若基类析构为虚:会根据 vtable 动态绑定,先调用派生类析构,再调用基类析构,确保资源完全释放。
  • 结论:只要类中有虚函数,或者打算作为基类使用,析构函数必须设为 virtual

Q8:static 关键字的作用?(分场景回答)

【参考答案】

  1. 修饰局部变量
    • 改变生命周期:变量存储在静态数据区,程序启动时初始化,结束时销毁,而非函数调用结束。
    • 作用域不变(仍只在函数内可见)。
  2. 修饰全局变量/函数
    • 改变作用域:限制在当前文件(.cpp)内可见,其他文件无法访问(内部链接),避免命名冲突。
  3. 修饰类成员
    • 静态成员变量:属于类而非对象,所有对象共享一份,需在类外初始化。
    • 静态成员函数:属于类,没有 this 指针,只能访问静态成员变量或其他静态函数。

Q9:sizeof 一个空类是多少?为什么?

【参考答案】

  • 结果1 字节
  • 原因:C++ 标准要求每个对象在内存中必须有唯一的地址。如果大小为 0,那么在数组中两个相邻元素地址将相同,无法区分。因此编译器会隐式插入一个占位字节(padding)。
  • 追问:如果有虚函数呢?
    • 结果:1 字节(占位) + 指针大小(32位4字节,64位8字节)。因为需要存储 vptr

2.2、内存管理

Q1. new/deletemalloc/free的区别?

参考回答:

特性new/deletemalloc/free
本质运算符库函数
内存分配自动计算大小手动指定大小
构造函数/析构函数会调用不会调用
返回值类型安全,直接返回类型指针返回void*,需要强转
失败处理抛出bad_alloc异常返回NULL
重载可以重载不可以

加分点:可以说明new实际上先调用operator new分配内存,再调用构造函数。

维度malloc/freenew/delete
性质库函数(需包含<stdlib.h>C++ 运算符
构造 / 析构仅分配 / 释放内存,不调用分配内存 + 调用构造函数;释放内存 + 调用析构函数
类型安全返回void*,需强制类型转换直接返回对应类型指针,无需转换
失败处理返回NULL抛出bad_alloc异常
数组操作malloc(n*sizeof(int))new int[n]/delete[]

Q2. 什么是内存泄漏?如何检测和避免?

参考回答:

  • 定义:动态分配的内存不再使用但未释放,导致内存占用持续增长。
  • 避免方法
    • 使用智能指针unique_ptrshared_ptrweak_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:面向对象的三大特性及其作用?

【参考答案】

  1. 封装(Encapsulation)
    • 含义:将数据和方法包装在类中,隐藏内部实现细节,仅通过公共接口(public)访问。
    • 作用:提高安全性,降低耦合,便于维护。
  2. 继承(Inheritance)
    • 含义:子类继承父类的属性和方法。
    • 作用:代码复用,建立类之间的层次关系(Is-a 关系)。
  3. 多态(Polymorphism)
    • 含义:同一接口在不同对象上有不同表现。
    • 分类
      • 静态多态:函数重载、模板(编译期确定)。
      • 动态多态:虚函数(运行期确定)。
    • 作用:提高扩展性,符合“开闭原则”(对扩展开放,对修改关闭)。

Q5. 虚函数和纯虚函数的区别?

  • 虚函数:声明加virtual,有函数体;用于实现运行时多态,子类可重写。
  • 纯虚函数virtual void func() = 0;,无函数体;包含纯虚函数的类为抽象类,不能实例化,子类必须实现纯虚函数才能实例化。
  • 应用:纯虚函数用于定义接口(如基类Shapedraw()),虚函数用于基类提供默认实现。

Q6. 析构函数为什么要声明为虚函数?

  • 场景:父类指针指向子类对象(Base *p = new Derived;),删除指针时(delete p),若父类析构非虚函数,仅调用父类析构,子类析构不执行,导致内存泄漏
  • 作用:声明为虚函数后,会根据对象实际类型(子类)调用对应析构函数,保证析构完整。

Q7. 拷贝构造函数的作用?什么时候会调用?

  • 作用:用已存在的对象初始化新对象,默认浅拷贝;若类有指针成员,需手动实现深拷贝(避免浅拷贝导致的野指针 / 重复释放)。
  • 调用场景:
    1. 用一个对象初始化另一个对象(A a2 = a1;);
    2. 函数参数按值传递;
    3. 函数返回值为类对象(编译器可能优化,但逻辑上会调用)。

Q8. 友元函数 / 友元类的作用与缺点?

  • 作用:突破封装,让外部函数 / 类访问类的私有 / 保护成员(如重载<<运算符)。
  • 缺点:破坏类的封装性,增加耦合度,尽量少用。

Q9. 继承中 public/protected/private 的访问权限?

基类权限public 继承protected 继承private 继承
publicpublicprotectedprivate
protectedprotectedprotectedprivate
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的底层实现及扩容机制?

参考回答:

  • 底层是连续内存数组,维护三个指针:startfinishend_of_storage
  • 扩容:当size() == capacity()时,重新分配新内存(通常是原容量的2倍),将元素拷贝/移动到新空间,释放原内存。
  • 扩容代价高,可提前用reserve()预分配空间避免多次扩容。

Q2. mapunordered_map的区别?

特性mapunordered_map
底层结构红黑树哈希表
有序性键有序键无序
时间复杂度O(log n)O(1) 平均,最坏O(n)
内存占用较小较大
适用场景需要有序遍历快速查找,不关心顺序

Q3. 左值和右值的区别?什么是移动语义?

参考回答:

  • 左值:有持久地址,可取地址,如变量。
  • 右值:临时对象、字面量,不能取地址。
  • 移动语义:通过std::move将左值转为右值引用,避免深拷贝,转移资源所有权。
  • 作用:提高性能,尤其对于大对象(如vectorstring),避免不必要的拷贝。

举例

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';
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

知行智研社

您的鼓励是我的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值