亚信科技 软件开发-C++ 一面

1. 请介绍你在项目中的主要工作内容和技术方向

2. 你在 C++ 开发中使用的是哪个标准,具体使用过 C++11/14/17 的哪些新特性

答案:主要使用 C++17,部分代码保持 C++11 兼容。C++11 里常用的是智能指针、右值引用、移动语义、lambda、auto、范围 for、nullptr、线程库、原子操作、overridefinal。C++14 里用过泛型 lambda、make_unique。C++17 里用过结构化绑定、if constexprstd::optionalstd::variantstd::string_view 和文件系统相关接口。

在项目里,智能指针主要用来管理连接对象、策略对象和异步任务上下文;移动语义用于减少事件对象在队列投递过程中的拷贝;string_view 用在协议解析和规则匹配里,避免不必要的字符串复制。不过新特性不是越多越好,项目里还是会优先考虑可读性和团队维护成本。

代码:

#include <optional>
#include <string_view>
#include <iostream>
using namespace std;

optional<int> parseLevel(string_view s) {
    if (s == "low") return 1;
    if (s == "middle") return 2;
    if (s == "high") return 3;
    return nullopt;
}

int main() {
    if (auto level = parseLevel("high"); level.has_value()) {
        cout << *level << endl;
    }
    return 0;
}

3. 请说明 Lambda 表达式在处理问题时的优势

答案:lambda 最大的优势是可以在使用点附近定义短小逻辑,同时还能捕获上下文变量。在排序、自定义过滤、回调、异步任务、定时器、事件处理这些场景里,用 lambda 会比单独写一个函数或仿函数更简洁。比如在安全事件处理里,需要按风险等级排序、按终端 ID 过滤、或者把任务投递到线程池时,lambda 都比较自然。

另外 lambda 本质上是函数对象,编译器能看到具体类型,很多时候可以内联优化。但是它也不能滥用,如果 lambda 太长,或者捕获了很多外部变量,代码反而会变得难读。

代码:

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

struct Event {
    int hostId;
    int risk;
};

int main() {
    vector<Event> events = {{1, 80}, {2, 30}, {3, 95}};

    sort(events.begin(), events.end(), [](const Event& a, const Event& b) {
        return a.risk > b.risk;
    });

    int threshold = 60;
    auto it = remove_if(events.begin(), events.end(), [threshold](const Event& e) {
        return e.risk < threshold;
    });

    events.erase(it, events.end());
    return 0;
}

4. 在使用 Lambda 表达式时遇到过哪些问题或坑

答案:最容易踩坑的是捕获方式,尤其是异步场景下捕获引用或者捕获 this。如果 lambda 被投递到线程池或者定时器里,外部对象可能已经析构了,这时候再访问引用或 this 就会产生悬空引用。同步场景里捕获引用问题不大,但异步场景更推荐捕获值,或者捕获 shared_ptr / weak_ptr 来控制生命周期。

另一个问题是 lambda 捕获大对象会产生拷贝成本。如果捕获的是大 vector、大字符串或者复杂对象,就要考虑是否用引用、移动捕获,或者只捕获必要字段。还有就是 lambda 太长时可读性会下降,这种情况我会把逻辑拆成普通函数或类方法。

代码:

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

class Session : public enable_shared_from_this<Session> {
public:
    void asyncCheck() {
        weak_ptr<Session> weakSelf = shared_from_this();

        auto cb = [weakSelf]() {
            if (auto self = weakSelf.lock()) {
                cout << "session still alive\n";
            }
        };

        cb();
    }
};

5. 你了解哪些智能指针,具体使用过哪些

答案:常用智能指针主要是 unique_ptrshared_ptrweak_ptrunique_ptr 表示独占所有权,不能拷贝,只能移动,适合资源归属明确的对象,比如策略解析器、文件句柄封装、buffer 对象。shared_ptr 表示共享所有权,适合对象会被多个模块或异步任务共同持有的场景,比如连接对象、任务上下文。weak_ptr 不增加引用计数,主要用来打破循环引用,或者在异步回调里判断对象是否还活着。

我一般不会默认使用 shared_ptr。如果一个对象的所有权非常明确,优先用 unique_ptr,这样代码更清楚,开销也更小。

代码:

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

class RuleSet {
public:
    vector<int> rules;
};

class Engine {
private:
    unique_ptr<RuleSet> ruleSet_;

public:
    Engine() : ruleSet_(make_unique<RuleSet>()) {}
};

6. 请说明智能指针在实际项目中的作用和使用场景

答案:智能指针在项目里的核心作用是把对象生命周期和资源释放管住,减少内存泄漏、重复释放和悬空指针。在云端安全策略编排与终端检测响应系统里,策略对象可能被多个工作线程读取,更新时会生成新版本策略,再通过原子替换或读写锁切换。旧策略如果还有线程在使用,就不能马上释放,这类场景可以用 shared_ptr<const RuleSet> 让读线程安全持有一份快照。

连接对象也适合用智能指针管理,因为连接关闭、读写回调、定时器回调可能同时涉及同一个对象。回调执行前拿一份 shared_ptr,可以避免处理过程中对象被提前析构。但对于内部独占资源,比如每个连接自己的输入缓冲区,用 unique_ptr 或直接作为成员对象更合适。

代码:

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

struct RuleSet {
    int version;
};

class RuleManager {
private:
    shared_ptr<const RuleSet> current_;

public:
    shared_ptr<const RuleSet> getSnapshot() {
        return atomic_load(¤t_);
    }

    void update(shared_ptr<const RuleSet> newRules) {
        atomic_store(¤t_, newRules);
    }
};

7. 你在项目中如何避免 shared_ptr 循环引用

答案:循环引用通常出现在两个对象互相持有对方的 shared_ptr。比如终端连接对象持有订阅任务,订阅任务又持有连接对象,如果两边都是 shared_ptr,即使外部已经不再使用,它们的引用计数也不会变成 0,最后导致内存泄漏。

解决方式是先明确所有权。谁真正拥有对象,谁用 shared_ptr;只是观察或者回调访问,就用 weak_ptr。在项目里,如果任务需要回调连接,我会让连接管理器持有连接的 shared_ptr,任务里只保存 weak_ptr,执行时通过 lock() 判断连接是否还存在。

代码:

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

class Connection {
public:
    void sendResult() {
        cout << "send result\n";
    }
};

class Task {
private:
    weak_ptr<Connection> conn_;

public:
    explicit Task(weak_ptr<Connection> conn) : conn_(conn) {}

    void finish() {
        if (auto conn = conn_.lock()) {
            conn->sendResult();
        }
    }
};

8. 在你的项目中,面向对象思想具体应用在哪些地方

答案:面向对象主要用在模块边界比较清晰、需要扩展的地方。比如安全事件处理可以抽象成统一的 EventHandler 接口,不同事件类型有不同处理器:进程启动事件、网络连接事件、文件变更事件、异常登录事件。调度器只负责根据事件类型找到处理器,不关心每个处理器内部怎么实现。

策略匹配模块也可以拆成规则接口,比如哈希匹配规则、正则匹配规则、阈值规则、时间窗口规则。这样后续新增一种规则时,不需要大改原来的调度逻辑。不过我也不会把所有代码都强行对象化。像协议解析、状态机推进、批量统计这种逻辑,用结构体加函数有时候更直接。

代码:

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

struct SecurityEvent {
    int type;
    int risk;
};

class EventHandler {
public:
    virtual void handle(const SecurityEvent& event) = 0;
    virtual ~EventHandler() = default;
};

class ProcessEventHandler : public EventHandler {
public:
    void handle(const SecurityEvent& event) override {
        // 处理进程事件
    }
};

class Dispatcher {
private:
    unordered_map<int, unique_ptr<EventHandler>> handlers_;

public:
    void registerHandler(int type, unique_ptr<EventHandler> handler) {
        handlers_[type] = move(handler);
    }

    void dispatch(const SecurityEvent& event) {
        auto it = handlers_.find(event.type);
        if (it != handlers_.end()) {
            it->second->handle(event);
        }
    }
};

9. 面向对象的继承和多态特性在你的项目中有实际应用吗,请举例说明

答案:有,但不会把继承层次设计得太深。项目里比较适合继承和多态的是规则执行器。不同安全规则的匹配逻辑不一样,比如进程名黑名单、端口扫描检测、异常频率检测、文件路径匹配,它们都可以实现同一个 Rule 接口。策略引擎只持有 vector<unique_ptr<Rule>>,对每个事件依次调用 match(),这样新增规则类型时只需要新增派生类。

多态的好处是调度逻辑稳定,具体规则可扩展。缺点是虚函数调用有一点开销,而且如果规则数量非常多,逐条虚调用可能影响性能。实际优化时,可以把规则按事件类型、字段索引、风险等级预分组,减少无效匹配。

代码:

#include <string>
#include <vector>
#include <memory>
using namespace std;

struct Event {
    string processName;
    int port;
};

class Rule {
public:
    virtu

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

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

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

全部评论

相关推荐

评论
点赞
1
分享

创作者周榜

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