常见问题:智能指针、多态、虚函数、STL原理、链表、排序、二叉树、设计模式、线程进程、内存
对象所有权
在接触智能指针之前首先要理解对象的所有权是什么,在这之前我们总是用new和delete来进行内存的申请与释放,在这种堆内存分配的方式中,要遵守一个很基本的原则-谁创建谁销毁原则,而对象所有权指的是谁来负责销毁这个对象的关系。根据几种智能指针的用途来分,对象的所有权可以分为独占所有权、分享所有权和弱引用。独占所有权(unique_ptr):独占该对象,不共享,所有权可以转移,但是转移之后,所有权也是独占。比如:Dad拥有Son的独占所有权,那么Son就必须由Dad来delete,独占意味着若有另一个对象Tmp想要拥有Son的话,就必须让Dad放弃对Son的所有权,此所谓独占,亦即不可分享。分享所有权(shared_ptr):与独占所有权正好相反,对某个对象的所有权可以共享。比如:此时A拥有对象C,在没有其他拥有该对象的情况下,对象C的释放由A来负责,如果此时B也想拥有该对象C,则A不需要放弃自己的所有权,而是将所有权分享给B,那么对象C的释放由最后一个拥有它的来负责(若A先销毁则由B负责,否则由A负责)。弱引用(weak_ptr):指的是可以使用该对象,但是没有所有权,由真正拥有其所有权的来负责释放。比如:A对B有弱引用的话,那么A可以使用B,但是A不负责释放B,如果B已经被拥有其所有权的对象(比如C)释放后,那么A还想继续使用B的时候就会获得一个nullptr。
- unique_ptr:使用上限制最多的一种智能指针,被用来取代之前的auto_ptr,一个对象只能被一个unique_ptr所拥有,而不能被共享,如果需要将其所拥有的对象转移给其他unique_ptr,则需要使用move语义
- shared_ptr:与unique_ptr不同是,unique_ptr是独占管理权,而shared_ptr则是共享管理权,即多个shared_ptr可以公用同一块关联对象,其内部采用的是引用计数,在拷贝的时候,引用计数+1,而在某个对象退出作用域或者释放的时候,引用数-1,当引用计数为0的时候,会自动释放其管理的对象。
- weak_ptr:weak_ptr的出现,主要是为了解决shared_ptr的循环引用(循环引用会导致内存无法正常释放,因为引用计数不会为0,以至于对象不能释放,导致内存泄漏),其主要是与shared_ptr一起来使用。和shared_ptr不同的地方在于,其并不会拥有资源,也就是说不能访问对象所提供的的成员函数,不过,可以通过weak_ptr.lock()来产生一个拥有访问权限的shared_ptr。例子:A类中有一个需求,需要存储其他A类对象的信息(比如,一个人的类,他需要有另一个朋友的信息,那么就需要有个指针指向他的朋友),如果使用shared_ptr,那么在销毁时会遇到循环依赖问题,所以我们这里需要用一个不需要拥有所有权的指针来标记该同类对象。weak_ptr不能单独存在,不能通过make_weak创建,一般通过shared_ptr来存在。
// 创建weak_ptr方式,通过shared_ptr来创建 shared_ptr s_p1 = make_shared("C1"); weak_ptr w_p1(s_p1);
智能指针实现原理
智能指针也是一个类,当超出类的作用域时,类会自动调用析构函数,析构函数会自动释放资源。所以智能指针的作用原理就是在函数结束时自动释放内存空间,而不需要手动释放内存空间。C++程序设计中使用堆内存是非常频繁的操作,堆内存的申请和释放都由程序员自己管理。程序员自己管理堆内存可以提高程序的效率,但是整体来说堆内存的管理时麻烦的,C++11中引入了智能指针的概念,方便管理堆内存。使用普通指针容易造成堆内存泄漏(忘记释放),二次释放,程序发生异常时内存泄漏等问题,使用智能指针能更好的的管理堆内存。
- 智能指针是利用了一种叫做RAII(资源获得即初始化)的技术对普通的指针进行封装,这使得智能指针实质是一个对象,行为表现的却像一个指针。
- 智能指针的作用是防止忘记调用delete释放内存和程序异常的进入catch块(捕获异常)忘记释放内存。另外指针的释放时机也是非常有考究的,多次释放同一个指针会造成程序崩溃,这些都可以通过智能指针来解决。
智能指针是一种用于管理动态分配内存的模板类对象。它们通过在其生命周期结束时自动释放内存,帮助避免内存泄漏和悬空指针等问题。智能指针的实现原理主要涉及两个概念:所有权和引用计数。智能指针的实现通常基于对象所有权的概念。所有权可以理解为对于某一块内存的拥有权,一个智能指针对象可以通过获取所有权来管理该内存。当一个智能指针获取了所有权后,其他指针将无法在对该内存进行访问,从而避免了多个指针同时访问同一块内存的问题,所有权确保内存的唯一管理者负责内存的分配和释放,提高内存管理的安全性和可靠性。引用计数是另一个智能指针的核心概念。每个智能指针对象都会记录对应内存的引用计数,即有多少个指针指向该内存。当引用计数变为0时,表示没有任何指针指向该内存,这时智能指针会自动释放内存。
智能指针的实现通常利用了C++的RAII(Resource Acquisition Is Initialization)机制,通过在构造函数中申请内存,在析构函数中释放内存。这样可以确保在智能指针对象的生命周期结束时,内存能够被正确地释放。另外,为了确保异常安全性,智能指针还可以重载拷贝构造函数和赋值运算符,以便正确处理内存的所有权转移。智能指针并不能解决所有的内存管理问题,例如循环引用。在使用智能指针时,仍然需要注意避免循环引用等问题,以免导致内存泄漏。
智能指针里面的计数器何时会改变
引用计数会在以下几种情况下改变:
- 智能指针的构造:当一个智能指针对象被创建时,引用计数会被初始化为这表示该智能指针对象认为自己是唯一拥有所指内存的指针。
- 拷贝构造:当一个智能指针对象被拷贝给另一个智能指针对象时,引用计数会增加。这是为了记录有多少个指针指向同一内存块。
- 赋值运算符:当一个智能指针对象被赋值给另一个智能指针对象时,引用计数会根据情况进行增加或减少,源对象(等号左边对象)的引用计数减1,目标对象(等号右边对象)的引用计数加如果原先的智能指针对象引用计数变为0,则可能会释放相关内容。
- 智能指针的析构:当一个智能指针对象的生命周期结束时,其析构函数会被调用,引用计数会减少。当引用计数减少到0时,表明没有任何智能指针对象指向该内存,内存会被自动释放。
智能指针和管理的对象分别在哪个区
智能指针实际上是一个栈对象,存储在栈区,托管的资源在堆区,利用了栈对象超出生命周期后自动析构的特征来释放被管理对象的内存,所以无需手动delete释放资源。
RAII机制
RAII: Resource Acquisition Is Initialization,资源获取即初始化,将资源的生命周期与一个对象的生命周期绑定,举例来说就是,把一些资源封装在类中,在构造函数中请求资源,在析构函数中释放资源且绝不抛出异常,而一个对象在生命周期结束时会自动调用析构函数,即资源的生命周期和一个对象的生命周期绑定。
volatile 关键字的作用?什么时候需要使用volatile 关键字
volatile关键字告诉编译器其修饰的变量是易变的,它会确保修饰的变量每次读操作都从内存里读取,每次写操作都将值写到内存里。如果一个变量需要被多个线程共享,就需要添加volatile
左值和右值、左值引用和右值引用
左值:在内存中有确定存储地址、有变量名、表达式结束依然存在的值。左值引用:绑定到左值的引用,通过&来获得左值引用。右值:在内存中没有确定存储位置、没有变量名,表达式结束就会销毁的值,比如:字面常量、表达式返回值,传值返回函数的返回值。右值不能出现在赋值符号的左边且不能取地址右值引用:绑定到右值的引用,通过&&来获得右值引用。
new和malloc的区别
特征 new/delete(操作符) malloc/free(库函数) 分配内存的位置 自由存储区 堆 内存分配失败 抛出异常 返回NULL 返回类型安全性 完整类型指针 void* 分配内存的大小 编译器根据类型计算得出 显式指定字节数 处理数组 有处理数组的new版本new[] 需要用户计算书组的大小后进行内存分配 已分配内存的扩张 不支持 使用realloc完成 分配内存时内存不足 无法通过用户代码进行处理 可以指定处理函数(realloc)或重新制定分配器 是否可以重载 可以 不可以 构造函数与析构函数 调用 不调用 网络协议栈
七层模型 五层模型 协议示例 应用层 应用层 应用程序 DNS ftp 表示层 会话层 传输层 传输层 内核程序 TCP UDP 网络层 网络层 IP 数据链路层 数据链路层 IEEE802.3 物理层 物理层 以太网 TCP
- TCP(传输控制协议)是一种面向连接的、可靠的、基于字节流的传输层通信协议,其传输的单位是报文段
特征:
- 面向连接
- 只能点对点通信(一对一)
- 可靠交互
- 全双工通信
- 面向字节流
TCP如何保证可靠传输:
- 确认和超时重传
- 数据合理分片和排序
- 流量控制
- 拥塞控制
- 数据校验
UDP
- UDP(用户数据报协议)是OSI(开放式系统互联)参考模型中的一种无连接的传输层协议,提供面向事务的简单不可靠信息传送服务,其传输的单位是用户数据报
特征:
- 无连接,实时性强
- 尽最大努力交付
- 面向报文
- 没有拥塞控制
- 支持一对一、一对多、多对一、多对多的交互通信
- 首部开销小
TCP与UDP区别
- TCP面向连接,UDP是无连接的
- TCP提供可靠的服务,也就是说,通过TCP连接传送的数据,无差错,不丢失,不重复,且按序到达;UDP尽最大努力交付,即不保证可靠交付
- TCP的逻辑通信信道是全双工的可靠信道;UDP则是不可靠信道
- 每一条TCP连接只能是点到点的;UDP支持一对一、一对多、多对一、多对多的交互通信
- TCP面向字节流(可能出现黏包问题),实际上是TCP把数据看成一连串无结构的字节流;UDP是面向报文的(不会出现黏包问题)
- UDP没有拥塞控制,因此网络出现拥塞不会使源主机的发送速率降低(对实时应用很有用,如IP电话,实时视频会议等)
- TCP首部开销20字节;UDP的首部开销小,只有8个字节
TCP黏包问题
原因TCP是一个基于字节流的传输服务(UDP基于报文的),"流"意味着TCP所传输的数据是没有边界的。所以可能会出现两个数据包黏在一起的情况解决
- 发送定长包。如果每个消息的大小都是一样的,那么在接收对等方只要累计接收数据,知道数据等于一个定长的数值就将它作为一个消息
- 包头加上包体长度。包头是定长的4个字节,说明了包体的长度。接收对等方先接收包头长度,依据包头长度来接收包体
- 在数据包之间设置边界,如添加特殊符号\r\n标记。FTP协议正式这么做的。但问题在于如果数据正文中也含有\r\n,则会误判为消息的边界
- 使用更加复杂的应用层协议
TCP流量控制
概念流量控制就是让发送方的发送速率不要太快,要让接收方来得及接收方法利用可变窗口(滑动窗口)进行流量控制
TCP拥塞控制
概念拥塞控制就是防止过多的数据注入到网络中,这样可以使网络中的路由器或链路不致过载方法
- 慢开始
- 拥塞避免
- 快重传
- 快恢复
TCP三次握手建立连接
[TCP建立连接全过程解释]
- 客户端发送SYN给服务器,说明客户端请求建立连接
- 服务端收到客户端发的SYN,并回复SYN+ACK给客户端(同意建立连接)
- 客户端收到服务端的SYN+ACK后,回复ACK给服务端(表示客户端收到了服务端发的同意报文)
- 服务端收到客户端的ACK,连接已建立,可以数据传输
TCP为什么要进行三次握手
- 因为信道不可靠,而TCP想在不可靠信道上建立可靠传输,那么三次通信是理论上的最小值(而UDP则不需建立可靠传输,因此UDP不需要三次握手)
- 因为双方都需要确认对方收到了自己发送的序列号,确认过程至少要进行三次通信
- 为了防止已失效的连接请求报文段突然又传送到了服务端,因而产生错误
TCP四次挥手释放连接
[TCP释放连接全过程解释]
- 客户端发送FIN给服务器,说明客户端不必发送数据给服务器了(请求释放从客户端到服务器的连接)
- 服务器接收到客户端发的FIN,并回复ACK给客户端(同意释放从客户端到服务器的连接)
- 客户端收到服务端回复的ACK,此时从客户端到服务器的连接已释放(但服务端到客户端的连接还未释放,并且客户端还可以接收数据)
- 服务端继续发送之前没发完的数据给客户端
- 服务端再发送FIN给客户端,说明服务端发送完了数据(请求释放从服务端到客户端的连接,就算没收到客户端的回复,过段时间也会自动释放)
- 客户端收到服务端的FIN+ACK,并回复ACK给服务端(同意释放从服务端到客户端的连接)
- 服务端收到客户端的ACK后,释放从服务端到客户端的连接
TCP为什么要进行四次挥手
[问题一] TCP为什么要进行四次挥手? / 为什么TCP建立连接需要三次,而释放连接则需要四次?[答案一] 因为TCP是全双工模式,客户端请求关闭连接后,客户端向服务端的连接关闭(一二次挥手),服务端继续传输之前没传完的数据给客户端(数据传输),服务端向客户端的连接关闭(三四次挥手)。所以TCP释放连接时服务器的ACK和FIN是分开发送的(中间隔着数据传输),而TCP建立连接时服务器的ACK和SYN是一起发送的(第二次握手),所以TCP建立连接需要三次,而释放连接则需要四次。[问题二] 为什么TCP连接时可以ACK和SYN一起发送,而释放时则ACK和FIN分开发送?(ACK和FIN分开是指第二次和第三次挥手)[答案二] 因为客户端请求释放时,服务器可能还有数据需要传输给客户端,因此服务端要先响应客户端FIN请求(服务端发送ACK),然后数据传输,传输完成后,服务端再提出FIN请求(服务端发送FIN);而连接时则没有中间的数据传输,因此连接时可以ACK和SYN一起发送[问题三] 为什么客户端释放后需要TIME-WAIT等待2MSL(报文最大生存时间 1MSL=2min)?[答案三]
- 为了保证客户端发送的最后一个ACK报文能够到达服务端。若未成功到达,则服务端超时重传FIN+ACK报文段,客户端再重传ACK,并重新计时
- 防止已失效的连接请求报文段出现在本连接中。TIME-WAIT持续2MSL可使本连接持续的时间内所产生的所有报文段都从网络中消失,这样可使下次连接中不会出现旧的连续报文段
socket编程
socket中的read()、write()函数
ssize_t read(int fd, void* buf, size_t count); ssize_t write(int fd, const void * buf, size_t count);
read()
- read函数是负责从文件描述符fd中读取内容
- 当读成功时,read返回实际所读的字节数
- 如果返回的值是0表示已经读到文件的结束了,小于0表示出现了错误
- 如果错误为EINTR说明读是由中断引起的;如果是ECONNREST表示网络连接出了问题
write()
- write函数将buf中的nbytes字节内容写入文件描述符fd
- 成功时返回写的字节数。失败时返回-1,并设置errno变量
- 在网络程序中,当我们向套接字文件描述符写时有两种可能
- write的返回值大于0,表示写了部分或者是全部的数据
- 返回值小于0,此时出现了错误
- 如果错误为EINTR表示在写的时候出现了中断的错误;如果为EPIPE表示网络连接出现了问题(对方已经关闭了连接)
多线程多进程
多进程和多线程间的对比、劣势与选择对比
对比维度 多进程 多线程 总结 数据共享、同步 数据共享复杂,需要用IPC(多进程通信)数据是分开的同步简单 因为共享进程数据,数据共享简单,但也是因为这个原因导致同步复杂 各有优势 内存、CPU 占用内存多、切换复杂、CPU利用率低 占用内存少,切换简单,CPU利用率高 线程占优 创建销毁、切换 创建销毁、切换复杂、速度慢 创建销毁、切换简单、速度很快 线程占优 编程、调试 编程简单、调试简单 编程复杂、调试复杂 进程占优 可靠性 进程间不会互相影响 一个线程挂掉将导致整个进程挂掉(当一个线程向非法地址读取或者写入,无法确认这个操作是否会影响同一进程中的其它线程,所以只能是整个进程一起崩溃) 进程占优 分布式 适合用于多核、多机分布式如果一台机器不够,扩展到多台机器比较简单 适应于多核分布式 进程占优 优劣
优劣 多进程 多线程 优点 编程、调试简单、可靠性高 创建、销毁、切换速度快内存、资源占用小、CPU利用率高 缺点 创建、销毁、切换速度慢内存、资源占用大、CPU利用率低 编程、调试复杂、可靠性差 选择
- 需要频繁创建销毁的优先用线程
- 需要进行大量计算的优先使用线程
- 强相关的处理用线程,弱相关的处理用进程(如消息收发和消息处理就是弱相关,用进程;消息解码和业务处理是强相关,用线程)
- 可能要扩展到多机分布的用进程,多核分布的用线程
为什么要用多线程
- 进程之间切换代价比较高,线程之间切换代价比较小
- 解决CPU和IO速度不匹配的问题,多线程更适合在IO切换频繁的场景
- 充分利用多核CPU资源、提高程序的并发效率
线程池主要优势
线程池的主要优势是它可以重用已经创建的线程,从而减少了线程创建和销毁的开销。这样可以提高任务的执行效率,避免频繁地创建和销毁线程所带来的性能损耗
线程池的应用场景
线程池适用于需要频繁执行任务的场景,比如网络服务器、多媒体应用等。在这些场景下,线程池可以提高程序的性能和响应速度,同时避免线程数量过多导致系统资源的浪费和竞争
线程池的组成
线程池通常由线程容器、任务队列、条件变量和互斥锁等组成。线程容器用于存储线程对象,任务队列用于存储待执行的任务,条件变量和互斥锁用于线程之间的同步和通信
线程池的工作流程
创建线程池:在程序启动时,创建一个线程池对象,并指定线程池中线程的数量添加任务:当有任务需要执行时,将任务添加到任务队列中线程执行任务:线程池中的线程会从任务队列中取出任务并执行,直到任务队列为空等待任务完成:将所有任务都执行完成后,线程池会等待所有线程执行完毕,并关闭线程池
线程与进程的区别
- 进程是资源分配的基本单位;线程是程序执行的基本单位
- 进程拥有自己的资源空间,每启动一个进程,系统就会为它分配地址空间;而线程与资源分配无关,多个线程共享同一进程内的资源,使用相同的地址空间
- 一个进程可以包含若干个线程
如何保证线程安全
- 互斥量(Mutex):通过互斥量锁定代码块,以保证只有一个线程同时访问该代码。
- 条件变量(Condition variable):在互斥量的基础上,当等待执行的线程满足条件时,唤醒执行。
- 原子操作(Atomic operation):使用原子变量来跟踪正在工作的线程数量,确保多个线程可以安全地更新此变量而不会发生数据竞争
- 信号量(Semaphore):通过信号量管理线程的并发访问,保证合理的资源分配。
- 读写锁(Read-write lock):读写锁分为读锁和写锁,读锁允许多个线程同时读,写锁只允许一个线程写。 这些方法可以根据具体的需求选择使用
常见锁机制
-
互斥锁:互斥锁用于控制多个线程对它们之间共享资源互斥访问的一个信号量。为了避免多个线程在某一时刻同时操作一个共享资源。
-
信号量Semaphore:二值信号量:信号量的值只有0和1,这和互斥量很类似,若资源被锁住,信号量的值为0,若资源可用,则信号量的值为1;计数信号量:信号量的值在0到一个大于1的限制值之间,该计数表示可用的资源的个数
-
**条件锁:**当某一个线程因为某个条件未满足时可以使用条件变量使该程序处于阻塞状态,一旦条件满足则以“信号量”的方式唤醒一个因为该条件而被阻塞的线程。
-
**读写锁:**多个线程可以并行读取数据,但只能独占式地写或修改数据
const关键字
作用:
- 修饰变量,说明该变量不可以被改变
- 修饰指针,分为**指针常量(指针类型的常量)和常量指针(指向常量的指针),在指针常量(int * const p)中,指针自身的值是一个常量,不可改变,始终指向同一个地址,但其内容可以修改;在常量指针(const int *p, int const * p)**中,指针指向的内容是不可改变的,但是指针可以指向其他地址。
- 修饰引用,**常量引用(const int &p),**它可以指向一个非常量对象,但是不允许用该引用修改非常量对象的值。并且指向常量对象时,一定要使用常量引用,而不能是一般的引用。不能让一个非常量引用指向一个常量对象。
- 修饰成员函数,说明该成员函数内不能修改成员变量
- 修饰函数,常量函数(int readme(int i) const), 修饰符const要加在函数说明的尾部,只有权读取外部的数据内容,但无权修改他们,也无法对外部数据进行任何写入操作(比如将i赋值给me)
static关键字
作用:
- 修饰普通变量,修改变量的存储区域和生命周期,使变量存储在静态区,在main函数运行前就分配了空间,如果有初始值就用初始值初始化它,如果没有初始值系统用默认值初始化它
- 修饰普通函数,表明函数的作用范围,仅在定义该函数的文件内才能使用,不能被其他文件使用。在多人开发项目时,为了防止与他人命名空间里的函数重名,可以将函数定位为static,这样不会发生冲突
- 修饰成员变量,修饰成员变量使所有的对象只保存一个该变量,而且不需要生成对象就可以访问该成员变量
- 修饰成员函数,修饰成员函数使得不需要生成对象就可以访问该成员函数,但是在static函数内不能访问非静态成员
this指针
- this指针是一个隐含于每一个非静态成员函数中的特殊指针。他指向调用该成员函数的那个对象
- 当对一个对象调用成员函数时,编译程序先将对象的地址赋值给this指针,然后调用成员函数,每次成员函数存取数据成员时,都隐式使用this指针
- 当一个成员函数被调用时,自动向它传递一个隐含的参数,该参数是一个指向这个成员函数所在的对象的指针
- this指针被隐含的声明为:ClassName* const this,这意味着不能给this指针赋值;在ClassName类的const成员函数中,this指针的类型为const ClassName* const,这说明不能对this指针所指向的这种对象进行修改(即不能对这种对象的数据成员进行赋值操作)
- this并不是一个常规变量,而是个右值,所以不能取得this的地址(不能&this)
- 在以下场景中,经常需要显式引用this指针:
- 为实现对象的链式引用
- 为避免对同一对象进行赋值操作
- 在实现一些数据结构时,如list
inline内联函数
内联函数的代码会被复制到每个调用它的地方,而普通函数的代码只会在调用时执行。特征:
- 相当于把内联函数里面的内容写在调用内联函数处
- 相当于不用执行进入函数的步骤,直接执行函数体
- 相当于宏,却比宏多了类型检查,真正具有函数特性
- 编译器一般不内联包含循环、递归、switch等复杂操作的内联函数
- 在类声明中定义的函数,除了虚函数的其他函数都会自动隐式地当成内联函数,即类内定义的函数都是内联函数
- 函数声明在类内,但定义在类外的看是否有inline修饰符,有就是内联函数,否则不是
- 内联函数在程序中调用几次,内联函数的代码就会复制几份在对应的位置上
利与弊:
- 利:避免了指令的来回跳转,加快程序执行速度
- 弊:代码被多次复制,增加了代码量,占用更多的内存空间
递归函数和函数代码量多功能复杂时不能使用内联函数函数本身内容较少,功能简单,被调用频繁的时候使用内联函数
内联函数与宏的区别
宏是由预处理器对宏进行替代(在编译时进行),而内联函数是通过编译器控制来实现的(程序运行时调用)内联函数比宏多了类型检查,更加安全,真正具有函数特性内联函数的调用是传参,宏定义只是简单的文本替换内联函数在运行时可调试,宏定义不可以
虚函数(virtual)
- 虚函数可以是内联函数,内联是可以修饰虚函数的,但是当虚函数表现多态性的时候不能内联
- 内联是在编译期建议编译器内联,而虚函数的多态性在运行期,编译器无法知道运行期调用哪个代码,因此虚函数表现为多态性时(运行期)不可以内联
- inline virtual唯一可以内联的时候是:编译器知道所调用的对象是那个类,这只有在编译器具有实际对象而不是对象的指针或引用时才会发生
虚函数是基类中的同名函数。虚函数的主要目的是实现多态为什么要使用虚函数:
- 多态:虚函数允许我们通过基类指针或引用来调用派生类的实现,从而实现多态。这使得我们可以编写更通用、可扩展的代码
- 可扩展性:通过使用虚函数,我们可以轻松地添加新的派生类,而无需修改现有的基类代码
- 代码重用:虚函数允许派生类重用和扩展基类的功能,而无需完全重写函数
以下是虚函数的简单示例:
#include class Animal{ public: virtual void makeSound(){ std::cout public: void makeSound(){ std::cout Animal* animal = new Dog(); animal-makeSound(); // 输出The dog barks delete animal; return 0; } unionTest() : i(10){}; int i; double d; }; static union{ int i; double d; }; int main(){ unionTest u; union{ int i; double d; }; cout explicit B(int){} explicit operator bool() const{return true;} }; void doB(B b){} int main() { B b1(1); // OK 直接初始化 B b2 = 1; // error 被explicit修饰构造函数的对象不可以复制初始化 B b3{1}; // OK 直接列表初始化 B b4 = {1}; // error 被explicit修饰构造函数的对象不可以复制列表初始化 B b5 = (B)1; // Ok 允许显式转换 doB(1); // error 被explicit修饰构造函数的对象不能从int到B的隐式转换 if(b1); // OK 被explicit修饰转换函数 B::operator bool()的对象可以从B到bool的按语境转换 bool b6(b1); // OK 同上 bool b7 = b1; // error 被explicit修饰转换函数B::operator bool()的对象不可以隐式转换 bool b8 = (bool)(b1) // OK 可以直接进行初始化,显式转换 return 0; } double width; public: double length; friend void printWidth(Box box); void setWidth(double wid); }; double width; public: double length; friend void printWidth(Box box); void setWidth(double wid); }; // 成员函数定义 void Box::setWidth(double wid){ width = wid; } //printWidth()不是任何类的成员函数 void printWidth(Box box){ // 因为printWidth()是Box的友元,他可以直接访问该类的任何成员 cout Box box; // 使用成员函数设置宽度 box.setWidth(10.0); // 使用友元函数输出宽度 printWidth(box); // 输出 Width of box: 10 return 0; } WIN,LOSE,TIE,CANCLE}; int main() { // 带不带enum关键字都可以 Result result; enum Result omit = CANCLE; int count = WIN; // 不能直接用一个整数给枚举值进行运算,需要进行强制类型转化 result = static_cast blue,red }; auto red = 0; // 错误,red已经在范围内被声明过 blue, red }; auto red = false; color a = color:red; private: string name; int id; public: Foo(string s, int i) : name(s), id(i){}; // 初始化列表 }; public: void do(int a); void do(int a, int b); }; //形状类 public: Shape(); virtual double calcArea(){} virtual ~Shape(); // 虚析构函数 }; class Circle : public Shape{ // 圆形类 public: virtual double calcArea(); }; class Rect : public Shape{ // 矩形类 public: virtual double calcArea(); }; int main() { Shape* shape1 = new Circle(4.0); Shape* shape2 = new Rect(5.0, 6.0); shape1-calcArea(); // 调用圆形类里面的方法 shape2-calcArea(); // 调用矩形类里面的方法 delete shape1; // 因为Shape有虚析构函数,所以delete释放内存时,先调用子类析构函数,再调用基类析构函数,防止内存泄漏 shape1 = nullptr; delete shape2; shape2 = nullptr; return 0; } public: virtual void split()=0; virtual ~ISplitter(){} }; class SplitterFactory{ public: virtual ISplitter* CreateSplitter()=0; virtual ~SplitterFactory(){} // 任何一个抽象基类都需要一个virtual的析构函数 }; // 具体类 class BinarySplitter : public ISplitter{ virtual void split(){ cout virtual void split(){ cout virtual void split(){ cout public: virtual ISplitter* CreateSplitter(){ return new BinarySplitter(); } }; class TextSplitterFactory : public SplitterFactory{ public: virtual ISplitter* CreateSplitter(){ return new TextSplitter(); } }; class VideoSplitterFactory : public SplitterFactory{ public: virtual ISplitter* CreateSplitter(){ return new VideoSplitter(); } }; int main() { VideoSplitterFactory* factory = new VideoSplitterFactory(); ISplitter* splitter = // 多态new 传进来的factory是什么类型的 此处就会创建对应的具体类 factory-CreateSplitter(); splitter-split(); // 输出I am VideoSplitter return 0; } public: //稳定 template method void Run(){ Step1(); if(Step2()){ // 支持变化 == 虚函数的多态使用 Step3(); } for(int i = 0; i BuildPart2(); bool flag = this->BuildPart3(); if(flag) this->BuildPart4(); this->BuildPart5(); } virtual ~House(){} protected: virtual void BuildPart1()=0; virtual void BuildPart2()=0; virtual bool BuildPart3()=0; virtual void BuildPart4()=0; virtual void BuildPart5()=0; }; class StoneHouse : public House{ protected: virtual void BuildPart1(){} virtual void BuildPart2(){} virtual bool BuildPart3(){} virtual void BuildPart4(){} virtual void BuildPart5(){} }; int main(){ House * pHouse = new StoneHouse(); pHouse->Init(); return 0; }
要点总结:
- Builder模式主要用于"分步骤构建一个复杂的对象"。在这其中"分步骤"是一个稳定的算法,而复杂对象的各部分则经常变化
- 变化点在哪里,封装哪里,Builder模式主要在于应对"复杂对象各个部分"的频繁需求变动。其缺点在于难以应对"分步骤构建算法"的需求变动
生成器和模板方法模式的区别:
- 生成器模式包括一个Director(指挥者)和一个Builder(生成器)。Director负责指导对象的构建过程,而Builder则负责实际构建对象的细节。
- 模板方法模式通过一个抽象类定义算法的结构,其中包含一个模板方法,该方法定义了算法的步骤和顺序。其中的具体步骤由子类通过重写来实现。
滑动窗口
class Solution { private: class MyQueue { //单调队列(从大到小) public: deque que; // 使用deque来实现单调队列 // 每次弹出的时候,比较当前要弹出的数值是否等于队列出口元素的数值,如果相等则弹出。 // 同时pop之前判断队列当前是否为空。 void pop(int value) { if (!que.empty() && value == que.front()) { que.pop_front(); } } // 如果push的数值大于入口元素的数值,那么就将队列后端的数值弹出,直到push的数值小于等于队列入口元素的数值为止。 // 这样就保持了队列里的数值是单调从大到小的了。 void push(int value) { while (!que.empty() && value > que.back()) { que.pop_back(); } que.push_back(value); } // 查询当前队列里的最大值 直接返回队列前端也就是front就可以了。 int front() { return que.front(); } }; public: vector maxSlidingWindow(vector& nums, int k) { MyQueue que; vector result; for (int i = 0; i
KMP算法
求解Next数组需要理解以下概念:
- 前缀:包含首位字符但不包含末位字符的子串
- 后缀:包含末位字符但不包含首位字符的子串
- next数组的定义:当主串与模式串的某一位字符不匹配时,模式串要 回退的位置
- next[j]:其值 = 第j位字符前面j-1位字符组成的子串的前后缀重合字符数+1
**next数组规律: **
- next[j]的值每次最多增加1
- 模式串的最后一位字符不影响next数组的结果
手算Next数组:(next数组第一位存放字符串长度)
j 1 2 3 4 5 6 7 8 P a b a a b c a c Next[j] 0 1 1 2 2 3 1 2 int GetNext(char ch[], int length, int next[]){ next[1] = 0; int i = 1, j = 0; while(i if(j == 0 || ch[i] == ch[j]) next[++i] = ++j; else j = next[j]; } return next; } next[1] = 0; int i = 1, j = 0; while(i val; } else{ return -1; } } void put(int key, int val){ if(mp.count(key)){ Node* node = mp[key]; //通过hash表获取节点 remove(node); insert(key,val); } else{ if(mp.size() == n){ Node* node = L->next; remove(node); insert(key, val); }else{ insert(key, val); } } } // 同时在链表和哈希表中删除某个key void remove(Node* node){ Node* pre = node->prev; Node* nxt = node->next; pre->next = nxt; nxt->prev = pre; mp.erase(node->key); } // 同时操作链表和哈希表进行插入 void insert(int key, int val){ Node* node = new Node(key,val); Node* pre = R->prev; Node* nxt = R; pre->next = node; node->next = nxt; node->prev = pre; nxt->prev = node; mp[key] = node; } void printmp(){ unordered_map::iterator it; for(it=mp.begin(); it !=mp.end(); it++) { cout first val LRUCache cache(2); cache.put(1,7); cache.put(2,5); cache.printmp(); cout {"tea",5}, {"tree",3}, {"aasd",4}}; vector sorted_pairs.push_back(it); } sort(sorted_pairs.begin(), sorted_pairs.end(), [](const pair return a.second
- UDP(用户数据报协议)是OSI(开放式系统互联)参考模型中的一种无连接的传输层协议,提供面向事务的简单不可靠信息传送服务,其传输的单位是用户数据报