[{"content":"之前从网上找了很多关于 IFS 的二手资料，每次用每次都得再看一遍，理解还是不够透彻。最近对照着 man bash ，终于把这个 IFS 搞清楚了。\nIFS 的三种作用 IFS 其实只在 3 个地方发挥作用：\n用于扩展带双引号的 \u0026quot;$*\u0026quot; 用于不带双引号的变量扩展 / 子命令扩展 / 算数扩展 用于在内建命令 read 中进行断词 可以看到，其实只有 bash 本身和 read 命令会用到 IFS 这个环境变量。所以，除了\nIFS=xxx bash -c \u0026#34;xxx\u0026#34; 和\nIFS=xxx read a b c 之外，任何的 IFS=xxx 临时环境变量都是不会起到任何作用的。\n辅助工具 先写一个小的辅助程序，名为 argsecho，用于直观展示所有参数列表。\nimport sys for ind, arg in enumerate(sys.argv[1:], 1): print(\u0026#34;${}: {}\u0026#34;.format(ind, arg), flush=True) 1. 用于扩展带双引号的 $* 对于一个数组\narr=(1 2 3 4 5) \u0026quot;${arr[@]}\u0026quot; 会展开为 \u0026quot;1\u0026quot; \u0026quot;2\u0026quot; \u0026quot;3\u0026quot; \u0026quot;4\u0026quot; \u0026quot;5\u0026quot; ，是最忠实于原数组的展开方式。通常用于将参数列表原封不动传递给子命令。展开的过程中不涉及 IFS。\nladyrick $ argsecho \u0026#34;${arr[@]}\u0026#34; $1: 1 $2: 2 $3: 3 $4: 4 $5: 5 ${arr[*]} 则会读取 IFS 的第一个字符，作为分隔符。设为 c，则会展开为 \u0026quot;1c2c3c4c5\u0026quot;。如果 IFS 为空，则直接拼接，不插入分隔符，直接展开为\u0026quot;12345\u0026quot;。\n$\u0026gt; IFS=c $\u0026gt; argsecho \u0026#34;${arr[*]}\u0026#34; $1: 1c2c3c4c5 bash 中凡是需要把数组展开的，都遵循这一规律。比如：\nladyrick $ set -- 1 2 3 4 5 ladyrick $ argsecho \u0026#34;$@\u0026#34; $1: 1 $2: 2 $3: 3 $4: 4 $5: 5 ladyrick $ IFS=c ladyrick $ argsecho \u0026#34;$*\u0026#34; $1: 1c2c3c4c5 再比如，根据前缀查找环境变量名：\nladyrick $ MYENV1=1 MYENV2=2 MYENV3=3 MYENV4=4 ladyrick $ argsecho \u0026#34;${!MYENV@}\u0026#34; $1: MYENV1 $2: MYENV2 $3: MYENV3 $4: MYENV4 ladyrick $ IFS=c ladyrick $ argsecho \u0026#34;${!MYENV*}\u0026#34; $1: MYENV1cMYENV2cMYENV3cMYENV4 再次强调，直接在命令前面设置临时变量 IFS 是没有用的，因为这种写法的 IFS 环境变量只会在 argsecho 命令内部生效，并不会在参数解析阶段生效。\nladyrick $ set -- 1 2 3 4 5 ladyrick $ IFS=c argsecho \u0026#34;$*\u0026#34; $1: 1 2 3 4 5 2. 用于不带双引号的变量扩展 / 子命令扩展 / 算数扩展 当不带双引号的这三种扩展作为命令行参数时，会根据 IFS 环境变量的值对这三种扩展的结果进行拆分，拆分成多个参数。如果 IFS 为空，则不会进行拆分。\n变量扩展 ladyrick $ args=\u0026#34;1:2,3.4/5\u0026#34; ladyrick $ IFS=:,./ ladyrick $ argsecho $args $1: 1 $2: 2 $3: 3 $4: 4 $5: 5 子命令扩展 ladyrick $ IFS=abcd ladyrick $ argsecho $(echo 1a2b3c4d5) $1: 1 $2: 2 $3: 3 $4: 4 $5: 5 算术扩展 ladyrick $ IFS=0 ladyrick $ argsecho $((102030400 + 5)) $1: 1 $2: 2 $3: 3 $4: 4 $5: 5 还有一些细节方面的注意点，一般不需要关注，仅供备忘查阅。\nThe shell treats each character of IFS as a delimiter, and splits the results of the other expansions into words on these characters. If IFS is unset, or its value is exactly \u0026lt;space\u0026gt;\u0026lt;tab\u0026gt;\u0026lt;newline\u0026gt;, the default, then sequences of \u0026lt;space\u0026gt;, \u0026lt;tab\u0026gt;, and \u0026lt;newline\u0026gt; at the beginning and end of the results of the previous expansions are ignored, and any sequence of IFS characters not at the beginning or end serves to delimit words. If IFS has a value other than the default, then sequences of the whitespace characters space and tab are ignored at the beginning and end of the word, as long as the whitespace character is in the value of IFS (an IFS whitespace character). Any character in IFS that is not IFS whitespace, along with any adjacent IFS whitespace characters, delimits a field. A sequence of IFS whitespace characters is also treated as a delimiter. If the value of IFS is null, no word splitting occurs.\n3. 用于在内建命令 read 中进行断词 这是唯一一个在命令内部生效的 IFS 用法，可以使用临时环境变量的写法。\nIFS 用于在 read 内部，把一行输入文本断为一个一个的词，然后赋值给传入的变量名。举个例子：\nladyrick $ IFS=0 read a b c \u0026lt;\u0026lt;\u0026lt; 10203 ladyrick $ argsecho \u0026#34;a=$a\u0026#34; \u0026#34;b=$b\u0026#34; \u0026#34;c=$c\u0026#34; $1: a=1 $2: b=2 $3: c=3 这里的输入数据为10203，read 从 stdin 接收输入后，在内部读取到 IFS 为字符 '0'，所以将它拆分为了[\u0026quot;1\u0026quot;, \u0026quot;2\u0026quot;, \u0026quot;3\u0026quot;]，然后分别赋值给a b c 三个变量。\n这里还有一点需要注意，假如传入的变量数量少于分词后词的数量，那么会依次赋值，最后一个变量接收剩余所有的内容，不再进行分词。举个例子：\nladyrick $ IFS=abcde read a b c \u0026lt;\u0026lt;\u0026lt; 1a2b3c4d5e6 ladyrick $ argsecho \u0026#34;a=$a\u0026#34; \u0026#34;b=$b\u0026#34; \u0026#34;c=$c\u0026#34; $1: a=1 $2: b=2 $3: c=3c4d5e6 注意这里 c 的获取，并不是先整体分词然后再组合，可以理解为一边分词一边赋值，最后 c 只剩一个变量，就不再分词。极端情况下，就是我们常见的只有一个变量的情况。这时也不会进行分词。\nPS：这里可能会有疑问，IFS 分词跟 read 的 -d 参数有什么区别？-d 也是作为分隔符。\n答案是，IFS 是在一行内进行分词，-d 是用来分割不同的行的。\n注意观察有没有 -d 参数的行为差别：\nladyrick $ IFS=0 read -a arr \u0026lt;\u0026lt;\u0026lt; \u0026#39;10203 40506\u0026#39; ladyrick $ argsecho \u0026#34;${arr[@]}\u0026#34; $1: 1 $2: 2 $3: 3 ladyrick $ IFS=0 read -a arr -d \u0026#39;\u0026#39; \u0026lt;\u0026lt;\u0026lt; \u0026#39;10203 40506\u0026#39; ladyrick $ argsecho \u0026#34;${arr[@]}\u0026#34; $1: 1 $2: 2 $3: 3 4 $4: 5 $5: 6 ladyrick $ # 思考题：为啥上面的输出 6 后面会多一个换行符？ 后记 其实 IFS 还有一个作用，是用在 complete 命令的 -W 参数中，进行分词用的。这个命令是用来进行 bash 自动补全的，比较小众，所以就不介绍了。\n","permalink":"https://idea.ladyrick.com/posts/bash-ifs-%E7%8E%AF%E5%A2%83%E5%8F%98%E9%87%8F%E7%94%A8%E6%B3%95%E6%80%BB%E7%BB%93/","summary":"之前从网上找了很多关于 IFS 的二手资料，每次用每次都得再看一遍，理解还是不够透彻。最近对照着 man bash ，终于把这个 IFS 搞清楚了。\nIFS 的三种作用 IFS 其实只在 3 个地方发挥作用：\n用于扩展带双引号的 \u0026quot;$*\u0026quot; 用于不带双引号的变量扩展 / 子命令扩展 / 算数扩展 用于在内建命令 read 中进行断词 可以看到，其实只有 bash 本身和 read 命令会用到 IFS 这个环境变量。所以，除了\nIFS=xxx bash -c \u0026#34;xxx\u0026#34; 和\nIFS=xxx read a b c 之外，任何的 IFS=xxx 临时环境变量都是不会起到任何作用的。\n辅助工具 先写一个小的辅助程序，名为 argsecho，用于直观展示所有参数列表。\nimport sys for ind, arg in enumerate(sys.argv[1:], 1): print(\u0026#34;${}: {}\u0026#34;.format(ind, arg), flush=True) 1. 用于扩展带双引号的 $* 对于一个数组\narr=(1 2 3 4 5) \u0026quot;${arr[@]}\u0026quot; 会展开为 \u0026quot;1\u0026quot; \u0026quot;2\u0026quot; \u0026quot;3\u0026quot; \u0026quot;4\u0026quot; \u0026quot;5\u0026quot; ，是最忠实于原数组的展开方式。通常用于将参数列表原封不动传递给子命令。展开的过程中不涉及 IFS。","title":"BASH IFS 环境变量用法总结"},{"content":"本文介绍了浮点数的表示方法，以及直观验证。\n介绍 简单地说，一个float型实数在内存中占4个字节，即32个二进制bit，从低位到高位依次叫第0位到第31位。 这32位可以分为3个部分：符号位（第31位），阶码（第30位到第23位共8位），尾数（最低23位）。\n符号位。最高位也就是第31位表示这个实数是正数还是负数，为0表示正数或0，为1表示负数。 阶码。第30位到第23位这8个二进制位表示该实数转化为规格化的二进制实数后的指数与127(127即所谓偏移量)之和即所谓阶码。 规格化的二进制实数的指数只能在-127~+127之间，所以，一个float型数的最大值在+2^127即+3.4*10^38，最小值在-2^127即-3.4*10^38。 尾数。其他最低的23位，即第22位到第0位，表示该实数转化为规格化的二进制实数后小数点以后的其余各位。 例一 例如，将十进制178.125表示成机器内的32个字节的二进制形式。\n第一步：将178.125表示成二进制数 (178.125)(十进制数)=(10110010.001)(二进制形式)\n第二步：将二进制形式的浮点实数转化为规格化的形式 小数点向左移动7个二进制位可以得到： 10110010.001=1.0110010001*2^7 因而产生了以下三项:\n符号位：该数为正数，故第31位为0，占一个二进制位。 阶码：指数为7，故其阶码为127+7=134=(10000110)(二进制)，占从第30到第23共8个二进制位。 尾数为小数点后的部分， 即0110010001。因为尾数共23个二进制位，在后面补13个0，即：01100100010000000000000 所以，178.125在内存中的实际表示方式为:\n0 10000110 01100100010000000000000 例二 再如，将-0.15625表示成机器内的32个字节的形式.\n第一步：将-0.15625表示成二进制形式 (-0.15625)(十进制数)=(-0.00101)(二进制形式)\n第二步：将二进制形式的浮点数转化为规格化的形式 小数点向右移动3个二进制位可以得到： -0.00101=-1.01*2^(-3) 同样，产生了三项:\n符号位：该数为负数，故第31位为1，占一个二进制位。 阶码：指数为-3，故其阶码为127+(-3)=124=01111100，占从第30到第23共8个二进制位。 尾数为小数点后的01，当然后面要补21个0。 所以，-0.15625在内存中的实际表示形式为: 1 01111100 01000000000000000000000 验证 可以通过以下的C++程序验证之。记得添加编译选项--std=c++11\n#include \u0026lt;iostream\u0026gt; #include \u0026lt;string\u0026gt; #include \u0026lt;sstream\u0026gt; #include \u0026lt;bitset\u0026gt; #include \u0026lt;typeinfo\u0026gt; using namespace std; class cast2bits { private: struct bits { unsigned char b7 : 1; unsigned char b6 : 1; unsigned char b5 : 1; unsigned char b4 : 1; unsigned char b3 : 1; unsigned char b2 : 1; unsigned char b1 : 1; unsigned char b0 : 1; }; int length; ostringstream oss; ostringstream data; public: template\u0026lt;class T\u0026gt; explicit cast2bits(const T \u0026amp;input) { data \u0026lt;\u0026lt; input \u0026lt;\u0026lt; \u0026#34; type: \u0026#34; \u0026lt;\u0026lt; typeid(input).name(); const T *pTInput = \u0026amp;input; auto *pBitsinput = (bits *) pTInput; int n = sizeof(T) / sizeof(bits); length = 9 * n - 1; for (int i = n - 1; i \u0026gt;= 0; --i) { oss \u0026lt;\u0026lt; (int) ((pBitsinput + i)-\u0026gt;b0) \u0026lt;\u0026lt; (int) ((pBitsinput + i)-\u0026gt;b1) \u0026lt;\u0026lt; (int) ((pBitsinput + i)-\u0026gt;b2) \u0026lt;\u0026lt; (int) ((pBitsinput + i)-\u0026gt;b3) \u0026lt;\u0026lt; (int) ((pBitsinput + i)-\u0026gt;b4) \u0026lt;\u0026lt; (int) ((pBitsinput + i)-\u0026gt;b5) \u0026lt;\u0026lt; (int) ((pBitsinput + i)-\u0026gt;b6) \u0026lt;\u0026lt; (int) ((pBitsinput + i)-\u0026gt;b7) \u0026lt;\u0026lt; \u0026#39; \u0026#39;; } } friend ostream \u0026amp;operator\u0026lt;\u0026lt;(ostream \u0026amp;os, const cast2bits \u0026amp;c) { os \u0026lt;\u0026lt; c.data.str() \u0026lt;\u0026lt; endl; os \u0026lt;\u0026lt; \u0026#34;M\u0026#34; \u0026lt;\u0026lt; string((unsigned long long int) (c.length - 2), \u0026#39; \u0026#39;) \u0026lt;\u0026lt; \u0026#34;L\u0026#34; \u0026lt;\u0026lt; endl; os \u0026lt;\u0026lt; c.oss.str() \u0026lt;\u0026lt; endl; return os; } }; int main() { cout \u0026lt;\u0026lt; cast2bits(178.125f) \u0026lt;\u0026lt; endl; cout \u0026lt;\u0026lt; cast2bits(-0.15625f) \u0026lt;\u0026lt; endl; return 0; } 输出为：\n178.125 type: f M L 01000011 00110010 00100000 00000000 -0.15625 type: f M L 10111110 00100000 00000000 00000000 最后，这个程序其实不仅可以处理浮点数，也可以处理其他各种数据类型在内存中的表示形式。 比如，你可以试试以下代码的输出结果。\ncout \u0026lt;\u0026lt; cast2bits(178.125) \u0026lt;\u0026lt; endl; cout \u0026lt;\u0026lt; cast2bits(-0.15625) \u0026lt;\u0026lt; endl; cout \u0026lt;\u0026lt; cast2bits(123) \u0026lt;\u0026lt; endl; cout \u0026lt;\u0026lt; cast2bits(123L) \u0026lt;\u0026lt; endl; cout \u0026lt;\u0026lt; cast2bits(123LL) \u0026lt;\u0026lt; endl; ","permalink":"https://idea.ladyrick.com/posts/c++-float%E6%95%B0%E6%8D%AE%E5%9C%A8%E5%86%85%E5%AD%98%E4%B8%AD%E7%9A%84%E8%A1%A8%E7%A4%BA%E5%BD%A2%E5%BC%8F/","summary":"\u003cp\u003e本文介绍了浮点数的表示方法，以及直观验证。\u003c/p\u003e","title":"C++ float数据在内存中的表示形式"},{"content":"需求 为了更方便地学习python，就寻思着在腾讯云服务器上部署一个jupyter服务。 但是，如果只是起一个jupyter notebook在后台运行，那我就必须访问ladyrick.com:8888来访问（假设端口为8888）。 这是非常丑陋的。 我希望可以通过访问jupyter.ladyrick.com来访问我的jupyter服务。\n另外，为了防止别人访问，只为我一个人服务，需要给jupyter服务设置密码。\n解决方案 1. 为jupyter设置密码 这个非常简单了。直接运行\njupyter notebook password 然后根据提示，输入密码即可。\n这会在~/.jupyter/jupyter_notebook_config.json文件中写入密码的哈希值，这样以后在新浏览器登陆时，就需要输入密码。\n2. 设置apache反向代理 首先我们先配置一下jupyter。\n生成jupyter的配置文件：\njupyter notebook --generate-config 这会将默认配置写入~/.jupyter/jupyter_notebook_config.py文件。\n打开配置文件，修改配置项如下：\nc.NotebookApp.port = 8888 # 自定义端口号。 c.NotebookApp.open_browser = False # 不打开浏览器。因为这是在命令行服务器上，自然没有浏览器。 c.NotebookApp.ip = \u0026#34;localhost\u0026#34; # 配置IP地址。localhost表明jupyter服务只能在本机访问。 c.NotebookApp.notebook_dir = \u0026#34;/home/username/jupyter\u0026#34; # jupyter的起始文件夹。不配置的话就是home文件夹。最好用绝对路径。 启用apache的一些mod。\nsudo a2enmod proxy proxy_http proxy_wstunnel 配置apache。 在/etc/apache2/sites-available下新建配置文件jupyter.conf。 配置文件内容如下：\n\u0026lt;VirtualHost *:80\u0026gt; ServerName jupyter.ladyrick.com ProxyRequests off \u0026lt;Location /\u0026gt; ProxyPass http://localhost:8888/ ProxyPassReverse http://localhost:8888/ ProxyPassReverseCookieDomain localhost jupyter.ladyrick.com RequestHeader set Origin \u0026#34;http://localhost:8888\u0026#34; \u0026lt;/Location\u0026gt; \u0026lt;Location /api/kernels/\u0026gt; ProxyPass ws://localhost:8888/api/kernels/ ProxyPassReverse ws://localhost:8888/api/kernels/ \u0026lt;/Location\u0026gt; \u0026lt;Location /terminals/websocket/\u0026gt; ProxyPass ws://localhost:8888/terminals/websocket/ ProxyPassReverse ws://localhost:8888/terminals/websocket/ \u0026lt;/Location\u0026gt; \u0026lt;/VirtualHost\u0026gt; 保存关闭，并启用该配置：\nsudo a2ensite jupyter.conf 最后重启apache即可:\nsudo service apache2 restart 总结 密码配置简单，反向代理也很好配置，主要是一开始配置的反向代理，所有ws://协议的请求都失败了。 因此需要对websocket请求处理一下，不能直接使用顶层的配置，不然会转成http请求。 这个问题还困扰了挺久的。\n","permalink":"https://idea.ladyrick.com/posts/%E5%9C%A8%E6%9C%8D%E5%8A%A1%E5%99%A8%E4%B8%8A%E9%83%A8%E7%BD%B2jupyter%E4%BE%9B%E8%87%AA%E5%B7%B1%E4%BD%BF%E7%94%A8/","summary":"\u003ch1 id=\"需求\"\u003e需求\u003c/h1\u003e\n\u003cp\u003e为了更方便地学习python，就寻思着在腾讯云服务器上部署一个jupyter服务。\n但是，如果只是起一个\u003ccode\u003ejupyter notebook\u003c/code\u003e在后台运行，那我就必须访问\u003ccode\u003eladyrick.com:8888\u003c/code\u003e来访问（假设端口为8888）。\n这是非常丑陋的。\n我希望可以通过访问\u003ccode\u003ejupyter.ladyrick.com\u003c/code\u003e来访问我的jupyter服务。\u003c/p\u003e\n\u003cp\u003e另外，为了防止别人访问，只为我一个人服务，需要给jupyter服务设置密码。\u003c/p\u003e","title":"在服务器上部署jupyter供自己使用"},{"content":"引子 给定一个数组，寻找出数组中前k小的元素。\n对于这个经典问题，我之前一直只知道可以用堆来解决。\n构造一个大根堆，然后遍历所有数据，如果数据小于堆顶元素，就替换堆顶元素，调整堆。\n这样最后堆中的元素就是原数组中前k小的元素。\n这个算法的时间复杂度为O(nlogk)。\n但是，我发现，C++ stl 中关于堆的函数有，make_heap，push_heap，pop_heap，sort_heap，is_heap。\n这其中，貌似并没有提供“替换堆顶元素，调整堆”的函数。要实现这个效果，可以通过先pop_heap，再push_heap来实现。\n因此，我对这两种方式进行了一个对比：\n方法1：先pop_heap，再push_heap来替换堆顶元素。 方法2：自己实现调整堆顶元素的功能。 方法1 使用pop_heap和push_heap来两步调整堆顶。\nint func(vector\u0026lt;int\u0026gt; \u0026amp;data, size_t k) { cout \u0026lt;\u0026lt; \u0026#34;std::pop_heap and std::push_heap\u0026#34; \u0026lt;\u0026lt; endl; vector\u0026lt;int\u0026gt; heap; heap.reserve(k); for (int i : data) { if (heap.size() \u0026lt; k) { heap.push_back(i); push_heap(heap.begin(), heap.end()); } else { if (i \u0026lt; heap.front()) { pop_heap(heap.begin(), heap.end()); heap.back() = i; push_heap(heap.begin(), heap.end()); } } } return heap.front(); } 方法2 自己实现调整堆顶。\nint func(vector\u0026lt;int\u0026gt; \u0026amp;data, size_t k) { cout \u0026lt;\u0026lt; \u0026#34;replace heap top and adjust heap\u0026#34; \u0026lt;\u0026lt; endl; vector\u0026lt;int\u0026gt; heap; heap.reserve(k); for (int i : data) { if (heap.size() \u0026lt; k) { heap.push_back(i); push_heap(heap.begin(), heap.end()); } else { if (i \u0026lt; heap.front()) { heap.front() = i; bool adjust = true; int st = 0; while (adjust) { int largest = st; int left = (st \u0026lt;\u0026lt; 1) + 1; int right = (st \u0026lt;\u0026lt; 1) + 2; int size = heap.size(); if (left \u0026lt; size \u0026amp;\u0026amp; heap[left] \u0026gt; heap[largest]) largest = left; if (right \u0026lt; size \u0026amp;\u0026amp; heap[right] \u0026gt; heap[largest]) largest = right; if (largest != st) { std::swap(heap[st], heap[largest]); st = largest; } else { adjust = false; } } } } } return heap.front(); } 方法1 和方法2 的运行结果如下：\nstd::pop_heap and std::push_heap result = 31415926 CORRECT copy data time used = 15ms pure compute time used = 233ms replace heap top and adjust heap result = 31415926 CORRECT copy data time used = 15ms pure compute time used = 246ms 果然，还是我写的调整堆顶太丑了，连stl的两步调整的效率都比不上。\n理论上，一步调整堆顶的效率肯定会比两步调整要高吧。\n所以结论是，下次遇到堆调整的问题，直接上pop_heap和push_heap。\n回到最初的问题。\n对于寻找数组前k小元素的问题，在知乎上讨论之后，发现其实还有比建堆更高效的方法。\n那就是不断使用partition算法。\npartition算法是这样的，随机取出一个元素（比如就取最后的元素）作为枢轴（pivot），然后调整数组，把数组分为两部分，左边的都是不大于枢轴的，右边的都是不小于枢轴的。\n通过使用一次partition算法，我们可以将数组分割。但是此时的分割不一定是符合要求的分割（枢轴不是第k个元素）。因此，我们需要不断分割，直到枢轴落到第k个元素。此时就找到了前k小的元素。\n这个算法的时间复杂度为O(n)。\n方法3 使用stl的std::nth_element函数。\n这个函数内部就是采用不断partition的方法。\nint func(vector\u0026lt;int\u0026gt; \u0026amp;data, size_t k) { cout \u0026lt;\u0026lt; \u0026#34;std::nth_element\u0026#34; \u0026lt;\u0026lt; endl; nth_element(data.begin(), data.begin() + k - 1, data.end()); display(data); return data[k - 1]; } 运行结果如下：\nstd::nth_element result = 31415926 CORRECT copy data time used = 14ms pure compute time used = 86ms 可以看到运行速度一下快了一大截。\n方法4 自己手痒实现的类似std::nth_element的算法。\nint func(vector\u0026lt;int\u0026gt; \u0026amp;data, size_t k) { cout \u0026lt;\u0026lt; \u0026#34;my partition function\u0026#34; \u0026lt;\u0026lt; endl; int left = 0, right = data.size() - 1; while (true) { int l = left, r = right; int pivot = data[r]; while (l \u0026lt; r) { while (l \u0026lt; r \u0026amp;\u0026amp; data[l] \u0026lt;= pivot) ++l; data[r] = data[l]; while (l \u0026lt; r \u0026amp;\u0026amp; data[r] \u0026gt;= pivot) --r; data[l] = data[r]; } data[l] = pivot; if (l \u0026lt; k - 1) { left = l + 1; } else if (l \u0026gt; k - 1) { right = l - 1; } else { return data[l]; } } } 运行结果如下：\nmy partition function result = 31415926 CORRECT copy data time used = 14ms pure compute time used = 84ms 自己写的算法效率跟stl差不多。\n方法5 知乎上还有人说用std::partial_sort函数。\n这个函数是部分排序函数，内部使用的是堆排序。\nint func(vector\u0026lt;int\u0026gt; \u0026amp;data, size_t k) { cout \u0026lt;\u0026lt; \u0026#34;std::partial_sort\u0026#34; \u0026lt;\u0026lt; endl; partial_sort(data.begin(), data.begin() + k, data.end()); display(data); return data[k - 1]; } 运行结果如下：\nstd::partial_sort result = 31415926 CORRECT copy data time used = 13ms pure compute time used = 830ms 太慢了吧。\n方法6 stl实现了一个优先队列，其实内部也是堆。\nint func(vector\u0026lt;int\u0026gt; \u0026amp;data, size_t k) { cout \u0026lt;\u0026lt; \u0026#34;std::priority_queue\u0026#34; \u0026lt;\u0026lt; endl; priority_queue\u0026lt;int, vector\u0026lt;int\u0026gt;, less\u0026lt;int\u0026gt;\u0026gt; prique; for (int i : data) { if (prique.size() \u0026lt; k) { prique.push(i); } else if (prique.top() \u0026gt; i) { prique.pop(); prique.push(i); } } return prique.top(); } 运行结果如下：\nstd::priority_queue result = 31415926 CORRECT copy data time used = 14ms pure compute time used = 268ms 可以看到跟维护堆的方法效率差不多。\n结论 对于“数组中前k小元素”的问题，有两种算法。\n维护堆数据结构。（O(nlogk)） 不断使用partition算法。（O(n)） 方法1的优点在于，可以不需要一次处理所有数据，也就是说可以处理流数据。\n而且，其实不需要纠结pop_heap+push_heap，跟直接手动替换堆顶元素再调整堆，哪个更快。\n除非stl官方实现了后者，我们还是用前者比较方便。或者用优先队列更省心。\n如果用方法2，虽然时间复杂度比较低，但是其实波动很大。\n在我的测试中，有时候可以比方法1高效10倍，有时候跟方法1差不多。\n而且这种方法必须一次性处理所有数据，必须纵观全局。\n至于partial_sort，拜拜了吧您。\n","permalink":"https://idea.ladyrick.com/posts/%E6%95%B0%E7%BB%84%E5%89%8Dk%E5%B0%8F%E5%85%83%E7%B4%A0%E7%9A%84%E5%90%84%E7%A7%8D%E6%96%B9%E6%B3%95%E5%AF%B9%E6%AF%94/","summary":"\u003ch1 id=\"引子\"\u003e引子\u003c/h1\u003e\n\u003cp\u003e\u003cstrong\u003e给定一个数组，寻找出数组中前k小的元素。\u003c/strong\u003e\u003c/p\u003e\n\u003cp\u003e对于这个经典问题，我之前一直只知道可以用\u003cstrong\u003e堆\u003c/strong\u003e来解决。\u003c/p\u003e\n\u003cp\u003e构造一个大根堆，然后遍历所有数据，如果数据小于堆顶元素，就替换堆顶元素，调整堆。\u003c/p\u003e\n\u003cp\u003e这样最后堆中的元素就是原数组中前k小的元素。\u003c/p\u003e\n\u003cp\u003e这个算法的时间复杂度为O(nlogk)。\u003c/p\u003e","title":"数组前k小元素的各种方法对比"},{"content":"本文搜集了一些有趣的 C++的题目，有些题目令人大跌眼镜。来看看！\n请问输出结果是什么？如何解释？？ #include \u0026lt;iostream\u0026gt; #define TOTAL_ELEMENTS (sizeof(array) / sizeof(array[0])) int array[] = {23, 34, 12, 17, 204, 99, 16}; int main() { for (int d = -1; d \u0026lt;= (TOTAL_ELEMENTS - 2); d++) std::cout \u0026lt;\u0026lt; array[d + 1] \u0026lt;\u0026lt; std::endl; return 0; } 查看提示 测试发现实际输出结果为空。\nsizeof 运算符得到的是一个 size_t 类型，而 size_t 类型是无符号的。\n当用有符号数跟无符号数比较时，会将有符号数转成无符号数，也就是将-1 转成无符号数的最大值。\n另外，size_t 类型在不同位宽的机器上定义不同。如下：\n#ifdef _WIN64 __MINGW_EXTENSION typedef unsigned __int64 size_t; #else typedef unsigned int size_t; #endif /* _WIN64 */ 请问输出结果是什么？如何解释？？ #include \u0026lt;iostream\u0026gt; #define f(a,b) a##b #define g(a) #a #define h(a) g(a) int main() { std::cout \u0026lt;\u0026lt; h(f(1,2)) \u0026lt;\u0026lt; std::endl; std::cout \u0026lt;\u0026lt; g(f(1,2)) \u0026lt;\u0026lt; std::endl; return 0; } 查看提示 测试发现输出结果为\n12 f(1,2) 这里首先需要知道宏定义里面#的作用。\n#： 其后必须跟一个宏参数，它的作用是将其后的参数内容转换为字符串。 ##: 它的作用是拼接两个符号，如a##1，得到a1这两个符号不必是宏参数。 然后需要了解宏替换的规则。\n如果宏定义中没有#或者##，则先展开参数再进行替换(跟嵌套的函数调用一样) 否则，参数不展开而是直接替换 在本题中，h(a)的定义中没有#或者##，所以宏定义展开流程是：\n展开f(1,2) -\u0026gt; 得到12 -\u0026gt; 展开h(12) -\u0026gt; 得到g(12) -\u0026gt; 展开g(12)发现定义中有# -\u0026gt; 直接替换，得到\u0026quot;12\u0026quot;\n而g(a)的定义中有#或者##，因此会直接替换，在参数两边加上引号，不论参数是什么。于是直接得到字符串\u0026quot;f(1,2)\u0026quot;\n程序会输出None吗？ #include \u0026lt;iostream\u0026gt; int main() { int a = 3; switch (a) { case 1: std::cout \u0026lt;\u0026lt; \u0026#34;ONE\u0026#34;; break; case 2: std::cout \u0026lt;\u0026lt; \u0026#34;TWO\u0026#34;; break; defalut: std::cout \u0026lt;\u0026lt; \u0026#34;NONE\u0026#34;; } return 0; } 查看提示 测试发现输出结果为空。\n这个问题其实在于default拼写错误，写成了defalut 。\n但是真正可怕的是，不像是其它类型的拼写错误，本题的拼写错误，编译没有任何问题，没有人提示你defalut未定义。\n它们相等吗？ #include \u0026lt;stdio.h\u0026gt; int main() { double f = 0.0; int i; for (i = 0; i \u0026lt; 10; i++) f = f + 0.1; if (f == 1.0) printf(\u0026#34;f is 1.0\\n\u0026#34;); else printf(\u0026#34;f is NOT 1.0\\n\u0026#34;); return 0; } 查看提示 测试发现输出结果显示 0.1 累加 10 次并不等于 1.0。\n这是因为计算机中的浮点数，包括float和double类型，都是不精确的。因此累加的话会产生累积误差。\n计算机中浮点数比较，不要直接用==比较，应该计算两者的差，当差小于某个阈值时认为相等。\n比较经典的例子是 0.1 + 0.2 != 0.3\n逗号表达式可以正确赋值吗？ #include \u0026lt;iostream\u0026gt; int main() { int a = 1, 2; std::cout \u0026lt;\u0026lt; \u0026#34;a : \u0026#34; \u0026lt;\u0026lt; a; return 0; } 查看提示 编译错误。\n逗号表达式在 C++中的运算优先级是最低的，因此，赋值运算会优先执行，导致编译器认为2是一个表达式，编译错误。\n正确的写法是\nint a = (1, 2); 输出结果是什么？ #include \u0026lt;iostream\u0026gt; #define SIZE 10 void size(int arr[SIZE]) { std::cout \u0026lt;\u0026lt; \u0026#34;size of array is:\u0026#34; \u0026lt;\u0026lt; sizeof(arr); } int main() { int arr[SIZE]; size(arr); return 0; } 查看提示 输出结果首先不是 40。事实上，如果你自己编译了的话，就可以看到显示的警告。\nwarning: \u0026#39;sizeof\u0026#39; on array function parameter \u0026#39;arr\u0026#39; will return size of \u0026#39;int*\u0026#39; [-Wsizeof-array-argument] 也就是说，虽然形参中定义的是数组，但是在函数中使用sizeof，还是会当作指针来计算。\n在我的机器上，输出为 8，因为我的是 64 位机器。\n为什么会产生编译错误？ 记住，将一个普通指针传递给 const 类型的指针是允许的。\nvoid foo(const char **p) {} int main(int argc, char **argv) { foo(argv); return 0; } 查看提示 因为这是二级指针，必须先强制类型转换才可以赋值。一级指针则不需要强制类型转换。\nconst char ** p的意思是，类似这样的语句是无效的:**p = 'a'。\n我们看看假如允许char**到const char**的隐式类型转换的话，会发生什么。\nint main() { // 我们假设编译器存在漏洞X，允许char** 到 const char**的隐式类型转换。 char const c = \u0026#39;a\u0026#39;; // c是一个常量。 char* p_stupid = \u0026amp;c; // 愚蠢的指针，直接赋值是不行的，太过于耿直，编译错误。 char* p_smart = nullptr; // 这里有一个聪明的指针，学会了利用漏洞X来修改常量c。 char const** p_jump = \u0026amp;p_smart; // 利用漏洞X作为跳板。 *p_jump = \u0026amp;c; // 通过跳板，让p_smart强制指向c。 // 注意这一步是最有意思的一步，因为这一步的两边都是const char*类型的，赋值完全没问题。 *p_smart = \u0026#39;b\u0026#39;; // 因为p_smart本身并不是const char*，所以现在p_smart就可以修改c了，把系统搞崩溃。 } 注意看上述步骤，可以看到，除了char const** p_jump = \u0026amp;p_smart;这一步，其他步骤没有任何漏洞。\n因此我们可以看到，char**到const char**的隐式类型转换是可以引发严重后果的。\n输出是什么？ #include \u0026lt;stdio.h\u0026gt; #include \u0026lt;stdlib.h\u0026gt; #define SIZEOF(arr) (sizeof(arr)/sizeof(arr[0])) #define PrintInt(expr) printf(\u0026#34;%s:%d\\n\u0026#34;,#expr,(expr)) int main() { /* The powers of 10 */ int pot[] = { 0001, 0010, 0100, 1000 }; int i; for (i = 0; i \u0026lt; SIZEOF(pot); i++) PrintInt(pot[i]); return 0; } 查看提示 有了前面的关于宏定义的知识，你可能会得出，输出结果是\npot[i]:1 pot[i]:10 pot[i]:100 pot[i]:1000 但是，虽然躲过了宏定义的坑，其实还是错了，因为数组中的前三个数字带有前导 0，这意味着是 8 进制。。。\n所以正确的输出是\npot[i]:1 pot[i]:8 pot[i]:64 pot[i]:1000 输出是什么？肯定不是 10 #include \u0026lt;stdio.h\u0026gt; #include \u0026lt;stdlib.h\u0026gt; #define PrintInt(expr) printf(\u0026#34;%s : %d\\n\u0026#34;,#expr,(expr)) int main() { int y = 100; int *p; p = (int*)malloc(sizeof(int)); *p = 10; y = y/*p; /*dividing y by *p */; PrintInt(y); return 0; } 查看提示 这道题需要注意的是y = y/*p;。\n如果是手写代码，或者没有代码高亮，可能确实会以为是y除以*p。\n但是一旦有了代码高亮，瞬间就能看出，/*p其实是形成了块注释的开头。因此并不会执行除法。\n如何实现乘 5？ #include \u0026lt;stdio.h\u0026gt; #define PrintInt(expr) printf(\u0026#34;%s : %d\\n\u0026#34;,#expr,(expr)) int FiveTimes(int a) { int t; t = a \u0026lt;\u0026lt; 2 + a; return t; } int main() { int a = 1, b = 2, c = 3; PrintInt(FiveTimes(a)); PrintInt(FiveTimes(b)); PrintInt(FiveTimes(c)); return 0; } 查看提示 需要注意的是移位运算符的优先级低于加法，因此得到了错误的结果。\n这个程序哪里有问题？ #include \u0026lt;stdio.h\u0026gt; #include \u0026lt;stdlib.h\u0026gt; int main() { int *ptr1, ptr2; ptr1 = (int *) malloc(sizeof(int)); ptr2 = ptr1; *ptr2 = 10; return 0; } 查看提示 定义指针类型，必须每个变量前面都加*。\n修改：\nint *ptr1, *ptr2; 这个程序可以运行吗？ #include \u0026lt;stdio.h\u0026gt; int main() { int a = 3, b = 5; printf(\u0026amp;a[\u0026#34;Ya!Hello! how is this? %s\\n\u0026#34;], \u0026amp;b[\u0026#34;junk/super\u0026#34;]); printf(\u0026amp;a[\u0026#34;WHAT%c%c%c %c%c %c !\\n\u0026#34;], 1[\u0026#34;this\u0026#34;], 2[\u0026#34;beauty\u0026#34;], 0[\u0026#34;tool\u0026#34;], 0[\u0026#34;is\u0026#34;], 3[\u0026#34;sensitive\u0026#34;], 4[\u0026#34;CCCCCC\u0026#34;]); return 0; } 查看提示 是可以运行的。我们知道，数组下标引用a[3]，其实相当于是*(a+3)。因此，a[3]也可以写成3[a]。\n因此，a[\u0026quot;Ya!Hello! how is this? %s\\n\u0026quot;]相当于是\u0026quot;Ya!Hello! how is this? %s\\n\u0026quot;[3]。\noffsetof的原理是什么？ #include \u0026lt;stdio.h\u0026gt; #define offsetof(a, b) ((size_t)(\u0026amp;(((a*)(0))-\u0026gt;b))) struct test { int a; double b; float c; int d[20]; }; int main() { printf(\u0026#34;%d\\n\u0026#34;, offsetof(test, a)); printf(\u0026#34;%d\\n\u0026#34;, offsetof(test, b)); printf(\u0026#34;%d\\n\u0026#34;, offsetof(test, c)); printf(\u0026#34;%d\\n\u0026#34;, offsetof(test, d)); } 查看提示 工作原理：\n( (size_t)( // 4. \u0026amp;( ( // 3. (a*)(0) // 1. )-\u0026gt;b ) // 2. ) ) Casting the value zero to the struct pointer type a* Getting the struct field b of this (illegally placed) struct object Getting the address of this b field Casting the address to a size_t 注意，现在已经不用这个了，用的是 stddef.h 自带的offsetof宏。它直接调用了编译器提供的函数__builtin_offsetof。 直接include \u0026lt;iostream\u0026gt;就可以使用了。\nSWAP宏是如何工作的？ #include \u0026lt;iostream\u0026gt; #define SWAP(a, b) ((a) ^= (b) ^= (a) ^= (b)) int main() { int a = 1; int b = 2; SWAP(a, b); std::cout \u0026lt;\u0026lt; a \u0026lt;\u0026lt; b; } 查看提示 可以把(a) ^= (b) ^= (a) ^= (b)展开来看。这个表达式，相当于以下三条语句依次执行的结果。\na = a ^ b; b = b ^ a; a = a ^ b; 进行一些代换，得到：\nint _a = a; int _b = b; a = _a ^ _b; b = _b ^ (_a ^ _b); a = (_a ^ _b) ^ (_b ^ (_a ^ _b)); 根据结合律，去掉所有括号。再根据交换律，以及x^x恒等于 0，可以化简为：\nb = _a; a = _b; 因此该表达式可以实现交换两个变量的功能。\n","permalink":"https://idea.ladyrick.com/posts/c++-puzzle/","summary":"\u003cp\u003e本文搜集了一些有趣的 C++的题目，有些题目令人大跌眼镜。来看看！\u003c/p\u003e","title":"C++ Puzzle"},{"content":"之前的博客一直用的是wordpress，因为hexo的话，每次还得手动运行hexo generate，然后把public文件夹复制到腾讯云上。如果是使用github pages的话，也需要每次把public文件夹push上去。就感觉很繁琐，觉得还不如用wordpress，在线写文章。\n后来，越来越觉得在线写文章是个很蛋疼的事情。而且，wordpress使用了数据库来存储文章，就感觉很不优雅。于是开始考虑换回hexo。\n关于hexo部署繁琐的问题，最近接触了持续集成(CI, Continuous integration)，感觉可以用于自动部署，于是研究了一下，成功了。\n目前使用的持续集成工具是travis，使用github pages托管静态页面。现在的效果是，只要往github上一推送，就可以自动generate并将public文件夹push到gh-pages分支，实现github pages的自动更新。\n原理 持续集成的原理是，每次检测到github上的提交，就运行一些预设的命令。 这些命令定义在仓库根目录下的.travis.yml文件中。 因此，只要合理配置这些命令，就可以让travis帮你完成generate和deploy的工作。\n另一个需要解决的问题是，要让travis推送到你的github仓库，需要它有读写权限。 但是，我们总不能把github的密码写入配置文件中去吧？更不能把ssh密钥写入配置文件中。 好在，travis提供了加密功能，可以加密一个字符串，然后把加密的字符串写入配置文件中。这样就只有travis和你自己知道原始字符串的内容了。\n另外，要让travis拥有读写仓库的权限，其实并不需要给它账号密码，或者ssh密钥。只需要给他一个personal access token即可。而且，这样还可以精确地控制travis的权限，而不是给它所有的用户权限。\n方法 1. 在github中添加personal access token 打开github的设置页面，点击左侧的 Develpoer settings 进入开发者设置页面。 点击 Personal access tokens，接着点击 Generate new token。 Token description 一栏可以随便填，以供日后辨认即可。 在 Select scopes 中，勾选第一个选项，也就是 repo 选项。 最后点击 Generate token，可以看到生成了一个很长的字符串，比如：\n274e38cdf16e254da99a190ffbd0740b5ac3dcac 记下它，以备后续使用。\n2. 在仓库根目录创建travis配置文件 打开仓库文件夹，在根目录下新建.travis.yml文件，其内容如下：\nlanguage: node_js node_js: stable # 要安装的node版本为当前的稳定版。 cache: directories: - node_modules # 要缓存的文件夹 install: - npm install # install阶段执行的命令。 script: - npx hexo generate # script阶段执行的命令 after_script: # 最后执行的命令 - cd public - echo blog.ladyrick.com \u0026gt; CNAME # github pages服务，自定义域名 - git init - git config user.name \u0026#34;ladyrick\u0026#34; # 配置git参数 - git config user.email \u0026#34;ladyrick@qq.com\u0026#34; # 配置git参数 - git add . - git commit -m \u0026#34;travis\u0026#34; - git push -f \u0026#34;https://${MY_TOKEN}@github.com/ladyrick/hexo-blog.git\u0026#34; master:gh-pages # 强制push到gh-pages分支。注意这里使用了环境变量 ${MY_TOKEN} # 另外，注意修改仓库地址。 branches: only: - master # 触发持续集成的分支 这里需要注意我们使用了${MY_TOKEN}环境变量。这个环境变量定义了我们前面得到的personal access token。 但是，到目前为止，travis还无法识别这个环境变量。\n3. 下载安装travis工具，定义环境变量。 首先需要安装ruby环境。Ruby官网 安装Ruby后，安装travis：\ngem install travis 接着，切换到仓库目录，执行以下命令(也可以不需要切换到仓库目录，但是需要添加参数-r owner/project)：\ntravis encrypt MY_TOKEN=\u0026#34;274e38cdf16e254da99a190ffbd0740b5ac3dcac\u0026#34; --add 这样即可将前面得到的personal access token赋给环境变量MY_TOKEN，生成加密字符串，并自动添加到.travis.yml文件中。 更多关于travis加密的内容请看travis文档。\n4. 在travis网站上启动持续集成 最后一个步骤，很简单，打开https://travis-ci.org/profile，用github账号登陆，找到你的仓库，点击按钮即可启动。\n这样，以后每次push变更，都会触发travis的持续集成服务，自动完成构建，并推送到gh-pages分支。\n更新（2022-12） 现在我用的是 hugo 代替 hexo，速度更快。用 github actions 替换 travis，不再依赖第三方工具，更安全。\n可以参考本博客的 github 页面：https://github.com/ladyrick/idea。\n","permalink":"https://idea.ladyrick.com/posts/%E4%BD%BF%E7%94%A8travis%E8%87%AA%E5%8A%A8%E9%83%A8%E7%BD%B2hexo%E5%8D%9A%E5%AE%A2%E5%88%B0github-pages/","summary":"\u003cp\u003e之前的博客一直用的是wordpress，因为hexo的话，每次还得手动运行\u003ccode\u003ehexo generate\u003c/code\u003e，然后把public文件夹复制到腾讯云上。如果是使用github pages的话，也需要每次把public文件夹push上去。就感觉很繁琐，觉得还不如用wordpress，在线写文章。\u003c/p\u003e\n\u003cp\u003e后来，越来越觉得在线写文章是个很蛋疼的事情。而且，wordpress使用了数据库来存储文章，就感觉很不优雅。于是开始考虑换回hexo。\u003c/p\u003e","title":"使用travis自动部署hexo博客到github pages"},{"content":"本文介绍了在清华大学校内使用ipv6的方法，包括windows系统和ubuntu系统。\n准备 首先，在往下看以前，先试一下，你所在的宿舍楼，实验室，能不能使用原生的ipv6。 所谓原生的ipv6，指的是，插上网线就可以自动连接ipv6。 原生的ipv6是最好用的，不需要配置。但是清华校内只有部分地方支持，我不知道为什么。。。\n好了，假如说你所在的网络不支持原生ipv6，那么你需要建立isatap隧道来连接ipv6。\nWindows 首先，需要禁用系统自带的6to4服务、teredo服务、以及原生ipv6环境。\n禁用6to4: netsh interface 6to4 set state disable 禁用teredo: netsh interface teredo set state disable 禁用原生ipv6环境: 打开控制面板-\u0026gt;网络和Internet-\u0026gt;网络连接，找到你正在使用的网络连接，右键-\u0026gt;属性，在打开的对话框内，找到“Internet协议版本6(TCP/IPv6)”，取消勾选。 接下来设置isatap。\n输入下面的语句来配置：\nnetsh interface isatap set route isatap.tsinghua.edu.cn netsh interface isatap set state enable 注意事项： 学校的isatap服务器是isatap.tsinghua.edu.cn，ip地址是166.111.21.1。\nUbuntu #!/bin/bash sudo modprobe ipv6 sudo ip tunnel del sit1 MYIP=$(ifconfig enp2s0 | grep \u0026#34;inet \u0026#34;|awk \u0026#39;{print $2}\u0026#39;) echo My ip address: ${MYIP} sudo ip tunnel add sit1 mode sit remote 166.111.21.1 local ${MYIP} sudo ifconfig sit1 up sudo ifconfig sit1 add 2402:f000:1:1501:200:5efe:${MYIP}/64 sudo ip route add ::/0 via 2402:f000:1:1501::1 metric 1 注意事项： 第4行，是获取本机IP地址的命令。enp2s0是网卡名称。 第4行的命令目的是提取本机的IP地址。由于每个电脑的ifconfig命令运行结果不同，所以这个命令在其他电脑上很可能无法正确运行。到时候，可以直接手写IP地址，就像这样：\nMYIP=255.255.255.255 第6行，学校的isatap服务器是isatap.tsinghua.edu.cn，ip地址是166.111.21.1。\n测试 无论windows还是ubuntu，均可以使用\nping ipv6.google.com 来测试是否成功连接ipv6。\n","permalink":"https://idea.ladyrick.com/posts/%E6%B8%85%E5%8D%8E%E5%A4%A7%E5%AD%A6%E6%A0%A1%E5%86%85%E8%AE%BE%E7%BD%AEisatap%E9%9A%A7%E9%81%93%E4%BD%BF%E7%94%A8ipv6%E6%96%B9%E6%B3%95/","summary":"\u003cp\u003e本文介绍了在清华大学校内使用ipv6的方法，包括windows系统和ubuntu系统。\u003c/p\u003e","title":"清华大学校内设置isatap隧道使用ipv6方法"},{"content":"explicit关键字作用于类的构造函数。一旦类的构造函数声明了explicit关键字，构造函数就必须使用显示的方式调用。这样做可以防止构造函数被不知不觉地，莫名其妙地调用。\n下面举例子。\n在普通构造函数中的explicit： 类的定义如下（未声明explicit）：\nclass num { int n; public: num(int n) { this-\u0026gt;n = n; cout \u0026lt;\u0026lt; \u0026#34;constructor: \u0026#34; \u0026lt;\u0026lt; n \u0026lt;\u0026lt; endl; } ~num() { cout \u0026lt;\u0026lt; \u0026#34;destructor: \u0026#34; \u0026lt;\u0026lt; n \u0026lt;\u0026lt; endl; } }; 此时，由于未声明explicit，因此，可以使用这种方式来初始化：\nnum a = 1; num b = 2; num c(3); 而且，如果你写了多个构造函数，有的有explicit，有的没有，代码就会自动去找没有声明explicit的构造函数去调用，就像是声明了explicit的构造函数不存在一样。例如：\nclass num { int n; public: num(int n) { this-\u0026gt;n = n; cout \u0026lt;\u0026lt; \u0026#34;int constructor: \u0026#34; \u0026lt;\u0026lt; n \u0026lt;\u0026lt; endl; } explicit num(float n) { this-\u0026gt;n = int(n); cout \u0026lt;\u0026lt; \u0026#34;float constructor: \u0026#34; \u0026lt;\u0026lt; n \u0026lt;\u0026lt; endl; } num(double n) { this-\u0026gt;n = int(n); cout \u0026lt;\u0026lt; \u0026#34;double constructor: \u0026#34; \u0026lt;\u0026lt; n \u0026lt;\u0026lt; endl; } ~num() { cout \u0026lt;\u0026lt; \u0026#34;destructor: \u0026#34; \u0026lt;\u0026lt; n \u0026lt;\u0026lt; endl; } }; 调用：\nnum a = 1; num b = 2.0f; num c = 3.0; 此时，编译器会自动将2.0f转换成double，导致double constructor调用两次。但是，假如声明了explicit的不是float constructor，而是int或者double，就会直接导致编译不通过。因为，如果是int constructor声明了explicit，那么编译器无法决定int该转成float还是double。同理，如果是double constructor声明了explicit，那么编译器无法决定double该转成float还是int。这就很坑。凭什么float就可以义无反顾地转成了double，到了int和double就迷茫了？因此，我建议不要采用这种写法。\n在拷贝构造函数中的explicit： 类的定义如下：\nclass num { int n; public: num(int n) { this-\u0026gt;n = n; cout \u0026lt;\u0026lt; \u0026#34;constructor: \u0026#34; \u0026lt;\u0026lt; n \u0026lt;\u0026lt; endl; } ~num() { cout \u0026lt;\u0026lt; \u0026#34;destructor: \u0026#34; \u0026lt;\u0026lt; n \u0026lt;\u0026lt; endl; } num operator++() { n = n + 1; return *this; } }; 调用：\nnum n(3); ++n; ++n; 需要注意的是，在重载++运算符时，我返回了一个对象。在运行过程中，可以发现，我写的构造函数调用了一次，析构函数却调用了3次。这就是因为，返回的对象，其实是调用了拷贝构造函数，然后返回的一个匿名对象。因此，默认的拷贝构造函数被悄悄调用了两次。在实际中，可以通过返回引用的方法避免生成匿名对象。但是如何让编译器来帮我们检查，以避免这种事情发生呢？方法就是，将拷贝构造函数声明为explicit。如下。\nclass num { int n; public: num(int n) { this-\u0026gt;n = n; cout \u0026lt;\u0026lt; \u0026#34;constructor: \u0026#34; \u0026lt;\u0026lt; n \u0026lt;\u0026lt; endl; } explicit num(const num\u0026amp; nm) { this-\u0026gt;n = nm.n; cout \u0026lt;\u0026lt; \u0026#34;copy constructor: \u0026#34; \u0026lt;\u0026lt; n \u0026lt;\u0026lt; endl; } ~num() { cout \u0026lt;\u0026lt; \u0026#34;destructor: \u0026#34; \u0026lt;\u0026lt; n \u0026lt;\u0026lt; endl; } num operator++() { n = n + 1; return *this; } }; 像这样写，就会导致编译错误。因为++运算符返回对象需要隐式调用拷贝构造函数，而explicit不让它被隐式调用。由于返回对象的时候，必然会隐式调用拷贝构造函数，所以这么做其实是根本性杜绝了返回对象这种操作（返回引用多好，返回对象吃力不讨好）。如果真的有需求，需要实现返回新的对象，可以在函数中构造一个local的对象，然后返回其引用。像这样：\nnum\u0026amp; operator++() { n = n + 1; num temp(*this); return temp; } 当然，这样会产生一个警告，告诉你返回了局部变量。注意，不要在函数中new一个对象，返回其引用。像这样：\nnum\u0026amp; operator++() { n = n + 1; num* temp = new num(*this); return *temp; } 这种做法会导致new的那个对象没法delete，对象没法析构，造成内存泄漏。如果运行的话，可以发现，析构函数调用次数会比构造函数少。\n题外话：关于直接赋值初始化\n前面说到，可以直接用赋值来进行初始化。例如：\nnum a = 1; num b = 2.0f; num c = 3.0; 在测试的时候，我发现，当普通构造函数和拷贝构造函数都自己写，并且拷贝构造函数的参数没有const关键字时，这种赋值的方式是会报错的，跟是否声明explicit无关。如下：\n#include \u0026lt;iostream\u0026gt; using namespace std; class num { int n; public: num(int n) { this-\u0026gt;n = n; cout \u0026lt;\u0026lt; \u0026#34;constructor: \u0026#34; \u0026lt;\u0026lt; n \u0026lt;\u0026lt; endl; } num(num\u0026amp; nm) { this-\u0026gt;n = nm.n; cout \u0026lt;\u0026lt; \u0026#34;copy constructor: \u0026#34; \u0026lt;\u0026lt; n \u0026lt;\u0026lt; endl; } ~num() { cout \u0026lt;\u0026lt; \u0026#34;destructor: \u0026#34; \u0026lt;\u0026lt; n \u0026lt;\u0026lt; endl; } }; int main(int argc, char* argv[]) { num a = 1; return 0; } 报的错是：error: invalid initialization of non-const reference of type 'num\u0026amp;' from an rvalue of type 'num'。\nOK，你说我不const，还说我是右值是吧？Fine。我改。修改方案1：参数改成const\nnum(const num\u0026amp; nm) { this-\u0026gt;n = nm.n; cout \u0026lt;\u0026lt; \u0026#34;const copy constructor: \u0026#34; \u0026lt;\u0026lt; n \u0026lt;\u0026lt; endl; } 修改方案2：改成右值引用\nnum(num\u0026amp;\u0026amp; nm) { this-\u0026gt;n = nm.n; cout \u0026lt;\u0026lt; \u0026#34;right value copy constructor: \u0026#34; \u0026lt;\u0026lt; n \u0026lt;\u0026lt; endl; } 经过测试，这两种修改方案，都不会报错。更神奇的是，无论哪种修改方案，输出都显示只调用了普通的构造函数 (╯-_-)╯┴**—**┴ 拷贝构造函数内心：你根本不调用我，还管我管这么宽？经过轮子哥指点，终于明白了。**GCC就是会帮你检查一下num(num(1))是否合法，然后当num(1)来生成代码的。**真是无语呢……\n","permalink":"https://idea.ladyrick.com/posts/2017-11-09-c++-explicit/","summary":"\u003cp\u003eexplicit关键字作用于类的构造函数。一旦类的构造函数声明了explicit关键字，构造函数就必须使用显示的方式调用。这样做可以防止构造函数被不知不觉地，莫名其妙地调用。\u003c/p\u003e","title":"C++：explicit关键字"},{"content":"比如，桌面文件夹，其实真实的路径是\nC:\\Users\\用户名\\Desktop 而不是桌面。\n方法很简单。 假设要创建一个文件夹，名称叫做“代码”，路径是“code” 首先新建一个文件夹，名为code。 在文件夹下新建文件：desktop.ini 文件内容为：\n[.ShellClassInfo] LocalizedResourceName=代码 保存。 最后，在当前目录中打开cmd，执行命令\nattrib +s . 为当前文件夹添加系统属性。 注意： 文件夹的真实路径必须是不带空格的英文。\n","permalink":"https://idea.ladyrick.com/posts/windows%E5%A6%82%E4%BD%95%E8%87%AA%E5%AE%9A%E6%96%87%E4%BB%B6%E5%A4%B9%E5%90%8D%E7%A7%B0/","summary":"\u003cp\u003e比如，桌面文件夹，其实真实的路径是\u003c/p\u003e\n\u003cpre tabindex=\"0\"\u003e\u003ccode\u003eC:\\Users\\用户名\\Desktop\n\u003c/code\u003e\u003c/pre\u003e\u003cp\u003e而不是桌面。\u003c/p\u003e","title":"Windows如何自定文件夹名称"},{"content":"罗姆楼的网络问题真的是困扰了我很久。 从进入实验室开始，就不停断网……\n一开始怀疑是网卡的问题，还自费买了个网卡，结果证明不是网卡的问题。 最神奇的是，使用 win7 好好的，一用 win10，不到三分钟铁定断网。 这导致整个实验室都是用 win7 的……真心醉了。\n后来找到了下面的方法，再加上自己的摸索，终于解决了这个问题。 不过，我猜，在我痛苦期间罗姆楼的网应该也整顿过…… 再加上 win10 不断更新完善， 反正现在已经完全不会断网了。\n方法 1：网关 MAC 静态绑定 适用于 windows7+ PS：请使用管理员身份打开命令提示符。\n步骤 1: 用netsh i i show in命令查出你的网卡物理接口 ID。\n一个示例如下：\nnetsh i i show in 该命令的输出可能是（每个计算机不一定一样）：\nIdx Met MTU 状态 名称 --- ---------- ---------- ------------ --------------------------- 1 75 4294967295 connected Loopback Pseudo-Interface 1 5 5 1500 disconnected 以太网 8 35 1500 connected 以太网 2 从输出可以看出，示例中，正在使用的以太网的 ID 是 8 （我有两个网口，ID 为 5 的没有使用，显示状态为 disconnected）\n步骤 2: 通过 netsh 命令实现网关 mac 的静态绑定\nnetsh -c \u0026#34;i i\u0026#34; add neighbors 8 \u0026#34;166.111.64.1\u0026#34; \u0026#34;00-22-93-59-8e-b1\u0026#34; 其中 8 是上面查到的网卡 ID，不同的电脑可能不一样。 00-22-93-59-8e-b1 是网关的真实 MAC 地址，不要修改。\n方法 2：重装驱动 如果以上方法还不行的话，建议去仔细地重装驱动，去笔记本电脑官网下载，去主板厂商官网下载。 不要用各种自动装驱动的软件去装。\n博主使用上述的绑定+装驱动的方法解决了断网问题。\n相关： 清华大学校内设置 isatap 隧道使用 ipv6 方法\n","permalink":"https://idea.ladyrick.com/posts/%E7%BD%97%E5%A7%86%E6%A5%BC%E7%BD%91%E7%BB%9C%E9%97%AE%E9%A2%98%E8%A7%A3%E5%86%B3%E6%96%B9%E6%A1%88/","summary":"\u003cp\u003e罗姆楼的网络问题真的是困扰了我很久。\n从进入实验室开始，就不停断网……\u003c/p\u003e\n\u003cp\u003e一开始怀疑是网卡的问题，还自费买了个网卡，结果证明不是网卡的问题。\n最神奇的是，使用 win7 好好的，一用 win10，不到三分钟铁定断网。\n这导致整个实验室都是用 win7 的……真心醉了。\u003c/p\u003e","title":"罗姆楼网络问题解决方案"}]