吉比特 C++开发 一面

1. 自我介绍

2. 进程和线程的区别,线程共享哪些资源,不共享哪些资源

答案:进程是资源分配的基本单位,线程是 CPU 调度的基本单位。一个进程有独立的虚拟地址空间、文件描述符表、信号处理信息等;同一个进程内的多个线程共享地址空间、全局变量、堆、打开的文件描述符和代码段。

线程之间不共享的是各自的栈、寄存器上下文、线程局部存储和调度状态。所以线程切换通常比进程切换轻,因为不需要切换完整地址空间;但线程共享内存也意味着更容易出现数据竞争、死锁和内存可见性问题。

在车载边缘诊断数据采集与规则告警平台里,接入线程、解析线程、规则线程会共享任务队列和配置快照,这些地方必须明确加锁、无锁队列或者只读快照,否则并发问题很难排查。

3. 进程里一定有线程吗,主线程退出后其他线程会怎样

答案:进程至少有一个执行流,也就是主线程。传统说法里,一个进程至少包含一个线程,否则没有代码能被调度执行。在 Linux 下,线程本质上是通过 clone 创建的轻量级任务,它们共享同一个进程地址空间。主线程如果从 main 返回,相当于调用 exit,通常会终止整个进程,其他线程也会被结束。

如果只想结束当前线程,应该调用 pthread_exit 或让线程函数返回,而不是让整个进程退出。工程里常见问题是主线程启动了后台工作线程后直接返回,导致服务刚启动就退出。正确做法是主线程进入事件循环,或者等待工作线程结束。

代码:

#include <pthread.h>
#include <iostream>
#include <unistd.h>
using namespace std;

void* worker(void*) {
    while (true) {
        cout << "worker running\n";
        sleep(1);
    }
    return nullptr;
}

int main() {
    pthread_t tid;
    pthread_create(&tid, nullptr, worker, nullptr);

    // 如果 main 直接 return,整个进程会退出
    pthread_join(tid, nullptr);
    return 0;
}

4. 进程状态有哪些,僵尸进程和孤儿进程分别是什么

答案:Linux 进程常见状态有运行态、可中断睡眠、不可中断睡眠、停止态、僵尸态等。运行态表示正在运行或在运行队列中等待 CPU;可中断睡眠一般是在等待 IO、锁、定时器等事件;不可中断睡眠通常是在等待内核态资源,不能被普通信号打断;停止态一般是被调试或信号暂停。

僵尸进程是子进程已经退出,但父进程没有调用 waitwaitpid 回收它的退出状态,进程表里还保留一条记录。大量僵尸进程会占用 pid 和进程表资源。孤儿进程是父进程先退出,子进程还在运行,它会被 init/systemd 接管,一般不算问题。

代码:

#include <sys/wait.h>
#include <unistd.h>
#include <iostream>
using namespace std;

int main() {
    pid_t pid = fork();

    if (pid == 0) {
        _exit(0);
    }

    int status = 0;
    waitpid(pid, &status, 0); // 回收子进程,避免僵尸进程
    cout << "child recycled\n";
    return 0;
}

5. 进程间通信方式有哪些,管道、消息队列、共享内存和 socket 怎么选

答案:常见进程间通信方式有管道、命名管道、消息队列、共享内存、信号、信号量、Unix Domain Socket、TCP/UDP socket、mmap 文件映射等。管道适合父子进程之间简单的字节流通信;消息队列适合传递结构化消息;共享内存速度最快,因为数据不需要在内核和用户态之间反复拷贝,但同步要自己处理;socket 最通用,可以跨机器,也可以本机进程间通信。

如果是高吞吐的大块数据交换,可以考虑共享内存加同步机制。如果是模块解耦和可维护性优先,Unix Domain Socket 或 TCP 更容易调试。实际项目里不能只看性能,还要看容错、权限、协议演进、故障隔离和排查成本。

6. C++ 多态的底层实现,虚函数表放在哪里,什么时候形成

答案:C++ 运行时多态通常通过虚函数表和虚表指针实现。类中只要有虚函数,编译器一般会为这个类生成虚函数表,表里存放虚函数入口地址。对象里会有虚表指针,指向它真实类型对应的虚函数表。

虚函数表属于编译器生成的静态数据,通常放在只读数据段附近,不是每个对象一份。每个对象里通常只保存虚表指针。对象构造过程中,vptr 会随着构造层级变化。先构造基类部分,此时 vptr 指向基类虚表;再构造派生类部分,vptr 才指向派生类虚表。所以构造函数里调用虚函数不会表现出派生类多态。

代码:

#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;
}

7. C++ 和 Java 的多态有什么本质差异

答案:C++ 的多态分为静态多态和运行时多态。模板、函数重载属于编译期确定;虚函数属于运行时动态绑定。C++ 只有被声明为 virtual 的成员函数才会走动态分派,普通函数调用默认是静态绑定。Java 中普通实例方法默认具有动态分派特性,除了 staticprivatefinal 等特殊情况。

C++ 的多态更贴近对象内存布局,开发者需要关心虚析构、对象切片、指针转换、构造析构阶段虚函数行为。Java 由 JVM 管理对象和方法分派,运行时还有 JIT、内联缓存、逃逸分析等优化。它少了手动内存释放问题,但多了 GC 和 JVM 运行时行为。

在工程上,C++ 多态性能可控但容易踩生命周期坑,Java 多态语义统一但运行时环境更复杂。

8. 如果不用 C++ 语法,用 C 语言怎么模拟多态

答案:C 语言可以通过函数指针表模拟虚函数表。做法是定义一张操作表,里面放函数指针;每个对象结构体里放一个指向操作表的指针。不同类型对象初始化时绑定不同的操作表,调用时通过函数指针间接调用。

这种方式在驱动、网络协议栈、组件插件系统里很常见。它的缺点是编译器不会帮你做类型检查和生命周期管理,函数签名、对象转换、析构释放都要自己保证。

代码:

#include <stdio.h>

typedef struct Device Device;

typedef struct {
    void (*start)(Device*);
    void (*stop)(Device*);
} DeviceOps;

struct Device {
    DeviceOps* ops;
    int id;
};

void sensorStart(Device* d) {
    printf("sensor %d start\n", d->id);
}

void sensorStop(Device* d) {
    printf("sensor %d stop\n", d->id);
}

DeviceOps sensorOps = {
    sensorStart,
    sensorStop
};

int main() {
    Device dev;
    dev.id = 1001;
    dev.ops = &sensorOps;

    dev.ops->start(&dev);
    dev.ops->stop(&dev);
    return 0;
}

9. 带有虚函数的类会有什么额外问题,为什么基类析构函数通常要写成 virtual

答案:带虚函数的类会引入虚表指针,对象大小会增加,函数调用也可能多一次间接跳转。更重要的是,它意味着这个类可能被当作基类使用,这时析构函数必须特别注意。

如果通过基类指针删除派生类对象,而基类析构函数不是虚函数,只会调用基类析构,派生类资源不会释放,导致资源泄漏甚至未定义行为。所以只要一个类准备作为多态基类使用,析构函数通常就应该声明为 virtual

另外带虚函数的类还要注意对象切片。按值传递基类对象会截掉派生类部分,导致多态失效。工程里多态对象一般通过引用、裸指针观察,或者用 unique_ptr<Base> / shared_ptr<Base> 管理。

代码:

#include <iostream>
using namespace std;

class Handler {
public:
    virtual void handle() = 0;
    virtual ~Handler() {
        cout << "Handler dtor\n";
    }
};

class AlarmHandler : public Handler {
public:
    ~AlarmHandler() {
        cout << "AlarmHandler dtor\n";
    }

    void handle() override {
        cout << "handle alarm\n";
    }
};

int main() {
    Handler* h = new AlarmHandler();
    delete h;
}

10. mapunordered_map 的底层结构、复杂度和迭代器失效规则有什么区别

答案:map 通常基于红黑树实现,key 有序,查找、插入、删除复杂度是 O(logN)。它适合需要有序遍历、范围查询、找前驱后继、按 key 顺序输出的场景。unordered_map 基于哈希表实现,平均查找、插入、删除是 O(1),但不保证顺序,哈希冲突严重时会退化。

迭代器失效上,map 插入一般不会导致已有迭代器失效,删除某个元素只会让被删除元素的迭代器失效。unordered_map 插入如果触发 rehash,会导致所有迭代器失效;删除元素会让被删除元素的迭代器失效。所以在遍历 unordered_map 的同时插入元素要特别小心,尤其是数据量增长触发扩容时。

代码:

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

int main() {
    unordered_map<int, int> mp;
    mp.reserve(10000);
    mp.max_load_factor(0.7);

    for (int i = 0; i < 1000; ++i) {
        mp[i] = i * 10;
    }

    auto it = mp.find(10);
    if (it != mp.end()) {
        cout << it->second << endl;
    }
}

11. mapunordered_map 常见操作的时间复杂度分别是多少,为什么最坏情况不一样

答案:map 的查找、插入、删除一般都是 O(logN),因为红黑树高度近似平衡。它的复杂度比较稳定,不太受 key 分布影响。unordered_map 平均情况下查找、插入、删除是 O(1),因为通过 hash 直接定位桶。但如果大量 key 落到同一个桶里,链表或桶内结构会变长,最坏可能退化到 O(N)

unordered_map 的性能受 hash 函数、负载因子、桶数量、rehash 时机影响很大。如果 key 是自定义类型,必须提供质量较好的 hash,否则表面上用了哈希表,实际性能可能非常差。

代码:

#include <unordered_ma

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

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

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

全部评论
最后通过了吗
点赞 回复 分享
发布于 昨天 23:37 北京

相关推荐

鼠鼠的暑期实习求职生涯结束了。回想上个月的今天,在4.8号开始投递,于5.8号终止。一共投递405家,测评11家,笔试10家,面试6家,收获一个外包offer(鼠鼠一看要求实习12个月才给offer,而且分基础薪资和岗位薪资就跑了),除此之外最高达到美团二面,此外都是一面挂。回顾这一个月,鼠鼠无疑是失败的。目光往前看看,鼠鼠大一认为一线企业会把绩点作为筛选标准之一,于是跻身课内。大二收获了一段项目经历,堪堪抓住了后端开发的尾巴,但没有跟上AI的脚步。考虑保研,但被参与的深度强化学习项目当头一棒,狠狠畏惧了。考虑考公,但竞争说不准哪个更大。直到大三才意识寻找工作对实习、经验和技术的重要性,可是由于拖延和心理原因没能及时准备。时间拨到4月,鼠鼠认为自己情况好不少,而且也不能再拖了。于是开始疯狂的补救:算法吃一些大一大二的老本,然后快速过hot100;项目就追紧现在热门的RAG、Agent这些。最终在4月8日投出了第一份简历,回想当初,不知道那时的我看到此时的境遇会作何感想。因为泡在温水中太久了,意识不到就业市场的严峻和自身技术的边界。因为985的头衔博得了不少面试机会,但是偏偏都不得不被淘汰——学历是把双刃剑,面试官对你承载了更多的期待。鼠鼠带着薄弱的基础闯到暑期实习,才发现到处神仙打架。起初一边过hot100,一边更新项目,一边背八股。因为笔试能力不行,丢掉了滴滴、华为、拼多多、得物等公司的机会,后面又不得不赶着去应付面试需要的知识。第二场面试后变成了坚实的八股党,问什么就答什么,但是在基础知识和项目的理解上差强人意,于是丢掉了一些公司的机会。通过舍友的帮助,鼠鼠明白了自己在表达、回答薄弱之处,不断改变,但是为时又已晚,美团二面击中了鼠鼠的项目架构理解,第一轮回答没跟上之后就已经宣告失败,后面的面试大同小异:八股的拷打和项目的拷打占比很少,场景题显著增加,又穿插一些笔试题——思路是有的,实现是抓马的。鼠鼠直接举手投降了。其实明白场景题实际上就是考对八股和实际场景的理解,但是更明白这需要更多的时间来学习,于是时间流逝到5月8日,最后一面结束,鼠鼠还是失败了。(写到这里的时候想起了23年的6月8日,那时我似乎在高考。想到这里时,一种难以言喻的情绪击中了鼠鼠)鼠鼠接下来准备all&nbsp;in日常实习了(如果各位佬有推荐的话恳请提示下鼠鼠),因为秋招对于实习经历还是比较看重的。也会考虑保研、考公等的路径,不想家里蹲来着。鼠鼠希望大家都能找到自己想要的offer,也希望自己有一份不错的工作。总结经验,希望能帮助到尚且来得及的各位:1.&nbsp;简历上面写的关于自己项目的亮点一定要知根知底,被面试官问起时能够立刻想起此功能整条链路和关联功能,以及方法的缺点、方法的选型原因、具体方法内容等,这个非常重要!2.&nbsp;八股要背全,认为自己悟透后多找找其他人出的八股,彼此应对,也能促进再记忆。3.&nbsp;hot100只是基础,想要通过笔试,还需要更强的做题能力以及一些运气。4.&nbsp;简历要找自己认可的佬评审,可以找AI评价,注意格式和表达,突出重点,展示亮点。不要太冗长,也不要过分无趣。项目描述中出现过的就不要在亮点中再出现了。5.&nbsp;多关注AI方面,读读新的热点话题,看看一些github的仓库,学习其他佬的设计。这样和面试官也能有话题可聊。6.&nbsp;面试时要给自己留出一些思考的时间,不能脑子一热就脱口而出,你不知道这个回答是否给自己留了坑。7.&nbsp;面试记得留一下备份,鼠鼠我一般通过手机的腾讯会议的录音功能,录完还能提取文字,很方便,正确的识别率也很高。8.&nbsp;面试前最好留出半天时间浏览牛客中各路大佬给出的面经,同样的岗位面试问题的重复率不低。9.&nbsp;擅于使用AI,可以通过AI来了解面试的岗位的信息,让AI拷打自己的项目,问场景等。一定要狠狠使用!10.&nbsp;面试的每个环节都很重要,个人介绍、拷打、算法题和反问环节。个人介绍和反问环节建议拿着相关的公司和岗位名去问AI,狠狠出一些反问的题和合理的表达。
yakuso:能保研就保研吧,,同鼠鼠现在还不敢开始面,八股记不住一点
点赞 评论 收藏
分享
评论
点赞
2
分享

创作者周榜

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