"Interview" Simple Huya

This is a pure C/C++ server-side development interview, but in addition to the language part, other parts also need to be mastered by everyone. Let’s start today’s interview journey

Huya and Douyu, this must have your youth, right? I really want to go back to the age of watching live broadcasts all night, but in the graduation season and job hunting season, we have to shift the center of our time to a place closer to our dreams.

Some time ago, Huya and Douyu did not happen, so I remembered that I had met before, and I probably recalled that the interview was not that difficult, but this company has a tonality. I lose my resume at any time. Who will try? know. See what you face

Before you start, the old rules, or remember what you did next weekend, first and the team’s sister Juan, Qian participated in an algorithmic rally, how to pull the law, is to give you some data, and then use the model to make predictions. , No matter whether you need to be familiar with the environment first, I haven’t read this for several months. Many libraries of py are almost forgotten, but our three efforts are still in the top ten, hahahaha

412acaa43ca70510e9c4276e09d3c6d8.jpegMr. Rice

Thinking about sitting basically for a week, I decided to play basketball with them in the afternoon. Now it is really sore. . .


1 side

The regular self-introduction leads the interviewer into the ocean of knowledge, so that the voyage will be more smooth.

After the interviewer asked about the content of my self-introduction, he began to check my resume. Of course, my resume is what I know. Not only that, even if you dig deeper, it is still in mine. In control

The difference between pure virtual function and virtual function

Even if you know the syntax of virtual functions, you may not know why you want to do it this way and what you think about its various behaviors. Guessing that you don’t want to be superficially grammatically plausible, then let’s uncover the layer of window paper between you and the virtual function.

Let’s take a look at his grammar specifications

  • Add virtual before the raw rice of the class member method becomes a virtual function

  • Add a 0 at the end of the virtual function declaration statement to become a pure virtual function

  • The subclass can redefine the virtual function of the base class, which is called replication

  • Subclasses can choose whether to provide their own personalized virtual function implementation

Talk about the simple implementation of virtual functions

For classes with virtual functions, the compiler will allocate a virtual function table for them, which will record the address of the virtual function. When this class is inherited, if the subclass is inherited, if the subclass is also inherited After writing a virtual function, overwrite the function address of the parent class in the virtual function table of the subclass, otherwise it will inherit the virtual function address of the parent class.

After instantiation, the object has a virtual function pointer, which points to the virtual function table, so that when the program is running, the virtual function table found through the virtual function pointer is pointed to according to the type of the object.

26410046e4c8f44e99d952a688b62f09.pngVirtual address table

Vector memory allocation and underlying implementation

When you learn about the underlying data structure of a certain knowledge, you will find that the data structure we learned in our freshman and sophomore year is so important. Vector uses a relatively simple data structure, using a continuous linear memory space, let's take a look at his source code

//_Alloc 表示内存分配器,此参数几乎不需要我们关心
template <class _Tyclass _Alloc = allocator<_Ty>>
class vector{

    ...
protected:
    pointer _Myfirst;
    pointer _Mylast;
    pointer _Myend;
};

Here are the use of Myfirst, Mylast and Myend respectively point to the starting byte position of the vecotr container object; Mylast only wants the last byte of the current last element; Myend only wants the last byte of the memory space occupied by the entire vector container

58705782cb493ea75db20322bf0821fb.pngMemory address

Don't underestimate these three iterators, it can represent a vector container with 2 elements and a capacity of 5. Through the combination of these three, we can compose such different scenarios

  • Myfirst and Mylst represent the memory space currently used by mobile phones in the vector container

  • Mylast and Myend represent the current free memory space of the vector

  • Myfirst and Myend represent the capacity of the vector container

I don’t know if you think of the sliding window of the network now. If you forget, you can check it right now. This may make your impact more profound.

If it is an empty vector container, since there is no space allocation for the elements, the first, last, and end are all null at this time. Through such three iterators, it is relatively easy to realize the functions of the head and tail expression, size, container judgment, etc.

template <class _Tyclass _Alloc = allocator<_Ty>>
class vector{

public
    iterator begin() {return _Myfirst;}
    iterator end() {return _Mylast;}
    size_type size() const {return size_type(end() - begin());}
    size_type capacity() const {return size_type(_Myend - begin());}
    bool empty() const {return begin() == end();}
    reference operator[] (size_type n) {return *(begin() + n);}
    reference front() return *begin();}
    reference back() {return *(end()-1);}
    ...
};

What is the expansion mechanism of vecotr?

Vecort is used to store elements and naturally has a concept of capacity. If the size and capacity of the vector are fully loaded, then adding elements to it at this time requires expansion of the vector. The expansion will go through three steps

  • Discard the current memory space and reapply for a larger memory space

  • Move the old memory space data to the new memory space

  • Free up old memory space

It seems that the expansion of vector is not easy and time-consuming. So in order to reduce the cost of re-allocating memory space, each time you expand the vector, apply for more memory space than the user needs. However, when the vecoter container expands, different compilers may be different. For example, when using VS, it Will expand 50% of the existing container capacity

#include <iostream>
#include <vector>
using namespace std;
int main()
{
    vector<int> a;
    cout << "a.size(): " << a.size() << "       a.capacity(): " << a.capacity() << endl;
    for (int i = 0; i < 10; i++)
    {
        a.push_back(i);
        cout << "a.size(): " << a.size() << "   a.capacity(): " << a.capacity() << endl;
    }
    return 0;
}

Talk about the thread safety of STL

这里可从操作系统 来回答线程安全,然后联系STL即可

什么时候用vector和list,实现的方案

简单说:vector和数组类似,连续的内存空间,那么随机访问,时间复杂度为O(1),由于内存空间连续,所以插入删除的时候,会造成内存块的拷贝,时间复杂度为O(n)

另外,如果数据内存空间不足,会采取扩容的方式,重新申请一块更大的内存空间进行拷贝

而list底层采用双向链表的方式,内存空间不连续,那么List查询效率较低,时间复杂度为O(n),但是插入和删除的效率比较搞,不用移动数据,移动指针即可

从迭代器的角度出发,在vector中,iterator支持“+”、"+="、“<”等操作,而list中不支持。到那时两者都是重载了"+="

说说HTTP代理

之前介绍的都是一问一答的情景,但是在大部分的情况下都会存在多台服务器进行通信服务。其中比较常见的就是在请求方与应答方中间增加一个中间代理。

909b2a649b27372282c2700b6972282a.png代理

代理

代理作为中间位置,相对请求方为服务端,相当于后端服务端为请求方。代理常见的功能为负载均衡。在负载均衡中需要区分正向代理与反向代理,其中也就会涉及调度算法,比如轮询还是一致性哈希等。

e9cac9c3d7f14d1961f31afe7587c665.png正向代理与反向代理

2 二面

面向对象的三个特点,简单总结

封装:使用类将自己的数据和方法让可信的类或者对象操作,对不可信的进行信息隐藏。比如对某些数据的权限设置为私有的,则不能被外界访问,不同对内部数据提供不同级别的保护,防止程序中无关的部分意外的改变或者错误的使用了对象的私有部分

继承:它可以使用现有类的所有功能,无需重新编写原来的类的情况下对这些功能进行扩展,被继承的类叫做基类,父类

多态:向不同的对象发出同意消息,不同的对象在接受的时候产生不同的行为

  • 静态多态:c++语言允许函数重载和运算符重载,模板

  • 动态多态:通过定义虚函数支持动态联编

进程间的通信,这是之前写过的,就直接拿过来了,免得大家还跳转过去看

管道

学习软件工程规范的时候,我们知道瀑布模型,在整个项目开发过程分为多个阶段,上一阶段的输出作为下一阶段的输入。各个阶段的具体内容如下图所示

7cc5a0280a9fe0be83b7b8670b0dc1f4.png

最初我们在学习Linux基本命令使用的时候,我们经常通过多个命令的组合来完成我们的需求。比如说我们想知道如何查看进程或者端口是否在使用,会使用下面的这条命令

008c7b66abaf1078a7564530163f8158.png

这里的"|"实际上就是管道的意思。"|"前面部分作为"|"后面的输入,很明显是单向的传输,这样的管道我们叫做"匿名管道",自行创建和销毁。既然有匿名管道,应该就有带名字的管道"命名管道"。如果你想双向传输,可以考虑使用两个管道拼接即可。

创建命名管道的方式

611441f1b219830d40d6773fb6d6a559.png

test即为管道的名称,在Linux中一切皆文件,管道也是以文件的方式存在,咋们可以使用ls -l 查看下文件的属性,它会"p"标识。

e31006ae3d3bb8b466009d689ab8d74b.png

下面我们向管道写入内容

echo "666" > test

3259667306c5f68d4e7054e15cc19bd2.png

此时按道理来说咋们已经将内容写入了test,没有直接输出是因为我们需要开启另一个终端进行输出(可以理解为暂存管道)

cat < test

ok,我们发现管道内容被读出来,同时echo退出。那么管道这种通信方式有什么缺点?我们知道瀑布模型的软件开发模式是非常低下的,同理采用管道进行通信的效率也很低,因为假设现在有AB两个进程,A进程将数据写入管道,B进程需要等待A进程将信息写完以后才能读出来,所以这种方案不适合频繁的通信。那优点是什么?

最明显的优点就是简单,我们平时经常使用以致于都不知道这是管道。鉴于上面的缺点,我们怎么去弥补呢?接着往下看

消息队列

管道通信属于一股脑的输入,能不能稍微温柔点有规矩点的发送消息?

答:可以的。消息队列在发送数据的时候,按照一个个独立单元(消息体)进行发送,其中每个消息体规定大小块,同时发送方和接收方约定好消息类型或者正文的格式。

在管道中,其大小受限且只能承载无格式字节流的方式,而消息队列允许不同进程以消息队列的形式发送给任意的进程。

但是当发送到消息队列的数据太大,需要拷贝的时间也就越多,所以还有其他的方式?继续看

共享内存

使用消息队列可以达到不错的效果,但是如果我们两个部门需要交换比较大的数据的时候,一发一收还是不能及时的感知数据。能不能更好的办法,双方能很快的分享内容数据,答:有的,共享内存

我们知道每个进程都有自己的虚拟内存空间,不同的进程映射到不同的物理内存空间。那么我们可不可以申请一块虚拟地址空间,不同进程通过这块虚拟地址空间映射到相同的屋里地址空间呢?这样不同进程就可以及时的感知进程都干了啥,就不需要再拷贝来拷贝去。

我们可以通过shmget创建一份共享内存,并可以通过ipcs命令查看我们创建的共享内存。此时如果一个进程需要访问这段内存,需要将这个内存加载到自己虚拟地址空间的一个位置,让内核给它一个合法地址。使用完毕接触板顶并删除内存对象。

那么问题来了,这么多进程都共享这块内存,如果同时都往里面写内容,难免会出现冲突的现象,比如A进程写了数字5,B进程同样的地址写了6就直接给覆盖了,这样就不友好了,怎么办?继续往下看

信号量

为了防止冲突,我们得有个约束或者说一种保护机制。使得同一份共享的资源只能一个进程使用,这里就出现了信号量机制。

信号量实际上是一个计数器,这里需要注意下,信号量主要实现进程之间的同步和互斥,而不是存储通信内容。

信号量定义了两种操作,p操作和v操作,p操作为申请资源,会将数值减去M,表示这部分被他使用了,其他进程暂时不能用。v操作是归还资源操作,告知归还了资源可以用这部分。

信号

从管道----消息队列-共享内存/信号量,有需要等待的管道机制,共享内存空间的进程通信方式,还有一种特殊的方式--信号

我们或许听说过运维或者部分开发需要7 * 24小时值守(项目需要上线的时候),当然也有各种监管,告警系统,一旦出现系统资源紧张等问题就会告知开发或运维人员,对应到操作系统中,这就是信号。

在操作系统中,不同信号用不同的值表示,每个信号设置相应的函数,一旦进程发送某一个信号给另一个进程,另一进程将执行相应的函数进行处理。也就是说把可能出现的异常等问题准备好,一旦信号产生就执行相应的逻辑即可。

套接字

上面的几种方式都是单机情况下多个进程的通信方式,如果我想和相隔几千里的小姐姐通信怎么办?

这就需要套接字socket了。其实这玩意随处可见,我们平时的聊天,我们天天请求浏览器给予的响应等,都是这老铁。

const和define的区别,哪种更好

  • 如果从起作用的阶段来说:#define在编译预处理阶段起作用,而const是在编译,运行的时候才起作用

  • 如果从起作用的方式而言:#define只是简单的字符串替换,没有类型检查。但是const有对应的数据类型,是需要判断的,可以避免一些低级的错误

  • 如果从存储方式而言,#define只是进行展开,多少地方使用就替换多少次,const定义的只读变量在程序运行过程中只有一份备份

使用C++实现LRU

实际上浏览器的缓存策略,memcached的缓存策略都采用了LRU的算法,它会将近期最不会访问的数据淘汰掉。之所以这么流行,在于实现比较简单又使用,命中率较高

  • 新的数据插入到链表的头部

  • 每当缓存命中,则将数据移动到链表头部

  • 当链表满的时候,将链表尾部的数据丢弃即可

那么实现它需要具备哪些操作

  • set(key ,value):如果key在hashmap中存在,则先重置对应的value值,然后获取对应节点的cur,将cur节点从链表中删除,并移动到头部。如果key在hashmap不存在,则新建一个节点,并将节点存放于链表的头部。如果cache满了,则将链表最后一个节点删除即可

  • get(key):如果key在hashmap存在,则将对应的节点放到链表头部,并返回对应的value值

The implementation here adopts a doubly linked list + Map method. Why do you use a doubly linked list, because if it is a singly linked list, I need to search from the head to delete an element, and the time complexity is O(N), but using a doubly linked list only needs to change The pointer of the predecessor can point to it, so only O(1) is needed. The use of map is to facilitate the search for elements in O(logn) time.

Let's look at the definition first

struct CacheNode {
  int key;      // 键
  int value;    // 值
  CacheNode *pre, *next;  // 节点的前驱、后继指针
  CacheNode(int k, int v) : key(k), value(v), pre(NULL), next(NULL) {}
};

Insert node into the head

void setHead(CacheNode *node)
{
  node -> next = head;
  node -> pre = NULL;

  if (head != NULL)
  {
    head -> pre = node;
  }
  head = node;
  if (tail == NULL)
  {
    tail = head;
  }
}

get(key) can directly determine whether the map has a key value

int get(int key)
{
  map<int, CacheNode *>::iterator it = mp.find(key);
  if (it != mp.end())
  {
    CacheNode *node = it -> second;
    remove(node);
    setHead(node);
    return node -> value;
  }
  else
  {
    return -1;


Guess you like

Origin blog.51cto.com/14984904/2547201