当前位置: 首页 > 面试经验 >

5月8日联想一面面经

优质
小牛编辑
80浏览
2023-05-09

5月8日联想一面面经

1.epollfd队列问题

问:epoll的底层是什么?

epollfd是一个 epoll 实例(一个文件描述符),用于监听多个文件描述符的 IO 事件。 epoll 内部使用了红黑树和双向链表数据结构来管理被监听的文件描述符。

问:如果epoll服务器监听端口是80,现有1024个客户端连接,内部的红黑树发生什么变化?

当epoll服务器监听端口是80时,每个客户端连接都会生成一个epoll_event结构体,该结构体包含有该连接的文件描述符、事件类型和回调函数等信息,并添加到内部的红黑树上。

当有1024个客户端连接时,会有1024个epoll_event结构体被添加到内部的红黑树上,这会导致红黑树的大小增加到1024个节点,同时由于红黑树的自平衡性质,可能会引起一些节点的旋转和重新着色,以维持红黑树的平衡。

2.如何实现单机百万并发?

系统可用端口数只有:65535 - 1024 = 64511,每个TCP连接需要占用一个独立的端口,那最多也只能做到6W多并发连接。虽然端口号在同一个IP下不能重复,但我们可以给一个网卡绑定多个IP地址,如果单机要主动发起100万并发连接,我们最少需要使用17个IP地址。

对于TCP服务器连接数压力测试来说,瓶颈在客户端,因为每个客户端要连接到TCP服务端需要使用一个本地端口,而对于一个IP地址来说,端口范围就是:0-65536,其中还要一些端口被系统或其他程序使用。所以从一台主机单个IP上发起同TCP服务器的连接数理论最大值为65535,当然我们可以给该主机绑定N个IP地址,同时从多个IP发起连接,所以理论上客户端可以发起的连接数为:IP数*65535,这时客户端的CPU、内存和带宽以及文件句柄资源就是限制。

3.简述EPOLL的LT和ET的区别

在LT模式下,如果某个文件描述符可以读或写,就会一直通知应用程序。应用程序只有在处理完该文件描述符后才会停止接收通知。在这种模式下,如果应用程序没有及时处理文件描述符的事件,会导致一些事件被漏掉。

而在ET模式下,只有当文件描述符从未就绪到就绪时,才会通知应用程序。一旦通知了就绪事件,应用程序必须尽可能快地处理该事件,否则下次将不会再次通知。因此,ET模式下对于每个就绪事件,应用程序只需要处理一次,这种模式下效率更高,避免了事件被漏掉的情况。

因此,ET模式相较于LT模式的优点是更加高效,对于高并发的应用程序,特别是对于处理大量并发连接的服务器,使用ET模式可以更好地发挥系统性能。但是,ET模式也更加复杂,需要应用程序处理更多的细节。

4.阻塞和非阻塞,同步和异步是什么

阻塞和非阻塞描述的是程序在等待系统返回数据时的状态。如果程序在等待数据时会一直阻塞等待直到数据返回,那么就是阻塞的;如果程序在等待数据时不会一直阻塞等待,而是会不断地查询数据是否准备好,那么就是非阻塞的。

同步和异步描述的是程序执行任务的方式。如果程序要自己主动地等待某个操作的完成,而不是等待操作完成后再去执行其他操作,那么就是同步的;如果程序不需要自己主动等待某个操作的完成,而是在操作完成后由操作系统通知程序,那么就是异步的。

综上所述,阻塞和非阻塞主要关注的是程序等待数据时的状态,而同步和异步则是关注程序执行任务的方式。阻塞和同步都是程序需要主动等待操作完成后再去执行其他操作的方式,而非阻塞和异步都是程序不需要主动等待操作完成的方式。

5.有限状态机用了哪些设计模式?

状态模式:状态机的核心是状态的转移,状态模式可以帮助将每一个状态封装为一个类,使得状态转移更加清晰、简单。状态模式也可以提高代码的可扩展性,因为在状态增加、修改时只需要修改对应的类,不会影响其他状态的实现。

观察者模式:状态机的状态转移通常是由外部事件触发的,观察者模式可以帮助实现事件和状态之间的解耦,当事件触发时,通知观察者进行状态转移。

单例模式:状态机通常只需要一个实例,使用单例模式可以确保只有一个状态机实例,并且可以方便地在整个程序中访问状态机。

但是在本项目中,我们的有限状态机并没有明显地使用某种特定的设计模式。它主要是实现了一个状态机,通过不同的状态和状态之间的转换来完成对HTTP请求报文的解析。这种方式可以看作是一种基于状态的行为设计模式。

6.cpp多态

class A{
  virtual void function(){}
}
class B: public A
{
	void function(){}override
}
B b;
A *a = b;
a->function();a执行的是A和function还是B的function。

在代码中,使用了指向派生类对象的基类指针a,通过该指针调用虚函数function()时,实际上是调用派生类B中的函数function(),因为B类重写了A类的虚函数。

如果我们使用override标记了某个函数,但该函数并没有覆盖虚函数,则编译器报错

7.auto的用法

(1)自动类型推导:auto可以用来让编译器自动推导变量的类型,根据变量初始化表达式的类型来确定变量类型

(2)迭代器声明:auto可以用于在for循环中自动推导迭代器类型

(3)decltype(auto)是C++14新增的类型指示符,可以用来声明变量以及指示函数返回类型。在使用时,会将“=”号右边的表达式替换掉auto,再根据decltype的语法规则来确定类型

int e = 4;
const int* f = &e; // f是底层const
decltype(auto) j = f;//j的类型是const int* 并且指向的是e

8.对于一个有5个元素的vector<string>,元素分别为"1","2","3","4","5",使用迭代器进行遍历,如果在遍历到"3"的时候将其删除,下一个遍历的是多少

结论:下一个遍历4

如果使用迭代器遍历,删除元素后,该迭代器会失效,需要使用返回值重新获取迭代器。可以使用erase函数删除元素并返回删除后的下一个元素的迭代器

std::vector<std::string> vec{"1", "2", "3", "4", "5"};
auto it = vec.begin();
while (it != vec.end()) {
    if (*it == "3") {
        it = vec.erase(it); // 删除"3"并获取下一个元素的迭代器,此时别++it
    } else {
        ++it;
    }
}

9.如何理解cout<<中的"<<",什么是namespace

cout是类ostream中的一个对象,这个对象有一个成员重载运算符函数operator<<,类ostream又属于iostream库中,头文件中的写法应该是

//iostream
class ostream
{
  public:
  	ostream operator<<(int n ){输出 n}
  	ostream operator<<(double n){输出 n}
  //...
 }ostream cout;


为了解决不同程序员编码的名字冲突问题,引入了名字空间这个概念,加入不写using namespace std,每一个cout都要写成std::cout

10.引用&和指针*的区别

(1)声明方式不同:引用使用&来声明,指针使用*来声明

(2)操作符重载:引用在使用时不需要使用任何操作符,因为它本身就是一个别名,直接使用即可。指针则需要使用解引用运算符 * 来访问所指向的对象。

(3)空值:指针可以被赋值为空指针,即 nullptr 或者 NULL。而引用不允许被赋值为空值,因为引用必须始终引用某个对象。

(4)引用不可变:一旦引用被初始化指向一个对象,它将一直指向该对象,不可更改。指针则可以被重新赋值指向另一个对象。因此,引用更适用于作为函数参数传递或者作为返回值,因为它更加安全,而指针则更适用于需要动态分配内存或者需要对内存进行操作的场合。

(5)对象别名:引用是对对象的一个别名,当对引用进行操作时,实际上是对所引用的对象进行操作,因此操作的是同一个对象。指针则是一个对象,可以对指针进行操作,也可以通过指针来操作所指向的对象。

总的来说,引用更加安全,因为它不允许为空,并且不可更改指向的对象,而指针则更加灵活,因为它可以重新赋值指向不同对象。在使用时应该根据具体情况选择使用哪种方式。

int a = 10, b = 20;
const int &c = a;//这是一个 const 引用。它创建了一个名为 c 的引用,指向变量 a 的地址,并且可以通过 c 来读取变量 a 的值。由于是 const 引用,因此无法通过 c 来修改变量 a 的值。这意味着下面的代码是非法的:
 c = 30;  // 非法,无法通过 c 修改 a 的值

11.简述main函数的两个参数

在C++中,main函数有两个参数,分别是argc和argv。

argc(argument count)表示命令行参数的个数,包括命令本身,至少为1

argv(argument vector)是一个指针数组,每个元素指向一个命令行参数的字符串。第一个元素指向命令本身的名称,后面的元素依次指向各个参数。

例如,在执行命令行程序./my_program arg1 arg2 arg3时,argc为4,argv数组中的元素分别为:

argv[0] = "./my_program"
argv[1] = "arg1"
argv[2] = "arg2"
argv[3] = "arg3"

12.如果linux中一个elf可执行文件a.out,执行./a.out和a.out有什么区别

在Linux中,使用命令行运行可执行文件时,需要使用"./"表示当前目录。因此,使用"./a.out"运行a.out文件,是指在当前目录下执行a.out文件。而直接使用"a.out"运行a.out文件,则需要在系统的环境变量PATH所包含的路径中搜索可执行文件,如果没有找到则会报错。因此,"./a.out"和"a.out"的区别在于指定可执行文件的路径方式不同。

13.linux网络编程中,一个进程最大允许打开的socket文件描述符的个数是多少、线程呢?

在Linux系统中,每个进程默认的最大文件描述符数是1024。这个限制可以通过修改系统参数进行修改,但是需要注意的是,如果设置过高,可能会导致系统性能下降,甚至崩溃。在实际编程中,应该注意对文件描述符的管理和回收,以避免不必要的资源浪费和程序异常。

当一个进程启动多个线程时,每个线程共享进程的文件描述符表,因此它们共享相同的文件描述符限制。因此,该进程的所有线程的文件描述符限制都是相同的,即与进程本身的文件描述符限制相同。

 类似资料: