2020年美团c/c++ linux后台开发岗精选面试题及答案

  1. 在ACM竞赛中,一支队伍由三名队员组成,现在有N+M名学生,其中有N名学生擅长算法,剩下M名学生擅长编程,这些学生要参加ACM竞赛,他们的教练要求每支队伍至少有一名擅长算法和一名擅长编程的学生,那么这些学生最多可以组成多少支队伍?
    输入: 输入两个整数M,N,其中1<N,M<10000000

输出: 最多可以组成的队伍数

#include
using namespace std;

int main()
{
int cnt = 0,n,m;
cout << “输入N个擅长算法的,M个擅长编程的:” << endl;
cin >> n >> m;
while(n!=0&&m!=0&&m+n!=2){
if(n>=m){
n = n-2;
m = m-1;
cnt++;
}
else if(n<m){
m = m-2;
n = n-1;
cnt++;
}
}
cout << “最大组对数量” << cnt << endl;
return 0;
}
2. REST请求中哪些是幂等操作
GET,PUT,DELETE都是幂等操作,而POST不是

分析

首先GET请求很好理解,对资源做查询多次,此实现的结果都是一样的。

PUT请求的幂等性可以这样理解,将A修改为B,它第一次请求值变为了B,再进行多次此操作,最终的结果还是B,与一次执行的结果是一样的,所以PUT是幂等操作。

同理可以理解DELETE操作,第一次将资源删除后,后面多次进行此删除请求,最终结果是一样的,将资源删除掉了。

POST不是幂等操作,因为一次请求添加一份新资源,二次请求则添加了两份新资源,多次请求会产生不同的结果,因此POST不是幂等操作。

  1. 根据幂等性区分POST与PUT的使用
    可根据idempotent(幂等性)做区分。

举一个简单的例子,假如有一个博客系统提供一个Web API,模式是这样http://superblogging/blogs/{blog-name},很简单,将{blog-name}替换为我们的blog名字,往这个URL发送一个HTTP PUT或者POST请求,HTTP的body部分就是博文,这是一个很简单的REST API例子。

我们应该用PUT方法还是POST方法?

取决于这个REST服务的行为是否是idempotent的,假如我们发送两个http://superblogging/blogs/post/Sample请求,服务器端是什么样的行为?如果产生了两个博客帖子,那就说明这个服务不是idempotent的,因为多次使用产生了副作用了嘛;如果后一个请求把第一个请求覆盖掉了,那这个服务就是idempotent的。前一种情况,应该使用POST方法,后一种情况,应该使用PUT方法。

  1. CAS的缺点及解决.
    CAS的缺点有如ABA问题,自旋锁消耗问题、多变量共享一致性问题.

1.ABA:

问题描述:线程t1将它的值从A变为B,再从B变为A。同时有线程t2要将值从A变为C。但CAS检查的时候会发现没有改变,但是实质上它已经发生了改变 。可能会造成数据的缺失。

解决方法:CAS还是类似于乐观锁,同数据乐观锁的方式给它加一个版本号或者时间戳,如AtomicStampedReference

2.自旋消耗资源:

问题描述:多个线程争夺同一个资源时,如果自旋一直不成功,将会一直占用CPU。

解决方法:破坏掉for死循环,当超过一定时间或者一定次数时,return退出。JDK8新增的LongAddr,和ConcurrentHashMap类似的方法。当多个线程竞争时,将粒度变小,将一个变量拆分为多个变量,达到多个线程访问多个资源的效果,最后再调用sum把它合起来。

虽然base和cells都是volatile修饰的,但感觉这个sum操作没有加锁,可能sum的结果不是那么精确。

2.多变量共享一致性问题:

解决方法: CAS操作是针对一个变量的,如果对多个变量操作,

  1. 可以加锁来解决。

  2. 封装成对象类解决。

  1. C++11创建线程的三种方式
  2. 通过函数

thread:标准库的类

join:阻塞主线程并等待

// MultiThread.cpp : Defines the entry point for the console application.
#include “stdafx.h”
#include
#include
#include
#include
#include

using namespace std;
void myPrint()
{
cout << “线程开始运行” << endl;
cout << “线程运行结束了” << endl;
}

int main()
{
std::thread my2Obj(myPrint); // 可调用对象
my2Obj.join();// 主线程阻塞在这,并等待myPrint()执行完
cout << “wangtao” << endl;
return 0;
}
detach(): 将主线程和子线程完全分离,子线程会驻留在后台运行,被C++运行时库接管,失去控制

void myPrint()
{
cout << “线程开始运行1” << endl;
cout << “线程开始运行2” << endl;
cout << “线程开始运行3” << endl;
cout << “线程开始运行4” << endl;
cout << “线程开始运行5” << endl;
cout << “线程开始运行6” << endl;
cout << “线程开始运行7” << endl;
cout << “线程开始运行8” << endl;
cout << “线程开始运行9” << endl;

}

int main()
{
std::thread my2Obj(myPrint); // 主线程阻塞在这,并等待myPrint()执行完
my2Obj.detach();
cout << “wangtao1” << endl;
cout << “wangtao2” << endl;
cout << “wangtao3” << endl;
cout << “wangtao4” << endl;
cout << “wangtao5” << endl;
cout << “wangtao6” << endl;
cout << “wangtao7” << endl;
cout << “wangtao8” << endl;
return 0;
}
joinable():判断是否可以成功使用join()或者detach()

程序说明:detach后不能在实施join

int main()
{
std::thread my2Obj(myPrint); // 主线程阻塞在这,并等待myPrint()执行完
if (my2Obj.joinable()){
cout << “1:joinable() == true” << endl;
}
else {
cout << “1:joinable() == false” << endl;
}
my2Obj.detach();

if (my2Obj.joinable()) {
cout << “2:joinable() == true” << endl;
}
else {
cout << “2:joinable() == false” << endl;
}
cout << “wangtao1” << endl;
cout << “wangtao2” << endl;
cout << “wangtao3” << endl;
cout << “wangtao4” << endl;
cout << “wangtao5” << endl;
cout << “wangtao6” << endl;
cout << “wangtao7” << endl;
cout << “wangtao8” << endl;
return 0;
}

int main()
{
std::thread my2Obj(myPrint); // 主线程阻塞在这,并等待myPrint()执行完
if (my2Obj.joinable()){
my2Obj.join();
}
cout << “wangtao1” << endl;
cout << “wangtao2” << endl;
cout << “wangtao3” << endl;
cout << “wangtao4” << endl;
cout << “wangtao5” << endl;
cout << “wangtao6” << endl;
cout << “wangtao7” << endl;
cout << “wangtao8” << endl;
return 0;
}
2.通过类对象创建线程

class CObject
{
public:
void operator ()() {
cout << “线程开始运行” << endl;
cout << “线程结束运行” << endl;
}
};

int main()
{
CObject obj;
std::thread my2Obj(obj); // 主线程阻塞在这,并等待myPrint()执行完
if (my2Obj.joinable()){
my2Obj.join();
}
cout << "see you " << endl;

return 0;

}

class CObject
{
int& m_obj;
public:
CObject(int& i) :m_obj(i) {}
void operator ()() { // 不带参数
cout << “线程开始运行1” << endl;
cout << “线程开始运行2” << endl;
cout << “线程开始运行3” << endl;
cout << “线程开始运行4” << endl;
cout << “线程开始运行5” << endl;
}
};
int main()
{
int i = 6;
CObject obj(i);
std::thread my2Obj(obj); // 主线程阻塞在这,并等待myPrint()执行完
if (my2Obj.joinable()){
my2Obj.detach();
}
cout << "see you " << endl;

return 0;

}

用detach() 主线程结束对象即被销毁,那么子线程的成员函数还能调用吗?

这里的的对象会被复制到子线程中,当主线程结束,复制的子线程对象并不会被销毁

只要是没有引用、指针就不会出现问题

通过复制构造函数和析构函数来验证对象是否复制到了子线程中

// MultiThread.cpp : Defines the entry point for the console application.
//

#include “stdafx.h”
#include
#include
#include
#include
#include
using namespace std;
class CObject
{
int& m_obj;
public:
CObject(int& i) :m_obj(i) {
cout << “ctor” << endl;
}
CObject(const CObject& m) :m_obj(m.m_obj) {
cout << “copy ctor” << endl;
}
~CObject(){
cout << “dtor” << endl;
}
void operator ()() { // 不带参数
cout << “线程开始运行1” << endl;
cout << “线程开始运行2” << endl;
cout << “线程开始运行3” << endl;
cout << “线程开始运行4” << endl;
cout << “线程开始运行5” << endl;
}
};
int main()
{
int i = 6;
CObject obj(i);
std::thread my2Obj(obj); // 主线程阻塞在这,并等待myPrint()执行完
if (my2Obj.joinable()){
my2Obj.detach();
}
cout << "see you " << endl;

return 0;

}
子线程的析构函数在后台执行,所以输出的dtor是主线程的。

3.通过lambda表达式创建线程

int main()
{
auto myLamThread = [] {
cout << “线程开始运行” << endl;
cout << “线程结束运行” << endl;
};
thread cthread(myLamThread);
cthread.join();
std::cout << "see you " << endl;

return 0;

}
6. 宏定义的优缺点
优点:

  1. 提高了程序的可读性,同时也方便进行修改;

  2. 提高程序的运行效率:使用带参的宏定义既可完成函数调用的功能,又能避免函数的出栈与入栈操作,减少系统开销,提高运行效率;

3.宏是由预处理器处理的,通过字符串操作可以完成很多编译器无法实现的功能。比如##连接符。

缺点:

  1. 由于是直接嵌入的,所以代码可能相对多一点;

  2. 嵌套定义过多可能会影响程序的可读性,而且很容易出错;

  3. 对带参的宏而言,由于是直接替换,并不会检查参数是否合法,存在安全隐患。

  4. Linux查看文件大小命令.

  5. 使用stat命令查看

stat命令一般用于查看文件的状态信息。stat命令的输出信息比ls命令的输出信息要更详细。

  1. 使用wc命令

wc命令一般用于统计文件的信息,比如文本的行数,文件所占的字节数。

  1. 使用du命令

du命令一般用于统计文件和目录所占用的空间大小。

  1. 使用ls命令

ls 命令一般用于查看文件和目录的信息,包括文件和目录权限、拥有者、所对应的组、文件大小、修改时间、文件对应的路径等等信息。

  1. 使用ll命令(其实就是ls -l的别名)

在大部分的Linux系统中,都已经设置了ls -l的别名为ll,所以并不存在ll的命令,ll只是一个别名命令而已。

Linux使用ll命令查看文件大小

  1. 布隆过滤算法
    布隆过滤器使用二进制向量结合hash函数来记录任意一条数据是否已经存在于集合中。

布隆过滤器的执行流程为:

首先申请包含SIZE个bit位的Bit集合,并将所有Bit置0。

然后使用数种(k)不同的哈希函数对目标数据进行哈希计算并得到k个哈希值(确保哈希值不超过SIZE大小),然后将Bit集合中以哈希值为下标所处的bit值置为1,由于使用了k个哈希函数,因此记录一条数据的信息将在Bit集合中把k个bit值置为1。

由于哈希函数的稳定性,任意两条相同的数据在Bit集合中所对应的k个bit位置是完全相同的。那么在检测某一条数据是否已经在Bit集合中有记录时,只需检测该条数据的k个哈希值在Bit集合中对应的位置的bit是否均已被标记为1,相反的只要其存在一个哈希值对应的bit位置未被标记为1,则证明该值未被记录过。

使用示例

布隆过滤器的示例如下:在这里插入图片描述

大体过程为:

首先初始化一个二进制的数组,长度设为 L(图中为 8),同时初始值全为 0 。

当写入一个 A1=1000 的数据时,需要进行 H 次 hash 函数的运算(这里为 2 次);与 HashMap 有点类似,通过算出的 HashCode 与 L 取模后定位到 0、2 处,将该处的值设为 1。

A2=2000 也是同理计算后将 4、7 位置设为 1。

当有一个 B1=1000 需要判断是否存在时,也是做两次 Hash 运算,定位到 0、2 处,此时他们的值都为 1 ,所以认为 B1=1000 存在于集合中。

当有一个 B2=3000 时,也是同理。第一次 Hash 定位到 index=4 时,数组中的值为 1,所以再进行第二次 Hash 运算,结果定位到 index=5 的值为 0,所以认为 B2=3000 不存在于集合中。

优缺点

优点

时间复杂度为O(n),且布隆过滤器不需要存储元素本身,使用位阵列,占用空间也很小。

缺点

通过布隆过滤,我们能够准确判断一个数不存在于某个集合中,但对于存在于集合中这个结论,布隆过滤会有误报(可能存在两组不同数据但其多个哈希值完全一样的情况)。但是通过控制Bit集合的大小(即SIZE)以及哈希函数的个数,可以将出现冲突的概率控制在极小的范围内,或者通过额外建立白名单的方式彻底解决哈希冲突问题。

误判率计算公式为(1 – e(-nk/SIZE))k,其中n为目标数据的数量,SIZE为Bit集合大小,k为使用的哈希函数个数;假设现有一千万条待处理数据,Bit集合大小为2^30(约10亿,即占用内存128MB),使用9个不同的哈希函数,计算可得任意两条数据其9次哈希得到的哈希值均相同(不考虑顺序)的概率为2.6e-10,约为38亿分之一。

  1. 请问共享内存相关api?
    Linux允许不同进程访问同一个逻辑内存,提供了一组API,头文件在sys/shm.h中。

1)新建共享内存shmget

int shmget(key_t key,size_t size,int shmflg);

key:共享内存键值,可以理解为共享内存的唯一性标记。

size:共享内存大小

shmflag:创建进程和其他进程的读写权限标识。

返回值:相应的共享内存标识符,失败返回-1

2)连接共享内存到当前进程的地址空间shmat

void *shmat(int shm_id,const void *shm_addr,int shmflg);

shm_id:共享内存标识符

shm_addr:指定共享内存连接到当前进程的地址,通常为0,表示由系统来选择。

shmflg:标志位

返回值:指向共享内存第一个字节的指针,失败返回-1

3)当前进程分离共享内存shmdt

int shmdt(const void *shmaddr);

4)控制共享内存shmctl

和信号量的semctl函数类似,控制共享内存

int shmctl(int shm_id,int command,struct shmid_ds *buf);

shm_id:共享内存标识符

command: 有三个值

IPC_STAT:获取共享内存的状态,把共享内存的shmid_ds结构复制到buf中。

IPC_SET:设置共享内存的状态,把buf复制到共享内存的shmid_ds结构。

IPC_RMID:删除共享内存

buf:共享内存管理结构体。

  1. 请问reactor模型组成.
    reactor模型要求主线程只负责监听文件描述上是否有事件发生,有的话就立即将该事件通知工作线程,除此之外,主线程不做任何其他实质性的工作,读写数据、接受新的连接以及处理客户请求均在工作线程中完成。其模型组成如下:在这里插入图片描述

1)Handle:即操作系统中的句柄,是对资源在操作系统层面上的一种抽象,它可以是打开的文件、一个连接(Socket)、Timer等。由于Reactor模式一般使用在网络编程中,因而这里一般指Socket Handle,即一个网络连接。

2)Synchronous Event Demultiplexer(同步事件复用器):阻塞等待一系列的Handle中的事件到来,如果阻塞等待返回,即表示在返回的Handle中可以不阻塞的执行返回的事件类型。这个模块一般使用操作系统的select来实现。

3)Initiation Dispatcher:用于管理Event Handler,即EventHandler的容器,用以注册、移除EventHandler等;另外,它还作为Reactor模式的入口调用Synchronous Event Demultiplexer的select方法以阻塞等待事件返回,当阻塞等待返回时,根据事件发生的Handle将其分发给对应的Event Handler处理,即回调EventHandler中的handle_event()方法。

4)Event Handler:定义事件处理方法:handle_event(),以供InitiationDispatcher回调使用。

5)Concrete Event Handler:事件EventHandler接口,实现特定事件处理逻辑。

  1. 请问fork和vfork的区别?
    fork的基础知识:

fork:创建一个和当前进程映像一样的进程可以通过fork( )系统调用:

#include <sys/types.h>

#include <unistd.h>

pid_t fork(void);

成功调用fork( )会创建一个新的进程,它几乎与调用fork( )的进程一模一样,这两个进程都会继续运行。在子进程中,成功的fork( )调用会返回0。在父进程中fork( )返回子进程的pid。如果出现错误,fork( )返回一个负值。

最常见的fork( )用法是创建一个新的进程,然后使用exec( )载入二进制映像,替换当前进程的映像。这种情况下,派生(fork)了新的进程,而这个子进程会执行一个新的二进制可执行文件的映像。这种“派生加执行”的方式是很常见的。

在早期的Unix系统中,创建进程比较原始。当调用fork时,内核会把所有的内部数据结构复制一份,复制进程的页表项,然后把父进程的地址空间中的内容逐页的复制到子进程的地址空间中。但从内核角度来说,逐页的复制方式是十分耗时的。现代的Unix系统采取了更多的优化,例如Linux,采用了写时复制的方法,而不是对父进程空间进程整体复制。

vfork的基础知识:

在实现写时复制之前,Unix的设计者们就一直很关注在fork后立刻执行exec所造成的地址空间的浪费。BSD的开发者们在3.0的BSD系统中引入了vfork( )系统调用。

#include <sys/types.h>

#include <unistd.h>

pid_t vfork(void);

除了子进程必须要立刻执行一次对exec的系统调用,或者调用_exit( )退出,对vfork( )的成功调用所产生的结果和fork( )是一样的。vfork( )会挂起父进程直到子进程终止或者运行了一个新的可执行文件的映像。通过这样的方式,vfork( )避免了地址空间的按页复制。在这个过程中,父进程和子进程共享相同的地址空间和页表项。实际上vfork( )只完成了一件事:复制内部的内核数据结构。因此,子进程也就不能修改地址空间中的任何内存。

vfork( )是一个历史遗留产物,Linux本不应该实现它。需要注意的是,即使增加了写时复制,vfork( )也要比fork( )快,因为它没有进行页表项的复制。然而,写时复制的出现减少了对于替换fork( )争论。实际上,直到2.2.0内核,vfork( )只是一个封装过的fork( )。因为对vfork( )的需求要小于fork( ),所以vfork( )的这种实现方式是可行的。

补充:写时复制

Linux采用了写时复制的方法,以减少fork时对父进程空间进程整体复制带来的开销。

写时复制是一种采取了惰性优化方法来避免复制时的系统开销。它的前提很简单:如果有多个进程要读取它们自己的那部门资源的副本,那么复制是不必要的。每个进程只要保存一个指向这个资源的指针就可以了。只要没有进程要去修改自己的“副本”,就存在着这样的幻觉:每个进程好像独占那个资源。从而就避免了复制带来的负担。如果一个进程要修改自己的那份资源“副本”,那么就会复制那份资源,并把复制的那份提供给进程。不过其中的复制对进程来说是透明的。这个进程就可以修改复制后的资源了,同时其他的进程仍然共享那份没有修改过的资源。所以这就是名称的由来:在写入时进行复制。

写时复制的主要好处在于:如果进程从来就不需要修改资源,则不需要进行复制。惰性算法的好处就在于它们尽量推迟代价高昂的操作,直到必要的时刻才会去执行。

在使用虚拟内存的情况下,写时复制(Copy-On-Write)是以页为基础进行的。所以,只要进程不修改它全部的地址空间,那么就不必复制整个地址空间。在fork( )调用结束后,父进程和子进程都相信它们有一个自己的地址空间,但实际上它们共享父进程的原始页,接下来这些页又可以被其他的父进程或子进程共享。

写时复制在内核中的实现非常简单。与内核页相关的数据结构可以被标记为只读和写时复制。如果有进程试图修改一个页,就会产生一个缺页中断。内核处理缺页中断的方式就是对该页进行一次透明复制。这时会清除页面的COW属性,表示着它不再被共享。

现代的计算机系统结构中都在内存管理单元(MMU)提供了硬件级别的写时复制支持,所以实现是很容易的。

在调用fork( )时,写时复制是有很大优势的。因为大量的fork之后都会跟着执行exec,那么复制整个父进程地址空间中的内容到子进程的地址空间完全是在浪费时间:如果子进程立刻执行一个新的二进制可执行文件的映像,它先前的地址空间就会被交换出去。写时复制可以对这种情况进行优化。

fork和vfork的区别:

  1. fork( )的子进程拷贝父进程的数据段和代码段;vfork( )的子进程与父进程共享数据段

  2. fork( )的父子进程的执行次序不确定;vfork( )保证子进程先运行,在调用exec或exit之前与父进程数据是共享的,在它调用exec或exit之后父进程才可能被调度运行。

  3. vfork( )保证子进程先运行,在它调用exec或exit之后父进程才可能被调度运行。如果在调用这两个函数之前子进程依赖于父进程的进一步动作,则会导致死锁。

4.当需要改变共享数据段中变量的值,则拷贝父进程。

  1. 请问虚拟内存置换的方式
    比较常见的内存替换算法有:FIFO,LRU,LFU,LRU-K,2Q。

1、FIFO(先进先出淘汰算法)

思想:最近刚访问的,将来访问的可能性比较大。

实现:使用一个队列,新加入的页面放入队尾,每次淘汰队首的页面,即最先进入的数据,最先被淘汰。

弊端:无法体现页面冷热信息

2、LFU(最不经常访问淘汰算法)

思想:如果数据过去被访问多次,那么将来被访问的频率也更高。

实现:每个数据块一个引用计数,所有数据块按照引用计数排序,具有相同引用计数的数据块则按照时间排序。每次淘汰队尾数据块。

开销:排序开销。

弊端:缓存颠簸。
在这里插入图片描述

3、LRU(最近最少使用替换算法)

思想:如果数据最近被访问过,那么将来被访问的几率也更高。

实现:使用一个栈,新页面或者命中的页面则将该页面移动到栈底,每次替换栈顶的缓存页面。

优点:LRU算法对热点数据命中率是很高的。

缺陷:

1)缓存颠簸,当缓存(1,2,3)满了,之后数据访问(0,3,2,1,0,3,2,1。。。)。

2)缓存污染,突然大量偶发性的数据访问,会让内存中存放大量冷数据。

4、LRU-K(LRU-2、LRU-3)

思想:最久未使用K次淘汰算法。

LRU-K中的K代表最近使用的次数,因此LRU可以认为是LRU-1。LRU-K的主要目的是为了解决LRU算法“缓存污染”的问题,其核心思想是将“最近使用过1次”的判断标准扩展为“最近使用过K次”。

相比LRU,LRU-K需要多维护一个队列,用于记录所有缓存数据被访问的历史。只有当数据的访问次数达到K次的时候,才将数据放入缓存。当需要淘汰数据时,LRU-K会淘汰第K次访问时间距当前时间最大的数据。

实现:

1)数据第一次被访问,加入到访问历史列表;

2)如果数据在访问历史列表里后没有达到K次访问,则按照一定规则(FIFO,LRU)淘汰;

3)当访问历史队列中的数据访问次数达到K次后,将数据索引从历史队列删除,将数据移到缓存队列中,并缓存此数据,缓存队列重新按照时间排序;

4)缓存数据队列中被再次访问后,重新排序;

5)需要淘汰数据时,淘汰缓存队列中排在末尾的数据,即:淘汰“倒数第K次访问离现在最久”的数据。

针对问题:

LRU-K的主要目的是为了解决LRU算法“缓存污染”的问题,其核心思想是将“最近使用过1次”的判断标准扩展为“最近使用过K次”。

5、2Q

类似LRU-2。使用一个FIFO队列和一个LRU队列。

实现:

1)新访问的数据插入到FIFO队列;

2)如果数据在FIFO队列中一直没有被再次访问,则最终按照FIFO规则淘汰;

3)如果数据在FIFO队列中被再次访问,则将数据移到LRU队列头部;

4)如果数据在LRU队列再次被访问,则将数据移到LRU队列头部;

5)LRU队列淘汰末尾的数据。

针对问题:LRU的缓存污染

弊端:

当FIFO容量为2时,访问负载是:ABCABCABC会退化为FIFO,用不到LRU。

篇幅有限,今天先分享到这里,需要更多大厂面试题可以加q裙812855908私聊管理免费领取
在这里插入图片描述

猜你喜欢

转载自blog.csdn.net/qq_40989769/article/details/105118554