面对现实,超越自己
逆水行舟,不进则退
posts - 269,comments - 32,trackbacks - 0

 近期在工作中发现,许多同事,尤其是我们的PHP开发者,基本不会用Linux/unix下的快捷方式,严重影响工作效率,所以特撰写此文,每个用法后我会详细注释。

  下述所有命令在Linux/unix的shell下有效,这里以bash为主。如有出入,以你自己的服务器为准。本文所指的Linux主要指RHEL/CentOS,unix指的是FreeBSD,这也是服务器中用得最多的版本。

  Ctrl + a 切换到命令行开始

  这个操作跟Home实现的结果一样的,但Home在某些unix环境下无法使用,便可以使用这个组合;在Linux下的vim,这个也是有效的;另外,在windows的许多文件编辑器里,这个也是有效的。

  Ctrl + e 切换到命令行末尾

  这个操作跟END实现的结果一样的,但End键在某些unix环境下无法使用,便可以使用这个组合;在Linux下的vim,这个也是有效的;另外,在windows的许多文件编辑器里,这个也是有效的。

  Ctrl + l 清除屏幕内容,效果等同于clear

  Ctrl + u 清除剪切光标之前的内容

  这个命令很有用,在nslookup里也是有效的。我有时看见同事一个字一个字的删除shell命令,十分崩溃!其实完全可以用一个Ctrl + u搞定。

  Ctrl + k 剪切清除光标之后的内容

  Ctrl + y 粘贴刚才所删除的字符

  此命令比较强悍,删除的字符有可能是几个字符串,但极有可能是一行命令。

  Ctrl + r 在历史命令中查找 (这个非常好用,输入关键字就调出以前的命令了)

  这个命令我强烈推荐,有时history比较多时,想找一个比较复杂的,直接在这里,shell会自动查找并调用,方便极了

  Ctrl + c 终止命令

  Ctrl + d 退出shell,logout

  Ctrl + z 转入后台运行

  不过,由Ctrl + z转入后台运行的进程在当前用户退出后就会终止,所以用这个不如用nohup命令&,因为nohup命令的作用就是用户退出之后进程仍然继续运行,而现在许多脚本和命令都要求在root退出时仍然有效。

  下面再被充下大家不是太熟悉,我用得比较多的操作方式:

  !!  重复执行最后一条命令

  history 显示你所有执行过的编号+历史命令。这个可以配合!编辑来执行某某命令

  ↑(Ctrl+p) 显示上一条命令

  ↓(Ctrl+n) 显示下一条命令

  !$ 显示系统最近的一条参数

  最后这个比较有用,比如我先用cat /etc/sysconfig/network-scripts/ifconfig-eth0,然后我想用vim编辑。一般的做法是先用↑ 显示最后一条命令,然后用Home移动到命令最前,删除cat,然后再输入vim命令。其实完全可以用vim !$来代替。

  开发和管理员的话,掌握以上用法后,基本上工作就很有效率了;用到最后,你会不经意发现,弹指之间,许多复杂的指令你会很轻松的搞定。

  附录:Linux下的桌面环境的快捷方式

  以下指令在Linux/unix的桌面环境(gnome)下有效,如有出入以你自己的服务器为准:

  Alt + F1 类似Windows下的Win键,在GNOME中打开"应用程序"菜单(Applications)

  Alt + F2 类似Windows下的Win + R组合键,在GNOME中运行应用程序

  Ctrl + Alt + D 类似Windows下的Win + D组合键,显示桌面

  Ctrl + Alt + L 锁定桌面并启动屏幕保护程序

  Alt + Tab 同Windows下的Alt + Tab组合键,在不同程序窗口间切换

  PrintScreen 全屏抓图

  Alt + PrintScreen 当前窗口抓图

  Ctrl + Alt + → / ← 在不同工作台间切换

  Ctrl + Alt + Shift + → / ← 移动当前窗口到不同工作台

  Ctrl+Alt+Shift+Fn 终端N或模拟终端N(n和N为数字1-6)

  Ctrl+Alt+Shift+F7 返回桌面

  Ctrl+Alt+Shift+F8 未知(终端或模拟终端)

  窗口操作快捷键

  Alt + F4 关闭窗口

  Alt + F5 取消最大化窗口 (恢复窗口原来的大小)

  Alt + F7 移动窗口 (注: 在窗口最大化的状态下无效)

  Alt + F8 改变窗口大小 (注: 在窗口最大化的状态下无效)

  Alt + F9 最小化窗口

  Alt + F10 最大化窗口

  Alt + Space 打开窗口的控制菜单 (点击窗口左上角图标出现的菜单)

  应用程序中的常用快捷键

  下面这些并不适用于所有程序。可以和Windows下的快捷键类比下:

  Ctrl+N 新建窗口

  Ctrl+X 剪切

  Ctrl+C 复制

  Ctrl+V 粘贴

  Ctrl+Z 撤销上一步操作

  Ctrl+Shift+Z 重做刚撤销的一步操作

  Ctrl+S 保存

  文件浏览器

  Ctrl+H 显示隐藏文件(切换键)

  Ctrl+T 新建标签

  Ctrl+Page Up 上一个标签

  Ctrl+Page Down 下一个标签

  Alt+N 切换到第N个标签(N为数字)

      打开终端的方式:

      1.鼠标点右键--terminal,即可打开。

      2.点任务栏的“application”里面的“terminal”打开

      3.命令方式:Alt+F2后在出现"运行应用程序"中输入x-terminal-emulator(一般在你输入到x-term后系统会自己显示全部)或者输入“gnome-terminal“


本为转自:http://linux.chinaitlab.com/command/815732.html

posted @ 2013-02-25 09:57 王海光 阅读(1295) | 评论 (0)编辑 收藏

   不管实在C还是C++代码中,typedef这个词都不少见,当然出现频率较高的还是在C代码中。typedef与#define有些相似,但更多的是不同,特别是在一些复杂的用法上,就完全不同了,看了网上一些C/C++的学习者的博客,其中有一篇关于typedef的总结还是很不错,由于总结的很好,我就不加修改的引用过来了,以下是引用的内容(红色部分是我自己写的内容)。

用途一:

定义一种类型的别名,而不只是简单的宏替换。可以用作同时声明指针型的多个对象。比如:

char* pa, pb; // 这多数不符合我们的意图,它只声明了一个指向字符变量的指针,

// 和一个字符变量;

以下则可行:

typedef char* PCHAR;

PCHAR pa, pb;  

这种用法很有用,特别是char* pa, pb的定义,初学者往往认为是定义了两个字符型指针,其实不是,而用typedef char* PCHAR就不会出现这样的问题,减少了错误的发生。

用途二:
用在旧的C代码中,帮助struct。以前的代码中,声明struct新对象时,必须要带上struct,即形式为: struct 结构名对象名,如:

struct tagPOINT1

 {
    int x;

    int y; 
};

struct tagPOINT1 p1;

而在C++中,则可以直接写:结构名对象名,即:tagPOINT1 p1;

typedef struct tagPOINT
{
    int x;

    int y;
}POINT;

POINT p1; // 这样就比原来的方式少写了一个struct,比较省事,尤其在大量使用的时

候,或许,在C++中,typedef的这种用途二不是很大,但是理解了它,对掌握以前的旧代

码还是有帮助的,毕竟我们在项目中有可能会遇到较早些年代遗留下来的代码。

用途三:

用typedef来定义与平台无关的类型。

比如定义一个叫 REAL 的浮点类型,在目标平台一上,让它表示最高精度的类型为:

typedef long double REAL;

在不支持 long double 的平台二上,改为:

typedef double REAL;

在连 double 都不支持的平台三上,改为:

typedef float REAL;

也就是说,当跨平台时,只要改下 typedef 本身就行,不用对其他源码做任何修改。

标准库就广泛使用了这个技巧,比如size_t。另外,因为typedef是定义了一种类型的新别名,不是简单的字符串替换,所以它比宏来得稳健。
     这个优点在我们写代码的过程中可以减少不少代码量哦!

用途四:

为复杂的声明定义一个新的简单的别名。方法是:在原来的声明里逐步用别名替换一部

分复杂声明,如此循环,把带变量名的部分留到最后替换,得到的就是原声明的最简化

版。举例: 

 原声明:void (*b[10]) (void (*)());

变量名为b,先替换右边部分括号里的,pFunParam为别名一:

typedef void (*pFunParam)();

再替换左边的变量b,pFunx为别名二:

typedef void (*pFunx)(pFunParam);

原声明的最简化版:

pFunx b[10];
 
原声明:doube(*)() (*e)[9];

变量名为e,先替换左边部分,pFuny为别名一:

typedef double(*pFuny)();

再替换右边的变量e,pFunParamy为别名二

typedef pFuny (*pFunParamy)[9];

原声明的最简化版:

pFunParamy e;

理解复杂声明可用的“右左法则”:从变量名看起,先往右,再往左,碰到一个圆括号

就调转阅读的方向;括号内分析完就跳出括号,还是按先右后左的顺序,如此循环,直

到整个声明分析完。举例:

int (*func)(int *p);

首先找到变量名func,外面有一对圆括号,而且左边是一个*号,这说明func是一个指针

;然后跳出这个圆括号,先看右边,又遇到圆括号,这说明(*func)是一个函数,所以

func是一个指向这类函数的指针,即函数指针,这类函数具有int*类型的形参,返回值

类型是int。

int (*func[5])(int *);

func右边是一个[]运算符,说明func是具有5个元素的数组;func的左边有一个*,说明

func的元素是指针(注意这里的*不是修饰func,而是修饰func[5]的,原因是[]运算符

优先级比*高,func先跟[]结合)。跳出这个括号,看右边,又遇到圆括号,说明func数

组的元素是函数类型的指针,它指向的函数具有int*类型的形参,返回值类型为int。

这种用法是比较复杂的,出现的频率也不少,往往在看到这样的用法却不能理解,相信以上的解释能有所帮助。

*****以上为参考部分,以下为本人领悟部分*****

使用示例:

1.比较一:

#include <iostream>

using namespace std;

typedef int (*A) (char, char);

int ss(char a, char b)
{
    cout<<"功能1"<<endl;

    cout<<a<<endl;

    cout<<b<<endl;

    return 0;
}
 
int bb(char a, char b)
{

    cout<<"功能2"<<endl;

    cout<<b<<endl;

    cout<<a<<endl;

    return 0;

}

void main()
{

    A a;

    a = ss;

    a('a','b');

    a = bb;

    a('a', 'b');
}

2.比较二:

typedef int (A) (char, char);

void main()
{

    A *a;

    a = ss;

    a('a','b');

    a = bb;

    a('a','b');
}
 

两个程序的结果都一样:

功能1

a

b

功能2

b

a

 

*****以下是参考部分*****

参考自:http://blog.hc360.com/portal/personShowArticle.do?articleId=57527

typedef 与 #define的区别:

案例一:

通常讲,typedef要比#define要好,特别是在有指针的场合。请看例子:

typedef char *pStr1;

#define pStr2 char *;

pStr1 s1, s2;

pStr2 s3, s4;
在上述的变量定义中,s1、s2、s3都被定义为char *,而s4则定义成了char,不是我们

所预期的指针变量,根本原因就在于#define只是简单的字符串替换而typedef则是为一

个类型起新名字。

案例二:

下面的代码中编译器会报一个错误,你知道是哪个语句错了吗?

typedef char * pStr;

char string[4] = "abc";

const char *p1 = string;

const pStr p2 = string;

p1++;

p2++;

  是p2++出错了。这个问题再一次提醒我们:typedef和#define不同,它不是简单的

文本替换。上述代码中const pStr p2并不等于const char * p2。const pStr p2和

const long x本质上没有区别,都是对变量进行只读限制,只不过此处变量p2的数据类

型是我们自己定义的而不是系统固有类型而已。因此,const pStr p2的含义是:限定数

据类型为char *的变量p2为只读,因此p2++错误。虽然作者在这里已经解释得很清楚了,可我在这个地方仍然还是糊涂的,真的希望哪位高手能帮忙指点一下,特别是这一句“只不过此处变量p2的数据类型是我们自己定义的而不是系统固有类型而已”,难道自己定义的类型前面用const修饰后,就不能执行更改运算,而系统定义的类型却可以?

本文转自:
http://www.cnblogs.com/csyisong/archive/2009/01/09/1372363.html

posted @ 2013-02-20 17:19 王海光 阅读(467) | 评论 (0)编辑 收藏
1、Replace函数替换查找

Replace函数返回值:返回被替换的字符数。如果这个字符串没有改变则返回零。

CString sTest="aabbccaadd";
int nCount=s.Replace("a","a");

nCount
就是你的想要的值

CString::Replace
int Replace( TCHAR chOld, TCHAR chNew );
int Replace( LPCTSTR lpszOld, LPCTSTR lpszNew );

Return Value
The number of replaced instances of the character. Zero if the string isn't changed.


2、标准函数 count_if

#include <iostream>
#include <string>
#include <functional>
#include <algorithm>
using namespace std;

int main( void )
{
    const string a = " 12113";
    cout << count_if( a.begin(), a.end(), bind2nd(equal_to<char>(),'1') ) << endl;

    return 0;
}

CString也一样,但它没有标准的迭代器,因此需要写成
count_if( (LPCTSTR)a, (LPCTSTR)a+a.GetLength(), bind2nd(equal_to<TCHAR>(),_T('某字符')) )
posted @ 2013-02-19 14:34 王海光 阅读(11810) | 评论 (2)编辑 收藏

说说异或运算^和他的一个常用作用。
异或的运算方法是一个二进制运算:
1^1=0
0^0=0
1^0=1
0^1=1

两者相等为0,不等为1.

这样我们发现交换两个整数的值时可以不用第三个参数。
如a=11,b=9.以下是二进制
a=a^b=1011^1001=0010;
b=b^a=1001^0010=1011;
a=a^b=0010^1011=1001;
这样一来a=9,b=13了。


举一个运用, 按一个按钮交换两个mc的位置可以这样。

mybt.onPress=function()
{
mc1._x=mc1._x^mc2._x;
mc2._x=mc2._x^mc1._x;
mc1._x=mc1._x^mc2._x;
//
mc1._y=mc1._y^mc2._y;
mc2._y=mc2._y^mc1._y;
mc1._y=mc1._y^mc2._y;
}

这样就可以不通过监时变量来传递了。

最后要声明:只能用于整数。

 

1. 位运算 请点评

整数在计算机中用二进制的位来表示,C语言提供一些运算符可以直接操作整数中的位,称为位运算,这些运算符的操作数都必须是整型的。在以后的学习中你会发现,有些信息利用整数中的某几个位来存储,要访问这些位,仅仅有对整数的操作是不够的,必须借助位运算,例如第 2 节 “Unicode和UTF-8” 介绍的UTF-8编码就是如此,学完本节之后你应该能自己写出UTF-8的编码和解码程序。本节首先介绍各种位运算符,然后介绍与位运算有关的编程技巧。

1.1. 按位与、或、异或、取反运算 请点评

第 3 节 “布尔代数” 讲过逻辑与、或、非运算,并列出了真值表,对于整数中的位也可以做与、或、非运算,C语言提供了按位与(Bitwise AND)运算符&、按位或(Bitwise OR)运算符|和按位取反(Bitwise NOT)运算符~,此外还有按位异或(Bitwise XOR)运算符^,我们在第 1 节 “为什么计算机用二进制计数” 讲过异或运算。下面用二进制的形式举几个例子。

图 16.1. 位运算

位运算

注 意,&、|、^运算符都是要做Usual Arithmetic Conversion的(其中有一步是Integer Promotion),~运算符也要做Integer Promotion,所以在C语言中其实并不存在8位整数的位运算,操作数在做位运算之前都至少被提升为int 型了,上面用8位整数举例只是为了书写方便。比如:

unsigned char c = 0xfc;
unsigned int i = ~c;

计算过程是这样的:常量0xfc是int 型的,赋给c 要转成unsigned char ,值不变;c 的十六进制表示是fc,计算~c 时先提升为整型(000000fc)然后取反,最后结果是ffffff03。注意,如果把~c 看成是8位整数的取反,最后结果就得3了,这就错了。为了避免出错,一是尽量避免不同类型之间的赋值,二是每一步计算都要按上一章讲的类型转换规则仔细检查。

1.2. 移位运算 请点评

移位运算符(Bitwise Shift)包括左移<<和右移>>。左移将一个整数的各二进制位全部左移若干位,例如0xcfffffff3<<2得到0x3fffffcc:

图 16.2. 左移运算

左移运算

最高两位的11被移出去了,最低两位又补了两个0,其它位依次左移两位。但要注意,移动的位数必须小于左操作数的总位数,比如上面的例子,左边是unsigned int 型,如果左移的位数大于等于32位,则结果是Undefined。移位运算符不同于+ - * / ==等运算符,两边操作数的类型不要求一致,但两边操作数都要做Integer Promotion,整个表达式的类型和左操作数提升后的类型相同。

复习一下第 2 节 “不同进制之间的换算” 讲过的知识可以得出结论,在一定的取值范围内,将一个整数左移1位相当于乘以2 。比如二进制11(十进制3)左移一位变成110,就是6,再左移一位变成1100,就是12。读者可以自己验证这条规律对有符号数和无符号数都成立,对负数也成立。当然,如果左移改变了最高位(符号位),那么结果肯定不是乘以2了,所以我加了个前提“在一定的取值范围内 ”。由于计算机做移位比做乘法快得多,编译器可以利用这一点做优化,比如看到源代码中有i * 8 ,可以编译成移位指令而不是乘法指令。

当操作数是无符号数时,右移运算的规则和左移类似,例如0xcfffffff3>>2得到0x33fffffc:

图 16.3. 右移运算

右移运算

最低两位的11被移出去了,最高两位又补了两个0,其它位依次右移两位。和左移类似,移动的位数也必须小于左操作数的总位数,否则结果是Undefined。在一定的取值范围内,将一个整数右移1位相当于除以2,小数部分截掉。

当操作数是有符号数时,右移运算的规则比较复杂:

  • 如果是正数,那么高位移入0

  • 如果是负数,那么高位移入1还是0不一定,这是Implementation-defined的。对于x86平台的gcc 编译器,最高位移入1,也就是仍保持负数的符号位,这种处理方式对负数仍然保持了“右移1位相当于除以2 ”的性质。

综上所述,由于类型转换和移位等问题,用有符号数做位运算是很不方便的,所以,建议只对无符号数做位运算,以减少出错的可能 。

习题 请点评

1、下面两行printf 打印的结果有何不同?请读者比较分析一下。%x 转换说明的含义详见第 2.9 节 “格式化I/O函数” 。

int i = 0xcffffff3;
printf("%x/n", 0xcffffff3>>2);
printf("%x/n", i>>2);

1.3. 掩码 请点评

如果要对一个整数中的某些位进行操作,怎样表示这些位在整数中的位置呢?可以用掩码(Mask)来表示。比如掩码0x0000ff00表示对一个32位整数的8~15位进行操作,举例如下。

1、取出8~15位。

unsigned int a, b, mask = 0x0000ff00;
a = 0x12345678;
b = (a & mask) >> 8; /* 0x00000056 */

这样也可以达到同样的效果:

b = (a >> 8) & ~(~0U << 8);

2、将8~15位清0。

unsigned int a, b, mask = 0x0000ff00;
a = 0x12345678;
b = a & ~mask; /* 0x12340078 */

3、将8~15位置1。

unsigned int a, b, mask = 0x0000ff00;
a = 0x12345678;
b = a | mask; /* 0x1234ff78 */

习题 请点评

1、统计一个无符号整数的二进制表示中1的个数,函数原型是int countbit(unsigned int x); 。

2、用位操作实现无符号整数的乘法运算,函数原型是unsigned int multiply(unsigned int x, unsigned int y); 。例如:(11011)2 ×(10010)2 =((11011)2 <<1)+((11011)2 <<4)。

3、对一个32位无符号整数做循环右移,函数原型是unsigned int rotate_right(unsigned int x, int n); 。所谓循环右移就是把低位移出去的部分再补到高位上去,例如rotate_right(0xdeadbeef, 8) 的值应该是0xefdeadbe。

1.4. 异或运算的一些特性 请点评

1、一个数和自己做异或的结果是0。如果需要一个常数0,x86平台的编译器可能会生成这样的指令:xorl %eax, %eax 。不管eax 寄存器里的值原来是多少,做异或运算都能得到0,这条指令比同样效果的movl $0, %eax 指令快,直接对寄存器做位运算比生成一个立即数再传送到寄存器要快一些。

2、从异或的真值表可以看出,不管是0还是1,和0做异或保持原值不变,和1做异或得到原值的相反值。可以利用这个特性配合掩码实现某些位的翻转,例如:

unsigned int a, b, mask = 1U << 6;
a = 0x12345678;
b = a ^ mask; /* flip the 6th bit */

3、如果a1 ^ a2 ^ a3 ^ ... ^ an 的结果是1,则表示a1 、a2 、a3 ...an 之中1的个数为奇数个,否则为偶数个。这条性质可用于奇偶校验(Parity Check),比如在串口通信过程中,每个字节的数据都计算一个校验位,数据和校验位一起发送出去,这样接收方可以根据校验位粗略地判断接收到的数据是否有误。

4、x ^ x ^ y == y,因为x ^ x == 0,0 ^ y == y。这个性质有什么用呢?我们来看这样一个问题:交换两个变量的值,不得借助额外的存储空间,所以就不能采用temp = a; a = b; b = temp; 的办法了。利用位运算可以这样做交换:

a = a ^ b;
b = b ^ a;
a = a ^ b;

分析一下这个过程。为了避免混淆,把a和b的初值分别记为a0 和b0 。第一行,a = a0 ^ b0 ;第二行,把a的新值代入,得到b = b0 ^ a0 ^ b0 ,等号右边的b0 相当于上面公式中的x,a0 相当于y,所以结果为a0 ;第三行,把a和b的新值代入,得到a = a0 ^ b0 ^ a0 ,结果为b0 。注意这个过程不能把同一个变量自己跟自己交换,而利用中间变量temp 则可以交换。

习题 请点评

1、请在网上查找有关RAID(Redundant Array of Independent Disks,独立磁盘冗余阵列)的资料,理解其实现原理,其实就是利用了本节的性质3和4。

2、交换两个变量的值,不得借助额外的存储空间,除了本节讲的方法之外你还能想出什么方法?本节讲的方法不能把同一个变量自己跟自己交换,你的方法有没有什么局限性?

本文转自:http://blog.csdn.net/yunyuehu/article/details/5408446#t1

posted @ 2013-01-18 11:07 王海光 阅读(886) | 评论 (0)编辑 收藏
示例代码:
DWORD CCommonFun::GetDesignatedDiskFreeSpace(const CString &szPath)
{
    DWORD dwTotalDiskSpace,dwFreeDiskSpace,dwUsedDiskSpace;    

    ULARGE_INTEGER uiFreeBytesAvailableToCaller;
    ULARGE_INTEGER uiTotalNumberOfBytes;
    ULARGE_INTEGER uiTotalNumberOfFreeBytes;

    if(GetDiskFreeSpaceEx(szPath, &uiFreeBytesAvailableToCaller,
        &uiTotalNumberOfBytes,
        &uiTotalNumberOfFreeBytes))
    {
        dwTotalDiskSpace = (DWORD)(uiTotalNumberOfBytes.QuadPart / 1024 / 1024);
        dwFreeDiskSpace  = (DWORD)(uiFreeBytesAvailableToCaller.QuadPart >> 20);
        dwUsedDiskSpace     = dwTotalDiskSpace - dwFreeDiskSpace;
        TRACE("硬盘%s::总空间%dMB, 已用%dMB, 可用%dMB\n", szPath,
            dwTotalDiskSpace, dwUsedDiskSpace, dwFreeDiskSpace);

        return dwFreeDiskSpace;
    }

    return -1;
}
posted @ 2013-01-16 17:00 王海光 阅读(2872) | 评论 (0)编辑 收藏
变灰代码:
CMenu*   menu   =   this->GetSystemMenu(FALSE);   
menu->EnableMenuItem(SC_CLOSE, MF_BYCOMMAND | MF_GRAYED);

恢复代码:
CMenu*   menu   =   this->GetSystemMenu(FALSE);   
menu->EnableMenuItem(SC_CLOSE, MF_BYCOMMAND | MF_ENABLED);
posted @ 2013-01-16 16:57 王海光 阅读(896) | 评论 (0)编辑 收藏
MFC另存为和保存对话框:

CString sPath;
TCHAR szFilters[]=_T("All files(*.*)|*.*||");
CFileDialog dlg(nFlag,NULL,_T(m_strTime),OFN_HIDEREADONLY| OFN_OVERWRITEPROMPT,szFilters);
dlg.m_ofn.lpstrInitialDir=_T("c:\\");
if(IDOK==dlg.DoModal())
{
    sPath=dlg.GetPathName();
}
nFlag值为true时,是保存对话框,为false时是另存为对话框,m_strTime为默认文件名字。


MFC弹出选择目录对话框:

LPMALLOC lpMalloc;
if(::SHGetMalloc(&lpMalloc)!=NOERROR)
{
AfxMessageBox("选择下载目录操作出错");
return;
}
char szDisplayName[_MAX_PATH];
char szBuffer[_MAX_PATH];
BROWSEINFO browseInfo;
browseInfo.hwndOwner=this->m_hWnd;
browseInfo.pidlRoot=NULL;
browseInfo.pszDisplayName=szDisplayName;
browseInfo.lpszTitle="请选择下载文件的存储路径";
browseInfo.ulFlags=BIF_RETURNFSANCESTORS|BIF_RETURNONLYFSDIRS;
browseInfo.lpfn=NULL;
browseInfo.lParam=0;
LPITEMIDLIST lpItemIDList;
if((lpItemIDList=::SHBrowseForFolder(&browseInfo))!=NULL)
{
if(::SHGetPathFromIDList(lpItemIDList,szBuffer))
{
if(szBuffer[0]=='\0')
{
AfxMessageBox("Fail to get directory",MB_ICONSTOP|MB_OK);
return;
}
DownFileDirectory=szBuffer;
}
else
{
AfxMessageBox("Fail to get directory!",MB_ICONSTOP|MB_OK);
return;
}
lpMalloc->Free(lpItemIDList);
lpMalloc->Release();
}
CString strMsg;
strMsg.Format("选择目录为:%s",DownFileDirectory);
AfxMessageBox(strMsg);
posted @ 2013-01-10 16:25 王海光 阅读(1404) | 评论 (0)编辑 收藏

转自:http://www.itivy.com/ivy/archive/2011/11/24/something-that-architecture-must-be-aware-of.html

对于大多数架构师而言,“可扩展性”在软件架构方面是最虚无缥缈的说法。这毫不奇怪,因为可扩展性正是如今软件设计领域最值得优先考虑的要素。然 而,计算机科学家们还无法了解一套单独的架构如何才能扩展至各类应用环境当中。相反,我们在数量繁多的方案中所设计出的可扩展性架构,往往以业界较为通用 的已知可扩展模式及个人偏好为标准。简单来讲,打造一套具备可扩展性的系统已经变得更像是一门艺术而不单单是技术。

我们常常会通过观摩杰作体会并学习艺术的精髓,而可扩展性也应该遵循同样的路线!

在这篇文章中,我将列出数款为大家所耳熟能详的可扩展性架构。通常情况下,架构师们完全可以借鉴已知的可扩展架构模式,进而创造出新的可扩展架构。

  1. LB (负载平衡器) + 无共享单位 - 该模型中包含一系列单元,各单元彼此间不共享任何内容,且一致指向一个将输入文讯按一定条件发往单元处的负载平衡器(这构成一个循 环,以负载等情况为基础)。每个单元可以是一个单独的节点或是紧密耦合的节点所构成的集群。用户可以使用DNS循环、硬件负载平衡器或者软件负载平衡器达 成负载平衡效果。创建一套负载均衡的层次结构,并在其中结合前面提到的各种负载平衡器也是可行的。在由Michael Stonebraker撰写的《 无共享体系架构实例 》一文中,专门讨论了此类架构。
     
  2. LB + 无状态节点 + 可扩展存储 - 传统的 三层式Web架构 使用的就是这种模型。该模型包括数个与可扩展存储交互的无状态节点以及一个分布于节点间负载中的负载平衡器。在这一模型中,存储通常作为限制因素存在,但NoSQL存储则可以利用这套模型创建出具备相当可扩展性的系统。
     
  3. 点对点架构 (分布式Hash列表 (简称DHT)以及内容寻址网络(简称CAN)) -这套模型提供了一些传统的 可扩展算法,这些算法的各个方面几乎全部按对数进行了等比例增加。举例来说,像Chord、Pastry(特指免费版)以及CAN都属于此类。而以 Cassandra为代表的、基于P2P架构的几款NoSQL系统也是其中的成员。《 展望P2P系统中的数据 》一文就深入探讨了这类模型的各种细节。
     
  4. 分布式队列 – 这种模型以将队列实施(即先进先出交付机制)作为网络服务处理为基础。该模型通过JMS队列而广泛得到采用。一般会遵循这种做法的有任务队列以及通过保持队列分级体系实现扩展性的任务队列版本,后者在负载无法及时处理时,任务会由低级层面向高级层面传递。
     
  5. 发布/订阅模式 - 一般用于通过网络向彼此发布订阅讯息。《 发布与订阅的多面性 》这一经典论文中详细的介绍这一模型,该模型方面最典型的例子即 NaradaBroker与 EventJava 
     
  6. 小道消息与自然灵感式模型 - 这种模型源自日常生活中小道消息的传播途径,也就是每个节点将随机选择后续节点以交 换信息。正如现实生活中的实际反馈,这种八卦型算法在信息传播方面出奇地迅速。该模型的另一大分支则是受到生物学影响的启发式算法。自然世界中存在着大量 协调及扩展方面极为卓越的固有算法。举例来说,蚂蚁、人类以及蜜蜂等等,都能够以最简洁的交流方式协调好扩展性方面的需要。模型中的算法正是借鉴了这些实 际存在的现象。在论文《 从流行病的蔓延到分布式计算 》中对这种模型有着详尽的叙述。
     
  7. 地图缩小/数据流 - 这一概念首先由谷歌公司提出,地图缩小为工作的描述及执行提供了一套可扩展的模式。虽然内容 简单,但它仍然成为联机分析处理方面的首要处理模式。数据流则是一种更先进的方式,用来表达执行信息;而像Dryad及Pig这样的项目为数据流的执行提 供了可扩展的框架。论文《 地图缩小:大型集群上的简化数据处理 》中设置了专门的主题,详细讨论这一内容。Apache的Hadoop就是这种模型的代表性产品。
     
  8. 责任树形图 - 这种模型打破了递归问题的束缚,将整个流程以树状形式加以处理;每个父节点将工作下放至子节点。这种模型扩展性强,并已经被应用于数款可扩展性架构当中。
     
  9. 流处理 - 这种模型被用于处理源源不断的数据流及数据。这种处理方式通过网络中的处理节点获得支持(例如Aurora、Twitter Strom以及Apache S4等)。
     
  10. 可扩展存储 – 该模型的应用范围从数据库、NoSQL存储、服务注册到文件系统都有体现。 链接中的这篇文章 以可扩展性为切入点对其进行了深入讨论。

综上所述,可扩展性的实现只有三种方式,即:分布、缓存及异步处理。前文所提到的各种架构事实上都是把这三种方式进行不同组合并加以实施。而另一方 面,不利于可扩展性的因素,除了糟糕的编码本身,全局性协调也起到了重要的影响。简单来说,任何一种全局性协调都会限制系统的可扩展性。本文中所提到的各 种架构也只是在做好了本地性协调,而非全局性协调。

然而,将它们有机地结合起来以创建一套极具可扩展性的架构可不像说起来那么容易,除非我们能找到一种全新的扩展模式。不过经验告诉我们,比起搞一套全新的架构,采用为我们所熟知且更易驾驭的可扩展性解决方案永远是更好的选择。

posted @ 2013-01-07 16:49 王海光 阅读(504) | 评论 (0)编辑 收藏

转自:http://www.infoq.com/cn/articles/cyw-evaluate-seachengine-result-quality

前言

搜索质量评估是搜索技术研究的基础性工作,也是核心工作之一。评价(Metrics)在搜索技术研发中扮演着重要角色,以至于任何一种新方法与他们的评价方式是融为一体的。


搜索引擎结果的好坏与否,体现在业界所称的在相关性(Relevance)上。相关性的定义包括狭义和广义两方面,狭义的解释是:检索结果和用户查询的相关程度。而从广义的层面,相关性可以理解为为用户查询的综合满意度。直观的来看,从用户进入搜索框的那一刻起,到需求获得满足为止,这之间经历的过程越顺畅,越便捷,搜索相关性就越好。本文总结业界常用的相关性评价指标和量化评价方法。供对此感兴趣的朋友参考。

Cranfield评价体系

A Cranfield-like approach这个名称来源于英国Cranfield University,因为在二十世纪五十年代该大学首先提出了这样一套评价系统:由查询样例集、正确答案集、评测指标构成的完整评测方案,并从此确立了“评价”在信息检索研究中的核心地位。

Cranfield评价体系由三个环节组成:

  1. 抽取代表性的查询词,组成一个规模适当的集合
  2. 针对查询样例集合,从检索系统的语料库中寻找对应的结果,进行标注(通常人工进行)
  3. 将查询词和带有标注信息的语料库输入检索系统,对系统反馈的检索结果,使用预定义好的评价计算公式,用数值化的方法来评价检索系统结果和标注的理想结果的接近程度

查询词集合的选取

Cranfield评价系统在各大搜索引擎公司内有广泛的应用。具体应用时,首先需要解决的问题是构造一个测试用查询词集合。

按照Andrei Broder(曾在AltaVista/IBM/Yahoo任职)的研究,查询词可分为3类:寻址类查询(Navigational)、信息类查询(Informational)、事务类查询(Transactional)。对应的比例分别为

Navigational : 12.3%  Informational : 62.0%  Transactional : 25.7% 

为了使得评估符合线上实际情况,通常查询词集合也会按比例进行选取。通常从线上用户的Query Log文件中自动抽取。

另外查询集合的构造时,除了上述查询类型外,还可以考虑Query的频次,对热门query(高频查询)、长尾query(中低频)分别占特定的比例。

另外,在抽取Query时,往往Query的长短也是一个待考虑的因素。因为短query(单term的查询)和长Query(多Term的查询)排序算法往往会有一些不同。

构成查询集合后,使用这些查询词,在不同系统(例如对比百度和Google)或不同技术间(新旧两套Ranking算法的环境)进行搜索,并对结果进行评分,以决定优劣。

附图:对同一Query:“社会保险法”,各大搜索引擎的结果示意图。下面具体谈谈评分的方法。

Precision-recall(准确率-召回率方法)

计算方法

信息检索领域最广为人知的评价指标为Precision-Recall(准确率-召回率)方法。该方法从提出至今已经历半个世纪,至今在很多搜索引擎公司的效果评估中使用。

顾名思义,这个方法由准确率和召回率这两个相互关联的统计量构成:召回率(Recall)衡量一个查询搜索到所有相关文档的能力,而准确率(Precision)衡量搜索系统排除不相关文档的能力。(通俗的解释一下:准确率就是算一算你查询得到的结果中有多少是靠谱的;而召回率表示所有靠谱的结果中,有多少被你给找回来了)。这两项是评价搜索效果的最基础指标,其具体的计算方法如下。

Precision-recall方法假定对一个给定的查询,对应一个被检索的文档集合和一个不相关的文档集合。这里相关性被假设为二元的,用数学形式化方法来描述,则是:

A表示相关文档集合

A表示不相关集合

B表示被检索到的文档集合

B表示未被检索到的文档集合

则单次查询的准确率和召回率可以用下述公式来表达:

(运算符∩ 表示两个集合的交集。|x|符号表示集合x中的元素数量)

从上面的定义不难看出,召回率和准确率的取值范围均在[0,1]之间。那么不难想象,如果这个系统找回的相关越多,那么召回率越高,如果相关结果全部都给召回了,那么recall此时就等于1.0。

 

相关的

不相关

被检索到

A∩ B

A∩ B

未被检索到

A∩B

AB

Precision-Recall曲线

召回率和准确率分别反映了检索系统的两个最重要的侧面,而这两个侧面又相互制约。因为大规模数据集合中,如果期望检索到更多相关的文档,必然需要“放宽”检索标准,因此会导致一些不相关结果混进来,从而使准确率受到影响。类似的,期望提高准确率,将不相关文档尽量去除时,务必要执行更“严格”的检索策略,这样也会使一些相关的文档被排除在外,使召回率下降。

所以为了更清晰的描述两者间的关系,通常我们将Precison-Recall用曲线的方式绘制出来,可以简称为P-R diagram。常见的形式如下图所示。(通常曲线是一个逐步向下的走势,即随着Recall的提高,Precision逐步降低)

P-R的其它形态

一些特定搜索应用,会更关注搜索结果中错误的结果。例如,搜索引擎的反作弊系统(Anti-Spam System)会更关注检索结果中混入了多少条作弊结果。学术界把这些错误结果称作假阳性(False Positive)结果,对这些应用,通常选择用虚报率(Fallout)来统计:

Fallout和Presion本质是完全相同的。只是分别从正反两方面来计算。实际上是P-R的一个变种。

再回到上图,Presion-Recall是一个曲线,用来比较两个方法的效果往往不够直观,能不能对两者进行综合,直接反映到一个数值上呢?为此IR学术界提出了F值度量(F -Measure)的方法。F-Measure通过Presion和Recall的调和平均数来计算,公式为:

其中参数λε(0,1)调节系统对Precision和Recall的平衡程度。(通常取λ=0.5,此时 

这里使用调和平均数而不是通常的几何平均或算术平均,原因是调和平均数强调较小数值的重要性,能敏感的反映小数字的变化,因此更适合用来反映检索效果。

使用F Measure的好处是只需要一个单一的数字就可以总结系统的检索效果,便于比较不同搜索系统的整体效果。

P@N方法

点击因素

传统的Precision-Recall并不完全适用对搜索引擎的评估,原因是搜索引擎用户的点击方式有其特殊性,包括:

A 60-65%的查询点击了名列搜索结果前10条的网页;  B 20-25%的人会考虑点击名列11到20的网页;  C 仅有3-4%的会点击名列搜索结果中列第21到第30名的网页 

也就是说,绝大部分用户是不愿意翻页去看搜索引擎给出的后面的结果。

而即使在搜索结果的首页(通常列出的是前10条结果),用户的点击行为也很有意思,我们通过下面的Google点击热图(Heat Map)来观察(这个热图在二维搜索结果页上通过光谱来形象的表达不同位置用户的点击热度。颜色约靠近红色表示点击强度越高):

从图中可以看出,搜索结果的前3条吸引了大量的点击,属于热度最高的部分。也就是说,对搜苏引擎来说,最前的几条结果是最关键的,决定了用户的满意程度。

康乃尔大学的研究人员通过eye tracking实验获得了更为精确的Google搜索结果的用户行为分析图。从这张图中可以看出,第一条结果获得了56.38%的搜索流量,第二条和第三条结果的排名依次降低,但远低于排名第一的结果。前三条结果的点击比例大约为11:3:2 。而前三条结果的总点击几乎分流了搜索流量的80%。

另外的一些有趣的结论是,点击量并不是按照顺序依次递减的。排名第七位获得的点击是最少的,原因可能在于用户在浏览过程中下拉页面到底部,这时候就只显示最后三位排名网站,第七名便容易被忽略。而首屏最后一个结果获得的注意力(2.55)是大于倒数第二位的(1.45),原因是用户在翻页前,对最后一条结果印象相对较深。搜索结果页面第二页排名第一的网页(即总排名11位的结果)所获得的点击只有首页排名第十网站的40%,与首页的第一条结果相比,更是只有其1/60至1/100的点击量。

因此在量化评估搜索引擎的效果时,往往需要根据以上搜索用户的行为特点,进行针对性的设计。

P@N的计算方法

P@N本身是Precision@N的简称,指的是对特定的查询,考虑位置因素,检测前N条结果的准确率。例如对单次搜索的结果中前5篇,如果有4篇为相关文档,则P@5 = 4/5 = 0.8 。

测试通常会使用一个查询集合(按照前文所述方法构造),包含若干条不同的查询词,在实际使用P@N进行评估时,通常使用所有查询的P@N数据,计算算术平均值,用来评判该系统的整体搜索结果质量。

N的选取

对用户来说,通常只关注搜索结果最前若干条结果,因此通常搜索引擎的效果评估只关注前5、或者前3结果,所以我们常用的N取值为P@3或P@5等。

对一些特定类型的查询应用,如寻址类的查询(Navigational Search),由于目标结果极为明确,因此在评估时,会选择N=1(即使用P@1)。举个例子来说,搜索“新浪网”、或“新浪首页”,如果首条结果不是 新浪网(url:www.sina.com.cn),则直接判该次查询精度不满足需求,即P@1=0

MRR

上述的P@N方法,易于计算和理解。但细心的读者一定会发现问题,就是在前N结果中,排序第1位和第N位的结果,对准确率的影响是一样的。但实际情况是,搜索引擎的评价是和排序位置极为相关的。即排第一的结果错误,和第10位的结果错误,其严重程度有天壤之别。因此在评价系统中,需要引入位置这个因素。

MRR是平均排序倒数(Mean Reciprocal Rank)的简称,MRR方法主要用于寻址类检索(Navigational Search)或问答类检索(Question Answering),这些检索方法只需要一个相关文档,对召回率不敏感,而是更关注搜索引擎检索到的相关文档是否排在结果列表的前面。MRR方法首先计算每一个查询的第一个相关文档位置的倒数,然后将所有倒数值求平均。例如一个包含三个查询词的测试集,前5结果分别为:

查询一结果:1.AN 2.AR 3.AN 4.AN 5.AR  查询二结果:1.AN 2.AR 3.AR 4.AR 5.AN  查询三结果:1.AR 2.AN 3.AN 4.AN 5.AR  

其中AN表示不相关结果,AR表示相关结果。那么第一个查询的排序倒数(Reciprocal Rank)RR1 = 1/2=0.5 ;第二个结果RR2 = 1/2 = 0.5 ; 注意倒数的值不变,即使查询二获得的相关结果更多。同理,RR3= 1/1 = 1。 对于这个测试集合,最终MRR=(RR1+RR2+RR3)/ 3 = 0.67

然而对大部分检索应用来说,只有一条结果无法满足需求,对这种情况,需要更合适的方法来计算效果,其中最常用的是下述MAP方法。

MAP

MAP方法是Mean Average Precison,即平均准确率法的简称。其定义是求每个相关文档检索出后的准确率的平均值(即Average Precision)的算术平均值(Mean)。这里对准确率求了两次平均,因此称为Mean Average Precision。(注:没叫Average Average Precision一是因为难听,二是因为无法区分两次平均的意义)

MAP 是反映系统在全部相关文档上性能的单值指标。系统检索出来的相关文档越靠前(rank 越高),MAP就应该越高。如果系统没有返回相关文档,则准确率默认为0。

例如:假设有两个主题:

主题1有4个相关网页,主题2有5个相关网页。

某系统对于主题1检索出4个相关网页,其rank分别为1, 2, 4, 7;

对于主题2检索出3个相关网页,其rank分别为1,3,5。

对于主题1,平均准确率MAP计算公式为:

(1/1+2/2+3/4+4/7)/4=0.83。 

对于主题2,平均准确率MAP计算公式为:

(1/1+2/3+3/5+0+0)/5=0.45。 

则MAP= (0.83+0.45)/2=0.64。”

DCG方法

DCG是英文Discounted cumulative gain的简称,中文可翻译为“折扣增益值”。DCG方法的基本思想是:

  1. 每条结果的相关性分等级来衡量
  2. 考虑结果所在的位置,位置越靠前的则重要程度越高
  3. 等级高(即好结果)的结果位置越靠前则值应该越高,否则给予惩罚

我们首先来看第一条:相关性分级。这里比计算Precision时简单统计“准确”或“不准确”要更为精细。我们可以将结果细分为多个等级。比如常用的3级:Good(好)、Fair(一般)、Bad(差)。对应的分值rel为:Good:3 / Fair:2 / Bad:1 。一些更为细致的评估使用5级分类法:Very Good(明显好)、Good(好)、Fair(一般)、Bad(差)、Very Bad(明显差),可以将对应分值rel设置为:Very Good:2 / Good:1 / Fair:0 / Bad:-1 / Very Bad: -2

评判结果的标准可以根据具体的应用来确定,Very Good通常是指结果的主题完全相关,并且网页内容丰富、质量很高。而具体到每条

DCG的计算公式并不唯一,理论上只要求对数折扣因子的平滑性。我个人认为下面的DCG公式更合理,强调了相关性,第1、2条结果的折扣系数也更合理:

此时DCG前4个位置上结果的折扣因子(Discount factor)数值为:

i

log2 (i+1)

1/log2 (i+1)

1

1

1

2

1.59

0.63

3

2

0.5

4

2.32

0.43

取以2为底的log值也来自于经验公式,并不存在理论上的依据。实际上,Log的基数可以根据平滑的需求进行修改,当加大数值时(例如使用log5 代替log2),折扣因子降低更为迅速,此时强调了前面结果的权重。

为了便于不同类型的query结果之间横向比较,以DCG为基础,一些评价系统还对DCG进行了归一,这些方法统称为nDCG(即 normalize DCG)。最常用的计算方法是通过除以每一个查询的理想值iDCG(ideal DCG)来进行归一,公式为:

求nDCG需要标定出理想情况的iDCG,实际操作的时候是异常困难的,因为每个人对“最好的结果”理解往往各不相同,从海量数据里选出最优结果是很困难的任务,但是比较两组结果哪个更好通常更容易,所以实践应用中,通常选择结果对比的方法进行评估。

怎样实现自动化的评估?

以上所介绍的搜索引擎量化评估指标,在Cranfield评估框架(Cranfield Evaluation Framework)中被广泛使用。业界知名的TREC(文本信息检索会议)就一直基于此类方法组织信息检索评测和技术交流。除了TREC外,一些针对不同应用设计的Cranfield评测论坛也在进行进行(如 NTCIR、IREX等)。

但Cranfield评估框架存在的问题是查询样例集合的标注上。利用手工标注答案的方式进行网络信息检索的评价是一个既耗费人力、又耗费时间的过程,只有少数大公司能够使用。并且由于搜索引擎算法改进、运营维护的需要,检索效果评价反馈的时间需要尽量缩短,因此自动化的评测方法对提高评估效率十分重要。最常用的自动评估方法是A/B testing系统。

A/B Testing

A/B Testing系统

A/B testing系统在用户搜索时,由系统来自动决定用户的分组号(Bucket id),通过自动抽取流量导入不同分支,使得相应分组的用户看到的是不同产品版本(或不同搜索引擎)提供的结果。用户在不同版本产品下的行为将被记录下来,这些行为数据通过数据分析形成一系列指标,而通过这些指标的比较,最后就形成了各版本之间孰优孰劣的结论。

在指标计算时,又可细分为两种方法,一种是基于专家评分的方法;一种是基于点击统计的方法。

专家评分的方法通常由搜索核心技术研发和产品人员来进行,根据预先设定的标准对A、B两套环境的结果给予评分,获取每个Query的结果对比,并根据nDCG等方法计算整体质量。

点击评分有更高的自动化程度,这里使用了一个假设:同样的排序位置,点击数量多的结果质量优于点击数量少的结果。(即A2表示A测试环境第2条结果,如果A2 > B2,则表示A2质量更好)。通俗的说,相信群众(因为群众的眼睛是雪亮的)。在这个假设前提下,我们可以将A/B环境前N条结果的点击率自动映射为评分,通过统计大量的Query点击结果,可以获得可靠的评分对比。

Interleaving Testing

另外2003年由Thorsten Joachims 等人提出的Interleaving testing方法也被广泛使用。该方法设计了一个元搜索引擎,用户输入查询词后,将查询词在几个著名搜索引擎中的查询结果随机混合反馈给用户,并收集随后用户的结果点击行为信息.根据用户不同的点击倾向性,就可以判断搜索引擎返回结果的优劣,

如下图所示,将算法A和B的结果交叉放置,并分流量进行测试,记录用户点击信息。根据点击分布来判断A和B环境的优劣。

Interleaving Testing评估方法

Joachims同时证明了Interleaving Testing评价方法与传统Cranfield评价方法的结果具有较高的相关性。由于记录用户选择检索结果的行为是一个不耗费人力的过程,因此可以便捷的实现自动化的搜索效果评估。

总结

没有评估就没有进步——对搜索效果的量化评测,目的是准确的找出现有搜索系统的不足(没有哪个搜索系统是完美的),进而一步一个脚印对算法、系统进行改进。本文为大家总结了常用的评价框架和评价指标。这些技术像一把把尺子,度量着搜索技术每一次前进的距离。


感谢张凯峰对 本文的审校。

给InfoQ中文站投稿或者参与内容翻译工作,请邮件至editors@cn.infoq.com。也欢迎大家加入到InfoQ中文站用户讨论组中与我们的编辑和其他读者 朋友交流。

posted @ 2013-01-07 16:46 王海光 阅读(415) | 评论 (0)编辑 收藏
     摘要: MySQL索引背后的数据结构及算法原理摘要本文以MySQL数据库为研究对象,讨论与数据库索引相关的一些话题。特别需要说明的是,MySQL支持诸多存储引擎,而各种存储引擎对索引的支持也各不相同,因此MySQL数据库支持多种索引类型,如BTree索引,哈希索引,全文索引等等。为了避免混乱,本文将只关注于BTree索引,因为这是平常使用MySQL时主要打交道的索引,至于哈希索引和全文索引本文暂不讨论。文...  阅读全文
posted @ 2013-01-07 16:43 王海光 阅读(301) | 评论 (0)编辑 收藏
仅列出标题
共27页: First 7 8 9 10 11 12 13 14 15 Last