字节跳动面试题汇总 -- C++后端(含答案)

Posted linux大本营

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了字节跳动面试题汇总 -- C++后端(含答案)相关的知识,希望对你有一定的参考价值。

malloc和new的区别

new/delete 是 C++关键字,需要编译器支持。malloc/free 是库函数,需要头文件支持 使用 new 操作符申请内存分配时无须指定内存块的大小,编译器会根据类型信息自行计算。而 malloc 则需要显式地指出所需内存的尺寸 new 操作符内存分配成功时,返回的是对象类型的指针,类型严格与对象匹配,无须进行类型转换,故 new 是符合类型安全性的操作符。而 malloc 内存分配成功则是返回 void * ,需要通过强制类型转换将 void*指针转换成我们需要的类型 new 内存分配失败时,会抛出 bad_alloc 异常。malloc 分配内存失败时返回 NULL new 会先调用 operator new 函数,申请足够的内存(通常底层使用 malloc实现)。然后调用类型的构造函数,初始化成员变量,最后返回自定义类型指针。delete 先调用析构函数,然后调用 operator delete 函数释放内存(通常底层使用 free 实现)。malloc/free 是库函数,只能动态的申请和释放内存,无法强制要求其做自定义类型对象构造和析构工作

排序算法时间复杂度

 如何用Linux shell命令统计一个文本中各个单词的个数

more log.txt | tr ' ' '\\n' | sort | uniq -c

Linux下需要打开或者查看大文件

查看文件的几行到几行 sed -n '10,10000p' log # 查看第10到10000行的数据

linux 怎么查看进程,怎么结束进程?原理是什么?

top, kill

ps -e 查看进程详细信息

Linux怎么查看当前的负载情况

uptime命令主要用于获取主机运行时间和查询linux系统负载等信息

cat /proc/loadavg

tload

top

Http Code

MySQL的底层索引结构,InnoDB里面的B+Tree

不同引擎索引的区别

i++是否原子操作

锁的底层实现

B Tree 和 B+ Tree的区别

b树和b+树

B树(B-tree)是一种树状数据结构,它能够存储数据、对其进行排序并允许以O(log n)的时间复杂度运行进行查找、顺序读取、插入和删除的数据结构。B树,概括来说是一个节点可以拥有多于2个子节点的二叉查找树。与自平衡二叉查找树不同,B-树为系统最优化大块数据的读和写操作。B-tree算法减少定位记录时所经历的中间过程,从而加快存取速度。普遍运用在数据库和文件系统。 B 树可以看作是对2-3查找树的一种扩展,即他允许每个节点有M-1个子节点。M阶B树具有以下特征:

  1. 根节点至少有两个子节点
  2. 每个节点有M-1个key,并且以升序排列
  3. 位于M-1和M key的子节点的值位于M-1 和M key对应的Value之间
  4. 其它节点至少有M/2个子节点

B+树 B+树是对B树的一种变形树,它与B树的差异在于:

  • 有k个子结点的结点必然有k个关键码
  • 非叶结点仅具有索引作用,跟记录有关的信息均存放在叶结点中
  • 树的所有叶结点构成一个有序链表,可以按照关键码排序的次序遍历全部记录

B树与B+树的区别:

  • B树每个节点都存储数据,所有节点组成这棵树。B+树只有叶子节点存储数据(B+数中有两个头指针:一个指向根节点,另一个指向关键字最小的叶节点),叶子节点包含了这棵树的所有数据,所有的叶子结点使用链表相连,便于区间查找和遍历,所有非叶节点起到索引作用。
  • B树中叶节点包含的关键字和其他节点包含的关键字是不重复的,B+树的索引项只包含对应子树的最大关键字和指向该子树的指针,不含有该关键字对应记录的存储地址。
  • B树中每个节点(非根节点)关键字个数的范围为m/2(向上取整)-1,m-1,并且具有n个关键字的节点包含(n+1)棵子树。B+树中每个节点(非根节点)关键字个数的范围为m/2(向上取整),m,具有n个关键字的节点包含(n)棵子树。
  • B+树中查找,无论查找是否成功,每次都是一条从根节点到叶节点的路径。

B树的优点:

  • B树的每一个节点都包含key和value,因此经常访问的元素可能离根节点更近,因此访问也更迅速。

B+树的优点:

  • 所有的叶子结点使用链表相连,便于区间查找和遍历。B树则需要进行每一层的递归遍历。相邻的元素可能在内存中不相邻,所以缓存命中性没有B+树好。
  • b+树的中间节点不保存数据,能容纳更多节点元素。

B树与B+树的共同优点:

  • 考虑磁盘IO的影响,它相对于内存来说是很慢的。数据库索引是存储在磁盘上的,当数据量大时,就不能把整个索引全部加载到内存了,只能逐一加载每一个磁盘页(对应索引树的节点)。所以我们要减少IO次数,对于树来说,IO次数就是树的高度,而“矮胖”就是b树的特征之一,m的大小取决于磁盘页的大小。

MySQL索引的发展过程?是一来就是B+Tree的么?从 没有索引、hash、二叉排序树、AVL树、B树、B+树

MySQL里面的事务,说说什么是事务

数据库事务(Database Transaction) ,是指作为单个逻辑工作单元执行的一系列操作,要么完全地执行,要么完全地不执行。 事务处理可以确保除非事务性单元内的所有操作都成功完成,否则不会永久更新面向数据的资源。通过将一组相关操作组合为一个要么全部成功要么全部失败的单元,可以简化错误恢复并使应用程序更加可靠。一个逻辑工作单元要成为事务,必须满足所谓的 ACID(原子性、一致性、隔离性和持久性)属性。事务是数据库运行中的逻辑工作单位,由 DBMS 中的事务管理子系统负责事务的处理。

MySQL里面有那些事务级别,并且不同的事务级别会出现什么问题

读未提交 (脏读):最低的隔离级别,什么都不需要做,一个事务可以读到另一个事务未提交的结果。所有的并发事务问题都会发生 读提交 (读旧数据,不可重复读问题):只有在事务提交后,其更新结果才会被其他事务看见。可以解决脏读问题。 可重复读 (解决了脏读但是有幻影读):在一个事务中,对于同一份数据的读取结果总是相同的,无论是否有其他事务对这份数据进行操作,以及这个事务是否提交。可以解决脏读、不可重复读。 串行化:事务串行化执行,隔离级别最高,牺牲了系统的并发性。可以解决并发事务的所有问题。

不可重复读和幻读的区别

  1. 不可重复读是读异常,但幻读则是写异常
  2. 不可重复读是读异常的意思是,如果你不多select几次,你是发现不了你曾经select过的数据行已经被其他人update过了。避免不可重复读主要靠一致性快照
  3. 幻读是写异常的意思是,如果不自己insert一下,你是发现不了其他人已经偷偷insert过相同的数据了。解决幻读主要靠间隙锁

数据库持久性是怎么实现的?

详细 持久性是指一个事务一旦被提交了,那么对数据库中的数据的改变就是永久性的,即便是在数据库系统遇到故障的情况下也不会丢失提交事务的操作。 mysql采用了一种叫WAL(Write Ahead Logging)提前写日志的技术。意思就是说,发生了数据修改操作先写日志记录下来,等不忙的时候再持久化到磁盘。这里提到的日志就是redo log。 redo log称为重做日志,当有一条记录需要修改的时候,InnoDB引擎会先把这条记录写到redo log里面。redo log是物理格式日志,它记录的是对于每个页的修改。 redo log是由两部分组成的:一是内存中的重做日志缓冲(redo log buffer);二是用来持久化的重做日志文件(redo log file)。为了消耗不必要的IO操作,事务再执行过程中产生的redo log首先会redo log buffer中,之后再统一存入redo log file刷盘进行持久化,这个动作称为fsync binlog记录了mysql执行更改了所有操作,但不包含select和show这类本对数据本身没有更改的操作。但是不是说对数据本身没有修改就不会记录binlog日志。

  • binlog是mysql自带的,他会记录所有存储引擎的日志文件。而redo log是InnoDB特有的,他只记录该存储引擎产生的日志文件
  • binlog是逻辑日志,记录这个语句具体操作了什么内容。Redo log是物理日志,记录的是每个页的更改情况
  • redo log是循环写,只有那么大的空间。binlog采用追加写入,当一个binlog文件写到一定大小后会切换到下一个文件

更新一条语句的流程

  1. 首先执行器调用引擎获取数据,如果数据在内存中就直接返回;否则先从磁盘中读取数据,写入内存后再返回。
  2. 修改数据后再调用引擎接口写入这行数据
  3. 引擎层将这行数据更新到内存中,然后将更新操作写入redo log,这时候redo log标记为prepare状态。然后告诉执行器我处理完了,可以提交事务了。
  4. 执行器生成这个操作的binlog,并把binlog写入磁盘,然后调用引擎提交事务
  5. 引擎收到commit命令后,把刚才写入的redo log改成commit状态 这里使用了两阶段提交prepare阶段和commit阶段

相关视频推荐

【mysql数据库】C++程序员眼中MySQL的索引和事务

内存泄漏的3个解决方案与原理实现,知道一个可以轻松应用开发工作

10道网络八股文,每道都很经典,让你在面试中逼格满满

需要C/C++ Linux服务器架构师学习资料及大厂面试题加qun812855908获取(资料包括C/C++,Linux,golang技术,Nginx,ZeroMQ,MySQL,Redis,fastdfs,MongoDB,ZK,流媒体,CDN,P2P,K8S,Docker,TCP/IP,协程,DPDK,ffmpeg等),免费分享

 

 数据库回表是什么?

详细 Innodb的索引存在两类,一类是聚簇索引一类是非聚簇索引,Innodb有且仅有一个聚簇索引。1. 如果表定义了PK,则PK就是聚集索引;2. 如果表没有定义PK,则第一个not NULL unique列是聚集索引;否则,InnoDB会创建一个隐藏的row-id作为聚集索引; InnoDB 聚集索引 的叶子节点存储行记录,而普通索引的叶子节点存储主键值 在使用聚簇索引时,可以一步直接获取到记录值,而使用普通索引时,会首先获取记录的PK,然后再从聚簇索引中查找对应的记录,这个过程叫做回表

索引覆盖

理解方式一:就是select的数据列只用从索引中就能够取得,不必读取数据行,换句话说查询列要被所建的索引覆盖 理解方式二:索引是高效找到行的一个方法,但是一般数据库也能使用索引找到一个列的数据,因此它不必读取整个行。毕竟索引叶子节点存储了它们索引的数据;当能通过读取索引就可以得到想要的数据,那就不需要读取行了。一个索引包含了(或覆盖了)满足查询结果的数据就叫做覆盖索引 理解方式三:是非聚集复合索引的一种形式,它包括在查询里的Select、Join和Where子句用到的所有列(即建索引的字段正好是覆盖查询条件中所涉及的字段,也即,索引包含了查询正在查找的数据)。 总结:要查找的数据可以都在索引中出现,而不需要再去查表获取完整记录 为了实现索引覆盖可以将被查询的字段,建立到联合索引里去

数据库读写锁发生死锁的情景

详细

  • MyISAM中不会出现死锁 在MyISAM中只用到表锁,不会有死锁的问题,锁的开销也很小,但是相应的并发能力很差。 解析:MyISAM不支持事务,即每次的读写都会隐性的加上读写锁,而我们知道读锁是共享的,写锁是独占的,意味着当一个Session在写时,另一个Session必须等待。
  • InnoDB中会出现死锁 InnoDB中实用了行锁和表锁,当未命中索引时,会自动退化为表锁。 解决方法为InnoDB中的MVCC机制

为什么推荐主键使用自增的整型,MySQL为什么主键自增好

为什么推荐主键:Innodb底层是B+树,数据和索引放在一起,因此需要一个主键作为索引,从而存储数据 为什么要自增:当新存储一条数据时,只需要向B+树后面的叶子节点插入即可,而不需要B+ 树为保持有序而进行旋转 为什么要整型:整形作为索引,容易直接判断大小而保持有序,使用String,相对于整数而言,不易判断大小

vector的底层实现,扩容机制

详细 使用一段连续的内存来存储数据,同时在数据结构中保存了三个指针来标记内存地址,首先是指向vector中数据的起始位置的_Myfirst指针,最后一个数据的位置的_Mylst指针,以及一个指向连续内存空间末尾的_Myend指针 当 vector 的大小和容量相等(size==capacity)也就是满载时,如果再向其添加元素,那么 vector 就需要扩容。vector 容器扩容的过程需要经历以下 3 步:

  1. 完全弃用现有的内存空间,重新申请更大的内存空间;
  2. 将旧内存空间中的数据,按原有顺序移动到新的内存空间中;
  3. 最后将旧的内存空间释放 不同的编译器,vector 有不同的扩容大小。在 vs 下是 1.5 倍,在 GCC 下是 2 倍 采用成倍方式扩容,可以保证常数的时间复杂度,而增加指定大小的容量只能达到O(n)的时间复杂度,因此,使用成倍的方式扩容 过大的倍数将导致大量空间的浪费 为什么扩容二倍

MySQL中如果使用like进行模糊匹配的时候,是否会使用索引

mysql在使用like查询的时候只有使用后面的%时,才会使用到索引

Volatile的作用,Volatile如何保证可见性的?以及如何实现可见性的机制

volatile 关键字是一种类型修饰符,用它声明的类型变量表示可以被某些编译器未知的因素更改,比如:操作系统、硬件或者其它线程等。遇到这个关键字声明的变量,编译器对访问该变量的代码就不再进行优化,从而可以提供对特殊地址的稳定访问。声明时语法:int volatile vInt; 当要求使用 volatile 声明的变量的值的时候,系统总是重新从它所在的内存读取数据,即使它前面的指令刚刚从该处读取过数据。而且读取的数据立刻被保存 volatile 用在如下的几个地方:

  1. 中断服务程序中修改的供其它程序检测的变量需要加 volatile;
  2. 多任务环境下各任务间共享的标志应该加 volatile;
  3. 存储器映射的硬件寄存器通常也要加 volatile 说明,因为每次对它的读写都可能有不同意义 详细. volatile作用: 锁总线,其它CPU对内存的读写请求都会被阻塞,直到锁释放,不过实际后来的处理器都采用锁缓存替代锁总线,因为锁总线的开销比较大,锁总线期间其他CPU没法访问内存 lock后的写操作会回写已修改的数据,同时让其它CPU相关缓存行失效,从而重新从主存中加载最新的数据 不是内存屏障却能完成类似内存屏障的功能,阻止屏障两遍的指令重排序

如果大量的使用Volatile存在什么问题

操作系统的线程,以及它的状态

线程的基本状态: 1.新建 new语句创建的线程对象处于新建状态,此时它和其他java对象一样,仅被分配了内存。 2.等待 当线程在new之后,并且在调用start方法前,线程处于等待状态。 3.就绪 当一个线程对象创建后,其他线程调用它的start()方法,该线程就进入就绪状态。处于这个状态的线程位于Java虚拟机的可运行池中,等待cpu的使用权。 4.运行状态 处于这个状态的线程占用CPU,执行程序代码。在并发运行环境中,如果计算机只有一个CPU,那么任何时刻只会有一个线程处于这个状态。只有处于就绪状态的线程才有机会转到运行状态。 5. 阻塞状态 阻塞状态是指线程因为某些原因放弃CPU,暂时停止运行。当线程处于阻塞状态时,Java虚拟机不会给线程分配CPU,直到线程重新进入就绪状态,它才会有机会获得运行状态。 6.死亡状态 当线程执行完run()方法中的代码,或者遇到了未捕获的异常,就会退出run()方法,此时就进入死亡状态,该线程结束生命周期。

进程和线程(进程与线程)的区别以及使用场景

线程产生的原因:进程可以使多个程序能并发执行,以提高资源的利用率和系统的吞吐量;但是其具有一些缺点:

  • 进程在同一时间只能干一件事
  • 进程在执行的过程中如果阻塞,整个进程就会挂起,即使进程中有些工作不依赖于等待的资源,仍然不会执行。
  • 因此,操作系统引入了比进程粒度更小的线程,作为并发执行的基本单位,从而减少程序在并发执行时所付出的时空开销,提高并发性

进程是资源分配的最小单位,线程是操作系统进行执行和调度的最小单位

  1. 同一进程的线程共享本进程的地址空间,而进程之间则是独立的地址空间;
  2. 同一进程内的线程共享本进程的资源,但是进程之间的资源是独立的;
  3. 一个进程崩溃后,在保护模式下不会对其他进程产生影响,但是一个线程崩溃整个进程崩溃,所以多进程比多线程健壮;
  4. 进程切换,消耗的资源大。所以涉及到频繁的切换,使用线程要好于进程;
  5. 两者均可并发执行;
  6. 每个独立的进程有一个程序的入口、程序出口。但是线程不能独立执行,必须依存在应用程序中,由应用程序提供多个线程执行控制

使用场景

  • 需要频繁创建销毁的优先用线程 最常见的应用就是 Web 服务器了,来一个连接建立一个线程,断了就销毁线程
  • 需要进行大量计算的优先使用线程 所谓大量计算,当然就是要耗费很多 CPU,切换频繁了,这种情况下线程是最合适的 最常见的是图像处理、算法处理
  • 强相关的处理用线程,弱相关的处理用进程
  • 可能要扩展到多机分布的用进程,多核分布的用线程

线程私有:线程栈,寄存器,程序寄存器 共享:堆,地址空间,全局变量,静态变量 进程私有:地址空间,堆,全局变量,栈,寄存器 共享:代码段,公共数据,进程目录,进程 ID

为什么线程创建和撤销开销大

当从一个线程切换到另一个线程时,不仅会发生线程上下文切换,还会发生特权模式切换。 既然是线程切换,那么一定涉及线程状态的保存和恢复,包括寄存器、栈等私有数据。另外,线程的调度是需要内核级别的权限的(操作CPU和内存),也就是说线程的调度工作是在内核态完成的,因此会有一个从用户态到内核态的切换。而且,不管是线程本身的切换还是特权模式的切换,都要进行CPU的上下文切换

线程和协程的由来和作用

协程,又称微线程。协程看上去也是子程序,但执行过程中,在子程序内部可中断,然后转而执行别的子程序,在适当的时候再返回来接着执行。 和多线程比,协程最大的优势就是协程极高的执行效率。因为子程序切换不是线程切换,而是由程序自身控制,因此,没有线程切换的开销,和多线程比,线程数量越多,协程的性能优势就越明显 第二大优势就是不需要多线程的锁机制,因为只有一个线程,也不存在同时写变量冲突,在协程中控制共享资源不加锁,只需要判断状态就好了,所以执行效率比多线程高很多 在协程上利用多核 CPU —— 多进程+协程,既充分利用多核,又充分发挥协程的高效率, 可获得极高的性能

多线程的通信和同步,多线程访问同一个对象怎么办

  • 互斥锁
  • 条件变量
  • 读写锁
  • 信号

查看端口号或者进程号,使用什么命令

查看程序对应的进程号: ps -ef | grep 进程名字 查看进程号所占用的端口号: netstat -nltp | grep 进程号 查看端口号所使用的进程号: lsof -i:端口号

信号量与mutex和自旋锁的区别

信号量(semaphore)用在多线程多任务同步的,一个线程完成了某一个动作就通过信号量告诉别的线程,别的线程再进行某些动作。而互斥锁(Mutual exclusion,缩写 Mutex)是用在多线程多任务互斥的,一个线程占用了某一个资源,那么别的线程就无法访问,直到这个线程unlock,其他的线程才开始可以利用这个资源 自旋锁与前两者的区别是自旋锁不会引起调用者睡眠,如果自旋锁已经被别的执行单元保持,调用者就一直循环在那里看是否该自旋锁的保持者已经释放了锁

非对称加密与对称加密

对称加密算法:加密效率高,速度快,适合大数据量加密。DES/AES 非对称加密算法:算法复杂,加密速度慢,安全性更高。结合对称加密使用。RSA、DH

在两列属性上分别建索引,则某个查询语句使用 where att1 = * and att2 = * 会怎么使用索引

一次查询只使用一个索引,因为每个索引都代表了一颗树,使用多个索引所带来的增益远小于增加的性能消耗。 对于联合索引来说会使用最左匹配 -- 在MySQL的user表中,对a,b,c三个字段建立联合索引,根据查询字段的位置不同来决定,如查询a, a,b a,b,c a,c 都可以走索引,其他条件的查询不能走索引 对于多个单列索引来说,MySQL会试图选择一个限制最严格的索引。但是,即使是限制最严格的单列索引,它的限制能力也肯定远远低于在多列上的多列索引

通过两个索引查询出来的结果,会进行什么要的操作?交集,并集

MySQL中遇到一些慢查询,有什么解决方法

  • 定位慢查询

根据慢查询日志定位慢查询sql

  1. slow_query_log 默认是off关闭的,使用时,需要改为on 打开
  2. slow_query_log_file 记录的是慢日志的记录文件
  3. long_query_time 默认是10S,每次执行的sql达到这个时长,就会被记录
  • 优化方案

优化数据库结构 分解关联查询: 很多高性能的应用都会对关联查询进行分解,就是可以对每一个表进行一次单表查询,然后将查询结果在应用程序中进行关联,很多场景下这样会更高效。 增加索引 建立视图 优化查询语句 添加存储过程 冗余保存数据

有了解过IO多路复用技术是个什么样的原理

I/O多路复用,I/O就是指的我们网络I/O,多路指多个TCP连接(或多个Channel),复用指复用一个或少量线程。串起来理解就是很多个网络I/O复用一个或少量的线程来处理这些连接。

通过一个线程,同时连接多个线程会不会存在多个线程切换

在操作系统中,有高速缓存,主存,虚拟内存,外存,有知道它们之间有什么样的关系,以及它们的作用是啥

缓存: 在CPU同时处理很多数据,而又不可能同时进行所有数据的传输的情况,把优先级低的数据暂时放入缓存中,等优先级高的数据处理完毕后再把它们从缓存中拿出来进行处理 主存:主存就是内存,是直接与CPU交换信息的存储器,指CPU能够通过指令中的地址码直接访问的存储器,常用于存放处于活动状态的程序和数据 虚拟内存:当运行数据超过内存限度,部分数据自动“溢出”,这时系统会将硬盘上的部分空间模拟成内存——虚拟内存,并且将暂时不运行的程序或不使用的数据存放到虚拟内存中等待需要时调用 辅存就是外存: 硬盘与磁盘、光盘、软盘、U盘等

缺页的产生和换页算法

缺页中断:进程线性地址空间里的页面不必常驻内存,在执行一条指令时,如果发现他要访问的页没有在内存中(存在位为0),那么停止该指令的执行,并产生一个页不存在异常,对应的故障处理程序可通过从外存加载该页到内存的方法来排除故障,之后,原先引起的异常的指令就可以继续执行,而不再产生异常 页面置换算法:将新页面调入内存时,如果内存中所有的物理页都已经分配出去,就要按某种策略来废弃某个页面,将其所占据的物理页释放出来,好的算法,让缺页率降低。

  1. 先进先出调度算法(FIFO)
  2. 最近最少调度算法(LFU,根据时间判断):利用局部性原理,根据一个作业在执行过程中过去的页面访问历史来推测未来的行为。它认为过去一段时间里不曾被访问过的页面,在最近的将来可能也不会再被访问。所以,这种算法的实质是:当需要淘汰一个页面时,总是选择在最近一段时间内最久不用的页面予以淘汰。
  3. 最近最不常用调度算法(LRU,根据使用频率判断
struct DLinkedNode 
    int key, value;
    DLinkedNode* prev;
    DLinkedNode* next;
    DLinkedNode(): key(0), value(0), prev(nullptr), next(nullptr) 
    DLinkedNode(int _key, int _value): key(_key), value(_value), prev(nullptr), next(nullptr) 
;

class LRUCache 
private:
    unordered_map<int, DLinkedNode*> cache;
    DLinkedNode* head;
    DLinkedNode* tail;
    int size;
    int capacity;

public:
    LRUCache(int _capacity): capacity(_capacity), size(0) 
        // 使用伪头部和伪尾部节点
        head = new DLinkedNode();
        tail = new DLinkedNode();
        head->next = tail;
        tail->prev = head;
    
    
    int get(int key) 
        if (!cache.count(key)) 
            return -1;
        
        // 如果 key 存在,先通过哈希表定位,再移到头部
        DLinkedNode* node = cache[key];
        moveToHead(node);
        return node->value;
    
    
    void put(int key, int value) 
        if (!cache.count(key)) 
            // 如果 key 不存在,创建一个新的节点
            DLinkedNode* node = new DLinkedNode(key, value);
            // 添加进哈希表
            cache[key] = node;
            // 添加至双向链表的头部
            addToHead(node);
            ++size;
            if (size > capacity) 
                // 如果超出容量,删除双向链表的尾部节点
                DLinkedNode* removed = removeTail();
                // 删除哈希表中对应的项
                cache.erase(removed->key);
                // 防止内存泄漏
                delete removed;
                --size;
            
        
        else 
            // 如果 key 存在,先通过哈希表定位,再修改 value,并移到头部
            DLinkedNode* node = cache[key];
            node->value = value;
            moveToHead(node);
        
    

    void addToHead(DLinkedNode* node) 
        node->prev = head;
        node->next = head->next;
        head->next->prev = node;
        head->next = node;
    
    
    void removeNode(DLinkedNode* node) 
        node->prev->next = node->next;
        node->next->prev = node->prev;
    

    void moveToHead(DLinkedNode* node) 
        removeNode(node);
        addToHead(node);
    

    DLinkedNode* removeTail() 
        DLinkedNode* node = tail->prev;
        removeNode(node);
        return node;
    
;
  1. 最佳置换算法(OPT):从主存中移出永远不再需要的页面;如无这样的页面存在,则选择最长时间不需要访问的页面。于所选择的被淘汰页面将是以后永不使用的,或者是在最长时间内不再被访问的页面,这样可以保证获得最低的缺页率

面向对象采用的设计模式有哪些

  • 在软件工程中,软件设计模式是通用的,可重用的在给定上下文中解决软件设计中常见问题的解决方案

单例模式(有的叫单元素模式,单态模式) 工厂模式 观察者模式 命令链模式 策略模式

设计模式的六大原则

为什么要有补码? (为了更方便的实现减法运算)

一致性哈希

面向对象有哪些设计原则

OCP原则(也叫开闭原则): 开闭原则就是说对扩展开放,对修改关闭。 SRP原则(职责单一原则): 一个类只负责一项职责,可以降低类的复杂度,提高类的可读性,提高系统的可维护性,当修改一个功能时,可以显著降低对其他功能的影响。 OCP原则(里氏替换原则):任何基类可以出现的地方,子类一定可以出现。通俗的理解即为子类可以扩展父类的功能,但不能改变父类原有的功能。 DIP原则(依赖倒置原则):高层模块不应该依赖低层模块,二者都应该依赖其抽象;抽象不应该依赖细节;细节应该依赖抽象。通俗点说:要求对抽象进行编程,不要对实现进行编程,这样就降低了客户与实现模块间的耦合。 LoD法则(迪米特法则):一个对象应该对其他对象保持最少的了解。通俗的来讲,就是一个类对自己依赖的类知道的越少越好。也就是说,对于被依赖的类来说,无论逻辑多么复杂,都尽量地的将逻辑封装在类的内部,对外除了提供的public方法,不对外泄漏任何信息。 接口隔离原则:建立单一接口,不要建立庞大臃肿的接口,尽量细化接口,接口中的方法尽量少。也就是说,我们要为各个类建立专用的接口,而不要试图去建立一个很庞大的接口供所有依赖它的类去调用。

说说你对于 “不要用共享内存来通信,而应该用通信来共享内存” 的理解

在锁模式中,一块内存可以被多个线程同时看到,所以叫共享内存。线程之间通过改变内存中的数据来通知其他线程发生了什么,所以是通过共享内存来通信。锁是为了保护一个线程对内存操作的逻辑完整性而引入的一种约定,注意是一种约定而不是规则(一个线程可以不获取锁就操作内存,也可以解锁其他线程加的锁从而破坏保护,这种错误很难发现)。这种约定要每个线程的编写人员自觉遵守,否则就会出现多线程问题,如数据被破坏,死锁,饥饿等。 在go模式中,一块内存同一时间只能被一个线程看到,另外一个线程要操作这块内存,需要当前线程让渡所有权,这个所有权的让渡过程是“通信”。通信的原子性由channel封装好了,内存同一时间只能被同一线程使用,所以这种模式下不需要显示的锁。然而go模式也有约定,如果传递的是内存的指针,或者是控制消息,还是等于共享了内存,还是要保证将所有权让渡后, 不能再操作这块内存。

什么是双向链表

双向链表也叫双链表,是链表的一种,它的每个数据结点中都有两个指针,分别指向直接后继和直接前驱。所以,从双向链表中的任意一个结点开始,都可以很方便地访问它的前驱结点和后继结点。

操作系统内存管理(分页、分段、段页式)

页式内存管理,内存分成固定长度的一个个页片。操作系统为每一个进程维护了一个从虚拟地址到物理地址的映射关系的数据结构,叫页表,页表的内容就是该进程的虚拟地址到物理地址的一个映射。页表中的每一项都记录了这个页的基地址。通过页表,由逻辑地址的高位部分先找到逻辑地址对应的页基地址,再由页基地址偏移一定长度就得到最后的物理地址,偏移的长度由逻辑地址的低位部分决定。一般情况下,这个过程都可以由硬件完成,所以效率还是比较高的。 页式内存管理的优点就是比较灵活,内存管理以较小的页为单位,方便内存换入换出和扩充地址空间。 分段存储管理方式的目的,主要是为了满足用户(程序员)在编程和使用上多方面的要求,其中有些要求是其他几种存储管理方式所难以满足的。因此,这种存储管理方式已成为当今所有存储管理方式的基础

  • 分页与分段的区别 页是信息的物理单位,分页是为实现离散分配方式,以消减内存的外零头,提高内存的利用率。或者说,分页仅仅是由于系统管理的需要而不是用户的需要。段则是信息的逻辑单位,它含有一组其意义相对完整的信息。分段的目的是为了能更好地满足用户的需要 页的大小固定且由系统决定,而段的长度却不固定 分页的作业地址空间是一维的,即单一的线性地址空间;而分段的作业地址空间则是二维的 分页系统能有效地提高内存利用率,而分段系统则能很好地满足用户需求。

段页式系统的基本原理,是分段和分页原理的结合,即先将用户程序分成若干个段,再把每个段分成若干个页,并为每一个段赋予一个段名。在段页式系统中,地址结构由段号、段内页号和页内地址三部分所组成。

浏览器输入网址到渲染的过程

详细

  • 浏览器构建HTTP Request请求
  • 网络传输
  • 服务器构建HTTP Response 响应
  • 网络传输
  • 浏览器渲染页面

DNS解析URL地址、生成HTTP请求报文、构建TCP连接、使用IP协议选择传输路线、数据链路层保证数据的可靠传输、物理层将数据转换成电子、光学或微波信号进行传输

DNS解析过程

  • DNS 协议运行在 UDP 协议之上,使用端口号 53,用于将域名转换为IP地址
  1. 浏览器先检查自身缓存中有没有被解析过这个域名对应的 ip 地址
  2. 如果浏览器缓存没有命中,浏览器会检查操作系统缓存中有没有对应的已解析过的结果。在 windows 中可通过 c 盘里 hosts 文件来设置
  3. 还没命中,请求本地域名服务器来解析这个域名,一般都会在本地域名服务器找到
  4. 本地域名服务器没有命中,则去根域名服务器请求解析
  5. 根域名服务器返回给本地域名服务器一个所查询域的主域名服务器
  6. 本地域名服务器向主域名服务器发送请求
  7. 接受请求的主域名服务器查找并返回这个域名对应的域名服务器的地址
  8. 域名服务器根据映射关系找到 ip 地址,返回给本地域名服务器
  9. 本地域名服务器缓存这个结果
  10. 本地域名服务器将该结果返回给用户

https讲一下?密钥怎么交换的?私钥存储在哪里

HTTPs 是以安全为目标的 HTTP 通道,简单讲是 HTTP 的安全版,即HTTP 下加入 SSL 层,HTTPS 的安全基础是 SSL,因此加密的详细内容就需要 SSL HTTPs的握手过程包含五步

  1. 浏览器请求连接
  2. 服务器返回证书:证书里面包含了网站地址,加密公钥,以及证书的颁发机构等信息,服务器采用非对称加密算法(RSA)生成两个秘钥,私钥自己保留
  3. 浏览器收到证书后作以下工作: 3.1 验证证书的合法性 3.2 生成随机(对称)密码,取出证书中提供的公钥对随机密码加密;浏览器即客户端使用非对称加密来加密对称加密规则,对称加密用于加密后续传输的信息 3.3 将之前生成的加密随机密码等信息发送给网站
  4. 服务器收到消息后作以下的操作 4.1 使用自己的私钥解密浏览器用公钥加密后的消息,并验证 HASH 是否与浏览器发来的一致;获得浏览器发过来的对称秘钥 4.2 使用加密的随机对称密码加密一段消息,发送给浏览器
  5. 浏览器解密并计算握手消息的 HASH:如果与服务端发来的 HASH 一致,此时握手过程结束,之后进行通信

http的流程

每个万维网的网点都有一个服务器进程,它不断的监听TCP端口80,以便发现是否有浏览器向它发出连接请求,一旦监听到连接建立请求,就通过三次握手建立TCP连接,然后浏览器会向服务器发出浏览某个页面的请求,服务器接着返回所请求的页面作为响应,然后TCP连接就被释放了。 这些响应和请求报文都遵循一定的格式,这就是HTTP协议所规定的。

SSL加密

  1. 客户端向服务器端索要并验证公钥
  2. 双方协商生成”对话密钥”。客户端用公钥对对话秘钥进行加密
  3. 服务器通过私钥解密出对话秘钥
  4. 双方采用”对话密钥”进行加密通信
  • 对称加密算法: 加密效率高,速度快,适合大数据量加密。DES/AES
  • 非对称加密算法:算法复杂,加密速度慢,安全性更高。结合对称加密使用。RSA、DH
  • 私钥存储在服务器上

session和cookie

  • cookie 是一种发送到客户浏览器的文本串句柄,并保存在客户机硬盘上,可以用来在某个WEB站点会话间持久的保持数据。
  • Session的本质上也是cookie,但是不同点是存在服务器上的。这就导致,你如果使用cookie,你关闭浏览器之后,就丢掉Cookie了,但是如果关掉浏览器,重新打开之后,发现还有相应的信息,那就说明用的是Session。因为cookie是存在本地的,所以也会有相应的安全问题,攻击者可以去伪造他,填写相应的字段名,就能登录你的账户,还有如果cookie的有效期很长的话,也不安全。
  • session 由服务器产生,对于客户端,只存储session id在cookie中

http和https的区别

  • https 协议需要到 ca 申请证书,一般免费证书较少,因而需要一定费用
  • http 是超文本传输协议,信息是明文传输,https 则是具有安全性的 ssl 加密传输协议
  • http 和 https 使用的是完全不同的连接方式,用的端口也不一样,前者是 80,后者是 443
  • http 的连接很简单,是无状态的;HTTPS 协议是由 SSL+HTTP 协议构建的可进行加密传输、身份认证的网络协议,比 http 协议安全

https的安全外壳是怎么实现的

HTTPS就是在原HTTP的基础上加上一层用于数据加密、解密、校验、身份认证的安全层SSL/TSL,用于解决HTTP存在的安全隐患 信息加密:所有信息都是加密传播,第三方无法窃听;内容经过对称加密,每个连接生成一个唯一的加密密钥; 身份认证:配备了身份认证,第三方无法伪造服务端(客户端)的身份 数据完整性校验:内容传输经过完整性校验,一旦报文被篡改,通信双方会立刻发现

TCP TIMEWAIT讲一下?为啥需要这个?

当断开连接时,客户端发送完ACK将处于TIME WAIT状态,保持2MSL,之后完全断开意义在于:

  1. 保证最后一次握手报文能到服务端,能进行超时重传
  2. 2MSL 后,这次连接的所有报文都会消失,不会影响下一次连接

说一下TCP/IP

TCP/IP协议是包含TCP协议和IP协议,UDP(User Datagram Protocol)协议、ICMP(Internet Control Message Protocol) 协议和其他一些的协议的协议组 TCP/IP定义了电子设备(如计算机)如何连入因特网,以及数据如何在它们之间传输的标准.它是互联网中的基本通信语言或协议,在私网中它也被用作通信协议,当用户直接网络连接时,计算机应提供一个TCP/IP程序的标准实现,而且接受所发送的信息的计算机也应只有一个TCP/IP程序的标准实现 TCP/IP协议并不完全符合OSI 标准定制的七层参考模型,它采取了四层的层级结构 网络接口层:接收IP数据包并进行传输,从网络上接收物理帧,抽取IP 转交给下一层,对实际网络的网络媒体的管理,定义如何使用物理网络 ,如以太网。 网际层IP: 负责提供基本的数据封包传送功能,让每一块数据包都能打到目的主机,但不检查是否被正确接收,主要表现为IP协议 传输层:在此层中,它提供了节点的数据传送,应用程序之间的通信服务,主要是数据格式化,数据确认和丢失重传等。主要协议包括TCP和UDP 应用层:应用程序间沟通单层,如万维网(WWW)、简单电子邮件传输(SMTP)、文件传输协议(FTP)、网络远程访问协议(Telnet)等

 

 fread和read的区别

read/write 操作文件描述符 (int型) fread/fwrite 操作文件流 (FILE*型) fread/fwrite 调用 read/write read/write是系统调用,要自己分配缓存,也就是说效率要自己根据实际情况来控制。 fread/fwrite是标准输入/输出函数,不需要自己分配缓存,对于一般情况具有较高的效率。

什么是内存栅栏

内存栅栏(Memory Barrier)就是从本地或工作内存到主存之间的拷贝动作。 仅当写操作线程先跨越内存栅栏而读线程后跨越内存栅栏的情况下,写操作线程所做的变更才对其他线程可见。关键字 synchronized 和 volatile 都强制规定了所有的变更必须全局可见,该特性有助于跨越内存边界动作的发生,无论是有意为之还是无心插柳。 在程序运行过程中,所有的变更会先在寄存器或本地 cache 中完成,然后才会被拷贝到主存以跨越内存栅栏。此种跨越序列或顺序称为 happens-before。 写操作必须要 happens-before 读操作,即写线程需要在所有读线程跨越内存栅栏之前完成自己的跨越动作,其所做的变更才能对其他线程可见

TCP拥塞控制

拥塞控制的最终受控变量是发送端向网络一次连续写入的数据量(收到其中第一个数据报的确认之前),称之为发送窗口(SWND),SWND 受接收方接受窗口(RWND)的影响。同时也受控于发送方的拥塞窗口(CWND)。SWND = min(RWND, CWND )

  • 当 cwnd < 慢开始门限(ssthresh) 时,使用慢开始算法
  • 当 cwnd > ssthresh 时,改用拥塞避免算法
  • 快重传要求接收方在收到一个失序的报文段后就立即发出重复确认(为的是使发送方及早知道有报文段没有到达对方)而不要等到自己发送数据时捎带确认。快重传算法规定,发送方只要一连收到三个重复确认就应当立即重传对方尚未收到的报文段,而不必继续等待设置的重传计时器时间到期。快重传配合使用的还有快恢复算法,是将 ssthresh减半,然后将 cwnd 设置为 ssthresh 的大小,之后执行拥塞避免算法

TCP为什么要三次握手、但是要四次挥手,握手第二步拆开行不行,挥手2 3步合并行不行,第三次握手确认的是什么能力

三次握手是为了避免僵尸连接,四次挥手是为了确保被断开方的数据能够全部完成传输,握手的第二部可以分开,不过需要增加一下状态。如果服务端没有数据要发送,挥手的2,3步可以合并,因为TCP是全双工的。第三次握手确认的是客户端时真实IP

TCP和UDP的区别

TCP 是面向连接的传输层协议,即传输数据之前必须先建立好连接, UDP 无连接 TCP 是点对点的两点间服务,即一条 TCP 连接只能有两个端点;UDP 支持一对一,一对多,多对一,多对多的交互通信 TCP 是可靠交付:无差错,不丢失,不重复,按序到达;UDP 是尽最大努力交付,不保证可靠交付 TCP 有拥塞控制和流量控制保证数据传输的安全性;UDP 没有拥塞控制,网络拥塞不会影响源主机的发送效率 TCP 是动态报文长度,即 TCP 报文长度是根据接收方的窗口大小和当前网络拥塞情况决定的。UDP 面向报文,不合并,不拆分,保留上面传下来报文的边界 TCP 首部开销大,首部 20 个字节;UDP 首部开销小,8 字节 如果数据完整性更重要,如文件传输、重要状态的更新等,应该选用 TCP 协议。如果通信的实时性较重要,如视频传输、实时通信等,则使用 UDP 协议

用udp会有什么问题

不可靠,不稳定 因为本身没有重传的控制机制,所以丢包率的可能是其最主要的问题

TCP是可靠的,为什么UDP还要去实现可靠连接

Linux查看网络连接的命令

使用netstat查看存在的网络连接 使用ping判断主机间联通情况

tcp可靠性传输怎么实现

序列号、确认应答、超时重传 窗口控制与高速重发控制/快速重传(重复确认应答) 拥塞控制 流量控制

虚函数的实现原理,继承的时候怎么实现的

在有虚函数的类中,类的最开始部分是一个虚函数表的指针,这个指针指向一个虚函数表,表中放了虚函数的地址,实际的虚函数在代码段(.text)中。当子类继承了父类的时候也会继承其虚函数表,当子类重写父类中虚函数时候,会将其继承到的虚函数表中的地址替换为重新写的函数地址。使用了虚函数,会增加访问内存开销,降低效率。

局部变量分配在哪

分配在栈区

进程的栈有多大

32位Windows,一个进程栈的默认大小是1M,在vs的编译属性可以修改程序运行时进程的栈大小 inux下进程栈的默认大小是10M,可以通过 ulimit -s查看并修改默认栈大小 默认一个线程要预留1M左右的栈大小,所以进程中有N个线程时,Windows下大概有N M的栈大小 堆的大小理论上大概等于进程虚拟空间大小-内核虚拟内存大小。windows下,进程的高位2G留给内核,低位2G留给用户,所以进程堆的大小小于2G。Linux下,进程的高位1G留给内核,低位3G留给用户,所以进程堆大小小于3G

进程的最大线程数

32位windows下,一个进程空间4G,内核占2G,留给用户只有2G,一个线程默认栈是1M,所以一个进程最大开2048个线程。当然内存不会完全拿来做线程的栈,所以最大线程数实际值要小于2048,大概2000个 32位Linux下,一个进程空间4G,内核占1G,用户留3G,一个线程默认8M,所以最多380个左右线程(ps:ulimit -a 查看电脑的最大进程数,大概7000多个)

怎么快速把进程的栈用完

对函数进行递归调用 在函数中定义大对象

C++内存对齐

为什么要内存对齐:

  1. 平台原因(移植原因):不是所有的硬件平台都能访问任意地址上的任意数据的;某些硬件平台只能在某些地址处取某些特定类型的数据,否则抛出硬件异常
  2. 性能原因:数据结构(尤其是栈)应该尽可能地在自然边界上对齐。原因在于,为了访问未对齐的内存,处理器需要作两次内存访问;而对齐的内存访问仅需要一次访问

如何进行内存对齐

  • 分配内存的顺序是按照声明的顺序
  • 每个变量相对于起始位置的偏移量必须是该变量类型大小的整数倍,不是整数倍空出内存,直到偏移量是整数倍为止
  • 最后整个结构体的大小必须是里面变量类型最大值的整数倍
class A 
	int a, b;
	char c;
;

class B 
	int a, b;
	double c;
;

class C 
	char a;
	int b;
	double c;
;

class D 
	int a;
	double b;
	int c;
;

class E 
	int a;
	double b;
	int c;
	char d;
;
int main() 
	cout << sizeof(A) << " " << sizeof(B) << " " 
        << sizeof(C) << " " << sizeof(D) << " " << sizeof(E) << endl;
	return 0;

// output
// 12 16 16 24 24

引用和指针的区别?对const型常量可以取引用吗

首先引用可以视作对象的别名,指针拥有自己的地址空间,其中保存着所指对象的地址 区别:

  1. 指针有自己的一块空间,而引用只是一个别名
  2. 使用 sizeof 看一个指针的大小是 4,而引用则是被引用对象的大小
  3. 指针可以被初始化为 NULL,而引用必须被初始化且必须是一个已有对象的引用
  4. 作为参数传递时,指针需要被解引用才可以对对象进行操作,而直接对引用的修改都会改变引用所指向的对象
  5. 指针在使用中可以指向其它对象,但是引用只能是一个对象的引用,不能被改变
  6. 指针可以有多级指针(**p),而引用只有一级
  7. 指针和引用使用++运算符的意义不一样
  8. 如果返回动态内存分配的对象或者内存,必须使用指针,引用可能引起内存泄露

http请求格式

HTTP 请求报文由请求行、请求头部、空行 和 请求包体 4 个部分组成

 get post的区别 put delete 知道吗 put和post

http 1.X 2.0区别 ( 帧 流 推送 头部压缩 安全性等等

联合索引:b+树是什么状态

HTTP头部字段

详细

http头部可以包含二进制吗

http2支持二进制,http1.x不支持

  • http 2 和 http 1 的区别
  1. HTTP2使用的是二进制传送,HTTP1.X是文本(字符串)传送。 二进制传送的单位是帧和流。帧组成了流,同时流还有流ID标示
  2. HTTP2支持多路复用 因为有流ID,所以通过同一个http请求实现多个http请求传输变成了可能,可以通过流ID来标示究竟是哪个流从而定位到是哪个http请求
  3. HTTP2头部压缩 HTTP2通过gzip和compress压缩头部然后再发送,同时客户端和服务器端同时维护一张头信息表,所有字段都记录在这张表中,这样后面每次传输只需要传输表里面的索引Id就行,通过索引ID查询表头的值
  4. HTTP2支持服务器推送 HTTP2支持在未经客户端许可的情况下,主动向客户端推送内容

MYSQL的事务

ACID特性,原子性、一致性、隔离性、持久性

多级缓存的由来和使用

计算机结构中CPU和内存之间一般都配有一级缓存、二级缓存来增加交换速度,这样当CPU调用大量数据时,就可避开内存直接从CPU缓存中调用,加快读取速度。 根据CPU缓存得出多级缓存的特点:

  • 每一级缓存中储存的是下一级缓存的一部分
  • 读取速度按级别依次递减,成本也依次递减,容量依次递增
  • 当前级别未命中时,才会去下一级寻找

项目文件传输时怎么限速

在客户端进行文件传输时,每当上传限制大小数据,就sleep一下

用户态和内核态,为啥这样做,好处是什么

用户态和内核态是操作系统的两种运行级别,两者最大的区别就是特权级不同。用户态拥有最低的特权级,内核态拥有较高的特权级。运行在用户态的程序不能直接访问操作系统内核数据结构和程序。内核态和用户态之间的转换方式主要包括:系统调用,异常和中断进程

堆和栈的区别

堆是由低地址向高地址扩展;栈是由高地址向低地址扩展 堆中的内存需要手动申请和手动释放;栈中内存是由 OS 自动申请和自动释放,存放着参数、局部变量等内存 堆中频繁调用 malloc 和 free,会产生内存碎片,降低程序效率;而栈由于其先进后出的特性,不会产生内存碎片 堆的分配效率较低,而栈的分配效率较高 栈是操作系统提供的数据结构,计算机底层对栈提供了一系列支持:分配专门的寄存器存储栈的地址,压栈和入栈有专门的指令执行;而堆是由 C/C++函数库提供的,机制复杂,需要一些列分配内存、合并内存和释放内存的算法,因此效率较低

五种IO模型

  1. 阻塞IO:调用者调用了某个函数,等待这个函数返回,期间什么也不做,不停的去检查这个函数有没有返回,必须等这个函数返回才能进行下一步动作
  2. 非阻塞IO:非阻塞等待,每隔一段时间就去检测 IO 事件是否就绪。没有就绪就可以做其他事
  3. 异步IO:
  4. 信号驱动IO:linux 用套接口进行信号驱动 IO,安装一个信号处理函数,进程继续运行并不阻塞,当 IO 时间就绪,进程收到 SIGIO 信号。然后处理 IO 事件
  5. 多路复用IO:linux 用 select/poll 函数实现 IO 复用模型,这两个函数也会使进程阻塞,但是和阻塞 IO 所不同的是这两个函数可以同时阻塞多个 IO 操作。而且可以同时对多个读操作、写操作的 IO 函数进行检测。知道有数据可读或可写时,才真正调用 IO 操作函数

bio nio aio 区别

详细 BIO (Blocking I/O):同步阻塞I/O模式,数据的读取写入必须阻塞在一个线程内等待其完成。这里使用那个经

以上是关于字节跳动面试题汇总 -- C++后端(含答案)的主要内容,如果未能解决你的问题,请参考以下文章

字节跳动Java实习面试凉凉经,含答案解析

2020 字节跳动 数据库面试题汇总

字节跳动Java研发面试99题(含答案):JVM+Spring+MySQL+线程池+锁

2019 字节跳动java面试笔试题 (含面试题解析)

最新腾讯面试题汇总--C++后端开发岗(部分含答案)

字节跳动Andorid岗25k+的面试题,含BATJM大厂