面试问题合集

自我介绍

面试官你好,我叫***,毕业于**大学软件工程专业,在校期间认真学习,成绩排名第三,考过六级、还有软件设计师等一些证书,我学过c++、qt等相关的技术栈,我具备良好的团队协作能力,能高效协调团队分工、推进项目进度,同时拥有独立解决问题的能力,可快速定位并解决开发中的技术难题,具备良好的沟通能力与学习能力,能快速适应岗位需求与新技术学习,对这个岗位比较感兴趣,希望可以给个机会,非常感谢。

为什么有空窗期

我5月份的时候实习完然后回学校做毕设答辩等一些学校的相关事情,然后我姐在成都,她六月底快生小孩,我妈的意思就是说让我过来这边帮忙照顾一下她或者小孩,有个照应,因为刚生完孩子她也要休息,然后我就过来照顾小孩了待了一段时间,现在不怎么需要我带了我就出来找工作了。

结束语

我对相关软件开发有浓厚的兴趣,也非常认同贵公司的技术方向与发展理念,非常希望能加入贵公司,将自己的技术能力应用到实际项目中,为团队创造价值,同时在工作中不断提升自己。感谢面试官给我这次面试机会!

为什么选择我们公司?

重点:夸公司+对岗位的了解+自身优势+与岗位的匹配度

参考:首先贵司的发展前景和口碑是非常不错的,对待新人有完整的培训和晋升机制。其次是我学的软件工程专业和该工作岗位非常匹配,并且我的实习经历也锻炼了我的相关开发能力,对于这方面工作我也有自己的理解和想法,希望贵司能给我一个机会,让我能成为贵司的一员。

为什么觉得你能胜任这份工作?

"面试官您好,关于这个问题,我理解贵公司这个C++/Qt岗位的核心,是负责工业控制上位机软件的开发和维护,因此对C++基础、界面编程和实际问题解决能力要求比较高。

其中,C++的实际应用能力独立完成模块的实战经验 是我比较匹配的优势。我毕业设计做的就是基于Qt的智能门禁系统 ,我独立负责 了人脸识别和数据库两个核心模块。这个过程让我把C++的面向对象、多线程和Qt的信号槽机制 真正用了起来,也积累了很多调试经验和解决编译、依赖问题的能力

如果我有幸入职,我会第一周 先熟悉公司的代码规范和项目架构,第二周 开始尝试参与一些小的任务,第一个月内希望能独立承担一个功能模块,尽快为团队做出贡献。"

加班问题怎么回答?

如果公司出现紧急状况需要加班处理,我是可以接受的。毕竟每个公司都会出现一些紧急情况,偶尔需要加班我不排斥。其次,如果是由于刚入职,不太熟悉公司的业务流程而进行加班,我愿意花时间去学、弥补不足。同时我也会提高个人工作效率,在规定时间内完成工作,减少不必要的加班。如果是工作需要我会义不容辞加班。我现在单身,没有任何家庭负担,可以全身心的投入工作。但同时,我也会提高工作效率,减少不必要的加班。但是我想提前了解一下贵公司的加班频率高吗?如果加班的话会给予什么样的补贴呢?

基于 Qt 的智能门禁系统项目讲解

你是零经验,为什么觉得能够胜任这份工作呢?

如果面试官提出这个问题,说明并不真正在乎"经验",关键是考察你的心态。

回答需要突出自己的掌握能力,快速上手能力。

作为应届毕业生,在工作经验方面确实有所欠缺,因此在读书期间我一直利用各种机会在这个行业里做实习。在实习过程中发现,我的适应能力和学习能力是很强的,而且比较勤奋,所以在实习中均能圆满完成各项工作,从中获取的经验也令我很受益。请放心,学校所学及实习的工作经验能使我胜任贵公司的职位。

加班问题怎么回答?

如果公司出现紧急状况需要加班处理,我是可以接受的。毕竟每个公司都会出现一些紧急情况,偶尔需要加班我不排斥。其次,如果是由于刚入职,不太熟悉公司的业务流程而进行加班,我愿意花时间去学、弥补不足。同时我也会提高个人工作效率,在规定时间内完成工作,减少不必要的加班。如果是工作需要我会义不容辞加班。我现在单身,没有任何家庭负担,可以全身心的投入工作。但同时,我也会提高工作效率,减少不必要的加班。但是我想提前了解一下贵公司的加班频率高吗?如果加班的话会给予什么样的补贴呢?

工作好找嘛,面试机会多嘛?面试几家了,

我这段时间也在有选择地面试,大概面了几家,主要在挑选和自己技术栈匹配、发展方向一致的平台,希望能找到一份可以长期稳定发展的工作。

面试官是直属领导可以问

如果有幸入职,我需要提前准备什么?

面试官是HR(一般是初面)可以问

请问公司的晋升机制是怎样的?多长时间一次?具体流程是怎样的?

这个岗位的考核指标是什么?各占多少比例?

岗位的试用期是多久?

转正考核标准是什么?

想了解下咱们团队目前的技术栈和主要业务方向是什么?

咱们部门的晋升和成长路径是怎样的?

新人入职后培训和带教机制是怎样的?

上下班时间分别是什么?

单休还是双休

是否是弹性工作制

实习期间需要加班吗,加班是否有补贴?感觉肯定是没有

实习期间是否有带薪休假,带薪病假制度

工作的部门规模和分工情况如何?

薪资的数目和构成?很多公司的工资是拼凑出来的。通常由基本工资、绩效、奖金、补贴等组成。

工资发放日期?

请假如何算工资

是否有转正名额,转正标准是什么?

问五险一金怎么交:什么时候开始交?按什么基数和比例交?有的公司入职就交,有的要转正后才交,所以得问清楚,避免社保断掉。基数和比例不同,每个月可能差几百到几干块。还要问是在本地交还是在异地交(有些公司总部在外地),这些都要搞清楚

打算找一份什么样的工作

我希望找一份技术方向匹配、能持续成长的工作,最好是嵌入式 / Linux/ Qt 相关的开发岗位。希望能在一个稳定的平台上,深入做技术,参与真实项目,不断提升自己的工程能力,长期稳定发展。

你对工作的期望是什么?

我期望能够在一个注重团队合作和沟通的环境中工作,这样我可以与同事共同学习共同进步。同时,我也希望公司能够提供更多的培训和学习机会,让我能够不断提升自己的专业技能和知识水平。我相信,在这样的环境中,我一定能够发挥自己的优势,为公司的发展做出贡献。

目前找工作比较看重什么?

平台,薪资,福利待遇,工作环境,团队

平台和发展空间:能不能学到东西、有没有成长路径;

团队技术氛围:技术氛围好,大家愿意交流,能一起进步;

工作内容匹配度:和我擅长的嵌入式 / Linux/Qt 方向一致;

薪资福利和工作环境,在合理范围内我都可以接受。

说说你的优点?

我觉得自己比较突出的优点是做事专注、有始有终,只要接手了任务,就一定会想办法完成,不喜欢半途而废。

另外我学习意愿比较强,愿意主动接触新技术、新方案,适应能力也不错。

平时做事比较沉着冷静、条理清晰,习惯列任务清单,按优先级安排工作,定期复盘,保证按时保质完成。

同时我也比较乐于助人、好沟通,心态乐观,能很快融入团队。

说说你的缺点?

我之前有个小不足,就是有时会过于追求细节完美,容易在一些非关键细节上花费过多时间,导致整体节奏偏慢。

现在我已经在有意识地调整,先抓重点、保进度,再优化细节,整体效率提升了很多,也学会更好地平衡质量和节奏。

你的职业规划是什么?

短期来看(比如未来1-2年),我希望在咱们这个岗位上,能把基本功打得更扎实,把具体业务吃透。中期来看(比如未来3-5年),我希望能在业务能力的基础上,能够往更全面的管理方向发展,做出点像样的成绩。当然,这些都是我自己的想法,具体怎么走,还得看公司平台给到的机会和培养。

我的职业规划是成为一名专业的技术人才,为公司带来更多的技术创新和价值。我希望能够在贵公司这样一个平台上,不断学习和成长。我会积极参与公司的技术交流和分享活动,与同行们共同探讨技术难题和前沿技术。同时,我也会注重实践经验的积累,主动争取参与公司的技术项目和实践机会。我相信,通过不断学习和实践,我一定能够实现自己的职业目标,为公司的发展贡献自己的力量。

你的抗压能力怎么样?

我觉得适度压力能激发潜力。我的处理方式是:首先列出所有任务,按重要性和紧急程度排序;然后主动和领导沟通资源调配;每天早上花10分钟规划当日重点,晚上复盘进度。最终不仅按时交付,还获得了客户好评。

如果我们没有录取你,下步有什么打算?

如果没能获得这个机会,我会先反思这次面试,看看哪些地方可以改进。但不会因此停下来取的脚步,会继续寻找合适的机会。不过说实话,贵公司确实是我的首选,如果这次不行,等贵公司下次有合适岗位时,我还会再来试试。当然,我要希望能直接加入,为团队创造价值。

如果你和同事发生冲突,你会怎么处理?

首先我会始终服从领导安排、配合同事工作,以大局为重,不影响项目进度和团队协作,做好本职工作是一切的前提。

我会先从自身找原因,复盘自己的工作交付、沟通方式、协作习惯,排查是否因技术问题、沟通不到位等引发矛盾,针对性地调整和改进。

如果无法定位自身问题,我会找合适的时机,以真诚请教的态度和领导、同事沟通,主动请他们指出我的不足,虚心接受反馈并及时改正。

我会持续保持积极的工作态度,虚心向团队学习,用稳定的工作输出和真诚的协作态度赢得信任,相信通过努力一定能融入团队,形成良好的工作氛围。

当你和领导意见不一致,你会怎么办?

首先,我会确认是否理解了领导的意图,有时候分歧源于信息不对称。如果确实有不同看法,我会找合适的时机私下沟通,用数据和案例说明我的观点,但最终会尊重并执行领导的决定。

尊重服从,主动沟通:首先我会尊重并执行领导的工作安排,同时在合适的时机,以请教的口吻向领导说明我的优化思路,结合技术细节、项目成本、效率提升等维度,分析两种方案的优劣,供领导参考决策。

坚决执行,保障交付:如果领导最终没有采纳我的建议,我会完全按照领导的要求,高质量完成工作,不打折扣、不消极怠工,确保项目进度和交付质量。

坚守原则,底线明确:如果领导的要求违反公司制度、行业规范或技术安全原则,我会明确提出反对意见,说明潜在风险;若领导仍坚持,我会按流程向上级领导反馈,坚守职业底线。

如果工作中因我的失误给公司造成经济损失?

第一时间主动向领导汇报情况,不推诿、不隐瞒,立刻配合团队采取技术补救、流程优化等措施,尽最大努力降低损失、挽回影响,若个人能力不足,主动申请公司资源支持解决。

客观复盘问题根源,明确责任归属。如果是我的责任,我愿意承担相应的处罚,并主动提出整改方案;如果是团队协作中的问题,我会和同事一起复盘,不甩锅、不抱怨,共同梳理问题、弥补漏洞。

深度复盘失误原因,从技术实现、流程规范、测试验证等维度梳理漏洞,形成完整的复盘报告,优化工作方法,完善自查机制,确保同类问题不再发生,同时将经验分享给团队,避免他人踩坑。

动态规划使用

"动态规划适用于最优子结构和重叠子问题的场景。核心思想是用空间换时间,把大问题拆成小问题,并保存子问题的解避免重复计算。

典型例子:斐波那契数列、背包问题、最长公共子序列。实现方式有自顶向下(递归+备忘录) 和自底向上(迭代+DP表) 两种。"

手写示例:斐波那契(自底向上)

cpp 复制代码
int fib(int n) {
    if (n <= 1) return n;
    vector<int> dp(n + 1, 0);
    dp[1] = 1;
    for (int i = 2; i <= n; ++i) {
        dp[i] = dp[i-1] + dp[i-2];
    }
    return dp[n];
}

sizeof 各种类型 & strlen vs sizeof

sizeof 是操作符,编译期计算,返回整个变量/类型占用的字节数。

strlen 是函数,运行期计算,返回字符串的长度(不含'\0')。

关键示例(笔试题常考)

cpp 复制代码
char arr[] = "Hello";     // 大小:6(包含'\0')

char* p = "Hello";        // 指针大小:4(32位)或 8(64位)

sizeof(arr);   // 6

sizeof(p);     // 4 或 8

strlen(arr);   // 5

strlen(p);     // 5

常见类型大小(64位系统)

cpp 复制代码
char: 1

short: 2

int: 4

long: 8(Linux)/ 4(Windows)

long long: 8

float: 4

double: 8

指针: 8

空类: 1(C++标准保证对象地址唯一)

出栈序列(栈的经典题)

题目形式:给定入栈序列(如 1,2,3,4,5),判断某个序列是否为合法出栈序列。

回答思路:

"使用一个辅助栈模拟入栈出栈过程。遍历入栈序列,每次入栈后检查栈顶是否等于出栈序列的当前元素,如果等于就出栈。最后如果栈为空,则说明是合法出栈序列。"

手写代码

cpp 复制代码
#include <stack>
#include <vector>
using namespace std;

bool isValidPopOrder(vector<int>& pushV, vector<int>& popV) {
    // 如果两个序列长度不同,直接返回 false
    if (pushV.size() != popV.size()) return false;
    
    stack<int> s;
    int j = 0; // popV 的索引
    
    for (int val : pushV) {
        s.push(val); // 模拟入栈
        
        // 检查栈顶是否等于当前期望的出栈元素
        while (!s.empty() && j < popV.size() && s.top() == popV[j]) {
            s.pop();
            j++;
        }
    }
    
    // 所有元素都成功出栈,说明合法
    return s.empty();
}

STL容器底层数据结构

|---------------|--------------|-------------------------------------|
| 容器 | 底层数据结构 | 特点 |
| vector | 动态数组 | 连续内存,随机访问O(1),尾部插入快,中间插入/删除慢(需移动元素) |
| deque | 分段连续空间(双端队列) | 双端插入/删除O(1),随机访问O(1)(比vector略慢) |
| map | 红黑树(平衡二叉搜索树) | 元素有序,增删查O(log n),内存开销大 |
| unordered_map | 哈希表 | 元素无序,平均增删查O(1),最坏O(n)(哈希冲突时) |

面试加分句:"unordered_map 的性能取决于哈希函数的质量,如果大量冲突会退化成链表,所以自定义类型作为key时需要提供好的哈希函数。"

哈希冲突解决方法

"解决哈希冲突主要有三种经典方法:

第一种是链地址法。当多个键哈希到同一个桶时,把它们放在一个链表里。unordered_map 采用的就是这种方法。它的优点是实现简单,而且链表可以无限增长(只要内存足够),不会像开放地址法那样出现"表满"的问题。缺点是缓存不友好,因为链表节点在内存中可能不连续,频繁查找时性能会下降。

第二种是开放地址法。冲突时按一定规则找下一个空闲位置,比如线性探测(挨个往后找)、二次探测(按平方步长找)或双哈希(用第二个哈希函数计算步长)。优点是所有数据都存储在数组内部,缓存友好,适合数据量小、负载因子低的场景。缺点是删除操作麻烦,而且负载因子不能太高,否则性能会急剧退化。

第三种是再哈希法。冲突时换一个哈希函数重新计算地址。这种方法实现相对复杂,通常不单独使用,而是作为开放地址法的一个变种来配合使用。

C++的 unordered_map 采用的是链地址法。当哈希冲突严重时,性能会退化到 O(n)。

手写 String 类的四个函数

cpp 复制代码
class MyString {
public:
    // 构造函数
    MyString(const char* str = nullptr) {
        if (str) {
            m_data = new char[strlen(str) + 1];
            strcpy(m_data, str);
        } else {
            m_data = new char[1];
            *m_data = '\0';
        }
    }

    // 拷贝构造函数(深拷贝)
    MyString(const MyString& other) {
        m_data = new char[strlen(other.m_data) + 1];
        strcpy(m_data, other.m_data);
    }

    // 赋值运算符(深拷贝 + 自赋值检查)
    MyString& operator=(const MyString& other) {
        if (this != &other) {  // 自赋值检查
            delete[] m_data;
            m_data = new char[strlen(other.m_data) + 1];
            strcpy(m_data, other.m_data);
        }
        return *this;
    }

    // 析构函数
    ~MyString() {
        delete[] m_data;
    }

private:
    char* m_data;
};

面试官关注点深拷贝 vs 浅拷贝自赋值检查内存泄漏 。一定要写 delete[]strcpy

常用智能指针及应用场景

智能指针 特点 应用场景
unique_ptr 独占所有权,不能拷贝,只能移动 明确唯一所有权的资源(如工厂模式返回的对象)
shared_ptr 共享所有权,引用计数 多个对象共享资源(如多个线程共享数据)
weak_ptr 弱引用,不增加引用计数 解决 shared_ptr 循环引用(如观察者模式)

回答加分句

"shared_ptr 的引用计数是线程安全的 ,但指向的对象不是 ,所以多线程操作对象本身仍需加锁。weak_ptr 常用于缓存和观察者模式,使用前需调用 lock() 获取有效 shared_ptr。"

多线程循环打印奇偶数

cpp 复制代码
#include <iostream>
#include <thread>
#include <mutex>
#include <condition_variable>
using namespace std;

mutex mtx;
condition_variable cv;
int num = 1;
const int MAX = 100;

void printOdd() {
    while (num <= MAX) {
        unique_lock<mutex> lock(mtx);
        cv.wait(lock, []{ return num % 2 == 1 || num > MAX; });
        if (num > MAX) break;
        cout << "Odd: " << num << endl;
        num++;
        cv.notify_all();
    }
}

void printEven() {
    while (num <= MAX) {
        unique_lock<mutex> lock(mtx);
        cv.wait(lock, []{ return num % 2 == 0 || num > MAX; });
        if (num > MAX) break;
        cout << "Even: " << num << endl;
        num++;
        cv.notify_all();
    }
}

int main() {
    thread t1(printOdd);
    thread t2(printEven);
    t1.join();
    t2.join();
    return 0;
}
cpp 复制代码
/**
 * ============================================================
 * 文件名    :OddEvenPrint.cpp
 * 功能描述  :使用互斥锁和条件变量实现两个线程交替打印奇偶数
 * 核心考点  :多线程同步、条件变量、unique_lock、虚假唤醒
 * 技术亮点  :使用条件变量避免忙等,实现线程间精准通知
 * ============================================================
 */

#include <iostream>  // 标准输入输出流库,提供 cout、endl
#include <thread>    // 线程库,提供 std::thread(C++11)
#include <mutex>     // 互斥锁库,提供 std::mutex、std::unique_lock(C++11)
#include <condition_variable>  // 条件变量库,提供 std::condition_variable(C++11)
using namespace std; // 使用标准命名空间

// ============================================================
// 1. 全局共享资源
// ============================================================

/**
 * 【互斥锁】保护共享资源 num
 * 作用:确保同一时刻只有一个线程能访问 num
 * 面试点:互斥锁是保证线程安全的基础
 */
mutex mtx;

/**
 * 【条件变量】线程间同步
 * 作用:让线程在条件不满足时休眠,条件满足时被唤醒
 * 面试点:条件变量用于"等待-通知"模式,比轮询更高效
 */
condition_variable cv;

/**
 * 【共享变量】当前要打印的数字
 * 两个线程共享这个变量,必须用互斥锁保护
 * 初始值为 1
 */
int num = 1;

/**
 * 【打印上限】常量
 * const 修饰,编译期确定,不可修改
 * 两个线程打印到 100 为止
 */
const int MAX = 100;

// ============================================================
// 2. 打印奇数线程函数
// ============================================================

/**
 * 【线程函数1】打印奇数
 * 
 * 执行流程:
 *   1. 获取锁(unique_lock 自动管理)
 *   2. 检查条件:当前数字是否为奇数 或 是否超过上限
 *   3. 如果条件不满足,线程休眠(wait 自动释放锁)
 *   4. 被唤醒后重新检查条件
 *   5. 打印当前数字,num++,通知另一个线程
 * 
 * 面试点1:为什么用 unique_lock 而不是 lock_guard?
 *   答:unique_lock 可以手动解锁/加锁,配合条件变量使用
 *       lock_guard 不能主动释放锁,不适用于条件变量
 * 
 * 面试点2:wait 的第二个参数(lambda)的作用?
 *   答:防止"虚假唤醒"(spurious wakeup)
 *       线程可能在没有被通知的情况下醒来
 *       必须再次检查条件,满足才继续执行
 * 
 * 面试点3:为什么退出前要 break?
 *   答:防止 num > MAX 时仍然执行打印和 num++ 操作
 *       避免越界或错误输出
 */
void printOdd() {
    // 循环直到 num 超过 MAX
    while (num <= MAX) {
        // 【加锁】创建 unique_lock 对象,自动加锁
        // 作用域:整个 while 循环体
        // 面试点:unique_lock 在析构时自动解锁(RAII)
        unique_lock<mutex> lock(mtx);
        
        /**
         * 【等待条件】条件变量等待
         * 
         * 执行过程:
         *   1. 检查 lambda 表达式的结果
         *   2. 如果为 true,继续执行(不阻塞)
         *   3. 如果为 false,释放锁并进入等待状态
         *   4. 被 notify 唤醒后,重新获取锁
         *   5. 再次检查 lambda 表达式
         *   6. 如果为 true,继续执行;否则继续等待
         * 
         * 条件:num % 2 == 1(奇数)或 num > MAX(退出条件)
         * 这样设计保证了只有奇数线程在轮到奇数时才能执行
         */
        cv.wait(lock, []{ return num % 2 == 1 || num > MAX; });
        
        /**
         * 【退出检查】防止越界
         * 如果 num > MAX,说明已经打印完所有数字
         * break 退出循环,线程结束
         */
        if (num > MAX) break;
        
        /**
         * 【打印奇数】
         * 此时条件满足:num 是奇数且未超过 MAX
         * 在锁保护下安全地访问 num
         */
        cout << "Odd: " << num << endl;
        
        /**
         * 【更新数字】
         * num++ 让下一个数字变为偶数
         * 在锁保护下安全地修改共享变量
         */
        num++;
        
        /**
         * 【通知另一个线程】
         * notify_all() 唤醒所有等待的线程
         * 因为只有偶数线程在等待,所以实际上唤醒的是偶数线程
         * 
         * 面试点:为什么用 notify_all 而不是 notify_one?
         *   答:虽然这里只用 notify_one 也可以
         *       但 notify_all 更安全,避免某些情况下线程丢失通知
         *       性能略有损失,但更健壮
         */
        cv.notify_all();
        
        /**
         * 【自动解锁】
         * 离开作用域时,lock 对象的析构函数自动调用 unlock()
         * 这就是 RAII 的好处:不会忘记解锁,异常安全
         */
    }
}

// ============================================================
// 3. 打印偶数线程函数
// ============================================================

/**
 * 【线程函数2】打印偶数
 * 
 * 原理与 printOdd 相同,区别在于:
 *   1. 等待条件变为:num % 2 == 0(偶数)
 *   2. 打印时输出 "Even: "
 * 
 * 面试点:两个线程共享同一个锁和条件变量
 *   通过不同的条件值(奇/偶)实现交替执行
 *   这是"生产者-消费者"模式的变体
 */
void printEven() {
    while (num <= MAX) {
        // 加锁
        unique_lock<mutex> lock(mtx);
        
        /**
         * 等待条件:num 是偶数 或 超过上限
         * 
         * 注意:lambda 中捕获了 num 和 MAX
         * 因为是引用捕获(默认),所以能读到最新的值
         */
        cv.wait(lock, []{ return num % 2 == 0 || num > MAX; });
        
        // 退出检查
        if (num > MAX) break;
        
        // 打印偶数
        cout << "Even: " << num << endl;
        
        // 更新数字(变为奇数)
        num++;
        
        // 通知另一个线程(奇数线程)
        cv.notify_all();
    }
}

// ============================================================
// 4. 主函数
// ============================================================

/**
 * 【主函数】创建并启动两个线程
 * 
 * 执行流程:
 *   1. 创建线程 t1,执行 printOdd
 *   2. 创建线程 t2,执行 printEven
 *   3. 等待 t1 执行完毕(join)
 *   4. 等待 t2 执行完毕(join)
 *   5. 程序结束
 * 
 * 输出结果:
 *   Odd: 1
 *   Even: 2
 *   Odd: 3
 *   Even: 4
 *   ...
 *   Odd: 99
 *   Even: 100
 * 
 * 面试点1:为什么用 join 而不是 detach?
 *   答:join 确保主线程等待子线程完成
 *       detach 会让子线程在后台运行,可能造成资源泄漏
 * 
 * 面试点2:线程创建的顺序重要吗?
 *   答:不重要,因为条件变量保证了正确的执行顺序
 *       即使两个线程同时启动,也会自动交替执行
 */
int main() {
    // 【创建线程】创建两个线程,分别执行 printOdd 和 printEven
    // thread 对象创建后立即启动线程
    thread t1(printOdd);   // 线程1:打印奇数
    thread t2(printEven);  // 线程2:打印偶数
    
    /**
     * 【等待线程结束】join
     * 
     * 作用:阻塞主线程,直到子线程执行完毕
     * 
     * 面试点:join 和 detach 的区别
     *   - join:等待线程结束,回收资源(推荐)
     *   - detach:分离线程,让其独立运行(危险)
     */
    t1.join();  // 等待奇数线程结束
    t2.join();  // 等待偶数线程结束
    
    /**
     * 程序退出
     * 此时两个线程都已结束,资源已释放
     */
    return 0;
}

面试官关注点条件变量的使用wait 的第二个参数(防止虚假唤醒)锁的作用域

互斥量、信号量、条件变量的区别
同步机制 核心作用 特点
互斥量(Mutex) 保护临界区,保证同一时刻只有一个线程访问共享资源 只能加锁/解锁,不能跨线程传递信号
信号量(Semaphore) 控制资源数量,允许多个线程同时访问有限资源 有计数功能,可跨线程释放
条件变量(Condition Variable) 线程间等待/通知,用于复杂同步(如生产者-消费者) 总是配合互斥量使用,可精准唤醒

面试加分句

"互斥量是锁机制 ,信号量和条件变量是同步机制 。条件变量可以让线程在条件不满足时阻塞等待,避免忙等,比单纯用互斥量更高效。"

层序遍历二叉树(BFS)

cpp 复制代码
#include <queue>
using namespace std;

struct TreeNode {
    int val;
    TreeNode* left;
    TreeNode* right;
    TreeNode(int x) : val(x), left(nullptr), right(nullptr) {}
};

vector<vector<int>> levelOrder(TreeNode* root) {
    vector<vector<int>> res;
    if (!root) return res;
    
    queue<TreeNode*> q;
    q.push(root);
    
    while (!q.empty()) {
        int size = q.size();
        vector<int> level;
        for (int i = 0; i < size; ++i) {
            TreeNode* node = q.front(); q.pop();
            level.push_back(node->val);
            if (node->left) q.push(node->left);
            if (node->right) q.push(node->right);
        }
        res.push_back(level);
    }
    return res;
}

面试官关注点队列的使用如何区分每一层(记录每层大小)。

反问环节(3个安全问题)

  1. 岗位相关:"这个岗位主要负责哪块业务?技术栈主要是C++和Qt吗?"

  2. 成长相关:"公司对新人的培养体系是什么样的?入职后会有导师带吗?"

  3. 后续流程:"请问后续还有几轮面试?大概什么时候会有结果?"

  4. 如果我有幸入职,我需要做些什么准备吗,比如学一下工作中设计到的内容

智能指针,shared_ptr是怎么实现共享计数的

通过引用计数(Reference Counting) 实现,shared_ptr 通过在堆上维护一个共享的引用计数对象 来实现共享所有权。这个计数对象记录了当前有多少个 shared_ptr 指向同一个资源。

核心原理:控制块(Control Block)

内存布局

当你创建一个 shared_ptr 时,实际上会分配两块内存

cpp 复制代码
┌─────────────────────────────┐      ┌─────────────────────────────────┐
│     shared_ptr 对象(栈)    │      │      控制块(Control Block)    │
├─────────────────────────────┤      │          (堆内存)              │
│  - 原始指针(指向资源)      │─────>│  ├─ 引用计数(strong count)    │
│  - 控制块指针(指向控制块)  │──┐   │  ├─ 弱引用计数(weak count)    │
└─────────────────────────────┘  │   │  ├─ 删除器(deleter)           │
                                 │   │  ├─ 分配器(allocator)          │
                                 │   └──┴─ 原始资源指针(可选)        │
                                 │                                    │
                                 └──> ┌─────────────────────────────┐
                                      │      原始资源对象(堆内存)   │
                                      │      (如 new int(42))      │
                                      └─────────────────────────────┘

关键机制详解

  1. 引用计数的生命周期
操作 引用计数变化 说明
构造函数(从裸指针创建) strong_count = 1 创建一个新的控制块
拷贝构造 strong_count++ shared_ptr 共享同一控制块
赋值 左侧 strong_count--(可能销毁),右侧 strong_count++ 先释放旧资源,再共享新资源
析构 strong_count-- 如果减到 0,删除资源和控制块
移动构造 不改变引用计数 所有权转移,源 shared_ptr 置空

"shared_ptr 是怎么实现共享计数的?"

"shared_ptr 通过在堆上维护一个独立的控制块 来实现引用计数。控制块中存储了强引用计数弱引用计数

当创建一个 shared_ptr 时,会分配一个控制块,引用计数初始化为 1。拷贝构造时,复制指针和控制块指针 ,并将引用计数加 1。析构时,引用计数减 1,当减到 0 时,会删除所管理的资源和控制块。

引用计数的增减是线程安全 的,通过原子操作实现。但 shared_ptr 管理的对象本身不是线程安全的,需要额外保护。

需要注意循环引用 的问题,两个 shared_ptr 互相引用会导致引用计数永远不会归零,造成内存泄漏。解决方法是使用 weak_ptr 来打破循环。"

找到最长的连续子序列

题目理解

"最长连续子序列"一般有两种理解,面试时如果不确定,先问清楚

类型 含义 示例 [100,4,200,1,3,2] 答案
连续递增子序列(子数组) 在原数组中连续的一段 [1,2,3][3,4] 3([1,2,3]
最长连续序列(非连续,排序后连续) 元素可以不连续,但排序后连续 [1,2,3,4] 4

面试时先说:"请问是最长连续递增子数组,还是排序后最长的连续数字序列?"------这样会显得你很严谨。

我下面给出最长连续递增子序列的解法,因为这道题更常考,而且更能体现编码功底。

思路

贪心 :用一个 maxLen 记录最长长度,curLen 记录当前连续递增的长度。遍历数组,如果当前元素大于前一个元素,curLen++;否则重置 curLen = 1,并更新 maxLen

手写代码

cpp 复制代码
int findLengthOfLCIS(vector<int>& nums) {
    if (nums.empty()) return 0;
    int maxLen = 1;
    int curLen = 1;
    for (int i = 1; i < nums.size(); ++i) {
        if (nums[i] > nums[i - 1]) {
            curLen++;
        } else {
            maxLen = max(maxLen, curLen);
            curLen = 1;
        }
    }
    return max(maxLen, curLen);
}

面试官:"请你找出数组中最长的连续递增子序列的长度。"

回答

"我用贪心法解决。用一个变量记录当前递增序列的长度,另一个变量记录全局最长。遍历数组,如果当前元素大于前一个元素,当前长度加 1;否则,更新全局最长,并重置当前长度为 1。最后返回全局最长即可。

时间复杂度是 O(n) ,空间复杂度 O(1),这是最优解法。"

如果面试官追问:"如果是无序、不连续的最长连续序列怎么做?"

回答

"那需要用哈希集合。把所有元素存入 unordered_set,然后遍历集合,对每个元素,如果它没有前驱(num-1 不存在),就从这个元素开始向后计数,找到最长的连续序列长度。时间复杂度也是 O(n)。"

二叉树的层序遍历

这道题考的是 BFS(广度优先搜索) ,面试官关注的是能不能按层区分输出。

手写代码

cpp 复制代码
#include <queue>
using namespace std;

struct TreeNode {
    int val;
    TreeNode* left;
    TreeNode* right;
    TreeNode(int x) : val(x), left(nullptr), right(nullptr) {}
};

vector<vector<int>> levelOrder(TreeNode* root) {
    vector<vector<int>> res;
    if (!root) return res;
    
    queue<TreeNode*> q;
    q.push(root);
    
    while (!q.empty()) {
        int size = q.size();  // 关键:当前层的节点数
        vector<int> level;
        for (int i = 0; i < size; ++i) {
            TreeNode* node = q.front();
            q.pop();
            level.push_back(node->val);
            if (node->left) q.push(node->left);
            if (node->right) q.push(node->right);
        }
        res.push_back(level);
    }
    return res;
}

面试官:"请层序遍历二叉树。"

回答

"我用广度优先搜索来做。使用一个队列,先把根节点入队。然后循环,每次处理队列时,先记录当前队列的大小,这个大小就是当前层的节点数。然后循环这个次数,逐个弹出节点,把它的值放到当前层的 vector 中,再将其左右子节点入队。这样一层处理完,就把当前层的 vector 放到结果中。

时间复杂度是 O(n) ,每个节点访问一次。空间复杂度最坏情况下是 O(n)(满二叉树时队列中最多存 n/2 个节点)。"

如果面试官追问:"如果要求从底层向上层序遍历呢?"

回答

"层序遍历完成后,把结果 vector 反转一下就行。或者用栈配合层序遍历来做,但反转更简单。"

项目介绍

回答结构(STAR法则)

"我介绍一个智能门禁系统的项目。

项目背景:这是一个基于Qt和OpenCV的跨平台门禁系统,解决小区/办公室的访客管理和门禁授权问题。

我的职责 :我独立完成了人脸识别模块数据库模块

技术实现:人脸识别用OpenCV的Haar Cascade做检测,然后通过灰度化、缩放到32x32、归一化提取特征向量,匹配时用余弦相似度。数据库用SQLite,我用Repository模式封装了数据访问。

重难点:性能优化方面,我把视频处理放到子线程避免卡界面;跨平台部署时,我用CMake自动拷贝OpenCV动态库解决依赖问题。

项目成果:识别准确率90%以上,所有事件自动记录到数据库。"

项目提问

(1)线程池的设计与实现

回答思路

"线程池的核心是任务队列 + 工作线程。多个线程从任务队列中取任务执行,避免频繁创建销毁线程。

具体设计:一个线程安全的任务队列 (用互斥锁+条件变量保护),一个工作线程列表 (每个线程循环取任务执行)。任务队列用std::queue存储任务对象(通常是std::function),互斥锁保护队列操作,条件变量用于通知线程有新任务。

关键点:线程安全 (锁+条件变量)、任务调度 (任务队列)、优雅停止(停止标志+条件变量唤醒)。"

手写简化版代码

cpp 复制代码
class ThreadPool {
public:
    ThreadPool(size_t numThreads) {
        for (size_t i = 0; i < numThreads; ++i) {
            workers.emplace_back([this] {
                while (true) {
                    std::function<void()> task;
                    {
                        std::unique_lock<std::mutex> lock(queueMutex);
                        condition.wait(lock, [this] { return stop || !tasks.empty(); });
                        if (stop && tasks.empty()) return;
                        task = std::move(tasks.front());
                        tasks.pop();
                    }
                    task();
                }
            });
        }
    }
    ~ThreadPool() {
        {
            std::unique_lock<std::mutex> lock(queueMutex);
            stop = true;
        }
        condition.notify_all();
        for (auto& worker : workers) {
            worker.join();
        }
    }
    template<typename F>
    void enqueue(F&& f) {
        {
            std::unique_lock<std::mutex> lock(queueMutex);
            tasks.emplace(std::forward<F>(f));
        }
        condition.notify_one();
    }
private:
    std::vector<std::thread> workers;
    std::queue<std::function<void()>> tasks;
    std::mutex queueMutex;
    std::condition_variable condition;
    bool stop = false;
};
(2)TCP特点、三握四挥、粘包半包

回答思路

  • TCP特点:面向连接、可靠传输、面向字节流、全双工。

  • 三次握手:SYN → SYN+ACK → ACK,建立连接。

  • 四次挥手:FIN → ACK → FIN → ACK,断开连接。

  • 粘包/半包 :TCP是字节流协议,数据无边界。粘包 指多个小包合并一次接收;半包 指一个完整数据包被分多次接收。解决方式:固定长度特定分隔符包头+包体(长度字段)。

(3)Socket编程基础流程

回答思路

  • 服务端socket()bind()listen()accept()(阻塞等待连接)→ recv()/send()close()

  • 客户端socket()connect()recv()/send()close()

套接字数量

  • 一个服务端监听一个端口,listen() 后有一个监听套接字

  • 每个客户端连接成功后,accept() 会返回一个新的套接字用于与该客户端通信。

  • 1个服务端 + 3个客户端:服务端有 1个监听套接字 + 3个通信套接字,共4个。

  • 这些套接字的端口号不同 (每个通信套接字会被分配一个临时端口),但IP地址可以相同

libevent建立HTTP连接

  • 基于事件驱动,event_base_new() 创建事件循环,evhttp_new() 创建HTTP服务,设置回调函数,最后进入事件循环。非阻塞、异步。

多线程同步方法

回答思路

  • 互斥锁:保护临界区,同一时刻只允许一个线程访问共享资源。

  • 读写锁读共享、写独占。读多写少时用读写锁,性能比互斥锁高。写多时用互斥锁。

  • 条件变量:让线程在条件不满足时休眠,满足时被唤醒。

  • 信号量:控制同时访问共享资源的线程数量。

  • 原子变量:适用于简单计数场景,比锁更轻量,无上下文切换开销。


Linux命令

命令 用途
top / htop 查看CPU占用率、内存使用、进程列表
ls -l 查看文件权限
chmod 修改文件权限
ps aux 查看进程状态

深拷贝 vs 浅拷贝

回答思路

  • 浅拷贝 :复制指针值,多个对象指向同一块内存。危害:析构时重复释放(double free),导致程序崩溃。

  • 深拷贝:分配新内存,复制数据。每个对象拥有独立资源。

  • 不需要手写拷贝构造的情况 :成员变量都是可拷贝的 (如intstring),且不管理资源,编译器生成的默认拷贝构造即可。典型场景 :成员变量是智能指针(shared_ptr),默认拷贝构造会正确增加引用计数。

STL底层实现

容器 底层数据结构 特点
list 双向链表 插入/删除O(1),随机访问O(n)
vector 动态数组 随机访问O(1),尾部插入快,中间插入/删除慢
map 红黑树 有序,增删查O(log n)
unordered_map 哈希表 无序,平均增删查O(1),最坏O(n)

函数重构(降低耦合度)

问题 :一个函数根据参数类型执行不同分支,if a 执行A逻辑,if b 执行B逻辑。

回答思路

  • 方案一:策略模式。定义接口,不同逻辑封装成不同类,通过多态调用。

  • 方案二:工厂模式 + 函数映射 。将逻辑与类型解耦,用 std::function + unordered_map 存储类型与处理函数的映射。

  • 方案三:模板特化。利用C++模板特化,针对不同类型提供不同实现。

手写示例(策略模式)

cpp 复制代码
函数重构(降低耦合度)
问题:一个函数根据参数类型执行不同分支,if a 执行A逻辑,if b 执行B逻辑。

回答思路:

方案一:策略模式。定义接口,不同逻辑封装成不同类,通过多态调用。

方案二:工厂模式 + 函数映射。将逻辑与类型解耦,用 std::function + unordered_map 存储类型与处理函数的映射。

方案三:模板特化。利用C++模板特化,针对不同类型提供不同实现。

手写示例(策略模式):

const成员函数和普通成员函数可以重载吗?为什么?

回答思路

可以重载const 成员函数可以作用于 const 对象,普通成员函数作用于非 const 对象。当对象是 const 时,只能调用 const 版本;非 const 时优先调用非 const 版本。这是C++编译器根据**this指针类型** 进行重载决议(const Class* vs Class*)。

shared_ptr 线程安全

回答思路

  • 引用计数是线程安全的:控制块的引用计数通过原子操作递增/递减。

  • 资源本身不是线程安全的 :多线程同时修改 shared_ptr 指向的对象,需要额外加锁。

  • 多个 shared_ptr 对象的读写安全 :如果多个线程操作同一个 shared_ptr 对象(如一个线程修改其指向,另一个线程读取),需要加锁保护。但每个线程操作自己的 shared_ptr 副本是安全的。

如果面试官总结不足,可以这样回应:

"非常感谢您的反馈,这些建议对我来说非常有价值。我会在接下来的学习中重点加强提到的不足,比如多读一些开源代码来弥补项目经验不足,或者针对您提到的某个技术点深入学习,尽快补齐短板。谢谢您的总结。"

C++继承、虚继承

考察重点:菱形继承问题、虚继承的内存布局、构造/析构顺序。

面试回答思路

"C++支持多继承,但会带来菱形继承 问题,即派生类通过多条路径继承同一个基类,导致基类成员被重复继承。虚继承 解决了这个问题,通过共享基类子对象,保证派生类中只有一份基类成员。虚继承的实现通常通过虚基类指针虚基类表,增加了间接访问的开销。"

  • 内存管理

    • 堆与栈的区别 :栈由编译器自动管理,速度快但空间小;堆由程序员手动管理(new/delete),速度慢但空间大,需要防止内存泄漏。

    • new/deletemalloc/freenew 会调用构造函数,delete 会调用析构函数,它们是运算符malloc/free函数,只分配/释放内存,不调用构造/析构。

判断MAC地址是否规范

解题思路 :MAC地址格式为 XX-XX-XX-XX-XX-XX,每组两位十六进制数(0-9, A-F)。

cpp 复制代码
#include <regex>
bool isValidMAC(const string& mac) {
    regex pattern("^([0-9A-F]{2}-){5}[0-9A-F]{2}$");
    return regex_match(mac, pattern);
}

面试回答

"我使用正则表达式来匹配格式。^([0-9A-F]{2}-){5}[0-9A-F]{2}$ 匹配六组两位十六进制数,用连字符分隔。如果没有正则库,可以遍历字符串,逐字符判断并检查连字符位置。"

(2) 合并两个有序链表

解题思路 :使用迭代递归,比较两个链表头节点,取较小值。

cpp 复制代码
ListNode* mergeTwoLists(ListNode* l1, ListNode* l2) {
    ListNode dummy(0);
    ListNode* tail = &dummy;
    while (l1 && l2) {
        if (l1->val < l2->val) {
            tail->next = l1;
            l1 = l1->next;
        } else {
            tail->next = l2;
            l2 = l2->next;
        }
        tail = tail->next;
    }
    tail->next = l1 ? l1 : l2;
    return dummy.next;
}
(3) 二叉树后序遍历(非递归)

解题思路 :使用两个栈一个栈 + 标记。后序遍历顺序为:左 -> 右 -> 根。

cpp 复制代码
vector<string> postorder(Node* root) {
    vector<string> res;
    if (!root) return res;
    stack<Node*> s1, s2;
    s1.push(root);
    while (!s1.empty()) {
        Node* node = s1.top(); s1.pop();
        s2.push(node);
        if (node->left) s1.push(node->left);
        if (node->right) s1.push(node->right);
    }
    while (!s2.empty()) {
        res.push_back(s2.top()->val);
        s2.pop();
    }
    return res;
}

快速排序

cpp 复制代码
void quickSort(vector<int>& arr, int left, int right) {
    if (left >= right) return;
    int pivot = arr[left];
    int i = left, j = right;
    while (i < j) {
        while (i < j && arr[j] >= pivot) j--;
        while (i < j && arr[i] <= pivot) i++;
        if (i < j) swap(arr[i], arr[j]);
    }
    swap(arr[left], arr[i]);
    quickSort(arr, left, i - 1);
    quickSort(arr, i + 1, right);
}

最大公因数和最小公倍数

cpp 复制代码
int gcd(int a, int b) {
    return b == 0 ? a : gcd(b, a % b);
}
int lcm(int a, int b) {
    return a / gcd(a, b) * b;
}
手写String类
继承相关知识点
概念 回答要点
多态 通过继承、虚函数、父类指针/引用指向子类对象,实现运行时动态绑定。
虚继承 解决菱形继承中基类成员重复的问题,通过虚基类指针共享基类子对象。
抽象类 含有纯虚函数的类,不能实例化,用于定义接口。
指针和引用的区别
区别 指针 引用
必须初始化
可重新绑定
可为空
算术运算 支持 不支持
智能指针

回答

"C++11提供了三种智能指针:unique_ptr(独占所有权)、shared_ptr(共享所有权,引用计数)、weak_ptr(弱引用,打破循环引用)。它们通过RAII自动管理资源,避免内存泄漏。"

数学问题
  • 点到直线距离 :公式为 d = |Ax0 + By0 + C| / sqrt(A^2 + B^2)

  • 特征值 :矩阵特征值用于分析线性变换性质,求解 det(A - λI) = 0

  • 导数/积分/偏导数:导数是函数变化率,积分是面积累积,偏导数是多元函数对单个变量的导数。

手写 String 类
cpp 复制代码
class MyString {
public:
    // 1. 构造函数
    MyString(const char* str = nullptr) {
        if (str) {
            m_data = new char[strlen(str) + 1];
            strcpy(m_data, str);
        } else {
            m_data = new char[1];
            *m_data = '\0';
        }
    }

    // 2. 拷贝构造函数(深拷贝)
    MyString(const MyString& other) {
        m_data = new char[strlen(other.m_data) + 1];
        strcpy(m_data, other.m_data);
    }

    // 3. 赋值运算符(深拷贝 + 自赋值检查)
    MyString& operator=(const MyString& other) {
        if (this != &other) {  // 自赋值检查
            delete[] m_data;
            m_data = new char[strlen(other.m_data) + 1];
            strcpy(m_data, other.m_data);
        }
        return *this;
    }

    // 4. 析构函数
    ~MyString() {
        delete[] m_data;
    }

private:
    char* m_data;
};

面试回答话术

"我实现了 String 类的四个核心函数。构造函数负责初始化,拷贝构造和赋值运算符都实现了深拷贝 ,确保每个对象拥有独立的内存,并加入了自赋值检查 防止意外释放。析构函数使用 delete[] 释放资源,避免内存泄漏。"

虚函数和纯虚函数

对比维度 虚函数(virtual) 纯虚函数(pure virtual)
声明 virtual void func(); virtual void func() = 0;
是否有实现 ✅ 必须有 ❌ 通常没有(但可以给实现)
类能否实例化 ✅ 可以 不能,该类成为抽象类
子类是否必须重写 ❌ 不必须 必须重写,否则子类也是抽象类

面试回答话术

"虚函数用于实现多态,子类可以重写它。纯虚函数则强制子类必须重写,含有纯虚函数的类叫抽象类,不能实例化,它主要用于定义接口规范。"

sizeof() 各类型大小

重点注意:指针大小在 32 位系统是 4 字节,在 64 位系统是 8 字节。

类型 大小(64位系统)
char 1
short 2
int 4
float 4
double 8
指针(int*) 8
指针(char*) 8
空类 1(C++标准保证对象地址唯一)

vector 原理

面试回答话术

"vector 底层是一个动态数组 ,在堆上连续分配内存。它维护三个核心指针:_start(首元素)、_finish(尾元素)、_end_of_storage(容量末尾)。

扩容机制 :当 size 达到 capacity 时,会重新分配一块更大的内存(通常是当前容量的 2 倍 ,VS 中是 1.5 倍),将旧元素拷贝或移动到新空间,然后释放旧内存。这是 O(n) 操作,但通过预分配(reserve)可以减少扩容次数。"

死锁怎么解决

面试回答话术

"死锁产生的四个必要条件:互斥、持有并等待、非抢占、循环等待

解决死锁的策略:

  1. 预防 :破坏四个条件之一,比如规定所有线程以固定顺序获取锁,破坏循环等待。

  2. 避免:使用银行家算法(实际操作较少)。

  3. 检测与恢复:允许死锁发生,但检测到后回滚或强制释放资源。

实际开发中,常用 RAII(如 lock_guard 自动管理锁,避免忘记解锁。使用 std::lock 可以一次性锁定多个互斥量,避免死锁。"

提高程序性能

1. 算法优化

  • 选择合适的数据结构 :如频繁查找用 unordered_map,有序遍历用 map

  • 使用移动语义(C++11):避免不必要的拷贝。

2. 内存管理

  • 对象池/内存池 :减少频繁 new/delete 的开销。

  • reserve 预分配 :减少 vector 扩容次数。

3. 设计模式

  • 单例模式:避免重复创建全局对象。

  • 对象池模式:重用对象,减少创建销毁开销。

4. 编译优化

  • 开启编译器优化(如 -O2)。

指针和引用的区别

区别 指针 引用
必须初始化 ❌ 否 必须初始化
是否可重绑定 ✅ 可以改变指向 ❌ 不可变,始终引用同一对象
是否可为空 ✅ 可以为 nullptr ❌ 不可以,必须引用有效对象
算术运算 支持(如 p++ 不支持
内存占用 占用内存(存储地址) 不占用(是别名)

引用的用法举例

cpp 复制代码
// 1. 函数参数(避免拷贝,可修改实参)
void swap(int& a, int& b) { int tmp = a; a = b; b = tmp; }

// 2. 函数返回值(支持链式调用)
MyString& operator=(const MyString& other) { return *this; }

// 3. 范围 for 循环(避免拷贝)
for (const auto& item : vec) { /* ... */ }
  • 内存管理

    • 堆与栈的区别 :栈由编译器自动管理,速度快但空间小;堆由程序员手动管理(new/delete),速度慢但空间大,需要防止内存泄漏。

    • new/deletemalloc/freenew 会调用构造函数,delete 会调用析构函数,它们是运算符malloc/free函数,只分配/释放内存,不调用构造/析构。

  • STL容器了解

    • 高频考点vector(动态数组,连续内存,扩容机制)、list(双向链表,插入删除快)、map(红黑树,有序)、unordered_map(哈希表,无序,平均O(1))。重点准备它们底层数据结构、时间复杂度、使用场景的对比。
  • 智能指针 (C++11)

    • unique_ptr:独占所有权,不能拷贝只能移动。

    • shared_ptr :共享所有权,引用计数 ,线程安全(但指向的对象不保证)。注意循环引用 问题,用weak_ptr解决。

    • weak_ptr:弱引用,不增加引用计数,用于观察者模式或打破循环。

Linux命令与操作系统

这部分考察你的工程实践环境多线程编程的理解。

  • Linux命令

    • 查看CPU占用率tophtop(更友好的交互式工具)。

    • 文件权限ls -l 查看权限,chmod 修改权限(如 chmod +x script.sh)。

  • 多线程与同步

    • 数据共享 :多线程访问共享数据时,需要同步机制来防止竞态条件

    • 互斥锁 (Mutex) vs 读写锁 (Read-Write Lock)

      • 互斥锁独占,同一时刻只允许一个线程访问共享资源,适用于写操作多或读写操作都频繁的场景。

      • 读写锁读共享、写独占 ,适用于读操作远多于写操作的场景,能提高并发性。

反问环节与面试总结

这是面试的收尾环节,也是你展示思考深度的机会。

  • 反问问题(安全且加分)

    1. "这个岗位的核心业务方向是什么?技术栈主要是C++和Qt吗?"

    2. "公司对新员工的培养体系是什么样的?入职后会有导师吗?"

    3. "请问后续还有几轮面试?大概什么时候会有结果?"

  • 应对面试总结 :如果面试官指出不足(如项目经验简单、某知识点不熟),应积极回应:"非常感谢您的反馈,这对我很有帮助。我接下来会重点加强XX方面的学习。"

结构体和类的区别

回答

"在C++中,结构体和类的唯一区别 是默认访问权限:struct 默认是 publicclass 默认是 private。除此之外,它们的功能完全一样,都可以有成员函数、构造函数、继承和多态。"

结构体与类的继承组合

回答

"在C++中,所有组合都是允许的 :结构体可以继承类,类也可以继承结构体,结构体可以继承结构体,类可以继承类。C++只关心访问控制,不区分 structclass 的语义。关键点在于继承方式public/private/protected),而不是关键字本身。"

单继承下的构造与析构顺序

回答

"构造顺序 :先构造基类,再构造派生类。在派生类中,按照成员变量在类中声明的顺序初始化。

析构顺序 :和构造顺序完全相反。先执行派生类自己的析构函数,然后成员变量逆序析构,最后调用基类析构函数。

简单记:先父后子(构造),先子后父(析构)。"

纯虚函数的写法

回答

"在函数声明后面加上 = 0 即可,例如 virtual void func() = 0;。含有纯虚函数的类被称为抽象类 ,不能实例化,它作为接口规范,强制要求子类必须实现该函数。"

多个实例间的数据共享

回答

"使用 static 成员变量 。因为 static 成员变量属于类本身 ,而不是属于某个对象,所有实例共享同一份数据。需要注意的是,static 成员变量必须在类外单独定义并初始化。"

希尔排序及其稳定性

回答

"希尔排序是插入排序的改进版 ,通过设置递减的步长(gap)将序列分组,在组内进行插入排序,随着步长减小到1,序列基本有序,最后再进行一次完整的插入排序。时间复杂度平均为 O(n log n) ,最坏为 O(n²)

稳定性 指排序后相等元素的相对顺序不变 。希尔排序是不稳定的,因为按步长分组时,相等的元素可能被分到不同组,从而改变它们的相对顺序。"

手写 String 类
cpp 复制代码
class MyString {
public:
    // 构造函数
    MyString(const char* str = nullptr) {
        if (str) {
            m_data = new char[strlen(str) + 1];
            strcpy(m_data, str);
        } else {
            m_data = new char[1];
            *m_data = '\0';
        }
    }

    // 拷贝构造函数(深拷贝)
    MyString(const MyString& other) {
        m_data = new char[strlen(other.m_data) + 1];
        strcpy(m_data, other.m_data);
    }

    // 析构函数
    ~MyString() {
        delete[] m_data;
    }

private:
    char* m_data;
};

回答话术

"构造函数负责初始化,拷贝构造实现了深拷贝 ,确保每个对象拥有独立的内存。如果不写深拷贝,编译器默认的浅拷贝会导致多个对象指向同一块内存,析构时会出现 double free 错误。"

sizeof 的计算

面试回答话术

"这题考察的是数组作为函数参数时的退化问题

cpp 复制代码
void func(char str[100]) {
    sizeof(str); // 这里 sizeof(str) = 8(64位系统)或 4(32位系统)
}

原因 :当数组作为函数参数时,它会退化为指针 。所以 sizeof(str) 实际上计算的是指针的大小,而不是数组的大小。在64位系统上,指针大小是8字节。"

其他情况

  • sizeof(str)main 函数中作用于数组本身,会返回整个数组的字节数。

  • sizeof(p) 如果 p 是指针,则返回指针大小。

  • sizeof(n) 如果 nint 类型,通常返回4(在大多数32/64位系统上)。

Qt信号槽机制的优势和不足

优点:类型安全,松散耦合。缺点:同回调函数相比,运行速度较慢。

优点:

类型安全:需要关联的信号槽的签名必须是等同的。即信号的参数类型和参数个数同接受该信号的槽的参数类型和参数个数相同。若信号和槽签名不一致,则编译器会报错。

松散耦合:信号和槽机制减弱了Qt对象的耦合度。激发信号的Qt对象无需知道是哪个对象的那个槽接收它发出的信号,它只需要在适当的时间发送适当的信号即可,而不需要关心是否被接收和哪个对象接收了。Qt保证适当的槽得到了调用,即使关联的对象在运行时删除,程序也不会崩溃。

灵活性:一个信号可以关联多个槽,多个信号也可以关联同一个槽。

缺点:

速度较慢:与回调函数相比,信号和槽机制运行速度比直接调用非虚函数慢10倍左右。

原因:

需要定位接收信号的对象。

安全地遍历所有槽。

编组,解组传递参数。

多线程的时候,信号需要排队等候。(然而,与创建对象的new操作及删除对象的delete操作相比,信号和槽的运行代价只是他们很少的一部分。信号和槽机制导致的这点性能损失,对于实时应用程序是可以忽略的。)

sizeofstrlen 的场景题

这是面试中的高频陷阱题 ,主要考察对数组与指针区别字符串结束符的理解。

核心区别

  • sizeof编译时 运算符,返回整个对象或类型占用的字节数

  • strlen运行时 函数,返回字符串的长度 (不包括结尾的 '\0')。

常见场景与输出(64位系统)

cpp 复制代码
char arr[] = "Hello";    // 数组
char* p = "Hello";       // 指针

sizeof(arr);   // 6(5个字符 + 结束符 '\0')
strlen(arr);   // 5
sizeof(p);     // 8(指针本身大小)
strlen(p);     // 5

void func(char str[100]) {
    sizeof(str); // 8(数组作为参数退化为指针)
}

面试回答话术

"sizeof 计算的是编译时确定的类型大小 ,而 strlen 计算的是运行时字符串的实际长度 。关键陷阱是数组作为函数参数时会退化为指针 ,此时 sizeof 计算的是指针大小。"

push_backemplace_back 的区别

这是C++11引入的性能优化考点。

核心区别

  • push_back:接受一个已构造好的对象 ,将其拷贝或移动到容器中。

  • emplace_back:接受构造函数的参数 ,在容器原地构造 对象,避免了一次拷贝或移动

示例

cpp 复制代码
struct MyType {
    MyType(int a, double b) { /* ... */ }
};

vector<MyType> vec;
vec.push_back(MyType(1, 2.0)); // 先构造临时对象,再拷贝/移动到容器
vec.emplace_back(1, 2.0);      // 直接在容器内构造,无临时对象

面试回答话术

"emplace_back 通过完美转发 ,将参数直接传递给对象的构造函数,在容器内存中直接构造,减少了一次临时对象的创建和拷贝,在构造复杂对象时性能更好。对于基本类型或已构造好的对象,两者差别不大。"

常用容器的底层实现

这是C++面试的必考题,需要清晰说出每个容器的数据结构、时间复杂度。

容器 底层数据结构 特点
vector 动态数组 连续内存,随机访问 O(1),尾部插入/删除 O(1),中间插入/删除 O(n)
list 双向链表 不连续内存,插入/删除 O(1),随机访问 O(n)
deque 分段连续空间(双端队列) 双端插入/删除 O(1),随机访问 O(1)
map / set 红黑树(平衡二叉搜索树) 元素有序,增删查 O(log n)
unordered_map / unordered_set 哈希表 元素无序,平均增删查 O(1),最坏 O(n)

面试回答话术

"vector 是动态数组,list 是双向链表,map/set 基于红黑树,unordered_map/set 基于哈希表。选择时,如果需要频繁随机访问,用 vector;需要频繁插入删除,用 list;需要有序,用 map;需要快速查找且不关心顺序,用 unordered_map。"

手撕链表反转的递归实现

cpp 复制代码
struct ListNode {
    int val;
    ListNode* next;
    ListNode(int x) : val(x), next(nullptr) {}
};

ListNode* reverseList(ListNode* head) {
    // 递归终止条件:空链表或只有一个节点
    if (head == nullptr || head->next == nullptr) {
        return head;
    }
    // 递归反转后面的链表
    ListNode* newHead = reverseList(head->next);
    // 将当前节点放到反转后链表的末尾
    head->next->next = head;
    head->next = nullptr;
    return newHead;
}

面试回答话术

"递归的核心是先反转当前节点之后的所有节点,然后将当前节点接到反转后链表的末尾。终止条件是链表为空或只有一个节点。时间复杂度 O(n),空间复杂度 O(n)(递归栈)。"

多态中的对象释放过程

面试回答话术

"当使用基类指针 指向派生类对象并 delete 时,如果基类析构函数是虚函数 ,会先调用派生类析构函数 ,再调用基类析构函数 ,实现对象的完整释放 。如果基类析构函数不是虚函数,则只会调用基类析构,派生类资源无法释放,造成内存泄漏 。这就是为什么所有可能被继承的类,析构函数都应声明为 virtual。"

互斥量、信号量、条件变量的区别

面试回答话术

"三者都是线程同步机制:

  • 互斥量 用于保护临界区,保证同一时刻只有一个线程访问资源,适用于资源独占。

  • 信号量 用于控制资源数量,允许多个线程同时访问有限资源,适用于线程池、连接池。

  • 条件变量 用于线程间等待/通知,让线程在条件不满足时休眠,满足时唤醒,通常与互斥量配合,适用于生产者-消费者模型。

互斥量是 ,信号量和条件变量是同步机制。条件变量可以避免忙等,比单纯用互斥量更高效。"

相关推荐
颜挺锐22 分钟前
如何轻松通过性能测试之第四篇:性能测试项目怎么交付?一文讲透九步实施流程(附面试话术 + 实战清单)
面试·职场和发展
敲代码的嘎仔1 小时前
28届后端开发-海康威视日常实习一面(已OC)
java·开发语言·后端·面试·海康威视·实习·大厂
kyriewen2 小时前
面试官让我用 AI 重构一个 8 年陈的 React 组件——他说他不看代码,只看我会不会拆
前端·javascript·面试
黄敬峰3 小时前
一文搞懂 Agent Memory 管理:从临时记忆到长期记忆
面试
汉堡大王95274 小时前
面试必考:手写代码 new 做了什么?从原理到实现全解析
前端·javascript·面试
数智启示录5 小时前
Apache Doris 4.0.8 混合检索实战(第 5 篇):三套系统只返回两条结果,一条 SQL 如何避开交集丢失
大数据·数据库·经验分享·sql·面试
带多刺的玫瑰5 小时前
Leecode#15刷题之三数之和
算法·leetcode·职场和发展
shehuiyuelaiyuehao7 小时前
算法32,连续数组,前缀和+哈希表
算法·leetcode·职场和发展
YonyouHRSaaS8 小时前
AI面试工具怎么选型?2026年企业AI面试系统选型标准是什么?
人工智能·面试·职场和发展·ai面试·ai招聘