金山世游 客户端开发 一面

1. 问了游戏相关的经历,有没有用过游戏引擎,做过什么项目

2. 项目怎么实现的,用了什么语言,为什么用 C++,有没有用 Redis,项目是多线程吗

答案:这个项目核心服务用 C++ 写,主要是因为房间状态推进、消息广播、序列化和连接管理对延迟和资源控制比较敏感,C++ 在对象生命周期、内存布局和性能调优上可控性更强。整体架构上分为网关层、房间服务、匹配服务、回放服务和存储层。网关层维护 TCP/WebSocket 长连接,房间服务负责创建房间、玩家加入退出、输入收集和固定帧率推进;匹配服务只做轻量匹配;回放服务异步消费对局关键帧并落盘。Redis 可以用在房间索引、玩家在线状态、匹配队列和临时会话信息上,但不会把每一帧完整状态都强依赖 Redis,因为帧数据量大、写入频繁,更适合批量落盘或者进入专门的日志存储。项目是多线程的,一般是 IO 线程处理连接读写,业务线程处理房间逻辑。房间会固定绑定到某个业务线程,避免多个线程同时修改同一个房间状态。

3. C++11 以上你常用的新特性有哪些

答案:C++11 之后比较常用的是右值引用、移动语义、智能指针、lambda、auto、范围 for、线程库、原子操作、nullptroverrideconstexpr。C++14/17 里我常用泛型 lambda、结构化绑定、if constexprstd::optionalstd::variantstd::string_view。实际项目里最有价值的还是移动语义、智能指针和 lambda。移动语义能减少消息对象、缓冲区和任务投递时的拷贝;智能指针能把异步对象生命周期管住;lambda 在回调、定时器和线程任务里很方便。不过这些特性不是越多越好,像 auto、模板和 lambda 如果滥用,也会让代码可读性变差。

代码:

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

optional<int> parseRoomId(string_view s) {
    if (s.empty()) return nullopt;
    int x = 0;
    for (char c : s) {
        if (c < '0' || c > '9') return nullopt;
        x = x * 10 + (c - '0');
    }
    return x;
}

int main() {
    if (auto id = parseRoomId("1024"); id.has_value()) {
        cout << *id << endl;
    }
    return 0;
}

4. 智能指针有哪些,unique_ptrshared_ptr 的区别

答案:常见智能指针有 unique_ptrshared_ptrweak_ptrunique_ptr 表示独占所有权,不能拷贝,只能移动,适合表达“这个资源只归一个对象管”。它开销小,语义清楚,比如连接对象内部独占的 buffer、定时器句柄、文件句柄封装都可以用它。shared_ptr 表示共享所有权,底层通过控制块维护引用计数,适合对象会被多个异步任务、回调或者容器共同持有的情况。但 shared_ptr 不是默认答案,它会带来原子引用计数开销,也容易让对象生命周期变得不直观。能用 unique_ptr 表达清楚的地方,不应该随手换成 shared_ptr

代码:

#include <memory>
using namespace std;

struct PlayerSession {
    int fd;
};

int main() {
    unique_ptr<PlayerSession> s1 = make_unique<PlayerSession>();
    s1->fd = 10;

    shared_ptr<PlayerSession> s2 = make_shared<PlayerSession>();
    auto s3 = s2; // 引用计数增加
    return 0;
}

5. shared_ptr 循环引用是什么问题,怎么解决

答案:循环引用是指两个对象互相用 shared_ptr 持有对方,导致引用计数永远无法归零,即使外部已经不再使用它们,对象也不会析构。比如房间对象持有玩家对象,玩家对象又持有房间对象,如果两边都是 shared_ptr,就容易产生环。解决方式是把其中一侧改成 weak_ptr,让它只观察对象,不参与所有权管理。访问时通过 lock() 临时拿到 shared_ptr,如果对象已经释放,lock() 会返回空。工程里一般会先明确所有权:房间拥有玩家,玩家可以弱引用房间;或者由统一的管理器拥有对象,其他模块只保存弱引用或 ID。

代码:

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

struct Room;

struct Player {
    weak_ptr<Room> room;
};

struct Room {
    vector<shared_ptr<Player>> players;
};

int main() {
    auto room = make_shared<Room>();
    auto player = make_shared<Player>();
    player->room = room;
    room->players.push_back(player);
    return 0;
}

6. 为什么需要 weak_ptr,直接用普通指针行不行

答案:普通指针可以用,但它只表示地址,不知道对象还活不活。在异步回调、定时器、跨线程任务里,如果对象已经销毁,普通指针还被回调访问,就会变成悬空指针。weak_ptr 的意义是配合 shared_ptr 做生命周期观察,不延长对象生命周期,但访问前可以通过 lock() 判断对象是否仍然存在。如果对象生命周期非常明确,并且不会跨异步边界,普通指针可以作为非拥有引用使用;但在网络连接、房间定时器、延迟任务这些场景里,weak_ptr 更安全。

代码:

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

struct Room {
    void tick() { cout << "room tick\n"; }
};

void postTimer(weak_ptr<Room> wroom) {
    if (auto room = wroom.lock()) {
        room->tick();
    }
}

7. 其他语言的垃圾回收机制了解吗,引用计数垃圾回收有什么问题

答案:了解一些。像 Java、Go 这类语言一般有运行时 GC,会根据可达性分析来回收不再使用的对象。它的好处是程序员不需要手动释放大部分对象,坏处是 GC 会带来额外运行时成本,并且在某些情况下可能影响延迟。引用计数的思路是对象被引用一次计数加一,引用消失计数减一,减到 0 就释放。它释放时机比较确定,但主要问题是循环引用无法自动处理,并且频繁增减引用计数也有开销。C++ 的 shared_ptr 更像是库层面的引用计数工具,不是完整 GC。它不会自动扫描对象图,所以一旦有循环引用,需要开发者主动用 weak_ptr 断环。

8. auto 的推导规则怎么理解,引用和 const 会不会丢

答案:auto 的推导规则和模板类型推导很像。如果写 auto x = expr,顶层 const 通常会被丢掉,引用也不会保留,x 会变成一个新的值对象。如果想保留引用,要写 auto&;如果想保留 const 引用,写 const auto&;如果想完美保留值类别,可以用 decltype(auto)。所以 auto 不是简单“自动照抄类型”,不同写法会得到不同结果。实际项目里如果遍历大对象容器,通常会写 const auto&,避免不必要拷贝。

代码:

#include <vector>
using namespace std;

int main() {
    const int a = 10;

    auto x = a;        // int,顶层 const 丢失
    const auto y = a;  // const int
    auto& r = x;       // int&
    const auto& cr = a;// const int&

    vector<string> v = {"a", "b"};
    for (const auto& s : v) {
        // 避免拷贝 string
    }
    return 0;
}

9. auto 导致类型信息丢失的问题怎么解决,decltypedecltype(auto) 有什么区别

答案:如果 auto 推导后不符合预期,通常要看自己到底想要值、引用还是 const 引用。普通场景可以显式写 auto&const auto&auto&&。如果要完全按照表达式规则保留类型,就用 decltypedecltype(auto)decltype(expr) 会根据表达式形式推导类型,比如变量名得到声明类型,带括号的左值表达式可能得到引用类型。decltype(auto) 常用于函数返回值,让返回类型跟 return 表达式保持一致。不过 decltype(auto) 也容易把局部变量引用返回出去,所以使用时要更谨慎。

代码:

#include <iostream>
using namespace std;

int global_value = 10;

decltype(auto) getRef() {
    return (global_value); // int&
}

int main() {
    auto a = getRef();      // int,拷贝
    decltype(auto) b = getRef(); // int&
   

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

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

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

全部评论

相关推荐

评论
点赞
收藏
分享

创作者周榜

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