腾讯实习生面试题答案整理

时间:2021-04-03 14:37:48
1.代码实现char *strcat(char *dst,char *src),和char *strcpy(char *dst,char *src)
char *strcat(char *dst,char *src)
{ char *temp;
  temp = src;
  if(NULL == dst)
  return src;
  if(NULL == src)
  return dst;
  while(*src !="\0")
  src++;
  while(*dst !="\0")
  src++=dest++;
  src='\0';
  return temp;  


}


char * strcpy(char *dst,char *src)
{
  if(NULL==dst || NULL == src)
  return NULL;
  char *temp = src;
  while(*dst !='\0')
  {
    *src++=*dst++;
  }  
  *(++src) ='\0';
  return temp;
}
2.交换机的工作原理
  数据链路层,为两个不同的局域网提供网络连接服务。


3.tcp/ip的工作原理
在互联网上源主机的协议层与目的主机的同层协议通过下层提供的服务实现对话。在源和目的主机的同层实体称为对等实体(Peer entities)或叫对等进程,它们之间的对话实际上是在源主机上从上到下然后穿越网络到达目的主机后再从下到上到达相应层。下面以使用TCP协议传送文件(如FTP应用程序)为例说明了TCP/IP的工作原理。(1) 在源主机上应用层将一串字节流传给传输层;(2) 传输层将字节流分成TCP段,加上TCP包头交给互联网络(IP)层;(3) IP层生成一个包,将TCP段放人其数据域,并加上源和目的主机的IPIP包交给数据链路层;(4) 数据链路层在其帧的数据部分装IP包,发往日的主机或IP路由器;(5) 在目的主机,数据链路层将数据链路层帧头去掉,将IP包交给互联网层;(6) IP层检查IP包头,如果包头中的校验和与计算出来的不一致,则丢弃该包;(7) 如果校验和一致,IP层去掉IP头,将TCP段交给TCP层,TCP层检查顺序号来判断是否为正确的TCP段;(8) TCP层为TCP包头计算TCP头和数据。如果不对,TCP层丢弃这个包,若对,则向源主机发送确认;(9) 在目的主机,TCP层去掉TCP头,将字节流传给应用程序;(10) 于是目的主机收到了源主机发来的字节流,就像直接从源主机发来的一样。实际上每往下一层,便多加了一个报头,而这个头对上层来说是透明的,上层根本感觉不到下面报头的存在。如下图3-10所示,假设物理网络是以太网,上述基于TCP/IP的文件传输(FTP)应用打包过程便是一个逐层封装的过程,当到达目的主机时,则从下而上去掉包头。


4.栈跟堆的区别,传参是哪个起的作用?
 栈: FILO 为局部变量分配内存,比较固定,程序员不能控制。传参是栈为形参分配内存。递归调用时要用栈来存储局部变量和返回地址。
 堆: 动态分配内存,比较*,程序员可以*控制,使用C函数malloc,free,C++函数new,delete。
5.简述用户态和核心态?
 内核态和用户态区别


当一个任务(进程)执行系统调用而陷入内核代码中执行时,我们就称进程处于内核运行态(或简称为内核态)。此时处理器处于特权级最高的(0级)内核代码中执行。当进程处于内核态时,执行的内核代码会使用当前进程的内核栈。每个进程都有自己的内核栈。当进程在执行用户自己的代码时,则称其处于用户运行态(用户态)。即此时处理器在特权级最低的(3级)用户代码中运行。当正在执行用户程序而突然被中断程序中断时,此时用户程序也可以象征性地称为处于进程的内核态。因为中断处理程序将使用当前进程的内核栈。这与处于内核态的进程的状态有些类似。


1、用系统调用时进入核心态。Linux对硬件的操作只能在核心态,这可以通过写驱动程序来控制。在用户态操作硬件会造成core    dump.   
2、要注意区分系统调用和一般的函数。系统调用由内核提供,如read()、write()、open()等。而一般的函数由软件包中的函数库提供,如sin()、cos()等。在语法上两者没有区别。   
3、一般情况:系统调用运行在核心态,函数运行在用户态。但也有一些函数在内部使用了系统调用(如fopen),这样的函数在调用系统调用是进入核心态,其他时候运行在用户态。


大概是当用户程序调用系统的API时,就产生中断,进入内核态的API,处理完成后,用中断再退出,返回用户态的调用函数。   
user    api    -->    interrupt    -->    kernel    api    -->    interrupt
2. 用户态和内核态的转换


1)用户态切换到内核态的3种方式


a. 系统调用


   这是用户态进程主动要求切换到内核态的一种方式,用户态进程通过系统调用申请使用操作系统提供的服务程序完成工作,比如前例中fork()实际上就是执行了一个创建新进程的系统调用。而系统调用的机制其核心还是使用了操作系统为用户特别开放的一个中断来实现,例如Linux的int 80h中断。


b. 异常


    当CPU在执行运行在用户态下的程序时,发生了某些事先不可知的异常,这时会触发由当前运行进程切换到处理此异常的内核相关程序中,也就转到了内核态,比如缺页异常。


c. 外围设备的中断


    当外围设备完成用户请求的操作后,会向CPU发出相应的中断信号,这时CPU会暂停执行下一条即将要执行的指令转而去执行与中断信号对应的处理程序,如果先前执行的指令是用户态下的程序,那么这个转换的过程自然也就发生了由用户态到内核态的切换。比如硬盘读写操作完成,系统会切换到硬盘读写的中断处理程序中执行后续操作等。


 


这3种方式是系统在运行时由用户态转到内核态的最主要方式,其中系统调用可以认为是用户进程主动发起的,异常和外围设备中断则是被动的。




6.数组和链表各种操作的时间复杂度。
数组插入,删除的时间复杂度为0(N),查找为O(1),
链表插入,删除的时间复杂度为O(1),查找为O(N)。


7.在一个有100个元素的数组里面查找元素x,时间复杂度?延伸到排序,二分法查找,大数据的排序,还有各种方法的时间复杂度?


二分法查找为(log2N),排序有冒泡O(N*N)稳定,选择0(N*N)不稳定,插入排序O(N*N)稳定
                      快排最好平均O(log2N)最坏的O(N*N)不稳定,适合大数据和基本没序
                      堆排序平均O(Log2N)不稳定
                      希尔排序不稳定


8.平衡二叉树的插入,删除(时间复杂度)?
平衡二叉树定义(AVL):它或者是一颗空树,或者具有以下性质的二叉树:它的左子树和右子树的深度之差的绝对值不超过1,且它的左子树和右子树都是一颗平衡二叉树。


最小不平衡子树:指离插入节点最近且以平衡因子的绝对值大于1的节点作为根的子树。 


平衡因子(bf):结点的左子树的深度减去右子树的深度,那么显然-1<=bf<=1;
插入操作


在平衡二叉树中插入结点与二叉查找树最大的不同在于要随时保证插入后整棵二叉树是平衡的。那么调整不平衡树的基本方法就是: 旋转,基本思路都是转换到左旋和右旋


  很显然,平衡二叉树是在二叉排序树(BST)上引入的,就是为了解决二叉排序树的不平衡性导致时间复杂度大大下降,那么AVL就保持住了(BST)的最好时间复杂度O(logn),所以每次的插入和删除都要确保二叉树的平衡
在平衡二叉树中插入结点与二叉查找树最大的不同在于要随时保证插入后整棵二叉树是平衡的。那么调整不平衡树的基本方法就是: 旋转,基本思路都是转换到左旋和右旋
1) 右旋: 在最小平衡子树根节点平衡因子>=2且在根节点的左孩子的左孩子插入元素,进行右旋
 
2) 左旋: 在最小平衡子树根节点平衡因子>=-2且在根节点的右孩子的右孩子插入元素,进行左旋。




9.查询左连接和右连接?
left join  保持左表的所有行与外表等值连接,如果左表的列右表不能匹配填充NULL值
right join 同理


10.你知道的索引有哪些?
唯一性索引,聚簇索引,分区索引
索引的作用:提高查询的速度(在大表上查询比较少的行),维护索引需要空间,在频繁插入删除的数据表上最好不要添加索引。要根据业务需要分析。
约束
主键约束,外键约束,用户自定义的约束


11.唯一索引的作用?
值不能重复,保证唯一,但可以为NULL值
主键不能为NULL,创建主键的时候自动创建一个唯一性索引
12.进程的栈和堆最大值为多少?


  .堆栈是一个用户空间的内存区域,进程使用堆栈作为临时存储.


  .堆栈中存放的是函数中的局部变量,在函数的生命周期中可以将变量压入堆栈,编译器需要确保堆栈指针在函数退出前恢复到初始位置,也就是说,内存是自动分配和释放的.


  .C/C++把存储在堆栈中的局部变量当作automatic存储,并使用auto关键字,这是局部变量的默认存储方式,所以现在没有人用auto关键词.


  .与动态存储相对映的静态存储,也就是用static定义的局部变量,它不用堆栈来存储,而是使用数据段来存储.


  .堆栈的基地址位于用户空间的最高虚拟地址附近,并从那里向下延伸.


  .一个进程开始时,堆栈的最大值就不能改变,如果占用的空间超过了堆栈大小,那么就会导致堆栈溢出.


  二)进程的内存组织形式


  进程被分为三个区域:文本,数据和堆栈.


  1)文本区域:


  文本区域也叫做代码段,是由程序确定的,它包括代码(指令)和只读数据,该区域通常被标记为只读,任何对其写入的操作会导致段错误.


  2)数据区域:


  数据区域也叫做数据段,它包括已初始化和未初始化的数据,静态变量存储在这个区域中,它的大小可以用系统调用brk(2)来改变.


  3)堆栈区域:


  堆栈区域也叫堆栈段,它用于给局部变量动态分配空间,同样函数传递参数和函数返回值也要用到堆栈.


  堆栈也可向下增长(向内存低地址)也可以向上增长,这依赖于具体的实现,通常都是向下增长的,而SP(堆栈指针)也是指向堆栈的最后地址.


  4)内存的分配区域:


  根据前面所述,堆栈是位于最高虚拟地址附近,而数据段则位于堆栈段之后,最后是代码段.


  三)堆栈着色


  当两个线程或进程使用相同的堆栈虚拟地址时,它们会争夺同一个cache行,导致竞争和降级行为.


  堆栈着色的技术使每一个进程的基址都不相同,通过随机分配堆栈基址,多个进程会使用不同的cache行来避免.


  四)堆栈的限制


  堆栈空间的最大值是由setrlimit系统调用确定的,也可以通过bash内建的ulimit命令来设定和查看.


  例如:


  查看当前可使用的最大堆栈(以KB为单位)


  ulimit -s


  8192


  设定为最大的使用堆栈为15KB


  ulimit -s 15


  此时执行ls将会得到一个段错误.


  ls -l /etc/


  total 1040


  Segmentation fault


  通过用strace跟踪ls命令,将发现有如下的系统调用


  getrlimit(RLIMIT_STACK, {rlim_cur=15*1024, rlim_max=15*1024}) = 0


  说明当前可用的堆栈空间,已经不足以运行strace命令了.


  五)常驻内存和锁定内存


  常驻内存专指存储在RAM中的内存部分,不包括存储在交换区和未存储的进程的内存.


  锁定内存是常驻内存的子集,它指被进程明确地锁定到RAM的虚拟内存中,不能用于交换,并一直常驻于RAM中.
、预备知识—程序的内存分配 
一个由c/C++编译的程序占用的内存分为以下几个部分 
1、栈区(stack)— 由编译器自动分配释放 ,存放函数的参数值,局部变量的值等。其操作方式类似于数据结构中的栈。 
2、堆区(heap) — 一般由程序员分配释放, 若程序员不释放,程序结束时可能由OS回收 。注意它与数据结构中的堆是两回事,分配方式倒是类似于链表,呵呵。 
3、全局区(静态区)(static)—,全局变量和静态变量的存储是放在一块的,初始化的全局变量和静态变量在一块区域, 未初始化的全局变量和未初始化的静态变量在相邻的另一块区域。 - 程序结束后有系统释放 
4、文字常量区 —常量字符串就是放在这里的。 程序结束后由系统释放 
5、程序代码区—存放函数体的二进制代码。 


二、例子程序 
这是一个前辈写的,非常详细 
//main.cpp 
int a = 0; 全局初始化区 
char *p1; 全局未初始化区 
main() 

int b; 栈 
char s[] = "abc"; 栈 
char *p2; 栈 
char *p3 = "123456"; 123456在常量区,p3在栈上。 
static int c =0; 全局(静态)初始化区 
p1 = (char *)malloc(10); 
p2 = (char *)malloc(20); 
分配得来得10和20字节的区域就在堆区。 
strcpy(p1, "123456"); 123456放在常量区,编译器可能会将它与p3所指向的"123456"优化成一个地方。 

二、堆和栈的理论知识 
2.1申请方式 
stack: 
由系统自动分配。 例如,声明在函数中一个局部变量 int b; 系统自动在栈中为b开辟空间 
heap: 
需要程序员自己申请,并指明大小,在c中malloc函数 
如p1 = (char *)malloc(10); 
在C++中用new运算符 
如p2 = (char *)malloc(10); 
但是注意p1、p2本身是在栈中的。 
2.2 
申请后系统的响应 
栈:只要栈的剩余空间大于所申请空间,系统将为程序提供内存,否则将报异常提示栈溢出。 
堆:首先应该知道操作系统有一个记录空闲内存地址的链表,当系统收到程序的申请时, 
会遍历该链表,寻找第一个空间大于所申请空间的堆结点,然后将该结点从空闲结点链表中删除,并将该结点的空间分配给程序,另外,对于大多数系统,会在这块内存空间中的首地址处记录本次分配的大小,这样,代码中的delete语句才能正确的释放本内存空间。另外,由于找到的堆结点的大小不一定正好等于申请的大小,系统会自动的将多余的那部分重新放入空闲链表中。 
2.3申请大小的限制 
栈:在Windows下,栈是向低地址扩展的数据结构,是一块连续的内存的区域。这句话的意思是栈顶的地址和栈的最大容量是系统预先规定好的,在 WINDOWS下,栈的大小是2M(也有的说是1M,总之是一个编译时就确定的常数),如果申请的空间超过栈的剩余空间时,将提示overflow。因此,能从栈获得的空间较小。 
堆:堆是向高地址扩展的数据结构,是不连续的内存区域。这是由于系统是用链表来存储的空闲内存地址的,自然是不连续的,而链表的遍历方向是由低地址向高地址。堆的大小受限于计算机系统中有效的虚拟内存。由此可见,堆获得的空间比较灵活,也比较大。 
2.4申请效率的比较: 
栈由系统自动分配,速度较快。但程序员是无法控制的。 
堆是由new分配的内存,一般速度比较慢,而且容易产生内存碎片,不过用起来最方便. 
另外,在WINDOWS下,最好的方式是用VirtualAlloc分配内存,他不是在堆,也不是在栈是直接在进程的地址空间中保留一快内存,虽然用起来最不方便。但是速度快,也最灵活 
2.5堆和栈中的存储内容 
栈: 在函数调用时,第一个进栈的是主函数中后的下一条指令(函数调用语句的下一条可执行语句)的地址,然后是函数的各个参数,在大多数的C编译器中,参数是由右往左入栈的,然后是函数中的局部变量。注意静态变量是不入栈的。 
当本次函数调用结束后,局部变量先出栈,然后是参数,最后栈顶指针指向最开始存的地址,也就是主函数中的下一条指令,程序由该点继续运行。 
堆:一般是在堆的头部用一个字节存放堆的大小。堆中的具体内容有程序员安排。 
2.6存取效率的比较 


char s1[] = "aaaaaaaaaaaaaaa"; 
char *s2 = "bbbbbbbbbbbbbbbbb"; 
aaaaaaaaaaa是在运行时刻赋值的; 
而bbbbbbbbbbb是在编译时就确定的; 
但是,在以后的存取中,在栈上的数组比指针所指向的字符串(例如堆)快。 
比如: 
#include <stdio.h> 
void main() 

char a = 1; 
char c[] = "1234567890"; 
char *p ="1234567890"; 
a = c[1]; 
a = p[1]; 
return; 

对应的汇编代码 
10: a = c[1]; 
00401067 8A 4D F1 mov cl,byte ptr [ebp-0Fh] 
0040106A 88 4D FC mov byte ptr [ebp-4],cl 
11: a = p[1]; 
0040106D 8B 55 EC mov edx,dword ptr [ebp-14h] 
00401070 8A 42 01 mov al,byte ptr [edx+1] 
00401073 88 45 FC mov byte ptr [ebp-4],al 
第一种在读取时直接就把字符串中的元素读到寄存器cl中,而第二种则要先把指针值读到edx中,在根据edx读取字符,显然慢了。 


使用栈就象我们去饭馆里吃饭,只管点菜(发出申请)、付钱、和吃(使用),吃饱了就走,不必理会切菜、洗菜等准备工作和洗碗、刷锅等扫尾工作,他的好处是快捷,但是*度小。 
使用堆就象是自己动手做喜欢吃的菜肴,比较麻烦,但是比较符合自己的口味,而且*度大。 


堆和栈的区别主要分: 
就是数据结构方面的堆和栈,这些都是不同的概念。这里的堆实际上指的就是(满足堆性质的)优先队列的一种数据结构,第1个元素有最高的优先权;栈实际上就是满足先进后出的性质的数学或数据结构。