阿里 飞猪-C++研发工程师 一面

1、自我介绍

2、项目拷打

3、实习内容

4、const 的四种常见用法

答案:const 常见用法可以从修饰对象、修饰指针、修饰参数、修饰成员函数这几个角度理解。

修饰普通变量时,表示这个值不能被修改。修饰指针时,要区分到底是“指针指向的内容不能改”,还是“指针本身不能改”。修饰函数参数时,通常表示函数内部不应该改这个对象,尤其是传大对象引用时很常见。修饰成员函数时,表示这个函数不会修改对象的逻辑状态,也就是 this 指向的是只读对象。再往下还可以延伸到 const 返回值、const 成员变量初始化这些内容。

代码:

#include <iostream>
using namespace std;

class A {
public:
    A(int x) : val(x) {}
    int get() const { return val; }

private:
    int val;
};

int main() {
    const int a = 10;
    int b = 20;

    const int* p1 = &b;
    int* const p2 = &b;

    A obj(42);A
    cout << obj.get() << endl;
}

5、引用的底层是什么

答案:从语言语义上说,引用是别名,不是对象本身,也不能像指针那样重新绑定。但从编译器实现角度看,引用很多时候会借助指针来完成,也就是说底层通常会把引用实现成一个隐藏指针,只不过语法层面对它做了更多限制,比如必须初始化、不能为 null、使用时不需要显式解引用。所以更准确的表述是:引用在语义上不是指针,但实现上通常依赖指针。

代码:

#include <iostream>
using namespace std;

void func(int& x) {
    x += 1;
}

int main() {
    int a = 5;
    int& ref = a;
    func(ref);
    cout << a << endl;
}

6、多态的实现原理

答案:C++ 运行时多态的核心是虚函数机制。只要类里定义了虚函数,编译器通常就会为类生成虚函数表,对象内部会保存一个虚表指针。当通过基类指针或引用调用虚函数时,不是在编译期决定调用哪个版本,而是在运行时根据对象实际类型找到对应虚表,再取出函数地址完成调用。这就是动态绑定。如果通过对象本身直接调用,或者函数不是虚函数,那走的还是静态绑定。

代码:

#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、智能指针,unique_ptrshared_ptr 的区别,底层如何实现

答案:unique_ptr 表示独占所有权,不能拷贝,只能移动,资源生命周期最清晰,额外开销也最小。shared_ptr 表示共享所有权,多个指针共同管理同一个对象,底层通过引用计数来决定什么时候释放对象。从实现角度讲,shared_ptr 一般会有一个单独的控制块,里面保存强引用计数、弱引用计数、删除器等信息。强引用计数归零时对象析构,弱引用也归零后控制块本身释放。unique_ptr 就简单得多,本质上是一个带删除器的独占包装。

代码:

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

class Test {
public:
    Test() { cout << "construct\n"; }
    ~Test() { cout << "destruct\n"; }
};

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

    shared_ptr<Test> p2 = make_shared<Test>();
    shared_ptr<Test> p3 = p2;
    cout << p2.use_count() << endl;
}

8、项目中为什么用 unordered_map 不用 map,它是线程安全的吗

答案:项目里选 unordered_map 的原因主要是查询场景远多于范围遍历场景,核心诉求是按 key 高效定位状态、版本或对象句柄。unordered_map 平均查询复杂度通常是 O(1),更适合这种高频点查场景;而 map 底层通常是红黑树,复杂度是 O(log n),它的优势更多在有序性、范围查询、上下界搜索,但这些在这个项目里不是重点。它本身不是线程安全的。只要有并发写,或者读写并发,就可能出问题,尤其是 rehash 时风险更大。所以如果多个线程访问同一个 unordered_map,必须自己加同步控制,或者做分片管理,不能把它当成并发容器用。

9、如何保证项目中的各个部分(类)同时不能赋值和拷贝

答案:最直接的方式就是把拷贝构造和拷贝赋值运算符显式删除。如果还想彻底限制对象语义,也可以把移动构造和移动赋值一起删掉。这种写法很适合资源管理类、上下文类、连接类这类语义上不该被随便复制的对象。

代码:

class NonCopyable {
public:
    NonCopyable() = default;
    ~NonCopyable() = default;

    NonCopyable(const NonCopyable&) = delete;
    NonCopyable& operator=(const NonCopyable&) = delete;

    NonCopyable(NonCopyable&&) = delete;
    NonCopyable& operator=(NonCopyable&&) = delete;
};

10、配置中心客户端为什么设计成单例,如何实现,是懒汉还是饿汉,区别是什么

答案:这类全局配置客户端通常适合做成单例,因为整个进程里只需要一份配置连接状态、版本缓存和订阅上下文。如果做成多个实例,一方面资源会重复占用,另一方面容易出现不同模块拿到不同配置视图的问题,管理上也更混乱。实现上更常见的是懒汉式里的局部静态对象写法,因为 C++11 之后局部静态变量初始化是线程安全的,而且代码简单。饿汉式是在程序启动时就初始化,优点是实现更直接,没有首次竞争问题;缺点是即使没用到也会提前构造。懒汉式是第一次使用时才创建,更节省资源,也更贴近这类模块的实际使用习惯。

代码:

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

class ConfigClient {
public:
    static ConfigClient& instance() {
        static ConfigClient client;
        return client;
    }

    string get(const string& key) const {
        auto it = kv_.find(key);
        return it == kv_.end() ? "" : it->second;
    }

    void set(const string& key, const string& value) {
        kv_[key] = value;
    }

private:
    ConfigClient() = default;
    ConfigClient(const ConfigClient&) = delete;
    ConfigClient& operator=(const ConfigClient&) = delete;

private:
    unordered_map<string, string> kv_;
};

11、如何设计一个信号量类

答案:如果不依赖标准库现成信号量,实现一个简化版信号量,核心就是一个计数器加互斥锁和条件变量。wait() 在资源数为 0 的时候阻塞,post() 增加资源数并唤醒等待线程。设计时最关键的是把计数器修改和条件判断放在同一个临界区里,避免丢唤醒和竞态问题。

代码:

#include <mutex>
#includ

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

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

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

全部评论

相关推荐

评论
点赞
3
分享

创作者周榜

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