腾讯后台开发实习生「2019-7-2」

一面已过,二面进行了一半,晚上说还要接着面。。。。

一面

经历最长的一次面试,没有之一,前前后后一共两个半小时

上来手撕三道编程题

第一题,实现内存拷贝函数 memcpy(void* pSrc, void* pDst, size_t length);

首先这道题给定的是void型,在拷贝的时候要转换成char*型,然后赋值就可以了,我的代码如下

void memcpy(void* pSrc, void* pDst, size_t length){
    while(pSrc && length){
        *(char*)pDst = *(char*)pSrc;
        (char*)pDst = (char*)pDst + 1;
        (char*)pSrc = (char*)pSrc + 1;
        length--;
    }
}

面试官看到我的代码之后,就开始吐槽了。。。代码太理想化,我赶紧回答可能传进来的地址是空,这时候直接返回就可以了。

然后面试官说还有特殊情况,当时想了半天没有想出来,面试官提示说,可能两个地址有重叠,我才想到,如果两个地址覆盖的内存有重叠的话,可能会导致将原来的值覆盖掉,然后说可以判断是否有冲突,然后如果有冲突,可以另寻一片地址。

面试官问:为什么一开始没有想到呢?

我。。。。。。

 

第二题,常规题目,手写快速排序算法。这个题目比较简单,也没有什么特殊情况要考虑,直接写了一个升序的快排,代码如下:

void QuickSort(vector<int> v, int low, int high){
    if(low>=high)    return;
    int first = low;
    int last = high;
    int key = v[first];
    while(first < last){
        while(first < last && v[last]>=key)
            last--;
        if(first < last)
            v[first] = v[last--];
        while(first < last && v[first]<=key)
            first++;
        if(first < last)
            v[last] = v[first++];
    }
    v[first] = key;
    QuickSort(v,low,first-1);
    QuickSort(v,first+1,high);
}

第三题,还是常规题目,用栈来实现队列,代码如下

#define Empty  -99
#define Full -100
#define Ok -101
using namespace std;

class my_queue{
public:
    int size;
    int length;
    stack<int> *stk1;
    stack<int> *stk2;
    my_queue(int size){
        this->size = size;
        stk1 = new stack<int>;
        stk2 = new stack<int>;
        length = 0;
    }

    ~my_queue();

    int my_push(int item);

    void my_pop(int &item);

    int my_size(){
        return length;
    }
};

my_queue::~my_queue(){
    delete stk1;
    delete stk2;
}

int my_queue::my_push(int item){
    if(stk1->size() < size){
        stk1->push(item);
        length++;
        return Ok;
    }
    else{
        if(stk2->empty()){
            for(int i = 0; i < this->size; i++){
                int temp = stk1->top();
                stk1->pop();
                stk2->push(temp);
            }
            stk1->push(item);
            length++;
            return Ok;
        }
        else
            return Full;
    }
}

void my_queue::my_pop(int &item){
    if(!stk2->empty()){
        item = stk2->top();
        stk2->pop();
        length--;
    }else{
        if(!stk1->empty()){
            int length = stk1->size();
            for(int i = 0; i < length ; i++){
                int temp = stk1->top();
                stk1->pop();
                stk2->push(temp);
            }
            item = stk2->top();
            stk2->pop();
            length--;
        }
        else
            item = Empty;
    }
}

写完之后面试官问我为什么中间卡顿了以下,我说是在将stk1中的元素弹出到stk2中时,for循环的判断调价是小于stk1.size(),但是stk1内的元素值一直在减少,这就导致了问题,后来在循环外将size()赋值给一个变量就好了。

 

三道题写完已经快一个小时了,我以为差不多就这样了,结果面试官说,我们来问点基础知识吧。。。。

C++篇

问:说一下C++的多态?

答:多态有三种,一种是静态的多态,即函数的重载;二是动态的多态,即通过父类的指针指向子类的对象;三就是泛型。

问:说一下动态的多态具体是怎么样的?

答:动态的多态是,在声明的时候,声明一个父类的指针,然后把这个指针指向子类的对象,然后可以调用子类的函数,因为编译器在编译时只检查类型,所以不会报错。

问:为什么父类的指针可以调用子类的函数呢?底层是怎么实现的呢?

答:子类继承了父类的虚函数表,如果子类没有对父类的虚函数进行改写的话,那么虚函数表指向的还是指向父类的虚函数。如果子类修改了父类的虚函数的话,就会把对应位置的地址变为子类虚函数的地址,这样就可以通过父类的指针调用子类的虚函数。(感觉这里说的不太清楚,但是也给过了)。

问:你说到虚函数,那么构造函数可以是虚函数么?析构函数呢?

答:构造函数不能是虚函数,析构函数可以是虚函数。

问:为什么呢?

答:构造函数因为要明确的构造出一个对象,所以不能定义为虚函数,虚函数构造出来的不确定。而析构函数在动态多态时,必须要定义为虚函数,因为通过delete释放对象的内存时会调用子类的析构函数,然后调用父类的析构函数。如果不定义为虚函数,那么只会调用父类的析构函数,导致子类没有调用析构函数。

问:说一下你对const的理解。

答:const是用来表明变量是个常量,程序不要对其进行修改,此外,const&可以当作右值引用来用。

问:说一下 int const *p 和int * const p的区别?

答:一个是常量指针,一个是指针常量。一个是指针指向的变量不能变,一个是指针指向的变量的值不能变。

(可能是觉得我说的不够清楚,所以又问了一下具体的例子,不再赘述)

问:char *p = new char(10),sizeof(p)的大小是多少?

答:4。

问:平常关联容器用的多的是哪些?

答:(一开始没听清是关联容器,说vector,然后面试官提醒关联容器,我说set和map)。

问:知道set和map的底层是根据什么实现的么?

答:红黑树。

问:说一下你知道的红黑树的特点。

答:首先红黑树是一种特殊的二叉排序树,因为二叉排序树在极端情况下可能退化为单链表,所以导致其查找效率退化为O(n),而另外一种二叉排序树——AVL树虽然比红黑树的要求更高,任意节点它到叶子节点的距离之差不超过1,这样严格的要求导致其过度平衡,在插入,删除和修改时需要的操作非常繁琐,开销很大,因此采用了中间的红黑树。红黑树有几个特点,首先根节点是黑色的,叶子节点也是黑色的,任意红色节点它的子节点必须是黑色的,新加的节点是红色的,任意节点到叶子节点经过的黑色节点个数相同,这些性质保证了任意节点到叶子节点的最长路径不会超过最短路径的两倍。

问:知道STL里的sort底层是怎么实现的么?

答:sort函数会根据要排序的数量,如果数量较大,会采用归并排序,当归并的子集内数的个数小于一定值之后,会采用快速排序和插入排序。

问:快排是稳定的么?复杂度是多少?

答:不稳定。一般情况下快排的时间复杂度是O(n*logn),极端情况下即有序的情况下会退化成O(n*n)。

问:说一下堆排序的思路?

答:以升序为例,首先要构建大端堆,将堆的首个节点和最后一个节点进行交换,然后调整堆,重复上面的操作,最后的到的就是一个升序序列。

问:堆排序的时间复杂度是多少?是否稳定?

答:稳定,O(n*logn)。(这一题后来查了一下,是不稳定的,当时面试官问我确不确定,我还非常笃定的说确定。。。。)

然后就是几道算法题。

如何等概率的从m个数中找出n个不相同的数?

两个尾部重合的单链表,如何找到重合点之前的那个节点?

一个 二叉排序树,给定两个节点,找出它们最早的公共子节点。

(答案的话我回答的都不一定是最优解,网上应该有类似的题目)。

网络篇:

问:说一下TCP和UDP的区别?

答:老问题,网上都有。

问:说一下TCP的拥塞控制算法?

答:老问题,网上都有。

问:TCP的三次握手,为什么是三次?如果是两次,会出现什么后果?

答:这个问题没有回答出来,只说是保证两边确认的最小的次数。

问:知道TIME_WAIT状态么?它是出现在什么时间点?

答:在客户端确认服务器发来的断开请求时进入TIME_WAIT状态。

问:网络编程写过吧?说一下服务器这边用到的API?

答:首先是socket函数创建socket,然后是bind函数将socket和对应的信息进行绑定,然后是listen函数,创建了监听套接字,然后是accept,返回三次握手结束的客户端套接字,然后是read和write,最后是close关闭套接字。

问:说一下各个函数的返回值情况?

答:这里说的不太好,只说了accept的read,write的情况,后来追问我read如果返回-1代表什么,我记得read是阻塞的,会一直到读取到指定的字符数量才会返回,然后balabala说了一堆,就跳过了。

问:知道select和epoll的区别么?

答:select是基于轮询的方式来检查监听的描述符的,而epoll是基于事件响应的机制来监听描述符的,(后面把epoll的底层原理说了一下,需要涉及到mmap,红黑树和readylist来实现)。

问:知道epoll有两个状态么?

答:一个是ET边沿触发,一个LT水平触发。这个的底层原理是balabala说了一堆,具体的内容在https://www.cnblogs.com/lojunren/p/3856290.html里有,说的非常详细。

后来面试官问我有没有用过epoll,我说没有,面试官说既然你底层知道的这么清楚,为什么没有用过呢?因为之前写网络实验的时候老师要求用多线程,当时只是知道这个然后看了一下,并没有试过。

操作系统篇:

问:平常linux用的多么?指令知道的怎么样?

答:一般的都还知道一点。

1——查看系统的各类资源占用情况:top。

2——查看端口的使用情况:netstat。

3——查看硬盘的使用情况:这个当时没想起来,只记得是disk free啥的,后来面试官问我是不是df和du,才想起来。。。

4——如何查看某个文档的关键字:管道+grep

5——如何统计一个文档里某个关键字的个数:这个真不清楚,后来查了一下是wc(word count)。

问:说一下进程间通信的方式?

答:管道,共享内存,消息队列,socket。

问:用过哪些?

答:管道和共享内存。

问:说一下共享内存是怎么使用的?

答:在两个进程中个申请一片缓冲区内存,然后把这两个内存映射到同一片物理内存上即可。(后面又追问了一些具体的细节,也不清楚回答的怎么样。。。)

问:说一下内存池?

答:首先申请一大片连续的内存,然后重载operator new,然后将内存划分为一片片小的区域,类似与页,然后创建结构体来管理这一片小的内存,包括是否使用的标记,起始地址,已使用内存的大小等等,然后申请内存的时候就从这些页上申请,然后将这些使用的页向链表一样链接起来,然后申请的时候有三种方式:最好适应算法,最坏适应算法和最早适应算法,balabalba说了一堆。

数据库篇:

问:平常用的哪些数据库?mysql有没有用过?

答:主要用的是mysql。

问:说一下你知道的mysql的底层引擎?

答:只知道InnoDB。

问:说一下InnoDB建索引是怎么建的?

答:索引有B+树索引和hash索引,采用索引的原因是因为如果从磁盘上读取数据,需要加载到内存上,虽然从内存中读取数据很快,但是将磁盘上的内容写到内存中就很慢,就是所谓的IO开销,利用B+树索引,可以精确的定位要找的数据在哪个磁盘上,这样直接加载到内存中,减少了IO的开销。

问:知道InnoDB的锁的默认粒度大小是多大么?表还是啥?

答:我这里没听清楚表和什么。。。只能说是表,面试官又问如果是表的话,会不会粒度太大,然后我balabala说了一堆,就过了。。。。

最后是问我有没有要问的,我就问了下实习时间的问题,然后他说实习地点是在深圳,我去那边的话有没有问题,我说没问题,然后就说他那边考虑一下,如果没有问题的话,会通知我下轮面试的时间,刚好今天早上收到周五上午8点面试。。。。。祝自己好运。。。

二面

二面相比一面就平和了许多,只是是早上8点钟。。。。感觉自己还没睡醒。。。

上来就是自我介绍,面试官问我是哪里人,我说是安徽的,结果面试官也是安徽的。。。

一开始还是聊项目,问我平时C++写的项目有哪些,写过多少行代码,平时看过哪些C++的书,大部分都是在聊天吧。

然后问了我了我一下一面的memcpy的函数,后来是怎么处理的,我就说了下全部的思路,和之前的差不多。

然后让我描述下内存池,问我如果要实现申请和释放都是O(1)的复杂度的该怎么办?

我就说要建立映射关系,每个链表要与一个指针数组向对应,然后维护一个已申请块个数的变量等。

然后又问我内存池的好处是什么?

我当时只提到了防止内存泄漏,可能还会有执行效率的问题。(后来查了一下应该是对内存碎片那里有一定的优化)

还是网络字节序的问题,问我为什么要字节对齐?

这个一面是问到过的。。只是我当时没有回去想一下,后来记得上学期考试的时候好像考过类似的问题,因为如果不对齐的话,因为计算机默认取32bit,如果一个int型变量分在两个32bit里,就会导致取两次的问题。。

然后是在一个大型工程里,怎么避免多个头文件可能重复引用的问题?

这个我一开始还是不知道的,后来想了下应该是可以用条件编译来解决,后来查了一下,还真是这个方法,此外还有#progma once。

又问了个STL中vector是怎么内存分配的?

我就说了下扩容然后内存转移的问题,面试官追问我说为什么要*2,而不是+100,+1000之类的。

这个问题我感觉没回答好,考虑的因素不全面,我只提到了如果本身vector的size很大的话,+100或者+1000会导致频繁的执行扩容操作和内存拷贝,如果vector内部是普通类型的变量,如int,char还好,只要单纯的进行值拷贝即可,但是如果内部是一个类的话,就要频繁的进行拷贝构造函数,这个的开销是很大的。(此外我觉得应该还有就是在vector很小的时候,如果+100或+1000的话,可能并不需要这么大的内存空间,这一部分就是浪费的了)。

让我说了下程序的结构以及函数的执行过程(汇编指令)。

程序结构的话

kernel memory //内核态的内存
stack         //栈段 从0xC0000000开始
heap          //堆段
.data+.bss    //数据段
.text         //代码段 从0x08048000开始

函数的执行过程的话,首先要保护现场,形成栈帧结构,函数退出前要还原现场等等。。

还问了下如何查看进程的栈信息?

我一开始说gdb调试里有个bt可以查看栈的信息。

然后追问我怎么让一个程序可以被gdb调试。

我说编译的时候加上 -g就可以。

然后我又突然想起来了pstack应该是可以的,然后就说了还有pstack。

然后又问如何看待TCP中黏包的问题?

这个我直接说这个黏包本身就是个伪命题。。。。因为报头本身包含报文段的长度,不会导致这个问题。。

之后就是聊天。。。聊职业规划,聊为什么报后台,后台哪个方向?等等等等。。

然后给我留了一道题目,说是晚上7点接着面试。

  • 0
    点赞
  • 9
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值