搜狐 后端开发-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++工程能力.

全部评论
智能指针我了解过,但从没用过
点赞 回复 分享
发布于 昨天 23:00 辽宁

相关推荐

评论
1
1
分享

创作者周榜

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