|
|
用户名:cjhacker 笔名:cjhacker 地区: 北京-丰台 行业:其他 |
| 日 | 一 | 二 | 三 | 四 | 五 | 六 |
[The Spirit Of Hacker]向往自由,乐于探索.........
C编程规范
(作者置顶)
C编程规范
感谢为编程规范作出辛勤劳动的作者!
本规范总则的内容包括:排版、注释、标识符命名、变量使用、代码可测性、程序效率、质量保证、代码编译、单元测试、程序版本与维护等。
本规范总则的示例都以C语言为背景,采用以下的术语描述:
★ 规则:编程时强制必须遵守的原则。
★ 建议:编程时必须加以考虑的原则。
★ 说明:对此规则或建议进行必要的解释。
★ 示例:对此规则或建议从正、反两个方面给出例子。
目 录
1 排版
2 注释
3 标识符命名
4 可读性
5 变量、结构
6 函数、过程
7 可测性
8 程序效率
9 质量保证
10 代码编辑、编译、审查
11 代码测试、维护
12 宏
1 排版
¹ 1-1:程序块要采用缩进风格编写,缩进的空格数为4个。
说明:对于由开发工具自动生成的代码可以有不一致。
¹ 1-2:相对独立的程序块之间、变量说明之后必须加空行。
示例:如下例子不符合规范。
if (!valid_ni(ni))
{
... // program code
}
repssn_ind = ssn_data[index].repssn_index;
repssn_ni = ssn_data[index].ni;
应如下书写
if (!valid_ni(ni))
{
... // program code
}
repssn_ind = ssn_data[index].repssn_index;
repssn_ni = ssn_data[index].ni;
¹ 1-3:较长的语句(>80字符)要分成多行书写,长表达式要在低优先级操作符处划分新行,操作符放在新行之首,划分出的新行要进行适当的缩进,使排版整齐,语句可读。
示例:
perm_count_msg.head.len = NO7_TO_STAT_PERM_COUNT_LEN
+ STAT_SIZE_PER_FRAM * sizeof( _UL );
act_task_table[frame_id * STAT_TASK_CHECK_NUMBER + index].occupied
= stat_poi[index].occupied;
act_task_table[taskno].duration_true_or_false
= SYS_get_sccp_statistic_state( stat_item );
report_or_not_flag = ((taskno < MAX_ACT_TASK_NUMBER)
&& (n7stat_stat_item_valid (stat_item))
&& (act_task_table[taskno].result_data != 0));
¹ 1-4:循环、判断等语句中若有较长的表达式或语句,则要进行适应的划分,长表达式要在低优先级操作符处划分新行,操作符放在新行之首。
示例:
if ((taskno < max_act_task_number)
&& (n7stat_stat_item_valid (stat_item)))
{
... // program code
}
for (i = 0, j = 0; (i < BufferKeyword[word_index].word_length)
&& (j < NewKeyword.word_length); i++, j++)
{
... // program code
}
for (i = 0, j = 0;
(i < first_word_length) && (j < second_word_length);
i++, j++)
{
... // program code
}
¹ 1-5:若函数或过程中的参数较长,则要进行适当的划分。
示例:
n7stat_str_compare((BYTE *) & stat_object,
(BYTE *) & (act_task_table[taskno].stat_object),
sizeof (_STAT_OBJECT));
n7stat_flash_act_duration( stat_item, frame_id *STAT_TASK_CHECK_NUMBER
+ index, stat_object );
¹ 1-6:不允许把多个短语句写在一行中,即一行只写一条语句。
示例:如下例子不符合规范。
rect.length = 0; rect.width = 0;
应如下书写
rect.length = 0;
rect.width = 0;
¹ 1-7:if、for、do、while、case、switch、default等语句自占一行,且if、for、do、while等语句的执行语句部分无论多少都要加括号{}。
示例:如下例子不符合规范。
if (pUserCR == NULL) return;
应如下书写:
if (pUserCR == NULL)
{
return;
}
¹ 1-8:对齐只使用空格键,不使用TAB键。
说明:以免用不同的编辑器阅读程序时,因TAB键所设置的空格数目不同而造成程序布局不整齐,不要使用BC作为编辑器合版本,因为BC会自动将8个空格变为一个TAB键,因此使用BC合入的版本大多会将缩进变乱。
¹ 1-9:函数或过程的开始、结构的定义及循环、判断等语句中的代码都要采用缩进风格,case语句下的情况处理语句也要遵从语句缩进要求。
¹ 1-10:程序块的分界符(如C/C++语言的大括号‘{’和‘}’)应各独占一行并且位于同一列,同时与引用它们的语句左对齐。在函数体的开始、类的定义、结构的定义、枚举的定义以及if、for、do、while、switch、case语句中的程序都要采用如上的缩进方式。
示例:如下例子不符合规范。
for (...) {
... // program code
}
if (...)
{
... // program code
}
void example_fun( void )
{
... // program code
}
应如下书写。
for (...)
{
... // program code
}
if (...)
{
... // program code
}
void example_fun( void )
{
... // program code
}
¹ 1-11:在两个以上的关键字、变量、常量进行对等操作时,它们之间的操作符之前、之后或者前后要加空格;进行非对等操作时,如果是关系密切的立即操作符(如->),后不应加空格。
说明:采用这种松散方式编写代码的目的是使代码更加清晰。
由于留空格所产生的清晰性是相对的,所以,在已经非常清晰的语句中没有必要再留空格,如果语句已足够清晰则括号内侧(即左括号后面和右号前面)不需要加空格,多重括号间不必加空格,因为在C/C++语言中括号已经是最清晰的标志了。
在长语句中,如果需要加的空格非常多,那么应该保持整体清晰,而在局部不加空格。给操作符留空格时不要连续留两个以上空格。
示例:
(1) 逗号、分号只在后面加空格。
int a, b, c;
(2)比较操作符, 赋值操作符"="、 "+=",算术操作符"+"、"%",逻辑操作符"&&"、"&",位域操作符"<<"、"^"等双目操作符的前后加空格。
if (current_time >= MAX_TIME_VALUE)
a = b + c;
a *= 2;
a = b ^ 2;
(3)"!"、"~"、"++"、"--"、"&"(地址运算符)等单目操作符前后不加空格。
*p = 'a'; // 内容操作"*"与内容之间
flag = !isEmpty; // 非操作"!"与内容之间
p = &mem; // 地址操作"&" 与内容之间
i++; // "++","--"与内容之间
(4)"->"、"."前后不加空格。
p->id = pid; // "->"指针前后不加空格
(5) if、for、while、switch等与后面的括号间应加空格,使if等关键字更为突出、明显。
if (a >= b && c > d)
½ 1-1:一行程序以小于80字符为宜,不要写得过长。
2 注释
¹ 2-1:一般情况下,源程序有效注释量必须在20%以上。
说明:注释的原则是有助于对程序的阅读理解,在该加的地方都加了,注释不宜太多也不能太少,注释语言必须准确、易懂、简洁。
¹ 2-2:说明性文件(如头文件.h文件、.inc文件、.def文件、编译说明文件.cfg等)头部应进行注释,注释必须列出:版权说明、版本号、生成日期、作者、内容、功能、与其它文件的关系、修改日志等,头文件的注释中还应有函数功能简要说明。
示例:下面这段头文件的头注释比较标准,当然,并不局限于此格式,但上述信息建议要包含在内。
/*************************************************
Copyright (C), 1988-1999, Huawei Tech. Co., Ltd.
File name: // 文件名
Author: Version: Date: // 作者、版本及完成日期
Description: // 用于详细说明此程序文件完成的主要功能,与其他模块
// 或函数的接口,输出值、取值范围、含义及参数间的控
// 制、顺序、独立或依赖等关系
Others: // 其它内容的说明
Function List: // 主要函数列表,每条记录应包括函数名及功能简要说明
1. ....
History: // 修改历史记录列表,每条修改记录应包括修改日期、修改
// 者及修改内容简述
1. Date:
Author:
Modification:
2. ...
*************************************************/
¹ 2-3:源文件头部应进行注释,列出:版权说明、版本号、生成日期、作者、模块目的/功能、主要函数及其功能、修改日志等。
示例:下面这段源文件的头注释比较标准,当然,并不局限于此格式,但上述信息建议要包含在内。
/************************************************************
Copyright (C), 1988-1999, Huawei Tech. Co., Ltd.
FileName: test.cpp
Author: Version : Date:
Description: // 模块描述
Version: // 版本信息
Function List: // 主要函数及其功能
1. -------
History: // 历史修改记录
<author> <time> <version > <desc>
David 96/10/12 1.0 build this moudle
***********************************************************/
说明:Description一项描述本文件的内容、功能、内部各部分之间的关系及本文件与其它文件关系等。History是修改历史记录列表,每条修改记录应包括修改日期、修改者及修改内容简述。
¹ 2-4:函数头部应进行注释,列出:函数的目的/功能、输入参数、输出参数、返回值、调用关系(函数、表)等。
示例:下面这段函数的注释比较标准,当然,并不局限于此格式,但上述信息建议要包含在内。
/*************************************************
Function: // 函数名称
Description: // 函数功能、性能等的描述
Calls: // 被本函数调用的函数清单
Called By: // 调用本函数的函数清单
Table Accessed: // 被访问的表(此项仅对于牵扯到数据库操作的程序)
Table Updated: // 被修改的表(此项仅对于牵扯到数据库操作的程序)
Input: // 输入参数说明,包括每个参数的作
// 用、取值说明及参数间关系。
Output: // 对输出参数的说明。
Return: // 函数返回值的说明
Others: // 其它说明
*************************************************/
¹ 2-5:边写代码边注释,修改代码同时修改相应的注释,以保证注释与代码的一致性。不再有用的注释要删除。
¹ 2-6:注释的内容要清楚、明了,含义准确,防止注释二义性。
说明:错误的注释不但无益反而有害。
规则2-7:避免在注释中使用缩写,特别是非常用缩写。
说明:在使用缩写时或之前,应对缩写进行必要的说明。
¹ 2-8:注释应与其描述的代码相近,对代码的注释应放在其上方或右方(对单条语句的注释)相邻位置,不可放在下面,如放于上方则需与其上面的代码用空行隔开。
示例:如下例子不符合规范。
例1:
/* get replicate sub system index and net indicator */
repssn_ind = ssn_data[index].repssn_index;
repssn_ni = ssn_data[index].ni;
例2:
repssn_ind = ssn_data[index].repssn_index;
repssn_ni = ssn_data[index].ni;
/* get replicate sub system index and net indicator */
应如下书写
/* get replicate sub system index and net indicator */
repssn_ind = ssn_data[index].repssn_index;
repssn_ni = ssn_data[index].ni;
¹ 2-9:对于所有有物理含义的变量、常量,如果其命名不是充分自注释的,在声明时都必须加以注释,说明其物理含义。变量、常量、宏的注释应放在其上方相邻位置或右方。
示例:
/* active statistic task number */
#define MAX_ACT_TASK_NUMBER 1000
#define MAX_ACT_TASK_NUMBER 1000 /* active statistic task number */
¹ 2-10:数据结构声明(包括数组、结构、类、枚举等),如果其命名不是充分自注释的,必须加以注释。对数据结构的注释应放在其上方相邻位置,不可放在下面;对结构中的每个域的注释放在此域的右方。
示例:可按如下形式说明枚举/数据/联合结构。
/* sccp interface with sccp user primitive message name */
enum SCCP_USER_PRIMITIVE
{
N_UNITDATA_IND, /* sccp notify sccp user unit data come */
N_NOTICE_IND, /* sccp notify user the No.7 network can not */
/* transmission this message */
N_UNITDATA_REQ, /* sccp user's unit data transmission request*/
};
¹ 2-11:全局变量要有较详细的注释,包括对其功能、取值范围、哪些函数或过程存取它以及存取时注意事项等的说明。
示例:
/* The ErrorCode when SCCP translate */
/* Global Title failure, as follows */ // 变量作用、含义
/* 0 - SUCCESS 1 - GT Table error */
/* 2 - GT error Others - no use */ // 变量取值范围
/* only function SCCPTranslate() in */
/* this modual can modify it, and other */
/* module can visit it through call */
/* the function GetGTTransErrorCode() */ // 使用方法
BYTE g_GTTranErrorCode;
¹ 2-12:注释与所描述内容进行同样的缩排。
说明:可使程序排版整齐,并方便注释的阅读与解。
示例:如下例子,排版不整齐,阅读稍感不方便。
void example_fun( void )
{
/* code one comments */
CodeBlock One
/* code two comments */
CodeBlock Two
}
应改为如下布局。
void example_fun( void )
{
/* code one comments */
CodeBlock One
/* code two comments */
CodeBlock Two
}
¹ 2-13:将注释与其上面的代码用空行隔开。
示例:如下例子,显得代码过于紧凑。
/* code one comments */
program code one
/* code two comments */
program code two
应如下书写
/* code one comments */
program code one
/* code two comments */
program code two
¹ 2-14:对变量的定义和分支语句(条件分支、循环语句等)必须编写注释。
说明:这些语句往往是程序实现某一特定功能的关键,对于维护人员来说,良好的注释帮助更好的理解程序,有时甚至优于看设计文档。
¹ 2-15:对于switch语句下的case语句,如果因为特殊情况需要处理完一个case后进入下一个case处理,必须在该case语句处理完、下一个case语句前加上明确的注释。
说明:这样比较清楚程序编写者的意图,有效防止无故遗漏break语句。
示例(注意斜体加粗部分):
case CMD_UP:
ProcessUp();
break;
case CMD_DOWN:
ProcessDown();
break;
case CMD_FWD:
ProcessFwd();
if (...)
{
...
break;
}
else
{
ProcessCFW_B(); // now jump into case CMD_A
}
case CMD_A:
ProcessA();
break;
case CMD_B:
ProcessB();
break;
case CMD_C:
ProcessC();
break;
case CMD_D:
ProcessD();
break;
...
½ 2-1:避免在一行代码或表达式的中间插入注释。
说明:除非必要,不应在代码或表达中间插入注释,否则容易使代码可理解性变差。
½ 2-2:通过对函数或过程、变量、结构等正确的命名以及合理地组织代码的结构,使代码成为自注释的。
说明:清晰准确的函数、变量等的命名,可增加代码可读性,并减少不必要的注释。
½ 2-3:在代码的功能、意图层次上进行注释,提供有用、额外的信息。
说明:注释的目的是解释代码的目的、功能和采用的方法,提供代码以外的信息,帮助读者理解代码,防止没必要的重复注释信息。
示例:如下注释意义不大。
/* if receive_flag is TRUE */
if (receive_flag)
而如下的注释则给出了额外有用的信息。
/* if mtp receive a message from links */
if (receive_flag)
½ 2-4:在程序块的结束行右方加注释标记,以表明某程序块的结束。
说明:当代码段较长,特别是多重嵌套时,这样做可以使代码更清晰,更便于阅读。
示例:参见如下例子。
if (...)
{
// program code
while (index < MAX_INDEX)
{
// program code
} /* end of while (index < MAX_INDEX) */ // 指明该条while语句结束
} /* end of if (...)*/ // 指明是哪条if语句结束
½ 2-5:注释格式尽量统一,建议使用“/* …… */”。
½ 2-6:注释应考虑程序易读及外观排版的因素,使用的语言若是中、英兼有的,建议多使用中文,除非能用非常流利准确的英文表达。
说明:注释语言不统一,影响程序易读性和外观排版,出于对维护人员的考虑,建议使用中文。
3 标识符命名
¹ 3-1:标识符的命名要清晰、明了,有明确含义,同时使用完整的单词或大家基本可以理解的缩写,避免使人产生误解。
说明:较短的单词可通过去掉“元音”形成缩写;较长的单词可取单词的头几个字母形成缩写;一些单词有大家公认的缩写。
示例:如下单词的缩写能够被大家基本认可。
temp 可缩写为 tmp ;
flag 可缩写为 flg ;
statistic 可缩写为 stat ;
increment 可缩写为 inc ;
message 可缩写为 msg ;
¹ 3-2:命名中若使用特殊约定或缩写,则要有注释说明。
说明:应该在源文件的开始之处,对文件中所使用的缩写或约定,特别是特殊的缩写,进行必要的注释说明。
¹ 3-3:自己特有的命名风格,要自始至终保持一致,不可来回变化。
说明:个人的命名风格,在符合所在项目组或产品组的命名规则的前提下,才可使用。(即命名规则中没有规定到的地方才可有个人命名风格)。
¹ 3-4:对于变量命名,禁止取单个字符(如i、j、k...),建议除了要有具体含义外,还能表明其变量类型、数据类型等,但i、j、k作局部循环变量是允许的。
说明:变量,尤其是局部变量,如果用单个字符表示,很容易敲错(如i写成j),而编译时又检查不出来,有可能为了这个小小的错误而花费大量的查错时间。
示例:下面所示的局部变量名的定义方法可以借鉴。
int liv_Width
其变量名解释如下:
l 局部变量(Local) (其它:g 全局变量(Global)...)
i 数据类型(Interger)
v 变量(Variable) (其它:c 常量(Const)...)
Width 变量含义
这样可以防止局部变量与全局变量重名。
¹ 3-5:命名规范必须与所使用的系统风格保持一致,并在同一项目中统一,比如采用UNIX的全小写加下划线的风格或大小写混排的方式,不要使用大小写与下划线混排的方式,用作特殊标识如标识成员变量或全局变量的m_和g_,其后加上大小写混排的方式是允许的。
示例: Add_User不允许,add_user、AddUser、m_AddUser允许。
½ 3-1:除非必要,不要用数字或较奇怪的字符来定义标识符。
示例:如下命名,使人产生疑惑。
#define _EXAMPLE_0_TEST_
#define _EXAMPLE_1_TEST_
void set_sls00( BYTE sls );
应改为有意义的单词命名
#define _EXAMPLE_UNIT_TEST_
#define _EXAMPLE_ASSERT_TEST_
void set_udt_msg_sls( BYTE sls );
½ 3-2:在同一软件产品内,应规划好接口部分标识符(变量、结构、函数及常量)的命名,防止编译、链接时产生冲突。
说明:对接口部分的标识符应该有更严格限制,防止冲突。如可规定接口部分的变量与常量之前加上“模块”标识等。
½ 3-3:用正确的反义词组命名具有互斥意义的变量或相反动作的函数等。
说明:下面是一些在软件中常用的反义词组。
add / remove begin / end create / destroy
insert / delete first / last get / release
increment / decrement put / get
add / delete lock / unlock open / close
min / max old / new start / stop
next / previous source / target show / hide
send / receive source / destination
cut / paste up / down
示例:
int min_sum;
int max_sum;
int add_user( BYTE *user_name );
int delete_user( BYTE *user_name );
½ 3-4:除了编译开关/头文件等特殊应用,应避免使用_EXAMPLE_TEST_之类以下划线开始和结尾的定义。
4 可读性
¹ 4-1:注意运算符的优先级,并用括号明确表达式的操作顺序,避免使用默认优先级。
说明:防止阅读程序时产生误解,防止因默认的优先级与设计思想不符而导致程序出错。
示例:下列语句中的表达式
word = (high << 8) | low (1)
if ((a | b) && (a & c)) (2)
if ((a | b) < (c & d)) (3)
如果书写为
high << 8 | low
a | b && a & c
a | b < c & d
由于
high << 8 | low = ( high << 8) | low,
a | b && a & c = (a | b) && (a & c),
(1)(2)不会出错,但语句不易理解;
a | b < c & d = a | (b < c) & d,(3)造成了判断条件出错。
¹ 4-2:避免使用不易理解的数字,用有意义的标识来替代。涉及物理状态或者含有物理意义的常量,不应直接使用数字,须用有意义的枚举或宏来代替。
示例:如下的程序可读性差。
if (Trunk[index].trunk_state == 0)
{
Trunk[index].trunk_state = 1;
... // program code
}
应改为如下形式。
#define TRUNK_IDLE 0
#define TRUNK_BUSY 1
if (Trunk[index].trunk_state == TRUNK_IDLE)
{
Trunk[index].trunk_state = TRUNK_BUSY;
... // program code
}
½ 4-1:源程序中关系较为紧密的代码应尽可能相邻。
说明:便于程序阅读和查找。
示例:以下代码布局不太合理。
rect.length = 10;
char_poi = str;
rect.width = 5;
若按如下形式书写,可能更清晰一些。
rect.length = 10;
rect.width = 5; // 矩形的长与宽关系较密切,放在一起。
char_poi = str;
½ 4-2:不要使用难懂的技巧性很高的语句,除非很有必要时。
说明:高技巧语句不等于高效率的程序,实际上程序的效率关键在于算法。
示例:如下表达式,考虑不周就可能出问题,也较难理解。
* stat_poi ++ += 1;
* ++ stat_poi += 1;
应分别改为如下。
*stat_poi += 1;
stat_poi++; // 此二语句功能相当于“ * stat_poi ++ += 1; ”
++ stat_poi;
*stat_poi += 1; // 此二语句功能相当于“ * ++ stat_poi += 1; ”
5 变量、结构
¹ 5-1:去掉没必要的公共变量。
说明:公共变量是增大模块间耦合的原因之一,故应减少没必要的公共变量以降低模块间的耦合度。
¹ 5-2:仔细定义并明确公共变量的含义、作用、取值范围及公共变量间的关系。
说明:在对变量声明的同时,应对其含义、作用及取值范围进行注释说明,同时若有必要还应说明与其它变量的关系。
¹ 5-3:明确公共变量与操作此公共变量的函数或过程的关系,如访问、修改及创建等。
说明:明确过程操作变量的关系后,将有利于程序的进一步优化、单元测试、系统联调以及代码维护等。这种关系的说明可在注释或文档中描述。
示例:在源文件中,可按如下注释形式说明。
RELATION System_Init Input_Rec Print_Rec Stat_Score
Student Create Modify Access Access
Score Create Modify Access Access, Modify
注:RELATION为操作关系;System_Init、Input_Rec、Print_Rec、Stat_Score为四个不同的函数;Student、Score为两个全局变量;Create表示创建,Modify表示修改,Access表示访问。
其中,函数Input_Rec、Stat_Score都可修改变量Score,故此变量将引起函数间较大的耦合,并可能增加代码测试、维护的难度。
¹ 5-4:当向公共变量传递数据时,要十分小心,防止赋与不合理的值或越界等现象发生。
说明:对公共变量赋值时,若有必要应进行合法性检查,以提高代码的可靠性、稳定性。
¹ 5-5:防止局部变量与公共变量同名。
说明:若使用了较好的命名规则,那么此问题可自动消除。
¹ 5-6:严禁使用未经初始化的变量作为右值。
说明:特别是在C/C++中引用未经赋值的指针,经常会引起系统崩溃。
½ 5-1:构造仅有一个模块或函数可以修改、创建,而其余有关模块或函数只访问的公共变量,防止多个不同模块或函数都可以修改、创建同一公共变量的现象。
说明:降低公共变量耦合度。
½ 5-2:使用严格形式定义的、可移植的数据类型,尽量不要使用与具体硬件或软件环境关系密切的变量。
说明:使用标准的数据类型,有利于程序的移植。
示例:如下例子(在DOS下BC3.1环境中),在移植时可能产生问题。
void main()
{
register int index; // 寄存器变量
_AX = 0x4000; // _AX是BC3.1提供的寄存器“伪变量”
... // program code
}
½ 5-3:结构的功能要单一,是针对一种事务的抽象。
说明:设计结构时应力争使结构代表一种现实事务的抽象,而不是同时代表多种。结构中的各元素应代表同一事务的不同侧面,而不应把描述没有关系或关系很弱的不同事务的元素放到同一结构中。
示例:如下结构不太清晰、合理。
typedef struct STUDENT_STRU
{
unsigned char name[8]; /* student's name */
unsigned char age; /* student's age */
unsigned char sex; /* student's sex, as follows */
/* 0 - FEMALE; 1 - MALE */
unsigned char
teacher_name[8]; /* the student teacher's name */
unisgned char
teacher_sex; /* his teacher sex */
} STUDENT;
若改为如下,可能更合理些。
typedef struct TEACHER_STRU
{
unsigned char name[8]; /* teacher name */
unisgned char sex; /* teacher sex, as follows */
/* 0 - FEMALE; 1 - MALE */
} TEACHER;
typedef struct STUDENT_STRU
{
unsigned char name[8]; /* student's name */
unsigned char age; /* student's age */
unsigned char sex; /* student's sex, as follows */
/* 0 - FEMALE; 1 - MALE */
unsigned int teacher_ind; /* his teacher index */
} STUDENT;
½ 5-4:不要设计面面俱到、非常灵活的数据结构。
说明:面面俱到、灵活的数据结构反而容易引起误解和操作困难。
½ 5-5:不同结构间的关系不要过于复杂。
说明:若两个结构间关系较复杂、密切,那么应合为一个结构。
示例:如下两个结构的构造不合理。
typedef struct PERSON_ONE_STRU
{
unsigned char name[8];
unsigned char addr[40];
unsigned char sex;
unsigned char city[15];
} PERSON_ONE;
typedef struct PERSON_TWO_STRU
{
unsigned char name[8];
unsigned char age;
unsigned char tel;
} PERSON_TWO;
由于两个结构都是描述同一事物的,那么不如合成一个结构。
typedef struct PERSON_STRU
{
unsigned char name[8];
unsigned char age;
unsigned char sex;
unsigned char addr[40];
unsigned char city[15];
unsigned char tel;
} PERSON;
½ 5-6:结构中元素的个数应适中。若结构中元素个数过多可考虑依据某种原则把元素组成不同的子结构,以减少原结构中元素的个数。
说明:增加结构的可理解性、可操作性和可维护性。
示例:假如认为如上的_PERSON结构元素过多,那么可如下对之划分。
typedef struct PERSON_BASE_INFO_STRU
{
unsigned char name[8];
unsigned char age;
unsigned char sex;
} PERSON_BASE_INFO;
typedef struct PERSON_ADDRESS_STRU
{
unsigned char addr[40];
unsigned char city[15];
unsigned char tel;
} PERSON_ADDRESS;
typedef struct PERSON_STRU
{
PERSON_BASE_INFO person_base;
PERSON_ADDRESS person_addr;
} PERSON;
½ 5-7:仔细设计结构中元素的布局与排列顺序,使结构容易理解、节省占用空间,并减少引起误用现象。
说明:合理排列结构中元素顺序,可节省空间并增加可理解性。
示例:如下结构中的位域排列,将占较大空间,可读性也稍差。
typedef struct EXAMPLE_STRU
{
unsigned int valid: 1;
PERSON person;
unsigned int set_flg: 1;
} EXAMPLE;
若改成如下形式,不仅可节省1字节空间,可读性也变好了。
typedef struct EXAMPLE_STRU
{
unsigned int valid: 1;
unsigned int set_flg: 1;
PERSON person ;
} EXAMPLE;
½ 5-8:结构的设计要尽量考虑向前兼容和以后的版本升级,并为某些未来可能的应用保留余地(如预留一些空间等)。
说明:软件向前兼容的特性,是软件产品是否成功的重要标志之一。如果要想使产品具有较好的前向兼容,那么在产品设计之初就应为以后版本升级保留一定余地,并且在产品升级时必须考虑前一版本的各种特性。
½ 5-9:留心具体语言及编译器处理不同数据类型的则及有关细节。
说明:如在C语言中,static局部变量将在内存“数据区”中生成,而非static局部变量将在“堆栈”中生成。这些细节对程序质量的保证非常重要。
½ 5-10:编程时,要注意数据类型的强制转换。
说明:当进行数据类型强制转换时,其数据的意义、转换后的取值等都有可能发生变化,而这些细节若考虑不周,就很有可能留下隐患。
½ 5-11:对编译系统默认的数据类型转换,也要有充分的认识。
示例:如下赋值,多数编译器不产生告警,但值的含义还是稍有变化。
char chr;
unsigned short int exam;
chr = -1;
exam = chr; // 编译器不产生告警,此时exam为0xFFFF。
½ 5-12:尽量减少没有必要的数据类型默认转换与强制转换。
½ 5-13:合理地设计数据并使用自定义数据类型,避免数据间进行不必要的类型转换。
½ 5-14:对自定义数据类型进行恰当命名,使它成为自描述性的,以提高代码可读性。注意其命名方式在同一产品中的统一。
说明:使用自定义类型,可以弥补编程语言提供类型少、信息量不足的缺点,并能使程序清晰、简洁。
示例:可参考如下方式声明自定义数据类型。
下面的声明可使数据类型的使用简洁、明了。
typedef unsigned char BYTE;
typedef unsigned short WORD;
typedef unsigned int DWORD;
下面的声明可使数据类型具有更丰富的含义。
typedef float DISTANCE;
typedef float SCORE;
½ 5-15:当声明用于分布式环境或不同CPU间通信环境的数据结构时,必须考虑机器的字节顺序、使用的位域及字节对齐等问题 。
说明:比如Intel CPU与68360 CPU,在处理位域及整数时,其在内存存放的“顺序”正好相反。
示例:假如有如下短整数及结构。
unsigned short int exam;
typedef struct EXAM_BIT_STRU
{ /* Intel 68360 */
unsigned int A1: 1; /* bit 0 7 */
unsigned int A2: 1; /* bit 1 6 */
unsigned int A3: 1; /* bit 2 5 */
} EXAM_BIT;
如下是Intel CPU生成短整数及位域的方式。
内存: 0 1 2 ... (从低到高,以字节为单位)
exam exam低字节 exam高字节
内存: 0 bit 1 bit 2 bit ... (字节的各“位”)
EXAM_BIT A1 A2 A3
如下是68360 CPU生成短整数及位域的方式。
内存: 0 1 2 ... (从低到高,以字节为单位)
exam exam高字节 exam低字节
内存: 7 bit 6 bit 5 bit ... (字节的各“位”)
EXAM_BIT A1 A2 A3
说明:在对齐方式下,CPU的运行效率要快得多。
示例:如下图,当一个long型数(如图中long1)在内存中的位置正好与内存的字边界对齐时,CPU存取这个数只需访问一次内存,而当一个long型数(如图中的long2)在内存中的位置跨越了字边界时,CPU存取这个数就需要多次访问内存,如i960cx访问这样的数需读内存三次(一个BYTE、一个SHORT、一个BYTE,由CPU的微代码执行,对软件透明),所有对齐方式下CPU的运行效率明显快多了。
1 8 16 24 32
------- ------- ------- -------
| long1 | long1 | long1 | long1 |
------- ------- ------- -------
| | | | long2 |
------- ------- ------- --------
| long2 | long2 | long2 | |
------- ------- ------- --------
| ....
6 函数、过程
¹ 6-1:对所调用函数的错误返回码要仔细、全面地处理。
¹ 6-2:明确函数功能,精确(而不是近似)地实现函数设计。
¹ 6-3:编写可重入函数时,应注意局部变量的使用(如编写C/C++语言的可重入函数时,应使用auto即缺省态局部变量或寄存器变量)。
说明:编写C/C++语言的可重入函数时,不应使用static局部变量,否则必须经过特殊处理,才能使函数具有可重入性。
¹ 6-4:编写可重入函数时,若使用全局变量,则应通过关中断、信号量(即P、V操作)等手段对其加以保护。
说明:若对所使用的全局变量不加以保护,则此函数就不具有可重入性,即当多个进程调用此函数时,很有可能使有关全局变量变为不可知状态。
示例:假设Exam是int型全局变量,函数Squre_Exam返回Exam平方值。那么如下函数不具有可重入性。
unsigned int example( int para )
{
unsigned int temp;
Exam = para; // (**)
temp = Square_Exam( );
return temp;
}
此函数若被多个进程调用的话,其结果可能是未知的,因为当(**)语句刚执行完后,另外一个使用本函数的进程可能正好被激活,那么当新激活的进程执行到此函数时,将使Exam赋与另一个不同的para值,所以当控制重新回到“temp = Square_Exam( )”后,计算出的temp很可能不是预想中的结果。此函数应如下改进。
unsigned int example( int para )
{
unsigned int temp;
[申请信号量操作] // 若申请不到“信号量”,说明另外的进程正处于
Exam = para; // 给Exam赋值并计算其平方过程中(即正在使用此
temp = Square_Exam( ); // 信号),本进程必须等待其释放信号后,才可继
[释放信号量操作] // 续执行。若申请到信号,则可继续执行,但其
// 它进程必须等待本进程释放信号量后,才能再使
// 用本信号。
return temp;
}
¹ 6-5:在同一项目组应明确规定对接口函数参数的合法性检查应由函数的调用者负责还是由接口函数本身负责,缺省是由函数调用者负责。
说明:对于模块间接口函数的参数的合法性检查这一问题,往往有两个极端现象,即:要么是调用者和被调用者对参数均不作合法性检查,结果就遗漏了合法性检查这一必要的处理过程,造成问题隐患;要么就是调用者和被调用者均对参数进行合法性检查,这种情况虽不会造成问题,但产生了冗余代码,降低了效率。
½ 6-1:防止将函数的参数作为工作变量。
说明:将函数的参数作为工作变量,有可能错误地改变参数内容,所以很危险。对必须改变的参数,最好先用局部变量代之,最后再将该局部变量的内容赋给该参数。
示例:下函数的实现不太好。
void sum_data( unsigned int num, int *data, int *sum )
{
unsigned int count;
*sum = 0;
for (count = 0; count < num; count++)
{
*sum += data[count]; // sum成了工作变量,不太好。
}
}
若改为如下,则更好些。
void sum_data( unsigned int num, int *data, int *sum )
{
unsigned int count ;
int sum_temp;
sum_temp = 0;
for (count = 0; count < num; count ++)
{
sum_temp += data[count];
}
*sum = sum_temp;
}
½ 6-2:函数的规模尽量限制在200行以内。
说明:不包括注释和空格行。
½ 6-3:一个函数仅完成一件功能。
½ 6-4:为简单功能编写函数。
说明:虽然为仅用一两行就可完成的功能去编函数好象没有必要,但用函数可使功能明确化,增加程序可读性,亦可方便维护、测试。
示例:如下语句的功能不很明显。
value = ( a > b ) ? a : b ;
改为如下就很清晰了。
int max (int a, int b)
{
return ((a > b) ? a : b);
}
value = max (a, b);
或改为如下。
#define MAX (a, b) (((a) > (b)) ? (a) : (b))
value = MAX (a, b);
½ 6-5:不要设计多用途面面俱到的函数。
说明:多功能集于一身的函数,很可能使函数的理解、测试、维护等变得困难。
½ 6-6:函数的功能该是可以预测的,也就是只要输入数据相同就应产生同样的输出。
说明:带有内部“存储器”的函数的功能可能是不可预测的,因为它的输出可能取决于内部存储器(如某标记)的状态。这样的函数既不易于理解又不利于测试和维护。在C/C++语言中,函数的static局部变量是函数的内部存储器,有可能使函数的功能不可预测,然而,当某函数的返回值为指针类型时,则必须是STATIC的局部变量的地址作为返回值,若为AUTO类,则返回为错针。
示例:如下函数,其返回值(即功能)是不可预测的。
unsigned int integer_sum( unsigned int base )
{
unsigned int index;
static unsigned int sum = 0; // 注意,是static类型的。
// 若改为auto类型,则函数即变为可预测。
for (index = 1; index <= base; index++)
{
sum += index;
}
return sum;
}
½ 6-7:尽量不要编写依赖于其他函数内部实现的函数。
说明:此条为函数独立性的基本要求。由于目前大部分高级语言都是结构化的,所以通过具体语言的语法要求与编译器功能,基本就可以防止这种情况发生。但在汇编语言中,由于其灵活性,很可能使函数出现这种情况。
示例:如下是在DOS下TASM的汇编程序例子。过程Print_Msg的实现依赖于Input_Msg的具体实现,这种程序是非结构化的,难以维护、修改。
... // 程序代码
proc Print_Msg // 过程(函数)Print_Msg
... // 程序代码
jmp LABEL
... // 程序代码
endp
proc Input_Msg // 过程(函数)Input_Msg
... // 程序代码
LABEL:
... // 程序代码
endp
½ 6-8:避免设计多参数函数,不使用的参数从接口中去掉。
说明:目的减少函数间接口的复杂度。
½ 6-9:非调度函数应减少或防止控制参数,尽量只使用数据参数。
说明:本建议目的是防止函数间的控制耦合。调度函数是指根据输入的消息类型或控制命令,来启动相应的功能实体(即函数或过程),而本身并不完成具体功能。控制参数是指改变函数功能行为的参数,即函数要根据此参数来决定具体怎样工作。非调度函数的控制参数增加了函数间的控制耦合,很可能使函数间的耦合度增大,并使函数的功能不唯一。
示例:如下函数构造不太合理。
int add_sub( int a, int b, unsigned char add_sub_flg )
{
if (add_sub_flg == INTEGER_ADD)
{
return (a + b);
}
else
{
return (a b);
}
}
不如分为如下两个函数清晰。
int add( int a, int b )
{
return (a + b);
}
int sub( int a, int b )
{
return (a b);
}
½ 6-10:检查函数所有参数输入的有效性。
½ 6-11:检查函数所有非参数输入的有效性,如数据文件、公共变量等。
说明:函数的输入主要有两种:一种是参数输入;另一种是全局变量、数据文件的输入,即非参数输入。函数在使用输入之前,应进行必要的检查。
½ 6-12:函数名应准确描述函数的功能。
½ 6-13:使用动宾词组为执行某操作的函数命名。如果是OOP方法,可以只有动词(名词是对象本身)。
示例:参照如下方式命名函数。
void print_record( unsigned int rec_ind ) ;
int input_record( void ) ;
unsigned char get_current_color( void ) ;
建议6-14:避免使用无意义或含义不清的动词为函数命名。
说明:避免用含义不清的动词如process、handle等为函数命名,因为这些动词并没有说明要具体做什么。
建议6-15:函数的返回值要清楚、明了,让使用者不容易忽视错误情况。
说明:函数的每种出错返回值的意义要清晰、明了、准确,防止使用者误用、理解错误或忽视错误返回码。
½ 6-16:除非必要,最好不要把与函数返回值类型不同的变量,以编译系统默认的转换方式或强制的转换方式作为返回值返回。
½ 6-17:让函数在调用点显得易懂、容易理解。
½ 6-18:在调用函数填写参数时,应尽量减少没有必要的默认数据类型转换或强制数据类型转换。
说明:因为数据类型转换或多或少存在危险。
½ 6-19:避免函数中不必要语句,防止程序中的垃圾代码。
说明:程序中的垃圾代码不仅占用额外的空间,而且还常常影响程序的功能与性能,很可能给程序的测试、维护等造成不必要的麻烦。
½ 6-20:防止把没有关联的语句放到一个函数中。
说明:防止函数或过程内出现随机内聚。随机内聚是指将没有关联或关联很弱的语句放到同一个函数或过程中。随机内聚给函数或过程的维护、测试及以后的升级等造成了不便,同时也使函数或过程的功能不明确。使用随机内聚函数,常常容易出现在一种应用场合需要改进此函数,而另一种应用场合又不允许这种改进,从而陷入困境。
在编程时,经常遇到在不同函数中使用相同的代码,许多开发人员都愿把这些代码提出来,并构成一个新函数。若这些代码关联较大并且是完成一个功能的,那么这种构造是合理的,否则这种构造将产生随机内聚的函数。
示例:如下函数就是一种随机内聚。
void Init_Var( void )
{
Rect.length = 0;
Rect.width = 0; /* 初始化矩形的长与宽 */
Point.x = 10;
Point.y = 10; /* 初始化“点”的坐标 */
}
矩形的长、宽与点的坐标基本没有任何关系,故以上函数是随机内聚。
应如下分为两个函数:
void Init_Rect( void )
{
Rect.length = 0;
Rect.width = 0; /* 初始化矩形的长与宽 */
}
void Init_Point( void )
{
Point.x = 10;
Point.y = 10; /* 初始化“点”的坐标 */
}
½ 6-21:如果多段代码重复做同一件事情,那么在函数的划分上可能存在问题。
说明:若此段代码各语句之间有实质性关联并且是完成同一件功能的,那么可考虑把此段代码构造成一个新的函数。
½ 6-22:功能不明确较小的函数,特别是仅有一个上级函数调用它时,应考虑把它合并到上级函数中,而不必单独存在。
说明:模块中函数划分的过多,一般会使函数间的接口变得复杂。所以过小的函数,特别是扇入很低的或功能不明确的函数,不值得单独存在。
½ 6-23:设计高扇入、合理扇出(小于7)的函数。
说明:扇出是指一个函数直接调用(控制)其它函数的数目,而扇入是指有多少上级函数调用它。
扇出过大,表明函数过分复杂,需要控制和协调过多的下级函数;而扇出过小,如总是1,表明函数的调用层次可能过多,这样不利程序阅读和函数结构的分析,并且程序运行时会对系统资源如堆栈空间等造成压力。函数较合理的扇出(调度函数除外)通常是3-5。扇出太大,一般是由于缺乏中间层次,可适当增加中间层次的函数。扇出太小,可把下级函数进一步分解多个函数,或合并到上级函数中。当然分解或合并函数时,不能改变要实现的功能,也不能违背函数间的独立性。
扇入越大,表明使用此函数的上级函数越多,这样的函数使用效率高,但不能违背函数间的独立性而单纯地追求高扇入。公共模块中的函数及底层函数应该有较高的扇入。
较良好的软件结构通常是顶层函数的扇出较高,中层函数的扇出较少,而底层函数则扇入到公共模块中。
½ 6-24:减少函数本身或函数间的递归调用。
说明:递归调用特别是函数间的递归调用(如A->B->C->A),影响程序的可理解性;递归调用一般都占用较多的系统资源(如栈空间);递归调用对程序的测试有一定影响。故除非为某些算法或功能的实现方便,应减少没必要的递归调用。
½ 6-25:仔细分析模块的功能及性能需求,并进一步细分,同时若有必要画出有关数据流图,据此来进行模块的函数划分与组织。
说明:函数的划分与组织是模块的实现过程中很关键的步骤,如何划分出合理的函数结构,关系到模块的最终效率和可维护性、可测性等。根据模块的功能图或/及数据流图映射出函数结构是常用方法之一。
½ 6-26:改进模块中函数的结构,降低函数间的耦合度,并提高函数的独立性以及代码可读性、效率和可维护性。优化函数结构时,要遵守以下原则:
(1)不能影响模块功能的实现。
(2)仔细考查模块或函数出错处理及模块的性能要求并进行完善。
(3)通过分解或合并函数来改进软件结构。
(4)考查函数的规模,过大的要进行分解。
(5)降低函数间接口的复杂度。
(6)不同层次的函数调用要有较合理的扇入、扇出。
(7)函数功能应可预测。
(8)提高函数内聚。(单一功能的函数内聚最高)
说明:对初步划分后的函数结构应进行改进、优化,使之更为合理。
½ 6-27:在多任务操作系统的环境下编程,要注意函数可重入性的构造。
说明:可重入性是指函数可以被多个任务进程调用。在多任务操作系统中,函数是否具有可重入性是非常重要的,因为这是多个进程可以共用此函数的必条件。另外,编译器是否提供可重入函数库,与它所服务的操作系统有关,只有操作系统是多任务时,编译器才有可能提供可重入函数库。如DOS下BC和MSC等就不具备可重入函数库,因为DOS是单用户单任务操作系统。
½ 6-28:避免使用BOOL参数。
说明:原因有二,其一是BOOL参数值无意义,TURE/FALSE的含义是非常模糊的,在调用时很难知道该参数到底传达的是什么意思;其二是BOOL参数值不利于扩充。还有NULL也是一个无意义的单词。
½ 6-29: 对于提供了返回值的函数,在引用时最好使用其返回值。
½ 6-30:当一个过程(函数)中对较长变量(一般是结构的成员)有较多引用时,可以用一个意义相当的宏代替。
说明:这样可以增加编程效率和程序的可读性。
示例:在某过程中较多引用TheReceiveBuffer[FirstSocket].byDataPtr,
则可以通过以下宏定义来代替:
# define pSOCKDATA TheReceiveBuffer[FirstScoket].byDataPtr
网通站即日起开通,希望大家点击此处帮助测试! 谢谢
[楼 主] | Posted: 2006-09-08 18:41
£壞√№人愛
级别: 一星少将
精华: 0
发帖: 153
联盟游侠威望: 931 点
联盟游侠金币: 4826 银两
联盟贡献值: 2600 点
联盟好评度: 377 点
联盟考核分数: 229 分
在线时间:130(小时)
注册时间:2006-04-11
最后登录:2006-10-07
7 可测性
¹ 7-1:在同一项目组或产品组内,要有一套统一的为集成测试与系统联调准备的调测开关及相应打印函数,并且要有详细的说明。
说明:本规则是针对项目组或产品组的。
¹ 7-2:在同一项目组或产品组内,调测打印出的信息串的格式要有统一的形式。信息串中至少要有所在模块名(或源文件名)及行号。
说明:统一的调测信息格式便于集成测试。
¹ 7-3:编程的同时要为单元测试选择恰当的测试点,并仔细构造测试代码、测试用例,同时给出明确的注释说明。测试代码部分应作为(模块中的)一个子模块,以方便测试代码在模块中的安装与拆卸(通过调测开关)。
说明:为单元测试而准备。
¹ 7-4:在进行集成测试/系统联调之前,要构造好测试环境、测试项目及测试用例,同时仔细分析并优化测试用例,以提高测试效率。
说明:好的测试用例应尽可能模拟出程序所遇到的边界值、各种复杂环境及一些极端情况等。
¹ 7-5:使用断言来发现软件问题,提高代码可测性。
说明:断言是对某种假设条件进行检查(可理解为若条件成立则无动作,否则应报告),它可以快速发现并定位软件问题,同时对系统错误进行自动报警。断言可以对在系统中隐藏很深,用其它手段极难发现的问题进行定位,从而缩短软件问题定位时间,提高系统的可测性。实际应用时,可根据具体情况灵活地设计断言。
示例:下面是C语言中的一个断言,用宏来设计的。(其中NULL为0L)
#ifdef _EXAM_ASSERT_TEST_ // 若使用断言测试
void exam_assert( char * file_name, unsigned int line_no )
{
printf( "\n[EXAM]Assert failed: %s, line %u\n",
file_name, line_no );
abort( );
}
#define EXAM_ASSERT( condition )
if (condition) // 若条件成立,则无动作
NULL;
else // 否则报告
exam_assert( __FILE__, __LINE__ )
#else // 若不使用断言测试
#define EXAM_ASSERT(condition) NULL
#endif /* end of ASSERT */
¹ 7-6:用断言来检查程序正常运行时不应发生但在调测时有可能发生的非法情况。
¹ 7-7:不能用断言来检查最终产品肯定会出现且必须处理的错误情况。
说明:断言是用来处理不应该发生的错误情况的,对于可能会发生的且必须处理的情况要写防错程序,而不是断言。如某模块收到其它模块或链路上的消息后,要对消息的合理性进行检查,此过程为正常的错误检查,不能用断言来实现。
¹ 7-8:对较复杂的断言加上明确的注释。
说明:为复杂的断言加注释,可澄清断言含义并减少不必要的误用。
¹ 7-9:用断言确认函数的参数。
示例:假设某函数参数中有一个指针,那么使用指针前可对它检查,如下。
int exam_fun( unsigned char *str )
{
EXAM_ASSERT( str != NULL ); // 用断言检查“假设指针不为空”这个条件
... //other program code
}
¹ 7-10:用断言保证没有定义的特性或功能不被使用。
示例:假设某通信模块在设计时,准备提供“无连接”和“连接” 这两种业务。但当前的版本中仅实现了“无连接”业务,且在此版本的正式发行版中,用户(上层模块)不应产生“连接”业务的请求,那么在测试时可用断言检查用户是否使用“连接”业务。如下。
#define EXAM_CONNECTIONLESS 0 // 无连接业务
#define EXAM_CONNECTION 1 // 连接业务
int msg_process( EXAM_MESSAGE *msg )
{
unsigned char service; /* message service class */
EXAM_ASSERT( msg != NULL );
service = get_msg_service_class( msg );
EXAM_ASSERT( service != EXAM_CONNECTION ); // 假设不使用连接业务
... //other program code
}
¹ 7-11:用断言对程序开发环境(OS/Compiler/Hardware)的假设进行检查。
说明:程序运行时所需的软硬件环境及配置要求,不能用断言来检查,而必须由一段专门代码处理。用断言仅可对程序开发环境中的假设及所配置的某版本软硬件是否具有某种功能的假设进行检查。如某网卡是否在系统运行环境中配置了,应由程序中正式代码来检查;而此网卡是否具有某设想的功能,则可由断言来检查。
对编译器提供的功能及特性假设可用断言检查,原因是软件最终产品(即运行代码或机器码)与编译器已没有任何直接关系,即软件运行过程中(注意不是编译过程中)不会也不应该对编译器的功能提出任何需求。
示例:用断言检查编译器的int型数据占用的内存空间是否为2,如下。
EXAM_ASSERT( sizeof( int ) == 2 );
¹ 7-12:正式软件产品中应把断言及其它调测代码去掉(即把有关的调测开关关掉)。
说明:加快软件运行速度。
¹ 7-13:在软件系统中设置与取消有关测试手段,不能对软件实现的功能等产生影响。
说明:即有测试代码的软件和关掉测试代码的软件,在功能行为上应一致。
¹ 7-14:用调测开关来切换软件的DEBUG版和正式版,而不要同时存在正式版本和DEBUG版本的不同源文件,以减少维护的难度。
¹ 7-15:软件的DEBUG版本和发行版本应该统一维护,不允许分家,并且要时刻注意保证两个版本在实现功能上的一致性。
½ 7-1:在编写代码之前,应预先设计好程序调试与测试的方法和手段,并设计好各种调测开关及相应测试代码如打印函数等。
说明:程序的调试与测试是软件生存周期中很重要的一个阶段,如何对软件进行较全面、高率的测试并尽可能地找出软件中的错误就成为很关键的问题。因此在编写源代码之前,除了要有一套比较完善的测试计划外,还应设计出一系列代码测试手段,为单元测试、集成测试及系统联调提供方便。
½ 7-2:调测开关应分为不同级别和类型。
说明:调测开关的设置及分类应从以下几方面考虑:针对模块或系统某部分代码的调测;针对模块或系统某功能的调测;出于某种其它目的,如对性能、容量等的测试。这样做便于软件功能的调测,并且便于模块的单元测试、系统联调等。
½ 7-3:编写防错程序,然后在处理错误之后可用断言宣布发生错误。
示例:假如某模块收到通信链路上的消息,则应对消息的合法性进行检查,若消息类别不是通信协议中规定的,则应进行出错处理,之后可用断言报告,如下例。
#ifdef _EXAM_ASSERT_TEST_ // 若使用断言测试
/* Notice: this function does not call 'abort' to exit program */
void assert_report( char * file_name, unsigned int line_no )
{
printf( "\n[EXAM]Error Report: %s, line %u\n",
file_name, line_no );
}
#define ASSERT_REPORT( condition )
if ( condition ) // 若条件成立,则无动作
NULL;
else // 否则报告
assert_report ( __FILE__, __LINE__ )
#else // 若不使用断言测试
#define ASSERT_REPORT( condition ) NULL
#endif /* end of ASSERT */
int msg_handle( unsigned char msg_name, unsigned char * msg )
{
switch( msg_name )
{
case MSG_ONE:
... // 消息MSG_ONE处理
return MSG_HANDLE_SUCCESS;
... // 其它合法消息处理
  default:
... // 消息出错处理
ASSERT_REPORT( FALSE ); // “合法”消息不成立,报告
return MSG_HANDLE_ERROR;
}
}
8 程序效率
¹ 8-1:编程时要经常注意代码的效率。
说明:代码效率分为全局效率、局部效率、时间效率及空间效率。全局效率是站在整个系统的角度上的系统效率;局部效率是站在模块或函数角度上的效率;时间效率是程序处理输入任务所需的时间长短;空间效率是程序所需内存空间,如机器代码空间大小、数据空间大小、栈空间大小等。
¹ 8-2:在保证软件系统的正确性、稳定性、可读性及可测性的前提下,提高代码效率。
说明:不能一味地追求代码效率,而对软件的正确性、稳定性、可读性及可测性造成影响。
¹ 8-3:局部效率应为全局效率服务,不能因为提高局部效率而对全局效率造成影响。
¹ 8-4:通过对系统数据结构的划分与组织的改进,以及对程序算法的优化来提高空间效率。
说明:这种方式是解决软件空间效率的根本办法。
示例:如下记录学生学习成绩的结构不合理。
typedef unsigned char BYTE;
typedef unsigned short WORD;
typedef struct STUDENT_SCORE_STRU
BYTE name[8];
BYTE age;
BYTE sex;
BYTE class;
BYTE subject;
float score;
} STUDENT_SCORE;
因为每位学生都有多科学习成绩,故如上结构将占用较大空间。应如下改进(分为两个结构),总的存贮空间将变小,操作也变得更方便。
typedef struct STUDENT_STRU
{
BYTE name[8];
BYTE age;
BYTE sex;
BYTE class;
} STUDENT;
typedef struct STUDENT_SCORE_STRU
{
WORD student_index;
BYTE subject;
float score;
} STUDENT_SCORE;
¹ 8-5:循环体内工作量最小化。
说明:应仔细考虑循环体内的语句是否可以放在循环体之外,使循环体内工作量最小,从而提高程序的时间效率。
示例:如下代码效率不高。
for (ind = 0; ind < MAX_ADD_NUMBER; ind++)
{
sum += ind;
back_sum = sum; /* backup sum */
}
语句“back_sum = sum;”完全可以放在for语句之后,如下。
for (ind = 0; ind < MAX_ADD_NUMBER; ind++)
{
sum += ind;
}
back_sum = sum; /* backup sum */
½ 8-1:仔细分析有关算法,并进行优化。
½ 8-2:仔细考查、分析系统及模块处理输入(如事务、消息等)的方式,并加以改进。
½ 8-3:对模块中函数的划分及组织方式进行分析、优化,改进模块中函数的组织结构,提高程序效率。
说明:软件系统的效率主要与算法、处理任务方式、系统功能及函数结构有很大关系,仅在代码上下功夫一般不能解决根本问题。
½ 8-4:编程时,要随时留心代码效率;优化代码时,要考虑周全。
½ 8-5:不应花过多的时间拼命地提高调用不很频繁的函数代码效率。
说明:对代码优化可提高效率,但若考虑不周很有可能引起严重后果。
½ 8-6:要仔细地构造或直接用汇编编写调用频繁或性能要求极高的函数。
说明:只有对编译系统产生机器码的方式以及硬件系统较为熟悉时,才可使用汇编嵌入方式。嵌入汇编可提高时间及空间效率,但也存在一定风险。
½ 8-7:在保证程序质量的前提下,通过压缩代码量、去掉不必要代码以及减少不必要的局部和全局变量,来提高空间效率。
说明:这种方式对提高空间效率可起到一定作用,但往往不能解决根本问题。
½ 8-8:在多重循环中,应将最忙的循环放在最内层。
说明:减少CPU切入循环层的次数。
示例:如下代码效率不高。
for (row = 0; row < 100; row++)
{
for (col = 0; col < 5; col++)
{
sum += a[row][col];
}
}
可以改为如下方式,以提高效率。
for (col = 0; col < 5; col++)
{
for (row = 0; row < 100; row++)
{
sum += a[row][col];
}
}
½ 8-9:尽量减少循环嵌套层次。
½ 8-10:避免循环体内含判断语句,应将循环语句置于判断语句的代码块之中。
说明:目的是减少判断次数。循环体中的判断语句是否可以移到循环体外,要视程序的具体情况而言,一般情况,与循环变量无关的判断语句可以移到循环体外,而有关的则不可以。
示例:如下代码效率稍低。
for (ind = 0; ind < MAX_RECT_NUMBER; ind++)
{
if (data_type == RECT_AREA)
{
area_sum += rect_area[ind];
}
else
{
rect_length_sum += rect[ind].length;
rect_width_sum += rect[ind].width;
}
}
因为判断语句与循环变量无关,故可如下改进,以减少判断次数。
if (data_type == RECT_AREA)
{
for (ind = 0; ind < MAX_RECT_NUMBER; ind++)
{
area_sum += rect_area[ind];
}
}
else
{
for (ind = 0; ind < MAX_RECT_NUMBER; ind++)
{
rect_length_sum += rect[ind].length;
rect_width_sum += rect[ind].width;
}
}
½ 8-11:尽量用乘法或其它方法代替除法,特别是浮点运算中的除法。
说明:浮点运算除法要占用较多CPU资源。
示例:如下表达式运算可能要占较多CPU资源。
#define PAI 3.1416
radius = circle_length / (2 * PAI);
应如下把浮点除法改为浮点乘法。
#define PAI_RECIPROCAL (1 / 3.1416 ) // 编译器编译时,将生成具体浮点数
radius = circle_length * PAI_RECIPROCAL / 2;
½ 8-12:不要一味追求紧凑的代码。
说明:因为紧凑的代码并不代表高效的机器码。
9 质量保证
¹ 9-1:在软件设计过程中构筑软件质量。
¹ 9-2:代码质量保证优先原则
(1)正确性,指程序要实现设计要求的功能。
(2)稳定性、安全性,指程序稳定、可靠、安全。
(3)可测试性,指程序要具有良好的可测试性。
(4)规范/可读性,指程序书写风格、命名规则等要符合规范。
(5)全局效率,指软件系统的整体效率。
(6)局部效率,指某个模块/子模块/函数的本身效率。
(7)个人表达方式/个人方便性,指个人编程习惯。
¹ 9-3:只引用属于自己的存贮空间。
说明:若模块封装的较好,那么一般不会发生非法引用他人的空间。
¹ 9-4:防止引用已经释放的内存空间。
说明:在实际编程过程中,稍不留心就会出现在一个模块中释放了某个内存块(如C语言指针),而另一模块在随后的某个时刻又使用了它。要防止这种情况发生。
¹ 9-5:过程/函数中分配的内存,在过程/函数退出之前要释放。
¹ 9-6:过程/函数中申请的(为打开文件而使用的)文件句柄,在过程/函数退出之前要关闭。
说明:分配的内存不释放以及文件句柄不关闭,是较常见的错误,而且稍不注意就有可能发生。这类错误往往会引起很严重后果,且难以定位。
示例:下函数在退出之前,没有把分配的内存释放。
typedef unsigned char BYTE;
int example_fun( BYTE gt_len, BYTE *gt_code )
{
BYTE *gt_buf;
gt_buf = (BYTE *) malloc (MAX_GT_LENGTH);
... //program code, include check gt_buf if or not NULL.
/* global title length error */
if (gt_len > MAX_GT_LENGTH)
{
return GT_LENGTH_ERROR; // 忘了释放gt_buf
}
... // other program code
}
应改为如下。
int example_fun( BYTE gt_len, BYTE *gt_code )
{
BYTE *gt_buf;
gt_buf = (BYTE * ) malloc ( MAX_GT_LENGTH );
... // program code, include check gt_buf if or not NULL.
/* global title length error */
if (gt_len > MAX_GT_LENGTH)
{
free( gt_buf ); // 退出之前释放gt_buf
return GT_LENGTH_ERROR;
}
... // other program code
}
¹ 9-7:防止内存操作越界。
说明:内存操作主要是指对数组、指针、内存地址等的操作。内存操作越界是软件系统主要错误之一,后果往往非常严重,所以当我们进行这些操作时一定要细小心。
示例:假设某软件系统最多可由10个用户同时使用,用户号为1-10,那么如下程序存在问题。
#define MAX_USR_NUM 10
unsigned char usr_login_flg[MAX_USR_NUM]= "";
void set_usr_login_flg( unsigned char usr_no )
{
if (!usr_login_flg[usr_no])
{
usr_login_flg[usr_no]= TRUE;
}
}
当usr_no为10时,将使用usr_login_flg越界。可采用如下方式解决。
void set_usr_login_flg( unsigned char usr_no )
{
if (!usr_login_flg[usr_no - 1])
{
usr_login_flg[usr_no - 1]= TRUE;
}
}
¹ 9-8:认真处理程序所能遇到的各种出错情况。
¹ 9-9:系统运行之初,要初始化有关变量及运行环境,防止未经初始化的变量被引用。
¹ 9-10:系统运行之初,要对加载到系统中的数据进行一致性检查。
说明:使用不一致的数据,容易使系统进入混乱状态和不可知状态。
¹ 9-11:严禁随意更改其它模块或系统的有关设置和配置。
说明:编程时,不能随心所欲地更改不属于自己模块的有关设置如常量、数组的大小等。
¹ 9-12:不能随意改变与其它模块的接口。
¹ 9-13:充分了解系统的接口之后,再使用系统提供的功能。
示例:在B型机的各模块与操作系统的接口函数中,有一个要由各模块负责编写的初始化过程,此过程在软件系统加载完成后,由操作系统发送的初始化消息来调度。因此就涉及到初始化消息的类型与消息发送的顺序问题,特别是消息顺序,若没搞清楚就开始编程,很容易引起严重后果。以下示例引自B型曾出现过的实际代码,其中使用了FID_FETCH_DATA与FID_INITIAL初始化消息类型,注意B型机的系统是在FID_FETCH_DATA之前发送FID_INITIAL的。
MID alarm_module_list[MAX_ALARM_MID];
int FAR SYS_ALARM_proc( FID function_id, int handle )
{
_UI i, j;
switch ( function_id )
{
... // program code
case FID_INITAIL:
for (i = 0; i < MAX_ALARM_MID; i++)
{
if (alarm_module_list== BAM_MODULE // **)
|| (alarm_module_list== LOCAL_MODULE)
{
for (j = 0; j < ALARM_CLASS_SUM; j++)
{
FAR_MALLOC( ... );
}
}
}
... // program code
break;
case FID_FETCH_DATA:
... // program code
Get_Alarm_Module( ); // 初始化alarm_module_list
break;
... // program code
}
}
由于FID_INITIAL是在FID_FETCH_DATA之前执行的,而初始化alarm_module_list是在FID_FETCH_DATA中进行的,故在FID_INITIAL中(**)处引用alarm_module_list变量时,它还没有被初始化。这是个严重错误。
应如下改正:要么把Get_Alarm_Module函数放在FID_INITIAL中(**)之前;要么就必须考虑(**)处的判断语句是否可以用(不使用alarm_module_list变量的)其它方式替代,或者是否可以取消此判断语句。
¹ 9-14:编程时,要防止差1错误。
说明:此类错误一般是由于把“<=”误写成“<”或“>=”误写成“>”等造成的,由此引起的后果,很多情况下是很严重的,所以编程时,一定要在这些地方小心。当编完程序后,应对这些操作符进行彻底检查。
¹ 9-15:要时刻注意易混淆的操作符。当编完程序后,应从头至尾检查一遍这些操作符,以防止拼写错误。
说明:形式相近的操作符最容易引起误用,如C/C++中的“=”与“==”、“|”与“||”、“&”与“&&”等,若拼写错了,编译器不一定能够检查出来。
示例:如把“&”写成“&&”,或反之。
ret_flg = (pmsg->ret_flg & RETURN_MASK);
被写为:
ret_flg = (pmsg->ret_flg && RETURN_MASK);
rpt_flg = (VALID_TASK_NO( taskno ) && DATA_NOT_ZERO( stat_data ));
被写为:
rpt_flg = (VALID_TASK_NO( taskno ) & DATA_NOT_ZERO( stat_data ));
¹ 9-16:有可能的话,if语句尽量加上else分支,对没有else分支的语句要小心对待;switch语句必须有default分支。
¹ 9-17:Unix下,多线程的中的子线程退出必需采用主动退出方式,即子线程应return出口。
¹ 9-18:不要滥用goto语句。
说明:goto语句会破坏程序的结构性,所以除非确实需要,最好不使用goto语句。
½ 9-1:不使用与硬件或操作系统关系很大的语句,而使用建议的标准语句,以提高软件的可移植性和可重用性。
½ 9-2:除非为了满足特殊需求,避免使用嵌入式汇编。
说明:程序中嵌入式汇编,一般都对可移植性有较大的影响。
½ 9-3:精心地构造、划分子模块,并按“接口”部分及“内核”部分合理地组织子模块,以提高“内核”部分的可移植性和可重用性。
说明:对不同产品中的某个功能相同的模块,若能做到其内核部分完全或基本一致,那么无论对产品的测试、维护,还是对以后产品的升级都会有很大帮助。
½ 9-4:精心构造算法,并对其性能、效率进行测试。
½ 9-5:对较关键的算法最好使用其它算法来确认。
½ 9-6:时刻注意表达式是否会上溢、下溢。
示例:如下程序将造成变量下溢。
unsigned char size ;
while (size-- >= 0) // 将出现下溢
{
... // program code
}
当size等于0时,再减1不会小于0,而是0xFF,故程序是一个死循环。应如下修改。
char size; // 从unsigned char 改为char
while (size-- >= 0)
{
... // program code
}
½ 9-7:使用变量时要注意其边界值的情况。
示例:如C语言中字符型变量,有效值范围为-128到127。故以下表达式的计算存在一定风险。
char chr = 127;
int sum = 200;
chr += 1; // 127为chr的边界值,再加1将使chr上溢到-128,而不是128。
sum += chr; // 故sum的结果不是328,而是72。
若chr与sum为同一种类型,或表达式按如下方式书写,可能会好些。
sum = sum + chr + 1;
½ 9-8:留心程序机器码大小(如指令空间大小、数据空间大小、堆栈空间大小等)是否超出系统有关限制。
½ 9-9:为用户提供良好的接口界面,使用户能较充分地了解系统内部运行状态及有关系统出错情况。
½ 9-10:系统应具有一定的容错能力,对一些错误事件(如用户误操作等)能进行自动补救。
½ 9-11:对一些具有危险性的操作代码(如写硬盘、删数据等)要仔细考虑,防止对数据、硬件等的安全构成危害,以提高系统的安全性。
½ 9-12:使用第三方提供的软件开发工具包或控件时,要注意以下几点:
(1)充分了解应用接口、使用环境及使用时注意事项。
(2)不能过分相信其正确性。
(3)除非必要,不要使用不熟悉的第三方工具包与控件。
说明:使用工具包与控件,可加快程序开发速度,节省时间,但使用之前一定对它有较充分的了解,同时第三方工具包与控件也有可能存在问题。
½ 9-13:资源文件(多语言版本支持),如果资源是对语言敏感的,应让该资源与源代码文件脱离,具体方法有下面几种:使用单独的资源文件、DLL文件或其它单独的描述文件(如数据库格式)
10 代码编辑、编译、审查
¹ 10-1:打开编译器的所有告警开关对程序进行编译。
¹ 10-2:在产品软件(项目组)中,要统一编译开关选项。
¹ 10-3:通过代码走读及审查方式对代码进行检查。
说明:代码走读主要是对程序的编程风格如注释、命名等以及编程时易出错的内容进行检查,可由开发人员自己或开发人员交叉的方式进行;代码审查主要是对程序实现的功能及程序的稳定性、安全性、可靠性等进行检查及评审,可通过自审、交叉审核或指定部门抽查等方式进行。
¹ 10-:测试部测试产品之前,应对代码进行抽查及评审。
½ 10-1:编写代码时要注意随时保存,并定期备份,防止由于断电、硬盘损坏等原因造成代码丢失。
½ 10-2:同产品软件(项目组)内,最好使用相同的编辑器,并使用相同的设置选项。
说明:同一项目组最好采用相同的智能语言编辑器,如Muiti Editor,Visual Editor等,并设计、使用一套缩进宏及注释宏等,将缩进等问题交由编辑器处理。
½ 10-3:要小心地使用编辑器提供的块拷贝功能编程。
说明:当某段代码与另一段代码的处理功能相似时,许多开发人员都用编辑器提供的块拷贝功能来完成这段代码的编写。由于程序功能相近,故所使用的变量、采用的表达式等在功能及命名上可能都很相近,所以使用块拷贝时要注意,除了修改相应的程序外,一定要把使用的每个变量仔细查看一遍,以改成正确的。不应指望编译器能查出所有这种错误,比如当使用的是全局变量时,就有可能使某种错误隐藏下来。
½ 10-4:合理地设计软件系统目录,方便开发人员使用。
说明:方便、合理的软件系统目录,可提高工作效率。目录构造的原则是方便有关源程序的存储、查询、编译、链接等工作,同时目录中还应具有工作目录----所有的编译、链接等工作应在此目录中进行,工具目录----有关文件编辑器、文件查找等工具可存放在此目录中。
½ 10-5:某些语句经编译后产生告警,但如果你认为它是正确的,那么应通过某种手段去掉告警信息。
说明:在Borland C/C++中,可用“#pragma warn”来关掉或打开某些告警。
示例:
#pragma warn -rvl // 关闭告警
int examples_fun( void )
{
// 程序,但无return语句。
}
#pragma warn +rvl // 打开告警
编译函数examples_fun时本应产生“函数应有返回值”告警,但由于关掉了此告警信息显示,所以编译时将不会产生此告警提示。
½ 10-6:使用代码检查工具(如C语言用PC-Lint)对源程序检查。
½ 10-7:使用软件工具(如 LogiSCOPE)进行代码审查。
11 代码测试、维护
¹ 11-1:单元测试要求至少达到语句覆盖。
¹ 11-2:单元测试开始要跟踪每一条语句,并观察数据流及变量的变化。
¹ 11-3:清理、整理或优化后的代码要经过审查及测试。
¹ 11-4:代码版本升级要经过严格测试。
¹ 11-5:使用工具软件对代码版本进行维护。
¹ 11-6:正式版本上软件的任何修改都应有详细的文档记录。
½ 11-1:发现错误立即修改,并且要记录下来。
½ 11-2:关键的代码在汇编级跟踪。
½ 11-3:仔细设计并分析测试用例,使测试用例覆盖尽可能多的情况,以提高测试用例的效率。
½ 11-4:尽可能模拟出程序的各种出错情况,对出错处理代码进行充分的测试。
½ 11-5:仔细测试代码处理数据、变量的边界情况。
½ 11-6:保留测试信息,以便分析、总结经验及进行更充分的测试。
½ 11-7:不应通过“试”来解决问题,应寻找问题的根本原因。
½ 11-8:对自动消失的错误进行分析,搞清楚错误是如何消失的。
½ 11-9:修改错误不仅要治表,更要治本。
½ 11-10:测试时应设法使很少发生的事件经常发生。
½ 11-11:明确模块或函数处理哪些事件,并使它们经常发生。
½ 11-12: 坚持在编码阶段就对代码进行彻底的单元测试,不要等以后的测试工作来发现问题。
½ 11-13:去除代码运行的随机性(如去掉无用的数据、代码及尽可能防止并注意函数中的“内部寄存器”等),让函数运行的结果可预测,并使出现的错误可再现。
12 宏
¹ 12-1:用宏定义表达式时,要使用完备的括号。
示例:如下定义的宏都存在一定的风险。
#define RECTANGLE_AREA( a, b ) a * b
#define RECTANGLE_AREA( a, b ) (a * b)
#define RECTANGLE_AREA( a, b ) (a) * (b)
正确的定义应为:
#define RECTANGLE_AREA( a, b ) ((a) * (b))
¹ 12-2:将宏所定义的多条表达式放在大括号中。
示例:下面的语句只有宏的第一条表达式被执行。为了说明问题,for语句的书写稍不符规范。
#define INTI_RECT_VALUE( a, b )
a = 0;
b = 0;
for (index = 0; index < RECT_TOTAL_NUM; index++)
INTI_RECT_VALUE( rect.a, rect.b );
正确的用法应为:
#define INTI_RECT_VALUE( a, b )
{
a = 0;
b = 0;
}
for (index = 0; index < RECT_TOTAL_NUM; index++)
{
INTI_RECT_VALUE( rect[index].a, rect[index].b );
}
¹ 12-3:使用宏时,不允许参数发生变化。
示例:如下用法可能导致错误。
#define SQUARE( a ) ((a) * (a))
int a = 5;
int b;
b = SQUARE( a++ ); // 结果:a = 7,即执行了两次增1。
正确的用法是:
b = SQUARE( a );
a++; // 结果:a = 6,即只执行了一次增1。
Lucene倒排索引原理
MYSQL初学者使用介绍
其实MYSQL的对数据库的操作与其它的SQL类数据库大同小异,您最好找本将SQL的书看看。我在这里只介绍一些基本的,其实我也就只懂这些了,呵呵。
一、连接MYSQL。
格式: mysql -h主机地址 -u用户名 -p用户密码
1、例1:连接到本机上的MYSQL。
首先在打开DOS窗口,然后进入目录 mysqlbin,再键入命令mysql -uroot -p,回车后提示你输密码,如果刚安装好MYSQL,超级用户root是没有密码的,故直接回车即可进入到MYSQL中了,MYSQL的提示符是:mysql>
2、例2:连接到远程主机上的MYSQL。假设远程主机的IP为:110.110.110.110,用户名为root,密码为abcd123。则键入以下命令:
mysql -h110.110.110.110 -uroot -pabcd123
(注:u与root可以不用加空格,其它也一样)
3、退出MYSQL命令: exit (回车)
二、修改密码。
格式:mysqladmin -u用户名 -p旧密码 password 新密码
1、例1:给root加个密码ab12。首先在DOS下进入目录mysqlbin,然后键入以下命令
mysqladmin -uroot -password ab12
注:因为开始时root没有密码,所以-p旧密码一项就可以省略了。
2、例2:再将root的密码改为djg345。
mysqladmin -uroot -pab12 password djg345
三、增加新用户。(注意:和上面不同,下面的因为是MYSQL环境中的命令,所以后面都带一个分号作为命令结束符)
格式:grant select on 数据库.* to 用户名@登录主机 identified by "密码"
例1、增加一个用户test1密码为abc,让他可以在任何主机上登录,并对所有数据库有查询、插入、修改、删除的权限。首先用以root户连入MYSQL,然后键入以下命令:
grant select,insert,update,delete on *.* to test1@"%" Identified by "abc";
但例1增加的用户是十分危险的,你想如某个人知道test1的密码,那么他就可以在internet上的任何一台电脑上登录你的mysql数据库并对你的数据可以为所欲为了,解决办法见例2。
例2、增加一个用户test2密码为abc,让他只可以在localhost上登录,并可以对数据库mydb进行查询、插入、修改、删除的操作(localhost指本地主机,即MYSQL数据库所在的那台主机),这样用户即使用知道test2的密码,他也无法从internet上直接访问数据库,只能通过MYSQL主机上的web页来访问了。
grant select,insert,update,delete on mydb.* to test2@localhost identified by "abc";
如果你不想test2有密码,可以再打一个命令将密码消掉。
grant select,insert,update,delete on mydb.* to test2@localhost identified by "";
我们来看看MYSQL中有关数据库方面的操作。注意:你必须首先登录到MYSQL中,以下操作都是在MYSQL的提示符下进行的,而且每个命令以分号结束。
一、操作技巧
1、如果你打命令时,回车后发现忘记加分号,你无须重打一遍命令,只要打个分号回车就可以了。也就是说你可以把一个完整的命令分成几行来打,完后用分号作结束标志就OK。
2、你可以使用光标上下键调出以前的命令。但以前我用过的一个MYSQL旧版本不支持。我现在用的是mysql-3.23.27-beta-win。
二、显示命令
1、显示数据库列表。
show databases;
刚开始时才两个数据库:mysql和test。mysql库很重要它里面有MYSQL的系统信息,我们改密码和新增用户,实际上就是用这个库进行操作。
2、显示库中的数据表:
use mysql; //打开库,学过FOXBASE的一定不会陌生吧
show tables;
3、显示数据表的结构:
describe 表名;
4、建库:
create database 库名;
5、建表:
use 库名;
create table 表名 (字段设定列表);
6、删库和删表:
drop database 库名;
drop table 表名;
7、将表中记录清空:
delete from 表名;
8、显示表中的记录:
select * from 表名;
三、一个建库和建表以及插入数据的实例
drop database if exists school; //如果存在SCHOOL则删除
create database school; //建立库SCHOOL
use school; //打开库SCHOOL
create table teacher //建立表TEACHER
(
id int(3) auto_increment not null primary key,
name char(10) not null,
address varchar(50) default '深圳',
year date
); //建表结束
//以下为插入字段
insert into teacher values('','glchengang','深圳一中','1976-10-10');
insert into teacher values('','jack','深圳一中','1975-12-23');
注:在建表中(1)将ID设为长度为3的数字字段:int(3)并让它每个记录自动加一:auto_increment并不能为空:not null而且让他成为主字段primary key(2)将NAME设为长度为10的字符字段(3)将ADDRESS设为长度50的字符字段,而且缺省值为深圳。varchar和char有什么区别呢,只有等以后的文章再说了。 (4)将YEAR设为日期字段。
如果你在mysql提示符键入上面的命令也可以,但不方便调试。你可以将以上命令原样写入一个文本文件中假设为school.sql,然后复制到c:\下,并在DOS状态进入目录\mysql\bin,然后键入以下命令:
mysql -uroot -p密码 < c:\school.sql
如果成功,空出一行无任何显示;如有错误,会有提示。(以上命令已经调试,你只要将//的注释去掉即可使用)。
四、将文本数据转到数据库中
1、文本数据应符合的格式:字段数据之间用tab键隔开,null值用\n来代替.
例:
3 rose 深圳二中 1976-10-10
4 mike 深圳一中 1975-12-23
2、数据传入命令 load data local infile "文件名" into table 表名;
注意:你最好将文件复制到\mysql\bin目录下,并且要先用use命令打表所在的库 。
五、备份数据库:(命令在DOS的\mysql\bin目录下执行)
mysqldump --opt school>school.bbb
注释:将数据库school备份到school.bbb文件,school.bbb是一个文本文件,文件名任取,打开看看你会有新发现。
后记:其实MYSQL的对数据库的操作与其它的SQL类数据库大同小异,您最好找本将SQL的书看看。我在这里只介绍一些基本的,其实我也就只懂这些了,呵呵。最好的MYSQL教程还是“晏子“译的“MYSQL中文参考手册“不仅免费每个相关网站都有下载,而且它是最权威的。可惜不是象"PHP4中文手册"那样是chm的格式,在查找函数命令的时候不太方便。
crack新手进化篇(新手级)
商朝子: 第五章--破解原理
第五章--破解原理
从本章开始,我们来一步一步学习Crack软件(80%读者昏死过去,且不省人世...另有20%在寻找附近可以用来打人的东西)
不可不说一下学习破解的三个阶段:
初级,修改程序,用ultraedit等工具修改exe文件,称暴力破解,简称爆破
中级,追出软件的注册码
高级,写出注册机
先说这爆破。所谓爆破,就是指通过修改可执行文件的源文件,来达到相应的目的。你不明白?呵呵,举个例子好了,比如说某共享软件,它比较用户输入的注册码,如果用户输入的,跟它通过用户名(或其它)算出来的注册码相等的话(也就是说用户输入的注册码正确了),那么它就会跳到注册成功的地方去,否则就跳到出错的地方去。
明白过来了吧,我们只要找到这个跳转指令,把它修改为我们需要的“造型”,这样,我们是不是就可以为所欲为了?(某软件双手放在胸口,你要干嘛?)
常见的修改方法有两种,我给你举例说明:
no.1
在某软件中,这样来进行注册:
00451239 CALL 00405E02 (关键CALL,用来判断用户输入的注册码是否正确)
0045123D JZ 004572E6 (!!!<--此为关键跳转,如果用户输入的注册码正确,就跳向成功处,即004572E6处)
0045XXXX YYYYYYYYYY
XXXXXXXX YYYYYYYYYY
XXXXXXXX YYYYYYYYYY
XXXXXXXX 执行到此处,就提示用户注册失败
...提示用户注册码不正确等相关信息
...
004572E6 ... <--(注册成功处!!!)
...提示用户注册成功等相关信息
呵呵,看明白了吗?没有的话,我来给你讲一下。在软件执行到00451239处的时候,CALL置0045E02处来进行注册码判断。接着回来后就来一个跳转语句,即如果用户输入的注册码正确就跳到004572E6处,跳到此处,就算是注册成功了。如果用户输入的注册码不正确的话,那么就不会在0045123D处进行跳转,而一直执行下去。在下面等它的,是注册失败部分。
想明白了吗?嘿嘿...没错,我们只要把那个关键跳转JZ给改为JNZ(如果用户输入的注册码错误,就注册成功,输入正确则注册失败)。当然你也可以将JNZ修改为Jmp,这样的话,你输入的注册码无论正确与否。都可以注册成功。
no.2
我们再来讲一下另外的一种情况:
00451239 CALL 00405E02 (关键CALL,用来判断用户输入的注册码是否正确)
005123D JNZ 004572E6 (!!!<--此为关键跳转,如果用户输入的注册码不正确,就跳向失败处,即004572E6处)
0045XXXX YYYYYYYYYY
XXXXXXXX YYYYYYYYYY
XXXXXXXX YYYYYYYYYY
XXXXXXXX 执行到此处,就提示用户注册成功
...提示用户注册成功等相关信息
...
004572E6 ... <--(注册失败处!!!)
...提示用户注册码不正确等相关信息
这次我相信,并且深信不疑。你一定明白了。我还是不明白...倒...
你一定看出跟第一种情况不同的地方了吧。没错!它与第一种不同的,就是第一种情况是如果注册码正确,就跳到注册成功处,如果没有跳走,就会执行到失败处。而这一种情况则是如果注册码不正确,就跳到注册失败处,否则将执行到注册成功处。
这种情况的修改,除了把JNZ改为JZ外,还可以将其改为Nop,Nop这个指令没有任何意义,将该条指令修改为Nop后,便可随意输入注册码来进行注册了。
原理以经给你讲了,下面我们再来讲一下具体的修改办法吧。(我假设你以经明白了我所说的工具的使用方法)
先说一下虚拟地址和偏移量转换的问题,在SoftICE和W32Dasm下显示的地址值是所谓的内存地址(memory offset),或称之为虚拟地址(Virual Address,VA)。而十六进制工具里,如:Hiew、Hex Workshop等显示的地址就是文件地址,称之为偏移量(File offset) 或物理地址(RAW offset)。
所以当我们要通过那些十六进制工具来对可执行文件中的相应指令进行修改的话,先要找到它的File offset。我们没有必要去使用那些专门的转换工具,在W32Dasm中就有这个功能,比如说你W32Dasm中来到0045123D处,在W32Dasm界面下方的状态栏中就会出现该条指令的虚拟地址和偏移地址,即@:0045123D @offset 0005063Dh 后面的这个0005063Dh就是相应的偏移地址。我们得到该地址后,便可用UltraEdit等十六进制工具来对可执行文件进行修改了。比如使用UltraEdit,你先用UltraEdit打开该可执行文件,然后按Ctrl+G,接着输入你得到的偏移地址,就可以来到其相应的机器码处。
再给你讲一下机器码,所谓的机器码。就是你看到的那些个十六进制数据了。还记的它们与汇编指令是一一对应的吗?
以下这几个是爆破时要用到的,其它的如果感兴趣,可自行查看相关资料:
JZ=74;JNZ=75;JMP=EB;Nop=90
爆破的时候,只要对以上机器码进行相应的修改就行了,比如第一种情况的时候,可以将74修改为EB,即将JZ修改为JMP。而第二种情况,责需将75修改为90,即将JNZ修改为Nop。
由于本章只讲原理,具体一点的。如怎样找到关键跳转等,我们在下一章中再讲。(一个砖头飞了上来!嘿嘿,这次被俺接到了)
上边讲了爆破的原理,你需要明白的是。爆破只是你学习Crack的开始,是很简单的手段。刚入门的时候可以玩玩儿,但希望你不要就此不前!
(嘿嘿,再说了。人家的软件中不是都说了嘛,不准对其进行逆向修改。你动了人家的身子,怎么能不买帐呢?
)
偶就不喜欢爆破,做不出注册机也要找出注册码。否则我就不会去注册这个软件,既然想不掏钱,就要靠你自己的本事。(等以后我有钱了,会考虑去注册那些优秀的共享软件的
)。所以,从某种意义上来说,我是一个正人君子
其实要找到注册码并不是一件多么难的事,我是指你所针对的软件不太那个的时候
不过你无需惧怕。
刚才我们说爆破的时候不提到过关键CALL吗?一般情况下,这个关键CALL就是对两个注册码(一个是软件自身通过你的注册名或机器什么的计算出来的正确的注册码,令一个就是你输入的错误的注册码)进行比较。我前边提到过,CALL之前一般会把所用到的数据先放到一个地方,CALL过去的时候再从这些地方把先前放入的数据取出来,进行相应的处理。这个关键CALL也是这样,在CALL之前,一般会把那两个注册码放到堆栈或某个寄存器中。嘿嘿,我们只要在调试器中,单步执行到该CALL,在未进去之前通过CALL之前的指令判断其将正确的和不正确的注册码放到哪里了。然后再用相应指令进行查看就成了,我说过不难的。
下面列出两个最常见的情况(可参考相关教程):
no.1
mov eax [ ] 这里可以是地址,也可以是其它寄存器
mov edx [ ] 同上,该条指令也可以是pop edx
call 00?????? 关键call
test eax eax
jz(jnz)或jne(je) 关键跳转
看明白了吧,在关键CALL之前,软件会把两个注册码分别放入eax和edx中,你只要在CALL处下d eax或d edx就能看到正确的注册码了。
no.2
mov eax [ ] 这里可以是地址,也可以是其它寄存器
mov edx [ ] 同上,该条指令也可以是pop edx
call 00?????? 关键call
jne(je) 关键跳转
以上两种情况最为常见,而那些个不太常见的情况,我们这里就不再提了。到下下一章的时候,我会给你讲相关方法的...
关于查找软件注册码的部分,就到这里。具体内容,下下一章咱们再说。(不是说了吗?我以经可以接到你的砖头了,干嘛还要丢呢?
)
最后,再来说最后的所谓的高级阶段,如果你相信自己。并且热爱Crack,那么你一定会熬到这个阶段的,只是时间因人而异。
其实分析软件的算法,是有好多技巧在里面的。呵呵,最起码我刚开始的时候就摸不着头脑,那么多CALL,每个看起来,都很重要,都追一遍?结果连好多API都被追了进去。等你自己真正用心分析了一个软件的算法,并写出了注册机后。你就会明白其中的道理了,我们下下下一章再说。(大哥,你不是吧,连你家太阳能都丢过来了
)
<本章完>
test和cmp一个很菜很基础的话题!
首先看看:状态寄存器(即标志寄存器)
PSW(Program Flag)程序状态字(即标志)寄存器,是一个16位寄存器,由条件码标志(flag)和控制标志构成,
如下所示:
15 14 13 12 11 10 9 8 7 6 5 4 3 2 1 0
OF DF IF TF SF ZF AF PF CF
条件码:
①OF(Overflow Flag)溢出标志,溢出时为1,否则置0.标明一个溢出了的计算,如:结构和目标不匹配.
②SF(Sign Flag)符号标志,结果为负时置1,否则置0.
③ZF(Zero Flag)零标志,运算结果为0时置1,否则置0.
④CF(Carry Flag)进位标志,进位时置1,否则置0.注意:Carry标志中存放计算后最右的位.
⑤AF(Auxiliary carry Flag)辅助进位标志,记录运算时第3位(半个字节)产生的进位置。
有进位时1,否则置0.
⑥PF(Parity Flag)奇偶标志.结果操作数中1的个数为偶数时置1,否则置0.
控制标志位:
⑦DF(Direction Flag)方向标志,在串处理指令中控制信息的方向。
⑧IF(Interrupt Flag)中断标志。
⑨TF(Trap Flag)陷井标志。
为举例方便说一下jnz和jz
测试条件
JZ ZF=1
JNZ ZF=0
即Jz=jump if zero (结果为0则设置ZF零标志为1,跳转)
Jnz=jump if not zero
好,接着来看test和cmp
******************************************************************************
test属于逻辑运算指令
功能: 执行BIT与BIT之间的逻辑运算
测试(两操作数作与运算,仅修改标志位,不回送结果).
Test对两个参数(目标,源)执行AND逻辑操作,并根据结果设置标志寄存器,结果本身不会保存。EST AX,BX 与 AND AX,BX 命令有相同效果
语法: TEST r/m,r/m/data
影响标志: C,O,P,Z,S(其中C与O两个标志会被设为0)
运用举例:
1.Test用来测试一个位,例如寄存器:
test eax, 100b; b后缀意为二进制
jnz ******; 如果eax右数第三个位为1,jnz将会跳转
我是这样想的,jnz跳转的条件是ZF=0,ZF=0意味着ZF(零标志)没被置位,即逻辑与结果为1.
2.Test的一个非常普遍的用法是用来测试一方寄存器是否为空:
test ecx, ecx
jz somewhere
如果ecx为零,设置ZF零标志为1,Jz跳转
*******************************************************************************
CMP属于算术运算指令
功能: 比较两个值(寄存器,内存,直接数值)
语法: CMP r/m,r/m/data
标志位: C,P,A,Z,O
CMP比较.(两操作数作减法,仅修改标志位,不回送结果).
cmp实际上是只设置标志不保存结构的减法,并设置Z-flag(零标志).
零标志很像carry,也是内部标志寄存器的一位.
例如:
Cmp eax, 2; 如果eax-2=0即eax=2就设置零标志为1
Jz ****; 如果设置了零标志就跳转
*******************************************************************************
我得出的结论
test逻辑与运算结果为零,就把ZF(零标志)置1;
cmp 算术减法运算结果为零,就把ZF(零标志)置1.
结论很简单嘛,之前我怎么就分不清呢,真是笨哪!
c++作业之运算符第4题
#include <iostream>
using namespace std;
const int SETSIZE = 500;
class Set
{
private:
int member[SETSIZE];
public:
Set()
{
for(int i=0;i<SETSIZE;i++)
member[i]=false;
}
Set(int a[],int n)
{
for(int i=0;i<SETSIZE;i++)
member[i]=false;
for(i=0;i<n;i++)
member[a[i]]=true;
}
int Insert(int n);
int Delete(int n);
friend Set operator +(Set &s1,Set &s2);
friend Set operator *(Set &s1,Set &s2);
friend bool operator ^(int n,Set &s);
void display();
};
int Set::Insert(int n)
{
if (n<SETSIZE)
{
member[n]=true;
return 1;
}
else
return 0;//插入失败
}
int Set::Delete(int n)
{
if (n<SETSIZE)
{
member[n]=false;
return 1;
}
else
return 0;//删除失败
}
Set operator +(Set &s1,Set &s2)
{
Set temp;
for(int i=0;i<SETSIZE;i++)
if (s1.member[i] || s2.member[i])
temp.member[i]=true;
return temp;
}
Set operator *(Set &s1,Set &s2)
{
Set temp;
for(int i=0;i<SETSIZE;i++)
if (s1.member[i] && s2.member[i])
temp.member[i]=true;
return temp;
}
bool operator ^(int n,Set &s)
{
if (s.member[n])
return true;
else
return false;
}
void Set::display()
{
cout << "{";
for(int i=0;i<SETSIZE;i++)
if (member[i])
cout << i << ",";
cout << "\b}";
}
int fillSet()
{
Set p;
for(int i=0;i<5;i++)
{
p.Insert(rand() % 5);
}
for(i=0;i<5;i++)
{
if(!(i^p))
return 0;
}
return 1;
}
int main()
{
int a[]={1, 5, 7, 12, 24, 36, 45, 103, 355, 499};
int b[]={2, 3, 5, 7, 8, 9, 12, 15, 36, 45, 103, 255, 355, 498};
Set S(a,10),T(b,14),U;
for (int i=0;i<50;i++)
U.Insert(i+1);
cout << "2." << endl;
cout << "S+T= " ;(S+T).display();cout << endl;
cout << endl << "3." << endl;
cout << "S*T= " ;(S*T).display();cout << endl;
cout << endl << "4." << endl;
cout << "S*U= " ;(S*U).display();cout << endl;
cout << endl << "5." << endl;
cout << "删除前:";T.display();cout << endl;
T.Delete(8);T.Delete(36);T.Delete(103);T.Delete(498);
cout << "删除后:";T.display();cout << endl;
cout << endl << "6." << endl;
int temp;
cout << "集合S: ";S.display();cout << endl;
for(i=0;i<5;i++)
{
temp=rand()%9+1;
cout << "生成随机数(1-9):" << temp ;
if (temp^S)
cout << " 该数在集合S中" << endl;
else
cout << " 该数不在集合S中" << endl;
}
cout << endl << "7." << endl;
int n=0;
for (long j=0;j<100000;j++)
if(fillSet()) n++;
cout << "概率是: " << n/100000.0 << endl;
return 0;
}
c++作业之运算符重载第2题
#include <iostream>
#include <math.h>
using namespace std;
class Complex
{
public:
Complex()
{
real=0.0;
imag=0.0;
}
Complex(double r,double i)//如果在参数里设默认值会和下面冲突
{
real=r;
imag=i;
}
Complex(double r)//转换构造函数
{
real=r;
imag=0.0;
}
friend Complex operator + (Complex &c1,Complex &c2);
friend Complex operator - (Complex &c1,Complex &c2);
friend Complex operator * (Complex &c1,Complex &c2);
friend Complex operator / (Complex &c1,Complex &c2);
Complex operator - ();
double magnitude();
double argz();
Complex conjugate();
friend ostream &operator << (ostream &output,Complex &c);
friend istream &operator >> (istream &input,Complex &c);
double getreal();
double getimag();
private:
double real;
double imag;
};
Complex operator + (Complex &c1,Complex &c2)//加:u + v = (a + c) + i(b + d)
{
return Complex(c1.real+c2.real,c1.imag+c2.imag);
}
Complex operator - (Complex &c1,Complex &c2)//减:u - v = (a - c) + i(b - d)
{
return Complex(c1.real-c2.real,c1.imag-c2.imag);
}
Complex operator * (Complex &c1,Complex &c2)//乘:u * v = (a*c - b*d) + i(b*c + a*d)
{
return Complex(c1.real*c2.real-c1.imag*c2.imag,c1.imag*c2.real+c1.real*c2.imag);
}
Complex operator / (Complex &c1,Complex &c2)//除:u / v = (a*c + b*d) / (c2 + d2) + i((b*c - a*d) / (c2 + d2))
{
return Complex((c1.real*c2.real+c1.imag*c2.imag)/(c2.real*c2.real+c2.imag*c2.imag)
,(c1.imag*c2.real-c1.real*c2.imag)/(c2.real*c2.real+c2.imag*c2.imag));
}
Complex Complex::operator - ()//求反:-u = -a + i(-b)
{
return Complex(-real,-imag);
}
double Complex::magnitude()//求模
{
return sqrt(real*real+imag*imag);
}
double Complex::argz()//求幅角
{
return atan(imag/real)*180/3.14159;
}
Complex Complex::conjugate()//求共轭
{
return Complex(real,-imag);
}
ostream& operator << (ostream& output,Complex& c)//重载<<
{
if(c.imag>=0)
output << "(" << c.real << "+" << c.imag << "i)";
else
output << "(" << c.real << c.imag << "i)";
return output;
}
istream& operator >> (istream& input,Complex& c)//重载>>
{
cout << "input real part and imaginary part of complex number:";
input >> c.real >> c.imag;
return input;
}
double Complex::getreal()//实部属性访问
{
return real;
}
double Complex::getimag()//虚部属性访问
{
return imag;
}
double Distance(Complex& a, Complex& b)//求两向量间距离
{
return sqrt((a.getreal()-b.getreal())*(a.getreal()-b.getreal())
+(a.getimag()-b.getimag())*(a.getimag()-b.getimag()));
}
int main()
{
Complex c1(1,1),c2(5,0),c3(0,-7);
cout << "1." << endl;
cout << "c1=" << c1 << endl;
cout << "c2=" << c2 << endl;
cout << "c3=" << c3 << endl;
cout << endl << "2." << endl;
cout << "i*i=" << Complex(0,1)*Complex(0,1) << endl;
cout << endl << "3." << endl;
cout << "c1共轭的模是 " << c1.conjugate().magnitude()<< " 幅角是 " << c1.conjugate().argz() << endl;
cout << "c2共轭的模是 " << c2.conjugate().magnitude()<< " 幅角是 " << c2.conjugate().argz() << endl;
cout << "c3共轭的模是 " << c3.conjugate().magnitude()<< " 幅角是 " << c3.conjugate().argz() << endl;
cout << endl << "4." << endl;
cout << "c1到c2的距离是 " << Distance(c1,c2) << endl;
cout << "c1到c3的距离是 " << Distance(c1,c3) << endl;
cout << "c2到c3的距离是 " << Distance(c2,c3) << endl;
cout << endl << "5." << endl;
Complex c[5]={
Complex(2,3),
Complex(-1,1),
Complex(1,1),
Complex(1,-1),
Complex(1,0)
};
for(int i=0;i<5;i++)
{
if ((c[i]*c[i]*c[i]-Complex(3)*c[i]*c[i]+Complex(4)*c[i]-Complex(2)).magnitude()==0)
cout << c[i] << endl;
}
return 0;
}
C/C++字节对齐
什么是对齐,以及为什么要对齐:
现代计算机中内存空间都是按照byte划分的,从理论上讲似乎对任何类型的变量的访问可以从任何地址开始,但实际情况是在访问特定变量的时候经常在特定的内存地址访问,这就需要各类型数据按照一定的规则在空间上排列,而不是顺序的一个接一个的排放,这就是对齐。
对齐的作用和原因:各个硬件平台对存储空间的处理上有很大的不同。一些平台对某些特定类型的数据只能从某些特定地址开始存取。其他平台可能没有这种情况,但是最常见的是如果不按照适合其平台要求对数据存放进行对齐,会在存取效率上带来损失。比如有些平台每次读都是从偶地址开始,如果一个int型(假设为32 位系统)如果存放在偶地址开始的地方,那么一个读周期就可以读出,而如果存放在奇地址始的地方,就可能会需要2个读周期,并对两次读出的结果的高低字节进行拼凑才能得到该int数据。显然在读取效
率上下降很多。这也是空间和时间的博弈。
对齐的实现
通常,我们写程序的时候,不需要考虑对齐问题。编译器会替我们选择适合目标平台的对齐策略。当然,我们也可以通知给编译器传递预编译指令而改变对指定数据的对齐方法。
但是,正因为我们一般不需要关心这个问题,所以因为编辑器对数据存放做了对齐,而我们不了解的话,常常会对一些问题感到迷惑。最常见的就是struct数据结构的sizeof结果,出乎意料。为此,我们需要对对齐算法所了解。
对齐的算法:
由于各个平台和编译器的不同,现以本人使用的gcc version 3.2.2编译器(32位x86平台)为例子,来讨论编译器对struct数据结构中的各成员如何进行对齐的。
设结构体如下定义:
struct A
{
int a;
char b;
short c;
};
结构体A中包含了4字节长度的int一个,1字节长度的char一个和2字节长度的short型数据一个。所以A用到的空间应该是7字节。但是因为编译器要对数据成员在空间上进行对齐。
所以使用sizeof(strcut A)值为8。
现在把该结构体调整成员变量的顺序。
struct B
{
char b;
int a;
short c;
};
这时候同样是总共7个字节的变量,但是sizeof(struct B)的值却是12。
下面我们使用预编译指令#progma pack ()来告诉编译器,使用我们指定的对齐值来取代缺省的。
#progma pack (2) /*指定按2字节对齐*/
struct C
{
char b;
int a;
short c;
};
#progma pack () /*取消指定对齐,恢复缺省对齐*/
sizeof(struct C)值是8。
修改对齐值为1:
#progma pack (1) /*指定按1字节对齐*/
struct D
{
char b;
int a;
short c;
};
#progma pack () /*取消指定对齐,恢复缺省对齐*/
sizeof(struct D)值为7。
对于char型数据,其自身对齐值为1,对于short型为2,对于int,float,double类型,其自身对齐值为4,单位字节。
这里面有四个概念值:
1.数据类型自身的对齐值:就是上面交代的基本数据类型的自身对齐值。
2.指定对齐值:#progma pack ()时的指定对齐值。
3.结构体或者类的自身对齐值:其成员中自身对齐值最大的那个值。
4.数据成员、结构体和类的有效对齐值:自身对齐值和指定对齐值中小的那个值。
有了这些值,我们就可以很方便的来讨论具体数据结构的成员和其自身的对齐方式。有效对齐值N是最终用来决定数据存放地址方式的值,最重要。有效对齐N,就是表示“对齐在N上”,也就是说该数据的"存放起始地址%N=0".而数据结构中的数据变量都是按定义的先后顺序来排放的。第一个数据变量的起始地址就是数据结构的起始地址。结构体的成员变量要对齐排放,结构体本身也要根据自身的有效对齐值圆整(就是结构体成员变量占用总长度需要是对结构体有效对齐值的整数倍,结合下面例子理解)。这样就不能理解上面的几个例子的值了。
例子分析:
分析例子B;
struct B
{
char b;
int a;
short c;
};
假设B从地址空间0x0000开始排放。该例子中没有定义指定对齐值,在笔者环境下,该值默认为4。第一个成员变量b的自身对齐值是1,比指定或者默认指定对齐值4小,所以其有效对齐值为1,所以其存放地址0x0000符合0x0000%1=0.第二个成员变量a,其自身对齐值为4,所以有效对齐值也为4,所以只能存放在起始地址为0x0004到0x0007这四个连续的字节空间中,复核0x0004%4=0,且紧靠第一个变量。第三个变量c,自身对齐值为 2,所以有效对齐值也是2,可以存放在0x0008到0x0009这两个字节空间中,符合0x0008%2=0。所以从0x0000到0x0009存放的都是B内容。再看数据结构B的自身对齐值为其变量中最大对齐值(这里是b)所以就是4,所以结构体的有效对齐值也是4。根据结构体圆整的要求, 0x0009到0x0000=10字节,(10+2)%4=0。所以0x0000A到0x000B也为结构体B所占用。故B从0x0000到0x000B 共有12个字节,sizeof(struct B)=12;
同理,分析上面例子C:
#progma pack (2) /*指定按2字节对齐*/
struct C
{
char b;
int a;
short c;
};
#progma pack () /*取消指定对齐,恢复缺省对齐*/
第一个变量b的自身对齐值为1,指定对齐值为2,所以,其有效对齐值为1,假设C从0x0000开始,那么b存放在0x0000,符合0x0000%1= 0;第二个变量,自身对齐值为4,指定对齐值为2,所以有效对齐值为2,所以顺序存放在0x0002、0x0003、0x0004、0x0005四个连续字节中,符合0x0002%2=0。第三个变量c的自身对齐值为2,所以有效对齐值为2,顺序存放
在0x0006、0x0007中,符合 0x0006%2=0。所以从0x0000到0x00007共八字节存放的是C的变量。又C的自身对齐值为4,所以C的有效对齐值为2。又8%2=0,C 只占用0x0000到0x0007的八个字节。所以sizeof(struct C)=8.
动物识别专家系统的程序(animal.h)
Protected Mode tutorial
| Real Mode | 16-bit Protected Mode | 32-bit Protected Mode | |
|---|---|---|---|
| Segment base address | 20-bit (1M byte range) = 16 * segment register | 24-bit (16M byte range), from descriptor | 32-bit (4G byte range), from descriptor |
| Segment size (limit) | 16-bit, 64K bytes (fixed) | 16-bit, 1-64K bytes | 20-bit, 1-1M bytes or 4K-4G bytes |
| Segment protection | no | yes | yes | Segment register | segment base address / 16 | selector | selector |
In protected mode, besides the segment base address, we also need the segment size (limit) and some flags indicating what the segment is used for. This information goes into an 8-byte data structure called a descriptor:
Table 2: code/data segment descriptor
| Lowest byte | Byte 1 | Byte 2 | Byte 3 | Byte 4 | Byte 5 | Byte 6 | Highest byte |
|---|---|---|---|---|---|---|---|
| Limit 7:0 | Limit 15:8 | Base 7:0 | Base 15:8 | Base 23:16 | Access | Flags, Limit 19:16 | Base 31:24 |
This is a 32-bit ('386) descriptor. For 16-bit ('286) descriptors, the top two bytes (Limit 19:16, Flags, and Base 31:24) are zero. The Access byte indicates segment usage (data segment, stack segment, code segment, etc.):
Table 3: access byte of code/data segment descriptor
| Highest bit | Bits 6, 5 | Bit 4 | Bits 3 | Bit 2 | Bit 1 | Lowest bit |
|---|---|---|---|---|---|---|
| Present | Privilege | 1 | Executable | Expansion direction/conforming | Writable/readable | Accessed |
Table 4: flags nybble
| Highest bit | Bit 6 | Bit 5 | Bit 4 |
|---|---|---|---|
| Granularity | Default Size | 0 | 0 |
The Granularity bit indicates if the segment limit is in units of 4K byte pages (G=1) or if the limit is in units of bytes (G=0).
For stack segments, the Default Size bit is also known as the B (Big) bit, and controls whether 16- or 32-bit values are pushed and popped. For code segments, the D bit indicates whether instructions will operate on 16-bit (D=0) or 32-bit (D=1) quantities by default. To expand upon this: when the D bit is set, the segment is USE32, named after the assembler directive of the same name. The following sequence of hex bytes:
B8 90 90 90 90will be treated by the CPU as a 32-bit instruction, and will disassemble as
mov eax, 90909090hIn a 16-bit (USE16) code segment, the same sequence of bytes would be equivalent to
mov ax,9090h nop nopTwo special opcode bytes called the Operand Size Prefix and the Address Length Prefix reverse the sense of the D bit for the instruction destination and source, respectively. These prefixes affect only the instruction that immediately follows them.
Bit 4 of the Access byte is set to one for code or data/stack segments. If this bit is zero, you have a system segment. These come in several varieties:
| Lowest byte | Byte 1 | Byte 2 | Byte 3 | Byte 4 | Byte 5 | Byte 6 | Highest byte |
|---|---|---|---|---|---|---|---|
| Offset 7:0 | Offset 15:8 | Selector 7:0 | Selector 15:8 | Word Count 4:0 | Access | Offset 23:16 | Offset 31:24 |
Note the Selector field. Gates work through indirection, and require a separate code or TSS descriptor to function.
Table 6: access byte of system segment descriptor.
| Highest bit | Bits 6, 5 | Bit 4 | Bits 3, 2, 1, 0 |
|---|---|---|---|
| Present | Privilege | 0 | Type |
Table 7: System segment types
| Type | Segment function | Type | Segment function | |
|---|---|---|---|---|
| 0 | (invalid) | 8 | (invalid) | |
| 1 | Available '286 TSS | 9 | Available '386 TSS | |
| 2 | LDT | 10 | (undefined, reserved) | |
| 3 | Busy '286 TSS | 11 | Busy '386 TSS | |
| 4 | '286 Call Gate | 12 | '386 Call Gate | |
| 5 | Task Gate | 13 | (undefined, reserved) | |
| 6 | '286 Interrupt Gate | 14 | '386 Interrupt Gate | |
| 7 | '286 Trap Gate | 15 | '386 Trap Gate |
Whew! For now, just remember that TSSes, LDTs, and gates are the three main types of system segment.
Limit = 64K (FFFFh) Byte-granular (G=0)
Expand-up (E=0) Writable (W=1)
Present (P=1) Base address = any valueThe bit values correspond to an Access byte with the binary value = 1xx1001x ; this code in NASM syntax
o32 lidt [real_idt]
...
real_idt:
dw 1023
dd 0xor eax,eax ... movzx ebp,bp movzx esp,sp
Actually, for DS, ES, FS and GS, the segment limit must be 0xFFFF or greater. If you give the segment a limit of 0xFFFFF and make it page-granular, you can access up to 4G of memory from real mode -- this is unreal mode. However, limits other than 0xFFFF (or page-granularity) for CS or SS cause big problems in real mode.
The GDT (as well as LDTs) should reside in RAM, because the processor modifies the accessed bit of descriptors.However, one of my sources (thanks Vinay) states that later CPUs will not attempt to set the Accessed bit in a descriptor if that bit is already set. Check the docs for the CPU you are using.
堆与栈的区别
一、预备知识—程序的内存分配
一个由c/C++编译的程序占用的内存分为以下几个部分
1、栈区(stack)— 由编译器自动分配释放 ,存放函数的参数值,局部变量的值等。其操作方式类似于
数据结构中的栈。
2、堆区(heap) — 一般由程序员分配释放, 若程序员不释放,程序结束时可能由OS回收 。注意它与
数据结构中的堆是两回事,分配方式倒是类似于链表,呵呵。
3、全局区(静态区)(static)—,全局变量和静态变量的存储是放在一块的,初始化的全局变量和静
态变量在一块区域, 未初始化的全局变量和未初始化的静态变量在相邻的另一块区域。 - 程序结束后有
系统释放
4、文字常量区 —常量字符串就是放在这里的。 程序结束后由系统释放
5、程序代码区—存放函数体的二进制代码。
例子程序
这是一个前辈写的,非常详细
//main.cpp
int a = 0; 全局初始化区
char *p1; 全局未初始化区
main()
{
int b; 栈
char s[] = "abc"; 栈
char *p2; 栈
char *p3 = "123456"; 123456\0在常量区,p3在栈上。
static int c =0; 全局(静态)初始化区
p1 = (char *)malloc(10);
p2 = (char *)malloc(20); 分配得来得10和20字节的区域就在堆区。
strcpy(p1, "123456"); 123456\0放在常量区,编译器可能会将它与p3所指向
的"123456"优化成一个地方
}
二、堆和栈的理论知识
2.1申请方式
stack:
由系统自动分配。 例如,声明在函数中一个局部变量 int b; 系统自动在栈中为b开辟空间
heap:
需要程序员自己申请,并指明大,在c中malloc函数
如p1 = (char *)malloc(10);
在C++中用new运算符
如p2 = (char *)malloc(10);
但是注意p1、p2本身是在栈中的。
2.2
申请后系统的响应
栈:只要栈的剩余空间大于所申请空间,系统将为程序提供内存,否则将报异常提示栈溢出。
堆:首先应该知道操作系统有一个记录空闲内存地址的链表,当系统收到程序的申请时,
会遍历该链表,寻找第一个空间大于所申请空间的堆结点,然后将该结点从空闲结点链表中删除,并将该
结点的空间分配给程序,另外,对于大多数系统,会在这块内存空间中的首地址处记录本次分配的大小,
这样,代码中的delete语句才能正确的释放本内存空间。另外,由于找到的堆结点的大小不一定正好等于
申请的大小,系统会自动的将多余的那部分重新放入空闲链表中。
2.3申请大小的限制
栈:在Windows下,栈是向低地址扩展的数据结构,是一块连续的内存的区域。这句话的意思是栈顶的地址
和栈的最大容量是系统预先规定好的,在 WINDOWS下,栈的大小是2M(也有的说是1M,总之是一个编译时
就确定的常数),如果申请的空间超过栈的剩余空间时,将提示overflow。因此,能从栈获得的空间较小
。
堆:堆是向高地址扩展的数据结构,是不连续的内存区域。这是由于系统是用链表来存储的空闲内存地址
的,自然是不连续的,而链表的遍历方向是由低地址向高地址。堆的大小受限于计算机系统中有效的虚拟
内存。由此可见,堆获得的空间比较灵活,也比较大。
2.4申请效率的比较:
栈由系统自动分配,速度较快。但程序员是无法控制的。
堆是由new分配的内存,一般速度比较慢,而且容易产生内存碎片,不过用起来最方便.
另外,在WINDOWS下,最好的方式是用VirtualAlloc分配内存,他不是在堆,也不是在栈是直接在进程的
地址空间中保留一快内存,虽然用起来最不方便。但是速度快,也最灵活
2.5堆和栈中的存储内容
栈: 在函数调用时,第一个进栈的是主函数中后的下一条指令(函数调用语句的下一条可执行语句)的
地址,然后是函数的各个参数,在大多数的C编译器中,参数是由右往左入栈的,然后是函数中的局部变
量。注意静态变量是不入栈的。
当本次函数调用结束后,局部变量先出栈,然后是参数,最后栈顶指针指向最开始存的地址,也就是主函
数中的下一条指令,程序由该点继续运行。
堆:一般是在堆的头部用一个字节存放堆的大小。堆中的具体内容有程序员安排.
2.6存取效率的比较
char s1[] = "aaaaaaaaaaaaaaa";
char *s2 = "bbbbbbbbbbbbbbbbb";
aaaaaaaaaaa是在运行时刻赋值的;
而bbbbbbbbbbb是在编译时就确定的;
但是,在以后的存取中,在栈上的数组比指针所指向的字符串(例如堆)快。
比如:
#include
void main()
{
char a = 1;
char c[] = "1234567890";
char *p ="1234567890";
a = c[1];
a = p[1];
return;
}
对应的汇编代码
10: a = c[1];
00401067 8A 4D F1 mov cl,byte ptr [ebp-0Fh]
0040106A 88 4D FC mov byte ptr [ebp-4],cl
11: a = p[1];
0040106D 8B 55 EC mov edx,dword ptr [ebp-14h]
00401070 8A 42 01 mov al,byte ptr [edx+1]
00401073 88 45 FC mov byte ptr [ebp-4],al
第一种在读取时直接就把字符串中的元素读到寄存器cl中,而第二种则要先把指针值读到edx中,在根据
edx读取字符,显然慢了。
2.7小结:
堆和栈的区别可以用如下的比喻来看出:
使用栈就象我们去饭馆里吃饭,只管点菜(发出申请)、付钱、和吃(使用),吃饱了就走,不必理会切
菜、洗菜等准备工作和洗碗、刷锅等扫尾工作,他的好处是快捷,但是自由度小。
使用堆就象是自己动手做喜欢吃的菜肴,比较麻烦,但是比较符合自己的口味,而且自由度大。
堆和栈的区别主要分:
操作系统方面的堆和栈,如上面说的那些,不多说了。
还有就是数据结构方面的堆和栈,这些都是不同的概念。这里的堆实际上指的就是(满足堆性质的)优先
队列的一种数据结构,第1个元素有最高的优先权;栈实际上就是满足先进后出的性质的数学或数据结构
。
虽然堆栈,堆栈的说法是连起来叫,但是他们还是有很大区别的,连着叫只是由于历史的原因。
-----------------------------------------------------------------------------------------------------------------
堆(heap)和栈(stack)是C/C++编程不可避免会碰到的两个基本概念。首先,这两个概念都可以在讲数
据结构的书中找到,他们都是基本的数据结构,虽然栈更为简单一些。
在具体的C/C++编程框架中,这两个概念并不是并行的。对底层机器代码的研究可以揭示,栈是机器
系统提供的数据结构,而堆则是C/C++函数库提供的。
具体地说,现代计算机(串行执行机制),都直接在代码底层支持栈的数据结构。这体现在,有专门的
寄存器指向栈所在的地址,有专门的机器指令完成数据入栈出栈的操作。这种机制的特点是效率高,支持
的数据有限,一般是整数,指针,浮点数等系统直接支持的数据类型,并不直接支持其他的数据结构。因
为栈的这种特点,对栈的使用在程序中是非常频繁的。对子程序的调用就是直接利用栈完成的。机器的
call指令里隐含了把返回地址推入栈,然后跳转至子程序地址的操作,而子程序中的ret指令则隐含从堆
栈中弹出返回地址并跳转之的操作。C/C++中的自动变量是直接利用栈的例子,这也就是为什么当函数返
回时,该函数的自动变量自动失效的原因(因为堆栈恢复了调用前的状态)。
和栈不同,堆的数据结构并不是由系统(无论是机器系统还是操作系统)支持的,而是由函数库提供的
。基本的malloc/realloc/free函数维护了一套内部的堆数据结构。当程序使用这些函数去获得新的内存
空间时,这套函数首先试图从内部堆中寻找可用的内存空间,如果没有可以使用的内存空间,则试图利用
系统调用来动态增加程序数据段的内存大小,新分配得到的空间首先被组织进内部堆中去,然后再以适当
的形式返回给调用者。当程序释放分配的内存空间时,这片内存空间被返回内部堆结构中,可能会被适当
的处理(比如和其他空闲空间合并成更大的空闲空间),以更适合下一次内存分配申请。这套复杂的分配机
制实际上相当于一个内存分配的缓冲池(Cache),使用这套机制有如下若干原因:
1. 系统调用可能不支持任意大小的内存分配。有些系统的系统调用只支持固定大小及其倍数的内存
请求(按页分配);这样的话对于大量的小内存分类来说会造成浪费。
2. 系统调用申请内存可能是代价昂贵的。系统调用可能涉及用户态和核心态的转换。
3. 没有管理的内存分配在大量复杂内存的分配释放操作下很容易造成内存碎片。
堆和栈的对比
从以上知识可知,栈是系统提供的功能,特点是快速高效,缺点是有限制,数据不灵活;而堆是函数
库提供的功能,特点是灵活方便,数据适应面广泛,但是效率有一定降低。栈是系统数据结构,对于进程
/线程是唯一的;堆是函数库内部数据结构,不一定唯一。不同堆分配的内存逻辑上无法互相操作。栈空
间分静态分配和动态分配两种。静态分配是编译器完成的,比如自动变量(auto)的分配。动态分配由
alloca函数完成。栈的动态分配无需释放(是自动的),也就没有释放函数。为可移植的程序起见,栈的动
态分配操作是不被鼓励的!堆空间的分配总是动态的,虽然程序结束时所有的数据空间都会被释放回系统
,但是精确的申请内存/释放内存匹配是良好程序的基本要素。
C++0x,崭新的C++,还是另一个JAVA?
免杀加花个人总结
花指令其实是一堆废话,也就是有没有这段花指令都不影响程序的正常运行,注意,添加的花指令要保持堆栈的平衡
我们把一段花指令比喻成一道复杂的数学运算题,无论你在这个过程用什么加减乘除,最终要保持这道数学题的结果是0 ,这样就达到堆栈的平衡的目的.
一般有四方法:替换法, 添加法, 移位法, 去除法
可综合使用,若程序找不到0区域,可先加个区段,再加花
方法1:替换法
注意事项:注意替换和被替换的指令功能一定要相同,否则导致程序出错.
mov eax,00478ac0
jmp eax
换成:
push 00478ac0
retn
方法2:添加法
注意:添加的指令也要保持堆栈平衡,否则导致程序出错.
添加以下指令:
push eax
pop eax
push eax
pop eax
方法3:移位法
注意:移位互换法灵活性比较强,只要改变顺序,让杀毒软件识别不出来,就达到目的了.
第11句的push eax 和第13句的pop eax 互换一下位置就可达到免杀效果.
方法4:去除法
注意:要去除的指令也要保持堆栈平衡.
把第11句的push eax 和第13句的pop eax 全部nop掉. 就达到免杀效果.
方法5:综合以上四种方法
注意:有时候一种方法修改后,不能达到免杀效果.这时候可以综合运用替换法,添加法,移位法,去除法进行修改.达到免杀效果.
三.解释一些指令含意
push ebp ----把基址指针寄存器压入堆栈
pop ebp ----把基址指针寄存器弹出堆栈
push eax ----把数据寄存器压入堆栈
pop eax ----把数据寄存器弹出堆栈
nop -----不执行
add esp,1-----指针寄存器加1
sub esp,1-----指针寄存器减1
inc ecx -----计数器加1
dec ecx -----计数器减1
sub esp,1 ----指针寄存器加1
sub esp,-1----指针寄存器加-1
jmp 入口地址----跳到程序入口地址
push 入口地址---把入口地址压入堆栈
retn ------ 反回到入口地址,效果与jmp 入口地址一样.
mov eax,入口地址 ------把入口地址转送到数据寄存器中.
jmp eax ----- 跳到程序入口地址
jb 入口地址
jnb 入口地址 ------效果和jmp 入口地址一样,直接跳到程序入口地址.
四.免杀花指令编写手册:
注:以后编写花指令,都可以参考本手册,灵活组合,快速写出自己的花指令.
*******************************************************************
push ebp
pop ebp
push eax
pop eax
push esp
pop esp
push 0
push 0
push 10 -------其中数字可以任意,注意与下面对应
push -10
nop -----------可任意在中间添加
move edi,edi ----效果与nop一样
add esp,1 -------其中数字可以任意,注意以下面对应
add esp,-1
add esp,1 --------其中数字可以任意,注意以下面对应
sub esp,1
inc ecx
dec ecx
sub eax, 2 ----------其中数字可任意,与inc的个数对应
inc eax
inc eax
add eax -2 ----------其中数字可任意,与inc的个数对应
inc eax
inc eax
jmp 下一个jmp地址
jmp 下一个地址
push ebp
mov ebp,esp -------可做为花指令的开头句
jmp 入口地址 ------跳到程序入口地址
与它效果一样的还有(以下三个):
push 入口地址
retn
jb 入口地址
jnb 入口地址
mov eax,入口地址
jmp eax
一篇很好的了解机器人入门知识的文章
给刚接触机器人的朋友们 -2003-9-4 2:21:17 | ||||||||||||||||||||||||||||||||||||||||
| ||||||||||||||||||||||||||||||||||||||||
数学学科分类标准
一份中国学科分类国家标准,看看,就一个数学中的一个分支一个人一辈子都研究不完。其中也说明了,应用数学归为每个具体应用学科里面。除了专门数学专业的,其他专业的也只是学了其中在本学科需要的一小部分而已。
110 数学
a.. 110.11 数学史
b.. 110.14 数理逻辑与数学基础
a.. 110.1410 演绎逻辑学 亦称符号逻辑学
b.. 110.1420 证明论 亦称元数学
c.. 110.1430 递归论
d.. 110.1440 模型论
e.. 110.1450 公理集合论
f.. 110.1460 数学基础
g.. 110.1499 数理逻辑与数学基础其他学科
c.. 110.17 数论
a.. 110.1710 初等数论
b.. 110.1720 解析数论
c.. 110.1730 代数数论
d.. 110.1740 超越数论
e.. 110.1750 丢番图逼近
f.. 110.1760 数的几何
g.. 110.1770 概率数论
h.. 110.1780 计算数论
i.. 110.1799 数论其他学科
d.. 110.21 代数学
a.. 110.2110 线性代数
b.. 110.2115 群论
c.. 110.2120 域论
d.. 110.2125 李群
e.. 110.2130 李代数
f.. 110.2135 Kac-Moody代数
g.. 110.2140 环论 包括交换环与交换代数,结合环与结合代数,非结合环与非结
合代数等
h.. 110.2145 模论
i.. 110.2150 格论
j.. 110.2155 泛代数理论
k.. 110.2160 范畴论
l.. 110.2165 同调代数
m.. 110.2170 代数K理论
n.. 110.2175 微分代数
o.. 110.2180 代数编码理论
p.. 110.2199 代数学其他学科
e.. 110.24 代数几何学
f.. 110.27 几何学
a.. 110.2710 几何学基础
b.. 110.2715 欧氏几何学
c.. 110.2720 非欧几何学 包括黎曼几何学等
d.. 110.2725 球面几何学
e.. 110.2730 向量和张量分析
f.. 110.2735 仿射几何学
g.. 110.2740 射影几何学
h.. 110.2745 微分几何学
i.. 110.2750 分数维几何
j.. 110.2755 计算几何学
k.. 110.2799 几何学其他学科
g.. 110.31 拓扑学
a.. 110.3110 点集拓扑学
b.. 110.3115 代数拓扑学
c.. 110.3120 同伦论
d.. 110.3125 低维拓扑学
e.. 110.3130 同调论
f.. 110.3135 维数论
g.. 110.3140 格上拓扑学
h.. 110.3145 纤维丛论
i.. 110.3150 几何拓扑学
j.. 110.3155 奇点理论
k.. 110.3160 微分拓扑学
l.. 110.3199 拓扑学其他学科
h.. 110.34 数学分析
a.. 110.3410 微分学
b.. 110.3420 积分学
c.. 110.3430 级数论
d.. 110.3499 数学分析其他学科
i.. 110.37 非标准分析
j.. 110.41 函数论
a.. 110.4110 实变函数论
b.. 110.4120 单复变函数论
c.. 110.4130 多复变函数论
d.. 110.4140 函数逼近论
e.. 110.4150 调和分析
f.. 110.4160 复流形
g.. 110.4170 特殊函数论
h.. 110.4199 函数论其他学科
k.. 110.44 常微分方程
a.. 110.4410 定性理论
b.. 110.4420 稳定性理论
c.. 110.4430 解析理论
d.. 110.4499 常微分方程其他学科
l.. 110.47 偏微分方程
a.. 110.4710 椭圆型偏微分方程
b.. 110.4720 双曲型偏微分方程
c.. 110.4730 抛物型偏微分方程
d.. 110.4740 非线性偏微分方程
e.. 110.4799 偏微分方程其他学科
m.. 110.51 动力系统
a.. 110.5110 微分动力系统
b.. 110.5120 拓扑动力系统
c.. 110.5130 复动力系统
d.. 110.5199 动力系统其他学科
n.. 110.54 积分方程
o.. 110.57 泛函分析
a.. 110.5710 线性算子理论
b.. 110.5715 变分法
c.. 110.5720 拓扑线性空间
d.. 110.5725 希尔伯特空间
e.. 110.5730 函数空间
f.. 110.5735 巴拿赫空间
g.. 110.5740 算子代数
h.. 110.5745 测度与积分
i.. 110.5750 广义函数论
j.. 110.5755 非线性泛函分析
k.. 110.5799 泛函分析其他学科
p.. 110.61 计算数学
a.. 110.6110 插值法与逼近论
b.. 110.6120 常微分方程数值解
c.. 110.6130 偏微分方程数值解
d.. 110.6140 积分方程数值解
e.. 110.6150 数值代数
f.. 110.6160 连续问题离散化方法
g.. 110.6170 随机数值实验
h..110.6180 误差分析
i.. 110.6199 计算数学其他学科
q.. 110.64 概率论
a.. 110.6410 几何概率
b.. 110.6420 概率分布
c.. 110.6430 极限理论
d.. 110.6440 随机过程 包括正态过程与平稳过程、点过程等
e.. 110.6450 马尔可夫过程
f.. 110.6460 随机分析
g.. 110.6470 鞅论
h.. 110.6480 应用概率论 具体应用入有关学科
i.. 110.6499 概率论其他学科
r.. 110.67 数理统计学
a.. 110.6710 抽样理论 包括抽样分布、抽样调查等
b.. 110.6715 假设检验
c.. 110.6720 非参数统计
d.. 110.6725 方差分析
e.. 110.6730 相关回归分析
f.. 110.6735 统计推断
g.. 110.6740 贝叶斯统计 包括参数估计等
h.. 110.6745 试验设计
i.. 110.6750 多元分析
j.. 110.6755 统计判决理论
k.. 110.6760 时间序列分析
l.. 110.6799 数理统计学其他学科
s.. 110.71 应用统计数学
a.. 110.7110 统计质量控制
b.. 110.7120 可靠性数学
c.. 110.7130 保险数学
d.. 110.7140 统计模拟
t.. 110.7199 应用统计数学其他学科
u.. 110.74 运筹学
a.. 110.7410 线性规划
b.. 110.7415 非线性规划
c.. 110.7420 动态规划
d.. 110.7425 组合最优化
e.. 110.7430 参数规划
f.. 110.7435 整数规划
g.. 110.7440 随机规划
h.. 110.7445 排队论
i.. 110.7450 对策论 亦称博奕论
j.. 110.7455 库存论
k.. 110.7460 决策论
l.. 110.7465 搜索论
m.. 110.7470 图论
n.. 110.7475 统筹论
o.. 110.7480 最优化
p.. 110.7499 运筹学其他学科
v.. 110.77 组合数学
w.. 110.81 离散数学
x.. 110.84 模糊数学
y.. 110.87 应用数学 具体应用入有关学科
z.. 110.99 数学其他学科
楼宇的迭代式开发
谈到架构,人们最常想到的就是建筑。这得感谢亚历山大大叔的功劳。另外,设计模式的成功引入到软件工程,也是一个促进。这个成功案例让很多人都去拜读《建筑之永恒之道》。
最近在讨论中,突然发现楼房在构建的时候,大部分都是采用迭代式开发的。感觉甚是有意义,特拿出来和大家分享。
我们将楼房中的每一个房屋比喻成软件中的功能。那么在开发过程中,如何计划这些功能的开发顺序,以及每一个功能的开发步骤,就是一个非常大的学问。相信每一个经历过项目的人都知道。安排不好,就会导致无谓的相互等待。
我们以前默认的工作方式是,将所有的功能模块,全部分出去。先做区域性划分。比如,什么类型的模块,由什么小组来负责。然后在此基础上,每个小组再安排自己的模块。因为人手一定是比模块数少的多的,所以必然有一部分认为不重要的模块被安排在后面。
请注意上面的模块安排方式。整个功能的安排,是根据开发小组的特性来安排的,加上层级的划分,对于软件工程整体计划的把握性越来越低。特别是项目在每一个时刻的整体状况不好判断。你说写完了一半的模块的软件是什么状况?不好说吧。我们一般可能会采用另外一种容易接受的方式来描述:
“嗯,现在的软件可以完成基本功能了。下周就可以交付客户验证业务流程了。”
这种描述,由于是从最终结果来描述的,所以往往能被领导和客户所接受。如果你细心点的话,你就会发现这里面有一个问题,如何衡量进度?软件工程目前并不成熟,所以才导致进度衡量非常麻烦。其中最麻烦的就是软件模块的整合。每一个人对于整合都是持怀疑态度的。观察一下其他成熟行业,特别是建筑行业,高度的规格化建设,保障了其在整合的时候的低风险。有一句话常常说,工业化就是规范化。一个软件公司或一个软件项目,如果做不到这些规范化,那么就会一直担心整合。对于进度的把握就会存在问题。
废话少说,我们看看楼宇的迭代过程。先来做基础,越高的房子,地基越要打号。再做框架部分,一般是浇筑钢筋混凝土;然后填补减力墙;水、电管道;装饰装修。显然,每一次里程碑的划分,并没有按照模块的多少,而是按照大楼的整体结构。
我们软件做不到类似建筑这样成熟的划分。但是我认为可以从需求角度,进行几个里程碑式的划分。
第一、完成系统基础技术的工作。类似于建筑的地基。
第二、完成系统中功能组合关系的框架。所有的功能都可以在这个框架下进行增加、调试。这里面要完成系统在架构过程中要求的模型及各种应对业务变化的模式。
第三、完成系统的基本功能。什么是基本功能?软件的目的是解决问题,如果能用使用我们的软件,虽然比较繁琐,但是已经可以解决问题了。那就是完成了基本功能。比如一个邮件系统,已经可以发送收取邮件了,那么基本功能就完成了。
第四、完成基础易用性需求。什么较基础易用性?我们软件在业务分析的时候,必然考虑了用户的一些基本需求。这时候已经是一个完整的软件了。但是没有什么亮点。
第五、完整实现系统的规划。将最后展现给用户的功能完全实现。其实用户只看到这些,其他的他们都不是很在乎。当然了,虽然他们会一直使用。就比如你住的房子,你最关注的是房子里的装修。但是如果没有水、电,房子有危险,你肯定会非常不满意。
我们一直在说迭代,但是基于迭代的方法及步骤却没有细化下来。建议我们来多做做这方面的知识积累。大家也多分享一些迭代的过程,共同成长。上面的里程碑划分,是参考建筑来做的,我在实践中并没有完全按照上面的方式,一般遇到第四个里程碑的时候,就会出现混乱。大家想法不容易在一个层次上集中。不过总算是一个想法吧。