这几天 Bad Apple 又逆袭了,比如这个和这个。虽然周末时候自己也做了一个,但介于身心憔悴和一个遗留问题,暂时先不和上次一样出视频了……
首先,看到那两个演示视频就可以联想到《编程之美》里面的一道面试题,但显然完全不是一回事,因为图像的 x → y
中 y
并不唯一。既然如此,那么就可以推定这是某种程度的「造假」,因为图像已经不是 CPU 占用率曲线了。
既然造假,我们就要造得有良心!那种类似直接在任务管理器上新建/覆盖一个视频窗口的做法略无节操了一些。作为一个死程,我们还是慢慢用程序解决吧……
视频预处理
这个就不用写程序了,看过我之前视频的童鞋一定知道我要用 ffmpeg 来把视频转成位图序列,没错,这次还是它!
而 ffmpeg 同时还提供了非常好用的滤镜支持,要把视频变成 CPU 占用率曲线的样子,我们需要边缘检测(edgedetect)和颜色通道混合器(colorchannelmixer)。
ffmpeg -i <文件名> -r <FPS> -vcodec bmp -vf edgedetect,colorchannelmixer=0:0:0:0:1:1:1:0:0:0:0:0 <输出文件名>
这样就能直接输出黑底绿线的位图序列,以备使用了。
屏蔽掉 CPU 占用率曲线的绘制
这里只考虑 Win 7 及之前的任务管理器,因为这货很好改。首先,这曲线一看就是 LineTo()
函数画出来的,那么我们用 OllyDbg 直接查看任务管理器中所有对于 LineTo()
的调用就可以找到对应代码。
我是 Win 7 的系统,用 OllyDbg 直接打开后,对 LineTo()
的调用只有:
- 绘制性能标签页中的背景网格
- 绘制 CPU 占用率曲线
- 绘制内存占用曲线
- 绘制联网标签页中的背景网格
- 绘制网络活动曲线
这几处,熟悉汇编的都可以很快定位整个语句的范围。至此,我们得到了一个地址及一个长度,把这个范围内的指令全部变成 Nop 指令即可。
当然 OllyDbg 中反汇编的地址是不能直接用的,还需要减去当前模块的基地址,得到偏移量以备使用……
那么来到 C++,FindWindow
→ GetWindowThreadProcessId
→ OpenProcess
即可获得任务管理器的进程句柄。
得到进程句柄后,我们首先要获得 taskmgr.exe 模块当前的基地址:EnumProcessModules
→ GetModuleInformation
。而后,就可以用上之前得到的偏移量和长度,使用 WriteProcessMemory
把绘制 CPU 曲线的代码覆写为一串 Nop 指令。
世界清静了。
绘制动画
这里就没有什么技术含量了。无非就是自己 SetTimer
开一个定时器,按照一定的帧率往窗口上 TransparentBlt
以前处理好的图片。
需要注意的是,背景网格,这货最麻烦了。我目前没有用 Hook,所以暂时的做法是:自己维护一份干净的背景网格。这样做的缺点是,网格更新时有一定几率察觉到曲线的缺失(用 Hook 后应该会好:Hook 后替换掉 Window Proc,然后正确的 WM_DRAWITEM
/WM_PAINT
之后立即把当前的图片绘制上去)。
以上。
今天在看 C++11 的 Inherited Constructors 特性时发现了一个以前不知道的传统 C++ 奇技淫巧。
class Base {
public:
void foo(float a);
};
class Derived: public Base {
public:
void foo(int a);
};
这样的代码,显然 Derived::foo()
会把 Base::foo()
覆盖。
今天得知,using
居然还可以把父类的成员「提升」到子类中:
class Derived: public Base {
public:
using Base::foo; // 看这里看这里看这里
void foo(int a);
};
如此,就相当于在 Derived
中添加了一个和 Base::foo()
一模一样的成员。(当然,从字面上也是很顾名思义的嘛。)
Derived d;
d.foo(3.14f); // 这样调用的就是 Derived::foo(float a) 了
之前我曾想,两个完全符合「is-a」关系的类,做成聚合显然是不甘心的。但如果父类接口有 N 个,子类只是想添加 1 个接口,然后将父类中的 M 个(M < N)接口暴露出来该怎么办?想想 private
继承,然后自己在子类中写 M 个 wrapper 似乎是个可行的方法,但如果 M 很大似乎依旧不甘心。现在知道了这个技巧似乎好解决了很多呢。(不过,整个类的声明也会随之变得脑残起来。)
顺带一提,传统 C++ 中的这个方法是无法提升构造函数的,而 C++11 中则加入了允许继承(提升)构造函数的特性(当然,目前貌似还没有编译器支持 = =)。
以上。
前文再续,书接上回。上回咱们讲到,如何在 Vim 中使用 OmniCppComplete 为 C/C++ 自动补全;今天偶然间发现了个新玩意儿:clang complete。
前情提要
OmniComplete 是 Vim 中的智能补全功能,而 OmniComplete 本身并不知道如何补全,具体的「通过光标前的内容猜测光标后可能出现的内容」的工作是由不同的外部插件实现的。
上回说到的 OmniCppComplete 就是这样一个插件。实际需要预先调用 ctags 对源代码进行词法分析(吧?)生成 tags 文件(token 列表),然后在这个 tags 文件中去进行匹配。所以局限也很快暴露出来:无论是库的头文件还是自己的源代码,都要用户自己事先对它运行一遍 ctags。
clang complete 则是借助 clang 来分析源代码,毕竟没有比编译器更了解代码的东西了。
如何安装
需要注意两个前提:
- clang complete 和 OmniCppComplete 冲突
- 如果想成功起效,请确保你有安装 clang……(废话 = =)
那么,你可以选择直接去 Vim 官方插件列表页获取,或者去作者的 GitHub clone
,抑或使用 Vundle 安装即可。
如果获取的是 vmb 文件,那么命令行执行
vim clang_complete.vmb -c 'so %' -c 'q'
Windows 下则直接用 Vim 打开,然后 :source %
……(呃,这好像也是废话 = =)
p.s. 强烈推荐试一试用 Vundle 来管理 Vim 的插件。
clang complete 是即拆即食的(OmniCppComplete 在安装完成后还需要到处生成 tag 文件),编辑状态输入 .
和 ->
和 ::
后都会自动进行补全,一般状态下 Ctrl-X Ctrl-O 或者 Ctrl-X Ctrl-U 都能手动调用补全。
配置文件
如果在补全时发现提示「Pattern not found」提示,可以试试 :copen
查看错误信息。
错误信息很可能是语法错误,或者头文件未找到。
没错,因为 clang 是编译器,每次补全实际上都对整个源代码进行分析。要正确编译,可能需要额外传一些 -D 宏定义
和 -I 头文件路径
给 clang。为了解决这一问题,clang complete 默认会在加载文件时读取该目录下名为 .clang_complete
的文件,该文件中就可以添加这样的信息,例如:
-DNDEBUG
-I../include
`pkg-config gtk+-2.0 --cflags`
当然,链接是不会执行的,所以不必把链接选项写进去。
其它
如果你觉得补全得慢,可以尝试让它使用 libclang 而不是 clang:
let g:clang_use_library=1
let g:clang_library_path="path to libclang"
这个功能需要你的 Vim 有 Python 支持……
在派生类构造函数中捕获基类构造函数异常
class Base {
public:
Base()
{
throw std::runtime_error("Error from Base");
}
};
class Derived : public Base {
public:
Derived()
{
try {
} catch(std::exception const& e) {
}
}
};
类似于这样的代码,是捕获不到异常的,因为执行到构造函数的函数体中时,基类已经构造完毕了。
索性 C艹 有一个神奇的写法:
Derived::Derived()
try : Base() {
} catch(...) {
}
没错,我没有漏写大括号……你完全可以直接把一个 try {} catch {}
当作函数体……同理,对于一般函数这么写也行:
void foo()
try {
} catch(...) {
}
当然,构造函数的 catch
里是无论如何都会帮你确保有一个异常被抛出去的。(你不显式抛,它就帮你把被捕获的异常抛出去。)
后来发现这个东西学名叫做:Function-try block。
C++11 的异常处理
查了一番,似乎大的改变在于:
- 废弃原先的 Dynamic Exception Specification
- 添加了
noexcept
代替之
先看看之前的 Dynamic Exception Specification 是什么:
void foo(); // (1) 允许抛出异常
void foo() throw(X, Y); // (2) 只允许抛出 X 和 Y 异常
void foo() throw(); // (3) 不允许抛出异常
坑爹之处在于第 2 点:Dynamic Exception Specification 是在运行时进行的,于是,为了确保只能抛出指定类型的异常,就会生成额外代码降低执行效率。
最扭曲的是可自定义的 unexcepted 处理函数。所谓 unexcepted 处理函数,就是一旦你抛出了 throw(...)
中没有出现的类型,系统会去调用的那个函数。默认的 unexcepted 函数直接调用 std::terminate()
结束程序;如果你觉得这不好,你可能会想要去替换掉这个全局的处理函数,你会发现你可以:
- 和默认行为一样,调用
std::terminate()
自尽。
- 抛出异常,但是:
- 这个异常存在于
throw(...)
列表中,那么正常抛出。
- 这个异常不存在于列表中,但是
std::bad_exception
在,那么抛 std::bad_exception
。
- 这个异常不存在于列表中,而且
std::bad_exception
也不在,那么系统 std::terminate()
之。
重点在于,这是一个「全局」的处理函数。如果有人自定义这个,相当于如果要写 throw
列表,就要把 std::bad_exception
加上。
于是 C++11 因为这个在实践中基本没人用的东西而废弃了这个名字很长的功能,用 noexcept
取而代之:
void foo(); // (1) 允许抛出异常,相当于 noexcept(false)
void foo() noexcept(constant-expression); // (2) expression 成立时不允许抛出异常
void foo() noexcept; // (3) 不允许抛出异常,相当于 noexcept(true)
也就是将原先的 (2) 去掉,然后扩展一下原先的 (3)。一旦不允许抛异常的函数抛出了异常,那么直接 std::terminate()
结束运行。
对应还有一个同名的操作符 noexcept
,用来在编译时判断表达式是否允许抛出异常(遇到函数时只通过其声明时的 noexcept
与否判断)。
noexcept(1 + 2) // false
noexcept(throw 1) // true
noexcept(foo()) // 返回 foo() 声明时的 noexcept 与否
最好在哪里捕获异常
处处都 catch
的话,就反而比 if
麻烦了。据推荐,最好在这些地方 catch
:
- 需要更换错误报告手段(例如:实现 C 接口、进入了
noexcept
函数、多线程、系统回调……)
- 允许这种异常的发生(例如:如果
GetEntryDataFromZip()
会在 zip 文件数据损坏时抛出异常,那么 DisplayImageFileInZip()
就不应该捕获;而 CopyAllEntriesToAnotherZip()
则可以选择捕获这个异常,生成一个空的 zip 文件。)
- 有替代方案
参考内容:Jon Kalb: Exception-Safe Coding in C++
我们平时所说的「文本」基本上都是在说「电脑屏幕上的字符」,但是小学生都知道「计算机只懂 0101」,那么电脑究竟是怎么处理三千世界中如此纷繁复杂的文字的呢?
开天辟地阿斯克
电脑是美国人发明的,所以最初的人们也只需要考虑「如何让电脑明白英文的字母和符号」的问题。
于是 ASCII(美国信息交换标准代码)诞生了。它参考了电报报文的设计,将 0 到 127 都赋予了对应的字母和符号。于是可以被电脑处理的数字序列 72 101 108 108 111 32 87 111 114 108 100 33
就可以通过查 ASCII 码表翻译成 Hello World!
。当然,用更「计算机」一些的十六进制表示每一个数,就是:
48 65 6c 6c 6f 20 57 6f 72 6c 64 21
H e l l o W o r l d !
那么,输入并显示一个字符的过程就变成了:
- 你按下键盘的 H 键
- 电脑在内存中保存 0x48
- 电脑在屏幕上显示 0x48 号图形
也就是说,电脑不需要能够「明白」字符,只需要能够用数字「表示」字符。
百家争鸣,被榨干的单字节
随着时代发展,电脑开始在各地使用。以法语、希腊语等为母语的人发现 ASCII 真心坑爹,没有 è、é 没有 α、β、θ、让我们怎么活?
好消息是:计算机终于迎来了以 8 位为 1 字节的时代,而 ASCII 只为 0x00 到 0x7F 规定了对应字符。也就是说,ASCII 只占了低 7 位 0XXX XXXX
,还有 0x80 到 0xFF 这余下的 128 个码位可以让人糟蹋。
这一利好消息的发现让人们大为振奋。
法国人开心地用 0xE8 表示 è,用 0xE9 表示 é;希腊人欢乐地用 0xE1 表示 α,用 0xE2 表示 β,用 0xE8 表示 θ……
E 9 7 1 7 5 6 9 7 6 6 1 7 5
1110 1001 0111 0001 0111 0101 0110 1001 0111 0110 0110 0001 0111 0101
é q u i v a u
所有 0 开头的字节在 ASCII 里找对应字符,所有 1 开头的字节在各自定义的字符集里找对应字符,问题解决了。
传说的巨龙,汉字的秘密
于是电脑来到了中国,但是中文不同于字母文字,如何给数万汉字编码就成了大问题。
一位伟人一拍脑袋:「一个字节只有 0 到 255,但两个字节就有 0 到 65535 啦!《新华字典》也就一万多个字,用两个字节表示一个汉字不就行了嘛~讨厌!」
没错,这就是一种后世所谓的多字节字符集(MBCS)。我们熟悉的 GB2312 和 GBK 使用的都是如下形式的二进制编码:
0XXX XXXX
1XXX XXXX XXXX XXXX
如果一个字节为最高位为 0,那么后续的 7 位表示一个字符(128 个码位)。如果最高位为 1,那么后续 15 位表示一个字符(32768 个码位)。
GB2312 前辈利用了这 32768 个码位中的 7445 个,而后辈 GBK 则利用了 21886 个。
例如二进制数据 48 69 20 CA C0 BD E7
的解析:
1| 4 8 6 9 2 0 C A C 0 B D E 7
2| 0100 1000 0110 1001 0010 0000 1100 1010 1100 0000 1011 1101 1110 0111
3| /100 1000 /110 1001 /010 0000 /100 1010 1100 0000 /011 1101 1110 0111
4| /4 8 /6 9 2 0 /4 A C 0 /3 D E 7
5| H i 世 界
- 字节序列(十六进制)
- 字节序列(二进制)
- 字节序列 --[GBK 解码]--> 码位(二进制)
- 字节序列 --[GBK 解码]--> 码位(十六进制)
- 码位 --[GBK 字符集]--> 字符
那么,几个之前没有讲的概念就比较明白了。
- 字符集:某种字符编码所能表示的所有字符集合。如:H、世
- 码位:为字符集中的每一个字符分配的唯一编号。如:0x48(H)、0x4AC0(世)
- 编码:将码位转换为字节序列。如:0x48→0x48、0x4AC0→0xCAC0
- 解码:将字节序列转换为码位。如:0x48→0x48、0xCACO→0x4AC0
危机!乱码的陷阱
于是,全世界人民都一本满足了,但是这番和谐景象的背后却隐藏着天大的危机。之前,大家都用着自己的字符编码相安无事,但全球化却导致乱码横行。
聪明的你也许已经发现了:之前我们说过,法国人的 è 用 0xE8 表示,而希腊人的 θ 也用 0xE8 表示。有一天,法国人写了封 Email 给希腊人:
Jeux de caractères codés
希腊人收到一看:
Jeux de caractθres codιs
这是毛啊?于是转发给了中国人,中国人打开一看:
Jeux de caract鑢es cod閟
擦,顿时感觉自己没文化了……于是回复:
我看不懂……
希腊人无辜地打开一看:
Ξ?Ώ΄²»Ά?‘‘
这这这……于是转发给法国人,法国人也一头雾水:
ÎÒ¿´²»¶®¡¡
「我还是删了吧,妈妈说不要跟外国人发邮件……」
1| J e u x d e c a r a c t è r e s c o d é s
2| 4a 65 75 78 20 64 65 20 63 61 72 61 63 74 e8 72 65 73 20 63 6f 64 e9 73
3| J e u x d e c a r a c t θ r e s c o d ι s
4| 4a/65/75/78/20/64/65/20/63/61/72/61/63/74/68 72/65/73/20/63/6f/64/68 73
5| J e u x d e c a r a c t 鑢 e s c o d 閟
- 法国人写的文字
- 法国人根据 latin-1 将文字转换为码位、并将码位编码得到实际保存的字节序列
- 希腊人根据 latin/greek 将序列解码得到码位、并将码位转换为字符,得到的文字
- 中国人根据 GBK 解码后得到的码位
- 中国人根据 GBK 字符集将码位转换到的字符
所以,二进制文本数据就相当于密文,而编码和解码如同加密和解密,只有用正确的密钥才能得到明文,也只有用正确的字符编码才能得到码位。然后通过码位在字符集里取得最终的字符。
(历史上,常常将字符集与字符编码等同起来。因为大部分字符集都是 8 位的,编码/解码形同虚设,N 编码后还是 N,可以直接 1:1 映射。例如上面例子中的 latin-1 和 lantin/greek,编码都是相同的 1:1 编码,只是字符集中相同码位对应着不同字符而已。)
大逆转,万国码的光荣
打开浏览器菜单,肯定存在一个叫做「编码」的选项,点开就能看到这世界上至少存在着多少种流行的字符编码。如果浏览器「自动检测」检测得不对,网页就乱码了。索性懒人是社会进步的阶梯。为了免去百家争鸣带来的麻烦,试图让全世界「书同文」的 Unicode 诞生了。
Unicode 标准化了一个字符集,包含了世界上所有的字符,每一个字符都拥有唯一的码位 U+XXXX。(起初的码位是 16 位的,可以容纳 65536 个字符。其后不断扩展,现今已经扩展到了 U+10FFFF。)
Unicode 还提供了几套编码方案,来将 U+XXXX 的码位编码为字节序列,例如:UTF-8 和 UTF-16。
UTF-8
UTF-8 顾名思义,是一套以 8 位为一个编码单位的可变长编码。会将一个码位编码为 1 到 4 个字节。
U+ 0000 ~ U+ 007F: 0XXXXXXX
U+ 0080 ~ U+ 07FF: 110XXXXX 10XXXXXX
U+ 0800 ~ U+ FFFF: 1110XXXX 10XXXXXX 10XXXXXX
U+10000 ~ U+1FFFF: 11110XXX 10XXXXXX 10XXXXXX 10XXXXXX
例如「萌」在 Unicode 中的码位为 U+840C,对应的 UTF-8 编码为 E8 90 8C
:
1| 8 4 0 C
2| 1000 0100 0000 1100
3| 1000 010000 001100
4| 11101000 10010000 10001100
5| E 8 9 0 8 C
- 码位(十六进制)
- 码位(二进制)
- 根据 UTF-8 编码规则将码位分为三段
- 为每一段加上前缀,编码完成(二进制)
- 编码完成(十六进制)
UTF-16
UTF-16 则是一套以 16 位为一个编码单位的可变长编码。会将一个码位编码为 1 到 2 个双字节。编码算法大同小异并不重要,但有一个问题却亟待解决:字节序。
不同的系统,字节序可能不同。小端序系统的 0x1234 实际上是 34 12
,把这个字节序列发给大端序系统,34 12
就会被理解成 0x3412。
于是,Unicode 中引入了一个特殊字符:字节序标(BOM),码位 U+FEFF。用于加在被编码的数据之前,表示编码时的字节序。于是,解码时,首先读出第一个双字节:
- 0xFEFF 当前的字节序和编码时的字节序相同
- 0xFFFE 当前的字节序和编码时的字节序相反
当然,UTF-16 还有派生的 UTF-16LE 和 UTF-16BE,实际上就是按字节序特化的版本。
- «
- 1
- ...
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- ...
- 15
- »