编写不可维护代码中的大部分技术是伪装的艺术,隐藏代码或者使代码看起来和实际的不一样。
许多技术都倚赖于一个事实:编译器比人眼或者一个文本编辑器更能分辨出细小的差别。
这里有一些最好的伪装技术:
-
编写“高效”的代码
“We should forget about small efficiencies, say about 97% of the time;
premature optimization is the root of all evil.”
“我们应该忘记小的效率,比如说 97% 的时间; 不成熟的优化是所有罪恶的根源.”
~ Donald Knuth(高德纳)
“More computing sins are committed in the name of efficiency (without
necessarily achieving it) than for any other single reason — including
blind stupidity.”
“更多的计算过失都是在效率(没有必要实现)的名义下产生的,
而不是其他任何一个原因 — 包括不加思考的蠢事.”
~ W. A. Wulf
把你那些让人混淆的成果掩盖起来最安全的办法,是看起来你想让程序运行得更快。
-
假装成注释的代码或者相反
: 包含一些被注释掉的代码,但是第一眼看上去却不是这样。
for ( j=0; j<array_len; j+ =8 )
{
total += array[j+0];
total += array[j+1];
total += array[j+2];
total += array[j+6];
total += array[j+7];
}
如果没有代码高亮显示,你能注意到那三行代码被注释掉了吗?
-
命名空间
: 结构/联合以及类型定义的结构/联合在 C 中是不同的命名空间(在 C++ 中则不同)。
在两种命名空间中对结构或者联合使用相同的名字。如果可能的话,让它们尽量兼容。
typedef struct {
char* pTr;
size_t lEn;
} snafu;
struct snafu {
unsigned cNt
char* pTr;
size_t lEn;
} A;
-
隐藏宏定义
: 把宏定义隐藏在垃圾注释中。程序员就会厌烦而不会全部读完注释,所以永远不会发现定义的宏。
保证宏用一些奇怪的操作来代替非常合法的赋值语句,一个简单的例子:
#define a=b a=0-b
-
看起来忙碌
: 使用 define 语句构造一些函数,只是很简单地注释掉参数。例如:
#define fastcopy(x,y,z)
…
fastcopy(array1, array2, size); /* does nothing */
-
利用延续来隐藏变量
: 不使用
#define local_var xy_z
而把 "xy_z" 分成两行:
#define local_var xy\
_z // local_var OK
这样,对于变量 xy_z 进行全文搜索结果什么也找不到。对于 C 预处理器来说,行尾的 "\"
表示把这一行和下一行连起来。
-
伪装成关键字的随意名字
: 写文档时,如果需要一个名字来表示一个文件名的话,那么就用 "file"。
绝不要使用类似 "Charlie.dat" 或者 "Frodo.txt" 之类很明显的名字。
一般情况下,在你的例子中,要使随意的名字看起来尽可能地象保留关键字。例如,参数或者变量的好名字可能是
"bank"、"blank"、"class"、"const "、
"constant"、"input"、"key"、"keyword"、
"kind"、"output"、"parameter" "parm"、
"system"、"type"、"value"、"var"
和 "variable"。如果你使用了真正的关键字,可能会被命令解释器或者编译器拒绝,
就更好了。如果你做得好的话,用户把你例子中的随意名字和保留字完全地混淆。
而你可以表现得很无辜,声明那只是要帮助他们把变量和其用途联系起来。
-
代码中的名字和显示出来的名字务必不匹配
: 选用那些和屏幕上要显示出来的名字完全没有关系的变量名。例如,在屏幕上把一个域叫做 "Postal Code",
而在代码中相应的变量是 "zip"。
-
不要改变名字
: 不要使用全局改名的方法来使两段代码同步,而是对同一个符号进行多次 TYPEDEF。
-
怎样隐藏禁用的全局变量
: 既然全局变量都是"魔鬼",那么就定义一个结构来保存所有你想全局使用的东西。
给它取一个聪明的名字,比如 EverythingYoullEverNeed。
让所有的函数都带一个指向这个结构的指针(把它叫做句柄来使事情更混淆)。
这就给出了这么一个印象: 你没有使用全局变量,而是通过一个"句柄"来访问一切的。
然后静态地声明一个,这样,所有的代码使用的就是同一个拷贝。
-
使用同义词来隐藏实例
: 维护程序员要看他们所做的修改是否能引起一连串的后果,会对变量名做全局搜索。
可以使用同义词这样一个简单手段来轻易解决,例如:
#define xxx global_var // in file std.h
#define xy_z xxx // in file ..\other\substd.h
#define local_var xy_z // in file ..\codestd\inst.h
这些定义可以分散在不同的包含文件中。如果这些头文件在不同的目录中,那就非常有效了。
另一个技巧就是在不同的作用范围内重用名字。编译器可以把它们区分开,
而一个头脑简单的文本搜索器却不行。不幸的是接下来十年的 SCID
技术可以让这个简单技巧不可能实现。因为编辑器可以和编译器一样理解作用范围规则。
-
类似的长变量名
: 使用非常长的变量或者类名,只是在一个字母或者大小写上有区别。
一对理想的变量名是 swimmer 和 swimner。
利用不同的字体不能明显区分 ilI1|
或者 oO08 这个好处,
可以使用类似 parselntparseInt
或者 D0Calc 和 DOCalc 这样的一对对标识符。
l 对于变量名来说是一个非常好的选择,
因为它在漫不经心的一瞥中看起来象常数 1。在许多字体中,rn 看起来象一个 m,
这样变量 swirnrner 看起来会怎么样? 创造一些只是在大小写上有分别的变量名,
比如 HashTable 和 Hashtable。
-
发音类似和形态类似的变量名
: 尽管我们已经有了一个叫做 xy_z 的变量,当然没有理由不使用其他类似的变量名,
象 xy_Z、xy__z、_xy_z、_xyz、XY_Z、xY_z 和 Xy_z。
有些程序员通过发音或者字母拼写而不是通过表现形式来记忆名字,
只在大小写和下划线上有所不同的相似变量名,就很容易让他们混淆。
-
重载和迷惑
: 在 C++ 中,使用 #define 来重载库函数。这样看起来好象你在用一个很熟悉的库函数,
而实际上用的东西完全不同。
-
选择最好的重载运算符
: 在 C++ 中,可以重载 +、-、*、/ 等,来做一些和加法,减法等完全无关的其他事情。
要知道,如果 Stroustroup 可以使用移位运算符来做 I/O,为什么你不能同样创新一点呢?
如果你重载了 +,那么保证 i = i + 5 的含义完全不同于
i += 5。这里有个例子,把运算符重载的混淆技术提升到了艺术的高度。
对类重载 '!' 运算符,但这个重载却和取反取非等没有任何联系,而返回一个整数值。
接着,为了让它获得一个合乎逻辑的值,你必须使用 '!!'。然而,这就颠倒了逻辑,
所以〔鼓声起〕你必须使用 '! ! !'。不要和 ! 操作符混淆起来,它使用逻辑位反操作符 ~ 来返回一个 0 或者 1 的布尔值。
-
重载 new
: 重载 "new" 操作符 - 比重载 +-*/ 更危险。
如果把它重载成做一些和原来的功能(但对于一个对象来说是必不可少的,所以很难改变)不一样的事,
就可能把事情搞得一团糟。这应该保证用户可以完全没有阻碍地创建一个动态实例。
可以和大小写敏感的技巧结合起来: 同样包含一个成员函数,以及一个叫做 "New" 的变量。
-
#define
:C++ 中的 #define 本身就值得用一整篇文章来探索它丰富的迷惑可能性。使用小写的 #define 变量,
这样它们可以伪装成一般的变量。在你的预处理函数中绝不要使用参数。对全局的 #define 做所有事情。
我听说过的最有想象力的预处理使用方法之一,是在代码需要通过 CPP 处理 5 遍,才可以真正编译。
通过 define 和 ifdef 的精巧使用,一个迷惑的能手可以让头文件根据它们被包含的次数来定义不同的东西。
当一个头文件包含在另一个头文件中时,就变得非常有意思。这里是一个特别迂回的例子:
#ifndef DONE
#ifdef TWICE
// put stuff here to declare 3rd time around
void g(char* str);
#define DONE
#else // TWICE
#ifdef ONCE
// put stuff here to declare 2nd time around
void g(void* str);
#define TWICE
#else // ONCE
// put stuff here to declare 1st time around
void g(std::string str);
#define ONCE
#endif // ONCE
#endif // TWICE
#endif // DONE
当给 g() 传一个 char* 的时候就变得很有趣,因为根据这个头文件被包含的次数,会调用不同版本的 g()。
-
编译器指示符
: 编译器指示符的设计本身就带有这样一个快速的目的: 让相同的代码运行地完全不一样。
再三把布尔短路指令以及长字符串指令强硬地打开关闭。
-
障眼法
: 给代码中夹杂着一些从来不会用到的变量和从来不会调用的函数。
不删除那些不再使用的代码就可以很容易地做到。你可以让这些代码待命,
说不定哪天它就需要复活了。如果这些无所事事的变量和函数的名字和那些真正有用的名字相同,
就会得到一些额外的奖赏。维护程序员不可避免地要混淆两者。
对那些没用代码的修改可以编译通过,但没有任何效果。