网易雷火服务端开发实习一面
好长时间没给大家分享过网易雷火的面经了,今天这位小伙伴刚好也投稿了,所以赶紧给大家分享一波。
在我印象中,网易雷火的笔试题特别难,没点东西恐怕还真是有点难。

1、前缀表达式求值(+-*2 3 5/...^2 4 6 = ?)
前缀表达式(也称为波兰式)是一种将运算符置于操作数之前的表达式表示方法。例如,中缀表达式 2 + 3 * 4 在前缀表达式中表示为 + 2 * 3 4。
前缀表达式的求值可以通过递归或栈来实现。以下是使用递归的方式来求解前缀表达式的步骤:
- 从左到右遍历表达式。
- 遇到操作数时,将其压入栈中。
- 遇到运算符时,从栈中弹出两个操作数,进行相应的运算,然后将结果压入栈中。
- 继续遍历表达式,直到最后得到的结果就是表达式的值。
以前缀表达式 + * 2 3 5 为例,按照上述步骤进行求解:
遇到 +,从栈中弹出 * 2 3 和 5,计算 2 * 3 = 6,然后将结果 6 和 5 相加,得到最终结果 11。
前缀表达式的求值可以通过递归的方式实现:
#include <iostream>
#include <sstream>
#include <vector>
int evaluatePrefixExpression(const std::vector<std::string>& tokens, int& index) {
if (index >= tokens.size()) {
// 边界情况:表达式为空
return 0;
}
// 当前操作符
const std::string& token = tokens[index++];
if (token == "+" || token == "-" || token == "*" || token == "/") {
// 遇到运算符,递归求解左右子表达式
int operand1 = evaluatePrefixExpression(tokens, index);
int operand2 = evaluatePrefixExpression(tokens, index);
if (token == "+") {
return operand1 + operand2;
} else if (token == "-") {
return operand1 - operand2;
} else if (token == "*") {
return operand1 * operand2;
} else if (token == "/") {
return operand1 / operand2;
}
} else {
// 遇到操作数,直接返回
return std::stoi(token);
}
// 默认返回0,表示表达式无效
return 0;
}
int main() {
std::vector<std::string> tokens = {"+", "*", "2", "3", "5"};
int index = 0;
int result = evaluatePrefixExpression(tokens, index);
std::cout << "Result: " << result << std::endl;
return 0;
}2、看代码的选择题,关于整数和字符相加,%c打印整数,%d打印字符的问题?
3、关于大小端的选择题,包括X86系统下是大端还是小端?
X86架构是小端序(Little Endian)的。在小端序中,低位字节存储在低地址,高位字节存储在高地址。例如,对于一个四字节的整数0x12345678,在内存中的存储顺序是0x78 0x56 0x34 0x12。这与大端序(Big Endian)相反,大端序中高位字节存储在低地址,低位字节存储在高地址。
在X86架构中,由于历史原因,选择了小端序作为默认的字节序。这种字节序的选择可以简化访问内存中多字节数据的操作,例如,通过指针访问一个整数时,可以直接按照顺序读取字节,而不需要考虑字节的顺序。
4、构造函数可以是虚函数吗?析构函数呢?
构造函数不能声明为虚函数。
构造函数的调用是静态绑定的,即在编译时确定的,而不是动态绑定的,因此构造函数不能是虚函数。因为在对象构造过程中,虚函数表还没有被构造完毕,无法实现动态绑定的特性。
析构函数可以是虚函数。
虚析构函数在释放对象时被调用,通常在需要通过基类指针来删除派生类对象时使用。如果类中存在虚函数,通常也会将析构函数声明为虚函数,以确保正确释放资源。
class Base {
public:
virtual ~Base() {}
};
class Derived : public Base {
public:
virtual ~Derived() {}
};5、new/delete和malloc/free的区别?
- 类型安全性:
new和delete是 C++ 中的运算符,可以为特定类型的对象分配和释放内存。在使用new时,编译器会自动计算所需的内存空间,并根据类型调用构造函数来初始化对象。而delete会调用析构函数来销毁对象,并释放相应的内存。malloc和free则是 C 标准库中的函数,它们操作的是void*类型的指针,不具备类型信息,需要显式地进行类型转换。因此,使用malloc和free时,需要手动管理内存分配和释放,包括计算所需的内存大小和调用构造和析构函数。
- 内存大小计算:
new和delete自动计算所需内存空间,不需要显式指定大小。malloc需要显式指定要分配的内存大小,而且返回的指针类型是void*,需要进行类型转换。
- 失败处理:
new在分配失败时会抛出std::bad_alloc异常,需要进行异常处理。malloc在分配失败时返回NULL,需要手动检查返回值并处理分配失败的情况。
- 对数组的支持:
new和delete支持对数组的动态内存分配和释放,可以使用[]运算符来处理数组。malloc和free不支持对数组的直接操作,需要手动计算数组的大小和释放每个元素的内存。
6、malloc申请100字节大小和申请100MB大小有什么区别?
- 内存分配方式:
- 申请 100 字节大小的内存通常由内存管理器从堆中的小块空闲内存中分配,这些小块内存通常称为内存池。这样的内存分配速度较快,因为只需要在内存池中找到一个足够大的空闲块即可。
- 申请 100MB 大小的内存则需要从堆中找到一块足够大的连续空闲内存块。对于大内存分配,内存管理器可能需要搜索整个堆来找到足够大的空闲块,这可能会导致分配速度较慢。
- 内存碎片问题:
- 申请小内存块容易产生内存碎片问题,即堆中存在很多小块的空闲内存,但这些小块的总大小无法满足大内存分配请求。这可能会导致无法分配出足够大的连续内存块,即使整个堆中的空闲内存总和足够。
- 申请大内存块则更容易引起内存碎片问题,因为大内存块的释放和重新分配可能会导致堆中的空闲内存被分割成许多小块,使得无法满足连续分配的请求。
- 内存分配效率:
- 申请小内存块的分配效率通常比较高,因为可以从内存池中快速找到足够大小的空闲块。
- 申请大内存块的分配效率可能较低,特别是在存在大量内存碎片的情况下,需要耗费更多时间来搜索和合并空闲内存块。
- 内存管理器的策略:
- 一些内存管理器在处理小内存分配时采用不同的策略,如使用内存池、使用分配器等,以提高分配效率和减少内存碎片。
- 对于大内存分配,内存管理器可能需要更多的算法和策略来处理内存碎片和效率问题,例如使用大块分配器或特殊的内存池管理。
7、函数参数在栈上的压栈顺序是什么?函数中的局部变量呢?
- 函数参数:
- 对于大多数 CPU 架构,函数参数的压栈顺序是从右向左的,即先压入最后一个参数,然后是倒数第二个参数,依此类推,最后压入第一个参数。
- 在一些特殊的 CPU 架构或编译器中,可能会有不同的规定。但在常见的 x86 和 x86-64 架构上,通常是从右向左压栈的。
- 局部变量:
- 局部变量的压栈顺序取决于其声明顺序和生命周期。通常,当函数被调用时,编译器会为局部变量在栈上分配空间,并根据其声明顺序依次压入栈中。
- 当局部变量超出作用域或函数执行结束时,编译器会自动回收这些局部变量所占用的栈空间。
给个代码展示函数参数和局部变量在栈上的压栈顺序:
#include <iostream>
void func(int a, int b, int c) {
int x = 1;
int y = 2;
int z = 3;
// 在这里可以访问 a, b, c, x, y, z
}
int main() {
func(10, 20, 30);
return 0;
}假设参数 a、b、c 的地址是连续的,那么它们在栈上的布局可能是:
| 30 (c) | 20 (b) | 10 (a) | return address |局部变量 x、y、z 的布局可能是:
| 3 (z) | 2 (y) | 1 (x) |8、在函数中创建数组,int a[100000000000000];会有什么问题吗?
- 栈空间不足:栈空间是有限的,通常只有几兆到几百兆不等。如果尝试在栈上分配一个如此巨大的数组,会导致栈溢出。因为栈空间是有限的,它通常比堆空间小得多,所以分配大型数据结构时应该使用堆空间。
- 内存分配效率低:即使栈空间足够大,也不推荐在栈上分配大量内存。在栈上分配内存比在堆上分配内存要快,但对于如此巨大的数组,栈空间的分配效率会很低,可能导致程序性能下降。
- 可能引起运行时错误:即使分配成功,访问如此巨大的数组也可能导致运行时错误,因为操作系统和硬件可能无法处理如此巨大的连续内存块。
对于需要大量内存的情况,应该使用堆空间来分配内存,例如使用 new 运算符或者标准库中的内存管理工具,如 std::vector。
#include <vector>
void func() {
std::vector<int> a(100000000000000); // 在堆上分配一个大型数组
// 使用 a
}9、什么是大小端?你怎样判断当前机器是大端还是小端?
大小端(Endian)是指在多字节数据存储时的字节序排列方式。在计算机系统中,一个整数由多个字节组成,如一个四字节(32位)的整数在内存中可能存储为0x12345678,那么这个整数的内存地址的最低字节(低地址)到最高字节(高地址)分别存储的是0x78、0x56、0x34、0x12,这种排列方式称为小端序(Little Endian)。相反,如果最高字节存储在最低地址,最低字节存储在最高地址,那么称为大端序(Big Endian)。
判断当前机器是大端还是小端可以通过以下方法:
- 查看网络字节序:网络字节序是大端序,可以通过查看网络数据传输的方式来确定当前机器的字节序。
- 利用联合体:可以使用联合体来查看一个整数在内存中的存储方式,例如:
#include <iostream>
int main() {
int num = 1;
char *ptr = reinterpret_cast<char*>(&num);
if (*ptr == 1) {
std::cout << "Little Endian" << std::endl;
} else {
std::cout << "Big Endian" << std::endl;
}
return 0;
}创建一个整数num,然后将其地址强制转换为一个指向字符的指针ptr,通过判断ptr指向的值是否为1来判断当前机器的字节序。
- 查看处理器手册:处理器手册通常会提供关于字节序的信息,可以查阅处理器手册来确定当前机器的字节序。
10、怎样判断一个编译器的栈区是从下到上增长还是从上到下增长的?
判断一个编译器的栈区是从下到上增长还是从上到下增长可以通过观察栈指针(Stack Pointer,SP)在函数调用过程中的变化方向来确定。在大多数情况下,栈是从高地址向低地址增长的(即从上到下增长),但也有一些特殊情况。
- 观察栈指针:在函数调用过程中,如果栈指针的值在逐渐减小,那么栈是从高地址向低地址增长的。可以通过以下示例代码来观察
#include <iostream>
void func() {
int a, b, c;
std::cout << "&a: " << &a << ", &b: " << &b << ", &c: " << &c << std::endl;
}
int main() {
func();
return 0;
}通过打印局部变量a、b、c的地址,可以观察栈指针的变化方向。
编译器文档:有些编译器的文档中会说明栈的增长方向,可以查阅相关文档来获取信息。
反汇编代码:可以通过反汇编函数代码来查看函数调用时栈指针的变化方向。
11、用static修饰局部变量会发生什么?这个变量还在栈上吗?
使用static修饰局部变量会改变变量的存储位置和生命周期,使其不再位于栈上。具体来说,static修饰的局部变量在程序运行期间只会被初始化一次,而不是每次函数调用都会初始化。此外,static修饰的局部变量的作用域仍然是局部的,但其生命周期延长到程序运行结束。
详细说明如下:
- 存储位置:
static修饰的局部变量存储在静态存储区(static storage area),而不是栈上。静态存储区是程序运行时分配的内存区域,其生命周期贯穿整个程序的运行周期。 - 生命周期:
static修饰的局部变量的生命周期从变量声明处开始,直到程序结束。这意味着即使函数执行完毕,该变量的值仍然保留在内存中,并且在下次函数调用时仍然有效。 - 初始化:
static修饰的局部变量只会被初始化一次,即使函数被调用多次。在首次调用函数时,静态局部变量会被初始化为其指定的初值(如果有指定初值),之后每次函数调用都会保留上一次的值。 - 线程安全:静态局部变量在多线程环境下是安全的,因为它们的初始化是在程序启动时进行的,并且不会被多个线程重复初始化。
12、Linux中,怎样快速在tmp.log日志文件中查找到error字段?
可以使用grep命令来快速查找日志文件中包含特定字段的行。
可以使用以下命令:
grep "error" tmp.log上述命令会在tmp.log文件中查找包含error字段的所有行,并将它们输出到标准输出。
如果要查找时忽略大小写,可以使用-i选项:
grep -i "error" tmp.log如果要查找匹配整个单词的行,可以使用-w选项:
grep -w "error" tmp.log如果要同时查找多个关键词,可以使用-e选项:
grep -e "error" -e "fail" tmp.log13、一个进程被异常终止后,怎样快速找到出错的地方?出错的常见原因?
- 查看日志和错误信息:首先查看进程的日志文件和标准错误输出,看是否有相关的错误信息。这些信息通常会指示出错的位置和可能的原因。
- 使用调试工具:可以使用调试工具(如gdb、lldb等)对进程进行调试,以定位出错的地方。通过设置断点、查看堆栈信息、跟踪变量值等方式,可以帮助找到导致进程异常终止的原因。
- 核心转储文件:如果进程产生了核心转储文件(core dump),可以使用调试工具分析核心转储文件,以了解进程异常终止时的状态和执行堆栈信息。
- 代码审查:对出错的代码进行仔细审查,检查是否有逻辑错误、内存泄漏、空指针引用等常见错误。
常见导致进程异常终止的原因包括:
- 内存错误:如内存泄漏、越界访问、空指针引用等。
- 异常信号:如段错误、浮点异常等。
- 系统调用失败:如文件打开失败、网络连接错误等。
- 资源耗尽:如内存耗尽、文件描述符耗尽等。
- 逻辑错误:如死锁、死循环等。
14、在gdb调试中,如何从当前函数的栈帧跳跃到其他函数的栈帧?
在gdb中,要从当前函数的栈帧跳转到其他函数的栈帧,可以使用up命令。这个命令会向上移动栈帧,并将当前上下文切换到指定的栈帧中。
步骤:
在当前函数中设置断点:首先在当前函数中设置一个断点,以便在跳转到其他函数的栈帧后能够回到当前函数的执行位置。
bash(gdb) break functionName运行程序:运行程序,使其停在设置的断点处。
bash(gdb) run跳转到其他函数的栈帧:使用
up命令跳转到其他函数的栈帧。例如,要跳转到调用当前函数的函数的栈帧,可以使用up命令:bash(gdb) up如果要跳转到更高层次的栈帧,可以多次使用
up命令。可以通过info frame命令查看当前所在的栈帧信息。查看当前栈帧的信息:在跳转到其他函数的栈帧后,可以使用
info locals、info args等命令查看当前栈帧的局部变量和参数信息。返回到原来的栈帧:在查看完其他函数的栈帧信息后,可以使用
down命令返回到原来的栈帧中。例如:bash(gdb) down
15、在析构函数中写delete this可以吗?在类中的其他函数中写呢?
析构函数中写
delete this:在析构函数中写delete this可以用于在对象生命周期结束时自动释放对象所占用的内存。但是需要注意以下几点:- 必须确保在
delete this之后不再访问已删除的对象,否则会导致未定义的行为。 - 使用
delete this会导致对象所在的内存空间被释放,因此在之后不能再使用对象的成员变量或调用对象的成员函数。 - 在使用
delete this时,要确保该对象是通过new运算符动态分配的,而不是在栈上或全局内存中创建的对象。
c++class MyClass { public: ~MyClass() { delete this; } };- 必须确保在
类的其他函数中写
delete this:在类的其他函数中写delete this是不安全的,会导致未定义的行为。因为对象在调用该函数时可能并不是在堆上动态分配的,而是在栈上或全局内存中创建的。在这种情况下,调用delete this会导致程序崩溃或其他意外行为。
16、将A端的结构体数据通过网络发送至b端,需要注意什么问题?
- 字节序(Endianness):发送端和接收端的字节序可能不同(大端序或小端序),需要在发送前将数据转换为网络字节序(通常是大端序),以确保接收端能正确解析数据。可以使用
htonl、htons等函数将主机字节序转换为网络字节序,使用ntohl、ntohs函数将网络字节序转换为主机字节序。 - 结构体对齐(Struct Padding):结构体在内存中的布局可能存在填充字节,导致结构体的大小不同于其成员变量大小之和。为避免填充字节对数据传输的影响,可以使用
#pragma pack指令或__attribute__((packed))属性来取消结构体的对齐。 - 数据序列化(Serialization):结构体数据需要序列化为字节流进行传输。可以自定义序列化和反序列化的函数,将结构体数据转换为字节流,并在接收端将字节流还原为结构体数据。
- 网络传输(Socket Programming):使用合适的网络传输方式(如TCP或UDP)发送数据。对于TCP连接,可以使用
send和recv函数发送和接收数据;对于UDP连接,可以使用sendto和recvfrom函数发送和接收数据。 - 数据完整性和可靠性:网络传输过程中可能会丢失数据或出现错误,因此需要考虑数据的完整性和可靠性。可以使用校验和、重传机制等方法来确保数据的完整传输。
- 协议一致性:发送端和接收端需要使用相同的协议来解析数据,包括数据的格式、序列化方式、字节序等。对于自定义协议,需要确保双方协商一致。
17、了解过哪些序列化反序列化的方式?
序列化(Serialization)和反序列化(Deserialization)是将数据转换为字节流或将字节流转换为数据的过程。
- JSON:JSON(JavaScript Object Notation)是一种轻量级的数据交换格式,易于阅读和编写。可以使用各种编程语言的JSON库来实现对象到JSON字符串的序列化,以及JSON字符串到对象的反序列化。
- XML:XML(eXtensible Markup Language)是一种标记语言,常用于数据的存储和传输。可以使用各种编程语言的XML库来实现对象到XML字符串的序列化,以及XML字符串到对象的反序列化。
- Protocol Buffers:Protocol Buffers是Google开发的一种高效的数据序列化格式,通常用于通信协议和数据存储。通过定义.proto文件来描述数据结构,然后使用相应的编译器生成序列化和反序列化的代码。
- MessagePack:MessagePack是一种轻量级的二进制数据交换格式,可以实现对象到MessagePack字节流的序列化,以及MessagePack字节流到对象的反序列化。
- BSON:BSON(Binary JSON)是一种二进制形式的JSON,用于在MongoDB等数据库中存储和交换数据。可以使用各种编程语言的BSON库来实现对象到BSON字节流的序列化,以及BSON字节流到对象的反序列化。
- Thrift:Thrift是由Facebook开发的一种跨语言的高性能数据通信框架,支持多种数据序列化格式(包括JSON、Binary等),可以根据IDL文件生成相应语言的代码,实现对象的序列化和反序列化。
- Avro:Avro是一种数据序列化系统,旨在支持大量语言。它使用JSON格式定义数据结构,并支持二进制数据序列化。
18、能不能想一些办法来解决UDP丟包的问题? (我感觉把TCP的内些方案说出来吧)
- 使用可靠性传输协议:可以在应用层实现一种简单的可靠性传输协议,通过在数据包中添加序列号、确认号、重传计时器等信息,实现数据包的可靠传输。
- 重传机制:当发送端发现数据包丢失时,可以通过定时器机制重传丢失的数据包,以确保数据的完整性。
- 前向纠错:可以在发送端对数据进行一定的编码,使得接收端即使丢失部分数据包也能通过已收到的数据包进行纠正。
- 增加重传次数:可以通过增加数据包的重传次数来提高数据包的可靠性,但同时也会增加网络负载。
- 使用数据包确认:接收端可以向发送端发送确认信息,告知发送端哪些数据包已经接收到,发送端可以根据确认信息进行重传。
- 使用滑动窗口:可以使用滑动窗口技术来提高数据包的传输效率和可靠性,确保数据包按顺序到达。
- 网络优化:优化网络环境,减少丢包率。可以通过增加带宽、改善网络拓扑、优化路由等方式来降低UDP丢包率。
19、socket套接字编程的一般流程?使用shutdown/close函数吗?
- 创建套接字(socket):调用socket函数创建一个套接字,指定协议族、套接字类型和协议。
- 绑定地址(bind):将套接字绑定到一个本地地址和端口号上,如果是服务器,通常会绑定到INADDR_ANY或具体的本地地址。
- 监听连接(listen):如果是服务器,调用listen函数开始监听连接请求。
- 接受连接(accept):如果是服务器,调用accept函数接受客户端的连接请求,返回一个新的套接字用于与客户端通信。
- 连接服务器(connect):如果是客户端,调用connect函数连接服务器。
- 发送和接收数据(send/recv):使用send和recv函数发送和接收数据。
- 关闭套接字(close/shutdown):通信完成后,调用close或shutdown函数关闭套接字。
在使用shutdown和close函数时,一般的做法是在通信结束后使用shutdown函数来优雅地关闭连接,然后再调用close函数关闭套接字。shutdown函数会发送一个关闭连接的消息给对方,而close函数则会释放套接字资源。如果直接调用close函数而没有先调用shutdown函数,可能会导致一些未发送完的数据被丢弃。
