<?xml version="1.0" encoding="UTF-8"?><?xml-stylesheet href="/scripts/pretty-feed-v3.xsl" type="text/xsl"?><rss version="2.0" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:h="http://www.w3.org/TR/html4/"><channel><title>Yrh&apos; Blog</title><description>A blog for my life and work</description><link>https://blog.myyrh.com</link><item><title>Java Stream &amp; Collectors</title><link>https://blog.myyrh.com/blog/java-collectors/java-collectors</link><guid isPermaLink="true">https://blog.myyrh.com/blog/java-collectors/java-collectors</guid><description>最近写Java被Collectors折磨的要死, 总结记录一下</description><pubDate>Fri, 19 Sep 2025 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;先偷张图
&lt;img src=&quot;./api-index.png&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;h2&gt;Stream&lt;/h2&gt;
&lt;h3&gt;map&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;&amp;#x3C;R&gt; stream&amp;#x3C;R&gt; map(Function&amp;#x3C;? super T, ? extends R&gt; mapper);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;挖个坑：这里他这么定义是遵从了一个 “输入逆变、输出协变“的一个规则，等后面再补一篇&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;Stream.of(&quot;hello&quot;, &quot;how&quot;, &quot;are&quot;, &quot;you&quot;)
  .map(String::length)
  .forEach(System.out::println);
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;flatMap&lt;/h3&gt;
&lt;p&gt;大概就是说把元素拍扁，然后把拍扁的元素重新组成Stream，然后把这些Stream串行合并成一条Stream&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;&amp;#x3C;R&gt; Stream&amp;#x3C;R&gt; flatMap(Function&amp;#x3C;? super T, ? extends Stream&amp;#x3C;? extends R&gt;&gt; mapper);
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;Stream.of(&quot;a-b-c-d&quot;, &quot;1-2-3-4&quot;)
  .flatMap(e -&gt; Arrays.stream(e.split(&quot;-&quot;)))
  .forEach(System.out::println);
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;limit&lt;/h3&gt;
&lt;p&gt;限制元素个数&lt;/p&gt;
&lt;h3&gt;distinct&lt;/h3&gt;
&lt;p&gt;去重，默认是根据元素的equals() 和 hashCode()来判断的&lt;/p&gt;
&lt;h3&gt;filter&lt;/h3&gt;
&lt;p&gt;给他一个条件函数传入参数返回true or false即可&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;Stream&amp;#x3C;T&gt; filter(Predicate&amp;#x3C;? super T&gt; predicate);
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;sorted&lt;/h3&gt;
&lt;p&gt;用法大差不差，实现Comparable接口就行&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;Stream&amp;#x3C;T&gt; sorted();                           // 自然顺序
Stream&amp;#x3C;T&gt; sorted(Comparator&amp;#x3C;? super T&gt; comp); // 自定义比较器
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;collect&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;&amp;#x3C;R, A&gt; R collect(Collector&amp;#x3C;? super T, A, R&gt; collector);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这个要整理一下Collectors的api了，看下文吧&lt;/p&gt;
&lt;h3&gt;reduce&lt;/h3&gt;
&lt;p&gt;类似于FP的规约操作&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;Optional&amp;#x3C;T&gt; reduce(BinaryOperator&amp;#x3C;T&gt; accumulator);
T reduce(T identity, BinaryOperator&amp;#x3C;T&gt; accumulator);
&amp;#x3C;U&gt; U reduce(U identity,
             BiFunction&amp;#x3C;U, ? super T, U&gt; accumulator,
             BinaryOperator&amp;#x3C;U&gt; combiner);
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;Optional&amp;#x3C;Integer&gt; sum = Stream.of(1, 2, 3, 4, 5)
  .reduce((a, b) -&gt; a + b);

int sum = Stream.of(1, 2, 3, 4, 5)
  .reduce(0, (a, b) -&gt; a + b);

int lengthSum = Stream.of(&quot;a&quot;, &quot;bb&quot;, &quot;ccc&quot;)
  .parallel()
  .reduce(
      0,
      (sum, str) -&gt; sum + str.length(),
      Integer::sum
  );

&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Collector&lt;/h2&gt;
&lt;h3&gt;Collectors.toCollection()&lt;/h3&gt;
&lt;p&gt;将数据转成Collection，只要是Collection 的实现都可以，例如ArrayList、HashSet ，该方法接受一个Collection 的实现对象或者说Collection 工厂的入参。
包括后面也有Collectors.toList()、Collectors.toSet() ，这里只是指定了容器的类型。&lt;/p&gt;
&lt;h3&gt;Collectors.joining()&lt;/h3&gt;
&lt;p&gt;基本语法&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;Collectors.joining()                 // 直接拼接
Collectors.joining(CharSequence delimiter) // 指定分隔符
Collectors.joining(CharSequence delimiter, CharSequence prefix, CharSequence suffix)
// 指定分隔符 + 前缀 + 后缀
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;未完待续....&lt;/p&gt;</content:encoded><h:img src="undefined"/><enclosure url="undefined"/></item><item><title>IO多路复用（埋坑）</title><link>https://blog.myyrh.com/blog/iomultiplexing</link><guid isPermaLink="true">https://blog.myyrh.com/blog/iomultiplexing</guid><description>简单整理了一下IO多路复用技术，主要是为了给Netty铺垫。</description><pubDate>Thu, 25 Jan 2024 00:00:00 GMT</pubDate><content:encoded>&lt;h1&gt;可能的疑问&lt;/h1&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;为什么不用多线程？&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;在很多rpc模型中可能会用到多线程，但是多线程上下文切换要处理操作句柄，线程多了的时候，代价很高。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;单线程操作会不会因为堵塞导致数据丢失？&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;不会，因为&lt;code&gt;DMA&lt;/code&gt;控制器会操作。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h1&gt;模型&lt;/h1&gt;
&lt;h2&gt;&lt;code&gt;C10K&lt;/code&gt;&lt;/h2&gt;
&lt;p&gt;是一个优化网络套接字以同时处理大量客户端连接的问题，表示处理 10000 个并发连接。&lt;/p&gt;
&lt;h2&gt;Socket模型&lt;/h2&gt;
&lt;p&gt;关于Socket，我看过最简洁的描述就是&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;Socket可以看成是一个四元组(Source:port, Desc:port)&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;他就是一个插头，一头插在客户端，一头插在服务端。
他如何运行的呢？
我们可以观察一下系统调用介绍
&lt;img src=&quot;https://i0.zstatic.net/images/2024/01/25-4a2c64035d0c0944535913b736c621c3.png&quot; alt=&quot;1706168204574.png&quot;&gt;
也就是说在调用的时候，需要传入三个参数，篇幅过长我直接总结，感兴趣的可以通过&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-shell&quot;&gt;man 2 socket
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;自行查阅。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;domain : 指定地址类型，比如ipv4 ipv6&lt;/li&gt;
&lt;li&gt;type: 通信的类型，比如tcp, udp&lt;/li&gt;
&lt;li&gt;protocol: socket传输协议编号，一般不设置。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;返回值就是&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;On success, a file descriptor for the new socket is returned. On error, -1 is returned, and errno is set to in‐dicate the error.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;Socket模型生命周期
&lt;img src=&quot;https://i0.zstatic.net/images/2024/01/25-013dfa94567f8901a865d34031f4ddb5.png&quot; alt=&quot;1706168532386.png&quot;&gt;&lt;/p&gt;
&lt;h2&gt;简单粗暴的IO模型？&lt;/h2&gt;
&lt;h3&gt;BIO&lt;/h3&gt;
&lt;p&gt;&lt;code&gt;Blocking&lt;/code&gt; 显而易见这是堵塞的IO模型&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;public class SocketBio {
	public static void main(String[] args) throws IOException {
		ServerSocket server = new ServerSocket(9999);
		System.out.println(&quot;listening on 9999&quot;);
		while (true){
			Socket client = server.accept();
			System.out.printf(&quot;client %s #online\n&quot;, client.getPort());
			new Thread(new Runnable() {
				@Override
				public void run() {
					InputStream in = null;
					try{
						in = client.getInputStream();
						BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(in));
						while(true){
							String dateLine = bufferedReader.readLine();
							if(dateLine != null){
								System.out.printf(&quot;client %s #received: \n&quot;,client.getPort(), dateLine);
							}else{
								System.out.printf(&quot;client: %s #closed\n&quot;,client.getPort());
								client.close();
								break;
							}
						}
					} catch (Exception e) {
						e.printStackTrace();
					}
				}
			}).start();
		}
	}
}

&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;NIO&lt;/h3&gt;
&lt;p&gt;&lt;code&gt;No-Blocking&lt;/code&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;public class SocketNio {
	public static void main(String[] args) throws Exception {
		LinkedList&amp;#x3C;SocketChannel&gt; clients = new LinkedList&amp;#x3C;&gt;();
		ServerSocketChannel ss = ServerSocketChannel.open();
		ss.bind(new InetSocketAddress(9999));
		ss.configureBlocking(false);
		ByteBuffer buffer = ByteBuffer.allocateDirect(4096);
		while(true){
			SocketChannel client = ss.accept();
			if(client == null){

			}else{
				client.configureBlocking(false);
				int port = client.socket().getPort();
				System.out.printf(&quot;client: %d #accepted\n&quot;, port);
				clients.add(client);
			}
			Iterator&amp;#x3C;SocketChannel&gt; iterator = clients.iterator();
			while(iterator.hasNext()){
				SocketChannel c = iterator.next();
				buffer.clear();
				int num = c.read(buffer);
				if(num &gt; 0){
					buffer.flip();
					byte[] tmp = new byte[buffer.limit()];
					buffer.get(tmp);
					System.out.printf(&quot;client %d #received: %s \n&quot;, c.socket().getPort(), new String(tmp));
				}else if(num == -1){
					System.out.printf(&quot;Client %d disconnected\n&quot;, c.socket().getPort());
					iterator.remove();
					c.close();
				}
			}
		}
	}
}

&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;IO多路复用&lt;/h3&gt;
&lt;p&gt;看上面的&lt;code&gt;BIO&lt;/code&gt;和&lt;code&gt;NIO&lt;/code&gt;的代码，我们就可以发现：
BIO的缺点：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;进程占据资源&lt;/li&gt;
&lt;li&gt;上下文切换&lt;/li&gt;
&lt;li&gt;...&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;NIO的缺点：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;当连接多了之后，需要维护的东西很多。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;于是可能会想到， 能否只用一个进程去维护多个socket呢？&lt;/p&gt;
&lt;h6&gt;select&lt;/h6&gt;
&lt;p&gt;如果有很多请求（但是select限制的最大请求上限是1024）连接到server，&lt;code&gt;select&lt;/code&gt;会对所有请求遍历，记录下有数据到达的请求IO，然后返回记录信息，客户端读取。
所以当请求的数量很多但是数据请求很少的时候， 会造成很多空读取。
&lt;img src=&quot;https://i0.zstatic.net/images/2024/01/25-eff1ccacbe0b52fc2ca7e7a80f4b0127.png&quot; alt=&quot;1706186857657.png&quot;&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-c&quot;&gt;int select(int maxfdp1,fd_set *readset,fd_set *writeset,fd_set *exceptset,const struct timeval *timeout)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;参数说明&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;mafdfp1 描述待轮讯的连接的个数&lt;/li&gt;
&lt;li&gt;fd_set: readset writeset exceptset 指定了让&lt;code&gt;kernel&lt;/code&gt;读、写、异常的描述&lt;/li&gt;
&lt;li&gt;timeout 描述了一个就绪可以花费多长时间。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;原理图（copy来的
&lt;img src=&quot;https://i0.zstatic.net/images/2024/01/25-c9f7c493ba7fbc6c8db907729b79eb21.png&quot; alt=&quot;1706187141159.png&quot;&gt;&lt;/p&gt;
&lt;h6&gt;poll&lt;/h6&gt;
&lt;p&gt;poll和select没有太大的区别，都是通过管理多个描述符来进行轮讯，但是poll没有最大连接数限制，与此同时，poll的一个缺点就是： 大量fd的数组被整体复制于用户态和内核的地址空间之间，而不论这些fd是否就绪，她的开销随着fd的数量增大线性增大。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-c&quot;&gt;# include &amp;#x3C;poll.h&gt;
int poll ( struct pollfd * fds, unsigned int nfds, int timeout);
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Epoll&lt;/h3&gt;
&lt;p&gt;epoll是在Linux-2.6中提出的，&lt;code&gt;select&lt;/code&gt;和&lt;code&gt;poll&lt;/code&gt;的增强版。
epoll使用一个文件描述管理多个fd，把用户关系的fd存放到kernel的一个事件表之中，这样用户空间和kernel的copy只需要一次。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://i0.zstatic.net/images/2024/01/25-b65cb237acd28a0910837247eaa02b1a.png&quot; alt=&quot;1706187866094.png&quot;&gt;&lt;/p&gt;
&lt;p&gt;大概就是：
java端创建一个socket，fd是fd4，bind端口之后开始监听，这时候epoll会调用epoll_create(int size) 创建红黑树空间fd6，这个空间是被监听事件的数目，然后执行epoll_ctl，把fd6和fd4关联起来，当有链接有数据的时候，记录IO的编号，把这个编号存储在一个链表之中，当epoll调用epoll_wait的时候，就把这个事件相关的链表返回，之后调用程序只需要根据记录的信息，读写产生的事件IO即可。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-c&quot;&gt;#include &amp;#x3C;sys/epoll.h&gt;
int epoll_create(int size);
int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);
int epoll_wait(int epfd, struct epoll_event * events, int maxevents, int timeout);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;先写这么多吧，后续在补充&lt;/p&gt;</content:encoded><h:img src="undefined"/><enclosure url="undefined"/></item><item><title>Go接口型函数</title><link>https://blog.myyrh.com/blog/go-interfacefunc</link><guid isPermaLink="true">https://blog.myyrh.com/blog/go-interfacefunc</guid><description>简单总结了一下Go接口型函数的使用</description><pubDate>Sat, 30 Dec 2023 00:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;Example&lt;/h2&gt;
&lt;pre&gt;&lt;code class=&quot;language-go&quot;&gt;type Example interface{
    handle(key int) (int, error)
}

type ExampleFunc func(key int)(int, error)

func(t ExampleFunc) handle(key int)(int, error){
    return t(key)
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这里定义了一个接口&lt;code&gt;Example&lt;/code&gt; ，包含一个方法&lt;code&gt;handle(key int) (int, error)&lt;/code&gt; ，然后定义了一个函数类型&lt;code&gt;ExampleFunc&lt;/code&gt;，定义了&lt;code&gt;Get&lt;/code&gt;方法，并且调用自己，这就是接口型函数。&lt;/p&gt;
&lt;h2&gt;why&lt;/h2&gt;
&lt;p&gt;假如这个&lt;code&gt;GetTest&lt;/code&gt;是通过一个接口返回数据，接口类型作为参数，代表数据源.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-go&quot;&gt;func GetTest(example Example, key int) int{
    if buf, err := example.Get(key); err != nil{
        return nil
    }else{
        return buf
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;方式一： ExampleFunc类型的函数作为参数&lt;/h2&gt;
&lt;pre&gt;&lt;code class=&quot;language-go&quot;&gt;GetTest(ExampleFunc(func(key int) (int, error){
    return int(key), nil
}), 1)

// or
func test(key int) (int, error){
    return int(key), nil
}
GetTest(ExampleFunc(test), 1)
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;方式二：实现Example接口的结构体&lt;/h2&gt;
&lt;pre&gt;&lt;code class=&quot;language-go&quot;&gt;type DB struct{params int}
func(db *DB)Query(){}
func(db *DB)Get(key int)(int, error){}

GetTest(new(DB), 1)
&lt;/code&gt;&lt;/pre&gt;</content:encoded><h:img src="undefined"/><enclosure url="undefined"/></item><item><title>Go并发编程部分知识</title><link>https://blog.myyrh.com/blog/go-concurrence</link><guid isPermaLink="true">https://blog.myyrh.com/blog/go-concurrence</guid><description>整理了一下Go并发编程的一些知识点，有点乱，勿喷</description><pubDate>Wed, 27 Dec 2023 00:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;锁&lt;/h2&gt;
&lt;p&gt;Go的&lt;code&gt;sync&lt;/code&gt;提供了互斥锁sync.Mutex和读写锁sync.RWMutex&lt;/p&gt;
&lt;h3&gt;互斥锁 (sync.Mutex)&lt;/h3&gt;
&lt;p&gt;互斥顾名思义。使用互斥锁的两段程序不能同时运行。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Lock 加锁&lt;/li&gt;
&lt;li&gt;Unlock 释放锁&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;我们可以通过在代码前调用Lock方法，在代码后面使用Unlock方法保证代码的互斥操作。类似C语言的&lt;code&gt;PV&lt;/code&gt;操作。一个Go协程调用Lock方法获得锁之后，其他协程都会堵塞在Lock方法，直到释放锁。&lt;/p&gt;
&lt;h3&gt;读写锁(sync.RWMutex)&lt;/h3&gt;
&lt;p&gt;在实际应用中，很大一部分场景是，写操作 $\lt$ 读操作， 所以如果对于一个读操作我们加锁，很有可能堵塞其他的读操作，而常识也知道，读操作不会影响数据。&lt;/p&gt;
&lt;p&gt;所以如果想要保证读操作的安全性，只需要保证并发读操作的时候没有写操作即可。&lt;/p&gt;
&lt;p&gt;Go语言的读写锁，分为读锁和写锁。读锁是允许同时进行的，但是写锁是互斥的。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;读锁不互斥，没有写锁的情况下，读锁不堵塞，允许多个协程获取读锁。&lt;/li&gt;
&lt;li&gt;写锁是互斥的，存在写锁，其他协程读锁会堵塞。&lt;/li&gt;
&lt;li&gt;读锁和写锁是互斥的，如果存在读锁、写锁堵塞，存在写锁、读锁堵塞。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;code&gt;sync.RWMutex&lt;/code&gt;提供了四个方法&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Lock 加写锁&lt;/li&gt;
&lt;li&gt;Unlock 释放写锁&lt;/li&gt;
&lt;li&gt;RLock 加读锁&lt;/li&gt;
&lt;li&gt;RUnlock 释放读锁&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;这个读写锁是为了解决读多写少时的问题。&lt;/p&gt;
&lt;h3&gt;性能比较&lt;/h3&gt;
&lt;p&gt;为了方便测试，都是实现了接口。&lt;/p&gt;
&lt;p&gt;假设一个读写操作都是&lt;code&gt;1μs&lt;/code&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;Lock&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-go&quot;&gt;package LockTest

import (
	&quot;sync&quot;
	&quot;time&quot;
)

type MUT interface {
	Read()
	Write()
}

type Lock struct {
	value int
	mu    sync.Mutex
}

func (L *Lock) Read() {
	L.mu.Lock()
	_ = L.value
	time.Sleep(time.Microsecond)
	L.mu.Unlock()
}

func (L *Lock) Write() {
	L.mu.Lock()
	L.value++
	time.Sleep(time.Microsecond)
	L.mu.Unlock()
}

&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;RWLock&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-go&quot;&gt;package LockTest

import (
	&quot;sync&quot;
	&quot;time&quot;
)

type RWLock struct {
	value int
	mu    sync.RWMutex
}

func (L *RWLock) Write() {
	L.mu.Lock()
	L.value++
	time.Sleep(time.Microsecond)
	L.mu.Unlock()
}

func (L *RWLock) Read() {
	L.mu.RLock()
	_ = L.value
	time.Sleep(time.Microsecond)
	L.mu.RUnlock()
}
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;test&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-go&quot;&gt;package Test

import (
	&quot;Go-Concurrency-Test_/LockTest&quot;
	&quot;sync&quot;
	&quot;testing&quot;
)

func test(b *testing.B, mut LockTest.MUT, readNum, writeNum int) {
	for i := 0; i &amp;#x3C; b.N; i++ {
		var wg sync.WaitGroup
		for j := 0; j &amp;#x3C;= readNum; j++ {
			wg.Add(1)
			go func() {
				mut.Read()
				wg.Done()
			}()
		}

		for j := 0; j &amp;#x3C;= writeNum; j++ {
			wg.Add(1)
			go func() {
				mut.Write()
				wg.Done()
			}()
		}
		wg.Wait()
	}
}
func BenchmarkReadMore(b *testing.B)    { test(b, &amp;#x26;LockTest.Lock{}, 8000, 1000) }
func BenchmarkReadRWMore(b *testing.B)  { test(b, &amp;#x26;LockTest.RWLock{}, 8000, 1000) }
func BenchmarkWriteMore(b *testing.B)   { test(b, &amp;#x26;LockTest.Lock{}, 8000, 1000) }
func BenchmarkWriteRWMore(b *testing.B) { test(b, &amp;#x26;LockTest.RWLock{}, 8000, 1000) }
func BenchmarkLock(b *testing.B)        { test(b, &amp;#x26;LockTest.Lock{}, 5000, 5000) }
func BenchmarkRWLock(b *testing.B)      { test(b, &amp;#x26;LockTest.RWLock{}, 5000, 5000) }

&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;运行测试&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;goos: linux
goarch: amd64
BenchmarkReadMore
BenchmarkReadMore-12       	       6	 262894692 ns/op
BenchmarkReadRWMore
BenchmarkReadRWMore-12     	      45	  24374298 ns/op
BenchmarkWriteMore
BenchmarkWriteMore-12      	       4	 258559273 ns/op
BenchmarkWriteRWMore
BenchmarkWriteRWMore-12    	      66	  22369855 ns/op
BenchmarkLock
BenchmarkLock-12           	       4	 293963420 ns/op
BenchmarkRWLock
BenchmarkRWLock-12         	      10	 123536948 ns/op

&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;sync.mutex分析&lt;/h2&gt;
&lt;p&gt;&lt;a href=&quot;https://colobu.com/2018/12/18/dive-into-sync-mutex/&quot;&gt;原文章&lt;/a&gt;&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;互斥锁有两种状态：正常状态和饥饿状态。&lt;/p&gt;
&lt;p&gt;在正常状态下，所有等待锁的 goroutine 按照FIFO顺序等待。唤醒的 goroutine 不会直接拥有锁，而是会和新请求锁的 goroutine 竞争锁的拥有。新请求锁的 goroutine 具有优势：它正在 CPU 上执行，而且可能有好几个，所以刚刚唤醒的 goroutine 有很大可能在锁竞争中失败。在这种情况下，这个被唤醒的 goroutine 会加入到等待队列的前面。 如果一个等待的 goroutine 超过 1ms 没有获取锁，那么它将会把锁转变为饥饿模式。&lt;/p&gt;
&lt;p&gt;在饥饿模式下，锁的所有权将从 unlock 的 goroutine 直接交给交给等待队列中的第一个。新来的 goroutine 将不会尝试去获得锁，即使锁看起来是 unlock 状态, 也不会去尝试自旋操作，而是放在等待队列的尾部。&lt;/p&gt;
&lt;p&gt;如果一个等待的 goroutine 获取了锁，并且满足一以下其中的任何一个条件：(1)它是队列中的最后一个；(2)它等待的时候小于1ms。它会将锁的状态转换为正常状态。&lt;/p&gt;
&lt;p&gt;正常状态有很好的性能表现，饥饿模式也是非常重要的，因为它能阻止尾部延迟的现象。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;空结构体&lt;/h2&gt;
&lt;pre&gt;&lt;code class=&quot;language-go&quot;&gt;package main

import (
	&quot;fmt&quot;
	&quot;unsafe&quot;
)

func main() {
	fmt.Println(unsafe.Sizeof(struct{}{}))
}

&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code class=&quot;language-shell&quot;&gt;$ go run main.go
0
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;空结构体实例&lt;code&gt;struct{}&lt;/code&gt; 不占据任何内存空间。&lt;/p&gt;
&lt;h2&gt;作用&lt;/h2&gt;
&lt;h3&gt;实现&lt;code&gt;Set&lt;/code&gt;&lt;/h3&gt;
&lt;p&gt;Go语言标准库没有提供&lt;code&gt;set&lt;/code&gt;的实现，通常用&lt;code&gt;map&lt;/code&gt;来实现。&lt;/p&gt;
&lt;p&gt;实际上在使用&lt;code&gt;set&lt;/code&gt;的时候，我们只关注&lt;code&gt;key&lt;/code&gt;而不需要&lt;code&gt;value&lt;/code&gt;，因此可以直接把&lt;code&gt;value&lt;/code&gt;设置成&lt;code&gt;struct{}&lt;/code&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-go&quot;&gt;type Set map[string]struct{}

func (s Set) Exists(key string) bool {
	_, ok := s[key]
	return ok
}

func (s Set) Add(key string) {
	s[key] = struct{}{}
}

func (s Set) Delete(key string) {
	delete(s, key)
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;不发送数据的&lt;code&gt;channel&lt;/code&gt;&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-go&quot;&gt;func work(ch chan struct{}){
    &amp;#x3C;- ch
    fmt.Println(&quot;1&quot;)
    close(ch)
}

func main(){
    ch := make(chan struct{})
    go work(ch)
    ch &amp;#x3C;- struct{}{}
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;&lt;code&gt;goroutine&lt;/code&gt;&lt;/h3&gt;
&lt;h4&gt;控制协程数量&lt;/h4&gt;
&lt;h5&gt;利用&lt;code&gt;channel&lt;/code&gt;缓冲区&lt;/h5&gt;
&lt;pre&gt;&lt;code class=&quot;language-go&quot;&gt;func MyRoutine() {
	var wg sync.WaitGroup

	ch := make(chan struct{}, 5)
	for i := 0; i &amp;#x3C;= 100; i++ {
		ch &amp;#x3C;- struct{}{}
		wg.Add(1)
		go func(i int) {
			defer wg.Done()
			log.Println(i)
			time.Sleep(time.Second)
			&amp;#x3C;-ch
		}(i)
	}
	wg.Wait()
}
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;make(chan sturct{}, 3)&lt;/code&gt; 创建缓冲区大小为3的&lt;code&gt;channel&lt;/code&gt;，在没有被接收的情况下，最多发送三个消息就会被堵塞。&lt;/li&gt;
&lt;/ul&gt;</content:encoded><h:img src="undefined"/><enclosure url="undefined"/></item><item><title>MIT6.824-Lab1</title><link>https://blog.myyrh.com/blog/mit6-824-lab1</link><guid isPermaLink="true">https://blog.myyrh.com/blog/mit6-824-lab1</guid><description>本文实现了mit6.824的lab1</description><pubDate>Tue, 19 Dec 2023 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;先来一张成功的截图&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://s0.zstatic.net/2023/10/b2ade5a45b7280bfe2141f785daf2b19_ab37fceacafdfe38d197a9dcf284b0033a009c17.png&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;说明：基于本人的&lt;strong&gt;poor english&lt;/strong&gt; 我选择阅读翻译版本的论文。&lt;/p&gt;
&lt;h2&gt;MapReduce框架&lt;/h2&gt;
&lt;p&gt;&lt;img src=&quot;https://pic3.zhimg.com/80/v2-3d07dd78da8061c0d6b5b06b8d83e0ba_720w.webp&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;论文中对于&lt;code&gt;MapReduce&lt;/code&gt; 主要分成四个角色：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;User Program&lt;/code&gt; : 客户端，用来协调&lt;code&gt;MapReduce&lt;/code&gt;任务。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Master&lt;/code&gt;: 用于分配任务、协调任务的进行。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Map Worker&lt;/code&gt;: 执行&lt;code&gt;Map&lt;/code&gt;方法的进程。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Reduce Worker&lt;/code&gt;: 执行&lt;code&gt;Reduer&lt;/code&gt;方法的进程。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;输入数据以文件形式存储在系统中（在下一节讲到了）。在&lt;code&gt;Master&lt;/code&gt; 进程的调整分配下运行&lt;code&gt;map&lt;/code&gt;任务，生成了一些中间体，这些中间体是以&lt;code&gt;k-v&lt;/code&gt;键值对形式存在的。另外一些进程运行了&lt;code&gt;Reduer&lt;/code&gt;任务，产生最终输出。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;Job&lt;/code&gt;: 整个&lt;code&gt;MapReduce&lt;/code&gt;计算称为&lt;code&gt;Job&lt;/code&gt;。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Task&lt;/code&gt;: 每一次&lt;code&gt;MapReduce&lt;/code&gt;调用称为&lt;code&gt;Task&lt;/code&gt;。&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;&lt;code&gt;Map&lt;/code&gt;函数&lt;/h3&gt;
&lt;p&gt;&lt;code&gt;Map&lt;/code&gt;函数使用一个&lt;code&gt;key&lt;/code&gt;和一个&lt;code&gt;value&lt;/code&gt;作为参数。&lt;/p&gt;
&lt;p&gt;普遍上，&lt;code&gt;key&lt;/code&gt;是输入文件的名字，我们一般不关心他，&lt;code&gt;value&lt;/code&gt;是输入文件的内容。&lt;/p&gt;
&lt;p&gt;对于一个单词计数器来说，&lt;code&gt;value&lt;/code&gt;包含了要统计的文本，我们先把这些文本拆分单词，对于每一个单词，我们都调用一个&lt;code&gt;emit&lt;/code&gt;。这个&lt;code&gt;emit&lt;/code&gt;由&lt;code&gt;MapReduce&lt;/code&gt;框架提供，并且这里的&lt;code&gt;emit&lt;/code&gt;是属于&lt;code&gt;Map&lt;/code&gt;的。&lt;code&gt;emit&lt;/code&gt;接收两个参数，其中一个是&lt;code&gt;key&lt;/code&gt;，一个是&lt;code&gt;value&lt;/code&gt;。比如在单词计数器里面，就是调&lt;code&gt;emit(&quot;apple&quot;,&quot;1&quot;)&lt;/code&gt;。在一个单词&lt;code&gt;MapReduce Job&lt;/code&gt;中，这就是&lt;code&gt;Map&lt;/code&gt;函数的实现。&lt;code&gt;Map&lt;/code&gt;函数实际上不需要知道任何分布式相关的信息，不需要知道有多少台设备，不需要知道会通过网络来传输数据。&lt;/p&gt;
&lt;h3&gt;&lt;code&gt;Reduce&lt;/code&gt;函数&lt;/h3&gt;
&lt;p&gt;&lt;code&gt;Reduce&lt;/code&gt;函数的入参是某个特定的&lt;code&gt;key&lt;/code&gt;的所有实例。所以接受的参数也是&lt;code&gt;key&lt;/code&gt;和&lt;code&gt;value&lt;/code&gt;。其中&lt;code&gt;value&lt;/code&gt;是一个数组，里面的每一个元素都是&lt;code&gt;Map&lt;/code&gt;函数输出的&lt;code&gt;key&lt;/code&gt;作为实例的&lt;code&gt;value&lt;/code&gt;。对于单词计数器来说，&lt;code&gt;key&lt;/code&gt;就是一个单词，&lt;code&gt;value&lt;/code&gt;是一串数组。在&lt;code&gt;Reduce&lt;/code&gt;函数中，也有自己的&lt;code&gt;emit&lt;/code&gt;函数，这里的&lt;code&gt;emit&lt;/code&gt;函数只接受一个&lt;code&gt;value&lt;/code&gt;参数，这个&lt;code&gt;value&lt;/code&gt;会作为&lt;code&gt;Reduce&lt;/code&gt;函数入参的&lt;code&gt;key&lt;/code&gt;的最终输出。&lt;/p&gt;
&lt;h3&gt;What&apos;s More&lt;/h3&gt;
&lt;ol&gt;
&lt;li&gt;实际上，对于复杂的阶段分析和迭代算法，比如评估网页的重要性，我们可能会把&lt;code&gt;Reduce&lt;/code&gt;的输出在丢到&lt;code&gt;Map&lt;/code&gt;里面继续迭代&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;Lab1&lt;/h2&gt;
&lt;p&gt;Lab1要求我们实现一个和&lt;a href=&quot;https://pdos.csail.mit.edu/6.824/papers/mapreduce.pdf&quot;&gt;Mapreduce&lt;/a&gt;类似机制的单词统计器。&lt;/p&gt;
&lt;p&gt;输入文件在&lt;code&gt;src/main&lt;/code&gt;中，文件名是&lt;code&gt;pg-*.txt&lt;/code&gt;，要统计他们出现的单词和出现的次数。&lt;/p&gt;
&lt;p&gt;首先观察文件结构&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;.
├── go.mod
├── Makefile
└── src
    ├── kvraft
    │   ├── client.go
    │   ├── common.go
    │   ├── config.go
    │   ├── server.go
    │   └── test_test.go
    ├── labgob
    │   ├── labgob.go
    │   └── test_test.go
    ├── labrpc
    │   ├── labrpc.go
    │   └── test_test.go
    ├── main
    │   ├── diskvd.go
    │   ├── lockc.go
    │   ├── lockd.go
    │   ├── mrmaster.go
    │   ├── mr-out-0
    │   ├── mrsequential.go
    │   ├── mrworker.go
    │   ├── pbc.go
    │   ├── pbd.go
    │   ├── pg-being_ernest.txt
    │   ├── pg-dorian_gray.txt
    │   ├── pg-frankenstein.txt
    │   ├── pg-grimm.txt
    │   ├── pg-huckleberry_finn.txt
    │   ├── pg-metamorphosis.txt
    │   ├── pg-sherlock_holmes.txt
    │   ├── pg-tom_sawyer.txt
    │   ├── pkg
    │   │   └── mod
    │   │       └── cache
    │   │           └── lock
    │   ├── test-mr.sh
    │   ├── viewd.go
    │   └── wc.so
    ├── models
    │   └── kv.go
    ├── mr
    │   ├── master.go
    │   ├── rpc.go
    │   └── worker.go
    ├── mrapps
    │   ├── crash.go
    │   ├── indexer.go
    │   ├── mtiming.go
    │   ├── nocrash.go
    │   ├── rtiming.go
    │   └── wc.go
    ├── porcupine
    │   ├── bitset.go
    │   ├── checker.go
    │   ├── model.go
    │   ├── porcupine.go
    │   └── visualization.go
    ├── raft
    │   ├── config.go
    │   ├── persister.go
    │   ├── raft.go
    │   ├── test_test.go
    │   └── util.go
    ├── shardkv
    │   ├── client.go
    │   ├── common.go
    │   ├── config.go
    │   ├── server.go
    │   └── test_test.go
    └── shardmaster
        ├── client.go
        ├── common.go
        ├── config.go
        ├── server.go
        └── test_test.go

&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;mrmaster.go&lt;/code&gt;：用来启动&lt;code&gt;master&lt;/code&gt;进程，启动后调用&lt;code&gt;mr/master&lt;/code&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;mrworker.go&lt;/code&gt;：用来启动&lt;code&gt;worker&lt;/code&gt;进程，启动后调用&lt;code&gt;mr/worker&lt;/code&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;mrapps&lt;/code&gt;：是我们实际上要执行的任务，具体的&lt;code&gt;map&lt;/code&gt;和&lt;code&gt;reduce&lt;/code&gt;任务。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;mr&lt;/code&gt;：实现&lt;code&gt;lab1&lt;/code&gt;的文件夹：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;master.go&lt;/code&gt;：作为&lt;code&gt;master&lt;/code&gt;节点所具备的功能&lt;/li&gt;
&lt;li&gt;&lt;code&gt;worker.go&lt;/code&gt;：作为&lt;code&gt;worker&lt;/code&gt;节点所具备的功能&lt;/li&gt;
&lt;li&gt;&lt;code&gt;rpc.go&lt;/code&gt;：实现&lt;code&gt;master&lt;/code&gt;和&lt;code&gt;worker&lt;/code&gt;远程调用的数据结构&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;hr&gt;
&lt;p&gt;之前的版本没法处理&lt;code&gt;crash-test&lt;/code&gt; 重新构建一个。现在有大概率失败，不知道为啥&lt;/p&gt;
&lt;p&gt;程序的大概流程是这样的，我拿drawio摸一个&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://s0.zstatic.net/2023/10/188ad550cb31ae1170b01bc97ee3c355_e31c6ec283e0729e5c7ddb2495b1963fbe26111d.png&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;h3&gt;结构分析&lt;/h3&gt;
&lt;h4&gt;rpc&lt;/h4&gt;
&lt;pre&gt;&lt;code class=&quot;language-go&quot;&gt;type RequestTaskReply struct {
	Task       *Task
	NReduce    int
	DoneStatus bool
}

type ReportTaskResponse struct {
	WorkType  int // 0 map 1 reduce
	WorkId    int
	WorkFiles []string
}

type DoneTaskReply struct {
	X int
}

type TaskArgs struct{}

const (
	MapTask    = 0
	ReduceTask = 1

	Maping    = 0
	Reduceing = 1

	Ready    = 0
	Running  = 1
	Finished = 2
)
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;master&lt;/h4&gt;
&lt;p&gt;对于一个&lt;code&gt;master&lt;/code&gt; 他需要有自己的结构&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-go&quot;&gt;type Master struct {
	MapQue      []*Task // Map任务队列 未完成的
	ReduceQue   []*Task // Reduce任务队列 未完成的
	Files       []string // 要处理的文件
	MapCount    int // Map任务 现在看来是没用了，因为本来是我给MapQue写的是channel后来改成了切片
	ReduceCount int // Reduce任务数量 用于work生成temp文件
	Whiching    int // 正在进行什么任务，map or reduce
	Lock        sync.Mutex // 锁
	WorkID      int // id
	IsDone      bool // 是否完成
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;task&lt;/code&gt;的结构&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-go&quot;&gt;type Task struct {
	TaskType   int // Task的类型 map or reduce
	MapId      int // map任务的id
	ReduceId   int // reduce任务id
	TaskStatus int // 状态 finished running ready
	InputFile  string // 操作的文件
	BeginTime  time.Time // 开始时间，用于处理crash
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;对于处理&lt;code&gt;map&lt;/code&gt;任务和&lt;code&gt;reduce&lt;/code&gt;任务的时候我是这样的：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;一个&lt;code&gt;worker&lt;/code&gt;刚开始的时候，肯定是处理&lt;code&gt;map&lt;/code&gt;任务的，所以在创建&lt;code&gt;work&lt;/code&gt;的时候就把&lt;code&gt;map&lt;/code&gt;任务生成，&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code class=&quot;language-go&quot;&gt;m.Lock.Lock()
for _, file := range files {
	task := Task{
		TaskType:   MapTask,
		InputFile:  file,
		TaskStatus: Ready,
		MapId:      0,
	}
	m.MapQue = append(m.MapQue, &amp;#x26;task)
}
&lt;/code&gt;&lt;/pre&gt;
&lt;ol start=&quot;2&quot;&gt;
&lt;li&gt;当处理完&lt;code&gt;map&lt;/code&gt;任务，就直接把reduce任务生成，供后续去获取任务&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code class=&quot;language-go&quot;&gt;		if flag := func() bool {
			m.Lock.Lock()
			for i := 0; i &amp;#x3C; len(m.MapQue); i++ {
				task := m.MapQue[i]
				if task.TaskStatus != Finished {
					m.Lock.Unlock()
					return false
				}
			}
			m.Lock.Unlock()
			return true
		}(); flag {
			m.Whiching = Reduceing
			m.Lock.Lock()
			for i := 0; i &amp;#x3C; m.ReduceCount; i++ {
				task := Task{
					TaskType:   ReduceTask,
					InputFile:  fmt.Sprintf(&quot;/var/tmp/mr-*-%v&quot;, i),
					TaskStatus: Ready,
					ReduceId:   0,
				}
				m.ReduceQue = append(m.ReduceQue, &amp;#x26;task)
			}
			m.Lock.Unlock()
		}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;对于&lt;code&gt;work&lt;/code&gt;获取任务的时候，肯定是先判断正在处理什么，简易的代码&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-go&quot;&gt;func (m *Master) GetTask(args *TaskArgs, reply *RequestTaskReply) error{
    if m.Done(){
        reply.DoneStatus = true
        return nil
    }
    if m.Whiching == Maping{
        for _, task := range m.MapQue{
            if task.TaskStatus == Ready{
                // 分发
                return nil
            }
        }
        // 根据上面的 判断是否完成map任务并生成reduce任务
    }else{
        for _, task := range m.ReduceQue{
            if task.TaskStatus == Ready{
                // 分发
                return nil
            }
        }
        flag := func(){}() // 判断reduce是否都完成
        if flag{ // 所有任务都已经完成
            reply.DoneStatus = true
			time.Sleep(time.Second * 2)
			m.Lock.Lock()
			m.IsDone = true
			m.Lock.Unlock()
        }
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;work&lt;/code&gt;完成任务肯定是要上报的&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-go&quot;&gt;func (m *Master) ReportWorkDone(args *ReportTaskResponse, reply *DoneTaskReply) error{
    if args.WorkType == MapTask{
        for task := range m.MapQue{
            // 先判断任务是否超时
            if task.MapId == mapWorkId &amp;#x26;&amp;#x26; task.TaskStatus == Running{
                //把temp文件移到工作目录
                task.TaskStatus = Finished
				for _, file := range args.WorkFiles {
					src, err := os.Open(file)
                    defer src.Close()
					if err != nil {
						log.Fatal( err)
					}
					DIR, _ := os.Getwd()
					filename := filepath.Base(file)
					f := DIR + &quot;/&quot; + filename
					dst, err := os.Create(f)
                    defer dst.Close()
					if err != nil {
						log.Fatal(err)
					}
					_, err = io.Copy(dst, src)
					if err != nil {
						log.Fatal(err)
					}
				}
				break
            }
        }
    }else{
        // 同上
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;对于处理&lt;code&gt;crash&lt;/code&gt;任务，我的想法是开一个协程，定时扫描正在进行的任务类型的所有任务，如果超时10s以上，那么直接把任务状态设置为Ready等待分发即可，但是就是因为这个&lt;code&gt;crash-test&lt;/code&gt;，我写了两个版本，第二个版本有时候可以pass，&lt;/p&gt;
&lt;p&gt;&lt;code&gt;crash.go&lt;/code&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-go&quot;&gt;func maybeCrash() {
	max := big.NewInt(1000)
	rr, _ := crand.Int(crand.Reader, max)
	if rr.Int64() &amp;#x3C; 330 {
		// crash!
		os.Exit(1)
	} else if rr.Int64() &amp;#x3C; 660 {
		// delay for a while.
		maxms := big.NewInt(10 * 1000)
		ms, _ := crand.Int(crand.Reader, maxms)
		time.Sleep(time.Duration(ms.Int64()) * time.Millisecond)
	}
}

&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;可以看出，有 1/3几率直接退出，1/3几率等到0-10s，1/3几率直接开始。&lt;/p&gt;
&lt;p&gt;我的想法是，开一个协程，每两秒扫一下&lt;code&gt;running&lt;/code&gt;的任务，如果超时了，就把状态设置&lt;code&gt;Ready&lt;/code&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-go&quot;&gt;func (m *Master) HeartBeatForTimeOut() { // 非心跳机制
	for !m.Done() {
		time.Sleep(2 * time.Second)
		m.Lock.Lock()
		if m.Whiching == Maping {
			for _, task := range m.MapQue {
				if task.TaskStatus == Running &amp;#x26;&amp;#x26; (time.Since(task.BeginTime) &gt; time.Duration(10*time.Second)) {
		task.TaskStatus = Ready
	}
			}
		} else {
			for _, task := range m.ReduceQue {
				if task.TaskStatus == Running &amp;#x26;&amp;#x26; (time.Since(task.BeginTime) &gt; time.Duration(10*time.Second)) {
		task.TaskStatus = Ready
	}
			}
		}
		m.Lock.Unlock()
	}
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;work&lt;/h4&gt;
&lt;p&gt;&lt;code&gt;work&lt;/code&gt;结构&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-go&quot;&gt;type OneWorker struct {
	WorkerID int // 工作 id
	Mapf     func(string, string) []KeyValue // 程序提供的Mapf
	Reducef  func(string, []string) string // 程序提供的Reducef
	Task     *Task // 执行的任务
	NReduce  int // educe任务的数量
	IsDone   bool // 是否完成
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;初始化的时候，直接&lt;code&gt;for {}&lt;/code&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-go&quot;&gt;func Worker(mapf func(string, string) []KeyValue, reducef func(string, []string) string) {
	worker := OneWorker{
		WorkerID: -1,
		Mapf:     mapf,
		Reducef:  reducef,
		IsDone:   false,
	}
	for !worker.IsDone {
		worker.work()
	}
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;对于一个&lt;code&gt;work&lt;/code&gt;工作，在流程就是 获取任务 -&gt; 处理任务 -&gt; 上报结果 -&gt; 获取任务 ....&lt;/p&gt;
&lt;p&gt;所以在work()里面就按照这个流程去工作&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-go&quot;&gt;func (work *OneWorker) work() {
	task := work.RequireTask() // 获取任务
	if task != nil {
		work.Task = task
		if task.TaskType == MapTask {
			work.WorkerID = task.MapId
			intermediate := work.generateMapResult(task) // 调用mapf生成结果
			file := work.writeMapTempFiles(intermediate) // 写到文件里面
			work.TaskDone(file, MapTask) // 上报
		} else {
			work.WorkerID = task.ReduceId
			intermediate := work.generatorReduceResult(task.InputFile) // 调用reducef
			files := work.writeReduceTempFiles(task, intermediate) // 写到文件
			work.TaskDone(files, ReduceTask) // 上报
		}
	}
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;获取任务&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-go&quot;&gt;func (work *OneWorker) RequireTask() *Task {
	args := TaskArgs{}
	reply := RequestTaskReply{}
	call(&quot;Master.GetTask&quot;, &amp;#x26;args, &amp;#x26;reply)
	if &amp;#x26;reply != nil {
		if reply.DoneStatus { // 如果任务都完成了
			work.IsDone = true // 当前任务不需要处理
			return nil
		}
		work.NReduce = reply.NReduce
		return reply.Task
	} else {
		return nil
	}
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;调用&lt;code&gt;mapf/reducef&lt;/code&gt;生成结果&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-go&quot;&gt;func (work *OneWorker) generateMapResult(task *Task) []KeyValue {
	NowIntermediate := make([]KeyValue, 0) // 中间文件
	file, err := os.Open(task.InputFile)
	if err != nil {
		log.Fatal(err)
	}
	content, err := ioutil.ReadAll(file)
	if err != nil {
		log.Fatal(err)
	}
	file.Close()
	kva := work.Mapf(task.InputFile, string(content))
	NowIntermediate = append(NowIntermediate, kva...)
	return NowIntermediate
}

func (work *OneWorker) generatorReduceResult(task *Task) []KeyValue {
	intermedia := make([]KeyValue, 0)
	files, err := filepath.Glob(task.InputFile)
	if err != nil {
		log.Fatal(err)
	}
	for _, filepath := range files {
		file, err := os.Open(filepath)
		if err != nil {
			log.Fatal(err)
		}
		dec := json.NewDecoder(file)
		for {
			var kv KeyValue
			if err := dec.Decode(&amp;#x26;kv); err != nil {
				break
			}
			intermedia = append(intermedia, kv)
		}
	}
    // mrsequential
	sort.Sort(ByKey(intermedia))
	res := make([]KeyValue, 0)
	i := 0
	for i &amp;#x3C; len(intermedia) {
		j := i + 1
		for j &amp;#x3C; len(intermedia) &amp;#x26;&amp;#x26; intermedia[j].Key == intermedia[i].Key {
			j++
		}
		values := make([]string, 0)
		for k := i; k &amp;#x3C; j; k++ {
			values = append(values, intermedia[k].Value)
		}
		//defer work.CrashRecover()
		output := work.Reducef(intermedia[i].Key, values)
		kv := KeyValue{Key: intermedia[i].Key, Value: output}
		res = append(res, kv)
		i = j
	}
	return res
}

&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;生成文件的时候，按照提示：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;map任务
&lt;ul&gt;
&lt;li&gt;先利用ioutil.TempFile生成临时文件，等&lt;code&gt;Json&lt;/code&gt;处理完，在&lt;code&gt;Rename&lt;/code&gt;成&lt;code&gt;mr-X-Y&lt;/code&gt;格式的文件&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;Reduce任务同上&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;任务上报 直接报就行&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-go&quot;&gt;func (work *OneWorker) TaskDone(files []string, Type int) {
	args := ReportTaskResponse{
		WorkType:  Type,
		WorkId:    work.WorkerID,
		WorkFiles: files,
	}
	reply := DoneTaskReply{}
	call(&quot;Master.ReportWorkDone&quot;, &amp;#x26;args, &amp;#x26;reply)
}

&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;差不多就这些了&lt;/p&gt;</content:encoded><h:img src="undefined"/><enclosure url="undefined"/></item><item><title>SpringBoot注解整理</title><link>https://blog.myyrh.com/blog/springboot-anotation</link><guid isPermaLink="true">https://blog.myyrh.com/blog/springboot-anotation</guid><description>整理了一些常用和用的不熟练的注解。</description><pubDate>Sat, 25 Nov 2023 00:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;SpringBoot 核心注解&lt;/h2&gt;
&lt;h3&gt;@SpringBootApplication&lt;/h3&gt;
&lt;p&gt;标识这是一个springboot应用，这个注解是&lt;a href=&quot;####SpringBootConfiguration&quot;&gt;SpringBootConfiguration&lt;/a&gt; &lt;a href=&quot;####EnableAutoConfiguration&quot;&gt;EnableAutoConfiguration&lt;/a&gt;&lt;a href=&quot;@@@@CompoentScan&quot;&gt;CompoentScan&lt;/a&gt;的结合体。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;@SpringBootApplication
public class MyApplication{
    public static void main(String[] args){
        SpringApplication.run(MyApplication.class, args);
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;@Configuration&lt;/h3&gt;
&lt;p&gt;用它来代替&lt;code&gt;applicationContext.xml&lt;/code&gt;文件，可以在这个注解下进行配置。通常用来自定义Bean&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;@Configuration
public class MyConfig{
    @Bean
    public Mybean bean(){
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;@SpringBootConfiguration&lt;/h3&gt;
&lt;p&gt;是 &lt;a href=&quot;####Configuration&quot;&gt;Configuration&lt;/a&gt; 注解的变体，用来修饰 springboot的配置。&lt;/p&gt;
&lt;h3&gt;@EnableAutoConfiguration&lt;/h3&gt;
&lt;p&gt;允许springboot自动配置注解，开启之后，springboot会在自动配置Bean&lt;/p&gt;
&lt;h3&gt;CompoentScan&lt;/h3&gt;
&lt;p&gt;自动扫描@Compoent注解，包括@Service @Controller @Repository注册bean到context&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;@SpringBootApplication
@CompoentScan(basePackage=&quot;&quot;)
public class MyApplication{
    public static void main(String[] args){
        SpringApplication.run(MyApplication.class, args);
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;@Autowired&lt;/h3&gt;
&lt;p&gt;可以自动把一个Bean注入到当前类中。默认情况下是根据类型进行匹配，可以用@Qualifier 进行限制&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;@Compoent
public class MyCompoent{
    @Autowired
    private UserImpl userImpl;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;@Value&lt;/h3&gt;
&lt;p&gt;从配置文件或者环境变量获取值。&lt;/p&gt;
&lt;h3&gt;@Bean&lt;/h3&gt;
&lt;p&gt;用于定义一个Bean，放到IOC容器中。一般在@Configuration中使用。&lt;/p&gt;
&lt;h2&gt;原型注解&lt;/h2&gt;
&lt;h3&gt;@Scope&lt;/h3&gt;
&lt;p&gt;常用于设置Bean的作用域，包括 Singleton、Prototype、Request、Session 等。默认情况下，Bean 是 Singleton 的。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Singleton: 默认，单例模式，全局仅有一个实例&lt;/li&gt;
&lt;li&gt;Prototype： 原型模式：每次获取Bean，都会有一个实例&lt;/li&gt;
&lt;li&gt;Request: 针对每一次Http都会创建一个实例，同时该实例仅在当前的Http Requset有效&lt;/li&gt;
&lt;li&gt;Session: 针对每次HTTP请求都会创建一个实例，同时该实例仅在当前的HTTP Session中有效&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;直接用字符串会有问题，用默认的参数：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;ConfigurableBeanFactory.SCOPE_PROTOTYPE&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;ConfigurableBeanFactory.SCOPE_SINGLETON&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;WebApplicationContext.SCOPE_REQUEST&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;WebApplicationContext.SCOPE_SESSION&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;大部分业务直接用Singleton就可以，但是如果实例内有非静态变量，就会导致线程安全问题。
设置为Prototype的时候，每次连接都会生成一个实例，GC频繁，性能下降。&lt;/p&gt;
&lt;h3&gt;@Lazy&lt;/h3&gt;
&lt;p&gt;用于设置Bean的延迟初始化。当IOC从XML文件、java配置或其他方式加载的时候，不会初始化这个Bean。&lt;/p&gt;
&lt;h3&gt;@DependsOn&lt;/h3&gt;
&lt;p&gt;表示Bean之间的依赖关系。IOC启动的时候，先创建@DependsOn标识的Bean。&lt;/p&gt;
&lt;h2&gt;SpringWeb常用注解&lt;/h2&gt;
&lt;h3&gt;@Controller&lt;/h3&gt;
&lt;p&gt;标志着类是一个SpringMVC控制器，用来处理HTTP请求&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;@Controller
public class MyController{}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;@RestController&lt;/h3&gt;
&lt;p&gt;他是 @Controller 和 @ResponseBody，用于创建Restful风格的控制器，表示控制器所有方法返回的都是&lt;code&gt;Json&lt;/code&gt;格式的。&lt;/p&gt;
&lt;h3&gt;@RequsetMapping&lt;/h3&gt;
&lt;p&gt;用于映射Http请求，可以用&lt;code&gt;value&lt;/code&gt;属性指定URL请求，可以用&lt;code&gt;Method&lt;/code&gt;指定请求方法。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;@RestController
@RequsetMapping(&quot;/v1&quot;)
publi class ApiController{

}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;@GetMapping&lt;/h3&gt;
&lt;p&gt;相当于 @RequsetMapping(method=RequestMethod.GET)&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;@RestController
@RequsetMapping(&quot;/v1&quot;)
publi class ApiController{
    @GetMapping(&quot;/hello&quot;)
    public void handleHello(){}
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;@PostMapping&lt;/h3&gt;
&lt;p&gt;相当于 @RequsetMapping(method=RequsetMethod.Post)&lt;/p&gt;
&lt;h3&gt;@PutMapping&lt;/h3&gt;
&lt;p&gt;同上&lt;/p&gt;
&lt;h3&gt;@DeleteMapping&lt;/h3&gt;
&lt;p&gt;同上&lt;/p&gt;
&lt;h3&gt;@RequestParma&lt;/h3&gt;
&lt;p&gt;用于将请求参数映射到方法的参数当中，可以使用&lt;code&gt;value&lt;/code&gt;属性指定参数名，&lt;code&gt;required&lt;/code&gt;指定是否必填，&lt;code&gt;defaultValue&lt;/code&gt;指定默认值&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;@RestController
@RequsetMapping(&quot;/v1&quot;)
publi class ApiController{
    @GetMapping(&quot;/hello&quot;)
    public String handleHello(@RequestParma(&quot;name&quot;) string name){
        return &quot;hello &quot; + name;
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;@PathVariable&lt;/h3&gt;
&lt;p&gt;用于把URL的占位符映射到方法的参数中。可以使用&lt;code&gt;value&lt;/code&gt;属性指定占位符名称。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;@RestController
@RequsetMapping(&quot;/v1&quot;)
publi class ApiController{
    @GetMapping(&quot;/{name}&quot;)
    public String handleName(@PathVariable(&quot;name&quot;) String name){

    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;@RequestBody&lt;/h3&gt;
&lt;p&gt;用于从请求体中获取数据。通常处理Post，Put请求&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;@RestController
@RequestMapping(&quot;/api&quot;)
public class MyController {
    @PostMapping(&quot;/greeting&quot;)
    public String greeting(@RequestBody GreetingRequest request) {
        return &quot;Hello, &quot; + request.getName() + &quot;!&quot;;
    }
}

@Data
public class GreetingRequest {
    private String name;
}

&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;@ResponseBody&lt;/h3&gt;
&lt;p&gt;注解表示该方法返回的结果直接输出到响应体中。&lt;/p&gt;
&lt;h3&gt;@ResponseStatus&lt;/h3&gt;
&lt;p&gt;注解用于指定请求处理完成后的状态码。&lt;/p&gt;
&lt;h3&gt;@Cacheable&lt;/h3&gt;
&lt;p&gt;表示该方法的结果被缓存，假如参数相同，就调用缓存的结果。&lt;/p&gt;
&lt;h3&gt;@CachePut&lt;/h3&gt;
&lt;p&gt;表示方法的结果应该被缓存起来，下次调用该方法时，不会返回缓存结果，而是重新计算结果并缓存起来。&lt;/p&gt;
&lt;h3&gt;@CacheEvict&lt;/h3&gt;
&lt;p&gt;表示方法执行后从缓存中删除指定项。&lt;/p&gt;
&lt;h2&gt;测试注解&lt;/h2&gt;
&lt;h3&gt;@SpringBootTest&lt;/h3&gt;
&lt;p&gt;它会创建一个完整的 Spring 应用程序上下文，并在测试期间使用它&lt;/p&gt;
&lt;h2&gt;数据库相关&lt;/h2&gt;
&lt;h3&gt;@Transactional&lt;/h3&gt;
&lt;p&gt;用于指定一个方法需要在事务中执行。默认情况下，只有 RuntimeException 会触发事务回滚。&lt;/p&gt;
&lt;h3&gt;@Reposity&lt;/h3&gt;
&lt;p&gt;用于标记数据访问层，表示这个类是一个数据仓库&lt;/p&gt;
&lt;h3&gt;@Entity&lt;/h3&gt;
&lt;p&gt;用于标记实体类，表示这个类是一个JPA实体，与数据库中的表对应。&lt;/p&gt;
&lt;h3&gt;@Id&lt;/h3&gt;
&lt;p&gt;标记主键字段&lt;/p&gt;
&lt;h3&gt;@GeneratedValue&lt;/h3&gt;
&lt;p&gt;指定主键的生成策略。&lt;/p&gt;
&lt;h3&gt;@Column&lt;/h3&gt;
&lt;p&gt;指定实体属性和数据库表之间的映射关系。&lt;/p&gt;</content:encoded><h:img src="undefined"/><enclosure url="undefined"/></item><item><title>实现一个分布式缓存</title><link>https://blog.myyrh.com/blog/go-cache</link><guid isPermaLink="true">https://blog.myyrh.com/blog/go-cache</guid><description>最近看教程和groupcache，糊了一个非RPC通信的分布式缓存，简单总结一下。</description><pubDate>Thu, 02 Nov 2023 00:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;分布式缓存&lt;/h2&gt;
&lt;p&gt;缓存非常常见，比如网站的CDN缓存，Redis集群等。&lt;/p&gt;
&lt;p&gt;对于类似map的&lt;code&gt;k-v&lt;/code&gt; 缓存策略，大概有几个问题&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;内存不够？
&lt;ul&gt;
&lt;li&gt;对于这个问题，我们可以采取一个策略，对于被策略淘汰的键值，我们在新增键值的时候替换掉即可。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;性能不够？
&lt;ul&gt;
&lt;li&gt;这个可以通过多机分布式来实现，但是我写的没有实现多机，只实现了多进程。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;负载均衡？
&lt;ul&gt;
&lt;li&gt;我只实现了一致性哈希，应该还需要一个&lt;code&gt;master&lt;/code&gt;来调控，我没写....`&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;缓存击穿？
&lt;ul&gt;
&lt;li&gt;实现思路大概就是，对于多次相同的请求，给一个缓冲，实现只处理一次请求，然后多次返回这个返回值。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;DO or TODO
&lt;ul&gt;
&lt;li&gt;[x] LRU&lt;/li&gt;
&lt;li&gt;[x] 单机并发、分布式节点&lt;/li&gt;
&lt;li&gt;[x] 防止缓存击穿&lt;/li&gt;
&lt;li&gt;[ ] PRC&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;实现&lt;/h2&gt;
&lt;h3&gt;LRU淘汰策略&lt;/h3&gt;
&lt;p&gt;LRU 顾名思义 &lt;code&gt;Least Recently Used&lt;/code&gt; ， 最近最少使用，相对于&lt;code&gt;LFR&lt;/code&gt;和&lt;code&gt;FIFO&lt;/code&gt;来说，应该是最适合的。&lt;/p&gt;
&lt;p&gt;我们维护一个队列，对于使用过的，就放到队列最前面，显而易见，淘汰的时候只需要把队尾删掉即可。&lt;/p&gt;
&lt;h4&gt;数据结构&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;map&lt;/code&gt;： 用来存储缓存的键值&lt;/li&gt;
&lt;li&gt;双向链表： 把所有的key映射到上面，这样的操作都是&lt;code&gt;O(1)&lt;/code&gt;的&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code class=&quot;language-go&quot;&gt;type Value interface { // Value 范型
	Len() int
}

type Entry struct { // 双向链表上面的
	key   string
	value Value
}

type Cache struct {
	maxBytes     int64 // 最大的容量
	nBytes       int64 // 当前已经使用的
	List         *list.List // 双向链表
	cache        map[string]*list.Element // 映射的数据
	handleRemove func(key string, value Value) // 自定义remove 类似于 删掉之后的后续操作
}

//new操作
func New(maxBytes int64, handleRemove func(string, Value)) *Cache {
	return &amp;#x26;Cache{
		maxBytes:     maxBytes,
		List:         list.New(),
		cache:        make(map[string]*list.Element),
		handleRemove: handleRemove,
	}
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;查找&lt;/h4&gt;
&lt;p&gt;查找操作就是，直接在&lt;code&gt;list&lt;/code&gt;上面查找，找到之后就把&lt;code&gt;*list.element&lt;/code&gt;移动到维护的队头。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-go&quot;&gt;func (c *Cache) Get(key string) (Value, bool) {
	if value, ok := c.cache[key]; ok {
		c.List.MoveToFront(value)
		kv := value.Value.(*Entry) // 断言
		return kv.value, true
	}
	return nil, false
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;删除&lt;/h4&gt;
&lt;p&gt;删除操作就是，查找队尾，如果不为空就remove，然后更新一下信息。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-go&quot;&gt;func (c *Cache) Delete() {
	ele := c.List.Back()
	if ele != nil {
		c.List.Remove(ele)
		kv := ele.Value.(*Entry)
		delete(c.cache, kv.key)
		c.nBytes -= int64(len(kv.key)) + int64(kv.value.Len())
		if c.handleRemove != nil {
			c.handleRemove(kv.key, kv.value)
		}
	}
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;添加&lt;/h4&gt;
&lt;p&gt;添加的时候，基本思路就是，如果存在，就更新value，如果不存在就扔到对头，然后执行淘汰。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-go&quot;&gt;func (c *Cache) Add(key string, value Value) {
	if ele, ok := c.cache[key]; ok {
		c.List.MoveToFront(ele)
		kv := ele.Value.(*Entry)
		c.nBytes += int64(len(kv.key)) + int64(kv.value.Len())
		kv.value = value
	} else {
		ele := c.List.PushFront(&amp;#x26;Entry{key, value})
		c.cache[key] = ele
		c.nBytes += int64(len(key)) + int64(value.Len())
	}
	for c.maxBytes != 0 &amp;#x26;&amp;#x26; c.maxBytes &amp;#x3C; c.nBytes {
		c.Delete()
	}
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;一致性哈希&lt;/h3&gt;
&lt;p&gt;为什么要使用一致性哈希？&lt;/p&gt;
&lt;p&gt;我们考虑这么一个问题，在分布式缓存中，当一个节点接收到一个请求，但是该节点并没有存储缓存值，那么他要面临：从谁那里获取数据。 我们假设一共有五个节点。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;随机选择节点： 每次将会有$\frac{1}{5}$ 的概率， 效率很低&lt;/li&gt;
&lt;li&gt;自定义哈希： 我们对于每一个&lt;code&gt;key&lt;/code&gt; ，利用某种算法，转成整数，然后 $% (sum(serve))$ 得到一个分布式节点的id，但是这种算法有个明显的缺陷是，假如有节点下线了，那么所有缓存都会失效&lt;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;算法原理&lt;/h4&gt;
&lt;p&gt;&lt;img src=&quot;https://s0.zstatic.net/2023/11/f9ecedf97396f4ce900fd4bf973d7b17_5f458cbd019109256c89bca9e9d9b005c030227f.jpg&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;h5&gt;步骤&lt;/h5&gt;
&lt;p&gt;一致性哈希维护一个环，他把&lt;code&gt;key&lt;/code&gt;映射到$2^{32}$大小的环上&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;计算 节点 的哈希值，放到环上&lt;/li&gt;
&lt;li&gt;计算&lt;code&gt;key&lt;/code&gt;的哈希值，放到环上，顺时针找到的第一个节点，就是应该选取的节点。&lt;/li&gt;
&lt;/ul&gt;
&lt;h5&gt;数据倾斜&lt;/h5&gt;
&lt;p&gt;假如系统节点太少，很容易导致&lt;code&gt;key&lt;/code&gt;的倾斜。例如上面例子中的 peer2，peer4，peer6 分布在环的上半部分，下半部分是空的。那么映射到环下半部分的 key 都会被分配给 peer2，key 过度向 peer2 倾斜，缓存节点间负载不均。&lt;/p&gt;
&lt;p&gt;应对这个问题，引出了虚拟节点的概念，一个真实节点对应多个虚拟节点。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-go&quot;&gt;type Hash func(data []byte) uint32

type Map struct {
	hash     Hash // 自定义哈希算法
	replicas int            //虚拟节点倍数
	keys     []int          // 哈希环
	hasMap   map[int]string // 虚拟节点和真实节点
}

func NewHash(replicas int, has Hash) *Map {
	m := &amp;#x26;Map{
		hash:     has,
		replicas: replicas,
		keys:     make([]int, 0),
		hasMap:   make(map[int]string),
	}
	if m.hash == nil {
		m.hash = crc32.ChecksumIEEE
	}
	return m
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;添加节点&lt;/h4&gt;
&lt;pre&gt;&lt;code class=&quot;language-go&quot;&gt;func (m *Map) Add(keys ...string) {
	for _, key := range keys {
		for i := 0; i &amp;#x3C; m.replicas; i++ {
			hash := int(m.hash([]byte(strconv.Itoa(i) + key)))
			m.keys = append(m.keys, hash)
			m.hasMap[hash] = key
		}
	}
	sort.Ints(m.keys)
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;获取节点&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-go&quot;&gt;
func (m *Map) Get(key string) string {
	if len(m.keys) == 0 {
		return &quot;&quot;
	}
	hash := int(m.hash([]byte(key)))
	idx := sort.Search(len(m.keys), func(i int) bool {
		return m.keys[i] &gt;= hash
	})
	return m.hasMap[m.keys[idx%len(m.keys)]]
}
&lt;/code&gt;&lt;/pre&gt;</content:encoded><h:img src="undefined"/><enclosure url="undefined"/></item><item><title>第一次ICPC游记</title><link>https://blog.myyrh.com/blog/myfirsticpc</link><guid isPermaLink="true">https://blog.myyrh.com/blog/myfirsticpc</guid><description>纪念第一次参加acm省赛，遗憾铜牌收尾</description><pubDate>Thu, 23 Jun 2022 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;第一次参加，摆烂很久了，要不是因为我眼瞎可以拿个银的，唉，还是菜，明年继续。&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://www.helloimg.com/image/ZoXCGq&quot;&gt;&lt;img src=&quot;https://www.helloimg.com/images/2022/05/23/ZoXCGq.png&quot; alt=&quot;ZoXCGq.png&quot;&gt;&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;早在几个月之前，&lt;code&gt;wqy&lt;/code&gt;就拉我组队，我去了肯定是拖后腿啊哈哈哈哈。&lt;/p&gt;
&lt;p&gt;比赛前几次的训练赛我们都没参加，主要是疫情封校学长进不来，我们以为就我俩参加，没啥信心了就摆烂。&lt;/p&gt;
&lt;p&gt;我自己也是基本都不写&lt;code&gt;acm&lt;/code&gt;题目了，天天写&lt;code&gt;java&lt;/code&gt;，写单子赚钱，写作业。&lt;/p&gt;
&lt;p&gt;考试之前我居然都不知道自己的队名，在见过一次面的&lt;code&gt;wqy&lt;/code&gt;的指引下找到了我们的桌子，在最后面，旁边的空调坏了，不过旁边桌子是大三学长，都是大佬，可以听听他们的思路哈哈哈哈。虽然他们失利打铁了。&lt;/p&gt;
&lt;p&gt;比赛开始之后我们看了看题，昏沉沉的，觉得&lt;code&gt;A&lt;/code&gt;能开，就开&lt;code&gt;A&lt;/code&gt;，发现我们没思路也不会，然后看后面的题，都是一眼会然后被否，以至于快&lt;code&gt;12&lt;/code&gt;点我们还没写代码，就没信心啦，都昏昏沉沉，然后我去上了个厕所，回来之后也不会。&lt;/p&gt;
&lt;p&gt;别的队伍已经最多的过了俩题了，我们还没过，以为得爆零了，就一点斗志也没了。&lt;/p&gt;
&lt;p&gt;&lt;code&gt;12&lt;/code&gt;点多我突然会写&lt;code&gt;A&lt;/code&gt;了，然后开写，我的做法是纯加法+打表，但是打表的时候我把&lt;code&gt;19&lt;/code&gt;写成&lt;code&gt;18&lt;/code&gt;了，导致调了很久都没发现，交了得&lt;code&gt;10&lt;/code&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;鲁哥他们还是比较可惜的，实力都很强但是没做出来。&lt;/p&gt;
&lt;p&gt;明年加油，保银争金！&lt;/p&gt;</content:encoded><h:img src="undefined"/><enclosure url="undefined"/></item><item><title>服务器搭建Hexo博客</title><link>https://blog.myyrh.com/blog/hexoforserver</link><guid isPermaLink="true">https://blog.myyrh.com/blog/hexoforserver</guid><description>因为最近拿服务器搞了`hexo`,索性写个教程吧，`hexo`可以搞在`GitHubPages`或者`Heroku`啥的，但是毕竟访问速度慢嘛。</description><pubDate>Thu, 24 Mar 2022 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;我服务器是&lt;code&gt;腾讯云轻量hk&lt;/code&gt;，搭建之后访问速度还是很快的。比上&lt;code&gt;cdn&lt;/code&gt;的&lt;code&gt;Typecho&lt;/code&gt; 快多了
&lt;a href=&quot;https://www.helloimg.com/image/R3BccT&quot;&gt;&lt;img src=&quot;https://www.helloimg.com/images/2022/03/24/R3BccT.png&quot; alt=&quot;R3BccT.png&quot;&gt;&lt;/a&gt;&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;一、在本地Windows上搭建&lt;a href=&quot;https://hexo.io/&quot;&gt;Hexo&lt;/a&gt;&lt;/h2&gt;
&lt;h3&gt;1.1 配置本地&lt;code&gt;node&lt;/code&gt;环境&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;node&lt;/code&gt;下载 &lt;a href=&quot;https://nodejs.org/zh-cn/download/&quot;&gt;官网下载&lt;/a&gt;，可以的话推荐使用 &lt;code&gt;nvm&lt;/code&gt; 安装&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;打开&lt;code&gt;cmd&lt;/code&gt;查看&lt;code&gt;node&lt;/code&gt;安装情况&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-shell&quot;&gt;node -v
npm -v
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;配置全局环境&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;进入安装目录，创建文件夹&lt;code&gt;node_global&lt;/code&gt;和&lt;code&gt;node_cache&lt;/code&gt; 并执行&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-shell&quot;&gt;npm config set prefix &quot;D:\programming\nodejs\node_global&quot;
npm config set cache &quot;D:\programming\nodejs\node_cache&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;环境配置：新增环境变量&lt;code&gt;NODE_PATH&lt;/code&gt;和添加&lt;code&gt;Path&lt;/code&gt;，两个值都为 &lt;code&gt;\node_global&lt;/code&gt;的目录位置&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;安装&lt;code&gt;hexo-cli&lt;/code&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-shell&quot;&gt;npm i hexo-cli -g
hexo
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;a href=&quot;https://www.helloimg.com/image/R3BzSS&quot;&gt;&lt;img src=&quot;https://www.helloimg.com/images/2022/03/24/R3BzSS.png&quot; alt=&quot;R3BzSS.png&quot;&gt;&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;随便找个地方初始化文件，执行如下命令：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-shell&quot;&gt;mkdir hexo-blog
cd hexo-blog &amp;#x26;&amp;#x26; npm init -y
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;1.2初始化&lt;code&gt;hexo&lt;/code&gt;项目&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;在 中的&lt;code&gt;hexo-blog&lt;/code&gt;文件下初始化&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-shell&quot;&gt;hexo init myblog &amp;#x26;&amp;#x26; cd myblog
npm i
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;下载主题&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;这里我用的是&lt;code&gt;next&lt;/code&gt;主题，其他同理&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code class=&quot;language-shell&quot;&gt;git clone https://github.com/iissnan/hexo-theme-next themes/next
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;在&lt;strong&gt;本地配置文件&lt;/strong&gt;&lt;code&gt;_confing.yml&lt;/code&gt;中设置&lt;code&gt;theme&lt;/code&gt;属性&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://www.helloimg.com/image/R3Bu5P&quot;&gt;&lt;img src=&quot;https://www.helloimg.com/images/2022/03/24/R3Bu5P.png&quot; alt=&quot;R3Bu5P.png&quot;&gt;&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;本地执行&lt;code&gt;hexo&lt;/code&gt;项目，添加&lt;code&gt;start&lt;/code&gt;脚本&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-shell&quot;&gt;&quot;start&quot; : &quot;hexo clean &amp;#x26;&amp;#x26; hexo g &amp;#x26;&amp;#x26; hexo s&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;a href=&quot;https://www.helloimg.com/image/R3C5dn&quot;&gt;&lt;img src=&quot;https://www.helloimg.com/images/2022/03/24/R3C5dn.png&quot; alt=&quot;R3C5dn.png&quot;&gt;&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;然后执行命令&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-shell&quot;&gt;npm run start
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;打开 &lt;a href=&quot;http://localhost:4000/&quot;&gt;http://localhost:4000&lt;/a&gt; 验证效果吧&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;1.3&lt;code&gt;git&lt;/code&gt;环境搭建&lt;/h3&gt;
&lt;p&gt;​&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;git&lt;/code&gt;安装：&lt;a href=&quot;https://git-scm.com/downloads&quot;&gt;官网下载&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;生成&lt;code&gt;ssh&lt;/code&gt;认证，执行如下命令&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-shell&quot;&gt;git config --global user.name &quot;yourname&quot;
git config --global user.email youremail@example.com
ssh-keygen -t rsa -C &quot;youremail@example.com&quot;
git config --global core.autocrlf false  // 禁用自动转换，
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;最后获取到的&lt;code&gt;ssh&lt;/code&gt;认证在&lt;code&gt;C:\Users\yourname\.ssh&lt;/code&gt;中&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;二、服务器配置&lt;/h2&gt;
&lt;p&gt;这个之前写过来着&lt;/p&gt;
&lt;p&gt;教程是&lt;code&gt;Centos&lt;/code&gt;服务器，&lt;code&gt;Ubuntu&lt;/code&gt; &lt;code&gt;Debian&lt;/code&gt; 命令自行更换即可。&lt;/p&gt;
&lt;h3&gt;1.1 搭建远程&lt;code&gt;Git&lt;/code&gt;私库&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;在服务器安装 &lt;code&gt;git&lt;/code&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-shell&quot;&gt;git --version // 如无，则安装
yum install curl-devel expat-devel gettext-devel openssl-devel zlib-devel perl-devel
yum install -y git
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;创建用户并配置其仓库&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-shell&quot;&gt;useradd git
passwd git // 设置密码
su git // 这步很重要，不切换用户后面会很麻烦
cd /home/git/
mkdir -p projects/blog // 项目存在的真实目录
mkdir repos &amp;#x26;&amp;#x26; cd repos
git init --bare blog.git // 创建一个裸露的仓库
cd blog.git/hooks
vi post-receive // 创建 hook 钩子函数，输入了内容如下
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code class=&quot;language-shell&quot;&gt;#!/bin/sh
git --work-tree=/home/git/projects/blog --git-dir=/home/git/repos/blog.git checkout -f
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;添加完毕后修改权限&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-shell&quot;&gt;chmod +x post-receive
exit // 退出到 root 登录
chown -R git:git /home/git/repos/blog.git // 添加权限
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;测试&lt;code&gt;git仓库&lt;/code&gt;是否可用，另找空白文件夹&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-shell&quot;&gt;git clone git@server_ip:/home/git/repos/blog.git
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;如果能把空仓库拉下来，就说明 git 仓库搭建成功了&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://www.helloimg.com/image/R3CqYT&quot;&gt;&lt;img src=&quot;https://www.helloimg.com/images/2022/03/24/R3CqYT.png&quot; alt=&quot;R3CqYT.png&quot;&gt;&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;建立&lt;code&gt;ssh&lt;/code&gt;信任关系，在&lt;strong&gt;本地电脑&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-shell&quot;&gt;ssh-copy-id -i C:/Users/yourname/.ssh/id_rsa.pub git@server_ip
ssh git@server_ip // 测试能否登录
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;注&lt;/strong&gt;：此时的 ssh 登录 git 用户不需要密码！否则就&lt;strong&gt;有错&lt;/strong&gt;，请仔细重复步骤 3-5&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;为了安全起见禁用 git 用户的 shell 登录权限，从而只能用 git clone，git push 等登录&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-shel&quot;&gt;cat /etc/shells // 查看 git-shell 是否在登录方式里面
which git-shell // 查看是否安装
vi /etc/shells
添加上2步显示出来的路劲，通常在 /usr/bin/git-shell
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;修改&lt;code&gt;/etc/passwd&lt;/code&gt;中的权限&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-shell&quot;&gt;// 将原来的
git:x:1000:1000::/home/git:/bin/bash

// 修改为
git:x:1000:1000:,,,:/home/git:/usr/bin/git-shell
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;1.2搭建&lt;code&gt;nginx&lt;/code&gt;服务器&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;下载并安装&lt;code&gt;nginx&lt;/code&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-shell&quot;&gt;cd /usr/local/src
wget http://nginx.org/download/nginx-1.15.2.tar.gz
tar xzvf nginx-1.15.2.tar.gz
cd nginx-1.15.2
./configure // 如果后面还想要配置 SSL 协议，就执行后面一句！
./configure --prefix=/usr/local/nginx --with-http_stub_status_module --with-http_ssl_module --with-file-aio --with-http_realip_module
make &amp;#x26;&amp;#x26; make install
alias nginx=&apos;/usr/local/nginx/sbin/nginx&apos; // 为 nginx 取别名，后面可直接用
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;配置&lt;code&gt;nginx&lt;/code&gt;文件&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;先启动是否安装成功&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-shell&quot;&gt;nginx // 直接来！浏览器查看 server_ip，默认是 80 端口
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;修改配置文件&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-shell&quot;&gt;nginx -s stop // 先停止nginx
cd /usr/local/nginx/conf
vi nginx.conf
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;修改&lt;code&gt;root&lt;/code&gt;解析路径，如下图&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://imgconvert.csdnimg.cn/aHR0cHM6Ly91c2VyLWdvbGQtY2RuLnhpdHUuaW8vMjAxOS85LzE0LzE2ZDJiNmYzZDJmYmEzMmM?x-oss-process=image/format,png&quot; alt=&quot;1&quot;&gt;&lt;/p&gt;
&lt;p&gt;同时将 &lt;code&gt;user &lt;/code&gt;改为&lt;code&gt;root&lt;/code&gt;如下图，不然&lt;code&gt;nginx&lt;/code&gt;无法访问&lt;code&gt; /home/git/projects/blog&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://imgconvert.csdnimg.cn/aHR0cHM6Ly91c2VyLWdvbGQtY2RuLnhpdHUuaW8vMjAxOS85LzE0LzE2ZDJiNmZiZWI2ZmE1ODU?x-oss-process=image/format,png&quot; alt=&quot;2&quot;&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-shell&quot;&gt;nginx -s reload
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;三、发布&lt;/h2&gt;
&lt;p&gt;至此我们就把本地和服务器的环境全部搭建完成，现在利用 &lt;a href=&quot;hexo.io&quot;&gt;hexo&lt;/a&gt; 配置文件进行链接&lt;/p&gt;
&lt;h3&gt;配置&lt;code&gt;_config.yml&lt;/code&gt;文件&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;编辑&lt;code&gt;_config.yml&lt;/code&gt;的 &lt;code&gt;deploy &lt;/code&gt;属性&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-shell&quot;&gt;deploy:
  type: git
  repo: git@ip:/home/git/repos/blog.git
  branch: master
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;在 &lt;code&gt;package.json&lt;/code&gt; 中添加&lt;code&gt;npm&lt;/code&gt;脚本&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-shell&quot;&gt;&quot;dd&quot;: &quot;hexo clean &amp;#x26;&amp;#x26; hexo g -d&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;a href=&quot;https://www.helloimg.com/image/R3CisQ&quot;&gt;&lt;img src=&quot;https://www.helloimg.com/images/2022/03/24/R3CisQ.png&quot; alt=&quot;R3CisQ.png&quot;&gt;&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;若在本地调试则使用&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-shell&quot;&gt;hexo clean &amp;#x26;&amp;#x26; hexo g &amp;#x26;&amp;#x26; hexos
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;或者要是跟我一样配置了&lt;code&gt;scripts&lt;/code&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-shell&quot;&gt;npm run start
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;若发布&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-shell&quot;&gt;hexo clean &amp;#x26;&amp;#x26; hexo g -d
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;或者要是跟我一样配置了&lt;code&gt;scripts&lt;/code&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-shell&quot;&gt;npm run dd
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ul&gt;</content:encoded><h:img src="undefined"/><enclosure url="undefined"/></item><item><title>给女朋友写的C语言期末笔记</title><link>https://blog.myyrh.com/blog/mycnoteforgirlfirend</link><guid isPermaLink="true">https://blog.myyrh.com/blog/mycnoteforgirlfirend</guid><description>期末考试，女朋友C语言怕挂课，我给她整理了一个秘籍，分享一下。</description><pubDate>Wed, 23 Mar 2022 00:00:00 GMT</pubDate><content:encoded>&lt;h1&gt;秘籍&lt;/h1&gt;
&lt;p&gt;​ &lt;code&gt;ps&lt;/code&gt; ：好多定义是百度百科的，看不懂一定要问我~&lt;/p&gt;
&lt;h2&gt;语言基础&lt;/h2&gt;
&lt;h3&gt;&lt;strong&gt;什么是&lt;/strong&gt; &lt;code&gt;main()&lt;/code&gt; &lt;strong&gt;？&lt;/strong&gt;&lt;/h3&gt;
&lt;p&gt;​ 可以理解为程序运行时就会执行 &lt;code&gt;main()&lt;/code&gt; 中的代码。&lt;/p&gt;
&lt;p&gt;​ 最后的 &lt;code&gt;return 0;&lt;/code&gt; 表示程序运行成功。默认情况下，程序结束时返回 0 表示一切正常，否则返回值表示错误代码。这个值返回给谁呢？其实就是调用你写的程序的系统或外部程序，它会在你的程序结束时接收到这个返回值。如果不写 &lt;code&gt;return&lt;/code&gt; 语句的话，程序正常结束默认返回值也是 0。&lt;/p&gt;
&lt;h3&gt;数据类型&lt;/h3&gt;
&lt;p&gt;C 内置了六种基本数据类型：&lt;/p&gt;
&lt;p&gt;|     类型 | 关键字 |
| -------: | :----- |
|          |        |
|   字符型 | char   |
|     整型 | int    |
|   浮点型 | float  |
| 双浮点型 | double |
|   无类型 | void   |&lt;/p&gt;
&lt;p&gt;对于 &lt;code&gt;int&lt;/code&gt; 关键字，可以使用如下修饰关键字进行修饰：&lt;/p&gt;
&lt;p&gt;符号性：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;signed&lt;/code&gt;：表示带符号整数（默认）；&lt;/li&gt;
&lt;li&gt;&lt;code&gt;unsigned&lt;/code&gt;：表示无符号整数。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;大小：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;short&lt;/code&gt;：表示 &lt;strong&gt;至少&lt;/strong&gt; $16$位整数；&lt;/li&gt;
&lt;li&gt;&lt;code&gt;long&lt;/code&gt;：表示 &lt;strong&gt;至少&lt;/strong&gt; $32$ 位整数；&lt;/li&gt;
&lt;li&gt;&lt;code&gt;long long&lt;/code&gt;：表示 &lt;strong&gt;至少&lt;/strong&gt; $64$ 位整数。&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;定义变量&lt;/h3&gt;
&lt;p&gt;简单地说，定义一个变量，需要包含类型说明符（指明变量的类型），以及要定义的变量名。&lt;/p&gt;
&lt;p&gt;例如，下面这几条语句都是变量定义语句。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-c&quot;&gt;int yz;
double yrh;
char yrh = &apos;s&apos;;
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;变量作用域&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;
&lt;pre&gt;&lt;code class=&quot;language-c&quot;&gt;int g = 20;  // 定义全局变量
int main() {
  int g = 10;         // 定义局部变量
  printf(&quot;%d\n&quot;, g);  // 输出 g
  return 0;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;如果一个代码块的内嵌块中定义了相同变量名的变量，则内层块中将无法访问外层块中相同变量名的变量。&lt;/p&gt;
&lt;p&gt;例如上面的代码中，输出的$g$的值将是$10$ 。因此为了防止出现意料之外的错误，请尽量避免局部变量与全局变量重名的情况。&lt;/p&gt;
&lt;h3&gt;常量&lt;/h3&gt;
&lt;p&gt;常量是固定值，在程序执行期间不会改变。&lt;/p&gt;
&lt;p&gt;常量的值在定义后不能被修改。定义时加一个 &lt;code&gt;const&lt;/code&gt; 关键字即可。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-c&quot;&gt;const int a = 2;
a = 3;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;如果修改了常量的值，在编译环节就会报错：&lt;code&gt;error: assignment of read-only variable‘a’&lt;/code&gt;。&lt;/p&gt;
&lt;h3&gt;&lt;code&gt;scanf&lt;/code&gt; 与 &lt;code&gt;printf&lt;/code&gt;&lt;/h3&gt;
&lt;p&gt;&lt;code&gt;scanf&lt;/code&gt; 与 &lt;code&gt;printf&lt;/code&gt; 其实是 C 语言提供的函数。&lt;/p&gt;
&lt;h4&gt;&lt;code&gt;scanf&lt;/code&gt; 为什么会有 &lt;code&gt;&amp;#x26;&lt;/code&gt;&lt;/h4&gt;
&lt;p&gt;在这里，&lt;code&gt;&amp;#x26;&lt;/code&gt; 实际上是取址运算符，返回的是变量在内存中的地址。而 &lt;code&gt;scanf&lt;/code&gt; 接收的参数就是变量的地址。具体可能要在 指针 才能完全清楚地说明，现在只需要记下来就好了。&lt;/p&gt;
&lt;h4&gt;什么是 &lt;code&gt;\n&lt;/code&gt;&lt;/h4&gt;
&lt;p&gt;&lt;code&gt;\n&lt;/code&gt; 是一种 &lt;strong&gt;转义字符&lt;/strong&gt;，表示换行。&lt;/p&gt;
&lt;p&gt;转义字符用来表示一些无法直接输入的字符，如由于字符串字面量中无法换行而无法直接输入的换行符，由于有特殊含义而无法输入的引号，由于表示转义字符而无法输入的反斜杠。&lt;/p&gt;
&lt;p&gt;常用的转义字符有：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;code&gt;\t&lt;/code&gt; 表示制表符。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;\\&lt;/code&gt; 表示 &lt;code&gt;\&lt;/code&gt;。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;\&quot;&lt;/code&gt; 表示 &lt;code&gt;&quot;&lt;/code&gt;。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;\0&lt;/code&gt; 表示空字符，用来表示 C 风格字符串的结尾。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;\r&lt;/code&gt; 表示回车。Linux 中换行符为 &lt;code&gt;\n&lt;/code&gt;，Windows 中换行符为 &lt;code&gt;\r\n&lt;/code&gt;。在 OI 中，如果输出需要换行，使用 &lt;code&gt;\n&lt;/code&gt; 即可。但读入时，如果使用逐字符读入，可能会由于换行符造成一些问题，需要注意。例如，&lt;code&gt;gets&lt;/code&gt; 将 &lt;code&gt;\n&lt;/code&gt; 作为字符串结尾，这时候如果换行符是 &lt;code&gt;\r\n&lt;/code&gt;，&lt;code&gt;\r&lt;/code&gt; 就会留在字符串结尾。&lt;/li&gt;
&lt;li&gt;特殊地，&lt;code&gt;%%&lt;/code&gt; 表示 &lt;code&gt;%&lt;/code&gt;，只能用在 &lt;code&gt;printf&lt;/code&gt; 或 &lt;code&gt;scanf&lt;/code&gt; 中，在其他字符串字面量中只需要简单使用 &lt;code&gt;%&lt;/code&gt; 就好了。&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code class=&quot;language-c&quot;&gt;#include &amp;#x3C;cstdio&gt;

int main() {
  int x, y;
  scanf(&quot;%d%d&quot;, &amp;#x26;x, &amp;#x26;y);   // 读入 x 和 y
  printf(&quot;%d\n%d&quot;, y, x);  // 输出 y，换行，再输出 x
  return 0;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;其中，&lt;code&gt;%d&lt;/code&gt; 表示读入/输出的变量是一个有符号整型 (&lt;code&gt;int&lt;/code&gt; 型）的变量。&lt;/p&gt;
&lt;p&gt;类似地：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;code&gt;%s&lt;/code&gt; 表示字符串。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;%c&lt;/code&gt; 表示字符。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;%lf&lt;/code&gt; 表示双精度浮点数 (&lt;code&gt;double&lt;/code&gt;)。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;%lld&lt;/code&gt; 表示长整型 (&lt;code&gt;long long&lt;/code&gt;)。根据系统不同，也可能是 &lt;code&gt;%I64d&lt;/code&gt;。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;%u&lt;/code&gt; 表示无符号整型 (&lt;code&gt;unsigned int&lt;/code&gt;)。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;%llu&lt;/code&gt; 表示无符号长整型 (&lt;code&gt;unsigned long long&lt;/code&gt;)，也可能是 &lt;code&gt;%I64u&lt;/code&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;除了类型标识符以外，还有一些控制格式的方式。许多都不常用，选取两个常用的列举如下：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;code&gt;%1d&lt;/code&gt; 表示长度为 1 的整型。在读入时，即使没有空格也可以逐位读入数字。在输出时，若指定的长度大于数字的位数，就会在数字前用空格填充。若指定的长度小于数字的位数，就没有效果。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;%.6lf&lt;/code&gt;，用于输出，保留六位小数。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;这两种运算符的相应地方都可以填入其他数字，例如 &lt;code&gt;%.3lf&lt;/code&gt; 表示保留三位小数。&lt;/p&gt;
&lt;h2&gt;运算&lt;/h2&gt;
&lt;h2&gt;算术运算符&lt;/h2&gt;
&lt;p&gt;| 运算符       | 功能 |
| :----------- | :--- |
| &lt;code&gt;+&lt;/code&gt; （单目） | 正   |
| &lt;code&gt;-&lt;/code&gt; （单目） | 负   |
| &lt;code&gt;*&lt;/code&gt; （双目） | 乘法 |
| &lt;code&gt;/&lt;/code&gt;          | 除法 |
| &lt;code&gt;%&lt;/code&gt;          | 取模 |
| &lt;code&gt;+&lt;/code&gt; （双目） | 加法 |
| &lt;code&gt;-&lt;/code&gt; （双目） | 减法 |&lt;/p&gt;
&lt;h4&gt;&lt;strong&gt;单目与双目运算符&lt;/strong&gt;&lt;/h4&gt;
&lt;p&gt;算术运算符中有两个单目运算符（正、负）以及五个双目运算符（乘法、除法、取模、加法、减法），其中单目运算符的优先级最高。&lt;/p&gt;
&lt;p&gt;其中取模运算符 &lt;code&gt;%&lt;/code&gt; 意为计算两个整数相除得到的余数，即求余数。&lt;/p&gt;
&lt;p&gt;而 &lt;code&gt;-&lt;/code&gt; 为双目运算符时做减法运算符，如 &lt;code&gt;2-1&lt;/code&gt; ；为单目运算符时做负值运算符，如 &lt;code&gt;-1&lt;/code&gt; 。&lt;/p&gt;
&lt;p&gt;使用方法如下&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;yz=y-r*h
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;得到的 &lt;code&gt;yz&lt;/code&gt; 的运算值遵循数学中加减乘除的优先规律，首先进行优先级高的运算，同优先级自左向右运算，括号提高优先级。&lt;/p&gt;
&lt;h4&gt;算术运算中的类型转换&lt;/h4&gt;
&lt;p&gt;对于双目算术运算符，当参与运算的两个变量类型相同时，不发生类型转换 ，运算结果将会用参与运算的变量的类型容纳，否则会发生类型转换，以使两个变量的类型一致。&lt;/p&gt;
&lt;p&gt;转换的规则如下：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;先将 &lt;code&gt;char&lt;/code&gt; ， &lt;code&gt;short&lt;/code&gt; 等类型提升至 &lt;code&gt;int&lt;/code&gt; （或 &lt;code&gt;unsigned int&lt;/code&gt; ，取决于原类型的符号性）类型；&lt;/li&gt;
&lt;li&gt;若存在一个变量类型为 &lt;code&gt;long double&lt;/code&gt; ，会将另一变量转换为 &lt;code&gt;long double&lt;/code&gt; 类型；&lt;/li&gt;
&lt;li&gt;否则，若存在一个变量类型为 &lt;code&gt;double&lt;/code&gt; ，会将另一变量转换为 &lt;code&gt;double&lt;/code&gt; 类型；&lt;/li&gt;
&lt;li&gt;否则，若存在一个变量类型为 &lt;code&gt;float&lt;/code&gt; ，会将另一变量转换为 &lt;code&gt;float&lt;/code&gt; 类型；&lt;/li&gt;
&lt;li&gt;否则（即参与运算的两个变量均为整数类型）：
&lt;ul&gt;
&lt;li&gt;若两个变量符号性一致，则将位宽较小的类型转换为位宽较大的类型；&lt;/li&gt;
&lt;li&gt;否则，若无符号变量的位宽不小于带符号变量的位宽，则将带符号数转换为无符号数对应的类型；&lt;/li&gt;
&lt;li&gt;否则，若带符号操作数的类型能表示无符号操作数类型的所有值，则将无符号操作数转换为带符号操作数对应的类型；&lt;/li&gt;
&lt;li&gt;否则，将带符号数转换为相对应的无符号类型。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;例如，对于一个整型（ &lt;code&gt;int&lt;/code&gt; ）变量 和另一个双精度浮点型（ &lt;code&gt;double&lt;/code&gt; ）类型变量 ：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;x/3&lt;/code&gt; 的结果将会是整型；&lt;/li&gt;
&lt;li&gt;&lt;code&gt;x/3.0&lt;/code&gt; 的结果将会是双精度浮点型；&lt;/li&gt;
&lt;li&gt;&lt;code&gt;x/y&lt;/code&gt; 的结果将会是双精度浮点型；&lt;/li&gt;
&lt;li&gt;&lt;code&gt;x*1/3&lt;/code&gt; 的结果将会是整型；&lt;/li&gt;
&lt;li&gt;&lt;code&gt;x*1.0/3&lt;/code&gt; 的结果将会是双精度浮点型；&lt;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;自增/自减 运算符&lt;/h4&gt;
&lt;p&gt;有时我们需要让变量进行增加 1（自增）或者减少 1（自减），这时自增运算符 &lt;code&gt;++&lt;/code&gt; 和自减运算符 &lt;code&gt;--&lt;/code&gt; 就派上用场了。&lt;/p&gt;
&lt;p&gt;自增/自减运算符可放在变量前或变量后面，在变量前称为前缀，在变量后称为后缀，单独使用时前缀后缀无需特别区别，如果需要用到表达式的值则需注意&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-c&quot;&gt;i = 100;

yz1 = i++;  // yz1 = 100，先 yz1 = i，然后 i = i + 1

i = 100;

yz2 = ++i;  // yz2 = 101，先 i = i + 1，然后赋值 yz2

i = 100;

yz3 = i--;  // yz3 = 100，先赋值 yz3，然后 i = i - 1

i = 100;

yz4 = --i;  // yz4 = 99，先 i = i - 1，然后赋值 yz4
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;复合赋值运算符&lt;/h4&gt;
&lt;p&gt;复合赋值运算符实际上是表达式的缩写形式。&lt;/p&gt;
&lt;p&gt;&lt;code&gt;yz = yz + 2&lt;/code&gt; 可以写成 &lt;code&gt; yz  += 2&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;&lt;code&gt;yz = yz - 2&lt;/code&gt; 可以写成 &lt;code&gt; yz  -= 2&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;&lt;code&gt;yz = yz * 2&lt;/code&gt; 可以写成 &lt;code&gt; yz  *= 2&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;&lt;code&gt;yz = yz / 2&lt;/code&gt; 可以写成 &lt;code&gt; yz  /= 2&lt;/code&gt;&lt;/p&gt;
&lt;h4&gt;比较运算符&lt;/h4&gt;
&lt;p&gt;| 运算符 | 功能     |
| :----- | :------- |
| &lt;code&gt;&gt;&lt;/code&gt;    | 大于     |
| &lt;code&gt;&gt;=&lt;/code&gt;   | 大于等于 |
| &lt;code&gt;&amp;#x3C;&lt;/code&gt;    | 小于     |
| &lt;code&gt;&amp;#x3C;=&lt;/code&gt;   | 小于等于 |
| &lt;code&gt;==&lt;/code&gt;   | 等于     |
| &lt;code&gt;!=&lt;/code&gt;   | 不等于   |&lt;/p&gt;
&lt;p&gt;其中特别需要注意的是要将等于运算符 &lt;code&gt;==&lt;/code&gt; 和赋值运算符 &lt;code&gt;=&lt;/code&gt; 区分开来，这在判断语句中尤为重要。&lt;/p&gt;
&lt;p&gt;&lt;code&gt;if(op=1)&lt;/code&gt; 与 &lt;code&gt;if(op==1)&lt;/code&gt; 看起来类似，但实际功能却相差甚远。第一条语句是在对 op 进行赋值，若赋值为非 0 时为真值，表达式的条件始终是满足的，无法达到判断的作用；而第二条语句才是对 &lt;code&gt;op&lt;/code&gt; 的值进行判断。&lt;/p&gt;
&lt;h4&gt;逻辑运算符&lt;/h4&gt;
&lt;p&gt;| 运算符 | 功能   |
| :----- | :----- | --- | ------ |
| &lt;code&gt;&amp;#x26;&amp;#x26;&lt;/code&gt;   | 逻辑与 |
| &lt;code&gt;     |        |&lt;/code&gt;   | 逻辑或 |
| &lt;code&gt;!&lt;/code&gt;    | 逻辑非 |&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-c&quot;&gt;Result = op1 &amp;#x26;&amp;#x26; op2;  // 当 op1 与 op2 都为真时则 Result 为真

Result = op1 || op2;  // 当 op1 或 op2 其中一个为真时则 Result 为真

Result = !op1;  // 当 op1 为假时则 Result 为真
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;分支&lt;/h3&gt;
&lt;h4&gt;if 语句&lt;/h4&gt;
&lt;pre&gt;&lt;code class=&quot;language-c&quot;&gt;if (条件) {
  主体;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;if 语句通过对条件进行求值，若结果为真（非 0），执行语句，否则不执行。&lt;/p&gt;
&lt;p&gt;如果主体中只有单个语句的话，花括号可以省略。&lt;/p&gt;
&lt;h4&gt;if...else 语句&lt;/h4&gt;
&lt;pre&gt;&lt;code class=&quot;language-c&quot;&gt;if (条件) {  主体1;} else {  主体2;}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;if...else 语句和 if 语句类似，else 不需要再写条件。当 if 语句的条件满足时会执行 if 里的语句，if 语句的条件不满足时会执行 else 里的语句。同样，当主体只有一条语句时，可以省略花括号。&lt;/p&gt;
&lt;h4&gt;else if 语句&lt;/h4&gt;
&lt;pre&gt;&lt;code class=&quot;language-c&quot;&gt;if (条件1) {  主体1;} else if (条件2) {  主体2;} else if (条件3) {  主体3;} else {  主体4;}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;else if 语句是 if 和 else 的组合，对多个条件进行判断并选择不同的语句分支。在最后一条的 else 语句不需要再写条件。例如，若条件 1 为真，执行主体 1，条件 3 为真而条件 1 和条件 2 都为假，执行主体 3，所有的条件都为假才执行主体 4。&lt;/p&gt;
&lt;p&gt;实际上，这一个语句相当于第一个 if 的 else 分句只有一个 if 语句，就将花括号省略之后放在一起了。如果条件相互之间是并列关系，这样写可以让代码的逻辑更清晰。&lt;/p&gt;
&lt;h4&gt;switch 语句&lt;/h4&gt;
&lt;pre&gt;&lt;code class=&quot;language-c&quot;&gt;switch (选择句) {
  case 标签1:
    主体1;
  case 标签2:
    主体2;
  default:
    主体3;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;switch 语句执行时，先求出选择句的值，然后根据选择句的值选择相应的标签，从标签处开始执行。其中，选择句必须是一个整数类型表达式，而标签都必须是整数类型的常量。例如：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-c&quot;&gt;int i = 1;  // 这里的 i 的数据类型是整型 ，满足整数类型的表达式的要求
switch (i) {
  case 1:
    printf(&quot; i love yz&quot;);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code class=&quot;language-c&quot;&gt;char i = &apos;A&apos;;
// 这里的 i 的数据类型是字符型 ，但 char
// 也是属于整数的类型，满足整数类型的表达式的要求
switch (i) {
  case &apos;A&apos;:
    printf(&quot; i love yz&quot;);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;switch 语句中还要根据需求加入 break 语句进行中断，否则在对应的 case 被选择之后接下来的所有 case 里的语句和 default 里的语句都会被运行。具体例子可看下面的示例。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-c&quot;&gt;char i = &apos;B&apos;;
switch (i) {
  case &apos;A&apos;:
    printf(&quot; i love yz&quot;);
    break;

  case &apos;B&apos;:
    printf(&quot; yz love yz&quot;);

  default:
    printf(&quot; yz love me&quot;);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;switch 的 case 分句中也可以选择性的加花括号。不过要注意的是，如果需要在 switch 语句中定义变量，花括号是必须要加的。例如：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-c&quot;&gt;char i = &apos;B&apos;;switch (i) {  case &apos;A&apos;: {    int i = 1, j = 2;    printf(&quot;i\n&quot;);    ans = i + j;    break;  }  case &apos;B&apos;: {    int qwq = 3;   printf(&quot;love\n&quot;);    ans = qwq * qwq;    break;  }  default: {    printf(&quot;yz\n&quot;);  }}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;循环&lt;/h3&gt;
&lt;h4&gt;&lt;code&gt;for&lt;/code&gt;语句&lt;/h4&gt;
&lt;pre&gt;&lt;code class=&quot;language-c&quot;&gt;for (初始化; 判断条件; 更新) {  循环体;}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;img src=&quot;https://oi-wiki.org/lang/images/for-loop.svg&quot; alt=&quot;img&quot;&gt;&lt;/p&gt;
&lt;p&gt;读入 n 个数：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-c&quot;&gt;for (int i = 1; i &amp;#x3C;= n; i++)	scanf(&quot;%d&quot;,&amp;#x26;a[i]);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;for 语句的三个部分中，任何一个部分都可以省略。其中，若省略了判断条件，相当于判断条件永远为真。&lt;/p&gt;
&lt;h4&gt;while 语句&lt;/h4&gt;
&lt;pre&gt;&lt;code class=&quot;language-c&quot;&gt;while (判断条件) {  循环体;}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;img src=&quot;https://oi-wiki.org/lang/images/while-loop.svg&quot; alt=&quot;img&quot;&gt;&lt;/p&gt;
&lt;h4&gt;do...while 语句&lt;/h4&gt;
&lt;pre&gt;&lt;code class=&quot;language-c&quot;&gt;do {  循环体;} while (判断条件);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;img src=&quot;https://oi-wiki.org/lang/images/do-while-loop.svg&quot; alt=&quot;img&quot;&gt;&lt;/p&gt;
&lt;p&gt;与 while 语句的区别在于，do...while 语句是先执行循环体再进行判断的。&lt;/p&gt;
&lt;h4&gt;三种语句的联系&lt;/h4&gt;
&lt;p&gt;可能需要当面讲解&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-c&quot;&gt;// for 语句for (statement1; statement2; statement3) {  statement4;}// while 语句statement1;while (statement2) {  statement4;  statement3;}
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code class=&quot;language-c&quot;&gt;// while 语句statement1;while (statement2) {  statement1;}// do...while 语句do {  statement1;} while (statement2);
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code class=&quot;language-c&quot;&gt;while (1) {  // do something...}for (;;) {  // do something...}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;可以看出，三种语句可以彼此代替，但一般来说，语句的选用遵守以下原则：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;循环过程中有个固定的增加步骤（最常见的是枚举）时，使用 for 语句；&lt;/li&gt;
&lt;li&gt;只确定循环的终止条件时，使用 while 语句；&lt;/li&gt;
&lt;li&gt;使用 while 语句时，若要先执行循环体再进行判断，使用 do...while 语句。一般很少用到，常用场景是用户输入。&lt;/li&gt;
&lt;/ol&gt;
&lt;h4&gt;break 与 continue 语句&lt;/h4&gt;
&lt;p&gt;break 语句的作用是退出循环。&lt;/p&gt;
&lt;p&gt;continue 语句的作用是跳过循环体的余下部分，回到循环的开头（for 语句的更新，while 语句的判断条件）。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-c&quot;&gt;for (int i = 1; i &amp;#x3C;= 10; ++i) {  printf(&quot;%d\n&quot;,i);  if (i &gt; 3) break;  if (i &gt; 2) continue;  printf(&quot;%d\n&quot;,i);}/*输出如下：112234*/
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;break 与 continue 语句均可在三种循环语句的循环体中使用。&lt;/p&gt;
&lt;p&gt;一般来说，break 与 continue 语句用于让代码的逻辑更加清晰，例如：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-c&quot;&gt;// 逻辑较为不清晰，大括号层次复杂for (int i = 1; i &amp;#x3C;= n; ++i) {  if (i != x) {    for (int j = 1; j &amp;#x3C;= n; ++j) {      if (j != x) {        // do something...      }    }  }}// 逻辑更加清晰，大括号层次简单明了for (int i = 1; i &amp;#x3C;= n; ++i) {  if (i == x) continue;  for (int j = 1; j &amp;#x3C;= n; ++j) {    if (j == x) continue;    // do something...  }}
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code class=&quot;language-c&quot;&gt;// for 语句判断条件复杂，没有体现“枚举”的本质for (int i = l; i &amp;#x3C;= r &amp;#x26;&amp;#x26; i % 10 != 0; ++i) {  // do something...}// for 语句用于枚举，break 用于“到何时为止”for (int i = l; i &amp;#x3C;= r; ++i) {  if (i % 10 == 0) break;  // do something...}
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code class=&quot;language-c&quot;&gt;// 语句重复，顺序不自然

statement1;
while (statement3) {
  statement2;
  statement1;
}

// 没有重复语句，顺序自然

while (1) {
  statement1;
  if (!statement3) break;
  statement2;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;高级结构&lt;/h2&gt;
&lt;h3&gt;数组&lt;/h3&gt;
&lt;p&gt;数组是存放相同类型对象的容器，数组中存放的对象没有名字，而是要通过其所在的位置访问。数组的大小是固定的，不能随意改变数组的长度。&lt;/p&gt;
&lt;h4&gt;定义数组&lt;/h4&gt;
&lt;p&gt;数组的声明形如 &lt;code&gt;a[d]&lt;/code&gt;，其中，&lt;code&gt;a&lt;/code&gt; 是数组的名字，&lt;code&gt;d&lt;/code&gt; 是数组中元素的个数。在编译时，&lt;code&gt;d&lt;/code&gt; 应该是已知的，也就是说，&lt;code&gt;d&lt;/code&gt; 应该是一个整型的常量表达式。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-c&quot;&gt;int d1 = 42;
const int d2 = 42;
int arr1[d1];  // 错误：d1 不是常量表达式
int arr2[d2];  // 正确：arr2 是一个长度为 42 的数组
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;不能将一个数组直接赋值给另一个数组：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-c&quot;&gt;int arr1[3];
int arr2 = arr1;  // 错误
arr2 = arr1;      // 错误
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;访问数组元素&lt;/h4&gt;
&lt;p&gt;可以通过下标运算符 &lt;code&gt;[]&lt;/code&gt; 来访问数组内元素，数组的索引（即方括号中的值）从 0 开始。以一个包含 10 个元素的数组为例，它的索引为 0 到 9，而非 1 到 10。为了使用方便，我通常会将数组开大一点，不使用数组的第一个元素，从下标 1 开始访问数组元素。&lt;/p&gt;
&lt;p&gt;例 1：从标准输入中读取一个整数$n$ ，再读取$n$个数，存入数组中。其中，$n \leq 1000$&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-c&quot;&gt;#include &amp;#x3C;stdio.h&gt;int main(){    int a[1010], n;//可以定义大一点 用不到没关系 也别太离谱  	scanf(&quot;%d&quot;,&amp;#x26;n);    int i ;    for(i = 1; i &amp;#x3C;= n; i++)//从下标1开始       scanf(&quot;%d&quot;,&amp;#x26;a[i]);}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;例 2：（接例 1）求和数组 &lt;code&gt;arr&lt;/code&gt; 中的元素，并输出和。满足数组中所有元素的和在&lt;code&gt;int&lt;/code&gt;范围内&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-c&quot;&gt;#include &amp;#x3C;stdio.h&gt;int main(){    int a[1010], n, sum = 0;//可以定义大一点 用不到没关系 也别太离谱  	scanf(&quot;%d&quot;,&amp;#x26;n);    int i ;    for(i = 1; i &amp;#x3C;= n; i++)//从下标1开始       scanf(&quot;%d&quot;,&amp;#x26;a[i]);   	for(i = 1; i &amp;#x3C;= n; i++)        sum += a[i];    printf(&quot;%d&quot;,sum);}
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;多维数组&lt;/h4&gt;
&lt;p&gt;多维数组的实质是「数组的数组」，即外层数组的元素是数组。一个二维数组需要两个维度来定义：数组的长度和数组内元素的长度。访问二维数组时需要写出两个索引：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-c&quot;&gt;int arr[3][4];  // 一个长度为 3 的数组，它的元素是「元素为 int 的长度为的 4                // 的数组」arr[2][1] = 1;  // 访问二维数组
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;我们经常使用嵌套的 for 循环来处理二维数组。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;C:%5CUsers%5CYuRuiH%5CAppData%5CRoaming%5CTypora%5Ctypora-user-images%5Cimage-20211206154215615.png&quot; alt=&quot;image-20211206154215615&quot;&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-c&quot;&gt;const int maxn = 1001;int pic[maxn][maxn];int n, m;scanf(&quot;%d%d&quot;,&amp;#x26;n,&amp;#x26;m);for (int i = 1; i &amp;#x3C;= n; i++)  for (int j = 1; j &amp;#x3C;= m; j++)  	scanf(&quot;%d&quot;,&amp;#x26;pic[i][j]);
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;函数&lt;/h3&gt;
&lt;h4&gt;函数的声明&lt;/h4&gt;
&lt;p&gt;编程中的函数（function）一般是若干语句的集合。我们也可以将其称作“&lt;strong&gt;子过程&lt;/strong&gt;（subroutine）”。在编程中，如果有一些重复的过程，我们可以将其提取出来，形成一个函数。函数可以接收若干值，这叫做函数的参数。函数也可以返回某个值，这叫做函数的返回值。&lt;/p&gt;
&lt;p&gt;声明一个函数，我们需要返回值类型、函数的名称，以及参数列表。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-c&quot;&gt;// 返回值类型 int// 函数的名称 some_function// 参数列表 int, intint some_function(int, int);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;如上，我们声明了一个名为 &lt;code&gt;some_function&lt;/code&gt; 的函数，它需要接收两个 &lt;code&gt;int&lt;/code&gt; 类型的参数，返回值类型也为 &lt;code&gt;int&lt;/code&gt;。可以认为，这个函数将会对传入的两个整数进行一些操作，并且返回一个同样类型的结果。&lt;/p&gt;
&lt;h4&gt;实现函数：编写函数的定义&lt;/h4&gt;
&lt;p&gt;只有函数的声明（declaration）还不够，他只能让我们在调用时能够得知函数的 &lt;strong&gt;接口&lt;/strong&gt; 类型（即接收什么数据、返回什么数据），但其缺乏具体的内部实现，也就是函数的 &lt;strong&gt;定义&lt;/strong&gt;（definition）。我们可以在 &lt;strong&gt;声明之后的其他地方&lt;/strong&gt; 编写代码 &lt;strong&gt;实现&lt;/strong&gt;（implement）这个函数（也可以在另外的文件中实现，但是需要将分别编译后的文件在链接时一并给出）。&lt;/p&gt;
&lt;p&gt;如果函数有返回值，则需要通过 &lt;code&gt;return&lt;/code&gt; 语句，将值返回给调用方。函数一旦执行到 &lt;code&gt;return&lt;/code&gt; 语句，则直接结束当前函数，不再执行后续的语句。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-c&quot;&gt;int some_function(int, int);  // 声明/* some other code here... */int some_function(int x, int y) {  // 定义  int result = 2 * x + y;  return result;  result = 3;  // 这条语句不会被执行}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;如果函数不需要有返回值，则将函数的返回值类型标为 &lt;code&gt;void&lt;/code&gt;；如果函数不需要参数，则可以将参数列表置空。同样，无返回值的函数执行到 &lt;code&gt;return;&lt;/code&gt; 语句也会结束执行。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-c&quot;&gt;void saylove(){    printf(&quot;love yz\n&quot;);}
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;函数的调用&lt;/h4&gt;
&lt;p&gt;和变量一样，函数需要先被声明，才能使用。使用函数的行为，叫做“调用（call）”。我们可以在任何函数内部调用其他函数，包括这个函数自身。函数调用自身的行为，称为 &lt;strong&gt;递归&lt;/strong&gt;（recursion）。&lt;/p&gt;
&lt;p&gt;在大多数语言中，调用函数的写法，是 &lt;strong&gt;函数名称加上一对括号&lt;/strong&gt; &lt;code&gt;()&lt;/code&gt;，如 &lt;code&gt;yz()&lt;/code&gt;。如果函数需要参数，则我们将其需要的参数按顺序填写在括号中，以逗号间隔，如 &lt;code&gt;yz(1, 2)&lt;/code&gt;。函数的调用也是一个表达式，&lt;strong&gt;函数的返回值&lt;/strong&gt; 就是 &lt;strong&gt;表达式的值&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;函数声明时候写出的参数，可以理解为在函数 &lt;strong&gt;当前次调用的内部&lt;/strong&gt; 可以使用的变量，这些变量的值由调用处传入的值初始化。看下面这个例子：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;void foo(int x, int y) {
  x = x * 2;
  y = y + 3;
}

/* ... */

a = 1;
b = 1;
// 调用前：a = 1, b = 1
foo(a, b);  // 调用 foo
            // 调用后：a = 1, b = 1
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;在上面的例子中，&lt;code&gt;yz(a, b)&lt;/code&gt; 是一次对 &lt;code&gt;foo&lt;/code&gt; 的调用。调用时，&lt;code&gt;yz&lt;/code&gt; 中的 &lt;code&gt;x&lt;/code&gt; 和 &lt;code&gt;y&lt;/code&gt; 变量，分别由调用处 &lt;code&gt;a&lt;/code&gt; 和 &lt;code&gt;b&lt;/code&gt; 的值初始化。因此，在 &lt;code&gt;yz&lt;/code&gt; 中对变量 &lt;code&gt;x&lt;/code&gt; 和 &lt;code&gt;y&lt;/code&gt; 的修改，&lt;strong&gt;并不会影响到调用处的变量的值&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;如果我们需要在函数（子过程）中修改变量的值，则需要采用“传引用”的方式。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-c&quot;&gt;void foo(int&amp;#x26; x, int&amp;#x26; y) {
  x = x * 2;
  y = y + 3;
}

/* ... */

a = 1;
b = 1;
// 调用前：a = 1, b = 1
foo(a, b);  // 调用 foo
            // 调用后：a = 2, b = 4
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;上述代码中，我们看到函数参数列表中的“&lt;code&gt;int&lt;/code&gt;”后面添加了一个“&lt;code&gt;&amp;#x26;&lt;/code&gt;（and 符号）”，这表示对于 &lt;code&gt;int&lt;/code&gt; 类型的 &lt;strong&gt;引用&lt;/strong&gt;（reference）。在调用 &lt;code&gt;foo&lt;/code&gt; 时，调用处 &lt;code&gt;a&lt;/code&gt; 和 &lt;code&gt;b&lt;/code&gt; 变量分别初始化了 &lt;code&gt;foo&lt;/code&gt; 中两个对 &lt;code&gt;int&lt;/code&gt; 类型的引用 &lt;code&gt;x&lt;/code&gt; 和 &lt;code&gt;y&lt;/code&gt;。在 &lt;code&gt;foo&lt;/code&gt; 中的 &lt;code&gt;x&lt;/code&gt; 和 &lt;code&gt;y&lt;/code&gt;，可以理解为调用处 &lt;code&gt;a&lt;/code&gt; 和 &lt;code&gt;b&lt;/code&gt; 变量的“别名”，即 &lt;code&gt;foo&lt;/code&gt; 中对 &lt;code&gt;x&lt;/code&gt; 和 &lt;code&gt;y&lt;/code&gt; 的操作，就是对调用处 &lt;code&gt;a&lt;/code&gt; 和 &lt;code&gt;b&lt;/code&gt; 的操作。&lt;/p&gt;
&lt;h3&gt;结构体&lt;/h3&gt;
&lt;p&gt;结构体（struct），可以看做是一系列称为成员元素的组合体。&lt;/p&gt;
&lt;p&gt;可以看做是自定义的数据类型。&lt;/p&gt;
&lt;h4&gt;定义结构&lt;/h4&gt;
&lt;p&gt;为了定义结构，必须使用 &lt;strong&gt;struct&lt;/strong&gt; 语句。struct 语句定义了一个包含多个成员的新的数据类型，struct 语句的格式如下：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-c&quot;&gt;struct tag {     member-list    member-list     member-list      ...} variable-list ;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;tag&lt;/strong&gt; 是结构体标签。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;member-list&lt;/strong&gt; 是标准的变量定义，比如 int i; 或者 float f，或者其他有效的变量定义。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;variable-list&lt;/strong&gt; 结构变量，定义在结构的末尾，最后一个分号之前，您可以指定一个或多个结构变量。下面是声明 Book 结构的方式：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-c&quot;&gt;struct Books{   char  title[50];   char  author[50];   char  subject[100];   int   book_id;} book;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;在一般情况下，&lt;strong&gt;tag、member-list、variable-list&lt;/strong&gt; 这 3 部分至少要出现 2 个。以下为实例：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-c&quot;&gt;//此声明声明了拥有3个成员的结构体，分别为整型的a，字符型的b和双精度的c
//同时又声明了结构体变量s1
//这个结构体并没有标明其标签
struct
{
    int a;
    char b;
    double c;
} s1;

//此声明声明了拥有3个成员的结构体，分别为整型的a，字符型的b和双精度的c
//结构体的标签被命名为SIMPLE,没有声明变量
struct SIMPLE
{
    int a;
    char b;
    double c;
};
//用SIMPLE标签的结构体，另外声明了变量t1、t2、t3
struct SIMPLE t1, t2[20], *t3;

//也可以用typedef创建新类型
typedef struct
{
    int a;
    char b;
    double c;
} Simple2;
//现在可以用Simple2作为类型声明新的结构体变量
Simple2 u1, u2[20], *u3;
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;结构体变量的初始化&lt;/h4&gt;
&lt;pre&gt;&lt;code class=&quot;language-c&quot;&gt;#include &amp;#x3C;stdio.h&gt;

struct Books
{
   char  title[50];
   char  author[50];
   char  subject[100];
   int   book_id;
} book = {&quot;C 语言&quot;, &quot;RUNOOB&quot;, &quot;编程语言&quot;, 123456};

int main()
{
    printf(&quot;title : %s\nauthor: %s\nsubject: %s\nbook_id: %d\n&quot;, book.title, book.author, book.subject, book.book_id);
}
/*
title : C 语言
author: RUNOOB
subject: 编程语言
book_id: 123456
*/
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;访问结构成员&lt;/h4&gt;
&lt;p&gt;为了访问结构的成员，我们使用&lt;strong&gt;成员访问运算符（.）&lt;/strong&gt;。成员访问运算符是结构变量名称和我们要访问的结构成员之间的一个句号。您可以使用 &lt;strong&gt;struct&lt;/strong&gt; 关键字来定义结构类型的变量。下面的实例演示了结构的用法：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-c&quot;&gt;#include &amp;#x3C;stdio.h&gt;#include &amp;#x3C;string.h&gt; struct Books{   char  title[50];   char  author[50];   char  subject[100];   int   book_id;}; int main( ){   struct Books Book1;        /* 声明 Book1，类型为 Books */   struct Books Book2;        /* 声明 Book2，类型为 Books */    /* Book1 详述 */   strcpy( Book1.title, &quot;C Programming&quot;);   strcpy( Book1.author, &quot;Nuha Ali&quot;);    strcpy( Book1.subject, &quot;C Programming Tutorial&quot;);   Book1.book_id = 6495407;    /* Book2 详述 */   strcpy( Book2.title, &quot;Telecom Billing&quot;);   strcpy( Book2.author, &quot;Zara Ali&quot;);   strcpy( Book2.subject, &quot;Telecom Billing Tutorial&quot;);   Book2.book_id = 6495700;    /* 输出 Book1 信息 */   printf( &quot;Book 1 title : %s\n&quot;, Book1.title);   printf( &quot;Book 1 author : %s\n&quot;, Book1.author);   printf( &quot;Book 1 subject : %s\n&quot;, Book1.subject);   printf( &quot;Book 1 book_id : %d\n&quot;, Book1.book_id);    /* 输出 Book2 信息 */   printf( &quot;Book 2 title : %s\n&quot;, Book2.title);   printf( &quot;Book 2 author : %s\n&quot;, Book2.author);   printf( &quot;Book 2 subject : %s\n&quot;, Book2.subject);   printf( &quot;Book 2 book_id : %d\n&quot;, Book2.book_id);    return 0;}/*Book 1 title : C ProgrammingBook 1 author : Nuha AliBook 1 subject : C Programming TutorialBook 1 book_id : 6495407Book 2 title : Telecom BillingBook 2 author : Zara AliBook 2 subject : Telecom Billing TutorialBook 2 book_id : 6495700*/
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;指针&lt;/h3&gt;
&lt;h4&gt;变量的地址、指针&lt;/h4&gt;
&lt;p&gt;在程序中，我们的数据都有其存储的地址。在程序每次的实际运行过程中，变量在物理内存中的存储位置不尽相同。不过，我们仍能够在编程时，通过一定的语句，来取得数据在内存中的地址。&lt;/p&gt;
&lt;p&gt;地址也是数据。存放地址所用的变量类型有一个特殊的名字，叫做“指针变量”，有时也简称做“指针”。&lt;/p&gt;
&lt;p&gt;地址只是一个刻度一般的数据，为了针对不同类型的数据，“指针变量”也有不同的类型，比如，可以有 &lt;code&gt;int&lt;/code&gt; 类型的指针变量，其中存储的地址（即指针变量存储的数值）对应一块大小为 32 位的空间的起始地址；有 &lt;code&gt;char&lt;/code&gt; 类型的指针变量，其中存储的地址对应一块 8 位的空间的起始地址。&lt;/p&gt;
&lt;p&gt;事实上，用户也可以声明指向指针变量的指针变量。&lt;/p&gt;
&lt;h4&gt;指针的声明与使用&lt;/h4&gt;
&lt;pre&gt;&lt;code class=&quot;language-c&quot;&gt;#include &amp;#x3C;stdio.h&gt; int main (){    int yrh = 10;    int *p;              // 定义指针变量    p = &amp;#x26;yrh;    printf(&quot;yrh 变量的地址： %p\n&quot;, p);   return 0;}/*yrh 变量的地址： 0x7ffeeaae08d8*/
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;什么是指针？&lt;/h4&gt;
&lt;p&gt;指针也就是内存地址，指针变量是用来存放内存地址的变量。就像其他变量或常量一样，您必须在使用指针存储其他变量地址之前，对其进行声明。指针变量声明的一般形式为：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-c&quot;&gt;type *var-name;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;在这里，&lt;strong&gt;type&lt;/strong&gt; 是指针的基类型，它必须是一个有效的 C 数据类型，&lt;strong&gt;var-name&lt;/strong&gt; 是指针变量的名称。用来声明指针的星号 &lt;strong&gt;*&lt;/strong&gt; 与乘法中使用的星号是相同的。但是，在这个语句中，星号是用来指定一个变量是指针。以下是有效的指针声明：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-c&quot;&gt;int    *ip;    /* 一个整型的指针 */double *dp;    /* 一个 double 型的指针 */float  *fp;    /* 一个浮点型的指针 */char   *ch;    /* 一个字符型的指针 */
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;所有实际数据类型，不管是整型、浮点型、字符型，还是其他的数据类型，对应指针的值的类型都是一样的，都是一个代表内存地址的长的十六进制数。&lt;/p&gt;
&lt;p&gt;不同数据类型的指针之间唯一的不同是，指针所指向的变量或常量的数据类型不同。&lt;/p&gt;
&lt;h4&gt;如何使用指针？&lt;/h4&gt;
&lt;p&gt;使用指针时会频繁进行以下几个操作：定义一个指针变量、把变量地址赋值给指针、访问指针变量中可用地址的值。这些是通过使用一元运算符 &lt;strong&gt;*&lt;/strong&gt; 来返回位于操作数所指定地址的变量的值。下面的实例涉及到了这些操作：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-c&quot;&gt;#include &amp;#x3C;stdio.h&gt; int main (){   int  var = 20;   /* 实际变量的声明 */   int  *ip;        /* 指针变量的声明 */    ip = &amp;#x26;var;  /* 在指针变量中存储 var 的地址 */    printf(&quot;var 变量的地址: %p\n&quot;, &amp;#x26;var  );    /* 在指针变量中存储的地址 */   printf(&quot;ip 变量存储的地址: %p\n&quot;, ip );    /* 使用指针访问值 */   printf(&quot;*ip 变量的值: %d\n&quot;, *ip );    return 0;}/*var 变量的地址: 0x7ffeeef168d8ip 变量存储的地址: 0x7ffeeef168d8*ip 变量的值: 20*/
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;C 中的 NULL 指针&lt;/h4&gt;
&lt;p&gt;在变量声明的时候，如果没有确切的地址可以赋值，为指针变量赋一个 NULL 值是一个良好的编程习惯。赋为 NULL 值的指针被称为&lt;strong&gt;空&lt;/strong&gt;指针。&lt;/p&gt;
&lt;p&gt;NULL 指针是一个定义在标准库中的值为零的常量&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-c&quot;&gt;#include &amp;#x3C;stdio.h&gt; int main (){   int  *ptr = NULL;    printf(&quot;ptr 的地址是 %p\n&quot;, ptr  );    return 0;}/*ptr 的地址是 0x0*/
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;在大多数的操作系统上，程序不允许访问地址为 0 的内存，因为该内存是操作系统保留的。然而，内存地址 0 有特别重要的意义，它表明该指针不指向一个可访问的内存位置。但按照惯例，如果指针包含空值（零值），则假定它不指向任何东西。&lt;/p&gt;
&lt;p&gt;如需检查一个空指针，可以使用 if 语句，如下所示：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-c&quot;&gt;if(ptr)     /* 如果 p 非空，则完成 */if(!ptr)    /* 如果 p 为空，则完成 */
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;指针的算术运算&lt;/h4&gt;
&lt;p&gt;C 指针是一个用数值表示的地址。因此，您可以对指针执行算术运算。可以对指针进行四种算术运算：++、--、+、-。&lt;/p&gt;
&lt;p&gt;假设 &lt;strong&gt;ptr&lt;/strong&gt; 是一个指向地址 1000 的整型指针，是一个 32 位的整数，让我们对该指针执行下列的算术运算：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-c&quot;&gt;ptr++
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;在执行完上述的运算之后，&lt;strong&gt;ptr&lt;/strong&gt; 将指向位置 1004，因为 ptr 每增加一次，它都将指向下一个整数位置，即当前位置往后移 4 字节。这个运算会在不影响内存位置中实际值的情况下，移动指针到下一个内存位置。如果 &lt;strong&gt;ptr&lt;/strong&gt; 指向一个地址为 1000 的字符，上面的运算会导致指针指向位置 1001，因为下一个字符位置是在 1001。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;指针的每一次递增，它其实会指向下一个元素的存储单元。&lt;/li&gt;
&lt;li&gt;指针的每一次递减，它都会指向前一个元素的存储单元。&lt;/li&gt;
&lt;li&gt;指针在递增和递减时跳跃的字节数取决于指针所指向变量数据类型长度，比如 int 就是 4 个字节。&lt;/li&gt;
&lt;/ul&gt;
&lt;h5&gt;递增一个指针&lt;/h5&gt;
&lt;pre&gt;&lt;code class=&quot;language-c&quot;&gt;#include &amp;#x3C;stdio.h&gt; const int MAX = 3; int main (){   int  var[] = {10, 100, 200};   int  i, *ptr;    /* 指针中的数组地址 */   ptr = var;   for ( i = 0; i &amp;#x3C; MAX; i++)   {       printf(&quot;存储地址：var[%d] = %p\n&quot;, i, ptr );      printf(&quot;存储值：var[%d] = %d\n&quot;, i, *ptr );       /* 指向下一个位置 */      ptr++;   }   return 0;}/*存储地址：var[0] = e4a298cc存储值：var[0] = 10存储地址：var[1] = e4a298d0存储值：var[1] = 100存储地址：var[2] = e4a298d4存储值：var[2] = 200*/
&lt;/code&gt;&lt;/pre&gt;
&lt;h5&gt;递减一个指针&lt;/h5&gt;
&lt;pre&gt;&lt;code class=&quot;language-c&quot;&gt;#include &amp;#x3C;stdio.h&gt; const int MAX = 3; int main (){   int  var[] = {10, 100, 200};   int  i, *ptr;    /* 指针中最后一个元素的地址 */   ptr = &amp;#x26;var[MAX-1];   for ( i = MAX; i &gt; 0; i--)   {       printf(&quot;存储地址：var[%d] = %p\n&quot;, i-1, ptr );      printf(&quot;存储值：var[%d] = %d\n&quot;, i-1, *ptr );       /* 指向下一个位置 */      ptr--;   }   return 0;}/*存储地址：var[2] = 518a0ae4存储值：var[2] = 200存储地址：var[1] = 518a0ae0存储值：var[1] = 100存储地址：var[0] = 518a0adc存储值：var[0] = 10*/
&lt;/code&gt;&lt;/pre&gt;
&lt;h5&gt;指针的比较&lt;/h5&gt;
&lt;p&gt;指针可以用关系运算符进行比较，如 ==、&amp;#x3C; 和 &gt;。如果 p1 和 p2 指向两个相关的变量，比如同一个数组中的不同元素，则可对 p1 和 p2 进行大小比较。&lt;/p&gt;
&lt;p&gt;下面的程序修改了上面的实例，只要变量指针所指向的地址小于或等于数组的最后一个元素的地址 &amp;#x26;var[MAX - 1]，则把变量指针进行递增：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-c&quot;&gt;#include &amp;#x3C;stdio.h&gt; const int MAX = 3; int main (){   int  var[] = {10, 100, 200};   int  i, *ptr;    /* 指针中第一个元素的地址 */   ptr = var;   i = 0;   while ( ptr &amp;#x3C;= &amp;#x26;var[MAX - 1] )   {       printf(&quot;存储地址：var[%d] = %p\n&quot;, i, ptr );      printf(&quot;存储值：var[%d] = %d\n&quot;, i, *ptr );       /* 指向上一个位置 */      ptr++;      i++;   }   return 0;}/*存储地址：var[0] = 0x7ffeee2368cc存储值：var[0] = 10存储地址：var[1] = 0x7ffeee2368d0存储值：var[1] = 100存储地址：var[2] = 0x7ffeee2368d4存储值：var[2] = 200*/
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;指针数组&lt;/h4&gt;
&lt;p&gt;我们想要让数组存储指向 int 或 char 或其他数据类型的指针。下面是一个指向整数的指针数组的声明：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-c&quot;&gt;int *ptr[MAX];
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;在这里，把 &lt;strong&gt;ptr&lt;/strong&gt; 声明为一个数组，由 MAX 个整数指针组成。因此，ptr 中的每个元素，都是一个指向 int 值的指针。下面的实例用到了三个整数，它们将存储在一个指针数组中，如下所示：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-c&quot;&gt;#include &amp;#x3C;stdio.h&gt; const int MAX = 3; int main (){   int  var[] = {10, 100, 200};   int i, *ptr[MAX];    for ( i = 0; i &amp;#x3C; MAX; i++)   {      ptr[i] = &amp;#x26;var[i]; /* 赋值为整数的地址 */   }   for ( i = 0; i &amp;#x3C; MAX; i++)   {      printf(&quot;Value of var[%d] = %d\n&quot;, i, *ptr[i] );   }   return 0;}/*Value of var[0] = 10Value of var[1] = 100Value of var[2] = 200*/
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;可以用一个指向字符的指针数组来存储一个字符串列表，如下：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-c&quot;&gt;#include &amp;#x3C;stdio.h&gt; const int MAX = 4; int main (){   const char *names[] = {                   &quot;yrh&quot;,                   &quot;love&quot;,                   &quot;yz&quot;,                   &quot;!&quot;,   };   int i = 0;    for ( i = 0; i &amp;#x3C; MAX; i++)   {      printf(&quot;Value of names[%d] = %s\n&quot;, i, names[i] );   }   return 0;}/*Value of names[0] = yrhValue of names[1] = loveValue of names[2] = yzValue of names[3] = !*/
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;指向指针的指针&lt;/h4&gt;
&lt;p&gt;指向指针的指针是一种多级间接寻址的形式，或者说是一个指针链。通常，一个指针包含一个变量的地址。当我们定义一个指向指针的指针时，第一个指针包含了第二个指针的地址，第二个指针指向包含实际值的位置。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://www.runoob.com/wp-content/uploads/2014/09/pointer_to_pointer.jpg&quot; alt=&quot;C 中指向指针的指针&quot;&gt;&lt;/p&gt;
&lt;p&gt;一个指向指针的指针变量必须如下声明，即在变量名前放置两个星号。例如，下面声明了一个指向 int 类型指针的指针：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-c&quot;&gt;int **var;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;当一个目标值被一个指针间接指向到另一个指针时，访问这个值需要使用两个星号运算符，如下面实例所示：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://www.runoob.com/wp-content/uploads/2014/09/c-pointerxxxxx.png&quot; alt=&quot;img&quot;&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-c&quot;&gt;#include &amp;#x3C;stdio.h&gt; int main (){   int  V;   int  *Pt1;   int  **Pt2;    V = 100;    /* 获取 V 的地址 */   Pt1 = &amp;#x26;V;    /* 使用运算符 &amp;#x26; 获取 Pt1 的地址 */   Pt2 = &amp;#x26;Pt1;    /* 使用 pptr 获取值 */   printf(&quot;var = %d\n&quot;, V );   printf(&quot;Pt1 = %p\n&quot;, Pt1 );   printf(&quot;*Pt1 = %d\n&quot;, *Pt1 );    printf(&quot;Pt2 = %p\n&quot;, Pt2 );   printf(&quot;**Pt2 = %d\n&quot;, **Pt2);    return 0;}/*var = 100Pt1 = 0x7ffee2d5e8d8*Pt1 = 100Pt2 = 0x7ffee2d5e8d0**Pt2 = 100*/
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;传递指针给函数&lt;/h4&gt;
&lt;p&gt;C 语言允许传递指针给函数，只需要简单地声明函数参数为指针类型即可。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-c&quot;&gt;#include &amp;#x3C;stdio.h&gt;
#include &amp;#x3C;time.h&gt;

void getSeconds(unsigned long *par);

int main ()
{
   unsigned long sec;


   getSeconds( &amp;#x26;sec );

   /* 输出实际值 */
   printf(&quot;Number of seconds: %ld\n&quot;, sec );

   return 0;
}

void getSeconds(unsigned long *par)
{
   /* 获取当前的秒数 */
   *par = time( NULL );
   return;
}
/*
Number of seconds :1294450468
*/
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;能接受指针作为参数的函数，也能接受数组作为参数，如下所示：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-c&quot;&gt;#include &amp;#x3C;stdio.h&gt;

/* 函数声明 */
double getAverage(int *arr, int size);

int main ()
{
   /* 带有 5 个元素的整型数组  */
   int balance[5] = {1000, 2, 3, 17, 50};
   double avg;

   /* 传递一个指向数组的指针作为参数 */
   avg = getAverage( balance, 5 ) ;

   /* 输出返回值  */
   printf(&quot;Average value is: %f\n&quot;, avg );

   return 0;
}

double getAverage(int *arr, int size)
{
  int    i, sum = 0;
  double avg;

  for (i = 0; i &amp;#x3C; size; ++i)
  {
    sum += arr[i];
  }

  avg = (double)sum / size;

  return avg;
}
/*
Average value is: 214.40000
*/
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;从函数返回指针&lt;/h4&gt;
&lt;p&gt;C 允许从函数返回指针。为了做到这点，必须声明一个返回指针的函数，如下所示：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-c&quot;&gt;int * myFunction(){...}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;另外，C 语言不支持在调用函数时返回局部变量的地址，除非定义局部变量为 &lt;strong&gt;static&lt;/strong&gt; 变量。&lt;/p&gt;
&lt;p&gt;它会生成 10 个随机数，并使用表示指针的数组名（即第一个数组元素的地址）来返回它们&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-c&quot;&gt;#include &amp;#x3C;stdio.h&gt;
#include &amp;#x3C;time.h&gt;
#include &amp;#x3C;stdlib.h&gt;

/* 要生成和返回随机数的函数 */
int * getRandom( )
{
   static int  r[10];
   int i;

   /* 设置种子 */
   srand( (unsigned)time( NULL ) );
   for ( i = 0; i &amp;#x3C; 10; ++i)
   {
      r[i] = rand();
      printf(&quot;%d\n&quot;, r[i] );
   }

   return r;
}

/* 要调用上面定义函数的主函数 */
int main ()
{
   /* 一个指向整数的指针 */
   int *p;
   int i;

   p = getRandom();
   for ( i = 0; i &amp;#x3C; 10; i++ )
   {
       printf(&quot;*(p + [%d]) : %d\n&quot;, i, *(p + i) );
   }

   return 0;
}
/*
1523198053
1187214107
1108300978
430494959
1421301276
930971084
123250484
106932140
1604461820
149169022
*(p + [0]) : 1523198053
*(p + [1]) : 1187214107
*(p + [2]) : 1108300978
*(p + [3]) : 430494959
*(p + [4]) : 1421301276
*(p + [5]) : 930971084
*(p + [6]) : 123250484
*(p + [7]) : 106932140
*(p + [8]) : 1604461820
*(p + [9]) : 149169022
*/
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;字符串&lt;/h3&gt;
&lt;h4&gt;定义&lt;/h4&gt;
&lt;p&gt;在 C 语言中，字符串实际上是使用 &lt;strong&gt;null&lt;/strong&gt; 字符 &lt;strong&gt;\0&lt;/strong&gt; 终止的一维字符数组。因此，一个以 &lt;strong&gt;null&lt;/strong&gt; 结尾的字符串，包含了组成字符串的字符。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-c&quot;&gt;char yrh[7] = {&apos;l&apos;,&apos;o&apos;,&apos;v&apos;,&apos;e&apos;,&apos;y&apos;,&apos;z&apos;,&apos;\0&apos;};
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;依据数组初始化规则，可以把上面的语句写成以下语句：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-c&quot;&gt;char yrh[] = &quot;loveyz&quot;;
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;操作字符串的函数:&lt;/h4&gt;
&lt;pre&gt;&lt;code class=&quot;language-c&quot;&gt;#include &amp;#x3C;string.h&gt;
| 序号 | 函数 &amp;#x26; 目的 |
| :--- | :----------------------------------------------------------------------------------------------------- |
| 1    | **strcpy(s1, s2);** 复制字符串 s2 到字符串 s1。                                                        |
| 2    | **strcat(s1, s2);** 连接字符串 s2 到字符串 s1 的末尾。                                                 |
| 3    | **strlen(s1);** 返回字符串 s1 的长度。                                                                 |
| 4    | **strcmp(s1, s2);** 如果 s1 和 s2 是相同的，则返回 0；如果 s1 &amp;#x3C; s2 则返回小于 0；如果 s1 &gt; s2 则返回大于 0。 |
| 5    | **strchr(s1, ch);** 返回一个指针，指向字符串 s1 中字符 ch 的第一次出现的位置。                         |
| 6    | **strstr(s1, s2);** 返回一个指针，指向字符串 s1 中字符串 s2 的第一次出现的位置。                       |

#include &amp;#x3C;stdio.h&gt;
#include &amp;#x3C;string.h&gt;

int main ()
{
   char str1[14] = &quot;yrh&quot;;
   char str2[14] = &quot;yz&quot;;
   char str3[14];
   int  len ;

   /* 复制 str1 到 str3 */
   strcpy(str3, str1);
   printf(&quot;strcpy( str3, str1) :  %s\n&quot;, str3 );

   /* 连接 str1 和 str2 */
   strcat( str1, str2);
   printf(&quot;strcat( str1, str2):   %s\n&quot;, str1 );

   /* 连接后，str1 的总长度 */
   len = strlen(str1);
   printf(&quot;strlen(str1) :  %d\n&quot;, len );

   return 0;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;排序算法&lt;/h2&gt;
&lt;h3&gt;冒泡排序&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-c&quot;&gt;#include &amp;#x3C;stdio.h&gt;
void bubble_sort(int arr[], int len) {
    int i, j, temp;
    for (i = 0; i &amp;#x3C; len - 1; i++)
        for (j = 0; j &amp;#x3C; len - 1 - i; j++)
            if (arr[j] &gt; arr[j + 1]) {
                temp = arr[j];
                arr[j] = arr[j + 1];
                arr[j + 1] = temp;
            }
}
int main() {
    int arr[] = { 22, 34, 3, 32, 82, 55, 89, 50, 37, 5, 64, 35, 9, 70 };
    int len = (int) sizeof(arr) / sizeof(*arr);
    bubble_sort(arr, len);
    int i;
    for (i = 0; i &amp;#x3C; len; i++)
        printf(&quot;%d &quot;, arr[i]);
    return 0;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;选择排序&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-c&quot;&gt;void selection_sort(int a[], int len)
{
    int i,j,temp;

    for (i = 0 ; i &amp;#x3C; len - 1 ; i++)
    {
        int min = i;                  // 记录最小值，第一个元素默认最小
        for (j = i + 1; j &amp;#x3C; len; j++)     // 访问未排序的元素
        {
            if (a[j] &amp;#x3C; a[min])    // 找到目前最小值
            {
                min = j;    // 记录最小值
            }
        }
        if(min != i)
        {
            temp=a[min];  // 交换两个变量
            a[min]=a[i];
            a[i]=temp;
        }
        /* swap(&amp;#x26;a[min], &amp;#x26;a[i]);  */   // 使用自定义函数交換
    }
}

/*
void swap(int *a,int *b) // 交换两个变量
{
    int temp = *a;
    *a = *b;
    *b = temp;
}
*/
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;插入排序&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-c&quot;&gt;void insertion_sort(int arr[], int len){
    int i,j,temp;
    for (i=1;i&amp;#x3C;len;i++){
            temp = arr[i];
            for (j=i;j&gt;0 &amp;#x26;&amp;#x26; arr[j-1]&gt;temp;j--)
                    arr[j] = arr[j-1];
            arr[j] = temp;
    }
}
&lt;/code&gt;&lt;/pre&gt;</content:encoded><h:img src="undefined"/><enclosure url="undefined"/></item><item><title>通过 webRTC 可以绕过获取真实IP</title><link>https://blog.myyrh.com/blog/webrtcgetip</link><guid isPermaLink="true">https://blog.myyrh.com/blog/webrtcgetip</guid><description>通过 webRTC 可以绕过获取真实IP</description><pubDate>Wed, 23 Mar 2022 00:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;webRTC是啥：&lt;/h2&gt;
&lt;blockquote&gt;
&lt;p&gt;WebRTC实现了基于网页的视频会议，标准是WHATWG
协议，目的是通过浏览器提供简单的javascript就可以达到实时通讯（Real-Time Communications (RTC)）能力。
WebRTC（Web Real-Time
Communication）项目的最终目的主要是让Web开发者能够基于浏览器（Chrome\FireFox...）轻易快捷开发出丰富的实时多媒体应用，而无需下载安装任何插件，Web开发者也无需关注多媒体的数字信号处理过程，只需编写简单的Javascript程序即可实现，W3C等组织正在制定Javascript
标准API，目前是WebRTC
1.0版本，Draft状态；另外WebRTC还希望能够建立一个多互联网浏览器间健壮的实时通信的平台，形成开发者与浏览器厂商良好的生态环境。同时，Google也希望和致力于让WebRTC的技术成为HTML5标准之一，可见Google布局之深远。
WebRTC提供了视频会议的核心技术，包括音视频的采集、编解码、网络传输、显示等功能，并且还支持跨平台：windows，linux，mac，android。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;发现通过 webRTC 可以绕过获取真实IP，可怕，放代码…&lt;/h2&gt;
&lt;pre&gt;&lt;code class=&quot;language-Javascript&quot;&gt;
function getIPs(callback){
    var ip_dups = {};

    var RTCPeerConnection = window.RTCPeerConnection
        || window.mozRTCPeerConnection
        || window.webkitRTCPeerConnection;
    var useWebKit = !!window.webkitRTCPeerConnection;

    if(!RTCPeerConnection){
        var win = iframe.contentWindow;
        RTCPeerConnection = win.RTCPeerConnection
            || win.mozRTCPeerConnection
            || win.webkitRTCPeerConnection;
        useWebKit = !!win.webkitRTCPeerConnection;
    }

    var mediaConstraints = {
        optional: [{RtpDataChannels: true}]
    };

    var servers = {iceServers: [{urls: &quot;stun:stun.l.google.com:19302&quot;}]};

    var pc = new RTCPeerConnection(servers, mediaConstraints);

    function handleCandidate(candidate){
        var ip_regex = /([0-9]{1,3}(\.[0-9]{1,3}){3}|[a-f0-9]{1,4}(:[a-f0-9]{1,4}){7})/
        var ip_addr = ip_regex.exec(candidate) &amp;#x26;&amp;#x26; ip_regex.exec(candidate)[1];

        if(!ip_addr) return;
        if(ip_dups[ip_addr] === undefined)
            callback(ip_addr);

        ip_dups[ip_addr] = true;
    }

    pc.onicecandidate = function(ice){
        if(ice.candidate)
            handleCandidate(ice.candidate.candidate);
    };

    pc.createDataChannel(&quot;&quot;);

    pc.createOffer(function(result){

        pc.setLocalDescription(result, function(){}, function(){});

    }, function(){});

    setTimeout(function(){
        var lines = pc.localDescription.sdp.split(&apos;\n&apos;);

        lines.forEach(function(line){
            if(line.indexOf(&apos;a=candidate:&apos;) === 0)
                handleCandidate(line);
        });
    }, 1000);
}

getIPs(function(ip){
    ip &amp;#x26;&amp;#x26; console.log(ip);
});

&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;丢入控制台运行就出来了，各位不要搞事~&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://www.helloimg.com/images/2021/09/06/CboZzX.png&quot; alt=&quot;1&quot;&gt;&lt;/p&gt;</content:encoded><h:img src="undefined"/><enclosure url="undefined"/></item></channel></rss>