ppq的面试题总结
C++
1.#include的顺序以及尖括号和双引号的区别
#include ""
按照:当前头文件目录--->编译器设置的头文件路径(编译器可使用-I显式指定搜索路径)
--->系统变量CPLUS_INCLUDE_PATH/C_INCLUDE_PATH指定的头文件路径
#include <>
按照:编译器设置的头文件路径(编译器可使用-I显式指定搜索路径)--->系统变量CPLUS_INCLUDE_PATH/C_INCLUDE_PATH指定的头文件路径
2.进程和线程,为什么要有线程
什么是进程
- 程序执行时的一个实例,有独立的内存地址空间
- 是系统进行资源分配和调度的基本单位
- 进程里的堆是一个进程中最大的一块内存,被进程中所有的线程共享的,在进程创建时分配,存放的是new 出来的对象
- 进程里的方法去是用来存放进程中的代码片段的,是线程共享的
- 在多线程的OS中,进程不是一个可执行的实体,即一个进程至少创建一个线程去执行代码
什么是线程
- 是进程中的一个实体
- 进程的一个执行路径
- CPU调度和分派的基本单位
- 线程本身不会独立存在
- 当前线程CPU时间片用完后,会让出CPU等下次轮到自己时候在执行
- 系统不会为线程分配内存,线程组之间只能共享进程的资源
- 线程只拥有在运行中必不可少的资源(程序计数器、栈)
- 线程里的程序计数器就是为了记录该线程让出CPU时候的执行地址,待再次分配到时间片时候就可以从自己私有的计数器指定地址继续执行
- 每个线程都有自己的栈资源,用于存储该线程的局部变量和调用帧栈,其他线程无权调用
为什么要有线程
每个进程都有自己的地址空间,即进程空间。
一个服务器通常需要接收大量并发请求,为每一个请求都创建一个进程系统开销大、请求响应效率低,因此操作系统引进线程。
3.C++11有哪些新特性
-
语法糖”:
nullptr
,auto
自动类型推导,范围for循环,初始化列表, lambda表达式等 -
Lambda表达式
#include<bits/stdc++.h> using namespace std; struct node { int a, b; } e[50]; int get_int(int n) { return rand() % n + 1; } int main() { srand((unsigned int)time(NULL)); auto add = [](int a, int b) { return a + b; }; cout << add(1, 2) << endl; int n = 10; for(int i = 1; i <= n; i++) { e[i].a = i; e[i].b = get_int(20); } for(int i = 1; i <= n; i++) { cout << e[i].a << " " << e[i].b << endl; } sort(e + 1, e + n + 1, [](const node & x, const node & y) { return x.b < y.b; }); cout << "**************" << endl; for(int i = 1; i <= n; i++) { cout << e[i].a << " " << e[i].b << endl; } }
-
右值引用和移动语义
完美转发就是在参数传递过程中,所有这些属性和参数值都不能改变。在泛型函数中,这样的需求十分普遍。
为了保证这些属性,泛型函数需要重载各种版本,左值右值不同版本,还要分别对应不同的const关系,
但是如果只定义一个右值引用参数的函数版本,这个问题就迎刃而解了,原因在于:
C++11对T&&的类型推导: 右值实参为右值引用,左值实参仍然为左值
- 智能指针
shared_ptr,基于引用计数的智能指针,会统计当前有多少个对象同时拥有该内部指针;当引用计数降为0时,自动释放
weak_ptr,基于引用计数的智能指针在面对循环引用的问题将无能为力,因此C++11还引入weak_ptr与之配套使用,weak_ptr只引用,不计数
unique_ptr: 遵循独占语义的智能指针,在任何时间点,资源智能唯一地被一个unique_ptr所占有,当其离开作用域时自动析构。资源所有权的转移只能通过std::move()
而不能通过赋值
- C++11多线程编程:
thread
库及其相配套的同步原语mutex
,lock_guard
,condition_variable
, 以及异步std::furture
4.malloc的原理,brk系统调用干什么的,mmap呢
Malloc函数用于动态分配内存。
为了减少内存碎片和系统调用的开销,malloc其采用内存池的方式,先申请大块内存作为堆区,然后将堆区分为多个内存块,以块作为内存管理的基本单位。当用户申请内存时,直接从堆区分配一块合适的空闲块。
Malloc采用隐式链表结构将堆区分成连续的、大小不一的块,包含已分配块和未分配块;同时malloc采用显示链表结构来管理所有的空闲块,即使用一个双向链表将空闲块连接起来,每一个空闲块记录了一个连续的、未分配的地址。
当进行内存分配时,Malloc会通过隐式链表遍历所有的空闲块,选择满足要求的块进行分配;当进行内存合并时,malloc采用边界标记法,根据每个块的前后块是否已经分配来决定是否进行块合并。
Malloc在申请内存时,一般会通过brk或者mmap系统调用进行申请。其中当申请内存小于128K时,会使用系统函数brk在堆区中分配;而当申请内存大于128K时,会使用系统函数mmap在映射区分配。
5.C++的内存管理方式,STL的allocaotr,最新版本默认使用的分配器
STL的分配器用于封装STL容器在内存管理上的底层细节。在C++中,其内存配置和释放如下:
new运算分两个阶段:
(1)调用::operator new配置内存;
(2)调用对象构造函数构造对象内容
delete运算分两个阶段:
(1)调用对象析构函数;
(2)调用::operator delete释放内存
为了精密分工,STL allocator将两个阶段操作区分开来:
内存配置有alloc::allocate()负责,
内存释放由alloc::deallocate()负责;
对象构造由::construct()负责,
对象析构由::destroy()负责。
同时为了提升内存管理的效率,减少申请小内存造成的内存碎片问题,SGI STL采用了两级配置器,当分配的空间大小超过128B时,会使用第一级空间配置器;当分配的空间大小小于128B时,将使用第二级空间配置器。
第一级空间配置器直接使用malloc()、realloc()、free()函数进行内存空间的分配和释放
第二级空间配置器采用了内存池技术,通过空闲链表来管理内存。
6.hash表的实现,包括STL中的哈希桶长度常数。
(1)对于构造哈希来说,主要包括直接地址法、平方取中法、除留余数法等。
(2)对于处理哈希冲突来说,最常用的处理冲突的方法有开放定址法、再哈
希法、链地址法、建立公共溢出区等方法。
SGL版本使用链地址法,使用一个链表保持相同散列值的元素。虽然链地址法并不要求哈希桶长度必须为质数,但SGI STL仍然以质数来设计哈希桶长度,并且将28个质数(逐渐呈现大约两倍的关系)计算好,以备随时访问,同时提供一个函数,用来查询在这28个质数之中,“最接近某数并大于某数”的质数。
7.hash表如何rehash,以及怎么处理其中保存的资源
C++的hash表中有一个负载因子loadFactor
当loadFactor<=1时,hash表查找的期望复杂度为O(1).
因此,每次往hash表中添加元素时,我们必须保证是在loadFactor <1的情况下,才能够添加。
因此,当Hash表中loadFactor==1时,Hash就需要进行rehash。
rehash过程中,会模仿C++的vector扩容方式,Hash表中每次发现loadFactor ==1时,就开辟一个原来桶数组的两倍空间,称为新桶数组,然后把原来的桶数组中元素全部重新哈希到新的桶数组中。
8.C++处理异常的两种方式,返回错误码、抛出异常的优劣
返回错误码:
每次调用都要处理, 不处理就会埋下地雷,而且可以在允许异常的工程使用而不存在问题.
抛异常:
异常可以一堆一起处理
缺点是速度稍慢(不重要),
代码膨胀(比较重要),
不能在不能使用异常的地方使用(比如C/C++混合, 或者项目要求不能用异常)(非常重要)
还有就是假如异常类设计得不科学就会出现问题. 比如内存不足,抛出一个内存不足的异常,
但是这个异常内部又要申请内存又得抛,然后就栈溢出了。
9.printf和cout的区别
printf是stdio库中的一个函数
cout是iostream类的一个对象,cout的成员运算符函数operator <<重载了各个不同的版本
cout可以连续输出的原因是就是this指针的作用,在完成一次输出之后返回this指针,因为在这里方法是藏在已经实例化的对象中,所以返回this指针,后面就可以连续输出。
10.为什么模板声明与定义要放在同一文件中?
通常情况下,你会在.h文件中声明函数和类,而将它们的定义放置在一个单独的.cpp文件中。
但是在使用模板时,这种习惯性做法将变得不再有用,因为当实例化一个模板时,编译器必须看到模板确切的定义,而不仅仅是它的声明。
因此,最好的办法就是将模板的声明和定义都放置在同一个.h文件中。这就是为什么所有的STL头文件都包含模板定义的原因。
标准要求编译器在实例化模板时必须在上下文中可以查看到其定义实体;
而反过来,在看到实例化模板之前,编译器对模板的定义体是不处理的——原因很简单,编译器怎么会预先知道 typename 实参是什么呢?因此模板的实例化与定义体必须放到同一翻译单元中
11.C语言中如何解决重复include的问题
采用#ifndef
格式如下:
#ifndef __SOMEFILE H
#define __SOMEFILE H
...//一些声明语句
#endif
12.C++的类中哪些成员会被算入sizeof中
1.非静态数据成员
2.还有要维护虚函数表的指针,占4字节。
解释:使用sizeof()计算类大小的一些基本原则:
类的大小为类的非静态成员数据的类型大小之和,也就是说静态成员数据不作考虑;
类的总大小也遵守类似class字节对齐的,调整规则;
成员函数都是不会被计算的;
如果是子类,那么父类中的成员也会被计算;
虚函数由于要维护虚函数表,所以要占据一个指针大小,也就是4字节。
总结:一个类中,虚函数、成员函数(包括静态与非静态)和静态数据成员都不占用类对象的存储空间。
计算机网络
TCP怎么保证可靠性,并且简述一下TCP建立连接和断开连接的过程
TCP保证可靠性:
(1)序列号、确认应答、超时重传
数据到达接收方,接收方需要发出一个确认应答,表示已经收到该数据段,并且确认序号会说明了它下一次需要接收的数据序列号。如果发送发迟迟未收到确认应答,那么可能是发送的数据丢失,也可能是确认应答丢失,这时发送方在等待一定时间后会进行重传。这个时间一般是2*RTT(报文段往返时间)+一个偏差值。
(2)窗口控制与高速重发控制/快速重传(重复确认应答)
TCP会利用窗口控制来提高传输速度,意思是在一个窗口大小内,不用一定要等到应答才能发送下一段数据,窗口大小就是无需等待确认而可以继续发送数据的最大值。如果不使用窗口控制,每一个没收到确认应答的数据都要重发。
使用窗口控制,如果数据段1001-2000丢失,后面数据每次传输,确认应答都会不停地发送序号为1001的应答,表示我要接收1001开始的数据,发送端如果收到3次相同应答,就会立刻进行重发;但还有种情况有可能是数据都收到了,但是有的应答丢失了,这种情况不会进行重发,因为发送端知道,如果是数据段丢失,接收端不会放过它的,会疯狂向它提醒......
(3)拥塞控制
如果把窗口定的很大,发送端连续发送大量的数据,可能会造成网络的拥堵(大家都在用网,你在这狂发,吞吐量就那么大,当然会堵),甚至造成网络的瘫痪。所以TCP在为了防止这种情况而进行了拥塞控制。
慢启动:定义拥塞窗口,一开始将该窗口大小设为1,之后每次收到确认应答(经过一个rtt),将拥塞窗口大小*2。
拥塞避免:设置慢启动阈值,一般开始都设为65536。拥塞避免是指当拥塞窗口大小达到这个阈值,拥塞窗口的值不再指数上升,而是加法增加(每次确认应答/每个rtt,拥塞窗口大小+1),以此来避免拥塞。
将报文段的超时重传看做拥塞,则一旦发生超时重传,我们需要先将阈值设为当前窗口大小的一半,并且将窗口大小设为初值1,然后重新进入慢启动过程。
快速重传:在遇到3次重复确认应答(高速重发控制)时,代表收到了3个报文段,但是这之前的1个段丢失了,便对它进行立即重传。
然后,先将阈值设为当前窗口大小的一半,然后将拥塞窗口大小设为慢启动阈值+3的大小。
这样可以达到:在TCP通信时,网络吞吐量呈现逐渐的上升,并且随着拥堵来降低吞吐量,再进入慢慢上升的过程,网络不会轻易的发生瘫痪。
数据库
1.数据库中delete和drop的区别
drop主要用于删除结构:
drop database XX,drop table XXXX,alter table XXX drop XXXX
delete主要用于删除数据
delete from XXX where xxx=“xxx”
2.数据库中视图的应用场景,数据库数据改变视图中的数据是否会改变
视图是用来提供给用户的,提取出一个表或者多个表中用户感兴趣的数据是视图的功能,数据库数据改变视图中的数据也会改变