搜狐 后端开发-C++ 一面(暑期)

1. 智能指针是什么,有哪几类,实现思路是什么

答案:智能指针本质上是用类去管理资源生命周期,把“申请-释放”这件事和对象构造、析构绑定起来,避免手动 new/delete 带来的泄漏和悬空问题。常见的有 unique_ptrshared_ptrweak_ptrunique_ptr 表示独占所有权,不能随意拷贝;shared_ptr 表示共享所有权,底层一般有控制块维护引用计数;weak_ptr 不拥有对象,只做观察,主要用来解决循环引用。实现上,unique_ptr 比较直接,析构时释放资源即可;shared_ptr 会额外维护强弱引用计数、删除器、分配器等信息;weak_ptr 依附控制块存在,不增加强引用计数。工程里更重要的是用它表达所有权,而不是单纯把裸指针替换掉。

代码:

#include <iostream>
#include <memory>
using namespace std;

struct Node {
    ~Node() { cout << "~Node\n"; }
};

int main() {
    unique_ptr<Node> p1 = make_unique<Node>();

    shared_ptr<Node> p2 = make_shared<Node>();
    weak_ptr<Node> wp = p2;

    if (auto sp = wp.lock()) {
        cout << "object alive\n";
    }
    return 0;
}

2. shared_ptr 是线程安全的吗,线程安全体现在哪

答案:shared_ptr 的线程安全是有限度的。通常说它线程安全,是指多个线程对同一个控制块做引用计数增减时,这部分一般通过原子操作保证安全,也就是多个线程拷贝、析构同一个 shared_ptr,引用计数不容易乱。但这不代表 shared_ptr 指向的对象本身线程安全。多个线程拿着同一个 shared_ptr 去同时修改对象内部成员,依然可能发生数据竞争。所以它解决的是“对象什么时候释放”的并发问题,不解决“对象内部状态怎么并发访问”的问题。

3. 如果继续追问,shared_ptr 的线程安全是怎么实现的

答案:一般实现里,shared_ptr 会把对象指针和控制块分开。控制块里至少有强引用计数和弱引用计数,这两个计数的增减通常用原子操作完成。这样多个线程同时复制 shared_ptr 时,强引用计数可以安全加一;析构时安全减一;当强引用减到零时释放对象本体;强弱计数都归零时再释放控制块。但这里的安全主要建立在计数层面。如果多个线程同时写同一个 shared_ptr 变量本身,比如一个线程 reset,另一个线程赋值,这通常仍然需要外部同步,或者用 std::atomic<std::shared_ptr<T>> 这类方案。所以这题真正考的是你能不能区分“控制块安全”“对象安全”“句柄变量安全”这三层。

4. C++ 多态的实现条件是什么

答案:要形成运行时多态,通常需要三个条件:有继承关系、基类里有虚函数、通过基类指针或引用调用虚函数。只有这样,编译器才会通过虚函数表在运行时根据对象真实类型决定调用哪个实现。如果只是对象切片后按值传递,或者没有虚函数,那么即使有继承关系也构不成运行时多态。这题继续深入,一般会问虚表指针放在哪、构造析构阶段多态是否生效、纯虚函数和抽象类的关系。

5. 什么是虚函数,底层实现原理是什么

答案:虚函数是允许派生类重写并在运行时动态绑定的成员函数。底层通常依赖虚函数表和虚表指针实现。带虚函数的类对象里一般会有一个隐藏的虚表指针,指向该类型对应的虚函数表;当通过基类指针或引用调用虚函数时,程序会根据对象当前的虚表指针去查表调用真实函数。不同编译器实现细节会有差异,但整体思路差不多。所以虚函数不是魔法,本质上是编译器帮你维护了一套运行时跳转机制。

代码:

#include <iostream>
using namespace std;

class Base {
public:
    virtual void run() { cout << "Base\n"; }
    virtual ~Base() = default;
};

class Derived : public Base {
public:
    void run() override { cout << "Derived\n"; }
};

int main() {
    Base* p = new Derived();
    p->run();
    delete p;
    return 0;
}

6. 父类析构函数必须是虚函数吗,为什么

答案:不是必须,但如果一个类会被当作基类使用,并且可能通过基类指针删除派生类对象,那析构函数就应该是虚函数。否则 delete basePtr 时只会调用基类析构,不会正确走到派生类析构,可能导致资源泄漏甚至对象析构不完整。如果这个类根本不打算被继承,或者不会通过基类指针释放对象,那不一定非要虚析构。面试里这题本质上是考你是否理解多态删除场景。

代码:

#include <iostream>
using namespace std;

class Base {
public:
    virtual ~Base() { cout << "~Base\n"; }
};

class Derived : public Base {
public:
    ~Derived() override { cout << "~Derived\n"; }
};

int main() {
    Base* p = new Derived();
    delete p;
    return 0;
}

7. static 在 C++ 里有哪些常见用法

答案:static 常见有几种语义。修饰局部变量时,表示静态存储期,函数退出后变量不会销毁;修饰类成员时,表示属于类本身而不是某个对象;修饰全局变量或函数时,通常表示限制链接范围到当前编译单元。如果放到工程里看,局部静态变量经常用来做单例或延迟初始化,类静态成员适合表达共享状态,全局 static 更多是一种符号可见性控制。这题如果深入,经常会追问局部静态变量初始化的线程安全。

8. vector 的扩容机制是什么,怎么避免频繁扩容

答案:vector 底层是连续内存,容量不够时会申请更大的一块新内存,把原有元素搬过去,再释放旧内存。扩容倍数标准并没有强制规定,不同实现可能不同,但常见会按 1.5 倍或 2 倍增长。频繁扩容的问题主要在于反复申请内存、元素移动或拷贝、迭代器失效。如果提前知道大概数据量,可以用 reserve 预留容量;如果对象移动代价高,还要关注是否提供高效移动构造。

代码:

#include <vector>
using namespace std;

int main() {
    vector<int> v;
    v.reserve(10000);
    for (int i = 0; i < 10000; ++i) {
        v.push_back(i);
    }
    return 0;
}

9. vector 扩容时迭代器为什么会失效

答案:因为 vector 是连续存储,一旦扩容,原来的整块内存可能被搬到新地址。原来指向旧内存的迭代器、指针、引用都会指向无效位置,所以会失效。如果没有触发扩容,在尾部插入时前面元素的引用通常还有效,但尾后迭代器可能变化;如果中间插入或删除,插入点之后的迭代器通常也会失效。这类问题本质上和底层存储结构有关。

10. mapunordered_map 的底层数据结构分别是什么

答案:map 底层通常是红黑树,元素天然有序,查找、插入、删除复杂度一般是 O(logn)unordered_map 底层通常是哈希表,通过哈希函数把 key 映射到桶里,平均查找、插入、删除复杂度接近 O(1),但最坏情况下可能退化。如果业务需要有序遍历、范围查找、上下界查询,map 更合适;如果主要追求单点查找效率且不关心顺序,unordered_map 更常用。面试继续问时,往往会追哈希冲突、rehash、负载因子和迭代器失效规则。

11. 怎么避免哈希冲突,只说开链和开放寻址还不够时怎么答

答案:严格来说,哈希冲突无法彻底避免,只能尽量降低概率并优化冲突处理。一方面要选合适的哈希函数,让 key 分布尽可能均匀;另一方面要控制负载因子,必要时及时扩容 rehash。冲突处理常见有拉链法、开放寻址、Robin Hood Hashing 等,不同实现会在缓存友好性、删除复杂度和最坏情况表现上做权

剩余60%内容,订阅专栏后可继续查看/也可单篇购买

C++ 常考面试题总结 文章被收录于专栏

本专栏系统梳理C++方向, 大中厂高频高频面试考点 , 内容皆来自真实面试经历,从基础语法、内存管理、STL与设计模式,到操作系统与项目实战,结合真实面试题深度解析,帮助开发者高效查漏补缺,提升技术理解与面试通过率,打造扎实的C++工程能力.

全部评论
uu一面八股问的多吗,没问项目吗
点赞 回复 分享
发布于 04-20 15:43 陕西
同学,考虑一下多多吗,招实习生啦:https://careers.pddglobalhr.com/campus/intern?t=FFEgIPlwIe
点赞 回复 分享
发布于 04-14 16:45 上海
智能指针我了解过,但从没用过
点赞 回复 分享
发布于 04-08 23:00 辽宁

相关推荐

头像
04-30 08:51
南京大学 Java
浮沉不改青云志,重踏燕城再启程。去年12月下旬,一下子堆了七八门期末考试,整个人忙得焦头烂额,根本没空考虑暑期实习的事。那段时间又赶上AI热潮,我对以后要走什么方向特别迷茫,特别焦虑,连要不要转Java技术栈都拿不定主意。放假在家、回学校之后,我索性狠下心突击学&nbsp;Java,开始准备找暑期实习。第一个面试约的就是拼多多,之前网上认识的学姐帮我仔细改了简历,我也挺看好她所在的部门。虽然当时感觉自己还没准备好,但还是硬着头皮上了,结果面试被两道手撕题目问得一脸懵,直接翻车。不过这次失败也让我看清了自己的短板,后面及时调整了准备方向,踏踏实实补基础、练项目,最后顺利拿到了京东正式Offer其实按道理说,我也不算纯零基础,考研381分上岸南软,平时也会用Java刷力扣,408知识点也没完全忘干净,偶尔还会翻出来复盘,省了不少从头学的时间。但真正系统学Java技术栈以及ai相关领域之后,才发现整个体系又大又杂、又深又广,每天都能接触到一大堆新知识,只能一点点慢慢啃、慢慢积累。准备阶段(12月-2月)-&nbsp;12月下旬+1月上旬:决定走Java后端+Agent开发双线路径(当时想法还是主要java,agent为能力提升方面,没想到今年agent岗位能如此大规模爆火)-&nbsp;1月-2月:速通Redis、MySQL、JVM,魔改黑马点评项目,自己手搓-&nbsp;2月底:制作初版简历AI&nbsp;Agent开发(2-3月)核心技术:LangChain/LangGraph、RAG、多Agent架构实践项目:航旅排障智能体(Supervisor-Planner-Executor架构&nbsp;+&nbsp;RAG知识库&nbsp;+&nbsp;工具调用)-3月中:修改简历,加入agent项目,开始海投东子时间线3.30:投递3.31:完成综合测评4.17:一面4.20:二面4.28:三面→&nbsp;当天拿到oc以及offer希望还是能给到大家一些建议:1、从大方面来说的话,暑期的整体战线还是很长的,有句话说得好,运气也是实力的一部分,很可能你面的这家公司、这个岗位刚好就非常契合你的能力素质,那么就肯定会一顺百顺。如果运气不好,上来干几道手撕算法、那谁也没辙,所以说一次的挂并不能代表你不够好,只是说明你和这个公司不是很匹配2、&nbsp;Agent时代要学会去拥抱新概念,有的团队就是期待能够发现问题、定义问题并解决问题的“全栈型”人才。拥抱前沿技术,利用AI工具提升个人生产力,但前提是必须打牢软件工程和后端的底子,在扎实的基础上再去做生产力升级。3、简历还是要有一些业务价值的体现,用数据说话:性能提升了多少、成本降低了多少、用户体验改善了多少。这样才不是泛泛而谈。并且要有一种能够在真实业务场景中快速模拟一种方案的能力,很多公司都比较看重这些4、八股的话还是要以理解为主,面试的时候我就喜欢一边手舞足蹈,一边比比划划的给面试官讲我的理解,这样子的话就算你记忆的不是很清楚,&nbsp;但大致意思表达的对,面试官也会对你比较满意5、手撕的话就不用多说了(刷就好了)记录下这一路的颠沛流离)一些过了简历筛,进行面试的公司,能被这些公司选中也是我个人的荣幸,之后如果有机会,也非常希望可以继续把流程推进下去(为什么反而是中小厂简历都过不去,十分好奇)pdd&nbsp;一面两道抽象手撕&nbsp;挂腾子&nbsp;wxg&nbsp;方向不匹配&nbsp;一面光速挂掉pcg&nbsp;腾讯视频&nbsp;一面过&nbsp;二面可能理解不够深入&nbsp;挂科大讯飞&nbsp;聊的比较满意&nbsp;一面还是挂了&nbsp;后续被捞&nbsp;但已有offer&nbsp;遂拒绝B站&nbsp;一面拷打&nbsp;没想到能过&nbsp;二面时间hr一拖再拖&nbsp;拖到有offer了&nbsp;遂拒绝蚂蚁&nbsp;一面过&nbsp;二面间隔太久了&nbsp;已有offer&nbsp;遂拒绝阿里&nbsp;钉钉&nbsp;电话面试完没消息了阿里国际&nbsp;一二面面试官都好好,感觉团队氛围非常好,只是可惜base广州,有点太远了字节&nbsp;一面过&nbsp;二面完成&nbsp;hr离职了&nbsp;没人帮忙推流程了&nbsp;所以先东子了祝大家都能找到心仪的offer!暑期实习准备战斗完结~撒花!
点赞 评论 收藏
分享
评论
1
4
分享

创作者周榜

更多
牛客网
牛客网在线编程
牛客网题解
牛客企业服务