C++八股

静态变量和全局变量、局部变量的区别、在内存上是怎么分布的 首先需要说明的是变量属性的三个维度: 作用域:可以分为块作用域和命名空间作用域。块作用域就是{}中的内容;命名空间作用域则是在整个命名空间范围内有效,全局变量在全局命名空间内可见; 生命周期:变量可以分为自动变量、静态变量、动态变量。自动变量在进入块作用域时创建,离开时销毁;静态变量在程序启动或首次执行到声明语句时初始化,随程序结束而销毁;动态变量由new/delete或malloc/free管理,存在于堆上; 链接性(linkage):分为外部链接、内部链接和无链接。外部链接的变量可以在其他翻译单元中通过extern声明来访问;内部链接只能在当前翻译单元(编译单元/.cpp文件)内使用;无链接则是像局部变量这样,仅在块作用域内可见; 全局变量 在全局命名空间内可见,默认具有外部链接; 初始化发生在main()之前,分两阶段:先进行零初始化,再进行动态初始化(如有构造函数或非常量初始化表达式); 加上static后唯一改变的是链接性,从外部链接变为内部链接,仅当前翻译单元可见; 局部变量 仅在块作用域内可见,无链接; 作为自动变量,随所在块的进入而在栈上创建,块结束时销毁; 加上static成为静态局部变量:作用域和链接性不变,但生命周期改变——首次执行到声明语句时初始化(C++11起保证线程安全),之后不再随块结束而销毁,直到程序结束才销毁; 内存布局 内存大致分为代码段(.text)、数据段、堆、栈: .data段:存放已初始化的全局变量和静态变量; .bss段:存放未初始化(或初始化为零)的全局变量和静态变量,在可执行文件中不占实际空间,程序加载时由操作系统清零; 栈:局部变量(自动变量)随函数调用在栈上分配,块作用域结束时销毁; 堆:动态分配的内存(new/malloc); 指针和引用的区别 简单来说,引用是一个已有变量的别名;指针是一个独立的对象,存放了一个地址。 初始化:引用声明时必须绑定到一个已存在的对象,不能为空;指针可以声明为空指针(nullptr),也可以先声明后赋值; 操作语义:引用是别名,所有对引用的操作都穿透到其绑定的对象上;指针本身是一个独立对象,可以进行++、--等指针算术运算,改变的是指针自身指向的地址; 换绑:引用一旦绑定就不能更换目标,对引用赋值实际上是对绑定对象赋值;指针可以随时改为指向另一个地址; sizeof:sizeof(引用)得到的是绑定对象的大小;sizeof(指针)得到的是指针本身的大小(32位系统4字节,64位系统8字节); 多级间接:没有"引用的引用"(int& &不合法),但可以有多级指针(int**); 底层实现:编译器通常将引用实现为一个不可变的指针(即 T* const),但这是实现细节,语言标准不要求引用占据存储空间; C++内存分区 C++标准只规定了四种存储期(storage duration):自动、静态、动态、线程局部; 而实践中操作系统会将一个C++程序的虚拟地址空间大致分为以下几个区域: 代码段(.text):存放编译后的机器指令,通常为只读; 数据段: .rodata:存放只读常量(如字符串字面量、const全局常量等); .data:存放已初始化(非零)的全局变量和静态变量; .bss:存放零初始化和未初始化的全局变量和静态变量,可执行文件中不占实际空间,加载时由OS清零; 堆(heap):存放由new/delete、malloc/free管理的动态分配内存,向高地址增长; 栈(stack):存放函数调用帧(返回地址、参数)和局部变量(自动变量),向低地址增长; static关键字和const关键字的作用 static static主要用于改变变量、函数的存储期和链接性: 修饰局部变量:存储期从自动变为静态,首次执行到声明时初始化,直到程序结束才销毁;作用域不变,仍为块作用域; 修饰全局变量:将链接性从外部链接改为内部链接,仅当前翻译单元可见; 修饰普通函数:效果与修饰全局变量类似,将函数的链接性改为内部链接,避免跨翻译单元的符号冲突; 修饰类的成员变量:该变量不属于任何实例而属于整个类,所有实例共享;必须在类外定义(C++17起可用inline在类内初始化); 修饰类的成员函数:没有this指针,因此不能访问非静态成员变量和非静态成员函数,只能访问静态成员; const const主要用于表达"不可修改"的语义: 修饰普通变量:变量不可修改;修饰全局变量时,在C++中默认链接性变为内部链接(与C不同); 修饰指针: const int* p(pointer to const):指向常量,不能通过该指针修改所指对象; int* const p(const pointer):指针本身为常量,不能改变指向; const int* const p:两者皆不可修改; 修饰函数参数:表明函数承诺不修改该参数,常用于引用参数(const T&)避免拷贝的同时保证不修改; 修饰函数返回值:返回值不可被修改,常用于返回引用或指针时防止调用方意外修改; 修饰类的成员函数(放在参数列表后面):承诺该函数不会修改对象的任何非mutable成员,this的类型变为const T*;const对象只能调用const成员函数; 常量指针和指针常量之间有什么区别 const pointer(指针常量):int* const p——指针本身是常量,指向的地址不可改变,但可以通过该指针修改所指对象的值; pointer to const(常量指针):const int* p(等价于int const* p)——指针指向的对象被视为常量,不能通过该指针修改所指对象,但指针本身可以改为指向其他地址; const pointer to const:const int* const p——指针本身不可变,也不能通过该指针修改所指对象; 记忆技巧:const修饰的是它左边的东西(如果左边没有则修饰右边)。const int*中const修饰int,所以指向的值不可变;int* const中const修饰*(即指针本身),所以指针不可变。 ...

March 21, 2026 · KahanaT800

操作系统八股

进程和线程之间有什么区别 进程(Process) 是操作系统进行资源分配的基本单位。每个进程有独立的地址空间、文件描述符、堆栈;可以把它理解为一个正在运行的程序实例; 线程(Thread) 是CPU调度和执行的基本单位。线程存在于进程的内部,同一个进程的多个线程共享该进程的地址空间和资源;但每个线程有自己独立的程序计数器、寄存器集合和栈; 核心区别: 资源开销不同:创建和销毁一个进程需要大量的资源;进程需要分配和回收独立的地址空间、文件句柄等系统资源;而线程只需要少量的私有状态; 隔离性和通信方式不同:进程之间的地址空间相互隔离,一个进程崩溃往往不会影响其他进程,但也意味着进程间通信(IPC)成本较高,需要借助管道、消息队列、共享内存、信号量、套接字等机制;而同一进程的不同线程共享地址空间,可以直接读写同一内存,读写效率很高,但相应的需要通过锁、信号量等同步机制防止数据竞态; 上下文切换代价不同:切换进程时需要切换整个地址空间、刷新页表等,代价很大;线程切换因为共享地址空间,只需要保存和恢复少量的寄存器和栈指针; 健壮性:因为进程之间隔离,所以多进程更加健壮;而线程因为共享内存,一个线程的非法内存访问可能会导致整个进程的崩溃; 并行和并发有什么区别 并行指的是多个任务在物理上同一时刻同时执行;这依赖于多核处理器或者多台机器等硬件条件,让不同的任务分配到不同的计算单元上同时执行; 并发指的是多个任务在逻辑上同时推进;它并不要求任务在物理时间上同时执行,而是通过快速的上下文切换,让多个任务交替使用CPU,宏观上看起来像是同时执行。 解释一下用户态和核心态,什么场景下会发生切换? 用户态和内核态是为了保护系统资源和实现权限控制而设计的两种不同的CPU运行级别; 用户态:程序只能访问受限的资源和指令集,不能直接访问操作系统的核心部分,也不能直接访问硬件资源; 内核态:操作系统的特权级别,允许程序执行特权指令和访问操作系统核心部分;程序可以直接操控硬件设备,可以执行任何CPU指令; 切换场景: 系统调用:唯一一种主动触发方式,用户态的程序需要请求操作系统提供的服务时,发起系统调用,CPU会切换至内核态,执行对应的内核函数,完成后切回用户态; 异常:当CPU在执行用户态指令时遇到错误或特殊情况,会被动地从用户态切到内核态;内核会根据异常类型决定是修复问题还是终止进程; 外部中断:当外部硬件设备(比如网卡收到了数据包)会通过中断控制器打断正在执行用户态程序的CPU,切换到内核态,跳转到对应的中断服务程序进行处理,这也是被动的;其中定时器就是非常重要的外部中断,操作系统正是靠它来实现任务调度:定时器周期性地触发中断,打断当前进程,让调度器获得执行机会来决定下一个运行哪个进程; 进程调度算法你了解多少 先到先服务(FCFS):按照任务进入就绪队列的顺序运行,非抢占式,前面的任务会阻塞后面的任务直至完成;如果任务时间过长会导致后面的任务迟迟无法执行; 最短时间优先:分为非抢占式的最短作业优先(SJF)和抢占式的最短剩余时间优先(SRTF);理论上平均等待时间最短,但现实中无法准确衡量任务的真实执行时间;如果短任务大量发生会饿死长任务; 时间片轮转(RR):给每个任务分配一个固定大小的时间片;时间片消耗完则执行下一任务; 优先级调度(PR):给任务安排不同的优先级,优先级高的任务先执行;如果优先级高的任务过多会饿死优先级低的任务;会引入衰老机制提升低优先级任务的优先级; 多级队列:给不同任务队列划分优先级,不同优先级内可执行不同的调度算法; 多级反馈队列:进一步加入优先级反馈调节机制,尽可能保证任务都能执行; 现代Linux曾长期使用的CFS采用加权公平调度的思想,使用红黑树按任务的虚拟运行时间进行排序,选取虚拟运行时间最小的进程进行执行;通过nice值影响虚拟运行时间的增长,保证每个任务都能被执行到;从Linux 6.6开始,CFS被EEVDF(Earliest Eligible Virtual Deadline First)替代,在保留公平性的基础上引入了虚拟截止时间,对延迟敏感型任务的响应更好; 进程间有哪些通信方式 管道:本质上是内核维护的一个缓冲区,只能接收无类型的字节流数据,没有边界;是一种半双工通信,即只能单向发送信息;管道分为匿名管道和命名管道,匿名管道只能用于父子进程间的通信;而命名管道有文件路径,可以用于没有亲缘关系的进程间通信; 消息队列:是内核维护的一个消息链表,可以传输包含类型的数据;消息队列的生命周期独立于进程,进程退出后消息还在;每条消息有大小限制,不适合传输大数据; 共享内存:两个或多个进程将自己虚拟内存中的一块映射到同一块物理内存上,不需要经过内核的拷贝,是最快的通信方式;但因为多个进程都可以读写,会有数据竞态问题,常配合信号量使用; 信号量:本质是内核维护的一个计数器,有两种操作,P操作是减,V操作是加;如果值是1的话则类似于互斥量; 信号:一种异步通知机制,内核或进程向目标进程发送一个信号编号,目标进程可以选择默认处理、忽略或自定义信号处理函数; 套接字:不仅可以用于本地进程间通信,基于TCP的网络套接字是不同机器间通信的最广泛方式; 解释一下进程同步和互斥,以及如何实现 互斥:同一时间只能由一个进程访问某个共享资源,其他进程不能访问和使用; 同步:多个并发的进程之间协调和管理它们的执行顺序,确保按照一定的顺序执行; 实现机制从底层到高层大致有: 硬件层面的原子操作:比如比较并交换(CAS)、测试并设置(TAS),自旋锁就是基于这些原子操作实现的; 互斥锁:获取不到锁的线程会睡眠让出CPU; 信号量:可以同时实现互斥和同步。用作互斥时初值设为1,P操作借出资源后只有V操作归还资源才能让下一个进程使用;用作同步时可以通过设置初值控制并发数量或执行顺序; 条件变量:通常配合互斥锁使用,让某个线程在某个条件不满足时睡眠,条件满足时唤醒,以此控制线程的执行顺序达成同步; 什么是死锁,如何避免死锁? 死锁就是两个或多个持有资源的进程在执行过程中,互相等待资源而陷入僵局的一种情况;比如A线程持有资源1需要资源2,而B线程持有资源2需要资源1,互相等待对方释放而互相卡死; 死锁的四个必要条件(缺一不可): 互斥条件:每个资源同一时刻只能被一个进程拥有; 持有且等待:进程已经持有了一个资源,同时又在请求其他被占用资源,并且在等待期间不会主动释放; 不可剥夺条件:进程占有的资源无法被强行夺走; 循环等待条件:存在一条进程-资源的循环等待链,就像A等B,B等C,C等A; 预防(提前打破条件): 打破持有且等待:要求进程在请求资源时直接占有全部所需资源; 打破不可剥夺:要求进程在请求别的资源时主动放弃已占有资源; 打破循环等待:给资源加上编号,严格按照编号递增顺序进行资源的申请; 避免(动态判断是否安全): 银行家算法:在每次分配资源前,先模拟分配后的状态,检查系统是否仍处于安全状态(即存在一个安全序列使所有进程都能顺利完成);如果不安全则拒绝本次分配,从而避免进入死锁; 介绍一下几种典型的锁 自旋锁:基于原子操作CAS和TAS实现的锁,线程如果申请锁失败则会在CPU上忙等;适合临界区很短、持锁时间极短的场景; 互斥锁:线程在获取锁失败后会被阻塞挂起,进入睡眠状态,直至锁被释放后唤醒;适合临界区较长的场景; 读写锁:自旋锁和互斥锁都是排他的,每个时刻只能由一个线程进入临界区;而读写锁把锁拆分为读锁和写锁——读锁与读锁兼容,多线程可以同时读;读锁与写锁互斥;写锁与写锁互斥,同一时刻只能有一个线程写; 乐观锁与悲观锁:悲观锁认为所有的操作都有竞态风险,所以在申请资源之前先加锁;乐观锁则认为所有的操作都是合法的,如果遇到不合法的情况再重试;可以通过给资源加版本号来验证是否合法; 讲一讲你理解的虚拟内存 是什么: 虚拟内存是操作系统为进程提供的一种抽象:让每个进程都认为自己独占了一块连续的、完整的、从零开始的地址空间,这块地址空间就是虚拟地址空间; ...

March 12, 2026 · KahanaT800

数据库八股

一条SQL查询语句是如何执行的? 连接器:首先需要和SQL客户端建立TCP连接,连接器负责身份认证和权限读取; 查询缓存:MySQL服务器拿到查询请求后,会会先看缓存,看之前是不是很执行过该语句;但是MySQL8.0已经删除了这一步,因为缓存命中率极差(只要表有了一点改动就要删除相关所有缓存); 分析器:分析器分为词法分析和语法分析,简单来说词法分析就是看用了哪些词,比如什么SELECT,WHERE;而语法分析则是构建一颗抽象语法树,检查语句是否合法; 优化器: 优化器的职能是指定查询的具体执行计划:比如如果有多个索引,用哪一个;多表JOIN的时候决定JION的顺序等; 执行器: 执行器阶段就是执行语句,通过存储引擎的接口进行查询操作; 事务的四大特性有哪些? 四大特性就是ACID: A:原子性;一个事务中的语句的执行要么全部成功,要么全部失败。不会出现成功一半的情况; C:一致性;事务执行之后,数据库必须从一个一致性状态转移到另一个一致性状态; I:隔离性;事务与事务之间互相隔离,一个事务的中间状态对其他事务不可见;为的就是解决脏读,不可重复读,幻读这些数据竞态问题; D:持久性:事务一旦提交,对数据库的修改是永久性的,掉电也不影响结果; 数据库的事务隔离级别有哪些? 数据库的事务隔离是为了解决以下三个问题: 脏读:事务A读取了事务B没有提交的数据,之后事务B进行了回滚,事务A读到了不存在的数据; 不可重复读:事务A读取某数据后,事务B修改了该数据,事务A再次读取发现两次结果不一样;强调的是已有数据的修改; 幻读: 事务A执行两次相同的范围查询,事务B提交了插入或删除,导致事务A的行数不一致,出现“幻影行”;强调的是行数增减; 事务隔离等级有四个: 读未提交:事务可以读取其他事务未提交的修改,没有任何隔离; 读已提交:事务只能读取其他事务已经提交的数据,解决了脏读;但不可重复读和幻读问题仍然存在; 可重复读:在同一个事务内,多次读取同一行数据的结果始终一致;即在事务开始和结束之间,其他事务对数据的修改不可见。但没有解决幻读; 串行化:拒绝并发就不会有数据竞态了,只在非常严格的时候出现; Inno DB虽然是可重复读的隔离等级,但是通过机制的特别设计解决了幻读: 快照读(普通 SELECT):只在Select第一次执行的时候创建一个ReadView快照,之后的所有查询都在快照中进行,直接解决了不可重复读的问题; 当前读(SELECT … FOR UPDATE / INSERT / UPDATE / DELETE):通过 Next-Key Lock(临键锁) 解决。Next-Key Lock = Record Lock(行锁)+ Gap Lock(间隙锁),它会锁住查询条件命中的记录以及记录之间的间隙,阻止其他事务在这个范围内插入新行。 如果事务中先用快照读读了一次(建立了 Read View),然后另一个事务插入并提交了新行,接着当前事务用当前读(比如 SELECT … FOR UPDATE)再读一次,就可能看到新插入的行 MySQL的执行引擎有哪些? 常见的执行引擎有三个: InnoDB:实际生产的唯一选择,支持事务,支持ACID特性,支持行级锁,支持MVCC;对并发访问和数据安全都有很好的实现; MyISAM:旧引擎,不支持事务、不支持行级锁(只有表级锁)、不支持外键、不支持崩溃安全恢复,并发性能很差; Memory:使用内存,数据在掉电之后就会消失,不常用; MySQL为什么使用B+树来作索引 数据存储在磁盘中,而磁盘的IO速度非常慢,索引就是为了降低查询时的磁盘IO次数而设计的; B+树的特性非常好: 层高低,每次查询的IO在2-3次:InnoDB的一页是16KB,而且所有的数据都放在叶子节点中,非叶子节点只存key,扇出非常大:一个非叶子节点能放1170的分支,两层就能放130万以上的记录;基本IO只需要两次就能查到数据;而与之相对的AVL树因为每个节点只存一个key,100w条记录需要20层,就是20次IO;跳表也是层高太高; 支持范围查询:B+树的叶子节点之间有链表(InnoDB是双向链表)进行连接,可以非常快速地进行范围查询;而B树没有,范围查询就要多次遍历,IO次数不可控;全表查询B+树只用走底层叶子链表一次,而B树要遍历全表;哈希则是做不了范围查询; 查询性能稳定:B+树的所有非叶子节点都不存数据,每次查询都要走到叶子层,路径长度一致,性能可预测;而B树因为把数据放在非叶子节点中,在降低了每层容纳分支的同时也导致了查询可能提前退出,IO次数不可控,对于优化来说是个大问题; 说一下索引失效的场景? 联合索引的基本规则就是最左前缀原则:如果建了一个联合索引 (a, b, c),B+ 树是按照 a → b → c 的顺序排列的。查询时必须从最左列开始连续匹配,才能走索引,如果使用索引时跳过了最左边的 a 则直接无法使用索引; ...

March 12, 2026 · KahanaT800

计算机网络八股

介绍一下TCP/IP模型和OSI模型的区别 OSI和TCP/IP都是计算机网络的分层模型; OSI 先有模型后有协议,是理论上的标准模型; TCP/IP 是先有协议后有模型,从实践中总结来的事实标准; OSI把网络从上到下分为应用层、表示层、会话层、传输层、网络层、数据链路层、物理层; 而TCP/IP分为: 应用层:TCP/IP认为应用层、表示层、会话层应由应用程序本身处理,合为一个应用层;提供与应用程序交互的接口,比如HTTP、FTP等; 传输层:提供进程间、端到端的通信;主要协议有TCP和UDP,其中TCP保证可靠性而UDP不保证可靠性; 网络层:提供跨网络的通信;主要协议是IP,提供数据包的路由和转发,也就是如何从一台电脑找到另一台电脑;IP协议使用IP地址来区分主机和网络,并进行寻址; 网络接口层:对应OSI模型的数据链路层和物理层;它负责物理传输媒介的传输;该层包含硬件地址,也就是MAC地址的管理; 从输入 URL 到页面展示到底发生了什么? 输入URL,解析并准备发送HTTP请求 浏览器进行缓存查询,如果没有则进入下一步网络请求 DNS域名解析:通过DNS解析获得想要访问的域名的IP地址,按照浏览器缓存 → 操作系统缓存 → 本地HOSTS文件 → 本地DNS服务器 → 迭代查询,直到找到域名的IP地址 通过TCP三次握手建立连接 Client发送HTTP请求:建立连接后浏览器会构建请求行、请求头等信息,发起HTTP请求数据; Server解析请求,返回响应数据 Client拿到数据,通过TCP四次挥手断开连接 浏览器解析数据并渲染页面: 如果响应头状态码是301、302,则会重定向 如果数据类型是字节流,则将请求提交给下载器 如果是HTML,则开始渲染,创建DOM树,解析CSS样式,将CSS与DOM合并,构建渲染树,最后布局和绘制渲染树得到页面 HTTP请求报文和响应报文是怎样的,有哪些常见的字段? HTTP请求报文由四部分组成:请求行、请求头、空行、请求体; 其中请求行由三个部分组成: 方法:如GET、POST、PUT、DELETE 资源路径:URI,资源唯一标识,绝大多数时候就是资源路径URL HTTP版本 请求头是键值对,常见字段有: Host:目标主机名 User-Agent:浏览器标识 Accept:可接受的响应类型 Accept-Language:可接受的语言 Cookie:携带的Cookie Connection:连接管理,如keep-alive 空行:用于分割head和body 请求体通常见于POST和PUT,包含发送给Server的数据; HTTP响应报文也是四部分组成:状态行、响应头、空行、响应体 其中状态行包括: HTTP版本 状态码:比如200、301、404 状态信息:OK 响应头常见字段有: Date:响应时间 Server:服务器标识 Content-Type:响应体的媒体类型及编码 Content-Length:响应体大小 Set-Cookie:向客户端写入Cookie Location:301、302重定向时跳转URL 响应体则是响应返回的数据,常见的有HTML、视频、图片等等; HTTP有哪些请求方式? 常见CRUD: GET:获取资源,幂等安全,可被缓存; POST:提交数据,非幂等,不可缓存; PUT:整体更新,替换资源,幂等; DELETE:删除指定资源,幂等; 其他: HEAD:只返回头部不返回资源,常用于检查资源是否存在; PATCH:部分更新; OPTIONS:查询某服务器支持的方法; GET请求和POST请求的区别 语义层面:GET用于获取资源,是读操作;POST用于提交数据,是写操作; 参数位置:GET的参数直接写在URL中,POST的参数放在请求体中; 安全性:GET是明文,可以直接看到;POST相对安全,但是如果不走HTTPS,抓包一样可以看到; 幂等性:GET是读操作不改变资源状态,天然幂等;POST提交数据会改变资源状态,不幂等; 缓存:GET会被浏览器缓存;POST则默认不缓存; 长度限制:GET有长度限制,最大一般几KB;POST理论上没有; 注意上述的一些区别主要来自于浏览器而不是HTTP协议本身,比如GET也可以写请求体,POST也可以用于获取数据,并不违反协议但是不够RESTful; ...

March 12, 2026 · KahanaT800