2009年4月13日星期一

约翰 卡马克

昨天看了《DOOM启示录》,准确的说是今天凌晨,看得我是汹涌澎湃,热血燃烧,相信每一个热爱游戏的人看完了都会有同样的感觉。约翰 卡马克,成为了神一般的存在。我多么想成为他那样的人,所以路还很长,很长。。。只有追求,还没有行动。

2009年4月10日星期五

OpenGL (1) 入门

前几天跟着师兄做东西,“被迫”学习了一下OpenGl,这几天又要开始忙着作业啊,考试啊,什么的,赶紧抓个空把学到的东西记录下来,不然一个不小心就“丢”了。

首先讨论一下开发环境配置问题,需要一些DLL、头文件、LIB文件,这些东西网上应该都有:

1) GLU.DLL GLUT.DLLGLUT32.DLL

2) GL.HGLAUX.HGLEXT.HGLU.HGLUT.HWGLEXT.H

3) GLAUX.LIBGLU32.LIBGLUT32.LIBOPENGL32.LIB

由于OpenGL是跨平台的,因此上面的是Win平台下需要的东西。其实GLUT是一个工具包,是Nate Robin开发的,他的主页上有非常详细的教程。GLUTWin的窗口、事件相关的API进行了封装,至少对于初学的我来说用起来十分舒服,而且它也被大家广泛使用的。

当然,我更喜欢原滋原味的东西,因此还是要看看Nehe的教程的。

不说废话了,就以我目前做的一个小例子来说说OpenGL的基本框架吧,当然这里面没有用到光照、雾、纹理等高级的东西,不过有涉及到拾取。

VS2008新建一个控制台程序,源代码如下:

#include

#include

#include

#include

注意这里只需要有GL/GLUT.H就够了,因为其内部包含了对OPENGL的头文件的正确引用。

#define BLACK_F 0.0, 0.0, 0.0

#define RED_F 1.0, 0.0, 0.0

#define GREEN_F 0.0, 1.0, 0.0

#define YELLOW_F 1.0, 1.0, 0.0

#define BLUE_F 0.0, 0.0, 1.0

#define WHITE_F 1.0, 1.0, 1.0

定义了浮点数形式的RGB数值,只是为了编程方便~ *_F”这种表示方法是模仿OpenGL的,因为OpenGL里的很多函数是根据参数的数据类型来命名最后一个的譬如glColor3f(),glColor3d(),分别表示参数为GLfloatGLdouble类型,但是其功能是一模一样的。

#define CUBE_NAME 1

#define PI 3.1415926

#define CUBE_SIZE 2.0

#define HEIGHT 600

#define WIDTH 800

#define SELECT_BUFF_SIZE 512

GLint wndWidth, wndHeight;

全局的变量用来记录当前的窗口宽和高

GLfloat angle = 0.0;

//

// Rotate Axis

//

GLfloat axis[ 3 ] = { 0.0, 0.0, 0.0 };

//

// Mouse location

//

GLfloat lastPos[ 3 ], currentPos[ 3 ];

//

// Camera Properties

//

GLfloat eye[3] = { 0.0, 0.0, 4.0 };

GLfloat at[3] = { 0.0, 0.0, 0.0 };

GLfloat up[3] = { 0.0, 1.0, 0.0 };

GLfloat lastMatrix[ 16 ] =

{1.0, 0.0, 0.0, 0.0,

0.0, 1.0, 0.0, 0.0,

0.0, 0.0, 1.0, 0.0,

0.0, 0.0, 0.0, 1.0};

GLfloat selectCopy[ 16 ] =

{1.0, 0.0, 0.0, 0.0,

0.0, 1.0, 0.0, 0.0,

0.0, 0.0, 1.0, 0.0,

0.0, 0.0, 0.0, 1.0};

unsigned char btnState;

GLboolean isSelected;

//

// Project a 2D mouse coordinates to a 3D sphere

//

int ProjectSphere( GLint x, GLint y, GLint r, GLfloat v[ 3 ] )

{

GLfloat z;

v[0] = ( GLfloat ) x * 2.0 - ( GLfloat ) r;

v[1] = ( GLfloat ) r - ( GLfloat ) y * 2.0;

z = r * r - v[0] * v[0] - v[1] * v[1];

if (z <>

{

return 0;

}

v[2] = sqrt( z );

//

// Normalise

//

v[0] /= ( GLfloat ) r;

v[1] /= ( GLfloat ) r;

v[2] /= ( GLfloat ) r;

return !0;

}

这个函数是将二维的鼠标坐标映射到一个单位球面上,通过它可以实现鼠标控制摄像机。

void Init( void )

{

glClearColor( WHITE_F, 0.0 );

glShadeModel( GL_FLAT );

//glEnable( GL_DEPTH_TEST );

}

做一些初始化的操作glClearColor()设置了清除颜色缓冲区时所试用的颜色,glShadeModel()则指明了着色方案,你只有两种选择GL_FLAT 或者是GL_SMOOTH,默认是后者

void Display( void )

{

glClear( GL_COLOR_BUFFER_BIT );

glClear( GL_DEPTH_BUFFER_BIT );

清除颜色缓冲区和深度缓冲区

glColor3f( RED_F );

设置了我们画东西需要用什么颜色来画

glMatrixMode( GL_MODELVIEW );

调整模型视图矩阵,这里需要特殊说明一下:

OpenGL在完成绘制前一般有几个步骤:

1) 调整投影矩阵, 简洁来说就是把照相机放好,放到哪个位置,朝哪个方向。

2) 调整模型视图矩阵,对物体进行的一系列变换,譬如我们要让它旋转、平移等。

3) 调整视口, 摄像机看到的东西要映射到屏幕上,需要调整映射到哪里的问题。

4) 绘制

glLoadIdentity();

//

// Apply the new transform with the old one

//

glRotatef( angle, axis[ 0 ], axis[ 1 ], axis[ 2 ] );

glMultMatrixf( lastMatrix );

首先进行了旋转,然后乘上了之前的变换矩阵,然而由于矩阵乘法顺序的问题,实际上我们等于先做了lastMatrix的变换,然后才有旋转的效果。

//

// Save as last Matrix

//

glGetFloatv(GL_MODELVIEW_MATRIX, lastMatrix);

glutSolidCube( CUBE_SIZE );

绘制函数,这里我们只是调用了一个GLUT的一个工具函数,画了一个实心的立方体,其实很多时候要绘制什么需要我们自己定义的,调用glDraw***一类的函数,然后放到glBegin()和glEnd()之间就可以了。。。

glutSwapBuffers();

为了实现旋转效果的平滑过渡,我们用了双缓冲,在后面的初始化里有这样的设置,当然你也可以设置成单缓冲,那么这里一定要有glFlush()

}

以上就是画好我们想要的东西,然后把这个函数设置为回调函数就可以了。那么对于用户输入的响应或者一些变换需要在下面实现:

void Reshape( GLsizei w, GLsizei h )

{

glViewport( 0, 0, w, h );

glMatrixMode( GL_PROJECTION );

glLoadIdentity();

gluPerspective( 60.0, ( GLfloat ) w / ( GLfloat ) h, 0.1, 8.0 );

gluLookAt( eye[0], eye[1], eye[2], at[0], at[1], at[2], up[0], up[1], up[2] );

对投影矩阵进行变换,之所以没有把它放到Display函数里,是因为当我们改变窗口大小的时候,会导致投影矩阵做相应的变换,同样视口也一样

//

// New window size

//

wndHeight = h;

wndWidth = w;

}

GLboolean JudgeSelect( GLint x, GLint y )

{

GLuint selectBUf[ SELECT_BUFF_SIZE ];

GLint hitNum;

GLint viewPort[ 4 ];

//

// Judge if select the cube

//

glGetIntegerv( GL_VIEWPORT, viewPort );

glSelectBuffer( SELECT_BUFF_SIZE ,selectBUf );

glRenderMode( GL_SELECT );

glInitNames();

glPushName( 0 );

glMatrixMode( GL_PROJECTION );

glPushMatrix();

glLoadIdentity();

//

// Set a small area to be hitted

//

gluPickMatrix( ( GLdouble ) x, ( GLdouble ) ( viewPort[ 3 ] - y ), 5.0, 5.0, viewPort );

gluPerspective( 60.0, ( GLfloat ) wndWidth / ( GLfloat ) wndHeight, 0.1, 8.0 );

gluLookAt( eye[0], eye[1], eye[2], at[0], at[1], at[2], up[0], up[1], up[2] );

glMatrixMode( GL_MODELVIEW );

glPushMatrix();

memcpy( selectCopy, lastMatrix, 16 * sizeof( lastMatrix[ 0 ] ) );

glLoadIdentity();

//

// Apply the new transform with the old one

//

glRotatef( angle, axis[ 0 ], axis[ 1 ], axis[ 2 ] );

glMultMatrixf( lastMatrix );

//

// Save as last Matrix

//

glGetFloatv(GL_MODELVIEW_MATRIX, lastMatrix);

glLoadName( CUBE_NAME );

glutSolidCube( CUBE_SIZE );

glPopMatrix();

memcpy( lastMatrix, selectCopy, 16 * sizeof( lastMatrix[ 0 ] ) );

glMatrixMode( GL_PROJECTION );

glPopMatrix();

glFlush( );

hitNum = glRenderMode( GL_RENDER );

if( hitNum <= 0 ) {

return FALSE;

} else {

return TRUE;

}

}

这一部分是用来实现选择的,即如果用户没有选择立方体,那么拖动鼠标就不会旋转,选择的机制这里先略过,因为我还没有彻底搞懂。。。,从结果上看,就是hitNum来记录点击的次数。

void Mouse( GLint button, GLint state, GLint x, GLint y)

{

//

// Record mouse state

//

btnState = (unsigned char) button;

btnState <<= 4;

btnState |= (unsigned char)state;

switch ( button )

{

case GLUT_LEFT_BUTTON:

//

// Initial the lastPos

//

ProjectSphere( x, y, wndHeight, lastPos );

isSelected = JudgeSelect( x, y );

break;

}

}

这个函数也是一个回调函数,用来处理鼠标事件,当点击鼠标之后我们就要准备进行旋转了,但是也不一定,isSelected会告诉我们是否点到了立方体上,是否需要旋转。

void Motion( GLint x, GLint y )

{

GLfloat d, dx, dy, dz;

if( ! isSelected ) {

return;

}

if ( ( ( btnState >> 4 ) & 0x0F) == GLUT_LEFT_BUTTON )

{

if( ! ProjectSphere( x, y, wndHeight, currentPos ) )

{

return;

}

dx = currentPos[ 0 ] - lastPos[ 0 ];

dy = currentPos[ 1 ] - lastPos[ 1 ];

dz = currentPos[ 2 ] - lastPos[ 2 ];

//

// If mouse has moved

//

if (dx || dy || dz)

{

//

// Moving angle

//

d = sqrt( dx * dx + dy * dy + dz * dz );

angle= d * 180.0;

//

// The nomal vector of the move plane

//

axis[0] = lastPos[1] * currentPos[2] - lastPos[2] * currentPos[1];

axis[1] = lastPos[2] * currentPos[0] - lastPos[0] * currentPos[2];

axis[2] = lastPos[0] * currentPos[1] - lastPos[1] * currentPos[0];

//

// Record the old coordinates

//

lastPos[0] = currentPos[0];

lastPos[1] = currentPos[1];

lastPos[2] = currentPos[2];

}

glutPostRedisplay();

}

}

鼠标移动的回调函数,这里我们实现了旋转需要的数学运算,最关键的是记得最后要有glutPostRedisplay()这个函数,它告诉窗体要重绘。

GLint main( GLint argc, char** argv )

{

glutInit( &argc, argv );

glutInitDisplayMode( GLUT_DOUBLE | GLUT_RGB );/* | GLUT_DEPTH );*/

glutInitWindowSize( WIDTH, HEIGHT );

glutInitWindowPosition( 100, 100 );

glutCreateWindow( "BLIZZARD" );

同样是一些初始化的工作,可以都一起扔到Init()函数里面。。。

Init();

glutDisplayFunc( Display );

glutReshapeFunc( Reshape );

glutMouseFunc( Mouse );

glutMotionFunc( Motion );

glutMainLoop();

这里就是对上面所有的回调函数进行绑定。GlutMainLoop则进入消息循环,里面会有一些默认的处理机制,譬如点窗口的X,就会关掉窗口等。。。

return 0;

}

使用glut工具包,确实非常方便,简化了和系统相关的编程部分。。。另外尽量使用GL开头的数据类型,这个是跨平台的问题了。。。

正则表达式 读书笔记 (一)

正则表达式是一种用来匹配和处理文本的字符串,主要就是实现字符串的搜索和替换功能。Firefox里有一个Regular Expression的插件,可以用来做一些简单的正则表达式的练习。

1) 匹配纯文本

2) . 字符可以匹配任何一个单个的字符(包括字符、字母、数字甚至是 . 字符本身)

3) 匹配特殊字符,需要使用转义字符 \

4) 匹配多个字符中的某一个,使用字符集合, 例如 [0-9]

5) 取非匹配,例如 [^0-9]

6) \f 换页符

\n 换行符

\r 回车符

\t 制表符

\v 垂直制表符

\d 任何一个数字字符

\D 任何一个非数字字符

\w 任何一个字母数字字符或者是下划线字符

\W 任何一个非字符数字且非下划线字符

\s 任何一个空白字符

\S 任何一个非空白字符

7) 匹配一个字符一次或多次重复,只需要加上后缀 + 例子:\w+

(未完)

创建一个窗口 -- From GameTutorials, LLC

#include // 必须的头文件
#include

LRESULT CALLBACK WndProc( HWND, UINT, WPARAM, LPARAM );
// LRESULT 是一个32位的数值,对于一个回调函数其返回类型是LRESULT。
// 其定义为:
// typedef LONG_PTR LRESULT;
// 关于回调函数(CALLBACK),它可以和一个窗口类相关联。
//MSDN中的解释是:CALLBACK Calling convention for callback functions.
//
//This type is declared in WinDef.h as follows:
//
//
//#define CALLBACK __stdcall

int WINAPI WinMain( HINSTANCE hInstance, HINSTANCE hPrevInstance,
PSTR szCmdLine, int iCmdShow )
// 同CALLBACK一样 WINAPI 也是 Calling convention for the Win32 API,
// 个人目前无法理解。。。
// WinMain各个参数的解释:
// HINSTANCE hInstance -- 应用程序的实例信息,一个程序可以有两个实例同时运行(开两个QQ)
// HINSTANCE hPrevInstance -- 一般情况下为NULL,没有实际用处。
// PSTR szCmdLine -- 传给应用程序的参数,PSTR就是字符串。
// int iCmdShow -- 保存关于窗口的信息,诸如最大化、最小化等等。
{
HWND hwnd; // HWND -- 窗口句柄,用它来追踪窗口,应用程序可以有多个窗口。
MSG msg; // MSG -- 消息,传递给窗口的。
WNDCLASSEX wndclass; // WNDCLASSEX -- 窗口类,存有窗口的常用信息如图标、标题、大小等。

// 设置窗口属性。
wndclass.cbSize = sizeof( wndclass );
wndclass.style = CS_HREDRAW | CS_VREDRAW;
wndclass.lpfnWndProc = WndProc; // 指定窗口过程,告知窗口去调用哪个回调函数来处理窗口消息。
wndclass.cbClsExtra = 0;
wndclass.cbWndExtra = 0; // 通常都赋值为0
wndclass.hInstance = hInstance; // ?不解啊
// 设置图标和箭头,空值为HINSTANCE类型。
wndclass.hIcon = LoadIcon( NULL, IDI_WINLOGO );
wndclass.hCursor = LoadCursor( NULL, IDC_ARROW );
wndclass.hbrBackground = ( HBRUSH ) GetStockObject( WHITE_BRUSH );
// The GetStockObject function retrieves a handle to one of the stock pens, brushes, fonts, or palettes.
// 返回值是VOID* 因此需要类型转换。
wndclass.lpszMenuName = NULL;
// 为窗口类起的名字,在创建窗口时将会用到。
wndclass.lpszClassName = TEXT( "Window Class" );
wndclass.hIconSm = LoadIcon( NULL, IDI_WINLOGO );

RegisterClassEx( &wndclass ); // 注册窗口类。

hwnd = CreateWindow( TEXT( "Window Class" ),
TEXT( "BLIZZARD Window" ), // 窗口标题。
WS_OVERLAPPEDWINDOW, // 窗口类型。
CW_USEDEFAULT, // 初始X位置。
CW_USEDEFAULT, // 初始Y位置。
CW_USEDEFAULT, // 初始X大小。
CW_USEDEFAULT, // 初始Y大小。
NULL, // 父窗口句柄。
NULL, // 菜单句柄。
hInstance, // 实例句柄。
NULL // 可以利用最后一个参数给WndProc传递变量,当然也可以用全局变量的方式。
);

ShowWindow( hwnd, iCmdShow ); // MSDN中的注释:The ShowWindow function sets the specified window's show state.
// 因此不能从翻译上理解ShowWindow函数调用后会显示窗口。
UpdateWindow( hwnd ); // 绘制窗口,会向窗口发送一个WM_PAINT消息。

// 消息循环。
// 如果收到WM_QUIT消息将返回0。
while( GetMessage( &msg, NULL, 0, 0 ) ) {
TranslateMessage( &msg );
DispatchMessage( &msg ); // 将消息发送给窗口过程。
}

UnregisterClass( TEXT( "Window Class" ), hInstance );

return msg.wParam;
}

LRESULT CALLBACK WndProc( HWND hwnd, UINT iMsg, WPARAM wParam, LPARAM lParam )
{
switch( iMsg ) {
case WM_CREATE : // 创建窗口。
break;
case WM_SIZE : // 改变窗口大小。
break;
case WM_PAINT : // 窗口重绘。
break;
case WM_DESTROY :
PostQuitMessage( 0 ); // 必须调用PostQuitMessage。
break;
}
return DefWindowProc( hwnd, iMsg, wParam, lParam );
// 调用DefWindowProc采用默认方式处理未处理消息。
}



学习WIN 32离不开MSDN,这点深有体会,许多东西必须查!

WIN 32 编程的一般步骤是:

1) 初始化窗口类。

2) 注册窗口类。

3) 创建窗口。

4) 从窗口过程获取消息。

5) 处理消息。

今天装了WINDOWS 7~

WIN7 ultimate装了有1个小时吧,由于是在虚拟机里面装的,即使给了1GB的内存但是感觉还是有点卡,不过比之VISTA,WIN7在内存消耗上少了不少,大概在400MB左右,还有一些新的桌面效果,新的计算器,新的画图。。。很多以前都在牙牙的本上见了,只不过这次是自己去体验一下罢了~总得来说还是很期待的

Unicode

Windows程序一般都有windows.h头文件,而windows.h中包含的winnt.h则用来处理基本的Unicode支持。

而为实现混合试用ASCII和Unicode,主就是试用宏,例如在WINUSER.H中就用这样的代码

#ifdef UNICODE
#define MessageBox MessageBoxW
#else
#define MessageBox MessageBoxA
#endif // !UNICODE



对于字符串常量则采用TEXT()宏

#define TEXT(quote) __TEXT(quote) // r_winnt



在WINNT.H中还定义了新的数据类型:

typedef char CHAR;

typedef wchar_t WCHAR; //wc

如何阅读代码(ZT)

++++++++++++

第一章: 导论

++++++++++++

1.要养成一个习惯, 经常花时间阅读别人编写的高品质代码.

2.要有选择地阅读代码, 同时, 还要有自己的目标. 您是想学习新的模式|编码风格|还是满足某些需求的方法.

3.要注意并重视代码中特殊的非功能性需求, 这些需求也许会导致特殊的实现风格.

4.在现有的代码上工作时, 请与作者和维护人员进行必要的协调, 以避免重复劳动或产生厌恶情绪.

5.请将从开放源码软件中得到的益处看作是一项贷款, 尽可能地寻找各种方式来回报开放源码社团.

6.多数情况下, 如果您想要了解"别人会如何完成这个功能呢?", 除了阅读代码以外, 没有更好的方法.

7.在寻找bug时, 请从问题的表现形式到问题的根源来分析代码. 不要沿着不相关的路径(误入歧途).

8.我们要充分利用调试器|编译器给出的警告或输出的符号代码|系统调用跟踪器|数据库结构化查询语言的日志机制|包转储工具和Windows的消息侦查程序, 定出的bug的位置.

9.对于那些大型且组织良好的系统, 您只需要最低限度地了解它的全部功能, 就能够对它做出修改.

10.当向系统中增加新功能时, 首先的任务就是找到实现类似特性的代码, 将它作为待实现功能的模板.

11.从特性的功能描述到代码的实现, 可以按照字符串消息, 或使用关键词来搜索代码.

12.在移植代码或修改接口时, 您可以通过编译器直接定位出问题涉及的范围, 从而减少代码阅读的工作量.

13.进行重构时, 您从一个能够正常工作的系统开始做起, 希望确保结束时系统能够正常工作. 一套恰当的测试用例(test case)可以帮助您满足此项约束.

14.阅读代码寻找重构机会时, 先从系统的构架开始, 然后逐步细化, 能够获得最大的效益.

15.代码的可重用性是一个很诱人, 但难以理解与分离, 可以试着寻找粒度更大一些的包, 甚至其他代码.

16.在复查软件系统时, 要注意, 系统是由很多部分组成的, 不仅仅只是执行语句. 还要注意分析以下内容: 文件和目录结构|生成和配置过程|用户界面和系统的文档.
18.可以将软件复查作为一个学习|讲授|援之以手和接受帮助的机会.
++++++++++++++++++++

第二章: 基本编程元素

++++++++++++++++++++

19.第一次分析一个程序时, main是一个好的起始点.

20.层叠if-else if-...-else序列可以看作是由互斥选择项组成的选择结构.

21.有时, 要想了解程序在某一方面的功能, 运行它可能比阅读源代码更为恰当.

22.在分析重要的程序时, 最好首先识别出重要的组成部分.

23.了解局部的命名约定, 利用它们来猜测变量和函数的功能用途.

24.当基于猜测修改代码时, 您应该设计能够验证最初假设的过程. 这个过程可能包括用编译器进行检查|引入断言|或者执行适当的测试用例.

25.理解了代码的某一部分, 可能帮助你理解余下的代码.

26.解决困难的代码要从容易的部分入手.

27.要养成遇到库元素就去阅读相关文档的习惯; 这将会增强您阅读和编写代码的能力.

28.代码阅读有许多可选择的策略: 自底向上和自顶向下的分析|应用试探法和检查注释和外部文档, 应该依据问题的需要尝试所有这些方法.

29.for (i=0; i<>n理解为a/k, k=2n.

47.每次只分析一个控制结构, 将它的内容看作是一个黑盒.

48.将每个控制结构的控制表达式看作是它所包含代码的断言.

49.return, goto, break和continue语句, 还有异常, 都会影响结构化的执行流程. 由于这些语句一般都会终止或重新开始正在进行的循环,因此要单独推理它们的行为.

50.用复杂循环的变式和不变式, 对循环进行推理.

51.使用保持含义不变的变换重新安排代码, 简化代码的推理工作.

+++++++++++++++++++

第三章: 高级C数据类型

+++++++++++++++++++

52.了解特定语言构造所服务的功能之后, 就能够更好地理解使用它们的代码.

53.识别并归类使用指针的理由.

54.在C程序中, 指针一般用来构造链式数据结构|动态分配的数据结构|实现引用调用|访问和迭代数据元素|传递数组参数|引用函数|作为其他

值的别名|代表字符串|以及直接访问系统内存.

55.以引用传递的参数可以用来返回函数的结果, 或者避免参数复制带来的开销.

56.指向数组元素地址的指针, 可以访问位于特定索引位置的元素.

57.指向数组元素的指针和相应的数组索引, 作用在二者上的运算具有相同的语义.

58.使用全局或static局部变量的函数大多数情况都不可重入(reentrant).

59.字符指针不同于字符数组.

60.识别和归类应用结构或共用体的每种理由.

61.C语言中的结构将多个数据元素集合在一起, 使得它们可以作为一个整体来使用, 用来从函数中返回多个数据元素|构造链式数据结构|映射数据在硬件设备|网络链接和存储介质上的组织方式|实现抽象数据类型|以及以面向对象的方式编程.

62.共用体在C程序中主要用于优化存储空间的利用|实现多态|以及访问数据不同的内部表达方式.

63.一个指针, 在初始化为指向N个元素的存储空间之后, 就可以作为N个元素的数组来使用.

64.动态分配的内在块可以电焊工地释放, 或在程序结束时释放, 或由垃圾回收器来完成回收; 在栈上分配的内存块当分配它的函数退出后释放.

65.C程序使用typedef声明促进抽象, 并增强代码的易读性, 从而防范可移植性问题, 并模拟C++和Java的类声明行为.

66.可以将typedef声明理解成变量定义: 变量的名称就是类型的名称; 变量的类型就是与该名称对应的类型.

+++++++++++++++

第四章: C数据结构

+++++++++++++++

67.根据底层的抽象数据类型理解显式的数据结构操作.

68.C语言中, 一般使用内建的数组类型实现向量, 不再对底层实现进行抽象.

69.N个元素的数组可以被序列for (i=0; i

70.表达式sizeof(x)总会得到用memset或memcpy处理数组x(不是指针)所需的正确字节数.

71.区间一般用区间内的第一个元素和区间后的第一个元素来表示.

72.不对称区间中元素的数目等于高位边界与低位边界的差.

73.当不对称区间的高位边界等于低位边界时, 区间为空.

74.不对称区间中的低位边界代表区间的第一个元素; 高位边界代表区间外的第一个元素.

75.结构的数组常常表示由记录和字段组成的表.

76.指向结构的指针常常表示访问底层记录和字段的游标.

77.动态分配的矩阵一般存储为指向数组列的指针或指向元素指针的指针; 这两种类型都可以按照二维数组进行访问.

78.以数组形式存储的动态分配矩阵, 用自定义访问函数定位它们的元素.

79.抽象数据类型为底层实现元素的使用(或误用)方式提供一种信心的量度.

80.数组用从0开始的顺序整数为键, 组织查找表.

81.数组经常用来对控制结构进行高效编码, 简化程序的逻辑.

82.通过在数组中每个位置存储一个数据元素和一个函数指针(指向处理数据元素的函数), 可以将代码与数据关联起来.

83.数组可以通过存储供程序内的抽象机(abstract machine)或虚拟机(virtual machine)使用的数据或代码, 控制程序的运作.

84.可以将表达式sizeof(x) / sizeof(x[0])理解为数组x中元素的个数.

85.如果结构中含有指向结构自身|名为next的元素, 一般说来, 该结构定义的是单向链表的结点.

86.指向链表结点的持久性(如全局|静态或在堆上分配)指针常常表示链表的头部.

87.包含指向自身的next和prev指针的结构可能是双向链表的结点.

88.理解复杂数据结构的指针操作可以将数据元素画为方框|指针画为箭头.

89.递归数据结构经常用递归算法来处理.

90.重要的数据结构操作算法一般用函数参数或模板参数来参数化.

91.图的结点常常顺序地存储在数组中, 链接到链表中, 或通过图的边链接起来.

92.图中的边一般不是隐式地通过指针, 就是显式地作为独立的结构来表示.

93.图的边经常存储为动态分配的数组或链表, 在这两种情况下, 边都锚定在图的结点上.

94.在无向图中, 表达数据时应该将所有的结点看作是等同的, 类似地, 进行处理任务的代码也不应该基于它们的方向来区分边.

95.在非连通图中, 执行遍历代码应该能够接通孤立的子图.

96.处理包含回路的图时, 遍历代码应该避免在处理图的回路进入循环.

97.复杂的图结构中, 可能隐藏着其他类型的独立结构.

+++++++++++++++++

第五章: 高级控制流程

+++++++++++++++++

98.采用递归定义的算法和数据结构经常用递归的函数定义来实现.

99.推理递归函数时, 要从基准落伍测试开始, 并认证每次递归调用如何逐渐接近非递归基准范例代码.

100.简单的语言常常使用一系列遵循该语言语法结构的函数进行语法分析.

101.推理互递归函数时, 要基于底层概念的递归定义.

102.尾递归调用等同于一个回到函数开始处的循环.

103.将throws子句从方法的定义中移除, 然后运行Java编译器对类的源代码进行编译, 就可以容易地找到那些可能隐式地生成异常的方法.

104.在多处理器计算机上运行的代码常常围绕进程或线程进行组织.

105.工作群并行模型用于在多个处理器间分配工作, 或者创建一个任务池, 然后将大量需要处理标准化的工作进行分配.

106.基于线程的管理者/工人并行模型一般将耗时的或阻塞的操作分配给工人子任务, 从而维护中心任务的响应性.

107.基于进程的管理者/工人并行模型一般用来重用现有的程序, 或用定义良好的接口组织和分离粗粒度的系统模块.

108.基于流水线的并行处理中, 每个任务都接收到一些输入, 对它们进行一些处理, 并将生成的输出传递给下一个任务, 进行不同的处理.

109.竞争条件很难捉摸, 相关的代码常常会将竞争条件扩散到多个函数或模块; 因而, 很难隔离由于竞争条件导致的问题.

110.对于出现在信号处理器中的数据结构操作代码和库调用要保持高度警惕.

111.在阅读包含宏的代码时, 要注意, 宏既非函数, 也非语句.

112.do…while(0)块中的宏等同于控制块中的语句.

113.宏可以访问在它的使用点可见的所有局部变量.

114.宏调用可改变参数的值
115.基于宏的标记拼接能够创建新的标记符.

+++++++++++++++++

第六章: 应对大型项目

+++++++++++++++++

116.我们可以通过浏览项目的源代码树—包含项目源代码的层次目录结构, 来分析一个项目的组织方式. 源码树常常能够反映出项目在构架和软件过程上的结构.

117.应用程序的源代码树经常是该应用程序的部署结构的镜像.

118.不要被庞大的源代码集合吓倒; 它们一般比小型的专门项目组织得更出色.

119.当您首次接触一个大型项目时, 要花一些时间来熟悉项目的目录树结构.

120.项目的源代码远不只是编译后可以获得可执行程序的计算机语言指令; 一个项目的源码树一般还包括规格说明|最终用户和开发人员文档|测试脚本|多媒体资源|编译工具|例子|本地化文件|修订历史|安装过程和许可信息.

121.大型项目的编译过程一般声明性地借助依赖关系来说明. 依赖关系由工具程序, 如make及其派生程序, 转换成具体的编译行动.

122.大型项目中, 制作文件常常由配置步骤动态地生成; 在分析制作文件之前, 需要先执行项目特定的配置.

123.检查大型编译过程的各个步骤时, 可以使用make程序的-n开关进行预演.

124.修订控制系统提供从储存库中获取源代码最新版本的方式.

125.可以使用相关的命令, 显示可执行文件中的修订标识关键字, 从而将可执行文件与它的源代码匹配起来.

126.使用修订日志中出现的bug跟踪系统内的编号, 可以在bug跟踪系统的数据库中找到有关的问题的说明.

127.可以使用修订控制系统的版本储存库, 找出特定的变更是如何实现的.

128.定制编译工具用在软件开发过程的许多方面, 包括配置|编译过程管理|代码的生成|测试和文档编制.

129.程序的调试输出可以帮助我们理解程序控制流程和数据元素的关键部分.

130.跟踪语句所在的地点一般也是算法运行的重要部分.

131.可以用断言来检验算法运作的步骤|函数接收的参数|程序的控制流程|底层硬件的属性和测试用例的结果.

132.可以使用对算法进行检验的断言来证实您对算法运作的理解, 或将它作为推理的起点.

133.对函数参数和结果的断言经常记录了函数的前置条件和后置条件.

134.我们可以将测试整个函数的断言作为每个给定函数的规格说明.

135.测试用例可以部分地代替函数规格说明.

136.可以使用测试用例的输入数据对源代码序列进行预演.

+++++++++++++++++++

第七章: 编码规范和约定

+++++++++++++++++++

137.了解了给定代码库所遵循的文件组织方式后, 就能更有效率地浏览它的源代码.

138.阅读代码时, 首先要确保您的编辑器或优美打印程序的tab设置, 与代码遵循的风格规范一致.

139.可以使用代码块的缩进, 快速地掌握代码的总体结构.

140.对编排不一致的代码, 应该立即给予足够的警惕.

141.分析代码时, 对标记为XXX, FIXME和TODO的代码序列要格外注意: 错误可能就潜伏在其中.

142.常量使用大写字母命名, 单词用下划线分隔.

143.在遵循Java编码规范的程序中, 包名(package name)总是从一个顶级的域名开始(例如, org, com), 类名和接口名由大写字母开始, 方法和变量名由小写字母开始.

144.用户界面控件名称之前的匈牙利记法的前缀类型标记可以帮助我们确定它的作用.

145.不同的编程规范对可移植构造的构成有不同的主张.

146.在审查代码的可移植性, 或以某种给定的编码规范作为指南时, 要注意了解规范对可移植性需求的界定与限制.

147.如果GUI功能都使用相应的编程结构来实现, 则通过代码审查可以轻易地验证给定用户界面的规格说明是否被正确地采用.

148.了解项目编译过程的组织方式与自动化方式之后, 我们就能够快速地阅读与理解对应的编译规则.

149.当检查系统的发布过程时, 常常可以将相应发行格式的需求作为基准.

++++++++++++

第八章: 文档

++++++++++++

150.阅读代码时, 应该尽可能地利用任何能够得到的文档.

151.阅读一小时代码所得到的信息只不过相当于阅读一分钟文档.

152.使用系统的规格说明文档, 了解所阅读代码的运行环境.

153.软件需求规格说明是阅读和评估代码的基准.

154.可以将系统的设计规格说明作为认知代码结构的路线图, 阅读具体代码的指引.

155.测试规格说明文档为我们提供可以用来对代码进行预演的数据.

156.在接触一个未知系统时, 功能性的描述和用户指南可以提供重要的背景信息,从而更好地理解阅读的代码所处的上下文.

157.从用户参考手册中, 我们可以快速地获取, 应用程序在外观与逻辑上的背景知识, 从管理员手册中可以得知代码的接口|文件格式和错误消息的详细信息.

158.利用文档可以快捷地获取系统的概况, 了解提供特定特性的代码.

159.文档经常能够反映和提示出系统的底层结构.

160.文档有助于理解复杂的算法和数据结构.

161.算法的文字描述能够使不透明(晦涩, 难以理解)的代码变得可以理解.

162.文档常常能够阐明源代码中标识符的含义.

163.文档能够提供非功能性需求背后的理论基础.

164.文档还会说明内部编程接口.

165.由于文档很少像实际的程序代码那样进行测试, 并受人关注, 所以它常常可能存在错误|不完整或过时.

166.文档也提供测试用例, 以及实际应用的例子.

167.文档常常还会包括已知的实现问题或bug.

168.环境中已知的缺点一般都会记录在源代码中.

169.文档的变更能够标出那些故障点.

170.对同一段源代码重复或互相冲突的更改, 常常表示存在根本性的设计缺陷, 从而使得维护人员需要用一系列的修补程序来修复.

171.相似的修复应用到源代码的不同部分, 常常表示一种易犯的错误或疏忽, 它们同样可能会在其他地方存在.

172.文档常常会提供不恰当的信息, 误导我们对源代码的理解.

173.要警惕那些未归档的特性: 将每个实例归类为合理|疏忽或有害, 相应地决定是否应该修复代码或文档.

174.有时, 文档在描述系统时, 并非按照已完成的实现, 而是系统应该的样子或将来的实现.

175.在源代码文档中, 单词gork的意思一般是指”理解”.

176.如果未知的或特殊用法的单词阻碍了对代码的理解, 可以试着在文档的术语表(如果存在的话)|New Hacker’s Dictionary[Ray96]|或在Web搜索引擎中查找它们.

177.总是要以批判的态度来看待文档, 注意非传统的来源, 比如注释|标准|出版物|测试用例|邮件列表|新闻组|修订日志|问题跟踪数据库|营销材料|源代码本身.

178.总是要以批判的态度来看待文档; 由于文档永远不会执行, 对文档的测试和正式复查也很少达到对代码的同样水平, 所以文档常常会误导读者, 或者完全错误.

179.对于那些有缺陷的代码, 我们可以从中推断出它的真实意图.

180.在阅读大型系统的文档时, 首先要熟悉文档的总体结构和约定.

181.在对付体积庞大的文档时, 可以使用工具, 或将文本输出到高品质输出设备上, 比如激光打印机, 来提高阅读的效率.

++++++++++++++

第九章: 系统构架

++++++++++++++

182.一个系统可以(在重大的系统中也确实如此)同时出多种不同的构架类型. 以不同的方式检查同一系统|分析系统的不同部分|或使用不同级别的分解, 都有可能发现不同的构架类型.

183.协同式的应用程序, 或者需要协同访问共享信息或资源的半自治进程, 一般会采用集中式储存库构架.

184.黑板系统使用集中式的储存库, 存储非结构化的键/值对, 作为大量不同代码元件之间的通信集线器.

185.当处理过程可以建模|设计和实现成一系列的数据变换时, 常常会使用数据流(或管道—过滤器)构架.

186.在批量进行自动数据处理的环境中, 经常会采用数据流构架, 在对数据工具提供大量支持的平台上尤其如此.

187.数据流构架的一个明显征兆是: 程序中使用临时文件或流水线(pipeline)在不同进程间进行通信.

188.使用图示来建模面向对象构架中类的关系.

189.可以将源代码输入到建模工具中, 逆向推导出系统的构架.

190.拥有大量同级子系统的系统, 常常按照分层构架进行组织.

191.分层构架一般通过堆叠拥有标准化接口的软件组件来实现.

192.系统中每个层可以将下面的层看作抽象实体, 并且(只要该层满足它的需求说明)不关心上面的层如何使用它.

193.层的接口既可以是支持特定概念的互补函数族, 也可以是一系列支持同一抽象接口不同底层实现的可互换函数.

194.用C语言实现的系统, 常常用函数指针的数组, 表达层接口的多路复用操作.

195.用面向对象的语言实现的系统, 使用虚方法调用直接表达对层接口的多嘴复用操作.

196.系统可以使用不同的|独特的层次分解模型跨各种坐标轴进行组织.

197.使用程序切片技术, 可以将程序中的数据和控制之间依赖关系集中到一起.

198.在并发系统中, 一个单独的系统组件起到集中式管理器的作用, 负责启动|停止和协调其他系统进程和任务的执行.

199.许多现实的系统都会博采众家之长. 当处理此类系统时, 不要徒劳地寻找无所不包的构架图; 应该将不同构架风格作为独立但相关的实体

来进行定位|识别并了解.

200.状态变迁图常常有助于理清状态机的动作.

201.在处理大量的代码时, 了解将代码分解成单独单元的机制极为重要.

202.大多数情况下, 模块的物理边界是单个文件|组织到一个目录中的多个文件或拥有统一前缀的文件的集合.

203.C中的模块, 由提供模块公开接口的头文件和提供对应实现的源文件组成.

204.对象的构造函数经常用来分配与对象相关的资源, 并初始化对象的状态. 函数一般用来释放对象在生命期中占用的资源.

205.对象方法经常使用类字段来存储控制所有方法运作的数据(比如查找表或字典)或维护类运作的状态信息(例如, 赋给每个对象一个标识符的

计数器).

206.在设计良好的类中, 所有的字段都应在声明为private, 并用公开的访问方法提供对它们的访问.

207.在遇到friend声明时, 要停下来分析一下, 看看绕过类封装在设计上的理由.

208.可以有节制地用运算符增强特定类的可用性, 但用运算符重载, 将类实现为拥有内建算术类型相关的全部功能的类实体, 是不恰当的.

209.泛型实现不是在编译期间通过宏替换或语言所支持的功能(比如C++模板和Ada的泛型包)来实现, 就是在运行期间通过使用数据元素的指针和函数的指针|或对象的多态性实现.

210.抽象数据类型经常用来封装常用的数据组织方案(比如树|列表或栈), 或者对用户隐藏数据类型的实现细节.

211.使用库的目的多种多样: 重用源代码或目标代码, 组织模块集合, 组织和优化编译过程, 或是用来实现应用程序各种特性的按需载入.

212.大型的|分布式的系统经常实现为许多互相协作的进程.

213.对于基于文本的数据储存库, 可以通过浏览存储在其中的数据, 破译出它的结构.

214.可以通过查询数据字典中的表, 或使用数据库专有的SQL命令, 比如show table, 来分析关系型数据库的模式.

215.识别出重用的构架元素后, 可以查找其最初的描述, 了解正确地使用这种构架的方式, 以及可能出现的误用.

216.要详细分析建立在某种框架之上的应用程序, 行动的最佳路线就是从研究框架自身开始.

217.在阅读向导生成的代码时, 不要期望太高, 否则您会感到失望.

218.学习几个基本的设计模式之后, 您会发现, 您查看代码构架的方式会发生改变: 您的视野和词汇将会扩展到能够识别和描述许多通用的形式.

219.频繁使用的一些模式, 但并不显式地指出它们的名称, 这是由于构架性设计的重用经常先于模式的形成.

220.请试着按照底层模式来理解构架, 即使代码中并没有明确地提及模式.

221.大多数解释器都遵循类似的处理构架, 围绕一个状态机进行构建, 状态机的操作依赖于解释器的当前状态|程序指令和程序状态.
222.多数情况下, 参考构架只是为应用程序域指定一种概念性的结构, 具体的实现并非必须遵照这种结构.

+++++++++++++++++

第十章: 代码阅读工具

+++++++++++++++++

223.词汇工具可以高效地在一个大代码文件中或者跨多个文件查找某种模式.

224.使用程序编辑器和正则表达式查找命令, 浏览庞大的源代码文件.

225.以只读方式浏览源代码文件.

226.使用正则表达式 ^function name 可以找出函数的定义.

227.使用正则表达式的字符类, 可以查找名称遵循特定模式的变量.

228.使用正则表达式的否定字符类, 可以避免非积极匹配.

229.使用正则表达式 symbol-1. *symbol-2, 可以查找出现在同一行的符号.

230.使用编辑器的 tags 功能, 可以快速地找出实体的定义.

231.可以用特定的 tag 创建工具, 增加编辑器的浏览功能.

232.使用编辑器的大纲视图, 可以获得源代码结构的鸟瞰图.

233.使用您的编辑器来检测源代码中圆括号|方括号和花括号的匹配.

234.使用 grep 跨多个文件查找代码模式.

235.使用 grep 定位符号的声明|定义和应用.

236.当您不能精确地表述要查找的内容时, 请使用关键单词的词干对程序的源代码进行查找.

237.用 grep 过滤其他工具生成的输出, 分离出您要查找的项.

238.将 grep 的输出输送到其他工具, 使复杂处理任务自动化.

239.通过对 grep 的输出进行流编辑, 重用代码查找的结果.

240.通过选取与噪音模式不匹配的输出行(grep-v), 过滤虚假的 grep 输出.

241.使用 fgrep 在源代码中查找字符串列表.

242.查找注释, 或标识符大小写不敏感的语言编写的代码时, 要使用大小写不敏感的模式匹配(grep -i).

243.使用 grep –n 命令行开关, 可以创建与给定正则表达式匹配的文件和行号的检查表.

244.可以使用 diff 比较文件或程序不同版本之间的差别.

245.在运行 diff 命令时, 可以使用 diff –b, 使文件比较算法忽略结尾的空格, 用 –w 忽略所有空白区域的差异, 用 –i 使文件比较对大小写不敏感.

246.不要对创建自己的代码阅读工具心存畏惧.

247.在构建自己的代码阅读工具时: 要充分利用现代快速原型语言所提供的能力; 从简单开始, 根据需要逐渐改进; 使用利用代码词汇结构的各种试探法; 要允许一些输出噪音或寂静(无关输出或缺失输出); 使用其他工具对输入进行预处理, 或者对输出进行后期处理.

248.要使编译器成为您的: 指定恰当级别的编译器警告, 并小心地评估生成的结果.

249.使用C预处理器理清那些滥用预处理器特性的程序.

250.要彻底地了解编译器如何处理特定的代码块, 需要查看生成的符号(汇编)代码.

251.通过分析相应目标文件中的符号, 可以清晰地了解源文件的输入和输出.

252.使用源代码浏览器浏览大型的代码集合以及对象类型.

253.要抵制住按照您的编码规范对外部代码进行美化的诱惑; 不必要的编排更改会创建不同的代码, 并妨碍工作的组织.

254.优美打印程序和编辑器语法着色可以使得程序的源代码为易读.

255.cdecl 程序可以将难以理解的C和C++类型声明转换成纯英语(反之亦然).

256.实际运行程序, 往往可以更深刻地理解程序的动作.

257.系统调用|事件和数据包跟踪程序可以增进对程序动作的理解.

258.执行剖析器可以找出需要着重优化的代码, 验证输入数据的覆盖性, 以及分析算法的动作.

259.通过检查从未执行的代码行, 可以找出测试覆盖的弱点, 并据此修正测试数据.

260.要探究程序动态动作时的每个细节, 需要在调试器中运作它.

261.将您觉得难以理解的代码打印到纸上.

262.可以绘制图示来描绘代码的动作.

263.可以试着向别人介绍您在阅读的代码, 这样做一般会增进您对代码的理解.

264.理解复杂的算法或巧妙的数据结构, 要选择一个安静的环境, 然后聚精会神地考虑, 不要借助于任何计算机化或自动化的帮助.

+++++++++++++++++++++

第十一章: 一个完整的例子

+++++++++++++++++++++

265.模仿软件的功能时, 要依照相似实体的线路(类|函数|模块). 在相似的现有实体中, 为简化对源代码库的文本查找, 应选取比较罕见的名称.

266.自动生成的文件常常会在文件的开关有一段注释, 说明这种情况.

267.如果试图精确地分析代码, 一般会陷入数量众多的类|文件和模块中, 这些内容会很快将我们淹没; 因此, 我们必须将需要理解的代码限定在绝对必需的范围之内.

268.采用一种广度优先查找策略, 从多方攻克代码阅读中存在的问题, 进到找出克服它们的方法为止.