<?xml version="1.0" encoding="UTF-8"?><rss version="2.0" xmlns:content="http://purl.org/rss/1.0/modules/content/"><channel><title>ZEROBOT</title><description>记录 C++、算法、Linux 与生活</description><link>https://www.zerobot.top/</link><language>zh_CN</language><item><title>linux中到五种IO模式</title><link>https://www.zerobot.top/posts/linux-wuzhong-io-moxing/</link><guid isPermaLink="true">https://www.zerobot.top/posts/linux-wuzhong-io-moxing/</guid><pubDate>Wed, 02 Nov 2022 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;IO模型的选择在Linux网络编程中十分重要，在Unix/Linux环境中主要提供了五种不同的IO模型，分别是阻塞式IO、非阻塞式IO、IO多路复用、信号驱动式IO和异步IO。&lt;/p&gt;
&lt;p&gt;通常一个输入操作包含两个不同阶段：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;等待数据准备好&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;从内核向进程复制数据&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;img src=&quot;https://user-images.githubusercontent.com/87457873/127826956-daff612a-711e-4f92-bcb3-89000b17d6a2.png&quot; alt=&quot;image&quot; /&gt;&lt;/p&gt;
&lt;p&gt;例如，对于一个网络套接字上的输入操作，第一步通常涉及到发生系统调用，用户态切换到内核态并等待数据从网络中到达，当所有等待分组到达时，数据被复制到内核中的某个缓冲区。第二步则是将数据从内核缓冲区复制到应用进程缓冲区。&lt;/p&gt;
&lt;blockquote&gt;&lt;/blockquote&gt;
&lt;p&gt;磁盘文件的IO比较特殊，内核采用缓冲区cache加速磁盘IO请求。因而一旦请求的数据到达内核缓冲区cache，对磁盘的write()操作立即返回，而不用等待将数据写入磁盘后再返回（除非在打开文件时指定了O_SYNC标志）。与之相对应的read()操作将数据从内核缓冲区cache移动到用户的缓冲区中，如果请求的数据不在内核缓冲区cache中，内核会让进程休眠，同时执行对磁盘的读操作。所以实际上在磁盘IO中，等待阶段是不存在的，因为磁盘文件并不像网络IO那样，需要等待远程传输数据。&lt;/p&gt;
&lt;h2&gt;阻塞式I/O模型&lt;/h2&gt;
&lt;p&gt;&lt;img src=&quot;https://user-images.githubusercontent.com/87457873/127827045-ba456dab-4e96-4c38-a572-e6337824e476.png&quot; alt=&quot;image&quot; /&gt;&lt;/p&gt;
&lt;p&gt;Linux中，默认情况下所有的socket都是阻塞的。这里有必要辨析以下阻塞和非阻塞这两个概念，这两个概念描述的是用户线程调用内核I/O操作的方式，其中阻塞是指I/O操作需要彻底完成后才返回到用户空间；而非阻塞则是指I/O操作被调用后立即返回给用户一个状态值，不需要等到I/O操作彻底完成。&lt;/p&gt;
&lt;p&gt;除非特别指定，几乎所有的I/O接口都是阻塞型的，即系统调用时不返回调用结果，只有当该系统调用获得结果或者超时出错才返回。这样的机制给网络编程带来了较大的影响，当线程因处理数据而处于阻塞状态时，线程将无法执行任何运算或者相应任何网络请求。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;改进方案&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;在服务器端使用阻塞I/O模型时结合多进程/多线程技术。让每一个连接都拥有独立的进程/线程，任何一个连接的阻塞都不会影响到其他连接。（选择多进程还是多线程并无统一标准，因为进程的开销远大于线程，所以在连接数较大的情况下推荐使用多线程。而进程相较于线程具有更高的安全性，所以如果单个服务执行体需要消耗较多的CPU资源，如需要进行大规模或长时间的数据运算或文件访问推荐使用多进程）。&lt;/p&gt;
&lt;p&gt;当连接数规模继续增大，无论使用多线程还是多进程都会严重占据系统资源，降低系统对外界的响应效率，线程或者进程本身也更容易陷入假死。此时可以采用“线程池”或“连接池”来降低创建和销毁进程/线程的频率，减少系统开销。&lt;/p&gt;
&lt;h2&gt;非阻塞式I/O模型&lt;/h2&gt;
&lt;p&gt;&lt;img src=&quot;https://user-images.githubusercontent.com/87457873/127827107-819785fd-e73d-4a88-a018-80179575931f.png&quot; alt=&quot;image&quot; /&gt;&lt;/p&gt;
&lt;p&gt;进程把一个套接字设置成非阻塞是在通知内核：当请求的I/O操作非得把本进程投入睡眠才能完成时，不要把本进程投入睡眠，而是返回一个错误。 因此如果在打开文件时设定了O_NONBLOCK标志，则会以非阻塞方式打开文件。如果I/O系统调用不能立即完成，则会返回错误而不是阻塞进程。非阻塞式I/O可以实现周期性检查（轮询）某个文件描述符是否可执行I/O操作。比如，设定一个输入文件描述符为非阻塞式的，然后周期性的执行非阻塞式读操作。如果需要同时检测多个文件描述符，则将其都设为非阻塞，然后一次轮询。但是这种轮询的效率不高，在轮询频率不高的情况下，程序响应I/O事件的延迟将难以接受。换句话说，在一个紧凑的循环中做轮询就是在浪费CPU时间，因为多数时间调用会立即出错并返回。&lt;/p&gt;
&lt;blockquote&gt;&lt;/blockquote&gt;
&lt;p&gt;对于不能满足非阻塞式I/O操作，System V会返回EAGAIN错误而源于Berkeley的4.3BSD返回EWOULDBLOCK。如今大多数系统都把这两个错误码定义为相同的值。（可查看&amp;lt;sys/errno.h&amp;gt;）&lt;/p&gt;
&lt;p&gt;如果不希望进程在对文件描述符执行I/O操作时被阻塞，则可以结合使用多进程/多线程技术，创建一个新的进程来执行I/O操作，而父进程则可以去做其他工作，子进程将阻塞到I/O操作完成。当有多个文件描述符进行I/O操作时，就需要创建多个子进程。当子进程收到文件结束符，那么该子进程将会终止，父进程收到SIGCHLD信号，但是当父进程终止，那么父进程应通知子进程停止，为此可以使用一个信号如SUGUSR1，这使得这种方法的开销将十分昂贵且复杂。使用多线程而不是多进程的方式将减少资源的占用，并有效简化程序设计。但线程之间仍然需要处理同步的问题，当面对大量并发客户线程时，但是这也使得程序编写十分复杂。&lt;/p&gt;
&lt;h2&gt;I/O多路复用模型&lt;/h2&gt;
&lt;p&gt;&lt;img src=&quot;https://user-images.githubusercontent.com/87457873/127828345-ca062966-dfd9-4c80-86c0-539e723ddb7f.png&quot; alt=&quot;image&quot; /&gt;&lt;/p&gt;
&lt;p&gt;I/O多路复用（也叫做事件驱动I/O）通过系统调用select()、poll、或者epoll()实现进程同时检查多个文件描述符，以找出其中任何一个是否可执行I/O操作。通过上图可以看出I/O多路复用与阻塞I/O模型差别并不大，事实上还要差一些，因为这里使用了两个系统调用而阻塞I/O只是用了一个系统调用。但是I/O多路复用的优势是其可以同时处理多个连接。因此如果处理的连接数不是特别多的情况下使用I/O多路复用模型的web server不一定比使用多线程技术的阻塞I/O模型好。&lt;/p&gt;
&lt;p&gt;select()和poll()的原理基本相同：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;注册待侦听的fd(这里的fd创建时最好使用非阻塞)&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;每次调用都去检查这些fd的状态，当有一个或者多个fd就绪的时候返回&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;返回结果中包括已就绪和未就绪的fd&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;int select (int maxfdp, fd_set *readfds, fd_set *writefds, fd_set *errorfds, struct timeval *timeout);
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;select&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;maxfdp 指集合中所有文件描述符的范围，即所有文件描述符的最大值+1&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;readfds、writefds、errorfds 指向文件描述符集合的指针，分别检测输入、输出是否就绪和异常情况是否发生&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;timeout 时select()的超时时间，控制着select()的阻塞行为&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote&gt;&lt;/blockquote&gt;
&lt;p&gt;readfds、writefds、errorfds所指结构体都是保存结果的地方，在调用select()之前，这些参数指向的结构体必须初始化以包含我们所感兴趣的文件描述符集合。之后select()会修改这些结构体，当其返回时他们包含的就是处于就绪态的文件描述符集合。&lt;/p&gt;
&lt;p&gt;当timeout设为NULL或者其指向的结构体字段非零时，select()将阻塞到有下列事件发生&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;readfds、writefds、errorfds 中指定的文件描述符中至少有一个成为就绪态（NULL）&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;该调用被信号处理程序中断&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;timeout中指定的时间上限已超时&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;select()的返回值
当select()函数返回-1表示出错，错误码包括EBADF表示存在非法文件描述符，EINTR表示该调用被信号处理程序中断了（select不会自动恢复）。返回0表示超时，此时每个文件描述符集合都会被清空。返回一个正整数表示准备就绪的文件描述符个数，如果同一个文件描述符在返回的描述符集中出现多次，select会将其统计多次。&lt;/p&gt;
&lt;blockquote&gt;&lt;/blockquote&gt;
&lt;p&gt;一个文件描述符是否阻塞并不影响select()是否阻塞，也就是说如果希望读一个非阻塞文件描述符，并且以5s为超时值调用select()，则select()最多阻塞5s。同理若是指定超时值为NULL，则在该描述符就绪或者捕捉到一个信号之前select()会一直阻塞。&lt;/p&gt;
&lt;p&gt;所有关于文件描述符集合的操作都是通过以下四个宏完成，除此之外，常量FD_SETSIZE规定了文件描述符的最大容量。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;#include 

void FD_ZERO(fd_set *fdset); //将fdset所指集合初始化为空
void FD_SET(int fd, fd_set *fdset); //将文件描述符fd添加到由fdset指向的集合中
void FD_CLR(int fd, fd_set *fdset); //将文件描述符fd从fdset所指集合中移出
void FD_ISSET(int fd, fd_set *fdset); //检测fd是否是fdset所指集合成员
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;poll&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;#include 
int poll (struct pollfd fds[], nfds_t nfds, int timeout);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;poll和select的任务很相似，主要区别在于我们如何指定待检查的文件描述符（程序接口不同）。poll不为每个条件构造一个描述符集合，而是构造了一个pollfd结构的数组，每个数组元素指定一个描述符编号以及我们对该描述符感兴趣的条件。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;struct pollfd {
  int fd; //文件描述符
  short events; //等待的事件
  short revents; //实际发生了的事件
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;每个pollfd结构体指定了一个被监视的文件描述符，可以传递多个结构体，指示poll()监视多个文件描述符。每个结构体的events域是监视该文件描述符的事件掩码，由用户来设置这个域的属性。revents域是文件描述符的操作结果事件掩码，内核在调用返回时设置这个域，并且events中请求的任何事件都可能在revents中返回。&lt;/p&gt;
&lt;p&gt;参数timeout的设置与select()中有所不同（poll的timeout参数是一个整型而select是一个结构体）。&lt;/p&gt;
&lt;p&gt;1、当timeout等于-1时，表示无限超时。poll会一直阻塞到fds数组中列出的文件描述符有一个达到就绪态（定义在对应的events字段中）或者捕捉到一个信号
2、当timeout等于0时，poll不会阻塞——只执行一次检查看看哪个文件描述符已经就绪
3、当timeout大于0时，poll至多阻塞timeout毫秒数，无论IO是否准备好，poll都会返回&lt;/p&gt;
&lt;p&gt;poll的返回值
当poll()函数返回-1表示出错，错误码包括EBADF表示存在非法文件描述符，EINTR表示该调用被信号处理程序中断了（poll不会自动恢复）。返回0表示超时。返回一个正整数表示准备就绪的文件描述符个数，与select不同，poll返回的就是就绪文件描述符的个数每个文件描述符只统计一次。&lt;/p&gt;
&lt;h3&gt;select()和poll()的区别&lt;/h3&gt;
&lt;p&gt;Linux实现层面&lt;/p&gt;
&lt;p&gt;select()和poll()都使用了相同的内核轮询（poll）程序集合，与系统调用poll()本身不同，内核的每个poll例程都返回有关单个文件描述符就绪的信息，这个信息以位掩码的形式返回，其值同poll()系统调用返回的revent字段中的比特值相关。poll()系统调用的实现包括为每个文件描述符调用内核poll例程，并将结果信息填入到对应的revents字段中。对于系统调用select()则可以使用一组宏将内核poll例程返回的信息转化为由select()返回的与之对应的事件集合。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;#define POLLIN_SET (POLLIN | POLLRDNORM | POLLRDBAND | POLLHUP | POLLERR) /*读就绪*/
#define POLLOUT_SET (POLLOUT | POLLWRNORM | POLLWRBAND | POLLERR) /*写就绪*/
#define POLLEX_SET (POLLPRI) /*异常*/
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;以上宏定义展现了select()和poll()返回信息间的语义关系，唯一一点不同是如果被检查的文件描述符中有一个关闭了，poll()在revent字段中返回POLLNVAL，而select()返回-1并把错误码置为EBADF。&lt;/p&gt;
&lt;p&gt;API设计层面&lt;/p&gt;
&lt;p&gt;1、select()使用的数据类型fd_set对于被检查的文件描述数量有一个上限（FD_SETSIZE）。相对也较小（1024/2048），如果要修改这个默认值需要重新编译内核。与之相反，poll()没有对于被检查文件描述符的数量限制。
2、由于select()的参数fd_set同时也是保存结果的地方，在select()返回之后会发生变化，所以每当在下一次进入select()之前需要重新初始化fd_set。poll()通过两个独立的字段events和revents将监控的输入输出分开，允许被监控的文件数组被复用而不需要重新初始化。
3、select()提供的超时精度（微妙）比poll()提供的超时精度（毫秒）高。
4、select()的超时参数在返回时也是未定义的，考虑到可移植性，每次在超时之后在下一次进入到select()之前都需要重新设置超时参数。
5、poll()不要求开发者计算最大文件描述符时进行+1操作&lt;/p&gt;
&lt;p&gt;性能层面&lt;/p&gt;
&lt;p&gt;在待检查文件描述符范围较小（最大文件描述符较低），或者有大量文件描述符待检查，但是其分布比较密集时poll()和select()性能相似。
在被检查文件描述符集合很稀疏的情况，poll()要优于select()。&lt;/p&gt;
&lt;h3&gt;select()和poll()的不足&lt;/h3&gt;
&lt;p&gt;1、IO效率随着文件描述符的数量增加而线性下降。每次调用select()或poll()内核都要检查所有的被指定的文件描述符的状态（但是实际上只有部分的文件描述符会是活跃的），当有文件描述符集合增大时，IO的效率也随之下降。
2、当检查大量文件描述符时，用户空间和内核空间消息传递速度较慢。每次调用select()或poll()时，程序都必须传递一个表示所有需要被检查的文件描述符的数据结构到内核，在内核完成检查之后，修个这个数据结构并返回给程序。（此外select()每次调用之前还需要初始化该数据结构）对于poll()调用需要将用户传入的pollfd数组拷贝到内核空间，这是一个O(n)的操作。当事件发生后，poll()将获得的数据传送到用户空间，并执行释放内存和剥离等待队列等工作同样是O(n)的操作。因此随着文件描述符的增加消息传递速度会逐步下降。对于select()来说，传递的数据结构大小固定为FD_SETSIZE，与待检查的文件描述符数量无关。
3、select()或poll()调用完成之后，程序必须检查返回的数据结构中每个元素，已确定那个文件描述符处于就绪态
4、select()对一个进程打开的文件描述符数目有上限值，而且较少（1024/2048）。&lt;/p&gt;
&lt;h3&gt;epoll&lt;/h3&gt;
&lt;p&gt;epoll API是Linux专有的特性，相较于select和poll，epoll更加灵活且没有描述符限制。epoll设计也与select和poll不同，主要包含以下三个接口：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;#include 

int epoll_create(int size); //创建一个epoll句柄
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;参数size指定内核需要监听的文件描述符个数，但该参数与select中的maxfdp不同，并非一个上限（Linux 2.6.8以后该参数被忽略不用）。此外函数返回代表新创建的epoll句柄的文件描述符（在Linux下查看/proc/进程的id/fd/可看到该fd的值），因此当不再使用该文件描述符时应该通过close()关闭，当所用与epoll句柄相关的文件描述符都关闭时，该句柄被销毁并被系统回收其资源。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;#include 

int epoll_ctl(int epfd, int op, int fd, struct epoll_event *ev); //修改兴趣列表（事件注册函数）
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;与select()的在监听事件时告诉内核需要监听的事件类型不同，epoll()需要先注册要监听的事件类型。参数op表示要执行的动作通过三个宏表示：1.EPOLL_CTL_ADD注册新的fd到epfd中；1.EPOLL_CTL_MOD修改已经注册的fd的监听事件；3.EPOLL_CTL_DEL从epfd中删除一个fd。参数fd表示需要监听的fd。最后一个参数ev指向结构体epoll_event则是告诉内核需要监听的事件类型，定义如下：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;struct epoll_event {
  uint32_t events; //epoll events (bit mask)
  epoll_data_t data; //user data variable
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;其中data的类型为：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;typedef union epoll_data {
  void    *ptr; //pointer to user defined data
  int     fd; //file descriptor
  uint_32 u32; //32-bit integer
  uint_64 u64; //64-bit integer
} epoll_data_t;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;其中字段event表示事件掩码指定待监听的文件描述符fd上所感兴趣的事件集合，除了增加了一个前缀E外，这些掩码的名称与poll中对应名称相同（两个例外EPOLLET表示设置为边缘触发、EPOLLONESHOT表示只监听一次）。data字段是一个联合体，当描述符fd就绪后，联合体成员可以用来指定传回给调用进程的信息。data字段是唯一可以获知同这个事件相关的文件描述符的途径，因此调用epoll_ctl()将文件描述符添加到兴趣列表中时，应该要么将ev.data.fd设为文件描述符，要么将ev.data.ptr设为指向包含该文件描述的结构体。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;#include 

int epoll_wait(int epfd, struct epoll_event *evlist, int maxevents, int timeout);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;等待事件的产生，参数evlist所指向的结构体数组中返回就需文件描述的信息，数组evlist的空间由调用者负责申请，所包含的元素个数由参数maxevents指定。&lt;/p&gt;
&lt;h3&gt;epoll的实现原理（以网络socket监听为例）&lt;/h3&gt;
&lt;p&gt;在linux，一切皆文件．所以当调用epoll_create时，内核给这个epoll分配一个文件描述符，但是这个不是普通的文件，而是只服务于epoll．&lt;/p&gt;
&lt;p&gt;所以当内核初始化epoll时，会开辟一块内核高速cache区，用于安置我们监听的socket，这些socket会以红黑树的形式保存在内核的cache里，以支持快速的查找，插入，删除．同时，建立了一个list链表，用于存储准备就绪的事件．所以调用epoll_wait时，在timeout时间内，只是简单的观察这个list链表是否有数据，如果没有，则睡眠至超时时间到返回；如果有数据，则在超时时间到，拷贝至用户态events数组中．&lt;/p&gt;
&lt;p&gt;那么，这个准备就绪list链表是怎么维护的呢？
当我们执行epoll_ctl()时，除了把socket放到epoll文件系统里file对象对应的红黑树上之外，还会给内核中断处理程序注册一个回调函数，告诉内核，如果这个句柄的中断到了，就把它放到准备就绪list链表里。所以，当一个socket上有数据到了，内核在把网卡上的数据copy到内核中后就来把socket插入到准备就绪链表里了。&lt;/p&gt;
&lt;p&gt;epoll支持两种模式LT(水平触发)和ET(边缘触发)，LT模式下，主要缓冲区数据一次没有处理完，那么下次epoll_wait返回时，还会返回这个句柄；而ET模式下，缓冲区数据一次没处理结束，那么下次是不会再通知了，只在第一次返回．所以在ET模式下，一般是通过while循环，一次性读完全部数据．epoll默认使用的是LT．&lt;/p&gt;
&lt;p&gt;这件事怎么做到的呢？当一个socket句柄上有事件时，内核会把该句柄插入上面所说的准备就绪list链表，这时我们调用epoll_wait，会把准备就绪的socket拷贝到用户态内存，然后清空准备就绪list链表，最后，epoll_wait干了件事，就是检查这些socket，如果不是ET模式（就是LT模式的句柄了），并且这些socket上确实有未处理的事件时，又把该句柄放回到刚刚清空的准备就绪链表了。所以，非ET的句柄，只要它上面还有事件，epoll_wait每次都会返回。而ET模式的句柄，除非有新中断到，即使socket上的事件没有处理完，也是不会次次从epoll_wait返回的．&lt;/p&gt;
&lt;p&gt;经常看到比较ET和LT模式到底哪个效率高的问题．有一个回答是说ET模式下减少epoll系统调用．这话没错，也可以理解，但是在ET模式下，为了避免数据饿死问题，用户态必须用一个循环，将所有的数据一次性处理结束．所以在ET模式下下，虽然epoll系统调用减少了，但是用户态的逻辑复杂了，write/read调用增多了．所以这不好判断，要看用户的性能瓶颈在哪．&lt;/p&gt;
&lt;h3&gt;epoll设计的特点&lt;/h3&gt;
&lt;p&gt;1、功能分离
socket低效的原因之一便是将“维护等待队列”和“阻塞进程”两个功能不加分离，每次调用 select 都需要这两步操作，然而大多数应用场景中，需要监视的 socket 相对固定，并不需要每次都修改。epoll 将这两个操作分开，先用epoll_ctl()维护等待队列，再调用 epoll_wait 阻塞进程。显而易见地，效率就能得到提升。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://user-images.githubusercontent.com/87457873/127828408-034ea5dc-b86f-4014-8291-10cc05d025f3.png&quot; alt=&quot;image&quot; /&gt;&lt;/p&gt;
&lt;p&gt;而epoll则是实现了功能分离，通过epoll_create()创建一个 epoll 对象 epfd，再通过epoll_ctl()将需要监视的 socket 添加到 epfd 中，最后调用 epoll_wait() 等待数据使得epoll有了优化的可能。&lt;/p&gt;
&lt;p&gt;2、就绪列表
select 低效的另一个原因在于程序不知道哪些 socket 收到数据，只能一个个遍历。如果内核维护一个“就绪列表”，引用收到数据的 socket，就能避免遍历。如下图所示，计算机共有三个 socket，收到数据的 sock2 和 sock3 被就绪列表 rdlist 所引用。当进程被唤醒后，只要获取 rdlist 的内容，就能够知道哪些 socket 收到数据。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://user-images.githubusercontent.com/87457873/127828469-df0914b1-dff1-4f43-907a-225ac483bb03.png&quot; alt=&quot;image&quot; /&gt;&lt;/p&gt;
&lt;h3&gt;epoll的实现细节&lt;/h3&gt;
&lt;p&gt;epoll主要由两个结构体：eventpoll与epitem。epitem是每一个IO所对应的的事件。比如 epoll_ctl()的EPOLL_CTL_ADD操作的时候，就需要创建一个epitem。eventpoll是每一个epoll所对应的。比如epoll_create就是创建一个eventpoll。如下图所示，eventpoll 包含了 lock、mtx、wq（等待队列）与 rdlist 等成员，其中 rdlist 和 rbr 是我们所关心的。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://user-images.githubusercontent.com/87457873/127828664-517706d2-58b9-4a11-bea7-59b0a2d84234.png&quot; alt=&quot;image&quot; /&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;就绪列表的数据结构（rdlist） 就绪列表引用着就绪的 socket，所以它应能够快速的插入数据。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;程序可能随时调用 epoll_ctl() 添加监视 socket，也可能随时删除。当删除时，若该 socket 已经存放在就绪列表中，它也应该被移除。所以就绪列表应是一种能够快速插入和删除的数据结构。&lt;/p&gt;
&lt;p&gt;双向链表就是这样一种数据结构，epoll 使用双向链表来实现就绪队列。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;索引结构（rbr) 既然 epoll 将“维护监视队列”和“进程阻塞”分离，也意味着需要有个数据结构来保存监视的 socket，至少要方便地添加和移除，还要便于搜索，以避免重复添加。红黑树是一种自平衡二叉查找树，搜索、插入和删除时间复杂度都是O(log(N))，效率较好，epoll 使用了红黑树作为索引结构（对应上图的 rbr）。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;注：因为操作系统要兼顾多种功能，以及由更多需要保存的数据，rdlist 并非直接引用 socket，而是通过 epitem 间接引用，红黑树的节点也是 epitem 对象。同样，文件系统也并非直接引用着 socket。为方便理解，本文中省略了一些间接结构。&lt;/p&gt;
&lt;h3&gt;epoll的优点&lt;/h3&gt;
&lt;p&gt;1、没有最大打开文件描述符限制
epoll支持的最大打开文件数与系统内存相关，可通
过cat /proc/sys/fs/file-max查看具体数目
2、IO效率不随文件描述符数目增加而线性下降
传统的select/poll在拥有较大的一个socket集合时，不过由于网络延迟，任意时间只有部分socket是活跃的，但是select/poll每次调用都会线性扫描全部的集合，导致效率呈线性下降。而epoll通过在内核中实现的根据每个文件描述符上的回调函数callback函数实现了每次只对“活跃的”的socket进行操作，从而使epoll实现了一个伪AIO，使其效率不会随文件描述符的增加而先行下降。
3、使用mmap加速内核与用户空间的消息传递
select、poll和epoll都需要内核把fd消息通知给用户空间，但是epoll采用了内核与用户空间mmap处于同一块内存来实现，具有较高的效率。&lt;/p&gt;
&lt;h2&gt;信号驱动式I/O模型&lt;/h2&gt;
&lt;p&gt;&lt;img src=&quot;https://user-images.githubusercontent.com/87457873/127829018-be125f77-6a05-497b-ab7d-a7d3927a1e33.png&quot; alt=&quot;image&quot; /&gt;&lt;/p&gt;
&lt;p&gt;信号驱动I/O中，当文件描述符上可执行I/O操作时，进程请求内核为自己发送一个信号，之后进程可以执行其他任务直到I/O就绪为止，此时内核会发送信号给进程。建立一个针对套接字的信号驱动式I/O需要进程执行以下三个步骤：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;建立SIGIO信号处理函数 2. 设置该套接字的属主，通常使用fcntl的F_SETOWN命令设置 3. 开启该套接字的信号驱动式I/O，通常通过使用fcnt的F_SETFL命令打开O_ASYNC标志完成&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;使用信号驱动式I/O模型的主要优点是在等待数据到达期间，进程不会被阻塞。&lt;/p&gt;
&lt;h3&gt;信号驱动式I/O的应用&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;对于UDP上的使用比较简单，SIGIO信号只有在数据报到达套接字或者套接字发生异步错误时产生。因此当捕获对于某个UDP套接字的SIGIO信号时，我们调用recvfrom或者读入到达的数据报或者获取发生的异步错误。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;信号驱动式I/O对于TCP套接字几乎无用，主要原因是SIGIO信号产生会过于频繁，并且其出现并没有告知我们发生了什么事件。比如，当一个进程既读又写一个TCP套接字时，当有数据到达或者当前写出的数据得到确认时，SIGIO信号都会产生，而信号处理函数无法区分这两种情况。 &amp;gt; 应该只考虑对监听TCP套接字使用SIGIO，因为对于监听TCP套接字产生SIGIO的唯一条件是某个新连接的完成。&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;实际上I/O多路复用、信号驱动I/O以及epoll都是用来实现同一个目标的技术——同时检查多个文件描述符是否准备好执行I/O操作（准确的说是看I/O系统调用是否可以非阻塞地执行）。文件描述符就绪状态的转化是通过一些I/O事件来触发的，如输入数据到达、套接字连接建立完成或者是之前满载的套接字发送缓冲区在TCP将队列中的数据传送到对端之后有了剩余空间。但是以上这三种技术都不会实际执行I/O操作，只会告诉我们某个文件描述符已经处于就绪状态，此时我们还需要调用其他系统调用来实际完成I/O操作。AIO技术是POSIX异步I/O，其允许进程将I/O操作排列到一个文件中，当操作完成后得到通知，其优点是最初的I/O调用会立刻返回，进程不会一直等待数据传达到内核或者等待操作完成。这使得进程可以同I/O操作一起并行处理其他任务。&lt;/p&gt;
&lt;h2&gt;I/O模型的技术选择&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;select()和poll()可移植性高但是当同时检查大量的文件描述符时性能延展性不佳。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;epoll()关键优势是可以高效的检查大量文件描述符，但是可移植性差属于只能用于Linux的API。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;信号驱动I/O和epoll一样可以高效的检查大量文件描述符，但是epool具有许多信号驱动i/O不具备的优势。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;避免了处理信号的复杂性&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;可以指定想要检查的事件类型（读就绪或者写就绪）&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;可以选择水平触发或者边缘触发的形式来通知进程。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;如要用到信号驱动I/O的优点需要用到不可移植的Linux的专有特性，如此其可移植性也不会优于epoll。&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote&gt;&lt;/blockquote&gt;
&lt;p&gt;通过以上总结可知，select和poll具有良好的可移植性而epoll和信号驱动I/O具有更好的性能，因此通过一个软件抽象层来检查文件描述符事件，从而可移植程序就能在提供epoll API系统上使用epoll而在其他系统使用select或poll了。如Libevent的底层机制能够使用以上四种机制中的任意一种。&lt;/p&gt;
&lt;h2&gt;文件描述符准备就绪的通知方式&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;水平触发通知：如果文件描述符上可以非阻塞地执行I/O系统调用，此时认为其已经就绪。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;边缘触发通知：如果文件描述符自上次状态检查以来有了新的I/O活动（如新的输入），此时需要触发通知。&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;img src=&quot;https://user-images.githubusercontent.com/87457873/127829103-223bdb2c-b788-4ccb-841e-025f4ef73601.png&quot; alt=&quot;image&quot; /&gt;&lt;/p&gt;
&lt;p&gt;通过上表可知，epoll与其他模式的区别在于其同时支持水平触发（默认）与边缘触发。&lt;/p&gt;
&lt;h3&gt;触发方式对程序设计的影响&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;水平触发 当采用水平触发时，我们可以在任意时刻检查文件描述符的就绪状态。当确定其就绪状态后就可以对其进行I/O操作，然后重复检查文件描述符，已确定是否仍然处于就绪状态，此时就可以执行更多的I/O。也就是说，因为水平触发允许我们在任意时刻重复检查I/O状态，也就没有必要每次文件描述符就绪后就尽可能多地执行I/O（尽可能多地读取字节，亦或是不去执行I/O）。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;边缘触发 当采用边缘触发时，只有当I/O事件发生时才会得到通知。在另一个I/O时间到来之前我们不会收到任何新的通知。此外当文件描述符收到I/O事件通知时，我们并不知道要处理多少I/O（有多少数据可读）。因此采用边缘触发通知的程序应该在接收到一个I/O事件通知后，程序在某个时刻（在有些时候我们确定文件描述符是就绪态时，此时不适合大量的I/O操作。因为如果我们仅对一个文件描述符执行大量I/O操作，可能会让其他文件描述符处于饥饿状态）应该在相应的文件描述符上尽可能多地执行I/O（尽可能多地读取字节，与水平触发相反）。但是若是程序如此设计，就可能失去执行I/O的机会。因为知道产生另一个I/O事件为止，在此之前程序都不会在接收到通知了，也就不知道何时执行I/O了。这也将导致数据丢失或者程序中出现阻塞。&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;如果程序采用循环来对文件描述符执行尽可能多的I/O，而文件描述符又被置为可阻塞的，那么最终当没有更多文件可执行时，I/O系统调用就会阻塞。因此，每个被检查的文件描述符通常都应该被设为非阻塞模式。在得到I/O事件通知后会重复执行I/O操作，直到相应的系统调用 (如read()、write()) 以错误码EAGAIN或EWOULDBLOCK的形式失败。&lt;/p&gt;
&lt;h2&gt;异步I/O模型&lt;/h2&gt;
&lt;p&gt;对于I/O操作主要有两种分别是异步I/O和同步I/O，对于同步I/O会导致请求进程阻塞，直到I/O操作完成，即必须等待I/O操作完成以后控制权才返回给用户进程；而异步I/O不会导致请求进程阻塞，即无需等待I/O操作完成就将控制权返回给用户进程。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;异步I/O模型的工作机制 告知内核启动某个操作，并让内核在整个操作（包括将数据从内核复制到进程缓冲区）完成后通知我们。主要方式是调用aio_read函数向内核传递描述符、缓冲区指针、缓冲区大小（与read相同的三个参数）和文件偏移，并告知内核当整个操作完成时如何通知用户进程。该系统调用立即返回，在等待I/O完成期间进程不被阻塞。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;与信号驱动式I/O模型的区别
信号驱动式I/O是由内核告诉我们何时可以启动一个I/O操作，而异步I/O模型则是由内核通知我们I/O操作何时完成。&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
</content:encoded></item><item><title>突然出现</title><link>https://www.zerobot.top/posts/turan-chuxian/</link><guid isPermaLink="true">https://www.zerobot.top/posts/turan-chuxian/</guid><pubDate>Sat, 08 Oct 2022 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;最近几个月一直没有更新，懒是主要因素，还有一个是工作上太忙了，留给自己思考的时间已经少的不能再少了。为什么突然现在想起来要写点什么，可能也是心血来潮。说说最近几个月自己干了些啥事吧。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;   工作上，每天8点半下班可能已经是常态了。不知道什么时候养成了这个习惯（？），感觉已经彻底成为代码工具了，最近工作一直不顺心，之前有个自己负责的项目采用了nginx做正向代理，在实现了数据交互调试完成之后，看起来一切都接近尾声，开始进入测试阶段了。但是由于性能方面之前由于赶进度以及相信nginx的优秀代理能力，自己没有做太多的自测工作。但是自己最后还是不放心，又自测了一遍，发现https代理的性能已经远远低于预期。于是我的噩梦开始了，由于项目考虑到安全性必须使用https道路，我不得不被迫走向一条性能优化的道路，中途看了nginx源码，了解了openssl加密原理，甚至尝试使用了硬件指令来加速加密解密操作。通过在nginx代码中分别计算https不同阶段的耗时想找到使用https耗时的主要来源，最后想从优化ssl握手过程中的加密解密入手，但是最后效果甚微。第一次有一种心累的感觉，看着那晦涩难懂的内核代码，真的是欲哭无泪。后来我就在想，为什么一开始要用nginx来做正向代理呢，我们的目的不就是做一个proxy吗？又没有负载均衡的需求，为什么不直接通过转发来实现呢？不知道，很心累，目前自己的心态就是向内核组的大佬请求支援，反正按照自己的能力应该不能搞出来了，或许这个优化根本不可能实现，哼= =（针对单个客户端相当于毫秒级的优化了）。当然除了这个，还有一些其他工作，就不再详述了，无非就是一些协议的处理，和一些bug的处理。

   生活上，这里具体讲一下自己的刷题，自己刷题大概从一月份已经坚持到现在了，这里给自己一个赞！虽然工作很忙，但是晚上回家还是有点时间刷个一两题的，甚至还走火入魔开始打cf上面的一些比赛了，虽然每次都被暴打，但是感觉很爽（奇怪的描述…)，可以说，刷题成了我放松的一种方式（？），曾经是带有功利性的目的来刷题，现在感觉完全是热爱上了算法了。不过我还是有自知之明的，目前我的水平大概只能够应付中等水平的面试，一些比较高深的算法知识自己还未打算深入，主要是时间的问题。毕竟自己要平衡工作和刷题这两个方面,我的打算是把刷题当成一个长期爱好来做，所以每天有一点小进步我已经很开心了，长时间搞自己身体可能吃不消哈哈。

我也不知道我以后会变成什么样子，但至少我现在是充实的，虽然有时候有一点点累（悲，加油啦！
&lt;/code&gt;&lt;/pre&gt;
</content:encoded></item><item><title>关于c++随机数的一些函数</title><link>https://www.zerobot.top/posts/cpp-suijishu-hanshu/</link><guid isPermaLink="true">https://www.zerobot.top/posts/cpp-suijishu-hanshu/</guid><pubDate>Fri, 10 Jun 2022 00:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;前言&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;     我们都知道由于rand()返回的是伪随机数，可以用rand()%n生成[0,n)范围内的随机数，但是最近发现它的一些缺陷，就是当需要在一个大数范围中生成一个随机数，可能达不到随机的效果，而且也不能在范围内生成浮点数。这是在c++标准库第二版看到的缺陷说明：
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;1.当商是小整数时，许多C++系统环境的伪随机数生成器所产生的余数并不是绝对随机的。例如当n等于2时，rand()%n连续结果就将在0和1之间选择。
2.当n的值非常大是，那么RAND_MAX（rand()返回的最大值）不会被均匀地被n除尽，一些余数出现的频率将会比其他的大。&lt;/p&gt;
&lt;p&gt;例如：假设RAND_MAX是32767（对于任何系统环境，RAND_MAX最小的允许值）且n是20000。这样rand（）将会有两个不同的值能令rand()%n等于10000（即10000和30000），但是rand()仅有一个值能让rand()%n等于15000(就是15000)。因此，简单实现产生10000将会是15000的两倍。&lt;/p&gt;
&lt;p&gt;其实c++11有提供相关的随机数生成函数：&lt;/p&gt;
&lt;h4&gt;随机生成浮点数：&lt;/h4&gt;
&lt;pre&gt;&lt;code&gt;mt19937 gen{random_device{}()};
uniform_real_distribution dis(min,max);
double x=dis(gen);//生成[min,max]之间的浮点数
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;随机生成整数：&lt;/h4&gt;
&lt;pre&gt;&lt;code&gt;mt19937 gen{random_device{}()};
uniform_int_distribution dis(min,max)；
int x=dis(gen); //生成[min,max]之间的非负整数
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;如果需要动态修改生成随机数的范围，可以这样写：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;//1
decltype(dis.param()) new_range (min, max);
dis.param(new_range);

//2
dis(eng, decltype(dis)::param_type(min, max))
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;经验证，两种方法都可以，个人推荐第二种。&lt;/p&gt;
</content:encoded></item><item><title>关于学到的一些新东西</title><link>https://www.zerobot.top/posts/guanyu-xuedao-de-xin-dongxi/</link><guid isPermaLink="true">https://www.zerobot.top/posts/guanyu-xuedao-de-xin-dongxi/</guid><pubDate>Sun, 05 Jun 2022 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;刚刚结束了这次的周赛，最后一题还是没有写出来，但是学到了关于字符串的一些新知识：&lt;/p&gt;
&lt;p&gt;这是原题：&lt;a href=&quot;https://leetcode.cn/problems/design-a-text-editor/&quot;&gt;https://leetcode.cn/problems/design-a-text-editor/&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;现在大概知道怎么做了，通过两个双端队列模拟即可，根本不需要维护光标的位置，这样思考量大大减少了。&lt;/p&gt;
&lt;p&gt;1.c++的string也有insert和erase函数，用法和vector等容器一样，这个我以前居然不知道…&lt;/p&gt;
&lt;p&gt;2.还有一种做法就是利用双向链表模拟，这个我就是这样做的，但是debug把自己绕糊涂了，自己对这个数据结构感觉还是比较生疏，但是学到了几个比较有用的api：next()和prev()还有advance(),    这些都是对迭代器操作的一些函数，具体用法为&lt;/p&gt;
&lt;p&gt;advance()的函数原型&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;templatevoid advance( InputIt&amp;amp; it, Distance n );

templateconstexpr void advance( InputIt&amp;amp; it, Distance n );
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;advance()的含义：
表示将输入迭代器it移动n个单位的步长，其中n可为正数与负数。注意：这里的输入迭代器为双向迭代器，即迭代器即可右移，也可左移。
若移动n个单位的步长后，超出迭代器范围[begin,end)，则此行为未定义。&lt;/p&gt;
&lt;p&gt;advance()函数返回值：
函数返回值为void，也就是将传入的迭代器用引用的方式移动n个单位了。&lt;/p&gt;
&lt;p&gt;next()的函数原型：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;templateForwardIt next(
	ForwardIt it, 
	typename std::iterator_traits::difference_type n = 1 );

templateconstexpr InputIt next(
	InputIt it, 
	typename std::iterator_traits::difference_type n = 1 );	
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;next()的含义：
表示迭代器右移n个单位的步长，即迭代器+n。
若移动n个单位的步长后，超出迭代器范围[begin,end)，则此行为未定义。&lt;/p&gt;
&lt;p&gt;next()函数返回值：
函数的返回值为一个迭代器，也就是传入迭代器右移n个单位后，返回这个移动后的新迭代器，其中n=1，可以省略第二个参数。&lt;/p&gt;
&lt;p&gt;prev()的函数原型：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;templateBidirIt prev(
  BidirIt it, 
  typename std::iterator_traits::difference_type n = 1 );

templateconstexpr BidirIt prev(
  BidirIt it, 
  typename std::iterator_traits::difference_type n = 1 );
prev()的含义：
表示迭代器左移n个单位，即迭代器-n。
若移动n个单位的步长后，超出迭代器范围[begin,end)，则此行为未定义。
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;prev()函数返回值：
函数的返回值为一个迭代器，也就是传入迭代器左移n个单位后，返回这个移动后的新迭代器，其中n=1，可以省略第二个参数。&lt;/p&gt;
&lt;p&gt;3.有关rope的学习&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;#include///头文件
using namespace __gnu_cxx;
rope  x;
int main(){
    x.push_back(x); ///在末尾加x
    x.insert(pos, x); ///在pos位置加入x
    x.erase(pos, x); ///从pos位置删除x个元素
    x.copy(pos, len, x); ///从pos开始len个元素用x代替
    x.replace(pos, x); ///从pos开始全部换为x
    x.substr(pos, x); ///提取pos开始x个元素
    x.at(x)/[x]; ///访问第x个元素
    return 0;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;现在才知道c++ stl中有这么一个东西，它结合了数组和链表的优点，用于这个题非常合适，但是只适用于int和char数组。&lt;/p&gt;
</content:encoded></item><item><title>2022-5-15周赛总结</title><link>https://www.zerobot.top/posts/2022-5-15-zhousai-zongjie/</link><guid isPermaLink="true">https://www.zerobot.top/posts/2022-5-15-zhousai-zongjie/</guid><pubDate>Sun, 15 May 2022 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;这次周赛和双周赛真的是惨不忍睹,真切地感受到自己与别人之间的差距,状态是一方面,感觉自己的思路真的是太局限了,看题解会做但是为什么比赛的时候想不到呢?我觉得有以下原因:&lt;/p&gt;
&lt;p&gt;1.基础不牢固,眼高手低.&lt;/p&gt;
&lt;p&gt;比如换一种形式的滑动窗口就不会做了,想不到这里来.自己平时可能过多追求一些比较难的算法(然而难的算法也没有掌握= =),对于比较基础的反而不会做了,有一种因小失大的感觉.&lt;/p&gt;
&lt;p&gt;2.思维不够开阔.&lt;/p&gt;
&lt;p&gt;比如今天的第三题,想到死估计自己也不会想到按位来计算,可能是最近刷题太多了,把自己思维固化住了,没有往更深层次的地方去思考,不过后面碰到位运算的题目,大概会知道从每个数字的位来思考了.&lt;/p&gt;
&lt;p&gt;3.总结比较少.&lt;/p&gt;
&lt;p&gt;虽然自己每天都在刷题,但是感觉自己越来越像个机器人了,不会做的直接copy代码.会做的做出来了也不思考能不能优化,很少带入自己的思考量进去.&lt;/p&gt;
&lt;p&gt;这两次周赛学到的东西:&lt;/p&gt;
&lt;p&gt;1.位运算的思考量,可以从每位入手.&lt;/p&gt;
&lt;p&gt;2.双周赛的第四题是一个好题目,枚举字母和字母之间的次数最大差值再转化成求最大连续数组和是一种巧妙的解法,在我看来这种思路有一种放缩的思想了,学会转化问题为自己熟悉的问题,可能就是算法的魅力吧.&lt;/p&gt;
&lt;p&gt;以后的计划:&lt;/p&gt;
&lt;p&gt;1.多写总结,刷题可以放慢一点,打好基础.&lt;/p&gt;
&lt;p&gt;2.对于一些常见的算法总结一套自己的写法出来.&lt;/p&gt;
&lt;p&gt;3.不要灰心…&lt;/p&gt;
</content:encoded></item><item><title>刷到的dp问题</title><link>https://www.zerobot.top/posts/shuadao-de-dp-wenti/</link><guid isPermaLink="true">https://www.zerobot.top/posts/shuadao-de-dp-wenti/</guid><pubDate>Mon, 09 May 2022 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;今天刷了几道dp问题,记一下.&lt;/p&gt;
&lt;p&gt;1.&lt;a href=&quot;https://leetcode.cn/problems/minimum-swaps-to-make-sequences-increasing/&quot;&gt;使序列递增的最小交换次数&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;这道题目自己思考是完全不会做的,在看了题解之后才明白,相当于在索引i出分别计算了需要交换和不需要交换的最少交换次数,还需要考虑一点,在索引i和索引i-1处,四个数的大小关系最多只有两种(为什么?因为题目说用例保证了结果是正确的= =)&lt;/p&gt;
&lt;p&gt;代码:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;class Solution {
public:
    int minSwap(vector&amp;amp; nums1, vector&amp;amp; nums2) {
        int m =nums1.size();
        vector change(m,INT_MAX);
        vector unchange(m,INT_MAX);
        change[0]=1;
        unchange[0]=0;
        for(int i=1;inums1[i-1] &amp;amp;&amp;amp; nums2[i]&amp;gt;nums2[i-1])
            {
                change[i]=change[i-1]+1;
                unchange[i]=unchange[i-1];
            }

            if(nums1[i]&amp;gt;nums2[i-1] &amp;amp;&amp;amp; nums2[i] &amp;gt; nums1[i-1])
            {
                change[i]=min(change[i],unchange[i-1]+1);//为什么这里求最小值,因为前一种情况可能也满足
                unchange[i]=min(unchange[i],change[i-1]);
            }
        }

        return min(change.back(),unchange.back());

    }
};
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;2.&lt;a href=&quot;https://leetcode.cn/problems/minimum-number-of-refueling-stops/&quot;&gt;最低加油次数&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;这道题也是按我目前水平永远都想不出来的,但是看完题解之后恍然大悟,首先关于它的dp解法,能想到这种构造方法我之前是从来没有接触过的dp[i]代表加油i次所经过的最大距离= =&lt;/p&gt;
&lt;p&gt;然后递推方程就是每经过一个加油站,说明i可以取得的值加一,然后更新[0,i+1]中的值,每多一个加油站 station[i] = (location, capacity)，如果之前可以通过加 t 次油到达这个加油站，现在就可以加 t+1 次油得到 capcity 的油量。&lt;/p&gt;
&lt;p&gt;举个例子，原本加一次油可以行驶的最远距离为 15，现在位置 10 有一个加油站，有 30 升油量储备，那么显然现在可以加两次油行驶 45 距离。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;class Solution {
public:
    //定义dp[i],为加油i次走的最远距离
    int minRefuelStops(int target, int startFuel, vector&amp;gt;&amp;amp; stations) {
        int m=stations.size();
        long dp[m+1];
        memset(dp,0,sizeof(dp));
        dp[0]=startFuel;

        for(int i=0;i=0;j--)
        {
            if(dp[j]&amp;gt;=stations[i][0])//如果到达不了该站,没必要更新了
            dp[j+1]=max(dp[j+1],dp[j]+(long)stations[i][1]);
        }

        for(int i=0;i= target)
        return i;

        return -1;

    }
};
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;还有优先队列的做法也需要知道,这里需要明白一点,在油耗尽之前,选择加油站的顺序其实对结果没有影响的.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;class Solution {
public:
    int minRefuelStops(int target, int startFuel, vector&amp;gt;&amp;amp; stations) {
       stations.push_back({target,0});//这里把目的也相当于一个油量为0的加油站,简化处理
       priority_queue pq;//默认构造的是最大堆
       int cnt=0;
       int rest=startFuel;
       int last=0;
       for(auto&amp;amp; v:stations)
       {
           rest = rest + last - v[0];
           //当油量小于0时,需要从前面经过的加油站选择油量最大的加上知道剩余油量大于0
           while(!pq.empty() &amp;amp;&amp;amp; rest&lt;/code&gt;&lt;/pre&gt;
</content:encoded></item><item><title>有关滑动窗口</title><link>https://www.zerobot.top/posts/youguan-huadong-chuangkou/</link><guid isPermaLink="true">https://www.zerobot.top/posts/youguan-huadong-chuangkou/</guid><pubDate>Mon, 09 May 2022 00:00:00 GMT</pubDate><content:encoded>&lt;h3&gt;题目链接:https://leetcode-cn.com/problems/subarray-product-less-than-k/&lt;/h3&gt;
&lt;p&gt;这道题其实自己开始能想到用双指针来做,&lt;/p&gt;
&lt;p&gt;但是自己用一个窗口来维护乘积小于k,但是在计算满足条件的个数走了弯路,自己是在刚好不满足小于k的时候来计算cnt的,这时候要考虑是否有多计算的情况,其实在窗口移动的过程中,已经可以计算了.&lt;/p&gt;
&lt;p&gt;自己的代码:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;class Solution {
public:
    int numSubarrayProductLessThanK(vector&amp;amp; nums, int k) {
		int cnt=0;
		int l=0;
		int r=0;
		int lastr=-1;
		int sum=1;
		if(k==0)
		return 0;
		while(rl)
				cnt+=(r-l)*(r-l+1)/2;
				if (lastr&amp;gt;=l)
				cnt-=(lastr-l+2)*(lastr-l+1)/2;	//这里要减去重复的区间
				lastr=r-1;

				while(sum&amp;gt;=k)//这里要更新窗口左端点
				{
					sum/=nums[l];
					l++;
				}

				r++;
			}

		}

		if(r&amp;gt;l)
		cnt+=(r-l)*(r-l+1)/2;
		if (lastr&amp;gt;=l)
		cnt-=(lastr-l+2)*(lastr-l+1)/2;	

		return cnt;

    }
};
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;官方题解:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;class Solution {
public:
    int numSubarrayProductLessThanK(vector&amp;amp; nums, int k) {
        int n = nums.size(), ret = 0;
        int prod = 1, i = 0;
        for (int j = 0; j = k) {
                prod /= nums[i];
                i++;
            }
            ret += j - i + 1;//这里要明白这个等式是怎么来的,相当于固定了右端点.求子集个数.
        }
        return ret;
    }
};
&lt;/code&gt;&lt;/pre&gt;
</content:encoded></item><item><title>有关括号匹配的问题</title><link>https://www.zerobot.top/posts/youguan-kuohao-pipei/</link><guid isPermaLink="true">https://www.zerobot.top/posts/youguan-kuohao-pipei/</guid><pubDate>Mon, 09 May 2022 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;虽然自己一看到括号匹配的问题就想到了栈处理,但是实际上还需要想到动态规划这一思想,别把思维局限在里面了.&lt;/p&gt;
&lt;p&gt;判断一个括号序列是否合法是经典问题。对于一个括号序列，我们从左向右遍历每个字符，同时维护变量 now（初值为 0）。遇到左括号时，now += 1，遇到右括号时，now -= 1。如果过程中 now 始终非负，且最后 now 变成 0 则序列合法&lt;/p&gt;
&lt;p&gt;1.首先看一下这道题:&lt;a href=&quot;https://leetcode-cn.com/problems/longest-valid-parentheses/&quot;&gt;最长有效括号&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;自己当时的解法:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;class Solution {
public:
    int longestValidParentheses(string s) {
       if(s.length()==0)
        return 0;
        vector dp(s.length());//dp[i]代表以字符i结尾的最长有效括号
        dp[0]=0;
        int max_num=0;
        for(int i=1;i=0)
            {
                //这里是考虑以i-dp[i]结尾处是不是也存在有效括号,也要算进去,因为刚好连起来了.
                dp[i]=dp[i-dp[i]]+dp[i];
            }

            max_num=max(dp[i],max_num);

        }

        return max_num;
    }
};
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;2.再看一下今天周赛的题目:&lt;a href=&quot;https://leetcode-cn.com/problems/check-if-there-is-a-valid-parentheses-string-path/&quot;&gt;检查是否有合法字符串路径&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;自己用回溯不出所料的超时了==,以后得少考虑回溯的做法,dfs剪枝这道题也可以做,而且速度更快&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;class Solution {
public:
    bool hasValidPath(vector&amp;gt; &amp;amp;grid) {
        int m = grid.size(), n = grid[0].size();
        if ((m + n) % 2 == 0 || grid[0][0] == &apos;)&apos; || grid[m - 1][n - 1] == &apos;(&apos;) return false; // 剪枝
        bool vis[m][n][(m + n) / 2 + 1]; memset(vis, 0, sizeof(vis));
        function dfs = [&amp;amp;](int x, int y, int c) -&amp;gt; bool {
            if (c &amp;gt; m - x + n - y - 1) return false; // 剪枝：即使后面都是 &apos;)&apos; 也不能将 c 减为 0
            if (x == m - 1 &amp;amp;&amp;amp; y == n - 1) return c == 1; // 终点一定是 &apos;)&apos;
            if (vis[x][y][c]) return false; // 重复访问
            vis[x][y][c] = true;
            c += grid[x][y] == &apos;(&apos; ? 1 : -1;
            return c &amp;gt;= 0 &amp;amp;&amp;amp; (x &amp;gt;&amp;amp; grid) {
        if (grid[0][0] == &apos;)&apos;) return false;

        int n = grid.size(), m = grid[0].size();
        vector&amp;gt;&amp;gt; f;
        for (int i = 0; i &amp;gt;());
            for (int j = 0; j (n + m));
        }
        f[0][0][1] = true;
        for (int i = 0; i = n + m) continue;
                if (i) f[i][j][k] = f[i][j][k] || f[i - 1][j][kk];
                if (j) f[i][j][k] = f[i][j][k] || f[i][j - 1][kk];
            }
        }
        return f[n - 1][m - 1][0];
    }

};
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;bitset优化,bitset可以表示n位的二进制数.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;using bs = bitset;
class Solution {
public:
    bool hasValidPath(vector&amp;gt;&amp;amp; grid) {
        if (grid[0][0] == &apos;)&apos;) return false;
        int n = size(grid), m = size(grid[0]);
        vector&amp;gt; f(n + 1, vector(m + 1)); // f[i,j,k]=1 表示到达点(i,j)的时候括号状态可以是 k
        f[0][1].set(0); // 设个初值，保证转移到 f[1,1] 的时候 f[1,1,1]=true
        for (int i = 1; i &amp;gt; 1) | (f[i][j - 1] &amp;gt;&amp;gt; 1);
            }
        }
        return f[n][m].test(0);
    }
};
&lt;/code&gt;&lt;/pre&gt;
</content:encoded></item><item><title>图有关算法</title><link>https://www.zerobot.top/posts/tu-youguan-suanfa/</link><guid isPermaLink="true">https://www.zerobot.top/posts/tu-youguan-suanfa/</guid><pubDate>Tue, 25 Jan 2022 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;这里搬运acwing的模板，感觉还不错~&lt;/p&gt;
&lt;p&gt;树与图的存储
树是一种特殊的图，与图的存储方式相同。
对于无向图中的边ab，存储两条有向边a-&amp;gt;b, b-&amp;gt;a。
因此我们可以只考虑有向图的存储。&lt;/p&gt;
&lt;p&gt;(1) 邻接矩阵：g[ a ] [ b ]存储边a-&amp;gt;b&lt;/p&gt;
&lt;p&gt;(2) 邻接表：&lt;/p&gt;
&lt;p&gt;// 对于每个点k，开一个单链表，存储k所有可以走到的点。h[k]存储这个单链表的头结点&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;int h[N], e[N], ne[N], idx;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;// 添加一条边a-&amp;gt;b&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;void add(int a, int b)
{
    e[idx] = b, ne[idx] = h[a], h[a] = idx ++ ;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;// 初始化&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;idx = 0;
memset(h, -1, sizeof h);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;树与图的遍历
时间复杂度 O(n+m)O(n+m), nn 表示点数，mm 表示边数
(1) 深度优先遍历 —— 模板题 AcWing 846. 树的重心&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;int dfs(int u)
{
    st[u] = true; // st[u] 表示点u已经被遍历过

    for (int i = h[u]; i != -1; i = ne[i])
    {
        int j = e[i];
        if (!st[j]) dfs(j);
    }

}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;(2) 宽度优先遍历 —— 模板题 AcWing 847. 图中点的层次&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;queue q;
st[1] = true; // 表示1号点已经被遍历过
q.push(1);

while (q.size())
{
    int t = q.front();
    q.pop();

    for (int i = h[t]; i != -1; i = ne[i])
    {
        int j = e[i];
        if (!st[j])
        {
            st[j] = true; // 表示点j已经被遍历过
            q.push(j);
        }
    }

}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;拓扑排序 —— 模板题 AcWing 848. 有向图的拓扑序列
时间复杂度 O(n+m)O(n+m), nn 表示点数，mm 表示边数&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;bool topsort()
{
    int hh = 0, tt = -1;

    // d[i] 存储点i的入度
    for (int i = 1; i  dist[j]))
                t = j;

        // 用t更新其他点的距离
        for (int j = 1; j  PII;

int n;      // 点的数量
int h[N], w[N], e[N], ne[N], idx;       // 邻接表存储所有边
int dist[N];        // 存储所有点到1号点的距离
bool st[N];     // 存储每个点的最短距离是否已确定
int dijkstra()
{
    memset(dist, 0x3f, sizeof dist);
    dist[1] = 0;
    priority_queue, greater&amp;gt; heap;
    heap.push({0, 1});      // first存储距离，second存储节点编号

    while (heap.size())
    {
        auto t = heap.top();
        heap.pop();

        int ver = t.second, distance = t.first;

        if (st[ver]) continue;
        st[ver] = true;

        for (int i = h[ver]; i != -1; i = ne[i])
        {
            int j = e[i];
            if (dist[j] &amp;gt; distance + w[i])
            {
                dist[j] = distance + w[i];
                heap.push({dist[j], j});
            }
        }
    }

    if (dist[n] == 0x3f3f3f3f) return -1;
    return dist[n];

}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Bellman-Ford算法 —— 模板题 AcWing 853. 有边数限制的最短路
时间复杂度 O(nm)O(nm), nn 表示点数，mm 表示边数
注意在模板题中需要对下面的模板稍作修改，加上备份数组，详情见模板题。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;int n, m;       // n表示点数，m表示边数
int dist[N];        // dist[x]存储1到x的最短路距离

struct Edge     // 边，a表示出点，b表示入点，w表示边的权重
{
    int a, b, w;
}edges[M];
// 求1到n的最短路距离，如果无法从1走到n，则返回-1。
int bellman_ford()
{
    memset(dist, 0x3f, sizeof dist);
    dist[1] = 0;

    // 如果第n次迭代仍然会松弛三角不等式，就说明存在一条长度是n+1的最短路径，由抽屉原理，路径中至少存在两个相同的点，说明图中存在负权回路。
    for (int i = 0; i  dist[a] + w)
                dist[b] = dist[a] + w;
        }
    }

    if (dist[n] &amp;gt; 0x3f3f3f3f / 2) return -1;
    return dist[n];

}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;spfa 算法（队列优化的Bellman-Ford算法） —— 模板题 AcWing 851. spfa求最短路
时间复杂度 平均情况下 O(m)O(m)，最坏情况下 O(nm)O(nm), nn 表示点数，mm 表示边数&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;int n;      // 总点数
int h[N], w[N], e[N], ne[N], idx;       // 邻接表存储所有边
int dist[N];        // 存储每个点到1号点的最短距离
bool st[N];     // 存储每个点是否在队列中
// 求1号点到n号点的最短路距离，如果从1号点无法走到n号点则返回-1
int spfa()
{
    memset(dist, 0x3f, sizeof dist);
    dist[1] = 0;

    queue q;
    q.push(1);
    st[1] = true;

    while (q.size())
    {
        auto t = q.front();
        q.pop();

        st[t] = false;

        for (int i = h[t]; i != -1; i = ne[i])
        {
            int j = e[i];
            if (dist[j] &amp;gt; dist[t] + w[i])
            {
                dist[j] = dist[t] + w[i];
                if (!st[j])     // 如果队列中已存在j，则不需要将j重复插入
                {
                    q.push(j);
                    st[j] = true;
                }
            }
        }
    }

    if (dist[n] == 0x3f3f3f3f) return -1;
    return dist[n];

}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;spfa判断图中是否存在负环 —— 模板题 AcWing 852. spfa判断负环
时间复杂度是 O(nm)O(nm), nn 表示点数，mm 表示边数&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;int n;      // 总点数
int h[N], w[N], e[N], ne[N], idx;       // 邻接表存储所有边
int dist[N], cnt[N];        // dist[x]存储1号点到x的最短距离，cnt[x]存储1到x的最短路中经过的点数
bool st[N];     // 存储每个点是否在队列中
// 如果存在负环，则返回true，否则返回false。
bool spfa()
{
    // 不需要初始化dist数组
    // 原理：如果某条最短路径上有n个点（除了自己），那么加上自己之后一共有n+1个点，由抽屉原理一定有两个点相同，所以存在环。

    queue q;
    for (int i = 1; i  dist[t] + w[i])
            {
                dist[j] = dist[t] + w[i];
                cnt[j] = cnt[t] + 1;
                if (cnt[j] &amp;gt;= n) return true;       // 如果从1号点到x的最短路中包含至少n个点（不包括自己），则说明存在环
                if (!st[j])
                {
                    q.push(j);
                    st[j] = true;
                }
            }
        }
    }

    return false;

}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;floyd算法 —— 模板题 AcWing 854. Floyd求最短路
时间复杂度是 O(n3)O(n3), nn 表示点数
初始化：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;for (int i = 1; i  dist[j]))
                t = j;

        if (i &amp;amp;&amp;amp; dist[t] == INF) return INF;

        if (i) res += dist[t];
        st[t] = true;

        for (int j = 1; j&lt;/code&gt;&lt;/pre&gt;
</content:encoded></item><item><title>关于C函数的实现</title><link>https://www.zerobot.top/posts/guanyu-c-hanshu-shixian/</link><guid isPermaLink="true">https://www.zerobot.top/posts/guanyu-c-hanshu-shixian/</guid><pubDate>Sun, 23 Jan 2022 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;一些关于c函数的实现，后面慢慢补…&lt;/p&gt;
&lt;p&gt;1.memcpy函数&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;void memcpy(void*psrc,void*pdst,size_t length)
{
    if(psrc == NULL || pdst == NULL)
        return;

    char*d = NULL;
    char*s = NULL;

    if(pdst &amp;gt; psrc + length || pdst&lt;/code&gt;&lt;/pre&gt;
</content:encoded></item><item><title>c++STL的二分查找</title><link>https://www.zerobot.top/posts/cpp-stl-erfen-chazhao/</link><guid isPermaLink="true">https://www.zerobot.top/posts/cpp-stl-erfen-chazhao/</guid><pubDate>Sun, 23 Jan 2022 00:00:00 GMT</pubDate><content:encoded>&lt;h1&gt;C++ STL中的Binary search（二分查找）&lt;/h1&gt;
&lt;h2&gt;一.解释&lt;/h2&gt;
&lt;p&gt;以前遇到二分的题目都是手动实现二分，不得不说错误比较多，关于返回值，关于区间的左闭右开等很容易出错，最近做题发现直接使用STL中的二分函数方便快捷还不会出错，不过对于没有接触过的同学，二分函数确实是一个头疼的部分，自己查的内容又有点乱，找不到具体的使用方法，有必要自己总结一份完整的以后备用。&lt;/p&gt;
&lt;h2&gt;二.常用操作&lt;/h2&gt;
&lt;h3&gt;1.头文件&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;#include 
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;2.使用方法&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;a.binary_search：查找某个元素是否出现。&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;a.函数模板：binary_search(arr[],arr[]+size , indx)&lt;/p&gt;
&lt;p&gt;b.参数说明：
arr[]： 数组首地址
size：数组元素个数
indx:需要查找的值&lt;/p&gt;
&lt;p&gt;c.函数功能： 在数组中以二分法检索的方式查找，若在数组(要求数组元素非递减)中查找到indx元素则真，若查找不到则返回值为假。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;2.lower_bound：查找第一个大于或等于某个元素的位置。&lt;/strong&gt;
a.函数模板：lower_bound(arr[],arr[]+size , indx):
b.参数说明：
arr[]： 数组首地址
size：数组元素个数
indx:需要查找的值
c.函数功能： 函数lower_bound()在first和last中的前闭后开区间进行二分查找，返回大于或等于val的第一个元素位置(注意是地址)。如果所有元素都小于val，则返回last的位置
d.举例如下：
　　一个数组number序列为：4,10,11,30,69,70,96,100.设要插入数字3,9,111.pos为要插入的位置的下标，则
　　/&lt;em&gt;注意因为返回值是一个指针，所以减去数组的指针就是int变量了&lt;/em&gt;/
　　pos = lower_bound( number, number + 8, 3) - number，pos = 0.即number数组的下标为0的位置。
　　pos = lower_bound( number, number + 8, 9) - number， pos = 1，即number数组的下标为1的位置（即10所在的位置）。
　　pos = lower_bound( number, number + 8, 111) - number， pos = 8，即number数组的下标为8的位置（但下标上限为7，所以返回最后一个元素的下一个元素）。
e.注意：函数lower_bound()在first和last中的前闭后开区间进行二分查找，返回大于或等于val的第一个元素位置。&lt;strong&gt;如果所有元素都小于val，则返回last的位置，且last的位置是越界的！&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;返回查找元素的第一个可安插位置，也就是“元素值&amp;gt;=查找值”的第一个元素的位置&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;3.upper_bound：查找第一个大于某个元素的位置。&lt;/strong&gt;
a.函数模板：upper_bound(arr[],arr[]+size , indx):
b.参数说明：
arr[]： 数组首地址
size：数组元素个数
indx:需要查找的值
c.函数功能：函数upper_bound()返回的在前闭后开区间查找的关键字的上界，返回大于val的第一个元素位置
　　例如：一个数组number序列1,2,2,4.upper_bound(2)后，返回的位置是3（下标）也就是4所在的位置,同样，&lt;strong&gt;如果插入元素大于数组中全部元素，返回的是last。(注意：数组下标越界)&lt;/strong&gt;
　　返回查找元素的最后一个可安插位置，也就是“元素值&amp;gt;查找值”的第一个元素的位置 。&lt;/p&gt;
&lt;h2&gt;三、代码&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;#include
#include
using namespace std;
int main()
{
    int a[100]= {4,10,11,30,69,70,96,100};
    int b=binary_search(a,a+9,4);//查找成功，返回1
    cout
#include 
using namespace std;
int a[100]= {4,10,11,30,69,70,96,100};
int binarySearch(int x,int n)
{
    int left =0;
    int right=n-1;
    while(lefta[mid])
        {
            left=mid+1;
        }
        else
        {
            right =mid-1;
        }
    }
    return -1;//未找到x
}
//二分搜索递归实现
int recurisonBinarySearch(int left,int right,int x)
{
    if(left&amp;gt;right)
    {
        return -1;
    }
    int mid =(left+right)/2;
    if(x==a[mid])
    {
        return mid;
    }
    if(x&amp;gt;a[mid])
    {
        return recurisonBinarySearch(mid+1,right,x);
    }
    else
    {
        return recurisonBinarySearch(left,mid-1,x);
    }
}
int main()
{
    int x;
    int ans1,ans2;
    scanf(&quot;%d&quot;,&amp;amp;x);
    ans1=binarySearch(x,8);
    ans2=recurisonBinarySearch(0,7,x);
    printf(&quot;%d %d\n&quot;,ans1,ans2);
    return 0;
}
&lt;/code&gt;&lt;/pre&gt;
</content:encoded></item><item><title>套接字编程---1</title><link>https://www.zerobot.top/posts/taojiezi-biancheng-1/</link><guid isPermaLink="true">https://www.zerobot.top/posts/taojiezi-biancheng-1/</guid><pubDate>Sun, 31 Oct 2021 00:00:00 GMT</pubDate><content:encoded>&lt;h3&gt;前言&lt;/h3&gt;
&lt;p&gt;认识套接字编程从套接字地址开始熟悉，这些结构可以在两个方向上传递：从进程到内核和从内核到进程。&lt;/p&gt;
&lt;h3&gt;一.套接字地址结构&lt;/h3&gt;
&lt;h4&gt;1.IPv4套接字地址结构&lt;/h4&gt;
&lt;p&gt;IPv4套接字地址结构通常也称为“网际套接字地址结构”，它以sockaddr_in命名，定义在&amp;lt;netinet/in.h&amp;gt;头文件当中，下面给出它的POSIX（可移植操作系统）定义。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;struct in_addr{
    in_addr_t s_addr;   //32-bit ipv4 address 网络字节序

}

struct sockaddr_in{
    uint8_t  sin_len; //length of structure
    sa_family_t sin_family; //AF_INFT
    in_port_t sin_port;  //16-bit TCP or UDP port number
                         //网络字节序
    struct in_addr   sin_addr;  //32-bit IPv4 address 网络字节序
    char             sin_zero[8];   //unused
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;几点说明&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;长度字段sin_len是为了增加对OSI协议的支持而随4.3BSD-Reno添加的。在此之前，第一个成员是sin_family,它是一个无符号整数。并不是所有的厂家都支持套接字地址结构的长度字段，而且POSIX规范也不要求有这个成员。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;POSIX规范只需要结构中有这三个字段：sin_family,sin_addr,sin_port。对于符合POSIX的实现来说，定义额外的结构字段是可以接受的，这对于网际套接字地址结构也是正常的、几乎所有的实现都增加了sin_zero字段，所以所有的套接字地址结构大小至少都是16字节。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;IPv4地址和TCP、UDP端口号在套接字地址结构总是以网络字节序存储。在使用这些字段时，我们必须牢记这一点。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;sin_zero字段未曾使用，不过在填写这种套接字地址结构时，我们总是把该字段置为0.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;套接字地址仅在本主机中使用：虽然结构中的某些字段结构总是用在不同主机的通信当中，但是结构把本身并不在主机之间传递.&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;2.通用套接字地址结构&lt;/h4&gt;
&lt;pre&gt;&lt;code&gt;     当作为一个参数传递进任何套接字函数时,套接字地址总是以引用形式(也就是以指向该指结构的指针)传递.然而以这样的指针作为参数之一的任何套接字函数必须处理来自所有支持的任何协议族的套接字地址结构.

      在如何声明所传递的数据类型上存在一个问题.有了ANSI C后解决问题很简单:void*指针.然而套接字函数是在ANSI C之前定义的,在1982年采取的办法是在&amp;lt;sys/socket.h&amp;gt;头文件中定义一个通用的套接字地址结构:
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;struct sockaddr{
     uint_8  sa_len;
     sa_family_t sa_family;  //address family:AF_XXX value
     char sa_data[14];  //protocol-specific address
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;比如bind函数原型定义:&lt;/p&gt;
&lt;p&gt;&lt;code&gt;int bind(int, struct sockaddr*,socklen_t)&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;这就要求我们对这些函数的任何调用都必须要将指向特定于协议的套接字地址结构地方指针进行强制转换,变成指向某个通用套接字地址结构的指针,比如:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;struct sockaddr_in serv; //ipv4 socket address structure

/*fill in int serv()*/
bind(sockfd,(struct socketaddr *)&amp;amp;serv,sizeof(serv));
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;从应用程序的开发人员角度来看,这些通用套接字的地址结构&lt;strong&gt;唯一用途就是用来强制转换&lt;/strong&gt;&lt;/p&gt;
&lt;h4&gt;3.IPv6套接字地址结构&lt;/h4&gt;
&lt;pre&gt;&lt;code&gt;struct in6_addr{
    unit8_t　　sa_addr[16];

};

#define　　SIN6_LEN

struct sockaddr_in6{
    unit8_t　　sin6_len;

    sa_family_t 　　sin6_family;

    in_port_t 　　port;

    unit32_t 　　sin6_flowinfo;

    struct in6_addr 　　sin6_addr;

    unit32-t 　　sin6_scope_id;

};
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;IPv6的地址族为AF_INET6,而IPv4的地址族为AF_INET.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;二.值-结果参数&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;  当往一个套接字函数传递一个套接字地址结构时,该结构总是以引用形式来传递,也就是说传递的是指向该结构的一个指针.该结构的长度也作为参数来传递,不过其传递方式取决于该结构的传递方向:是从进程到内核,还是从内核到进程.
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;(1)从进程到内核传递套接字地址结构的函数有3个:bind,connect,sendto.这些函数的一个参数是指向某个套接字地址结构的指针,另一个参数是该结构的整数大小,例如:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;struct sockaddr_in serv;

connect(sockfd,(SA*)&amp;amp;serv,sizeof(serv));
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;既然指针和指针大小都传给了内核,于是内核知道到底需要从进程中复制多少数据进来.后面我们可以知道,套接字地址大小的数据类型其实是socklen_t,而不是int,不过POSIX规范建议将socklen_t定义为int.&lt;/p&gt;
&lt;p&gt;(2)从内核到进程的传递套接字地址结构的函数有4个:accept,recvfrom,getsockname和getpeername.这四个函数的其中两个参数是指向某个套接字地址结构的指针和指向表示该结构大小的整数变量的指针.例如:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;struct sockaddr_in cli;
socklen_t len;

len = sizeof(cli);
getpeername(fd,(SA*)&amp;amp;cli,&amp;amp;len);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;其中把套接字地址结构大小这个参数从一个整数改为指向某个整数的指针的原因在于:当函数被调用时,结构大小是一个值,它会告诉内核的大小,这样内核在写该结构的时候就不会越界;当函数返回时,结构大小又是另外一个值,它告诉进程内核在该结构中究竟储存了多少信息.这种类型的参数称为值-类型(value-result)参数.&lt;/p&gt;
&lt;p&gt;当使用值-结果作为套接字地址结构的参数时,如果套接字地址结构是固定长度的话,那么从内核返回的值总是固定长度,例如IPv4的sockaddr_in长度为16,IPv6的sockaddr_in6的长度为28.然而对于可变长度的地址结构(比如Unix域的sockaddr_un)返回值可能小于该结构的最大长度.&lt;/p&gt;
&lt;p&gt;后面还会遇到其他使用值-结果参数的函数&lt;/p&gt;
&lt;p&gt;后面总结…&lt;/p&gt;
</content:encoded></item><item><title>c++---有关string用法总结</title><link>https://www.zerobot.top/posts/cpp-string-yongfa/</link><guid isPermaLink="true">https://www.zerobot.top/posts/cpp-string-yongfa/</guid><pubDate>Sat, 30 Oct 2021 00:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;C++的string标准库&lt;/h2&gt;
&lt;p&gt;string是C++标准库的重要部分，主要用于字符串处理。使用string库需要在同文件中包括该库 &lt;code&gt;#include&lt;/code&gt;&lt;/p&gt;
&lt;h3&gt;声明&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;string s;
string ss[10];
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;初始化&lt;/h3&gt;
&lt;p&gt;使用等号的初始化叫做拷贝初始化，不使用等号的初始化叫直接初始化。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;#include
#include

using namespace std;

int main(){
    string s;               //  默认初始化，一个空白的字符串
    string s1(&quot;ssss&quot;);      // s1是字面值&quot;ssss&quot;的副本
    string s2(s1);          // s2是s1的副本
    string s3 = s2;         // s3是s2的副本
    string s4(10, &apos;4&apos;);     // s4初始化
    string s5 = &quot;Andre&quot;;    // 拷贝初始化
    string s6 = string(10, &apos;c&apos;);    // 可拷贝初始化，生成一个初始化好的对象，拷贝给s6

    char cs[] = &quot;12345&quot;;
    string s7(cs, 3);       // 复制字符串cs的前三个字符到s当中

    string s8 = &quot;abcde&quot;;
    string s9(s8, 2);

    string s10 = &quot;asdsfasdgf&quot;;
    string s11(s10, 3, 4);  // s4是s3从下标s开始4个字符的拷贝，超出s10.size出现未定义
    return 0;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;string类型的读入&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;string s;
cin &amp;gt;&amp;gt; s;   //不能读入空格，以空格，制表符，回车符作为结束标志
getline(cin, s); //可以读入空格和制表符，以回车符作为结束的标志
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;字符串末尾添加字符&lt;/h3&gt;
&lt;p&gt;可以用+号和append()函数在函数的末尾添加字符。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;string s;
s += &apos;a&apos;;
s.append(&apos;a&apos;);
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;sting类型变量的访问&lt;/h3&gt;
&lt;p&gt;string字符串变量访问可以采用at, operator[]来访问指定index对应的字符。其中at有越界检查,如果index越界，无论Debug还是在Release编译的环境下，程序异常跳出执行；operator[]无越界检查，如果index越界，则会取得不可预知的字符。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;string s(&quot;abcd&quot;);
cout 
using namespace std;
stringstream ss;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&amp;lt; sstream &amp;gt; 库定义了三种类：istringstream、ostringstream和stringstream，分别用来进行流的输入、输出和输入输出操作。另外，每个类都有一个对应的宽字符集版本。一般情况下使用stringstream就足够，因为字符串要频繁的涉及到输入输出。&lt;/p&gt;
&lt;p&gt;&amp;lt; sstream &amp;gt; 使用string对象来代替字符数组，这样可以避免缓冲区溢出的危险。而且，传入参数和目标对象的类型被自动推导出来，即便使用了不正确的格式化符也没有危险。&lt;/p&gt;
&lt;p&gt;与文件流fstream类似，通过插入器(&amp;lt;&amp;lt;)和析取器(&amp;gt;&amp;gt;)这两个运算符可以直接对stringstream上的数据输入输出，而将stringstream中的全部数据输出则是使用成员函数str()，其有两种形式：
1、void &lt;strong&gt;str()&lt;/strong&gt; //无参形式，用于将stringstream流中的数据以string字符串的形式输出
2、void &lt;strong&gt;str (const string&amp;amp; s)&lt;/strong&gt;//以字符串为参数，用以覆盖stringstream流中的数据&lt;/p&gt;
&lt;p&gt;可以用stringstream完成int与string之间的转换&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;stringstream s1;
int i = 22;
s1 &amp;gt; i;
cout&lt;/code&gt;&lt;/pre&gt;
</content:encoded></item><item><title>c++常见问题---1</title><link>https://www.zerobot.top/posts/cpp-changjian-wenti-1/</link><guid isPermaLink="true">https://www.zerobot.top/posts/cpp-changjian-wenti-1/</guid><pubDate>Sat, 23 Oct 2021 00:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;前言&lt;/h2&gt;
&lt;p&gt;收集一些关于c/c++的常见问题，以备不时之需。&lt;/p&gt;
&lt;h3&gt;1.staic关键字在C和C++当中的作用以及它们之间的区别。&lt;/h3&gt;
&lt;h4&gt;（1）在c当中的作用&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;隐藏（对变量和函数）&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;在C语言中，static修饰的函数和变量具有只对本文件可见的作用，因此它具有对其他源文件隐藏的功能，利用这一特性可以在不同源文件定义同名函数和同名变量，从而不会引起命名冲突。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;保持变量持久，默认初始值为0（对变量）&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;也就是说，用static修饰的变量，生命周期是全局的，会一直存在到程序运行结束，但它的定义域还是跟普通变量一样。用static修饰的变量会存储在静态存储区，这里的变量会在程序刚开始运行的时候就进行初始化，也是唯一的一次初始化。同时静态存储区的字节默认初始值都为0x00，全局变量也存储在这里。&lt;/p&gt;
&lt;h4&gt;（2）在c++当中的作用&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;当然static在C中的作用在C++中同样适用。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;类的静态成员函数是属于整个类的而非某个对象,并且它没有this指针。由此引申出,它&lt;strong&gt;不能访问非静态数据和非静态成员函数,以及不能被声明为virtual、const、volatile&lt;/strong&gt;。这几点需要对C++的内存模型有一定的理解才能明白。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;类的静态数据成员同样是属于类的,对象都可以进行访问。&lt;strong&gt;初始化需要在类外进行。初始化格式：&amp;lt;数据类型&amp;gt;&amp;lt;类名&amp;gt;::&amp;lt;静态数据成员名&amp;gt;=&amp;lt;值&amp;gt;。&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;一个注意点&lt;/strong&gt;：只有静态常量整型变量才可以在类里面初始化，其他都不行。&lt;/p&gt;
&lt;p&gt;在C++中，static静态成员变量不能在类的内部初始化。在类的内部只是声明，定义必须在类定义体的外部，通常在类的实现文件中初始化，如：double Account::Rate = 2.25;static关键字只能用于类定义体内部的声明中，定义时不能标示为static&lt;/p&gt;
&lt;p&gt;在C++中，const成员变量也不能在类定义处初始化，只能通过构造函数初始化列表进行，并且必须有构造函数。&lt;/p&gt;
&lt;p&gt;const数据成员 只在某个对象生存期内是常量，而对于整个类而言却是可变的。因为类可以创建多个对象，不同的对象其const数据成员的值可以不同。所以不能在类的声明中初始化const数据成员，因为类的对象没被创建时，编译器不知道const数据成员的值是什么。&lt;/p&gt;
&lt;p&gt;const数据成员的初始化只能在类的构造函数的初始化列表中进行。要想建立在整个类中都恒定的常量，应该用类中的枚举常量来实现，或者static cosnt。&lt;/p&gt;
&lt;h3&gt;2.C++中的NULL和nullptr之间的区别是什么？&lt;/h3&gt;
&lt;h4&gt;（1）C中的NULL&lt;/h4&gt;
&lt;p&gt;在C语言中我们将NULL用作空指针之用，NULL在C语言中的定义如下：&lt;/p&gt;
&lt;p&gt;&lt;code&gt;#define NULL    ((void *)0)&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;可以看到，C语言中NULL是一个void*指针，我们常用其来对指针变量进行初始化赋值或者作为返回类型为指针的函数的返回值（如函数执行失败时）等等。&lt;/p&gt;
&lt;h4&gt;（2）C++中的NULL&lt;/h4&gt;
&lt;p&gt;在C++中，NULL的定义如下：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;`/* Define NULL pointer value */
#ifndef NULL
    #ifdef __cplusplus
        #define NULL    0
    #else  /* __cplusplus */
        #define NULL    ((void *)0)
    #endif  /* __cplusplus */
#endif  /* NULL */
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;可以看到，在C++中，NULL被定义为0，而不是(void*)0，为什么C++在NULL的定义上不继续兼容C延续(void*)0的定义呢？因为C++中不能将void&lt;em&gt;类型的指针隐式转换成其他指针类型，所以将NULL定义为(void&lt;/em&gt;)0的话并不能起到空指针的作用，如以下代码：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;void hello(char* a) {
    printf(&quot;char\n&quot;);
}

int main() {
    void* a = (void*) 0;
    char* b = a;     //在C中可以，C++不行
    hello(b);        //在C中可以，C++不行
    return 0;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;因为C++中不能将void&lt;em&gt;类型的指针隐式转换成其他指针类型，所以用(void**)0对其他类型指针赋初值是不行的。既然(void&lt;/em&gt;)0不能起到空指针的作用，不如干脆将NULL定义为0，引入0来表示空指针，可以对各种类型的指针进行赋值。&lt;/p&gt;
&lt;h4&gt;（3）C++中的nullptr&lt;/h4&gt;
&lt;p&gt;用过C++的童鞋都知道C++中有个nullptr的关键字可以用作空指针，既然已经有了定义为0的NULL，为何还要nullptr呢？这是因为定义为0的NULL很容易引起混淆，尤其是函数重载调用时，比如说：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;void hello(char* a) {
    printf(&quot;char\n&quot;);
}
void hello(int a) {
    printf(&quot;int\n&quot;);
}

int main() {
    hello（NULL）;    //调用hello(int a)函数
    return 0;
}
如果在C++中还是用NULL来用作空指针的话，我们会以为hello(NULL)调用的是hello(char* a)，但实际上，因为NULL定义为0，所以调用的是hello(int a)。所以为了避免这种混淆，C++定义了nullptr关键字用作空指针。
&lt;/code&gt;&lt;/pre&gt;
&lt;h5&gt;nullptr的使用&lt;/h5&gt;
&lt;p&gt;nullptr关键字用于标识空指针，是std::nullptr_t类型的（constexpr）变量。它可以转换成任何指针类型和bool布尔类型（主要是为了兼容普通指针可以作为条件判断语句的写法），但是不能被转换为整数。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;char *p1 = nullptr;     // 正确
int  *p2 = nullptr;     // 正确
bool b = nullptr;       // 正确. if(b)判断为false
int a = nullptr;        // 错误
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;注意nullptr和NULL以及0在作为条件判断时值都为false，它们两两之间进行等于（==）判断时值为true。&lt;/p&gt;
&lt;h3&gt;3.void*指针。&lt;/h3&gt;
&lt;p&gt;void* 是一种特殊的指针类型，可用于存放任意对象的地址。一个 void* 指针存放着一个地址，这一点和其他指针类似。&lt;/p&gt;
&lt;p&gt;在介绍 void 指针前，简单说一下 void 关键字使用规则：&lt;/p&gt;
&lt;p&gt;如果函数没有返回值，那么应声明为 void 类型；
如果函数无参数，那么应声明其参数为 void；（常省略）
如果函数的参数或返回值可以是任意类型指针，那么应声明其类型为 void* ；
void 的字面意思是“无类型”，void*则为“无类型指针”，void不能代表一个真实的变量，void体现了一种抽象。&lt;/p&gt;
&lt;h4&gt;（1）任何类型的指针都可以直接赋值给void指针， 且无需进行强制类型转换。&lt;/h4&gt;
&lt;p&gt;任何类型指针都可以直接赋值给void指针。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;double obj = 3.14, *pd = &amp;amp;obj;
void* pv = &amp;amp;obj;        // 正确，void* 能存放任意类型对象的地址
                        // obj 可以是任意类型的对象
pv = pd;   

             // 正确，pv 可以存放任意类型的指针
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;（2）void指针并不能无需类型转换直接赋值给其他类型，如果要把 void 类型的指针赋值给其他类型的指针，需要进行显式转换。&lt;/h4&gt;
&lt;p&gt;（2）void指针并不能无需类型转换直接赋值给其他类型
如果要把 void 类型的指针赋值给其他类型的指针，需要进行显式转换。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;double obj = 3.14, *pd = &amp;amp;obj;
void *pv = &amp;amp;obj;

double *pd1 = pv;               // 错误，不可以直接赋值
double *pd2 = (double*)pv;      // 必须进行显示类型转换
cout  func1(); ×
aObj-&amp;gt; func2(); 正确
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;三、将Const类型转化为非Const类型的方法&lt;/h4&gt;
&lt;p&gt;采用const_cast 进行转换。
用法：const_cast  (expression)
该运算符用来修改类型的const或volatile属性。除了const 或volatile修饰之外， type_id和expression的类型是一样的。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;常量指针被转化成非常量指针，并且仍然指向原来的对象；&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;常量引用被转换成非常量引用，并且仍然指向原来的对象；&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;常量对象被转换成非常量对象。&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h5&gt;四、使用const的一些建议&lt;/h5&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;要大胆的使用const，这将给你带来无尽的益处，但前提是你必须搞清楚原委；&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;要避免最一般的赋值操作错误，如将const变量赋值，具体可见思考题；&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;在参数中使用const应该使用引用或指针，而不是一般的对象实例，原因同上；&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;const在成员函数中的三种用法（参数、返回值、函数）要很好的使用；&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;不要轻易的将函数的返回值类型定为const;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;除了重载操作符外一般不要将返回值类型定为对某个对象的const引用;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;任何不会修改数据成员的函数都应该声明为const 类型。&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h5&gt;五、补充重要说明&lt;/h5&gt;
&lt;ul&gt;
&lt;li&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;类内部的常量限制：使用这种类内部的初始化语法的时候，常量必须是被一个常量表达式初始化的整型或枚举类型，而且必须是static和const形式。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;如何初始化类内部的常量：一种方法就是static 和 const 并用，在外部初始化，例如：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;class A 
{ 
  public: A() {} 

  private: static const int i;//注意必须是静态的！
}；

  const int A::i=3;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;另一个很常见的方法就是初始化列表：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;class A 
{ 
 public: A(int i=0):test(i) {} 
 private: const int i; 
}；
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;还有一种方式就是在外部初始化，&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;如果在非const成员函数中，this指针只是一个类类型的；如果在const成员函数中，this指针是一个const类类型的；如果在volatile成员函数中,this指针就是一个volatile类类型的。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;new返回的指针必须是const类型的。。&lt;/p&gt;
</content:encoded></item><item><title>我还活着</title><link>https://www.zerobot.top/posts/wo-hai-huozhe/</link><guid isPermaLink="true">https://www.zerobot.top/posts/wo-hai-huozhe/</guid><pubDate>Mon, 06 Sep 2021 00:00:00 GMT</pubDate><content:encoded>&lt;h4&gt;emmm&lt;/h4&gt;
&lt;p&gt;大晚上的爬起来就是为了证明这个网站还活着！！！&lt;/p&gt;
&lt;p&gt;后续应该也会慢慢更新的..&lt;/p&gt;
&lt;p&gt;要睡了&lt;/p&gt;
&lt;p&gt;晚安,好梦~&lt;/p&gt;
</content:encoded></item><item><title>linux网络协议栈---废话</title><link>https://www.zerobot.top/posts/linux-wangluo-xieyi-zhan/</link><guid isPermaLink="true">https://www.zerobot.top/posts/linux-wangluo-xieyi-zhan/</guid><pubDate>Mon, 28 Jun 2021 00:00:00 GMT</pubDate><content:encoded>&lt;h3&gt;先说点废话&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;  由于本人最近似乎有点点怠惰了,确确实实也有点拖延的毛病,不然之前打算写点技术博客的我也不至于坐在电脑面前满脑子都是生活的废料,今天和同事摸鱼聊天,感叹万物皆可互联网,各路大神争先恐后往里卷,像我们这种处于食物链下层的自然明白物竞天择的道理,居安思危这个词在哪儿都适用.
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;flag&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt; emmm,虽然前人总说不要轻易立flag,但是嘛,生活的仪式感还是要有的,万一自己做到了呢,是不,当然哈哈哈哈,说这句话自己没啥底气==,很早以前就想开linux网络协议栈的坑了,奈何本人拖延症晚期,一直没能好好实行下去,今天趁着这股雄心壮志还将泯未泯,就当一个好的开始啦.
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;introduction&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;  linux是个开源项目,因此相对于其他专用操作系统具有如下优势:遵照通用公共许可证(GPL)条款,用户可以免费获得其源码.linux内核网络栈是linux内核中的一个极其重要的子系统.在基于linux的系统中,不使用任何网络功能的很少.linux内核网络协议栈主要涉及OSI七层模型中的L2,L3,L4层,分别是数据链路层,网络层和传输层.从本质上说,Linux内核栈的任务主要是将接收到的数据包从L(网络设备驱动程序)传递给L(网络层,通常为ip4,ipv6).接下来,如果数据包目的地为当前设备,linux内核网络栈就将其传递给L4(传输层,应用tcp或者udp协议侦听套接字);如果数据包需要转发,就将其交还给L2继续传输.对于本地生成的出站数据包,将从L4依次传递给L2和L3,再由网络设备驱动程序再进行传输.这个过程分很多阶段,期间可能发生如下行为.
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;根据协议规则(如Ipsec规则或者NAT规则),可能需要对数据包进行修改.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;数据包可能被丢弃.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;数据包可能导致设备发送错误消息.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;可能会对数据包进行分段.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;可能需要重组数据包.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;需要计算数据包的校验和.&lt;/p&gt;
&lt;p&gt;要注意的是:内核并不涉及L4之上.这些层(会话层,表示层和应用层)的任务由用户空间应用程序来实现.此外,linux内核也不涉及物理层.&lt;/p&gt;
&lt;p&gt;网络子系统最重要的两个结构：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;net_device结构体：网络设备&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;sk_buff：表示套接字缓冲区的结构体&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
</content:encoded></item><item><title>随便写写</title><link>https://www.zerobot.top/posts/suibian-xiexie/</link><guid isPermaLink="true">https://www.zerobot.top/posts/suibian-xiexie/</guid><pubDate>Sun, 20 Jun 2021 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;明天就要上班了，也就是说写完这个就要上床睡觉了，这周末自己已经搬家啦，开始到一个新地方住了，房租比原来高了700左右，当交房租的那一刻自己还是有感到肉疼的，也就是说明天是第一天从新地方出发到公司上班，给自己从二手平台上买了一辆自行车，买小了点，将就用着。再者就是最近的疫情自己很是担心，毕竟身处这里，好像除了戴口罩，配合做核酸打疫苗自己也做不了啥事，只能默默祈祷这该死的病毒赶紧消失吧！&lt;/p&gt;
&lt;p&gt;还有这周工作上的事情，上面给了项目交期时间，这自己可不能划水了，还要做好加班的准备，其实说实话这个项目都是自己在琢磨，请教的渠道及反馈实在是有限且延迟较高，自己慢慢适应吧，有些不太感冒这种沟通方式但是却也无可奈何，自己对通信上面的名词了解也比较少，多学习吧！&lt;/p&gt;
</content:encoded></item><item><title>begin</title><link>https://www.zerobot.top/posts/begin/</link><guid isPermaLink="true">https://www.zerobot.top/posts/begin/</guid><pubDate>Mon, 14 Jun 2021 00:00:00 GMT</pubDate><content:encoded>&lt;h4&gt;开始&lt;/h4&gt;
&lt;p&gt;这是我的第一个博客，打算弄一个自己的博客这个想法存在很久了，今天终于有一个关于这个的雏形诞生，希望以后自己能跟好好维护它==&lt;/p&gt;
&lt;h4&gt;未来&lt;/h4&gt;
&lt;p&gt;以后自己在这上面记录一些生活的点滴以及所见所闻，还有最最重要的，希望自己能够在这上面完成技术积累，最近生活上发生了很多变化，下周也打算搬家了，搬到了比这更远的地方，好在公司有班车接送，倒不用担心上下班的事情，算是一个新的开始吧，加油hhh！&lt;/p&gt;
</content:encoded></item></channel></rss>