这是一份为大部分会用到C++的岗位的求职者的题单。
一、C++基础概念
1. C和C++的主要区别是什么?
- 解析: C++是C的超集,但增加了面向对象编程(类、继承、多态)、泛型编程(模板)、异常处理、命名空间、函数重载、引用等特性。C是过程式语言,而C++是多范式语言。
- 注解: 面试官问这个是为了确认你对C++的定位有基本了解。可以补充说明“C++不是纯粹的OOP语言,它支持多种编程风格”。
2. const
和 #define
定义常量有什么区别?
- 解析:
#define
是预处理指令,进行简单的文本替换,没有类型检查,不占用内存(在符号表中)。const
是编译器处理的常量,有类型检查,会占用内存(可能在只读数据区或被优化掉)。const
常量可以被调试,而#define
的宏在调试时已经被替换。
- 注解: 强调类型安全和可调试性是现代C++更推荐使用
const
的原因。
3. static
关键字有哪些用法?
- 解析: 这是一个高频考点。
- 在函数/文件内:修饰局部变量,改变其生命周期,使其在程序整个生命周期内存在,且只初始化一次。
- 在全局变量/函数前:改变其链接性,使其仅在当前文件内可见(内部链接)。
- 在类中:
- 修饰成员变量:该变量属于类本身,所有对象共享一份数据。
- 修饰成员函数:该函数属于类本身,不能访问类的非静态成员(没有
this
指针)。
- 注解: 可以按作用域(函数、文件、类)来分类回答,显得条理清晰。
4. sizeof
和 strlen
的区别?
- 解析:
sizeof
是运算符,在编译时计算数据类型或对象所占的内存大小(字节数)。对于字符串,它包含末尾的\0
。strlen
是库函数,在运行时计算字符串的实际长度(字符数),遇到\0
即停止,不包含\0
。
- 注解: 举例说明:
char str[] = "hello";
,sizeof(str)
是6(5个字符+\0
),strlen(str)
是5。
二、面向对象编程(OOP)
1. C++面向对象的三大特性是什么?
- 解析: 封装、继承、多态。
- 封装: 将数据和操作数据的函数绑定在一起,并对外部隐藏实现细节。
- 继承: 允许新类(派生类)从现有类(基类)获取属性和行为,实现代码复用。
- 多态: 允许不同类的对象对同一消息做出不同的响应。主要通过虚函数和继承实现。
- 注解: 不要只背名字,要能简要解释每个特性的目的。
2. 什么是构造函数和析构函数?它们可以是虚函数吗?
- 解析:
- 构造函数: 在对象创建时自动调用,用于初始化对象。不能是虚函数,因为在调用构造函数时,对象的虚表指针(vptr)还未正确初始化。
- 析构函数: 在对象销毁时自动调用,用于清理资源。基类的析构函数必须是虚函数(除非类不被继承),否则通过基类指针删除派生类对象时,只会调用基类的析构函数,导致派生类的资源泄漏(派生类对象析构不完全)。
- 注解: “基类析构函数必须为虚函数”是面试必考点,务必理解其背后的原因。
3. 什么是虚函数?它的实现原理是什么?
- 解析: 使用
virtual
关键字声明的成员函数,用于实现运行时多态。 - 原理: 虚函数表(vtable) 和 虚表指针(vptr)。
- 编译器会为每个包含虚函数的类创建一个虚函数表(vtable),表中存放该类所有虚函数的函数指针。
- 每个该类对象内部都有一个隐藏的虚表指针(vptr),指向该类的vtable。
- 当通过基类指针调用虚函数时,程序通过对象的vptr找到对应的vtable,再从vtable中找到正确的函数地址进行调用。
- 注解: 这是理解C++多态机制的基石,必须掌握。可以画图辅助说明。
4. 重载(Overload)、覆盖(Override)、隐藏(Hiding)的区别?
- 解析:
- 重载: 同一作用域内,函数名相同但参数列表不同(类型、个数、顺序)。与返回值、
virtual
无关。 - 覆盖/重写: 发生在派生类和基类之间。派生类重新定义基类的虚函数,要求函数名、参数列表、返回值都必须相同(协变返回值除外)。
- 隐藏: 派生类的函数屏蔽了基类的同名函数(无论是否为虚函数)。只要函数名相同,且不构成覆盖,就会发生隐藏。
- 重载: 同一作用域内,函数名相同但参数列表不同(类型、个数、顺序)。与返回值、
- 注解: 这是一个极易混淆的概念,可以通过代码示例来区分。
三、内存管理
1. new
/delete
和 malloc
/free
的区别?
- 解析:
特性 new
/delete
malloc
/free
语言 C++运算符 C库函数 内存大小 编译器自动计算 需手动指定字节数 返回值 返回确切类型指针 返回 void*
,需强制转换失败行为 抛出 bad_alloc
异常返回 NULL
构造/析构 会调用构造函数和析构函数 不会调用 重载 可以重载 不可重载 - 注解: 核心区别在于
new/delete
会管理对象的生命周期(调用构造和析构),而malloc/free
只负责分配和释放raw memory。
2. 什么是内存泄漏?如何避免?
- 解析: 程序在运行中未能释放不再使用的内存,导致可用内存逐渐减少,最终可能耗尽。
- 避免方法:
- 遵循RAII原则:使用智能指针(
std::unique_ptr
,std::shared_ptr
)管理资源。 - 谁申请,谁释放:确保成对使用
new/delete
和new[]/delete[]
。 - 使用容器替代手动管理:如
std::vector
替代动态数组。
- 遵循RAII原则:使用智能指针(
- 注解: 现代C++中,“几乎永远不要使用裸指针和
new/delete
” 是避免内存泄漏的最佳实践。
3. 什么是智能指针?std::unique_ptr
和std::shared_ptr
的区别?
- 解析: 智能指针是RAII理念的实践者,用对象来管理裸指针,在其析构时自动释放内存。
std::unique_ptr
: 独占所有权。同一时间只能有一个unique_ptr
指向一个对象。无法被复制,只能被移动(std::move
)。std::shared_ptr
: 共享所有权。通过引用计数机制,记录有多少个shared_ptr
指向同一对象。计数为0时,对象被销毁。std::weak_ptr
: 伴随shared_ptr
使用,是弱引用。它不增加引用计数,用于解决shared_ptr
的循环引用问题。
- 注解: 这是现代C++内存管理的核心,必须熟练掌握其用法和区别。
四、STL(标准模板库)
1. 说说你常用的STL容器及其底层实现和时间复杂度。
- 解析:
容器 底层实现 插入/删除(平均) 查找(平均) vector
动态数组 尾部O(1),头部/中部O(n) O(n) list
/forward_list
双向/单向链表 O(1)(已知位置) O(n) deque
分段连续空间 头尾O(1) O(n) map
/set
红黑树 O(log n) O(log n) unordered_map
/unordered_set
哈希表 O(1)(最理想) O(1)(最理想) - 注解: 要能根据应用场景(频繁查找、频繁头尾插入、需要有序性)选择合适的容器。
2. vector
的底层原理和扩容机制?
- 解析:
- 原理: 使用一段连续的动态分配数组来存储元素。
- 扩容机制: 当当前容量(
capacity()
)不足以容纳新元素时,会申请一块更大的新内存(通常是原大小的1.5或2倍,取决于编译器实现),将旧数据拷贝或移动到新内存,然后释放旧内存。
- 注解: 理解
size()
和capacity()
的区别。频繁扩容会导致性能开销,如果知道元素大概数量,可以用reserve()
预分配空间。
3. map
和 unordered_map
的区别?如何选择?
- 解析:
特性 map
unordered_map
底层结构 红黑树(平衡二叉搜索树) 哈希表 元素顺序 按键排序,有序 无序 操作时间复杂度 O(log n) 平均O(1),最差O(n) 是否需要哈希函数 否 是 - 选择: 如果需要元素有序,或者对遍历顺序有要求,用
map
。如果只需要高效的查找速度,且不关心顺序,用unordered_map
。
五、高级与C++11/14/17新特性
1. 左值
、右值
、左值引用
、右值引用
的区别?std::move
和std::forward
的作用?
- 解析:
- 左值: 有标识符、可取地址的表达式(如变量、函数返回的左值引用)。
- 右值: 临时对象,即将消亡的值,不可取地址(如字面量、函数返回的非引用值、
std::move
的结果)。 - 左值引用:
T&
,只能绑定到左值。 - 右值引用:
T&&
,只能绑定到右值。其核心目的是实现移动语义,避免不必要的深拷贝,提升性能。 std::move
: 无条件将其参数转换为右值引用。它本身不移动任何东西,只是告诉编译器“这个对象可以被移动”。std::forward
: 有条件的转换,用于完美转发(Perfect Forwarding),在模板函数中保持参数的原始值类别(左值性或右值性)。
- 注解: 这是现代C++性能优化的关键,也是难点。理解移动语义和完美转发是高级C++工程师的标志。
2. Lambda表达式的组成部分是什么?
- 解析:
[捕获列表] (参数列表) -> 返回类型 { 函数体 }
- 捕获列表
[]
: 指定如何捕获外部变量。[=]
:以值方式捕获所有外部变量。[&]
:以引用方式捕获所有外部变量。[a, &b]
:以值捕获a,以引用捕获b。[this]
:捕获当前类的this
指针。
- 其他部分与普通函数类似,参数列表和返回类型可省略。
- 捕获列表
- 注解: 理解捕获列表是使用Lambda的关键,要特别注意按值捕获和按引用捕获的生命周期问题。
3. 什么是RAII?
- 解析: 资源获取即初始化。其核心思想是:将资源(内存、文件句柄、锁等)的生命周期与对象的生命周期绑定。
- 在构造函数中获取资源。
- 在析构函数中释放资源。
- 注解: RAII是C++管理资源的根本大法。智能指针、
std::fstream
、std::lock_guard
等都是RAII的典型应用。它可以保证异常安全(Exception Safety)。
六、实战编程题
1. 实现一个单例模式(Singleton)。
class Singleton {
public:
// 删除拷贝构造和赋值操作,确保唯一性
Singleton(const Singleton&) = delete;
Singleton& operator=(const Singleton&) = delete;
static Singleton& getInstance() {
static Singleton instance; // C++11保证局部静态变量初始化是线程安全的
return instance;
}
private:
Singleton() {} // 构造函数私有化
~Singleton() {}
};
// 使用: Singleton& s = Singleton::getInstance();
- 注解: 注意C++11后,局部静态变量初始化是线程安全的,因此这是最简洁优雅的实现(Meyers’ Singleton)。
2. 实现一个智能指针(如unique_ptr
的简化版)。
template<typename T>
class SimpleUniquePtr {
public:
explicit SimpleUniquePtr(T* ptr = nullptr) : ptr_(ptr) {}
~SimpleUniquePtr() { delete ptr_; }
// 禁止拷贝
SimpleUniquePtr(const SimpleUniquePtr&) = delete;
SimpleUniquePtr& operator=(const SimpleUniquePtr&) = delete;
// 允许移动
SimpleUniquePtr(SimpleUniquePtr&& other) noexcept : ptr_(other.ptr_) {
other.ptr_ = nullptr;
}
SimpleUniquePtr& operator=(SimpleUniquePtr&& other) noexcept {
if (this != &other) {
delete ptr_;
ptr_ = other.ptr_;
other.ptr_ = nullptr;
}
return *this;
}
T* get() const { return ptr_; }
T& operator*() const { return *ptr_; }
T* operator->() const { return ptr_; }
private:
T* ptr_;
};
- 注解: 这道题考察对资源管理、移动语义、模板、
noexcept
等的综合运用。
3. 反转一个单链表。
struct ListNode {
int val;
ListNode *next;
ListNode(int x) : val(x), next(nullptr) {}
};
class Solution {
public:
ListNode* reverseList(ListNode* head) {
ListNode *prev = nullptr;
ListNode *curr = head;
while (curr != nullptr) {
ListNode *nextTemp = curr->next; // 保存下一个节点
curr->next = prev; // 反转指针
prev = curr; // 移动prev
curr = nextTemp; // 移动curr
}
return prev; // prev现在是新的头节点
}
};
- 注解: 经典的数据结构题,考察指针操作和逻辑思维能力。务必能在白板上清晰写出。
你们可以把想要的岗位可以发到评论区里面,我可以专门发篇文章,然后列一个题单解析或注解。