C++
1、C++多态性与虚函数表
①C++多态的实现
多态分为静态多态和静态多态。静态多态通过重载和模板技术实现,在编译时候确定。动态多态通过虚函数和继承关系来实现,在运行时候确定。动态多态实现的几个条件:
- 虚函数
- 一个基类指针指向派生类的对象
基类指针在调用成员函数(虚函数)时,就会去查找该对象的虚函数表,虚函数表的地址在每个对象的首地址,查找该虚函数表中该函数的指针进行调用。
虚函数表中为什么就能准确查找相应的函数指针呢?因为在类设计的时候,虚函数表直接从基类也继承过来,如果覆盖了其中的某个虚函数,那么虚函数表的指针就会被替换,因此可以根据指针准确找到该调用哪个函数。
编译器为每个类维护一个虚函数表,同一个类的不同对象实际指向同一个虚函数表。
②虚函数的作用
- 实现多态
- 在设计上具有封装和抽象的作用,比如抽象类。
③为什么析构函数需要定义成虚函数
为了实现多态进行动态绑定,将派生类对象指针绑定到基类指针上,对象销毁时,若析构函数没有定义成虚函数,则会调用基类的析构函数,显然只能释放基类部分内存。若需要调用派生对象的析构函数,需要将析构函数定义成虚函数,销毁时根据虚函数表找到派生类的析构函数并调用,之后再调用基类的析构函数。
④为什么构造函数不能是虚函数?
虚函数的调用是通过虚函数指针在对象的虚函数表中查找,但调用构造函数前对象还未初始化,即内存空间还没有,虚指针也没有。
⑤析构函数能抛出异常吗?
不能,通常异常发生时,C++机制会调用对象的析构函数来释放资源,相当于异常处理的一部分。但若析构抛出异常,则前一个异常尚未处理,又有新的异常,会造成程序崩溃。
⑥不能在构造、析构函数中调用虚函数
1.2 C++对象内存模型
2、智能指针
shared_ptr、unique_ptr、weak_ptr、auto_ptr
2.1 std :: enable_shared_from_this
①auto_ptr
- 优势
创建auto_ptr对象,并初始化为Test类对象。当p离开作用域时,它会自动释放已分配的内存。
int main() {
std::auto_ptr<Test> p( new Test(5) );
cout << p->m_a << endl;
}
- 不足
① 当把一个auto_ptr
赋给另外一个auto_ptr
时,它的所有权(ownship)也转移了。比如在Fun()
中的参数auto_ptr p1
,然后在Fun()
中把实参auto_ptr p
传递给了Fun()
函数,当Fun()
函数执行完毕时,指针的所有权不会再返还给p
。
void Fun(auto_ptr<Test> p1) {}
int main() {
std::auto_ptr<Test> p( new Test(5) );
Fun(p);
std::cout << p->value_;
return 0;
}
auto_prt
的野指针行为会导致上述代码崩溃。当Fun
调用时,p
把内存的所有权转移给了p1
,函数执行结束,p1
离开了作用域,其关联的内存块也释放了,p
仍试图访问对象的成员变量,造成crash。
②auto_ptr不能和标准容器一起使用,也不能指向一组对象数组,std::auto_ptr<Test> p(new Test[5]);
是非法的。
②shared_ptr
- 优势
多个shared_ptr可以共享一个对象的所有权。当最后一个shared_ptr离开作用域时,内存才会自动释放。引用计数
的概念,可以通过调用use_count()
可以得到引用计数,当计数为0时,对象自动析构。
get()
: 获取shared_ptr绑定的资源.reset()
: 释放关联内存块的所有权,如果是最后一个指向该资源的shared_ptr,就释放这块内存unique
: 判断是否是唯一指向当前内存的shared_ptroperator bool
: 判断当前的shared_ptr是否指向一个内存块,可以用if 表达式判断
- 不足
循环引用问题
class B;
class A {
public:
shared_ptr<B> pb;
~A() {
cout << "kill A\n";
}
};
class B {
public:
shared_ptr<A> pa;
~B() {
cout <<"kill B\n";
}
};
int main() {
shared_ptr<A> sa(new A());
shared_ptr<B> sb(new B());
if(sa && sb) {
sa->pb=sb;
sb->pa=sa;
}
return 0;
}
③weak_ptr
weak_ptr可以解决shared_ptr的循环引用问题,以上述为例,将class A
中的shared_ptr<B> pb
以及class B
中的shared_ptr<A> pa
改为weak_ptr
类型。从shared_ptr
创建一个weak_ptr
增加了共享指针的弱引用计数(weak reference)
,但这个计数不作为是否释放资源的依据。换句话说,就是除非强引用计数变为0,才会释放掉指针指向的资源。
所以,当shared_ptr
离开作用域时,其内的资源释放了,这时候指向该资源的weak_ptr
发生了什么?weak_ptr过期了(expired)。如何判断weak_ptr是否指向有效资源,有两种方法:
- 调用use-count()去获取引用计数,该方法只返回强引用计数,并不返回弱引用计数。
- 调用expired()方法。比调用use_count()方法速度更快。
④unique_ptr
unique_ptr
遵循着独占语义,unique_ptr具有唯一性,对指向的对象值存在唯一的unique_ptr。unique_ptr不可复制、赋值。当unique_ptr
离开作用域,所包含的资源被释放。
3、C++内存管理
①C++内存分为哪几块?每块存储哪些变量?
- 栈区
通常是局部变量、函数参数等。由编译器在需要的时候分配,在不需要的时候自动清除的变量的存储区。 - 堆区(自由存储区)
由 malloc分配的内存块,他们的释放编译器不去管,由应用程序去控制,一般一个 new 就要对应一个 delete。如果程序员没有释放掉,那么在程序结束后,操作系统会自动回收。
new、malloc的区别…… - 静态/全局存储区
全局变量和静态变量、以及常量被分配到同一块内存中 - 代码区
存放程序的代码,即CPU执行的机器指令,并且是只读的;
4、C++11新特性
5、指针和引用的区别
// TODO
6、一些关键字 // TODO
①volatile
C++中volatile的作用
常用点:多线程应用中被多个任务共享的变量
②override
③explicit
数据结构
1、二叉树
熟悉这些概念是基础,不然面试的时候没有leetcode示例的加成,毫无思路可言。
- 满二叉树:
一棵深度为k,且有2^k-1个节点的树是满二叉树。
另一种定义:除了叶结点外每一个结点都有左右子叶且叶子结点都处在最底层的二叉树。
- 完全二叉树
如果二叉树中除去最后一层节点为满二叉树,且最后一层的结点依次从左到右分布,则此二叉树被称为完全二叉树。
- 平衡二叉树(AVL树)
空节点是二叉平衡树,父节点的左子树和右子树的高度之差不能大于1,递归定义。
下图高亮区域是一棵不平衡子树。
- 二叉搜索树(二叉查找树或者二叉排序树)
数据是有序的。二叉查找树中,左子树都比节点小,右子树都比节点大,递归定义。
特征:中序遍历是递增的。
- 后继节点与前驱节点
一个节点的后继节点是指这个节点在中序遍历序列中的下一个节点;
前驱节点是指中序遍历序列中的上一个节点。
网络
1、TCP与UDP
①TCP与UDP的区别
- 1、基于连接与无连接:TCP面向连接,要三次握手才会建立连接。UDP是基于非连接的协议,传输数据之前源端和终端不建立连接, 当它想传送时就简单地去抓取来自应用程序的数据,并尽可能快地把它扔到网络上。 在发送端,UDP传送数据的速度仅仅是受应用程序生成数据的速度、 计算机的能力和传输带宽的限制; 在接收端,UDP把每个消息段放在队列中,应用程序每次从队列中读一个消息段。
- 2、TCP可靠,保证数据正确性;UDP可能丢包;(校验和、滑动窗口(保证数据次序又提高吞吐量)、超时重发与快速重发机制……)
- 3、字节流模式与数据报模式 ;UDP是面向报文的。发送方的UDP对应用程序交下来的报文, 在添加首部后就向下交付给IP层。既不拆分,也不合并,而是保留这些报文的边界。而TCP是基于数据流的,发送端会对数据进行合并。
- 4、流量控制:TCP的滑动窗口
- 5、拥塞控制:TCP慢开始、快重发、快恢复;UDP吞吐量不受拥挤控制算法的调节,只受应用软件生成数据的速率、传输带宽、 源端和终端主机性能的限制
②TCP三次握手与四次挥手
- 为什么连接的时候是三次握手,关闭的时候却是四次握手?
- 为什么TIME_WAIT状态需要经过2MSL(最大报文段生存时间)才能返回到CLOSE状态?
- 为什么不能用两次握手进行连接?
③TCP协议策略
- 连接管理——3次握手和4次握手
- 数据破坏——通过校验和
- 丢包——应答与超时重传/快速重传机制
- 分片乱序——序列号
- 窗口滑动——提高发送效率,对发送端和接收端流量进行控制
- 加快通信速度——快速重发,三次收到重发消息进行重发
- 流控制——避免网络流量浪费
- 拥塞控制——慢启动算法,拥塞窗口
2、HTTP与HTTPS
①http与https区别,如何实现加密传输?
- http明文传输,数据都是未加密的;https就是在http与传输层之间加上了一个SSL
- 对称加密与非对称加密
②get与post区别
- get重点在从服务器上获取资源,post重点在向服务器发送数据;
- get传输数据是通过URL请求,以field(字段)= value的形式,置于URL后,并用"?“连接,多个请求数据间用”&"连接,如http://127.0.0.1/Test/login.action?name=admin&password=admin,这个过程用户是可见的;post传输数据通过Http的post机制,将字段与对应值封存在请求实体中发送给服务器,这个过程对用户是不可见的;
- Get传输的数据量小,因为受URL长度限制,但效率较高;Post可以传输大量数据,所以上传文件时只能用Post方式;
- get是不安全的,因为URL是可见的,可能会泄露私密信息,如密码等;post较get安全性较高;
③返回状态码
1xx:指示信息–表示请求已接收,继续处理。
2xx:成功–表示请求已被成功接收、理解、接受。
3xx:重定向–要完成请求必须进行更进一步的操作。
4xx:客户端错误–请求有语法错误或请求无法实现。
5xx:服务器端错误–服务器未能实现合法的请求。
200:请求被正常处理 204:请求被受理但没有资源可以返回
203:鉴权失败
206:客户端只是请求资源的一部分,服务器只对请求的部分资源执行GET方法,相应报文中通过Content-Range指定范围的资源。
301:永久性重定向
302:临时重定向
303:与302状态码有相似功能,只是它希望客户端在请求一个URI的时候,能通过GET方法重定向到另一个URI上
304:发送附带条件的请求时,条件不满足时返回,与重定向无关 307:临时重定向,与302类似,只是强制要求使用POST方法
400:请求报文语法有误,服务器无法识别
401:请求需要认证
403:请求的对应资源禁止被访问
404:服务器无法找到对应资源
500:服务器内部错误
503:服务器正忙
④一个URL从浏览器输入到响应页面,整个过程是怎么样的
总体来说分为以下几个过程:
- DNS 解析:将域名解析成 IP 地址
- TCP 连接:TCP 三次握手
- 发送 HTTP 请求:请求行(请求方法,URL,协议版本)、请求头、请求体
- 服务器处理HTTP请求并返回报文
- 浏览器解析渲染页面
- 断开连接:TCP 四次挥手
浏览器中输入URL,首先浏览器要将URL解析为IP地址,解析域名就要用到DNS协议,默认53端口,首先主机会查询DNS的缓存(浏览器缓存、操作系统缓存、路由缓存、),如果没有就给本地DNS发送查询请求。DNS查询分为两种方式,一种是递归查询,一种是迭代查询。如果是迭代查询,本地的DNS服务器,向根域名服务器发送查询请求,根域名服务器告知该域名的一级域名服务器,然后本地服务器给该一级域名服务器发送查询请求,然后依次类推直到查询到该域名的IP地址。DNS协议基于TCP或UDP,客户端默认使用UDP,但报文长度超出范围后转换为TCP协议。
得到IP地址后,浏览器就要与服务器建立一个HTTP连接。因此要用到HTTP协议,HTTP生成一个GET请求报文,将该报文传给TCP层处理。如果采用HTTPS还会先对数据进行加密。TCP层如果有需要先将HTTP数据包分片,分片依据路径MTU和MSS。TCP的数据包然后会发送给IP层,用到IP协议。IP层通过路由选路,一跳一跳发送到目的地址。当然在一个网段内的寻址是通过以太网协议实现(也可以是其他物理层协议,比如PPP,SLIP),以太网协议需要直到目的IP地址的物理地址,有需要ARP协议。
Linux
1、进程与线程的区别
- 进程可以作为独立的运行程序,线程是进程的内部的一个执行序列
- 进程是系统进行资源分配的单元,线程是执行和调度的单元
- (同一进程的)线程在共享内存空间中运行,多个线程共享进程的资源,而进程在单独的内存空间中运行。
- 多进程的优点:每个进程互相独立,不影响主程序的稳定性,子进程崩溃没关系;逻辑控制复杂,需要和主程序交互;
- 多线程的特点: 程序逻辑和控制方式简单; 所有线程可以直接共享内存和变量等; 线程方式消耗的总资源比进程方式好;上下文切换开销较少;不足:线程之间的同步和加锁控制比较麻烦; 一个线程的崩溃可能影响到整个程序的稳定性;
2、Linux中进程和线程使用的函数
- 获取当前进程号:
pid_t getpid(void)
- 获取父进程号:
pid_t getppid(void)
- 创建进程:
pid_t fork(void)
pid_t vfork(void)
区别:
fork()
在当前进程中创建一个进程,与父进程共享代码段,复制父进程的堆栈段和数据段,子进程复制父进程,然后执行fork()后的代码。vfork()
保证了子进程先执行,子进程退出后父进程才执行,而且在创建时不像fork分配一片新的进程空间,而是在父进程的空间里执行。
- 进程退出:
void exit(int value)
- 进程的等待函数:
pid_t wait(int *status)
挂起调用他(现在)的进程,直到子进程结束,然后才接着运行该进程 - 系统调用:
int system(const char *file)
- 线程创建:
int pthread_create
- 线程退出:
void pthread_exit
- 等待线程结束:
int pthread_join
- 线程取消:
int pthread_cancel
3、进程间的通信方式
- 管道/匿名管道(pipe)
- 命名管道(FIFO)
- 信号(Signal)
- 消息队列
- 共享内存
- 信号量
- 套接字
4、线程间的同步方式
linux下线程同步的三种方式
linux下有:互斥锁pthread_mutex_t
、条件变量pthread_cond_t
、信号量sem