├── README.md ├── docs ├── C++primer5笔记代码资料 │ ├── 第一章 │ │ ├── C++primer5第一章代码.md │ │ └── 第一章完整书店程序 │ │ │ ├── Sales_item.h │ │ │ └── bookstore.cpp │ └── 第二章 │ │ └── 测试声明与定义 │ │ ├── bin │ │ ├── test │ │ └── test1 │ │ ├── test.cpp │ │ ├── test.h │ │ ├── test1.cpp │ │ └── test1.h ├── Sales_item.h ├── linux常用操作命令.md └── 刷leetcode究竟要不要使用库函数.md └── problems ├── #define和const的区别有那些?.md ├── 32位系统一个进程最多有多少堆内存.md ├── BTREE和B+TREE的区别.md ├── BloomFilter原理.md ├── C++中struct和class有什么区别?.md ├── C++中动态链接库和静态连接库的区别是什么?.md ├── C++中右值引用有什么作用?.md ├── C++中四种cast的转换?.md ├── C++中常用的类优化技术有那些?.md ├── C++中类成员的访问权限有那些?.md ├── C++中结构体内存布局的规则是什么?.md ├── C++中,结构体可以直接赋值吗?.md ├── C++内存分配.md ├── C++单例模式.md ├── C++多态的实现有那几种?他们有什么不同?.md ├── C++的atomic-bool代码底层是如何实现的?.md ├── C++的智能指针及其原理.md ├── C++结构体内存对齐.md ├── Cookie和SessioN.md ├── DNS和HTTP协议,HTTP请求方式.md ├── DNS域名缓存是什么?.md ├── DNS解析的过程?.md ├── GET请求中,URL编码有什么含义?.md ├── HTTP中常用的状态码都有那些?.md ├── HTTP方法都有那些?.md ├── HTTP长连接和短链接都用在那些场景?.md ├── IO模型了解哪些?.md ├── Innodb和Myisam的区别.md ├── Linux上有个二进制程序一直在运行,我修改代码置换重新编译把原来的二进制程序覆盖了,会怎么样?.md ├── Map-Reduce原理.md ├── MySQL的主从复制是如何实现的?.md ├── MySQL的行级锁有那些种类?.md ├── NGINX在Linux上是如何工作的?简单描述一下.md ├── OSI七层模型分别是?各自的功能分别是什么?.md ├── POST和GET的主要区别有那些?.md ├── RedisHash的原理和使用场景.md ├── RedisList的原理和使用场景.md ├── RedisSet的原理和使用场景.md ├── RedisString原理和使用场景(分布式锁).md ├── RedisZSet的原理和使用场景(延迟队列).md ├── Redis不是单线程吗,为什么会存在并发安全问题.md ├── Redis中Stream的原理和使用场景.md ├── Redis中数据(键值对)是怎么存储的.md ├── Redis为什么这么快.md ├── Redis主从复制、哨兵、集群的区别.md ├── Redis内存淘汰策略.md ├── Redis单线程在多核机器里使用会不会浪费机器资源.md ├── Redis和Memcached的区别.md ├── Redis如何判断键是否过期?过期键的删除策略有哪些.md ├── Redis数据类型(对象)有哪些.md ├── Redis是单线程还是多线程?Redis6.0之后为何又引入了多线程.md ├── Redis有什么作用?为什么要用 Redis.md ├── Redis的BigKey问题及其解决方案.md ├── Redis的Bitmap的原理和使用场景.md ├── Redis的HyperLogLog的原理和使用场景.md ├── Redis的ke设定24h过期时间,那么24h后就一定会过期吗.md ├── Redis的两种持久化方式以及优缺点.md ├── Redis的底层数据结构有哪些.md ├── Redis缓存击穿问题及其解决方案.md ├── Redis缓存穿透问题及其解决方案.md ├── Redis缓存雪崩问题及其解决方案.md ├── STL中一般都有那些常见的算法库呢?.md ├── STL中的优先级队列是如何实现的?.md ├── STL中,map的底层是如何实现的?.md ├── STL中,set的底层是如何实现的?.md ├── STL原理及实现.md ├── STL容器是线程安全的吗?.md ├── SYN队列和Accept队列.md ├── SYN队列溢出了怎么办.md ├── Socket和WebSocket的区别.md ├── TCP协议是如何保证可靠传输的?.md ├── TCP和UDP三次握手和四次挥手状态及消息类型.md ├── TCP和UDP区别.md ├── TCP和UDP头部字节定义.md ├── TCP和UDP对应常见的应用层协议有那些?.md ├── TCP和UDP的区别.md ├── TCP和UDP的首部长什么样子?.md ├── TCP的最大连接数是多少?.md ├── TCP粘包是怎么产生的?.md ├── Trie树原理.md ├── UDP中使用connect的好处.md ├── WebScoket底层原理.md ├── bitmap.md ├── connect会阻塞怎么解决.md ├── epoll哪些触发模式,有啥区别?.md ├── forward和redirect的区别是什么?.md ├── git的merge和rebase有什么区别.md ├── ip地址和mac地址的区别都有那些?.md ├── keepalive是什么?如何使用?.md ├── linux文件系统:inode,inode存储了哪些东西,目录名,文件名存在哪里.md ├── linux的内存管理机制内存寻址方式什么叫虚拟内存内存调页算法任务调度算法.md ├── linux系统的各类同步机制、linux系统的各类异步机制.md ├── lower_bound()和upper_bound()有什么区别?.md ├── malloc,strcpy,strcmp的实现,常用库函数实现,哪些库函数属于高危函数.md ├── mysql为啥会产生死锁呢?如何避免他?.md ├── mysql数据库中,产生的redolog都会直接写入磁盘吗?.md ├── mysql架构是什么样的?.md ├── mysql的索引都有那些?.md ├── mysql索引失效有哪几种情况?.md ├── override和overload的区别有那些.md ├── selectpollepoll分别讲讲.md ├── select模型和poll模型epoll模型.md ├── set,mutiset,map,mutimap之间都有什么区别?.md ├── socket什么情况下可读?.md ├── socket服务端的实现,select和epoll的区别.md ├── time_wait,close_wait状态产生原因,keepalive.md ├── union和join.md ├── volatile,static,const,extern等关键字.md ├── weak_ptr是如何解决shared_ptr循环引用的?.md ├── 一条SQL语句在数据库框架中的执行过程.md ├── 一段代码从程序到执行经历怎么样的过程(程序在计算机中是如何运行起来的).md ├── 为什么AOF后台重写和BGSAVE命令都用子进程而不是线程?.md ├── 为什么CPU访问寄存器的速度比访问内存或CPUCache的速度快.md ├── 为什么DNS会使用UDP而不使用TCP呢?.md ├── 为什么Redis使用跳表而不是红黑树来实现Zset.md ├── 为什么Zset需要同时使用跳表和字典来实现?.md ├── 为什么有了进程还需要线程和协程?.md ├── 为什么用户态和内核态的相互切换过程开销比较大.md ├── 为什么需要allocator?他在STL中有什么作用?.md ├── 为什么需要三次握手,两次不行?.md ├── 为什么需要虚析构函数,什么时候不需要.md ├── 为什么需要虚继承.md ├── 二叉树和链表的区别.md ├── 五种IO模式.md ├── 什么时候会产生栈溢出,为什么一直递归就会栈溢出.md ├── 什么是C++的内存模型?.md ├── 什么是IP地址?可以简单介绍下吗?.md ├── 什么是PCB?.md ├── 什么是RAII原则,他在STL是怎么应用的?.md ├── 什么是mac地址?可以简单介绍下吗?.md ├── 什么是一致性哈希,Redis集群为什么不用一致性哈希.md ├── 什么是二叉树、二叉搜索树、平衡二叉树、完全二叉树、满二叉树.md ├── 什么是僵尸进程?.md ├── 什么是哈夫曼树?构造过程?应用场景.md ├── 什么是堆?如何维护堆.md ├── 什么是大小端,他在计算机网络中都有什么应用呢.md ├── 什么是完美转发?.md ├── 什么是左值?什么是右值?有什么不同?.md ├── 什么是拆包和粘包?.md ├── 什么是数字签名?.md ├── 什么是数字证书?.md ├── 什么是泛型编程,他在STL中是怎么使用的?.md ├── 什么是滑动窗口,超时重传.md ├── 什么是红黑树?红黑树与平衡二叉树、B和B+树的区别.md ├── 什么是缓冲区溢出?.md ├── 什么是缺页中断.md ├── 什么是跳表?跳表和平衡二叉树的区别.md ├── 什么是进程?什么是线程?他们的区别是什么?.md ├── 什么是页表、什么是快表.md ├── 介绍 Redis的skiplist.md ├── 介绍 intset及其升级过程,支持降级吗.md ├── 介绍Redis事务(Redis能实现ACID吗).md ├── 介绍SDSRedis为什么要使用SDS而不是C字符串.md ├── 介绍dict,什么是rehash?什么是渐进式rehash.md ├── 介绍intset及其升级过程,支持降级吗.md ├── 介绍ziplist,什么是连锁更新?quicklist、lispack.md ├── 介绍下Socket编程.md ├── 介绍下分层存储体系和CPU三级缓存.md ├── 介绍下进程的地址空间(虚拟地址和物理地址).md ├── 从本地读取一个文件通过网络发送到另一端,中间涉及几次拷贝.md ├── 优化索引的办法有那些.md ├── 关系型数据库和非关系数据库的特点.md ├── 内存映射文件是什么?如何用它来处理大文件?.md ├── 内存有限,如何在100亿数据中找到中位数.md ├── 内存有限,如何在20亿个整数中找到出现次数最多的数.md ├── 内存有限,如何在2亿个整数中找出不连续的最小数.md ├── 内存有限,如何在40亿个非负整数中找到所有未出现的数.md ├── 内存有限,怎么对100亿数据进行排序(大数据小内存排序问题).md ├── 内存池是什么?在C++中如何设计一个简单的内存池?.md ├── 内联函数、构造函数、静态成员函数可以是虚函数吗.md ├── 写string类的构造,析构,拷贝函数.md ├── 函数参数的入栈顺序是什么,从左到右还是从右到左.md ├── 分段和分页的区别有那些?.md ├── 分页和分段的区别是什么?.md ├── 列举你所知道的tcp选项.md ├── 动态链接和静态链接的区别.md ├── 协程是什么,为什么需要协程.md ├── 单线程怎么保证高并发?.md ├── 原子变量的内存序是什么?.md ├── 同一个类的两个对象的虚函数表是如何维护的?.md ├── 同步,异步,阻塞和非阻塞的概念.md ├── 固态硬盘和机械硬盘区别.md ├── 在32位和64位系统中,指针分别为多大?.md ├── 在C++中为什么需要深拷贝,浅拷贝会存在哪些问题?.md ├── 在C++中,三个全局变量相互依赖,程序应该如何初始化呢?300个呢?.md ├── 在C++中,对一个对象先malloc后delete这样使用可以吗?有什么风险.md ├── 在C++中,用堆和用栈谁更快一点?.md ├── 在C++的map中,[]与insert有那些区别?.md ├── 在C++的算法库中,find()和binary_search()有什么区别?.md ├── 在Mysql中,数据要写入磁盘,redolog也要写入磁盘,为什么要多此一举?.md ├── 在TCP三次握手的时候,如果网络情况非常好且百分百不会发生拥塞,不会重传,不会有历史链接问题,那么三次握手可以改为两次吗?.md ├── 在TCP拥塞控制中,使用了什么样的算法?.md ├── 外中断和内中断有什么区别?.md ├── 如何使用gdb来定位C++程序中的死锁?.md ├── 如何判断图中是否有环(拓扑排序).md ├── 如何判断某网页的URL是否存在于包含100亿条数据的黑名单上(大数据小内存去重问题).md ├── 如何定义一个只能在堆上定义对象的类栈上呢.md ├── 如何实现守护进程.md ├── 如何构造一个类使得只能在堆上或者栈上分配内存?.md ├── 如何解决Redis集群数据丢失问题(异步复制、集群脑裂).md ├── 如何解决缓存和数据库一致性问题.md ├── 如何选择合适的STL容器.md ├── 如何避免悬挂指针?.md ├── 如果A是某一个类的指针,那么在它等于nullptr的情况下能直接调用里面的A对应类里面的public函数吗.md ├── 如果A这个对象对应的类是一个空类,那么sizeof(A)的值是多少?.md ├── 如果A这个指针指向一个数组,那么sizeof(A)的值是多少?.md ├── 如果发现自己的Linux服务器负载过高,应该怎么排查原因呢?.md ├── 如果拿到虚函数表的储存地址,是否可以改写虚函数表的内容?.md ├── 宏定义和展开、内联函数区别.md ├── 对称加密和非对称加密的区别都有那些?.md ├── 局域网的IP分配策略是什么?它是怎么实现的?.md ├── 常见排序算法及其时间复杂度、各种排序算法对比.md ├── 常见的信号、系统如何将一个信号通知到进程.md ├── 并行和并发的区别.md ├── 循环队列怎么实现.md ├── 悬挂指针和野指针有什么区别?.md ├── 指针和引用在内存中的表现形式有何不同?.md ├── 指针和引用有什么区别呢?.md ├── 指针常量和常量指针的区别.md ├── 操作系统本身为用户提供什么功能.md ├── 数据库中乐观锁与悲观锁的区别.md ├── 数据库中常见锁都有哪些.md ├── 数据库事务隔离级别有那些?.md ├── 数据库日志类型作用.md ├── 数据库的ACID特性.md ├── 数据库范式第一第二第三范式.md ├── 数组和链表的区别、适用场景.md ├── 时间复杂度和空间复杂度的定义?时间换空间&空间换时间的例子有哪些.md ├── 构造函数中可以调用虚函数吗.md ├── 标准库函数和系统调用的区别.md ├── 栈和队列的区别、适用场景.md ├── 检查手机号是否存在于百万数据电话号中.md ├── 死锁必要条件及避免算法.md ├── 现在普通关系数据库用得数据结构是什么类型的数据结构.md ├── 用C++设计一个不能被继承的类.md ├── 用户态和内核态的区别.md ├── 程序编译的过程.md ├── 简单说说STL中的优先级队列是如何实现的?.md ├── 类构造和析构的顺序.md ├── 索引的优点和缺点.md ├── 纯虚函数,为什么需要纯虚函数.md ├── 线程间的通信方式有那些?.md ├── 线程,进程和协程是否有自己独立的堆区和栈区?.md ├── 虚函数是否可以声明为static?.md ├── 虚函数是针对类还是针对对象的?.md ├── 虚函数的作用和实现原理,什么是虚函数,有什么作用.md ├── 讲一讲C++中的原子操作有那些?.md ├── 讲一讲Redis主从复制.md ├── 讲一讲Redis的哨兵.md ├── 讲一讲Redis的集群.md ├── 讲讲函数调用的过程.md ├── 说说你对TCP流量控制的理解?.md ├── 说说你对TCP滑动窗口的理解?.md ├── 请你介绍一下http1.0.md ├── 请你介绍一下http1.1.md ├── 请你介绍一下http2.0.md ├── 请你介绍一下http3.0.md ├── 谈谈数据库中索引的理解,索引和主键区别.md ├── 软中断和硬中断分别指的是什么?.md ├── 进程的状态转换有那些?.md ├── 进程终止的方式有那些?.md ├── 进程间的通信方式有那些?.md ├── 迭代器与普通指针有什么区别.md ├── 针对于ipv4地址不够用的情况,我们是如何解决的?.md ├── 锁:互斥锁,乐观锁,悲观锁.md ├── 长连接和短连接.md ├── 静态分配内存和动态分配内存有什么区别?.md └── 面向对象的三大特征是什么.md /docs/C++primer5笔记代码资料/第一章/C++primer5第一章代码.md: -------------------------------------------------------------------------------- 1 | 2 | # 1.2 初识C++的输入输出 3 | 4 | ``` 5 | #include 6 | int main() 7 | { 8 | std::cout << "Enter two numbers:" << std::endl; 9 | int v1 = 0, v2 = 0; 10 | std::cin >> v1 >> v2; 11 | std::cout << "The sum of " << v1 << " and " << v2 << " is " << v1 + v2 << std::endl; 12 | return 0; 13 | } 14 | ``` 15 | 16 | ## 测试缓冲区代码 17 | 18 | ``` 19 | void test_stream1() { 20 | cout << "代码随想录"; 21 | } 22 | 23 | void test_stream2() { 24 | cout << "代码随想录"; 25 | while (1) { 26 | } 27 | } 28 | 29 | void test_stream3() { 30 | cout << "代码随想录"<< endl; 31 | while (1) { 32 | } 33 | } 34 | 35 | ``` 36 | 37 | # 1.4.3 38 | 39 | ``` 40 | #include 41 | int main() 42 | { 43 | int sum = 0, value = 0; 44 | // read until end-of-file, calculating a running total of all values read 45 | while (std::cin >> value) 46 | sum += value; // equivalent to sum = sum + value 47 | std::cout << "Sum is: " << sum << std::endl; 48 | return 0; 49 | } 50 | ``` 51 | 52 | # 1.5 类 53 | 54 | ``` 55 | #include 56 | #include "Sales_item.h" 57 | int main() 58 | { 59 | Sales_item book; 60 | // read ISBN, number of copies sold, and sales price 61 | std::cin >> book; 62 | // write ISBN, number of copies sold, total revenue, and average price 63 | std::cout << book << std::endl; 64 | return 0; 65 | } 66 | 67 | ``` 68 | 69 | # 1.6 70 | 71 | ``` 72 | #include 73 | #include "Sales_item.h" 74 | int main() 75 | { 76 | Sales_item total; // variable to hold data for the next transaction 77 | // read the first transaction and ensure that there are data to process 78 | if (std::cin >> total) { 79 | Sales_item trans; // variable to hold the running sum 80 | // read and process the remaining transactions 81 | while (std::cin >> trans) { 82 | // if we're still processing the same book 83 | if (total.isbn() == trans.isbn()) 84 | total += trans; // update the running total 85 | else { 86 | // print results for the previous book 87 | std::cout << total << std::endl; 88 | total = trans; // total now refers to the next book 89 | } 90 | } 91 | std::cout << total << std::endl; // print the last transaction 92 | } else { 93 | // no input! warn the user 94 | std::cerr << "No data?!" << std::endl; 95 | return -1; // indicate failure 96 | } 97 | return 0; 98 | } 99 | ``` 100 | 101 | 102 | -------------------------------------------------------------------------------- /docs/C++primer5笔记代码资料/第一章/第一章完整书店程序/bookstore.cpp: -------------------------------------------------------------------------------- 1 | /* ************************************************************************ 2 | > File Name: test.cpp 3 | > Author: SunXiuyang 4 | > 微信公众号: 代码随想录 5 | > Created Time: Tue Sep 1 18:11:26 2020 6 | > Description: 7 | ************************************************************************/ 8 | 9 | #include 10 | #include "Sales_item.h" 11 | int main() 12 | { 13 | Sales_item total; // variable to hold data for the next transaction 14 | // read the first transaction and ensure that there are data to process 15 | if (std::cin >> total) { 16 | Sales_item trans; // variable to hold the running sum 17 | // read and process the remaining transactions 18 | while (std::cin >> trans) { 19 | // if we're still processing the same book 20 | if (total.isbn() == trans.isbn()) 21 | total += trans; // update the running total 22 | else { 23 | // print results for the previous book 24 | std::cout << total << std::endl; 25 | total = trans; // total now refers to the next book 26 | } 27 | } 28 | std::cout << total << std::endl; // print the last transaction 29 | } else { 30 | // no input! warn the user 31 | std::cerr << "No data?!" << std::endl; 32 | return -1; // indicate failure 33 | } 34 | return 0; 35 | } 36 | 37 | -------------------------------------------------------------------------------- /docs/C++primer5笔记代码资料/第二章/测试声明与定义/bin/test: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/youngyangyang04/TechCPP/12d224a7948935f3089a2bfc363ec203a0433939/docs/C++primer5笔记代码资料/第二章/测试声明与定义/bin/test -------------------------------------------------------------------------------- /docs/C++primer5笔记代码资料/第二章/测试声明与定义/bin/test1: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/youngyangyang04/TechCPP/12d224a7948935f3089a2bfc363ec203a0433939/docs/C++primer5笔记代码资料/第二章/测试声明与定义/bin/test1 -------------------------------------------------------------------------------- /docs/C++primer5笔记代码资料/第二章/测试声明与定义/test.cpp: -------------------------------------------------------------------------------- 1 | /* ************************************************************************ 2 | > File Name: test.cpp 3 | > Author: 程序员Carl 4 | > 微信公众号: 代码随想录 5 | > Created Time: Tue Sep 29 16:45:38 2020 6 | > Description: 7 | ************************************************************************/ 8 | #include "test.h" 9 | int main() { 10 | std::cout << name << std::endl; 11 | } 12 | -------------------------------------------------------------------------------- /docs/C++primer5笔记代码资料/第二章/测试声明与定义/test.h: -------------------------------------------------------------------------------- 1 | /* ************************************************************************ 2 | > File Name: test.h 3 | > Author: 程序员Carl 4 | > 微信公众号: 代码随想录 5 | > Created Time: Tue Sep 29 16:41:48 2020 6 | > Description: 7 | ************************************************************************/ 8 | 9 | #include 10 | -------------------------------------------------------------------------------- /docs/C++primer5笔记代码资料/第二章/测试声明与定义/test1.cpp: -------------------------------------------------------------------------------- 1 | /* ************************************************************************ 2 | > File Name: test1.cpp 3 | > Author: 程序员Carl 4 | > 微信公众号: 代码随想录 5 | > Created Time: Tue Sep 29 16:45:38 2020 6 | > Description: 7 | ************************************************************************/ 8 | #include "test1.h" 9 | int main() { 10 | std::cout << name << std::endl; 11 | } 12 | -------------------------------------------------------------------------------- /docs/C++primer5笔记代码资料/第二章/测试声明与定义/test1.h: -------------------------------------------------------------------------------- 1 | /* ************************************************************************ 2 | > File Name: test1.h 3 | > Author: 程序员Carl 4 | > 微信公众号: 代码随想录 5 | > Created Time: Tue Sep 29 16:49:26 2020 6 | > Description: 7 | ************************************************************************/ 8 | 9 | #include "test.h" 10 | 11 | void print() { 12 | std::cout << name << std::endl; 13 | } 14 | -------------------------------------------------------------------------------- /docs/Sales_item.h: -------------------------------------------------------------------------------- 1 | /* 2 | * This file contains code from "C++ Primer, Fifth Edition", by Stanley B. 3 | * Lippman, Josee Lajoie, and Barbara E. Moo, and is covered under the 4 | * copyright and warranty notices given in that book: 5 | * 6 | * "Copyright (c) 2013 by Objectwrite, Inc., Josee Lajoie, and Barbara E. Moo." 7 | * 8 | * 9 | * "The authors and publisher have taken care in the preparation of this book, 10 | * but make no expressed or implied warranty of any kind and assume no 11 | * responsibility for errors or omissions. No liability is assumed for 12 | * incidental or consequential damages in connection with or arising out of the 13 | * use of the information or programs contained herein." 14 | * 15 | * Permission is granted for this code to be used for educational purposes in 16 | * association with the book, given proper citation if and when posted or 17 | * reproduced.Any commercial use of this code requires the explicit written 18 | * permission of the publisher, Addison-Wesley Professional, a division of 19 | * Pearson Education, Inc. Send your request for permission, stating clearly 20 | * what code you would like to use, and in what specific way, to the following 21 | * address: 22 | * 23 | * Pearson Education, Inc. 24 | * Rights and Permissions Department 25 | * One Lake Street 26 | * Upper Saddle River, NJ 07458 27 | * Fax: (201) 236-3290 28 | */ 29 | 30 | /* This file defines the Sales_item class used in chapter 1. 31 | * The code used in this file will be explained in 32 | * Chapter 7 (Classes) and Chapter 14 (Overloaded Operators) 33 | * Readers shouldn't try to understand the code in this file 34 | * until they have read those chapters. 35 | */ 36 | 37 | #ifndef SALESITEM_H 38 | // we're here only if SALESITEM_H has not yet been defined 39 | #define SALESITEM_H 40 | 41 | // Definition of Sales_item class and related functions goes here 42 | #include 43 | #include 44 | 45 | class Sales_item { 46 | // these declarations are explained section 7.2.1, p. 270 47 | // and in chapter 14, pages 557, 558, 561 48 | friend std::istream& operator>>(std::istream&, Sales_item&); 49 | friend std::ostream& operator<<(std::ostream&, const Sales_item&); 50 | friend bool operator<(const Sales_item&, const Sales_item&); 51 | friend bool 52 | operator==(const Sales_item&, const Sales_item&); 53 | public: 54 | // constructors are explained in section 7.1.4, pages 262 - 265 55 | // default constructor needed to initialize members of built-in type 56 | Sales_item() = default; 57 | Sales_item(const std::string &book): bookNo(book) { } 58 | Sales_item(std::istream &is) { is >> *this; } 59 | public: 60 | // operations on Sales_item objects 61 | // member binary operator: left-hand operand bound to implicit this pointer 62 | Sales_item& operator+=(const Sales_item&); 63 | 64 | // operations on Sales_item objects 65 | std::string isbn() const { return bookNo; } 66 | double avg_price() const; 67 | // private members as before 68 | private: 69 | std::string bookNo; // implicitly initialized to the empty string 70 | unsigned units_sold = 0; // explicitly initialized 71 | double revenue = 0.0; 72 | }; 73 | 74 | // used in chapter 10 75 | inline 76 | bool compareIsbn(const Sales_item &lhs, const Sales_item &rhs) 77 | { return lhs.isbn() == rhs.isbn(); } 78 | 79 | // nonmember binary operator: must declare a parameter for each operand 80 | Sales_item operator+(const Sales_item&, const Sales_item&); 81 | 82 | inline bool 83 | operator==(const Sales_item &lhs, const Sales_item &rhs) 84 | { 85 | // must be made a friend of Sales_item 86 | return lhs.units_sold == rhs.units_sold && 87 | lhs.revenue == rhs.revenue && 88 | lhs.isbn() == rhs.isbn(); 89 | } 90 | 91 | inline bool 92 | operator!=(const Sales_item &lhs, const Sales_item &rhs) 93 | { 94 | return !(lhs == rhs); // != defined in terms of operator== 95 | } 96 | 97 | // assumes that both objects refer to the same ISBN 98 | Sales_item& Sales_item::operator+=(const Sales_item& rhs) 99 | { 100 | units_sold += rhs.units_sold; 101 | revenue += rhs.revenue; 102 | return *this; 103 | } 104 | 105 | // assumes that both objects refer to the same ISBN 106 | Sales_item 107 | operator+(const Sales_item& lhs, const Sales_item& rhs) 108 | { 109 | Sales_item ret(lhs); // copy (|lhs|) into a local object that we'll return 110 | ret += rhs; // add in the contents of (|rhs|) 111 | return ret; // return (|ret|) by value 112 | } 113 | 114 | std::istream& 115 | operator>>(std::istream& in, Sales_item& s) 116 | { 117 | double price; 118 | in >> s.bookNo >> s.units_sold >> price; 119 | // check that the inputs succeeded 120 | if (in) 121 | s.revenue = s.units_sold * price; 122 | else 123 | s = Sales_item(); // input failed: reset object to default state 124 | return in; 125 | } 126 | 127 | std::ostream& 128 | operator<<(std::ostream& out, const Sales_item& s) 129 | { 130 | out << s.isbn() << " " << s.units_sold << " " 131 | << s.revenue << " " << s.avg_price(); 132 | return out; 133 | } 134 | 135 | double Sales_item::avg_price() const 136 | { 137 | if (units_sold) 138 | return revenue/units_sold; 139 | else 140 | return 0; 141 | } 142 | #endif 143 | -------------------------------------------------------------------------------- /docs/刷leetcode究竟要不要使用库函数.md: -------------------------------------------------------------------------------- 1 | 一些同学经常疑惑,经常看到leetcode上直接调用库函数的评论和题解,**其实我感觉娱乐一下还是可以的,但千万别当真,别沉迷!** 2 | 3 | 例如:151. 翻转字符串里的单词,这道题目本身是综合考察同学们对字符串的处理能力,如果 split + reverse的话,那就失去了题目的意义了。 4 | 5 | 有的同学甚至不屑于实现这么简单的功能,直接调库函数完事,把字符串分成一个个单词,一想就是那么一回事,多简单。 6 | 7 | 相信我, 很多面试题都是一想很简单,实现起来一堆问题。 所以刷leetcode本来就是为面试,也为了提高自己的代码能力,扎实一点没坏处。 8 | 9 | 那么如果在现场面试中,我们什么时候使用库函数,什么时候不要用库函数呢。 10 | 11 | **如果题目关键的部分直接用库函数就可以解决,建议不要使用库函数**,毕竟面试官一定不是考察你对库函数的熟悉程度。 12 | 13 | 如果使用python 和 java 的同学更需要注意这一点,因为python java 提供的库函数十分丰富。 14 | 15 | **如果库函数仅仅是 解题过程中的一小部分,并且你已经很清楚这个库函数的内部实现原理的话,可以考虑使用库函数。** 16 | 17 | 在leetcode上练习算法的时候本着这样的原则去练习,这样才有助于对算法的理解。 18 | 19 | -------------------------------------------------------------------------------- /problems/#define和const的区别有那些?.md: -------------------------------------------------------------------------------- 1 | 1. 作用域不同: 2 | - \#define定义的常量是一个预处理宏,它在编译之前被替换,作用域为定义处到文件结束。 3 | - const定义的常量是一个真正的变量,其作用域根据定义的位置而定,可以是局部或全局作用域。 4 | 2. 类型安全性: 5 | - \#define不具有类型检查,在预处理阶段只是简单地进行文本替换,容易导致一些潜在的错误。 6 | - const定义的常量具有类型检查,编译器会对其进行类型检查,提供更好的类型安全性。 7 | 3. 调试信息: 8 | - \#define在预处理阶段进行文本替换,因此在调试时无法查看使用#define定义的常量的值。 9 | - const定义的常量是真正的变量,可以被调试器识别并显示其值。 -------------------------------------------------------------------------------- /problems/32位系统一个进程最多有多少堆内存.md: -------------------------------------------------------------------------------- 1 | 我们以Linux为例子: 2 | 3 | 在32位的Linux系统中,一个进程的虚拟内存空间通常是4GB(2的32次方字节)。这个空间**通常被平分为两部分**,一半用于用户空间,一半用于内核空间。也就是说,一个进程最多可以拥有2GB的用户空间内存。 4 | 5 | 堆区位于用户空间,通常主要用于动态内存分配。**所以理论上,一个进程最多可以拥有接近2GB的堆内存**。但是,实际上可能不能使用全部的2GB,因为用户空间还包括了其他部分,如代码段、数据段、栈等。 6 | 7 | 需要注意的是,这个限制是可以配置的,有些Linux系统通过配置可以让一个进程的用户空间内存达到3GB或者更多。同时,这只是理论上的限制,实际使用中还需要考虑系统的其他资源限制,比如总的可用内存等。 -------------------------------------------------------------------------------- /problems/BTREE和B+TREE的区别.md: -------------------------------------------------------------------------------- 1 | 在实际应用中,**B+树**由于其读写性能稳定和叶子节点的顺序链表特性,使得它**更适合用于数据库索引**。而**B树**则有时候用于那些**需要频繁对数据进行更新的场景。** 2 | 3 | **共同点**: 4 | 5 | 1. 都是多路平衡查找树,有高度平衡的性质。 6 | 2. 节点可以拥有多个子项(children),不仅是两个,因此是多叉的。 7 | 3. 树中的每个节点都是按照键值有序排列的,并且每个节点中的键分隔了其子节点的范围。 8 | 4. 搜索、插入、删除操作的时间复杂度都是O(log n)。 9 | 10 | **主要区别**: 11 | 12 | 1. **键和数据的存储**: 13 | - 在B树中,每个节点都包含键和数据。因此,数据可以在非叶子节点找到。 14 | - 在B+树中,所有的数据都保留在叶子节点中,并且通常以链表的方式进行连接,非叶子节点只存储键信息。 15 | 2. **叶子节点的性质**: 16 | - B树的叶子节点没有特别的标记,它们与内部节点大体相似。 17 | - B+树的叶子节点通过指针串联成一个链表,方便全范围扫描和顺序访问。 18 | 3. **空间利用率**: 19 | - B+树的内部节点不存储数据,所以能存更多的键,这样树可以更矮更胖,减少IO次数。 20 | - B树的内部节点存储数据,可能导致树更高一些,增加IO次数。 21 | 4. **搜索效率**: 22 | - B树中,搜索可以在找到第一个匹配的键时停止,无论它在内部节点还是叶子节点。 23 | - B+树中,搜索总是会走到叶子节点,因为数据只能在叶子节点找到。 24 | 5. **复制和维护**: 25 | - B+树由于所有数据都存在于叶子节点且通过指针连接,所以更适合做全库扫描。 26 | - B树在更新数据时,修改更简单,因为数据可以直接在找到的节点中修改。 -------------------------------------------------------------------------------- /problems/BloomFilter原理.md: -------------------------------------------------------------------------------- 1 | BloomFilter(布隆过滤器)是一种用于判断元素是否属于集合的概率数据结构。它具有空间效率高和查询时间快的特点,但是存在一定的误判率。下面我将详细介绍BloomFilter的原理。 2 | 3 | ## BloomFilter的基本概念 4 | 5 | 1. 位数组(Bit Array): 6 | - BloomFilter使用一个固定大小的位数组来表示集合。 7 | - 位数组中的每个比特位初始化为0,表示集合为空。 8 | - 位数组的大小与期望的元素数量和误判率有关。 9 | 2. 哈希函数(Hash Functions): 10 | - BloomFilter使用多个哈希函数将元素映射到位数组中的不同位置。 11 | - 每个哈希函数将元素映射到一个固定范围内的整数,对应位数组中的一个索引。 12 | - 哈希函数的选择应该尽量独立和均匀,以减小冲突的概率。 13 | 3. 添加元素(Add Element): 14 | - 将一个元素添加到BloomFilter中时,使用所有的哈希函数计算该元素的哈希值。 15 | - 将位数组中对应哈希值的比特位设置为1,表示该元素已经添加到集合中。 16 | 4. 查询元素(Query Element): 17 | - 查询一个元素是否属于BloomFilter表示的集合时,使用相同的哈希函数计算该元素的哈希值。 18 | - 检查位数组中对应哈希值的比特位是否都为1。 19 | - 如果所有对应的比特位都为1,则认为该元素可能属于集合(存在误判)。 20 | - 如果任意一个对应的比特位为0,则确定该元素不属于集合(不存在漏判)。 21 | 22 | ## BloomFilter的工作原理 23 | 24 | 1. 初始化: 25 | - 创建一个固定大小的位数组,并将所有比特位初始化为0。 26 | - 选择适当数量的哈希函数,用于将元素映射到位数组中。 27 | 2. 添加元素: 28 | - 对于要添加的每个元素,使用所有的哈希函数计算其哈希值。 29 | - 将位数组中对应哈希值的比特位设置为1。 30 | - 重复以上步骤,直到所有元素都添加到BloomFilter中。 31 | 3. 查询元素: 32 | - 对于要查询的元素,使用相同的哈希函数计算其哈希值。 33 | - 检查位数组中对应哈希值的比特位是否都为1。 34 | - 如果所有对应的比特位都为1,则认为该元素可能属于集合。 35 | - 如果任意一个对应的比特位为0,则确定该元素不属于集合。 36 | 4. 误判率: 37 | - BloomFilter存在一定的误判率,即将不属于集合的元素判断为属于集合。 38 | - 误判率与位数组的大小、元素数量和哈希函数的数量有关。 39 | - 通过增加位数组的大小和哈希函数的数量,可以降低误判率,但会增加空间开销。 40 | 41 | ## BloomFilter的优缺点 42 | 43 | 优点: 44 | 45 | 1. 空间效率高:BloomFilter使用位数组存储信息,占用空间小于直接存储元素。 46 | 2. 查询时间快:BloomFilter的查询时间与哈希函数的数量成正比,与元素数量无关。 47 | 3. 插入时间快:将元素添加到BloomFilter中的时间与哈希函数的数量成正比。 48 | 49 | 缺点: 50 | 51 | 1. 存在误判:BloomFilter存在将不属于集合的元素判断为属于集合的可能性。 52 | 2. 无法删除元素:BloomFilter不支持删除已添加的元素,因为删除可能影响其他元素的判断。 53 | 3. 不能确定元素的具体信息:BloomFilter只能判断元素是否属于集合,无法获取元素的具体值。 -------------------------------------------------------------------------------- /problems/C++中struct和class有什么区别?.md: -------------------------------------------------------------------------------- 1 | 1. 默认的访问权限: 2 | - 在struct中,默认的成员变量和成员函数的访问权限是public的,意味着它们可以被外部访问。 3 | - 在class中,默认的成员变量和成员函数的访问权限是private的,意味着它们只能够在类的内部访问。 4 | 2. 继承方式: 5 | - 在struct中,继承的默认访问权限是public的,派生类可以访问基类的public和protected成员。 6 | - 在class中,继承的默认访问权限是private的,派生类可以访问基类的public和protected成员。 7 | -------------------------------------------------------------------------------- /problems/C++中动态链接库和静态连接库的区别是什么?.md: -------------------------------------------------------------------------------- 1 | ### 1. 链接时期 2 | 3 | - **静态链接库**:在程序编译时,静态库的内容会被复制到最终的可执行文件中。当你运行程序时,不需要库文件,因为所有的功能都已经包含在可执行文件里了。 4 | - **动态链接库**:程序在编译时并不复制库中的代码,而是在程序运行时加载库文件。这意味着库文件必须在程序运行时可用。 5 | 6 | ### 2. 文件大小 7 | 8 | - **静态链接库**通常会导致较大的可执行文件大小,因为所有使用的库代码都被复制进去了。 9 | - **动态链接库**允许可执行文件小一些,因为代码是在运行时才被加载。 10 | 11 | ### 3. 内存占用 12 | 13 | - **静态链接库**的缺点是如果有多个程序使用相同的库,每个程序都有自己的副本,这将导致内存的浪费。 14 | - **动态链接库**可以由多个正在运行的程序共享,只需在内存中有一个副本即可。 15 | 16 | ### 4. 分发和更新 17 | 18 | - **静态链接库**使得更新库变得复杂,因为每个应用都有自己的副本,所以每个应用都需要重新编译和分发。 19 | - 使用**动态链接库**时,只需替换库文件并且确保API兼容性,所有使用该库的应用程序就可以直接利用新版本的库,无须重新编译。 20 | 21 | ### 5. 跨平台兼容性 22 | 23 | - **静态链接库**生成的可执行文件更易于在没有安装相应库的不同系统上运行,因为它们包括了所有需要的代码。 24 | - 对于**动态链接库**,需要确保目标系统上存在正确版本的库文件。 25 | 26 | ### 6. 链接错误和冲突 27 | 28 | - **静态链接库**可能会引起版本冲突问题,尤其是当不同的库依赖同一个库但又各自静态链接了不同版本时。 29 | - **动态链接库**可以减少这种冲突,因为同一份库文件被所有依赖它的程序共享。 -------------------------------------------------------------------------------- /problems/C++中右值引用有什么作用?.md: -------------------------------------------------------------------------------- 1 | C++11引入了右值引用,用来支持移动语义和完美转发。 2 | 3 | 1. 移动语义:传统的复制操作需要额外的时间和空间,而有了移动语义后,可以直接将资源(如内存)从一个对象转移到另一个对象,而不必创建并删除临时对象。这对于大对象或者拥有独占所有权资源的对象特别有用。例如,unique_ptr和std::vector等STL容器就利用了移动语义实现了高效的操作。 4 | 2. 完美转发:在函数模板中,我们想把参数原封不动地传递给其他函数。由于传参可能存在值传递、左值引用、常量左值引用、右值引用等情况,为了保证参数的属性和类型不发生变化,我们需要使用std::forward实现完美转发。 5 | 6 | 右值引用主要用于两种场景:一是对象的移动(Move),二是万能引用(Forwarding Reference)。对于第一种情况,它是为了解决对象的复制效率问题;对于第二种情况,则是为了实现参数的完美传递,避免不必要的拷贝。 -------------------------------------------------------------------------------- /problems/C++中四种cast的转换?.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | 1. **static_cast**: `static_cast` 是用于编译时检测到的相关类型之间的转换,比如整型和浮点数、派生类和基类之间的指针或引用转换。它不能用于含有虚函数的多态基类指针或引用到派生类的转换,因为这需要运行时信息。 4 | 5 | 示例: 6 | 7 | ``` 8 | int i = 10; 9 | float f = static_cast(i); // 整型到浮点数的转换 10 | ``` 11 | 12 | 2. **dynamic_cast**: `dynamic_cast` 主要用于处理多态性。当涉及到继承体系中的向下转换(将基类的指针或引用转换为派生类类型)时,这个转换会检查转换的安全性。如果转换无效,对于指针,它会返回nullptr;对于引用,则抛出一个`std::bad_cast`异常。使用`dynamic_cast`需要运行时类型信息(RTTI),因此它有一定的性能代价。 13 | 14 | 示例: 15 | 16 | ``` 17 | Base* b = new Derived(); // 基类指针指向派生类对象 18 | Derived* d = dynamic_cast(b); // 向下转型成功 19 | if (d) { 20 | // 转型有效,'d' 不是 nullptr 21 | } 22 | ``` 23 | 24 | 3. **const_cast**: `const_cast` 用于移除或添加`const`或`volatile`属性。通常情况下,它被用于移除对象的常量性,允许修改原本被声明为`const`的变量。需要注意的是,去除一个本质上确实是常量的对象的`const`标记并进行修改可能会导致未定义行为。 25 | 26 | 示例: 27 | 28 | ``` 29 | const int ci = 10; 30 | int& modifiable = const_cast(ci); // 移除常量性以便修改 31 | modifiable = 20; // 注意:如果原对象真的是const,这里可能是未定义行为 32 | ``` 33 | 34 | 4. **reinterpret_cast**: `reinterpret_cast` 是最危险的cast,它能够执行低级的强制类型转换。尽管几乎没有任何语义检查,但它能够在几乎任意两种类型之间转换,例如整数与指针之间的转换。由于它的不安全性,应该尽可能避免使用`reinterpret_cast`,除非你完全理解所进行的转换。 35 | 36 | 示例: 37 | 38 | ``` 39 | int* p = new int(65); 40 | char* ch = reinterpret_cast(p); // 强制指针类型转换 41 | ``` 42 | 43 | 总结: 44 | 45 | - `static_cast`在相关类型间做安全转换。 46 | - `dynamic_cast`在类层次结构中转换,并支持运行时检查。 47 | - `const_cast`改变类型的`const`或`volatile`限定。 48 | - `reinterpret_cast`进行低级别、不安全的强制类型转换。 -------------------------------------------------------------------------------- /problems/C++中常用的类优化技术有那些?.md: -------------------------------------------------------------------------------- 1 | 1. 使用成员函数而非友元函数:将函数作为类的成员函数而不是友元函数可以提高封装性和代码可读性,并避免频繁地访问类的私有成员。 2 | 2. 使用引用参数而非传值参数:通过使用引用参数而不是传值参数来传递参数,可以减少内存开销和提高程序性能。特别是对于大型对象,避免了不必要的对象拷贝操作。 3 | 3. 使用移动语义:对于需要频繁进行资源管理的类,例如具有动态分配内存的类或具有文件句柄等资源的类,使用移动语义可以避免不必要的复制开销,提高程序效率。 4 | 4. 使用智能指针:在需要动态内存管理的情况下,使用智能指针(如 std::shared_ptr、std::unique_ptr 等)可以避免内存泄漏和悬空指针问题,提高代码的安全性和可靠性。 5 | 5. 使用const成员函数:将不会修改对象状态的函数声明为 const 成员函数可以提高类的接口清晰度,并增强代码的可维护性。 6 | 6. 避免不必要的动态内存分配:在设计类时,可以考虑避免过多的动态内存分配,尽量减少内存申请和释放的次数,以提高程序的性能和稳定性。 7 | 7. 使用内联函数:将简单的、频繁调用的函数声明为内联函数可以减少函数调用的开销,提高程序的执行效率。 -------------------------------------------------------------------------------- /problems/C++中类成员的访问权限有那些?.md: -------------------------------------------------------------------------------- 1 | 1. **Public**: 2 | - 使用`public`标签指定的成员可以被任何访问该类对象的代码访问。 3 | - 公开成员定义了类的外部接口。 4 | 2. **Protected**: 5 | - 使用`protected`标签指定的成员只能被以下几种代码访问: 6 | - 类本身内部的成员函数。 7 | - 继承自该类的派生类中的成员函数。 8 | - 保护成员通常用于在基类和派生类之间共享数据或行为,同时对类的其他使用者隐藏这些细节。 9 | 3. **Private**: 10 | - 使用`private`标签指定的成员只能被类本身内部的成员函数(以及其友元)访问。 11 | - 私有成员是实现类内部封装的关键,防止了对类实现细节的外部访问。 12 | 13 | 下面是一个简单的类声明示例,展示了如何使用这三种不同的访问说明符: 14 | 15 | ```c++ 16 | class MyClass { 17 | public: // 公开成员 18 | int publicVariable; 19 | 20 | void publicMethod() { 21 | // ... 22 | } 23 | 24 | protected: // 保护成员 25 | int protectedVariable; 26 | 27 | void protectedMethod() { 28 | // ... 29 | } 30 | 31 | private: // 私有成员 32 | int privateVariable; 33 | 34 | void privateMethod() { 35 | // ... 36 | } 37 | }; 38 | ``` -------------------------------------------------------------------------------- /problems/C++中结构体内存布局的规则是什么?.md: -------------------------------------------------------------------------------- 1 | 1. **成员顺序**: 结构体的成员变量在内存中按它们声明的顺序依次排列。 2 | 2. **数据对齐**: 为了提高访问速度,编译器会根据目标平台的要求对结构体成员进行对齐。这可能导致在成员之间或结构体末尾存在填充字节(padding)。 3 | - 对齐规则通常要求一个类型的数据地址必须是其大小的整数倍。例如,一个4字节的`int`通常需要放置在地址为4的倍数的位置上。 4 | - `#pragma pack`或编译器特定属性可用于改变或禁用默认的对齐行为。 5 | 3. **继承**: 如果结构体是从一个或多个其他结构体/类继承而来,则基类的成员将首先出现在派生类对象的内存中,后面跟着派生类自己的成员。 6 | 4. **虚函数**: 如果结构体有虚函数,编译器通常会在结构体的内存布局中加入一个指向虚函数表(vtable)的指针。这个指针位于对象的开始处,但具体位置取决于编译器的实现。 7 | 5. **虚继承**: 使用虚继承时,为了解决菱形继承问题,编译器会采取复杂的策略来安排内存布局,这通常涉及额外的指针和调整对象模型。 8 | 6. **静态成员**: 静态成员变量不作为结构体的一部分存储在每个对象的内存中,它们在全局/静态存储区有单独的存储空间。 9 | 7. **位域**: 如果结构体使用了位域,则相邻的位域可以被紧密打包以减少空间占用。但是,如果跨越了底层类型的边界,位域可能会被分割开。 10 | 8. **零大小数组**: 某些编译器允许在结构体末尾使用零大小数组作为柔性数组成员(flexible array member),而这通常不占用结构体的内存空间,只是作为一个符号占位符。 -------------------------------------------------------------------------------- /problems/C++中,结构体可以直接赋值吗?.md: -------------------------------------------------------------------------------- 1 | 结构体是一种用户自定义的数据类型,可以**像内置数据类型一样进行赋值操作**。结构体可以直接进行赋值,包括使用另外一个同类型的结构体来对它进行赋值。 2 | 3 | ``` 4 | struct Point { 5 | int x; 6 | int y; 7 | }; 8 | 9 | int main() { 10 | Point p1 = {3, 4}; 11 | Point p2; 12 | p2 = p1; // 可以直接将p1的值赋给p2 13 | } 14 | 15 | ``` 16 | 17 | -------------------------------------------------------------------------------- /problems/C++内存分配.md: -------------------------------------------------------------------------------- 1 | 2 | 内存分配方式有三种: 3 | 4 | (1)从静态存储区域分配。内存在程序编译的时候就已经分配好,这块内存在程序的整个运行期间都存在。例如全局变量,static变量。 5 | 6 | (2)在栈上创建。在执行函数时,函数内局部变量的存储单元都可以在栈上创建,函数执行结束时这些存储单元自动被释放。栈内存分配运算内置于处理器的指令集中,效率很高,但是分配的内存容量有限。 7 | 8 | (3)从堆上分配,亦称动态内存分配。程序在运行的时候用malloc或new申请任意多少的内存,程序员自己负责在何时用free或delete释放内存。动态内存的生存期由我们决定,使用非常灵活,但问题也最多。 9 | 10 | c++运行时各类型内存分配(堆,栈,静态区,数据段,BSS,ELF),BSS段, 11 | sizeof一个类求大小(字节对齐原则)、 12 | 13 | -------------------------------------------------------------------------------- /problems/C++单例模式.md: -------------------------------------------------------------------------------- 1 | 在C++中,单例模式是一种设计模式,用来限制一个类只能创建一个对象。这对于需要全局访问点的情况非常有用,例如日志记录或者数据库连接。 2 | 3 | 下面是一个简单的单例模式的实现 4 | 5 | ```C++ 6 | class Singleton { 7 | private: 8 | static Singleton* instance; 9 | // 私有构造函数,防止从其他地方创建对象。 10 | Singleton() {} 11 | 12 | public: 13 | // 静态访问方法。 14 | static Singleton* getInstance() { 15 | if (instance == 0) { 16 | instance = new Singleton(); 17 | } 18 | return instance; 19 | } 20 | }; 21 | 22 | // 初始化指针为零,这样在第一次调用getInstance时可以初始化 23 | Singleton* Singleton::instance = 0; 24 | 25 | int main() { 26 | // 创建一个新的Singleton对象。 27 | Singleton* s = Singleton::getInstance(); 28 | 29 | // 使用Singleton对象。 30 | 31 | return 0; 32 | } 33 | 34 | ``` 35 | 36 | -------------------------------------------------------------------------------- /problems/C++多态的实现有那几种?他们有什么不同?.md: -------------------------------------------------------------------------------- 1 | C++中,多态性主要通过两种方式实现:编译时多态(静态多态)和运行时多态(动态多态)。这两种多态的机制、特点和用途有所不同。 2 | 3 | 1. **编译时多态(静态多态)**: 编译时多态是在程序编译阶段实现的多态性。主要通过函数重载和运算符重载来实现。 4 | 5 | - **函数重载**: 同一个作用域内存在多个同名函数,但它们的参数类型或数量不同。根据调用时实际传递的参数类型和数量,编译器决定调用哪个函数。 6 | - **运算符重载**: 允许定义或重新定义大部分C++内置的运算符,使得它们可以根据操作数的类型执行不同的操作。 7 | 8 | **编译时多态的决策是在编译时做出的**,因此它不支持在运行时根据对象的实际类型来选择相应的成员函数。 9 | 10 | 2. **运行时多态(动态多态)**: 运行时多态是在程序运行阶段实现的多态性。它主要通过虚函数和继承来实现。 11 | 12 | - **虚函数**: 通过在基类中声明虚函数,允许派生类中重写该函数。当通过基类的指针或引用调用虚函数时,实际执行的是与指针或引用所指对象的实际类型相对应的函数版本。 13 | - **抽象类和纯虚函数**: 抽象类至少包含一个纯虚函数。纯虚函数在基类中没有实现,派生类必须重写这个函数。抽象类不能被实例化。 14 | 15 | **运行时多态的决策是在程序运行时做出的**,这就**需要运行时类型信息和虚函数表(vtable)**。在运行时,根据对象的实际类型来动态调用相应的成员函数,从而实现多态。 16 | 17 | 总的来说: 18 | 19 | - **编译时多态**是静态的,主要通过函数重载和运算符重载来实现,决策发生在编译阶段。 20 | - **运行时多态**是动态的,需要虚函数机制,并且决策发生在程序运行时。 21 | - **运行时多态**能够提供更高的灵活性和扩展性,是实现框架和库中一些高级功能(如插件架构或事件处理系统)的关键。 22 | - **编译时多态**由于在编译期就已经确定了调用哪个函数,通常性能更高,因为它避免了运行时查找虚函数表的开销。 -------------------------------------------------------------------------------- /problems/C++的atomic-bool代码底层是如何实现的?.md: -------------------------------------------------------------------------------- 1 | 在C++中,`std::atomic` 是一个模板类,用于提供对基础类型的原子操作。`std::atomic` 是该模板类针对布尔类型的特化。原子操作保证了即使在多线程环境中,每个操作也是不可分割的,从而避免了竞态条件。 2 | 3 | `std::atomic` 底层实现通常依赖于硬件和编译器的支持来提供原子性保证。具体实现可能涉及以下几个方面: 4 | 5 | 1. **内存屏障**(Memory Barriers/Fences): 内存屏障用于确保指令的执行顺序,阻止编译器或者处理器重排操作顺序。 6 | 2. **锁前缀指令**(Lock Prefix): 在x86架构中,处理器提供了带有`LOCK`前缀的指令,比如`LOCK XCHG`,它可以将操作变为原子性的。当CPU执行带有`LOCK`前缀的指令时,会确保指令完整执行,期间不会被其他处理器打断。 7 | 3. **特殊的原子指令**: 现代处理器提供了一系列原子指令,比如`XADD`(交换并加),`CMPXCHG`(比较并交换)等,可以用来实现原子变量。对于布尔变量,可能会使用这些指令来实现无锁的原子读写操作。 8 | 4. **编译器内建函数**(Compiler Intrinsics): 编译器可能提供特殊的内建函数来映射到底层的原子指令。 9 | 10 | 例如,在GCC和Clang上,通常会使用GCC的内建函数来实现原子操作。例如: 11 | 12 | ``` 13 | bool old_value = __atomic_fetch_and(&my_atomic_bool, true, __ATOMIC_SEQ_CST); 14 | ``` 15 | 16 | 这里的 `__atomic_fetch_and` 函数是GCC提供的内建函数,用于执行原子AND操作,并返回变量的旧值。第三个参数 `__ATOMIC_SEQ_CST` 表示使用最严格的内存顺序:Sequentially Consistent。 17 | 18 | 对于不支持原子指令的数据类型或复杂操作,可能需要使用锁(比如互斥锁)来保证操作的原子性。但由于 `bool` 类型非常简单,大多数平台都能够提供无锁的原子操作支持。 19 | 20 | 不同的平台和编译器可能有不同的实现方式,因此没有一个统一的实现细节。如果你想知道具体的实现,可以查看特定编译器的源代码或者汇编输出。 -------------------------------------------------------------------------------- /problems/C++的智能指针及其原理.md: -------------------------------------------------------------------------------- 1 | ### 智能指针简介 2 | 3 | C++的智能指针是一种用于管理动态分配内存的对象,它们提供了自动内存管理机制,避免了手动释放内存的繁琐和潜在的内存泄漏问题。 4 | 5 | 智能指针的原理基于RAII原则,即资源获取即初始化。智能指针通过在构造函数中获取资源(动态分配的内存),并在析构函数中释放资源,从而确保资源的正确释放。 6 | 7 | C++标准库提供了两种主要的智能指针:`std::unique_ptr`和`std::shared_ptr`。 8 | 9 | 1. `std::unique_ptr`: `std::unique_ptr`是独占所有权的智能指针,它确保只有一个指针可以访问所管理的对象。当`std::unique_ptr`被销毁时,它会自动释放所拥有的对象。它不能被复制,但可以通过std::move()函数进行所有权转移。 10 | 2. `std::shared_ptr`: `std::shared_ptr`是共享所有权的智能指针,它可以被多个指针同时访问和共享所管理的对象。它使用引用计数来追踪有多少个指针指向该对象,当引用计数为0时,即没有任何指针指向对象时,资源会被释放。`std::shared_ptr`可以被复制和赋值。 11 | 12 | 13 | 14 | 注意:weak_ptr严格来说,不能算是“智能指针”,他只是一个类的弱引用,是用来解决两个`std::shared_ptr`相互进行引用的问题的 15 | 16 | 17 | 18 | ### 侵入式和非侵入式的智能指针 19 | 20 | 在C++中,智能指针又被分为两种:侵入式和非侵入式的 21 | 22 | 1. **侵入式智能指针**: 侵入式智能指针需要被管理的类提供特定的接口或继承指定的基类,以支持智能指针的操作。这意味着被管理的类必须拥有与智能指针相关的成员函数或遵循特定的约定。侵入式智能指针可以更好地控制资源的生命周期,但需要修改被管理类的定义。 23 | 24 | 例如,Boost库中的`boost::intrusive_ptr`就是一种侵入式智能指针。被管理的类必须实现`add_ref()`和`release()`等函数,以增加和释放引用计数。 25 | 26 | 2. **非侵入式智能指针**: 非侵入式智能指针不需要被管理的类提供额外的接口或继承特定的基类。它通过自身的机制来管理资源的生命周期,而不需要对被管理的类做任何修改。这样可以更方便地将智能指针应用到已有的类中。 27 | 28 | C++标准库中的`std::shared_ptr`就是一种非侵入式智能指针。它可以管理任何动态分配的对象,而不需要对被管理的类做任何特殊要求。 29 | 30 | 非侵入式智能指针相对于侵入式智能指针更加灵活和方便,但在某些情况下侵入式智能指针可能提供更细粒度的资源管理控制。选择使用哪种类型的智能指针取决于具体的需求和设计考虑。 -------------------------------------------------------------------------------- /problems/C++结构体内存对齐.md: -------------------------------------------------------------------------------- 1 | C++中的结构体内存对齐是为了提高内存访问效率而采用的一种内存布局优化方式。在结构体中,根据处理器的架构和编译器设定的规则,可能会自动插入填充字节(padding),以确保结构体的成员变量按照一定的对齐方式存储。 2 | 3 | 以下是结构体内存对齐的基本原则: 4 | 5 | 1. 结构体的起始地址能够被其最宽基本类型成员的大小所整除。 6 | 2. 结构体中每个成员相对于结构体起始地址的偏移量(offset)都是该成员类型大小的整数倍,这就可能造成内存空间的浪费,即前面提到的填充字节。 7 | 3. 结构体的总大小为结构体最宽基本类型成员大小的整数倍,如果不足,会在结构体末尾添加填充字节。 8 | 4. 如果结构体中包含其他结构体或联合体,那么也需要按照这些内部结构体或联合体的对齐要求来对齐。 9 | 10 | 举例说明: 11 | 12 | ``` 13 | struct MyStruct { 14 | char a; // 1字节 15 | int b; // 4字节 16 | short c; // 2字节 17 | }; 18 | ``` 19 | 20 | 在上述结构体中,`int` 类型通常需要按照4字节对齐,在32位或64位架构下。假设编译器按照4字节对齐规则对该结构体进行内存对齐,则实际内存布局可能如下: 21 | 22 | - `char a;` 占用1字节,后面跟着3字节的填充,以确保接下来的 `int b;` 能在4字节边界上对齐。 23 | - `int b;` 直接按照4字节对齐,紧接着 `char a;` 后面的填充字节。 24 | - `short c;` 占用2字节,并且因为已经处于4字节边界上,所以不需要额外填充。 25 | 26 | 结构体总大小:`sizeof(MyStruct)` 很可能是8个字节(1字节`char` + 3字节填充 + 4字节`int`),但实际上还有一个2字节的`short`,所以需要再加上2个字节的填充,使得整个结构体大小为12字节,满足最大对齐成员`int`的4字节的整数倍。 27 | 28 | 记住,具体的对齐情况取决于编译器设置(例如GCC的`__attribute__((packed))`、`#pragma pack`等)和目标平台的硬件架构。开发者可以通过这些手段来修改默认的对齐规则,以满足特定的内存或性能要求。 -------------------------------------------------------------------------------- /problems/Cookie和SessioN.md: -------------------------------------------------------------------------------- 1 | ### Cookie 2 | 3 | Cookie是存储在客户端(通常是用户的浏览器)上的小型数据片段。服务器通过HTTP响应头向浏览器发送Cookie,浏览器会将这些信息存储起来,并在之后的每个请求中通过HTTP请求头将Cookie发送回服务器。Cookie主要用于: 4 | 5 | - 跟踪用户会话 6 | - 存储用户偏好 7 | - 实现自动登录等功能 8 | 9 | Cookie数据是以键值对的形式存储的,每个Cookie都有过期时间,过期后会自动删除。由于Cookie存储在客户端,因此其容量受到限制(每个域名下大约4KB),且存在安全隐患,比如易于被篡改和第三方读取。 10 | 11 | ### Session 12 | 13 | Session是服务器端用来存储信息的机制。当用户访问Web应用时,服务器可以为该用户创建一个唯一的Session对象,并为这个会话分配一个唯一的Session ID。这个Session ID通常会通过Cookie发送给用户浏览器存储(尽管也有其他传输方式,如URL重写)。用户在后续请求中提交这个Session ID,服务器就能识别出对应的用户会话。 14 | 15 | 使用Session的目的是: 16 | 17 | - 管理用户会话 18 | - 存储用户特定的数据,如购物车内容、用户ID等 19 | 20 | 与Cookie相比,Session更加安全,因为数据存储在服务器端,外界无法直接访问。此外,由于存储在服务器,理论上可以存储更多的数据,不过这也会增加服务器的内存消耗。 21 | 22 | ### Cookie与Session的关联 23 | 24 | 虽然Cookie和Session各有不同,但它们经常一起使用来管理用户会话。一个典型的流程是: 25 | 26 | 1. 用户首次访问网站时,服务器创建一个Session对象并生成一个唯一的Session ID。 27 | 2. 这个Session ID通过设置Cookie发送给用户浏览器。 28 | 3. 用户在后续的请求中,浏览器会自动将这个Session ID随着请求一起发送给服务器。 29 | 4. 服务器接收到Session ID后,识别出对应的用户会话,进行相应的处理。 -------------------------------------------------------------------------------- /problems/DNS和HTTP协议,HTTP请求方式.md: -------------------------------------------------------------------------------- 1 | **DNS(域名系统)**: 2 | 3 | - DNS是一个分布式的服务,它将人类可读的域名(如 [www.example.com](http://www.example.com/))转换为机器可读的IP地址(如 192.0.2.1),使得用户能够通过域名访问网站而无需记住复杂的IP地址。 4 | - 当你输入一个网址时,你的设备会使用DNS来查找对应的IP地址,从而能够连接到正确的服务器。 5 | - DNS查询通常在用户感知不到的情况下在后台进行,并且大多使用UDP协议进行通信,因为它比TCP更快,而DNS查询需要速度。 6 | 7 | **HTTP(超文本传输协议)**: 8 | 9 | - HTTP是一种用于传输超媒体文档(例如HTML)的应用层协议。它构建在TCP/IP协议之上,主要用于Web浏览器和服务器之间的通信。 10 | - HTTP工作在客户端-服务器架构上。用户的Web浏览器(客户端)会发起请求到服务器,服务器处理请求并返回响应。 11 | - HTTP定义了一系列的请求方法,常见的包括GET、POST、PUT、DELETE等。 12 | 13 | **HTTP请求方法**: 14 | 15 | 1. **GET**: 请求指定的页面信息,并返回实体主体。用于获取资源而不会影响资源状态。 16 | 2. **POST**: 向指定资源提交数据进行处理请求(例如提交表单或者上传文件)。数据包含在请求体中。POST请求可能会导致新的资源的建立或已有资源的修改。 17 | 3. **PUT**: 从客户端向服务器传送的数据取代指定的文档的内容。 18 | 4. **DELETE**: 请求服务器删除指定的页面。 19 | 5. **HEAD**: 类似于GET方法,但服务器将不返回实体的主体部分,用于获取报头。 20 | 6. **OPTIONS**: 允许客户端查看服务器的性能。 21 | 7. **PATCH**: 对资源进行部分修改。 22 | 8. **CONNECT**: 通常用于SSL加密服务器的通信以及代理服务器。 23 | 9. **TRACE**: 回显服务器收到的请求,主要用于测试或诊断。 24 | 25 | 这些方法各自有其特定的使用场景,它们共同构成了HTTP的一部分,让Web开发者可以根据不同的需求选择合适的方式来与服务器进行交互。 -------------------------------------------------------------------------------- /problems/DNS域名缓存是什么?.md: -------------------------------------------------------------------------------- 1 | ### 缓存目的和好处 2 | 3 | DNS域名缓存的主要目的是减少对远端DNS服务器的查询次数,加快域名解析速度,减轻DNS服务器的负担,从而提高整个互联网的效率和性能。具体来说,DNS缓存带来的好处包括: 4 | 5 | - **提高解析速度**:通过从缓存中直接获取解析结果,避免了每次都进行完整的DNS解析流程,大大加快了域名到IP地址的转换速度。 6 | - **减少网络延迟**:由于减少了对远端DNS服务器的查询,从而降低了网络延迟。 7 | - **减轻DNS服务器负担**:缓存可以显著减少DNS服务器接收的请求数量,有助于缓解服务器负载。 8 | 9 | ### 缓存位置 10 | 11 | 1. **浏览器缓存**:现代Web浏览器都会维护自己的DNS缓存,以便重复访问的网站可以更快加载。 12 | 2. **操作系统缓存**:操作系统也会缓存DNS查询结果,当应用程序请求DNS解析时,首先会检查操作系统的DNS缓存。 13 | 3. **递归DNS服务器缓存**:当用户的查询请求发送到递归DNS服务器时,这些服务器也会缓存一份DNS查询结果,供后续相同的查询请求使用。 14 | 4. **权威DNS服务器**:虽然权威DNS服务器本身不缓存外部域名的解析结果,但它们会为自己负责的域名提供TTL(生存时间),告诉其他DNS服务器和客户端可以缓存解析结果的时间长度。 -------------------------------------------------------------------------------- /problems/DNS解析的过程?.md: -------------------------------------------------------------------------------- 1 | ### 1. 浏览器缓存 2 | 3 | - 当用户在浏览器中输入一个网址时,浏览器首先检查自己的缓存,看看该域名的IP地址是否已经保存在本地。如果找到了相应的记录,DNS解析过程就此结束。 4 | 5 | ### 2. 系统缓存 6 | 7 | - 如果浏览器缓存中没有找到,操作系统会检查自己的DNS缓存中是否有这个域名对应的IP地址。Windows系统可以通过命令`ipconfig /displaydns`来查看本地DNS缓存内容。 8 | 9 | ### 3. 路由器缓存 10 | 11 | - 如果操作系统缓存中也没有记录,请求会被发送到路由器,路由器通常会有自己的DNS缓存。 12 | 13 | ### 4. ISP的DNS服务器 14 | 15 | - 如果之前的步骤都未能解析域名,那么查询请求会被发送到ISP(互联网服务提供商)的DNS服务器。ISP的DNS服务器会检查它的缓存,看看是否可以找到这个域名对应的IP地址。 16 | 17 | ### 5. 根DNS服务器 18 | 19 | - 如果ISP的DNS服务器也无法解析,它会向根DNS服务器发起请求。根服务器是顶级的DNS服务器,它不直接知道域名的IP地址,但能指引下一步应该查询哪个顶级域(TLD,例如`.com`、`.net`等)服务器。 20 | 21 | ### 6. 顶级域(TLD)服务器 22 | 23 | - 根据根服务器的指引,ISP的DNS服务器接着向适当的TLD服务器发送查询请求。TLD服务器管理着在该顶级域下注册的所有域名的信息,并能提供存储该域名记录的权威DNS服务器的地址。 24 | 25 | ### 7. 权威DNS服务器 26 | 27 | - 最后,ISP的DNS服务器会向该域名的权威DNS服务器发起请求。权威服务器直接包含了映射到该域名的IP地址的记录。 28 | 29 | ### 8. 缓存结果并返回给客户端 30 | 31 | - 一旦ISP的DNS服务器收到权威DNS服务器提供的IP地址,它会缓存这个结果(以便于未来加速同一域名的解析),然后把这个IP地址返回给最初发起请求的客户端(用户的计算机)。 32 | 33 | ### 9. 浏览器访问网站 34 | 35 | - 浏览器最终收到IP地址后,就可以使用该地址与目标服务器建立连接,并开始加载网站内容。 -------------------------------------------------------------------------------- /problems/GET请求中,URL编码有什么含义?.md: -------------------------------------------------------------------------------- 1 | ### URL编码的主要目的包括: 2 | 3 | 1. **处理非ASCII字符**:URLs原则上仅支持ASCII字符集。对于不属于这个字符集的内容,例如中文、阿拉伯文或特殊符号等,使用URL编码可以安全地加以传输。 4 | 2. **转义保留字符**:URL具有特定的格式,其中某些字符(如`/`, `?`, `&`, `=`等)具有特殊意义。如果这些字符用于其它目的(例如作为数据值的一部分),需要通过URL编码来转义,以避免混淆。例如,在GET请求的查询字符串中,`&`用于分隔键值对,若键或值实际包含`&`,该字符就必须被编码。 5 | 3. **处理空格和控制字符**:URL中直接包含空格(例如,空格通常会被替换为`+`符号或`%20`)和控制字符(如换行符)是不允许的,因此这些字符也需要经过编码后才能在URL中使用。 -------------------------------------------------------------------------------- /problems/HTTP中常用的状态码都有那些?.md: -------------------------------------------------------------------------------- 1 | ### 1xx:信息性状态码 2 | 3 | - **100 Continue**:客户端应继续其请求 4 | 5 | ### 2xx:成功 6 | 7 | - **200 OK**:请求成功,对GET、PUT、PATCH或POST操作的标准响应 8 | - **201 Created**:请求已经被实现,且新的资源已经被创建 9 | - **204 No Content**:服务器成功处理了请求,但不需要返回任何实体内容 10 | 11 | ### 3xx:重定向 12 | 13 | - **301 Moved Permanently**:请求的页面已永久移动到新位置 14 | - **302 Found**(之前“Moved Temporarily”):服务器目前从不同位置的网页响应请求,但请求者应继续使用原有位置来进行以后的请求 15 | - **304 Not Modified**:自从上次请求后,请求的网页未修改过 16 | 17 | ### 4xx:客户端错误 18 | 19 | - **400 Bad Request**:服务器无法理解请求的格式,客户端不应该再次尝试发送同样的请求 20 | - **401 Unauthorized**:请求未经授权,这个状态代码必须和WWW-Authenticate报头一起使用 21 | - **403 Forbidden**:服务器拒绝请求 22 | - **404 Not Found**:请求失败,请求所希望得到的资源未被在服务器上发现 23 | - **405 Method Not Allowed**:请求行中指定的请求方法不能被用于请求相应的资源 24 | 25 | ### 5xx:服务器错误 26 | 27 | - **500 Internal Server Error**:服务器遇到了一个未曾预料的状况,导致它无法完成对请求的处理 28 | - **501 Not Implemented**:服务器不支持当前请求所需要的某个功能 29 | - **502 Bad Gateway**:作为网关或代理工作的服务器尝试执行请求时,从上游服务器接收到无效的响应 30 | - **503 Service Unavailable**:由于临时的服务器维护或过载,服务器当前无法处理请求 31 | - **504 Gateway Timeout**:作为网关或代理的服务器,未能及时从上游服务器或辅助服务器接收请求 -------------------------------------------------------------------------------- /problems/HTTP方法都有那些?.md: -------------------------------------------------------------------------------- 1 | 1. **GET**:请求指定的资源。GET请求应该只用于获取数据,并且不应当引起服务器上资源状态的改变。 2 | 2. **POST**:向指定的资源提交数据以进行处理(例如,提交表单或上传文件)。数据包含在请求体中。POST请求可能会创建新的资源或修改现有资源。 3 | 3. **PUT**:将请求体中的数据发送到指定的资源以创建或替换该资源。相对于POST,PUT具有幂等性,意味着多次执行相同的PUT请求应该产生相同的结果。 4 | 4. **DELETE**:删除指定的资源。 5 | 5. **HEAD**:与GET方法类似,但服务器在响应中只返回头部信息,不返回实际的资源内容。这常用于检测资源的有效性或最近更新时间。 6 | 6. **OPTIONS**:描述目标资源的通信选项,用于确定服务器支持的HTTP方法。 7 | 7. **PATCH**:对资源应用部分修改。它比PUT更加精细,只更新资源的一部分而不是整个资源。 8 | 8. **CONNECT**:将连接转换为透明的TCP/IP隧道,通常用于SSL加密服务器的代理(通过CONNECT方法将请求转发给HTTPS端口)。 9 | 9. **TRACE**:执行一个消息回环测试,沿路径回显收到的请求。这允许客户端看到请求被中间服务器添加的或修改的字段。 -------------------------------------------------------------------------------- /problems/HTTP长连接和短链接都用在那些场景?.md: -------------------------------------------------------------------------------- 1 | ### 短连接 2 | 3 | 在短连接模式下,客户端与服务器之间的每个请求/响应对都会打开一个新的连接,并在交换完成后立即关闭连接。短连接适用于以下场景: 4 | 5 | - **低频请求**:如果客户端只偶尔与服务器通信,使用短连接可以减少服务器维护空闲连接所需的资源。 6 | - **简单的请求/响应交互**:对于简单且数量不多的请求,短连接可以快速建立,完成数据交换后即刻释放资源。 7 | - **负载分配**:在负载均衡的环境中,短连接有助于将来自不同客户端的请求更公平地分配到不同的服务器。 8 | 9 | ### 长连接 10 | 11 | HTTP/1.1默认采用长连接(持久连接),在此模式下,TCP连接在多个请求/响应之间保持开放状态,直到由客户端或服务器明确关闭。长连接适合于: 12 | 13 | - **高频请求**:当客户端需要频繁地向服务器发送请求时,长连接减少了因为建立和关闭连接而带来的额外开销。 14 | - **减少延迟**:对于需要快速响应的应用,如网页浏览和在线游戏,长连接能够减少每次请求所需的往返时间(RTT),提高用户体验。 15 | - **实时通信**:在聊天应用、实时数据更新等场景中,长连接可以保持一个持续的数据流,允许服务器主动向客户端推送信息。 -------------------------------------------------------------------------------- /problems/IO模型了解哪些?.md: -------------------------------------------------------------------------------- 1 | 1. **阻塞IO模型**(Blocking IO): 2 | - 在阻塞IO模型中,当应用程序发起IO操作时,会被阻塞直到数据准备好或者IO完成。 3 | - 这意味着应用程序在进行IO操作时会暂停执行,直到数据准备好可以读取或写入完成后才会继续执行。 4 | - 阻塞IO模型简单易用,但可能导致资源浪费和性能下降。 5 | 2. **非阻塞IO模型**(Non-blocking IO): 6 | - 在非阻塞IO模型中,应用程序发起IO操作后,并不会被阻塞,而是立即返回结果。 7 | - 应用程序需要通过轮询等方式主动查询IO操作是否完成,从而实现异步IO操作。 8 | - 非阻塞IO模型相比阻塞IO模型可以提高系统的并发性能。 9 | 3. **多路复用IO模型**(I/O multiplexing): 10 | - 多路复用IO模型利用操作系统提供的select、poll、epoll等机制同时监控多个文件描述符的IO状态。 11 | - 当某个文件描述符就绪时,应用程序可以通过事件通知来进行IO操作,避免了阻塞等待。 12 | - 多路复用IO模型可以有效地管理多个IO操作,提高系统的效率和吞吐量。 13 | 4. **信号驱动IO模型**(Signal-driven IO): 14 | - 信号驱动IO模型通过向内核注册信号处理函数,在IO操作完成时由内核发送信号来通知应用程序。 15 | - 应用程序可以继续执行其他任务,而无需阻塞等待IO操作完成。 16 | - 信号驱动IO模型相对于非阻塞IO模型提供了更好的异步IO支持。 17 | 5. **异步IO模型**(Asynchronous IO): 18 | - 异步IO模型通过操作系统提供的异步IO接口来实现IO操作,应用程序可以在IO操作完成后得到通知。 19 | - 应用程序无需关心IO操作的具体状态,可以继续执行其他任务。 20 | - 异步IO模型适合处理大量的IO请求,提高系统的响应速度和并发处理能力。 -------------------------------------------------------------------------------- /problems/Innodb和Myisam的区别.md: -------------------------------------------------------------------------------- 1 | 数据库引擎是用于存储、处理和保护数据的核心服务 2 | 3 | 1.事务 4 | 5 | InnoDB 支持事务,事务安全。Myisam 非事务安全,也不支持事务 6 | 7 | 2.锁 8 | 9 | innoDb 行级锁,myisam 针对表加锁 10 | 11 | 3.索引 12 | 13 | innodb 不支持全文索引,myisam 支持全局索引 14 | 15 | 4.适用场景 16 | 17 | myisam 效率快于 innodb ,适用于小型应用,扩平台支持。大量查询 select 18 | 19 | innodb 支持事务,也有 ACID 的特性还有 insert update 对于事务的控制操作 -------------------------------------------------------------------------------- /problems/Linux上有个二进制程序一直在运行,我修改代码置换重新编译把原来的二进制程序覆盖了,会怎么样?.md: -------------------------------------------------------------------------------- 1 | 在Linux系统中,如果一个正在运行的二进制程序被覆盖(即,文件内容被新的编译版本替换),通常情况下运行中的程序不会受到影响。这是因为当程序启动时,它的可执行文件内容会被加载到内存中,操作系统会维护对这些数据的引用,并且即便原始的文件系统上的可执行文件被覆盖或删除,这些内存映射通常仍然有效。 2 | 3 | 4 | 5 | 但是,替换正在运行的二进制文件并不会影响到当前运行中的进程,**但会影响后续启动的进程**,**因为后续运行的文件就会使用文件的最新版本进行运行**。 6 | 7 | 8 | 9 | 此外,面试的时候可以加上一个和**共享库的区别**:**共享库一般依赖于动态链接,所以如果在保证兼容性的情况下是可以进行热更新的,就是更新文件的同时更新程序,但是这要和程序本身做区分** 10 | 11 | -------------------------------------------------------------------------------- /problems/Map-Reduce原理.md: -------------------------------------------------------------------------------- 1 | Map-Reduce是一种用于处理大规模数据集的并行计算模型和框架。它由Google公司提出,旨在简化大规模数据处理任务的编程和执行。下面我将详细介绍Map-Reduce的原理。 2 | 3 | ## Map-Reduce的基本概念 4 | 5 | 1. Map阶段: 6 | - 将输入数据分割成多个独立的子问题,每个子问题可以并行处理。 7 | - 对每个子问题应用Map函数,将其转换为一组中间的键值对(key-value pairs)。 8 | - Map函数的输出结果是一组中间键值对,其中键(key)用于后续的Reduce阶段。 9 | 2. Shuffle阶段: 10 | - 将Map阶段输出的中间键值对按照键(key)进行分组和排序。 11 | - 将具有相同键的值组合在一起,形成的形式。 12 | - Shuffle阶段通常由Map-Reduce框架自动完成,无需用户干预。 13 | 3. Reduce阶段: 14 | - 对每个键(key)及其对应的值列表(list(values))应用Reduce函数。 15 | - Reduce函数对值列表进行合并、聚合或转换操作,生成最终的结果。 16 | - 每个Reduce任务的输出结果是一组最终的键值对,表示问题的解。 17 | 18 | ## Map-Reduce的工作流程 19 | 20 | 1. 数据分割: 21 | - 将大规模的输入数据集分割成多个独立的数据块(splits)。 22 | - 每个数据块可以在不同的机器上并行处理,提高计算效率。 23 | 2. Map任务: 24 | - 为每个数据块创建一个Map任务,并将其分配给集群中的工作节点。 25 | - 每个Map任务读取其对应的数据块,对每个数据项应用Map函数,生成中间键值对。 26 | - Map函数的输出结果暂时存储在本地磁盘或内存中。 27 | 3. Shuffle和排序: 28 | - Map任务完成后,Map-Reduce框架对中间键值对进行分组和排序。 29 | - 具有相同键的值被组合在一起,形成的形式。 30 | - Shuffle过程通常在Map任务和Reduce任务之间进行,由框架自动完成。 31 | 4. Reduce任务: 32 | - 为每个唯一的键创建一个Reduce任务,并将其分配给集群中的工作节点。 33 | - 每个Reduce任务读取属于其键的所有值,对值列表应用Reduce函数,生成最终结果。 34 | - Reduce函数的输出结果通常写入到分布式文件系统或数据库中。 35 | 5. 结果合并: 36 | - 所有Reduce任务完成后,将它们的输出结果合并成最终的结果集。 37 | - 最终结果可以存储在分布式文件系统中,或者返回给用户进行后续处理。 38 | 39 | ## Map-Reduce的优点 40 | 41 | 1. 可扩展性:Map-Reduce可以轻松地扩展到大规模集群,处理PB级别的数据。 42 | 2. 容错性:Map-Reduce框架自动处理节点故障和任务失败,确保计算的可靠性。 43 | 3. 易用性:用户只需编写Map和Reduce函数,无需关心分布式计算的细节。 44 | 4. 灵活性:Map-Reduce可以应用于各种数据处理场景,如数据分析、机器学习等。 -------------------------------------------------------------------------------- /problems/MySQL的主从复制是如何实现的?.md: -------------------------------------------------------------------------------- 1 | 1. **日志文件和位置(Binary Log and Position)**: 2 | - 主服务器上的所有数据更改(如INSERT、UPDATE、DELETE等)都会记录在二进制日志(Binary Log)中。这个日志包含了所有更改数据的事件以及标识这些事件位置的日志点(Log Position)。 3 | 2. **配置主服务器**: 4 | - 在主服务器上,需要启用二进制日志,并配置一个唯一的服务器ID。 5 | 3. **配置从服务器**: 6 | - 在从服务器上,也需要配置一个唯一的服务器ID,它与主服务器的ID不同。 7 | - 设置从服务器以连接到主服务器,并指定要开始复制的二进制日志文件名和日志位置。 8 | - 从服务器会启动两个重要的线程:I/O线程和SQL线程。 9 | - **I/O线程**:连接到主服务器并请求从指定的日志文件和位置开始复制数据,然后将接收到的二进制日志事件复制到从服务器的中继日志(Relay Log)。 10 | - **SQL线程**:读取中继日志中的事件并在从服务器上重新执行这些事件,以此来更新从服务器上的数据。 11 | 4. **数据复制过程**: 12 | - 主服务器的二进制日志被不断记录数据变更。 13 | - 从服务器的I/O线程从主服务器请求并获取二进制日志的更新,并将它们写入中继日志。 14 | - 从服务器的SQL线程读取中继日志中的事件,并将这些事件应用到从服务器的数据库中,确保与主服务器的数据一致。 15 | 5. **监控和维护**: 16 | - 复制过程可以监控各种状态和性能指标,如延迟、错误和日志文件的大小。 17 | - 维护复制环境可能涉及处理错误、重新同步数据和升级服务器。 -------------------------------------------------------------------------------- /problems/MySQL的行级锁有那些种类?.md: -------------------------------------------------------------------------------- 1 | 1. **记录锁(Record Locks)** 2 | - 基本的行级锁,直接作用于索引记录上。 3 | 2. **间隙锁(Gap Locks)** 4 | - 锁定一个范围,但不包括记录本身,主要用于防止幻读(phantom reads)。 5 | 3. **临键锁(Next-Key Locks)** 6 | - 是记录锁和间隙锁的组合,锁定一个范围并且包括记录本身。临键锁可以防止幻读,并且是InnoDB默认的行级锁形式。 -------------------------------------------------------------------------------- /problems/NGINX在Linux上是如何工作的?简单描述一下.md: -------------------------------------------------------------------------------- 1 | 1. **事件驱动与非阻塞I/O**:Nginx的Worker进程内部使用了异步非阻塞的I/O处理方式和事件驱动的机制来处理请求。Nginx可以处理大量并发连接,而且每个Worker进程都可以同时处理多个请求,这意味着Nginx可以支持高并发且高效率。 2 | 2. **反向代理和负载均衡**:Nginx可以设置成反向代理服务器,在这种模式下,Nginx可以接收客户端的请求,然后将该请求转发到后端的其他服务器,并将从后端服务器获取的响应返回给客户端。同时,Nginx也提供负载均衡功能,可以根据预设的策略(如轮询、最少连接等)将请求分发到不同的后端服务器。 3 | 3. **静态资源服务**:对于静态资源的请求,Nginx可以直接读取磁盘上的静态文件(如HTML、CSS、JavaScript文件或图片等)并返回。 4 | 4. **(可选,如果你不了解面试建议不说)Master-Worker架构**:Nginx采用了Master-Worker的模型。在启动Nginx服务时,首先会有一个Master进程被创建出来,在这个Master进程中,会创建多个Worker进程。Master进程主要负责管理Worker进程,包括读取并验证配置信息、创建、绑定套接字然后传递给worker进程、启动、关闭、维护worker进程等。而Worker进程则负责处理实际的请求。 -------------------------------------------------------------------------------- /problems/OSI七层模型分别是?各自的功能分别是什么?.md: -------------------------------------------------------------------------------- 1 | ### 1. 物理层 2 | 3 | **功能:** 物理层负责数据在物理媒介上的传输,将来自数据链路层的数据帧转换为电信号、光信号或无线信号。它涉及的范围包括电缆类型、接口形状、引脚数量等。 4 | 5 | ### 2. 数据链路层 6 | 7 | **功能:** 数据链路层管理介质访问控制并进行错误检测与纠正。它将Raw位流组织成逻辑结构称为“数据帧”,并在两个相邻节点之间传输这些帧。此层还负责流量控制和帧同步。 8 | 9 | ### 3. 网络层 10 | 11 | **功能:** 网络层负责设备间的数据传输和寻址。它决定数据的路径路由,并使用IP地址和子网划分来导向目标位置。它也处理分组路由、拥塞控制和网络互联问题。 12 | 13 | ### 4. 传输层 14 | 15 | **功能:** 传输层提供端到端的通信服务。它负责数据的分段和重组,并保证数据完整性。主要协议有TCP和UDP,分别提供可靠的连接和不可靠的连接。 16 | 17 | ### 5. 会话层 18 | 19 | **功能:** 会话层建立、管理和终止应用程序之间的会话。它的职责是建立和维护持久的数据通信会话,并在必要时重新启动会话。 20 | 21 | ### 6. 表示层 22 | 23 | **功能:** 表示层负责数据的编码、转换和解压。它确保来自应用层的数据被网络格式化为适合传输的格式,并在达到目标后被恢复原样。它还可以处理加密和解密。 24 | 25 | ### 7. 应用层 26 | 27 | **功能:** 应用层为最终用户提供网络服务接口。它直接支持各种端用户应用,如Web浏览器、Email客户端、远程文件服务等。此层负责识别和建立与远程应用的通信。 -------------------------------------------------------------------------------- /problems/POST和GET的主要区别有那些?.md: -------------------------------------------------------------------------------- 1 | 1. **语义上的区别**: 2 | - GET请求通常用于请求服务器发送资源或数据。它意味着获取信息,而不应该引起服务器上任何资源的状态改变。 3 | - POST请求则用于向服务器提交数据以创建或更新资源。它通常会引起服务器上资源的状态变化或副作用。 4 | 2. **数据传输位置**: 5 | - 在GET请求中,数据附加在URL之后作为查询字符串参数进行发送,形式为`?key1=value1&key2=value2`。 6 | - POST请求将数据包含在请求体中发送给服务器,这意味着数据不会出现在URL中,适合传输敏感信息或大量数据。 7 | 3. **数据大小限制**: 8 | - GET请求由于数据直接附加在URL后面,因此受到URL长度限制(由浏览器和服务器决定),通常不适合传输大量数据。 9 | - POST请求没有这样的限制,理论上可以传输更多数据,更适合大量数据的传输。 10 | 4. **安全性和隐私**: 11 | - GET请求中的数据暴露在URL中,可能会被浏览器历史、Web服务器日志等记录下来,因此不适合传输敏感信息。 12 | - POST请求中的数据在请求体内,不会直接暴露在URL中,相对更安全。 13 | 5. **缓存和书签**: 14 | - GET请求可以被缓存,也可以保存为书签。 15 | - POST请求不会被缓存,也不能保存为书签。 -------------------------------------------------------------------------------- /problems/RedisHash的原理和使用场景.md: -------------------------------------------------------------------------------- 1 | ### Redis Hash 原理: 2 | 3 | - Redis的Hash是一个键值对集合,类似于Python的字典(dictionary),可以存储多个字段和对应的值。 4 | - 在内部实现上,Redis的Hash使用类似于Java HashMap的方式来存储数据,通过哈希表和链地址法解决哈希冲突。 5 | - Hash在Redis中是一种非常高效的数据结构,可以快速插入、查找、更新和删除字段及其对应的值。 6 | 7 | ### Redis Hash 使用场景: 8 | 9 | 1. **存储对象属性**:Hash适合用于存储对象的属性信息,比如用户信息、商品详情等。每个字段表示对象的一个属性,对应的值为属性值,方便管理和查询。 10 | 2. **缓存数据**:Hash可以作为缓存数据的存储结构,将复杂数据序列化后存储在Hash中,快速读取以提升性能。 11 | 3. **计数器**:Hash的字段值可以是整数类型,因此可以用于实现计数器功能,比如统计网站访问量、点赞次数等。 12 | 4. **配置信息**:Hash可用于存储配置信息,例如系统参数、功能开关等,方便进行动态调整。 13 | 5. **存储用户会话信息**:Hash可以用来存储用户的会话信息,每个用户对应一个Hash,字段表示不同的会话属性。 -------------------------------------------------------------------------------- /problems/RedisList的原理和使用场景.md: -------------------------------------------------------------------------------- 1 | ### Redis List 原理: 2 | 3 | - Redis的List类型是一个双向链表,支持在两端进行元素的插入和删除操作,即头部(左侧)和尾部(右侧)。 4 | - List可以存储有序的字符串元素,允许重复元素的存在,内部采用链表结构来存储数据。 5 | - List类型提供了丰富的操作命令,如从指定位置插入元素、获取范围内的元素、根据值查找索引等。 6 | 7 | ### Redis List 使用场景: 8 | 9 | 1. 消息队列:List类型常用于实现简单的消息队列,生产者通过将消息推入列表尾部,消费者则从列表头部弹出消息,实现基本的消息发布与订阅功能。 10 | 2. 实时排行榜:可以利用List类型存储用户得分信息,并根据得分的高低排序,快速获取用户在排行榜中的位置。 11 | 3. 循环任务调度:通过List的阻塞弹出操作(BLPOP、BRPOP)实现轮询循环任务的调度,比如定时任务、延时任务等。 12 | 4. 数据同步:List类型可以用于记录数据变更日志,实现数据同步功能,生产端生成数据变更事件并推入列表,消费端消费事件并执行相应的同步操作。 -------------------------------------------------------------------------------- /problems/RedisSet的原理和使用场景.md: -------------------------------------------------------------------------------- 1 | ### Redis Set 原理: 2 | 3 | - Redis的Set类型是一个无序、不重复的集合,内部使用哈希表实现,保证了集合中元素的唯一性。 4 | - Set类型支持添加、删除、判断元素是否存在等操作,时间复杂度为O(1)。 5 | - Redis还提供了丰富的集合运算命令,如求交集、并集、差集等,方便对多个集合进行操作。 6 | 7 | ### Redis Set 使用场景: 8 | 9 | 1. 标签系统:Set类型适合用于存储对象的标签信息,每个对象可以关联一个或多个标签,通过集合操作实现标签之间的交集、并集查询。 10 | 2. 好友关系:可以将用户的好友列表存储在Set中,利用集合运算命令快速实现共同好友、推荐好友等功能。 11 | 3. 唯一性校验:通过Set类型存储数据的唯一性信息,可以快速进行去重操作,确保数据的唯一性。 12 | 4. 点赞/收藏功能:可以利用Set类型记录用户点赞或收藏的内容,确保每个用户对同一内容只能进行一次操作。 -------------------------------------------------------------------------------- /problems/RedisString原理和使用场景(分布式锁).md: -------------------------------------------------------------------------------- 1 | Redis中的String是一种简单的键值对数据类型,用于存储字符串、整数或二进制数据。在Redis中,String类型是最基础和常用的数据类型之一。 2 | 3 | ### Redis String原理: 4 | 5 | - String在Redis中是一个二进制安全的字符串,可以存储任意类型的数据。 6 | - Redis的String类型是动态字符串(dynamic string),即内部使用了预分配的缓冲区来保存字符串值,并能够自动扩展。 7 | - String类型支持常见的操作,如设置值、获取值、追加、自增、自减等,操作时间复杂度为O(1)。 8 | 9 | ### Redis String使用场景: 10 | 11 | 1. 缓存:String类型常用于缓存数据,如缓存页面内容、计算结果等。由于Redis的快速读写能力,String类型适合作为缓存数据的存储方式。 12 | 2. 计数器:可以利用String的自增、自减操作实现计数器功能,如统计网站访问量、商品库存等。 13 | 3. 分布式锁:通过String类型的SETNX命令(Set if Not eXists)可以实现分布式锁。即当某个键不存在时,设置该键为特定值,以此实现互斥锁的功能。 -------------------------------------------------------------------------------- /problems/RedisZSet的原理和使用场景(延迟队列).md: -------------------------------------------------------------------------------- 1 | ### Redis ZSet 原理: 2 | 3 | - Redis的ZSet(有序集合)是在Set基础上增加了一个分数(score)字段,用于对集合中的元素进行排序。 4 | - ZSet内部使用跳跃表(Skip List)和哈希表结合的方式实现,保证了元素的有序性和唯一性。 5 | - ZSet支持添加、删除、更新元素,并且可以根据分数范围、排名等条件快速获取元素。 6 | 7 | ### Redis ZSet 使用场景: 8 | 9 | 1. 排行榜:ZSet适合用于实现各种类型的排行榜功能,比如用户积分排行榜、文章热度排行榜等,通过设置分数来排序。 10 | 2. 时间轴:可以利用ZSet记录时间戳及相关事件信息,实现用户时间轴功能,按时间顺序展示用户的动态。 11 | 3. 延迟队列:ZSet常用于实现延迟队列,即将需要延迟处理的任务以时间戳作为分数存储在有序集合中,通过定时轮询或者通过有序集合提供的带有时间区间的命令来获取需要执行的任务。 12 | 4. 范围查询:ZSet提供了根据分数范围获取元素的功能,可用于实现范围查询,比如根据积分范围查找用户。 -------------------------------------------------------------------------------- /problems/Redis不是单线程吗,为什么会存在并发安全问题.md: -------------------------------------------------------------------------------- 1 | 虽然Redis主要是单线程的,但在某些情况下仍可能存在并发安全问题。这些并发安全问题通常涉及到对Redis的数据结构进行操作时,特别是在多个客户端同时对同一数据进行读写操作时可能会发生。 2 | 3 | 以下是一些可能导致并发安全问题的情况: 4 | 5 | 1. **竞态条件**:当多个客户端同时对同一键进行读取和写入操作时,可能会出现竞态条件。例如,在一个客户端读取一个值后,另一个客户端修改了这个值,导致第一个客户端获得了过期或错误的值。 6 | 2. **原子性操作**:有些操作需要保证原子性,即要么全部执行成功,要么全部不执行。如果多个客户端同时进行类似于INCR或DECR等原子性操作,可能会导致数据不一致或丢失。 7 | 3. **并发写入**:当多个客户端同时对同一个键进行写入操作时,可能会造成数据覆盖或混乱。由于Redis是单线程的,所以它无法处理多个写操作之间的并发性。 8 | 9 | 为了避免这些并发安全问题,可以采取以下措施: 10 | 11 | - 使用Redis的事务(Transaction)来保证一系列操作的原子性。 12 | - 使用乐观锁或悲观锁来控制并发访问。 13 | - 通过监控系统和日志来发现并发安全问题,并及时处理。 14 | - 在应用程序层面实现并发控制,如使用分布式锁。 15 | 16 | 总的来说,尽管Redis主要是单线程的,但在高并发环境下仍需注意并发安全问题,以确保数据操作的正确性和一致性。通过合理设计应用架构和采取相应的解决方案,可以有效避免并发安全问题的发生。 -------------------------------------------------------------------------------- /problems/Redis中Stream的原理和使用场景.md: -------------------------------------------------------------------------------- 1 | Redis中的Stream是一种数据结构,用于将消息顺序存储在Redis中,并支持不断添加新的消息、消费消息和实时流处理。Stream基于日志结构,具有高性能和高可靠性。 2 | 3 | Stream的原理是将消息按照时间顺序存储在一个类似列表的结构中,每个消息包含一个唯一的ID和消息内容。消费者可以从Stream中读取消息,并根据ID进行去重,实现消息消费进度的追踪。 4 | 5 | 使用场景: 6 | 7 | 1. 消息队列:Stream可以用作轻量级的消息队列,支持发布-订阅模式和多播功能,适合处理实时数据流。 8 | 2. 日志引擎:Stream可用于记录事件日志或应用程序日志,便于实时监控和故障排查。 9 | 3. 实时数据处理:Stream支持消费者组,可以实现分布式实时数据处理,比如流式计算、事件驱动架构等。 10 | 4. 定时任务调度:通过Stream可以实现定时任务的调度和分发,保证任务执行的顺序和一致性。 11 | 12 | 总的来说,Redis中的Stream提供了一种高效的消息处理机制,适用于需要实时数据处理和事件驱动的场景,能够帮助开发者构建高性能、可扩展的实时应用系统。 -------------------------------------------------------------------------------- /problems/Redis中数据(键值对)是怎么存储的.md: -------------------------------------------------------------------------------- 1 | Redis中的数据(键值对)是以内存为主存储方式,持久化存储则可以选择RDB快照或者AOF日志等方式。在内存中,Redis使用哈希表来存储键值对数据,这使得Redis能够快速地根据键名查找到对应的值。 2 | 3 | 具体来说,Redis使用一个全局哈希表(dict)来存储所有的键值对数据。每个键值对被存储在哈希表的一个桶(bucket)中,桶中包含了键、值和其它元数据信息。当需要查找或操作某个键值对时,Redis会首先计算出键的哈希值,然后根据哈希值定位到对应的桶,最终找到目标键值对。 4 | 5 | 此外,Redis还支持多种数据类型,如字符串、列表、集合、有序集合、哈希等,不同数据类型在底层存储结构上有所区别,但大部分数据类型也是基于哈希表实现的。 6 | 7 | 总的来说,Redis中的数据键值对是通过哈希表来存储的,这种存储方式使得Redis能够高效地进行数据访问和操作,同时支持丰富的数据类型和功能特性。 -------------------------------------------------------------------------------- /problems/Redis为什么这么快.md: -------------------------------------------------------------------------------- 1 | 1. **基于内存存储**:Redis将数据存储在内存中,相比于传统数据库需要从磁盘读写数据,内存访问速度更快,能够大大提高读写性能。 2 | 2. **非阻塞IO**:Redis使用了多路复用技术,采用非阻塞IO模型,能够同时处理多个连接请求,充分利用系统资源,提高并发处理能力。 3 | 3. **单线程模型**:虽然Redis是单线程的,但它通过事件驱动、异步非阻塞的方式处理请求,减少了线程切换和同步开销,降低了整体的耗时。 4 | 4. **精简的数据结构**:Redis使用了简洁高效的数据结构,如字符串、哈希表、列表等,这些数据结构底层实现经过优化,能够快速执行各种操作。 5 | 5. **数据分区和持久化**:Redis支持数据分区和持久化机制,可以水平扩展并保证数据不丢失,提高了系统的可靠性和性能。 6 | 6. **内置命令优化**:Redis内置了丰富的命令集合,并对常用命令进行了优化,保证了命令的执行效率。 7 | 7. **高效的网络通信**:Redis使用自定义的协议与客户端通信,协议简单轻量,减少了通信开销,加快了数据传输速度。 -------------------------------------------------------------------------------- /problems/Redis主从复制、哨兵、集群的区别.md: -------------------------------------------------------------------------------- 1 | 1. 主从复制:Redis主从复制是指将一个Redis实例(主节点)的数据复制到多个从节点的过程。主节点负责写操作和读操作的处理,从节点负责接收主节点发送过来的数据并进行复制,从而实现数据备份和读取负载均衡。主从复制提供了数据冗余和故障恢复功能。 2 | 2. 哨兵:Redis Sentinel是一种监控系统,用于监视Redis主从复制架构中的各个节点,并在主节点失效时自动将其中一个从节点升级为新的主节点,以保证系统的高可用性。哨兵系统保持对所有节点的监控,并在需要时执行自动故障转移,确保系统稳定运行。 3 | 3. 集群:Redis集群是一种分布式部署方式,通过将数据分片存储在不同的节点上,实现了数据水平扩展,提高了系统的性能和容量。Redis集群采用哈希槽分配数据的方式,每个节点负责管理一部分哈希槽,可以动态扩展和收缩节点。集群还提供了数据复制和故障转移等功能,确保数据的可靠性和高可用性。 -------------------------------------------------------------------------------- /problems/Redis内存淘汰策略.md: -------------------------------------------------------------------------------- 1 | Redis的内存淘汰策略用于在内存达到设定阈值时,选择哪些键需要被删除以释放空间。常见的Redis内存淘汰策略包括: 2 | 3 | 1. **LRU(Least Recently Used,最近最少使用)**:根据键的最近使用时间来淘汰数据,优先删除最长时间未被访问的键。 4 | 2. **LFU(Least Frequently Used,最不经常使用)**:根据键被访问的频率来淘汰数据,优先删除访问频率最低的键。 5 | 3. **TTL(Time To Live,生存时间)**:根据键的过期时间来淘汰数据,优先删除即将过期的键。 6 | 4. **Random(随机淘汰)**:随机选择要删除的键,这种策略相对简单但可能导致不均匀的内存占用。 7 | 5. **MaxMemoryPolicy(最大内存策略)**:根据配置的内存使用策略来淘汰数据,如noeviction(拒绝写入新键)、allkeys-lfu、allkeys-lru、volatile-lru等。 8 | 9 | 根据具体的业务需求和系统性能要求,可以选择适合的内存淘汰策略。一般情况下,LRU是比较常用的策略,因为它能够比较好地利用历史数据访问模式来决定淘汰哪些数据,而LFU则更加关注数据访问频率。此外,TTL策略适合于需要根据键的过期时间来淘汰数据的场景,而Random策略则相对简单且不依赖历史数据。 10 | 11 | 总的来说,选择合适的内存淘汰策略有助于提高Redis的性能和稳定性,确保系统在内存达到限制时能够有效地释放空间。 -------------------------------------------------------------------------------- /problems/Redis单线程在多核机器里使用会不会浪费机器资源.md: -------------------------------------------------------------------------------- 1 | Redis的单线程架构在多核机器上运行可能会浪费机器资源,因为单个Redis实例只能利用一个CPU核心。这是因为Redis采用了事件驱动的模型,通过一个主事件循环来处理所有的请求和操作,使得其本身无法利用多核CPU的并行计算能力。 2 | 3 | 然而,即便在多核机器上运行单个Redis实例可能会浪费部分机器资源,但可以通过以下几种方式来充分利用多核机器的资源,以提高整体性能: 4 | 5 | 1. **多实例部署**:可以在同一台机器上启动多个Redis实例,每个实例绑定到不同的CPU核心上运行,从而充分利用多核CPU的计算能力。这样可以提高整体吞吐量和并发性能。 6 | 2. **集群部署**:Redis提供了集群模式(Redis Cluster),可以横向扩展到多个节点,每个节点可以运行在不同的机器上,以实现更好的负载均衡和高可用性。 7 | 3. **使用多线程实验功能**:在Redis 6.0版本之后引入了多线程支持(experimental),可以试验性地启用多线程功能来利用多核CPU的优势。虽然目前还处于实验阶段,但可以尝试在适当的场景下使用多线程功能来提高性能。 8 | 4. **配合其他技术**:可以结合其他技术如代理服务器、缓存服务等,将部分计算或IO密集型任务外移,以减轻Redis单线程的压力。 9 | 10 | 总的来说,在多核机器上运行单个Redis实例可能会浪费部分机器资源,但可以通过合理的架构设计和配置来充分利用多核CPU的性能优势,以提高系统的整体性能和可扩展性。 -------------------------------------------------------------------------------- /problems/Redis和Memcached的区别.md: -------------------------------------------------------------------------------- 1 | Redis和Memcached是两种常见的内存缓存系统,它们在功能、性能和应用场景上有一些不同之处。以下是Redis和Memcached的主要区别: 2 | 3 | 1. **数据结构支持**: 4 | - Redis支持更丰富的数据结构(如字符串、哈希、列表、集合、有序集合等),可以进行更复杂的数据操作和计算。 5 | - Memcached只支持简单的key-value数据结构,适合存储简单的数值或文本数据。 6 | 2. **持久化**: 7 | - Redis支持持久化机制,可以将数据存储到磁盘中,保证数据不会因服务重启而丢失。同时也支持RDB快照和AOF日志等方式。 8 | - Memcached不支持持久化,数据仅存在于内存中,服务重启后数据会全部丢失。 9 | 3. **数据淘汰策略**: 10 | - Redis支持多种数据淘汰策略,如LRU(最近最少使用)、TTL(过期时间)等,可以根据需求自定义数据淘汰规则。 11 | - Memcached采用LRU淘汰策略,当内存不足时会淘汰访问时间最早的数据。 12 | 4. **分布式支持**: 13 | - Redis可以通过集群模式实现分布式缓存,支持数据分片和故障转移,提高了可扩展性和容错性。 14 | - Memcached天生就是分布式的,可以通过添加新的节点来扩展缓存容量,但没有内置的分片和故障转移机制。 15 | 5. **数据复杂度**: 16 | - Redis适用于需要复杂数据类型和数据结构的场景,如存储对象、列表、计数器等。 17 | - Memcached适用于简单的键值对存储,对于复杂数据结构的处理能力较弱。 18 | 6. **性能比较**: 19 | - 在读取和写入大量数据时,Memcached通常比Redis性能略好,因为它专注于快速的缓存读写操作。 20 | - 对于复杂数据结构和计算密集型任务,Redis通常表现更优秀,因为其丰富的数据结构和功能。 -------------------------------------------------------------------------------- /problems/Redis如何判断键是否过期?过期键的删除策略有哪些.md: -------------------------------------------------------------------------------- 1 | Redis通过定时任务来主动检查键是否过期。Redis会在每个键的设定过期时间时,同时创建一个定时器(timer)以监视键的过期情况。当键到达过期时间时,Redis会根据不同的删除策略来处理过期键的清理工作。 2 | 3 | 常见的过期键删除策略包括: 4 | 5 | 1. 惰性删除(Lazy Expiration):在访问某个过期键时,Redis会先检查该键是否过期,如果过期则立即删除。这种方式会导致过期键在过期后仍保留一段时间,直到被访问到时才被清理。 6 | 2. 定期删除(Eviction Policy):Redis会定期地(通常是100毫秒)随机抽取一些过期键进行检查和删除。这种策略能够在一定程度上保持系统性能稳定,但不能保证过期键被及时清理。 7 | 3. 定时删除(Expiration Check):Redis会在每次执行命令时,随机抽取一些过期键进行检查和删除。这种策略相比于定期删除更加灵活,可以根据实际负载情况动态调整清理频率。 8 | 4. 内存淘汰策略:当内存占用达到一定阈值时,Redis会根据配置的内存淘汰策略(如LRU、LFU等)来选择要删除的键。这种策略虽然不是专门针对过期键的删除,但也能帮助清理过期键以释放内存空间。 9 | 10 | 总的来说,Redis通过定时任务和不同的过期键删除策略来管理过期键的清理工作,确保系统能够及时有效地清理过期数据,保持系统性能和内存稳定。 -------------------------------------------------------------------------------- /problems/Redis数据类型(对象)有哪些.md: -------------------------------------------------------------------------------- 1 | Redis中有五种主要的数据类型(对象),分别是: 2 | 3 | 1. String(字符串):最简单的数据类型,可以存储文本、整数或二进制数据。常见的操作包括设置和获取值、追加、自增、自减等。 4 | 2. List(列表):有序的字符串列表,支持从两端进行插入、删除操作,提供了丰富的列表操作命令,如插入元素、弹出元素、范围查询等。 5 | 3. Set(集合):无序的字符串集合,不允许重复元素的存在,常用于存储唯一值,并提供了集合运算(交集、并集、差集)等功能。 6 | 4. Hash(哈希):键值对的集合,适用于存储对象属性等结构化数据,常用于存储用户信息、配置信息等。 7 | 5. Sorted Set(有序集合):类似于集合,但每个元素都关联一个分数(score),根据分数进行排序。常用于实现排行榜、计分板等需求。 8 | 9 | 除了上述五种主要的数据类型外,Redis还支持一些其他数据类型,如: 10 | 11 | - HyperLogLog:用于基数统计的数据结构,可以估算集合中不同元素的数量。 12 | - GeoSpatial:用于存储地理位置信息的数据结构,支持空间索引和相关的地理位置查询操作。 13 | - Bitmaps:位图数据结构,适用于高效存储位操作相关的信息,如用户在线状态、用户签到记录等。 -------------------------------------------------------------------------------- /problems/Redis是单线程还是多线程?Redis6.0之后为何又引入了多线程.md: -------------------------------------------------------------------------------- 1 | 在Redis的设计中,它是单线程的,即主要处理请求的是一个事件循环。这个单线程模型使得Redis能够避免了多线程带来的复杂性和线程安全等问题,同时也更容易实现高性能和低延迟。 2 | 3 | 然而,在Redis 6.0之后,引入了多线程支持(experimental)的功能,主要是为了利用多核处理器的优势,提高Redis在某些场景下的并发处理能力。引入多线程的主要原因包括: 4 | 5 | 1. **利用多核CPU**:随着硬件的发展,多核CPU已经成为主流,但传统的单线程模型无法充分利用多核CPU的性能优势。引入多线程可以让Redis在多核CPU上进行并行处理,提高性能和扩展性。 6 | 2. **I/O密集型操作**:当Redis面临大量的I/O密集型操作时,单线程可能会成为瓶颈,引入多线程可以更好地处理I/O并发操作,提高吞吐量。 7 | 3. **降低阻塞风险**:在特定情况下,例如进行长时间的计算或阻塞调用,单线程可能导致整个系统出现阻塞。引入多线程可以通过将部分任务放到其他线程中执行,减少阻塞风险。 8 | 9 | 需要注意的是,多线程功能目前仍处于实验阶段,可能存在一些限制和稳定性问题。在使用多线程功能时,需要慎重考虑业务需求和系统的可靠性,确保能够正确配置和管理多线程环境,以便获得更好的性能提升。 10 | 11 | 综上所述,Redis在6.0版本引入多线程支持主要是为了提高在某些场景下的性能和并发处理能力,以适应不同的应用需求。 -------------------------------------------------------------------------------- /problems/Redis有什么作用?为什么要用 Redis.md: -------------------------------------------------------------------------------- 1 | Redis是一个开源的内存数据库,它主要用于缓存、消息队列、会话管理等功能。Redis具有以下几个作用和优点: 2 | 3 | 1. 快速:Redis将数据存储在内存中,因此读写速度非常快,适合对响应时间要求较高的场景。 4 | 2. 支持丰富的数据结构:除了简单的字符串外,Redis还支持列表、集合、有序集合、哈希等复杂数据结构,使其适用于各种场景。 5 | 3. 持久化:Redis支持数据的持久化,可以定期将内存中的数据保存到磁盘,保证数据不丢失。 6 | 4. 高并发:Redis支持多个客户端同时访问,并且提供了原子性操作,保证数据的一致性。 7 | 5. 分布式:Redis支持分布式部署,可以通过主从复制、集群等方式实现水平扩展。 -------------------------------------------------------------------------------- /problems/Redis的BigKey问题及其解决方案.md: -------------------------------------------------------------------------------- 1 | Redis的BigKey问题是指在Redis中存在占用大量内存空间的key,会导致Redis内存占用过高、影响性能甚至引发Redis服务宕机等问题。以下是一些解决Redis BigKey问题的方法: 2 | 3 | 1. **监控和排查**:定期监控Redis内存使用情况,通过Redis命令`memory usage`可以查看各个key的内存占用情况,及时发现潜在的BigKey。 4 | 2. **拆分大key**:对于已经存在的BigKey,可以考虑将其拆分成多个小key,根据业务需求来设计合适的数据结构,将大key的数据分散存储在多个小key中,减少单个key的内存占用。 5 | 3. **使用Hash数据结构**:对于存储大量字段的数据,可以使用Redis的Hash数据结构来代替单个key,将大key拆分为多个field存储在Hash中,降低内存占用。 6 | 4. **压缩数据**:对于存储文本类型数据的key,可以考虑对数据进行压缩(如GZIP压缩),减少内存占用。需要注意的是,在每次读写操作时都需要进行解压缩,可能会增加CPU负载。 7 | 5. **设置过期时间**:对于临时性数据或者不常访问的大key,可以设置过期时间,当数据过期后自动释放内存,避免长时间占用内存。 8 | 6. **持久化数据**:将不常访问的大key数据持久化到磁盘,比如使用Redis的RDB快照或者AOF持久化功能,将数据从内存释放出来,减轻内存压力。 9 | 7. **限制数据大小**:在应用层面限制数据写入的大小,避免写入过大的数据导致BigKey问题,可以通过配置Redis参数`hash-max-ziplist-value`和`hash-max-ziplist-entries`等来限制Hash数据结构的大小。 -------------------------------------------------------------------------------- /problems/Redis的Bitmap的原理和使用场景.md: -------------------------------------------------------------------------------- 1 | Redis的Bitmap(位图)是一种数据结构,用来存储大量二进制位数据。每个位可以表示一种状态,通常用于标记某些事件或属性的存在与否。 2 | 3 | Bitmap的原理是通过使用位操作来实现对位的设置、清除和查询。在Redis中,可以使用字符串来表示位图,一个字节可以存储8个位,通过位运算可以方便地对位进行操作。 4 | 5 | 使用场景: 6 | 7 | 1. 状态标记:Bitmap可用于标记某个事件或属性的状态,如用户签到、在线状态等,每个位代表一个状态。 8 | 2. 去重统计:Bitmap可以用于去重统计,比如记录用户的点击行为,避免重复计数。 9 | 3. 计算交集、并集、差集:通过位操作,可以计算不同Bitmap之间的交集、并集、差集,实现高效的集合运算。 10 | 4. 压缩存储:当需要存储大量的布尔值信息时,Bitmap可以节省空间,因为它是基于位存储的,非常紧凑。 11 | 12 | 总的来说,Bitmap适用于需要高效存储和处理大量二进制数据的场景,能够在节省空间的同时提供快速的位操作功能。 -------------------------------------------------------------------------------- /problems/Redis的HyperLogLog的原理和使用场景.md: -------------------------------------------------------------------------------- 1 | Redis的HyperLogLog(简称HLL)是一种基数估计算法,用于估计一个集合中不重复元素的数量。它通过使用较小的固定大小的空间来存储大型数据集的不重复元素数量,具有较低的内存消耗。 2 | 3 | HyperLogLog的原理主要是利用概率统计的方法,在牺牲一定精确度的前提下,使用较小的空间来估算大数据集的基数。具体实现是通过在Redis中存储一个位数组,对输入的元素进行哈希映射,然后根据哈希值的前导零位数来估算基数。通过对多个哈希值进行统计和处理,可以准确地估计出大数据集的基数。 4 | 5 | 使用场景: 6 | 7 | 1. 统计UV(Unique Visitors):在网站分析中,统计独立访客数是常见需求,HyperLogLog可以用于快速估计独立访客数而无需存储每个访客的详细信息。 8 | 2. 数据去重:当需要对大量数据进行去重操作时,HyperLogLog可以帮助快速计算数据集中的不重复元素数量。 9 | 3. 实时计数:适用于需要实时统计不同元素个数的场景,如实时监控、实时日志分析等。 10 | 11 | 总的来说,HyperLogLog适用于需要估计大数据集基数的场景,能够在节省内存空间的同时提供相对准确的估算结果。 -------------------------------------------------------------------------------- /problems/Redis的ke设定24h过期时间,那么24h后就一定会过期吗.md: -------------------------------------------------------------------------------- 1 | 在Redis中,设置键的过期时间并不是一定会在指定时间点立即过期的,而是由Redis的过期键删除策略来决定何时清理过期键。因此,即使你设置某个键为24小时后过期,也不能保证在24小时后一定会立即被删除。 2 | 3 | Redis的过期键删除策略包括惰性删除、定期删除和定时删除等方式。在这些策略中,Redis会根据具体的运行情况和负载来判断何时清理过期键,可能会有一定的延迟。 4 | 5 | 另外,在Redis的处理机制中,并非所有的操作都会立即检查是否有过期键需要清理,有时候需要等到某个操作触发或者达到一定条件才会执行过期键的清理工作。因此,即使设置了24小时的过期时间,也可能存在一定的误差。 6 | 7 | 总的来说,尽管设置了24小时的过期时间,但不能保证在24小时后一定会立即过期。Redis的过期键删除是基于一定的策略和条件进行的,可能会有一定的延迟或误差。 -------------------------------------------------------------------------------- /problems/Redis的两种持久化方式以及优缺点.md: -------------------------------------------------------------------------------- 1 | Redis支持两种主要的持久化方式:RDB(Redis DataBase)和AOF(Append Only File)。以下是它们的优缺点: 2 | 3 | 1. RDB持久化: 4 | - 优点: 5 | - RDB快速且高效,适合用于备份数据和全量恢复。 6 | - RDB生成的快照文件较小,节省存储空间。 7 | - 当需要对数据库进行频繁备份时,RDB持久化可以减少对系统性能的影响。 8 | - 缺点: 9 | - RDB是定期将内存中的数据集快照保存到磁盘,如果发生故障,可能会造成部分数据丢失。 10 | - RDB生成的快照只包含最后一次持久化时的数据,可能会导致数据丢失。 11 | 2. AOF持久化: 12 | - 优点: 13 | - AOF记录了每个写操作的日志,通过重新执行这些写操作可以完全恢复数据,保证数据的完整性。 14 | - AOF持久化模式下的数据更加安全,可以最大程度地避免数据丢失。 15 | - AOF文件是一个可追加的日志文件,可以不断追加操作记录,保证数据的持久性。 16 | - 缺点: 17 | - AOF持久化相比RDB持久化占用更多磁盘空间,且恢复速度相对慢。 18 | - AOF文件可能会越来越大,需要定期进行压缩或重写以避免过大的AOF文件。 19 | - AOF持久化对硬盘有一定的写入压力,可能会影响系统的性能。 20 | 21 | 综上所述,RDB持久化适合用于备份数据和全量恢复,对于要求快速且高效的情况较为适用;而AOF持久化适合对数据完整性要求较高的场景,能够最大程度地避免数据丢失。根据具体需求和对数据一致性的要求,可以选择适合的持久化方式或结合两种方式进行配置。 -------------------------------------------------------------------------------- /problems/Redis的底层数据结构有哪些.md: -------------------------------------------------------------------------------- 1 | Redis的底层数据结构主要包括以下几种: 2 | 3 | 1. 字符串(string):最简单的数据结构,可以存储字符串、整数或者浮点数。 4 | 2. 列表(list):一个双向链表,可以存储有序的字符串元素。 5 | 3. 集合(set):一个无序集合,其中的每个元素都是唯一的。 6 | 4. 有序集合(sorted set):类似于集合,但每个元素都会关联一个分数,根据分数可以进行排序。 7 | 5. 哈希表(hash):类似于关联数组,可以存储键值对。 -------------------------------------------------------------------------------- /problems/Redis缓存击穿问题及其解决方案.md: -------------------------------------------------------------------------------- 1 | Redis缓存击穿是指在高并发情况下,一个不存在的key被大量请求同时访问,导致这些请求绕过缓存直接查询数据库,从而造成数据库压力剧增。以下是一些解决Redis缓存击穿问题的方法: 2 | 3 | 1. **设置热点数据预加载**:在系统启动或者运行期间,提前将热点数据加载到缓存中,即使某个key暂时失效也能保证缓存命中率。通过定期刷新热点数据,确保缓存中始终存在热门数据。 4 | 2. **使用互斥锁**:在查询缓存之前,可以使用分布式锁或者互斥锁来控制对数据库的访问,避免多个线程同时去查询数据库。只有一个线程去查询数据库,其他线程等待其返回结果后再从缓存获取数据。 5 | 3. **缓存穿透处理**:当缓存查询不到数据时,可以考虑返回默认值、空值或者错误提示作为缓存对象,避免频繁查询数据库。同时,可以设置较短的过期时间,以便尽快从数据库获取最新数据更新缓存。 6 | 4. **熔断策略**:实施熔断机制来保护后端服务,当大量请求同时访问数据库时,可以暂时关闭数据库访问,返回错误信息或者降级处理,防止数据库崩溃。 7 | 5. **使用一致性哈希算法**:通过一致性哈希算法将请求均匀地分布到不同的节点上,避免某个节点集中承载大量请求,减少单点压力。 8 | 6. **提升缓存容量和性能**:根据业务需求和流量情况,合理调整Redis的内存容量和性能配置,确保能够承载更高的并发请求,提高缓存命中率。 -------------------------------------------------------------------------------- /problems/Redis缓存穿透问题及其解决方案.md: -------------------------------------------------------------------------------- 1 | 1. **布隆过滤器**:使用布隆过滤器在缓存层进行数据预先过滤,将可能存在的数据放入布隆过滤器中。当请求到来时,先根据布隆过滤器判断是否存在于缓存中,避免直接查询数据库。 2 | 2. **空值缓存**:当查询结果为空时,也将该空结果进行缓存,设置较短的过期时间。这样下次再有相同的查询请求时,就可以从缓存中获取空结果,减少对数据库的频繁查询。 3 | 3. **缓存雪崩处理**:设置不同的过期时间或者使用分布式锁等机制,避免大量缓存同时失效导致的缓存雪崩问题,提高系统的稳定性。 4 | 4. **热点数据预热**:在系统启动或者运行期间,提前加载热门数据到缓存中,避免冷启动时出现缓存穿透的情况。 5 | 5. **限流控制**:对请求进行频率限制,通过限流算法(如令牌桶、漏桶等)控制请求的访问频率,防止恶意请求导致缓存穿透。 6 | 6. **使用缓存标记:在缓存中存储数据是否存在的标记,如果查询结果为空,也将此标记缓存起来,下次查询时先检查标记,若不存在则直接返回空结果。 -------------------------------------------------------------------------------- /problems/Redis缓存雪崩问题及其解决方案.md: -------------------------------------------------------------------------------- 1 | Redis缓存雪崩是指在某个时间点,大量缓存同时失效或者被清除,导致大量请求直接访问数据库或者后端服务,造成系统压力剧增,甚至导致系统瘫痪。以下是一些解决Redis缓存雪崩问题的方法: 2 | 3 | 1. **缓存数据过期时间随机性**:设置缓存数据的过期时间时,可以在原有过期时间基础上加上一个随机值,避免大量缓存同时失效。比如对于同一种类型的缓存数据,可以设置一个范围在原定过期时间上波动的随机值。 4 | 2. **二级缓存策略**:使用多级缓存架构,例如引入本地缓存(比如内存)作为一级缓存,在Redis之前缓存一层,当Redis中的缓存发生雪崩时,本地缓存可以作为备用,减轻数据库负载。 5 | 3. **缓存数据自动刷新**:在缓存数据即将过期时,异步或者定时任务重新加载缓存数据,保证缓存数据的稳定性。这样可以避免大规模同时失效,减少缓存雪崩的可能性。 6 | 4. **限流控制和熔断降级**:在缓存雪崩发生时,实施限流控制,通过限制请求的并发数量或者调整查询频率等方式来平滑处理请求,避免一次性压垮数据库。同时,可以考虑实施熔断降级策略,暂时关闭部分功能或者返回缓存响应较慢的提示信息,降低系统的压力。 7 | 5. **监控报警系统**:建立完善的监控系统,实时监测缓存状态和请求量,设置预警规则,及时发现异常情况并采取相应的应对措施,防止缓存雪崩的发生。 -------------------------------------------------------------------------------- /problems/STL中一般都有那些常见的算法库呢?.md: -------------------------------------------------------------------------------- 1 | 一般常见的STL算法库包括: 2 | 3 | 1. 非修改性序列操作算法:如std::for_each, std::count, std::find, std::binary_search等。 4 | 2. 修改性序列操作算法:如std::sort, std::reverse, std::swap, std::rotate等。 5 | 3. 排列组合算法:如std::next_permutation, std::prev_permutation, std::merge等。 6 | 4. 数值操作算法:如std::accumulate, std::inner_product, std::partial_sum等。 7 | 5. 堆操作算法:如std::make_heap, std::push_heap, std::pop_heap等。 8 | 6. 划分操作算法:如std::partition, std::stable_partition等。 -------------------------------------------------------------------------------- /problems/STL中的优先级队列是如何实现的?.md: -------------------------------------------------------------------------------- 1 | 在 C++ STL 中,`std::priority_queue` 默认情况下使用 `std::vector` 作为其底层容器,并且使用 `std::make_heap`、`std::push_heap` 和 `std::pop_heap` 算法来维护堆的性质。 2 | 3 | `std::priority_queue` 允许用户指定一个比较函数对象,这个比较对象定义了元素的优先级。默认情况下,它使用 `std::less`,这意味着队列使用最大堆,最大的元素总 -------------------------------------------------------------------------------- /problems/STL中,map的底层是如何实现的?.md: -------------------------------------------------------------------------------- 1 | 在C++标准模板库(STL)中,`map` 是一种关联容器,它以键值对的方式存储元素,其中每个键都是唯一的。底层实现通常使用红黑树(Red-Black Tree),这是一种自平衡的二叉搜索树。 2 | 3 | 红黑树保持了树的平衡性,即从根到所有叶子节点的最长路径不会超过最短路径的两倍。这种性质确保了`map`中的操作(如插入、删除和查找)可以在对数时间复杂度O(log n)内完成。 4 | 5 | 红黑树有以下特性: 6 | 7 | 1. 每个节点要么是红色,要么是黑色。 8 | 2. 根节点是黑色。 9 | 3. 所有叶子节点(NIL节点,空节点)都是黑色。 10 | 4. 每个红色节点必须有两个黑色的子节点(不能有两个连续的红色节点)。 11 | 5. 从任一节点到其每个叶子的所有简单路径都包含相同数目的黑色节点。 12 | 13 | 因为`map`的底层是红黑树这种高度平衡的数据结构,所以它能够提供良好的性能保证,使得即使在大量元素存储的情况下也能保持效率。 14 | 15 | 需要注意的是,在C++11后,还引入了`unordered_map`,它使用哈希表作为底层实现,提供平均常数时间复杂度O(1)的访问性能,但它不保证元素的顺序,并且在最坏情况下可能退化为线性时间复杂度O(n)。 -------------------------------------------------------------------------------- /problems/STL中,set的底层是如何实现的?.md: -------------------------------------------------------------------------------- 1 | 在C++标准模板库(STL)中,`set` 是基于关联容器的一个抽象数据类型,用于存储不重复的元素。与 `map` 类似,`set` 的底层实现也通常采用红黑树(一种自平衡的二叉搜索树)。这使得 `set` 中的大多数操作(例如插入、删除和搜索)都能以对数时间复杂度 O(log n) 来执行,其中 n 是集合中元素的数量。 2 | 3 | 使用红黑树作为底层数据结构,`set` 可以保证元素会按照特定的顺序排序,通常是按照键值的递增顺序。红黑树确保了任何时候树都是相对平衡的,所以 `set` 容器在处理大量动态插入和删除操作时依然能够提供良好的性能。 4 | 5 | 除了 `set`,STL 还提供了 `unordered_set` 容器,其底层实现是基于哈希表。`unordered_set` 不保证元素的有序性,但在理想情况下可以提供更快的平均时间复杂度 O(1) 的访问性能。不过,在最坏的情况下(例如,当哈希函数导致很多碰撞时),它的性能可能会退化到 O(n)。 -------------------------------------------------------------------------------- /problems/STL原理及实现.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | STL提供六大组件,彼此可以组合套用: 4 | 5 | 1、容器(Containers):各种数据结构,如:序列式容器vector、list、deque、关联式容器set、map、multiset、multimap。用来存放数据。从实现的角度来看,STL容器是一种class template。 6 | 7 | 2、算法(algorithms):各种常用算法,如:sort、search、copy、erase。从实现的角度来看,STL算法是一种 function template。注意一个问题:任何的一个STL算法,都需要获得由一对迭代器所标示的区间,用来表示操作范围。这一对迭代器所标示的区间都是前闭后开区间,例如[first, last) 8 | 9 | 3、迭代器(iterators):容器与算法之间的胶合剂,是所谓的“泛型指针”。共有五种类型,以及其他衍生变化。从实现的角度来看,迭代器是一种将 operator * 、operator->、operator++、operator- - 等指针相关操作进行重载的class template。所有STL容器都有自己专属的迭代器,只有容器本身才知道如何遍历自己的元素。原生指针(native pointer)也是一种迭代器。 10 | 11 | 4、仿函数(functors):行为类似函数,可作为算法的某种策略(policy)。从实现的角度来看,仿函数是一种重载了operator()的class或class template。一般的函数指针也可视为狭义的仿函数。 12 | 13 | 5、配接器(adapters):一种用来修饰容器、仿函数、迭代器接口的东西。例如:STL提供的queue 和 stack,虽然看似容器,但其实只能算是一种容器配接器,因为它们的底部完全借助deque,所有操作都由底层的deque供应。改变 functors接口者,称为function adapter;改变 container 接口者,称为container adapter;改变iterator接口者,称为iterator adapter。 14 | 15 | 6、配置器(allocators):负责空间配置与管理。从实现的角度来看,配置器是一个实现了动态空间配置、空间管理、空间释放的class template。 16 | 17 | 这六大组件的交互关系:container(容器) 通过 allocator(配置器) 取得数据储存空间,algorithm(算法)通过 iterator(迭代器)存取 container(容器) 内容,functor(仿函数) 可以协助 algorithm(算法) 完成不同的策略变化,adapter(配接器) 可以修饰或套接 functor(仿函数) 18 | 19 | ## 序列式容器: 20 | 21 | vector-数组,元素不够时再重新分配内存,拷贝原来数组的元素到新分配的数组中。 22 | list-单链表。 23 | deque-分配中央控制器map(并非map容器),map记录着一系列的固定长度的数组的地址.记住这个map仅仅保存的是数组的地址,真正的数据在数组中存放着.deque先从map中央的位置(因为双向队列,前后都可以插入元素)找到一个数组地址,向该数组中放入数据,数组不够时继续在map中找空闲的数组来存数据。当map也不够时重新分配内存当作新的map,把原来map中的内容copy的新map中。所以使用deque的复杂度要大于vector,尽量使用vector。 24 | 25 | stack-基于deque。 26 | queue-基于deque。 27 | heap-完全二叉树,使用最大堆排序,以数组(vector)的形式存放。 28 | priority_queue-基于heap。 29 | slist-双向链表。 30 | 31 | ## 关联式容器 32 | 33 | set,map,multiset,multimap-基于红黑树(RB-tree),一种加上了额外平衡条件的二叉搜索树。 34 | 35 | hash table-散列表。将待存数据的key经过映射函数变成一个数组(一般是vector)的索引,例如:数据的key%数组的大小=数组的索引(一般文本通过算法也可以转换为数字),然后将数据当作此索引的数组元素。有些数据的key经过算法的转换可能是同一个数组的索引值(碰撞问题,可以用线性探测,二次探测来解决),STL是用开链的方法来解决的,每一个数组的元素维护一个list,他把相同索引值的数据存入一个list,这样当list比较短时执行删除,插入,搜索等算法比较快。 36 | 37 | hash_map,hash_set,hash_multiset,hash_multimap-基于hashtable。 38 | 39 | 什么是“标准非STL容器”? 40 | 41 | ## list和vector有什么区别? 42 | 43 | vector拥有一段连续的内存空间,因此支持随机存取,如果需要高效的随即存取,而不在乎插入和删除的效率,使用vector。 44 | list拥有一段不连续的内存空间,因此不支持随机存取,如果需要大量的插入和删除,而不关心随即存取,则应使用list。 45 | 46 | -------------------------------------------------------------------------------- /problems/STL容器是线程安全的吗?.md: -------------------------------------------------------------------------------- 1 | C++标准库(STL)中的容器本身不是线程安全的。这意味着在没有采取外部同步措施的情况下,如果有多个线程同时对同一个容器实例进行写操作,或者同时有一个线程在写操作和另一个线程在读操作,那么这可能会导致数据竞争和未定义行为。 2 | 3 | 因此,当多个线程需要访问相同的容器时,就需要通过其他方式来确保线程安全。常用的同步机制包括: 4 | 5 | 1. **互斥锁(Mutexes)**:使用互斥锁来同步对容器的访问。例如,可以在每次操作容器之前加锁,操作完毕后解锁。 6 | 2. **读写锁(Reader-Writer Locks)**:如果你的应用程序涉及到更多的读操作而较少的写操作,可以使用读写锁来允许多个读取者同时访问容器,而写入者则需要独占访问权限。 7 | 3. **原子操作**:对于简单的操作,如对单个元素的更新,可以考虑使用原子类型 `std::atomic`。 8 | 4. **并发容器**:某些场景下可以使用专为并发设计的容器,如 `boost` 库提供的一些线程安全版本的容器,或者 `tbb::concurrent_vector` 等。 9 | 5. **细粒度锁或无锁编程技术**:在高级应用程序中,可能会使用更复杂的策略,比如分段锁或无锁数据结构,以减小锁的粒度或避免锁的开销。 -------------------------------------------------------------------------------- /problems/SYN队列和Accept队列.md: -------------------------------------------------------------------------------- 1 | ### SYN 队列和 Accept 队列 2 | 3 | (1)SYN 半链接队列 4 | 5 | SYN队列存储了收到 SYN 包的连接,它的职责是回复 SYN+ACK 包,并且在没有收到 ACK 包时重传。发送完SYN+ACK之后,SYN 队列等待从客户端发出的ACK包(也即三次握手的最后一个包)。 6 | 7 | 当收到ACK包时,首先找到对应的SYN队列,再在对应的SYN队列中检查相关的数据看是否匹配,如果匹配,内核将该连接相关的数据从SYN队列中移除,创建一个完整的连接,并将这个连接加入Accept队列。 8 | 9 | (2)Accept 全连接队列 10 | 11 | (2)Accept(全连接) 12 | 13 | Accept队列中存放的是已建立好的连接,也即等待被上层应用程序取走的连接。当进程调用accept(),这个socket从队列中取出,传递给上层应用程序。 -------------------------------------------------------------------------------- /problems/SYN队列溢出了怎么办.md: -------------------------------------------------------------------------------- 1 | 查看 SYN 队列 2 | 3 | 就是查看处在 SYN_RECV 状态的进程连接个数 4 | 5 | ``` 6 | netstat -natp | grep SYN_RECV | wc -l 7 | ``` 8 | 9 | 查看溢出情况 10 | 11 | ``` 12 | netstat -s 13 | ``` 14 | 15 | 预防 SYN 攻击 16 | 17 | - 增大半连接队列 18 | 19 | - 开启 SYN cookies 算法 20 | 21 | - 减少 SYN+ACK 重传次数 22 | 23 | 当服务端受到 SYN 攻击时,就会有大量处于 SYN_REVC 状态的 TCP 连接,处于这个状态的 TCP 会重传 SYN+ACK ,当重传超过次数达到上限后,就会断开连接。那我们减少了重传次数,就会加速断开连接,这里可以联想如果在第三次握手失败了之后的场景 -------------------------------------------------------------------------------- /problems/Socket和WebSocket的区别.md: -------------------------------------------------------------------------------- 1 | 1. **Socket**: 2 | - Socket是传统的网络编程接口,用于在客户端和服务器之间建立连接并进行数据传输。Socket基于TCP/IP协议,通过套接字(Socket)接口实现数据交换。 3 | - Socket是一种全双工、点对点的通信方式,使用面向连接的TCP协议或无连接的UDP协议。 4 | - Socket编程需要程序员手动处理数据的发送和接收,包括数据的分割、粘包等问题。 5 | - Socket适用于实时性要求高的一对一通信场景,如即时通讯、远程控制等。 6 | 2. **WebSocket**: 7 | - WebSocket是一种在单个TCP连接上实现全双工通信的通信协议,基于HTTP协议的握手机制建立连接,然后升级为WebSocket协议。 8 | - WebSocket支持客户端和服务器之间双向实时通信,可以在同一个连接上进行低延迟的数据传输。 9 | - WebSocket提供了更高层次的抽象,封装了底层数据帧的处理,简化了数据传输的管理。 10 | - WebSocket适用于Web应用中需要实时数据更新的场景,如在线游戏、聊天应用、实时数据展示等。 -------------------------------------------------------------------------------- /problems/TCP协议是如何保证可靠传输的?.md: -------------------------------------------------------------------------------- 1 | TCP协议通过以下几种机制来保证可靠传输: 2 | 3 | 1. 序列号和确认应答:TCP利用序列号和确认号来对数据包进行排序和确认,确保数据包按照正确的顺序传输并且无丢失。 4 | 2. 数据校验和重传机制:TCP采用校验和机制来检测数据是否在传输过程中发生了损坏,如果发现数据错误,则会要求重传。重传机制能够保证数据的完整性。 5 | 3. 滑动窗口和流量控制:TCP使用滑动窗口机制来控制发送端发送数据的速率,避免网络拥塞,并通过流量控制来保证接收方可以及时处理数据。 6 | 4. 连接管理:TCP建立连接时采用三次握手和断开连接时采用四次挥手的方式,确保通信双方同步状态,避免数据丢失或乱序。 7 | 5. 拥塞控制:TCP通过拥塞窗口控制、超时重传和快速重传机制来应对网络拥塞情况,保证数据的稳定传输。 8 | 9 | 这些机制共同作用,使得TCP协议具有较高的可靠性和稳定性,在互联网上广泛应用于数据传输。 -------------------------------------------------------------------------------- /problems/TCP和UDP三次握手和四次挥手状态及消息类型.md: -------------------------------------------------------------------------------- 1 | TCP和UDP是互联网协议套件中的两种主要传输层协议。**TCP是面向连接的协议,提供可靠的、面向字节流的通信,而UDP是无连接的协议,不保证消息的可靠传输。** 2 | 3 | TCP的三次握手和四次挥手是建立和关闭连接的过程。这里先说明TCP的三次握手和四次挥手状态及消息类型,再简述UDP。 4 | 5 | **TCP三次握手** 6 | 7 | 建立TCP连接时,需要执行以下步骤: 8 | 9 | 1. SYN(同步):客户端发送一个具有SYN标志的TCP包到服务器以请求建立连接。此时客户端进入SYN_SENT状态。 10 | 2. SYN-ACK(同步应答):服务器收到SYN包后,返回一个具有SYN和ACK标志的TCP包。此时服务器进入SYN_RCVD状态。 11 | 3. ACK(确认):客户端收到SYN-ACK包后,发送一个具有ACK标志的TCP包来确认连接建立。此时客户端和服务器均进入ESTABLISHED状态。 12 | 13 | **TCP四次挥手** 14 | 15 | 关闭TCP连接时,需要执行以下步骤: 16 | 17 | 1. FIN(结束):当一方(如客户端)完成数据传输,发送一个具有FIN标志的TCP包给对方(如服务器),请求关闭连接。此时客户端进入FIN_WAIT_1状态。 18 | 2. ACK(确认):对方(如服务器)收到FIN包后,发送一个具有ACK标志的TCP包确认收到。此时客户端进入FIN_WAIT_2状态,服务器继续处理剩余数据。 19 | 3. FIN(结束):当对方(如服务器)完成数据传输,也发送一个具有FIN标志的TCP包给发起方(如客户端),请求关闭连接。此时服务器进入LAST_ACK状态。 20 | 4. ACK(确认):发起方(如客户端)收到对方的FIN包后,发送一个具有ACK标志的TCP包作为最后的确认。此时客户端进入TIME_WAIT状态,经过一段时间后释放连接,服务器在收到ACK包后则直接释放连接。 21 | 22 | 值得注意的是,在实际场景中,客户端和服务器通常都可以作为发起方或对方,上述描述仅作为示例。 23 | 24 | **UDP** 25 | 26 | 与TCP不同,UDP是无连接的协议,因此没有类似于三次握手和四次挥手的过程。**在UDP中,应用程序直接将数据封装成数据报,并发送给接收方**。虽然UDP不能保证数据的顺序或可靠性,但由于其低延迟和高效率特点,**在实时应用、广播和多播等场景下非常适用。** -------------------------------------------------------------------------------- /problems/TCP和UDP区别.md: -------------------------------------------------------------------------------- 1 | TCP和UDP都是在网络通信中使用的协议,它们都位于网络模型的第四层(传输层)。但它们之间有一些关键的区别: 2 | 3 | 1. **连接类型**: 4 | - TCP是一种面向连接的协议。在数据传输之前,它需要建立一个连接,这就像是打电话,你需要先拨号建立连接,然后才能通话。 5 | - UDP是一种无连接的协议。它不需要预先建立连接,就可以直接发送数据,这就像是寄信,你直接投递到邮筒,不需要先与对方建立联系。 6 | 2. **数据传输的可靠性**: 7 | - TCP提供了一种可靠的数据传输服务。它有确认、重传和拥塞控制机制,可以保证数据的正确性和顺序性。 8 | - UDP则不提供数据传输的可靠性保证,它只是简单地将数据包发送出去,不关心数据包是否到达目的地,因此可能会出现数据丢失的情况。 9 | 3. **传输速度**: 10 | - 由于TCP需要进行连接建立、确认和重传等操作,所以相对来说,其传输速度比UDP慢。 11 | - UDP由于没有复杂的控制机制,所以其传输速度通常比TCP要快。 12 | 4. **使用场景**: 13 | - TCP常用于需要高可靠性的应用,如网页浏览(HTTP、HTTPS)、邮件发送(SMTP)等。 14 | - UDP则适合对实时性要求较高,可容忍少量数据丢失的应用,如视频会议、语音通话、直播等。 15 | 5. **头部开销**: 16 | - TCP的头部开销较大,最小20字节,提供了许多选项,如错误检测,序列号,确认号等。 17 | - UDP的头部开销小,只有8字节,只提供了最基本的功能。 -------------------------------------------------------------------------------- /problems/TCP和UDP头部字节定义.md: -------------------------------------------------------------------------------- 1 | **TCP的头部结构:** 2 | 3 | 1. **源端口号**:16位,表示数据发送者的端口号。 4 | 2. **目标端口号**:16位,表示数据接收者的端口号。 5 | 3. **序列号**:32位,用于标识从TCP源端向目的端发送的字节流,它表示在这个报文段中的的第一个数据字节。 6 | 4. **确认序列号**:32位,只有ACK标志位为1时,确认序列号字段才有效。它含有期望收到对方下一个报文段的数据的第一个字节的序列号。 7 | 5. **头部长度**:4位,给出了头部长度,以32位为单位。 8 | 6. **保留**:6位,为将来使用而保留,目前未被使用。 9 | 7. **控制位**:其中包括URG,ACK,PSH,RST,SYN,FIN等6个标志位。 10 | 8. **窗口大小**:16位,指定了本段所能接收的最大窗口大小。 11 | 9. **校验和**:16位,用于检测头部和数据部分是否发生错误。 12 | 10. **急救指针**:16位,仅在URG标记为1时有效,否则通常设置为0。 13 | 11. **选项**:可变长,如果存在的话,用于一些额外的功能。 14 | 15 | **UDP的头部结构:** 16 | 17 | 1. **源端口号**:16位,表示数据发送者的端口号。 18 | 2. **目标端口号**:16位,表示数据接收者的端口号。 19 | 3. **长度**:16位,包括UDP头部和数据部分的总长度。 20 | 4. **校验和**:16位,用于检测头部和数据部分是否发生错误。 21 | -------------------------------------------------------------------------------- /problems/TCP和UDP对应常见的应用层协议有那些?.md: -------------------------------------------------------------------------------- 1 | TCP和UDP是传输层协议,常见的应用层协议对应如下: 2 | 3 | TCP常见的应用层协议有: 4 | 5 | 1. HTTP(超文本传输协议):用于在Web服务器和客户端之间传输信息。 6 | 2. FTP(文件传输协议):用于在网络上进行文件传输。 7 | 3. SMTP(简单邮件传输协议):用于电子邮件的发送。 8 | 4. Telnet(远程登录协议):用于远程登陆到主机进行操作。 9 | 10 | UDP常见的应用层协议有: 11 | 12 | 1. DNS(域名系统):用于将域名解析为IP地址。 13 | 2. DHCP(动态主机配置协议):用于自动分配IP地址。 14 | 3. SNMP(简单网络管理协议):用于网络设备的管理和监控。 15 | 4. TFTP(简单文件传输协议):用于在网络上进行文件传输。 -------------------------------------------------------------------------------- /problems/TCP和UDP的区别.md: -------------------------------------------------------------------------------- 1 | ### TCP 和 UDP 各自的优点 2 | 3 | 1.连接 4 | 5 | TCP是⾯向连接的,在传输前需要三次握⼿建⽴连接,UDP不需要连接,即刻传输数据。 6 | 7 | 2、服务形式 8 | 9 | TCP只能⼀对⼀,点对点服务,UDP⽀持⼀对⼀、⼀对多、多对多通信。 10 | 11 | 3、可靠性(传输的数据) 12 | 13 | TCP保证数据可靠交付,拥有**确认应答**和**重传机制**,⽆重复、不丢失、按序到达; 14 | 15 | UDP尽可能交付,不保证可靠性。 16 | 17 | 4、连接控制机制(传输路途) 18 | 19 | TCP拥有流量控制、拥塞控制,保证传输安全性等,UDP在⽹络拥堵情况下不会降低发送速率。 20 | 21 | 5、⾸部⼤⼩ 22 | 23 | TCP⾸部⻓度不使⽤选项字段是20字节,使⽤选项字段⻓度增加(可变) 24 | 25 | 8 位 = 1 字节 26 | 27 | image-20220328231152943 28 | 29 | UDP⾸部固定8字节。 30 | 31 | image-20220328231247292 32 | 33 | 6、数据格式 34 | 35 | TCP基于字节流,没有边界,但是保证传输顺序和可靠性; 36 | 37 | UDP继承了IP层特性,基于数据包,有边界可能出现乱序和丢包。 38 | 39 | 7、分⽚⽅式 40 | 41 | TCP数据⼤于 MSS 时会在TCP层将数据进⾏分⽚传输,到达⽬的地后同样在传输层进⾏合并,如果有某个⽚丢失则只需要重传丢失的分⽚即可; 42 | 43 | UDP数据⼤于MTU时会在IP层分⽚,同样也在⽬的IP层合并,如果某个IP分⽚丢失,则需要将所有分⽚都进⾏重传,开销⼤。 44 | 45 | 8.应用场景 46 | 47 | TCP:FTP 文件传输;Http/Https 48 | 49 | UDP:视频音频等多媒体通信;广播通知;包总量较少的通信,如 DNS ,SNMP -------------------------------------------------------------------------------- /problems/TCP和UDP的首部长什么样子?.md: -------------------------------------------------------------------------------- 1 | TCP和UDP的首部如下: 2 | 3 | TCP⾸部⻓度不使⽤选项字段是20字节,使⽤选项字段⻓度增加(可变) 4 | 5 | 8 位 = 1 字节 6 | 7 | image-20220328231152943 8 | 9 | UDP⾸部固定8字节。 10 | 11 | image-20220328231247292 -------------------------------------------------------------------------------- /problems/TCP的最大连接数是多少?.md: -------------------------------------------------------------------------------- 1 | TCP的最大连接数取决于多个因素,**包括操作系统、硬件配置和网络环境等**。一般来说,TCP连接数受到以下因素的限制: 2 | 3 | 1. 操作系统的限制:不同的操作系统对于TCP连接数有不同的限制。例如,在Linux系统中,默认情况下可以支持数以万计的TCP连接,但是可以通过修改内核参数来增加这个限制。 4 | 2. 硬件资源限制:服务器的硬件资源(如CPU、内存)也会影响TCP连接数的最大限制。如果服务器的硬件资源有限,那么能够支持的TCP连接数也会受到限制。 5 | 3. 网络设备限制:网络设备(如防火墙、路由器)的配置也可能对TCP连接数造成限制。这些设备可能会对每个连接的最大并发数设置限制。 6 | 7 | 总的来说,TCP连接数没有一个固定的最大值,而是受到多个因素的影响。如果需要提高服务器的TCP连接数,可以通过优化操作系统、增加硬件资源、调整网络设备配置等方式来实现。 -------------------------------------------------------------------------------- /problems/TCP粘包是怎么产生的?.md: -------------------------------------------------------------------------------- 1 | TCP粘包指的是接收方在一次读取数据时,将多个发送方发送的数据包合并成一个或者少于原始数据包数量的现象。TCP粘包通常发生在基于流式传输(如TCP)的网络通信中,其主要原因有以下几点: 2 | 3 | 1. TCP为面向流的协议:TCP是面向流的传输协议,发送方可以将数据划分为任意大小的数据块发送,接收方可能一次性接收到多个数据包,导致多个数据包被合并成一个大的数据块。 4 | 2. 接收方缓冲区未及时读取:如果接收方没有及时从缓冲区中读取数据,而发送方持续发送数据,则多个数据包可能会在接收方的缓冲区中累积,从而造成粘包现象。 5 | 3. 网络延迟和拥塞:网络延迟、拥塞等因素也可能导致数据包在传输过程中聚集在一起,形成粘包现象。 6 | 4. 操作系统对数据处理方式不同:不同操作系统对于接收数据的处理方式可能不同,有些操作系统会尽量将接收到的数据进行合并,从而可能导致粘包现象。 7 | 8 | 为避免TCP粘包问题,可以采用以下方法: 9 | 10 | - 在数据包中增加长度字段,让接收方根据长度字段来正确解析数据包。 11 | - 使用特殊标记或分隔符来标识数据包的边界。 12 | - 对数据进行序列化和反序列化处理,确保数据的完整性和正确性。 13 | - 合理设计应用层协议,规范数据的传输方式,避免产生粘包问题。 -------------------------------------------------------------------------------- /problems/Trie树原理.md: -------------------------------------------------------------------------------- 1 | Trie树,也称为前缀树或字典树,是一种用于存储和检索字符串集合的树形数据结构。它利用字符串的公共前缀来节省存储空间和加速查询操作。下面我将详细介绍Trie树的原理。 2 | 3 | ## Trie树的基本概念 4 | 5 | 1. 节点(Node): 6 | - Trie树由一个根节点和多个子节点组成。 7 | - 每个节点表示一个字符,从根节点到某一节点的路径表示一个字符串的前缀。 8 | - 节点通常包含一个字符、一个布尔标志(表示是否为字符串的结尾)和多个子节点指针。 9 | 2. 边(Edge): 10 | - Trie树中的边表示字符之间的关系。 11 | - 每条边连接一个父节点和一个子节点,表示从父节点到子节点的字符。 12 | - 边的数量取决于字符集的大小,通常使用数组或哈希表来存储子节点指针。 13 | 3. 字符串插入(String Insertion): 14 | - 将一个字符串插入到Trie树中时,从根节点开始,沿着字符串的字符逐个遍历。 15 | - 如果当前字符在当前节点的子节点中不存在,则创建一个新的子节点。 16 | - 重复以上步骤,直到字符串的所有字符都插入到Trie树中。 17 | - 在字符串的最后一个字符对应的节点上,将布尔标志设置为true,表示该字符串存在于Trie树中。 18 | 4. 字符串查询(String Query): 19 | - 查询一个字符串是否存在于Trie树中时,从根节点开始,沿着字符串的字符逐个遍历。 20 | - 如果在某个节点上无法找到对应的子节点,则说明该字符串不存在于Trie树中。 21 | - 如果成功遍历完字符串的所有字符,并且最后一个字符对应的节点的布尔标志为true,则说明该字符串存在于Trie树中。 22 | 23 | ## Trie树的工作原理 24 | 25 | 1. 插入字符串: 26 | - 从根节点开始,依次处理字符串的每个字符。 27 | - 对于当前字符,检查当前节点是否有对应的子节点: 28 | - 如果有,则移动到该子节点,继续处理下一个字符。 29 | - 如果没有,则创建一个新的子节点,将当前字符作为该节点的字符,然后移动到新创建的子节点。 30 | - 重复以上步骤,直到字符串的所有字符都处理完毕。 31 | - 在最后一个字符对应的节点上,将布尔标志设置为true,表示该字符串存在于Trie树中。 32 | 2. 查询字符串: 33 | - 从根节点开始,依次处理字符串的每个字符。 34 | - 对于当前字符,检查当前节点是否有对应的子节点: 35 | - 如果有,则移动到该子节点,继续处理下一个字符。 36 | - 如果没有,则说明该字符串不存在于Trie树中,返回false。 37 | - 如果成功遍历完字符串的所有字符,检查最后一个字符对应的节点的布尔标志: 38 | - 如果为true,则说明该字符串存在于Trie树中,返回true。 39 | - 如果为false,则说明该字符串是某个字符串的前缀,但本身不存在于Trie树中,返回false。 40 | 3. 查找前缀: 41 | - Trie树还支持查找以给定字符串为前缀的所有字符串。 42 | - 从根节点开始,沿着给定字符串的字符遍历Trie树,直到无法继续遍历或到达字符串的末尾。 43 | - 从当前节点开始,通过深度优先搜索或广度优先搜索遍历所有子节点,收集以当前节点为前缀的所有字符串。 44 | 45 | ## Trie树的优缺点 46 | 47 | 优点: 48 | 49 | 1. 查询效率高:Trie树的查询时间复杂度与字符串的长度成正比,与字符串的数量无关。 50 | 2. 前缀查询:Trie树支持高效地查找以给定字符串为前缀的所有字符串。 51 | 3. 节省空间:Trie树利用字符串的公共前缀来减少重复存储,节省了空间。 52 | 53 | 缺点: 54 | 55 | 1. 空间占用:尽管Trie树节省了重复存储的空间,但是由于需要存储大量的节点和边,空间占用仍然较大。 56 | 2. 插入和删除操作:在Trie树中插入和删除字符串可能需要创建或删除多个节点,操作相对复杂。 57 | 3. 字符集大小:Trie树的空间复杂度与字符集的大小有关,对于大字符集(如Unicode)的情况,空间占用会更加显著。 -------------------------------------------------------------------------------- /problems/UDP中使用connect的好处.md: -------------------------------------------------------------------------------- 1 | 在使用UDP进行网络编程时,**`connect`函数通常不是必须的**,因为UDP是一个无连接的协议,它不需要像TCP那样进行三次握手来建立连接。**然而,在某些场景下,即使对于UDP,使用`connect`也有一些好处**: 2 | 3 | 1. **指定默认的对等方**:调用`connect`后,你可以使用`send`和`recv`(而不是`sendto`和`recvfrom`)函数来发送和接收数据。系统会自动使用`connect`时指定的地址作为数据的目的地,这使得代码更简洁,因为你不需要每次发送数据时都提供对等方的地址。 4 | 2. **错误报告**:一旦对UDP套接字调用了`connect`,当往指定的对等方发送数据时,如果发生传输错误,操作系统会将错误报告给套接字。例如,如果目标主机不可达,你可能会收到`ECONNREFUSED`错误。这在没有调用`connect`的情况下是不可能的,因为UDP通常不会为无连接的操作提供错误反馈。 5 | 3. **过滤接收到的数据包**:使用`connect`连接到特定的远程端点后,该套接字只接受来自这个特定端点的数据包。这意味着,该套接字不会接收到其他任何地址的数据,从而相当于为该套接字设置了一个过滤器。 6 | 4. **效率**:虽然这个优点可能不是非常显著,但是在某些情况下,由于内核不需要在每次数据传输时处理源和目的地址,可能会略微减少CPU的工作负载。 7 | 5. **简化状态管理**:对于需要持久通信状态的应用程序,例如一个客户端频繁地与同一个服务器端点进行交互,使用`connect`可以简化程序的状态管理。 8 | 6. **安全性**:使用`connect`可以提高一定的安全性,因为套接字只接收来自已连接对等方的数据,这减少了接收到恶意或无关数据包的可能性。 9 | 10 | 尽管有这些好处,也要考虑到`connect`对UDP套接字的影响,因为它使得套接字从一个可以与多个对等方通信的套接字变成了只能与一个对等方通信的套接字。如果需要与多个对等方通信,你或许需要创建多个套接字或者不使用`connect`。 -------------------------------------------------------------------------------- /problems/WebScoket底层原理.md: -------------------------------------------------------------------------------- 1 | 1. **握手阶段**: 2 | - 客户端发起WebSocket连接请求时,首先会发送一个HTTP请求给服务器,包含特定的WebSocket头部信息,如Upgrade和Connection字段。 3 | - 服务器收到请求后,进行握手确认,返回状态码101 Switching Protocols,表示已经切换到WebSocket协议。 4 | 2. **建立连接**: 5 | - 握手完成后,客户端和服务器之间建立了WebSocket连接,此时是持久连接,双方可以随时发送数据。 6 | 3. **数据帧格式**: 7 | - WebSocket数据传输采用数据帧的方式,在实际通信中,将数据封装成一系列的数据帧进行传输。 8 | - 数据帧由固定的格式组成,包括FIN(结束标志位)、RSV(保留位)、Opcode(操作码)、Mask(掩码位)等字段,用于表示数据的类型、长度和处理方式。 9 | 4. **心跳检测**: 10 | - WebSocket连接建立后,客户端和服务器之间可以通过心跳机制来维持连接的稳定性,避免连接超时断开。 11 | - 双方定期发送心跳数据帧,以确认对方仍然处于连接状态。 12 | 5. **断开连接**: 13 | - 当需要断开WebSocket连接时,可以通过特定的关闭数据帧来结束连接,双方收到关闭帧后,即可安全地关闭连接。 -------------------------------------------------------------------------------- /problems/bitmap.md: -------------------------------------------------------------------------------- 1 | 在处理海量数据时,bitmap是一种常用的数据结构,它以位为单位来表示数据,可以高效地进行某些操作,如查询、去重和压缩等。 2 | 3 | ## Bitmap的原理 4 | 5 | 1. 位图表示: 6 | - 每个数据项用一个比特(bit)表示其状态或属性。 7 | - 通常用0表示不存在或false,用1表示存在或true。 8 | - 多个数据项可以组成一个bitmap,每个比特的位置对应数据项的编号或键值。 9 | 2. 数据存储: 10 | - Bitmap通常以紧凑的方式存储在内存中,每个字节(byte)存储8个比特。 11 | - 可以使用C++中的`bitset`、`vector`或自定义的位操作函数来实现bitmap。 12 | - bitmap的大小与数据项的数量有关,需要根据实际情况选择合适的存储方式。 13 | 3. 常见操作: 14 | - 设置位(set):将指定位置的比特设为1,表示该数据项存在或为true。 15 | - 清除位(clear):将指定位置的比特设为0,表示该数据项不存在或为false。 16 | - 查询位(test):检查指定位置的比特是否为1,判断该数据项是否存在或为true。 17 | - 压缩存储:由于bitmap中通常包含大量的0,可以使用压缩算法(如行程编码)减小存储空间。 18 | 19 | ## 在C++中使用Bitmap处理海量数据 20 | 21 | 1. 去重和判重: 22 | - 将每个数据项映射到bitmap中的一个比特位,通过设置位来标记数据的存在性。 23 | - 判断数据是否存在时,只需查询对应的比特位即可,时间复杂度为O(1)。 24 | - bitmap可以高效地完成海量数据的去重和判重操作,节省存储空间和查询时间。 25 | 2. 数据压缩: 26 | - 将原始数据转换为bitmap表示,可以大大减小数据规模。 27 | - 使用压缩算法对bitmap进行编码,进一步减小存储空间。 28 | - 压缩后的bitmap在进行某些操作时,无需完全解压,可以直接在压缩数据上进行位操作。 29 | 3. 快速查询和统计: 30 | - 通过遍历bitmap,可以快速统计数据的基数(cardinality),即不同数据项的数量。 31 | - 对bitmap进行位运算,如与(AND)、或(OR)、异或(XOR)等,可以实现多个集合之间的交、并、差等操作。 32 | - bitmap的查询和统计操作通常具有较低的时间复杂度,适用于实时性要求较高的场景。 -------------------------------------------------------------------------------- /problems/connect会阻塞怎么解决.md: -------------------------------------------------------------------------------- 1 | 当你调用`connect`函数对远程服务器进行连接时,如果使用的是阻塞式套接字,`connect`会一直阻塞到连接成功或者发生错误为止。这可能导致应用程序的其它部分无法执行,尤其是在图形用户界面或需要同时处理多个连接的服务器应用程序中,这可能是不可接受的。为了解决`connect`函数的阻塞问题,可以采用以下几种方法: 2 | 3 | 1. **使用非阻塞套接字**: 4 | - 将套接字设置为非阻塞模式,这样`connect`调用将立即返回。如果连接未立即建立,`connect`会返回一个错误码,通常是`EINPROGRESS`,表明连接尝试正在进行中。 5 | - 接下来,你可以使用`select`,`poll`或`epoll`等函数来监视套接字的状态。当套接字可写时(`select`等的写集合中返回),这通常意味着连接已经建立或者连接尝试失败,此时可以使用`getsockopt`来检查套接字的`SO_ERROR`选项,以确定是否连接成功。 6 | 2. **使用异步I/O或事件驱动模型**: 7 | - 通过异步I/O库(比如Linux的`libaio`或Windows的`IOCP`)或者事件驱动的网络库(比如`libevent`、`libuv`或`Boost.Asio`),可以在不阻塞主线程的情况下执行`connect`。 8 | - 这些库通常提供了在连接完成时触发的回调机制。 9 | 3. **创建辅助线程或进程**: 10 | - 在单独的线程或进程中执行`connect`调用,这样即使`connect`阻塞,也不会影响主线程或进程的执行。 11 | - 连接成功后,可以通过线程间通信机制(如信号量、消息队列、事件等)通知主线程或进程。 12 | 4. **使用`O_NONBLOCK`与`connect`结合后的特殊处理**: 13 | - 在某些系统中,对于非阻塞套接字,`connect`可能立即返回`-1`,并设置`errno`为`EINPROGRESS`。在这种情况下,可以使用`select`或`poll`等函数来监视连接是否成功。 14 | 15 | 16 | 17 | 下面用代码来举一个例子:我们先将套接字设置为非阻塞,然后尝试连接。如果`connect`返回`EINPROGRESS`错误,我们利用`select`监视套接字的写事件,如果`select`返回后,套接字的写事件被触发,则调用`getsockopt`检查`SO_ERROR`,看是否连接成功。 18 | 19 | ```c 20 | #include 21 | #include 22 | #include 23 | #include 24 | #include 25 | #include 26 | #include 27 | // ... 28 | 29 | int sockfd = socket(AF_INET, SOCK_STREAM, 0); 30 | if (sockfd < 0) { 31 | perror("socket"); 32 | return -1; 33 | } 34 | 35 | // 设置套接字为非阻塞 36 | int flags = fcntl(sockfd, F_GETFL, 0); 37 | fcntl(sockfd, F_SETFL, flags | O_NONBLOCK); 38 | 39 | struct sockaddr_in server_addr; 40 | // 设置server_addr的成员变量... 41 | // ... 42 | 43 | int result = connect(sockfd, (struct sockaddr *)&server_addr, sizeof(server_addr)); 44 | if (result < 0) { 45 | if (errno == EINPROGRESS) { 46 | // 连接正在尝试中 47 | fd_set writefds; 48 | FD_ZERO(&writefds); 49 | FD_SET(sockfd, &writefds); 50 | struct timeval timeout = {5, 0}; // 设置超时时间 51 | 52 | // 使用select等待连接完成或超时 53 | result = select(sockfd + 1, NULL, &writefds, NULL, &timeout); 54 | if (result <= 0) { 55 | // select错误或超时 56 | perror("select"); 57 | close(sockfd); 58 | return -1; 59 | } else { 60 | int error = 0; 61 | socklen_t len = sizeof(error); 62 | // 获取套接字选项 63 | if (getsockopt(sockfd, SOL_SOCKET, SO_ERROR, &error, &len) < 0) { 64 | perror("getsockopt"); 65 | close(sockfd); 66 | return -1; 67 | } 68 | // 检查是否有错误发生 69 | if (error) { 70 | errno = error; 71 | perror("connect"); 72 | close(sockfd); 73 | return -1; 74 | } 75 | } 76 | } else { 77 | // 连接尝试出错 78 | perror("connect"); 79 | close(sockfd); 80 | return -1; 81 | } 82 | } 83 | 84 | // 套接字现在已连接 85 | // ... 86 | 87 | close(sockfd); 88 | 89 | ``` 90 | 91 | -------------------------------------------------------------------------------- /problems/epoll哪些触发模式,有啥区别?.md: -------------------------------------------------------------------------------- 1 | epoll有两种触发模式:水平触发(LT)和边缘触发(ET)。 2 | 3 | 1. 水平触发(LT):这是epoll的默认工作模式。在这种模式下,只要被监控的文件描述符还有数据可以读取或者还能写入数据,就会一直通知该事件。也就是说,如果你没有处理完所有的数据,那么每次调用epoll的时候,它都会响应这个事件。只有你读或写到了EAGAIN,也就是没有更多的数据可以处理了,那么它在下一次epoll调用的时候才不会响应。 4 | 2. 边缘触发(ET):在这种模式下,当被监控的文件描述符状态发生变化时,epoll只会通知一次该事件,直到该文件描述符的状态再次发生变化为止。也就是说,如果你没有处理完所有的数据或没有处理该事件,那么在下一次epoll调用的时候,它不会再响应这个事件。 5 | 6 | ET模式比LT模式效率更高,因为它避免了多次响应同一个事件,但使用ET模式需要更小心,因为如果你没有处理完所有的数据或没有处理该事件,那么可能会丢失数据。在使用ET模式时,我们通常会使用非阻塞IO,这样我们可以尽可能的读取或写入所有的数据,直到收到EAGAIN错误为止。 -------------------------------------------------------------------------------- /problems/forward和redirect的区别是什么?.md: -------------------------------------------------------------------------------- 1 | "forward" 和 "redirect" 是web开发中常用的两个概念,它们之间有着明显的区别: 2 | 3 | 1. Forward(转发):当服务器收到一个请求时,可以将该请求转发给另一个资源进行处理,但是客户端并不知道这个过程。在转发过程中,客户端发送的请求仍然保持原始URL,最终结果是由转发目标资源产生并返回给客户端。通常情况下,转发是在服务器内部完成的,因此客户端感知不到。 4 | 2. Redirect(重定向):与转发不同,重定向会告诉客户端,请求的资源已经被移动到另一个位置。服务器会返回一个特殊的响应码(如302 Found或者301 Moved Permanently),告诉客户端需要重新发送请求到新的URL。客户端接收到重定向响应后,会自动跳转到新的URL去获取资源。 5 | 6 | 总结来说,转发是服务器内部处理请求并将控制权交给另一个资源,而重定向是告知客户端资源的位置已经改变并需要重新发送请求到新的URL。 -------------------------------------------------------------------------------- /problems/git的merge和rebase有什么区别.md: -------------------------------------------------------------------------------- 1 | Git的merge和rebase都是将两个分支合并的方法,但它们的实现方式不同,会对代码库的历史记录造成影响。 2 | 3 | merge是将两个分支上的更改集合合并到一起,并形成一个新的提交节点。在merge时,Git 会自动创建一个新的提交节点,这个节点包含了两个分支的更改,同时保留了两个分支的历史记录。这样做可以避免各种冲突,并且保证每个分支的历史记录都被记录下来。 4 | 5 | 而rebase则是将当前分支的更改重新基于另一个分支进行重放。重放是指将当前分支中的所有提交按序应用到目标分支的最新提交之后。这样做的效果是让当前分支看起来像是从目标分支最新提交开始开发的,避免了一些无意义的merge节点,并且让提交历史线变得更加清晰。然而,rebase会对提交历史进行修改,因为它会把当前分支的提交作为全新的提交,而不是merge提交。 -------------------------------------------------------------------------------- /problems/ip地址和mac地址的区别都有那些?.md: -------------------------------------------------------------------------------- 1 | MAC地址是网络设备在数据链路层中使用的物理地址,用于唯一标识网络设备。每个网络设备都有一个唯一的MAC地址,通常由48位二进制数表示,分为6个十六进制数对,用冒号或短横线分隔,如00:1A:2B:3C:4D:5E。 2 | 3 | MAC地址主要用于通过局域网传输数据时,帮助网络设备进行识别和寻址。与IP地址不同,MAC地址是固定的且与硬件设备绑定,不会因为设备连接到不同网络而改变。当数据包在局域网内传输时,源设备将目标设备的MAC地址作为目标地址写入数据包头部,以确保数据包能够准确地从发送者传输到接收者。 4 | 5 | 需要注意的是,MAC地址是在数据链路层中使用的标识符,只在局域网范围内有效。在互联网通信中,数据包最终是通过IP地址来路由和传递的,而在局域网中则是通过MAC地址来实现设备之间的直接通信。MAC地址与IP地址结合起来,共同协助实现了数据在网络中的正确传输和交换。 -------------------------------------------------------------------------------- /problems/keepalive是什么?如何使用?.md: -------------------------------------------------------------------------------- 1 | Keepalive是一个网络功能,用于检测两台计算机之间的连接是否仍然活跃。在TCP/IP协议中,它是指定期发送探测包以确保连接仍然存在并且对方尚未崩溃或无法到达。Keepalive可以帮助识别死亡、挂起或不再活跃的连接,并允许应用程序采取相应的行动,如重新连接或释放资源。 2 | 3 | 在很多情况下,默认的TCP连接没有开启Keepalive,因为频繁地发送Keepalive探测包可能会增加不必要的网络流量。但是,在长时间打开的连接中(例如数据库连接),可能需要使用Keepalive来确保持续的服务可用性。 4 | 5 | **如何使用Keepalive:** 6 | 7 | 1. 在Socket编程中设置: 8 | 9 | Keepalive参数通常可以通过socket的选项来设置。例如,在C语言中,你可以使用`setsockopt`函数来设置TCP Keepalive: 10 | 11 | ```c 12 | int optval = 1; 13 | socklen_t optlen = sizeof(optval); 14 | 15 | // 开启Keepalive属性 16 | if(setsockopt(sock, SOL_SOCKET, SO_KEEPALIVE, &optval, optlen) < 0) { 17 | perror("setsockopt"); 18 | close(sock); 19 | exit(EXIT_FAILURE); 20 | } 21 | ``` 22 | 23 | 具体的Keepalive参数(例如探测包的发送频率和重试次数)依赖于操作系统,并且可能有不同的接口进行配置。 24 | 25 | 2. 在操作系统级别设置: 26 | 27 | 对于类Unix系统,比如Linux,你可以通过修改系统文件中的以下参数来调整Keepalive的行为: 28 | 29 | - `/proc/sys/net/ipv4/tcp_keepalive_time`:在开始发送Keepalive探测前的空闲时间。 30 | - `/proc/sys/net/ipv4/tcp_keepalive_probes`:在断开连接前发送Keepalive探测的最大次数。 31 | - `/proc/sys/net/ipv4/tcp_keepalive_intvl`:两个Keepalive探测之间的间隔。 32 | 33 | **例如**: 34 | 35 | ``` 36 | echo 600 > /proc/sys/net/ipv4/tcp_keepalive_time 37 | echo 10 > /proc/sys/net/ipv4/tcp_keepalive_probes 38 | echo 60 > /proc/sys/net/ipv4/tcp_keepalive_intvl 39 | ``` 40 | 41 | 这些命令将设置TCP连接在空闲10分钟后开始发送Keepalive探测,每次发送间隔1分钟,总共发送10次探测。 42 | 43 | 3. 在应用程序层面设置: 44 | 45 | 很多高级语言的网络库也提供了设置Keepalive的接口。例如,在Python的`socket`模块中,可以这样设置Keepalive: 46 | 47 | ```python 48 | import socket 49 | 50 | s = socket.socket(socket.AF_INET, socket.SOCK_STREAM) 51 | s.setsockopt(socket.SOL_SOCKET, socket.SO_KEEPALIVE, 1) 52 | ``` 53 | 54 | 当Keepalive被启用时,如果在设定的时间内没有数据交换,系统会自动发送Keepalive探测包。如果对端响应,则连接继续保持活跃状态;如果对端未响应(经过一定数量的重试后),则操作系统会认为连接已经失效,并通知应用程序(通过返回错误码等方式),应用程序可以据此关闭连接,清理资源,或者尝试重连等操作。 -------------------------------------------------------------------------------- /problems/linux文件系统:inode,inode存储了哪些东西,目录名,文件名存在哪里.md: -------------------------------------------------------------------------------- 1 | 在Linux文件系统中,每个文件和目录都由一个称为inode的数据结构表示,inode存储了以下信息: 2 | 3 | 1. **文件的用户ID(所有者)** 4 | 2. **文件的组ID** 5 | 3. **文件的大小** 6 | 4. 文件的创建,访问和修改时间戳 7 | 5. 文件的权限(读,写,执行等) 8 | 6. 文件的链接数(即有多少硬链接指向该文件) 9 | 7. 文件的数据块位置(指向实际存储文件数据的磁盘块位置) 10 | 8. 文件的类型(例如,它是常规文件、目录还是符号链接等) 11 | 12 | **inode并不存储文件名或目录名。**这些信息是存储在目录文件中的。**也就是说,他们是分开的!**在Linux中,目录实际上是一个特殊类型的文件,它包含一系列的目录项。每个目录项都包含一个文件或子目录的名称,以及指向相应inode的指针。所以,文件名和目录名实际上是存储在目录文件中的。 13 | 14 | -------------------------------------------------------------------------------- /problems/linux的内存管理机制内存寻址方式什么叫虚拟内存内存调页算法任务调度算法.md: -------------------------------------------------------------------------------- 1 | **内存管理机制**:Linux使用分段和分页的混合方式进行内存管理。Linux将每个进程的虚拟地址空间划分为若干个段每个段下面有若干个大小固定的页面,而物理内存也被划分为若干个页。通过建立从虚拟页到物理页帧的映射,来实现对内存的管理。(**物理内存并非段页式!而是纯分页进行管理**) 2 | 3 | **内存寻址方式**: Linux使用虚拟内存寻址方式,每个进程都有其独立的虚拟地址空间,这些地址用于指向该进程的代码、数据和堆栈等内容。操作系统通过维护一个页表来管理虚拟地址和物理地址之间的映射关系。 4 | 5 | **虚拟内存**: 虚拟内存是一种内存管理技术,它允许一个进程使用的地址空间能超过实际存在的物理内存。当进程要访问的内存不在物理内存中时(产生缺页中断),就会从硬盘上的交换区载入所需的内存页。 6 | 7 | **内存调页算法**: Linux使用的是二次机会算法(Second Chance Algorithm,也称为Clock置换算法)。该算法在FIFO算法的基础上增加了访问位,并采用了类似时钟的循环队列,降低了页面置换的频率,提高了性能。 8 | 9 | **任务调度算法**:Linux主要有以下几种任务调度算法: 10 | 11 | - **完全公平调度器(CFS)**:它是Linux默认的进程调度器,使用红黑树来存储待调度的进程,保证每个进程得到公平的CPU时间片。 12 | - **实时调度器**:包括FIFO调度策略和轮询(RR)调度策略,主要用于需要硬实时或者软实时的系统。 13 | - **Deadline调度器**:针对实时任务,采取动态优先级(越接近deadline,优先级越高)的方式,确保任务在deadline前完成。 -------------------------------------------------------------------------------- /problems/linux系统的各类同步机制、linux系统的各类异步机制.md: -------------------------------------------------------------------------------- 1 | **同步机制** 2 | 3 | 1. 互斥锁(Mutex):互斥锁用于保护临界区资源,确保同一时刻只有一个线程能访问特定资源。 4 | 2. 读写锁(Read-Write Lock):允许多个读者同时访问,但在写者访问时,其他所有读写者都将阻塞。 5 | 3. 条件变量(Conditional Variable):线程间同步的另一种方式,它可以允许某些情况下的线程有序地访问共享数据。 6 | 4. 信号量(Semaphore):原始信号量和 POSIX 信号量是 Linux 提供的两种信号量。 7 | 5. 自旋锁(spinlock):当锁已经被占用时,继续尝试获取锁,直到成功为止。 8 | 6. 屏障(barrier):确保所有进程在继续执行前达到某个点。 9 | 10 | **异步机制** 11 | 12 | 1. 异步IO(AIO):在发起IO请求后,立即返回,不阻塞当前进程或线程。当IO操作完成后,再通知用户程序。 13 | 2. 信号(Signal):当某些事件发生时,系统会发送信号给进程。对信号的处理可以是忽略、捕捉(指定处理函数)、执行默认操作等。 14 | 3. 回调函数:在某些操作完成后,调用预先定义的函数。 15 | 4. epoll/select/poll异步IO模型:Linux下的多路复用IO模型,可以同时监控多个文件描述符的读写状态,当文件描述符准备好后,通过回调通知应用程序。 -------------------------------------------------------------------------------- /problems/lower_bound()和upper_bound()有什么区别?.md: -------------------------------------------------------------------------------- 1 | 1. **lower_bound()**: 2 | - 返回一个指向范围内第一个**不小于**(即大于或等于)给定值的元素的迭代器。 3 | - 如果给定值不存在于容器中,该函数返回指向第一个大于该值的元素的迭代器。如果所有元素都小于给定值,函数将返回一个指向容器末尾(end)的迭代器。 4 | 2. **upper_bound()**: 5 | - 返回一个指向范围内第一个**大于**给定值的元素的迭代器。 6 | - 如果所有元素都小于或等于给定值,函数将返回一个指向容器末尾(end)的迭代器。 7 | 8 | **举例说明**: 9 | 10 | 假设我们有一个包含 {1, 2, 4, 4, 5, 6, 8} 的整数vector,并且我们想要搜索数字4。 11 | 12 | - 使用 `lower_bound()` 寻找4会返回指向第一个数字4的迭代器,因为4是数组中第一个"不小于"4的值。 13 | - 使用 `upper_bound()` 寻找4会返回指向数字5的迭代器,这是因为5是数组中第一个"大于"4的值。 -------------------------------------------------------------------------------- /problems/malloc,strcpy,strcmp的实现,常用库函数实现,哪些库函数属于高危函数.md: -------------------------------------------------------------------------------- 1 | `malloc`:此函数用于在堆上动态分配内存 2 | 3 | ```c++ 4 | void *malloc(size_t size) { 5 | //sbrk()是一种在C和C++中用于增加或减少程序数据段大小的系统调用。它通过改变堆的末尾地址来改变程序的内存空间。 6 | //这里的sbrk是一种可能的调用方式,在每个操作系统中不一定相同,具体可以看对应操作系统的底层源码,比如Windows下会调用virtual allocated函数一样 7 | 8 | void *p = sbrk(0); 9 | void *request = sbrk(size); 10 | if (request == (void*) -1) { 11 | return NULL; // sbrk failed. 12 | } else { 13 | assert(p == request); // Not thread safe. 14 | return p; 15 | } 16 | } 17 | ``` 18 | 19 | `strcpy`:此函数用于复制字符串。 20 | 21 | ```c++ 22 | char *strcpy(char *dest, const char *src) { 23 | char *ret = dest; 24 | while ((*dest++ = *src++) != '\0') 25 | ; 26 | return ret; 27 | } 28 | ``` 29 | 30 | `strcmp`: 此函数用于比较两个字符串。 31 | 32 | ```c++ 33 | int strcmp(const char *str1, const char *str2) { 34 | while (*str1 && (*str1 == *str2)) { 35 | str1++; 36 | str2++; 37 | } 38 | return *(unsigned char *)str1 - *(unsigned char *)str2; 39 | } 40 | ``` 41 | 42 | **关于高危函数** 43 | 44 | - 一般来说,任何可以导致缓冲区溢出、整数溢出、空指针引用或其他形式的未定义行为的函数都可能是高风险的。在C/C++语言中,一些常见的例子包括`gets()`, `scanf()`, `strcpy()`, `strcat()`, `sprintf()`等,这些函数在使用不当时可能会导致安全问题。 45 | - 对于C++中的STL中来说,使用iterator可能会导致不安全的行为,这个通常会在进行循环的时候会用到,我们一般情况下可以用at()函数或者使用类似于for(auto element : elements),以及特定的STL自带的库函数。 -------------------------------------------------------------------------------- /problems/mysql为啥会产生死锁呢?如何避免他?.md: -------------------------------------------------------------------------------- 1 | 可能产生死锁的几个原因(其实都离不开死锁的产生条件,围绕这个回答即可): 2 | 3 | 1. **互斥条件**:一个资源每次只能被一个事务使用。 4 | 2. **占有且等待**:一个事务至少已经占有一个资源,且正在等待获取其他事务持有的资源。 5 | 3. **非强制性释放**:资源只能被占有它们的事务在完成任务后才释放。 6 | 4. **循环等待**:存在一种事务之间的循环等待关系。 7 | 8 | 为了避免死锁,可以采取以下措施: 9 | 10 | 1. **有序资源分配**:确保所有事务请求资源的顺序一致,减少循环等待的可能性。例如,可以按照表名、行ID的字典序来访问数据库资源。 11 | 2. **超时机制**:设置超时时间,当事务等待特定资源超过设定时间后自动回滚,从而打破等待状态。 12 | 3. **死锁检测与解除**:MySQL InnoDB 存储引擎提供了自动的死锁检测和处理机制,当检测到死锁时,会自动回滚事务中的某些操作来解除死锁。 13 | 4. **尽量使用行级锁**:行级锁比较细粒度,相对于表级锁而言,发生死锁的几率较小。 14 | 5. **减少事务大小和持续时间**:尽量避免长事务,因为它们更容易与其他事务发生资源竞争。确保数据库操作尽可能快地执行,并提交事务。 15 | 6. **避免不必要的锁定**:仅在必要时对数据进行锁定,减少不必要的SELECT FOR UPDATE或LOCK IN SHARE MODE。 16 | 7. **使用低隔离级别**:如果业务逻辑允许,可以考虑将事务的隔离级别设置得更低一些,降低隔离级别会减少加锁的范围。 17 | 8. **优化查询逻辑**:优化 SQL 查询语句,减少锁竞争,比如通过建立合适的索引来加速查询,从而减少锁定资源的时间。 18 | 19 | -------------------------------------------------------------------------------- /problems/mysql数据库中,产生的redolog都会直接写入磁盘吗?.md: -------------------------------------------------------------------------------- 1 | 这个问题实际上是在考redo log的结构 2 | 3 | Redo Log分为两个部分: 4 | 5 | 1. **内存中的日志缓冲区**:事务的修改首先记录到内存中的日志缓冲区。 6 | 2. **磁盘上的日志文件**:日志缓冲区中的数据会在特定条件下刷新到磁盘上的日志文件中。这个刷新操作可以是因为以下几个原因触发的: 7 | - **日志缓冲区满**:当日志缓冲区接近或达到其容量限制时,其中的数据会被刷新到磁盘。 8 | - **事务提交**:当一个事务提交时,为了保证其持久性,与该事务相关的日志会被写入到磁盘。但由于性能的考虑,MySQL可能会延迟物理写入到磁盘,并使用组提交来提高效率。 9 | - **定期刷新**:MySQL配置中有一个`innodb_flush_log_at_trx_commit`参数,它控制着Redo Log是如何从日志缓冲区刷新到磁盘的。这个参数有三个值: 10 | - `0`:每秒将日志缓冲区的内容写入到磁盘一次,不考虑事务何时提交。 11 | - `1`:每次事务提交都将日志缓冲区的内容写入到磁盘,这可以提供最高的耐故障能力。 12 | - `2`:每次事务提交,日志缓冲区的内容只是写入到操作系统的缓冲区中,然后由操作系统来决定何时写入到磁盘。 13 | - **其他情况**:比如执行了FLUSH LOGS命令,或者系统执行了检查点操作。 14 | 15 | 因此,不是所有产生的Redo Log都会直接写入磁盘;这**取决于MySQL的配置和当前的系统状态**。**在高并发的环境下,MySQL会尝试优化I/O操作以提高性能,但这可能会稍微降低数据的持久性。**如果你需要保证每个事务的持久性,应该设置`innodb_flush_log_at_trx_commit`为`1`,这样就可以确保每次事务提交时Redo Log都会被刷新到磁盘。 -------------------------------------------------------------------------------- /problems/mysql架构是什么样的?.md: -------------------------------------------------------------------------------- 1 | 1. 客户端连接层: 这是MySQL最上层,主要负责连接处理、授权认证、安全等功能。当客户端连接MySQL服务器时,该层会对其进行身份验证。如果通过验证,就会创建一个新的线程用来处理这个连接的请求。 2 | 2. 查询解析和查询优化器层: 在这一层,MySQL会解析SQL语句,生成查询的执行计划,并对其进行优化。它包括SQL解析器(把SQL语句解析为一个查询树)、预处理器(对查询进行一些补充工作比如补全字段名、表名等)和查询优化器(决定如何访问数据,即选择最佳的执行计划)。 3 | 3. 存储引擎层: 这是MySQL的核心层,主要处理MySQL中数据的存储和提取。它定义了数据的存储方式,但并不关心数据中的具体内容。MySQL的插件式存储引擎架构在这一层为不同的存储引擎提供了统一的API接口,使得不同的存储引擎可以按照自己的方式管理数据和索引。常见的存储引擎有:InnoDB、MyISAM、Memory等。 4 | 4. 插件式存储引擎: MySQL支持多种存储引擎,每种存储引擎都有各自的特性。例如,InnoDB支持事务处理以及行级锁定,而MyISAM则更适合于只读查询,因为它的设计重点是高速查找和缓存。 5 | 5. 文件系统: MySQL需要依赖于操作系统的文件系统来存储所有的表格和索引数据。这些文件可能被直接放置在文件系统中,或者被数据库引擎管理。 -------------------------------------------------------------------------------- /problems/mysql的索引都有那些?.md: -------------------------------------------------------------------------------- 1 | 1. **B-Tree索引**: 2 | - 这是最常见的索引类型,它适用于全键值、键值范围和键值排序操作。 3 | - 大部分MySQL存储引擎都支持B-Tree索引,包括InnoDB、MyISAM、Memory等。 4 | 2. **哈希索引**: 5 | - 哈希索引基于哈希表实现,只有精确匹配索引所有列的查询才有效。 6 | - 它们通常用于等值比较,如使用`=`, `IN()`, 和`<=>`操作符的查询。 7 | - Memory存储引擎使用哈希索引作为默认索引类型。 8 | 3. **FULLTEXT(全文)索引**: 9 | - 全文索引用于对文本内容进行全文搜索。 10 | - 它只适用于CHAR、VARCHAR或TEXT列。 11 | - InnoDB和MyISAM存储引擎支持全文索引。 -------------------------------------------------------------------------------- /problems/mysql索引失效有哪几种情况?.md: -------------------------------------------------------------------------------- 1 | 1. **使用“!=”或“<>”操作符:**对索引列使用这些操作符,将遍历全表数据,所以索引会失效。 2 | 2. **对索引列进行计算或函数操作:**如果在查询时对索引列进行了运算或者使用了函数,那么数据库无法使用该索引。 3 | 3. **隐式类型转换:**如果查询条件中的数据类型与索引列的数据类型不一致,MySQL会自动进行类型转换,这种情况下索引可能会失效。 4 | 4. **联合索引未使用最左前缀原则:**在使用联合索引时,如果未按照索引的顺序进行查询,或者未使用到联合索引的第一个字段,那么索引也会失效。 5 | 5. **like语句以通配符开头:**对于`LIKE`关键字,在使用模糊查询时,如果以'%'开头,则索引会失效而进行全表扫描。 6 | 6. **NULL值问题:**对含有空值的列进行索引,那么在查询时如果包括NULL值,可能导致索引失效。 7 | 7. **数据分布不均匀:**当表中某个数据段的值过于集中时(例如性别字段),即使该列建立了索引,优化器也可能不会选择使用它,因为全表扫描的代价可能更低。 -------------------------------------------------------------------------------- /problems/override和overload的区别有那些.md: -------------------------------------------------------------------------------- 1 | 函数重载(Overload):当一个作用域内有两个或更多个函数名相同但参数列表不同的函数时,我们就说这些函数构成了重载。参数列表不同可以是参数数量不同,也可以是参数类型不同。函数重载使得我们可以使用一样的函数名来完成类似的操作,提高代码的可读性和易用性。 2 | 3 | 示例: 4 | 5 | ```c++ 6 | void foo(int a); 7 | void foo(double a); 8 | ``` 9 | 10 | 函数覆盖(Override):当一个派生类声明了一个与基类中虚函数完全相同(函数名、参数类型和个数、常量属性、返回值类型)的函数时,我们就说派生类的这个函数覆盖了基类的虚函数。这使得我们可以通过基类指针或引用来调用派生类的函数,实现多态。 11 | 12 | 示例: 13 | 14 | ```c++ 15 | class Base { 16 | public: 17 | virtual void foo(int a); 18 | }; 19 | 20 | class Derived : public Base { 21 | public: 22 | void foo(int a) override; // 覆盖基类的虚函数foo 23 | }; 24 | ``` 25 | 26 | -------------------------------------------------------------------------------- /problems/selectpollepoll分别讲讲.md: -------------------------------------------------------------------------------- 1 | 1. **select**: 2 | - select是最早的多路复用IO模型之一,通过select系统调用可以同时监视多个文件描述符的IO事件。 3 | - 在使用select时,需要将需要监视的文件描述符加入到一个文件描述符集合中,并指定监视的IO事件类型(读、写、异常等)。 4 | - select会阻塞当前进程,直到有文件描述符就绪或超时等事件发生,然后返回就绪文件描述符的数量,应用程序需要遍历就绪文件描述符进行处理。 5 | 2. **poll**: 6 | - poll是对select的改进,在Linux系统中更常见,也可以同时监视多个文件描述符的IO事件。 7 | - 使用poll时,将需要监视的文件描述符加入到pollfd结构体数组中,并指定监视的IO事件类型。 8 | - poll调用会阻塞当前进程,直到有文件描述符就绪或超时等事件发生,然后返回就绪文件描述符的数量,应用程序需要遍历就绪文件描述符进行处理。 9 | 3. **epoll**: 10 | - epoll是Linux特有的高性能IO多路复用机制,相比select和poll具有更好的性能。 11 | - epoll使用基于事件驱动的方式,通过epoll_create创建一个epoll句柄,将需要监听的文件描述符加入到epoll句柄中。 12 | - 当有IO事件发生时,epoll_wait系统调用不会阻塞整个进程,而是立即返回就绪的文件描述符列表,应用程序只需处理这些就绪文件描述符。 13 | - epoll还支持边缘触发(EPOLLET)和水平触发(EPOLLONESHOT)两种工作模式,可以进一步提高IO效率。 -------------------------------------------------------------------------------- /problems/select模型和poll模型epoll模型.md: -------------------------------------------------------------------------------- 1 | 下面只是一个简单的介绍,比较适合面试的时候概括: 2 | 1. **select模型**:这是最古老的一种IO多路复用模型。它的主要功能是**监视多个文件描述符**(在网络编程中,文件描述符通常代表一个socket连接),**直到其中一个文件描述符准备好进行某种IO操作(如读或写)为止。**使用select模型的**优点是跨平台性好**,基本上所有的操作系统都支持。但是它有一些明显的**缺点,如单个进程能够监视的文件描述符数量有限(通常是1024),处理效率较低**(每次调用select都需要遍历所有的文件描述符),以及它不能随着连接数的增加而线性扩展。 3 | 2. **poll模型**:**poll模型和select模型非常相似,但它没有最大文件描述符数量的限制**。和select一样,poll每次调用时也需要遍历所有的文件描述符,同样不能随着连接数的增加而线性扩展。 4 | 3. **epoll模型**:这是一个在Linux 2.6及以后版本中引入的新型IO多路复用模型。与select和poll相比,epoll在处理大量并发连接时更高效。它默认使用了一个事件驱动的方式(ET),**以红黑树作为底层的数据结构**,只有当某个文件描述符准备好进行IO操作时,它才会将这个文件描述符添加到就绪列表中,这避免了遍历所有文件描述符的开销。另外,epoll没有最大文件描述符数量的限制,因此它可以处理更多的并发连接。 5 | 6 | 每种模型都有其优点和缺点,选择哪种模型取决于你的具体应用和环境。 -------------------------------------------------------------------------------- /problems/set,mutiset,map,mutimap之间都有什么区别?.md: -------------------------------------------------------------------------------- 1 | 1. `set`: 2 | - 存储唯一键值的集合,即不允许重复的元素。 3 | - 元素本身就是键值。 4 | - 元素按照特定顺序存储(默认为递增顺序)。 5 | 2. `multiset`: 6 | - 类似于 `set`,但它允许重复的键值,即可以有多个相等的元素。 7 | - 元素同样按照特定顺序存储。 8 | 3. `map`: 9 | - 存储键值对(pair),每个键值对由一个键和一个值组成。 10 | - 每个键在 `map` 中是唯一的,你不能有两个具有相同键的键值对。 11 | - 键值对按照键的顺序存储。 12 | 4. `multimap`: 13 | - 类似于 `map`,但它允许键不唯一,即可以有多个键值对拥有相同的键。 14 | - 键值对同样按照键的顺序存储。 15 | 16 | 使用情况概述: 17 | 18 | - 当你需要存储不重复元素的有序集合时,使用 `set`。 19 | - 当你需要存储可能重复元素的有序集合时,使用 `multiset`。 20 | - 当你需要维护一组键到值的映射,并且每个键只能关联一个值时,使用 `map`。 21 | - 当你需要维护一组键到值的映射,并且一个键可以关联多个值时,使用 `multimap`。 22 | 23 | 所有这四种类型的底层实现通常是红黑树,除了提供插入、删除和搜索操作外,还保证了元素的有序性。然而,如果你不需要元素的排序,并且关注更高效的插入和查找性能,可以选择使用基于哈希表实现的 `unordered_set`, `unordered_multiset`, `unordered_map`, `unordered_multimap` 等无序容器。 -------------------------------------------------------------------------------- /problems/socket什么情况下可读?.md: -------------------------------------------------------------------------------- 1 | 在网络编程中,`socket` 可读的情况通常指的是套接字上存在可供读取的数据或者在某些特定情况下表明套接字的状态发生了变化。以下是几种情况下一个套接字可能被认为是可读的: 2 | 3 | 1. **数据到达**:远程发送方向本地套接字发送了数据,并且这些数据已经到达本地缓冲区,等待被读取。 4 | 2. **连接建立**:对于非阻塞的套接字,在执行`connect()`操作后,当连接成功建立时,套接字变为可读。 5 | 3. **监听套接字**:对于处于监听状态的服务器套接字(例如,执行了`listen()`调用的TCP服务器),当有新的连接请求到达时,套接字变为可读。 6 | 4. **连接关闭**:当远程端点关闭了连接,即发送了FIN包,本地套接字会收到一个空的数据段,此时套接字可读以允许本地程序识别到关闭事件。 7 | 5. **错误或中断**:如果在套接字上发生了错误或者其他异常情况,它可能也会被标记为可读,以便应用程序可以通过检查套接字的状态或者进行错误处理。 8 | 6. **紧急数据**:对于支持OutOf-Band(OOB)数据的协议(如TCP),当有紧急数据到达时,套接字也可能被标记为可读。 9 | 10 | 要检测一个套接字是否可读,可以使用多种方法,包括使用`select()`、`poll()`或`epoll()`等系统调用,在给定的一组套接字上等待事件发生,从而在非阻塞的情况下有效地管理多个连接。这些调用都可以告诉你哪些套接字是可读的,哪些是可写的,以及是否有任何异常情况需要注意。 -------------------------------------------------------------------------------- /problems/socket服务端的实现,select和epoll的区别.md: -------------------------------------------------------------------------------- 1 | ### **Socket服务端示例:** 2 | 3 | 一般来讲,我们要实现socket的时候,有五个关键的的步骤: 4 | 5 | - 创建socket:使用`socket()`系统调用创建一个新的socket文件描述符。 6 | - 绑定socket:使用`bind()`系统调用将新创建的socket绑定到一个地址和端口上。对于服务器程序来说,这个地址通常是服务器的IP地址,端口是你希望服务器监听的端口。 7 | - 监听连接:使用`listen()`系统调用使得socket进入监听模式,等待客户端的连接请求。 8 | - 接受连接:当一个客户端连接请求到来时,可以使用`accept()`系统调用接受这个连接,并获取一个新的socket文件描述符,这个描述符代表了服务器与客户端之间的连接。 9 | - 读写数据:通过`read()`和`write()`或者`send()`、`recv()`系统调用在连接上读取或写入数据。 10 | 11 | 以上就是一个最基本的Socket服务器的工作流程。当然,在实际的应用开发中,根据应用的需要,可能还会使用更多的系统调用和库函数,例如使用`select()`, `poll()` 或 `epoll()` 处理多个并发连接,使用线程或进程来并行处理多个连接等。 12 | 13 | 14 | 15 | 一个简单的示例: 16 | 17 | ```c++ 18 | #include 19 | #include 20 | #include 21 | #include 22 | 23 | #define MAX_BUFFER_SIZE 4096 24 | #define PORT 8080 25 | 26 | int main() { 27 | int server_fd, new_socket; 28 | struct sockaddr_in address; 29 | int opt = 1; 30 | int addrlen = sizeof(address); 31 | char buffer[MAX_BUFFER_SIZE] = {0}; 32 | 33 | // 创建 socket 文件描述符 34 | if ((server_fd = socket(AF_INET, SOCK_STREAM, 0)) == 0) { 35 | std::cerr << "socket failed" << std::endl; 36 | exit(EXIT_FAILURE); 37 | } 38 | 39 | // 绑定 socket 到 localhost 的 8080 端口 40 | address.sin_family = AF_INET; 41 | address.sin_addr.s_addr = INADDR_ANY; 42 | address.sin_port = htons(PORT); 43 | 44 | if (bind(server_fd, (struct sockaddr *)&address, sizeof(address)) < 0) { 45 | std::cerr << "bind failed" << std::endl; 46 | exit(EXIT_FAILURE); 47 | } 48 | 49 | // 使服务器开始监听,这里我们设定最大待处理连接数为 3 50 | if (listen(server_fd, 3) < 0) { 51 | std::cerr << "listen failed" << std::endl; 52 | exit(EXIT_FAILURE); 53 | } 54 | 55 | while(1) { 56 | std::cout << "\nWaiting for a connection..." << std::endl; 57 | 58 | // 接受客户端连接 59 | if ((new_socket = accept(server_fd, (struct sockaddr *)&address, (socklen_t*)&addrlen))<0) { 60 | std::cerr << "accept failed" << std::endl; 61 | exit(EXIT_FAILURE); 62 | } 63 | 64 | // 清空 buffer 65 | memset(buffer, 0, MAX_BUFFER_SIZE); 66 | 67 | // 读取客户端发送的数据 68 | int valread = read(new_socket , buffer, MAX_BUFFER_SIZE); 69 | std::cout << "Client says: " << buffer << std::endl; 70 | 71 | // 向客户端发送消息 72 | send(new_socket , "Hello from server" , strlen("Hello from server") , 0 ); 73 | } 74 | 75 | return 0; 76 | } 77 | 78 | ``` 79 | 80 | 81 | 82 | ### select、poll和epoll的区别 83 | 84 | 当有多个计算机对我们的服务端发起链接请求的时候,我们需要处理这些请求,于是我们就有了这些处理的方法,当然,他们是最常见的,面试常考的 85 | 86 | 1. select: 87 | - **select工作方式是轮询检查**用户所关心的**socket是否就绪**,然后进行处理。由于每次调用select都要在用户态和内核态之间进行切换,且需要重新传递数据结构,所以效率较低。 88 | - **select对socket数量也有限制,默认是1024个**,因为它使用了位图的方式来存储socket信息。 89 | - select无法扩展到大规模并发连接,主要是因为它采用线性遍历的方式来处理事件,同时每次调用select都需要遍历全部的文件描述符。 90 | 2. poll: 91 | - **poll没有最大文件描述符数量的限制**。在select中,由于它使用了位图的方式来存储文件描述符,所以默认的最大数目是1024。而**poll使用链表来存储**,所以理论上只受限于系统内存。 92 | - poll提供了更多的事件类型,并且它的事件类型是通过结构体的方式进行传递的,这远比select的位操作要清晰很多。 93 | - 当文件描述符就绪时,select需要重新设置所有文件描述符,但poll只需要关注那些就绪的文件描述符。 94 | 3. epoll 95 | - **epoll是Linux特有的I/O多路复用机制,相比于select和poll,通过使用基于红黑树的底层数据结构使得epoll更加灵活且没有最大并发限制。** 96 | - epoll使用一个文件描述符管理多个事件,通过系统调用epoll_ctl注册fd,然后epoll_wait获取已经准备好的描述符。 97 | - epoll中的系统调用会将所有监听的socket进行逻辑上的分组,应用程序只需要检查已经就绪的socket。这样在实际开发中,可以根据应用程序的实际需求,只监听并处理那些真正需要关注的事件,而无需像select和poll那样每次都进行全量的轮询。 98 | - epoll的效率比select高,主要体现在大量并发连接的处理上。 -------------------------------------------------------------------------------- /problems/time_wait,close_wait状态产生原因,keepalive.md: -------------------------------------------------------------------------------- 1 | `TIME_WAIT`和`CLOSE_WAIT`是TCP连接处于关闭过程中的两种状态。下面分别解释它们产生的原因以及TCP Keepalive机制。 2 | 3 | **TIME_WAIT** 4 | 5 | `TIME_WAIT`状态发生在四次挥手过程的最后阶段。当一方(如客户端)收到另一方发送的具有FIN标志的TCP包,它会发送一个带有ACK标志的TCP包作为确认,并进入`TIME_WAIT`状态。之所以存在这个状态,主要有以下原因: 6 | 7 | 1. 确保对方收到最后一个带有ACK标志的TCP包。如果对方没有收到这个包,它会重新发送FIN包。在`TIME_WAIT`状态期间,如果收到重发的FIN包,可以再次发送ACK包进行确认。 8 | 2. 避免老的数据包干扰新连接。由于网络延迟等原因,已结束连接的数据包可能在网络上滞留一段时间。`TIME_WAIT`状态能够阻止新连接在短时间内使用相同的源和目标IP地址及端口号,从而避免旧数据包干扰新连接。 9 | 10 | `TIME_WAIT`状态默认持续2倍Maximum Segment Lifetime(MSL),通常为1-4分钟。经过这段时间后,连接被彻底关闭。 11 | 12 | **CLOSE_WAIT** 13 | 14 | `CLOSE_WAIT`状态出现在接收到对方发送的带有FIN标志的TCP包时。当一方(如服务器)收到请求关闭连接的FIN包后,它会发送一个带有ACK标志的TCP包进行确认,并进入`CLOSE_WAIT`状态。然后,这一方需要等待应用程序关闭套接字,之后才能发送自己的FIN包并进入`LAST_ACK`状态。 15 | 16 | 如果某个连接长时间停留在`CLOSE_WAIT`状态,通常表示应用程序没有正确关闭套接字。这可能导致资源泄漏和性能问题。为解决这个问题,需要检查应用程序逻辑确保套接字被正确关闭。 17 | 18 | **TCP Keepalive** 19 | 20 | TCP Keepalive是一种可选的心跳机制,用于检测对方是否仍然活跃。当连接在一定时间内没有数据传输时,Keepalive能够确定对方是否仍然可达,从而避免因对方意外断开而导致的长时间无响应。 21 | 22 | 在Keepalive启用的情况下,系统会在连接空闲期间周期性地发送探测包。如果连续发送多个探测包都没有得到ACK响应,则认为连接已断开,并将其关闭。Keepalive参数(如空闲时间、探测间隔和失败尝试次数)可以根据实际需求进行配置。**一般为八小时** 23 | 24 | 需要注意的是,并非所有场景都需要启用Keepalive。在一些情况下,如HTTP长轮询或WebSockets,应用层协议自身就具备类似的心跳检测功能,此时不必启用Keepalive。 -------------------------------------------------------------------------------- /problems/union和join.md: -------------------------------------------------------------------------------- 1 | **1. UNION:** 2 | 3 | - **作用**: 4 | - `UNION` 操作用于合并两个或多个SELECT语句的结果集,并消除重复行。 5 | - **特点**: 6 | - 参与 `UNION` 操作的每个SELECT语句都必须拥有相同数量的列,且对应列的数据类型也需相似,以便可以进行合并。 7 | - 结果中的每一列的数据类型是由所有查询中对应列的数据类型“兼容得出”的数据类型(通常取其共同超类)。 8 | - 默认情况下,`UNION` 对合并结果进行去重处理(相当于执行了`DISTINCT`),只返回不重复的记录。 9 | - 如果想要包含所有的重复记录,可以使用 `UNION ALL`,这样可以提高查询效率,因为不需要进行去重操作。 10 | 11 | **示例**: 12 | 13 | ```sql 14 | SELECT column_name(s) FROM table1 15 | UNION 16 | SELECT column_name(s) FROM table2; 17 | ``` 18 | 19 | **2. JOIN:** 20 | 21 | - **作用**: 22 | - `JOIN` 操作用于根据两个表中的共同字段,将这些表中的行组合起来形成一个新的结果集。 23 | - **特点**: 24 | - `JOIN` 操作可以是内连接(`INNER JOIN`)、左连接(`LEFT JOIN`)、右连接(`RIGHT JOIN`)、全连接(`FULL JOIN`)等,根据不同的需求组合表中的数据。 25 | - 在进行 `JOIN` 时,通常需要在 `ON` 子句中指定连接条件,即定义如何匹配来自连接表的行。 26 | - `JOIN` 操作不仅可以合并行,还能从两个表中选择所需的列,创建一个由这些列组成的新结果集。 27 | 28 | **示例**: 29 | 30 | ```sql 31 | SELECT columns 32 | FROM table1 33 | INNER JOIN table2 34 | ON table1.column_name = table2.column_name; 35 | ``` 36 | 37 | 总结而言,**`UNION` 主要用于垂直地合并相似结构表的数据,而 `JOIN` 主要用于水平地合并不同表中相关联的行。**二者都是数据库查询时非常有用的工具,但适用于不同的情境和需求。 -------------------------------------------------------------------------------- /problems/volatile,static,const,extern等关键字.md: -------------------------------------------------------------------------------- 1 | 2 | # volatile作用 3 | 4 | Volatile关键词的第一个特性:易变性。所谓的易变性,在汇编层面反映出来,就是两条语句,下一条语句不会直接使用上一条语句对应的volatile变量的寄存器内容,而是重新从内存中读取。 5 | 6 | Volatile关键词的第二个特性:“不可优化”特性。volatile告诉编译器,不要对我这个变量进行各种激进的优化,甚至将变量直接消除,保证程序员写在代码中的指令,一定会被执行。 7 | Volatile关键词的第三个特性:”顺序性”,能够保证Volatile变量间的顺序性,编译器不会进行乱序优化。 8 | C/C++ Volatile变量,与非Volatile变量之间的操作,是可能被编译器交换顺序的。C/C++ Volatile变量间的操作,是不会被编译器交换顺序的。哪怕将所有的变量全部都声明为volatile,哪怕杜绝了编译器的乱序优化,但是针对生成的汇编代码,CPU有可能仍旧会乱序执行指令,导致程序依赖的逻辑出错,volatile对此无能为力 9 | 针对这个多线程的应用,真正正确的做法,是构建一个happens-before语义。 10 | 11 | 12 | # static 13 | 14 | 控制变量的存储方式和可见性。 15 | 16 | (1)修饰局部变量 17 | 18 | 一般情况下,对于局部变量是存放在栈区的,并且局部变量的生命周期在该语句块执行结束时便结束了。但是如果用static进行修饰的话,该变量便存放在静态数据区,其生命周期一直持续到整个程序执行结束。但是在这里要注意的是,虽然用static对局部变量进行修饰过后,其生命周期以及存储空间发生了变化,但是其作用域并没有改变,其仍然是一个局部变量,作用域仅限于该语句块。 19 | 20 | (2)修饰全局变量 21 | 22 | 对于一个全局变量,它既可以在本源文件中被访问到,也可以在同一个工程的其它源文件中被访问(只需用extern进行声明即可)。用static对全局变量进行修饰改变了其作用域的范围,由原来的整个工程可见变为本源文件可见。 23 | 24 | (3)修饰函数 25 | 26 | 用static修饰函数的话,情况与修饰全局变量大同小异,就是改变了函数的作用域。 27 | 28 | (4)C++中的static 29 | 30 | 如果在C++中对类中的某个函数用static进行修饰,则表示该函数属于一个类而不是属于此类的任何特定对象;如果对类中的某个变量进行static修饰,表示该变量为类以及其所有的对象所有。它们在存储空间中都只存在一个副本。可以通过类和对象去调用。 31 | 32 | # const的含义及实现机制 33 | 34 | const名叫常量限定符,用来限定特定变量,以通知编译器该变量是不可修改的。习惯性的使用const,可以避免在函数中对某些不应修改的变量造成可能的改动。 35 | 36 | (1)const修饰基本数据类型 37 | 38 | 1.const修饰一般常量及数组 39 | 40 | 基本数据类型,修饰符const可以用在类型说明符前,也可以用在类型说明符后,其结果是一样的。在使用这些常量的时候,只要不改变这些常量的值便好。 41 | 42 | 2.const修饰指针变量*及引用变量& 43 | 44 | 如果const位于星号*的左侧,则const就是用来修饰指针所指向的变量,即指针指向为常量; 45 | 46 | 如果const位于星号的右侧,const就是修饰指针本身,即指针本身是常量。 47 | 48 | (2)const应用到函数中, 49 | 50 | 1.作为参数的const修饰符 51 | 52 | 调用函数的时候,用相应的变量初始化const常量,则在函数体中,按照const所修饰的部分进行常量化,保护了原对象的属性。 53 | [注意]:参数const通常用于参数为指针或引用的情况; 54 | 55 | 2.作为函数返回值的const修饰符 56 | 57 | 声明了返回值后,const按照"修饰原则"进行修饰,起到相应的保护作用。 58 | 59 | (3)const在类中的用法 60 | 61 | 不能在类声明中初始化const数据成员。正确的使用const实现方法为:const数据成员的初始化只能在类构造函数的初始化表中进行 62 | 类中的成员函数:A fun4()const; 其意义上是不能修改所在类的的任何变量。 63 | 64 | (4)const修饰类对象,定义常量对象 65 | 常量对象只能调用常量函数,别的成员函数都不能调用。 66 | 67 | # extern 68 | 69 | 在C语言中,修饰符extern用在变量或者函数的声明前,用来说明“此变量/函数是在别处定义的,要在此处引用”。 70 | 71 | 注意extern声明的位置对其作用域也有关系,如果是在main函数中进行声明的,则只能在main函数中调用,在其它函数中不能调用。其实要调用其它文件中的函数和变量,只需把该文件用#include包含进来即可,为啥要用extern?因为用extern会加速程序的编译过程,这样能节省时间。 72 | 73 | 在C++中extern还有另外一种作用,用于指示C或者C++函数的调用规范。比如在C++中调用C库函数,就需要在C++程序中用extern “C”声明要引用的函数。这是给链接器用的,告诉链接器在链接的时候用C函数规范来链接。主要原因是C++和C程序编译完成后在目标代码中命名规则不同,用此来解决名字匹配的问题。 74 | 75 | -------------------------------------------------------------------------------- /problems/weak_ptr是如何解决shared_ptr循环引用的?.md: -------------------------------------------------------------------------------- 1 | 当两个对象相互引用并使用`shared_ptr`时,就会形成循环引用。例如,考虑一个简单的场景: 2 | 3 | ``` 4 | #include 5 | #include 6 | 7 | class B; // 前置声明 8 | 9 | class A { 10 | public: 11 | std::shared_ptr b_ptr; 12 | A() { std::cout << "A constructor" << std::endl; } 13 | ~A() { std::cout << "A destructor" << std::endl; } 14 | }; 15 | 16 | class B { 17 | public: 18 | std::shared_ptr a_ptr; 19 | B() { std::cout << "B constructor" << std::endl; } 20 | ~B() { std::cout << "B destructor" << std::endl; } 21 | }; 22 | 23 | int main() { 24 | std::shared_ptr a = std::make_shared(); 25 | std::shared_ptr b = std::make_shared(); 26 | 27 | a->b_ptr = b; 28 | b->a_ptr = a; 29 | 30 | return 0; 31 | } 32 | ``` 33 | 34 | 在这个例子中,类 `A` 拥有一个指向类 `B` 的 `shared_ptr`,而类 `B` 拥有一个指向类 `A` 的 `shared_ptr`。这样就形成了循环引用。 35 | 36 | 为了避免循环引用,我们可以改用 `weak_ptr` 来解决这个问题: 37 | 38 | ``` 39 | #include 40 | #include 41 | 42 | class B; // 前置声明 43 | 44 | class A { 45 | public: 46 | std::shared_ptr b_ptr; 47 | A() { std::cout << "A constructor" << std::endl; } 48 | ~A() { std::cout << "A destructor" << std::endl; } 49 | }; 50 | 51 | class B { 52 | public: 53 | std::weak_ptr a_weak_ptr; // 使用 weak_ptr 54 | B() { std::cout << "B constructor" << std::endl; } 55 | ~B() { std::cout << "B destructor" << std::endl; } 56 | }; 57 | 58 | int main() { 59 | std::shared_ptr a = std::make_shared(); 60 | std::shared_ptr b = std::make_shared(); 61 | 62 | a->b_ptr = b; 63 | b->a_weak_ptr = a; // 使用 weak_ptr 64 | 65 | return 0; 66 | } 67 | ``` 68 | 69 | 通过将类 `B` 中指向类 `A` 的指针改为 `weak_ptr`,我们成功地避免了循环引用问题。 -------------------------------------------------------------------------------- /problems/一条SQL语句在数据库框架中的执行过程.md: -------------------------------------------------------------------------------- 1 | 1. **连接**:首先,客户端需要通过网络连接到数据库服务器。这个连接过程可能涉及用户身份验证、权限检查等。 2 | 2. **解析**:接收到SQL查询后,数据库的解析器会检查SQL语句的语法和语义,确保它满足语法规则并且能被正确解释。如果有错误,就会生成一个错误消息并返回给客户端。 3 | 3. **预处理**:解析无误后,预处理器进一步检查SQL语句,比如检查所引用的表和列是否存在,用户是否有足够的权限进行查询等。 4 | 4. **优化**:然后,查询优化器尝试找出执行查询的最佳方式。优化器会考虑多种可能的执行计划,并根据统计信息选出成本最低(即最快)的那一个。这个过程也被称为查询优化。 5 | 5. **执行**:确定了执行计划后,数据库系统开始实际执行SQL语句。对于读操作(SELECT),数据库会从存储引擎抽取满足条件的数据并返回给客户端;对于写操作(INSERT, UPDATE, DELETE等),数据库不仅会修改相应的数据,还可能需要更新索引、写入日志等。 6 | 6. **返回结果**:最后,数据库系统将结果返回给客户端。对于查询语句,这通常是一组满足条件的数据;对于修改语句,可能是修改的行数或者某种成功的确认信号。 -------------------------------------------------------------------------------- /problems/一段代码从程序到执行经历怎么样的过程(程序在计算机中是如何运行起来的).md: -------------------------------------------------------------------------------- 1 | 1. **编写代码**: 2 | - 程序员使用文本编辑器或集成开发环境(IDE)编写源代码,实现所需功能。 3 | 2. **编译器编译**: 4 | - 源代码经过编译器(Compiler)翻译为目标代码,目标代码可以是机器码、字节码或其他形式。编译器将源代码转换为可执行程序的指令集。 5 | 3. **链接器链接**: 6 | - 如果程序由多个源文件组成,链接器(Linker)将这些源文件中的目标代码链接在一起,生成一个可执行文件。 7 | 4. **加载器加载**: 8 | - 当用户运行程序时,操作系统的加载器(Loader)负责将可执行文件装入内存。加载器解析可执行文件的头部信息,创建进程控制块(PCB),分配内存空间等。 9 | 5. **程序执行**: 10 | - CPU根据程序计数器(Program Counter)指向的地址,逐条执行内存中的指令。指令集包括算术运算、逻辑运算、条件跳转等操作。 11 | 6. **内存访问**: 12 | - 程序执行过程中,访问的数据和指令都存储在内存中。CPU通过内存管理单元(MMU)将虚拟地址映射为物理地址,实现对内存的读写操作。 13 | 7. **系统调用**: 14 | - 程序可能需要与操作系统进行交互,如请求IO操作、创建新进程等。此时,程序通过系统调用进入内核态执行相应的系统服务。 15 | 8. **异常处理**: 16 | - 在程序执行过程中,可能发生各种异常情况,如除零错误、内存访问越界等。操作系统会捕获这些异常并进行相应的处理,如终止进程或给出错误提示。 17 | 9. **结束执行**: 18 | - 程序执行完毧后,操作系统回收资源,释放进程占用的内存空间,并通知用户程序执行完毕 -------------------------------------------------------------------------------- /problems/为什么AOF后台重写和BGSAVE命令都用子进程而不是线程?.md: -------------------------------------------------------------------------------- 1 | 在Redis中,AOF后台重写和BGSAVE命令都使用子进程而不是线程的主要原因有几点: 2 | 3 | 1. **数据隔离**:子进程拥有独立的地址空间,与父进程完全隔离,可以避免由于共享内存造成的数据不一致性问题。如果使用线程,在多线程环境下可能会出现竞态条件和数据共享导致的数据错误。 4 | 2. **稳定性**:使用子进程可以提高系统的稳定性。如果一个线程发生了异常导致崩溃,可能会影响整个进程的稳定性;而如果是子进程发生了异常,只会影响当前子进程,不会影响到父进程。 5 | 3. **并发处理**:使用子进程能够更好地实现并发处理,尤其适合于CPU密集型的任务。每个子进程都有自己的CPU时间片和寄存器上下文,能够更好地利用多核处理器的优势。 6 | 4. **资源管理**:线程共享相同的资源空间,例如打开的文件描述符、信号处理等,这可能会增加复杂性和潜在的风险。而子进程有独立的资源管理,更容易进行控制和管理。 7 | 8 | 综上所述,虽然线程在某些情况下可能会更轻量级且更高效,但使用子进程在数据隔离、系统稳定性、并发处理和资源管理方面有更多的优势,尤其是对于需要进行IO密集操作或者对数据完整性要求较高的场景,选择使用子进程更为安全和可靠。因此,Redis选择使用子进程来执行AOF后台重写和BGSAVE命令。 -------------------------------------------------------------------------------- /problems/为什么CPU访问寄存器的速度比访问内存或CPUCache的速度快.md: -------------------------------------------------------------------------------- 1 | 1. **距离和接近性**:寄存器是位于CPU核心内部的存储单元,与CPU核心之间的物理距离非常近,信号传输路径短,因此访问速度非常快。而内存和CPU Cache则位于CPU核心外部,需要通过数据总线等较长的物理路径进行访问,导致访问速度相对较慢。 2 | 2. **存储介质速度**:寄存器通常采用静态随机存取存储器(SRAM)构成,速度非常快,可以在一个时钟周期内完成读写操作;而内存和CPU Cache采用动态随机存取存储器(DRAM)构成,速度相对较慢,需要多个时钟周期才能完成读写操作。 3 | 3. **访问控制**:由于寄存器位于CPU核心内部,可以直接被CPU访问,访问控制开销很小;而内存和CPU Cache需要通过总线进行访问,存在访问控制和协议处理等额外开销,使得访问速度相对较慢。 4 | 4. **容量大小**:寄存器的容量一般很小,只能存储少量数据;而内存和CPU Cache容量较大,能够存储更多的数据,但也因此访问速度相对较慢。 -------------------------------------------------------------------------------- /problems/为什么DNS会使用UDP而不使用TCP呢?.md: -------------------------------------------------------------------------------- 1 | DNS(域名系统)主要使用UDP(用户数据报协议)而不是TCP(传输控制协议)出于以下几个原因: 2 | 3 | 1. **效率和速度**:DNS请求通常非常小,只需要一个小的查询包和响应。UDP不像TCP那样需要建立连接,这意呀着使用UDP可以更快完成查询。没有三次握手的过程,减少了通信延迟。 4 | 2. **资源消耗较少**:UDP是无连接的,服务器不需要维持连接状态(例如,连接的建立、存活、终止等),这就减少了服务器的资源消耗。对于高频率访问的DNS服务来说,这一点尤其重要。 5 | 3. **简单性**:UDP的协议比TCP简单得多,处理起来更为直接和轻量级。对于标准的DNS查询,这种简洁性意味着实现起来更加容易且高效。 6 | 4. **广泛兼容**:DNS设计之初就是基于UDP的,因此大部分网络设备和系统都默认支持通过UDP进行DNS查询。这保证了它的广泛兼容性和可靠性。 7 | 8 | 然而,也存在一些情况下DNS会使用TCP,比如: 9 | 10 | - **当响应数据超过512字节时**:标准的DNS通过UDP响应有大小限制(通常是512字节)。如果响应数据超过这个限制,就会使用TCP传输,因为TCP可以处理更大的数据包。 11 | - **区域传输(AXFR/IXFR)**:在DNS服务器之间同步数据库(区域文件)时,会使用TCP,因为这些信息通常远大于UDP所能支持的大小。 12 | - **DNS安全考虑(DNSSEC)**:DNSSEC提供了对DNS响应的验证,以确保它们未被篡改。由于DNSSEC增加了响应体积,因此可能需要使用TCP来传输更大的响应数据。 -------------------------------------------------------------------------------- /problems/为什么Redis使用跳表而不是红黑树来实现Zset.md: -------------------------------------------------------------------------------- 1 | Redis在实现ZSet(有序集合)时选择使用跳表而不是红黑树的主要原因包括: 2 | 3 | 1. **实现简单**:跳表相对于红黑树来说,实现更加简单和直观。跳表的数据结构相对较为简单,易于理解和实现,而红黑树则需要复杂的平衡调整操作,实现难度较大。 4 | 2. **性能稳定**:虽然红黑树在理论上的时间复杂度(O(log n))比跳表略优,但跳表在实际应用中的性能表现通常更加稳定。跳表无需频繁的旋转和颜色调整等操作,对于插入、删除等操作的性能更加稳定可靠。 5 | 3. **空间占用**:相比红黑树,跳表的实现更加简洁,不需要维护额外的颜色信息和指针关系,因此在一定程度上节省了空间。这对于内存占用敏感的场景而言尤为重要。 6 | 4. **易扩展性**:跳表的结构天然支持并发访问和快速查找,适合于分布式环境下的并发操作。而红黑树在分布式环境下的并发性能可能会受到一定影响。 -------------------------------------------------------------------------------- /problems/为什么Zset需要同时使用跳表和字典来实现?.md: -------------------------------------------------------------------------------- 1 | Redis的ZSet(有序集合)需要同时使用跳表和字典来实现,主要是为了综合利用二者各自的优势,以提高数据结构的性能和效率,具体原因包括: 2 | 3 | 1. **有序性和快速查找**:跳表(Skip List)作为一种有序数据结构,可以保证元素按照分数有序存储。而哈希表(字典)则提供了快速的查找操作,通过元素的键值直接定位元素,时间复杂度为O(1)。结合跳表和哈希表,可以同时满足有序性和快速查找的需求。 4 | 2. **元素唯一性**:跳表可以保证集合中元素的唯一性,即相同的元素只会出现一次。在ZSet中,每个元素在跳表中对应一个节点,在哈希表中对应一个键值对,通过这两者的配合实现了元素唯一性。 5 | 3. **排序和范围查询**:ZSet常用于需要按照分数排序和根据分数范围查询元素的场景。跳表提供了按照分数有序排列的功能,使得范围查询操作更加高效。 6 | 4. **内存占用和性能平衡**:跳表在实现时相对比较灵活,但消耗的额外内存较多;而哈希表可以高效地存储键值对,但无法保证有序性。利用跳表和哈希表的结合,可以在内存占用和性能之间取得一个平衡,既保证了有序性又提升了查找效率。 -------------------------------------------------------------------------------- /problems/为什么有了进程还需要线程和协程?.md: -------------------------------------------------------------------------------- 1 | 尽管进程提供了资源隔离和独立的执行环境,但它们的创建和管理相对较重,不适用于需要频繁交互或共享状态的场景。线程作为轻量级的进程,提供了更快的上下文切换和高效的资源共享,使得在同一进程内可以有多个并发执行流。然而,线程的调度仍然受操作系统控制,可能涉及到**用户态和内核态的切换开销**。 2 | 3 | 协程进一步降低了并发编程的复杂度和开销,**因为它完全是在用户态下使用的**,尤其在 I/O 密集型任务和微服务架构中表现出色,它们提供了更细粒度的操作和更高效的CPU利用率。由于这些优势,现代编程语言和框架越来越多地采用协程来处理并行和异步任务。 -------------------------------------------------------------------------------- /problems/为什么用户态和内核态的相互切换过程开销比较大.md: -------------------------------------------------------------------------------- 1 | 1. **上下文切换**:在用户态和内核态之间切换时,需要保存当前进程或线程的上下文信息,包括程序计数器、寄存器状态、栈指针等。这些上下文信息保存在内存中或者内核数据结构中,在切换完成后需要再次加载回来。这个过程会消耗一定的时间和资源。 2 | 2. **权限切换**:用户态和内核态具有不同的权限级别,内核态拥有更高的特权级别,可以执行一些用户态不可访问的指令和操作。因此,从用户态切换到内核态需要进行特权级别的变更,这个过程也会增加开销。 3 | 3. **地址空间切换**:用户态和内核态通常有不同的地址空间,为了保护操作系统内核不受用户程序的直接影响,当切换到内核态时需要进行地址空间的切换和映射,这也会增加开销。 4 | 4. **TLB刷新**:TLB是CPU中的一种缓存,用于存储虚拟地址到物理地址的映射关系。当发生用户态和内核态的切换时,可能导致TLB中的缓存无效,需要进行TLB刷新,这会引起一定的性能损失。 -------------------------------------------------------------------------------- /problems/为什么需要allocator?他在STL中有什么作用?.md: -------------------------------------------------------------------------------- 1 | ### 1. 抽象化内存管理 2 | 3 | - `allocator` 提供了一个抽象层,使得容器能够专注于数据结构和算法的实现,而不必担心具体的内存分配和回收细节。这样,容器的设计和实现就可以独立于底层的内存管理机制。 4 | 5 | ### 2. 提供统一的接口 6 | 7 | - 所有的 STL 容器都使用相同的 `allocator` 接口来分配和释放内存。这提供了一致性,并且使得开发者在需要时能够更容易地替换默认的内存分配策略。 8 | 9 | ### 3. 支持自定义内存管理策略 10 | 11 | - 通过自定义 `allocator`,开发者可以根据应用程序的特定需求调整内存分配策略。例如,在特定场景下可能需要一个高性能的内存池分配器,或者跟踪内存使用情况的分配器,这些都可以通过自定义 `allocator` 来实现。 12 | 13 | ### 4. 性能优化 14 | 15 | - 默认的内存分配器可能并不总是满足特定应用程序的性能需求。通过使用自定义的 `allocator`,开发者可以利用特定的内存分配技巧(如小对象优化、内存池分配等)来提升性能。 16 | 17 | ### 5. 内存对齐 18 | 19 | - 对于某些特定类型的对象,可能需要特殊的内存对齐以达到最佳性能或满足硬件要求。`allocator` 允许对这些对象的内存分配进行适当的对齐处理。 -------------------------------------------------------------------------------- /problems/为什么需要三次握手,两次不行?.md: -------------------------------------------------------------------------------- 1 | 1. **确认双方的接收与发送能力**: 2 | - 第一次握手:客户端发送一个SYN包(同步序列编号),告诉服务器它想建立连接,并且客户端能发送数据。 3 | - 第二次握手:服务器回应一个SYN-ACK包,确认收到了客户端的SYN包,同时告知客户端它也愿意建立连接,并且服务器能接收数据。 4 | - 第三次握手:客户端再次发送ACK包响应服务器的SYN-ACK包,这次确认后,客户端和服务器都确认双方的发送和接收能力正常。 5 | 2. **防止失效的连接请求突然又传送到了服务器端,造成资源浪费**: 如果只有两次握手,那么只要服务器端发送了ACK包确认,就会建立连接。假设这个时候存在一个失效的连接请求延迟到达服务器,服务器处理这个错误的请求并打开连接,等待客户端发送数据,但客户端并没有实际发出这个请求或者已经不需要这个连接了,这就导致了服务器白白浪费资源等待一个不存在的发送方。 6 | 3. **可靠性协议的要求**: TCP作为一个可靠性传输协议,不仅要保证数据的完整性,还要保证连接的稳定性。通过三次握手的流程,可以更好地抵御如同步SYN Flood Attack这样的网络攻击。 -------------------------------------------------------------------------------- /problems/为什么需要虚析构函数,什么时候不需要.md: -------------------------------------------------------------------------------- 1 | 2 | 一般情况下类的析构函数里面都是释放内存资源,而析构函数不被调用的话就会造成内存泄漏。这样做是为了当用一个基类的指针删除一个派生类的对象时,派生类的析构函数会被调用。 3 | 当然,并不是要把所有类的析构函数都写成虚函数。因为当类里面有虚函数的时候,编译器会给类添加一个虚函数表,里面来存放虚函数指针,这样就会增加类的存储空间。所以,只有当一个类被用来作为基类的时候,才把析构函数写成虚函数。 4 | 5 | -------------------------------------------------------------------------------- /problems/为什么需要虚继承.md: -------------------------------------------------------------------------------- 1 | 2 | 虚拟继承是多重继承中特有的概念。虚拟基类是为解决多重继承而出现的。 3 | 如:类D继承自类B1、B2,而类B1、B2都继 承自类A,因此在类D中两次出现类A中的变量和函数。为了节省内存空间,可以将B1、B2对A的继承定义为虚拟继承,而A就成了虚拟基类,虚拟继承在一般的应用中很少用到,所以也往往被忽视,这也主要是因为在C++中,多重继承是不推荐的,也并不常用,而一旦离开了多重继承,虚拟继承就完全失去了存在的必要因为这样只会降低效率和占用更多的空间。 4 | 5 | 虚继承的特点是,在任何派生类中的virtual基类总用同一个(共享)对象表示, 6 | 7 | -------------------------------------------------------------------------------- /problems/二叉树和链表的区别.md: -------------------------------------------------------------------------------- 1 | 1. 结构: 2 | - 链表是由节点顺序连接而成的线性数据结构,每个节点包含数据域和指向下一个节点的指针。 3 | - 二叉树是一种树状结构,每个节点最多有两个子节点,分别为左子节点和右子节点。 4 | 2. 存储方式: 5 | - 链表的节点在内存中是按顺序存储的,通过指针进行连接。 6 | - 二叉树的节点在内存中可以采用链式存储(使用指针连接)或者数组存储(通过计算索引实现)。 7 | 3. 操作效率: 8 | - 在链表中,插入和删除操作的时间复杂度为 O(1),查找操作的时间复杂度为 O(n)。 9 | - 在二叉树中,对于平衡二叉搜索树,插入、查找、删除等操作的时间复杂度为 O(log n)。 10 | 4. 应用场景: 11 | - 链表适合动态管理数据集合,特别是频繁需要插入和删除操作的场景。 12 | - 二叉树适合用于搜索、排序等需要快速查找的应用,如二叉搜索树用于快速查找。 -------------------------------------------------------------------------------- /problems/五种IO模式.md: -------------------------------------------------------------------------------- 1 | 1. **阻塞I/O**: 这是最常见的I/O模型。在此模式中,当应用程序执行I/O操作时,如果数据还没有准备好,应用程序就会被阻塞(挂起),直到数据准备好为止。这期间,应用程序不能做其他事情。 2 | 2. **非阻塞I/O)**:在此模式中,如果I/O操作的数据还没有准备好,操作会立即返回一个错误,而不是阻塞应用程序。应用程序可以继续执行其他操作,也可以反复尝试该I/O操作。 3 | 3. **I/O多路复用**:也常称为事件驱动I/O。在此模式中,应用程序可以同时监控多个I/O描述符(比如,socket),当任何一个I/O描述符准备好数据时,应用程序就可以对其进行处理。这可以在一个单独的进程或线程中同时处理多个I/O操作,并且不需要阻塞或轮询。select、poll、epoll都是这种模型的实现。 4 | 4. **信号驱动)**:在此模型中,应用程序可以向操作系统注册一个信号处理函数,当数据准备好时,操作系统会发送一个信号,应用程序可以在接收到信号时读取数据。这种模式避免了阻塞和轮询,但是编程复杂性较高。 5 | 5. **异步I/O)**:在此模型中,应用程序发起I/O操作后,可以立即开始做其他事情,当数据准备好时,操作系统会将数据复制到应用程序的缓冲区,并通知应用程序。这种模型的优点是应用程序不需要等待I/O操作的完成,缺点是编程复杂性较高。 -------------------------------------------------------------------------------- /problems/什么时候会产生栈溢出,为什么一直递归就会栈溢出.md: -------------------------------------------------------------------------------- 1 | 栈溢出是指程序运行时使用的栈空间超过了系统为栈分配的内存大小,导致栈溢出错误。栈通常用来存储函数调用、局部变量和临时数据,当递归层次过深或者函数调用过多时,会消耗大量栈空间,导致栈溢出。 2 | 3 | 以下是一些产生栈溢出的常见情况: 4 | 5 | 1. 递归调用:在递归函数中,每次调用都会将函数的参数、返回地址等信息存放在栈中,如果递归层次过深,栈空间会被消耗殆尽。 6 | 2. 大量局部变量:如果函数中定义了大量的局部变量,也会增加栈空间的使用。 7 | 3. 无限循环:在一个循环中不断地调用函数或者产生新的栈帧,会迅速消耗栈空间。 8 | 9 | 递归函数容易导致栈溢出的原因包括: 10 | 11 | 1. 没有合适的终止条件:如果递归函数没有设置终止条件或者终止条件设计不当,会导致无限递归调用。 12 | 2. 递归调用次数过多:每次递归调用都会在栈中创建一个新的栈帧,如果递归调用次数过多,会消耗大量栈空间。 -------------------------------------------------------------------------------- /problems/什么是C++的内存模型?.md: -------------------------------------------------------------------------------- 1 | 1. **对象存储期(Storage Duration)**: 2 | - 自动存储期(Automatic): 在栈上自动创建和销毁的对象,例如函数内的局部变量。 3 | - 静态存储期(Static): 程序开始执行时分配直到程序结束才销毁的对象,包括全局变量和静态变量。 4 | - 动态存储期(Dynamic): 通过`new`或`malloc`在堆上分配的内存,必须显式用`delete`或`free`来释放。 5 | - 线程存储期(Thread-local): 每个线程有自己独立实例的变量,其生命周期与所属线程一致。 6 | 2. **内存分区**: 7 | - 栈(Stack): 用来存储自动存储期的局部变量,函数参数等。是由编译器自动管理内存的区域。 8 | - 堆(Heap): 用于动态内存分配,需要程序员或智能指针来管理内存的释放。 9 | - 全局/静态存储区(Global/Static Area): 存放全局变量和静态变量。 10 | - 常量存储区(Constant Storage Area): 存储常量数据,如字符串常量等。 11 | - 代码区(Code or Text Segment): 存放程序的机器代码。 12 | 3. **多线程与原子操作**: C++11引入了对并发的支持,定义了原子类型`std::atomic`和内存序(memory order)概念,使得程序可以在多线程环境下安全地进行读写操作而不引发数据竞争。 13 | 4. **内存顺序**: 内存模型还详细规定了操作的内存顺序,这涉及到编译器优化和处理器重排指令时必须遵守的规则,确保一致性和可见性。 14 | 5. **同步和互斥**: C++标准库提供了多种同步机制,如互斥锁`std::mutex`,条件变量`std::condition_variable`等,它们的行为也被内存模型所覆盖。 15 | 6. **顺序一致性**: 当多个线程访问共享数据时,内存模型定义了保证顺序一致性的方法,即在不同线程看来,所有操作都是以相同顺序发生的。 -------------------------------------------------------------------------------- /problems/什么是IP地址?可以简单介绍下吗?.md: -------------------------------------------------------------------------------- 1 | IP地址是用于在网络中唯一标识和定位设备的一种地址。IP地址是网络层协议中使用的一种地址,它分为IPv4地址和IPv6地址两种类型。 2 | 3 | 1. IPv4地址:IPv4地址是32位的二进制地址,通常用点分十进制表示,如192.168.1.1。IPv4地址由四个8位字节组成,每个字节范围从0到255之间,共计可表示约42亿个不同的地址。然而,由于IPv4地址资源有限,目前已经出现了地址枯竭的问题。 4 | 2. IPv6地址:为了解决IPv4地址资源枯竭的问题,IPv6地址被引入并取代了IPv4地址。IPv6地址是128位的地址,采用冒号分隔的八组十六进制数表示,如2001:0db8:85a3:0000:0000:8a2e:0370:7334。IPv6地址提供了极其庞大的地址空间,理论上可以支持几乎无限数量的设备连接到互联网。 5 | 6 | IP地址的作用是在互联网上唯一标识一个设备或主机,使得数据包能够准确地被发送到目的地。在TCP/IP协议中,IP地址与MAC地址结合使用,实现了设备之间的通信和数据传输。 -------------------------------------------------------------------------------- /problems/什么是PCB?.md: -------------------------------------------------------------------------------- 1 | PCB 它是操作系统中用于存储关于进程信息的一个重要数据结构。**PCB 是操作系统用来管理和跟踪进程状态的一种方式,确保进程能够有序地执行和切换。** 2 | 3 | 通常包含以下信息: 4 | 5 | 1. **进程标识符**(PID):一个唯一的标识号,用于区分不同的进程。 6 | 2. **进程状态**:如就绪、运行、等待、终止)等。 7 | 3. **程序计数器**:指向进程下一个要执行的指令地址。 8 | 4. **CPU 寄存器信息**:保存进程被中断或切换时,CPU 寄存器中的数据,以便恢复时可以继续执行。 9 | 5. **CPU 调度信息**:包括进程优先级、调度队列指针和其他调度参数。 -------------------------------------------------------------------------------- /problems/什么是RAII原则,他在STL是怎么应用的?.md: -------------------------------------------------------------------------------- 1 | ### RAII原则的核心思想: 2 | 3 | - 在构造函数中获取资源。 4 | - 在析构函数中释放资源。 5 | - 不直接操作资源,而是通过管理资源的对象来使用资源。 6 | 7 | 这样做有几个好处: 8 | 9 | 1. **安全性**:避免资源泄露。由于资源的释放是自动的,因此即使在异常发生时,对象的析构函数也会被调用,资源也相应地会被释放。 10 | 2. **简洁性**:代码通常更加简洁,因为资源的管理是自动的,不需要程序员显式编写资源释放代码。 11 | 3. **异常安全**:RAII可以帮助提供强异常安全保障,因为资源释放不依赖于程序路径。 12 | 13 | ### RAII在STL中的应用: 14 | 15 | 在STL中,RAII广泛应用于各种容器和其他组件中。例如: 16 | 17 | - **智能指针**:`std::unique_ptr` 和 `std::shared_ptr` 是智能指针类,它们对动态分配的内存进行管理。当智能指针的实例离开作用域时,其析构函数会自动释放其所管理的内存。 18 | - **容器类**:如 `std::vector`、`std::string`、`std::map` 等,都负责自己内部数据的内存管理。当一个容器对象被销毁时,它的析构函数会释放所有占用的内存,并适当地销毁其元素。 19 | - **锁管理类**:如 `std::lock_guard` 和 `std::unique_lock`,它们在构造时获取锁,在析构时释放锁,从而确保在持有锁的代码块执行完毕后,无论是正常退出还是因异常退出,锁都会被释放。 -------------------------------------------------------------------------------- /problems/什么是mac地址?可以简单介绍下吗?.md: -------------------------------------------------------------------------------- 1 | MAC地址是网络设备在数据链路层中使用的物理地址,用于唯一标识网络设备。每个网络设备都有一个唯一的MAC地址,通常由48位二进制数表示,分为6个十六进制数对,用冒号或短横线分隔,如00:1A:2B:3C:4D:5E。 2 | 3 | MAC地址主要用于通过局域网传输数据时,帮助网络设备进行识别和寻址。与IP地址不同,MAC地址是固定的且与硬件设备绑定,不会因为设备连接到不同网络而改变。当数据包在局域网内传输时,源设备将目标设备的MAC地址作为目标地址写入数据包头部,以确保数据包能够准确地从发送者传输到接收者。 4 | 5 | 需要注意的是,MAC地址是在数据链路层中使用的标识符,只在局域网范围内有效。在互联网通信中,数据包最终是通过IP地址来路由和传递的,而在局域网中则是通过MAC地址来实现设备之间的直接通信。MAC地址与IP地址结合起来,共同协助实现了数据在网络中的正确传输和交换。 -------------------------------------------------------------------------------- /problems/什么是一致性哈希,Redis集群为什么不用一致性哈希.md: -------------------------------------------------------------------------------- 1 | 一致性哈希是一种解决分布式系统中数据分片和负载均衡的算法。它通过将数据映射到一个固定范围的圆环上,然后根据数据的键值在圆环上寻找最近的节点来确定数据存储的位置,从而实现动态扩展和节点失效时的平滑数据迁移。 2 | 3 | Redis集群之所以不用一致性哈希算法,主要有以下几个原因: 4 | 5 | 1. Redis Cluster采用哈希槽分配数据:Redis集群将所有数据划分为16384个哈希槽,每个节点负责管理其中的一部分哈希槽,数据的位置由哈希槽来确定。这种方式简化了数据定位和节点管理,避免了传统一致性哈希算法中需要维护虚拟节点、一致性哈希环等复杂逻辑。 6 | 2. Redis Cluster提供内置的故障检测和自动迁移功能:Redis集群具有自动故障检测和节点替换的能力,当节点失效时可以自动将其槽位重新分配给其他可用节点,确保数据的可靠性和高可用性。这种自动化的机制大大简化了集群的管理和维护。 7 | 3. Redis Cluster支持节点间的无中心化通信:Redis集群中的各个节点都可以相互通信,而不依赖于单个中心节点进行协调。这样可以降低系统的单点故障风险,提高了系统的稳定性和容错能力。 -------------------------------------------------------------------------------- /problems/什么是二叉树、二叉搜索树、平衡二叉树、完全二叉树、满二叉树.md: -------------------------------------------------------------------------------- 1 | 1.二叉搜索树: 2 | 3 | - 二叉搜索树是一种特殊的二叉树,对于每个节点,其左子树中的所有节点值都小于该节点的值,右子树中的所有节点值都大于该节点的值。 4 | 5 | - 二叉搜索树的中序遍历结果是有序的。 6 | 7 | 2.平衡二叉树: 8 | 9 | - 平衡二叉树是一种特殊的二叉搜索树,它保持左右子树的高度差不超过1,以确保树的高度平衡。 10 | - 平衡二叉树的插入和删除操作会导致树的自平衡调整,以保持平衡性。 11 | 12 | 3.完全二叉树: 13 | 14 | - 完全二叉树是一个二叉树,除了最后一层外,其他层都是满的,且最后一层的节点从左向右依次排列。 15 | - 完全二叉树通常使用数组来存储,可以利用数组索引计算节点之间的关系。 16 | 17 | 4.满二叉树: 18 | 19 | - 满二叉树是一种特殊的二叉树,每个节点要么没有子节点,要么有两个子节点。 20 | - 满二叉树的叶子节点都在同一层,且所有非叶子节点都有两个子节点。 21 | 22 | 总结: 23 | 24 | - 二叉树是具有两个子节点的树状结构。 25 | - 二叉搜索树是一种特殊的二叉树,左子树节点值小于根节点,右子树节点值大于根节点。 26 | - 平衡二叉树是一种保持平衡性的二叉搜索树。 27 | - 完全二叉树是除了最后一层外都是满的二叉树。 28 | - 满二叉树是每个节点要么没有子节点,要么有两个子节点的二叉树。 -------------------------------------------------------------------------------- /problems/什么是僵尸进程?.md: -------------------------------------------------------------------------------- 1 | 僵尸进程的产生通常发生在以下情况: 2 | 3 | 1. 当一个子进程结束执行后,它会向父进程发送一个SIGCHLD信号,表示它已经执行完毕。 4 | 2. 根据操作系统的设计,父进程需要通过调用`wait()`或`waitpid()`函数来读取子进程的退出状态。这个操作被称为“收割”子进程。 5 | 3. 如果父进程没有调用这些函数,子进程的PCB就不会被清除,从而成为僵尸进程。 6 | 7 | 僵尸进程**本身不消耗除了PCB之外的资源,但如果大量的僵尸进程累积,它们将占用系统的进程表空间,可能会导致系统无法创建新的进程**。通常,僵尸进程是由于程序设计不当导致的,开发者应当确保进程能够被正确地清理。 8 | 9 | 当僵尸进程的父进程终止时,僵尸进程会被操作系统的init进程(或其他类似的系统进程)接管,并由它来完成收割工作,从而释放僵尸进程占用的PCB。在大多数现代操作系统中,这种现象是暂时的,因为系统设计确保了僵尸进程最终会被清理 -------------------------------------------------------------------------------- /problems/什么是哈夫曼树?构造过程?应用场景.md: -------------------------------------------------------------------------------- 1 | 哈夫曼树是一种带权路径长度最短的二叉树,通常用于数据压缩中。构造哈夫曼树的过程称为哈夫曼编码。 2 | 3 | 构造哈夫曼树的步骤如下: 4 | 5 | 1. 将所有的节点按照权值进行升序排序。 6 | 2. 选取权值最小的两个节点作为左右子节点,生成一个新的节点,其权值为这两个节点的权值之和。 7 | 3. 将新生成的节点插入到原来的节点集合中,并从节点集合中删除这两个被合并的节点。 8 | 4. 重复上述步骤,直到节点集合只剩下一个节点,即为根节点,构成哈夫曼树。 9 | 10 | 哈夫曼树的应用场景主要在数据压缩中,通过构建哈夫曼树并生成对应的哈夫曼编码(即将频率高的字符编码短、频率低的字符编码长),可以实现数据的有损压缩,减少数据的存储空间和传输带宽。常见的应用包括文本文件压缩、图像压缩、音频压缩等。哈夫曼编码还被广泛应用于网络传输、文件存储等领域。 -------------------------------------------------------------------------------- /problems/什么是堆?如何维护堆.md: -------------------------------------------------------------------------------- 1 | 堆是一种特殊的树形数据结构,通常用于实现优先队列。堆分为最大堆(Max Heap)和最小堆(Min Heap),其中最大堆满足父节点的值大于等于子节点的值,最小堆满足父节点的值小于等于子节点的值。 2 | 3 | 维护堆的主要操作有两个:插入和删除。在插入一个新元素时,需要保持堆的性质不变;在删除堆顶元素时,同样需要保持堆的性质不变。以下是维护堆的具体操作: 4 | 5 | 1. 插入元素: 6 | - 将新元素插入到堆的末尾。 7 | - 从新元素开始向上调整(最大堆时向下调整),直至满足堆的性质。 8 | 2. 删除堆顶元素: 9 | - 将堆顶元素(根节点)与堆的最后一个元素交换。 10 | - 删除最后一个元素,即原来的堆顶元素。 11 | - 从根节点开始向下调整(最大堆时向上调整),直至满足堆的性质。 12 | 13 | 在维护堆时,通常会使用“上浮”和“下沉”的方式进行调整,保持堆的性质。具体来说,插入时新元素上浮,删除堆顶元素后,末尾元素下沉。这样可以确保堆的顺序关系不被破坏。 14 | 15 | 维护堆的时间复杂度为 O(log n),其中 n 为堆的大小。 -------------------------------------------------------------------------------- /problems/什么是大小端,他在计算机网络中都有什么应用呢.md: -------------------------------------------------------------------------------- 1 | 大小端是指字节的排列方式或者说存储顺序,主要有大端模式和小端模式两种。 2 | 3 | 1. 大端模式:数据的高字节存储在低地址中,低字节存储在高地址中。 4 | 2. 小端模式:与大端模式相反,数据的低字节存储在低地址中,高字节存储在高地址中。 5 | 6 | 大端模式和小端模式的应用: 7 | 8 | - **计算机硬件设备**:不同的硬件设备可能采用不同的端模式。例如,Intel的x86架构处理器使用小端模式,而IBM的Power Architecture处理器使用大端模式。这在硬件设计和编程时需要特别注意。 9 | - **网络通信**:在网络协议如TCP/IP中,数字都是以网络字节序发送的,也就是大端模式。因此,如果本地系统使用的是小端模式,在进行网络传输时就需要将其转换为大端模式,这个过程称为字节序转换。 10 | - **文件存储**:在一些文件格式或数据库中,也可能规定了特定的字节序。比如,Java的class文件、UTF-16文本文件等就规定了特定的字节序。 -------------------------------------------------------------------------------- /problems/什么是完美转发?.md: -------------------------------------------------------------------------------- 1 | 完美转发是一个与模板和函数重载相关的概念,它允许一个函数将其接收到的参数以原始的值类别(左值或右值)传递给另一个函数。这意味着**如果你传递了一个左值给包装函数,那么被调用的函数也会接收到一个左值;如果传递的是一个右值,则同样地,被调用的函数会接收到一个右值。** 2 | 3 | 4 | 5 | ``` 6 | #include 7 | 8 | // 这个函数负责“转发”它的参数到另一个函数 9 | template 10 | void wrapper(T&& arg) { 11 | // 使用 std::forward 来确保 arg 的值类别得以保持不变 12 | target(std::forward(arg)); 13 | } 14 | 15 | // 一个可能接受左值或右值参数的目标函数 16 | void target(int& x) { 17 | // 处理左值 18 | } 19 | 20 | void target(int&& x) { 21 | // 处理右值 22 | } 23 | 24 | int main() { 25 | int lv = 5; // 左值 26 | wrapper(lv); // 应该调用 void target(int& x) 27 | wrapper(10); // 应该调用 void target(int&& x) 28 | } 29 | 30 | ``` 31 | 32 | -------------------------------------------------------------------------------- /problems/什么是左值?什么是右值?有什么不同?.md: -------------------------------------------------------------------------------- 1 | ### 左值 (Lvalue): 2 | 3 | 一个**左值**表示表达式结束后依然存在的对象。它指向一个具体的内存位置,并且你可以取得其地址。通常情况下,左值表达式可能出现在赋值操作的左侧。 4 | 5 | ### 右值 (Rvalue): 6 | 7 | 一个**右值**通常是暂时的并且不会长时间存在,它不能被赋予另一个值。右值通常是直接的数据值或者无法通过标识符直接访问的临时对象。 8 | 9 | **不同点:** 10 | 11 | - **身份**: 左值具有明确的内存地址,而右值通常没有固定的内存地址。 12 | - **持久性**: 左值代表长期存在的对象,右值代表临时或即将销毁的对象。 13 | - **可移动性**: 右值可以被移动,而左值通常不能,除非显式地转换成右值引用。 14 | - **引用类型**: 可以声明左值引用指向左值(`T&`),而右值引用(`T&&`)可以绑定到右值上,优化资源使用。 -------------------------------------------------------------------------------- /problems/什么是拆包和粘包?.md: -------------------------------------------------------------------------------- 1 | 在网络通信中,拆包指的是将一个完整的数据包拆分成多个小包发送,而粘包则是将多个小包合并成一个完整的数据包接收。 2 | 3 | 拆包和粘包常常出现在基于流传输协议(如TCP)的网络通信中。由于TCP是面向流的协议,发送方可以将数据按照任意大小的数据块划分为多个段发送,接收方也可能一次性接收到多个数据段,从而导致拆包和粘包问题。 4 | 5 | 拆包问题会导致接收方无法正确解析数据,因为一个完整的数据包被分割成了多个部分;而粘包问题则会使接收方难以区分多个数据包的边界,造成数据解析错误。 6 | 7 | 为避免拆包和粘包问题,通常可采用以下方法: 8 | 9 | 1. 在数据包中增加长度字段,让接收方根据长度字段来解析数据包。 10 | 2. 使用分隔符或者特殊标记来标识数据包的边界。 11 | 3. 对数据包进行序列化和反序列化,确保数据的完整性和正确性。 12 | 4. 应用层协议设计时考虑消息头和消息体的格式,规范数据的传输方式。 13 | 14 | 通过以上方法,可以有效地解决拆包和粘包问题,确保数据在网络传输中能够正确解析和处理。 -------------------------------------------------------------------------------- /problems/什么是数字签名?.md: -------------------------------------------------------------------------------- 1 | 数字签名是一种用于验证数字信息完整性和来源的技术,它利用密码学原理来模拟物理签名的功能。数字签名能够保证一个数字文件或者数据传输过程中没有被篡改,并且可以确认数据的发送方是可信的。 2 | 3 | 数字签名通常基于非对称加密算法(如RSA、ECDSA等)实现。在非对称加密中,有两个密钥:一个私钥和一个公钥。私钥是保密的,仅被数据的发送方所持有;公钥则可以公开,任何人都可以使用它来验证签名。 4 | 5 | 数字签名的创建和验证过程大致如下: 6 | 7 | 1. **生成密钥对**:首先,发送方会生成一对密钥(即上述的私钥和公钥)。私钥用于签名,而公钥用于验证签名。 8 | 2. **签名过程**: 9 | - 发送方将消息(例如电子文档、软件或交易信息)通过哈希函数处理,生成一个固定长度的哈希值(摘要)。 10 | - 然后,发送方使用自己的私钥对这个哈希值进行加密,生成数字签名。 11 | - 这个数字签名随着原始消息一起发送给接收方。 12 | 3. **验证过程**: 13 | - 接收方收到消息和数字签名后,使用相同的哈希函数处理消息,生成一个新的哈希值。 14 | - 接收方还会使用发送方的公钥对数字签名进行解密,得到一个哈希值。 15 | - 如果这两个哈希值相同,则说明消息在传输过程中未被更改,签名也证明了消息确实是由持有相应私钥的发送方所发出的。 16 | 17 | 数字签名的安全性依赖于私钥的保密性。如果私钥不小心泄露,任何人都可以伪造签名。因此,私钥的管理和存储需要非常谨慎。 18 | 19 | 数字签名广泛用于多种场合,包括但不限于软件分发、电子邮件、电子商务和各类在线交易,以及其他需要高度安全性的通信和数据交换场景。 -------------------------------------------------------------------------------- /problems/什么是数字证书?.md: -------------------------------------------------------------------------------- 1 | 数字证书是一种用于确认实体身份的电子文档,它利用公钥加密的原理来建立一个可信任的身份验证机制。数字证书通常由受信任的第三方机构,即证书颁发机构(Certificate Authority, CA)签发。它包含了证书持有者的信息以及与之关联的公钥,并且由CA用其自己的私钥进行签名。 2 | 3 | 数字证书主要包含以下几个关键部分: 4 | 5 | 1. **证书持有者的信息**:包括名称、组织和其他标识信息。 6 | 2. **证书持有者的公钥**:用于在公钥基础设施(PKI)中验证持有者的私钥签名,并且用于加密信息,只有持有对应私钥的持有者才能解密。 7 | 3. **证书颁发机构(CA)的信息**:提供了签发该证书的CA的识别信息。 8 | 4. **有效期限**:证书的开始和结束有效日期。 9 | 5. **证书序列号**:证书的唯一标识符。 10 | 6. **数字签名**:CA使用其私钥产生的对上述所有信息的数字签名。 11 | 12 | 数字证书的作用如下: 13 | 14 | - **认证身份**:证明一个公钥确实属于其声称的实体,比如一个人、服务器或组织。 15 | - **建立信任关系**:在开展电子交易或通信时,帮助建立起参与各方之间的信任关系,因为证书是由权威的CA签发的。 16 | - **数据加密**:提供加密通信所需的公钥,确保信息安全传输。 17 | - **安全通信**:在TLS/SSL协议中使用,确保网络通信如HTTPS等的安全。 18 | 19 | 当你访问一个网站时,如果网站使用了SSL/TLS安全协议,你的浏览器会自动检查该网站的SSL证书是否有效,是否由受信任的CA颁发,以及是否与网站域名匹配。如果一切正常,浏览器就会建立安全连接;否则,可能显示警告信息。 20 | 21 | 数字证书的有效性依赖于颁发证书的CA的可信度。如果CA遭到破坏或不再被信任,则通过该CA签发的所有证书都可能变得不可靠。因此,维护CA的安全性和可靠性是数字证书体系中至关重要的一环。 -------------------------------------------------------------------------------- /problems/什么是泛型编程,他在STL中是怎么使用的?.md: -------------------------------------------------------------------------------- 1 | 泛型编程是一种软件开发方法论,其核心思想是通过抽象和重用代码来增强软件的灵活性、可维护性和复用性。在泛型编程中,算法或数据结构被设计为独立于它们所操作的具体数据类型。这种抽象化允许程序员使用相同的代码基础处理不同类型的数据,只要这些数据类型支持算法所需的操作。 2 | 3 | ### STL中的泛型编程 4 | 5 | C++的标准模板库(STL)是泛型编程的一个经典实例。STL提供了一套通用的容器类(如`vector`、`list`、`map`等),算法(如`sort`、`find`、`accumulate`等),以及其他实用工具(如迭代器、函数对象等),它们都是泛型化的,可以与任何符合要求的数据类型一起工作。 6 | 7 | #### 容器 8 | 9 | STL容器是泛型的,因为它们可以存储任何类型的对象。例如,`std::vector` 可以存储整数,而 `std::vector` 可以存储字符串。容器通过模板参数化其元素的类型: 10 | 11 | ``` 12 | std::vector intVec; // 存储整数的向量 13 | std::list dblList; // 存储双精度浮点数的列表 14 | ``` 15 | 16 | #### 算法 17 | 18 | STL算法也是泛型的,它们通过迭代器与容器进行交互,而不是直接操作容器。这种设计使得相同的算法可以应用于不同类型的容器,只要容器提供了适当类型的迭代器。例如,`std::sort`函数可以对任何连续存储的元素序列进行排序,无论它是`std::vector`、数组还是`std::array`: 19 | 20 | ``` 21 | std::vector vec = {4, 1, 3, 5, 2}; 22 | std::sort(vec.begin(), vec.end()); // 对向量进行排序 23 | ``` 24 | 25 | #### 迭代器 26 | 27 | 迭代器在STL的泛型编程中扮演着中介的角色。它们提供了一种访问容器中元素的方法,同时隐藏了容器的内部结构。通过使用迭代器,STL算法可以在不知道或不关心容器具体实现的情况下工作。 28 | 29 | #### 函数对象和Lambda表达式 30 | 31 | STL允许你通过函数对象(包括lambda表达式)来自定义某些操作,比如自定义比较函数。这增加了STL的灵活性和泛型能力,因为你可以定义算法的行为,而无需修改算法本身。 32 | 33 | ``` 34 | std::sort(vec.begin(), vec.end(), [](int a, int b) { return a > b; }); // 使用lambda表达式降序排序 35 | ``` 36 | 37 | 总之,STL通过泛型编程提供了一套强大的、类型无关的工具,使得开发者能够写出既安全又高效的代码,而不用牺牲代码的通用性和复用性。 -------------------------------------------------------------------------------- /problems/什么是滑动窗口,超时重传.md: -------------------------------------------------------------------------------- 1 | **滑动窗口** 2 | 3 | 滑动窗口是TCP和其他网络协议中用于控制数据传输的一种技术。它的主要目的是防止发送方发送过多数据,从而导致接收方无法处理。 4 | 5 | 在TCP中,滑动窗口由发送窗口和接收窗口组成,**它们分别代表了发送方可以发出的数据量和接收方可以接受的数据量。每次数据传输后,窗口会“滑动”以适应新的数据流。**滑动窗口的大小根据网络拥塞情况、接收方处理能力等因素动态调整,从而实现TCP的流量控制和拥塞控制。 6 | 7 | **超时重传** 8 | 9 | **超时重传是TCP中用于保证数据可靠传输的一种机制。**当发送方发出一个数据包后,它会启动一个定时器等待接收方的确认。如果在定时器超时之前接收到确认,则表示数据包已成功传送;否则发送方会认为该数据包在网络中丢失,需要进行重传。 10 | 11 | 超时重传能够确保即使在网络环境不理想的情况下,数据也能最终被接收到。这是TCP提供可靠传输服务的关键机制之一。然而,由于超时重传可能增加网络拥塞,所以TCP还配备了拥塞控制机制来避免过度重传。 12 | 13 | 需要注意的是,**TCP的超时时间并非固定,而是根据Round-Trip Time(往返时间)动态调整**。这样可以更好地适应不同的网络条件,提高效率。 -------------------------------------------------------------------------------- /problems/什么是红黑树?红黑树与平衡二叉树、B和B+树的区别.md: -------------------------------------------------------------------------------- 1 | 红黑树是一种自平衡二叉查找树,具有以下特点: 2 | 3 | 1. 每个节点要么是红色,要么是黑色。 4 | 2. 根节点是黑色。 5 | 3. 叶子节点(NIL节点)是黑色。 6 | 4. 如果一个节点是红色,则它的子节点必须是黑色。 7 | 5. 从任一节点到其每个叶子节点的所有路径都包含相同数目的黑色节点。 8 | 9 | 红黑树通过对节点着色和旋转等操作来维持平衡,保证在最坏情况下的查询、插入、删除等操作的时间复杂度为 O(log n),是广泛应用于数据结构和算法中的一种重要数据结构。 10 | 11 | 与平衡二叉树和B/B+树相比,红黑树有以下几点区别: 12 | 13 | 1. 平衡二叉树(如AVL树):平衡二叉树要求左右子树的高度差不超过1,因此需要频繁地进行旋转操作来维持平衡,相比之下红黑树更加灵活,在实际应用中性能更好。 14 | 2. B/B+树:B树和B+树是多路搜索树,适合磁盘存储等场景,可以一次读取多个关键字。而红黑树更适用于内存中数据结构的实现,虽然红黑树也可以作为B树和B+树的基础实现。 15 | 3. 红黑树相对于B树和B+树来说,对于数据的插入、删除等操作可能更为简单高效,但在某些特定场景下B树和B+树可能更适合。 -------------------------------------------------------------------------------- /problems/什么是缓冲区溢出?.md: -------------------------------------------------------------------------------- 1 | 缓冲区溢出通常是由于程序设计或实现上的错误造成的,例如没有对输入数据进行有效的边界检查、使用不安全的函数等。攻击者可以利用缓冲区溢出漏洞来执行恶意代码、修改程序的控制流或获取未经授权的系统访问权限,从而对系统进行攻击。 2 | 3 | 缓冲区溢出可能导致以下问题: 4 | 5 | 1. **程序崩溃或异常**:如果缓冲区溢出引起写入了超出缓冲区范围的数据,可能会破坏程序的数据结构,导致程序崩溃或产生不可预测的行为。 6 | 2. **安全漏洞**:攻击者可以利用缓冲区溢出漏洞来执行恶意代码,控制程序的行为,并可能获得系统权限,造成严重安全问题。 7 | 3. **信息泄露**:缓冲区溢出可能导致敏感数据泄露,包括密码、加密密钥等,对系统和用户数据安全构成威胁。 8 | 9 | 为防止缓冲区溢出问题,程序开发人员应遵循安全编程实践,包括: 10 | 11 | - 对用户输入进行有效的边界检查,限制输入数据的长度,并确保数据不超出缓冲区范围。 12 | - 使用安全的字符串处理函数,如`strncpy()`代替`strcpy()`,避免潜在的缓冲区溢出风险。 13 | - 尽量避免使用固定大小的缓冲区,可以考虑动态分配内存或使用安全的数据结构。 -------------------------------------------------------------------------------- /problems/什么是缺页中断.md: -------------------------------------------------------------------------------- 1 | 缺页中断是计算机操作系统中的一个重要概念,发生在程序访问虚拟内存时,需要加载的页面不在主存中,需要从辅存(如硬盘)中读取的情况下。当程序试图访问一个已经被映射到虚拟地址空间但尚未载入物理内存的页面时,就会引发缺页中断。 2 | 3 | 具体来说,当程序访问一个虚拟地址时,操作系统会首先检查该地址对应的页面是否已经在主存中。如果页面在主存中,那么程序可以直接访问;如果页面不在主存中,就会触发缺页中断。此时,操作系统会进行以下步骤: 4 | 5 | 1. **中断处理**:CPU接收到缺页中断信号后,暂停当前正在执行的程序,将控制权交给操作系统内核。 6 | 2. **处理程序**:操作系统会根据页面表或其他映射信息确定页面所在的位置(通常是磁盘),并将页面加载到主存中的空闲页面框中。 7 | 3. **更新页表**:操作系统更新页表中有关该页面的信息,包括物理地址等。 8 | 4. **恢复程序**:一旦页面加载到内存中,操作系统会重新启动之前暂停的程序,使其继续执行。 -------------------------------------------------------------------------------- /problems/什么是跳表?跳表和平衡二叉树的区别.md: -------------------------------------------------------------------------------- 1 | 跳表是一种基于并行链表的数据结构,用于在有序链表中快速查找、插入和删除操作。跳表通过在底层链表上建立多层索引来加速查找操作,每一层索引都是原始链表的一个子集,使得查找元素的时间复杂度降低为 O(log n),类似于二分查找的效果。 2 | 3 | 跳表与平衡二叉树的区别如下: 4 | 5 | 1. 结构形式不同:跳表是基于链表实现的,而平衡二叉树是基于树的实现。 6 | 2. 平衡性要求不同:平衡二叉树需要保持左右子树高度差不超过1,而跳表通过增加多级索引来实现平衡性,并且不需要旋转等操作来维护平衡。 7 | 3. 调整代价不同:对于插入、删除等操作,平衡二叉树可能需要频繁地进行旋转调整以保持平衡,而跳表只需要简单的插入和删除元素即可。 8 | 4. 存储空间需求不同:平衡二叉树通常需要额外的指针来连接左右子节点,而跳表中的索引节点可以共享底层链表节点,相对节省存储空间。 9 | 5. 并发性能不同:由于跳表中每一层索引是一个独立的有序链表,因此在并发环境下,跳表的查询操作可以更容易实现并行化,提高并发性能。 -------------------------------------------------------------------------------- /problems/什么是进程?什么是线程?他们的区别是什么?.md: -------------------------------------------------------------------------------- 1 | ### 进程 2 | 3 | 一个**进程**是操作系统分配资源的基本单位。它代表了应用或程序的一次执行实例。**每个进程都拥有独立的内存空间、数据栈以及其他用于跟踪执行的辅助数据。**操作系统管理着所有进程,并为它们分配CPU时间。 4 | 5 | **进程之间相互隔离,通常情况下,一个进程无法直接访问另一个进程的资源**,这种设计提高了系统稳定性。 6 | 7 | ### 线程 8 | 9 | 一个**线程**是操作系统能够进行运算调度的最小单位。**它被包含在进程之中,是进程中的实际运行单位。一个进程可以拥有一个或多个线程,所有线程共享进程的内存和资源。**但是,每个线程还会拥有自己的执行堆栈和程序计数器。 10 | 11 | **线程也称为轻量级进程**,因为它们比完整的进程更“轻”,创建和上下文切换的开销较小。 12 | 13 | ### 进程与线程的区别 14 | 15 | 1. **内存分配**: 16 | - 进程:每个进程有自己独立的地址空间,一个进程崩溃后,在保护模式下不会影响其他进程。 17 | - 线程:所有线程共享其母进程的地址空间,一个线程的错误可能会导致整个进程的崩溃。 18 | 2. **通信方式**: 19 | - 进程:进程间通信需要特殊的IPC机制。 20 | - 线程:线程间可以直接读写进程数据段(如全局变量)来进行通信。 21 | 3. **系统开销**: 22 | - 进程:进程在创建、销毁、切换时的系统开销大,因为涉及到对应的地址空间的创建和销毁。 23 | - 线程:线程的创建、销毁、切换的开销相对较小,因为它们共享很多资源。 24 | 4. **资源管理**: 25 | - 进程:作为资源分配的基本单位,独立进程拥有完整资源集合。 26 | - 线程:作为调度执行的基本单位,只维护必要的信息和资源供运行。 27 | 5. **依赖关系**: 28 | - 进程:进程可以独立执行,不依赖其他进程。 29 | - 线程:线程是进程的一部分,依赖于进程的存在。 30 | 6. **执行环境**: 31 | - 进程:每个进程提供给其内部线程的执行环境相对复杂。 32 | - 线程:线程拥有较为简单的执行环境。 -------------------------------------------------------------------------------- /problems/什么是页表、什么是快表.md: -------------------------------------------------------------------------------- 1 | 页表是操作系统中用于虚拟地址到物理地址映射的数据结构。在虚拟内存管理中,操作系统将进程的虚拟地址空间划分为固定大小的页面,同时将物理内存也划分成同样大小的页框(Page Frame)。页表记录了虚拟地址空间中每个页面与对应的物理页框之间的映射关系。 2 | 3 | 具体来说,页表通常由多级结构组成,包括一级页表、二级页表等。当进程访问某个虚拟地址时,通过页表查找,可以确定该虚拟地址对应的物理地址,实现地址转换。 4 | 5 | 快表(TLB)是页表的高速缓存,用于加速虚拟地址到物理地址的转换过程。在进行内存地址转换时,CPU首先会查询快表,如果在快表中找到对应的物理地址映射,则可以直接进行访存操作,节省了时间;如果未命中快表,则需要通过页表从主内存中获取对应的映射关系。 6 | 7 | 快表是一个小而快速的高速缓存,通常位于CPU芯片内部,存储最近使用的虚拟地址到物理地址的映射信息。TLB的命中率直接影响了地址转换的性能,高命中率可以减少访问主内存的次数,提高系统运行效率。 8 | 9 | 总的来说,页表负责管理虚拟地址到物理地址的映射关系,而快表则作为页表的高速缓存,加速地址转换过程,提高系统的性能和效率。 -------------------------------------------------------------------------------- /problems/介绍 Redis的skiplist.md: -------------------------------------------------------------------------------- 1 | Redis中的skiplist(跳表)是一种有序数据结构,用于实现有序集合(sorted set)数据类型。skiplist通过层级索引的方式来加快元素的查找速度,同时保持元素的有序性。 2 | 3 | 在skiplist中,每个节点包含一个指向下一个节点的指针,以及若干个指向同一层级的其他节点的指针。通过这种多级索引的方式,skiplist可以在查找时进行跳跃,从而减少查找的时间复杂度,提高了查找效率。 4 | 5 | skiplist的插入、删除和查找操作的平均时间复杂度为O(log n),在元素数量较大时依然能够保持较高的性能。相比于传统的平衡树结构,skiplist的实现更加简单,并且在维护有序集合的基础上兼顾了高效的查找性能。 6 | 7 | Redis使用skiplist作为有序集合(sorted set)数据类型的底层实现,通过skiplist可以支持有序集合中元素的快速查找、插入、删除等操作。skiplist在Redis中发挥着重要的作用,为有序集合类型带来了高效的操作性能和良好的扩展性。 -------------------------------------------------------------------------------- /problems/介绍 intset及其升级过程,支持降级吗.md: -------------------------------------------------------------------------------- 1 | intset是Redis中用于存储整数集合的数据结构。它是一种紧凑且高效的数据结构,能够在内存中存储一组整数,并且根据整数的类型使用不同的编码方式,包括int16、int32和int64,以节省内存空间。 2 | 3 | 在Redis中,当一个intset需要插入一个新元素时,会先判断该元素的类型是否超过了当前intset所能存储的最大类型,如果超过了,则会触发升级过程。升级过程会重新分配一个更大的intset,并将原有的元素全部复制到新的intset中,然后再插入新的元素。这样可以确保intset始终选择最适合的编码方式来存储整数,避免浪费内存空间。 4 | 5 | 至于降级操作,Redis并没有提供直接的降级机制。一旦intset发生了升级,就无法再回到低级别的编码方式。因为降级可能导致数据精度丢失或者造成数据不一致,所以Redis设计上保持了一种"只升级不降级"的策略。因此,在使用intset时,需要根据实际情况选择合适的整数数据范围,避免频繁升级带来的性能损耗。 -------------------------------------------------------------------------------- /problems/介绍Redis事务(Redis能实现ACID吗).md: -------------------------------------------------------------------------------- 1 | Redis支持事务,可以通过MULTI、EXEC、DISCARD和WATCH等命令实现简单的事务操作。在Redis事务中,一组命令被原子性地执行,要么全部成功,要么全部失败,保证了数据的一致性。然而,Redis的事务并不是严格意义上的ACID事务,因为它缺少隔离性和持久性。因此,在需要强一致性和高隔离性的场景下,建议考虑使用关系型数据库等支持ACID事务的解决方案。 2 | 3 | -------------------------------------------------------------------------------- /problems/介绍SDSRedis为什么要使用SDS而不是C字符串.md: -------------------------------------------------------------------------------- 1 | SDS(Simple Dynamic Strings)是Redis自己实现的动态字符串结构,相比于C语言中的普通字符串(以'\0'结尾的字符数组),SDS具有以下优势: 2 | 3 | 1. 动态扩容:SDS可以根据需要动态分配内存空间,随着字符串长度的增加而自动扩容,避免了C字符串需要手动管理内存大小的问题。 4 | 2. 高效获取长度:SDS结构中包含了字符串的长度信息,因此获取字符串长度的操作时间复杂度为O(1),而C字符串需要遍历整个字符串才能获取长度。 5 | 3. 二进制安全:SDS可以存储任意二进制数据,不受'\0'结束符限制,适合存储图片、视频等二进制数据。 6 | 4. 兼容部分C字符串函数:Redis实现了一些SDS专属的API,并且支持一部分C字符串API,方便在一定情况下进行转换和使用。 -------------------------------------------------------------------------------- /problems/介绍dict,什么是rehash?什么是渐进式rehash.md: -------------------------------------------------------------------------------- 1 | 在Redis中,dict(字典)是一种用于存储键值对的数据结构,它类似于哈希表,可以实现快速的查找、插入和删除操作。dict内部使用哈希表来存储键值对,每个键值对会映射到哈希表中的一个桶(bucket)中。 2 | 3 | rehash是指Redis在进行扩容或缩容时,重新计算并重建哈希表的过程。当哈希表中元素数量超过了指定阈值(load factor)时,Redis会触发rehash操作来扩容哈希表,以减少冲突并提高查询效率。反之,如果元素数量变少,Redis也会通过rehash来缩小哈希表,节省内存空间。 4 | 5 | 渐进式rehash是一种优化技术,用于在进行rehash操作时避免长时间阻塞服务。传统的rehash操作需要一次性将所有键值对重新映射到新的哈希表中,可能会造成短暂的阻塞,影响系统性能。而渐进式rehash则会将rehash操作分解为多个小步骤,每次处理一部分键值对,逐步完成整个rehash过程,让系统在较长时间内平稳进行rehash操作,避免长时间阻塞。 6 | 7 | 通过渐进式rehash,Redis可以在不影响系统性能的情况下进行哈希表的扩容或缩容操作,保证服务的持续可用性。这种方式使得Redis能够更好地处理大规模数据集的哈希表操作,提升系统的稳定性和性能。 -------------------------------------------------------------------------------- /problems/介绍intset及其升级过程,支持降级吗.md: -------------------------------------------------------------------------------- 1 | intset是Redis中用于存储整数集合的数据结构。它是一种紧凑且高效的数据结构,能够在内存中存储一组整数,并且根据整数的类型使用不同的编码方式,包括int16、int32和int64,以节省内存空间。 2 | 3 | 在Redis中,当一个intset需要插入一个新元素时,会先判断该元素的类型是否超过了当前intset所能存储的最大类型,如果超过了,则会触发升级过程。升级过程会重新分配一个更大的intset,并将原有的元素全部复制到新的intset中,然后再插入新的元素。这样可以确保intset始终选择最适合的编码方式来存储整数,避免浪费内存空间。 4 | 5 | 至于降级操作,Redis并没有提供直接的降级机制。一旦intset发生了升级,就无法再回到低级别的编码方式。因为降级可能导致数据精度丢失或者造成数据不一致,所以Redis设计上保持了一种"只升级不降级"的策略。因此,在使用intset时,需要根据实际情况选择合适的整数数据范围,避免频繁升级带来的性能损耗。 -------------------------------------------------------------------------------- /problems/介绍ziplist,什么是连锁更新?quicklist、lispack.md: -------------------------------------------------------------------------------- 1 | ziplist是Redis中用于存储小规模列表和哈希集合的紧凑数据结构。ziplist将多个元素连续地存储在一段内存区域中,节省了内存空间,并且在元素数量较少时可以提高访问效率。 2 | 3 | 连锁更新是指在进行ziplist插入操作时,如果新元素的大小超过了ziplist当前可用的剩余空间,则会触发连锁更新机制。连锁更新会将当前的ziplist转换为一个普通的链表结构(即linked list),然后再进行插入操作,以确保插入成功。 4 | 5 | quicklist是Redis中用于存储列表类型的数据结构,它由多个ziplist组成的列表的双向链表。quicklist可以在不同的ziplist之间进行快速的插入和删除操作,同时也支持在两端进行快速的推入和弹出操作,适用于需要频繁修改的列表场景。 6 | 7 | lispack是Redis Labs开发的一种新型数据结构,旨在更好地处理大规模的列表数据。lispack结合了quicklist和压缩列表的优势,采用分层结构存储数据,并支持批量、范围等操作,提升了对大规模列表数据的处理性能和效率。lispack的引入使得Redis在处理大型列表时具有更好的灵活性和性能优势。 -------------------------------------------------------------------------------- /problems/介绍下Socket编程.md: -------------------------------------------------------------------------------- 1 | Socket编程是一种网络编程技术,用于在计算机网络上实现进程间的通信。通过Socket编程,程序员可以利用TCP/IP协议族中的套接字(Socket)接口,实现客户端和服务器之间的数据交换。 2 | 3 | 下面是Socket编程的基本流程: 4 | 5 | 1. **创建Socket**: 6 | - 在编写网络应用程序时,首先需要创建一个Socket对象。Socket是通信的端点,用于标识网络上的一个地址和端口。 7 | 2. **绑定Socket**: 8 | - 服务器程序需要将Socket绑定到特定的IP地址和端口号上,以便客户端能够连接到服务器。 9 | 3. **监听连接**: 10 | - 服务器Socket调用listen()函数开始监听来自客户端的连接请求。 11 | 4. **接受连接**: 12 | - 服务器Socket调用accept()函数接受客户端的连接请求,与客户端建立连接。 13 | 5. **发送和接收数据**: 14 | - 客户端和服务器可以通过send()和recv()等函数进行数据的发送和接收操作,实现通信。 15 | 6. **关闭连接**: 16 | - 通信结束后,客户端和服务器分别调用close()函数关闭连接,释放资源。 -------------------------------------------------------------------------------- /problems/介绍下分层存储体系和CPU三级缓存.md: -------------------------------------------------------------------------------- 1 | 分层存储体系是指在计算机系统中,根据访问速度和成本等因素将存储器按照层次结构划分为不同层次的存储介质,以实现性能和成本的平衡。一般来说,分层存储体系包括以下几个层次: 2 | 3 | 1. **寄存器**:位于CPU内部,速度最快,用于存储指令、数据和中间结果。 4 | 2. **高速缓存(Cache)**:位于CPU和主存之间,用于加速对常用数据的访问。通常分为多级缓存,如L1、L2、L3缓存,容量逐级增大、速度逐级减小。 5 | 3. **主存(RAM)**:容量较大但速度较快,存储程序运行时需要的数据和指令。 6 | 4. **辅助存储器**:容量更大但速度较慢,例如硬盘、固态硬盘(SSD),存储数据和程序文件等。 7 | 8 | 分层存储体系通过将数据根据其访问频率和重要性放置在不同层次的存储介质中,可以在保证性能的同时降低成本,并提高整体系统的效率。 9 | 10 | 至于CPU缓存(CPU Cache),是指位于CPU核心内部的高速缓存,用于存储当前或即将要访问的指令和数据。CPU缓存的设计是为了解决CPU与内存之间速度不匹配导致的性能瓶颈问题。CPU缓存一般包括以下几种: 11 | 12 | 1. **L1缓存**:位于CPU核心内部,速度最快,用于存储当前正在执行的指令和数据,分为指令缓存和数据缓存。 13 | 2. **L2缓存**:位于CPU核心外部,速度略慢于L1缓存,用于存储L1缓存未命中的数据。 14 | 3. **L3缓存**:位于多个CPU核心之间共享,速度相对较慢,用于存储多个核心共享的数据。 -------------------------------------------------------------------------------- /problems/介绍下进程的地址空间(虚拟地址和物理地址).md: -------------------------------------------------------------------------------- 1 | 1. **虚拟地址空间**: 2 | - 每个进程在运行时都有自己独立的虚拟地址空间,从逻辑上看,每个进程都拥有连续而私有的地址空间。 3 | - 虚拟地址空间是一个抽象概念,它包含了进程可以访问的所有内存地址,但不需要实际的对应物理内存。通过虚拟地址空间,进程可以访问代码、数据、堆、栈以及操作系统内核提供的其他资源。 4 | - 虚拟地址空间通常被分为几个部分,如代码段(text segment)、数据段(data segment)、堆(heap)和栈(stack)等,用于存放不同类型的数据和程序代码。 5 | 2. **物理地址空间**: 6 | - 物理地址空间是实际存在于计算机硬件中的内存地址空间,用于存储进程的真实数据和程序代码。 7 | - 当进程访问虚拟地址时,操作系统会将虚拟地址转换为对应的物理地址,从而找到数据或指令在内存中的真正位置。 8 | - 物理地址空间是由计算机的物理内存组成,包括主存(RAM)、磁盘交换空间(swap space)等。 9 | 3. **地址映射**: 10 | - 在操作系统中,虚拟地址到物理地址的映射是通过内存管理单元(MMU)来实现的。MMU负责将进程发出的虚拟地址转换为对应的物理地址,以实现内存的访问。 11 | - 当进程访问某个虚拟地址时,如果该地址未映射到物理内存,则会触发缺页中断(page fault),操作系统负责将相应的页面加载到内存中,并更新页表进行地址映射。 12 | 4. **地址空间的划分**: 13 | - 操作系统将地址空间划分为多个页面(page)或段(segment),以便更有效地管理内存。每个页面或段有固定大小,通常为4KB~8KB。 14 | - 虚拟地址空间中的页面称为虚拟页面(virtual page),物理地址空间中的页面称为物理页面(physical page)。页面的映射关系由页表记录,操作系统负责维护和管理页表。 15 | 16 | 总的来说,进程的地址空间既包括虚拟地址空间(供进程使用的逻辑地址)也包 -------------------------------------------------------------------------------- /problems/从本地读取一个文件通过网络发送到另一端,中间涉及几次拷贝.md: -------------------------------------------------------------------------------- 1 | 1. **从本地读取文件**: 2 | - 首先,在本地计算机上打开待发送的文件,并从文件中读取数据。 3 | - 读取数据时,通常会将数据存储在内存中的缓冲区中。 4 | 2. **发送数据到网络**: 5 | - 接下来,将内存中的数据通过网络发送到目标计算机的服务器端。 6 | - 在发送数据之前,可能需要将内存中的数据进行分段、封装成数据包等操作。 7 | 3. **接收数据到目标计算机**: 8 | - 目标计算机的服务器端接收到数据后,将数据存储在内存中的缓冲区中。 9 | 4. **将数据写入目标文件**: 10 | - 最后,目标计算机上的程序将接收到的数据写入到目标文件中。 11 | 12 | 在这个过程中,涉及到的数据拷贝包括: 13 | 14 | - 本地文件数据到内存缓冲区的拷贝:从文件系统读取数据到内存中。 15 | - 内存缓冲区数据到网络发送缓冲区的拷贝:将内存中的数据复制到网络发送缓冲区。 16 | - 网络接收缓冲区到内存缓冲区的拷贝:从网络接收数据到内存中。 17 | - 内存缓冲区数据到目标文件的拷贝:将内存中的数据写入到目标文件中。 -------------------------------------------------------------------------------- /problems/优化索引的办法有那些.md: -------------------------------------------------------------------------------- 1 | 1. **合理设置索引:** 不是所有列都需要设置索引,如果一个列上的值大部分都相同,那么对这样的列创建索引可能并不会带来太大的性能提升。你只应该为经常出现在查询条件(WHERE 子句)中或者经常需要排序、分组的列创建索引。 2 | 2. **使用最适合的索引类型:** 根据你的数据和查询需求选择最适合的索引类型。如 B-Tree 索引适用于全值匹配和范围查询,Hash 索引适用于等值查询,Bitmap 索引适用于低基数列,Gin 和 Gist 索引适用于全文搜索和地理空间数据查询等。 3 | 3. **多列索引:** 如果你的查询通常包含多个列,那么可以考虑创建涵盖所有这些列的复合索引。注意复合索引的列顺序也会影响其效果,所以要根据实际的查询习惯来设置列的顺序。 4 | 4. **保持索引尽量小:** 索引越小,IO 操作越少,性能就越好。你可以考虑只为字符串的前面几个字符创建索引(前缀索引),或者只为频繁查询的部分数据创建索引等方式来减小索引的大小。 5 | 5. **定期维护和重建索引:** 随着数据的增加和删除,索引可能会变得碎片化,导致性能下降。定期的维护和重建索引可以保持索引的效率。 6 | 6. **监控索引的使用情况:** 多数数据库管理系统都有工具或命令可以查看索引的使用情况,通过这些工具你可以找出哪些索引被利用了,哪些索引从未使用过。这样可以帮助你清理无用的索引,节省存储空间。 7 | 7. **谨慎处理更新频繁的列:** 对一个列的更新操作,都会引发相关索引的更新,如果一个列的更新非常频繁,那么它的索引可能会成为性能瓶颈。对于这样的列,你需要权衡索引带来的查询性能提升和更新性能损失,决定是否为其创建索引。 8 | 8. **适当的数据库设计:** 数据库设计也与索引优化相关,例如 normalization 和 denormalization,可以考虑把常用的计算结果保存下来而不是每次查询时都重新计算。 -------------------------------------------------------------------------------- /problems/关系型数据库和非关系数据库的特点.md: -------------------------------------------------------------------------------- 1 | **关系型数据库的特点**: 2 | 3 | 1. **结构化查询语言(SQL)**:关系型数据库使用SQL作为查询语言,这是一种强大的工具,可以用来执行各种复杂的查询和数据操作。 4 | 2. **表格结构**:数据以表格的形式存储,表格由行和列组成。每行代表一个记录,每列代表一个字段。 5 | 3. **数据完整性**:提供一系列的完整性约束,包括主键、外键、唯一性约束等,以保证数据的准确性和一致性。 6 | 4. **事务支持**:支持事务处理,允许多个数据库操作作为一个单一的工作单元来执行,它们要么全部完成,要么全部不执行(具备ACID属性:原子性、一致性、隔离性和持久性)。 7 | 5. **规范化**:数据库设计通常使用规范化来避免数据冗余和依赖,使得数据模型更加稳定和可扩展。 8 | 9 | **非关系型数据库的特点**: 10 | 11 | 1. **灵活的数据模型**:非关系型数据库通常不需要固定的模式,可以存储结构化、半结构化或非结构化的数据。 12 | 2. **水平可扩展性**:非关系型数据库通常设计为易于扩展,可以通过增加更多的服务器来扩展数据库,以便处理更大的数据量和更高的负载。 13 | 3. **多样化的数据类型**:NoSQL数据库有多种类型,如键值存储、文档数据库、宽列存储和图数据库等,每种类型都有其特定的用例和优势。 14 | 4. **性能优化**:为了提高性能和可扩展性,一些非关系型数据库牺牲了ACID事务特性,采用了最终一致性模型。 15 | 5. **高吞吐量**:非关系型数据库通常可以提供更高的读写吞吐量,特别是在分布式数据存储的情况下。 16 | 17 | **适用场景**: 18 | 19 | - **关系型数据库**:适合于需要严格数据完整性、复杂事务处理和明确数据结构的应用场景,如金融服务、会计系统和其他需要精确数据管理的环境。 20 | - **非关系型数据库**:适用于需要处理大量非结构化数据、快速迭代开发和强调水平可扩展性的场景,如社交网络、大规模在线服务和实时数据分析等。 -------------------------------------------------------------------------------- /problems/内存映射文件是什么?如何用它来处理大文件?.md: -------------------------------------------------------------------------------- 1 | 内存映射文件是一种内存管理功能,它允许应用程序将磁盘上的文件内容映射到进程的地址空间。这样做的好处是可以像访问普通内存那样访问文件数据,通过指针直接读写文件,而不是调用传统的文件I/O操作(如read和write)。这种方式可以提高处理大型文件时的性能,特别是需要频繁随机访问文件部分内容的场景,常见的如`mmap`: 2 | 3 | ### 使用内存映射文件处理大文件 4 | 5 | 1. **创建内存映射**: 在Unix-like系统中,可以使用`mmap`函数来创建内存映射;在Windows系统中,可以使用`CreateFileMapping`和`MapViewOfFile`等函数。 6 | 2. **访问数据**: 一旦文件被映射到内存,你就可以使用指针来访问文件的内容,就像它是一个巨大的数组或者是连续内存块。 7 | 3. **同步和清理**: 当对映射区域进行了写操作后,你可能需要确保数据刷新到磁盘。在Unix系统中,可以使用`msync`来同步;在完成操作后,使用`munmap`撤销映射。在Windows系统中,则使用`UnmapViewOfFile`来撤销映射,使用`FlushViewOfFile`来同步数据。 -------------------------------------------------------------------------------- /problems/内存有限,如何在100亿数据中找到中位数.md: -------------------------------------------------------------------------------- 1 | 在内存有限的情况下,可以使用分块算法和中位数选择算法来解决这个问题。具体步骤如下: 2 | 3 | 1. 将100亿数据划分为多个块(每个块包含一部分数据),并对每个块进行排序。 4 | 2. 对于每个块,找到其中位数,并将这些中位数组成一个新的数组。 5 | 3. 使用中位数选择算法(如快速选择算法)找到这个新数组的中位数。 6 | 4. 根据中位数将原始数据分为两部分,分别统计比中位数小和比中位数大的数的数量。 7 | 5. 根据数量的关系,递归地在小于中位数的部分或者大于中位数的部分中继续寻找中位数,直到找到整个数据集的中位数。 8 | 9 | 需要注意的是,在实际处理过程中,需要合理选择块的大小以平衡排序时间和内存占用,同时也要考虑优化策略,比如采用外部排序算法、利用二进制搜索等方法来提高效率。 -------------------------------------------------------------------------------- /problems/内存有限,如何在20亿个整数中找到出现次数最多的数.md: -------------------------------------------------------------------------------- 1 | 在内存有限的情况下,可以使用外部排序算法和哈希计数来解决这个问题。具体步骤如下: 2 | 3 | 1. 使用外部排序算法将20亿个整数分为多个小文件,并对每个小文件进行内部排序。 4 | 2. 遍历每个小文件,使用哈希表来计算每个数字出现的次数。 5 | 3. 将所有小文件中的哈希表合并到一个大的哈希表中,相同数字的计数值累加。 6 | 4. 遍历大的哈希表,找到出现次数最多的数字即可。 7 | 8 | 需要注意的是,在实际处理过程中,还需要考虑一些优化策略,以提高计算效率和减少内存占用,比如使用位图法来减少哈希表的大小、采用分布式计算等技术。 -------------------------------------------------------------------------------- /problems/内存有限,如何在2亿个整数中找出不连续的最小数.md: -------------------------------------------------------------------------------- 1 | 在内存有限的情况下,可以使用位图法来解决这个问题。具体步骤如下: 2 | 3 | 1. 遍历2亿个整数,并对每个整数在位图中做标记(将对应位置置为1)。 4 | 2. 从最小的整数开始逐个检查位图,找到第一个未被标记的位置,即为最小的不连续整数。 5 | 3. 如果所有整数都已经被标记,则说明不存在不连续的整数。 6 | 7 | 需要注意的是,在实际处理过程中,要考虑位图的大小和内存占用情况,以及可能存在的位图压缩技术来节省空间。另外,在实际实现时,也可以结合其他数据结构和算法来进一步优化性能。 -------------------------------------------------------------------------------- /problems/内存有限,如何在40亿个非负整数中找到所有未出现的数.md: -------------------------------------------------------------------------------- 1 | 在内存有限的情况下,可以使用分块算法来解决这个问题。具体步骤如下: 2 | 3 | 1. 将40亿个非负整数划分为多个块(比如每个块包含1亿个数),并对每个块进行排序。 4 | 2. 对于每个块,记录其中出现的最小和最大数,以及包含的数字总数。 5 | 3. 遍历所有块,记录出现过的数字。 6 | 4. 找到所有未出现的数字,可以根据每一块中的信息计算出来。 7 | 8 | 需要注意的是,在实际处理过程中,还需要考虑一些优化策略,以提高计算效率和减少内存占用,比如采用BitMap技术来记录数字的出现情况,或者使用哈希表等数据结构来提高查找速度,同时也要合理选择块的大小,以平衡排序时间和遍历时间的开销。 -------------------------------------------------------------------------------- /problems/内存有限,怎么对100亿数据进行排序(大数据小内存排序问题).md: -------------------------------------------------------------------------------- 1 | 对于内存有限的情况下需要对大规模数据进行排序,可以采用外部排序(External Sorting)算法。外部排序是一种适用于处理大规模数据且内存有限的排序方法,它通常涉及到磁盘I/O 操作,将数据划分成多个块并在内存中进行排序。 2 | 3 | 以下是一个简单的基于外部排序的思路,以对 100 亿数据进行排序为例: 4 | 5 | 1. 将待排序的 100 亿数据分成若干个小块,每个小块的大小适应你的内存大小。 6 | 2. 对每个小块使用内存排序算法(如快速排序或归并排序)进行排序。 7 | 3. 将排好序的小块写入外部存储(如硬盘)。 8 | 4. 逐个读取已排序的小块,并使用合并排序(merge sort)或堆排序等合并算法来将这些有序小块进行合并排序。 9 | 5. 最终得到完整的有序数据集。 10 | 11 | 需要注意的是,在第 4 步进行合并排序时,需要在内存中维护一个最小堆或者缓冲区,从每个小块中依次读取数据并按照顺序合并排序。这样做的好处是可以减少对磁盘的访问次数,提高排序效率。 12 | 13 | 外部排序虽然需要额外的磁盘I/O 操作,但可以有效地处理大规模数据且内存有限的情况。在实际应用中,可以根据具体需求和环境选择合适的外部排序算法来解决大数据小内存排序问题。 -------------------------------------------------------------------------------- /problems/内存池是什么?在C++中如何设计一个简单的内存池?.md: -------------------------------------------------------------------------------- 1 | 内存池是一种内存分配方式,它预先在内存中分配一定数量的块或对象,形成一个“池”。当程序需要分配内存时,它从这个池中分配一个块;当内存被释放时,这个块返回到池中以供再次使用。内存池可以显著减少频繁分配和释放内存所带来的开销,并且有助于避免内存碎片化,提高内存使用效率。 2 | 3 | 下面展示了如何设计一个简单的内存池,这个简单的内存池设计包括以下几个关键特性: 4 | 5 | - **预分配**:在构造函数中预先分配了一定数量的固定大小内存块。 6 | - **分配与释放**:`allocate` 方法从池中分配一个内存块,而 `deallocate` 方法则将不再使用的内存块返还给池。 7 | - **管理策略**:本例中使用 `std::list` 来管理空闲内存块,但实际应用中可能需考虑更高效的数据结构。 8 | 9 | ``` 10 | #include 11 | #include 12 | 13 | class MemoryPool { 14 | public: 15 | MemoryPool(size_t size, unsigned int count) { 16 | for (unsigned int i = 0; i < count; ++i) { 17 | freeBlocks.push_back(new char[size]); 18 | } 19 | blockSize = size; 20 | } 21 | 22 | ~MemoryPool() { 23 | for (auto block : freeBlocks) { 24 | delete[] block; 25 | } 26 | } 27 | 28 | void* allocate() { 29 | if (freeBlocks.empty()) { 30 | throw std::bad_alloc(); 31 | } 32 | 33 | char* block = freeBlocks.front(); 34 | freeBlocks.pop_front(); 35 | return block; 36 | } 37 | 38 | void deallocate(void* block) { 39 | freeBlocks.push_back(static_cast(block)); 40 | } 41 | 42 | private: 43 | std::list freeBlocks; 44 | size_t blockSize; 45 | }; 46 | 47 | // 使用示例 48 | int main() { 49 | const int blockSize = 32; // 块大小 50 | const int blockCount = 10; // 块数量 51 | MemoryPool pool(blockSize, blockCount); 52 | 53 | // 分配内存 54 | void* ptr1 = pool.allocate(); 55 | void* ptr2 = pool.allocate(); 56 | 57 | // 使用ptr1和ptr2... 58 | 59 | // 释放内存 60 | pool.deallocate(ptr1); 61 | pool.deallocate(ptr2); 62 | 63 | return 0; 64 | } 65 | 66 | ``` 67 | 68 | -------------------------------------------------------------------------------- /problems/内联函数、构造函数、静态成员函数可以是虚函数吗.md: -------------------------------------------------------------------------------- 1 | 2 | inline, static, constructor三种函数都不能带有virtual关键字。 3 | inline是编译时展开,必须有实体; 4 | static属于class自己的,也必须有实体; 5 | virtual函数基于vtable(内存空间),constructor函数如果是virtual的,调用时也需要根据vtable寻找,但是constructor是virtual的情况下是找不到的,因为constructor自己本身都不存在了,创建不到class的实例,没有实例,class的成员(除了public static/protected static for friend class/functions,其余无论是否virtual)都不能被访问了。 6 | 7 | 虚函数实际上不能被内联:虚函数运行时所需的代价主要是虚函数不能是内联函。这也是非常好理解的,是因为内联函数是指在编译期间用被调用的函数体本身来代替函数调用的指令,但是虚函数的“虚”是指“直到运行时才能知道要调用的是哪一个函数。”但虚函数的运行时多态特性就是要在运行时才知道具体调用哪个虚函数,所以没法在编译时进行内联函数展开。当然如果通过对象直接调用虚函数它是可以被内联,但是大多数虚函数是通过对象的指针或引用被调用的,这种调用不能被内联。 因为这种调用是标准的调用方式,所以虚函数实际上不能被内联。 8 | 9 | 构造函数不能是虚函数。而且,在构造函数中调用虚函数,实际执行的是父类的对应函数,因为自己还没有构造好, 多态是被disable的。 10 | 11 | 静态的对象是属于整个类的,不对某一个对象而言,同时其函数的指针存放也不同于一般的成员函数,其无法成为一个对象的虚函数的指针以实现由此带来的动态机制。 12 | 13 | -------------------------------------------------------------------------------- /problems/写string类的构造,析构,拷贝函数.md: -------------------------------------------------------------------------------- 1 | 下面是一个简单的示例(以C++ 03为例) 2 | 3 | 注意在写的时候,针对于指针资源的配置与释放需要尤其注意,以及字符串的自赋值。这是写的时候比较容易出错的两个关键点,切记。 4 | 5 | ```C++ 6 | class MyString { 7 | private: 8 | char* m_Data; 9 | int m_Length; 10 | 11 | public: 12 | // 默认构造函数 13 | MyString() : m_Data(new char[1]), m_Length(0) { 14 | m_Data[0] = '\0'; 15 | } 16 | 17 | // 参数化构造函数 18 | MyString(const char* str) { 19 | m_Length = strlen(str); 20 | m_Data = new char[m_Length + 1]; 21 | strcpy(m_Data, str); 22 | } 23 | 24 | // 拷贝构造函数 25 | MyString(const MyString& other) { 26 | m_Length = other.m_Length; 27 | if (m_Length > 0) { 28 | m_Data = new char[m_Length + 1]; 29 | strcpy(m_Data, other.m_Data); 30 | } else { 31 | m_Data = new char[1]; 32 | m_Data[0] = '\0'; 33 | } 34 | } 35 | 36 | // 析构函数 37 | ~MyString() { 38 | delete[] m_Data; 39 | } 40 | 41 | // 拷贝赋值运算符 42 | MyString& operator=(const MyString& other) { 43 | 44 | // 注意! 45 | if (&other == this) { 46 | return *this; 47 | } 48 | 49 | delete[] m_Data; 50 | m_Length = other.m_Length; 51 | if (m_Length > 0) { 52 | m_Data = new char[m_Length + 1]; 53 | strcpy(m_Data, other.m_Data); 54 | } else { 55 | m_Data = new char[1]; 56 | m_Data[0] = '\0'; 57 | } 58 | 59 | return *this; 60 | } 61 | }; 62 | 63 | ``` 64 | 65 | -------------------------------------------------------------------------------- /problems/函数参数的入栈顺序是什么,从左到右还是从右到左.md: -------------------------------------------------------------------------------- 1 | 在C++中,函数参数的入栈顺序是从右到左的。也就是说,最后一个参数先入栈,第一个参数最后入栈。 -------------------------------------------------------------------------------- /problems/分段和分页的区别有那些?.md: -------------------------------------------------------------------------------- 1 | 1. 目的不同:分段是为了使程序和数据可以分开处理,并且可以根据需要动态地改变长度。而分页则主要是为了简化内存管理,避免产生外部碎片。 2 | 2. 管理单位不同:分段的基本单位是段,每个段都有一定的逻辑意义,如程序、数据等;分页的基本单位是页,页通常固定大小(如4KB),并无特定的逻辑意义。 3 | 3. 处理方式不同:分段是根据用户的需求进行划分,每个段的长度会随着需求的不同而不同。分页则是将内存划分为一定大小的块,每个进程也被划分为相同大小的页。 4 | 4. 地址结构不同:分段的地址由段号和段内偏移量组成;分页的地址由页号和页内偏移量组成。 5 | 5. 内存利用率:分段可能会出现内部碎片,因为段的大小不一致,可能无法完全使用已分配的内存。分页则能很好地避免内部碎片,但可能会产生较小的外部碎片。 6 | 6. 碎片问题:分段可能导致外部碎片,因为当段被释放后,剩余的空间可能无法满足其他段的需求。分页由于页面大小的统一,能够有效地避免外部碎片的问题。 -------------------------------------------------------------------------------- /problems/分页和分段的区别是什么?.md: -------------------------------------------------------------------------------- 1 | - **目标**:分页的设计目的主要是为了简化内存管理和消除外部碎片;而分段则旨在反映程序的逻辑结构,使之与物理内存分配相匹配。 2 | - **灵活性**:分段提供了更多的灵活性,允许程序自然地划分为意义不同的部分;分页则主要关注于内存的有效利用。 3 | - **易用性**:分页对于程序员来说是透明的,更易于使用;分段则要求程序员有更多的内存管理责任。 -------------------------------------------------------------------------------- /problems/列举你所知道的tcp选项.md: -------------------------------------------------------------------------------- 1 | TCP选项是TCP首部中的一部分,用来指定一些可选的协议参数或功能。除了常见的大小和头部信息,以下是一些常见的功能性TCP选项: 2 | 3 | 1. **MSS**:这个选项用于指定TCP数据段的最大长度。它在建立连接时由双方协商确定,以适应网络环境并避免IP包的分片。 4 | 2. **窗口缩放**:窗口缩放选项使得TCP可以使用更大的接收窗口,从而提高数据传输效率。它通过指定一个缩放因子,该因子用于将16位的窗口字段左移以得到实际的接收窗口大小。 5 | 3. **时间戳)**:时间戳选项为每个TCP包添加发送和接收的时间信息,用于RTT(往返时延)测量和PAWS(防止旧分片)等功能。 6 | 4. **选择性确认**:SACK选项允许接收方只确认收到的非连续数据段,而不是确认收到的最后一个连续数据段。这样可以减少网络拥塞情况下不必要的重传。 7 | 5. **NOP:NOP选项没有任何操作,主要用于填充空间,确保其他选项可以在32位边界上对齐。 8 | 6. **结束**:这个选项表示TCP选项列表的结束,通常用于当选项列表未能填充满整个TCP首部时的填充。 9 | 10 | -------------------------------------------------------------------------------- /problems/动态链接和静态链接的区别.md: -------------------------------------------------------------------------------- 1 | 主要区别在于链接到程序的**时间**和**方式**: 2 | 3 | 1. 静态链接: 4 | - **在编译期间,静态库的代码就被包含进了目标可执行文件中,之后即使没有静态库文件,也能正常运行。** 5 | - 生成的可执行文件较大,因为它包含了所有需要的库函数。 6 | - 更改静态库的功能或修复错误需要重新编译应用程序,分发新版本的可执行文件。 7 | 2. 动态链接: 8 | - **动态链接是在运行时完成的**,也就是说,在编译阶段并不会把库文件的代码加入到可执行文件,而只是添加一些引用信息。在程序执行时,由操作系统负责将动态库加载到内存中供程序使用。 9 | - 可执行文件较小,因为它仅仅包含对动态库的引用,而非实际的代码。 10 | - 如果多个应用程序都使用相同的库,那么这个库只需要在内存中保留一份即可,节约了系统资源。 11 | - **当动态库更新时,不需要重新编译和链接应用程序,运行程序时自然会调用新版本的库。** 12 | 13 | 每种链接方式都有其利弊。静态链接产生的程序更独立,但可能导致二进制文件很大;动态链接可以共享库提高效率,但如果相关的库不在系统上,那么程序无法运行。 -------------------------------------------------------------------------------- /problems/协程是什么,为什么需要协程.md: -------------------------------------------------------------------------------- 1 | #### 1.协程 2 | 3 | 本质⽤户空间下的线程 4 | 5 | 拥有⾃⼰的寄存器上下⽂和栈 6 | 7 | 切换情况:先将寄存器上下⽂和栈保存,等切换回来的时候再进⾏恢复 8 | 9 | **2、原因** 10 | 11 | ⼀是节省 CPU,避免系统内核级的线程频繁切换,造成的 CPU 资源浪费。⽽协程是⽤户态的线程,⽤户可以⾃⾏ 12 | 13 | 控制协程的创建与销毁,极⼤程度避免了系统级线程上下⽂切换造成的资源浪费。 14 | 15 | ⼆是节约内存,在 64 位的 Linux 中,**⼀个线程**需要分配 8MB 栈内存和 64MB 堆内存,系统内存的制约⽆法开启 16 | 17 | 更多线程实现⾼并发。但是协程他的大小只有几十kb ,可以轻松有⼗⼏万协程,这是线程⽆法⽐拟的。 18 | 19 | 三是稳定性,前⾯提到线程之间通过内存来共享数据,这也导致了⼀个问题,任何⼀个线程出错时,进程中的所有 20 | 21 | 线程都会跟着⼀起崩溃。 22 | 23 | 四是开发效率,使⽤协程在开发程序之中,可以很⽅便的将⼀些耗时的 IO 操作异步化,例如写⽂件、耗时 IO 请求 24 | 25 | 等。 -------------------------------------------------------------------------------- /problems/单线程怎么保证高并发?.md: -------------------------------------------------------------------------------- 1 | 1. **事件驱动模型**: 2 | - 使用事件驱动的方式,将任务分解成独立的事件或任务,并通过事件循环机制依次处理这些事件。 3 | - 单线程通过事件驱动模型可以实现非阻塞IO操作,提高系统的并发处理能力。 4 | 2. **异步编程**: 5 | - 利用异步编程模型,将IO操作转化为异步调用,在等待IO操作完成时不会阻塞主线程执行。 6 | - 可以通过回调函数、Promise对象、async/await等方式来处理异步任务,充分利用CPU资源。 7 | 3. **线程池**: 8 | - 单线程环境下可以使用线程池来管理多个任务的执行,通过合理配置线程池的大小和任务队列来提高并发处理能力。 9 | - 在单线程中采用线程池技术可以实现任务的并行处理,提高系统的性能。 10 | 4. **利用缓存**: 11 | - 对于需要频繁访问的数据,可以使用缓存机制来提高数据读取速度,减少对数据库或文件系统的IO操作。 12 | - 合理利用内存缓存可以降低系统负载,提高响应速度。 13 | 5. **优化算法和数据结构**: 14 | - 设计高效的算法和数据结构可以减少计算和存储开销,提高系统的运行效率。 15 | - 在单线程环境中,通过优化算法和数据结构可以减少计算时间和空间复杂度,从而实现高并发处理。 -------------------------------------------------------------------------------- /problems/原子变量的内存序是什么?.md: -------------------------------------------------------------------------------- 1 | 在C++11及之后的标准中,为了给开发者提供更细粒度的控制以及可能的性能优化空间,引入了多种内存顺序选项: 2 | 3 | 1. **memory_order_relaxed**:放松的内存顺序。不对执行顺序做任何保证,除了原子操作本身的原子性。这意味着,在没有其他同步操作手段的情况下,读写操作的顺序可能与程序代码中的顺序不一致。 4 | 2. **memory_order_consume**:较轻量级的保序需求,用于指定操作依赖于先前的某些操作结果。这在实际实现中通常被视为与`memory_order_acquire`相同。 5 | 3. **memory_order_acquire**:获取操作,禁止后续的读或写被重排到当前操作之前。用于读取操作。 6 | 4. **memory_order_release**:释放操作,防止之前的读或写操作被重排到当前操作之后。用于写入操作。 7 | 5. **memory_order_acq_rel**:同时包含获取和释放语义。适用于同时具有读取和写入特性的操作。 8 | 6. **memory_order_seq_cst**:顺序一致性内存顺序。所有线程看到的操作顺序一致。这是默认的内存顺序,并且提供了最强的顺序保证。 9 | 10 | 内存顺序的选择影响着程序的正确性和性能。较弱的内存顺序(例如`memory_order_relaxed`)可能带来更好的性能,因为它们允许更多的指令重排序;但是,使用它们也需要更小心地设计程序,以避免数据竞争和其他并发相关的错误。相反,`memory_order_seq_cst`提供了最简单和最直观的并发模型,但可能因为额外的同步代价而影响性能。 -------------------------------------------------------------------------------- /problems/同一个类的两个对象的虚函数表是如何维护的?.md: -------------------------------------------------------------------------------- 1 | 虚函数表是由类维护的,而非单独为每个对象维护的。虚函数表的维护发生在编译期和链接期间,编译器和链接器会处理虚函数表的创建和初始化。运行时,当调用对象的虚函数时,程序会通过对象的虚函数表指针(vptr)来查找相应函数的地址,并执行对应的函数。 2 | 3 | 以下是虚函数表的工作流程的简化视图: 4 | 5 | 1. 类定义:编译器检测到类中有虚函数声明,为该类创建一个虚函数表。 6 | 2. 类实例化:当创建类的对象时,每个对象在内存中都会包含一个指向类虚函数表的指针(vptr)。同一个类的所有对象共享同一虚函数表,不会为每个对象单独创建新的虚函数表。 7 | 3. 虚函数调用:当通过基类指针或引用调用虚函数时,程序使用vptr找到虚函数表,然后通过表中的适当条目来定位要调用的函数地址,从而实现多态性。 8 | 9 | 因此,同一个类的两个对象使用相同的虚函数表,但它们各自持有自己的指向这个虚函数表的指针。如果派生类重写了某些虚函数,那么它将拥有自己的虚函数表,其中包含了重写函数的新地址,以及未被重写函数的原始地址。派生类对象的虚函数表指针将指向这个派生类特有的虚函数表。 -------------------------------------------------------------------------------- /problems/同步,异步,阻塞和非阻塞的概念.md: -------------------------------------------------------------------------------- 1 | 1. 同步:在同步操作中,调用者会等待操作完成后才返回。例如,在进行磁盘读写或者网络数据传输时,如果使用同步方式,那么在整个读写或者传输过程完成之前,调用者会一直等待。 2 | 2. 异步:在异步操作中,调用者发起操作后不会立刻等待结果,而是可以继续做其他事情。当操作完成后,通过某种机制通知调用者。这样,异步操作可以帮助提高程序的并发性和响应性。 3 | 3. 阻塞:在阻塞状态下,调用者在等待操作完成期间无法进行其他操作,必须等待当前操作完成后才能继续,它会导致程序暂停执行。 4 | 4. 非阻塞:在非阻塞状态下,即使操作还没有完成,调用者也能立即返回,进行其他操作。如果操作未完成,调用者可能需要定期检查操作状态,或者通过某种机制接收操作完成的通知。 5 | 6 | 这四种概念往往会成对出现,例如“同步阻塞”和“异步非阻塞”。一个典型的例子就是网络编程中的I/O模型,其中包括同步阻塞I/O、同步非阻塞I/O、异步阻塞I/O和异步非阻塞I/O。 -------------------------------------------------------------------------------- /problems/固态硬盘和机械硬盘区别.md: -------------------------------------------------------------------------------- 1 | 1. **工作原理**: 2 | - 固态硬盘:固态硬盘使用闪存芯片作为存储介质,数据通过电子方式读写,不需要移动部件。固态硬盘属于非易失性存储器,数据保存在芯片中不会因断电而丢失。 3 | - 机械硬盘:机械硬盘使用旋转磁盘和磁头进行数据读写,数据存储在磁盘上的涂层中,需要通过机械运动扫描磁道来读取数据。 4 | 2. **速度**: 5 | - 固态硬盘:由于没有移动部件,固态硬盘的读写速度非常快,可以显著提高系统的响应速度和文件传输速度。 6 | - 机械硬盘:机械硬盘受制于物理旋转速度和寻道时间,速度相对较慢,尤其在随机读写操作时表现不如固态硬盘。 7 | 3. **耐用性**: 8 | - 固态硬盘:固态硬盘没有移动部件,抗震抗摔性能好,更耐用,并且支持更多次的读写操作。 9 | - 机械硬盘:机械硬盘内部有旋转磁盘,易受震动影响,容易损坏,并且使用寿命相对较短。 10 | 4. **功耗和噪音**: 11 | - 固态硬盘:固态硬盘不需要机械运动,功耗较低,不产生噪音。 12 | - 机械硬盘:机械硬盘需要马达和磁头等机械部件运作,功耗较高,并且会产生一定的噪音。 13 | 5. **价格**: 14 | - 固态硬盘:相对于机械硬盘,固态硬盘的价格通常更高,但随着技术的发展和产量的增加,价格正在逐渐下降。 15 | - 机械硬盘:机械硬盘的价格相对便宜,容量大,适合存储大量数据。 -------------------------------------------------------------------------------- /problems/在32位和64位系统中,指针分别为多大?.md: -------------------------------------------------------------------------------- 1 | 在32位系统中,指针的大小通常为4字节,即32位。 2 | 3 | 而在64位系统中,指针的大小通常为8字节,即64位。 4 | 5 | 这是因为在不同位数的系统中,内存寻址空间的大小不同。32位系统的地址总线宽度为32位,所以能够寻址的内存空间为2^32个地址,即4GB;而64位系统的地址总线宽度为64位,因此可以寻址的内存空间更大,达到了2^64个地址。 -------------------------------------------------------------------------------- /problems/在C++中为什么需要深拷贝,浅拷贝会存在哪些问题?.md: -------------------------------------------------------------------------------- 1 | 浅拷贝可能带来以下问题: 2 | 3 | - **双重释放**:当原始对象和拷贝对象生命周期结束时,它们的析构函数可能都会尝试释放同一个内存块,导致运行时错误。 4 | - **数据竞争**:两个对象操作相同的资源可能导致数据不一致。 5 | - **野指针**:一个对象释放了共享内存后,另一个对象就持有了一个野指针,继续访问该内存区域会引发未定义行为。 6 | 7 | 深拷贝解决了浅拷贝的以下问题: 8 | 9 | - **避免双重释放**:每个对象负责其自己的资源释放,因此不会出现双重释放问题。 10 | - **避免数据竞争和野指针**:由于每个对象都有自己的数据副本,它们互不干扰。 -------------------------------------------------------------------------------- /problems/在C++中,三个全局变量相互依赖,程序应该如何初始化呢?300个呢?.md: -------------------------------------------------------------------------------- 1 | 在C++中,如果有多个全局变量相互依赖,初始化顺序可能会导致问题,因为**全局变量的初始化顺序在不同编译单元之间是未定义的**。这意味着,如果一个全局变量依赖于另一个尚未初始化的全局变量,那么程序可能会出现运行时错误。 2 | 3 | 我们可以利用**构造函数中的初始化技巧,确保每个变量首次访问的时候进行初始化**,像这样,并且在变量多的时候去利用单例模式去控制他们的初始化顺序: 4 | 5 | ```c++ 6 | class GlobalResources { 7 | private: 8 | A a; 9 | B b; // 假设B依赖A 10 | C c; // 假设C依赖A和B 11 | // ... 其他依赖项 12 | 13 | GlobalResources() : a(), b(a), c(a, b) /* etc. */ {} 14 | 15 | public: 16 | static GlobalResources& instance() { 17 | static GlobalResources instance; 18 | return instance; 19 | } 20 | 21 | A& getA() { return a; } 22 | B& getB() { return b; } 23 | C& getC() { return c; } 24 | // ... 提供对其他资源的访问 25 | }; 26 | 27 | ``` 28 | 29 | -------------------------------------------------------------------------------- /problems/在C++中,对一个对象先malloc后delete这样使用可以吗?有什么风险.md: -------------------------------------------------------------------------------- 1 | 不可以 2 | 3 | 风险如下: 4 | 5 | 1. 构造函数未被调用:使用 `malloc` 分配内存时,对象的构造函数不会被执行,导致对象可能处于未初始化的状态。 6 | 2. 析构函数问题:如果你使用 `malloc` 分配而后使用 `delete` 释放内存,会尝试调用对象的析构函数,但因为构造函数从未被调用过,这可能导致未定义行为,比如资源泄露、崩溃或数据损坏等问题。 7 | 3. 兼容性问题:`malloc` 和 `delete` 内部实现可能有差异,混用可能导致堆损坏。 8 | 4. 异常安全性:由于 `malloc` 和 `delete` 的异常处理机制不同(`new` 可以抛出异常,而 `malloc` 返回NULL),这会进一步使得代码难以维护,并可能导致错误处理不当。 -------------------------------------------------------------------------------- /problems/在C++中,用堆和用栈谁更快一点?.md: -------------------------------------------------------------------------------- 1 | 在C++中,栈分配通常比堆分配要快。 2 | 3 | - 栈分配非常快。分配内存仅仅是涉及到移动栈指针的操作。由于栈是线性且连续的内存区域,增加或减少栈空间只需要调整栈顶指针的位置,与栈相比,堆分配通常较慢。分配堆内存需要在堆的数据结构中寻找足够大的空闲块,可能还需要添加新的内存页到进程空间。这个过程可能涉及到复杂的内存管理算法,如空闲列表(free lists)、二叉堆等,并且还需要处理碎片化问题。 4 | - 栈上的数据通常具有更好的缓存局部性。因为栈是连续分配的,最近分配的变量很可能在CPU缓存中,而堆上的对象可能分布在内存的不同地方,导致缓存命中率降低。 -------------------------------------------------------------------------------- /problems/在C++的map中,[]与insert有那些区别?.md: -------------------------------------------------------------------------------- 1 | 1. 语法:使用`[]`运算符时,**如果键已经存在于map中,则会返回对应的值;如果不存在,则会插入一个新的键值对。**而`insert`函数则需要传入一个`std::pair`类型的参数,其中包括要插入的键值对。 2 | 2. 返回值:**`[]`运算符会返回键对应的值,而`insert`函数会返回一个`std::pair`类型的迭代器和一个布尔值**,表示插入是否成功。 3 | 3. 覆盖:**如果使用`[]`运算符插入已存在的键,它会覆盖掉原来的值**;而`insert`函数不会覆盖已存在的键,如果键已经存在,插入操作将不会生效。 4 | 5 | -------------------------------------------------------------------------------- /problems/在C++的算法库中,find()和binary_search()有什么区别?.md: -------------------------------------------------------------------------------- 1 | 1. **算法复杂度和预期使用场景**: 2 | - `find()` 函数执行线性查找。它逐个检查容器中的元素,直到找到等于指定值的元素或结束。因为它是通过遍历实现的,所以其时间复杂度为 O(n),其中 n 是容器中元素的数量。`find()` 不要求容器中的元素是事先排序的。 3 | - `binary_search()` 函数执行二分查找。它要求容器中的元素已经按非降序排序,并且通过不断将搜索范围缩小一半来查找特定值。因此,其时间复杂度为 O(log n) 。由于这种查找方式依赖于容器的元素顺序,所以在未排序的容器上使用 `binary_search()` 会得到未定义的行为。 4 | 2. **返回值**: 5 | - `find()` 返回一个迭代器,指向在容器中找到的第一个等于指定值的元素。如果没有找到,它返回一个等于 end() 迭代器的值。 6 | - `binary_search()` 返回一个布尔值,如果找到指定值则返回 true,否则返回 false。注意,它并不返回目标元素的位置或迭代器。 7 | 3. **通用性**: 8 | - `find()` 可以用于任何类型的容器,包括列表、向量、集合等,而且不需要元素是排序的。 9 | - `binary_search()` 通常用于数组或向量等随机访问容器,并且前提是这些容器中的元素已经被排序。 -------------------------------------------------------------------------------- /problems/在Mysql中,数据要写入磁盘,redolog也要写入磁盘,为什么要多此一举?.md: -------------------------------------------------------------------------------- 1 | - redo log的主要目的是提供**数据恢复能力**。在系统发生故障时(如断电或数据库崩溃),可以利用redo log来恢复未写入磁盘的数据页。Redo log记录了自上次检查点以来所发生的所有修改操作。它确保即使在系统突然崩溃的情况下,所有提交的事务也能够被恢复到最近一次一致的状态,并且能够撤销那些尚未提交的事务。 2 | - Redo log通常是循环使用的,被设计成追加写入的形式,这样可以快速地连续写入磁盘,提供高效率的磁盘I/O,减少磁盘的压力 -------------------------------------------------------------------------------- /problems/在TCP三次握手的时候,如果网络情况非常好且百分百不会发生拥塞,不会重传,不会有历史链接问题,那么三次握手可以改为两次吗?.md: -------------------------------------------------------------------------------- 1 | 即便在网络状况非常好,且不存在拥塞、重传或历史链接问题的理想情况下,TCP三次握手通常也不能简化为两次握手。这是因为三次握手不仅仅是为了应对网络问题,它还解决了一些关键的协议设计问题,尤其是确保双方都准备好进行数据传输,并且同步序列号。 2 | 3 | 4 | 5 | TCP三次握手的核心目的在于**双向确认通信双方的接收能力**。在第二次握手时,服务器只是让客户端知道“我已经准备好接收你的数据”;而第三次握手则是客户端告诉服务器“我知道你已准备好接收我的数据,同时请注意,我也已准备好接收你的数据”。如果只有两次握手,则无法确保这种双向确认。 -------------------------------------------------------------------------------- /problems/在TCP拥塞控制中,使用了什么样的算法?.md: -------------------------------------------------------------------------------- 1 | 在TCP拥塞控制中,常用的算法包括慢启动(Slow Start)、拥塞避免(Congestion Avoidance)、快重传(Fast Retransmit)和快恢复(Fast Recovery)等。这些算法帮助TCP适应网络拥塞情况,调整发送速率以保证网络的稳定性和可靠性。 2 | 3 | 1. 慢启动(Slow Start):在连接刚建立时,TCP发送方会以指数增长的速率增加发送窗口,即每收到一个确认就将发送窗口大小翻倍,直到达到拥塞窗口阈值(cwnd),从而快速填充网络的带宽。 4 | 2. 拥塞避免(Congestion Avoidance):一旦发送窗口大小达到拥塞窗口阈值,TCP发送方将进入拥塞避免状态,在该状态下发送窗口线性增长,即每收到一个确认就将发送窗口大小加1,以缓慢增加发送速率,避免引起网络拥塞。 5 | 3. 快重传(Fast Retransmit):当发送方连续收到三个相同的重复确认时,表明有报文段丢失,发送方会立即重传该丢失的报文段,而不必等待超时重传计时器。 6 | 4. 快恢复(Fast Recovery):在进行快重传后,TCP发送方会将拥塞窗口大小减半,并进入快恢复状态,此时发送方会继续以线性增长的速率增加发送窗口,直到重新达到拥塞窗口阈值。 7 | 8 | 这些算法共同作用于TCP拥塞控制机制,通过动态调整发送窗口大小、重传机制和拥塞控制状态,使得TCP能够有效地适应网络中的拥塞情况,保证数据传输的可靠性和稳定性。 -------------------------------------------------------------------------------- /problems/外中断和内中断有什么区别?.md: -------------------------------------------------------------------------------- 1 | ### 外中断 2 | 3 | - **来源**:外中断通常由处理器外部的事件引起,例如I/O设备(如键盘、鼠标、打印机等)、硬件计时器或其他计算机系统的信号。 4 | - **异步性**:外中断是异步发生的,它们不是由程序的执行直接引起的,而是由外部硬件事件触发的。 5 | - **硬件中断**:外中断也被称为硬件中断,因为它们通常涉及硬件设备。 6 | - **目的**:外中断的主要目的是允许处理器对外设的操作做出响应,如响应用户的输入或处理完成信号。 7 | 8 | ### 内中断 9 | 10 | - **来源**:内中断通常由程序执行中发生的事件引起,例如除零错误、无效指令、访问越界的内存地址等。 11 | - **同步性**:内中断是同步发生的,它们是由程序的执行流程中的特定指令或条件触发的。 12 | - **软件中断**:内中断也被称为软件中断,可以由特定的指令(如x86架构中的`INT`指令)直接调用。 13 | - **目的**:内中断的主要目的是处理异常情况,如程序错误或特殊的程序控制流程。 14 | 15 | ### 共同点与区分 16 | 17 | - **中断处理**:无论是外中断还是内中断,在处理器响应时,都会将当前的执行状态保存起来,转而执行一个特定的中断处理例程,处理完毕后再恢复原来的执行状态。 18 | - **响应方式**:某些内中断(如软件中断)可以被程序员在代码中有意识地触发,而外中断通常是无法预知的,处理器必须随时准备响应。 -------------------------------------------------------------------------------- /problems/如何使用gdb来定位C++程序中的死锁?.md: -------------------------------------------------------------------------------- 1 | 1. 编译程序时要确保开启调试信息,以便gdb能够正确地显示源代码和变量信息。这通常通过在编译命令中添加 `-g` 参数来实现。 2 | 2. 启动gdb并加载可执行文件。假设你的可执行文件名为 `my_program`,则可以在终端中输入 `gdb my_program` 来启动gdb并加载该程序。 3 | 3. 使用gdb的各种命令来观察程序的状态。例如: 4 | - `run`: 运行程序 5 | - `bt`: 打印当前的函数调用栈 6 | - `thread apply all bt`: 打印所有线程的函数调用栈 7 | - `info threads`: 显示当前所有线程的信息 8 | - `thread `: 切换到指定线程 9 | - `watch `: 设置一个监视点,当指定变量的值发生变化时停下来 10 | - `info variables`: 显示当前作用域内的变量信息 11 | 4. 当程序陷入死锁时,使用以上命令来观察每个线程的状态,查看各个线程的函数调用栈,以及他们正在等待的资源。 12 | 5. 分析线程调用栈以找出导致死锁的原因。检查是否有互斥锁未被正确释放,或者是否存在循环依赖的资源请求。 -------------------------------------------------------------------------------- /problems/如何判断图中是否有环(拓扑排序).md: -------------------------------------------------------------------------------- 1 | 1. 统计每个顶点的入度(in-degree),即指向该节点的边的数量。 2 | 2. 将所有入度为 0 的顶点加入一个队列。 3 | 3. 从队列中依次取出顶点,并将其邻接节点的入度减 1。 4 | 4. 如果邻接节点的入度变为 0,则将其加入队列。 5 | 5. 重复步骤 3 和步骤 4,直到队列为空。 -------------------------------------------------------------------------------- /problems/如何判断某网页的URL是否存在于包含100亿条数据的黑名单上(大数据小内存去重问题).md: -------------------------------------------------------------------------------- 1 | 对于判断某网页的 URL 是否存在于包含 100 亿条数据的黑名单上这样的大数据小内存去重问题,一种解决方法是使用布隆过滤器。 2 | 3 | 布隆过滤器是一种空间效率高、时间效率快的概率型数据结构,适用于快速判断一个元素是否可能在一个集合中。它通过多个哈希函数和一个位数组来表示集合,并在判断一个元素是否存在时进行位操作,具有一定的误判率。 4 | 5 | 以下是使用布隆过滤器判断某网页的 URL 是否在黑名单上的步骤: 6 | 7 | 1. 初始化一个布隆过滤器,用于表示黑名单数据集。 8 | 2. 将黑名单中的所有 URL 经过多次哈希函数映射到布隆过滤器的位数组上。 9 | 3. 当需要判断某个 URL 是否在黑名单中时,将该 URL 经过相同的哈希函数映射到布隆过滤器的位数组上,并检查对应的位是否都为1。 10 | 4. 如果所有对应的位都为1,则认为该 URL 可能在黑名单上;如果有任何一位为0,则可确定该 URL 不在黑名单上。 11 | 12 | 需要注意的是,布隆过滤器在判断某个元素不在集合中时可能出现误判,即存在一定的假阳性率。因此,在使用布隆过滤器时,需要根据实际情况权衡误差率和空间利用率,以及根据具体需求来确定哈希函数数量、位数组大小等参数。 13 | 14 | 布隆过滤器可以有效地处理大规模数据且内存有限的去重问题,是一种常用的解决方案之一。在实践中,可以根据具体的黑名单数据规模和性能要求选择合适的布隆过滤器参数,以提高判断的准确性和效率。 -------------------------------------------------------------------------------- /problems/如何定义一个只能在堆上定义对象的类栈上呢.md: -------------------------------------------------------------------------------- 1 | **只能在堆上创建对象的类** 2 | 3 | 要实现这一点,我们需要将它的析构函数设置为私有。此外,我们需要提供一个public的接口来删除这个对象。 4 | 5 | ```C++ 6 | class HeapOnly { 7 | private: 8 | ~HeapOnly() {} // 私有析构函数 9 | 10 | public: 11 | static HeapOnly* CreateInstance() { 12 | return new HeapOnly(); 13 | } 14 | 15 | static void DeleteInstance(HeapOnly* p) { 16 | delete p; 17 | } 18 | }; 19 | ``` 20 | 21 | 在这个例子中,我们不能在栈上创建`HeapOnly`类的对象,因为析构函数是私有的。但我们仍然可以在堆上创建,并且需要调用`DeleteInstance()`来删除这个对象。 22 | 23 | **只能在栈上创建对象的类** 24 | 25 | 要实现这一点,我们可以将new操作符重载设为私有。这样就无法使用`new`来在堆上创建对象了。 26 | 27 | ```c++ 28 | class StackOnly { 29 | private: 30 | void* operator new(size_t size) = delete; // 禁用new操作符 31 | void operator delete(void* p) = delete; // 禁用delete操作符 32 | 33 | public: 34 | StackOnly() {} 35 | ~StackOnly() {} 36 | }; 37 | ``` 38 | 39 | 在这个例子中,我们不能在堆上创建`StackOnly`类的对象,因为new操作符已被私有化,但我们仍然可以在栈上创建。 -------------------------------------------------------------------------------- /problems/如何实现守护进程.md: -------------------------------------------------------------------------------- 1 | 首先,守护进程实在unix和Linux中后台运行的进程,它通常在系统引导的时候启动,系统关闭前结束 2 | 3 | 下面以Linux为例,描述一下创建一个简单的Linux守护进程的几个关键步骤: 4 | 5 | 1. 调用fork()创建子进程。这使得父进程可以结束,这样新进程就不再是一个会话的领头进程,这是创建新会话的前提。 6 | 2. 在子进程中调用setsid()创建新的会话,当前进程成为新会话的领头进程和新进程组的组长进程。同时让进程摆脱原会话,原进程组的控制。 7 | 3. 调用fork()再次创建子进程,然后让第一子进程退出。这保证了该守护进程不是会话的领头进程,同时让它失去了在控制终端上打开文件的能力。 8 | 4. 改变当前的工作目录。守护进程应该在文件系统的根目录下运行,以防止它们阻止文件系统被卸载。 9 | 5. 重设文件权限掩码。将文件掩码设置为0确保守护进程具有最大的文件权限。 10 | 6. 关闭所有打开的文件描述符。因为守护进程不应在终端上打开文件。 11 | 7. 重定向标准输入、标准输出和错误输出流。因为守护进程和任何终端都不再关联,它们的标准输入、标准输出和标准错误(如果不关闭的话)也应该被重定向到/dev/null或者某个日志文件。 -------------------------------------------------------------------------------- /problems/如何构造一个类使得只能在堆上或者栈上分配内存?.md: -------------------------------------------------------------------------------- 1 | ### 只在栈上创建对象 2 | 3 | 要使得对象只能在栈上创建,可以将类的new操作符设置为private或者delete。这样一来,由于在堆上创建对象需要调用new操作符,这个类的对象就只能在栈上创建了。例如: 4 | 5 | ``` 6 | class StackOnly { 7 | private: 8 | static void* operator new(size_t size) = delete; 9 | static void* operator new[](size_t size) = delete; 10 | 11 | public: 12 | StackOnly() {} 13 | ~StackOnly() {} 14 | }; 15 | ``` 16 | 17 | ### 只在堆上创建对象 18 | 19 | 要使得对象只能在堆上创建,可以将类的析构函数设置为private,并提供一个public的函数来删除对象。这样一来,由于在栈上创建对象需要在作用域结束时调用析构函数,这个类的对象就只能在堆上创建了。 20 | 21 | 但是这样会导致一个问题,就是当我们创建对象后忘记手动删除,就会引发内存泄漏。因此,可以将删除函数封装在智能指针中,从而避免这个问题。例如: 22 | 23 | ``` 24 | class HeapOnly { 25 | public: 26 | HeapOnly() {} 27 | 28 | static HeapOnly* Create() { return new HeapOnly(); } 29 | static void Destroy(HeapOnly* instance) { delete instance; } 30 | 31 | private: 32 | ~HeapOnly() {} 33 | }; 34 | ``` -------------------------------------------------------------------------------- /problems/如何解决Redis集群数据丢失问题(异步复制、集群脑裂).md: -------------------------------------------------------------------------------- 1 | 1. 同步复制:使用同步复制可以确保数据在主节点写入后立即被复制到所有从节点,从而减少数据丢失的可能性。虽然同步复制会增加写入延迟,但可以提高数据的可靠性。 2 | 2. 持久化策略:配置Redis集群的持久化机制,将数据写入磁盘,以防止数据在节点故障时丢失。可以选择使用RDB快照、AOF日志或者混合持久化方式来保护数据。 3 | 3. 监控和警报系统:建立监控系统,实时监测Redis集群的健康状态,包括节点的负载情况、延迟情况等。设置警报规则,当发生异常情况时及时通知管理员进行处理,避免数据丢失。 4 | 4. 集群配置和维护:合理配置Redis集群的参数,包括超时设置、最大连接数、最大内存限制等,以避免因配置不当导致的性能问题和数据丢失。定期检查集群的状态,并及时更新补丁和升级版本,以提高系统的稳定性和安全性。 5 | 5. 避免集群脑裂(Split-Brain):集群脑裂是指网络分区导致集群中的节点无法互相通信,从而出现数据不一致的情况。为避免集群脑裂,可以使用哨兵系统对集群进行监控和自动故障转移,保证集群中只有一个主节点对外提供服务。 -------------------------------------------------------------------------------- /problems/如何解决缓存和数据库一致性问题.md: -------------------------------------------------------------------------------- 1 | 1. **读写分离**:将读写操作分离到不同的数据库实例中,读操作直接访问缓存以提高性能,写操作同时更新数据库和缓存。这样可以避免脏数据出现在缓存中。 2 | 2. **缓存穿透处理**:当缓存查询不到数据时,可以返回默认值、空值或错误提示作为缓存对象,同时查询数据库并将结果更新到缓存中。这样可以避免因缓存失效引起大量请求直接访问数据库。 3 | 3. **双写策略**:在更新数据库的同时,也更新缓存。确保数据库和缓存中的数据始终保持一致。可以使用事务来保证两者操作的原子性。 4 | 4. **缓存更新策略**:采用主动刷新或者定时刷新缓存的方式,使得缓存中的数据与数据库中的数据保持一致。可以根据业务需求选择合适的更新策略,比如定时刷新、异步刷新等。 5 | 5. **缓存失效处理**:当数据库中的数据发生变化时,及时使缓存失效,下次请求访问时重新从数据库获取最新数据。可以通过监听数据库的变更事件,自动使缓存失效。 6 | 6. **版本控制**:为缓存中的数据引入版本号,每次数据更新时更新版本号。在读取数据时,先比较版本号是否一致,不一致则重新从数据库获取数据并更新缓存。 7 | 7. **缓存锁**:在并发情况下,给涉及缓存和数据库的操作加锁,确保只有一个线程能够进行操作,防止脏数据的产生。 -------------------------------------------------------------------------------- /problems/如何选择合适的STL容器.md: -------------------------------------------------------------------------------- 1 | 选择合适的STL容器依赖于你的特定需求,包括你的数据结构、性能要求以及如何使用这些数据。下面是一些常用的STL容器和它们的特点,以及何时最适合使用它们: 2 | 3 | 1. **std::vector**: 4 | - 动态数组,提供快速的随机访问(O(1)时间复杂度)。 5 | - 适用于元素数量经常变化,但主要操作是在尾部添加或移除元素的场景。 6 | - 不适合频繁在中间或头部插入/删除操作,因为这样的操作会导致后续所有元素移动。 7 | 2. **std::deque**: 8 | - 双端队列,支持在头部和尾部高效的插入和删除操作。 9 | - 当需要快速地在序列的两端进行插入或删除时,更优于`std::vector`。 10 | 3. **std::list** 和 **std::forward_list**: 11 | - 分别代表双向和单向链表。 12 | - 提供在任意位置高效插入和删除操作(O(1)时间复杂度)。 13 | - 不支持快速随机访问。 14 | - 当数据结构需要频繁在中间位置插入和删除元素时,链表可能是一个好选择。 15 | 4. **std::set** 和 **std::multiset**: 16 | - 基于红黑树实现的有序集合和多重集合。 17 | - 自动对元素排序,并保证唯一性(`std::multiset` 允许重复元素)。 18 | - 插入、查找和删除操作具有对数时间复杂度(O(log n))。 19 | - 当需要保存有序的唯一元素集合,并且频繁查询是否存在某个元素时使用。 20 | 5. **std::map** 和 **std::multimap**: 21 | - 基于红黑树的键-值对集合,自动按键排序。 22 | - `std::multimap` 允许键不唯一。 23 | - 适用于当需要根据键来存取元素,并且需要保持键的有序性时。 24 | 6. **std::unordered_set**、**std::unordered_map**、**std::unordered_multiset** 和 **std::unordered_multimap**: 25 | - 基于哈希表实现的无序容器。 26 | - 提供平均常数时间复杂度的插入、查找和删除操作,但最坏情况下会退化到线性时间。 27 | - 当元素的顺序不重要,且期望快速访问时使用。 28 | 7. **std::stack**、**std::queue** 和 **std::priority_queue**: 29 | - 封装了其他容器的适配器,分别提供了栈、队列和优先队列的接口。 30 | - `std::stack` 和 `std::queue` 通常基于 `std::deque` 实现。 31 | - `std::priority_queue` 通常基于 `std::vector` 实现,并通过使堆来管理元素的优先级。 32 | - 适用于特定的数据结构需求,如LIFO(后进先出)、FIFO(先进先出)或优先级排序。 33 | 8. **std::array**: 34 | - 固定大小的数组封装,提供了标准容器接口。 35 | - 当数组大小已知且不变时使用,它提供了比原始数组更安全和易于使用的接口。 36 | 37 | 选择合适容器的一般建议是: 38 | 39 | - 首选 `std::vector`,除非有特定理由选择其他容器。 40 | - 如果需要高效的插入和删除,考虑 `std::list` 或 `std::deque`。 41 | - 如果需要保存唯一元素并保持顺序,使用 `std::set`。 42 | - 如果元素顺序不重要,但想要快速查找,使用 `std::unordered_set` 或 `std::unordered_map`。 43 | - 对于特殊用途的容器,如栈、队列或优先队列,选择相应的适配器。 44 | 45 | 模板参数、内存分配器选项和成员函数的选择也可以影响容器的行为和性能,所以在选择时还需要考虑这些因素。 -------------------------------------------------------------------------------- /problems/如何避免悬挂指针?.md: -------------------------------------------------------------------------------- 1 | 1. 立即清空: 释放了指针指向的内存后,立即将该指针设置为`nullptr`(C++11引入的空指针字面量),这样就不会再指向之前的内存地址了。 2 | 3 | ```c++ 4 | delete ptr; 5 | ptr = nullptr; 6 | ``` 7 | 8 | 2. 范围意识: 尽量使用局部变量来管理动态分配的内存,这样当控制流离开变量的作用域时,可以自动释放资源。智能指针(如`std::unique_ptr`和`std::shared_ptr`)在C++中是自动管理动态内存的很好选择。 9 | 10 | 3. 避免野指针: 声明指针时,如果不立即初始化,就需要给它赋予`nullptr`,确保它不会成为野指针,指向某个随机的、不确定的内存位置。 11 | 12 | ```c++ 13 | int* ptr = nullptr; // 初始化为nullptr,而不是未初始化的野指针 14 | ``` 15 | 16 | 4. 适时销毁: 在对象的生命周期结束时(例如,在析构函数中),确保释放所有动态分配的内存。对于类的设计,要格外注意拷贝构造函数和赋值操作符的正确实现,避免复制和赋值导致的悬挂指针问题。 17 | 18 | 5. 引用计数: 使用引用计数如`std::shared_ptr`智能指针,它通过内部机制确保只有最后一个指针持有者销毁时才释放内存。 19 | 20 | 6. 定期检查: 使用代码审查、静态分析工具和运行时调试工具来检测和定位可能的悬挂指针。 21 | 22 | 7. 函数返回值: 避免从函数返回局部变量的地址或者引用,因为函数返回后,局部变量的生命周期就结束了。 23 | 24 | 8. 注意回调和事件处理: 如果你注册了指针为回调或事件处理器,确保在废弃指针之前注销这些回调或事件处理器。 -------------------------------------------------------------------------------- /problems/如果A是某一个类的指针,那么在它等于nullptr的情况下能直接调用里面的A对应类里面的public函数吗.md: -------------------------------------------------------------------------------- 1 | 在A是某一个类的指针,并且等于nullptr的情况下,是不能直接调用A对应类里面的public函数的。当A为nullptr时,表示它不指向任何有效的对象,因此调用A对应类里面的函数会导致空指针解引用错误,从而引发未定义行为。 -------------------------------------------------------------------------------- /problems/如果A这个对象对应的类是一个空类,那么sizeof(A)的值是多少?.md: -------------------------------------------------------------------------------- 1 | 如果A这个对象对应的类是一个空类(即没有任何成员变量或成员函数),那么sizeof(A)的值通常会是1。 2 | 3 | C++标准规定,空类的实例在内存中至少占用一个字节的空间,这是为了确保每个实例都有独一无二的地址。因此,即使类里面没有定义任何成员变量,编译器也会给这个实例分配一个字节的空间。 -------------------------------------------------------------------------------- /problems/如果A这个指针指向一个数组,那么sizeof(A)的值是多少?.md: -------------------------------------------------------------------------------- 1 | 如果A是一个指针,指向一个数组,那么sizeof(A)的值取决于编译器和操作系统的位数。 2 | 3 | 在大多数32位系统上,sizeof(A)的值通常是4,因为指针占用4个字节。而在64位系统上,sizeof(A)的值通常是8,因为指针占用8个字节。 -------------------------------------------------------------------------------- /problems/如果发现自己的Linux服务器负载过高,应该怎么排查原因呢?.md: -------------------------------------------------------------------------------- 1 | 1. **查看当前系统的运行状态**: 使用`top`或`htop`命令可以查看当前系统的整体运行状态,包括CPU利用率、内存使用情况、负载情况以及各个进程的资源使用情况。 2 | 2. **检查CPU**: 如果发现CPU使用率非常高,那么可以先查看哪些进程占用了大部分的CPU资源。`top`命令默认就会按照CPU使用率进行排序,显示在最上方的进程就是占用CPU最高的。 3 | 3. **检查内存**: 如果内存资源紧张,也会导致系统变慢。使用`free -m`命令可以查看内存使用情况。如果发现物理内存接近耗尽而且swap区域使用较多,说明内存资源已不足。然后还可以通过`top`命令找出占用内存最多的进程。 4 | 4. **检查磁盘I/O**: 磁盘I/O问题是导致系统负载过高的常见原因之一。可以使用`iostat`命令来查看磁盘的读写速度和IOPS(每秒输入输出操作数),查看是否有异常的磁盘I/O行为。 5 | 5. **检查网络**: 使用`netstat`或`ss`命令查看网络连接状态,特别是查看大量TIME_WAIT、CLOSE_WAIT状态的连接,可能会引发网络相关的性能问题。 -------------------------------------------------------------------------------- /problems/如果拿到虚函数表的储存地址,是否可以改写虚函数表的内容?.md: -------------------------------------------------------------------------------- 1 | **理论上,如果你能获取到虚函数表的存储地址,你确实可以修改虚函数表的内容**,将虚函数的地址替换为其他函数的地址。 2 | 3 | 然而,这样做通常是**非常危险**,**因为它破坏了C++的抽象和类型安全**,可能会导致未定义的行为。**在大多数操作系统和编译器实现中,虚函数表存放在只读数据段**,直接尝试修改虚函数表将会导致程序崩溃。 4 | 5 | **此外,破坏虚函数表还会引入潜在的安全问题**。在某些情况下,恶意软件可能会尝试这样的操作来jc程序的控制流。因此,现代操作系统通常会采取一些保护措施,例如数据执行保护(DEP)和地址空间布局随机化(ASLR)来阻止这种类型的攻击。 -------------------------------------------------------------------------------- /problems/宏定义和展开、内联函数区别.md: -------------------------------------------------------------------------------- 1 | 2 | 内联函数是代码被插入到调用者代码处的函数。如同 #define 宏,内联函数通过避免被调用的开销来提高执行效率,尤其是它能够通过调用(“过程化集成”)被编译器优化。 宏定义不检查函数参数,返回值什么的,只是展开,相对来说,内联函数会检查参数类型,所以更安全。 内联函数和宏很类似,而区别在于,宏是由预处理器对宏进行替代,而内联函数是通过编译器控制来实现的。而且内联函数是真正的函数,只是在需要用到的时候,内联函数像宏一样的展开,所以取消了函数的参数压栈,减少了调用的开销。 3 | 4 | 宏是预编译器的输入,然后宏展开之后的结果会送去编译器做语法分析。宏与函数等处于不同的级别,操作不同的实体。宏操作的是 token, 可以进行 token的替换和连接等操作,在语法分析之前起作用。而函数是语言中的概念,会在语法树中创建对应的实体,内联只是函数的一个属性。 5 | 对于问题:有了函数要它们何用?答案是:一:函数并不能完全替代宏,有些宏可以在当前作用域生成一些变量,函数做不到。二:内联函数只是函数的一种,内联是给编译器的提示,告诉它最好把这个函数在被调用处展开,省掉一个函数调用的开销(压栈,跳转,返回) 6 | 7 | 内联函数也有一定的局限性。就是函数中的执行代码不能太多了,如果,内联函数的函数体过大,一般的编译器会放弃内联方式,而采用普通的方式调用函数。这样,内联函数就和普通函数执行效率一样 8 | 9 | 内联函数必须是和函数体申明在一起,才有效。 10 | 11 | 12 | -------------------------------------------------------------------------------- /problems/对称加密和非对称加密的区别都有那些?.md: -------------------------------------------------------------------------------- 1 | ### 密钥 2 | 3 | - **对称加密**:使用同一个密钥进行加密和解密。这意味着加密方和解密方必须事先共享同一个密钥,并且保证这个密钥的安全。 4 | - **非对称加密**:使用一对密钥,一个公开密钥(公钥)用于加密,一个私有密钥(私钥)用于解密。公钥可以公开分享,而私钥必须保持私密。 5 | 6 | ### 加密速度 7 | 8 | - **对称加密**:通常更快,因为它使用较简单的算法来处理大量数据。 9 | - **非对称加密**:由于其复杂的数学运算,尤其是在处理大量数据时,比对称加密慢得多。 10 | 11 | ### 安全性 12 | 13 | - **对称加密**:虽然对称加密算法通常很难破解,但密钥的管理和分发过程可能导致安全漏洞。 14 | - **非对称加密**:提供了更高的安全性,因为即使公钥被公开,没有私钥也无法解密信息。不过,实现上更为复杂,需要更小心地保护私钥。 15 | 16 | ### 使用场景 17 | 18 | - **对称加密**:适用于需要快速处理大量数据的场景,如文件加密、数据库加密、网络数据传输加密等。 19 | - **非对称加密**:常用于安全敏感的通信中,如数字签名、SSL/TLS证书验证、安全电子邮件等。由于其速度较慢,通常用于加密少量数据或用于加密对称加密中使用的密钥。 20 | 21 | ### 典型算法 22 | 23 | - **对称加密算法**:AES(高级加密标准)、DES(数据加密标准)、3DES(三重数据加密算法)、RC4等。 24 | - **非对称加密算法**:RSA、ECC(椭圆曲线密码学)、Diffie-Hellman密钥交换协议、ElGamal等。 -------------------------------------------------------------------------------- /problems/局域网的IP分配策略是什么?它是怎么实现的?.md: -------------------------------------------------------------------------------- 1 | 1. 静态IP分配: 在静态IP分配中,网络管理员手动为每台设备指定一个**唯一**的IP地址。这个过程通常涉及到在网络设备(如计算机、打印机等)的设置界面中手动配置IP地址、子网掩码、默认网关以及DNS服务器地址等参数。静态IP分配通常用于确保特定设备始终拥有相同的IP地址,这对于服务器、网络打印机或其他需要稳定IP地址的设备来说非常重要。 2 | 2. 动态IP分配: 动态IP分配则是**通过动态主机配置协议自动完成的**。在这种策略下,设备启动时会向网络发送广播消息,请求IP地址信息。局域网中的DHCP服务器接收到请求后,从其配置的地址池中分配一个IP地址给该设备,并将此信息连同子网掩码、默认网关和DNS服务器地址等通过DHCP响应包发送给设备。 3 | 4 | 实现动态IP分配的步骤一般包括以下几个阶段: 5 | 6 | - DHCP发现(DHCPDISCOVER):客户端发送广播消息,寻找可用的DHCP服务器。 7 | - DHCP提供(DHCPOFFER):DHCP服务器响应客户端的请求,并提供IP地址租约信息。 8 | - DHCP请求(DHCPREQUEST):客户端选择接受其中一个提供的IP地址,并再次以广播的方式发送确认请求。 9 | - DHCP确认(DHCPACK):DHCP服务器确认IP地址租约,并将最终确认信息和其他网络配置参数发送给客户端。 10 | 11 | 优点和缺点: 12 | 13 | - 静态IP分配可以提供稳定性和确定性,便于管理和网络访问控制,但它不够灵活并且难以扩展到大量设备,尤其是当网络规模变化时,手动配置工作量很大。 14 | - 动态IP分配非常灵活,易于管理大量设备,特别适合设备经常变动的网络环境。然而,由于IP地址可能会变化,对于需要固定IP地址的设备(如某些服务器),动态分配可能不是最佳选择。 -------------------------------------------------------------------------------- /problems/常见排序算法及其时间复杂度、各种排序算法对比.md: -------------------------------------------------------------------------------- 1 | 最常见的有六种,他们的时间复杂度如下: 2 | 3 | 1. 冒泡排序(Bubble Sort): 4 | - 平均时间复杂度:O(n^2) 5 | - 最好情况时间复杂度:O(n) 6 | - 最坏情况时间复杂度:O(n^2) 7 | 2. 选择排序(Selection Sort): 8 | - 平均时间复杂度:O(n^2) 9 | - 最好情况时间复杂度:O(n^2) 10 | - 最坏情况时间复杂度:O(n^2) 11 | 3. 插入排序(Insertion Sort): 12 | - 平均时间复杂度:O(n^2) 13 | - 最好情况时间复杂度:O(n) 14 | - 最坏情况时间复杂度:O(n^2) 15 | 4. 快速排序(Quick Sort): 16 | - 平均时间复杂度:O(nlogn) 17 | - 最好情况时间复杂度:O(nlogn) 18 | - 最坏情况时间复杂度:O(n^2) (当待排序数组已经有序时) 19 | 5. 归并排序(Merge Sort): 20 | - 时间复杂度:O(nlogn) 21 | 6. 堆排序(Heap Sort): 22 | - 时间复杂度:O(nlogn) -------------------------------------------------------------------------------- /problems/常见的信号、系统如何将一个信号通知到进程.md: -------------------------------------------------------------------------------- 1 | **Linux操作系统中常见的信号有** 2 | 3 | 1. SIGHUP:挂起进程 4 | 2. SIGINT:中断进程 5 | 3. SIGQUIT:退出进程和生成核心文件 6 | 4. SIGILL:非法指令 7 | 5. SIGTRAP:跟踪/断点陷阱 8 | 6. SIGABRT:异常终止 9 | 7. SIGBUS:总线错误 10 | 8. SIGFPE:浮点异常 11 | 9. SIGKILL:ss进程,该信号不能被阻塞,处理或忽略,一旦接收就会ss进程 12 | 10. SIGUSR1、SIGUSR2:用户自定义信号 13 | 11. SIGSEGV:无效内存引用 14 | 12. SIGPIPE:管道破碎:写到一个没有读者的管道 15 | 13. SIGALRM:实时定时器超时 16 | 14. SIGTERM:终止进程 17 | 15. SIGCHLD:子进程已经停止或终止 18 | 16. SIGCONT:如果进程已经停止,那么继续运行进程 19 | 17. SIGSTOP:停止执行进程 20 | 18. SIGTSTP、SIGTTIN、SIGTTOU:停止进程的运行 21 | 22 | **一个进程接收到一个信号后,可以有三种方式处理** 23 | 24 | 1. 忽略这个信号。 25 | 2. 捕捉这个信号。一旦一个进程决定要捕捉某种信号,就需要提供一个函数,这个函数被称为信号处理程序。当这种信号发给该进程时,内核就运行该信号处理程序。 26 | 3. 执行默认操作。 27 | 28 | **系统如何将一个信号通知到进程** 29 | 30 | 1. 内核会修改进程上下文信息,并设置标识表明收到信号。 31 | 2. 当进程再次被调度执行时,它会先检查是否有未处理的信号,如果有,就调用相应的信号处理函数。 32 | 3. 如果没有为该信号指定处理函数或者信号被阻塞,那么就执行系统默认的操作,可能是忽略、停止进程或者终止进程等。 -------------------------------------------------------------------------------- /problems/并行和并发的区别.md: -------------------------------------------------------------------------------- 1 | 1. **并行**: 2 | 3 | - **定义**:并行指的是多个任务同时执行的能力,即多个任务在同一时刻同时运行。在计算机系统中,通常通过多核处理器或多线程来实现并行。 4 | - **特点**:并行指的是真正同时处理多个任务,各个任务之间可以独立运行,互不干扰。在并行处理中,多个任务可以同时进行,加快整体计算速度。 5 | 6 | 1. **并发**: 7 | 8 | - **定义**:并发指的是多个任务在同一时间段内交替执行的能力,即多个任务在一段时间内交替进行。在计算机系统中,通常通过操作系统的调度机制来实现并发。 9 | - **特点**:并发指的是多个任务共享系统资源,通过时间片轮转或事件驱动等机制,实现多任务之间的交替执行。虽然看起来像是同时进行,但实际上是交替执行的。 -------------------------------------------------------------------------------- /problems/循环队列怎么实现.md: -------------------------------------------------------------------------------- 1 | 1. 使用数组来存储循环队列的元素,并采用两个指针 front 和 rear 分别指向队头和队尾。 2 | 2. 初始化循环队列时,front 和 rear 均指向数组的第一个位置,队列为空。 3 | 3. 入队操作: 4 | - 首先判断队列是否满,如果 **(rear + 1) % 数组长度 == front**,则表示队列已满,无法入队。 5 | - 否则,将元素插入到 rear 所指向的位置,然后将 rear 移动到下一个位置:rear = (rear + 1) % 数组长度。 6 | 4. 出队操作: 7 | - 首先判断队列是否为空,如果 front == rear,则表示队列为空,无法出队。 8 | - 否则,取出 front 所指向的元素,然后将 front 移动到下一个位置:front = (front + 1) % 数组长度。 9 | 5. 获取队头元素: 10 | - 直接返回 front 所指向的元素即可。 11 | 6. 判断队列是否为空: 12 | - 如果 front == rear,则队列为空。 13 | 7. 判断队列是否为满: 14 | - 如果 **(rear + 1) % 数组长度 == front**,则队列为满。 -------------------------------------------------------------------------------- /problems/悬挂指针和野指针有什么区别?.md: -------------------------------------------------------------------------------- 1 | 1. **悬挂指针**: 悬挂指针产生于指针所指向的内存已被释放或者失效后,指针本身没有及时更新或清空。在该内存释放之后,任何通过这个悬挂指针的引用或操作都是不安全的,因为这块内存可能已经重新分配给了其他的数据。 2 | 3 | 示例:当一个指针指向动态分配(比如使用`malloc`或`new`)的内存,并且随后该内存被释放掉(使用`free`或`delete`),而没有将指针设置为`NULL`,此时这个指针就变成了悬挂指针。 4 | 5 | 2. **野指针**: 野指针通常是指未初始化的指针,它没有被设置为任何有效的地址。由于它可能指向任意位置,对野指针的解引用是危险的,并且可能会导致难以预测的行为甚至程序崩溃。 6 | 7 | 示例:声明了一个指针变量但是没有给它赋予确定的初始值,然后就开始使用这个指针。 8 | 9 | 尽管两者看似相似,但是产生原因和解决方式有所不同: 10 | 11 | - **悬挂指针问题**可以通过确保指针在释放关联的内存资源后立即被设为`NULL`来避免。 12 | - **野指针问题**则需要确保每个指针变量在使用前都被明确初始化为一个合法的地址或`NULL`。 13 | 14 | 处理这两种类型的指针时,编程中的最佳实践是始终确保你的指针在声明后得到适当的初始化,在资源被释放之后更新状态,并且在解引用之前检查其有效性。 -------------------------------------------------------------------------------- /problems/指针和引用在内存中的表现形式有何不同?.md: -------------------------------------------------------------------------------- 1 | 在很多实现中,引用通常通过编译器使用指针来实现。然而,从语义上讲,引用更类似于直接操作所引用的对象,而不需要通过解引用指针那样的间接层。 2 | 3 | - 在**汇编层面,对引用的操作往往会被转换成直接对引用所绑定对象的内存地址的操作,就像使用指针一样。** 4 | - 当函数参数以引用方式传递时,底层可能也是通过传递对象的地址(即指针)来实现的,但在函数内部,这个地址被自动解引用,因此开发者感受到的是直接操作对象本身。 5 | 6 | 编译器可能会对引用和指针的使用进行优化: 7 | 8 | - 对于引用,在某些情况下(尤其是在优化等级较高时),**编译器可能完全不会分配用于存储地址的内存,特别是当引用作为函数参数或局部变量时,编译器可以选择直接操作原始对象**。 9 | - 对于指针,由于存在指针算术和可能的重新赋值,编译器的优化能力可能会受到限制。 -------------------------------------------------------------------------------- /problems/指针和引用有什么区别呢?.md: -------------------------------------------------------------------------------- 1 | 1. 基本概念: 2 | - 指针是一个变量,其值为另一个变量的地址,即直接存储了内存中的一个位置。可以通过解引用操作来访问或修改指针指向的数据。 3 | - 引用是一个变量的别名,对引用的操作就是对被引用变量的操作。引用在声明时必须初始化,并且不能改变引用的对象。 4 | 2. 语法: 5 | - 指针:`类型 *指针名;` 6 | - 引用:`类型 &引用名 = 被引用变量;` 7 | 3. 空值: 8 | - 指针可以有空值(NULL, nullptr),表示它不指向任何对象。 9 | - 引用必须连接到一块合法的内存,一旦绑定到一个对象上,就不能再改变引用的目标。 10 | 4. 初始化: 11 | - 指针可以在任何时候被初始化,也可以先声明后赋值。 12 | - 引用在声明的时候必须被初始化,并且一旦指定了一个对象后,就不能再引用其他对象。 13 | 5. 内存地址: 14 | - 指针本身是一个独立的变量,因此除了能存储地址之外,还有自己的内存地址。 15 | - 引用并不占据内存空间,它只是原始变量的一个别名。 16 | 6. 操作符: 17 | - 指针使用*进行解引用,用以访问目标变量;使用&取得一个变量的地址。 18 | - 引用使用&在声明时建立关联,之后无需特殊操作符,就像操作普通变量一样。 19 | 7. 用途差异: 20 | - 指针的灵活性更高,更适合于执行复杂的内存操作,例如动态内存分配、数组遍历等。 21 | - 引用通常用作函数参数或返回值,以简化代码和避免指针可能导致的错误。 22 | 8. 重新赋值: 23 | - 指针可以被重新赋值,指向另一个不同的地址。 24 | - 引用一旦被绑定到一个对象上,就不能改变这个绑定关系。 -------------------------------------------------------------------------------- /problems/指针常量和常量指针的区别.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | 1. **常量指针: 常量指针是一个指向常量数据的指针,这意味着指针指向的数据不可以通过这个指针被修改。然而,指针本身的值(即存储的地址)是可以更改的;它可以指向另一个常量数据或非常量数据的地址。 4 | 5 | 在C/C++中,常量指针的声明如下: 6 | 7 | ```c 8 | const int *ptr; 9 | // 或者 10 | int const *ptr; 11 | ``` 12 | 13 | 上述两种声明方式都表示`ptr`是一个指向`int`类型常量的指针。你不能通过`ptr`来改变所指向的值,但可以改变`ptr`的值使其指向另一个`int`类型的地址。 14 | 15 | 2. **指针常量 : 指针常量是一个指针,它自身的值是常量,也就是说,一旦被初始化之后,指针的值(它所存储的地址)就不能再改变了。但是,如果指针指向的不是常量,那么你仍然可以通过这个指针修改指向的数据。 16 | 17 | 在C/C++中,指针常量的声明如下: 18 | 19 | ```c 20 | int *const ptr = &someVariable; 21 | ``` 22 | 23 | 这里`ptr`必须在声明时初始化,并且以后不能再指向其他任何地址。但是,`ptr`所指向的`int`类型的数据可以通过`ptr`来修改。 24 | 25 | 因此,核心区别在于: 26 | 27 | - 常量指针主要限制通过这个指针改变所指向的数据,但允许改变指针指向的地址。 28 | - 指针常量主要限制改变指针所存储的地址,但允许通过这个指针改变所指向的数据(除非它同时也是一个常量指针,下文将说明)。 29 | 30 | 还有一种情况是两者结合使用,创建一个指向常量数据的固定指针(即指针的地址和它指向的数据都不能改变): 31 | 32 | ```c 33 | const int *const ptr = &someConstantVariable; 34 | ``` 35 | 36 | 在这种声明中,`ptr`是一个指针常量,同时指向一个整型常量,因此无法修改`ptr`的值(地址),也不能通过`ptr`来修改其指向的数据。 -------------------------------------------------------------------------------- /problems/操作系统本身为用户提供什么功能.md: -------------------------------------------------------------------------------- 1 | 1. **资源管理**:操作系统负责管理计算机系统的各种资源,包括CPU、内存、硬盘、网络等。它协调和分配这些资源,确保各个程序能够得到足够的资源来运行。 2 | 2. **进程管理**:操作系统负责创建、调度和终止进程(程序的执行实例),实现多任务处理。它控制进程的执行顺序,分配时间片和优先级,管理进程间的通信与同步。 3 | 3. **文件系统**:操作系统负责管理文件和目录,提供文件的读写、复制、移动等操作。它通过文件系统来组织和存储数据,使用户能够方便地访问和管理文件。 4 | 4. **设备管理**:操作系统管理计算机系统上的各种设备,包括输入输出设备、外部存储设备等。它提供设备驱动程序,控制设备的访问和操作,使用户能够与设备进行交互。 5 | 5. **用户界面**:操作系统提供了用户与计算机系统交互的接口,包括图形用户界面(GUI)和命令行界面(CLI)。用户可以通过操作系统提供的界面来执行命令、启动程序、管理文件等操作。 6 | 6. **内存管理**:操作系统负责管理计算机系统的内存,包括内存分配、虚拟内存管理、内存保护等功能。它确保不同程序之间的内存空间不会相互干扰,实现内存的合理利用。 7 | 7. **安全性**:操作系统提供安全机制,包括用户身份验证、权限控制、文件加密等功能,保护系统和用户数据不受未经授权的访问和恶意攻击。 8 | 8. **错误处理**:操作系统具有错误检测和处理机制,可以监控系统状态,及时发现并处理系统出现的错误或异常情况,避免系统崩溃或数据丢失。 -------------------------------------------------------------------------------- /problems/数据库中乐观锁与悲观锁的区别.md: -------------------------------------------------------------------------------- 1 | **悲观锁**: 2 | 3 | 1. **策略**:悲观锁假设冲突是常见的,因此在数据处理过程中(如读取或修改数据)会先锁定数据,防止其他事务对这些数据进行并发修改。 4 | 2. **锁定行为**:事务在访问数据时直接进行锁定,其他事务必须等待锁释放后才能对数据进行操作。 5 | 3. **适用场景**:适用于多事务竞争访问同一数据资源且冲突概率高的环境。 6 | 4. **优点**:在高冲突环境中可以保持数据的一致性。 7 | 5. **缺点**:可能降低并发性能,增加了死锁的可能性,同时锁定资源可能导致其他事务等待,影响系统的响应时间。 8 | 9 | **乐观锁**: 10 | 11 | 1. **策略**:乐观锁假设冲突较少发生,不会在事务开始时就锁定数据。而是在事务提交的时候检查在事务执行期间是否发生了冲突。 12 | 2. **冲突检测**:通常是通过数据版本号(如时间戳或者递增序列)来实现。在事务提交前,检查数据的版本号是否发生改变,如果改变了,表示有其他事务对数据进行了修改。 13 | 3. **适用场景**:适用于冲突概率低,读多写少的应用环境。 14 | 4. **优点**:提高了系统的并发性,因为大部分时间不需要锁定资源,减少了不必要的等待和锁竞争。 15 | 5. **缺点**:如果冲突发生,可能需要额外的重试逻辑处理,并且在高并发冲突环境中可能会降低性能。 -------------------------------------------------------------------------------- /problems/数据库中常见锁都有哪些.md: -------------------------------------------------------------------------------- 1 | 1. **共享锁**: 2 | - 共享锁允许事务读取一行数据。 3 | - 当事务对数据加上共享锁时,其他事务可以对该数据加共享锁,但不能加排他锁。 4 | - 即多个事务可以同时读取同一数据,但任何事务都不能写入。 5 | 2. **排他锁**: 6 | - 排他锁允许事务更新或删除一行数据。 7 | - 当事务对数据加上排他锁时,其他事务既不能加共享锁,也不能加排他锁。 8 | - 即当一个事务获得排他锁时,它可以执行写操作,而其他事务则不能对该数据执行读或写操作。 9 | 3. **意向锁**: 10 | - 意向锁是表级锁,用来表示事务对表中某些行加了共享锁或排他锁的意图。 11 | - 意向锁的种类通常包括意向共享锁(IS),意向排他锁(IX),以及共享意向排他锁(SIX)。 12 | - 它们主要用于在多粒度锁定系统中,帮助避免在行级和表级锁之间的冲突,并提高系统的并发性能。 13 | 4. **更新锁**: 14 | - 更新锁是一种特殊类型的锁,用于处理可能会导致死锁的情况。 15 | - 它通常在事务想要获取排他锁之前暂时使用,以防止其他事务同时也试图获取排他锁。 16 | - 更新锁可以转换成排他锁,但是在转换过程中不允许其他事务介入。 17 | 5. **记录锁**: 18 | - 在某些系统中,行级锁可能还包括对单个记录或键值进行锁定的机制。 19 | - 这些锁确保对索引记录的访问也被同步,可以避免幻读现象。 20 | 6. **间隙锁**: 21 | - 间隙锁用于锁定索引记录之间的范围,而不是单独的记录。 22 | - 这种锁主要用于隔离级别较高的事务中,以防止幻读。 23 | 7. **临键锁**: 24 | - 临键锁是记录键锁和间隙锁的组合,它锁定一个范围并且包括了范围的起始记录。 25 | - 这种锁在某些数据库系统(如MySQL的InnoDB存储引擎)中用于可重复读的隔离级别,以确保范围查询的一致性。 -------------------------------------------------------------------------------- /problems/数据库事务隔离级别有那些?.md: -------------------------------------------------------------------------------- 1 | 1. **读未提交(Read Uncommitted):** 这是最低的隔离级别,允许事务看到其他未提交事务的修改。这可能会导致脏读、不可重复读和幻读问题。 2 | 2. **读已提交(Read Committed):** 允许事务只能看到其他已经提交事务的修改。这限制了脏读问题,但仍然可能出现不可重复读和幻读问题。 3 | 3. **可重复读(Repeatable Read):** 保证在一个事务内多次读取同一数据的结果是一致的。该隔离级别可以避免脏读和不可重复读问题,但在某些情况下还是可能出现幻读问题。 4 | 4. **序列化(Serializable):** 这是最高的隔离级别,会在读取的每一行数据上都加锁,从而完全防止脏读、不可重复读和幻读问题,但相应的性能开销也最大。 -------------------------------------------------------------------------------- /problems/数据库日志类型作用.md: -------------------------------------------------------------------------------- 1 | 在数据库系统中,日志(或称为事务日志)是记录所有数据库操作的详细信息的文件或一组文件,它们对于确保数据的完整性和恢复机制至关重要。不同类型的数据库可能具有不同的日志实现方式和名称,但通常可以划分为以下几种基本类型: 2 | 3 | **事务日志(Transaction Log)**: 4 | 5 | - **作用**:记录了修改数据库内容的所有事务操作,包括增加、删除、更新数据等。 6 | - **目的**:提供事务的持久性,即使发生故障也能保证已提交事务的更改不会丢失。 7 | - **恢复使用**:可以用来在系统崩溃后恢复数据到最后一次一致的状态。 8 | 9 | **审计日志(Audit Log)**: 10 | 11 | - **作用**:记录了数据库操作的详细历史,比如谁什么时候对数据库做了什么操作。 12 | - **目的**:主要用于安全和合规目的,可以追踪异常行为或未授权的操作。 13 | 14 | **错误日志(Error Log)**: 15 | 16 | - **作用**:记录数据库运行过程中出现的各种错误信息,包括启动、运行或停止数据库服务器时发生的错误。 17 | - **目的**:帮助数据库管理员诊断问题,进行错误分析和故障排除。 18 | 19 | **二进制日志(Binary Log)**: 20 | 21 | - **作用**:特定于MySQL数据库,记录了影响数据库数据更改的所有语句(数据定义语言-DDL与数据操纵语言-DML操作),以事件序列的形式存储。 22 | - **目的**:主要用于复制和数据恢复。在复制中,二进制日志被用来在一个或多个从服务器上重放主服务器上的操作。 23 | 24 | **归档日志(Archive Log)**: 25 | 26 | - **作用**:在某些数据库管理系统中如Oracle,归档日志记录了自上次备份以来所有的事务日志。 27 | - **目的**:用于数据的恢复,可以将数据库恢复到任意时间点,而不只是最后的一致状态。 28 | 29 | 这些日志类型共同作用于数据库的安全性、可靠性和稳定性,通过记录不同层面的数据变化和事件,它们支撑着数据库的备份与恢复、故障修复、数据复制、审计和性能分析等关键功能。由于日志文件通常成长很快,需要定期管理和维护,以防止占用过多磁盘空间或影响系统性能。 -------------------------------------------------------------------------------- /problems/数据库的ACID特性.md: -------------------------------------------------------------------------------- 1 | 1. **原子性(Atomicity)**:原子性意味着事务内的操作要么全部完成,要么全部不执行。事务是数据库的最小操作单位,作为一个整体来执行。如果事务中的某个操作失败,整个事务会被回滚,数据库状态会恢复到事务开始之前的状态。 2 | 2. **一致性(Consistency)**:一致性确保事务从一个一致的状态转换到另一个一致的状态。在事务开始和完成时,数据库的完整性约束不能被破坏。例如,如果数据库的一个规则是所有的账户余额不能为负数,那么任何事务结束时都必须保证这个规则得到满足。 3 | 3. **隔离性(Isolation)**:隔离性指的是并发的事务之间的操作是隔离的,即事务之间不会互相影响。当多个事务同时运行时,一个事务的中间状态不应该对其他事务可见。隔离性可以通过“隔离级别”来实现,不同的隔离级别可以防止不同程度的并发问题,如脏读(dirty read)、不可重复读(non-repeatable read)和幻读(phantom read)。 4 | 4. **持久性(Durability)**:持久性意味着一旦事务被提交,它对数据库的更改就是永久性的。即使发生系统崩溃,数据库也必须能够保证已经提交的事务的所有操作都不会丢失,这通常通过写入到磁盘上的日志文件(如Redo Log)来实现。 -------------------------------------------------------------------------------- /problems/数据库范式第一第二第三范式.md: -------------------------------------------------------------------------------- 1 | **第一范式(1NF)**: 2 | 3 | - **定义**:确保每个表格都是二维的,即每个字段(列)都含有原子性的值,并且每个记录(行)都是唯一的。 4 | - 要求 5 | - 表中的所有字段值都是不可分割的原子值。 6 | - 每一列都是唯一的,不能存在两列具有相同的属性。 7 | - 每一行都有唯一性,通常通过实现一个主键来保证。 8 | 9 | **第二范式(2NF)**: 10 | 11 | - **定义**:在满足1NF的基础上,消除非主属性对于候选键的部分函数依赖。 12 | - 要求 13 | - 表必须先满足第一范式的要求。 14 | - 表中的非主属性完全依赖于整个候选键,而不仅仅是依赖于候选键的一部分(适用于复合主键)。 15 | 16 | **第三范式(3NF)**: 17 | 18 | - **定义**:在满足2NF的基础上,消除非主属性对于任何候选键的传递函数依赖。 19 | - 要求 20 | - 表必须先满足第二范式的要求。 21 | - 所有非主属性不依赖于其他非主属性,即每个非主属性只依赖于候选键。 -------------------------------------------------------------------------------- /problems/数组和链表的区别、适用场景.md: -------------------------------------------------------------------------------- 1 | 1. 数组: 2 | 3 | - 数组是一种线性数据结构,元素在内存中连续存储。 4 | - 数组具有固定的大小,在创建时需要指定大小。 5 | - 可以通过索引直接访问数组中的元素,时间复杂度为 O(1)。 6 | - 插入和删除元素时,需要移动其他元素来保持连续性,时间复杂度为 O(n)。 7 | - 适合用于元素个数固定、对随机访问要求较高的场景。 8 | 9 | 1. 链表: 10 | 11 | - 链表是一种非连续的线性数据结构,通过指针将元素串联在一起。 12 | - 链表的大小可以动态调整,不需要预先指定大小。 13 | - 链表插入和删除元素的操作简单,时间复杂度为 O(1)。 14 | - 链表不能通过索引直接访问元素,需要从头节点开始遍历,时间复杂度为 O(n)。 15 | - 分为单向链表、双向链表和循环链表等多种形式,提供了更多的灵活性。 16 | - 适合用于频繁插入和删除操作、对内存空间要求较高或元素个数变化较大的场景。 -------------------------------------------------------------------------------- /problems/时间复杂度和空间复杂度的定义?时间换空间&空间换时间的例子有哪些.md: -------------------------------------------------------------------------------- 1 | 时间复杂度是算法运行所需的时间量度,通常用大 O 表示,表示算法执行步骤数目与输入规模之间的关系。时间复杂度描述了算法运行时间随着输入规模增长时的增长趋势。 2 | 3 | 空间复杂度是算法在运行过程中所需的存储空间量度,同样用大 O 表示,表示算法运行所需的额外存储空间与输入规模之间的关系。空间复杂度描述了算法运行所需的额外存储空间随着输入规模增长时的增长趋势。 4 | 5 | "时间换空间"和"空间换时间"是指在设计算法时,可以根据具体情况选择使用更多的时间来节省空间,或者使用更多的空间来节省时间。以下是一些时间换空间和空间换时间的例子: 6 | 7 | 1. 时间换空间:缓存技术就是典型的时间换空间策略。通过将计算结果缓存起来,下次再遇到相同输入时可以直接返回缓存的结果,省去重复计算时间,但需要占用额外的空间存储缓存结果。 8 | 2. 空间换时间:排序算法中的归并排序和快速排序,虽然时间复杂度较低,但需要额外的空间来存储中间结果,属于典型的空间换时间策略。 9 | 3. 时间换空间:动态规划算法中常使用时间换空间策略,通过保存中间计算结果来避免重复计算,提高算法效率。 10 | 4. 空间换时间:哈希表(Hash Table)是一种典型的空间换时间的数据结构,通过消耗额外的空间来实现常量级别的查找、插入和删除操作。 -------------------------------------------------------------------------------- /problems/构造函数中可以调用虚函数吗.md: -------------------------------------------------------------------------------- 1 | 先说结论:理论可以,但是真的去这么做了,会出大问题,所以一般情况下我们不允许这么做。 2 | 3 | 4 | 5 | 让我们来具体分析下: 6 | 7 | ​ 首先,在C++编程语言中,构造函数中是可以调用虚函数的,C++的规则里面并没有任何对于这方面的限制,但是考虑一个问题,假设有两个对象,他们分别为继承关系,因为当一个类对象(包括派生类)正在被创建时,其类型会逐步从基类变化到最终的派生类。**如果在这个过程中调用了虚函数,那么这个虚函数调用将不会下降到更深层次的派生类。** 8 | 9 | ​ 具体来说,如果在基类的构造函数中调用了虚函数,并且这个虚函数在某个派生类中被重写了,那么在创建这个派生类的对象时,虚函数调用将只会执行基类版本的虚函数,而不会执行派生类版本的虚函数。这是因为,此时派生类部分的对象还没有被完全构造好,所以不能调用派生类的成员函数。 10 | 11 | ​ 至于为什么会这样,**主要是原因在于C++中的虚函数通过一个称为vtable(虚表)的机制来实现动态绑定。每个包含虚函数的类都有一个与之关联的虚表,其中包含了指向该类的虚函数的指针。** 12 | 13 | ​ 当一个对象被创建时,它的构造函数会先调用其基类的构造函数,然后按照声明顺序初始化其成员,并最后执行其自身的构造函数代码。**当在一个构造函数中调用虚函数时,由于此时正在构造的对象尚未完成构造,其类型被视为当前正在执行的构造函数所在的类,而不是最终派生类。**因此,如果在基类的构造函数中调用虚函数,将使用基类的虚表,从而调用基类版本的虚函数,而不是派生类版本的虚函数。 14 | 15 | ​ 至于为什么不是最终派生类,我们都知道,虚函数表只会被创建一份,但是具体在什么地方创建,这个取决于编译器如何实现,但是他的虚函数表指针一般会放在对象头部,也就是说,每个类只有一个虚函数表,但是每个对象都有自己的虚函数表指针。这意味着,**同一类型的所有对象都共享同一个虚函数表的内容,但是他们各自的虚函数表指针可能指向不同的虚函数表**(如果它们是不同类型的对象)。这也是为什么派生类可以覆盖基类的虚函数的原因:因为派生类的虚函数表中,对应的函数指针会被更新以指向派生类的版本。而在现在这个情况,由于派生类并未构造完成,所以派生类的的构造函数调用的虚函数只能参考基类而不是派生类,从而导致出现错误 16 | 17 | ​ 总的来说,虽然技术上允许在构造函数中调用虚函数,但是实际操作中却应该避免。一般来说,构造函数应该尽量简单,只做必要的初始化工作,而将复杂的逻辑放入其他成员函数中。 -------------------------------------------------------------------------------- /problems/标准库函数和系统调用的区别.md: -------------------------------------------------------------------------------- 1 | 1. 来源: 2 | - 系统调用:**这些函数来自操作系统内核**。它们是操作系统提供给应用程序直接访问硬件和系统资源的基础界面,例如读写文件、发送网络数据、创建进程等。 3 | - 标准库函数:**这些函数是由C语言(或其他语言)运行时环境提供的**,例如printf、strcpy、malloc等。 4 | 2. 实现: 5 | - 系统调用:**由操作系统内核代码实现**,当一个进程执行系统调用时,**它需要切换到内核模式来运行特权代码。** 6 | - 标准库函数:**通常使用用户模式下的普通代码实现**,并且可能在其内部使用系统调用以提供其功能。 7 | 3. 性能: 8 | - 系统调用:因为**涉及用户空间到内核空间的上下文切换**,所以相比于标准库函数,**系统调用通常会有更高的开销。** 9 | - 标准库函数:也可能引入一定的性能开销,如果它们内部使用了系统调用,但如果只是在用户空间进行计算,那么它们的开销就会小得多。 10 | 4. 可移植性: 11 | - 系统调用:**通常依赖于特定的操作系统**,所以在不同操作系统之间的可移植性较差。 12 | - 标准库函数:大多数情况下,**标准库函数在各种平台上的行为都是一致的,所以具有更好的可移植性。** -------------------------------------------------------------------------------- /problems/栈和队列的区别、适用场景.md: -------------------------------------------------------------------------------- 1 | 1. 栈: 2 | 3 | - 栈是一种后进先出(LIFO,Last In First Out)的数据结构,类似于一摞盘子,只能在栈顶进行操作。 4 | - 栈的插入(压栈)和删除(弹栈)操作都发生在栈顶元素,时间复杂度为 O(1)。 5 | - 栈通常用于实现函数调用、表达式求值、括号匹配等场景。 6 | - 常见的栈包括数组实现的顺序栈和链表实现的链式栈。 7 | 8 | 1. 队列: 9 | 10 | - 队列是一种先进先出(FIFO,First In First Out)的数据结构,类似于排队等候的过程,新元素在队尾插入,从队头删除。 11 | - 队列的插入(入队)和删除(出队)操作分别在队尾和队头进行,时间复杂度为 O(1)。 12 | - 队列通常用于任务调度、缓冲区管理、广度优先搜索等场景。 13 | - 常见的队列包括数组实现的顺序队列和循环队列,以及链表实现的链式队列。 14 | 15 | 适用场景: 16 | 17 | - 栈适合用于需要后进先出的场景,如函数调用、逆波兰表达式求值等。 18 | - 队列适合用于需要先进先出的场景,如任务调度、消息传递等。 19 | - 在实际应用中,根据具体需求选择合适的数据结构可以提高程序效率和简化算法设计。 -------------------------------------------------------------------------------- /problems/检查手机号是否存在于百万数据电话号中.md: -------------------------------------------------------------------------------- 1 | 要检查一个手机号是否存在于百万条数据中,可以使用哈希算法或者数据库索引来提高效率。一种常见的做法是将这些电话号码存储在一个哈希表中,然后通过计算待查找手机号的哈希值来加快搜索速度。另外,如果数据量非常大,可以考虑使用分布式计算框架,如Hadoop或Spark来处理这个任务。通过数据分片和并行计算,可以更快地检查手机号是否存在于百万数据电话号中。 -------------------------------------------------------------------------------- /problems/死锁必要条件及避免算法.md: -------------------------------------------------------------------------------- 1 | **死锁的四个必要条件:** 2 | 3 | 1. 互斥条件:进程对所分配到的资源进行排他性使用,即在一段时间内某资源只由一个进程占用。如果此时还有其他进程请求资源,则请求者只能等待,直至占有资源的进程用毕释放。 4 | 2. 请求和保持条件:进程已经保持至少一个资源,但又提出了新的资源请求,而该资源已被其他进程占有,此时请求进程阻塞,但对自己已获得的资源保持不放。 5 | 3. 不剥夺条件:进程已获得的资源,在未使用完之前,不能强行剥夺。 6 | 4. 循环等待条件:存在一个等待序列 {P0, P1, ..., Pn},其中 P0 正在等待 P1 占用的资源;P1 正在等待 P2 占用的资源,……,Pn 正在等待已经被 P0 占用的资源。 7 | 8 | **避免死锁的方法:** 9 | 10 | 1. 鸽巢算法:避免死锁最具代表性的算法是鸽巢算法。它通过预测分配资源可能导致死锁的情况,防止死锁的发生。 11 | 2. 银行家算法:这是一种用于避免死锁并确保系统稳定运作的算法。该算法在为进程分配资源之前,会判断进行资源分配后是否安全,即是否可能导致系统出现死锁。如果判断可能发生死锁,那么就拒绝进行资源分配。 12 | 3. 资源有序分配法:系统给每类资源赋予一个编号,每一个进程按编号递增的顺序请求资源,释放资源则可以任意顺序。这样就破坏了循环等待条件,因此可避免死锁。 13 | 4. 尽量避免使用不可抢占调度算法:使用可抢占的调度算法可以让已经占用资源的进程,在必要时释放资源,从而避免死锁。 14 | 15 | 实际上并没有万能的解决死锁的方法,比如**操作系统内核中断等问题会让原先的死锁算法失效**。需要根据实际情况选择合适的策略。 -------------------------------------------------------------------------------- /problems/现在普通关系数据库用得数据结构是什么类型的数据结构.md: -------------------------------------------------------------------------------- 1 | 普通关系数据库通常用B树(特别是B+树)这种类型的数据结构来实现索引。B树和它的变种,如B+树、B*树,在数据库中的使用非常广泛,因为它们具有高效的搜索、插入和删除操作的特点,尤其是在处理大量数据时。 2 | 3 | 以下是一些关键点,解释了为什么B树(尤其是B+树)被广泛用于数据库索引: 4 | 5 | 1. **平衡性**:B树是一种平衡树,意味着所有叶子节点都位于同一层。这保证了从根节点到任何叶子节点的路径长度相同,因此每次查找操作的时间复杂度都是一致的。 6 | 2. **分支因子**:B树的一个节点可以有多个子节点(分支因子或阶数),这比二叉树的两个子节点要多得多。这降低了树的总体高度,从而减少了磁盘I/O操作的次数,这对于数据库中的大量数据是非常重要的。 7 | 3. **磁盘友好**:B树的设计能够很好地适应系统的磁盘访问特性。节点的大小通常设置为与磁盘块(页)的大小相匹配,这样每次磁盘I/O可以读取或写入完整的B树节点。 8 | 4. **顺序访问优化**:尤其是B+树,其所有的值都存储在叶子节点,并且叶子节点之间按顺序相连。这使得对数据进行顺序遍历变得非常高效,这在执行范围查询时非常有用。 9 | 5. **最小化空间浪费**:由于B树的节点通常很大,且每个节点存储多个键,这有助于减少未使用空间,同时保持树的平衡。 10 | 11 | 虽然B树是数据库索引的主流选择,但其他类型的数据结构也可能被使用,这取决于特定的应用场景和数据库的设计。例如,对于全文搜索,倒排索引可能会被使用;而对于地理空间数据,R树是更好的选择。哈希表也可能用于特殊情况下的索引,尤其是当查询完全基于等值查找时。 -------------------------------------------------------------------------------- /problems/用C++设计一个不能被继承的类.md: -------------------------------------------------------------------------------- 1 | 使用C++中的关键字`final`。将`final`关键字放在类的声明中,表示该类不能再被其他类所继承。如果有其他类试图进行继承,编译器将会报错。 2 | 3 | ```C++ 4 | class FinalClass final { 5 | public: 6 | // 类的成员函数和数据成员... 7 | }; 8 | 9 | ``` 10 | 11 | -------------------------------------------------------------------------------- /problems/用户态和内核态的区别.md: -------------------------------------------------------------------------------- 1 | 1. 权限: 2 | - 用户态(User Mode): 用户态是一种受限的状态,程序在这个状态下运行时,有很多敏感的指令不能执行,也不能直接访问系统硬件资源。 3 | - 内核态(Kernel Mode): 内核态也称为超级用户模式或系统模式,程序在内核态下运行时,没有任何限制,可以执行所有指令,可以直接访问和控制硬件。 4 | 2. 功能: 5 | - 用户态主要用于运行应用程序。当一个应用程序需要执行一个系统调用(比如读取文件、发送网络数据等)时,它会切换到内核态,然后由内核来完成这个操作。 6 | - 内核态主要用于运行操作系统的内核和驱动程序。内核提供系统服务、管理硬件资源等。 7 | 3. 资源访问: 8 | - 用户态无法直接访问硬件资源,只能通过系统调用来请求内核提供的服务。 9 | - 内核态可以直接访问硬件资源,例如内存、磁盘、网络等。 10 | 4. 切换: 11 | - 用户态到内核态的切换通常通过系统调用或者硬件中断来实现,这个过程需要一定的时间和资源,因此频繁切换会影响系统性能。 12 | - 内核态到用户态的切换通常在完成系统调用或者中断处理之后发生。 -------------------------------------------------------------------------------- /problems/程序编译的过程.md: -------------------------------------------------------------------------------- 1 | 程序编译是将高级语言代码翻译成机器可执行的目标代码或字节码的过程。下面是程序编译的基本过程: 2 | 3 | 1. **词法分析(Lexical Analysis)**:编译过程的第一步是将源代码分解成词法单元,也称为词法分析。词法分析器扫描源代码,识别关键字、标识符、操作符等各种词法单元,并生成对应的记号流。 4 | 2. **语法分析(Syntax Analysis)**:接下来是语法分析,也称为语法解析。语法分析器根据词法分析器生成的记号流,检查代码是否符合语法规则,构建抽象语法树(Abstract Syntax Tree,AST),用于表示程序的结构和组织。 5 | 3. **语义分析(Semantic Analysis)**:在语义分析阶段,编译器会对抽象语法树进行语义检查,确保代码的含义是正确的。这包括类型检查、作用域分析、常量折叠等操作,以确保程序的逻辑正确性。 6 | 4. **中间代码生成(Intermediate Code Generation)**:在进行中间代码生成之前,有些编译器可能会生成中间表示形式,例如三地址码、四地址码等形式。然后,编译器会根据抽象语法树生成中间代码,将高级语言代码转换为类似汇编语言的中间代码。 7 | 5. **优化(Optimization)**:优化是编译过程中非常重要的一步,目的是提高程序的性能和效率。编译器会对中间代码进行各种优化,例如常量传播、死代码消除、循环展开等,以减少程序运行时间和内存占用。 8 | 6. **目标代码生成(Code Generation)**:最后一步是目标代码生成,将优化后的中间代码转换为特定平台的机器代码或字节码。这个阶段会涉及到指令选择、寄存器分配、内存分配等过程,生成可被计算机执行的最终目标代码。 9 | 7. **链接(Linking)**:对于需要多个源文件组成的程序,还需要进行链接操作。链接器会将各个目标文件合并生成一个可执行文件,解析外部引用符号,处理库文件等,最终生成完整的可执行程序。 -------------------------------------------------------------------------------- /problems/简单说说STL中的优先级队列是如何实现的?.md: -------------------------------------------------------------------------------- 1 | 在C++标准模板库(STL)中,优先级队列是通过一个称为堆的数据结构实现的,通常用一个向量(通常是 `std::vector`)来表示。具体来说,默认情况下,优先级队列使用最大堆来组织元素,这意味着队列顶部总是最大的元素。如果需要最小元素优先,可以通过提供自定义比较函数来实现最小堆。 2 | 3 | 优先级队列在STL中是用模板类 `std::priority_queue` 实现的,该类在 `` 头文件中定义。它允许插入和取出元素的操作,其中插入操作是将新元素添加到正确位置以保持堆的性质,取出操作是移除队列顶部的元素。其余的元素会重新排列以保持堆的性质,确保下一个最大(或最小,取决于比较函数)元素移动到队列顶部。 -------------------------------------------------------------------------------- /problems/类构造和析构的顺序.md: -------------------------------------------------------------------------------- 1 | 在C++中,类的构造和析构顺序遵循以下规则: 2 | 3 | 1. 构造顺序: 4 | - 基类优先于派生类进行构造。 5 | - 在同一个类中,成员变量按照声明的顺序进行初始化。 6 | - 对于多个基类的情况,会按照声明顺序来初始化。 7 | 2. 析构顺序: 8 | - 析构顺序与构造顺序相反。 9 | - 派生类对象先于基类对象进行析构。 10 | - 在同一个类中,成员变量按照声明顺序的逆序进行析构。 11 | - 对于多个基类的情况,会按照声明顺序的逆序进行析构。 12 | 13 | 举个例子: 14 | 15 | ```c++ 16 | #include 17 | using namespace std; 18 | 19 | class Base { 20 | public: 21 | Base() { cout << "构造基类对象\n"; } 22 | ~Base() { cout << "析构基类对象\n"; } 23 | }; 24 | 25 | class Derived : public Base { 26 | public: 27 | Derived() { cout << "构造派生类对象\n"; } 28 | ~Derived() { cout << "析构派生类对象\n"; } 29 | }; 30 | 31 | int main() { 32 | Derived d; 33 | return 0; 34 | } 35 | ``` 36 | 37 | 输出将会是: 38 | 39 | ```c++ 40 | 复制代码构造基类对象 41 | 构造派生类对象 42 | 析构派生类对象 43 | 析构基类对象 44 | ``` 45 | 46 | 对于成员变量,我们可以稍微修改一下代码: 47 | 48 | ```c++ 49 | #include 50 | using namespace std; 51 | 52 | class Member { 53 | public: 54 | Member(const char* s) { cout << "构造成员对象: " << s << "\n"; } 55 | ~Member() { cout << "析构成员对象\n"; } 56 | }; 57 | 58 | class WithMembers { 59 | public: 60 | WithMembers() : m2("m2"), m1("m1") { cout << "构造包含成员的对象\n"; } 61 | ~WithMembers() { cout << "析构包含成员的对象\n"; } 62 | 63 | private: 64 | Member m1; 65 | Member m2; 66 | }; 67 | 68 | int main() { 69 | WithMembers wm; 70 | return 0; 71 | } 72 | ``` 73 | 74 | 你会看到如下的输出: 75 | 76 | ``` 77 | 复制代码构造成员对象: m1 78 | 构造成员对象: m2 79 | 构造包含成员的对象 80 | 析构包含成员的对象 81 | 析构成员对象 82 | 析构成员对象 83 | ``` 84 | 85 | 即使在构造函数中 `m2` 在 `m1` 之前初始化,`m1` 和 `m2` 的构造顺序还是按照它们在类中声明的顺序。同样,析构顺序与构造顺序相反。 86 | -------------------------------------------------------------------------------- /problems/索引的优点和缺点.md: -------------------------------------------------------------------------------- 1 | **索引的优点**: 2 | 3 | 1. **提高查询速度**:索引可以极大地加快数据检索的速度。这是通过使用快速的查找算法(如二进制搜索、B树和散列)来实现的,这些算法比全表扫描要快得多。 4 | 2. **排序数据记录**:索引可以在数据库中为数据记录进行排序,从而减少了排序操作(ORDER BY)的需要,同时也加快了排序查询的响应时间。 5 | 3. **唯一性约束**:唯一索引可以保证每一行数据的唯一性。这经常用于主键索引以确保主键不重复。 6 | 4. **更有效的数据访问计划**:对于复杂的查询语句,包含多个连接(JOINs)和条件子句(WHERE)的查询,数据库优化器能够利用索引来创建一个更有效的数据访问策略。 7 | 5. **支持索引的SQL语句**:通过使用索引,可以快速完成许多SQL操作,如COUNT, MIN, MAX等。 8 | 9 | **索引的缺点**: 10 | 11 | 1. **增加写操作的成本**:每次对数据库表的插入或更新操作时,索引也必须更新。这使得写操作比没有索引时更加昂贵和慢。 12 | 2. **占用更多的存储空间**:索引需要额外的磁盘空间。对于拥有大量索引的数据库,这可能会成为问题,因为它们不仅会使用更多的磁盘空间,还可能导致内存中缓存的数据页更少。 13 | 3. **管理开销**:索引需要维护。随着数据的变更,索引可能会变得碎片化,这需要定期的维护以保持性能,例如通过重建或重新组织索引。 14 | 4. **索引设计**:确定哪些列应该被索引并不总是直接明了的。过多或错误的索引设计可能会损害性能,尤其是在选择错误的索引类型或创建与查询模式不匹配的索引时。 15 | 5. **影响优化器**:错误的索引可能会误导数据库查询优化器,导致选择了非最佳的查询计划。 -------------------------------------------------------------------------------- /problems/纯虚函数,为什么需要纯虚函数.md: -------------------------------------------------------------------------------- 1 | 2 | 纯虚函数是在基类中声明的虚函数,它在基类中没有定义,但要求任何派生类都要定义自己的实现方法。在基类中实现纯虚函数的方法是在函数原型后加“=0” 3 | 4 | `virtual void funtion1()=0` 5 | 6 | 原因: 7 | 1、为了方便使用多态特性,我们常常需要在基类中定义虚拟函数。 8 | 2、在很多情况下,基类本身生成对象是不合情理的。例如,动物作为一个基类可以派生出老虎、孔雀等子类,但动物本身生成对象明显不合常理。 9 | 10 | 为了解决上述问题,引入了纯虚函数的概念,将函数定义为纯虚函数(方法:virtual ReturnType Function()= 0;),则编译器要求在派生类中必须予以重写以实现多态性。同时含有纯虚拟函数的类称为抽象类,它不能生成对象。这样就很好地解决了上述两个问题。声明了纯虚函数的类是一个抽象类。所以,用户不能创建类的实例,只能创建它的派生类的实例。 11 | 12 | **定义纯虚函数的目的在于,使派生类仅仅只是继承函数的接口。** 13 | 纯虚函数的意义,让所有的类对象(主要是派生类对象)都可以执行纯虚函数的动作,但类无法为纯虚函数提供一个合理的缺省实现。所以类纯虚函数的声明就是在告诉子类的设计者,“你必须提供一个纯虚函数的实现,但我不知道你会怎样实现它”。 14 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /problems/线程间的通信方式有那些?.md: -------------------------------------------------------------------------------- 1 | 1. **共享内存**:线程可以直接访问进程的内存空间。共享数据的访问通常需要同步机制来防止出现竞态条件。 2 | 2. **互斥锁**:用于控制对共享资源的访问,保证在同一时间只有一个线程访问共享资源。 3 | 3. **读写锁**:允许多个线程同时读取一个资源,但写入时需要独占访问。 4 | 4. **条件变量**:允许一个或多个线程在某个条件发生前处于睡眠状态,等待另一个线程在该条件上发出通知或广播。 5 | 5. **信号量**:可以用于限制对共享资源的访问,也用于线程间的同步。 6 | 6. **事件(event)**:允许一个线程通知一个或多个等待的线程某个事件已发生。 7 | 7. **屏障(barrier)**:允许多个线程等待直到所有线程都已经达到某个同步点,然后再一起继续执行。 8 | 8. **线程局部存储(TLS)**:为每个线程提供独立的变量副本,避免了同步问题,但不适用于线程间的数据共享。 9 | 9. **原子操作**:利用CPU提供的原子指令集来进行无需加锁的线程安全操作。 -------------------------------------------------------------------------------- /problems/线程,进程和协程是否有自己独立的堆区和栈区?.md: -------------------------------------------------------------------------------- 1 | **进程有独立的堆区和栈区,线程共享进程的堆区但拥有独立的栈区,而协程通常共享堆区**,并且拥有自己的、通常是程序运行时分配的栈区。协程的栈区通常有别于传统的操作系统提供的线程栈,更加灵活和高效。 -------------------------------------------------------------------------------- /problems/虚函数是否可以声明为static?.md: -------------------------------------------------------------------------------- 1 | 虚函数不能声明为静态(static)。在C++中,虚函数是通过指针的动态绑定来实现多态性的,而静态成员函数是与类本身相关联的,不依赖于特定对象的实例化,因此无法被继承并重写。 2 | 3 | 当我们声明一个成员函数为虚函数时,编译器会为每个类维护一个虚函数表(vtable),用于存储该类的虚函数地址。然后,在运行时,通过对象的指针或引用调用虚函数时,程序会根据实际对象类型的虚函数表来确定要调用的函数。 4 | 5 | 相反,静态成员函数不与任何特定对象实例相关联,它属于类而不属于对象。因此,静态函数没有 this 指针,并且不参与多态性和动态绑定。由于静态成员函数没有虚函数表的概念,因此它们不能被声明为虚函数。 6 | 7 | 因此,在C++中,虚函数不能声明为静态。 -------------------------------------------------------------------------------- /problems/虚函数是针对类还是针对对象的?.md: -------------------------------------------------------------------------------- 1 | 虚函数是针对类的一个概念,而不是针对单个对象。当在C++中定义一个类时,可以将类的成员函数声明为虚函数。这意味着当通过基类指针或引用调用该函数时,将会根据对象的实际类型来决定调用哪个版本的函数,这一过程称为动态绑定或多态。 2 | 3 | 当派生类继承了包含虚函数的基类时,它可以重写这些虚函数,提供自己特定的实现。如果对象是派生类的实例,即使使用基类类型的指针或引用去调用虚函数,程序也会调用派生类中重写的版本。 4 | 5 | 这是多态性的核心:同样的函数调用,依据对象的实际类型,可以有不同的行为。因此,虚函数机制是设计在类层面上的,但它影响的是通过那些类创建的对象的行为。每个对象不需要单独定义虚函数,它们都是从其类定义中继承而来的。 -------------------------------------------------------------------------------- /problems/虚函数的作用和实现原理,什么是虚函数,有什么作用.md: -------------------------------------------------------------------------------- 1 | 2 | C++的多态分为静态多态(编译时多态)和动态多态(运行时多态)两大类。静态多态通过重载、模板来实现;动态多态就是通过本文的主角虚函数来体现的。 3 | 4 | 虚函数实现原理:包括虚函数表、虚函数指针等 5 | 6 | 虚函数的作用说白了就是:当调用一个虚函数时,被执行的代码必须和调用函数的对象的动态类型相一致。编译器需要做的就是如何高效的实现提供这种特性。不同编译器实现细节也不相同。大多数编译器通过vtbl(virtual table)和vptr(virtual table pointer)来实现的。 当一个类声明了虚函数或者继承了虚函数,这个类就会有自己的vtbl。vtbl实际上就是一个函数指针数组,有的编译器用的是链表,不过方法都是差不多。vtbl数组中的每一个元素对应一个函数指针指向该类的一个虚函数,同时该类的每一个对象都会包含一个vptr,vptr指向该vtbl的地址。 7 | 8 | 结论: 9 | 10 | 每个声明了虚函数或者继承了虚函数的类,都会有一个自己的vtbl 11 | 同时该类的每个对象都会包含一个vptr去指向该vtbl 12 | 虚函数按照其声明顺序放于vtbl表中, vtbl数组中的每一个元素对应一个函数指针指向该类的虚函数 13 | 如果子类覆盖了父类的虚函数,将被放到了虚表中原来父类虚函数的位置 14 | 在多继承的情况下,每个父类都有自己的虚表。子类的成员函数被放到了第一个父类的表中 15 | 16 | ## 衍生问题:为什么 C++里访问虚函数比访问普通函数慢? 17 | 18 | 单继承时性能差不多,多继承的时候会慢 19 | 20 | ## 调用性能方面 21 | 22 | 从前面虚函数的调用过程可知。当调用虚函数时过程如下(引自More Effective C++): 23 | 24 | 通过对象的 vptr 找到类的 vtbl。这是一个简单的操作,因为编译器知道在对象内 哪里能找到 vptr(毕竟是由编译器放置的它们)。因此这个代价只是一个偏移调整(以得到 vptr)和一个指针的间接寻址(以得到 vtbl)。 25 | 找到对应 vtbl 内的指向被调用函数的指针。这也是很简单的, 因为编译器为每个虚函数在 vtbl 内分配了一个唯一的索引。这步的代价只是在 vtbl 数组内 的一个偏移。 26 | 调用第二步找到的的指针所指向的函数。 27 | 在单继承的情况下,调用虚函数所需的代价基本上和非虚函数效率一样,在大多数计算机上它多执行了很少的一些指令,所以有很多人一概而论说虚函数性能不行是不太科学的。在多继承的情况下,由于会根据多个父类生成多个vptr,在对象里为寻找 vptr 而进行的偏移量计算会变得复杂一些,但这些并不是虚函数的性能瓶颈。 虚函数运行时所需的代价主要是虚函数不能是内联函。这也是非常好理解的,是因为内联函数是指在编译期间用被调用的函数体本身来代替函数调用的指令,但是虚函数的“虚”是指“直到运行时才能知道要调用的是哪一个函数。”但虚函数的运行时多态特性就是要在运行时才知道具体调用哪个虚函数,所以没法在编译时进行内联函数展开。当然如果通过对象直接调用虚函数它是可以被内联,但是大多数虚函数是通过对象的指针或引用被调用的,这种调用不能被内联。 因为这种调用是标准的调用方式,所以虚函数实际上不能被内联。 28 | 29 | ## 占用空间方面 30 | 31 | 32 | 在上面的虚函数实现原理部分,可以看到为了实现运行时多态机制,编译器会给每一个包含虚函数或继承了虚函数的类自动建立一个虚函数表,所以虚函数的一个代价就是会增加类的体积。在虚函数接口较少的类中这个代价并不明显,虚函数表vtbl的体积相当于几个函数指针的体积,如果你有大量的类或者在每个类中有大量的虚函数,你会发现 vtbl 会占用大量的地址空间。但这并不是最主要的代价,主要的代价是发生在类的继承过程中,在上面的分析中,可以看到,当子类继承父类的虚函数时,子类会有自己的vtbl,如果子类只覆盖父类的一两个虚函数接口,子类vtbl的其余部分内容会与父类重复。这在如果存在大量的子类继承,且重写父类的虚函数接口只占总数的一小部分的情况下,会造成大量地址空间浪费。在一些GUI库上这种大量子类继承自同一父类且只覆盖其中一两个虚函数的情况是经常有的,这样就导致UI库的占用内存明显变大。 由于虚函数指针vptr的存在,虚函数也会增加该类的每个对象的体积。在单继承或没有继承的情况下,类的每个对象会多一个vptr指针的体积,也就是4个字节;在多继承的情况下,类的每个对象会多N个(N=包含虚函数的父类个数)vptr的体积,也就是4N个字节。当一个类的对象体积较大时,这个代价不是很明显,但当一个类的对象很轻量的时候,如成员变量只有4个字节,那么再加上4(或4N)个字节的vptr,对象的体积相当于翻了1(或N)倍,这个代价是非常大的。 33 | 34 | 35 | ## 总结 36 | 37 | 最后,总结一下关于虚函数的一些常见问题: 38 | 39 | 1) 虚函数是动态绑定的,也就是说,使用虚函数的指针和引用能够正确找到实际类的对应函数,而不是执行定义类的函数。这是虚函数的基本功能,就不再解释了。 40 | 41 | 2) 构造函数不能是虚函数。而且,在构造函数中调用虚函数,实际执行的是父类的对应函数,因为自己还没有构造好, 多态是被disable的。 42 | 43 | 3) 析构函数可以是虚函数,而且,在一个复杂类结构中,这往往是必须的。 44 | 45 | 4) 将一个函数定义为纯虚函数,实际上是将这个类定义为抽象类,不能实例化对象。 46 | 47 | 5) 纯虚函数通常没有定义体,但也完全可以拥有。 48 | 49 | 6) 析构函数可以是纯虚的,但纯虚析构函数必须有定义体,因为析构函数的调用是在子类中隐含的。 50 | 51 | 7) 非纯的虚函数必须有定义体,不然是一个错误。 52 | 53 | 8) 派生类的override虚函数定义必须和父类完全一致。除了一个特例,如果父类中返回值是一个指针或引用,子类override时可以返回这个指针(或引用)的派生。例如,在上面的例子中,在Base中定义了 virtual Base* clone(); 在Derived中可以定义为 virtual Derived* clone()。可以看到,这种放松对于Clone模式是非常有用的。 54 | 55 | -------------------------------------------------------------------------------- /problems/讲一讲C++中的原子操作有那些?.md: -------------------------------------------------------------------------------- 1 | 在C++中,原子操作是由 `` 头文件提供的一组操作和类型,它们确保了在多线程环境中对共享数据的修改是不可分割的,即这些操作在执行过程中不会被其他线程打断。原子操作主要用于实现无锁(lock-free)编程,允许多个线程安全地并发访问和修改数据。 2 | 3 | 4 | 5 | `std::atomic` 是C++11引入的一个模板类,用来封装任意类型(满足TriviallyCopyable)的值,并且保证对这些值的操作是原子的。以下是一些常见的原子操作: 6 | 7 | 1. `store`:将值写入原子对象。 8 | 2. `load`:从原子对象读取值。 9 | 3. `exchange`:替换原子对象的值,并返回该对象之前的值。 10 | 4. `compare_exchange_weak` / `compare_exchange_strong`:比较原子对象的值,并在相等时替换为新值。 11 | 5. `fetch_add` / `fetch_sub`:对原子对象进行加/减操作,并返回操作前的值。 12 | 6. `fetch_and` / `fetch_or` / `fetch_xor`:对原子对象进行位与/位或/位异或操作,并返回操作前的值。 13 | 7. `++` / `--`:原子地递增或递减对象的值。 14 | 15 | 16 | 17 | 原子操作还包括一些内存顺序(memory order)的概念,如 `memory_order_relaxed`, `memory_order_consume`, `memory_order_acquire`, `memory_order_release`, `memory_order_acq_rel`, `memory_order_seq_cst`。这些内存顺序用于控制操作的执行顺序,以及如何处理编译器和处理器级别的优化。 -------------------------------------------------------------------------------- /problems/讲一讲Redis主从复制.md: -------------------------------------------------------------------------------- 1 | Redis主从复制是指一个Redis实例(称为主节点)可以将自己的数据复制到另一个或多个Redis实例(称为从节点)的过程。主从复制的作用在于提高数据的可用性和扩展性,以及增强系统的容错能力。 2 | 3 | 以下是Redis主从复制的一般工作流程: 4 | 5 | 1. **建立连接**:从节点通过向主节点发送SYNC命令来请求数据复制。主节点接收到SYNC命令后,会开始进行数据同步。 6 | 2. **数据同步**:主节点会将自己的数据集发送给从节点,并持续地将新写入的数据异步传输给从节点。从节点接收到数据后会更新自己的数据集。 7 | 3. **复制初始化**:当从节点首次进行复制时,主节点会发送其整个数据集给从节点,完成一次全量复制。之后的复制过程将变为增量复制。 8 | 4. **增量复制**:主节点会将新写入的命令以数据流的形式发送给从节点,从节点接收并执行这些命令,保持与主节点数据的一致性。 9 | 5. **心跳检测**:主从节点之间会定期发送心跳包进行检测,确保连接的稳定性。如果从节点长时间没有收到主节点的心跳包,会尝试重新连接或重新发起同步。 10 | 11 | 通过主从复制,可以实现以下功能和优势: 12 | 13 | - **读写分离**:主节点负责处理写操作,从节点负责处理读操作,有效分担了主节点的压力和提高了系统的并发能力。 14 | - **故障恢复**:当主节点发生故障时,可以快速切换一个从节点为新的主节点,保证系统的可用性。 15 | - **数据备份**:从节点可以作为主节点的数据备份,避免数据丢失。 16 | - **横向扩展**:通过添加更多的从节点,可以横向扩展系统的读能力。 17 | 18 | 需要注意的是,在主从复制中,主节点负责写操作,因此主节点的性能和可靠性对整个系统至关重要。同时,需要确保主从节点之间的网络连接稳定,以避免数据同步延迟或不一致的问题。 -------------------------------------------------------------------------------- /problems/讲一讲Redis的哨兵.md: -------------------------------------------------------------------------------- 1 | Redis的哨兵(Sentinel)是一个用于监控和管理Redis主从复制集群的工具。哨兵的作用在于监控Redis实例的健康状态,当主节点出现故障或不可用时,自动进行故障转移,并选择一个合适的从节点升级为新的主节点,以保证系统的高可用性。 2 | 3 | 以下是Redis哨兵的主要功能和特点: 4 | 5 | 1. **监控**:哨兵定期检查Redis实例的健康状态,包括主节点是否存活、从节点是否正常复制等,以确保整个集群的稳定性。 6 | 2. **故障检测**:当主节点宕机或不可用时,哨兵会立即发现并启动自动故障转移流程,选择一个从节点晋升为新的主节点。 7 | 3. **故障转移**:哨兵通过投票机制选举出新的主节点,然后通知其他从节点切换到新的主节点并重新进行复制。 8 | 4. **配置维护**:哨兵可以对Redis集群的配置进行动态更新,例如添加或删除节点、修改节点参数等。 9 | 5. **集群管理**:哨兵可以管理多个Redis集群,监控它们的状态并及时处理故障,提高整个系统的可用性。 10 | 6. **自动恢复**:一旦主节点恢复正常,哨兵会自动将其恢复为主节点,并重新进行数据同步。 11 | 12 | 通过使用Redis哨兵,可以有效地提高Redis集群的可用性和稳定性,避免单点故障对系统造成影响。哨兵是一个轻量级的监控工具,可以方便地部署和配置,使得Redis集群更加健壮和可靠。 -------------------------------------------------------------------------------- /problems/讲一讲Redis的集群.md: -------------------------------------------------------------------------------- 1 | Redis集群是用于横向扩展和提高Redis性能的分布式解决方案。Redis集群允许在多个Redis节点之间分散数据,以实现更好的负载均衡、高可用性和扩展性。以下是关于Redis集群的一些重要信息: 2 | 3 | 1. **分区**:Redis集群将数据分成16384个槽(slot),每个槽对应一个数据片段。这些槽均匀分布在集群中的各个节点上,通过hash算法确定数据所属的槽。 4 | 2. **节点**:Redis集群由多个节点组成,每个节点负责管理一部分数据槽。节点之间通过集群总线(cluster bus)进行通信和协作。 5 | 3. **主从复制**:每个主节点都有若干个从节点,用于备份数据和提高读取性能。当一个主节点下线时,其从节点可以被晋升为新的主节点,确保数据的可用性。 6 | 4. **客户端请求路由**:客户端可以通过Redis集群的代理节点(proxy)来发送请求,代理节点负责将请求路由到正确的节点上,实现对分布式数据的访问。 7 | 5. **自动故障转移**:当一个主节点宕机或不可用时,Redis集群会自动触发故障转移流程,选择一个从节点晋升为新的主节点,保证数据的连续性和可用性。 8 | 6. **配置和管理**:Redis集群支持动态扩容和缩减,可以通过添加或删除节点来调整集群的规模。同时,集群还提供了命令行工具和API来管理集群的配置和状态。 9 | 7. **负载均衡**:Redis集群通过将数据分布在多个节点上,有效地分散了负载,提高了系统的并发处理能力。 10 | 11 | 通过部署Redis集群,可以实现更高的性能、可用性和扩展性,适用于大规模的数据存储和处理场景。需要注意的是,在使用Redis集群时,需要考虑数据的分片策略、节点的拓扑结构、故障恢复机制等因素,以保证集群的稳定运行和高效工作。 -------------------------------------------------------------------------------- /problems/讲讲函数调用的过程.md: -------------------------------------------------------------------------------- 1 | 在C++中,函数的调用过程主要包括以下几个步骤: 2 | 3 | 1. 压入返回地址:当一个函数被调用时,程序会先将当前函数的返回地址压入栈中。这个返回地址指向调用该函数之后需要返回到的下一个指令地址。 4 | 2. 压入参数:接下来,函数的参数按照从右到左的顺序被压入栈中。这些参数将作为函数的局部变量使用。 5 | 3. 保存寄存器状态:如果函数需要使用一些寄存器,那么在执行函数之前,需要先将这些寄存器的值保存在栈中,以防止它们被修改。 6 | 4. 跳转到函数体:现在,程序跳转到被调用的函数体开始执行。在执行函数时,函数的局部变量以及其他需要用到的内存空间也会被分配在栈上。 7 | 5. 执行函数体:函数体被执行并返回结果。在函数执行过程中,程序会按照语句的顺序依次执行每一条语句,并可能调用其他的子函数。 8 | 6. 弹出栈中的内容:当函数执行完毕之后,会将所有被压入栈中的内容依次弹出。这个过程包括弹出所有的局部变量、恢复寄存器状态、弹出所有的参数以及返回地址。 9 | 7. 返回到调用者:最后,程序跳转回到调用该函数的地方,并将函数的返回值传递给调用者。这时候,程序会从返回地址指针中读取下一个指令地址,并开始执行下一个指令。 -------------------------------------------------------------------------------- /problems/说说你对TCP流量控制的理解?.md: -------------------------------------------------------------------------------- 1 | TCP流量控制是指通过调整发送方的发送速率,使其不会发送过多数据导致接收方无法及时处理或网络拥塞的机制。TCP流量控制主要通过滑动窗口来实现。 2 | 3 | 在TCP通信中,每个TCP报文段都包含一个窗口字段,表示接收方当前能够接收的字节数。发送方根据接收方通知的窗口大小来确定发送数据的数量,如果窗口大小为0,则发送方暂停发送数据,等待接收方处理完数据后再次通知窗口大小。 4 | 5 | 通过不断调整窗口大小,TCP流量控制可以实现以下目标: 6 | 7 | 1. 避免发送方发送过多数据导致接收方缓冲区溢出; 8 | 2. 避免网络拥塞,保证网络传输的稳定性和可靠性; 9 | 3. 优化网络资源利用率,提高数据传输效率。 10 | 11 | TCP流量控制是TCP协议的重要特性之一,它能够保证数据在网络传输过程中按序、可靠地到达目的地,并且有效地调节发送端和接收端之间的数据交换速率,从而保证了网络通信的质量和性能。 -------------------------------------------------------------------------------- /problems/说说你对TCP滑动窗口的理解?.md: -------------------------------------------------------------------------------- 1 | TCP滑动窗口是一种流量控制和拥塞控制的机制,用于优化数据传输的效率。在TCP通信中,发送方和接收方之间维护一个窗口大小,表示接收方可以接收的数据量。 2 | 3 | 发送方根据接收方通知的窗口大小来确定发送数据的数量,发送数据后等待接收方确认,确认后再发送下一批数据。如果接收方准备好接收更多数据,它会增大窗口大小,如果接收方暂时无法处理更多数据,它会减小窗口大小或者发送窗口关闭通知。 4 | 5 | 通过调整滑动窗口大小,TCP可以实现有效的流量控制,避免发送过多数据导致网络拥塞,同时也可以提高数据传输的效率,使数据在网络上能够快速、稳定地传输。 -------------------------------------------------------------------------------- /problems/请你介绍一下http1.0.md: -------------------------------------------------------------------------------- 1 | ### 简单请求-响应模型 2 | 3 | HTTP/1.0 遵循一种简单的请求-响应模型。客户端(如 Web 浏览器)向服务器发送一个 HTTP 请求,然后服务器返回一个响应。请求和响应都包含起始行、头部字段和可选的消息主体。 4 | 5 | ### 无状态协议 6 | 7 | 与所有 HTTP 版本一样,HTTP/1.0 是一种无状态协议。这意味着每个请求都是独立的,服务器不会保留之前请求的任何状态信息。虽然这简化了服务器的设计,但也限制了某些应用场景,比如用户身份验证和会话管理。 8 | 9 | ### 连接的非持久性 10 | 11 | 在 HTTP/1.0 中,默认情况下每个请求/响应对都需要一个新的 TCP 连接。这意味着客户端和服务器之间的每次交互都要经历建立连接的开销,包括三次握手延迟和增加的资源消耗。这种非持久连接导致了所谓的 "C10k 问题"(同时处理成千上万的连接)以及性能问题,尤其是在加载资源多的网页时。 12 | 13 | ### 方法、状态码和头部 14 | 15 | HTTP/1.0 引入了多种请求方法,例如 GET、POST 和 HEAD,使得客户端可以执行不同类型的操作,比如获取资源、提交表单数据等。它还定义了一系列状态码,让服务器能够告知客户端请求是否成功,或者如果失败了是因为什么原因。此外,通过请求和响应头部,客户端和服务器可以交换附加信息,比如内容类型、缓存策略等。 16 | 17 | ### 缺点 18 | 19 | 由于非持久连接和无状态的特性,HTTP/1.0 效率相对较低。每个资源的请求都需要单独的 TCP 连接,增加了传输延迟。此外,HTTP/1.0 不支持像 Host 头这样的现代 HTTP 功能,这意味着每个 IP 地址只能托管一个网站,限制了虚拟主机的使用。 -------------------------------------------------------------------------------- /problems/请你介绍一下http1.1.md: -------------------------------------------------------------------------------- 1 | HTTP/1.1相比其前身HTTP/1.0引入了许多改进和新特性,旨在提高效率、减少延迟,并更有效地利用网络资源。主要特性包括: 2 | 3 | 1. **持久连接**:HTTP/1.1默认启用持久连接(也称为连接复用),这意味着在一个TCP连接上可以传输多个HTTP请求和响应,而不需要为每个请求/响应对打开一个新的连接,从而减少了连接建立的开销和延迟。 4 | 2. **管道化**:HTTP/1.1支持请求的管道化,允许客户端在收到前一个响应之前发送多个请求。这可以进一步减少通信延迟,尽管在实践中由于各种原因(如服务器和代理不完全支持等),这一特性并未广泛使用。 5 | 3. **分块传输编码**:这允许服务器动态生成内容并在完全生成内容之前开始发送响应,从而减少了首字节的延迟时间。 6 | 4. **缓存控制**:HTTP/1.1引入了更加复杂和强大的缓存控制机制,包括通过`Cache-Control`头部字段进行精细控制,这有助于减少客户端重复请求同一资源的情况,降低服务器负载以及减少网络拥塞。 7 | 5. **内容协商**:允许客户端和服务器就响应的最佳格式进行交流。例如,根据Accept语言头部信息选择合适的语言版本的资源。 8 | 6. **错误处理与状态代码扩展**:HTTP/1.1增加了许多新的状态代码,为错误处理提供了更多的上下文信息。 -------------------------------------------------------------------------------- /problems/请你介绍一下http2.0.md: -------------------------------------------------------------------------------- 1 | HTTP/2引入了若干关键改进来优化数据传输,提高效率和速度,其中包括: 2 | 3 | 1. **二进制帧层**:HTTP/2采用二进制格式传输数据,而不是HTTP/1.x的文本格式。这种改变使得请求和响应可以被分割成更小、易于管理的帧,这些帧隶属于一个流,每个流有一个唯一的标识符。二进制协议也更加高效,易于解析和减少错误。 4 | 2. **多路复用(Multiplexing)**:在HTTP/1.x中,每个请求都需要一个单独的TCP连接,或者通过管道化来尝试解决这个问题,但存在队头阻塞问题。HTTP/2允许在单个连接上同时发送多个请求和响应,无需等待前一个传输完成,大大减少了延迟并提高了页面加载速度。 5 | 3. **服务器推送(Server Push)**:HTTP/2允许服务器在客户端需要之前就主动将资源推送给客户端,这样可以进一步减少等待时间,提高性能。例如,当客户端请求一个HTML文件时,服务器可以预测客户端接下来会请求的CSS文件和JavaScript文件,并主动发送这些资源。 6 | 4. **头部压缩**:HTTP/2使用HPACK压缩格式减小了头部大小。由于HTTP/1.x的头部数据每次请求几乎都是重复的,因此这种压缩显著减少了传输的数据量。 7 | 5. **优先级和流控制**:HTTP/2允许设置数据流、消息和帧的优先级,使得管理数据流的顺序和资源分配更加高效。此外,流控制机制防止任何单个数据流消耗所有可用带宽。 -------------------------------------------------------------------------------- /problems/请你介绍一下http3.0.md: -------------------------------------------------------------------------------- 1 | HTTP/3的关键改进包括: 2 | 3 | 1. **减少连接建立时间**:通过将传输层安全性(TLS)握手直接集成到连接建立过程中,HTTP/3可以减少建立新连接所需的往返次数,特别是在已经与服务器有过交互的客户端再次连接时。 4 | 2. **避免队头阻塞**:HTTP/3通过使用QUIC协议,使得即便某一数据包丢失,也不会阻塞其它数据包的处理和传输,从而避免了HTTP/2在TCP基础上存在的队头阻塞问题。 5 | 3. **连接迁移**:QUIC还支持连接ID的概念,这允许即便底层IP地址或端口发生变化(例如用户的移动设备从Wi-Fi切换到4G网络),连接也能够继续保持,这对于移动环境下的性能和稳定性是一个重大改进。 6 | 4. **内置加密**:QUIC内置了加密支持,TLS 1.3是其安全基础,这使得所有基于HTTP/3的通信默认都是加密的,增强了数据传输的安全性。 7 | 5. **流优先级和控制**:与HTTP/2类似,HTTP/3支持流的概念,但通过QUIC提供了更有效的流优先级设置和流量控制机制,允许更精细地管理数据流和资源分配。 -------------------------------------------------------------------------------- /problems/谈谈数据库中索引的理解,索引和主键区别.md: -------------------------------------------------------------------------------- 1 | 数据库索引是一种数据结构,它可以提高数据库查询的速度。就像图书的目录一样,索引能够帮助数据库管理系统快速定位到表中的数据,而不需要扫描整个表。索引通常是基于一个或多个列(字段)构建的,并且能够大幅减少数据查找时间。 2 | 3 | 索引的主要作用有: 4 | 5 | - 提升查询效率:通过索引,可以快速访问到数据库表中的特定信息,无需遍历全表。 6 | - 保持数据排序:某些类型的索引还可以保证数据行的物理顺序与索引顺序相同,如聚簇索引。 7 | - 唯一性检验:通过创建唯一性索引,可以确保某一列或列组合的值是唯一的,避免重复数据。 8 | 9 | 索引通常使用B树、哈希表、R树等数据结构实现,取决于具体的应用场景和DBMS。 10 | 11 | 主键和索引之间有以下区别: 12 | 13 | 1. 主键: 14 | 15 | - 是数据库表中的一个特殊约束,它唯一标识表中的每条记录。 16 | - 一个表中只能有一个主键。 17 | - 主键的值必须是唯一的,不能为NULL。 18 | - 主键本身就是一个索引(通常是聚簇索引),DBMS自动为主键创建索引。 19 | 20 | 1. 索引: 21 | 22 | - 可以是任何普通的数据库表列,它们没有主键的约束条件。 23 | - 一个表中可以有多个索引,用于加速各种查询操作。 24 | - 索引可以是唯一的(保证列值的唯一性),也可以是非唯一的(允许列值重复)。 25 | - 创建索引并不改变数据本身的物理存储,除非它是聚簇索引。 26 | 27 | 总结来说,主键是表中用于唯一标识记录的特殊列,通常会伴随一个自动创建的索引,而索引是为了加速查询而对一个或多个列进行优化的数据结构。主键是表级约束的一部分,而索引是性能优化的工具。 -------------------------------------------------------------------------------- /problems/软中断和硬中断分别指的是什么?.md: -------------------------------------------------------------------------------- 1 | 1. 硬中断:硬中断通常由硬件设备产生,如键盘、鼠标、打印机、磁盘驱动器等外部设备,在完成某项任务或需要CPU注意时会发送硬中断。例如,当我们按下键盘上的一个键时,键盘就会向CPU发送一个硬中断信号,告诉CPU有一个新的字符输入。 2 | 2. 软中断:也被称为程序中断或软件中断,通常由操作系统内的程序产生,来执行一些特殊操作。例如,当一个程序需要进行I/O操作或请求系统服务时,可以通过触发软中断来调用操作系统的内核函数。软中断在内核中等效于一个子程序调用,它被用于处理一些不紧急的任务。 3 | 4 | 无论是硬中断还是软中断,一旦中断信号被触发,CPU都会暂停当前的任务,先去处理中断请求。中断处理完成后,CPU再恢复原先的任务。这样可以保证系统对立即需求的快速响应,提高整体的系统性能。 -------------------------------------------------------------------------------- /problems/进程的状态转换有那些?.md: -------------------------------------------------------------------------------- 1 | ### 1. 创建 2 | 3 | 当进程被创建时,它首先进入新建状态。在这个状态下,操作系统为进程分配了必要的资源(例如内存)和初始化信息。 4 | 5 | ### 2. 就绪 6 | 7 | 在新建状态后,进程转移到就绪状态。在就绪状态下,进程已准备好运行并等待CPU时间片以便执行。它们在就绪队列中排队,等待调度程序将它们分配到处理器上。 8 | 9 | ### 3. 运行 10 | 11 | 当进程获得CPU时间片并开始执行其指令时,它进入运行状态。在这个状态下,进程可以进行计算和执行任务。 12 | 13 | ### 4. 等待 14 | 15 | 如果进程因为某事件(如输入/输出操作或等待其他资源变得可用)而无法继续执行,它会从运行状态转移到等待状态。在等待状态下,进程释放CPU并等待事件完成。即使有空闲的CPU时间片,进程也不能执行,因为它正在等待外部事件。 16 | 17 | ### 5. 完成 18 | 19 | 当进程完成其执行或者被操作系统强制停止时,它进入终止状态。在这个状态下,操作系统会回收分配给进程的所有资源,并清理其在系统中的记录。进程的输出和状态可能会被保存下来供以后使用。 -------------------------------------------------------------------------------- /problems/进程终止的方式有那些?.md: -------------------------------------------------------------------------------- 1 | 进程终止通常有以下几种方式: 2 | 3 | 1. 正常退出(自愿):当进程完成其任务后,它会自动结束并释放其占用的资源。这是最常见的进程结束方式。 4 | 2. 错误退出(自愿):如果进程在执行过程中遇到无法处理的错误情况,比如除零操作、访问非法内存地址等,它可能会选择主动终止。 5 | 3. 致命错误(强制):当进程发生严重错误,如段错误(segmentation fault),或者操作系统检测到一个不能允许进程继续运行的状态(例如保护性错误)时,操作系统将强制结束这个进程。 6 | 4. 被其他进程杀死(强制):在UNIX/Linux系统中,进程可以接收到来自其他进程的信号,其中一些信号可以导致进程结束,如SIGKILL和SIGTERM。管理员或具有足够权限的用户可以使用kill命令发送这样的信号以结束进程。 7 | 5. 父进程终止(强制):在某些系统中,如果父进程结束,那么它的所有子进程也将被终止 -------------------------------------------------------------------------------- /problems/进程间的通信方式有那些?.md: -------------------------------------------------------------------------------- 1 | 常见: 2 | 3 | 1. **管道 (Pipe)**: 4 | - 无名管道(匿名管道):主要用于有亲缘关系的进程之间的通信(例如,父子进程)。数据是单向流动的。 5 | - 命名管道(FIFO):允许无亲缘关系的进程之间通信,它在文件系统中有一个名字。 6 | 2. **信号 (Signal)**:一种用于通知接收进程某个事件已经发生的简单机制。 7 | 3. **消息队列 (Message Queue)**:允许一个或多个进程向另一个进程发送格式化的数据块。数据块在消息队列中按照一定的顺序排列。 8 | 4. **共享内存 (Shared Memory)**:让多个进程共享一个给定的存储区,是最快的IPC方式,因为数据不需要在进程间复制。 9 | 5. **信号量 (Semaphore)**:主要用于同步进程间的操作,而不是传递数据,但通过控制资源的访问,它可以作为通信的一种手段。 10 | 6. **套接字 (Socket)**:提供网络通信的机制,可用于不同机器上的进程间通信,也可以在同一台机器上的进程之间进行通信。 11 | 12 | 拓展: 13 | 14 | 1. **文件系统**:进程可以通过读写文件来交换信息,虽然这不是最高效的IPC方式,但它的使用非常简单且不受平台限制。 15 | 2. **共享文件和映射内存 (Memory-mapped file)**:通过将磁盘上的文件映射到进程的地址空间,多个进程可以访问文件内容,就好像它们在操作共享内存一样。 16 | 3. **远程过程调用 (RPC)**:允许一个程序调用另一台计算机上的程序,就像调用本地程序一样。 17 | 4. **消息传递接口 (MPI)**:常用于高性能计算中,提供了一组标准化的通信协议,用于不同进程(可能在不同的物理机器上)的通信。 18 | 5. **本地过程调用 (LPC)**:在同一台计算机上运行的进程间的通信机制,通常比远程过程调用(RPC)更快。 19 | 6. **Domain Sockets**:类似于网络套接字,但仅限于同一台计算机上的进程间通信,不通过网络层传输数据。 20 | 21 | -------------------------------------------------------------------------------- /problems/迭代器与普通指针有什么区别.md: -------------------------------------------------------------------------------- 1 | 1. 服务对象不同:迭代器主要面对STL容器提供服务,为STL容器提供遍历,删除等功能,而指针主要面向内存底层进行服务。 2 | 2. 安全性:迭代器可以提供更高级的安全性,因为它们可以对越界和空指针进行检查,并且可以被设计为只读或者只写。而普通指针则需要程序员自行管理边界和空指针的情况。 3 | 3. 可移植性:迭代器在不同容器和数据结构之间具有更好的可移植性,因为它们隐藏了底层的实现细节,使得代码更易于重用。普通指针则依赖于具体的数据结构和内存布局。 4 | 4. 功能扩展:标准库定义了多种类型的迭代器,这些迭代器具有不同的特性和功能,如输入迭代器、输出迭代器、前向迭代器、双向迭代器和随机访问迭代器等。这些迭代器提供了更丰富的功能和算法支持。普通指针则只能进行基本的指针运算。 5 | 6 | 总的来说,迭代器提供了更丰富的抽象和功能,使得它们在许多场景下比普通指针更加灵活和安全。 -------------------------------------------------------------------------------- /problems/针对于ipv4地址不够用的情况,我们是如何解决的?.md: -------------------------------------------------------------------------------- 1 | 针对IPv4地址不够用的情况,互联网工程任务组(IETF)提出了IPv6协议作为解决方案。IPv6是IPv4的后继版本,采用128位地址长度,远远超过IPv4的32位地址长度,为互联网提供了更加广阔和充足的地址空间。 2 | 3 | IPv6相较于IPv4有以下优势和特点: 4 | 5 | 1. 更大的地址空间:IPv6采用128位地址长度,可以提供约340亿亿亿亿个地址,远超过IPv4的42亿个地址,几乎可以满足未来无限连接设备的需求。 6 | 2. 更好的安全性:IPv6在设计上考虑了安全性,包括内置IPSec支持、地址隐私功能等,提高了网络通信的安全性。 7 | 3. 简化的头部格式:IPv6简化了数据包头部的格式,减少了路由器在处理数据包时的负担,提高了网络传输效率。 8 | 4. 支持多播和任播:IPv6原生支持多播和任播,能够更好地满足不同应用场景下的需求。 9 | 10 | 目前,IPv6已经逐渐被部署并广泛应用于互联网中,以解决IPv4地址枯竭问题。大多数现代操作系统和网络设备都已经支持IPv6,各大互联网服务提供商也在逐步推动IPv6的部署和普及,以确保互联网能够持续发展并支持更多的设备连接。 -------------------------------------------------------------------------------- /problems/锁:互斥锁,乐观锁,悲观锁.md: -------------------------------------------------------------------------------- 1 | **互斥锁**:互斥锁是一种用于线程同步的工具,能够保证同一时刻只有一个线程可以访问共享资源。如果一个线程已经取得了互斥锁,其他尝试获得该锁的线程将会被阻塞,直到第一个线程释放了该锁。 2 | 3 | **乐观锁**:乐观锁假定冲突发生的几率很小,因此在数据操作前并不会加锁,但会在进行更新等操作时检查是否有其他线程对数据进行了修改。如果有,则操作失败;没有,则操作成功。乐观锁一般适用于读多写少的场景。 4 | 5 | **悲观锁**:悲观锁假设冲突发生的几率很大,所以在每次读写数据前都会先加锁。这种方式虽然保证了数据的一致性,但也可能造成资源的浪费。悲观锁主要通过数据库提供的锁机制实现,例如行级锁、表级锁等。 -------------------------------------------------------------------------------- /problems/长连接和短连接.md: -------------------------------------------------------------------------------- 1 | 在计算机网络和通信中,"长连接"和"短连接"是指客户端和服务器之间的连接持续时间的两种不同模式。 2 | 3 | **短连接**: 4 | 5 | - 每次通信都需要建立一个新的连接。 6 | - 客户端向服务器发送请求后,服务器响应请求,数据发送完成之后,立即断开连接。 7 | - 短连接适用于请求-响应模式,常见于HTTP/1.0协议的交互。 8 | - 短连接由于频繁地进行TCP三次握手和四次挥手过程,对于服务器资源消耗较大,但可以较好地释放资源,适用于轻量级的、偶尔的数据交换。 9 | - 短连接通常用于处理瞬间高并发的场景。 10 | 11 | **长连接**: 12 | 13 | - 一旦建立,连接会保持开放,直到客户端或服务器明确地关闭它。 14 | - 客户端与服务器建立连接后,可以进行多次的数据传输,直到任一方主动关闭连接。 15 | - 长连接减少了因为建立和关闭连接而产生的额外开销,适用于需要频繁交换数据的应用。 16 | - 在HTTP/1.1协议中,默认使用长连接,通过`Connection: keep-alive`头部实现。 17 | - 长连接适用于需要维持持久状态或频繁通信的场景,如数据库连接、文件传输、实时通信等。 18 | - 心跳机制常用于维护长连接,确保连接的活性,并能及时发现异常断开。 19 | 20 | **如何选择**: 21 | 22 | - 应用场景:如果客户端与服务器之间的交云频繁且持续,长连接可以减少因建立和关闭连接而产生的开销。如果交互是偶尔的,可能短连接更合适。 23 | - 资源消耗:长连接可以减少CPU和网络的消耗,但会占用更多的内存资源,因为连接需要保持状态。短连接会增加 -------------------------------------------------------------------------------- /problems/静态分配内存和动态分配内存有什么区别?.md: -------------------------------------------------------------------------------- 1 | 静态内存分配和动态内存分配是两种不同的内存管理方式,它们的主要区别在于分配时间、大小变化和灵活性: 2 | 3 | 1. 静态内存分配:在程序编译阶段,内存就已经被分配好。程序运行时,静态分配的内存大小是固定不变的。比如全局变量,常量,以及函数中声明的局部变量等都是静态分配内存的。优点是管理起来比较简单,缺点是可能会造成内存空间的浪费。 4 | 2. 动态内存分配:在程序运行过程中,根据需要动态地分配内存空间。使用者可以自己控制何时申请内存,何时释放内存,具有很大的灵活性。例如在C语言中,我们可以通过malloc,calloc,realloc等函数进行动态内存分配。优点是可以更有效地利用内存资源,缺点是如果管理不当(例如忘记释放内存),就可能导致内存泄漏问题。 -------------------------------------------------------------------------------- /problems/面向对象的三大特征是什么.md: -------------------------------------------------------------------------------- 1 | 面向对象编程的三大特征是:封装、继承和多态。 2 | 3 | 1. 封装(Encapsulation):封装是把数据(变量)和操作数据的函数结合在一起,形成一个“对象”。这个数据类型的内部实现细节可以被隐藏起来,只暴露必要的接口给外部使用。封装可以提高代码的重用性,改善程序设计的可维护性。 4 | 2. 继承(Inheritance):继承是子类自动共享父类数据结构和方法的机制,这使得子类对象可以达到父类对象的所有属性和行为。子类还可以添加自己的新的属性和行为。这种特性有助于减少代码重复,并且可以提高代码的可维护性和复用性。 5 | 3. 多态(Polymorphism):多态意味着调用哪个对象的哪个方法,取决于运行时该对象所属的类。多态可以提高代码的灵活性和可扩展性。在C++中,多态通常通过虚函数实现。 --------------------------------------------------------------------------------