影石 音视频开发- C++ 一面

1. 自我介绍

2. 说一下 new 和 malloc 的底层实现原理

答案:malloc 是 C 里的内存分配函数,本质上就是向运行时分配器申请一块指定大小的原始内存,返回的是 void*,它只负责分配内存,不会调用构造函数。底层分配器一般会维护空闲链表、内存池之类的数据结构,再通过 brkmmap 向操作系统要内存。new 是 C++ 的运算符,它通常分两步:先调用 operator new 分配原始内存,再在这块内存上调用构造函数完成对象初始化。如果构造函数抛异常,C++ 还会负责把前面申请到的内存回收。所以严格来说,newmalloc 最大的区别不是“一个是运算符一个是函数”这么简单,而是 new 带有对象语义,知道要构造;delete 也知道要析构。malloc/free 则只处理字节内存,不理解类对象生命周期。

代码:

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

class A {
public:
    A() { cout << "A ctor\n"; }
    ~A() { cout << "A dtor\n"; }
};

int main() {
    A* p1 = new A;
    delete p1;

    A* p2 = (A*)malloc(sizeof(A));
    free(p2);
    return 0;
}

3. 说一下 vector 和 list 的底层实现原理

答案:vector 底层是连续内存空间,核心就是一段可以动态扩容的数组。它一般会维护 beginendcapacity 之类的信息。优点是随机访问快、缓存局部性好、尾部插入均摊复杂度低;缺点是在中间插入删除代价高,扩容时可能触发整段搬迁。list 底层通常是双向链表,每个节点单独分配,节点里保存前驱和后继指针。它的优点是在已知位置插入删除比较稳定,不需要搬移其他元素;缺点是随机访问很差,内存碎片多,缓存命中率低。工程里很多人会下意识觉得“频繁插入删除就用 list”,但实际上如果数据量不大或者插入位置不确定,vector 往往依然更快,因为 CPU cache 友好。面试里答到底层结构只是第一层,最好能再说一句它们在性能模型上的差异。

代码:

#include <iostream>
#include <vector>
#include <list>
using namespace std;

int main() {
    vector<int> v = {1, 2, 3};
    v.push_back(4);
    cout << v[2] << endl;

    list<int> lst = {1, 2, 3};
    auto it = lst.begin();
    ++it;
    lst.insert(it, 100);

    for (auto x : lst) cout << x << " ";
    cout << endl;
    return 0;
}

4. 右值引用是什么,解决了什么问题

答案:右值引用是 C++11 引入的一个重要特性,本质上是为了区分“可以安全偷资源的临时对象”和“仍然有名字、后续还要继续使用的左值对象”。它最大的价值是支撑移动语义和完美转发。以前很多对象在函数返回、容器扩容、参数传递时只能拷贝,现在如果对象内部持有堆内存、文件句柄、socket 之类资源,就可以把资源所有权从一个对象转移到另一个对象,避免昂贵的深拷贝。通常和右值引用一起出现的就是 std::movestd::forwardmove 本身不移动,只是把对象显式转成右值引用,告诉编译器“这个对象可以被搬走了”;forward 则主要用于模板里保留值类别。

代码:

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

class Buffer {
public:
    Buffer(size_t n) : data_(new int[n]), size_(n) {
        cout << "ctor\n";
    }

    Buffer(const Buffer& other) : data_(new int[other.size_]), size_(other.size_) {
        cout << "copy\n";
        for (size_t i = 0; i < size_; ++i) data_[i] = other.data_[i];
    }

    Buffer(Buffer&& other) noexcept : data_(other.data_), size_(other.size_) {
        cout << "move\n";
        other.data_ = nullptr;
        other.size_ = 0;
    }

    ~Buffer() { delete[] data_; }

private:
    int* data_ = nullptr;
    size_t size_ = 0;
};

int main() {
    vector<Buffer> v;
    v.push_back(Buffer(10));
    return 0;
}

5. C++ 动态绑定是怎么实现的

答案:动态绑定也就是运行时多态。它成立的前提一般是有继承关系、基类里有虚函数、通过基类指针或者引用去调用派生类对象的方法。底层实现通常依赖虚函数表。编译器会为包含虚函数的类生成一张虚表,对象内部会隐含一个虚表指针。程序在调用虚函数时,不是编译期把函数地址完全写死,而是运行时通过对象里的虚表指针找到实际应该执行的函数地址。所以动态绑定的关键不只是“virtual”这个关键字,而是“通过基类接口在运行时分派到真实对象的行为”。这也是为什么如果发生对象切片,多态效果就没了。

代码:

#include <iostream>
using namespace std;

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

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

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

6. 虚函数和虚函数表怎么理解

答案:虚函数是实现运行时多态的语言机制,虚函数表则是主流编译器完成这一机制的常见实现方式。一个类只要声明了虚函数,编译器一般就会给它准备一张虚表,表里存放对应虚函数的入口地址。对象实例内部会带一个隐藏

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

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

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

全部评论

相关推荐

评论
点赞
收藏
分享

创作者周榜

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