学习
一个持续不断的过
经验
一个不断积累的过程


目录

1. C/C++ 8
1.1. 内存管理机制 8
1.1.1. Java的内存管理机制 8
1.1.2. C++内存存储区域 8
1.1.3. 内存对齐 8
1.1.4. 堆与栈 12
1.1.5. 指针 14
1.1.6. 数组 14
1.1.7. Union的使用 14
1.1.8. 虚继承与内联函数 14
1.1.9. 虚函数、抽象函数与继承的问题 14
1.1.10. 为什么返回局部变量引用不好 14
1.1.11. sizeof 14
1.1.12. typedef 16
1.1.13. Static的意义 17
1.1.14. Const的用法 17
1.1.15. 值类型与引用类型 18
1.2. 面向对象的基本思想 18
1.2.1. 面向对象的主要思想 18
1.2.2. 封装 18
1.2.3. 继承 18
1.2.4. 多态 18
1.2.5. 重载与override 19
1.2.6. 接口与类 20
1.2.7. 抽象类与接口 20
1.2.8. 结构与类 21
1.2.9. 联合与类 22
1.2.10. 结构 22
1.2.11. 枚举 28
1.3. 一般问题 30
1.3.1. C++二元运算符的两种实现方式 30
1.3.2. 线程同步的三种方法 32
2. 数据结构 33
2.1. 2.1二叉树 33
2.1.1. 对分查找二叉树 33
2.2. 链表 35
2.2.1. 创建链表 35
2.2.2. 链表的反向输出 36
2.2.3. 连接两个链表 37
2.3. 数组 39
2.4. 队列 39
2.4.1. 存储结构 39
2.4.2. 循环队列 39
2.4.3. 排序 39
2.5. 堆 39
2.5.1. 最大值堆 39
2.5.2. private void siftdown(int position) { 40
2.5.3. public void insert(Element element) { 41
2.5.4. public Object remove(int position) { 41
2.6. 字符串 42
2.6.1. 字符串排序 42
2.6.2. 计算字符串中子串出现次数 43
2.6.3. 字符串拷贝函数strcpy() 43
2.6.4. 内存拷贝函数memcpy() 44
2.6.5. 子字符串查找strstr() 44
2.7. 排序理论 44
2.7.1. 冒泡法 46
2.7.2. 交换法 47
2.7.3. 选择法 48
2.7.4. 插入法 49
2.7.5. 快速排序 51
2.7.6. 双向冒泡 52
2.7.7. SHELL排序 53
2.7.8. 基于模板的通用排序 55
2.8. 常用算法分析 58
2.8.1. 反转字符串 58
2.8.2. 字符串连接 58
2.8.3. Strstr() 58
2.8.4. Void *memcpy(void *dest, const void *src, size_t count) 58
2.8.5. 七孔桥问题 58
2.8.6. 链表逆序 59
2.8.7. 质数 59
2.8.8. 素数 59
2.8.9. 最大公约数与最小公倍数 60
2.8.10. 分解质因数 60
2.8.11. 闰年的判断方法 60
2.8.12. Atoi 60
2.8.13. 十进制转16进制 61
2.8.14. 十进制转二进制 61
2.8.15. 八进制转十进制 61
3. 网络 62
3.1. 基础理论 62
3.1.1. UDP连接和TCP连接的异同 62
3.1.2. 什么是 socket 62
3.1.3. Internet 套接字的两种类型 63
3.1.4. 网络理论 63
3.1.5. 结构体 64
3.1.6. 本机转换 65
3.1.7. IP 地址和如何处理它们 66
3.1.8. socket()函数 67
3.1.9. bind()函数 67
3.1.10. connect()程序 68
3.1.11. listen()函数 69
3.1.12. accept()函数 70
3.1.13. send() and recv()函数 71
3.1.14. sendto() 和 recvfrom()函数 72
3.1.15. close()和shutdown()函数 72
3.1.16. getpeername()函数 73
3.1.17. gethostname()函数 73
3.1.18. 域名服务(DNS) 73
3.1.19. 客户-服务器背景知识 75
3.1.20. 简单的服务器 75
3.1.21. 简单的客户程序 77
3.1.22. 数据包 Sockets 78
3.1.23. 阻塞 81
3.1.24. select()--多路同步 I/O 82
4. .Net学习 84
4.1. 基础理论 84
4.1.1. 托管代码 84
4.1.2. CLR 84
4.1.3. CLS 84
4.1.4. CTS 84
4.1.5. GAC 84
4.1.6. 公共语言运行库 84
4.1.7. 中间语言的特性 87
4.1.8. 内存管理 87
4.1.9. 配件 94
4.1.10. Run time environment的应用领域 94
4.1.11. 装箱与拆箱 94
4.1.12. 委托与代理 94
4.1.13. 垃圾回收机制 95
4.1.14. 错误处理机制 95
4.1.15. 应用程序域 96
4.1.16. 预处理 96
4.1.17. C#中的引用类型 99
4.1.18. C#中的class与interface 99
4.1.19. C#中的class和struct 100
4.1.20. 中间语言的特征 100
4.1.21. 进程与线程 100
4.1.22. Code-Behind技术 100
4.1.23. 活动目录 100
4.1.24. 反射 101
4.1.25. 序列化 104
4.1.26. Partial class 104
4.1.27. 内部类 107
4.1.28. 内部类与匿名类 110
4.1.29. O/R Mapping的工作原理 113
4.1.30. .Net Remoting工作原理 113
4.2. ASP.Net 113
4.2.1. 用户控件 113
4.2.2. 身份验证 113
4.2.3. 页面间传值的方法 114
4.2.4. 服务器控件的生命周期 114
4.2.5. Session的bug及解决方法 114
4.2.6. XML 与 HTML 的主要区别 114
4.3. ADO.Net 115
4.3.1. 读写数据库常用类 115
4.3.2. ADO.Net中的常用对象 115
4.3.3. DataGrid的数据源类型 115
4.3.4. 共享类与数据库特定类 116
4.4. Web services 116
4.4.1. UDDI 116
4.4.2. WSDL 117
4.4.3. SOAP 118
4.4.4. WSE 118
4.4.5. 调用Web Service的方法 118
4.5. 多线程 118
4.5.1. 线程池 118
4.5.2. 线程同步 121
4.5.3. 异步调用 142
4.5.4. 进程与线程 151
4.5.5. sleep() 和 wait() 151
4.6. 一般问题 152
4.6.1. 有用的工具 152
4.6.2. using和new的意义 152
4.6.3. C#的五种可访问性 152
4.6.4. C#的特点 152
4.6.5. 对象间可能存在的三种关系 153
4.6.6. Main()方法 154
4.6.7. 构造函数与析构函数 156
4.6.8. 关键字this 164
4.6.9. ref和out 165
4.6.10. HashMap和Hashtable 166
4.6.11. error和exception 166
4.6.12. Array复制到ArrayList 166
4.6.13. 属性和索引器 167
4.6.14. C#修饰符 174
4.6.15. final, finally, finalize 174
4.6.16. Sealed 175
5. 5数据库学习 175
5.1. 基础理论 175
5.1.1. 数据库的隔离级别 175
5.1.2. Transaction执行时是否可以共享读 176
5.1.3. 数据库范式 176
5.1.4. 索引 177
5.1.5. NULL 177
5.1.6. 主键与外键 177
5.1.7. 触发器 177
5.1.8. Check限制 177
5.1.9. 返回参数和OUTPUT参数 177
5.1.10. 相关子查询 178
5.1.11. 获得最后更新的事务号 178
5.1.12. 取出表A中第31到第40记录 178
5.1.13. 如何处理几十万条并发数据 178
5.1.14. SQL注入 178
6. 项目管理 178
7. 软件测试 178

1. C/C++
C与C++理论中包括相关的理论知识

1.1. 内存管理机制
1.1.1. Java的内存管理机制

1.1.2. C++内存存储区域
在C++中,内存分成5个区,他们分别是堆、栈、自由存储区、全局/静态存储区和常量存储区。
栈,就是那些由编译器在需要的时候分配,在不需要的时候自动清楚的变量的存储区。里面的变量通常是局部变量、函数参数等。
堆,就是那些由new分配的内存块,他们的释放编译器不去管,由我们的应用程序去控制,一般一个new就要对应一个delete。如果程序员没有释放掉,那么在程序结束后,操作系统会自动回收。
自由存储区,就是那些由malloc等分配的内存块,他和堆是十分相似的,不过它是用free来结束自己的生命的。
全局/静态存储区,全局变量和静态变量被分配到同一块内存中,在以前的C语言中,全局变量又分为初始化的和未初始化的,在C++里面没有这个区分了,他们共同占用同一块内存区。
常量存储区,这是一块比较特殊的存储区,他们里面存放的是常量,不允许修改(当然,你要通过非正当手段也可以修改,而且方法很多,在《const的思考》一文中,我给出了6种方法)
1.1.3. 内存对齐
考虑下面的结构:
         struct foo
         {
           char c1;
           short s;
           char c2;
           int i;
          };
     假设这个结构的成员在内存中是紧凑排列的,假设c1的地址是0,那么s的地址就应该是1,c2的地址就是3,i的地址就是4。也就是
    c1 00000000, s 00000001, c2 00000003, i 00000004。
    可是,我们在Visual c/c++ 6中写一个简单的程序:
         struct foo a;
    printf("c1 %p, s %p, c2 %p, i %p\n",
        (unsigned int)(void*)&a.c1 - (unsigned int)(void*)&a,
        (unsigned int)(void*)&a.s - (unsigned int)(void*)&a,
        (unsigned int)(void*)&a.c2 - (unsigned int)(void*)&a,
        (unsigned int)(void*)&a.i - (unsigned int)(void*)&a);
    运行,输出:
         c1 00000000, s 00000002, c2 00000004, i 00000008。
    为什么会这样?这就是内存对齐而导致的问题。
为什么会有内存对齐
    以下内容节选自《Intel Architecture 32 Manual》。
    字,双字,和四字在自然边界上不需要在内存中对齐。(对字,双字,和四字来说,自然边界分别是偶数地址,可以被4整除的地址,和可以被8整除的地址。)
    无论如何,为了提高程序的性能,数据结构(尤其是栈)应该尽可能地在自然边界上对齐。原因在于,为了访问未对齐的内存,处理器需要作两次内存访问;然而,对齐的内存访问仅需要一次访问。
    一个字或双字操作数跨越了4字节边界,或者一个四字操作数跨越了8字节边界,被认为是未对齐的,从而需要两次总线周期来访问内存。一个字起始地址是奇数但却没有跨越字边界被认为是对齐的,能够在一个总线周期中被访问。
    某些操作双四字的指令需要内存操作数在自然边界上对齐。如果操作数没有对齐,这些指令将会产生一个通用保护异常(#GP)。双四字的自然边界是能够被16整除的地址。其他的操作双四字的指令允许未对齐的访问(不会产生通用保护异常),然而,需要额外的内存总线周期来访问内存中未对齐的数据。
编译器对内存对齐的处理
    缺省情况下,c/c++编译器默认将结构、栈中的成员数据进行内存对齐。因此,上面的程序输出就变成了:
c1 00000000, s 00000002, c2 00000004, i 00000008。
编译器将未对齐的成员向后移,将每一个都成员对齐到自然边界上,从而也导致了整个结构的尺寸变大。尽管会牺牲一点空间(成员之间有空洞),但提高了性能。
也正是这个原因,我们不可以断言sizeof(foo) == 8。在这个例子中,sizeof(foo) == 12。
如何避免内存对齐的影响
    那么,能不能既达到提高性能的目的,又能节约一点空间呢?有一点小技巧可以使用。比如我们可以将上面的结构改成:
struct bar
{
    char c1;
    char c2;
    short s;
    int i;
};
    这样一来,每个成员都对齐在其自然边界上,从而避免了编译器自动对齐。在这个例子中,sizeof(bar) == 8。
    这个技巧有一个重要的作用,尤其是这个结构作为API的一部分提供给第三方开发使用的时候。第三方开发者可能将编译器的默认对齐选项改变,从而造成这个结构在你的发行的DLL中使用某种对齐方式,而在第三方开发者哪里却使用另外一种对齐方式。这将会导致重大问题。
    比如,foo结构,我们的DLL使用默认对齐选项,对齐为
c1 00000000, s 00000002, c2 00000004, i 00000008,同时sizeof(foo) == 12。
而第三方将对齐选项关闭,导致
    c1 00000000, s 00000001, c2 00000003, i 00000004,同时sizeof(foo) == 8。
如何使用c/c++中的对齐选项
    vc6中的编译选项有 /Zp[1|2|4|8|16] ,/Zp1表示以1字节边界对齐,相应的,/Zpn表示以n字节边界对齐。n字节边界对齐的意思是说,一个成员的地址必须安排在成员的尺寸的整数倍地址上或者是n的整数倍地址上,取它们中的最小值。也就是:
    min ( sizeof ( member ),  n)
    实际上,1字节边界对齐也就表示了结构成员之间没有空洞。
    /Zpn选项是应用于整个工程的,影响所有的参与编译的结构。
    要使用这个选项,可以在vc6中打开工程属性页,c/c++页,选择Code Generation分类,在Struct member alignment可以选择。
    要专门针对某些结构定义使用对齐选项,可以使用#pragma pack编译指令。指令语法如下:
#pragma pack( [ show ] | [ push | pop ] [, identifier ] , n  )
    意义和/Zpn选项相同。比如:
#pragma pack(1)
struct foo_pack
{
    char c1;
    short s;
    char c2;
    int i;
};
#pragma pack()
栈内存对齐
    我们可以观察到,在vc6中栈的对齐方式不受结构成员对齐选项的影响。(本来就是两码事)。它总是保持对齐,而且对齐在4字节边界上。
验证代码
#include <stdio.h>
struct foo
{
    char c1;
    short s;
    char c2;
    int i;
};
struct bar
{
    char c1;
    char c2;
    short s;
    int i;
};
#pragma pack(1)
struct foo_pack
{
    char c1;
    short s;
    char c2;
    int i;
};
#pragma pack()
int main(int argc, char* argv[])
{
    char c1;
    short s;
    char c2;
    int i;

    struct foo a;
    struct bar b;
    struct foo_pack p;
    printf("stack c1 %p, s %p, c2 %p, i %p\n",
        (unsigned int)(void*)&c1 - (unsigned int)(void*)&i,
        (unsigned int)(void*)&s - (unsigned int)(void*)&i,
        (unsigned int)(void*)&c2 - (unsigned int)(void*)&i,
        (unsigned int)(void*)&i - (unsigned int)(void*)&i);

    printf("struct foo c1 %p, s %p, c2 %p, i %p\n",
        (unsigned int)(void*)&a.c1 - (unsigned int)(void*)&a,
        (unsigned int)(void*)&a.s - (unsigned int)(void*)&a,
        (unsigned int)(void*)&a.c2 - (unsigned int)(void*)&a,
        (unsigned int)(void*)&a.i - (unsigned int)(void*)&a);

    printf("struct bar c1 %p, c2 %p, s %p, i %p\n",
        (unsigned int)(void*)&b.c1 - (unsigned int)(void*)&b,
        (unsigned int)(void*)&b.c2 - (unsigned int)(void*)&b,
        (unsigned int)(void*)&b.s - (unsigned int)(void*)&b,
        (unsigned int)(void*)&b.i - (unsigned int)(void*)&b);

    printf("struct foo_pack c1 %p, s %p, c2 %p, i %p\n",
        (unsigned int)(void*)&p.c1 - (unsigned int)(void*)&p,
        (unsigned int)(void*)&p.s - (unsigned int)(void*)&p,
        (unsigned int)(void*)&p.c2 - (unsigned int)(void*)&p,
        (unsigned int)(void*)&p.i - (unsigned int)(void*)&p);
    printf("sizeof foo is %d\n", sizeof(foo));
    printf("sizeof bar is %d\n", sizeof(bar));
    printf("sizeof foo_pack is %d\n", sizeof(foo_pack));
    return 0;
}
 vc6中的编译选项有 /Zp[1|2|4|8|16] ,/Zp1表示以1字节边界对齐,相应的,/Zpn表示以n字节边界对齐。
n字节边界对齐的意思是说,一个成员的地址必须安排在成员的尺寸的整数倍地址上或者是n的整数倍地址上,取它们中的最小值。也就是:
    min ( sizeof ( member ),  n)
    实际上,1字节边界对齐也就表示了结构成员之间没有空洞。
/*1字节边界对齐表示结构成员之间没有空隙,个个成员变量在内存中是紧密排列的*/
    /Zpn选项是应用于整个工程的,影响所有的参与编译的结构。
1.1.4. 堆与栈
首先,我们举一个例子:
void f() { int* p=new int[5]; }
这条短短的一句话就包含了堆与栈,看到new,我们首先就应该想到,我们分配了一块堆内存,那么指针p呢?他分配的是一块栈内存,所以这句话的意思就是:在栈内存中存放了一个指向一块堆内存的指针p。在程序会先确定在堆中分配内存的大小,然后调用operator new分配内存,然后返回这块内存的首地址,放入栈中,他在VC6下的汇编代码如下:
00401028 push 14h
0040102A call operator new (00401060)
0040102F add esp,4
00401032 mov dword ptr [ebp-8],eax
00401035 mov eax,dword ptr [ebp-8]
00401038 mov dword ptr [ebp-4],eax
  这里,我们为了简单并没有释放内存,那么该怎么去释放呢?是delete p么?澳,错了,应该是delete []p,这是为了告诉编译器:我删除的是一个数组,VC6就会根据相应的Cookie信息去进行释放内存的工作。
好了,我们回到我们的主题:堆和栈究竟有什么区别?
主要的区别由以下几点:
1、管理方式不同;
2、空间大小不同;
3、能否产生碎片不同;
4、生长方向不同;
5、分配方式不同; 
6、分配效率不同;  
管理方式:对于栈来讲,是由编译器自动管理,无需我们手工控制;对于堆来说,释放工作由程序员控制,容易产生memory leak。
空间大小:一般来讲在32位系统下,堆内存可以达到4G的空间,从这个角度来看堆内存几乎是没有什么限制的。但是对于栈来讲,一般都是有一定的空间大小的,例如,在VC6下面,默认的栈空间大小是1M(好像是,记不清楚了)。当然,我们可以修改:打开工程,依次操作菜单如下:Project->Setting->Link,在Category 中选中Output,然后在Reserve中设定堆栈的最大值和commit。注意:reserve最小值为4Byte;commit是保留在虚拟内存的页文件里面,它设置的较大会使栈开辟较大的值,可能增加内存的开销和启动时间。
碎片问题:对于堆来讲,频繁的new/delete势必会造成内存空间的不连续,从而造成大量的碎片,使程序效率降低。对于栈来讲,则不会存在这个问题,因为栈是先进后出的队列,他们是如此的一一对应,以至于永远都不可能有一个内存块从栈中间弹出,在他弹出之前,在他上面的后进的栈内容已经被弹出,详细的可以参考数据结构,这里我们就不再一一讨论了。
生长方向:对于堆来讲,生长方向是向上的,也就是向着内存地址增加的方向;对于栈来讲,它的生长方向是向下的,是向着内存地址减小的方向增长。
分配方式:堆都是动态分配的,没有静态分配的堆。栈有2种分配方式:静态分配和动态分配。静态分配是编译器完成的,比如局部变量的分配。动态分配由alloca函数进行分配,但是栈的动态分配和堆是不同的,他的动态分配是由编译器进行释放,无需我们手工实现。
分配效率:栈是机器系统提供的数据结构,计算机会在底层对栈提供支持:分配专门的寄存器存放栈的地址,压栈出栈都有专门的指令执行,这就决定了栈的效率比较高。堆则是C/C++函数库提供的,它的机制是很复杂的,例如为了分配一块内存,库函数会按照一定的算法(具体的算法可以参考数据结构/操作系统)在堆内存中搜索可用的足够大小的空间,如果没有足够大小的空间(可能是由于内存碎片太多),就有可能调用系统功能去增加程序数据段的内存空间,这样就有机会分到足够大小的内存,然后进行返回。显然,堆的效率比栈要低得多。
从这里我们可以看到,堆和栈相比,由于大量new/delete的使用,容易造成大量的内存碎片;由于没有专门的系统支持,效率很低;由于可能引发用户态和核心态的切换,内存的申请,代价变得更加昂贵。所以栈在程序中是应用最广泛的,就算是函数的调用也利用栈去完成,函数调用过程中的参数,返回地址,EBP和局部变量都采用栈的方式存放。所以,我们推荐大家尽量用栈,而不是用堆。
虽然栈有如此众多的好处,但是由于和堆相比不是那么灵活,有时候分配大量的内存空间,还是用堆好一些。
无论是堆还是栈,都要防止越界现象的发生(除非你是故意使其越界),因为越界的结果要么是程序崩溃,要么是摧毁程序的堆、栈结构,产生以想不到的结果,就算是在你的程序运行过程中,没有发生上面的问题,你还是要小心,说不定什么时候就崩掉,那时候debug可是相当困难的:)
1.1.5. 指1.1.6. 针
指针可以随时指向任意类型的内存块,它的特征是“可变”,所以我们常用指针来操作动态内存。指针远比数组灵活,但也更危险。

1.1.7. 数组
数组要么在静态存储区被创建(如全局数组),要么在栈上被创建。数组名对应着(而不是指向)一块内存,其地址与容量在生命期内保持不变,只有数组的内容可以改变。


1.1.8. Union的使用
1.1.9. 虚继承与内联函数

1.1.10. 虚函数、抽象函数与继承的问题
虚函数:没有实现的,可由子类继承并重写的函数
抽象函数:规定其非虚子类必须实现的函数,必须被重写
1.1.11. 为什么返回局部变量引用不1.1.12. 好
1.1.13. sizeof
一、sizeof的概念
  sizeof是C语言的一种单目操作符,如C语言的其他操作符++、--等。它并不是函数。sizeof操作符以字节形式给出了其操作数的存储大小。操作数可以是一个表达式或括在括号内的类型名。操作数的存储大小由操作数的类型决定。
二、sizeof的使用方法
  1、用于数据类型
  sizeof使用形式:sizeof(type)
  数据类型必须用括号括住。如sizeof(int)。
  2、用于变量
  sizeof使用形式:sizeof(var_name)或sizeof var_name
  变量名可以不用括号括住。如sizeof (var_name),sizeof var_name等都是正确形式。带括号的用法更普遍,大多数程序员采用这种形式。
  注意:sizeof操作符不能用于函数类型,不完全类型或位字段。不完全类型指具有未知存储大小的数据类型,如未知存储大小的数组类型、未知内容的结构或联合类型、void类型等。
  如sizeof(max)若此时变量max定义为int max(),sizeof(char_v) 若此时char_v定义为char char_v [MAX]且MAX未知,sizeof(void)都不是正确形式。
三、sizeof的结果
  sizeof操作符的结果类型是size_t,它在头文件<stddef.h>中typedef为unsigned int类型。该类型保证能容纳实现所建立的最大对象的字节大小。
  1、若操作数具有类型char、unsigned char或signed char,其结果等于1。
  ANSI C正式规定字符类型为1字节。
  2、int、unsigned int 、short int、unsigned short 、long int 、unsigned long 、float、double、long double类型的sizeof 在ANSI C中没有具体规定,大小依赖于实现,一般可能分别为2、2、2、2、4、4、4、8、10。
  3、当操作数是指针时,sizeof依赖于编译器。例如Microsoft C/C++7.0中,near类指针字节数为2,far、huge类指针字节数为4。一般Unix的指针字节数为4。
  4、当操作数具有数组类型时,其结果是数组的总字节数。
  5、联合类型操作数的sizeof是其最大字节成员的字节数。结构类型操作数的sizeof是这种类型对象的总字节数,包括任何垫补在内。
  让我们看如下结构:
  struct {char b; double x;} a;
  在某些机器上sizeof(a)=12,而一般sizeof(char)+ sizeof(double)=9。
  这是因为编译器在考虑对齐问题时,在结构中插入空位以控制各成员对象的地址对齐。如double类型的结构成员x要放在被4整除的地址。
  6、如果操作数是函数中的数组形参或函数类型的形参,sizeof给出其指针的大小。
四、sizeof与其他操作符的关系
  sizeof的优先级为2级,比/、%等3级运算符优先级高。它可以与其他操作符一起组成表达式。如i*sizeof(int);其中i为int类型变量。
五、sizeof的主要用途
  1、sizeof操作符的一个主要用途是与存储分配和I/O系统那样的例程进行通信。例如:
  void *malloc(size_t size),
  size_t fread(void * ptr,size_t size,size_t nmemb,FILE * stream)。
  2、sizeof的另一个的主要用途是计算数组中元素的个数。例如:
  void * memset(void * s,int c,sizeof(s))。
六、建议
  由于操作数的字节数在实现时可能出现变化,建议在涉及到操作数字节大小时用ziseof来代替常量计算。
1.1.14. typedef
1. 基本解释
  typedef为C语言的关键字,作用是为一种数据类型定义一个新名字。这里的数据类型包括内部数据类型(int,char等)和自定义的数据类型(struct等)。
  在编程中使用typedef目的一般有两个,一个是给变量一个易记且意义明确的新名字,另一个是简化一些比较复杂的类型声明。
  至于typedef有什么微妙之处,请你接着看下面对几个问题的具体阐述。
  2. typedef & 结构的问题
  当用下面的代码定义一个结构时,编译器报了一个错误,为什么呢?莫非C语言不允许在结构中包含指向它自己的指针吗?请你先猜想一下,然后看下文说明:
typedef struct tagNode
{
 char *pItem;
 pNode pNext;
} *pNode; 
  答案与分析:
  1、typedef的最简单使用
typedef long byte_4;
  给已知数据类型long起个新名字,叫byte_4。
  2、 typedef与结构结合使用
typedef struct tagMyStruct
{
 int iNum;
 long lLength;
} MyStruct;
  这语句实际上完成两个操作:
  1) 定义一个新的结构类型
struct tagMyStruct
{
 int iNum;
 long lLength;
};
  分析:tagMyStruct称为“tag”,即“标签”,实际上是一个临时名字,struct 关键字和tagMyStruct一起,构成了这个结构类型,不论是否有typedef,这个结构都存在。
  我们可以用struct tagMyStruct varName来定义变量,但要注意,使用tagMyStruct varName来定义变量是不对的,因为struct 和tagMyStruct合在一起才能表示一个结构类型。
  2) typedef为这个新的结构起了一个名字,叫MyStruct。
typedef struct tagMyStruct MyStruct;
  因此,MyStruct实际上相当于struct tagMyStruct,我们可以使用MyStruct varName来定义变量。
  答案与分析
  C语言当然允许在结构中包含指向它自己的指针,我们可以在建立链表等数据结构的实现上看到无数这样的例子,上述代码的根本问题在于typedef的应用。
  根据我们上面的阐述可以知道:新结构建立的过程中遇到了pNext域的声明,类型是pNode,要知道pNode表示的是类型的新名字,那么在类型本身还没有建立完成的时候,这个类型的新名字也还不存在,也就是说这个时候编译器根本不认识pNode。
  解决这个问题的方法有多种:
  1)、
typedef struct tagNode
{
 char *pItem;
 struct tagNode *pNext;
} *pNode;
  2)、
typedef struct tagNode *pNode;
struct tagNode
{
 char *pItem;
 pNode pNext;
};
  注意:在这个例子中,你用typedef给一个还未完全声明的类型起新名字。C语言编译器支持这种做法。
  3)、规范做法:
struct tagNode
{
 char *pItem;
 struct tagNode *pNext;
};
typedef struct tagNode *pNode;
1.1.15. Static的意义
1.1.16. Const的用法

1.1.17. 值类型与引用类型
值类型的变量本身包含他们的数据,而引用类型的变量包含的是指向包含数据的内存块的引用或叫句柄。
值类型变量存储在堆栈。每个程序在执行时都有自己的堆栈,其他程序不能访问。
引用类型存储在堆。引用类型存贮实际数据的引用值的地址。
C#中的引用类型有4种(类、代表、数组、接口)
所有的值类型均隐式派生自 System.ValueType。
与引用类型不同,从值类型不可能派生出新的类型。但与引用类型相同的是,结构也可以实现接口。
与引用类型不同,值类型不可能包含 null 值。然而,可空类型功能允许将 null 赋给值类型。
每种值类型均有一个隐式的默认构造函数来初始化该类型的默认值。
值类型主要由两类组成:结构、枚举
结构分为以下几类:Numeric(数值)类型、整型、浮点型、decimal、bool、用户定义的结构。
引用类型的变量又称为对象,可存储对实际数据的引用。声明引用类型的关键字:class、interface、delegate、内置引用类型: object、string


1.2. 面向对象的基本思想
1.2.1. 面向对象的主要思想
封装:用抽象的数据类型将数据和基于数据的操作封装在一起,数据被保护在抽象数据类型内部。
继承:子类拥有父类的所有数据和操作。
多态:一个程序中同名的不同方法共存的情况。有两种形式的多态– 重载与重写。
1.2.2. 封装

1.2.3. 继承

1.2.4. 多态
从广义上说,多态性是指一段程序能够处理多种类型对象的能力。在C++语言中,这种多态性可以通过强制多态、重载多态、类型参数化多态、包含多态4种形式来实现。类型参数化多态和包含多态统称为一般多态性,用来系统地刻画语义上相关的一组类型。重载多态和强制多态统称为特殊多态性,用来刻画语义上无关联的类型间的关系。
包含多态是指通过子类型化,1个程序段既能处理类型T的对象,也能够处理类型T的子类型S的对象,该程序段称为多态程序段。公有继承能够实现子类型。在包含多态中,1个对象可以被看作属于不同的类,其间包含关系的存在意味着公共结构的存在。包含多态在不少语言中存在,如整数类型中的子集构成1个子类型。每一个子类型中的对象可以被用在高一级的类型中,高一级类型中的所有操作可用于下一级的对象。在C++中公有继承关系是一种包含多态,每一个类可以直接公有继承父类或多个父类,如语句class Dpublic P1,public P2{……};表示类D分别是类P1和类P2的子类型。
类型参数化多态是指当1个函数(类)统一地对若干类型参数操作时,这些类型表现出某些公共的语义特性,而该函数(类)就是用来描述该特性的。在类型参数化多态中,1个多态函数(类)必须至少带有1个类型参数,该类型参数确定函数(类)在每次执行时操作数的类型。这种函数(类)也称类属函数(类)。类型参数化多态的应用较广泛,被称为最纯的多态。
重载是指用同一个名字命名不同的函数或操作符。函数重载是C++对一般程序设计语言中操作符重载机制的扩充,它可使具有相同或相近含义的函数用相同的名字,只要其参数的个数、次序或类型不一样即可。例如:
  int min(int x,int y);     //求2个整数的最小数
  int min(int x,int y,int z); //求3个整数的最小数
  int min(int n,int a[]);  //求n个整数的最小数
  当用户要求增加比较2个字符串大小的功能时,只需增加:
char*min(char*,char*);

而原来如何使用这组函数的逻辑不需改变,min的功能扩充很容易,也就是说维护比较容易,同时也提高了程序的可理解性,“min”表示求最小值的函数。
强制是指将一种类型的值转换成另一种类型的值进行的语义操作,从而防止类型错误。类型转换可以是隐式的,在编译时完成,如语句D=I把整型变量转换为实型;也可以是显式的,可在动态运行时完成。
从总体上来说,一般多态性是真正的多态性;特殊多态性只是表面的多态性。因为重载只允许某一个符号有多种类型,而它所代表的值分别具有不同的、不相兼容的类型。类似地,隐式类型转换也不是真正的多态,因为在操作开始前,各值必须转换为要求的类型,而输出类型也与输入类型无关。相比之下,子类与继承却是真正的多态。类型参数化多态也是一种纯正的多态,同一对象或函数在不同的类型上下文中统一地使用而不需采用隐式类型转换、运行时检测或其它各种限制。
1.2.5. 重载与override
重载是指针对所继承下来的方法,重新设计其处理方式,为将来原本处理方式覆盖过去。
在派生类要覆盖的方法前加override修饰,而基类的同名方法前加virtual修饰。这样就能实现多态。多态指一个程序中同名的不同方法共存的情况。  有两种形式的多态– 重载与重写。

1、方法的覆盖是子类和父类之间的关系,是垂直关系;方法的重载是同一个类中方法之间的关系,是水平关系
2、覆盖只能由一个方法,或只能由一对方法产生关系;方法的重载是多个方法之间的关系。
3、覆盖要求参数列表相同;重载要求参数列表不同。
4、覆盖关系中,调用那个方法体,是根据对象的类型(对象对应存储空间类型)来决定;重载关系,是根据调用时的实参表与形参表来选择方法体的。
1.2.6. 接口与类
什么是类?类可以这么理解.类就是功能的集合.类也可以看做是实现一种功能的集合或者方法.
接口的概念:什么是接口?接口可以理解为,对类的规定,对类的约束,甚至对整个项目的约束.其目的就是让这些方法可以作为接口实例被引用。
接口不能被实例化。接口是负责功能的定义,项目中通过接口来规范类,操作类以及抽象类的概念!而类是负责功能的具体实现!在类中也有抽象类的定义,
抽象类与接口的区别在于:抽象类是一个不完全的类,类里面有抽象的方法,属性,也可以有具体的方法和属性,需要进一步的专业化。但接口是一个行为的规范,里面的所有东西都是抽象的!一个类只可以继承一个基类也就是父类,但可以实现多个接口
1.2.7. 抽象类与接口
抽象类:
抽象类是特殊的类,只是不能被实例化;除此以外,具有类的其他特性;重要的是抽象类可以包括抽象方法,这是普通类所不能的。抽象方法只能声明于抽象类中,且不包含任何实现,派生类必须覆盖它们。
另外,抽象类可以派生自一个抽象类,可以覆盖基类的抽象方法也可以不覆盖,如果不覆盖,则其派生类必须覆盖它们。
接口:
接口是引用类型的,类似于类,更和抽象类有所相似,以至于很多人对抽象类和接口的区别比较模糊。和抽象类的相似之处有三点:
1、不能实例化;
2、包含未实现的方法声明;
3、派生类必须实现未实现的方法,抽象类是抽象方法,接口则是所有成员(不仅是方法包括其他成员);
另外,接口有如下特性:
接口除了可以包含方法之外,还可以包含属性、索引器、事件,而且这些成员都被定义为公有的。除此之外,不能包含任何其他的成员,例如:常量、域、构造函数、析构函数、静态成员。
一个类可以直接继承多个接口,但只能直接继承一个类(包括抽象类)。
注意!还有另外一种类不能被实例化:
所有构造函数都被标记为private,这种类也是不能被实例化的,严格的说是不能在类外被实例化,可以在此类的内部实例化(这种方式可以用于实现单件设计模式)。注意一点,这样的类也不能够作为基类来继承。
抽象类和接口的使用:
抽象类用于部分实现一个类,再由用户按需求对其进行不同的扩展和完善;接口只是定义一个行为的规范或规定。
抽象类在组件的所有实现间提供通用的已实现功能;接口创建在大范围全异对象间使用的功能。
抽象类主要用于关系密切的对象;而接口适合为不相关的类提供通用功能。
抽象类主要用于设计大的功能单元;而接口用于设计小而简练的功能块。

例如:
Window窗体可以用抽象类来设计,可以把公有操作和属性放到一个抽象类里,让窗体和对话框继承自这个抽象类,再根据自己的需求进行扩展和完善。
打印操作可以作为一个接口提供给每个需要此功能的窗体,因为窗体的内容不同,就要根据他们自己的要求去实现自己的打印功能。打印时只通过接口来调用,而不用在乎是那个窗体要打印。
1.2.8. 结构与类
值类型与引用类型
结构是值类型:值类型在堆栈上分配地址,所有的基类型都是结构类型,例如:int 对应System.int32 结构,string 对应 system.string 结构 ,通过使用结构可以创建更多的值类型
类是引用类型:引用类型在堆上分配地址
堆栈的执行效率要比堆的执行效率高,可是堆栈的资源有限,不适合处理大的逻辑复杂的对象。所以结构处理作为基类型对待的小对象,而类处理某个商业逻辑
因为结构是值类型所以结构之间的赋值可以创建新的结构,而类是引用类型,类之间的赋值只是复制引用
注:
1.虽然结构与类的类型不一样,可是他们的基类型都是对象(object),c#中所有类型的基类型都是object
2.虽然结构的初始化也使用了New 操作符可是结构对象依然分配在堆栈上而不是堆上,如果不使用“新建”(new),那么在初始化所有字段之前,字段将保持未赋值状态,且对象不可用
继承性
结构:不能从另外一个结构或者类继承,本身也不能被继承,虽然没有明确sealed声明,可结构是隐式的sealed .
类:完全可扩展的,除非显式声明sealed, 否则类可以继承其他类和接口,自身也能被继承 。注:虽然结构不能被继承 ,可结构能够继承接口,方法和类继承接口一样 。
例如:结构实现接口
  interface IImage{    
void Paint();

struct Picture : IImage{    
public void Paint()    {         // painting code goes here    }    
private int x, y, z;  // other struct members

内部结构的区别
结构:
没有默认的构造函数,可以添加构造函数
没有析构函数
没有 abstract 和 sealed(因为不能继承)
不能有protected 修饰符
可以不用new 初始化
在结构中初始化实例字段是错误的
类:
有默认的构造函数
有析构函数
可以使用 abstract 和 sealed
有protected 修饰符
必须使用new 初始化
如何选择结构还是类
讨论了结构与类的相同之处和差别之后,下面讨论如何选择使用结构还是类:
1.堆栈的空间有限,对于大量的逻辑的对象,创建类要比创建结构好一些
2.结构表示如点、矩形和颜色这样的轻量对象,例如,如果声明一个含有 1000 个点对象的数组,则将为引用每个对象分配附加的内存。在此情况下,结构的成本较低。
3.在表现抽象和多级别的对象层次时,类是最好的选择
4.大多数情况下该类型只是一些数据时,结构时最佳的选择
1.2.9. 联合与类
用联合也可以定义类
联合可包含函数和变量,还可包含构造函数和析构函数。所有的数据成员共享相同的存储地址。可节省空间。缺省存取级别是公有的。
但是使用联合时必须要注意:
    联合不能继承其他任何类型的类
    不能是基类,不能包含虚成员函数
    静态变量不能成为联合的成员
    如果一个对象有构造函数和析构函数,那么它不能成为联合的成员
    如果一个对象有重载操作符“=”,那么它不能成为联合成员
1.2.10. 结构
1. struct的巨大作用
  面对一个人的大型C/C++程序时,只看其对struct的使用情况我们就可以对其编写者的编程经验进行评估。因为一个大型的C/C++程序,势必要涉及一些(甚至大量)进行数据组合的结构体,这些结构体可以将原本意义属于一个整体的数据组合在一起。从某种程度上来说,会不会用struct,怎样用struct是区别一个开发人员是否具备丰富开发经历的标志。
  在网络协议、通信控制、嵌入式系统的C/C++编程中,我们经常要传送的不是简单的字节流(char型数组),而是多种数据组合起来的一个整体,其表现形式是一个结构体。
  经验不足的开发人员往往将所有需要传送的内容依顺序保存在char型数组中,通过指针偏移的方法传送网络报文等信息。这样做编程复杂,易出错,而且一旦控制方式及通信协议有所变化,程序就要进行非常细致的修改。
  一个有经验的开发者则灵活运用结构体,举一个例子,假设网络或控制协议中需要传送三种报文,其格式分别为packetA、packetB、packetC:
struct structA
{
int a;
char b;
};
struct structB
{
char a;
short b;
};
struct structC
{
int a;
char b;
float c;
}
  优秀的程序设计者这样设计传送的报文:
struct CommuPacket
{
int iPacketType;  //报文类型标志
union      //每次传送的是三种报文中的一种,使用union
{
  struct structA packetA;
  struct structB packetB;
  struct structC packetC;
}
};
  在进行报文传送时,直接传送struct CommuPacket一个整体。
  假设发送函数的原形如下:
// pSendData:发送字节流的首地址,iLen:要发送的长度
Send(char * pSendData, unsigned int  iLen);
发送方可以直接进行如下调用发送struct CommuPacket的一个实例sendCommuPacket:
Send( (char *)&sendCommuPacket , sizeof(CommuPacket) );
假设接收函数的原形如下:
// pRecvData:发送字节流的首地址,iLen:要接收的长度
//返回值:实际接收到的字节数
unsigned int Recv(char * pRecvData, unsigned int  iLen);
  接收方可以直接进行如下调用将接收到的数据保存在struct CommuPacket的一个实例recvCommuPacket中:
Recv( (char *)&recvCommuPacket , sizeof(CommuPacket) );
  接着判断报文类型进行相应处理:
switch(recvCommuPacket. iPacketType)
{
    case PACKET_A:
    …    //A类报文处理
    break;
    case PACKET_B:
    …   //B类报文处理
    break;
    case PACKET_C:
    …   //C类报文处理
    break;
}
  以上程序中最值得注意的是

Send( (char *)&sendCommuPacket , sizeof(CommuPacket) );
Recv( (char *)&recvCommuPacket , sizeof(CommuPacket) );
  中的强制类型转换:(char *)&sendCommuPacket、(char *)&recvCommuPacket,先取地址,再转化为char型指针,这样就可以直接利用处理字节流的函数。
  利用这种强制类型转化,我们还可以方便程序的编写,例如要对sendCommuPacket所处内存初始化为0,可以这样调用标准库函数memset():
memset((char *)&sendCommuPacket,0, sizeof(CommuPacket));
2. struct的成员对齐
  Intel、微软等公司曾经出过一道类似的面试题:
1. #include <iostream.h>
2. #pragma pack(8)
3. struct example1
4. {
5. short a;
6. long b;
7. };
8. struct example2
9. {
10. char c;
11. example1 struct1;
12. short e;   
13. };
14. #pragma pack()
15. int main(int argc, char* argv[])
16. {
17. example2 struct2;
18. cout << sizeof(example1) << endl;
19. cout << sizeof(example2) << endl;
20. cout << (unsigned int)(&struct2.struct1) - (unsigned int)(&struct2)
<< endl;
21. return 0;
22. }
  问程序的输入结果是什么?
  答案是:
8
16
4
  不明白?还是不明白?下面一一道来:
2.1 自然对界
  struct是一种复合数据类型,其构成元素既可以是基本数据类型(如int、long、float等)的变量,也可以是一些复合数据类型(如array、struct、union等)的数据单元。对于结构体,编译器会自动进行成员变量的对齐,以提高运算效率。缺省情况下,编译器为结构体的每个成员按其自然对界(natural alignment)条件分配空间。各个成员按照它们被声明的顺序在内存中顺序存储,第一个成员的地址和整个结构的地址相同。
  自然对界(natural alignment)即默认对齐方式,是指按结构体的成员中size最大的成员对齐。
  例如:
struct naturalalign
{
char a;
short b;
char c;
};
  在上述结构体中,size最大的是short,其长度为2字节,因而结构体中的char成员a、c都以2为单位对齐,sizeof(naturalalign)的结果等于6;
  如果改为:
struct naturalalign
{
char a;
int b;
char c;
};
  其结果显然为12。
2.2指定对界
  一般地,可以通过下面的方法来改变缺省的对界条件:
  · 使用伪指令#pragma pack (n),编译器将按照n个字节对齐;
  · 使用伪指令#pragma pack (),取消自定义字节对齐方式。
  注意:如果#pragma pack (n)中指定的n大于结构体中最大成员的size,则其不起作用,结构体仍然按照size最大的成员进行对界。
  例如:
#pragma pack (n)
struct naturalalign
{
char a;
int b;
char c;
};
#pragma pack ()
  当n为4、8、16时,其对齐方式均一样,sizeof(naturalalign)的结果都等于12。而当n为2时,其发挥了作用,使得sizeof(naturalalign)的结果为8。
  在VC++ 6.0编译器中,我们可以指定其对界方式,其操作方式为依次选择projetct > setting > C/C++菜单,在struct member alignment中指定你要的对界方式。
  另外,通过__attribute((aligned (n)))也可以让所作用的结构体成员对齐在n字节边界上,但是它较少被使用,因而不作详细讲解。
2.3 面试题的解答
  至此,我们可以对Intel、微软的面试题进行全面的解答。
  程序中第2行#pragma pack (8)虽然指定了对界为8,但是由于struct example1中的成员最大size为4(long变量size为4),故struct example1仍然按4字节对界,struct example1的size为8,即第18行的输出结果;
  struct example2中包含了struct example1,其本身包含的简单数据成员的最大size为2(short变量e),但是因为其包含了struct example1,而struct example1中的最大成员size为4,struct example2也应以4对界,#pragma pack (8)中指定的对界对struct example2也不起作用,故19行的输出结果为16;
  由于struct example2中的成员以4为单位对界,故其char变量c后应补充3个空,其后才是成员struct1的内存空间,20行的输出结果为4。

3. C和C++间struct的深层区别
  在C++语言中struct具有了“类” 的功能,其与关键字class的区别在于struct中成员变量和函数的默认访问权限为public,而class的为private。
  例如,定义struct类和class类:
struct structA
{
char a;

}
class classB
{
      char a;
      …
}
  则:
struct A a;
a.a = 'a';    //访问public成员,合法
classB b;
b.a = 'a';    //访问private成员,不合法
  许多文献写到这里就认为已经给出了C++中struct和class的全部区别,实则不然,另外一点需要注意的是:
  C++中的struct保持了对C中struct的全面兼容(这符合C++的初衷——“a better c”),因而,下面的操作是合法的:
//定义struct
struct structA
{
char a;
char b;
int c;
};
structA a = {'a' , 'a' ,1};    //  定义时直接赋初值
  即struct可以在定义的时候直接以{ }对其成员变量赋初值,而class则不能,在经典书目《thinking C++ 2nd edition》中作者对此点进行了强调。
4. struct编程注意事项
  看看下面的程序:
1. #include <iostream.h>
2. struct structA
3. {
4. int iMember;
5. char *cMember;
6. };
7. int main(int argc, char* argv[])
8. {
9. structA instant1,instant2;
10.char c = 'a';
    11. instant1.iMember = 1;
12. instant1.cMember = &c;
13.instant2 = instant1;
14.cout << *(instant1.cMember) << endl;
15.*(instant2.cMember) = 'b';
16. cout << *(instant1.cMember) << endl;
17. return 0;
}
  14行的输出结果是:a
  16行的输出结果是:b
  Why?我们在15行对instant2的修改改变了instant1中成员的值!
  原因在于13行的instant2 = instant1赋值语句采用的是变量逐个拷贝,这使得instant1和instant2中的cMember指向了同一片内存,因而对instant2的修改也是对instant1的修改。
  在C语言中,当结构体中存在指针型成员时,一定要注意在采用赋值语句时是否将2个实例中的指针型成员指向了同一片内存。
  在C++语言中,当结构体中存在指针型成员时,我们需要重写struct的拷贝构造函数并进行“=”操作符重载。
1.2.11. 枚举
枚举是用户定义的整数类型。在声明一个枚举时,要指定该枚举可以包含的一组可接受的实例值。不仅如此,还可以给值指定易于记忆的名称。如果在代码的某个地方,要试图把一个不在可接受范围内的值赋予枚举的一个实例,编译器就会报告一个错误。这个概念对于VB程序员来说是新的。C++支持枚举,但C#枚举要比C++枚举强大得多。
从长远来看,创建枚举可以节省大量的时间,减少许多麻烦。使用枚举比使用无格式的整数至少有如下三个优势:
●       如上所述,枚举可以使代码更易于维护,有助于确保给变量指定合法的、期望的值。
●       枚举使代码更清晰,允许用描述性的名称表示整数值,而不是用含义模糊的数来表示。
●       枚举使代码更易于键入。在给枚举类型的实例赋值时,VS .NET IDE会通过IntelliSense弹出一个包含可接受值的列表框,减少了按键次数,并能够让我们回忆起可选的值。
定义如下的枚举:
public enum TimeOfDay
{
   Morning = 0,
   Afternoon = 1,
   Evening = 2
}
在本例中,在枚举中使用一个整数值,来表示一天的每个阶段。现在可以把这些值作为枚举的成员来访问。例如,TimeOfDay.Morning返回数字0。使用这个枚举一般是把合适的值传送给方法,在switch语句中迭代可能的值。
class EnumExample
{
   public static int Main()
   {
      WriteGreeting(TimeOfDay.Morning);
      return 0;
   }
   static void WriteGreeting(TimeOfDay timeOfDay)
   {
      switch(timeOfDay)
      {
         case TimeOfDay.Morning:
            Console.WriteLine("Good morning!");
            break;
         case TimeOfDay.Afternoon:
            Console.WriteLine("Good afternoon!");
            break;
         case TimeOfDay.Evening:
            Console.WriteLine("Good evening!");
            break;
         default:
            Console.WriteLine("Hello!");
            break;
      }
   }
}
在C#中,枚举的真正强大之处是它们在后台会实例化为派生于基类System.Enum的结构。这表示可以对它们调用方法,执行有用的任务。注意因为.NET Framework的执行方式,在语法上把枚举当做结构是不会有性能损失的。实际上,一旦代码编译好,枚举就成为基本类型,与int和float类似。
可以获取枚举的字符串表示,例如使用前面的TimeOfDay枚举:
TimeOfDay time = TimeOfDay.Afternoon;
Console.WriteLine(time.ToString());
会返回字符串Afternoon。
另外,还可以从字符串中获取枚举值:
TimeOfDay time2 = (TimeOfDay) Enum.Parse(typeof(TimeOfDay), "afternoon", true);
Console.WriteLine((int)time2);
这段代码说明了如何从字符串获取枚举值,并转换为整数。要从字符串中转换,需要使用静态的Enum.Parse()方法,这个方法带3个参数,第一个参数是要使用的枚举类型。其句法是关键字typeof后跟放在括号中的枚举类名。typeof运算符将在第5章详细论述。第二个参数是要转换的字符串,第三个参数是一个bool,指定在进行转换时是否忽略大小写。最后,注意Enum.Parse()方法实际上返回一个对象引用—— 我们需要把这个字符串显式转换为需要的枚举类型(这是一个拆箱操作的例子)。对于上面的代码,将返回1,作为一个对象,对应于TimeOfDay. Afternoon的枚举值。在显式转换为int时,会再次生成1。
System.Enum上的其他方法可以返回枚举定义中的值的个数、列出值的名称等。详细信息参见MSDN文档。
1.3. 一般问题
1.3.1. C++二元运算符的两种实现方式
对于二元运算符的定义,在C++中十分灵活,你可以将它看作当前类的一个成员,也可以声明一个全局定义,前者的好处在于处理简单,运算速度较快,但相对来说灵活性较差,而后者则是灵活性极高,但是其可能会多做一次Copy constructor.
 
1.对于第一种情况,请看下面的例子://当前类的一个成员
class Integer
{
private:
    int a;
public:
   // 注意传入参数b,是按引用传址的,它可以减少一次copy constructor.
   Integer& operator+(Integer& b)
  {
     a+= b.a;
     return *this;
  }

这样做效率,可以将copy constructor减少到最低,执行效率应该是最好的,但是它改变了当前对象的值,在有些场合这可能是不允许的,如果将返回值改为Integer,则会多一到两次copy constructor,但是可以不改变当前对象的值:
Integer operator+(Integer& b)
{
   Ineger c;//1次
   c.a = a + b.a;
  return c;//2次

另外还有一个问题就是,它不能处理lefthand为非当前对象的问题,这时候,就要用到
第二种方法://声明一个全局定义
class Integer
{

Integer(&) operator+(Integer&a, (const)Integer& b)
{
}

Intger operator+(int a, Integer& b)
{
}

static Intger operator+(const int a, const Integer& b)
等等... 
由于考虑到定义这种全局函数不能方便的访问到class的private和protected变量
所以一般会加上这句,表示该全局函数可以访问该类的任何成员:
class Integer
{
   friend Integer operator+(const int a, const Integer& b);
}
等等。
注意在cpp中写这种定义在类中的friend函数,不要加类前缀,比如
Integer Integer::operator+(....)
这是一种错误的写法。
 BTW:
另外在某些特殊的情况下,说到copy constructor次数最低的一种实现和应用方法是
.h
class Integer
{
   friend Integer& operator+(Integer&a, const Integer& b);
}
 
.cpp
Integer& operator+(Integer&a, const Integer& b)
{
    a.a += b.a;
    return a;
/**
   千万不要写成这样,因为temp是stack上分配的,它会被注销掉。
    Integer temp;
    temp.a = a.a + b.a;
    return temp;
**/
}
 
main
Integer a(1);
Integer b(2);
Integer& c= a + b;

1.3.2. 线程同1.3.3. 步的三种方法
互斥对象
事件对象
关键代码段
三者的比较:
n互斥对象和事件对象属于内核对象,利用内核对象进行线程同步,速度较慢,但利用互斥对象和事件对象这样的内核对象,可以在多个进程中的各个线程间进行同步。
n关键代码段是工作在用户方式下,同步速度较快,但在使用关键代码段时,很容易进入死锁状态,因为在等待进入关键代码段时无法设定超时值。

使隶属于同一进程的各线程协调一致地工作称为线程的同步
线程是进程中的实体,一个进程可以拥有多个线程,一个线程必须有一个父进程。线程不拥有系统资源,只有运行必须的一些数据结构;它与父进程的其它线程共享该进程所拥有的全部资源。线程可以创建和撤消线程,从而实现程序的并发执行。一般,线程具有就绪、阻塞和运行三种基本状态。


2. 数据结构
2.1. 二叉树
二叉树是另一种树型结构,它的特点是每个结点至多只有二棵子树(即二叉树中不存在度大于2的结点),并且,二叉树的子树有左右之分,其次序不能任意颠倒。
一棵深度为k且有2(k)-1个结点的二叉树称为满二叉树。如果有深度为k的,有n个结点的二叉树,当且仅当其每一个结点都与深度为k的满二叉树中编号从1至n的结点一一对应时,称之为完全二叉树。
2.1.1. 二叉树的数据结构
ADT BinaryTree{
数据对象D:D是具有相同特性的数据元素的集合。
数据关系R:
基本操作P:
InitBiTree(&T);
DestroyBiTree(&T);
CreateBiTree(&T,definition);
ClearBiTree(&T);
BiTreeEmpty(T);
BiTreeDepth(T);
Root(T);
Value(T,e);
Assign(T,&e,value);
Parent(T,e);
LeftChild(T,e);
RightChild(T,e);
LeftSibling(T,e);
RightSibling(T,e);
InsertChild(T,p,LR,c);
DeleteChild(T,p,LR);
PreOrderTraverse(T,visit());
InOrderTraverse(T,visit());
2.1.2. 二叉树的性质
(1)若二叉树的层次从0开始,则在二叉树的第i层最多结点数有2i ( i=0,1,… i≥0).
(2)高度为 h 的二叉树最多有2h+1-1 个结点数(h≥-1)。
(3)对任何一条二叉树,如果其叶结点个数为n0,度为2的非叶结点个数为n2,则有n0=n2+1.
(4)具有n个结点的完全二叉树的高度 h=「log2(n+1)|-1.
(5)如果将一棵有n个结点的完全二叉树顺序存储时(自上而下,同一层自左而右给结点编号):0,1,2,…n-1,然后按此结点编号将树中各结点顺序地存放在一个一维树组中,并简称编号为i的结点为结点 i (0≤i≤n-1),则有以下关系:
①若i= = 0,则结点i为根,无双亲;若i>0,则结点i的双亲为结点|(i-1)/2」.
②若2*i+1<n,则结点i的左子女为结点2*i+1.
③若2*i+2<n,则结点i的右子女为结点2*i+2.
④若结点编号i为偶数,且i!= 0, 则它的左兄弟为结点i-1.
⑤若结点编号i为奇数,且i!= n-1, 则它的右兄弟为结点i+1.
⑥结点i所在的层次为 |log2(i+1)」.
2.1.3. 对分查找二叉树
树是一种非线性的二维数据结构。
这里要说的是一种特殊的二叉树,叫对分查找树。特点在于:左子树的所有值都比根节点小,右子树的所有值都比根节点大。
    对分查找树的三种遍历:
    中序遍历(inOrder)        遍历左子树;处理节点中的值;遍历右子树。
    前序遍历(preOrder)     处理节点中的值;遍历左子树;遍历右子树。
    后序遍历(postOrder)   遍历左子树;遍历右子树;处理节点中的值。
注意的是,对分查找树中序遍历的结果是对数列进行升序排列。因此中序遍历又叫二叉树排序。
   下面的程序说明对分查找树的生成和三种遍历的实现。
/*author:zhanglin*/
#include
#include
#include
struct treeNode{
 struct treeNode *leftPtr;
 int data;
 struct treeNode *rightPtr;
};
typedef struct treeNode TreeNode;
typedef TreeNode *TreeNodePtr;
void insertNode(TreeNodePtr *, int);
void inOrder(TreeNodePtr);
void preOrder(TreeNodePtr);
void postOrder(TreeNodePtr);
int main(){
 int i, item;
 TreeNodePtr rootPtr= NULL;
 srand(time(NULL));
 /*insert random values between 1 and 15 in the tree*/
 printf("the number being placed in the tree are:\n");
 for(i=0; i<10;i++){
  item = rand()%15;
  printf("%3d", item);
  insertNode(&rootPtr, item);
 }
 /*traverse the tree in inOrder*/
 printf("\ntraverse the tree in inOrder\n");
 inOrder(rootPtr);
 /*traverse the tree in preOrder*/
 printf("\ntraverse the tree in preOrder\n");
 preOrder(rootPtr);
 /*traverse the tree in postOrder*/
 printf("\ntraverse the tree in postOrder\n");
 postOrder(rootPtr);
 return 0;
}
void insertNode(TreeNodePtr *ptr, int value){
 if(*ptr==NULL){
  *ptr = (TreeNodePtr)malloc(sizeof(TreeNode));
  if(*ptr!=NULL){
            (*ptr)->data = value;
   (*ptr)->leftPtr = NULL;
   (*ptr)->rightPtr = NULL;
  }else{
   printf("\nerror. No memory available\n");
  }
 }else{
  if(value<(*ptr)->data){
   insertNode(&((*ptr)->leftPtr), value);
  }else if(value>(*ptr)->data){
            insertNode(&((*ptr)->rightPtr), value);
  }else{
            printf("\nduplicate data\n");
  }
 }
}
void inOrder(TreeNodePtr ptr){
 if(ptr!=NULL){
   inOrder(ptr->leftPtr);
   printf("%3d", ptr->data);
   inOrder(ptr->rightPtr);
 }
}
void preOrder(TreeNodePtr ptr){
 if(ptr!=NULL){
   printf("%3d", ptr->data);
   preOrder(ptr->leftPtr);
   preOrder(ptr->rightPtr);
 }
}
void postOrder(TreeNodePtr ptr){
 if(ptr!=NULL){
   postOrder(ptr->leftPtr);
   postOrder(ptr->rightPtr);
   printf("%3d", ptr->data);
 }
}

2.1.4. 二叉搜索树
也称为二叉查找树或者二叉排序树,Binary Search Tree
二叉搜索树或者是一棵空树,或者是具有下列性质的二叉树:
1、每个结点都有一个作为搜索依据的关键码(key),所有结点的关键码互不相同。
2、左子树(如果存在)上所有结点的关键码都小于根结点的关键码。
3、右子树(如果存在)上所有结点的关键码都大于根结点的关键码。
4、左子树和右子树也是二叉搜索树。
//********************************************************************
//最优二叉搜索树
//********************************************************************
#include "iostream.h"
#define N 5
void OptimaBinarySearchTree(int *a,int *b,int n,int m[][N],int s[][N],int w[][N])
{
int i,j,r,k,t;
for(i=0;i<=n;i++)
{
 w[i+1][i]=a[i];
 m[i+1][i]=0;
}
for(r=0;r<n;r++)
for(i=1;i<=n-r;i++)
{
j=i+r;
w[i][j]=w[i][j-1]+a[j]+b[j];
m[i][j]=m[i+1][j];
s[i][j]=i;
for(k=i+1;k<=j;k++)
{
t=m[i][k-1]+m[k+1][j];
if(t<m[i][j])
{
 m[i][j]=t;
s[i][j]=k;
cout<<"最优解:s["<<i<<"]["<<j<<"]="<<s[i][j]<<"\n";
}
}
m[i][j]+=w[i][j];
}
}
void main()
{
cout<<"最优二叉搜索树"<<"\n";
int i;
int a[N],b[N];
int m[N][N],s[N][N],w[N][N];
cout<<"初始化概率数组a["<<N<<"]:\n";
for(i=0;i<N;i++)
cin>>a[i];
cout<<"初始化概率数组b["<<N<<"]:\n";
for(i=1;i<N;i++)
cin>>a[i];
OptimaBinarySearchTree(a,b,N,m,s,w);
}
2.1.5. 平衡二叉树
平衡二叉树(Balanced Binary Tree) 是二叉搜索树(又名二叉查找树排序二叉树)的一种。在二叉搜索树中,搜索、插入、删除的复杂度都和书的高度相关,因此树高是制约二叉搜索树时间效率的最大瓶颈。理论上,任意高度为h二叉树最多能容纳2h − 1个元素,即h=O(lg n)。实际上,由于普通二叉树的形态常常受操作顺序的影响,各子树左右儿子节点数目相差比较大,极端情况下,二叉树蜕化成一条链,此时h=O(n)
平衡二叉树通过一组平衡化旋转规则,使得各个子树的形态发生变化,从而使树高趋近于lg n。
左旋转
Left-Rotate (t)
1     k ← right[t]
2     right[t] ← left[k]
3     left[k] ← t
4     s[k] ← s[t]
5     s[t] ← s[left[t]] + s[right[t]] + 1
6     t ← k
右旋转
Right-Rotate(t)
1     k ← left[t]
2     left[t] ← right[k]
3     right[k] ← t
4     s[k] ← s[t]
5     s[t] ← s[left[t]] + s[right[t]] + 1
6     t ← k
--- 常见的平衡二叉树有如下的几种
AVL树 其主要思想是维护树高,使之平衡
红黑树 其主要思想是对节点染色,对不同颜色的节点采用不同的判断,编程复杂度较高
AA树 是红黑树的一种特例
伸展树 有四种旋转规则
Treap 其主要思想是对每个节点附加随机权值,并根据权值维护为堆,因此被命名为Tree+Heap=Treap,其编程复杂度较低,性价比较高。
Size Balanced Tree 其主要思想为直接维护各子树的节点个数,使之严格平衡。其论文由中国OIer广东纪念中学的陈启峰于2006年底完成,并在Winter Camp 2007中发表。
2.2. 链表
2.2.1. 静态链表
如果数组中每一个元素附加一个链表指针,就形成静态链表。它容许我们不改变各元素的物理位置,只要重新链接就能够改变这些元素的逻辑顺序。由于它是利用数组定义的,在整个运算过程中存储空间的大小不会改变,因此称之为静态链表。
它的每个节点由两个数据成员构成:data域存储数据,link域存储链接指针。所有节点形成一个节点数组。
2.2.2. 创建链表
/*creat a list*/
#include "stdlib.h"
#include "stdio.h"
struct list
{
 int data;
struct list *next;
};
typedef struct list node;
typedef node *link;
void main()
{
 link ptr,head;
int num,i;
ptr=(link)malloc(sizeof(node));
ptr=head;
printf("please input 5 numbers==>\n");
for(i=0;i<=4;i++)
{
     scanf("%d",&num);
     ptr->data=num;
 ptr->next=(link)malloc(sizeof(node));
    if(i==4) ptr->next=NULL;
    else ptr=ptr->next;
}
ptr=head;
while(ptr!=NULL)
{
printf("The value is ==>%d\n",ptr->data);
     ptr=ptr->next;
}
}
2.2.3. 链表的反向输出
/*reverse output a list*/
#include "stdlib.h"
#include "stdio.h"
struct list
{
 int data;
 struct list *next;
};
typedef struct list node;
typedef node *link;
void main()
{
link ptr,head,tail; 
 int num,i;
 tail=(link)malloc(sizeof(node));
 tail->next=NULL;
 ptr=tail;
 printf("\nplease input 5 data==>\n");
 for(i=0;i<=4;i++)
 {
  scanf("%d",&num);
  ptr->data=num;
  head=(link)malloc(sizeof(node));
  head->next=ptr;
  ptr=head;
 }
ptr=ptr->next;
while(ptr!=NULL)
{
printf("The value is ==>%d\n",ptr->data);
 ptr=ptr->next;
}
}
2.2.4. 连接两个链表
#include "stdlib.h"
#include "stdio.h"
struct list
{ int data;
struct list *next;
};
typedef struct list node;
typedef node *link;
//删除
link delete_node(link pointer,link tmp)
{
if (tmp==NULL) /*delete first node*/
 return pointer->next;
else
{ if(tmp->next->next==NULL)/*delete last node*/
  tmp->next=NULL;
 else /*delete the other node*/
  tmp->next=tmp->next->next;
 return pointer;
}
}
//选择排序
void selection_sort(link pointer,int num)
{ link tmp,btmp;
 int i,min;
 for(i=0;i<NUM;I++)
 {
 tmp=pointer;
 min=tmp->data;
 btmp=NULL;
 while(tmp->next)
 { if(min>tmp->next->data)
 {min=tmp->next->data;
  btmp=tmp;
 }
 tmp=tmp->next;
 }
printf("\40: %d\n",min);
pointer=delete_node(pointer,btmp);
}
}
//创建
link create_list(int array[],int num)
{ link tmp1,tmp2,pointer;
int i;
pointer=(link)malloc(sizeof(node));
pointer->data=array[0];
tmp1=pointer;
for(i=1;i<NUM;I++)
{ tmp2=(link)malloc(sizeof(node));
 tmp2->next=NULL;
 tmp2->data=array[i];
 tmp1->next=tmp2;
 tmp1=tmp1->next;
}
return pointer;
}
//合并
link concatenate(link pointer1,link pointer2)
{ link tmp;
tmp=pointer1;
while(tmp->next)
 tmp=tmp->next;
tmp->next=pointer2;
return pointer1;
}
void main(void)
{ int arr1[]={3,12,8,9,11};
 link ptr;
 ptr=create_list(arr1,5);
 selection_sort(ptr,5);
}
2.3. 广义表
表中元素类型不固定,可以具有不同的数据结构
2.4. 栈
后进先出的顺序表
2.4.1. 顺序栈
Template <class Type> class stack
{
 Public :
  Stack(int =10);
~stack(){delete [] elements};
Void push(const Type &item);
Type pop();
Type GetTop();
Void MakeEmpty(){ top = -1 ; }
Int IsEmpty()const{return top == -1 };
Int isFull() const {return top == maxSize -1 ;}
 Private:
  Int top;
Type *elements ;
Int maxSize ;
}
2.4.2. 链式存储

2.5. 数组
合并
2.6. 队列
限定存取位置的线性表。它只容许在表的一端插入,在另一端删除。
如果队头指针front == rear ,队列为空
Rear == maxSize -1 队列满

循环队列:
队头指针进1:front = (front +1)%maxSize
队尾指针进1:rear = (rear +1)%maxSize

2.6.1. 存储结构
2.6.2. 循环队列
2.6.3. 排序
2.7. 堆
2.7.1. 最大值堆
最大值堆(MAX-HEAP)的性质是任意一个结点的值都大于或者等于其任意一个子结点存储的值。由于根结点包含大于或等于其子结点的值,而其子结点又依次大于或者等于各自结点的值,所以根结点存储着该树的所有结点中的最大值。
最大值堆(MAX-HEAP)的性质是任意一个结点的值都大于或者等于其任意一个子结点存储的值。由于根结点包含大于或等于其子结点的值,而其子结点又依次大于或者等于各自结点的值,所以根结点存储着该树的所有结点中的最大值。
最小值堆(MIN-HEAP)的性质是任意一个结点的值都小于或者等于其子结点存储的值。
无论最小值堆还是最大值堆,任何一个结点与其兄弟之间都没有必然的联系。
public class MaxHeap {
private Element[] heap;
private int maxSize;
private int size;
public MaxHeap(Element[] heap, int size, int maxSize) {
     this.heap = heap;
     this.size = size;
     this.maxSize = maxSize;
}
public int size() {
     return this.size;
}
public boolean isLeaf(int position) {
return (position >= size / 2) && (position < size);
}
public int leftChild(int position) {
return 2 * position + 1;
}

public int rightChild(int position) {
return 2 * position + 2;
}
public int parent(int position) {
return (position - 1) / 2;
}
public void buildheap() {
   for (int i = size / 2; i >= 0; i--) {
          siftdown(i);
    }
}
2.7.2. private void siftdown(int position) {
   while (!isLeaf(position)) {
        int j = leftChild(position);
        if ((j < (size - 1)) && (heap[j].getKey() < heap[j + 1].getKey())) {
              j++;
        }
        if (heap[j].key >= heap[j + 1].getKey()) {
              return;
        }
        swap(position, j);
        position = j;
    }
}
2.7.3. public void insert(Element element) {
        int position = size++;
        heap[position] = element;
        while ((position != 0)
          && (heap[position].getKey() > heap[parent(position)].getKey())) {
                swap(position, parent(position));
                position = parent(position);
        }
}
public Object removeMax() {
        return remove(0);
}
2.7.4. public Object remove(int position) {
        swap(position, --size);
        if (size != 0) {
                siftdown(position);
        }
        return heap[size];
}
private void swap(int src, int dest) {
        Element element = heap[src];
        heap[src] = heap[dest];
        heap[dest] = element;
}
/**
* @param args
*/
public static void main(String[] args) {
// TODO Auto-generated method stub
}
public class Element {
private Object value;
private int key;
public Element() {
}
public int getKey() {
return key;
}
public void setKey(int key) {
this.key = key;
}
public Object getValue() {
return value;
}
public void setValue(Object value) {
this.value = value;
}
}
}
2.8. 最优二叉树(哈夫曼树----带权路径长度最短的树)

2.8.1. 哈夫曼树的基本概念
(1)路径从树中一个结点到另一个结点之间的分支。
(2)路径长度路径上的分支数目称为路径长度。
(3)树的路径长度从树根到每一结点的路径长度之和,称为树的路径长度,完全二叉树是路径长度最短的二叉树。
(4)结点的带权路径长度从该结点到树根之间的路径长度和结点上权的乘积。
(5)树的带权路径长度树中所有叶子结点的带权路径长度之和,通常记为WPL=wili,
(6)哈夫曼树(最优二叉树)带权路径长度之和最小的二叉树称为哈夫曼树(最优二叉树)
(7)哈夫曼编码在哈夫曼树上,左分枝为0,右分枝为1,从根结点开始,直到叶子结点所组成的编码序列,称为叶子结点的哈夫曼编码。
2.8.2. 哈夫曼树在判定问题中的应用
将百分制转为五级记分制的例子。说明哈夫曼树判定次数最少的树(即带权路径长度最短、最节省计算时间。)
2.8.3. 如何构造哈夫曼树
(1)根据给定的n个权值{w1,w2,…,wn},构成n棵二叉树的集合F={T1,T2,…,Tn},每棵二叉树Ti只有根结点。
(2)在F中选两棵根结点权值最小的树作为左右子树,构造一棵二叉树,新二叉树根结点的权值等于其左右子树根结点权值之和。
(3)在F中删除这两棵子树,同时将新得到的二叉树加入F中。
(4)重复(2)和(3),直到F中只剩一棵树(即哈夫曼树)为止。
举例说明这一过程:
应该说明,哈夫曼树的形态不是唯一的,但对具有一组权值的各哈夫曼树的WPL是唯一的。

2.8.4. 哈夫曼编码提出的背景
(1)如何使电报编码变短,非前缀编码出现二义性。
(2)用二叉树可以构造前缀编码。
(3)由哈夫曼树得到最优编码。
2.8.5. 构造哈夫曼树和哈夫曼编码的算法
typedefstruct
{
 unsignedintw;
 unsignedintparent,lchild,rchild;
}HTNode,*HTree;
typedefstruct
{
 unsignedintbits[n];
 unsignedintstart;
}HTCode;

#defineMAX最大数/*准备在选取最小权结点时作比较用*/
voidCreatHuffman(ht,weight,hcd)
HTreeht[];
unsignedintweight[];
HTCodehcd[];
{
 inti,j,x,y,m,n;
 for(i=1;i<=2*n-1;i++)/*初始化数组,n为权叶结点的个数*/
 {
  ht[i].parent=ht[i].lchild=ht[i].rchild=0;
  if(i<=n)ht[i].w=weight[i];/*数组中前n个结点的权即为叶结点的权*/
  elseht[i].w=0;
}
/*选取两个权最小的结点,分别用x,y表示其下标,用m,n来表示最小权*/
/*及次最小的权*/
for(i=1;i<=n;i++)
{
 x=y=0;m=n=MAX;
 for(j=1;j<n+i;j++)
 if(ht[j].w<m&&ht[j].parent==0)/*选取最小的权*/
 {
  n=m;y=x;m=ht[j].w;x=j; 
 }
 elseif(ht[j].w<n&&ht[j].parent==0)/*选取次小的权*/
 {
  n=ht[j].w;y=j; 
 }
 /*准备合并两棵二叉树*/
 ht[x].parent=n+i;
 ht[y].parent=n+i;
 ht[n+i].w=m+n,
 ht[n+i].lchild=x;ht[n+i].rcnild=y;
}/*endoffor(i=1;i<=n;i++)*/
for(i=1;i<=n;i++)/*求哈夫曼编码*/
{
 cd.start=n;/*cd为型变量*/
 c=i;f=ht[c].parent;
 while(f)
 {
  if(ht[f].lchild=ccd.bits[cd.start]=0;/*左分枝赋0*/
  elsecd.bits[cd.start]=1;/*右分枝赋1*/
  cd.start--;c=f;f=ht[f].parent;
 }
 hcd[i]=cd;/*结构赋值*/
}/*结束求哈夫曼编码*/
}

2.8.6. 树的计数

具有n个结点的不相似的二叉树的个数为(1/(n+1))*((2n!)/(n!)*(n!)),这也是具有n个元素的序列,通过栈后所能得到的出栈序列的个数。
2.9. 字符串
2.9.1. 字符串排序
main()
{
char *str1[20],*str2[20],*str3[20];
char swap();
printf("please input three strings\n");
scanf("%s",str1);
scanf("%s",str2);
scanf("%s",str3);
if(strcmp(str1,str2)>0) swap(str1,str2);
if(strcmp(str1,str3)>0) swap(str1,str3);
if(strcmp(str2,str3)>0) swap(str2,str3);
printf("after being sorted\n");
printf("%s\n%s\n%s\n",str1,str2,str3);
}
char swap(p1,p2)
char *p1,*p2;
{
char *p[20];
strcpy(p,p1);strcpy(p1,p2);strcpy(p2,p);
}
2.9.2. 计算字符串中子串出现次数
#include "string.h"
#include "stdio.h"
main()
{ char str1[20],str2[20],*p1,*p2;
int sum=0;
printf("please input two strings\n");
scanf("%s%s",str1,str2);
p1=str1;p2=str2;
while(*p1!='\0')
{
if(*p1==*p2)
{while(*p1==*p2&&*p2!='\0')
{p1++;
p2++;}
}
else
p1++;
if(*p2=='\0')
sum++;
p2=str2;
}
printf("%d",sum);
getch();}

2.9.3. 字符串拷贝函数strcpy()
#include <iostream>
using namespace std;
char* strcpy(char* dest, const char *src )
{
// char* pdest = static_cast<char*>(dest);
// const char* psrc = static_cast<const  char*>(src);
if((dest==NULL)||(src==NULL))
throw"error";
char* strdest = dest;
while((*dest++ = *src++)!='\0');
return strdest;
}
2.9.4. 内存拷贝函数memcpy()
void* memcpy(void * dest, const void *src, size_t count )
{
 char* pdest = static_cast<char*>(dest);
 const char* psrc = static_cast<char*>(src);
 if(pdest>psrc && pdest<psrc+count)
 {
  for(size_t i=count-1; i!=1; --i)
   pdest[i] = psrc[i];
 }
 else
 {
  for(size_t i=0; i<count; ++i)
   pdest[i]=psrc[i];
 }
 return dest;
}
2.9.5. 子字符串查找strstr()
char *strstr(char* str, char * substr)
2.10. 图
2.10.1. 存储方式
邻接矩阵
邻接表
邻接多重表
2.10.2. 深度优先搜索
(1)图中有 n 个顶点,e 条边。
(2)如果用邻接表表示图,沿 link 链可以找到某个顶点 v 的所有邻接顶点 w。由于总共有 2e 个边结点,所以扫描边的时间为O(e)。而且对所有顶点递归访问1次,所以遍历图的时间复杂性为O(n+e)。
(3)如果用邻接矩阵表示图,则查找每一个顶点的所有的边,所需时间为O(n),则遍历图中所有的顶点所需的时间为O(n2)。

viod Graph::DFS ( const int v, int visited [ ] ){
   cout << GetValue (v) << ‘ ’; //访问顶点 v
   visited[v] = 1; //顶点 v 作访问标记
   int w = GetFirstNeighbor (v);//取 v 的第一个邻接顶点 w
   while ( w != -1 ) { //若邻接顶点 w 存在
    if ( !visited[w] ) DFS ( w, visited );
     //若顶点 w 未访问过, 递归访问顶点 w
    w = GetNextNeighbor ( v, w );
     //取顶点 v 的排在 w 后面的下一个邻接顶点
    }
   }

  void Graph::DFS ( ) {
   visited = new Boolean [n]; //创建数组 visited
   for ( int i = 0; i < n; i++ ) visited [i] = 0;
    //访问标记数组 visited 初始化
   DFS (0);
   delete [ ] visited; //释放 visited
  }
2.10.3. 广度优先搜索
基本原理:图的广度优先遍历类似于二叉树的按照层次遍历,用到一个辅助队列,存储访问过的顶点指针或者编号。 这里省去对队列进行操作的函数的详细实现,只提供名字供参考:
int Init(Qlist* qlist);//初始化一个带头节点的空队列
int EnQueue(Qlist* qlist, int num);//入队
int QuitQueue(Qlist* qlist, int* num);//出队
由于是基于邻接表这种存储结构,所以广度优先遍历的本质和DFS一样都是查找邻接点,自然要用到下面两个函数:
int FirstAdjVex(int num);//第一个邻接点
int NextAdjVex(int num ,int lastV);//下一个未被访问的邻接点
具体实现可以参考这里。
以下是BFS算法的核心实现部分:
void BFS(void){
    int v;
    int u,w;
    Qlist queue;
    Init(&queue);
    for(v = 0; v < G.vertexNum; ++v)
            visited[v] = 0;
    for(v = 0; v <G.vertexNum; ++v)
        if(!visited[v]){
                   printf("%c",Ver[v].data);
                   visited[v] = 1;
                   EnQueue(&queue,v);
        while(queue.front != queue.rear)...{
                  QuitQueue(&queue,&u); //Not v
        for(w = FirstAdjVex(u); w >= 0; w = NextAdjVex(u,w))
            if(!visited[w]){
                        visited[w] = 1;
                        printf("%c",Ver[w].data);
                        EnQueue(&queue,w);
            }//if
        }//while
    }//if
}
2.10.4. 最小生成树
Prim算法  
    a .设置两个集合U.V,分别表示已经添加到生成树中的顶点集合和剩余顶点集合,初始U为空,V包括所有顶点。  
    b.任选V中一个顶点作为开始,将其添加到U中。  
    c.在所有连通U中顶点和V中顶点的边中选取权值最小的边,每加入一条边,则将其在V中的顶点删除,添加到U中。  
    d.循环执行c,直到V集合空,则算法结束。  
  void   prim_mst(graph   G)  
  {  
              int   make[n];     //存储标识进入集合U的顶点;  
              for(i=0;i<G.vexnum;i++)  
              {  
                        mark[i]=0;  
              }  
              int   v=0;  
              mark[v]=1;  
              for(i=1;i<G.vexnum;i++)  
            {  
                        min_edge=9999;  
                        for(j=0;j<G.vexnum;j++)  
                        {  
                                      if(mark[j])  
                                      {  
                                              for(k=0;k<G.vexnum;k++)  
                                                        if(G.arc[j][k]<min_edge)  
                                                        {min_edge=G.arc[j][k];u=j;v=k;}  
                                      }  
                          }  
                        G.arc[u][v]=G.arc[v][u]=9999;     //找到的不再重复  
                        mark[v]=1;                                         //将新的顶点加入U  
                        To[i][0]=u;     To[i][1]=v;             //存储生成树信息  
              }  
  }  
Kruskal算法  
  a.设置计数器K,初值为0,记录已选中的边数。将所有边从小到大排序,存于E中。  
  b.从E中选择一条权值最小的边,检查其加入到最小生成树中是否会构成回路,若是,则此边不加入生成树;否则,加入到生成树中,计数器K累加1。  
  c.从E中删除此最小边,转b继续执行,直到k=n-1,算法结束  
  d.判断是否构成回路的方法:  
  设置集合S,其中存放已加入到生成树中的边所连接的顶点集合,当一条新的边要加入到生成树中时,检查此边所连接的两个顶点是否都已经在S中,若是,则表示构成回路,否则,若有一个顶点不在S中或者两个顶点都不在S中,则不够成回路。
void   kruskal_mst(graph   G)  
  {  
              int   make[n];     //存储标识进入集合U的顶点;  
              for(i=0;i<G.vexnum;i++)  
              {  
                        mark[i]=0;  
              }  
              min_edge=9999;m=0;  
  bg:       for(i=0;i<G.vexnum;i++)  
            {                            
                        for(j=0;j<G.vexnum;j++)  
                        {  
                                      for(k=0;k<G.vexnum;k++)  
                                                        if(G.arc[j][k]<min_edge)  
                                                        {min_edge=G.arc[j][k];u=j;v=k;}     //首先找出代价最小的边                                         
                          }  
              }  
              flag=0;  
              for(i=0;i<=G.vexnum:i++)  
              if(To[i][0]==u&&To[i][1]==v)  
                                flag=1;         //判断是否有回路  
              if(flag==0)        
                      {  
                                        G.arc[u][v]=G.arc[v][u]=9999;     //找到的边不再重复  
                                        mark[u]=mark[v]=1;  //将新的顶点加入U  
                                        To[m++][0]=u;     To[m++][1]=v;    //存储生成树信息  
                      }  
              if(m<G.vexnum)  
                          goto  bg;  
              else  
                          go exit;  
  exit:  
      {;}  
             
  } 
2.10.5. 最短路经
分成两个集合,然后选择集合中路经最小的两个点
2.11. 排序理论
排序算法是一种基本并且常用的算法。由于实际工作中处理的数量巨大,所以排序算法对算法本身的速度要求很高。
而一般我们所谓的算法的性能主要是指算法的复杂度,一般用O方法来表示。在后面我将给出详细的说明。
对于排序的算法我想先做一点简单的介绍,也是给这篇文章理一个提纲。
我将按照算法的复杂度,从简单到难来分析算法。
第一部分是简单排序算法,后面你将看到他们的共同点是算法复杂度为O(N*N)(因为没有使用word,所以无法打出上标和下标)。
第二部分是高级排序算法,复杂度为O(Log2(N))。这里我们只介绍一种算法。另外还有几种算法因为涉及树与堆的概念,所以这里不于讨论。
第三部分类似动脑筋。这里的两种算法并不是最好的(甚至有最慢的),但是算法本身比较奇特,值得参考(编程的角度)。同时也可以让我们从另外的角度来认识这个问题。
第四部分是我送给大家的一个餐后的甜点——一个基于模板的通用快速排序。由于是模板函数可以对任何数据类型排序(抱歉,里面使用了一些论坛专家的呢称)。
插入排序:Θ(n2)   稳定
起泡排序:Θ(n2)   稳定
选择排序:Θ(n2)    不稳定
希尔排序:Θ(n1.5)    不稳定
快速排序:Θ(nlog2n) 不稳定
堆排序: Θ(nlog2n)     不稳定
归并排序:Θ(nlog2n)  稳定

例如,给定排序码为:68  55  44  22  10  15  12   20
希尔排序的每一趟排序结果如下:
第一趟: 10  15  12  20  68  55  44  22
第二趟: 10  15  12  20  44  22  68  55
第三趟: 10  12  15  20  22  44  55  68

归并排序的每一趟排序结果如下:
第一趟: [10  15 ]  [ 12  20 ]  [ 55  68 ]  [ 22  44 ]
第二趟: [10  12  15  20  ]   [ 22  44  55  68  ]
第三趟: [10  12  15  20  22  44  55  68 ]

例如,给定排序码为:68  55  44  22  10  15  12   20
快速排序的实现如下:
第一趟:[ 12   15  10  20 ] 22 [ 55  68  44 ]
第二趟:[ 12  10 ] 15  [ 20 ] 22 [44 ]  55  [ 68 ]
第三趟:[10 ] 12  15  20  22  44  55  68
第四趟:10  12  15  20  22  44  55  68
得到有先后顺序的线性序列(拓扑序列)的过程,称为拓扑排序。

   
2.11.1. 冒泡法
这是最原始,也是众所周知的最慢的算法了。他的名字的由来因为它的工作看来象是冒泡:
#include <iostream.h>
void BubbleSort(int* pData,int Count)
{
    int iTemp;
    for(int i=1;i<Count;i++)
    {
        for(int j=Count-1;j>=i;j--)
        {
            if(pData[j]<pData[j-1])
            {
                iTemp = pData[j-1];
                pData[j-1] = pData[j];
                pData[j] = iTemp;
            }
        }
    }
}
void main()
{
    int data[] = {10,9,8,7,6,5,4};
    BubbleSort(data,7);
    for (int i=0;i<7;i++)
        cout<<data[i]<<" ";
    cout<<"\n";
}
分析:将最后面的一个最小数值往前赶
倒序(最糟情况)
第一轮:10,9,8,7->10,9,7,8->10,7,9,8->7,10,9,8(交换3次)
第二轮:7,10,9,8->7,10,8,9->7,8,10,9(交换2次)
第一轮:7,8,10,9->7,8,9,10(交换1次)
循环次数:6次
交换次数:6次
其他:
第一轮:8,10,7,9->8,10,7,9->8,7,10,9->7,8,10,9(交换2次)
第二轮:7,8,10,9->7,8,10,9->7,8,10,9(交换0次)
第一轮:7,8,10,9->7,8,9,10(交换1次)
循环次数:6次
交换次数:3次
上面我们给出了程序段,现在我们分析它:这里,影响我们算法性能的主要部分是循环和交换,
显然,次数越多,性能就越差。从上面的程序我们可以看出循环的次数是固定的,为1+2+...+n-1。
写成公式就是1/2*(n-1)*n。
现在注意,我们给出O方法的定义:
若存在一常量K和起点n0,使当n>=n0时,有f(n)<=K*g(n),则f(n) = O(g(n))。(呵呵,不要说没
现在我们来看1/2*(n-1)*n,当K=1/2,n0=1,g(n)=n*n时,1/2*(n-1)*n<=1/2*n*n=K*g(n)。所以f(n)=O(g(n))=O(n*n)。所以我们程序循环的复杂度为O(n*n)。
再看交换。从程序后面所跟的表可以看到,两种情况的循环相同,交换不同。其实交换本身同数据源的有序程度有极大的关系,当数据处于倒序的情况时,交换次数同循环一样(每次循环判断都会交换),复杂度为O(n*n)。当数据为正序,将不会有交换。复杂度为O(0)。乱序时处于中间状态。正是由于这样的原因,我们通常都是通过循环次数来对比算法。
2.11.2. 交换法
交换法的程序最清晰简单,每次用当前的元素一一的同其后的元素比较并交换。
#include <iostream.h>
void ExchangeSort(int* pData,int Count)
{
    int iTemp;
    for(int i=0;i<Count-1;i++)
    {
        for(int j=i+1;j<Count;j++)
        {
            if(pData[j]<pData[i])
            {
                iTemp = pData[i];
                pData[i] = pData[j];
                pData[j] = iTemp;
            }
        }
    }
}
void main()
{
    int data[] = {10,9,8,7,6,5,4};
    ExchangeSort(data,7);
    for (int i=0;i<7;i++)
        cout<<data[i]<<" ";
    cout<<"\n";
}
评价:以第一个与后面的每一个进行比较,取最小放在前面
倒序(最糟情况)
第一轮:10,9,8,7->9,10,8,7->8,10,9,7->7,10,9,8(交换3次)
第二轮:7,10,9,8->7,9,10,8->7,8,10,9(交换2次)
第一轮:7,8,10,9->7,8,9,10(交换1次)
循环次数:6次
交换次数:6次
其他:第一轮:8,10,7,9->8,10,7,9->7,10,8,9->7,10,8,9(交换1次)
第二轮:7,10,8,9->7,8,10,9->7,8,10,9(交换1次)
第一轮:7,8,10,9->7,8,9,10(交换1次)
循环次数:6次
交换次数:3次
从运行的表格来看,交换几乎和冒泡一样糟。事实确实如此。循环次数和冒泡一样也是1/2*(n-1)*n,所以算法的复杂度仍然是O(n*n)。由于我们无法给出所有的情况,所以只能直接告诉大家他们在交换上面也是一样的糟糕(在某些情况下稍好,在某些情况下稍差)。
2.11.3. 选择法
现在我们终于可以看到一点希望:选择法,这种方法提高了一点性能(某些情况下)这种方法类似我们人为的排序习惯:从数据中选择最小的同第一个值交换,在从省下的部分中选择最小的与第二个交换,这样往复下去。
#include <iostream.h>
void SelectSort(int* pData,int Count)
{
    int iTemp;
    int iPos;
    for(int i=0;i<Count-1;i++)
    {
        iTemp = pData[i];
        iPos = i;
        for(int j=i+1;j<Count;j++)
        {
            if(pData[j]<iTemp)
            {
                iTemp = pData[j];
                iPos = j;
            }
        }
        pData[iPos] = pData[i];
        pData[i] = iTemp;
    }
}
void main()
{
    int data[] = {10,9,8,7,6,5,4};
    SelectSort(data,7);
    for (int i=0;i<7;i++)
        cout<<data[i]<<" ";
    cout<<"\n";
}
倒序(最糟情况)
第一轮:10,9,8,7->(iTemp=9)10,9,8,7->(iTemp=8)10,9,8,7->(iTemp=7)7,9,8,10(交换1次)
第二轮:7,9,8,10->7,9,8,10(iTemp=8)->(iTemp=8)7,8,9,10(交换1次)
第一轮:7,8,9,10->(iTemp=9)7,8,9,10(交换0次)
循环次数:6次
交换次数:2次
其他:
第一轮:8,10,7,9->(iTemp=8)8,10,7,9->(iTemp=7)8,10,7,9->(iTemp=7)7,10,8,9(交换1次)
第二轮:7,10,8,9->(iTemp=8)7,10,8,9->(iTemp=8)7,8,10,9(交换1次)
第一轮:7,8,10,9->(iTemp=9)7,8,9,10(交换1次)
循环次数:6次
交换次数:3次
遗憾的是算法需要的循环次数依然是1/2*(n-1)*n。所以算法复杂度为O(n*n)。
我们来看他的交换。由于每次外层循环只产生一次交换(只有一个最小值)。所以f(n)<=n
所以我们有f(n)=O(n)。所以,在数据较乱的时候,可以减少一定的交换次数。
2.11.4. 插入法
插入法较为复杂,它的基本工作原理是抽出牌,在前面的牌中寻找相应的位置插入,然后继续下一张
#include <iostream.h>
void InsertSort(int* pData,int Count)
{
    int iTemp;
    int iPos;
    for(int i=1;i<Count;i++)
    {
        iTemp = pData[i];
        iPos = i-1;
        while((iPos>=0) && (iTemp<pData[iPos]))
        {
            pData[iPos+1] = pData[iPos];
            iPos--;
        }
        pData[iPos+1] = iTemp;
    }
}

void main()
{
    int data[] = {10,9,8,7,6,5,4};
    InsertSort(data,7);
    for (int i=0;i<7;i++)
        cout<<data[i]<<" ";
    cout<<"\n";
}
倒序(最糟情况)
第一轮:10,9,8,7->9,10,8,7(交换1次)(循环1次)
第二轮:9,10,8,7->8,9,10,7(交换1次)(循环2次)
第一轮:8,9,10,7->7,8,9,10(交换1次)(循环3次)
循环次数:6次
交换次数:3次
其他:
第一轮:8,10,7,9->8,10,7,9(交换0次)(循环1次)
第二轮:8,10,7,9->7,8,10,9(交换1次)(循环2次)
第一轮:7,8,10,9->7,8,9,10(交换1次)(循环1次)
循环次数:4次
交换次数:2次
上面结尾的行为分析事实上造成了一种假象,让我们认为这种算法是简单算法中最好的,其实不是,
因为其循环次数虽然并不固定,我们仍可以使用O方法。从上面的结果可以看出,循环的次数f(n)<=
1/2*n*(n-1)<=1/2*n*n。所以其复杂度仍为O(n*n)(这里说明一下,其实如果不是为了展示这些简单
排序的不同,交换次数仍然可以这样推导)。现在看交换,从外观上看,交换次数是O(n)(推导类似
选择法),但我们每次要进行与内层循环相同次数的‘=’操作。正常的一次交换我们需要三次‘=’
而这里显然多了一些,所以我们浪费了时间。
最终,我个人认为,在简单排序算法中,选择法是最好的。
2.11.5. 快速排序
它的工作看起来仍然象一个二叉树。首先我们选择一个中间值middle程序中我们使用数组中间值,然后把比它小的放在左边,大的放在右边(具体的实现是从两边找,找到一对后交换)。然后对两边分别使用这个过程(最容易的方法——递归)。
#include <iostream.h>
void run(int* pData,int left,int right)
{
    int i,j;
    int middle,iTemp;
    i = left;
    j = right;
    middle = pData[(left+right)/2];  //求中间值
    do{
        while((pData[i]<middle) && (i<right))//从左扫描大于中值的数
            i++;          
        while((pData[j]>middle) && (j>left))//从右扫描大于中值的数
            j--;
        if(i<=j)//找到了一对值
        {
            //交换
            iTemp = pData[i];
            pData[i] = pData[j];
            pData[j] = iTemp;
            i++;
            j--;
        }
    }while(i<=j);//如果两边扫描的下标交错,就停止(完成一次)
    //当左边部分有值(left<j),递归左半边
    if(left<j)
        run(pData,left,j);
    //当右边部分有值(right>i),递归右半边
    if(right>i)
        run(pData,i,right);
}
void QuickSort(int* pData,int Count)
{
    run(pData,0,Count-1);
}
void main()
{
    int data[] = {10,9,8,7,6,5,4};
    QuickSort(data,7);
    for (int i=0;i<7;i++)
        cout<<data[i]<<" ";
    cout<<"\n";
}
这里我没有给出行为的分析,因为这个很简单,我们直接来分析算法:首先我们考虑最理想的情况
1.数组的大小是2的幂,这样分下去始终可以被2整除。假设为2的k次方,即k=log2(n)。
2.每次我们选择的值刚好是中间值,这样,数组才可以被等分。
第一层递归,循环n次,第二层循环2*(n/2)......
所以共有n+2(n/2)+4(n/4)+...+n*(n/n) = n+n+n+...+n=k*n=log2(n)*n
所以算法复杂度为O(log2(n)*n)
其他的情况只会比这种情况差,最差的情况是每次选择到的middle都是最小值或最大值,那么他将变成交换法(由于使用了递归,情况更糟)。但是你认为这种情况发生的几率有多大??呵呵,你完全不必担心这个问题。实践证明,大多数的情况,快速排序总是最好的。如果你担心这个问题,你可以使用堆排序,这是一种稳定的O(log2(n)*n)算法,但是通常情况下速度要慢于快速排序(因为要重组堆)。
2.11.6. 双向冒泡
通常的冒泡是单向的,而这里是双向的,也就是说还要进行反向的工作。
代码看起来复杂,仔细理一下就明白了,是一个来回震荡的方式。
写这段代码的作者认为这样可以在冒泡的基础上减少一些交换(我不这么认为,也许我错了)。
反正我认为这是一段有趣的代码,值得一看。
#include <iostream.h>
void Bubble2Sort(int* pData,int Count)
{
    int iTemp;
    int left = 1;
    int right =Count -1;
    int t;
    do
    {
        //正向的部分
        for(int i=right;i>=left;i--)
        {
            if(pData[i]<pData[i-1])
            {
                iTemp = pData[i];
                pData[i] = pData[i-1];
                pData[i-1] = iTemp;
                t = i;
            }
        }
        left = t+1;
        //反向的部分
        for(i=left;i<right+1;i++)
        {
            if(pData[i]<pData[i-1])
            {
                iTemp = pData[i];
                pData[i] = pData[i-1];
                pData[i-1] = iTemp;
                t = i;
            }
        }
        right = t-1;
    }while(left<=right);
}
void main()
{
    int data[] = {10,9,8,7,6,5,4};
    Bubble2Sort(data,7);
    for (int i=0;i<7;i++)
        cout<<data[i]<<" ";
    cout<<"\n";
}
2.11.7. SHELL排序
这个排序非常复杂,看了程序就知道了。
首先需要一个递减的步长,这里我们使用的是9、5、3、1(最后的步长必须是1)。
工作原理是首先对相隔9-1个元素的所有内容排序,然后再使用同样的方法对相隔5-1个元素的排序
以次类推。
#include <iostream.h>
void ShellSort(int* pData,int Count)
{
    int step[4];
    step[0] = 9;
    step[1] = 5;
    step[2] = 3;
    step[3] = 1;
    int iTemp;
    int k,s,w;
    for(int i=0;i<4;i++)
    {
        k = step[i];
        s = -k;
        for(int j=k;j<Count;j++)
        {
            iTemp = pData[j];
            w = j-k;//求上step个元素的下标
            if(s ==0)
            {
                s = -k;
                s++;
                pData[s] = iTemp;
            }
            while((iTemp<pData[w]) && (w>=0) && (w<=Count))
            {
                pData[w+k] = pData[w];
                w = w-k;
            }
            pData[w+k] = iTemp;
        }
    }
}
void main()
{
    int data[] = {10,9,8,7,6,5,4,3,2,1,-10,-1};
    ShellSort(data,12);
    for (int i=0;i<12;i++)
        cout<<data[i]<<" ";
    cout<<"\n";
}
呵呵,程序看起来有些头疼。不过也不是很难,把s==0的块去掉就轻松多了,这里是避免使用0步长造成程序异常而写的代码。这个代码我认为很值得一看。
这个算法的得名是因为其发明者的名字D.L.SHELL。依照参考资料上的说法:“由于复杂的数学原因避免使用2的幂次步长,它能降低算法效率。”另外算法的复杂度为n的1.2次幂。
2.11.8. 基于模板的通用排序
这个程序我想就没有分析的必要了,大家看一下就可以了。
类似于冒泡排序
MyData.h文件
///////////////////////////////////////////////////////
class CMyData 
{
public:
    CMyData(int Index,char* strData);
    CMyData();
    virtual ~CMyData();
    int m_iIndex;
    int GetDataSize(){ return m_iDataSize; };
    const char* GetData(){ return m_strDatamember; };
    //这里重载了操作符:
    CMyData& operator =(CMyData &SrcData);
    bool operator <(CMyData& data );
    bool operator >(CMyData& data );
private:
    char* m_strDatamember;
    int m_iDataSize;
};
////////////////////////////////////////////////////////
MyData.cpp文件
////////////////////////////////////////////////////////
CMyData::CMyData():
m_iIndex(0),
m_iDataSize(0),
m_strDatamember(NULL)
{
}
CMyData::~CMyData()
{
    if(m_strDatamember != NULL)
        delete[] m_strDatamember;
    m_strDatamember = NULL;
}
CMyData::CMyData(int Index,char* strData):
m_iIndex(Index),
m_iDataSize(0),
m_strDatamember(NULL)
{
    m_iDataSize = strlen(strData);
    m_strDatamember = new char[m_iDataSize+1];
    strcpy(m_strDatamember,strData);
}
CMyData& CMyData::operator =(CMyData &SrcData)
{
    m_iIndex = SrcData.m_iIndex;
    m_iDataSize = SrcData.GetDataSize();
    m_strDatamember = new char[m_iDataSize+1];
    strcpy(m_strDatamember,SrcData.GetData());
    return *this;
}
bool CMyData::operator <(CMyData& data )
{
    return m_iIndex<data.m_iIndex;
}
bool CMyData::operator >(CMyData& data )
{
    return m_iIndex>data.m_iIndex;
}
///////////////////////////////////////////////////////////
//////////////////////////////////////////////////////////
//主程序部分
#include <iostream.h>
#include "MyData.h"
template <class T>
void run(T* pData,int left,int right)
{
    int i,j;
    T middle,iTemp;
    i = left;
    j = right;
    //下面的比较都调用我们重载的操作符函数
    middle = pData[(left+right)/2];  //求中间值
    do{
        while((pData[i]<middle) && (i<right))//从左扫描大于中值的数
            i++;          
        while((pData[j]>middle) && (j>left))//从右扫描大于中值的数
            j--;
        if(i<=j)//找到了一对值
        {
            //交换
            iTemp = pData[i];
            pData[i] = pData[j];
            pData[j] = iTemp;
            i++;
            j--;
        }
    }while(i<=j);//如果两边扫描的下标交错,就停止(完成一次)
    //当左边部分有值(left<j),递归左半边
    if(left<j)
        run(pData,left,j);
    //当右边部分有值(right>i),递归右半边
    if(right>i)
        run(pData,i,right);
}
template <class T>
void QuickSort(T* pData,int Count)
{
    run(pData,0,Count-1);
}
void main()
{
    CMyData data[] = {
        CMyData(8,"xulion"),
        CMyData(7,"sanzoo"),
        CMyData(6,"wangjun"),
        CMyData(5,"VCKBASE"),
        CMyData(4,"jacky2000"),
        CMyData(3,"cwally"),
        CMyData(2,"VCUSER"),
        CMyData(1,"isdong")
    };
    QuickSort(data,8);
    for (int i=0;i<8;i++)
        cout<<data[i].m_iIndex<<"  "<<data[i].GetData()<<"\n";
    cout<<"\n";
}

2.12. 查找
查找就是在按某种数据结构形式存储的数据集合中,找出满足指定条件的结点。
按查找的条件分类:
u  有按结点的关键码查找;
u  关键码以外的其他数据项查找;
u  其他数据项的组合查找;
按查找数据在内存或外存:分内存查找和外存查找。
按查找目的:
u  查找如果只是为了确定指定条件的结点存在与否,成为静态查找;
u  查找是为确定结点的插入位置或为了删除找到的结点,称为动态查找。
这里简单介绍几种常见的查找方法。
2.12.1. 顺序存储线性表的查找
这是最常见的查找方式。结点集合按线性表组织,采用顺序存储方式,结点只含关键码,并且是整数。如果线性表无序,则采用顺序查找,即从线性表的一端开始逐一查找。而如果线性表有序,则可以使用顺序查找、二分法查找或插值查找。
2.12.2. 分块查找
分块查找的过程分两步,先用二分法在索引表中查索引项,确定要查的结点在哪一块。然后,再在相应块内顺序查找。
2.12.3. 链接存储线性表的查找
对于链接存储线性表的查找只能从链表的首结点开始顺序查找。同样对于无序的链表和有序的链表查找方法不同。
动态查找表的不同表示方法:
二叉排序树(二叉查找树):或者是一棵空树,或者是具有下列性质的一棵树:
u  若左子树不空,则左子树上的所有节点的值都小于根节点的值;
u  若右子树不空,则右子树上的所有节点的值都大于根节点的值;
u  它的左右子树也分别是二叉排序树;
二叉排序树的查找分析:在随机的情况下,其平均查找长度为1+4logn;
平衡二叉树:或者是棵空树,或者是具有下列性质的二叉树:
u  它的左子树和右子树都是平衡二叉树;
u  左子树和右子树的深度之差不会超过1;
平衡二叉树的查找分析:在查找过程中和给定值进行比较的关键字个数不超过树的深度,,因此其平均查找的时间复杂度是O(logn);
2.12.4. 散列表的查找
散列表又称杂凑表,是一种非常实用的查找技术。它的原理是在结点的存储位置和它的关键码间建立一个确定的关系,从而让查找码直接利用这个关系确定结点的位置。其技术的关键在于解决两个问题。
I.找一个好的散列函数
II.设计有效解决冲突的方法
常见的散列函数有:
I.质数除取余法
II.基数转换法
III.平方取中法
IV.折叠法
V.移位法
2.12.5. 常见的解决冲突的方法
I.线性探查法
II.双散列函数法
III.拉链法
假设HASH表的地址集为0-n-1,冲突是指由关键字得到的HASH地址的位置上已经存在记录,则处理冲突就是为该关键字的记录找到另一个空的HASH地址用于存放。
处理冲突的方法:
开放地址法(线性探查法):二次地址=(一次地址+增量序列) MOD 散列表长度M
再HASH法(双散列函数法):使用不同的散列函数计算,互相作为补偿;
二次地址=RH(key) R和H都是散列函数
拉链法(链地址法):设立一个指针数组,初始状态是空指针,HASH地址为i的记录都插入到指针数组中第i项所指向的链表中,保持关键字有序。
建立一个公共的溢出区:将所有冲突的关键字和记录都添入到溢出区。
HASH查找分析: HASH的查找长度与查找表的长度无关,只与装添因子有关
装添因子=表中添入的记录数/HASH的长度
2.12.6. 查找算法实现
//线性查找算法1
long linearSearch(Type* arr,long len,Type target)
{
       long i=0,ansPos;
       ansPos=-1;
       for(i=0;i<len;i++)
              if(arr[i]==target)
              {
                     ansPos=i;
                     break;
              }
       return ansPos;
}
//改进的线性查找算法
long linearSearch2(Type* arr,long len,Type target)
{
       long i,ansPos;
       ansPos=-1;
       for(i=0;i<len;i++)
              if(arr[i]>=target)
             {
           ansPos=i;
                    break;
              }
       if(arr[i]>target)
       {
              ansPos=-1;
              printf("\n data not found.\n");
       }
       return ansPos;
}

//使用顺序查找法的查找函数
//seqSearch(const int arr[],int first,int last,int target)
template <typename T>
int seqSearch(const T arr[],int first,int last,const T& target)
{
 int i=first;
 //扫描下标范围first<=i<last; 测试是否有匹配
 //或下标超出范围
 while (!(i==last)&&!(arr[i]==target))
  i++;
 return i;    //i是匹配值的下标,或者,如果没有匹配,则i=last
}

//模板函数find_last_of()的实现
template <typename T>
int find_last_of(const T arr[],int first,int last,const T& target)
{
 int i=last-1;

 //描扫下标范围first<=i<last;测试是否有匹配
 //或下标超出范围
 while(i>=first&&target!=arr[i])
  i--;
 if (i<first) return last; //没找到,则返回last
 return i;
}

//二分查找算法函数binSearch()的实现
template <typename T>
int binSearch(const T arr[],int first,int last,const T& target)
{
 int mid;                    //中间点的下标
 T midValue;               //用于保存arr[mid]元素值
 int origLast=last;          //保存last的初始值
 while(first<last)           //测试非空子表,如果是非空子表,则继续查找
 {
  mid=(first+last)/2;
  midValue=arr[mid];
  if (target==midValue)   //有一个匹配
   return mid;
  //确定要查找哪个子表
  else if(target<midValue)
   last=mid;
  else
   first=mid+1;        //查找子表的后半部分,重新设置first
 }
 return origLast;            //没有找到目标值
}

2.13. 常用算法分析
2.13.1. 反转字符串

2.13.2. 字符串连接

2.13.3. Strstr()

2.13.4. Void *memcpy(void *dest, const void *src, size_t count)

2.13.5. 七孔桥问题


2.13.6. 质数
#include "iostream.h"
#include "stdio.h"

void main()
{
 int count=0;
 for(int i=100;i<1000;i++)
 {
  //偶数,就跳过,它肯定不是质数
  if(i%2==0)
   continue;
  //判断3,5,7,9……i/2是否有i的因子
  int j=3;
  while(j<=i/2&&i%j!=0)
   j+=2;
  //若上述数都不是i的因子,则i是质数
  if(j>i/2)
  {
//每行输出 8 个数,每8个数输出一回车键
   if(count%8==0)
    cout<<endl;
   //输出质数
   cout<<i<<"  ";
   count++;
  }
 }
 cout<<endl;
}
2.13.7. 素数
用一个数分别去除2到sqrt(这个数),如果能被整除,则表明此数不是素数,反之是素数
2.13.8. 最大公约数与最小公倍2.13.9. 数
main()
{
 int a,b,num1,num2,temp;
 printf("please input two numbers:\n");
 scanf("%d,%d",&num1,&num2);
 if(num1  { temp=num1;
  num1=num2; 
  num2=temp;
 }
a=num1;b=num2;
while(b!=0)/*利用辗除法,直到b为0为止*/
 {
  temp=a%b;
  a=b;
  b=temp;
 }
printf("gongyueshu:%d\n",a);
printf("gongbeishu:%d\n",num1*num2/a);
}

2.13.10. 分解质因数
应先找到一个最小的质数k,然后按下述步骤完成:
(1)如果这个质数恰等于n,则说明分解质因数的过程已经结束,打印出即可。
(2)如果n<>k,但n能被k整除,则应打印出k的值,并用n除以k的商,作为新的正整数你n,.重复执行第一步。
(3)如果n不能被k整除,则用k+1作为k的值,重复执行第一步。

2.13.11. 闰年的判断方法
if(year%400==0||(year%4==0&&year%100!=0))

2.13.12. Atoi

2.13.13. 十进制转16进制
char *dectohex(int dec, int len)
{
  static char buf[256];
  char cell[] = "0123456789ABCDEF";
  int i = 0;
  memset(buf, 0, 256);
  memset(buf, '0', len);
  while (dec != 0)
  {
    buf[i++] = cell[dec % 16];
    dec = dec / 16;
  }
  strcat(buf, "x0");
  return (strrev(buf));
}

2.13.14. 十进制转二进制

char *dectobin(int dec, int len)
{
  int i = 0;
  static char buf[256];
  memset(buf, 0, 256);
  memset(buf, '0', len);
  while (dec != 0)
  {
    buf[i++] = dec % 2+48;
    dec = dec / 2;
  }
  return strrev(buf);
}
2.13.15. 八进制转十进制
main()
{ char *p,s[6];int n;
p=s;
gets(p);
n=0;
while(*(p)!='\0')
{n=n*8+*p-'0';
p++;}
printf("%d",n);
}
3. 网络
3.1. 基础理论
3.1.1. UDP连接和TCP连接的异同3.1.2. 
前者只管传,不管数据到不到,无须建立连接.后者保证传输的数据准确,须要连结.
TCP需要进行三次握手
3.1.3. 什么是 socket
你经常听到人们谈论着 “socket”,或许你还不知道它的确切含义。现在让我告诉你:它是使用标准Unix 文件描述符 (file descriptor) 和其它程序通讯的方式。什么?你也许听到一些Unix高手(hacker)这样说过:“呀,Unix中的一切就是文件!”那个家伙也许正在说到一个事实:Unix 程序在执行任何形式的 I/O 的时候,程序是在读或者写一个文件描述符。一个文件描述符只是一个和打开的文件相关联的整数。但是(注意后面的话),这个文件可能是一个网络连接, FIFO,管道,终端,磁盘上的文件或者什么其它的东西。Unix 中所有的东西就是文件!所以,你想和Internet上别的程序通讯的时候,你将要使用到文件描述符。你必须理解刚才的话。现在你脑海中或许冒出这样的念头:“那么我从哪里得到网络通讯的文件描述符呢?”,这个问题无论如何我都要回答:你利用系统调用 socket(),它返回套接字描述符 (socket descriptor),然后你再通过它来进行send() 和 recv()调用。 “但是...”,你可能有很大的疑惑,“如果它是个文件描述符,那么为什 么不用一般调用read()和write()来进行套接字通讯?”简单的答案是:“你可以使用!”。详细的答案是:“你可以,但是使用send()和recv()让你更好的控制数据传输。”
存在这样一个情况:在我们的世界上,有很多种套接字。有DARPA Internet 地址 (Internet 套接字),本地节点的路径名 (Unix套接字),CCITT X.25地址 (你可以将X.25 套接字完全忽略)。也许在你的Unix 机器上还有其它的。我们在这里只讲第一种:Internet 套接字。

3.1.4. Internet 套接字的两种类型
  什么意思?有两种类型的Internet 套接字?是的。不,我在撒谎。其实还有很多,但是我可不想吓着你。我们这里只讲两种。除了这些, 我打算另外介绍的 "Raw Sockets" 也是非常强大的,很值得查阅。
那么这两种类型是什么呢?一种是"Stream Sockets"(流格式),另外一种是"Datagram Sockets"(数据包格式)。我们以后谈到它们的时候也会用到 "SOCK_STREAM" 和 "SOCK_DGRAM"。数据报套接字有时也叫“无连接套接字”(如果你确实要连接的时候可以用connect()。) 流式套接字是可靠的双向通讯的数据流。如果你向套接字按顺序输出“1,2”,那么它们将按顺序“1,2”到达另一边。它们是无错误的传递的,有自己的错误控制,在此不讨论。
有什么在使用流式套接字?你可能听说过 telnet,不是吗?它就使用流式套接字。你需要你所输入的字符按顺序到达,不是吗?同样,WWW浏览器使用的 HTTP 协议也使用它们来下载页面。实际上,当你通过端口80 telnet 到一个 WWW 站点,然后输入 “GET pagename” 的时候,你也可以得到 HTML 的内容。为什么流式套接字可以达到高质量的数据传输?这是因为它使用了“传输控制协议 (The Transmission Control Protocol)”,也叫 “TCP” (请参考 RFC-793 获得详细资料。)TCP 控制你的数据按顺序到达并且没有错误。你也许听到 “TCP” 是因为听到过 “TCP/IP”。这里的 IP 是指“Internet 协议”(请参考 RFC-791。) IP 只是处理 Internet 路由而已。
那么数据报套接字呢?为什么它叫无连接呢?为什么它是不可靠的呢?有这样的一些事实:如果你发送一个数据报,它可能会到达,它可能次序颠倒了。如果它到达,那么在这个包的内部是无错误的。数据报也使用 IP 作路由,但是它不使用 TCP。它使用“用户数据报协议 (User Datagram Protocol)”,也叫 “UDP” (请参考 RFC-768。)为什么它们是无连接的呢?主要是因为它并不象流式套接字那样维持一个连接。你只要建立一个包,构造一个有目标信息的IP 头,然后发出去。无需连接。它们通常使用于传输包-包信息。简单的应用程序有:tftp, bootp等等。
你也许会想:“假如数据丢失了这些程序如何正常工作?”我的朋友,每个程序在 UDP 上有自己的协议。例如,tftp 协议每发出的一个被接受到包,收到者必须发回一个包来说“我收到了!” (一个“命令正确应答”也叫“ACK” 包)。如果在一定时间内(例如5秒),发送方没有收到应答,它将重新发送,直到得到 ACK。这一ACK过程在实现 SOCK_DGRAM 应用程序的时候非常重要。

3.1.5. 网络理论
  既然我刚才提到了协议层,那么现在是讨论网络究竟如何工作和一些关于 SOCK_DGRAM 包是如何建立的例子。当然,你也可以跳过这一段, 如果你认为已经熟悉的话。现在是学习数据封装 (Data Encapsulation) 的时候了!它非常非常重 要。它重要性重要到你在网络课程学(图1:数据封装)习中无论如何也得也得掌握它。主要 的内容是:一个包,先是被第一个协议(在这里是TFTP )在它的报头(也许是报尾)包装(“封装”),然后,整个数据(包括 TFTP 头)被另外一个协议 (在这里是 UDP )封装,然后下一个( IP ),一直重复下去,直到硬件(物理) 层( 这里是以太网 )。当另外一台机器接收到包,硬件先剥去以太网头,内核剥去IP和UDP 头,TFTP程序再剥去TFTP头,最后得到数据。现在我们终于讲到声名狼藉的网络分层模型 (Layered Network Model)。这种网络模型在描述网络系统上相对其它模型有很多优点。例如, 你可以写一个套接字程序而不用关心数据的物理传输(串行口,以太网,连接单元接口 (AUI) 还是其它介质),因为底层的程序会为你处理它们。实际 的网络硬件和拓扑对于程序员来说是透明的。不说其它废话了,我现在列出整个层次模型。如果你要参加网络考试,可一定要记住:
应用层 (Application)
表示层 (Presentation)
会话层 (Session)
传输层(Transport)
网络层(Network)
数据链路层(Data Link)
物理层(Physical)
物理层是硬件(串口,以太网等等)。应用层是和硬件层相隔最远的--它 是用户和网络交互的地方。这个模型如此通用,如果你想,你可以把它作为修车指南。把它对应 到 Unix,结果是:
应用层(Application Layer) (telnet, ftp,等等)传输层(Host-to-Host Transport Layer) (TCP, UDP)Internet层(Internet Layer) (IP和路由)网络访问层 (Network Access Layer) (网络层,数据链路层和物理层)

现在,你可能看到这些层次如何协调来封装原始的数据了。
看看建立一个简单的数据包有多少工作?哎呀,你将不得不使用 "cat" 来建立数据包头!这仅仅是个玩笑。对于流式套接字你要作的是 send() 发送数据。对于数据报式套接字,你按照你选择的方式封装数据然后使用 sendto()。内核将为你建立传输层和 Internet 层,硬件完成网络访问层。 这就是现代科技。现在结束我们的网络理论速成班。哦,忘记告诉你关于路由的事情了。但是我不准备谈它,如果你真的关心,那么参考 IP RFC。

3.1.6. 结构体
  终于谈到编程了。在这章,我将谈到被套接字用到的各种数据类型。因为它们中的一些内容很重要了。首先是简单的一个:socket描述符。它是下面的类型:int
仅仅是一个常见的 int。从现在起,事情变得不可思议了,而你所需做的就是继续看下去。注 意这样的事实:有两种字节排列顺序:重要的字节 (有时叫 "octet",即八位位组) 在前面,或者不重要的字节在前面。前一种叫“网络字节顺序 (Network Byte Order)”。有些机器在内部是按照这个顺序储存数据,而另外 一些则不然。当我说某数据必须按照 NBO 顺序,那么你要调用函数(例如 htons() )来将它从本机字节顺序 (Host Byte Order) 转换过来。如果我没有 提到 NBO,那么就让它保持本机字节顺序。我的第一个结构(在这个技术手册TM中)--struct sockaddr.。这个结构 为许多类型的套接字储存套接字地址信息:
struct sockaddr {
   unsigned short sa_family; /* 地址家族, AF_xxx */
   char sa_data[14]; /*14字节协议地址*/
   };
sa_family 能够是各种各样的类型,但是在这篇文章中都是 "AF_INET"。 sa_data包含套接字中的目标地址和端口信息。这好像有点 不明智。
为了处理struct sockaddr,程序员创造了一个并列的结构: struct sockaddr_in ("in" 代表 "Internet"。)
struct sockaddr_in {
short int sin_family; /* 通信类型 */
unsigned short int sin_port; /* 端口 */
struct in_addr sin_addr; /* Internet 地址 */
unsigned char sin_zero[8]; /* 与sockaddr结构的长度相同*/
   };
用这个数据结构可以轻松处理套接字地址的基本元素。注意 sin_zero (它被加入到这个结构,并且长度和 struct sockaddr 一样) 应该使用函数 bzero() 或 memset() 来全部置零。 同时,这一重要的字节,一个指向 sockaddr_in结构体的指针也可以被指向结构体sockaddr并且代替它。这样的话即使 socket() 想要的是 struct sockaddr *,你仍然可以使用 struct sockaddr_in,并且在最后转换。同时,注意 sin_family 和 struct sockaddr 中的 sa_family 一致并能够设置为 "AF_INET"。最后,sin_port和 sin_addr 必须是网络字节顺序 (Network Byte Order)!你也许会反对道:"但是,怎么让整个数据结构 struct in_addr sin_addr 按照网络字节顺序呢?" 要知道这个问题的答案,我们就要仔细的看一看这 个数据结构: struct in_addr, 有这样一个联合 (unions):
/* Internet 地址 (一个与历史有关的结构) */
   struct in_addr {
   unsigned long s_addr;
   };
它曾经是个最坏的联合,但是现在那些日子过去了。如果你声明 "ina" 是数据结构 struct sockaddr_in 的实例,那么 "ina.sin_addr.s_addr" 就储 存4字节的 IP 地址(使用网络字节顺序)。如果你不幸的系统使用的还是恐怖的联合 struct in_addr ,你还是可以放心4字节的 IP 地址并且和上面 我说的一样(这是因为使用了“#define”。)

3.1.7. 本机转换
  我们现在到了新的章节。我们曾经讲了很多网络到本机字节顺序的转换,现在可以实践了!你能够转换两种类型: short (两个字节)和 long (四个字节)。这个函数对于变量类型 unsigned 也适用。假设你想将 short 从本机字节顺序转 换为网络字节顺序。用 "h" 表示 "本机 (host)",接着是 "to",然后用 "n" 表 示 "网络 (network)",最后用 "s" 表示 "short": h-to-n-s, 或者 htons() ("Host to Network Short")。
太简单了...
如果不是太傻的话,你一定想到了由"n","h","s",和 "l"形成的正确 组合,例如这里肯定没有stolh() ("Short to Long Host") 函数,不仅在这里 没有,所有场合都没有。但是这里有:
htons()--"Host to Network Short"
  htonl()--"Host to Network Long"
  ntohs()--"Network to Host Short"
  ntohl()--"Network to Host Long"
现在,你可能想你已经知道它们了。你也可能想:“如果我想改变 char 的顺序要怎么办呢?” 但是你也许马上就想到,“用不着考虑的”。你也许会想到:我的 68000 机器已经使用了网络字节顺序,我没有必要去调用 htonl() 转换 IP 地址。你可能是对的,但是当你移植你的程序到别的机器上的时候,你的程序将失败。可移植性!这里是 Unix 世界!记住:在你将数据放到网络上的时候,确信它们是网络字节顺序的。
最后一点:为什么在数据结构 struct sockaddr_in 中, sin_addr 和 sin_port 需要转换为网络字节顺序,而sin_family 需不需要呢? 答案是: sin_addr 和 sin_port 分别封装在包的 IP 和 UDP 层。因此,它们必须要 是网络字节顺序。但是 sin_family 域只是被内核 (kernel) 使用来决定在数 据结构中包含什么类型的地址,所以它必须是本机字节顺序。同时, sin_family 没有发送到网络上,它们可以是本机字节顺序。

3.1.8. IP 地址和如何处理它们
现在我们很幸运,因为我们有很多的函数来方便地操作 IP 地址。没有必要用手工计算它们,也没有必要用"<<"操作来储存成长整字型。首先,假设你已经有了一个sockaddr_in结构体ina,你有一个IP地址"132.241.5.10"要储存在其中,你就要用到函数inet_addr(),将IP地址从 点数格式转换成无符号长整型。使用方法如下:
ina.sin_addr.s_addr = inet_addr("132.241.5.10");
注意,inet_addr()返回的地址已经是网络字节格式,所以你无需再调用 函数htonl()。
我们现在发现上面的代码片断不是十分完整的,因为它没有错误检查。 显而易见,当inet_addr()发生错误时返回-1。记住这些二进制数字?(无符号数)-1仅仅和IP地址255.255.255.255相符合!这可是广播地址!大错特 错!记住要先进行错误检查。
好了,现在你可以将IP地址转换成长整型了。有没有其相反的方法呢? 它可以将一个in_addr结构体输出成点数格式?这样的话,你就要用到函数 inet_ntoa()("ntoa"的含义是"network to ascii"),就像这样:
printf("%s",inet_ntoa(ina.sin_addr));
它将输出IP地址。需要注意的是inet_ntoa()将结构体in-addr作为一个参数,不是长整形。同样需要注意的是它返回的是一个指向一个字符的 指针。它是一个由inet_ntoa()控制的静态的固定的指针,所以每次调用 inet_ntoa(),它就将覆盖上次调用时所得的IP地址。例如:
char *a1, *a2;
.
.
a1 = inet_ntoa(ina1.sin_addr); /* 这是198.92.129.1 */
a2 = inet_ntoa(ina2.sin_addr); /* 这是132.241.5.10 */
printf("address 1: %sn",a1);
printf("address 2: %sn",a2);
输出如下:
address 1: 132.241.5.10
address 2: 132.241.5.10
假如你需要保存这个IP地址,使用strcopy()函数来指向你自己的字符指针。
上面就是关于这个主题的介绍。稍后,你将学习将一个类 似"wintehouse.gov"的字符串转换成它所对应的IP地址(查阅域名服务,稍后)。

3.1.9. socket()函数
我想我不能再不提这个了-下面我将讨论一下socket()系统调用。
下面是详细介绍:
#include <sys/types.h>
#include <sys/socket.h>
int socket(int domain, int type, int protocol);
但是它们的参数是什么? 首先,domain 应该设置成 "AF_INET",就 象上面的数据结构struct sockaddr_in 中一样。然后,参数 type 告诉内核 是 SOCK_STREAM 类型还是 SOCK_DGRAM 类型。最后,把 protocol 设置为 "0"。(注意:有很多种 domain、type,我不可能一一列出了,请看 socket() 的 man帮助。当然,还有一个"更好"的方式去得到 protocol。同 时请查阅 getprotobyname() 的 man 帮助。)

socket() 只是返回你以后在系统调用种可能用到的 socket 描述符,或 者在错误的时候返回-1。全局变量 errno 中将储存返回的错误值。(请参考 perror() 的 man 帮助。)

3.1.10. bind()函数
  一旦你有一个套接字,你可能要将套接字和机器上的一定的端口关联起来。(如果你想用listen()来侦听一定端口的数据,这是必要一步--MUD 告 诉你说用命令 "telnet x.y.z 6969"。)如果你只想用 connect(),那么这个步 骤没有必要。但是无论如何,请继续读下去。
这里是系统调用 bind() 的大概:
#include <sys/types.h>
#include <sys/socket.h>
int bind(int sockfd, struct sockaddr *my_addr, int addrlen);
sockfd 是调用 socket 返回的文件描述符。my_addr 是指向数据结构 struct sockaddr 的指针,它保存你的地址(即端口和 IP 地址) 信息。 addrlen 设置为 sizeof(struct sockaddr)。
简单得很不是吗? 再看看例子:
#include <string.h>
#include <sys/types.h>
#include <sys/socket.h>
#define MYPORT 3490
main()
   {
   int sockfd;
   struct sockaddr_in my_addr;
sockfd = socket(AF_INET, SOCK_STREAM, 0); /*需要错误检查 */
my_addr.sin_family = AF_INET; /* host byte order */
   my_addr.sin_port = htons(MYPORT); /* short, network byte order */
   my_addr.sin_addr.s_addr = inet_addr("132.241.5.10");
   bzero(&(my_addr.sin_zero),; /* zero the rest of the struct */
/* don't forget your error checking for bind(): */
   bind(sockfd, (struct sockaddr *)&my_addr, sizeof(struct sockaddr));
  这里也有要注意的几件事情。my_addr.sin_port 是网络字节顺序, my_addr.sin_addr.s_addr 也是的。另外要注意到的事情是因系统的不同, 包含的头文件也不尽相同,请查阅本地的 man 帮助文件。
在 bind() 主题中最后要说的话是,在处理自己的 IP 地址和/或端口的 时候,有些工作是可以自动处理的。
my_addr.sin_port = 0; /* 随机选择一个没有使用的端口 */
  my_addr.sin_addr.s_addr = INADDR_ANY; /* 使用自己的IP地址 */
通过将0赋给 my_addr.sin_port,你告诉 bind() 自己选择合适的端 口。同样,将 my_addr.sin_addr.s_addr 设置为 INADDR_ANY,你告诉 它自动填上它所运行的机器的 IP 地址。如果你一向小心谨慎,那么你可能注意到我没有将 INADDR_ANY 转换为网络字节顺序!这是因为我知道内部的东西:INADDR_ANY 实际上就 是 0!即使你改变字节的顺序,0依然是0。但是完美主义者说应该处处一致,INADDR_ANY或许是12呢?你的代码就不能工作了,那么就看下面 的代码:
my_addr.sin_port = htons(0); /* 随机选择一个没有使用的端口 */
my_addr.sin_addr.s_addr = htonl(INADDR_ANY);/* 使用自己的IP地址 */
你或许不相信,上面的代码将可以随便移植。我只是想指出,既然你 所遇到的程序不会都运行使用htonl的INADDR_ANY。
bind() 在错误的时候依然是返回-1,并且设置全局错误变量errno。
在你调用 bind() 的时候,你要小心的另一件事情是:不要采用小于 1024的端口号。所有小于1024的端口号都被系统保留!你可以选择从1024 到65535的端口(如果它们没有被别的程序使用的话)。 你要注意的另外一件小事是:有时候你根本不需要调用它。如果你使 用 connect() 来和远程机器进行通讯,你不需要关心你的本地端口号(就象你在使用 telnet 的时候),你只要简单的调用 connect() 就可以了,它会检查套接字是否绑定端口,如果没有,它会自己绑定一个没有使用的本地端 口。

3.1.11. connect()程序
  现在我们假设你是个 telnet 程序。你的用户命令你得到套接字的文件 描述符。你听从命令调用了socket()。下一步,你的用户告诉你通过端口 23(标准 telnet 端口)连接到"132.241.5.10"。你该怎么做呢? 幸运的是,你正在阅读 connect()--如何连接到远程主机这一章。你可不想让你的用户失望。 connect() 系统调用是这样的:
#include <sys/types.h>
#include <sys/socket.h>
int connect(int sockfd, struct sockaddr *serv_addr, int addrlen);
sockfd 是系统调用 socket() 返回的套接字文件描述符。serv_addr 是 保存着目的地端口和 IP 地址的数据结构 struct sockaddr。addrlen 设置 为 sizeof(struct sockaddr)。
想知道得更多吗?让我们来看个例子:
#include <string.h>
#include <sys/types.h>
#include <sys/socket.h>
#define DEST_IP "132.241.5.10"
  #define DEST_PORT 23
main()
   {
int sockfd;
struct sockaddr_in dest_addr; /* 目的地址*/
sockfd = socket(AF_INET, SOCK_STREAM, 0); /* 错误检查 */
dest_addr.sin_family = AF_INET; /* host byte order */
dest_addr.sin_port = htons(DEST_PORT); /* short, network byte order */
dest_addr.sin_addr.s_addr = inet_addr(DEST_IP);
bzero(&(dest_addr.sin_zero),; /* zero the rest of the struct */
/* don't forget to error check the connect()! */
connect(sockfd, (struct sockaddr *)&dest_addr, sizeof(struct sockaddr));
   .
   .
   .
  再一次,你应该检查 connect() 的返回值--它在错误的时候返回-1,并 设置全局错误变量 errno。同时,你可能看到,我没有调用 bind()。因为我不在乎本地的端口号。 我只关心我要去那。内核将为我选择一个合适的端口号,而我们所连接的 地方也自动地获得这些信息。一切都不用担心。
3.1.12. listen()函数
  是换换内容得时候了。假如你不希望与远程的一个地址相连,或者说,仅仅是将它踢开,那你就需要等待接入请求并且用各种方法处理它们。处 理过程分两步:首先,你听--listen(),然后,你接受--accept() (请看下面的 内容)。
除了要一点解释外,系统调用 listen 也相当简单。
int listen(int sockfd, int backlog);
sockfd 是调用 socket() 返回的套接字文件描述符。backlog 是在进入 队列中允许的连接数目。什么意思呢? 进入的连接是在队列中一直等待直到你接受 (accept() 请看下面的文章)连接。它们的数目限制于队列的允许。 大多数系统的允许数目是20,你也可以设置为5到10。和别的函数一样,在发生错误的时候返回-1,并设置全局错误变量 errno。你可能想象到了,在你调用 listen() 前你或者要调用 bind() 或者让内核随便选择一个端口。如果你想侦听进入的连接,那么系统调用的顺序可 能是这样的:
socket();
  bind();
listen();
  /* accept() 应该在这 */
因为它相当的明了,我将在这里不给出例子了。(在 accept() 那一章的 代码将更加完全。)真正麻烦的部分在 accept()。

3.1.13. accept()函数
  准备好了,系统调用 accept() 会有点古怪的地方的!你可以想象发生 这样的事情:有人从很远的地方通过一个你在侦听 (listen()) 的端口连接 (connect()) 到你的机器。它的连接将加入到等待接受 (accept()) 的队列 中。你调用 accept() 告诉它你有空闲的连接。它将返回一个新的套接字文件描述符!这样你就有两个套接字了,原来的一个还在侦听你的那个端口, 新的在准备发送 (send()) 和接收 ( recv()) 数据。这就是这个过程!函数是这样定义的:
#include <sys/socket.h>
int accept(int sockfd, void *addr, int *addrlen);
sockfd 相当简单,是和 listen() 中一样的套接字描述符。addr 是个指 向局部的数据结构 sockaddr_in 的指针。这是要求接入的信息所要去的地方(你可以测定那个地址在那个端口呼叫你)。在它的地址传递给 accept 之 前,addrlen 是个局部的整形变量,设置为 sizeof(struct sockaddr_in)。 accept 将不会将多余的字节给 addr。如果你放入的少些,那么它会通过改
变 addrlen 的值反映出来。
同样,在错误时返回-1,并设置全局错误变量 errno。
现在是你应该熟悉的代码片段。
#include <string.h>
#include <sys/socket.h>
#include <sys/types.h>
#define MYPORT 3490 /*用户接入端口*/
#define BACKLOG 10 /* 多少等待连接控制*/
main()
   {
  int sockfd, new_fd; /* listen on sock_fd, new connection on new_fd */
  struct sockaddr_in my_addr; /* 地址信息 */
  struct sockaddr_in their_addr; /* connector's address information */
  int sin_size;
sockfd = socket(AF_INET, SOCK_STREAM, 0); /* 错误检查*/
my_addr.sin_family = AF_INET; /* host byte order */
  my_addr.sin_port = htons(MYPORT); /* short, network byte order */
  my_addr.sin_addr.s_addr = INADDR_ANY; /* auto-fill with my IP */
  bzero(&(my_addr.sin_zero),; /* zero the rest of the struct */
/* don't forget your error checking for these calls: */
  bind(sockfd, (struct sockaddr *)&my_addr, sizeof(struct sockaddr));
listen(sockfd, BACKLOG);
sin_size = sizeof(struct sockaddr_in);
  new_fd = accept(sockfd, &their_addr, &sin_size);
   .
   .
   .
注意,在系统调用 send() 和 recv() 中你应该使用新的套接字描述符 new_fd。如果你只想让一个连接进来,那么你可以使用 close() 去关闭原来的文件描述符 sockfd 来避免同一个端口更多的连接。

3.1.14. send() and recv()函数
  这两个函数用于流式套接字或者数据报套接字的通讯。如果你喜欢使 用无连接的数据报套接字,你应该看一看下面关于sendto() 和 recvfrom() 的章节。
send() 是这样的:
int send(int sockfd, const void *msg, int len, int flags);
sockfd 是你想发送数据的套接字描述符(或者是调用 socket() 或者是 accept() 返回的。)msg 是指向你想发送的数据的指针。len 是数据的长度。 把 flags 设置为 0 就可以了。(详细的资料请看 send() 的 man page)。
这里是一些可能的例子:
char *msg = "Beej was here!";
  int len, bytes_sent;
  .
  len = strlen(msg);
  bytes_sent = send(sockfd, msg, len, 0);
  .   .
send() 返回实际发送的数据的字节数--它可能小于你要求发送的数 目! 注意,有时候你告诉它要发送一堆数据可是它不能处理成功。它只是发送它可能发送的数据,然后希望你能够发送其它的数据。记住,如果 send() 返回的数据和 len 不匹配,你就应该发送其它的数据。但是这里也有个好消息:如果你要发送的包很小(小于大约 1K),它可能处理让数据一 次发送完。最后要说得就是,它在错误的时候返回-1,并设置 errno。
recv() 函数很相似:
int recv(int sockfd, void *buf, int len, unsigned int flags);
sockfd 是要读的套接字描述符。buf 是要读的信息的缓冲。len 是缓 冲的最大长度。flags 可以设置为0。(请参考recv() 的 man page。) recv() 返回实际读入缓冲的数据的字节数。或者在错误的时候返回-1, 同时设置 errno。
很简单,不是吗? 你现在可以在流式套接字上发送数据和接收数据了。 你现在是 Unix 网络程序员了!
3.1.15. sendto() 和 recvfrom()函数
  “这很不错啊”,你说,“但是你还没有讲无连接数据报套接字呢?” 没问题,现在我们开始这个内容。
既然数据报套接字不是连接到远程主机的,那么在我们发送一个包之 前需要什么信息呢? 不错,是目标地址!看看下面的:
int sendto(int sockfd, const void *msg, int len, unsigned int flags,
  const struct sockaddr *to, int tolen);
你已经看到了,除了另外的两个信息外,其余的和函数 send() 是一样 的。 to 是个指向数据结构 struct sockaddr 的指针,它包含了目的地的 IP 地址和端口信息。tolen 可以简单地设置为 sizeof(struct sockaddr)。 和函数 send() 类似,sendto() 返回实际发送的字节数(它也可能小于 你想要发送的字节数!),或者在错误的时候返回 -1。
相似的还有函数 recv() 和 recvfrom()。recvfrom() 的定义是这样的:
int recvfrom(int sockfd, void *buf, int len, unsigned int flags,  struct sockaddr *from, int *fromlen);
又一次,除了两个增加的参数外,这个函数和 recv() 也是一样的。from 是一个指向局部数据结构 struct sockaddr 的指针,它的内容是源机器的 IP 地址和端口信息。fromlen 是个 int 型的局部指针,它的初始值为 sizeof(struct sockaddr)。函数调用返回后,fromlen 保存着实际储存在 from 中的地址的长度。
recvfrom() 返回收到的字节长度,或者在发生错误后返回 -1。
记住,如果你用 connect() 连接一个数据报套接字,你可以简单的调 用 send() 和 recv() 来满足你的要求。这个时候依然是数据报套接字,依然使用 UDP,系统套接字接口会为你自动加上了目标和源的信息。

3.1.16. close()和shutdown()函数
  你已经整天都在发送 (send()) 和接收 (recv()) 数据了,现在你准备关 闭你的套接字描述符了。这很简单,你可以使用一般的 Unix 文件描述符 的 close() 函数:
  close(sockfd);
它将防止套接字上更多的数据的读写。任何在另一端读写套接字的企 图都将返回错误信息。
如果你想在如何关闭套接字上有多一点的控制,你可以使用函数 shutdown()。它允许你将一定方向上的通讯或者双向的通讯(就象close()一 样)关闭,你可以使用:
int shutdown(int sockfd, int how);
sockfd 是你想要关闭的套接字文件描述复。how 的值是下面的其中之 一:
  0 – 不允许接受
  1 – 不允许发送
  2 – 不允许发送和接受(和 close() 一样)
shutdown() 成功时返回 0,失败时返回 -1(同时设置 errno。) 如果在无连接的数据报套接字中使用shutdown(),那么只不过是让 send() 和 recv() 不能使用(记住你在数据报套接字中使用了 connect 后 是可以使用它们的)。

3.1.17. getpeername()函数
  这个函数太简单了。
它太简单了,以至我都不想单列一章。但是我还是这样做了。 函数 getpeername() 告诉你在连接的流式套接字上谁在另外一边。函数是这样的:
#include <sys/socket.h>
int getpeername(int sockfd, struct sockaddr *addr, int *addrlen);
sockfd 是连接的流式套接字的描述符。addr 是一个指向结构 struct sockaddr (或者是 struct sockaddr_in) 的指针,它保存着连接的另一边的信息。addrlen 是一个 int 型的指针,它初始化为 sizeof(struct sockaddr)。函数在错误的时候返回 -1,设置相应的 errno。 一旦你获得它们的地址,你可以使用 inet_ntoa() 或者 gethostbyaddr() 来打印或者获得更多的信息。但是你不能得到它的帐号。(如果它运行着愚蠢的守护进程,这是可能的,但是它的讨论已经超出了本文的范围,请参考 RFC-1413 以获得更多的信息。)

3.1.18. gethostname()函数
  甚至比 getpeername() 还简单的函数是 gethostname()。它返回你程 序所运行的机器的主机名字。然后你可以使用 gethostbyname() 以获得你 的机器的 IP 地址。
  下面是定义:
  #include <unistd.h>
int gethostname(char *hostname, size_t size);
参数很简单:hostname 是一个字符数组指针,它将在函数返回时保存
主机名。size是hostname 数组的字节长度。
函数调用成功时返回 0,失败时返回 -1,并设置 errno。

3.1.19. 域名3.1.20. 服3.1.21. 务(DNS)
  如果你不知道 DNS 的意思,那么我告诉你,它代表域名服务(Domain Name Service)。它主要的功能是:你给它一个容易记忆的某站点的地址,它给你 IP 地址(然后你就可以使用 bind(), connect(), sendto() 或者其它 函数) 。当一个人输入:
   $ telnet whitehouse.gov
telnet 能知道它将连接 (connect()) 到 "198.137.240.100"。
但是这是如何工作的呢? 你可以调用函数 gethostbyname():
#include <netdb.h>
  struct hostent *gethostbyname(const char *name);
很明白的是,它返回一个指向 struct hostent 的指针。这个数据结构 是这样的:
   struct hostent {
   char *h_name;
   char **h_aliases;
   int h_addrtype;
   int h_length;
   char **h_addr_list;
   };
   #define h_addr h_addr_list[0]
这里是这个数据结构的详细资料:
struct hostent:
  h_name – 地址的正式名称。
  h_aliases – 空字节-地址的预备名称的指针。
  h_addrtype –地址类型; 通常是AF_INET。
  h_length – 地址的比特长度。
  h_addr_list – 零字节-主机网络地址指针。网络字节顺序。
  h_addr - h_addr_list中的第一地址。
gethostbyname() 成功时返回一个指向结构体 hostent 的指针,或者 是个空 (NULL) 指针。(但是和以前不同,不设置errno,h_errno 设置错 误信息。请看下面的 herror()。)
但是如何使用呢? 有时候(我们可以从电脑手册中发现),向读者灌输 信息是不够的。这个函数可不象它看上去那么难用。
这里是个例子:
#include <stdio.h>
  #include <stdlib.h>
  #include <errno.h>
  #include <netdb.h>
  #include <sys/types.h>
  #include <netinet/in.h>
int main(int argc, char *argv[])
   {
   struct hostent *h;
if (argc != 2) { /* 检查命令行 */
   fprintf(stderr,"usage: getip addressn");
   exit(1);
   }
if ((h=gethostbyname(argv[1])) == NULL) { /* 取得地址信息 */
   herror("gethostbyname");
   exit(1);
   }
printf("Host name : %sn", h->h_name);
  printf("IP Address : %sn",inet_ntoa(*((struct in_addr *)h->h_addr)));
return 0;
   }
在使用 gethostbyname() 的时候,你不能用 perror() 打印错误信息 (因为 errno 没有使用),你应该调用 herror()。
相当简单,你只是传递一个保存机器名的字符串(例如 "whitehouse.gov") 给 gethostbyname(),然后从返回的数据结构 struct hostent 中获取信息。
唯一也许让人不解的是输出 IP 地址信息。h->h_addr 是一个 char *, 但是 inet_ntoa() 需要的是 struct in_addr。因此,我转换 h->h_addr 成 struct in_addr *,然后得到数据。

3.1.22. 客户-服3.1.23. 务器背景知识
  这里是个客户--服务器的世界。在网络上的所有东西都是在处理客户进程和服务器进程的交谈。举个telnet 的例子。当你用 telnet (客户)通过23 号端口登陆到主机,主机上运行的一个程序(一般叫 telnetd,服务器)激活。它处理这个连接,显示登陆界面,等等。

图2:客户机和服务器的关系
图 2 说明了客户和服务器之间的信息交换。
注意,客户--服务器之间可以使用SOCK_STREAM、SOCK_DGRAM 或者其它(只要它们采用相同的)。一些很好的客户--服务器的例子有 telnet/telnetd、 ftp/ftpd 和 bootp/bootpd。每次你使用 ftp 的时候,在远 端都有一个 ftpd 为你服务。
一般,在服务端只有一个服务器,它采用 fork() 来处理多个客户的连 接。基本的程序是:服务器等待一个连接,接受 (accept()) 连接,然后 fork() 一个子进程处理它。这是下一章我们的例子中会讲到的。

3.1.24. 简单的服3.1.25. 务器
  这个服务器所做的全部工作是在流式连接上发送字符串 "Hello, World!n"。你要测试这个程序的话,可以在一台机器上运行该程序,然后 在另外一机器上登陆:
   $ telnet remotehostname 3490
remotehostname 是该程序运行的机器的名字。
服务器代码:
#include <stdio.h>
  #include <stdlib.h>
  #include <errno.h>
  #include <string.h>
  #include <sys/types.h>
  #include <netinet/in.h>
  #include <sys/socket.h>
  #include <sys/wait.h>
#define MYPORT 3490 /*定义用户连接端口*/
#define BACKLOG 10 /*多少等待连接控制*/
main()
   {
   int sockfd, new_fd; /* listen on sock_fd, new connection on new_fd
*/
   struct sockaddr_in my_addr; /* my address information */
   struct sockaddr_in their_addr; /* connector's address information */
   int sin_size;
if ((sockfd = socket(AF_INET, SOCK_STREAM, 0)) == -1) {
   perror("socket");
   exit(1);
   }

my_addr.sin_family = AF_INET; /* host byte order */
   my_addr.sin_port = htons(MYPORT); /* short, network byte order */
   my_addr.sin_addr.s_addr = INADDR_ANY; /* auto-fill with my IP */
   bzero(&(my_addr.sin_zero),; /* zero the rest of the struct */

if (bind(sockfd, (struct sockaddr *)&my_addr, sizeof(struct
sockaddr))== -1) {
   perror("bind");
   exit(1);
   }
if (listen(sockfd, BACKLOG) == -1) {
   perror("listen");
   exit(1);
   }

while(1) { /* main accept() loop */
   sin_size = sizeof(struct sockaddr_in);
   if ((new_fd = accept(sockfd, (struct sockaddr *)&their_addr,
   &sin_size)) == -1) {
   perror("accept");
   continue;
   }
   printf("server: got connection from %sn",
   inet_ntoa(their_addr.sin_addr));
   if (!fork()) { /* this is the child process */
   if (send(new_fd, "Hello, world!n", 14, 0) == -1)
   perror("send");
   close(new_fd);
   exit(0);
   }
   close(new_fd); /* parent doesn't need this */
while(waitpid(-1,NULL,WNOHANG) > 0); /* clean up child processes */
   }
   }
如果你很挑剔的话,一定不满意我所有的代码都在一个很大的main() 函数中。如果你不喜欢,可以划分得更细点。
你也可以用我们下一章中的程序得到服务器端发送的字符串。

3.1.26. 简单的客户程序
  这个程序比服务器还简单。这个程序的所有工作是通过 3490 端口连接到命令行中指定的主机,然后得到服务器发送的字符串。
客户代码:
#include <stdio.h>
  #include <stdlib.h>
  #include <errno.h>
  #include <string.h>
  #include <sys/types.h>
  #include <netinet/in.h>
  #include <sys/socket.h>
  #include <sys/wait.h>
#define PORT 3490 /* 客户机连接远程主机的端口 */
#define MAXDATASIZE 100 /* 每次可以接收的最大字节 */
int main(int argc, char *argv[])
   {
   int sockfd, numbytes;
   char buf[MAXDATASIZE];
   struct hostent *he;
   struct sockaddr_in their_addr; /* connector's address information */
if (argc != 2) {
   fprintf(stderr,"usage: client hostnamen");
   exit(1);
   }
if ((he=gethostbyname(argv[1])) == NULL) { /* get the host info */
   herror("gethostbyname");
   exit(1);
   }

if ((sockfd = socket(AF_INET, SOCK_STREAM, 0)) == -1) {
   perror("socket");
   exit(1);
   }

their_addr.sin_family = AF_INET; /* host byte order */
  their_addr.sin_port = htons(PORT); /* short, network byte order */
  their_addr.sin_addr = *((struct in_addr *)he->h_addr);
  bzero(&(their_addr.sin_zero),; /* zero the rest of the struct */
if (connect(sockfd, (struct sockaddr *)&their_addr,sizeof(struct
sockaddr)) == -1) {
   perror("connect");
   exit(1);
   }
if ((numbytes=recv(sockfd, buf, MAXDATASIZE, 0)) == -1) {
   perror("recv");
   exit(1);
   }
buf[numbytes] = '';
printf("Received: %s",buf);
close(sockfd);
return 0;
   }
注意,如果你在运行服务器之前运行客户程序,connect() 将返回 "Connection refused" 信息,这非常有用。

3.1.27. 数据包 Sockets
  我不想讲更多了,所以我给出代码 talker.c 和 listener.c。
listener 在机器上等待在端口 4590 来的数据包。talker 发送数据包到 一定的机器,它包含用户在命令行输入的内容。
这里就是 listener.c:
#include <stdio.h>
  #include <stdlib.h>
  #include <errno.h>
  #include <string.h>
  #include <sys/types.h>
  #include <netinet/in.h>
  #include <sys/socket.h>
  #include <sys/wait.h>
#define MYPORT 4950 /* the port users will be sending to */
#define MAXBUFLEN 100
main()
   {
   int sockfd;
   struct sockaddr_in my_addr; /* my address information */
   struct sockaddr_in their_addr; /* connector's address information */
   int addr_len, numbytes;
   char buf[MAXBUFLEN];
if ((sockfd = socket(AF_INET, SOCK_DGRAM, 0)) == -1) {
   perror("socket");
   exit(1);
   }
my_addr.sin_family = AF_INET; /* host byte order */
   my_addr.sin_port = htons(MYPORT); /* short, network byte order */
   my_addr.sin_addr.s_addr = INADDR_ANY; /* auto-fill with my IP */
   bzero(&(my_addr.sin_zero),; /* zero the rest of the struct */
if (bind(sockfd, (struct sockaddr *)&my_addr, sizeof(struct sockaddr))

   == -1) {
   perror("bind");
   exit(1);
   }
addr_len = sizeof(struct sockaddr);
   if ((numbytes=recvfrom(sockfd, buf, MAXBUFLEN, 0,
   (struct sockaddr *)&their_addr, &addr_len)) == -1) {
   perror("recvfrom");
   exit(1);
   }
printf("got packet from %sn",inet_ntoa(their_addr.sin_addr));
   printf("packet is %d bytes longn",numbytes);
   buf[numbytes] = '';
   printf("packet contains "%s"n",buf);
close(sockfd);
   }
注意在我们的调用 socket(),我们最后使用了 SOCK_DGRAM。同时, 没有必要去使用 listen() 或者 accept()。我们在使用无连接的数据报套接 字!
下面是 talker.c:
#include <stdio.h>
  #include <stdlib.h>
  #include <errno.h>
  #include <string.h>
  #include <sys/types.h>
  #include <netinet/in.h>
  #include <sys/socket.h>
  #include <sys/wait.h>
#define MYPORT 4950 /* the port users will be sending to */
int main(int argc, char *argv[])
   {
   int sockfd;
   struct sockaddr_in their_addr; /* connector's address information */
   struct hostent *he;
   int numbytes;

if (argc != 3) {
   fprintf(stderr,"usage: talker hostname messagen");
   exit(1);
   }

if ((he=gethostbyname(argv[1])) == NULL) { /* get the host info */
   herror("gethostbyname");
   exit(1);
   }

if ((sockfd = socket(AF_INET, SOCK_DGRAM, 0)) == -1) {
   perror("socket");
   exit(1);
   }

their_addr.sin_family = AF_INET; /* host byte order */
   their_addr.sin_port = htons(MYPORT); /* short, network byte order
*/
   their_addr.sin_addr = *((struct in_addr *)he->h_addr);
   bzero(&(their_addr.sin_zero),; /* zero the rest of the struct */
if ((numbytes=sendto(sockfd, argv[2], strlen(argv[2]), 0,
   (struct sockaddr *)&their_addr, sizeof(struct sockaddr))) == -1) {
   perror("sendto");
   exit(1);
   }
printf("sent %d bytes to
%sn",numbytes,inet_ntoa(their_addr.sin_addr));
close(sockfd);
return 0;
   }
这就是所有的了。在一台机器上运行 listener,然后在另外一台机器上 运行 talker。观察它们的通讯!
除了一些我在上面提到的数据套接字连接的小细节外,对于数据套接 字,我还得说一些,当一个讲话者呼叫connect()函数时并指定接受者的地址时,从这点可以看出,讲话者只能向connect()函数指定的地址发送和接受信息。因此,你不需要使用sendto()和recvfrom(),你完全可以用send() 和recv()代替。

3.1.28. 阻塞
  阻塞,你也许早就听说了。"阻塞"是 "sleep" 的科技行话。你可能注意到前面运行的 listener 程序,它在那里不停地运行,等待数据包的到来。实际在运行的是它调用 recvfrom(),然后没有数据,因此 recvfrom() 说" 阻塞 (block)",直到数据的到来。
很多函数都利用阻塞。accept() 阻塞,所有的 recv*() 函数阻塞。它 们之所以能这样做是因为它们被允许这样做。当你第一次调用 socket() 建立套接字描述符的时候,内核就将它设置为阻塞。如果你不想套接字阻塞,你就要调用函数 fcntl():
#include <unistd.h>
  #include <fontl.h>
   .
   .
   sockfd = socket(AF_INET, SOCK_STREAM, 0);
   fcntl(sockfd, F_SETFL, O_NONBLOCK);
   .
   .
  通过设置套接字为非阻塞,你能够有效地"询问"套接字以获得信息。如果你尝试着从一个非阻塞的套接字读信息并且没有任何数据,它不允许阻 塞--它将返回 -1 并将 errno 设置为 EWOULDBLOCK。
但是一般说来,这种询问不是个好主意。如果你让你的程序在忙等状 态查询套接字的数据,你将浪费大量的 CPU 时间。更好的解决之道是用下一章讲的 select() 去查询是否有数据要读进来。

3.1.29. select()--多路同3.1.30. 步 I/O
  虽然这个函数有点奇怪,但是它很有用。假设这样的情况:你是个服务器,你一边在不停地从连接上读数据,一边在侦听连接上的信息。 没问题,你可能会说,不就是一个 accept() 和两个 recv() 吗? 这么 容易吗,朋友? 如果你在调用 accept() 的时候阻塞呢? 你怎么能够同时接受 recv() 数据? “用非阻塞的套接字啊!” 不行!你不想耗尽所有的 CPU 吧? 那么,该如何是好?
select() 让你可以同时监视多个套接字。如果你想知道的话,那么它就 会告诉你哪个套接字准备读,哪个又准备写,哪个套接字又发生了例外 (exception)。
闲话少说,下面是 select():
#include <sys/time.h>
  #include <sys/types.h>
  #include <unistd.h>
int select(int numfds, fd_set *readfds, fd_set *writefds,fd_set
*exceptfds, struct timeval *timeout);
这个函数监视一系列文件描述符,特别是 readfds、writefds 和 exceptfds。如果你想知道你是否能够从标准输入和套接字描述符 sockfd 读入数据,你只要将文件描述符 0 和 sockfd 加入到集合 readfds 中。参 数 numfds 应该等于最高的文件描述符的值加1。在这个例子中,你应该 设置该值为 sockfd+1。因为它一定大于标准输入的文件描述符 (0)。 当函数 select() 返回的时候,readfds 的值修改为反映你选择的哪个文件描述符可以读。你可以用下面讲到的宏 FD_ISSET() 来测试。 在我们继续下去之前,让我来讲讲如何对这些集合进行操作。每个集合类型都是 fd_set。下面有一些宏来对这个类型进行操作:
FD_ZERO(fd_set *set) – 清除一个文件描述符集合
  FD_SET(int fd, fd_set *set) - 添加fd到集合
  FD_CLR(int fd, fd_set *set) – 从集合中移去fd
  FD_ISSET(int fd, fd_set *set) – 测试fd是否在集合中
最后,是有点古怪的数据结构 struct timeval。有时你可不想永远等待别人发送数据过来。也许什么事情都没有发生的时候你也想每隔96秒在终 端上打印字符串 "Still Going..."。这个数据结构允许你设定一个时间,如果时间到了,而 select() 还没有找到一个准备好的文件描述符,它将返回让你继续处理。
数据结构 struct timeval 是这样的:
struct timeval {
   int tv_sec; /* seconds */
   int tv_usec; /* microseconds */
   };
只要将 tv_sec 设置为你要等待的秒数,将 tv_usec 设置为你要等待的微秒数就可以了。是的,是微秒而不是毫秒。1,000微秒等于1毫秒,1,000 毫秒等于1秒。也就是说,1秒等于1,000,000微秒。为什么用符号 "usec" 呢? 字母 "u" 很象希腊字母 Mu,而 Mu 表示 "微" 的意思。当然,函数 返回的时候 timeout 可能是剩余的时间,之所以是可能,是因为它依赖于 你的 Unix 操作系统。
哈!我们现在有一个微秒级的定时器!别计算了,标准的 Unix 系统 的时间片是100毫秒,所以无论你如何设置你的数据结构 struct timeval,你都要等待那么长的时间。
还有一些有趣的事情:如果你设置数据结构 struct timeval 中的数据为 0,select() 将立即超时,这样就可以有效地轮询集合中的所有的文件描述符。如果你将参数 timeout 赋值为 NULL,那么将永远不会发生超时,即一直等到第一个文件描述符就绪。最后,如果你不是很关心等待多长时间,那么就把它赋为 NULL 吧。
下面的代码演示了在标准输入上等待 2.5 秒:
#include <sys/time.h>
  #include <sys/types.h>
  #include <unistd.h>
#define STDIN 0 /* file descriptor for standard input */
main()
   {
  struct timeval tv;
  fd_set readfds;
tv.tv_sec = 2;
  tv.tv_usec = 500000;
FD_ZERO(&readfds);
  FD_SET(STDIN, &readfds);
/* don't care about writefds and exceptfds: */
  select(STDIN+1, &readfds, NULL, NULL, &tv);
if (FD_ISSET(STDIN, &readfds))
  printf("A key was pressed!n");
  else
  printf("Timed out.n");
  }
如果你是在一个 line buffered 终端上,那么你敲的键应该是回车 (RETURN),否则无论如何它都会超时。
现在,你可能回认为这就是在数据报套接字上等待数据的方式--你是对 的:它可能是。有些 Unix 系统可以按这种方式,而另外一些则不能。你 在尝试以前可能要先看看本系统的 man page 了。
最后一件关于 select() 的事情:如果你有一个正在侦听 (listen()) 的套 接字,你可以通过将该套接字的文件描述符加入到 readfds 集合中来看是否有新的连接。
这就是我关于函数select() 要讲的所有的东西。

4. .Net学习
4.1. 基础理论
4.1.1. 托管代码
使用基于公共语言运行库的语言编译器开发的代码称为托管代码;托管代码具有许多优点,例如:跨语言集成、跨语言异常处理、增强的安全性、版本控制和部署支持、简化的组件交互模型、调试和分析服务等。
4.1.2. CTS
一种确定公共语言运行库如何定义、使用和管理类型的规范。
这个类型系统不但实现了COM的变量兼容类型,而且还定义了通过用户自定义类型的方式来进行类型扩展。任何以.NET平台作为目标的语言必须建立它的数据类型与CTS的类型间的映射。所有.NET语言共享这一类型系统,实现它们之间无缝的互操作。该方案还提供了语言之间的继承性。例如,用户能够在VB.NET中派生一个由C#编写的类。
4.1.3. CLS
要和其他对象完全交互,而不管这些对象是以何种语言实现的,对象必须只向调用方公开那些它们必须与之互用的所有语言的通用功能。为此定义了公共语言规范 (CLS),它是许多应用程序所需的一套基本语言功能。
CLS制定了一种以.NET平台为目标的语言所必须支持的最小特征,以及该语言与其他.NET语言之间实现互操作性所需要的完备特征。认识到这点很重要,这里讨论的特征问题已不仅仅是语言间的简单语法区别。例如,CLS并不去关心一种语言用什么关键字实现继承,只是关心该语言如何支持继承。
CLS是CTS的一个子集。这就意味着一种语言特征可能符合CTS标准,但又超出CLS的范畴。例如:C#支持无符号数字类型,该特征能通过CTS的测试,但CLS却仅仅识别符号数字类型。因此,如果用户在一个组件中使用C#的无符号类型,就可能不能与不使用无符号类型的语言(如VB.NET)设计的.NET组件实现互操作。这里用的是“可能不”,而不是“不可能”,因为这一问题实际依赖于对non-CLS-compliant项的可见性。事实上,CLS规则只适用于或部分适用于那些与其他组件存在联系的组件中的类型。实际上,用户能够安全实现含私有组件的项目,而该组件使用了用户所选择使用的.NET语言的全部功能,且无需遵守CLS的规范。另一方面,如果用户需要.NET语言的互操作性,那么用户的组件中的公共项必须完全符合CLS规范。
4.1.4. CLR
公共语言运行时(Common Language Runtime,CLR),提供了一个可靠而完善的多语言运行环境。CLR是一个软件引擎,用于加载应用程序、检查错误、进行安全许可认证、执行和清空内存。它属于纯动态运行时的一种,它的主要组成部分是虚拟执行引擎VEE(Virtual Execution Enging),它可以打破语言和平台的限制。
CLR是CTS的实现,也就是说,CLR是应用程序的执行引擎和功能齐全的类库,该类库严格按照CTS规范实现。作为程序执行引擎,CLR负责安全地载入和运行用户程序代码,包括对不用对象的垃圾回收和安全检查。在CLR监控之下运行的代码,称为托管代码(managed code)。作为类库,CLR提供上百个可用的有用类型,而这些类型可通过继承进行扩展。对于文件I/O、创建对话框、启动线程等类型—— 基本上能使用Windows API来完成的操作,都可由其完成。
4.1.5. GAC
全局程序集缓存
4.1.6. 公共语言运行库
.NET Framework的核心是其运行库的执行环境,称为公共语言运行库(CLR)或.NET运行库。通常将在CLR的控制下运行的代码称为托管代码(managed code)。
但是,在CLR执行开发的源代码之前,需要编译它们(在C#中或其他语言中)。在.NET中,编译分为两个阶段:
(1) 把源代码编译为Microsoft中间语言(IL)。
(2) CLR把IL编译为平台专用的代码。
这个两阶段的编译过程非常重要,因为Microsoft中间语言(托管代码)是提供.NET的许多优点的关键。
托管代码的优点
Microsoft中间语言与Java字节代码共享一种理念:它们都是一种低级语言,语法很简单(使用数字代码,而不是文本代码),可以非常快速地转换为内部机器码。对于代码来说,这种精心设计的通用语法,有很重要的优点。
1. 平台无关性
首先,这意味着包含字节代码指令的同一文件可以放在任一平台中,运行时编译过程的最后阶段可以很容易完成,这样代码就可以运行在该特定的平台上。换言之,编译为中间语言就可以获得.NET平台无关性,这与编译为Java字节代码就会得到Java平台无关性是一样的。
注意.NET的平台无关性目前只是一种可能,因为在编写本书时,.NET只能用于Windows平台,但人们正在积极准备,使它可以用于其他平台(参见Mono项目,它用于创建.NET的开放源代码的实现,参见http://www.go-mono.com/)。
2. 提高性能
前面把IL和Java做了比较,实际上,IL比Java字节代码的作用还要大。IL总是即时编译的(称为JIT编译),而Java字节代码常常是解释性的,Java的一个缺点是,在运行应用程序时,把Java字节代码转换为内部可执行代码的过程会导致性能的损失(但在最近,Java在某些平台上能进行JIT编译)。
JIT编译器并不是把整个应用程序一次编译完(这样会有很长的启动时间),而是只编译它调用的那部分代码(这是其名称由来)。代码编译过一次后,得到的内部可执行代码就存储起来,直到退出该应用程序为止,这样在下次运行这部分代码时,就不需要重新编译了。Microsoft认为这个过程要比一开始就编译整个应用程序代码的效率高得多,因为任何应用程序的大部分代码实际上并不是在每次运行过程中都执行。使用JIT编译器,从来都不会编译这种代码。
这解释了为什么托管IL代码的执行几乎和内部机器代码的执行速度一样快,但是并没有说明为什么Microsoft认为这会提高性能。其原因是编译过程的最后一部分是在运行时进行的,JIT编译器确切地知道程序运行在什么类型的处理器上,可以利用该处理器提供的任何特性或特定的机器代码指令来优化最后的可执行代码。
传统的编译器会优化代码,但它们的优化过程是独立于代码所运行的特定处理器的。这是因为传统的编译器是在发布软件之前编译为内部机器可执行的代码。即编译器不知道代码所运行的处理器类型,例如该处理器是x86兼容处理器还是Alpha处理器,这超出了基本操作的范围。例如Visual Studio 6为一般的Pentium机器进行了优化,所以它生成的代码就不能利用Pentium III处理器的硬件特性。相反,JIT编译器不仅可以进行Visual Studio 6所能完成的优化工作,还可以优化代码所运行的特定处理器。

3. 语言的互操作性
使用IL不仅支持平台无关性,还支持语言的互操作性。简言之,就是能将任何一种语言编译为中间代码,编译好的代码可以与从其他语言编译过来的代码进行交互操作。
那么除了C#之外,还有什么语言可以通过.NET进行交互操作呢?下面就简要讨论其他常见语言如何与.NET交互操作。
(1) Visual Basic 2005
Visual Basic 6在升级到Visual Basic .NET 2002时,经历了一番脱胎换骨的变化,才集成到.NET Framework的第一版中。Visual Basic 语言对Visual Basic 6进行了很大的演化,也就是说,Visual Basic 6并不适合运行.NET程序。例如,它与COM的高度集成,且只把事件处理程序作为源代码显示给开发人员,大多数后台代码不能用作源代码。另外,它不支持继承,Visual Basic使用的标准数据类型也与.NET不兼容。
Visual Basic 6在2002年升级为Visual Basic .NET,对Visual Basic进行的改变非常大,完全可以把Visual Basic当作是一种新语言。现有的Visual Basic 6代码不能编译为Visual Basic 2005代码(或Visual Basic .NET 2002和2003代码),把Visual Basic 6程序转换为Visual Basic 2005时,需要对代码进行大量的改动,但大多数修改工作都可以由Visual Studio 2005(Visual Studio的升级版本,用于与.NET一起使用)自动完成。如果要把一个Visual Basic 6项目读取到Visual Studio 2005中,Visual Studio 2005就会升级该项目,也就是说把Visual Basic 6源代码重写为Visual Basic 2005源代码。虽然这意味着其中的工作已大大减轻,但用户仍需要检查新的Visual Basic 2005代码,以确保项目仍可正确工作,因为这种转换并不十分完美。
这种语言升级的一个副作用是不能再把Visual Basic 2005编译为内部可执行代码了。Visual Basic 2005只编译为中间语言,就像C#一样。如果需要继续使用Visual Basic 6编写程序,就可以这么做,但生成的可执行代码会完全忽略.NET Framework,如果继续把Visual Studio作为开发环境,就需要安装Visual Studio 6。
(2) Visual C++ 2005
Visual C++ 6有许多Microsoft对Windows的特定扩展。通过Visual C++ .NET,又加入了更多的扩展内容,来支持.NET Framework。现有的C++源代码会继续编译为内部可执行代码,不会有修改,但它会独立于.NET运行库运行。如果要让C++代码在.NET Framework中运行,就要在代码的开头添加下述命令:
#using <mscorlib.dll>
还可以把标记/clr传递给编译器,这样编译器假定要编译托管代码,因此会生成中间语言,而不是内部机器码。C++的一个有趣的问题是在编译托管代码时,编译器可以生成包含内嵌本机可执行代码的IL。这表示在C++代码中可以把托管类型和非托管类型合并起来,因此托管C++代码:
class MyClass
{
定义了一个普通的C++类,而代码:
__gc class MyClass
{
生成了一个托管类,就好像使用C#或Visual Basic 2005编写类一样。实际上,托管C++比C#更优越的一点是可以在托管C++代码中调用非托管C++类,而不必采用COM交互功能。
如果在托管类型上试图使用.NET不支持的特性(例如,模板或类的多继承),编译器就会出现一个错误。另外,在使用托管类时,还需要使用非标准的C++特性(例如上述代码中的__gc关键字)。
因为C++允许低级指针操作,C++编译器不能生成可以通过CLR内存类型安全测试的代码。如果CLR把代码标识为内存类型安全是非常重要的,就需要用其他一些语言编写源代码,例如C# 或Visual Basic 2005。
(3) Visual J# 2005
最新添加的语言是Visual J# 2005。在.NET Framework 1.1版本推出之前,用户必须下载相应的软件,才能使用J#。现在J#语言内置于.NET Framework中。因此,J#用户可以利用Visual Studio 2005的所有常见特性。Microsoft希望大多数J++用户认为他们在使用.NET时,将很容易使用J#。J#不使用Java运行库,而是使用与其他.NET兼容语言一样的基类库。这说明,与C#和Visual Basic 2005一样,可以使用J#创建ASP.NET Web应用程序、Windows窗体、XML Web服务和其他应用程序。
(4) 脚本语言
脚本语言仍在使用之中,但由于.NET的推出,一般认为它们的重要性在降低。另一方面,JScript升级到JScript.NET。现在ASP.NET页面可以用JScript.NET编写,可以把JScript.NET当作一种编译语言来运行,而不是解释性的语言,也可以编写强类型化的JScript.NET代码。有了ASP.NET后,就没有必要在服务器端的Web页面上使用脚本语言了,但VBA仍用作Office文档和Visual Studio宏语言。
(5) COM和COM+
从技术上讲,COM 和 COM+并不是面向.NET的技术,因为基于它们的组件不能编译为IL(但如果原来的COM组件是用C++编写的,使用托管C++,在某种程度上可以这么做)。但是,COM+仍然是一个重要的工具,因为其特性没有在.NET中完全实现。另外,COM组件仍可以使用——.NET组合了COM的互操作性,从而使托管代码可以调用COM组件,COM组件也可以调用托管代码(见第33章)。在一般情况下,把新组件编写为.NET组件,其多数目的是比较方便,因为这样可以利用.NET基类和托管代码的其他优点。

4.1.7. 中间语言的特性
● 面向对象和使用接口
● 值类型和引用类型之间的巨大差别
● 强数据类型
● 使用异常来处理错误
● 使用属性(attribute)
4.1.8. 内存管理
作为一个.NET程序员,我们知道托管代码的内存管理是自动的。.NET可以保证我们的托管程序在结束时全部释放,这为我们编程人员省去了不少麻烦,我们可以连想都不想怎么去管理内存,反正.NET自己会保证一切。好吧,有道理,有一定的道理。问题是,当我们用到非托管资源时.NET就不能自动管理了。这是因为非托管代码不受CLR(Common Language Runtime)控制,超出CLR的管理范围。那么如何处理这些非托管资源呢,.NET又是如何管理并释放托管资源的呢?
--------------------------------------------------------------------------------
自动内存管理和GC
在原始程序中堆的内存分配是这样的:找到第一个有足够空间的内存地址(没被占用的),然后将该内存分配。当程序不再需要此内存中的信息时程序员需要手动将此内存释放。堆的内存是公用的,也就是说所有进程都有可能覆盖另一进程的内存内容,这就是为什么很多设计不当的程序甚至会让操作系统本身都down掉。我们有时碰到的程序莫名其妙的死掉了(随机现象),也是因为内存管理不当引起的(可能由于本身程序的内存问题或是外来程序造成的)。另一个常见的实例就是大家经常看到的游戏的Trainer,他们通过直接修改游戏的内存达到"无敌"的效果。明白了这些我们可以想象如果内存地址被用混乱了的话会多么危险,我们也可以想象为什么C++程序员(某些)一提起指针就头疼的原因了。另外,如果程序中的内存不被程序员手动释放的话那么这个内存就不会被重新分配,直到电脑重起为止,也就是我们所说的内存泄漏。
所说的这些是在非托管代码中,CLR通过AppDomain实现代码间的隔离避免了这些内存管理问题,也就是说一个AppDomain在一般情况下不能读/写另一AppDomain的内存。托管内存释放就由GC(Garbage Collector)来负责。我们要进一步讲述的就是这个GC,但是在这之前要先讲一下托管代码中内存的分配,托管堆中内存的分配是顺序的,也就是说一个挨着一个的分配。这样内存分配的速度就要比原始程序高,但是高出的速度会被GC找回去。为什么?看过GC的工作方式后你就会知道答案了。
--------------------------------------------------------------------------------
GC工作方式
首先我们要知道托管代码中的对象什么时候回收我们管不了(除非用GC.Collect强迫GC回收,这不推荐,后面会说明为什么)。GC会在它"高兴"的时候执行一次回收(这有许多原因,比如内存不够用时。这样做是为了提高内存分配、回收的效率)。那么如果我们用Destructor呢?同样不行,因为.NET中Destructor的概念已经不存在了,它变成了Finalizer,这会在后面讲到。目前请记住一个对象只有在没有任何引用的情况下才能够被回收。为了说明这一点请看下面这一段代码:
[C#]
object objA = new object();
object objB = objA;
objA = null;
// 强迫回收。
GC.Collect();
objB.ToString(); 
这里objA引用的对象并没有被回收,因为这个对象还有另一个引用,ObjB。
对象在没有任何引用后就有条件被回收了。当GC回收时,它会做以下几步:
确定对象没有任何引用。 
检查对象是否在Finalizer表上有记录。 
如果在Finalizer表上有记录,那么将记录移到另外的一张表上,在这里我们叫它Finalizer2。 
如果不在Finalizer2表上有记录,那么释放内存。 
在Finalizer2表上的对象的Finalizer会在另外一个low priority的线程上执行后从表上删除。当对象被创建时GC会检查对象是否有Finalizer,如果有就会在Finalizer表中添加纪录。我们这里所说的记录其实就是指针。如果仔细看这几个步骤,我们就会发现有Finalizer的对象第一次不会被回收,也就是,有Finalizer的对象要一次以上的Collect操作才会被回收,这样就要慢一步,所以作者推荐除非是绝对需要不要创建Finalizer。为了证明GC确实这么工作而不是作者胡说,我们将在对象的复活一章中给出一个示例,眼见为实,耳听为虚嘛!^_^
GC为了提高回收的效率使用了Generation的概念,原理是这样的,第一次回收之前创建的对象属于Generation 0,之后,每次回收时这个Generation的号码就会向后挪一,也就是说,第二次回收时原来的Generation 0变成了Generation 1,而在第一次回收后和第二次回收前创建的对象将属于Generation 0。GC会先试着在属于Generation 0的对象中回收,因为这些是最新的,所以最有可能会被回收,比如一些函数中的局部变量在退出函数时就没有引用了(可被回收)。如果在Generation 0中回收了足够的内存,那么GC就不会再接着回收了,如果回收的还不够,那么GC就试着在Generation 1中回收,如果还不够就在Generation 2中回收,以此类推。Generation也有个最大限制,根据Framework版本而定,可以用GC.MaxGeneration获得。在回收了内存之后GC会重新排整内存,让数据间没有空格,这样是因为CLR顺序分配内存,所以内存之间不能有空着的内存。现在我们知道每次回收时都会浪费一定的CPU时间,这就是我说的一般不要手动GC.Collect的原因(除非你也像我一样,写一些有关GC的示例!^_^)。
--------------------------------------------------------------------------------
Destructor的没落,Finalizer的诞生
对于Visual Basic程序员来说这是个新概念,所以前一部分讲述将着重对C++程序员。我们知道在C++中当对象被删除时(delete),Destructor中的代码会马上执行来做一些内存释放工作(或其他)。不过在.NET中由于GC的特殊工作方式,Destructor并不实际存在,事实上,当我们用Destructor的语法时,编译器会自动将它写为protected virtual void Finalize(),这个方法就是我所说的Finalizer。就象它的名字所说,它用来结束某些事物,不是用来摧毁(Destruct)事物。在Visual Basic中它就是以Finalize方法的形式出现的,所以Visual Basic程序员就不用操心了。C#程序员得用Destructor的语法写Finalizer,不过千万不要弄混了,.NET中已经没有Destructor了。C++中我们可以准确的知道什么时候会执行Destructor,不过在.NET中我们不能知道什么时候会执行Finalizer,因为它是在第一次对象回收操作后才执行的。我们也不能知道Finalizer的执行顺序,也就是说同样的情况下,A的Finalize可能先被执行,B的后执行,也可能A的后执行而B的先执行。也就是说,在Finalizer中我们的代码不能有任何的时间逻辑。下面我们以计算一个类有多少个实例为示例,指出Finalizer与Destructor的不同并指出在Finalizer中有时间逻辑的错误,因为Visual Basic中没有过Destructor所以示例只有C#版:
[C#]
public class CountObject {
  public static int Count = 0;

  public CountObject() {
    Count++;
  }

  ~CountObject() {
    Count--;
  }
}

static void Main() {
  CountObject obj;
  for (int i = 0; i < 5; i++) {
    obj = null; // 这一步多余,这么写只是为了更清晰些!
   obj = new CountObject();
  }
  // Count不会是1,因为Finalizer不会马上被触发,要等到有一次回收操作后才会被触发。
  Console.WriteLine(CountObject.Count);
  Console.ReadLine();
}
注意以上代码要是改用C++写的话会发生内存泄漏,因为我们没有用delete操作符手动清理内存,但是在托管代码中却不会发生内存泄漏,因为GC会自动检测没有引用了的对象并回收。这里作者推荐你只在实现IDisposable接口时配合使用Finalizer,在其他的情况下不要使用(可能会有特殊情况)。在非托管资源的释放一章我们会更好的了解IDisposable接口,现在让我们来做耶稣吧!
--------------------------------------------------------------------------------
对象的复活
什么?回收的对象也可以"复活"吗?没错,虽然这么说的定义不准确。让我们先来看一段代码:
[C#]
public class Resurrection {
  public int Data;
  public Resurrection(int data) {
    this.Data = data;
  }
  ~Resurrection() {
    Main.Instance = this;
  }
}
public class Main {
  public static Resurrection Instance;
  public static void Main() {
    Instance = new Resurrection(1);
    Instance = null;
    GC.Collect();
    GC.WaitForPendingFinalizers();
    // 看到了吗,在这里“复活”了。
    Console.WriteLine(Instance.Data);
    Instance = null;
    GC.Collect();
    Console.ReadLine();
  }
}
你可能会问:"既然这个对象能复活,那么这个对象在程序结束后会被回收吗?"。会,"为什么?"。让我们按照GC的工作方式走一遍你就明白是怎么回事了。
1、执行Collect。检查引用。没问题,对象已经没有引用了。
2、创建新实例时已经在Finalizer表上作了纪录,所以我们检查到了对象有Finalizer。
3、因为查到了Finalizer,所以将记录移到Finalizer2表上。
4、在Finalizer2表上有记录,所以不释放内存。
5、Collect执行完毕。这时我们用了GC.WaitForPendingFinalizers,所以我们将等待所有Finalizer2表上的Finalizers的执行。
6、Finalizer执行后我们的Instance就又引用了我们的对象。(复活了)
7、再一次去除所有的引用。
8、执行Collect。检查引用。没问题。
9、由于上次已经将记录从Finalizer表删除,所以这次没有查到对象有Finalizer。
10、在Finalizer2表上也不存在,所以对象的内存被释放了。
现在你明白原因了,让我来告诉你"复活"的用处。嗯,这个……好吧,我不知道。其实,复活没有什么用处,而且这样做也非常的危险。看来这只能说是GC机制的漏洞(请参看GC.ReRegisterForFinalize再动脑筋想一下就知道为什么可以说是漏洞了)。作者建议大家忘掉有什么复活,避免这类的使用。可能你会问:"那你干吗还要对我们说这些?"我说这些为的是让大家更好的了解GC的工作机制!^_^
--------------------------------------------------------------------------------
非托管资源的释放
到现在为止,我们说了托管内存的管理,那么当我们利用如数据库、文件等非托管资源时呢?这时我们就要用到.NET Framework中的标准:IDisposable接口。按照标准,所有有需要手动释放非托管资源的类都得实现此接口。这个接口只有一个方法,Dispose(),不过有相对的Guidelines指示如何实现此接口,在这里我向大家说一说。实现IDisposable这个接口的类需要有这样的结构:
[C#]
public class Base : IDisposable {
  public void Dispose() {
    this.Dispose(true);
    GC.SupressFinalize(this);
  }
  protected virtual void Dispose(bool disposing) {
    if (disposing) {
      // 托管类
    }
    // 非托管资源释放
  }

  ~Base() {
    this.Dispose(false);
  }
}
public class Derive : Base {
  protected override void Dispose(bool disposing) {
    if (disposing) {
      // 托管类
    }
    // 非托管资源释放
    base.Dispose(disposing);
  }
}
为什么要这样设计呢?让我在后面解说一下。现在我们讲讲实现这个Dispose方法的几个准则:
它不能扔出任何错误,重复的调用也不能扔出错误。也就是说,如果我已经调用了一个对象的Dispose,当我第二次调用Dispose的时候程序不应该出错,简单地说程序在第二次调用Dispose时不会做任何事。这些可以通过一个flag或多重if判断实现。 
一个对象的Dispose要做到释放这个对象的所有资源。拿一个继承类为例,继承类中用到了非托管资源所以它实现了IDisposable接口,如果继承类的基类也用到了非托管资源那么基类也得被释放,基类的资源如何在继承类中释放呢?当然是通过一个virtual/Overridable方法了,这样我们能保证每个Dispose都被调用到。这就是为什么我们的设计有一个virtual/Overridable的Dispose方法。注意我们首先要释放继承类的资源然后再释放基类的资源。 
因为非托管资源一定要被保障正确释放所以我们要定义一个Finalizer来避免程序员忘了调用Dispose的情况。上面的设计就采用了这种形式。如果我们手动调用Dispose方法就没有必要再保留Finalizer了,所以在Dispose中我们用了GC.SupressFinalize将对象从Finalizer表去掉,这样再回收时速度会更快。 
那么那个disposing和"托管类"是怎么回事呢?是这样:在"托管类"中写所有你想在调用Dispose时让其处于可释放状态的托管代码。还记得我们说过我们不知道托管代码是什么时候释放的吗?在这里我们只是去掉成员对象的引用让它处于可被回收状态,并不是直接释放内存。在"托管类"中这里我们也要写上所有实现了IDisposable的成员对象,因为他们也有Dispose,所以也需要在对象的Dispose中调用他们的Dispose,这样才能保证第二个准则。disposing是为了区分Dispose的调用方法,如果我们手动调用那么为了第二个准则"托管类"部分当然得执行,但如果是Finalizer调用的Dispose,这时候对象已经没有任何引用,也就是说对象的成员自然也就不存在了(无引用),也就没有必要执行"托管类"部分了,因为他们已经处于可被回收状态了。好了,这就是IDisposable接口的全部了。现在让我们来回想一下,以前我们可能认为有了Dispose内存就会马上被释放,这是错误的。只有非托管内存才会被马上释放,托管内存的释放由GC管理,我们不用管。
--------------------------------------------------------------------------------
弱引用的使用
A = B,我们称这样的引用叫做强引用,GC就是通过检查强引用来决定一个对象是否是可以回收的。另外还有一种引用称作弱引用(WeakReference),这种引用不影响GC回收,这就是它的用处所在。你会问到底有什么用处。现在我们来假设我们有一个很胖的对象,也就是说它占用很多内存。我们用过了这个对象,打算将它的引用去掉好让GC可以回收内存,但是功夫不大我们又需要这个对象了,没办法,重新创建实例,怎么创建这么慢啊?有什么办法解决这样的问题?有,将对象留在内存中不就快了嘛!不过我们不想这样胖得对象总占着内存,而我们也不想总是创建这样胖的新实例,因为这样很耗时。那怎么办……?聪明的朋友一定已经猜到了我要说解决方法是弱引用。不错,就是它。我们可以创建一个这个胖对象的弱引用,这样在内存不够时GC可以回收,不影响内存使用,而在没有被GC回收前我们还可以再次利用该对象。这里有一个示例:
[C#]
public class Fat {
  public int Data;

  public Fat(int data) {
    this.Data = data;
  }
}
public class Main {
  public static void Main() {
    Fat oFat = new Fat(1);
    WeakReference oFatRef = new WeakReference(oFat);
    // 从这里开始,Fat对象可以被回收了。
    oFat = null;
    if (oFatRef.IsAlive) {
      Console.WriteLine(((Fat) oFatRef.Target).Data); // 1
    }
    // 强制回收。
    GC.Collect();
    Console.WriteLine(oFatRef.IsAlive); // False
    Console.ReadLine();
  }
}
这里我们的Fat其实并不是很胖,但是可以体现示例的本意:如何使用弱引用。那如果Fat有Finalizer呢,会怎样?如果Fat有Finalizer那么我们可能会用到WeakReference的另一个构造函数,当中有一参数叫做TrackResurrection,如果是True,只要Fat的内存没被释放我们就可以用它,也就是说Fat的Finalizer执行后我们还是可以恢复Fat(相当于第一次回收操作后还可恢复Fat);如果TrackResurrection是False,那么第一次回收操作后就不能恢复Fat对象了。
总结
我在这里写出了正篇文章的要点:
一个对象只当在没有任何引用的情况下才会被回收。 
一个对象的内存不是马上释放的,GC会在任何时候将其回收。 
一般情况下不要强制回收工作。 
如果没有特殊的需要不要写Finalizer。 
不要在Finalizer中写一些有时间逻辑的代码。 
在任何有非托管资源或含有Dispose的成员的类中实现IDisposable接口。 
按照给出的Dispose设计写自己的Dispose代码。 
当用胖对象时可以考虑弱引用的使用。 
好了,就说到这里了,希望对GC的了解会让您的代码更加稳固,更加简洁,更加快!更重要的,不再会有内存管理问题,无论是托管还是非托管!
4.1.9. .NET Framework类
.NET Framework类是一个内容丰富的托管代码类集合,它可以完成以前要通过Windows API来完成的绝大多数任务。这些类派生于与中间语言相同的对象模型,也基于单一继承性。可以实例化.NET Framework类,也可以从它们派生自己的类。.NET Framework类直观易用,它结合了Visual Basic和Java库的易用性和Windows API函数的丰富功能。.NET Framework类包括:
1.  IL提供的核心功能,例如,通用类型系统中的基本数据类型
2.  Windows GUI支持和控件
3.  Web窗体
4.  数据访问
5.  目录访问
6.  文件系统和注册表访问
7.  网络和web浏览
8.  .NET特性和反射
9.  访问Windows操作系统的各个方面(如环境变量等)
10.COM互操作性
4.1.10. 配件
程序集。(中间语言,源数据,资源,装配清单)
4.1.11. Run time environment的应用领域

4.1.12. 装箱与拆箱
装箱(boxing)和拆箱(unboxing)机制使得在C#类型系统中,任何值类型、引用类型和object(对象)类型之间进行转换,这种转换称为绑定连接。简单地说,有了装箱和拆箱的概念,对任何类型的值来说最终都可看作是object类型。  
装箱转换含义:将一个值类型隐式地转换成一个object类型,或把这个值类型转换成一个被该值类型应用的接口类型,把一个值类型的值装箱,就是创建一个object实例并将值复制给这个object如:
int I=10;  
object obj=I;  
也可用显式的方法进行装箱操作:
object obj=(object)I; 

拆箱转换  和装箱相反,拆箱转换是指将一个对象类型显式地转换成一个值类型,或将一个接口类型显式地转换成一个执行该接口的值类型。  过程分两步:
首先,检查这个对象实例,看它是否为给定的值类型的装箱值
然后,把这个实例的值拷贝给值类型的变量。如:
int I=10;  
object obj=I;
Int j=(int)obj;

4.1.13. 委托与代理
代理是C#中的一种新的类型,要把方法作为参数传递给其他方法时,需要用到代理。
方法通过参数获得外界传递给他的数据,并对这些数据进行一定的操作。
代理四步曲:
a.生成自定义代理类:
delegate int MyDelegate();
b.用New运算符实例化代理类:
MyDelegate d = new MyDelegate(MyClass.MyMethod);
c.最后通过实例对象调用方法:
int ret = d();
d. 在程序中像调用方法一样应用代理的实例对象调用它指向的方法。
delegate int d(int I);
委托是一种引用方法的类型。一旦为委托分配了方法,委托将与该方法具有完全相同的行为。委托方法的使用可以像其他任何方法一样,具有参数和返回值,如下面的示例所示:
C#
public delegate int PerformCalculation(int x, int y);
与委托的签名(由返回类型和参数组成)匹配的任何方法都可以分配给该委托。这样就可以通过编程方式来更改方法调用,还可以向现有类中插入新代码。只要知道委托的签名,便可以分配自己的委托方法。
将方法作为参数进行引用的能力使委托成为定义回调方法的理想选择。例如,可以向排序算法传递对比较两个对象的方法的引用。分离比较代码使得可以采用更通用的方式编写算法。
委托具有以下特点:
委托类似于 C++ 函数指针,但它是类型安全的。
委托允许将方法作为参数进行传递。
委托可用于定义回调方法。
委托可以链接在一起;例如,可以对一个事件调用多个方法。
方法不需要与委托签名精确匹配。有关更多信息,请参见协变和逆变。
C# 2.0 版引入了匿名方法的概念,此类方法允许将代码块作为参数传递,以代替单独定义的方法。
若要调用委托,可使用 Invoke 方法,或者使用 BeginInvoke 和 EndInvoke 方法异步调用委托。


4.1.14. 垃圾回收机制
如果发现内存不够,则垃圾回收器,将全部对象作为无效对象(被回收对象),
然后先将全局变量,static,处于活动中的局部变量,以及当前CG指针指向的对象放入一个表中.
然后会搜索新列表中的对象所引用的对象.加入列表中,其他没有被加入列表的对象都会被回收.
非托管对象要记得释放资源就行了吧

.NET Framework 的垃圾回收器管理应用程序的内存分配和释放。每次您使用 new 运算符创建对象时,运行库都从托管堆为该对象分配内存。只要托管堆中有地址空间可用,运行库就会继续为新对象分配空间。但是,内存不是无限大的。最终,垃圾回收器必须执行回收以释放一些内存。垃圾回收器优化引擎根据正在进行的分配情况确定执行回收的最佳时间。当垃圾回收器执行回收时,它检查托管堆中不再被应用程序使用的对象并执行必要的操作来回收它们占用的内存。

4.1.15. 错误处理机制
错误的处理顺序:finally先,catch次之,最后退会try代码..取消此次操作.返回catch中的异常信息.当然,你也可以定制自己的错误处理机制...如果你的异常处理中包含finally块.则此finally无论是否发生异常始终会被执行...

4.1.16. 应用程序域
应用程序域为隔离正在运行的应用程序提供了一种灵活而安全的方法。
应用程序域通常由运行库宿主创建和操作。有时,您可能希望应用程序以编程方式与应用程序域交互,例如想在不停止应用程序运行的情况下卸载某个组件时。
应用程序域使应用程序以及应用程序的数据彼此分离,有助于提高安全性。单个进程可以运行多个应用程序域,并具有在单独进程中所存在的隔离级别。在单个进程中运行多个应用程序提高了服务器伸缩性。
下面的代码示例创建一个新的应用程序域,然后加载并执行以前生成的程序集 HelloWorld.exe,该程序集存储在驱动器 C 上。
C#
static void Main() { // Create an Application Domain:
System.AppDomain newDomain = System.AppDomain.CreateDomain("NewApplicationDomain"); // Load and execute an assembly:
newDomain.ExecuteAssembly(@"c:\HelloWorld.exe"); // Unload the application domain: S System.AppDomain.Unload(newDomain);
}
应用程序域具有以下特点:
必须先将程序集加载到应用程序域中,然后才能执行该程序集。一个应用程序域中的错误不会影响在另一个应用程序域中运行的其他代码。
能够在不停止整个进程的情况下停止单个应用程序并卸载代码。不能卸载单独的程序集或类型,只能卸载整个应用程序域。
4.1.17. 预处理
C#还有许多名为“预处理器指令”的命令。这些命令从来不会转化为可执行代码中的命令,但会影响编译过程的各个方面。例如,使用预处理器指令可以禁止编译器编译代码的某一部分。如果计划发布两个版本的代码,即基本版本和有更多功能的企业版本,就可以使用这些预处理器指令。在编译软件的基本版本时,使用预处理器指令还可以禁止编译器编译与额外功能相关的代码。另外,在编写提供调试信息的代码时,也可以使用预处理器指令。实际上,在销售软件时,一般不希望编译这部分代码。
预处理器指令的开头都有符号#。
注意:
C++开发人员应知道在C和C++中,预处理器指令是非常重要的,但是,在C#中,并没有那么多的预处理器指令,它们的使用也不太频繁。C#提供了其他机制来实现许多C++指令的功能,例如定制特性。还要注意,C#并没有一个像C++那样的独立预处理器,所谓的预处理器指令实际上是由编译器处理的。尽管如此,C#仍保留了一些预处理器指令,因为这些命令对预处理器有一定的影响。
下面简要介绍预处理器指令的功能。
#define和 #undef
#define的用法如下所示:
#define DEBUG
它告诉编译器存在给定名称的符号,在本例中是DEBUG。这有点类似于声明一个变量,但这个变量并没有真正的值,只是存在而已。这个符号不是实际代码的一部分,而只在编译器编译代码时存在。在C#代码中它没有任何意义。
#undef正好相反—— 删除符号的定义:
#undef DEBUG
如果符号不存在,#undef就没有任何作用。同样,如果符号已经存在,#define也不起作用。
必须把#define和#undef命令放在C#源代码的开头,在声明要编译的任何对象的代码之前。
#define本身并没有什么用,但当与其他预处理器指令(特别是#if)结合使用时,它的功能就非常强大了。
注意:
这里应注意一般的C#语法的一些变化。预处理器指令不用分号结束,一般是一行上只有一个命令。这是因为对于预处理器指令,C#不再要求命令用分号结束。如果它遇到一个预处理器指令,就会假定下一个命令在下一行上。
#if, #elif, #else和#endif
这些指令告诉编译器是否要编译某个代码块。考虑下面的方法:
   int DoSomeWork(double x)
   {
      // do something
      #if DEBUG
         Console.WriteLine("x is " + x);
      #endif
   }
这段代码会像往常那样编译,但Console.WriteLine命令包含在#if子句内。这行代码只有在前面的#define命令定义了符号DEBUG后才执行。当编译器遇到#if语句后,将先检查相关的符号是否存在,如果符号存在,就只编译#if块中的代码。否则,编译器会忽略所有的代码,直到遇到匹配的#endif指令为止。一般是在调试时定义符号DEBUG,把不同的调试相关代码放在#if子句中。在完成了调试后,就把#define语句注释掉,所有的调试代码会奇迹般地消失,可执行文件也会变小,最终用户不会被这些调试信息弄糊涂(显然,要做更多的测试,确保代码在没有定义DEBUG的情况下也能工作)。这项技术在C和C++编程中非常普通,称为条件编译(conditional compilation)。
#elif (=else if)和#else指令可以用在#if块中,其含义非常直观。也可以嵌套#if块:
#define ENTERPRISE
#define W2K
// further on in the file
#if ENTERPRISE
   // do something
  #if W2K
      // some code that is only relevant to enterprise
      // edition running on W2K
   #endif
#elif PROFESSIONAL
   // do something else
#else
   // code for the leaner version
#endif
注意:
与C++中的情况不同,使用#if不是条件编译代码的惟一方式,C#还通过Conditional特性提供了另一种机制。
#if和 #elif还支持一组逻辑运算符!、==、!=和 ||。如果符号存在,就被认为是true,否则为false,例如:
#if W2K && (ENTERPRISE==false)   // if W2K is defined but ENTERPRISE isn't
#warning和# error
另外两个非常有用的预处理器指令是#warning和#error,当编译器遇到它们时,会分别产生一个警告或错误。如果编译器遇到#warning指令,会给用户显示#warning指令后面的文本,之后编译继续进行。如果编译器遇到#error指令,就会给用户显示后面的文本,作为一个编译错误信息,然后会立即退出编译,不会生成IL代码。
使用这两个指令可以检查#define语句是不是做错了什么事,使用#warning语句可以让自己想起做过什么事:
#if DEBUG && RELEASE
  #error "You've defined DEBUG and RELEASE simultaneously! "
#endif
#warning "Don't forget to remove this line before the boss tests the code! "
  Console.WriteLine("*I hate this job*");
#region和#endregion
#region和 #endregion指令用于把一段代码标记为有给定名称的一个块,如下所示。
#region Member Field Declarations
   int x;
   double d;
   Currency balance;
#endregion
这看起来似乎没有什么用,它不影响编译过程。这些指令的优点是它们可以被某些编辑器识别,包括Visual Studio .NET编辑器。这些编辑器可以使用这些指令使代码在屏幕上更好地布局。第14章会详细介绍它们。
#line
#line指令可以用于改变编译器在警告和错误信息中显示的文件名和行号信息。这个指令用得并不多。如果编写代码时,在把代码发送给编译器前,要使用某些软件包改变键入的代码,就可以使用这个指令,因为这意味着编译器报告的行号或文件名与文件中的行号或编辑的文件名不匹配。#line指令可以用于恢复这种匹配。也可以使用语法#line default把行号恢复为默认的行号:
#line 164 "Core.cs"   // we happen to know this is line 164 in the file
                   // Core.cs, before the intermediate
                   // package mangles it.
// later on
#line default      // restores default line numbering
#pragma
#pragma指令可以抑制或恢复指定的编译警告。与命令行选项不同,#pragma指令可以在类或方法上执行,对抑制什么警告和抑制的时间进行更精细的控制。下面的例子禁止字段使用警告,然后在编译MyClass类后恢复该警告。
#pragma warning disable 169
public class MyClass
{
  int neverUsedField;
}
#pragma warning restore 169
4.1.18. C#中的引用类型
可参见
1.1.12值类型与引用类型

4.1.19. C#中的class与interface
异:
不能直接实例化接口。
接口不包含方法的实现。
接口、类和结构可从多个接口继承。但是C# 只支持单继承:类只能从一个基类继承实现。
类定义可在不同的源文件之间进行拆分。
同:
接口、类和结构可从多个接口继承。
接口类似于抽象基类:继承接口的任何非抽象类型都必须实现接口的所有成员。
接口可以包含事件、索引器、方法和属性。
一个类可以实现多个接口。

4.1.20. C#中的class和struct
结构与类共享几乎所有相同的语法,但结构比类受到的限制更多:
尽管结构的静态字段可以初始化,结构实例字段声明还是不能使用初始值设定项。
结构不能声明默认构造函数(没有参数的构造函数)或析构函数。
结构的副本由编译器自动创建和销毁,因此不需要使用默认构造函数和析构函数。实际上,编译器通过为所有字段赋予默认值(参见默认值表)来实现默认构造函数。结构不能从类或其他结构继承。
结构是值类型 -- 如果从结构创建一个对象并将该对象赋给某个变量,变量则包含结构的全部值。复制包含结构的变量时,将复制所有数据,对新副本所做的任何修改都不会改变旧副本的数据。由于结构不使用引用,因此结构没有标识 -- 具有相同数据的两个值类型实例是无法区分的。C# 中的所有值类型本质上都继承自 ValueType,后者继承自 Object。
编译器可以在一个称为装箱的过程中将值类型转换为引用类型。
结构具有以下特点:
结构是值类型,而类是引用类型。
向方法传递结构时,结构是通过传值方式传递的,而不是作为引用传递的。
与类不同,结构的实例化可以不使用 new 运算符。
结构可以声明构造函数,但它们必须带参数。
结构可以实现接口。
在结构中初始化实例字段是错误的。

4.1.21. C#消息处理机制
在C#中,程序采用了的驱动采用了事件驱动而不是原来的消息驱动,虽然.net框架提供的事件已经十分丰富,但是在以前的系统中定义了丰富的消息对系统的编程提供了方便的实现方法,因此在C#中使用消息有时候还是大大提高编程的效率的。
  1 定义消息
   在c#中消息需要定义成windows系统中的原始的16进制数字,比如
    const int WM_Lbutton = 0x201; //定义了鼠标的左键点击消息
     public const int USER = 0x0400 // 是windows系统定义的用户消息
  2 消息发送
    消息发送是通过windows提供的API函数SendMessage来实现的它的原型定义为
  [DllImport("User32.dll",EntryPoint="SendMessage")]
      private static extern int SendMessage(
                   int hWnd,      // handle to destination window
                   int Msg,       // message
                   int wParam,  // first message parameter
                   int lParam // second message parameter
       );
3 消息的接受
  在C#中,任何一个窗口都有也消息的接收处理函数,就是defproc函数
你可以在form中重载该函数来处理消息
protected override void DefWndProc ( ref System.WinForms.Message m )
{
 switch(m.msg)
 {
 case WM_Lbutton :
  ///string与MFC中的CString的Format函数的使用方法有所不同
  string message = string.Format("收到消息!参数为:{0},{1}",m.wParam,m.lParam);
  MessageBox.Show(message);///显示一个消息框
  break;
 default:
  base.DefWndProc(ref m);///调用基类函数处理非自定义消息。
  break;
 }
}
其实,C#中的事件也是通过封装系统消息来实现的,如果你在DefWndProc函数中不处理该
那么,他会交给系统来处理该消息,系统便会通过代理来实现鼠标单击的处理函数,因此你可以通过defproc函数来拦截消息,比如你想拦截某个按钮的单击消息
4 C#中其他的消息处理方法
   在C#中有的时候需要对控件的消息进行预处理,比如你用owc的spreedsheet控件来处理Excel文件,你不想让用户可以随便选中数据进行编辑,你就可以屏蔽掉鼠标事件,这个时候就必须拦截系统预先定义好的事件(这在MFC中称为子类化),你可以通过C#提供的一个接口IMessageFilter来实现消息的过滤
public class Form1: System.Windows.Forms.Form,IMessageFilter
{
  const int WM_MOUSEMOVE = 0x200
  public bool PreFilterMessage(ref Message m)  {   Keys keyCode = (Keys)(int)m.WParam & Keys.KeyCode;   if(m.Msg == m.Msg==WM_MOUSEMOVE)    //||m.Msg == WM_LBUTTONDOWN   {    //MessageBox.Show("Ignoring Escape...");    return true;   }   return false;  }
}

4.1.22. Code-Behind技术
代码分离,这是个明智的东西,像ASP这样混成一堆很不爽.或者可以理解成HTML代码写在前台,C#代码写在后台.当然前台也有脚本,类的调用等。即.aspx和.aspx.cs文件的分离。

4.1.23. 活动目录
Active Directory存储了有关网络对象的信息,并且让管理员和用户能够轻松地查找和使用这些信息。Active Directory使用了一种结构化的数据存储方式,并以此作为基础对目录信息进行合乎逻辑的分层组织。

4.1.24. 反射
公共语言运行库加载器管理应用程序域。这种管理包括将每个程序集加载到相应的应用程序域以及控制每个程序集中类型层次结构的内存布局。
程序集包含模块,而模块包含类型,类型又包含成员。反射则提供了封装程序集、模块和类型的对象。您可以使用反射动态地创建类型的实例,将类型绑定到现有对象,或从现有对象中获取类型。然后,可以调用类型的方法或访问其字段和属性。

反射的概述:
          反射的定义:审查元数据并收集关于它的类型信息的能力。元数据(编译以后的最基本数据单元)就是一大堆的表,当编译程序集或者模块时,编译器会创建一个类定义表,一个字段定义表,和一个方法定义表等,。System.reflection命名空间包含的几个类,允许你反射(解析)这些元数据表的代码  

       System.Reflection.Assembly
       System.Reflection.MemberInfo
       System.Reflection.EventInfo
       System.Reflection.FieldInfo
       System.Reflection.MethodBase
       System.Reflection.ConstructorInfo
       System.Reflection.MethodInfo
       System.Reflection.PropertyInfo
       System.Type
反射的作用:
1、可以使用反射动态地创建类型的实例,将类型绑定到现有对象,或从现      有对象中获取类型
2、应用程序需要在运行时从某个特定的程序集中载入一个特定的类型,以便实现某个任务时可以用到反射。
3、反射主要应用与类库,这些类库需要知道一个类型的定义,以便提供更多的功能。
应用要点:
1、现实应用程序中很少有应用程序需要使用反射类型
2、使用反射动态绑定需要牺牲性能
3、有些元数据信息是不能通过反射获取的
4、某些反射类型是专门为那些clr 开发编译器的开发使用的,所以你要意识到不是所有的反射类型都是适合每个人的。
反射appDomain 的程序集:
当你需要反射AppDomain 中包含的所有程序集,示例如下:
static void Main
{
       //通过GetAssemblies 调用appDomain的所有程序集
       foreach (Assembly assem in Appdomain.currentDomain.GetAssemblies())
      {
       //反射当前程序集的信息
            reflector.ReflectOnAssembly(assem)
      }
}
说明:调用AppDomain 对象的GetAssemblies 方法 将返回一个由System.Reflection.Assembly元素组成的数组。
反射单个程序集:
上面的方法讲的是反射AppDomain的所有程序集,我们可以显示的调用其中的一个程序集,system.reflecton.assembly 类型提供了下面三种方法:
1、Load 方法:极力推荐的一种方法,Load 方法带有一个程序集标志并载入它,Load 将引起CLR把策略应用到程序集上,先后在全局程序集缓冲区,应用程序基目录和私有路径下面查找该程序集,如果找不到该程序集系统抛出异常
2、LoadFrom 方法:传递一个程序集文件的路径名(包括扩展名),CLR会载入您指定的这个程序集,传递的这个参数不能包含任何关于版本号的信息,区域性,和公钥信息,如果在指定路径找不到程序集抛出异常。
3、LoadWithPartialName:永远不要使用这个方法,因为应用程序不能确定再在载入的程序集的版本。该方法的唯一用途是帮助那些在.Net框架的测试环节使用.net 框架提供的某种行为的客户,这个方法将最终被抛弃不用。
注意:system.AppDomain 也提供了一种Load 方法,他和Assembly的静态Load 方法不一样,AppDomain的load 方法是一种实例方法,返回的是一个对程序集的引用,Assembly的静态Load 方发将程序集按值封装发回给发出调用的AppDomain.尽量避免使用AppDomain的load 方法
利用反射获取类型信息:
前面讲完了关于程序集的反射,下面在讲一下反射层次模型中的第三个层次,类型反射
一个简单的利用反射获取类型信息的例子:
using system;
using sytem.reflection;
class reflecting
{
       static void Main(string[]args)
       {
             reflecting reflect=new reflecting();//定义一个新的自身类
             //调用一个reflecting.exe程序集
             assembly myAssembly =assembly.loadfrom(“reflecting.exe”)
             reflect.getreflectioninfo(myAssembly);//获取反射信息
       }
       //定义一个获取反射内容的方法
       void getreflectioninfo(assembly myassembly)
       {
             type[] typearr=myassemby.Gettypes();//获取类型
             foreach (type type in typearr)//针对每个类型获取详细信息
            {
                   //获取类型的结构信息
                  constructorinfo[] myconstructors=type.GetConstructors;
                 //获取类型的字段信息
                 fieldinfo[] myfields=type.GetFiedls()
                 //获取方法信息
                 MethodInfo   myMethodInfo=type.GetMethods();
                 //获取属性信息
                 propertyInfo[] myproperties=type.GetProperties
                 //获取事件信息
                 EventInfo[] Myevents=type.GetEvents;
           }
      }
}
其它几种获取type对象的方法:
1、System.type   参数为字符串类型,该字符串必须指定类型的完整名称(包括其命名空间)
2、System.type 提供了两个实例方法:GetNestedType,GetNestedTypes
3、Syetem.Reflection.Assembly 类型提供的实例方法是:GetType,GetTypes,GetExporedTypes
4、System.Reflection.Moudle 提供了这些实例方法:GetType,GetTypes,FindTypes
设置反射类型的成员:
反射类型的成员就是反射层次模型中最下面的一层数据。我们可以通过type对象的GetMembers 方法取得一个类型的成员。如果我们使用的是不带参数的GetMembers,它只返回该类型的公共定义的静态变量和实例成员,我们也可以通过使用带参数的GetMembers通过参数设置来返回指定的类型成员。具体参数参考msdn 中system.reflection.bindingflags 枚举类型的详细说明。
例如:
//设置需要返回的类型的成员内容
bindingFlags bf=bingdingFlags.DeclaredOnly|bingdingFlags.Nonpublic|BingdingFlags.Public;
foreach (MemberInfo mi int t.getmembers(bf))
{
       writeline(mi.membertype)    //输出指定的类型成员
}
通过反射创建类型的实例:
通过反射可以获取程序集的类型,我们就可以根据获得的程序集类型来创建该类型新的实例,这也是前面提到的在运行时创建对象实现晚绑定的功能
我们可以通过下面的几个方法实现:
1、System.Activator 的CreateInstance方法。该方法返回新对象的引用。具体使用方法参见msnd
2、System.Activator 的createInstanceFrom 与上一个方法类似,不过需要指定类型及其程序集
3、System.Appdomain 的方法:createInstance,CreateInstanceAndUnwrap,CreateInstranceFrom和CreateInstraceFromAndUnwrap
4、System.type的InvokeMember实例方法:这个方法返回一个与传入参数相符的构造函数,并构造该类型。
5、System.reflection.constructinfo 的Invoke实例方法
反射类型的接口:
如果你想要获得一个类型继承的所有接口集合,可以调用Type的FindInterfaces GetInterface或者GetInterfaces。所有这些方法只能返回该类型直接继承的接口,他们不会返回从一个接口继承下来的接口。要想返回接口的基础接口必须再次调用上述方法。
反射的性能:
使用反射来调用类型或者触发方法,或者访问一个字段或者属性时clr 需 要做更多的工作:校验参数,检查权限等等,所以速度是非常慢的。所以尽量不要使用反射进行编程,对于打算编写一个动态构造类型(晚绑定)的应用程序,可以采取以下的几种方式进行代替:
1、通过类的继承关系。让该类型从一个编译时可知的基础类型派生出来,在运行时生成该类 型的一个实例,将对其的引用放到其基础类型的一个变量中,然后调用该基础类型的虚方法。
2、通过接口实现。在运行时,构建该类型的一个实例,将对其的引用放到其接口类型的一个变量中,然后调用该接口定义的虚方法。
3、通过委托实现。让该类型实现一个方法,其名称和原型都与一个在编译时就已知的委托相符。在运行时先构造该类型的实例,然后在用该方法的对象及名称构造出该委托的实例,接着通过委托调用你想要的方法。这个方法相对与前面两个方法所作的工作要多一些,效率更低一些。
 

4.1.25. 序列化
序列化是将对象状态转换为可保持或传输的格式的过程。与序列化相对的是反序列化,它将流转换为对象。这两个过程结合起来,可以轻松地存储和传输数据。

4.1.26. Partial class
虽然在单个文件中维护某个类的所有源代码是很好的编程习惯,但是有时一个类会变得非常大,在这种情况下,这种做法反而成为一种不切实际的限制。此外,程序员经常使用源代码生成器来生成应用程序的初始结构,然后修改得到代码。遗憾的是,当将来某个时候再次用代码生成器来生成源代码时,已有的修改会被源代码生成器改写或者删除。分部类可以很好地解决这类问题。
分部类型(Partial type)可以将类(以及结构和接口)划分为多个部分,存储在不同的源文件中,以便于开发和维护。此外,分部类型允许将计算机生成的类型部分和用户编写的类型部分互相分开,以便更容易地扩充工具生成的代码。
C# 2.0新增了类修饰符partial,用来实现通过多个部分来定义一个类。partial修饰符必须直接放在class关键字的前面。分部类声明的每个部分都必须包含partial修饰符,并且其声明必须与其他部分位于同一命名空间。partial修饰符说明在其他位置可能还有同一个类型声明的其他部分,但是这些其他部分并非必须存在;如果只有一个类型声明,包含partial修饰符也是有效的。
当分部类型声明指定了可访问性(public、protected、internal和private修饰符)时,它必须与所有其他部分所指定的可访问性一致。
分部类型的所有部分必须一起编译,以使这些部分可在编译时被合并。注意,分部类型不允许用于扩展已经编译的类型。
partial修饰符可以用于在多个部分中声明嵌套类型。通常,其包含类型也使用partial声明,并且嵌套类型的每个部分均可在该包含类的不同部分中声明。
下面是一个分为两部分来实现的分部类示例:
// 位于文件A1.cs中
public partial class A
{
   protected string name;
   public A()
   {
   }
   partial class Inner  
   {
       int y;
  }
}
// 位于文件A2.cs中
public partial class A
{
   private int x;
   public void f()
   {
   }
   public void g()
   {
   }
   partial class Inner  
   {
       int z;
       void h()
       {
       }
   }
}
当将上述两个部分一起编译时,结果代码与在同一个源文件中编写整个类的代码时完全相同:
// 位于文件A.cs中
public class A
{
   protected string name;
   private int x;
   public A()
   {
   }
   public void f()
   {
   }
   public void g()
   {
   }
   class Inner  
   {
       int y;
       int z;
       void h()
       {
       }
   }
}
在多个部分中声明的类的成员是每个部分中声明的成员的并集。所有部分的类声明主体都表示同一个类,并且每个成员的范围都扩展到所有部分的主体。任何成员的可访问性域都包含类的所有部分;在一个部分中声明的private成员可从其他部分随意访问。在类的多个部分中声明同一个成员将引起编译时错误,除非该成员是带有partial修饰符的类型。比如:
partial class A
{
   int x;              // 错误,x被重复声明
   partial class Inner   // 正确,Inner类是分部类
   {
      int y;
   }
}
partial class A
{
   int x;              // 错误,x被重复声明
   partial class Inner   // 正确,Inner类是分部类
   {
      int z;
   }
}
上面的例子中,字段x被重复定义,是错误的;而Inner类是partial类型的,因此可以分开定义。
4.1.27. 内部类
类的定义是可以嵌套的,即在类的内部还可以定义其他的类。类内声明的类称为内部类(internal class)或者嵌套类(nested class)。在编译单元或命名空间内声明的类称为顶级类或者非嵌套类型(non-nested class)。
比如,下面的List类中定义了一个private类型的内部类Node:
public class List
{
   // private内部类
   private class Node
   {
      public object Data;
      public Node Next;
      public Node(object data, Node next)
      {
        this.Data = data;        // this是Node类的对象
        this.Next = next;
      }
   }
   private Node first = null;
   private Node last = null;
  
   // public方法
   public void AddToFront(object o) {...}
   public void AddToBack(object o) {...}
   public object RemoveFromFront() {...}
   public object RemoveFromBack() {...}
   public int Count { get {...} }
}
内部类和包含它的那个类并不具有特殊的关系。在内部类内,this不能用于引用包含它的那个类的实例成员,而只能引用内部类自己的成员。比如,上述代码中的内部类Node中的this只能引用Node的对象,而不能代表List类的对象。
如果类A是类B的内部类,当需要在内部类A的内部访问类B的实例成员时,可以在类B中将代表类B的实例的this作为一个参数传递给内部类A的构造函数,这样就可以实现在类A的内部对类B的访问。比如:
// NestedClass1.cs
// 内部类的示例
using System;
class Wrapper
{
    string name = "Wrapper";
    public void F()
    {
        // 构造内部类实例时,传入包含内部类的类的this实例
        Nested n = new Nested(this);
        n.G();
    }

   public class Nested
    {
       Wrapper thisW;        // 用于保存外部类的实例
        public Nested(Wrapper w)
        {
            thisW = w;
        }
        public void G()
        {
           Console.WriteLine(thisW.name);
        }
    }
}
class Test
{
    static void Main()
    {
        Wrapper w = new Wrapper();
        w.F();
    }
}
Wrapper实例创建了一个Nested实例,并将代表它自己的this传递给Nested的构造函数,这样,就可以对Wrapper的实例成员进行后续访问了。
内部类可以访问包含它的那个类可访问的所有成员,包括该类自己的具有private和protected声明可访问性的成员。比如:
// NestedClass2.cs
// 内部类的示例
using System;
class Wrapper
{
    protected string name = "Wrapper";
    private void F()
    {
        Console.WriteLine("Wrapper.F()");
    }
    public class Nested
    {
        public void G()
       {
            Wrapper w = new Wrapper();
            Console.WriteLine(w.name);
            w.F();
        }
    }
}
class Test
{
    static void Main()
    {
        Wrapper.Nested n = new Wrapper.Nested();
        n.G();
    }
}
上述代码中,类Wrapper包含内部类Nested。在Nested内,方法G引用在Wrapper中定义的protected字段name和private方法F()。
内部类的完全限定名为S.N,其中S是声明了N类的那个类的完全限定名。
非嵌套类可以具有public或internal访问修饰符,默认的访问修饰符是internal。但是,内部类具有5种访问修饰符(public、protected internal、protected、private)中的任何一种,而且与其他类成员一样,默认的已访问修饰符是private。
内部类的可访问域受包含它的类的访问修饰符和它自身的访问修饰符的限制。内部类的可访问域至少为包含它的类体。内部类可访问域是声明它的类的可访问域的子集。
内部类的成员的可访问域受包含内部类的类访问修饰符、内部类的访问修饰符和它自身的访问修饰符的限制。内部类成员的可访问域是内部类的可访问域的子集。比如,
internal class B
{
   public static int X;
   internal static int Y;
   private static int Z;
   public class C
   {
      public static int X;
      internal static int Y;
      private static int Z;
   }
   private class D
   {
      public static int X;
      internal static int Y;
      private static int Z;
   }
}
上述代码中的类和成员的可访问域分别为:
—  B、B.X、B.Y、B.C、B.C.X和B.C.Y的可访问域是定义类B的程序。
—  B.Z和B.D的可访问域是B的代码体,包括B.C和B.D的代码体。
—  B.C.Z的可访问域是B.C的代码体。
—  B.D.X和B.D.Y的可访问域是B的代码体,包括B.C和B.D的代码体。
—  B.D.Z的可访问域是B.D的代码体。
当内部类的成员与定义它的类的成员重名时,内部类成员会隐藏外部类的成员。比如,在上面的例子中,在B.C内直接使用X、Y、Z指的是B.C.X、B.C.Y、B.C.Z,而不是B.X、B.Y、B.Z。
4.1.28. 内部类与匿名4.1.29. 类
在另一个类中定义的类就是嵌套类(nested classes)。嵌套类的范围由装入它的类的范围限制。这样,如果类B被定义在类A之内,那么B为A所知,然而不被A的外面所知。嵌套类可以访问嵌套它的类的成员,包括private 成员。但是,包围类不能访问嵌套类的成员。
嵌套类一般有2种类型:前面加static标识符的和不加static 标识符的。一个static的嵌套类有static修饰符。因为它是static,所以只能通过对象来访问它包围类的成员。也就是说,它不能直接引用它包围类的成员。因为有这个限制,所以static嵌套类很少使用。
嵌套类最重要的类型是内部类(inner class)。内部类是非static的嵌套类。它可以访问它的外部类的所有变量和方法,它可以直接引用它们,就像外部类中的其他非static成员的功能一样。这样,一个内部类完全在它的包围类的范围之内。
下面的程序示例了如何定义和使用一个内部类。名为Outer 的类有一个名为outer_x 的示例变量,一个名为test()的实例方法,并且定义了一个名为Inner 的内部类。
// Demonstrate an inner class.
class Outer {
int outer_x = 100;
void test() {
     Inner inner = new Inner();
      inner.display();
}
// this is an inner class class Inner { void display() {System.out.println("display: outer_x = " + outer_x); }}}
class InnerClassDemo {
public static void main(String args[]) {
        Outer outer = new Outer();
        outer.test();
}
}
该程序的输出如下所示:
display: outer_x = 100
在本程序中,内部类Inner 定义在Outer 类的范围之内。因此,在Inner 类之内的任何代码可以直接访问变量outer_x 。实例方法display() 定义在Inner 的内部,该方法以标准的输出流显示 outer_x 。InnerClassDemo 的main( ) 方法创建类Outer 的一个实例并调用它的test( )方法。创建类Inner 和display() 方法的一个实例的方法被调用。
认识到Inner 类只有在类Outer 的范围内才是可知的是很重要的。如果在类Outer 之外的任何代码试图实例化Inner 类,Java 编译器会产生一条错误消息。总体来说,一个嵌套类和其他任何另外的编程元素没有什么不同:它仅仅在它的包围范围内是可知的。
我们解释过,一个内部类可以访问它的包围类的成员,但是反过来就不成立了。内部类的成员只有在内部类的范围之内是可知的,而且不能被外部类使用。例如:
// This program will not compile.
class Outer {
int outer_x = 100;
void test() {
        Inner inner = new Inner();
        inner.display();
}
// this is an inner class
class Inner {
int y = 10; // y is local to Inner
void display() {
       System.out.println("display: outer_x = " + outer_x);
}
}
void showy() { System.out.println(y); // error,y not known here!}}
class InnerClassDemo {
public static void main(String args[]) {
     Outer outer = new Outer();
      outer.test();
}
}
这里,y是作为Inner 的一个实例变量来声明的。这样对于该类的外部它就是不可知的,因此不能被showy() 使用。
尽管我们强调嵌套类在它的外部类的范围之内声明,但在几个程序块的范围之内定义内部类是可能的。例如,在由方法定义的块中,或甚至在for 循环体内部,你也可以定义嵌套类,如下面的程序所示:
// Define an inner class within a for loop.
class Outer {
int outer_x = 100;
void test() {
     for(int i=0; i<10; i++)
    {
 class Inner { void display() {System.out.println("display: outer_x = " + outer_x); }
     }
     Inner inner = new Inner();
    inner.display();
}
}
}
class InnerClassDemo {
public static void main(String args[]) {
     Outer outer = new Outer();
     outer.test();
}
}
该程序的这个版本的输出如下所示。
display: outer_x = 100
display: outer_x = 100
display: outer_x = 100
display: outer_x = 100
display: outer_x = 100
display: outer_x = 100
display: outer_x = 100
display: outer_x = 100
display: outer_x = 100
4.1.30. O/R Mapping的工作原理

4.1.31. .Net Remoting工作原理


4.2. ASP.Net
4.2.1. 运行机制/体系结构
 
当请求一个*.aspx文件的时候,同样的这个http request会被inetinfo.exe进程截获,她判断文件的后缀之后,将这个请求转交给ASPNET_ISAPI.dll,ASPNET_ISAPI.dll会通过一个被称为Http PipeLine的管道,将请求发送给ASPNET_WP.exe进程,当这个http request进入ASPNET_WP.exe进程之后,会通过HttpRuntime来处理这个请求,处理完毕将结果返回客户端。
当Http Request进入HttpRuntime之后,会继续进入到一个被称之为HttpApplication Factory的一个Container中,她会给出一个HttpApplication来处理传递进来的请求,这个请求会依次进入如下几个Container:HttpModule->HttpHandler Factory->HttpHandler。
当系统内部的HttpHandler的ProcessResquest方法处理完毕之后,整个Http Request就完成了,客户端也就得到相应的东东了。
整理一下ASP.NET Framework处理一个Http Request的流程:
HttpRequest-->inetinfo.exe-->ASPNET_ISAPI.dll-->Http Pipeline-->ASPNET_WP.exe-->HttpRuntime-->HttpApplication Factory-->HttpApplication-->HttpModule-->HttpHandler Factory-->HttpHandler-->HttpHandler.ProcessRequest()
4.2.2. 用户控件

4.2.3. 身份验证
成功的应用程序安全策略的基础都是稳固的身份验证和授权手段,以及提供机密数据的保密性和完整性的安全通讯。
    身份验证(authentication)是一个标识应用程序客户端的过程,这里的客户端可能包括终端用户、服务、进程或计算机,通过了身份验证的客户端被称为主体(principal)。身份验证可以跨越应用程序的多个层发生。终端用户起初由Web应用程序进行身份验证,通常根据用户名和密码进行;随后终端用户的请求由中间层应用程序服务器和数据库服务器进行处理,这过程中也将进行身份验证以便验证并处理这些请求。
ASP.NET身份验证模式包括Windows、Forms(窗体)、Passport(护照)和None(无)。
1 Windows身份验证
    使用这种身份验证模式时,ASP.NET依赖于IIS对用户进行验证,并创建一个Windows访问令牌来表示已通过验证的标识。IIS提供以下几种身份验证机制:
基本身份验证
简要身份验证
集成Windows身份验证
证书身份验证
匿名身份验证
2 护照身份验证
使用这种身份验证模式时,ASP.NET使用Microsoft Passport的集中式身份验证服务,ASP.NET为Microsoft Passport软件开发包(SDK)所提供的功能提供了一个方便的包装(Wrapper)。此SDK必须安装在WEB服务器上。
3.窗体身份验证
这种验证方式使用客户端重定向功能,将未通过身份验证的用户转发到特定的登录窗体,要求用户输入其凭据信息(通常是用户名和密码)。这些凭据信息被验证后,系统生成一个身份验证票证(ticket)并将其返回客户端。身份验证票证可在用户的会话期间维护用户的身份标识信息,以及用户所属的角色列表(可选)。
4. None
使用这种身份验证模式,表示你不希望对用户进行验证,或是采用自定义的身份验证协议。

4.2.4. 页面间传值的方法
使用QueryString,  如....?id=1; response. Redirect().... 
使用Session变量 
使用Server.Transfer 
使用Cookie
Hidden
database

4.2.5. 服4.2.6. 务器控件的生命周期
服务器控件生命周期所要经历的11个阶段。
(1)初始化-- --在此阶段中,主要完成两项工作:一、初始化在传入Web请求生命周期内所需的设置;二、跟踪视图状态。首先,页面框架通过默认方式引发Init事件,并调用OnInit()方法,控件开发人员可以重写该方法为控件提供初始化逻辑。此后,页面框架将调用TrackViewState方法来跟踪视图状态。需要注意的是:多数情况下,Control基类提供的TrackViewState方法实现已经足够了。只有在控件定义了复杂属性时,开发人员才可能需要重写TrackViewState方法。
(2)加载视图状态----此阶段的主要任务是检查服务器控件是否存在以及是否需要将其状态恢复到它在处理之前的请求结束的状态。因此该过程发生在页面回传过程中,而不是初始化请求过程。在此阶段,页面框架将自动恢复ViewState字典。如果服务器控件不维持其状态,或者它有能力通过默认方式保存其所有状态而使用ViewState字典,那么开发人员则不必实现任何逻辑。针对那些无法在 ViewState字典中存储的数据类型或者需要自定义状态管理的情况,开发人员可以通过重写LoadViewState方法来自定义状态的恢复和管理。
(3)处理回发数据----若要使控件能够检查客户端发回的窗体数据,那么必须实现System.Web.UI.IPostBackDataHandler接口的 LoadPostData()方法。因此只有处理回发数据的控件参与此阶段。
(4)加载----至此阶段开始,控件树中的服务器控件已创建并初始化、状态已还原并且窗体控件反映了客户端的数据。此时,开发人员可以通过重写OnLoad()方法来实现每个请求共同的逻辑。
(5)发送回发更改通知----在此阶段,服务器控件通过引发事件作为一种信号,表明由于回发而发生的控件状态变化(因此该阶段仅用于回发过程)。为了建立这种信号,开发人员必须再次使用System.Web.UI.IPostBackDataHandler接口,并实现另一方法- RaisePostBackChangedEvent()。其判断过程为:如果控件状态因回发而更改,则LoadPostData()返回true;否则返回false。页面框架跟踪所有返回true的控件并在这些控件上调用RaisePostDataChangedEvent()。
(6)处理回发事件----该阶段处理引起回发的客户端事件。为了便于将客户端事件映射到服务器端事件上进行处理,开发人员在此阶段可以通过实现 System.Web.UI.IPostBackEventHandler接口的RaisePostBackEvent()方法来实现该逻辑。由此途径,服务器控件将成功捕获回发的客户端事件进行服务器端的相应处理。
(7)预呈现----该阶段完成在生成控件之前所需要的任何工作。通常情况下是通过重写OnPreRender()方法完成该工作。需要注意的是:在该阶段,可以保存在预呈现阶段对控件状态所做的更改,而在呈现阶段进行的更改则会丢失。
(8)保存状态----如果服务器控件不维持状态,或者它有能力通过默认方式保存其所有状态而使用ViewState字典,那么开发人员不必在该阶段实现任何逻辑。因为这个保存状态的过程是自动的。如果服务器控件需要自定义状态保存,或者控件无法在ViewState字典中存储特殊的数据类型,则需要通过重写SaveViewState()方法来实现状态保存。
(9)呈现----表示向HTTP输出流中写入标记文本的过程。开发人员通过重写Render()方法使其在输出流上自定义标记文本。
(10)处置----在此阶段中,通过重写Dispose ()方法完成释放对昂贵资源的引用,如数据库链接等。
(11)卸载----完成的工作与"处置"阶段相同,但是,开发人员通常在Dispose()方法中执行清除,而不处理Unload事件。
小结
服务器控件在ASP.NET 2.0框架中起着举足轻重的作用,是构建Web应用程序最关键、最重要的组成元素。对于一个优秀的开发人员,掌握服务器控件的基础知识是非常重要的。本文就服务器控件的概念、类型、生命周期等关键内容进行了介绍。希望读者能够将这些内容牢固掌握,为写出精彩的服务器控件打下良好的基础。
4.2.7. Session的bug及解决方法
iis中由于有进程回收机制,系统繁忙的话Session会丢失,可以用Sate   server或SQL   Server数据库的方式存储Session不过这种方式比较慢,而且无法捕获Session的END事件
4.2.8. XML 与 HTML 的主要区别
1. XML是区分大小写字母的,HTML不区分
2. 在HTML中,如果上下文清楚地显示出段落或者列表键在何处结尾,那么你可以省略</p>或者</li>之类的结束标记。在XML中,绝对不能省略掉结束标记。
3. 在XML中,拥有单个标记而没有匹配的结束标记的元素必须用一个 / 字符作为结尾。这样分析器就知道不用查找结束标记了。
4. 在XML中,属性值必须分装在引号中。在HTML中,引号是可用可不用的。
5. 在HTML中,可以拥有不带值的属性名。在XML中,所有的属性都必须带有相应的值。
4.3. Ajax
当我们进行网络编程时,客户希望得到一个功能更完备的应用,而开发人员想避开繁琐的部署工作,不想把可执行文件逐个地部署到数以千计的工作站上。我们的开发人员已经做过很多尝试,但是任何一种方法都不像他们原来标榜得那么完美。
  而今天,我们可以解决这个问题了。因为我们有了它,AJAX。坦率的说,AJAX也不是什么新鲜事物,它不是某项特定的技术,它是此前多项技术的综合,更应是一种技巧。
  本篇文章为本人在学习AJAX所搜集的一些资料及本人的学习心得以整理编辑而成,有不正之处,望大家批评指出。谢谢!
4.3.1. AJAX的介绍
   Ajax(即异步 JavaScript 和 XML)是一种 Web 应用程序开发的手段,它采用客户端脚本与 Web 服务器交换数据。所以,不必采用会中断交互的完整页面刷新,就可以动态地更新 Web 页面。使用 Ajax,可以创建更加丰富、更加动态的 Web 应用程序用户界面,其即时性与可用性甚至能够接近本机桌面应用程序。这两项性能在多年来一直被网络开发者所忽略,直到最近Gmail, Google suggest和Google Maps的横空出世才使人们开始意识到其重要性。
   AJAX是一种运用于浏览器中的技术。在浏览器和服务器之间,它使用异步数据进行转换,并允许网页向服务器索取少量信息而非整个网页。这项技术标志着网络应用程序的微小化、迅捷化以及便捷化。
   AJAX是一种不需依靠服务器软件而独立运做的浏览器技术。
   AJAX基于以下一些公共标准: XML 可扩展标记语言,HTML 超文本标记语言,CSS 层叠样式表。
   运用于ALAX的公共标准被很好的定义并且得到一些主要的常用浏览器的支持。
1. AJAX的定义:
   AJAX(Asynchronous JavaScript and XML)是多种技术的综合,包括JavaScript、XHTML和CSS、DOM、XML和XSTL、XMLHttpRequest。AJAX是一种运用JavaScript和可扩展标记语言(XML),在网络浏览器和服务器之间传送或接受数据的技术。
2.   AJAX的对象:
   AJAX的对象包括JavaScript、XHTML和CSS、DOM、XML和XSTL、XMLHttpRequest。它们在AJAX中所起的作用分别是:
    1) 使用XHTML和CSS标准化呈现;
    2) 使用DOM实现动态显示和交互;
    3) 使用XML和XSTL进行数据交换与处理;
    4) 使用XMLHttpRequest对象进行异步数据读取;
    5) 使用JavaScript绑定和处理所有数据。
4.3.2. AJAX的核心及工作原理
1.   AJAX的核心
    Ajax的核心是JavaScript对象XmlHttpRequest,它目前提供了以下两种核心技术:
    1)支持 Javascript 和支持 XMLHTTP 和 XMLHttpRequest 对象的浏览器;
    2)能够以 XML 响应的 HTTP 服务器技术。
    因为所有流行的浏览器都支持 Javascript 和必要的 XMLHTTP 请求对象,且几乎所有 Web 服务器技术均可生成 XML(或任何标记),所以核心 AJAX 技术普遍适用。最简单的 AJAX 应用程序实质上就是一个带有 Javascript 函数的标准 HTML 用户界面,该界面可与能动态生成 XML 的 HTTP 服务器进行交互。任何动态 Web 技术都可充当服务器端AJAX 技术。
    核心 AJAX 应用程序的主要组件包括:
    1)  HTML 页面,其中包含: 与 AJAX JavaScript 函数交互的UI元素; 与 AJAX 服务器交互的JavaScript函数。
    2)  可处理 HTTP 请求并以 XML 标记响应的服务器端 Web 技术。这些元素如图 1 所示。
 
示例:
   了解了关键元素后,我们就可以设计一个包含输入域、按钮或任何可链接至 Javascript 的元素的 HTML 用户界面了。例如,按下按钮可激活某个 Javascript 函数,或者更深入些,在用户向输入域键入内容时可激活某个 Javascript 函数。为此,您可以将 onkeyup= 赋予 Javascript 函数的值来处理输入域中的数据。
例如,当发生 onkeyup 事件(即键入内容)时,输入域“searchField”将调用 Javascript 函数lookup( )。
  <input type="text" id="searchField"
  size="20" onkeyup="lookup(’searchField’);">
除了响应用户界面交互外,AJAX Javascript 函数还可根据自己的计时器进行独立操作。如可以使用该方法执行AJAXautosave(自动保存)特性。
2. AJAX的工作原理
   一个Ajax交互从一个称为XMLHttpRequest的JavaScript对象开始。它允许一个客户端脚本来执行HTTP请求,并且将会解析一个XML格式的服务器响应。Ajax处理过程中的第一步是创建一个XMLHttpRequest实例。使用HTTP方法(GET或POST)来处理请求,并将目标URL设置到XMLHttpRequest对象上。
   现在,记住Ajax如何首先处于异步处理状态?当你发送HTTP请求,你不希望浏览器挂起并等待服务器的响应,取而代之的是,你希望通过页面继续响应用户的界面交互,并在服务器响应真正到达后处理它们。要完成它,你可以向XMLHttpRequest注册一个回调函数,并异步地派发XMLHttpRequest请求。控制权马上就被返回到浏览器,当服务器响应到达时,回调函数将会被调用。
   在JavaWeb服务器上,到达的请求与任何其它HttpServletRequest一样。在解析请求参数后,servlet执行必需的应用逻辑,将响应序列化到XML中,并将它写回HttpServletResponse。
 
4.3.3. AJAX的应用场景
  Ajax不是万能的,在适合的场合使用Ajax,才能充分发挥它的长处,改善系统性能和用户体验,绝不可以为了技术而滥用。Ajax的特点在于异步交互,动态更新web页面,因此它的适用范围是交互较多,频繁读取数据的web应用。现在来看几个Ajax的应用实例,读者可以了解如何使用Ajax技术改进现有的web应用系统。
   1.数据校验:
   在填写表单内容时,需要保证数据的唯一性(例如新用户注册填写的用户名),因此必须对用户输入的内容进行数据验证。数据验证通常有两种方式:一种是直接填写,然后提交表单,这种方式需要将这个页面提交到服务器端进行验证,整个过程不仅时间长而且造成了服务器不必要的负担;第二种方式是改进了的验证过程,用户可以通过点击相应的验证按钮,打开新窗口查看验证结果,但是这样需要新开一个浏览器窗口或者对话框,还需要专门编写验证的页面,比较耗费系统资源。而使用Ajax技术,可以由XMLHttpRequest对象发出验证请求,根据返回的HTTP响应判断验证是否成功,整个过程不需要弹出新窗口,也不需要将整个页面提交到服务器,快速而又不加重服务器负担。
   2.按需取数据:
   分类树或者树形结构在web应用系统中使用得非常广泛,例如部门结构,文档得分类结构常常使用树形空间呈现。以前每次对分类树得操作都会引起页面重载,为了避免这种情况出现,一般不采用每次调用后台得方式,而是一次性将分类结果中得数据一次性读取出来并写入数组,然后根据用户的操作,用JavaScript来控制节点的呈现,这样虽然解决了操作响应速度,不重复载入页面以及避免向服务器频繁发送请求的问题,但是如果用户不对分类进行操作或者只对分类树中的一部分数据进行操作的话(这种情况很普遍的),那么读取的数据中就会有相当大的冗余,浪费了用户的资源。特别是在分类结构复杂,数据庞大的情况下,这种弊端就更加明显了。
   现在应用Ajax改进分类树的实现机制。在初始化页面时,只获取第一级子分类的数据并且显示;当用户点开一级分类的第一节点时,页面会通过Ajax向服务器请求当前分类所属的二级子分类的所有数据;如果再请求已经呈现的二级分类的某一节点时,再次向服务器请求当前分类所属的三级子分类的所有数据,以此类推。页面会根据用户的操作向服务器请求它所需要的数据,这样就不会存在数据的冗余,减少了数据下载总量。同时,更新页面时不需要重载所有内容,只更新需要更新的那部分内容即可,相对于以前后台处理并且重载的方式,大大缩短了用户的等待时间。
   3.自动更新页面:
   在web应用中有很多数据的变化时十分迅速的,例如最新的热点新闻,天气预报以及聊天室内容等。在Ajax出现之前,用户为了及时了解相应的内容必须不断刷新页面,查看是否有新的内容变化,或者页面本身实现定时刷新的功能(大多数聊天室页面就有此功能)。还有可能会发生这种情况:有一段时间网页的内容没有发生任何变化,但是用户并不知道,仍然不断的刷新页面;或者用户失去了耐心,放弃了刷新页面,却很有可能在此有新的消息出现,这样就错过了第一时间得到消息的机会。
   应用Ajax可以改善这种情况,页面加载以后,会通过Ajax引擎在后台进行定时的轮询,向服务器发送请求,查看是否有最新的消息。如果有则将新的数据(而不是所有数据)下载并且在页面上进行动态的更新,通过一定的方式通知用户(实现这样的功能正是JavaScript的强项)。这样既避免了用户不断手工刷新页面的不便,也不会因为重复刷新页面造成资源浪费。
 
4.3.4. AJAX实用技术
    1. JavaScript
   J avaScript是种脚本语言,它是一种基于对象和事件驱动的编程语言,因而它本身提供了非常丰富的内部对象供程序设计人员使用。它可以被嵌入 HTML 的文件之中。通过 JavaScript 可以做到响应用户的需求事件,如表单的输入。这样当一位使用者输入一项信息时,它不需要通过网络传送到服务器端进行处理再传回来的过程,而可以直接在客户端进行事件的处理。因此,在AJAX中用JavaScript来绑定和处理数据。
    2.XmlHttpRequest
   Ajax的一个最大的特点是无需刷新页面便可向服务器传输或读写数据(又称无刷新更新页面),这一特点主要得益于XMLHTTP组件XMLHTTPRequest对象。这样就可以使桌面应用程序只同服务器进行数据层面的交换,而不用每次都刷新界面也不用每次将数据处理的工作提交给服务器来做,这样即减轻了服务器的负担又加快了响应速度、缩短了用户等候时间。
XMLHttpRequest及XHMHTTP在JS中的应用:
 1)  XMLHttpRequest对象在JS中的应用:var xmlhttp = new XMLHttpRequest();
 2) 微软的XMLHTTP组件在JS中的应用:
var xmlhttp = new ActiveXObject(Microsoft.XMLHTTP);
var xmlhttp = new ActiveXObject(Msxml2.XMLHTTP);
XMLHttpRequest 对象方法
方法  描述
 abort()  停止当前请求
 getAllResponseHeaders()  作为字符串返问完整的headers
 getResponseHeader("headerLabel" )  作为字符串返问单个的header标签
 open("method","URL"[,asyncFlag[,"userName"[, "password"]]])  设置未决的请求的目标 URL, 方法, 和其他参数
 send(content)  发送请求
 setRequestHeader("label", "value" )  设置header并和请求一起发送
 
XMLHttpRequest 对象属性
 属性  描述
 onreadystatechange 状态改变的事件触发器
 readyState 对象状态(integer):
0 = 未初始化
1 = 读取中
2 = 已读取
3 = 交互中
4 = 完成
 responseText  服务器进程返回的数据文本信息
 responseXML  服务器进程返回数据的兼容DOM的XML文档对象
 status  服务器返回的状态码, 如:404 = "文件末找到" 、200 ="成功"
 statusText  服务器进程返回的状态文本信息

3. DOM
  DOM是给HTML和XML文件使用的一组API(应用编程接口)。它提供了文件的结构表述,让你可以改变其中的內容及可见物。其本质是建立网页与 Script 或程序语言沟通的桥梁。
所有WEB开发人员可操作及建立文件的属性、方法及事件都以对象来展现。例如,document 就代表“文件本身“这个对像,table 对象则代表 HTML 的表格对象等等。这些对象可以由当今大多数的浏览器以 Script 来取用。
一个用HTML或XHTML构建的网页也可以看作是一组结构化的数据,这些数据被封在DOM(Document Object Model)中,DOM提供了网页中各个对象的读写的支持。
4. XML
  可扩展的标记语言(Extensible Markup Language)具有一种开放的、可扩展的、可自描述的语言结构,它已经成为网上数据和文档传输的标准。它是用来描述数据结构的一种语言,就正如他的名字一样。他使对某些结构化数据的定义更加容易,并且可以通过他和其他应用程序交换数据。

4.3.5. AJAX的综合应用
   1.  如何发出 XML HTTP 请求
 上面,我们知道了AJAX的基本原理,现在我们来学习如何调用AJAX Javascript代码。请看以下 Javascript 代码,该代码可发出一个 XML HTTP 请求:
   if (window.XMLHttpRequest) {   req = new XMLHttpRequest();}
   else if (window.ActiveXObject) {   req = newActiveXObject("Microsoft.XMLHTTP");}  
利用该段代码,主要的浏览器(Internet Explorer 和 Mozilla/Safari)都可向服务器发出独立的 HTTP 请求。该代码首先检查浏览器是否支持上文提及的两个支持的 XMLHTTP 对象,然后对其中之一进行实例化。一旦对 XMLHttpRequest(或 Microsoft 的 XMLHTTP)进行了实例化,即可以通过完全相同的方式对其进行操作。
  要初始化到服务器的连接,需使用以下 open 方法:
  req.open("GET", url, true);
第一个参数是HTTP方法(GET 或 POST)。第二个参数是服务器(或使用 POST 的表单操作)的 URL;第三个参数为 true,则表明可进行异步调用(“A”代表 AJAX)。这意味着该浏览器可以在实现请求的同时继续执行其他操作。open 方法中若为 false 值,则表明为非异步处理或顺序处理。我们不建议如此,这是因为您的浏览器会在返回响应前停止操作。
使用 open 初始化连接后,可进行 onreadystatechange 调用(只适用于异步调用)。这将注册一个回调函数,一旦请求完成就会调用该函数:
   req.onreadystatechange = processXMLResponse;
在完成请求后,将调用处理 XML 响应的 processXMLResponse( ) 函数。可以通过 onreadystatechange 语句以内联方式声明回调函数:
  req.onreadystatechange = processXMLResponse() {   // process request };
还可使用 req.setRequestHeader 指定任何标题内容,如:
  req.setRequestHeader("Cookie", "someKey=true");
一旦完全初始化了 XMLHTTP 请求对象 (req),就可使用 send( ) 初始化对服务器的调用:
  req.send(null); 对于 GET 请求,使用 null 值或空字符串“”。
POST 请求包含一个带有表单数据的字符串参数。它们也要求在请求的标题中设置 Content-Type。以下两行演示了如何执行 AJAX POST 请求:
  req.setRequestHeader("Content-Type","application/x-www-form-urlencoded"; req.send("name=scott&email=stiger@foocorp.com");
完成请求后调用的回调函数通常具有确保请求不会发生错误的代码。这可通过检查 readyState 以及 HTTP 请求的整体状态来实现。(readystate 为 4 表示 XMLHTTP 请求完整,而 200 表示请求成功(404 含义正好相反)。
  function processXMLResponse() { if (req.readyState == 4) { if (request.status != 200) {// Process the XML response }   } }
XML 响应的处理是通过使用标准 Javascript DOM 方法完成的。例如,要从输入的 XML 流中抽取员工姓名:
  <employee> Chris</employee>
我们可以使用以下代码:
   var name = req.responseXML.getElementsByTagName("employee")[0];
分析更为复杂的 XML 会使用如下代码迭代元素:
  for (i=0;i<elements.length;i++) {for (j=0;j<elements[i].childNodes.length;j++) {var ElementData = elements[i].childNodes[j].firstChild.nodeValue;   } }
2.结合使用 XMLHttpRequest 和 HTML
 请注意,通过 XMLHttpRequest 获得 XML 响应无需总是格式良好和有效。因此,AJAX 服务器端组件可以直接将 HTML 内容发送至客户端。然后,JavaScript 可使用 req.responseText 方法/属性(它只是将内容作为字符串进行检索)检索该 HTML 内容。可以使用该 HTML 字符串文本以任何方式更改页面。例如,对于以下 HTML 流:
  <h2>Hello there!</h2>
  <p> This is <b>HTML</b></p>
可使用以下语句检索至一个字符串中:
  var HTMLcontent = req.responseText;
之后通过 id="div1" 添加至指定的 HTML DIV。
  document.getElementById("div1").innerHTML += HTMLcontent;
4.4. ADO.Net
4.4.1. 体系结构
 
4.4.2. 读写数据库常用类
Connection
DataAdapter
DataSet
Command
DataReader

4.4.3. ADO.Net中的常用对象
Connection.Command,Parameter,Recordset,Field,Property,Error.此外,还包括四个集合,Fields,Properties,Parameters,Errors.
这几个对象的功能如下:
Connection对象提供与包含路径,口令和连接选项的数据源的链接;
Command对象保存一个针对数据源的将被执行的命令,最常见的是SQL命令或存储过程;
Recordset对象保存在记录集中执行查询参数的记录以及漫游记录的光标;
Error对象包含关于数据访问期间可能发生错误的错误信息;
Parameter对象存储由Command对象使用的单个参数;
Field对象为记录集中包含的所有字段集合;
Property对象是由Data Provider驱动程序返回的数据源的属性。
ADO的核心是Connection,Recordset,Command对象。这三个对象可独立使用,也可互相连接使用。而其他对象,如Error集合存储在Connection对象中,在使用这些对象前都必须先声明对象变量,然后用Set进行赋值,才可使用,在声明时还可以用WithEvents将事件也声明进来,使得ADO对象变得象控件那样易于使用。有两个对象中包含了事件,即Recordset对象和Connection对象。只要如Dim WithEvents rst As ADODB.Recordset这样声明后,就能在代码窗口的下拉表中找到该对象,真的和ADO控件一样易于使用,连事件也大致相同。

4.4.4. DataGrid的数据源类型
DataTable
DataView
DataSet
DataViewManager
任何实现IListSource接口的组件
任何实现IList接口的组件

4.4.5. 共享类与数据库特定类
共享类
DataSet
DataTable
DataRow
DataColumn
DataRelation
Constraint
DataColumnMapping
DataTableMapping
特定类
(x)Connection
(x)Command
(x)CommandBuilder
(x)DataAdapter
(x)DataReader
(x)Parameter
(x)Transaction

4.5. Web services
4.5.1. UDDI
UDDI (统一描述、发现和集成,即Universal Description, Discovery以及 Integration)
一个基于Web services的“电话号码簿” 。UDDI开始是作为一个协议而产生,这一个协议是描述Web services地址和提供这些地址的公司或企业的规范。现在UDDI技术已经包括UDDI业务注册中心(UDDI Business Registry)──有时候也称之为cloud services。这一注册中心与一个电话号码簿非常类似,因为顾客可以通过注册中心查询已经注册Web services的公司列表。一个UDDI注册中心的每一Web service都可以以三个部分来描述:第一,“白页(White pages)”描述了提供Web service的公司的所有信息,包括产品,联系信息等。第二,“黄页(Yellow pages)”通过分类很容易地划分和定位类似的Web服务,比如将Web services分成PDAs,无线电通讯,体育评说等。最后,“绿页(Green pages)”提供了有关联系这一Web service方法的详细信息,比如一个SOAP的URI地址,或者描述这一服务及其性质的WSDL文件。“绿页”的内容是由Wed service提供者提供的,一般都是提供进一步联系信息的网址或者一个Java RMI。

4.5.2. WSDL
WSDL(Web Service Description Language)Web服务器描述语言是用XML文档来描述Web服务的标准,是Web服务的接口定义语言,由Ariba、Intel、IBM、MS等共同提出,通过WSDL,可描述Web服务的三个基本属性:
·服务做些什么——服务所提供的操作(方法)
·如何访问服务——和服务交互的数据格式以及必要协议
·服务位于何处——协议相关的地址,如URL
WSDL文档以端口集合的形式来描述Web服务,WSDL 服务描述包含对一组操作和消息的一个抽象定义,绑定到这些操作和消息的一个具体协议,和这个绑定的一个网络端点规范。WSDL 文档被分为两种类型:服务接口(service interface )和 服务实现(service implementations),文档基本结构框架如下:


 

 

 

 

 

 

 


服务接口文档中的主要元素作用分别为:
types:定义了Web服务使用的所有数据类型集合,可被元素的各消息部件所引用。它使用某种类型系统(一般地使用XML Schema中的类型系统)。
message:通信消息数据结构的抽象类型化定义。使用Types所定义的类型来定义整个消息的数据结构。
operation:对服务中所支持操作的抽象描述。一般单个operation描述了一个访问入口的请求/响应消息对。
portType:对于某个访问入口点类型所支持操作的抽象集合。这些操作可以由一个或多个服务访问点来支持。
binding:包含了如何将抽象接口的元素(portType)转变为具体表示的细节,具体表示也就是指特定的数据格式和协议的结合;特定端口类型的具体协议和数据格式规范的绑定。
port:定义为协议/数据格式绑定与具体Web访问地址组合的单个服务访问点。
service:这是一个粗糙命名的元素,代表端口的集合;相关服务访问点的集合。
可见,portType(与message和type元素的细节相结合)描述了Web服务是什么,binding元素描述了如何使用Web服务,port及service元素描述了Web服务的位置。

4.5.3. SOAP
SOAP(Simple Object Access Protocol )简单对象访问协议是在分散或分布式的环境中交换信息并执行远程过程调用的协议,是一个基于XML的协议。使用SOAP,不用考虑任何特定的传输协议(最常用的还是HTTP协议),可以允许任何类型的对象或代码,在任何平台上,以任何一种语言相互通信。这种相互通信采用的是XML格式的消息,
SOAP包括四个部分:
SOAP封装(envelop),封装定义了一个描述消息中的内容是什么,是谁发送的,谁应当接受并处理它以及如何处理它们的框架;
SOAP编码规则(encoding rules),用于表示应用程序需要使用的数据类型的实例;
SOAP RPC表示(RPC representation),表示远程过程调用和应答的协定;
SOAP绑定(binding),使用底层协议交换信息。
应用中比较关注的是envelop,由一个或多个Header和一个Body组成。
4.5.4. WSE
WSE (Web Service Extension) 包来提供最新的WEB服务安全保证,目前最新版本2.0
4.5.5. 调用Web Service的方法
1.使用WSDL.exe命令行工具。
2.使用VS.NET中的Add Web Reference菜单选项

4.6. 多线程
4.6.1. 线程池
许多应用程序使用多个线程,但这些线程经常在休眠状态中耗费大量的时间来等待事件发生。其他线程可能进入休眠状态,并且仅定期被唤醒以轮询更改或更新状态信息,然后再次进入休眠状态。为了简化对这些线程的管理,.NET框架为每一个进程提供了一个线程池,使应用程序能够根据需要来有效地利用多个线程。一个线程监视排到线程池的若干个等待操作的状态。当一个等待操作完成时,线程池中的一个辅助线程就会执行对应的回调函数。线程池中的线程由系统进行管理,程序员不需要费力于线程管理,可以集中精力处理应用程序任务。
线程池是一种多线程处理形式,处理过程中将任务添加到队列,然后在创建线程后自动启动这些任务。线程池线程都是后台线程。每个线程都使用默认堆栈大小,以默认的优先级运行,并处于多线程单元中。如果某个线程在托管代码中空闲(如正在等待某个事件),则线程池将插入另一个辅助线程来使所有处理器保持繁忙。如果所有线程池线程都始终保持繁忙,但队列中包含挂起的工作,则线程池将在一段时间之后创建另一个辅助线程。但线程的数目永远不会超过最大值。超过最大值的其他线程可以排队,但它们要等到其他线程完成后才启动。
线程池特别适合于执行一些需要多个线程的任务。使用线程池能够优化这些任务的执行过程,从而提高吞吐量,它不仅能够使系统针对此进程优化该执行过程,而且还能够使系统针对计算机上的其他进程优化该执行过程。如果需要启动多个不同的任务,而不想分别设置每个线程的属性,则可以使用线程池。
如果应用程序需要对线程进行特定的控制,则不适合使用线程池,需要创建并管理自己的线程。不适合使用线程池的情形包括:
— 如果需要使一个任务具有特定的优先级。
— 如果具有可能会长时间运行(并因此阻塞其他任务)的任务。
— 如果需要将线程放置到单线程单元中(线程池中的线程均处于多线程单元中)。
— 如果需要用永久标识来标识和控制线程,比如想使用专用线程来中止该线程,将其挂起或按名称发现它。
System.Threading.ThreadPool类实现了线程池。ThreadPool类是一个静态类,它提供了管理线程池的一系列方法。
ThreadPool.QueueUserWorkItem方法在线程池中创建一个线程池线程来执行指定的方法(用委托WaitCallback来表示),并将该线程排入线程池的队列等待执行。QueueUserWorkItem方法的原型为:
public static Boolean QueueUserWorkItem(WaitCallback wc, Object state);
public static Boolean QueueUserWorkItem(WaitCallback wc);
这些方法将“工作项”(和可选状态数据)排列到线程池的线程中,并立即返回。工作项只是一种方法(由wc参数标识),它被调用并传递给单个参数,即状态(状态数据)。没有状态参数的QueueUserWorkItem版本将null传递给回调方法。线程池中的某些线程将调用System.Threading.WaitCallback委托表示的回调方法来处理该工作项。回调方法必须与System.Threading.WaitCallback委托类型相匹配。WaitCallback定义如下:
public delegate void WaitCallback(Object state);
调用QueueUserWorkItem时传入的Object类型参数将传递到任务过程,可以通过这种方式来向任务过程传递参数。如果任务过程需要多个参数,可以定义包含这些数据的类,并将类的实例强制转换为Object数据类型。
每个进程都有且只有一个线程池。当进程启动时,线程池并不会自动创建。当第一次将回调方法排入队列(比如调用ThreadPool.QueueUserWorkItem方法)时才会创建线程池。一个线程监视所有已排队到线程池中的任务。当某项任务完成后,线程池中的线程将执行相应的回调方法。在对一个工作项进行排队之后将无法取消它。
线程池中的线程数目仅受可用内存的限制。但是,线程池将对允许在进程中同时处于活动状态的线程数目强制实施限制(这取决于CPU的数目和其他因素)。默认情况下,每个系统处理器最多可以运行25个线程池线程。通过使用ThreadPool.GetMaxThreads和ThreadPool.SetMax Threads方法,可以获取和设置线程池的最大线程数。
即使是在所有线程都处于空闲状态时,线程池也会维持最小的可用线程数,以便队列任务可以立即启动。将终止超过此最小数目的空闲线程,以节省系统资源。默认情况下,每个处理器维持一个空闲线程。使用ThreadPool.GetMinThreads和ThreadPool.SetMinThreads方法可以获取和设置线程池所维持的空闲线程数。
下面的程序用于计算如下的函数的值:
// ThreadPool.cs
// 线程池示例
using System;
using System.Threading;
public class Test
{
    // 存放要计算的数值的字段
    static double number1 = -1;
    static double number2 = -1;
    public static void Main()
    {
        // 获取线程池的最大线程数和维护的最小空闲线程数
        int maxThreadNum, portThreadNum;
        int minThreadNum;
        ThreadPool.GetMaxThreads(out maxThreadNum, out portThreadNum);
        ThreadPool.GetMinThreads(out minThreadNum, out portThreadNum);
        Console.WriteLine("最大线程数:{0}", maxThreadNum);
        Console.WriteLine("最小空闲线程数:{0}", minThreadNum);
        // 函数变量值
        int x = 15600;
        // 启动第一个任务:计算x的8次方
        Console.WriteLine("启动第一个任务:计算{0}的8次方。", x);
        ThreadPool.QueueUserWorkItem(new WaitCallback(TaskProc1), x);
        // 启动第二个任务:计算x的8次方根
        Console.WriteLine("启动第二个任务:计算{0}的8次方根。", x);
        ThreadPool.QueueUserWorkItem(new WaitCallback(TaskProc2), x);
        // 等待,直到两个数值都完成计算
        while (number1 == -1 || number2 == -1) ;
        // 打印计算结果
        Console.WriteLine("y({0}) = {1}", x, number1 + number2);
    }
    // 启动第一个任务:计算x的8次方
    static void TaskProc1(object o)
    {
        number1 = Math.Pow(Convert.ToDouble(o), 8);
    }
    // 启动第二个任务:计算x的8次方根
    static void TaskProc2(object o)
    {
        number2 = Math.Pow(Convert.ToDouble(o), 1.0 / 8.0);
    }
}
该程序的输出结果为:
最大线程数:25
最小空闲线程数:1
启动第一个任务:计算15600的8次方。
启动第二个任务:计算15600的8次方根。
y(15600) = 3.50749278894883E+33

4.6.2. 线程同4.6.3. 步
编写线程应用程序时,可能需要使各单个线程与程序的其他部分同步。同步可以在多线程编程的非结构化特性与同步处理的结构化顺序之间进行平衡。同步技术具有以下用途:
— 如果必须以特定顺序执行任务,那么使用同步技术可以显式控制代码运行的顺序。
— 使用同步技术可以避免当两个线程同时共享同一资源时可能会发生的问题。
.NET框架提供了两种方法可以实现同步,即简单方法和高级方法。简单方法包括轮询和等待,高级方法使用同步对象。
轮询通过循环重复地检查异步调用的状态。轮询是效率最低的线程管理方法,因为它重复地检查各个线程属性的状态,因而浪费了大量资源。例如,可以使用IsAlive属性来轮询检查线程是否已退出。使用该属性时应谨慎,因为处于活动状态的线程并不一定正在运行。
可以使用线程的ThreadState属性来获取有关线程状态的详细信息。因为线程可以在任意给定时间具有多种状态,所以存储在ThreadState中的值可以是System.Threading.ThreadState枚举中值的组合。因此,轮询时应仔细检查所有相关线程的状态。例如,如果某个线程的状态指示它不是Running,该线程可能已完成。但该线程也可能处于挂起或休眠状态。
通过轮询来实现对线程运行顺序的控制将牺牲多线程处理的许多优点。更有效的方法是使用Thread类的Join方法控制线程。Join方法对于在启动另一任务之前确定线程是否已完成很有用。Join方法在线程结束前等待一段指定的时间。如果线程在超时之前结束,则Join返回True;否则返回False。Join使调用过程在线程完成或调用超时(如果指定了超时的话)之前处于等待状态。之所以称其为“Join”,是因为创建新线程是执行路径中的一个分支。使用Join可以重新将单独的执行路径合并为一个线程。
图19-1显示了下面的程序的执行过程:
// Join.cs
// Join示例
using System;
using System.Threading;
public class Test
{
    // Main启动主线程,称之为线程1
   public static void Main()
    {
        Console.WriteLine("进入线程1");
        Console.WriteLine("启动线程2");
        Thread thread2 = new Thread(new ThreadStart(ThreadProc2));
        thread2.Start();
        Console.WriteLine("启动线程3");
        Thread thread3 = new Thread(new ThreadStart(ThreadProc3));
        thread3.Start();
        Console.WriteLine("Join线程2");
        thread2.Join();
        Console.WriteLine("Join线程3");
        thread3.Join();
    }
    static void ThreadProc2()
    {
        Console.WriteLine("进入线程2");
        for (int i = 1; i < 4; ++i)
        {
            Thread.Sleep(50);
            Console.WriteLine("\t+++++++线程2+++++++++");
        }
        Console.WriteLine("退出线程2");
    }
    static void ThreadProc3()
    {
        Console.WriteLine("进入线程3");
        for (int i = 1; i < 8; ++i)
        {
            Thread.Sleep(50);
            Console.WriteLine("\t-------线程3---------");
        }
        Console.WriteLine("退出线程3");
    }
}

图19-1  用Join方法来控制线程同步
Join是一个同步调用或阻止调用。调用Join或等待句柄的等待方法后,调用过程即会停止,并等待该线程发出信号指示它已经完成。上述程序的输出很好地说明了这一点:
进入线程1
启动线程2
进入线程2
启动线程3
进入线程3
Join线程2
        +++++++线程2+++++++++
        -------线程3---------
        +++++++线程2+++++++++
        -------线程3---------
        +++++++线程2+++++++++
退出线程2
Join线程3
        -------线程3---------
        -------线程3---------
        -------线程3---------
        -------线程3---------
        -------线程3---------
退出线程3
简单方法不但低效,而且不太可靠,只适合于管理少量线程的情况,不适合于管理大型项目。对于大型项目来说,应该利用使用同步对象的高级技术。
.NET框架提供了一系列同步类来控制线程的同步,最常用的同步类包括Interlocked、Monitor和Mutex。
线程同步是一种协作,所有使用共享资源(通常把这种资源叫做受保护资源)的线程都必须遵守同步机制。只要有一个线程不遵守同步机制而直接访问受保护资源,同步机制就可能失效。
当多个并行运行的线程需要访问受保护资源时,使它们保持同步是非常重要的,否则就可能出现死锁和竞争条件,从而导致线程不能继续执行或者得到不正确的运行结果。
19.3.1  使用Interlocked
nterlocked类是一种互锁操作,提供对多个线程共享的变量进行同步访问的方法。如果线程共享的变量位于共享内存中,那么不同进程的线程就可以使用Interlocked类对象来进行同步。互锁操作具有原子性,即整个操作是不能由相同变量上的另一个互锁操作所中断的单元。在抢先式多线程操作系统中,线程可以在从某个内存地址加载值之后但是在有机会更改和存储该值之前被挂起,因此互锁操作对于抢先多线程操作系统非常重要。
Interlocked类提供了以下功能:
— 在.NET框架2.0中,Add方法向变量添加一个整数值并返回该变量的新值。
— 在.NET框架2.0中,Read方法作为一个原子操作读取一个64位整数值。这在32位操作系统上是有用的,在32位操作系统上,读取一个64位整数通常不是一个原子操作。
—  Increment和Decrement方法递增或递减某个变量,并返回结果值。
—  Exchange方法执行指定变量的值的原子交换,返回该值并将其替换为新值。在.NETFramework 2.0中,可以使用此方法的一个泛型重载对任何引用类型的变量执行这种交换。
—  CompareExchange方法也交换两个值,但是根据比较的结果而进行操作。在.NET框架 2.0中,可以使用此方法的一个泛型重载对任何引用类型的变量执行这种交换。
在现代处理器中,Interlocked类的方法经常可以由单个指令来实现。因此,它们提供了性能非常高的同步,并且可用于构建更高级的同步机制。
可以使用Interlocked类来解决生产者-消费者关系中的竞争条件问题。在下面的程序中,标志bufferEmpty用来表示共享缓冲区是否为空。只有当共享缓冲区为空时,生产者才能向共享缓冲区中存入数据;只有当共享缓冲区为满时,消费者才能从共享缓冲区中取出数据。标志bufferEmpty的读取或修改分别使用Interlocked.Read或Interlocked.Increment、Interlocked. Decrement方法来进行,因此这个标志在任何时刻都只能被一个线程访问或修改。
// Interlocked.cs
// Interlocked示例
using System;
using System.Threading;
class Test
{
    private long bufferEmpty = 0;
    private string buffer = null;
    static void Main()
    {
        Test t = new Test();
        // 进行测试
        t.Go();
    }
    public void Go()
    {
        Thread t1 = new Thread(new ThreadStart(Producer));
        t1.Name = "生产者线程";
        t1.Start();
        Thread t2 = new Thread(new ThreadStart(Consumer));
        t2.Name = "消费者线程";
        t2.Start();
        // 等待两个线程结束
        t1.Join();
        t2.Join();
    }
    // 生产者方法
    public void Producer()
    {
        Console.WriteLine("{0}:开始执行", Thread.CurrentThread.Name);
        try
        {
            for (int j = 0; j < 16; ++j)
            {
                // 等待共享缓冲区为空
                while (Interlocked.Read(ref bufferEmpty) != 0)
                    Thread.Sleep(100);
                // 构造共享缓冲区
                Random r = new Random();
                int bufSize = r.Next() % 64;
                char[] s = new char[bufSize];
                for (int i = 0; i < bufSize; ++i)
                {
                    s[i] = (char)((int)'A' + r.Next() % 26);
                }
                buffer = new string(s);
                Console.WriteLine("{0}:{1}", Thread.CurrentThread.Name,
                                     buffer);
                // 互锁加一,成为1,标志共享缓冲区已满
                Interlocked.Increment(ref bufferEmpty);
                // 休眠,将时间片让给消费者
                Thread.Sleep(10);
            }
        
               Console.WriteLine("{0}:执行完毕", Thread.CurrentThread.Name);
        }
        catch (System.Threading.ThreadInterruptedException)
        {
            Console.WriteLine("{0}:被终止", Thread.CurrentThread.Name);
        }
    }
    // 消费者方法
    public void Consumer()
    {
        Console.WriteLine("{0}:开始执行", Thread.CurrentThread.Name);
        try
        {
            for (int j = 0; j < 16; ++j)
            {
                while (Interlocked.Read(ref bufferEmpty) == 0)
                    Thread.Sleep(100);
                // 打印共享缓冲区
                Console.WriteLine("{0}:{1}", Thread.CurrentThread.Name,
                                     buffer);
                // 互锁减一,成为0,标志共享缓冲区已空
                Interlocked.Decrement(ref bufferEmpty);
                // 休眠,将时间片让给生产者
                Thread.Sleep(10);
            }
            Console.WriteLine("{0}:执行完毕", Thread.CurrentThread.Name);
        }
        catch (System.Threading.ThreadInterruptedException)
        {
            Console.WriteLine("{0}:被终止", Thread.CurrentThread.Name);
        }
    }
}

述程序的输出结果如下:
生产者线程:开始执行
生产者线程:YHPJVKTWJDIOTWMVPTPECIENLZZGRQOZMPSZKSNWZNNKBDVTJZKG
消费者线程:开始执行
消费者线程:YHPJVKTWJDIOTWMVPTPECIENLZZGRQOZMPSZKSNWZNNKBDVTJZKG
生产者线程:M
消费者线程:M
生产者线程:QNVGDPMJYBAEUOVNASMWLPUPMKLFAQGTAICQSVDKJXEUAWZBSEXPFQBBT
消费者线程:QNVGDPMJYBAEUOVNASMWLPUPMKLFAQGTAICQSVDKJXEUAWZBSEXPFQBBT
生产者线程:QNVGDPMJYBAEUOVNASMWLPUPMKLFAQGTAICQSVDKJXEUAWZBSEXPFQBBT
消费者线程:QNVGDPMJYBAEUOVNASMWLPUPMKLFAQGTAICQSVDKJXEUAWZBSEXPFQBBT
生产者线程:EFLWFD
消费者线程:EFLWFD
生产者线程:HWUHZQZNHQBKKXMIOYYAQCXNJDCQHZOIGLLDVIFIXCFNZWCZWFYBBMPCWEYW
消费者线程:HWUHZQZNHQBKKXMIOYYAQCXNJDCQHZOIGLLDVIFIXCFNZWCZWFYBBMPCWEYW
生产者线程:ONHRFHDRJMPHBQITGJTXABXHBDSJQFSJHFGIIPEBLHBMXQAEITZB
消费者线程:ONHRFHDRJMPHBQITGJTXABXHBDSJQFSJHFGIIPEBLHBMXQAEITZB
生产者线程:Z
消费者线程:Z
生产者线程:GWNOQMTEYKKXCLRLUIQPMIKJCOEFCIKDVYNZQSUPYUPWWGEMRBMHBYLVJ
消费者线程:GWNOQMTEYKKXCLRLUIQPMIKJCOEFCIKDVYNZQSUPYUPWWGEMRBMHBYLVJ
生产者线程:GWNOQMTEYKKXCLRLUIQPMIKJCOEFCIKDVYNZQSUPYUPWWGEMRBMHBYLVJ
消费者线程:GWNOQMTEYKKXCLRLUIQPMIKJCOEFCIKDVYNZQSUPYUPWWGEMRBMHBYLVJ
生产者线程:RLDBSA
消费者线程:RLDBSA
生产者线程:FWGSEIQSSBQKRZWOXPFBFOACVWGUXODYNIPVLCJWZMZFWQDCMUAQYNH
消费者线程:FWGSEIQSSBQKRZWOXPFBFOACVWGUXODYNIPVLCJWZMZFWQDCMUAQYNH
生产者线程:FWGSEIQSSBQKRZWOXPFBFOACVWGUXODYNIPVLCJWZMZFWQDCMUAQYNH
消费者线程:FWGSEIQSSBQKRZWOXPFBFOACVWGUXODYNIPVLCJWZMZFWQDCMUAQYNH
生产者线程:QLWI
消费者线程:QLWI
生产者线程:XCJPMNGFHZIDSRIGIOCTRVQEWHSTJOSVBBZJTIWNMZQPVJHKVCNWXUZZMCQF
消费者线程:XCJPMNGFHZIDSRIGIOCTRVQEWHSTJOSVBBZJTIWNMZQPVJHKVCNWXUZZMCQF
生产者线程:XCJPMNGFHZIDSRIGIOCTRVQEWHSTJOSVBBZJTIWNMZQPVJHKVCNWXUZZMCQF
消费者线程:XCJPMNGFHZIDSRIGIOCTRVQEWHSTJOSVBBZJTIWNMZQPVJHKVCNWXUZZMCQF
生产者线程:执行完毕
消费者线程:执行完毕
显然,我们得到了正确的结果:消费者得到的数据与生产者提供的数据相同。
19.3.2  使用Monitor和lock
onitor类通过给单个线程授予对象锁来控制对象的访问。对象锁提供限制访问代码段(称为临界区)的能力。当一个线程拥有对象的锁时,其他任何线程都不能获取该锁。还可以使用Monitor类来确保不会允许其他的线程访问正在由锁的所有者执行的应用程序代码段,除非另一个线程正在使用其他的锁定对象执行该代码。
Monitor类具有以下功能:
— 它根据需要与某个对象相关联。
— 它是未绑定的,也就是说可以直接从任何上下文调用它。
— 不能创建Monitor类的实例。
Monitor类将维护每个同步对象的以下信息:
— 对当前持有锁的线程的引用。
— 对就绪队列的引用,它包含准备获取锁的线程。
— 对等待队列的引用,它包含正在等待锁定对象的状态发生变化的线程。
Monitor类通过使用静态方法Monitor.Enter、Monitor.TryEnter和Monitor.Exit来使特定对象获取锁和释放锁,以实现同步访问临界区的能力。在获取临界区的锁后,就可以使用静态方法Monitor.Wait、Monitor.Pulse和Monitor.PulseAll来与其他线程进行通信。这些方法的功能如表19-5所示。
表19-5  Monitor 类方法的功能
操    作        说    明
nter
TryEnter 获取对象锁。此操作同样会标记临界区的开头。其他任何线程都不能进入临界区,除非它使用其他锁定对象执行临界区中的指令
Wait 释放对象上的锁以便允许其他线程锁定和访问该对象。在其他线程访问对象时,调用线程将等待。脉冲信号用于通知等待线程有关对象状态的更改
Pulse
PulseAll 向一个或多个等待线程发送信号。该信号通知等待线程锁定对象的状态已更改,并且锁的所有者准备释放该锁。等待线程被放置在对象的就绪队列中以便它可以最后接收对象锁。一旦线程拥有了锁,它就可以检查对象的新状态以查看是否达到所需状态
Exit 释放对象上的锁。此操作还标记受锁定对象保护的临界区的结尾
 只能使用Monitor类来锁定引用类型(对象),而不能用于锁定值类型(值)。尽管可以向Monitor.Enter和Monitor.Exit传递值类型,但是每次调用值类型都会分别装箱。因此,每次调用都会创建一个不同的对象,传递给Monitor.Exit的对象不同于传递给Monitor.Enter的对象。所以,Monitor.Enter永远不会阻止,它要保护的代码并没有真正同步。由于传递给Monitor.Exit的对象不同于传递给Monitor.Enter的对象,Monitor.Monitor将引发SynchronizationLock Exception异常。
下面的程序使用Monitor类来同步生产者和消费者线程:
// Monitor.cs
// Monitor示例
using System;
using System.Threading;
class Test
{
    private object synObj = new object();
    private string buffer = null;
    static void Main()
    {
        Test t = new Test();
        // 进行测试
        t.Go();
    }
    public void Go()
    {
        Thread t1 = new Thread(new ThreadStart(Producer));
        t1.Name = "生产者线程";
        t1.Start();
        Thread t2 = new Thread(new ThreadStart(Consumer));
        t2.Name = "消费者线程";
        t2.Start();
        // 等待两个线程结束
        t1.Join();
        t2.Join();
    }
    // 生产者方法
    public void Producer()
    {
        Console.WriteLine("{0}:开始执行", Thread.CurrentThread.Name);
        for (int j = 0; j < 16; ++j)
        {
            try
            {
                // 进入临界区
                Monitor.Enter(synObj);
                // 构造共享缓冲区
                Random r = new Random();
                int bufSize = r.Next() % 64;
                char[] s = new char[bufSize];
                for (int i = 0; i < bufSize; ++i)
                {
                    s[i] = (char)((int)'A' + r.Next() % 26);
                }
                buffer = new string(s);
                Console.WriteLine("{0}:{1}", Thread.CurrentThread.Name,
                                        buffer);
                // 通知消费者数据已经准备好
                Monitor.Pulse(synObj);
                // 休眠,将时间片让给消费者
                Thread.Sleep(10);
            }
            catch (System.Threading.ThreadInterruptedException)
            {
                Console.WriteLine("{0}:被终止", Thread.CurrentThread.Name);
                break;
            }
            finally
            {
                // 退出临界区
                Monitor.Exit(synObj);
            }
        }
    
        Console.WriteLine("{0}:执行完毕", Thread.CurrentThread.Name);
    }
    // 消费者方法
    public void Consumer()
    {
        Console.WriteLine("{0}:开始执行", Thread.CurrentThread.Name);
        // 如果共享缓冲区为空,则休眠一秒,等待生产者线程构造缓冲区
        if (buffer == null)
            Thread.Sleep(1000);
        for (int j = 0; j < 16; ++j)
        {
            try
            {
                // 进入临界区
                Monitor.Enter(synObj);
                // 打印共享缓冲区
               Console.WriteLine("{0}:{1}", Thread.CurrentThread.Name,
                                    buffer);
                // 等待生产者的通知
                Monitor.Wait(synObj, 1000);
            }
            catch (System.Threading.ThreadInterruptedException)
            {
                Console.WriteLine("{0}:被终止", Thread.CurrentThread.Name);
                break;
            }
            finally
            {
                // 退出临界区
                Monitor.Exit(synObj);
            }
        }
        Console.WriteLine("{0}:执行完毕", Thread.CurrentThread.Name);
    }
}
当生产者将共享缓冲区填满后,就调用Monitor.Pulse方法通知消费者;当消费者取出了数据之后,就调用Monitor.Wait方法等待生成者的通知。注意,为了确保能从临界区中退出,应该在try语句的finally块中调用Monitor.Exit方法。
当一个线程A执行Monitor.Enter方法时,如果锁正被另一个线程B获得,那么线程A就会被阻塞,处于等待状态。在这种情形中,如果不想使线程A阻塞,就应该使用Monitor.TryEnter方法。
与Monitor.Enter方法不同,Monitor.TryEnter方法总会立即返回。如果锁未被其他线程获取,调用方法的线程就会获得锁,Monitor.TryEnter方法立即返回true值;如果锁已经被其他线程获取,调用方法的线程就不能获得锁,Monitor.TryEnter方法立即返回false值。因此,Monitor.TryEnter方法的基本调用方式如下:
if (Monitor.TryEnter(obj))
{
    // 获得锁,访问临界区
    // ……
}
else
{
    // 没有获得锁,以后再试
    // ……
}
为了方便Monitor类的使用,并确保能够从临界区中退出,C#提供了lock语句。lock语句的基本形式如下:
lock (【要锁定的引用类型对象】)
{
   【临界区语句块】
}
lock语句将【临界区语句块】标记为临界区,方法是获取给定对象的互斥锁,执行语句,然后释放该锁。lock语句确保当一个线程位于代码的临界区时,另一个线程不进入临界区。如果其他线程试图进入一段锁定代码,那么,在锁定对象被释放之前,它将一直处于等待状态。
实际上,lock语句完全等价于如下的Monitor类调用形式:
try
{
   Monitor.Enter(【要锁定的引用类型对象】);
   【临界区语句块】
}
finally
{
   Monitor.Exit(【要锁定的引用类型对象】);
}
因此上述程序的Producer和Consumer方法也可以简写为:
// 生产者方法
public void Producer()
{
    Console.WriteLine("{0}:开始执行", Thread.CurrentThread.Name);
    for (int j = 0; j < 16; ++j)
    {
        try
        {
            // 临界区
            lock (synObj)
            {
                // 构造共享缓冲区
                Random r = new Random();
                int bufSize = r.Next() % 64;
                char[] s = new char[bufSize];
                for (int i = 0; i < bufSize; ++i)
                {
                    s[i] = (char)((int)'A' + r.Next() % 26);
                }
                buffer = new string(s);
                Console.WriteLine("{0}:{1}", Thread.CurrentThread.Name,
                                               buffer);
                // 通知消费者数据已经准备好
                Monitor.Pulse(synObj);
                // 休眠,将时间片让给消费者
                Thread.Sleep(10);
            }
        }
        catch (System.Threading.ThreadInterruptedException)
        {
            Console.WriteLine("{0}:被终止", Thread.CurrentThread.Name);
            break;
        }
    }
    Console.WriteLine("{0}:执行完毕", Thread.CurrentThread.Name);
}
// 消费者方法
public void Consumer()
{
    Console.WriteLine("{0}:开始执行", Thread.CurrentThread.Name);
    // 如果共享缓冲区为空,则休眠一秒,等待生产者线程构造缓冲区
    if (buffer == null)
        Thread.Sleep(1000);
    for (int j = 0; j < 16; ++j)
    {
        try
        {
            // 临界区
            lock (synObj)
            {
                // 打印共享缓冲区
                Console.WriteLine("{0}:{1}", Thread.CurrentThread.Name,
                                             buffer);
                // 等待生产者的通知
                Monitor.Wait(synObj, 1000);
            }
        }
        catch (System.Threading.ThreadInterruptedException)
        {
            Console.WriteLine("{0}:被终止", Thread.CurrentThread.Name);
            break;
        }
    }
    Console.WriteLine("{0}:执行完毕", Thread.CurrentThread.Name);
}
使用lock语句不仅简便,还可以避免一种很严重的错误。如果使用Monitor.Enter和Monitor.Exit时传入了一个值类型,编译器不能发现问题,程序能正确编译,但是程序将出现错误。而使用lock语句时,如果锁定的对象是值类型,就会出现编译错误。
19.3.3  使用Mutex
utex类是另一种常用的同步类。使用Mutex对象可以对资源进行独占访问。Mutex类比Monitor类使用更多的系统资源,但是它可以跨越应用程序域的边界进行封送处理,可用于控制多个等待的线程,并且可用于同步不同进程中的线程。
线程调用Mutex.WaitOne方法请求所有权。该调用会一直阻塞到Mutex可用为止,或直至达到指定的超时间隔。如果没有任何线程拥有它,则Mutex的状态为已发信号的状态。
线程通过调用Mutex.ReleaseMutex方法释放Mutex。Mutex是与线程关联的,即Mutex只能由拥有它的线程释放。如果线程释放不是它拥有的Mutex,则会在该线程中引发ApplicationException异常。
如果线程终止而未释放Mutex,则认为该Mutex已放弃。这是严重的编程错误,因为该Mutex正在保护的资源可能会处于不一致的状态。在.NET框架2.0中,获取该Mutex的下一个线程中会引发AbandonedMutexException异常。但是,在.NET框架1.0和1.1中不会引发异常,放弃的Mutex被设置为已发送信号状态,下一个等待线程将获得所有权。如果没有等待线程,则Mutex保持已发送信号状态。
Mutex分两种类型:本地Mutex和命名的系统Mutex。如果使用接受名称的构造函数创建了Mutex对象,那么该对象将与具有该名称的操作系统对象相关联。对于命名的系统Mutex,即使它们分别在不同的应用程序、进程或者内存空间中创建,只要它们的名称相同,在操作系统中都会指向同一个Mutex对象。本地Mutex仅存在于进程当中。进程中引用本地Mutex对象的任意线程都可以使用本地Mutex。
Mutex与Monitor存在两个很大的区别:
—  Mutex可以用来同步属于不同应用程序或者进程的线程;而Monitor没有这个能力。
— 如果获得了Mutex的线程终止了,系统就会认为Mutex被自动释放,其他线程可以获得其控制权,而Monitor没有这种特征。
为了说明Mutex类的用法,我们将生产者和消费者线程分别放在两个应用程序中。在两个应用程序中都各自创建一个同名的Mutex对象,并利用它们来对生产者和消费者线程同步。
生产者线程所在的应用程序的代码如下:
// Mutex1.cs
// Mutex1示例
using System;
using System.IO;
using System.Threading;
using System.Diagnostics;
class Test
{
    static void Main()
    {
        Test t = new Test();
        // 进行测试
        t.Go();
    }
    public void Go()
    {
        // 创建并启动线程
        Thread t1 = new Thread(new ThreadStart(Producer));
        t1.Name = "生产者线程";
        t1.Start();
        // 等待线程结束
        t1.Join();
        Console.WriteLine("按Enter键退出...");
        Console.Read();
    }
    // 生产者方法
    public void Producer()
    {
        Console.WriteLine("{0}:开始执行", Thread.CurrentThread.Name);
        // 创建互斥体
        Mutex mutex = new Mutex(false, "CSharp_Mutex_test");
        // 启动消费者进程
        Process.Start("Mutex2.exe");
        for (int j = 0; j < 16; ++j)
        {
            try
            {
                // 进入互斥体
                mutex.WaitOne();
                FileStream fs = new FileStream(@"d:\text.txt",
                                        FileMode.OpenOrCreate, FileAccess.Write);
                StreamWriter sw = new StreamWriter(fs);
                // 构造字符串
                Random r = new Random();
                int bufSize = r.Next() % 64;
                char[] s = new char[bufSize];
                for (int i = 0; i < bufSize; ++i)
                {
                    s[i] = (char)((int)'A' + r.Next() % 26);
                }
                string str = new string(s);
                // 将字符串写入文件
                sw.WriteLine(str);
                sw.Close();
                Console.WriteLine("{0}:{1}", Thread.CurrentThread.Name,
                                        str);
            }
            catch (System.Threading.ThreadInterruptedException)
            {
                Console.WriteLine("{0}:被终止", Thread.CurrentThread.Name);
                break;
            }
            finally
            {
                // 退出互斥体
                mutex.ReleaseMutex();
            }
            // 休眠,将时间片让给消费者
            Thread.Sleep(1000);
        }
        // 关闭互斥体
        mutex.Close();
        Console.WriteLine("{0}:执行完毕", Thread.CurrentThread.Name);
   }
}
消费者线程所在的应用程序的代码如下:
// Mutex2.cs
// Mutex2示例
using System;
using System.IO;
using System.Threading;
class Test
{
    static void Main()
    {
        Test t = new Test();
        // 进行测试
        t.Go();
    }
    public void Go()
    {
        // 创建并启动线程
        Thread t2 = new Thread(new ThreadStart(Consumer));
        t2.Name = "消费者线程";
        t2.Start();
        // 等待线程结束
        t2.Join();
        Console.WriteLine("按Enter键退出...");
        Console.Read();
    }
    // 消费者方法
    public void Consumer()
    {
        Console.WriteLine("{0}:开始执行", Thread.CurrentThread.Name);
        // 创建互斥体
        Mutex mutex = new Mutex(false, "CSharp_Mutex_test");
        for (int j = 0; j < 16; ++j)
        {
            try
            {
                // 进入互斥体
                mutex.WaitOne();
                StreamReader sr = new StreamReader(@"d:\text.txt");
                string s = sr.ReadLine();
                sr.Close();
                // 显示字符串的值
                Console.WriteLine("{0}:{1}", Thread.CurrentThread.Name, s);
            }
            catch (System.Threading.ThreadInterruptedException)
            {
                Console.WriteLine("{0}:被终止", Thread.CurrentThread.Name);
                break;
            }
            finally
            {
                // 退出互斥体
                mutex.ReleaseMutex();
            }
            // 休眠,将时间片让给消费者
            Thread.Sleep(1000);
        }
        // 关闭互斥体
        mutex.Close();
        Console.WriteLine("{0}:执行完毕", Thread.CurrentThread.Name);
    }
}
在【命令提示】窗口中启动Mutex1.exe,它会在另一个【命令提示】窗口中启动Mutex2.exe。可以看出,命名的系统Mutex将两个位于不同进程中的线程同步得非常好。
Mutex经常用于保护多个进程或者应用程序需要共享的资源,比如文件和内存等。
19.3.4  死锁和竞争条件
锁是指使用共享资源的两个或多个线程各自占有一些共享资源,并且互相等待其他线程占有的资源才能继续进行,而导致两个或多个线程都在等待对方释放资源,都停止执行的情形。
如果线程之间的同步出现问题,就会导致死锁。下面的程序演示一种典型的死锁的情形。线程一和线程二都需要锁对象obj1和obj2。线程一首先锁定了obj1,而线程二首先锁定了obj2。当线程一试图获取锁obj2时发现不能获取,于是开始等待。当线程二试图获取obj1时也发现不能获取,于是也开始等待。结果是线程一和线程二互相等待对方占有的共享资源,都不能继续执行下去。
// Deadlock.cs
// 死锁示例
using System;
using System.IO;
using System.Threading;
class Test
{
    static object obj1 = new object();
    static object obj2 = new object();
    static void Main()
    {
        Console.WriteLine("创建线程");
        // 创建线程
        Thread thread1 = new Thread(new ThreadStart(ThreadProc1));
        thread1.Name = "线程一";
        Thread thread2 = new Thread(new ThreadStart(ThreadProc2));
        thread2.Name = "线程二";
        // 启动线程
        thread1.Start();
        thread2.Start();
        // 等待两个线程结束
        thread1.Join();
        thread2.Join();
    }
    public static void ThreadProc1()
    {
        Console.WriteLine("{0}开始执行", Thread.CurrentThread.Name);
        Console.WriteLine("{0}: 锁定obj1", Thread.CurrentThread.Name);
        lock (obj1)
        {
            // 模拟一些工作
            Thread.Sleep(1000);
            Console.WriteLine("{0}: 等待obj2", Thread.CurrentThread.Name);
           
            lock (obj2)
            {
                // 模拟一些工作
               Thread.Sleep(1000);
           }
        }
        Console.WriteLine("{0}结束", Thread.CurrentThread.Name);
    }
    public static void ThreadProc2()
    {
        Console.WriteLine("{0}开始执行", Thread.CurrentThread.Name);
        Console.WriteLine("{0}: 锁定obj2", Thread.CurrentThread.Name);
        lock (obj2)
        {
            // 模拟一些工作
            Thread.Sleep(1000);
            Console.WriteLine("{0}: 等待obj1", Thread.CurrentThread.Name);
            lock (obj1)
            {
                // 模拟一些工作
                Thread.Sleep(1000);
            }
        }
        Console.WriteLine("{0}结束", Thread.CurrentThread.Name);
    }
}
竞争条件是当程序的结果取决于两个或更多个线程中的哪一个先到达某一特定代码块时出现的一种缺陷。多次运行程序将产生不同的结果,而且给定的任何一次运行的结果都不可预知。
下面的程序演示了常见的生产者-消费者关系中,因为同步不当而引起的竞争条件问题。
// RaceConditions.cs
// 生产者消费者造成的竞争条件示例
using System;
using System.Threading;
class Test
{
    private long counter = 0;
    static void Main()
    {
        Test t = new Test();
        // 进行测试
        t.Go();
    }
    public void Go()
    {
        Thread t1 = new Thread(new ThreadStart(Producer));
        t1.Name = "生产者线程";
        t1.Start();
        Thread t2 = new Thread(new ThreadStart(Consumer));
        t2.Name = "消费者线程";
        t2.Start();
        // 等待两个线程结束
        t1.Join();
        t2.Join();
    }
    // 生产者方法
    public void Producer()
    {
        Console.WriteLine("{0}:开始执行", Thread.CurrentThread.Name);
        while (counter < 1000)
        {
            long temp = counter;
        
           // 模拟一些工作
            Thread.Sleep(100);
            lock (this)
            {
                temp++; // 加一
                // 模拟一些工作
                Thread.Sleep(10);
                Monitor.Pulse(this);
            }
           
            // 给counter赋值,并显示结果
            counter = temp;
            Console.WriteLine("{0}:counter = {1}",
                              Thread.CurrentThread.Name, counter);
            // 休眠,将时间片让给消费者
            Thread.Sleep(100);
        }
        Console.WriteLine("{0}:执行完毕", Thread.CurrentThread.Name);
    }
    // 消费者方法
    public void Consumer()
    {
        Console.WriteLine("{0}:开始执行", Thread.CurrentThread.Name);
        while (counter < 1000)
        {
            long temp;
            lock (this)
            {
                temp = counter;
                // 模拟一些工作
                Thread.Sleep(10);
                Monitor.Wait(this, 1000);
            }
            // 显示counter的值
            Console.WriteLine("{0}:counter = {1}",
                              Thread.CurrentThread.Name, temp);
            // 休眠,将时间片让给消费者
            Thread.Sleep(100);
        }
        Console.WriteLine("{0}:执行完毕", Thread.CurrentThread.Name);
    }
}
上述程序的输出结果如下:
生产者线程:开始执行
消费者线程:开始执行
生产者线程:counter = 1
消费者线程:counter = 0
生产者线程:counter = 2
消费者线程:counter = 1
生产者线程:counter = 3
消费者线程:counter = 2
生产者线程:counter = 4
消费者线程:counter = 3
生产者线程:counter = 5
消费者线程:counter = 4
产者线程:counter = 6
消费者线程:counter = 5
生产者线程:counter = 7
消费者线程:counter = 6
生产者线程:counter = 8
消费者线程:counter = 7
消费者线程:counter = 8
生产者线程:counter = 9
消费者线程:counter = 9
生产者线程:counter = 10
消费者线程:counter = 10
可以看出,消费者从生产者那里得到的数据有很多错误。与死锁相比,竞争条件更难发现,经常会造成很大的危害。
4.6.4. 异步调用
异步操作通常用于执行完成时间可能较长的任务,如打开大文件、连接远程计算机或查询数据库。异步操作在主应用程序线程以外的线程中执行。应用程序调用方法异步执行某个操作时,应用程序可在异步方法执行其任务时继续执行。
.NET框架能够对任何方法进行异步调用。进行异步调用时,需要定义与异步调用的方法具有相同签名的委托。公共语言运行时会自动使用适当的签名为该委托定义BeginInvoke和EndInvoke方法。
BeginInvoke方法用于启动异步调用。它与需要异步执行的方法具有相同的参数。此外,它还有两个可选参数。第一个参数是一个AsyncCallback委托,该委托引用在异步调用完成时要调用的方法。第二个参数是一个用户定义的对象,该对象可向回调方法传递数据。BeginInvoke立即返回,不会等待异步调用完成,被调用的方法将在线程池线程中执行。因此,提交请求的原始线程与执行异步方法的线程池线程是并行执行的。BeginInvoke会返回一个IAsyncResult对象,可以使用该对象来监视异步调用进度,也可将该对象传递给EndInvoke方法,以获取异步执行的方法的返回值。
EndInvoke方法用于检索异步调用的结果。调用BeginInvoke方法后可随时调用EndInvoke方法;如果异步调用尚未完成,EndInvoke方法将一直阻塞调用线程,直到异步调用完成后才允许调用线程执行。EndInvoke方法的参数包括需要异步执行的方法的out和ref参数,以及由BeginInvoke返回的IAsyncResult对象。因此,通过EndInvoke方法可以获得异步调用的方法的所有输出数据,包括返回值、out和ref参数。
AsyncCallback委托表示在异步操作完成时调用的回调方法,其定义如下:
public delegate void AsyncCallback(IAsyncResult ar);
System.IAsyncResult接口表示异步操作的状态。IAsyncResult接口由包含可异步操作的方法的类实现。它是启动异步操作的方法的返回类型,也是结束异步操作的方法的第三个参数的类型。当异步操作完成时,IAsyncResult对象也将传递给由AsyncCallback委托调用的方法。支持IAsyncResult接口的对象存储异步操作的状态信息,并提供同步对象以允许线程在操作完成时终止。IAsyncResult接口定义了四个公开属性,通过它们可以获取异步调用的状态。
—  AsyncState:获取用户定义的对象,它限定或包含关于异步操作的信息。
—  AsyncWaitHandle:获取用于等待异步操作完成的 WaitHandle。
—  CompletedSynchronously:获取异步操作是否同步完成的指示。
—  IsCompleted:获取异步操作是否已完成的指示。
使用BeginInvoke和EndInvoke进行异步调用的常用方法主要有四种:
— 调用BeginInvoke方法启动异步方法,进行某些操作,然后调用EndInvoke方法来一直阻止请求线程到调用完成。
— 调用BeginInvoke方法启动异步方法,使用System.IAsyncResult.AsyncWaitHandle属性获取WaitHandle,使用它的WaitOne方法一直阻止执行直到发出WaitHandle信号,然后调用EndInvoke方法。
— 调用BeginInvoke方法启动异步方法,轮询由BeginInvoke返回的IAsyncResult,确定异步调用何时完成,然后调用EndInvoke。
— 调用BeginInvoke方法启动异步方法时,将代表异步方法完成时需要回调的方法的委托传递给BeginInvoke。异步调用完成后,将在ThreadPool线程上执行该回调方法。在该回调方法中调用EndInvoke。
每种方法都是通过BeginInvoke方法来启动异步方法,调用EndInvoke方法来完成异步调用。
1.直接调用EndInvoke 方法等待异步调用结束
步执行方法的最简单的方式是通过调用委托的BeginInvoke方法来开始执行方法,在主线程上执行一些工作,然后调用委托的EndInvoke方法。EndInvoke可能会阻止调用线程,因为它直到异步调用完成之后才返回。这种技术非常适合于文件或网络操作,但是由于EndInvoke会阻止它,所以不要从服务于用户界面的线程中调用它。
下面的代码说明了如何使用这种方法来进行异步调用,并获得异步方法的结果:
// AsynCall1.cs
// 异步调用示例: 直接调用EndInvoke 方法等待异步调用结束
using System;
using System.Threading;
// 定义异步调用方法的委托
// 它的签名必须与要异步调用的方法一致
public delegate int AsynComputeCaller(ulong l, out ulong factorial);
public class Factorial
{
    // 计算阶乘
    public ulong Compute(ulong l)
    {
        // 不要太快 :-)
        Thread.Sleep(50);
        if (l == 1)
        {
            return 1;
        }
        else
        {
            return l * Compute(l - 1);
        }
    }
    // 要异步调用的方法
    // 1. 调用Factorial方法来计算阶乘,并用out参数返回
    // 2. 统计计算阶乘所用的时间,并返回该值
    public int AsynCompute(ulong l, out ulong factorial)
    {
        Console.WriteLine("开始异步方法");
       DateTime startTime = DateTime.Now;
        factorial = Compute(l);
        TimeSpan usedTime =
                      new TimeSpan(DateTime.Now.Ticks - startTime.Ticks);
       
        Console.WriteLine("结束异步方法");
        return usedTime.Milliseconds;
    }
}
public class Test
{
    public static void Main()
    {
        // 创建包含异步方法的类的实例
        Factorial fact = new Factorial();
        // 创建异步委托
        AsynComputeCaller caller = new AsynComputeCaller(fact.AsynCompute);
        Console.WriteLine("启动异步调用");
        ulong l = 30;
        ulong lf;
        // 启动异步调用
        IAsyncResult result = caller.BeginInvoke(l, out lf, null, null);
        // 主线程进行一些操作
        Thread.Sleep(0);
        Console.WriteLine("主线程进行一些操作");
        // 调用EndInvoke来等待异步调用结束,并获得结果
        int returnValue = caller.EndInvoke(out lf, result);
        // 异步调用的方法已经结束,显示结果
        Console.WriteLine("已经得到结果:{0}的阶乘为{1},计算时间为{2}毫秒",
                               l, lf, returnValue);
    }
}
2.使用 WaitHandle 等待异步调用结束
以使用BeginInvoke方法返回的IAsyncResult的AsyncWaitHandle属性来获取WaitHandle。异步调用完成时会发出WaitHandle信号,可以通过调用WaitOne方法来等待它。
如果使用WaitHandle,则在异步调用完成之前或之后,在通过调用EndInvoke检索结果之前,还可以执行其他处理。
下面的代码说明了如何使用这种方法来进行异步调用,并获得异步方法的结果:
// AsynCall2.cs
// 异步调用示例:使用 WaitHandle 等待异步调用结束
using System;
using System.Threading;
// 定义异步调用方法的委托
// 它的签名必须与要异步调用的方法一致
public delegate int AsynComputeCaller(ulong l, out ulong factorial);
public class Factorial
{
    // 计算阶乘
    public ulong Compute(ulong l)
    {
        // 不要太快 :-)
        Thread.Sleep(50);
        if (l == 1)
        {
            return 1;
        }
        else
        {
            return l * Compute(l - 1);
        }
    }
    // 要异步调用的方法
    // 1. 调用Factorial方法来计算阶乘,并用out参数返回
    // 2. 统计计算阶乘所用的时间,并返回该值
    public int AsynCompute(ulong l, out ulong factorial)
    {
        Console.WriteLine("开始异步方法");
        DateTime startTime = DateTime.Now;
        factorial = Compute(l);
        TimeSpan usedTime =
                        new TimeSpan(DateTime.Now.Ticks - startTime.Ticks);
       
        Console.WriteLine("结束异步方法");
        return usedTime.Milliseconds;
    }
}
public class Test
{
    public static void Main()
    {
        // 创建包含异步方法的类的实例
        Factorial fact = new Factorial();
        // 创建异步委托
        AsynComputeCaller caller = new AsynComputeCaller(fact.AsynCompute);
        Console.WriteLine("启动异步调用");
        ulong l = 30;
        ulong lf;
        // 启动异步调用
        IAsyncResult result = caller.BeginInvoke(l, out lf, null, null);
        // 主线程进行一些操作
        Thread.Sleep(0);
        Console.WriteLine("主线程进行一些操作");
        // 等待WaitHandle接收到信号
        Console.WriteLine("等待WaitHandle接收到信号");
        result.AsyncWaitHandle.WaitOne();
        // 主线程进行一些操作
        Thread.Sleep(0);
        Console.WriteLine("异步方法已经结束,主线程进行另外一些操作");
        // 调用EndInvoke来获得结果
        int returnValue = caller.EndInvoke(out lf, result);
        // 异步调用的方法已经结束,显示结果
        Console.WriteLine("{0}的阶乘为{1},计算时间为{2}毫秒",
                              l, lf, returnValue);
    }
}
3.轮询异步调用是否完成
以使用由BeginInvoke方法返回的IAsyncResult的IsCompleted属性来发现异步调用何时完成。从用户界面的服务线程中进行异步调用时可以执行此操作。轮询完成允许调用线程在异步调用在线程池线程上执行时继续执行。
下面的代码说明了如何使用这种方法来进行异步调用,并获得异步方法的结果:
// AsynCall3.cs
// 异步调用示例: 轮询异步调用是否完成
using System;
using System.Threading;
// 定义异步调用方法的委托
// 它的签名必须与要异步调用的方法一致
public delegate int AsynComputeCaller(ulong l, out ulong factorial);
public class Factorial
{
    // 计算阶乘
    public ulong Compute(ulong l)
    {
        // 不要太快 :-)
        Thread.Sleep(50);
        if (l == 1)
        {
            return 1;
        }
        else
        {
            return l * Compute(l - 1);
        }
    }
    // 要异步调用的方法
    // 1. 调用Factorial方法来计算阶乘,并用out参数返回
    // 2. 统计计算阶乘所用的时间,并返回该值
    public int AsynCompute(ulong l, out ulong factorial)
    {
        Console.WriteLine("开始异步方法");
       
        DateTime startTime = DateTime.Now;
        factorial = Compute(l);
        TimeSpan usedTime =
                               new TimeSpan(DateTime.Now.Ticks - startTime.Ticks);
       
        Console.WriteLine("\n结束异步方法");
        return usedTime.Milliseconds;
    }
}
public class Test
{
    public static void Main()
    {
        // 创建包含异步方法的类的实例
        Factorial fact = new Factorial();
        // 创建异步委托
        AsynComputeCaller caller = new AsynComputeCaller(fact.AsynCompute);
        Console.WriteLine("启动异步调用");
        ulong l = 30;
        ulong lf;
        // 启动异步调用
        IAsyncResult result = caller.BeginInvoke(l, out lf, null, null);
        // 轮询异步方法是否结束
        Console.WriteLine("主线程进行一些操作");
        while (result.IsCompleted == false)
        {
            // 主线程进行一些操作
            Thread.Sleep(10);
            Console.Write(".");
        }
        // 主线程进行一些操作
        Thread.Sleep(0);
        Console.WriteLine("异步方法已经结束,主线程进行另外一些操作");
        // 调用EndInvoke来获得结果
        int returnValue = caller.EndInvoke(out lf, result);
        // 异步调用的方法已经结束,显示结果
        Console.WriteLine("已经得到结果:{0}的阶乘为{1},计算时间为{2}毫秒",
                               l, lf, returnValue);
    }
}
4.在异步调用完成时执行回调方法
果启动异步调用的线程是不需要处理结果的线程,则可以在调用完成时执行回调方法。回调方法在线程池线程上执行。
若要使用回调方法,必须将引用回调方法的AsyncCallback委托传递给BeginInvoke。也可以传递包含回调方法将要使用的信息的对象。例如,可以传递启动调用时曾使用的委托,以便回调方法能够调用EndInvoke方法。
下面的代码说明了如何使用这种方法来进行异步调用,并获得异步方法的结果:
// AsynCall4.cs
// 异步调用示例: 在异步调用完成时执行回调方法
using System;
using System.Threading;
// 定义异步调用方法的委托
// 它的签名必须与要异步调用的方法一致
public delegate int AsynComputeCaller(ulong l, out ulong factorial);
public class Factorial
{
    // 计算阶乘
    public ulong Compute(ulong l)
    {
        // 不要太快 :-)
        Thread.Sleep(50);
        if (l == 1)
        {
            return 1;
        }
        else
        {
            return l * Compute(l - 1);
        }
    }
    // 要异步调用的方法
    // 1. 调用Factorial方法来计算阶乘,并用out参数返回
    // 2. 统计计算阶乘所用的时间,并返回该值
    public int AsynCompute(ulong l, out ulong factorial)
    {
        Console.WriteLine("开始异步方法");
        DateTime startTime = DateTime.Now;
        factorial = Compute(l);
        TimeSpan usedTime =
                               new TimeSpan(DateTime.Now.Ticks - startTime.Ticks);
       
        Console.WriteLine("结束异步方法");
        return usedTime.Milliseconds;
    }
}
public class Test
{
    static ulong l = 30;
    static ulong lf;
    public static void Main()
    {
        // 创建包含异步方法的类的实例
        Factorial fact = new Factorial();
        // 创建异步委托
        AsynComputeCaller caller = new AsynComputeCaller(fact.AsynCompute);
        // 启动异步调用
        Console.WriteLine("启动异步调用");
        IAsyncResult result = caller.BeginInvoke(l, out lf,
                                new AsyncCallback(CallbackMethod), caller);
        // 主线程进行一些操作
        Thread.Sleep(0);
        Console.WriteLine("主线程进行一些操作");
        Console.WriteLine("按Enter键结束程序...");
        Console.ReadLine();
    }
    // 在异步调用完成时执行的回调方法
    // 该回调方法的签名必须与AsyncCallback委托一致
    static void CallbackMethod(IAsyncResult ar)
    {
        // 获取委托
        AsynComputeCaller caller = (AsynComputeCaller)ar.AsyncState;
        // 调用EndInvoke来获得结果
        int returnValue = caller.EndInvoke(out lf, ar);
        // 异步调用的方法已经结束,显示结果
        Console.WriteLine("已经得到结果:{0}的阶乘为{1},计算时间为{2}毫秒",
                               l, lf, returnValue);
    }
}
4.6.5. 进程与线程
通俗解释:进程是老子,线程是儿子,没有老子就没有儿子,一个老子可以有多个儿子.一个儿子可以成为别人的儿子,一个老子也可以为别的老子生儿子.
进程简单理解为单个程序吧(按ctrl+alt+del)可以看到的.它至少有一个主线程 .
 进程和线程都是由操作系统所体会的程序运行的基本单元,系统利用该基本单元实现系统对应用的并发性。进程和线程的区别在于:
简而言之,一个程序至少有一个进程,一个进程至少有一个线程. 
线程的划分尺度小于进程,使得多线程程序的并发性高。
另外,进程在执行过程中拥有独立的内存单元,而多个线程共享内存,从而极大地提高了程序的运行效率。
线程在执行过程中与进程还是有区别的。每个独立的线程有一个程序运行的入口、顺序执行序列和程序的出口。但是线程不能够独立执行,必须依存在应用程序中,由应用程序提供多个线程执行控制。
从逻辑角度来看,多线程的意义在于一个应用程序中,有多个执行部分可以同时执行。但操作系统并没有将多个线程看做多个独立的应用,来实现进程的调度和管理以及资源分配。这就是进程和线程的重要区别。

进程是具有一定独立功能的程序关于某个数据集合上的一次运行活动,进程是系统进行资源分配和调度的一个独立单位.线程是进程的一个实体,是CPU调度和分派的基本单位,它是比进程更小的能独立运行的基本单位.线程自己基本上不拥有系统资源,只拥有一点在运行中必不可少的资源(如程序计数器,一组寄存器和栈),但是它可与同属一个进程的其他的线程共享进程所拥有的全部资源.一个线程可以创建和撤销另一个线程;同一个进程中的多个线程之间可以并发执行.

4.6.6. sleep() 和 wait()
sleep()方法是使线程停止一段时间的方法。在sleep 时间间隔期满后,线程不一定立即恢复执行。这是因为在那个时刻,其它线程可能正在运行而且没有被调度为放弃执行,除非(a)“醒来”的线程具有更高的优先级 (b)正在运行的线程因为其它原因而阻塞。
wait()是线程交互时,如果线程对一个同步对象x 发出一个wait()调用,该线程会暂停执行,被调对象进入等待状态,直到被唤醒或等待时间到。
4.7. 一般问题
4.7.1. 有用的工具
Lutz Roeder 的 .NET Reflector(用于分析程序集)
NDoc(用于创建代码文档资料)
NAnt(用于生成解决方案)
ASP.NET 版本转换器
FxCop(用于监视代码)
编写NUnit测试(用于编写单元测试)
CodeSmith(用于生成代码)
Regulator(用于生成正则表达式)
Snippet Compiler(用于编译少量代码)

4.7.2. using和new的意义
using 的用途是为某个namespace建立别名,或者引用存在系统中的其它namespace.
New 用来创建实例,或者覆写方法。
(1)new 运算符
用于创建对象和调用构造函数。
(2)new 修饰符
用于向基类成员隐藏继承成员。
(3)new 约束
用于在泛型声明中约束可能用作类型参数的参数的类型。

4.7.3. C#的五种可访问性
public:成员可以从任何代码访问。
protected:成员只能从派生类访问。
internal:成员只能从同一程序集的内部访问。
protected internal:成员只能从同一程序集内的派生类访问。
private:成员只能在当前类的内部访问。

4.7.4. C#的特点
A.简单
1.C#中指针已经消失.
2.不安全的操作,比方说直接内存操作不被允许了.
3.C#中"::"或"->"操作符是没用的
4.因为它是基于.NET平台的,它继承了自动内存管理和垃圾回收的特点.
5.原始数据类型可变的数据范围象Integer,Floats等.
6.整形数值0和1不再作为布尔值出现.C#中的布尔值是纯粹的true和false值而且没有更多的"="操作符和"=="操作符错误."=="被用于进行比较操作而"="被用做赋值操作.
B.现代
1.C#建立在当前的潮流上,对于创建相互兼容的,可伸缩的,健壮的应用程序来说是非常强大和简单的.
2.C#拥有内建的支持来将任何组件转换成一个web service,运行在任何平台上的任何应用程序都可以通过互联网来使用这个服务.
C.面向对象的
1.C#支持数据封装,继承,多态和对象界面(即java中的interface关键字).
2.(int,float,double)在java中都不是对象,但是C#引入和结构体(structs)来使原始数据类型变成对象int i=1;String a=i Tostring();//转换(或者)Boxing
D.类型安全
1.在C#中我们不能进行不安全的类型转换象将double转换成boolean.
2.值类型(常量类型)被初始化为零值而引用类型(对象和类被编译器自动初始化为零值.
3.数组类型下标从零开始而且进行越界检查.
4.类型溢出将被检查.
E.相互兼容性
1.C#提供对COM和基于windows的应用程序的原始的支持.
2.允许对原始指针的有限制的使用.
3.用户不再需要显式的实现unkown和其它COM界面,这些功能已经内建.
4.C#允许用户将指针作为不安全的代码段来操作老的代码.
5.VB.NET和其它中间代码语言中的组件可以在C#中直接使用
F.可伸缩性和可升级性
1..NET引入了零部件的概念,它们通过其"手册"具有自描述的功能.手册确立了零部件的身份,版本,语言和数字签名等.零部件不需要在任何地方注册.
2.要扩展我们的程序,我们只需要删除老的文件并用新的文件来升级它们.不需要注册动态链接库.
3.升级软件组件的过程只是一个错误探测的任务.对代码的修改能够影响现存的程序,C#在语言中支持版本修改.对界面和方法重载的支持使得复杂的程序框架能随着时间发展和进化
结论
C#是一种现代的,类型安全的,面向对象的编程语言,它使得程序员能够快速而容易的为微软.NET平台开发解决方案.

4.7.5. 对象间可能存在的三种关系
聚合,一个(较复杂的)对象由其他若干(较简单的)对象作为其构成部分,称作聚合。
关联,对象之间的静态联系(即通过对象属性体现的联系)
4.7.6. Main()方法
C#程序是从方法Main()开始执行的。这个方法必须是类或结构的静态方法,并且其返回类型必须是int或void。
虽然显式指定public修饰符是很常见的,因为按照定义,必须在程序外部调用该方法,但我们给该方法指定什么访问级别并不重要,即使把该方法标记为private,它也可以运行。
多个Main()方法
在编译C#控制台或Windows应用程序时,默认情况下,编译器会在与上述签名匹配的类中查找Main方法,并使这个类方法成为程序的入口。如果有多个Main方法,编译器就会返回一个错误,例如,考虑下面的代码MainExample.cs:
using System;
namespace Wrox.ProCSharp.Basics
{
   class Client
   {
      public static int Main()
      {
         MathExample.Main();
         return 0;
      }
   }
   class MathExample
   {
      static int Add(int x, int y)
      {
         return x + y;
      }
      public static int Main()
      {
         int i = Add(5,10);
         Console.WriteLine(i);
         return 0;
      }
   }
}
上述代码中包含两个类,它们都有一个Main()方法。如果按照通常的方式编译这段代码,就会得到下述错误:
csc MainExample.cs
Microsoft (R) Visual C# .NET Compiler version 8.00.40607.16
for Microsoft (R) .NET Framework version 2.00.40607

Copyright (C) Microsoft Corporation 2001-2003. All rights reserved.
MainExample.cs(7,23): error CS0017: Program 'MainExample.exe' has more than one entry point defined: 'Wrox.ProCSharp.Basics.Client.Main()'
MainExample.cs(21,23): error CS0017: Program 'MainExample.exe' has more than one entry point defined: 'Wrox.ProCSharp.Basics.MathExample.Main()'
但是,可以使用/main选项,其后跟Main()方法所属类的全名(包括命名空间),明确告诉编译器把哪个方法作为程序的入口点:
csc MainExample.cs /main:Wrox.ProCSharp.Basics.MathExample
给Main()方法传送参数
前面的例子只介绍了不带参数的Main()方法。但在调用程序时,可以让CLR包含一个参数,将命令行参数转送给程序。这个参数是一个字符串数组,传统称为args(但C#可以接受任何名称)。在启动程序时,可以使用这个数组,访问通过命令行传送过来的选项。
下面的例子ArgsExample.cs是在传送给Main方法的字符串数组中迭代,并把每个选项的值写入控制台窗口:
using System;
namespace Wrox.ProCSharp.Basics
{
   class ArgsExample
   {
      public static int Main(string[] args)
      {
         for (int i = 0; i < args.Length; i++)
         {
            Console.WriteLine(args[i]);
         }
         return 0;
      }
   }
}
通常使用命令行就可以编译这段代码。在运行编译好的可执行文件时,可以在程序名的后面加上参数,例如:
ArgsExample /a /b /c
/a
/b
/c
4.7.7. 构造函数与析构函数
构造函数和析构函数是类的特殊方法,分别用来初始化类的实例和销毁类的实例。
构造函数
构造函数(constructor)用于执行类的实例的初始化。每个类都有构造函数。即使没有为类声明构造函数,编译器也会自动地为类提供一个默认的构造函数。
1.构造函数的特征
构造函数的名称与类名相同,其基本特征为:
— 构造函数不声明返回类型(甚至也不能使用void),也不能返回值。
— 一般地,构造函数总是public 类型的。private 类型的构造函数表明类不能被实例化,通常用于只含有静态成员的类。
— 在构造函数中不要做对类的实例进行初始化以外的事情,也不能被显式地调用。

下面的代码说明了构造函数的声明方式:
// Dog.cs
// 构造对象时将执行构造函数
using System;
public class Dog
{
    public string name;
    public Dog()    // 声明构造函数
    {
        name = "未知";
        Console.WriteLine("Dog():Dog类已被初始化。");
    }
    public void Bark()
    {
        Console.WriteLine("汪汪!");
    }
    public static void Main()
    {
        Dog myDog = new Dog();  // 会调用构造函数
        Console.WriteLine("myDog的名字为“{0}”,叫声为:", myDog.name);
        myDog.Bark();
    }
}
在访问一个类的时候,系统将首先执行构造函数中的语句。构造函数的功能是创建对象,使对象的状态合法化。在从构造函数返回之前,对象都是不确定的,不能用于执行任何操作;只有在构造函数执行完成之后,存放对象的内存块中才存放这一个类的实例。上述代码的执行结果如下:
Dog():Dog类已被初始化。
myDog的名字为“未知”,叫声为:
汪汪!
显然,在创建对象时执行了构造函数。
创建类的一个实例时,在执行构造函数之前,系统会给所有未初始化字段赋一个无害的默认值(各种类型字段的默认值如表7-2所示),然后以文本顺序执行各个实例字段的初始化。
表7-2  不同类型的字段的默认值
类    型           默 认 值
 数值型(byte、int、long等)  0
 char  '\0'
bool  false
枚举  0
引用类型  null

下面的程序演示了这种性质:
// DefaultConstructedDog.cs
// 系统将为未在构造函数赋值的字段赋一个无害的值
using System;
public class Dog
{
    public string name;
    public int age;
    public void Bark()
    {
        Console.WriteLine("汪汪!");
    }
    public static void Main()
    {
        Dog myDog = new Dog();
        Console.WriteLine("myDog的名字为“{0}”,年龄为{1}。",
                          myDog.name, myDog.age);
    }
}
上述程序中,当创建对象myDog时,会调用默认的构造函数。所有字段都会被赋给一个默认的值。输出结果为:
myDog的名字为“”,年龄为0。
这种特性虽然能够避免编译错误,但是违背了变量的“先赋值、后使用”原则,这些“无害的”默认值有时却是很有害的。因此,笔者建议你尽可能地在构造函数中对所有字段赋值。

2.构造函数的参数
上面的例子中的构造函数是不带参数的,这样的构造函数以固定的方式来初始化类的实例。构造函数也可以带参数,这样就能传递不同的数据来对类的实例进行不同的初始化。
一个类可以有多个具有不同参数的构造函数。下面的例子说明了不同参数的构造函数的定义和使用方法:
// DogWithSeveralConstructors.cs
// 具有多个构造函数的类
using System;
public class Dog
{
    public string name;
    public int age;
    public Dog()             // 无参数的构造函数
    {
        name = "未知";
        age = 1;
        Console.WriteLine("Dog():Dog类已被初始化。");
    }
    public Dog(string dogName)    // 带一个参数的构造函数
    {
        name = dogName;
        age = 1;
        Console.WriteLine("Dog(string):Dog类已被初始化。");
    }
    public Dog(string, int)     // 带两个参数的构造函数
    {
        name = dogName;
        age = dogAge;
        Console.WriteLine("Dog(string, int):Dog类已被初始化。");
    }
    public void Bark()
    {
        Console.WriteLine("汪汪!");
    }
    public static void Main()
    {
        // 将使用没有参数的构造函数
          Dog dog1 = new Dog();
          Console.WriteLine("dog1的名字为“{0}”,年龄为{1}",
                    dog1.name, dog1.age);
        // 将使用一个参数的构造函数
          Dog dog2 = new Dog("飞骑将军");
          Console.WriteLine("dog2的名字为“{0}”,年龄为{1}",
                          dog2.name, dog2.age);
        // 将使用两个参数的构造函数
          Dog dog3 = new Dog("小桥流水", 2);
          Console.WriteLine("dog3的名字为“{0}”,年龄为{1}",
                          dog3.name, dog3.age);
    }
}
上述代码的执行结果为:
Dog():Dog类已被初始化。
dog1的名字为“未知”,年龄为1
Dog(string):Dog类已被初始化。
dog2的名字为“飞骑将军”,年龄为1
Dog(string, int):Dog类已被初始化。
dog3的名字为“小桥流水”,年龄为2

3.私有构造函数
如果构造函数被声明为private类型,则这个构造函数不能从类外访问,因此也不能用来在类外创建对象。
私有构造函数一般仅仅应用于只包含静态成员的类。这样的类不需要实例化,就能通过类名来引用所有的静态成员。由于通过对象反而不能引用它的静态成员,因此需要有意使它不能被实例化。只需给这样的类添加一个空的私有实例构造函数,即可达到这一目的。例如:
class Trig
{
   private Trig() {}     // 阻止被实例化
   public const double PI = 3.14159265358979323846;
   public static double Sin(double x) {...}
   public static double Cos(double x) {...}
   public static double Tan(double x) {...}
}
Trig类用于将相关的方法和常量组合在一起,但是它不能被实例化。比如,下面的代码将会产生编译错误CS0122(error CS0122: 'Trig.Trig()' is inaccessible due to its protection level):
class Prog
{
    public static void Main()
    {
        Trig t = new Trig();
    }
}

4.静态构造函数
静态构造函数(static constructor)用于实现初始化类(而不是初始化实例或对象)所需的操作。声明静态构造函数只需在构造函数名前使用关键字static。
类的静态构造函数在给定程序中至多执行一次。在程序中第一次发生以下事件时将触发静态构造函数的执行:
— 创建类的实例。
— 引用类的任何静态成员。
当初始化一个类时,在执行静态构造函数之前,首先将该类中的所有静态字段初始化为它们的默认值(参见表7-2),然后以文本顺序执行各个静态字段初始化。
如果类中包含用Main方法,那么该类的静态构造函数将在调用Main方法之前执行。如果类包含任何在声明时初始化的静态字段,则在执行该类的静态构造函数时,先要按照文本顺序执行那些初始化。比如:
// StaticConstructor.cs
// 静态构造函数的例子
using System;
class Test
{
    static void Main()
    {
        A.F();
        B.F();
    }
}
class A
{
    static A()    // 静态构造函数
    {
        Console.WriteLine("执行A的静态构造函数来初始化A");
    }
    public static void F()
    {
        Console.WriteLine("调用A.F()");
    }
}
class B
{
    static B()    // 静态构造函数
    {
        Console.WriteLine("执行B的静态构造函数来初始化B");
    }
    public static void F()
    {
        Console.WriteLine("调用B.F()");
    }
}
因为A的静态构造函数的执行是通过调用A.F()触发的,而B的静态构造函数的执行是通过调用B.F()触发的,因此,上述程序的输出结果为:
执行A的静态构造函数来初始化A
调用A.F()
执行B的静态构造函数来初始化B
调用B.F()
上述过程有可能构造出循环依赖关系,其中,在声明时初始化的静态字段将在其处于默认值状态时被应用。观察如下的程序:
// ComplexStaticConstructor.cs
// 复杂静态构造函数的例子
using System;
class A
{
    public static int X;
    static A()
    {
        X = B.Y + 1;
        Console.WriteLine("执行A的静态构造函数来初始化A");
    }
}
class B
{
    public static int Y = A.X + 1;
    static B()
    {
        Console.WriteLine("执行B的静态构造函数来初始化B");
    }
    static void Main()
    {
        Console.WriteLine("X = {0}, Y = {1}", A.X, B.Y);
    }
}
在执行Main方法之前,系统需要运行类B的静态构造函数,而这又首先要进行B.Y初始化。因为在进行B.Y初始化时引用了A.X的值,所以Y的初始化导致运行A的静态构造函数。这样,A的静态构造函数将计算A.X的值,而这又需要使用B.Y的值,只能使用B.Y的默认值0,因此A.X被初始化为1。这样就完成了运行A的静态字段初始化和静态构造函数的执行,控制返回到Y的初始值的计算,计算结果为2。上述程序的输出结果为:
执行A的静态构造函数来初始化A
执行B的静态构造函数来初始化B
X = 1, Y = 2

析构函数
析构函数(destructor)用于实现销毁类的实例所需的操作,并释放实例所占的内存。析构函数的名称由类名前面加上“~”字符构成。析构函数具有如下特征:
— 没有返回值类型(甚至也不能使用void),也不能返回值。
— 没有参数。
— 一个类只能有一个析构函数。
析构函数是自动调用的,它不能被显式调用。当某个类的实例被认为不再有效、任何代码都不再使用它时,该实例就符合被销毁的条件。此后,它所对应的实例的析构函数就随时可能被调用,但是调用时机由公共语言运行时的垃圾回收机制确定,而不是由用户程序确定。
考察如下的程序:
// Destructor.cs
// 析构函数的例子
using System;
class A
{
    public A()
    {
        Console.WriteLine("执行A的构造函数");
    }
    ~A()
    {
        Console.WriteLine("执行A的析构函数");
    }
}
class B
{
    public B()
    {
        Console.WriteLine("执行B的构造函数");
    }
    ~B()
    {
        Console.WriteLine("执行B的析构函数");
    }
    public void CreateAobject()
    {
        Console.WriteLine("进入B.CreateAobject()");
        A oA = new A();
        Console.WriteLine("退出B.CreateAobject()");
    }
    static void Main()
   {
        Console.WriteLine("进入Main()");
        B oB = new B();
        oB.CreateAobject();
        Console.WriteLine("按Enter键退出程序...");
        Console.Read();
        Console.WriteLine("退出Main()");    }
}
它的输出结果为:
进入Main()
执行B的构造函数
进入B.CreateAobject()
执行A的构造函数
退出B.CreateAobject()
按Enter键退出程序...
退出Main()
执行A的析构函数
执行B的析构函数
运行上述程序,当屏幕上打印出“按Enter键退出程序...”时,B.CreateAobject()已经执行完,类A的对象oA已经无效。但是我们没有看见oA的析构函数被执行(没有打印出“执行A的析构函数”)。等候几分钟,但是我们依然没有看见oA的析构函数被执行。只有在退出程序之后,oA的析构函数才被执行(先打印“退出Main()”,后打印“执行A的析构函数”)。这说明,类A的对象oA在退出B.CreateAobject()后就无效了,它的析构函数就随时可能被调用,当不一定会立即被调用,我们不能确定它的调用时机。
因此,C#的析构函数不会在对象无效时被立即执行。所以,如果某种资源需要在对象无效时立即被释放,那么释放这种资源的代码就不应该在析构函数中。
4.7.8. 关键字this
关键字this有两种基本的用法,一是用来进行this访问,二是在声明构造函数时指定需要先执行的构造函数。
this访问
在类的实例构造函数和实例函数成员中,关键字this表示当前的类实例或者对象的引用。this不能用在静态构造函数和静态函数成员中,也不能在其他地方使用。
当在实例构造函数或方法内使用了与字段名相同的变量名或参数名时,可以使用this来区别字段和变量或者参数。下面的代码演示了this的用法。
public class Dog
{
    public string name;
    public int age;
    public Dog()
    {
    }
    public Dog(string name)       // 在这个函数内,name是指传入的参数name
    {
        this.name = name;           // this.name表示字段name
    }
    public Dog(string name, int age) // 在这个函数内,name是指传入的参数name
    {                     // age是指传入的参数age
        this.name = name;            // this.name表示字段name
        this.age = age;           // this.age表示字段age
    }
}
实际上,this被定义为一个常量,因此,虽然在类的实例构造函数和实例函数成员中,this可以用于引用该函数成员调用所涉及的实例,但是不能对this本身赋值或者改变this的值。比如,this++,--this之类的操作都是非法的。
his用于构造函数声明
可以使用如下的形式来声明实例构造函数:
『访问修饰符』【类名】(『形式参数表』) : this(『实际参数表』)
{
   【语句块】
}
其中的this表示该类本身所声明的、形式参数表与『实际参数表』最匹配的另一个实例构造函数,这个构造函数会在执行正在声明的构造函数之前执行。
比如:
// ThisAndConstructor.cs
// 关键字this用于声明构造函数
using System;
class A
{
    public A(int n)
    {
        Console.WriteLine("A.A(int n)");
    }
    public A(string s, int n) : this(0)
    {
        Console.WriteLine("A.A(string s, int n)");
    }
}
class Test
{
    static void Main()
    {
        A a = new A("A Class", 1);
    }
}
将输出:
A.A(int n)
A.A(string s, int n)
这说明,执行构造函数A(string s, int n)之前先执行了构造函数A(int n)。
4.7.9. ref和out
参数可以通过引用和值传递给方法。通过引用传递给方法的变量可以有调用它的方法作自由改变,所作的修改会影响原来的变量的值;在C#中,除非特别说明,所有的参数都是值传递。这是默认情况,也可以使用ref关键字,迫使值参数通过引用传递给方法,则给方法对变量所作的修改都会影响原来对象的值。在定义该方法时把该参数定义为ref后,在调用该方法时,还需要添加ref关键字。
static void myFun(int x,ref int y)
{
    y = x;
}
//调用该方法
myFun(x,ref y);
有时为了从一个函数中返回多个值,我们需要使用out关键字,把输出值赋给通过引用传递给方法的变量(也就是参数)。但C#要求变量再被引用的前必须初始化。在调用该方法时,还需要添加out关键字。
static void myFun(out int y)
{
    y = 10; //在这里进行初始化
}
//
//调用该方法
public static int Main()
{
    int i;
    myFun(out i);
    Console.Writeline(i);
    return 0;
}
从上面的例子我们可以看出,out关键字和ref的不同点主要在:使用out关键字时,必须在方法体内为变量提供初始值
4.7.10. HashMap和Hashtable
HashMap是Hashtable的轻量级实现(非线程安全的实现),他们都完成了Map接口,主要区别在于HashMap允许空(null)键值(key),由于非线程安全,效率上可能高于Hashtable
4.7.11. error和exception
rror 表示恢复不是不可能但很困难的情况下的一种严重问题。比如说内存溢出。不可能指望程序能处理这样的情况。
exception 表示一种设计或实现问题。也就是说,它表示如果程序运行正常,从不会发生的情况
4.7.12. Array复4.7.13. 制到ArrayList
(1) 实现1
string[] s ={ "111", "22222" };
ArrayList list = new ArrayList();
list.AddRange(s);


(2)实现2
string[] s ={ "111", "22222" };
ArrayList list = new ArrayList(s);
4.7.14. 属性和索引器
面向对象编程的封装性原则要求不能直接访问类中的数据成员。这主要是因为:
— 如果直接访问类的数据成员,就必须充分了解类的实现细节,这有悖于隐藏设计细节的思想,会限制代码的重用性和维护性。
— 如果直接访问类的数据成员,就可能有意或无意地破坏对象中的数据,可能会导致难以调试的程序缺陷。
因此,好的做法是将数据成员的访问方式都设定为private。那么又如何访问类的数据成员呢?答案是通过特定的方法来获取和设置数据成员的值。C++和Java通过声明Get和Set方法来获取和设置数据成员的值。C#定义了一种名为属性的访问器来访问数据成员。
属性
属性是对现实世界中实体特征的抽象,提供了对类或对象性质的访问。类的属性所描述的是状态信息,在类的某个实例中,属性的值表示该对象的状态值。属性是字段的自然扩展,它们都是具有关联类型的命名成员,而且访问字段和属性的语法是相同的。然而,与字段不同,属性不表示存储位置。属性有访问器(accessor),这些访问器指定在它们的值被读取或写入时需执行的语句。因此属性提供了一种机制,它把读取和写入对象的某些性质与一些操作关联起来。它们甚至还可以对此类性质进行计算。因此,属性被称为聪明的字段。
属性的声明形式如下:
『特性』
『修饰符』【类型】 【属性名】  
{  
      『get { 【get访问器体】 }』
      『set { 【set访问器体】 }』
}
『特性』和『修饰符』都是可选的,适用于字段和方法的所有特性和修饰符都适用于属性,最常用的就是访问修饰符。也可以使用static修饰符。当属性声明包含static修饰符时,称该属性为静态属性(static property)。当不存在static修饰符时,称该属性为实例属性(instance property)。
静态属性不与特定实例相关联,因此在静态属性的访问器内引用this会导致编译时错误。
实例属性与类的一个给定实例相关联,并且可以在属性的访问器内通过this来访问该实例。
属性的类型可以是任何的预定义或者自定义类型。属性名是一种标识符,命名规则与字段相同。但是,属性名的第一个字母通常都大写。
C#中的属性通过get和set访问器来对属性的值进行读写。get和set访问器分别用关键字get和set,以及位于一对大括号内的【get访问器体】和【set访问器体】代码块构成。【get访问器体】和【set访问器体】代码块分别指定调用相应访问器时需执行的语句块。get和set访问器是可选的。
get访问器相当于一个具有属性类型返回值的无参数方法。除了作为赋值的目标,当在表达式中引用属性时,将调用该属性的get访问器以计算该属性的值。get访问器体必须用return语句来返回,并且所有的return语句都必须返回一个可隐式转换为属性类型的表达式。
set访问器相当于一个具有单个属性类型值参数和void返回类型的方法。set访问器的隐式参数始终命名为value。当一个属性作为赋值的目标,或者作为++或--运算符的操作数被引用时,就会调用set访问器,所传递的参数(其值为赋值右边的值或者++或--运算符的操作数)将提供新值。不允许set访问器体中的return语句指定表达式。由于set访问器隐式具有名为value的参数,因此在set访问器中不能自定义使用名称为value的局部变量或常量。
根据get和set访问器是否存在,属性可分成如下类型。
— 读写(read-write)属性:同时包含 get 访问器和 set 访问器的属性。
— 只读(read-only)属性:只具有 get 访问器的属性。将只读属性作为赋值目标会导致编译时错误。
— 只写(write-only)属性:只具有 set 访问器的属性。除了作为赋值的目标外,在表达式中引用只写属性会出现编译时错误。
由于属性的set访问器中可以包含大量的语句,因此可以对赋予的值进行检查,如果值不安全或者不符合要求,就可以进行提示。这样就可以避免因为给类的数据成员设置了错误的值而导致的错误。
说起来好像很复杂,其实则不然,看看实际的例子就明白了。请看下面的例子:
// Rectangle.cs
// 属性示例
using System;
public class Rectangle
{
    protected int width;
    protected int height;
    public int Width      // 读写属性
    {
        get
        {
            return width;
        }
        set
        {
            if (value > 0)
                width = value;
            else
                Console.WriteLine("Width的值不能为负数。");
        }
    }
    public int Height        // 读写属性
    {
        get
        {
            return height;
        }
        set
        {
            if (value > 0)
                height = value;
            else
                Console.WriteLine("Height的值不能为负数。");
        }
    }
    public int Area       // 只读属性
    {
        get
        {
            return width * height;
        }
    }
    public Rectangle()
    {
    }
    public Rectangle(int cx, int cy)
    {
        Width = cx;
        Height = cy;
    }
    public static void Main()
    {
        Rectangle rect = new Rectangle();
        rect.Width = 2;
        rect.Height = 4;
        Console.WriteLine("Rectangle: W = {0}, H = {1}, Area = {2}",
                          rect.Width, rect.Height, rect.Area);
        rect.Width = -2;
        rect.Height = -4;
        Console.WriteLine("Rectangle: W = {0}, H = {1}, Area = {2}",
                          rect.Width, rect.Height, rect.Area);
        Rectangle rect1 = new Rectangle(-4, -6);
        Console.WriteLine("Rectangle: W = {0}, H = {1}, Area = {2}",
                          rect1.Width, rect1.Height, rect1.Area);
    }
}
上面的例子定义了一个Rectangle类,它的高和宽被定义为protected字段,这样既保证了数据隐藏,又能满足继承它的子类的需要。为了获取和设置高和宽的值,我们分别定义了两个读写属性Height和Width。Rectangle还包含一个只读属性Area用来读取面积的值。面积是通过高和宽计算得到的,而不能被赋值,因此使用只读属性,以避免错误赋值。
在Height和Width的set访问器中检查了所赋的值是否合理,如果不合理(高和宽的值为负值),则会报错。上面的例子的输出如下:
Rectangle: W = 2, H = 4, Area = 8
Width的值不能为负数。
Height的值不能为负数。
Rectangle: W = 2, H = 4, Area = 8
Width的值不能为负数。
Height的值不能为负数。
Rectangle: W = 0, H = 0, Area = 0
可以看出,每当给Height和Width赋负值时(不论在类内还是类外),就会打印错误信息。我们在这里采用了最简单的方法来提示错误。其实更好的方法是引发一个异常,这样就能在代码中处理出现的错误。我们将在第10章介绍这种方法。
属性的引用方法与字段完全一样,因此使用非常方便。但是,应该记住,属性本质上是方法,而不是数据成员。
索引器
们可以使用索引来操作数组的元素,C#支持一种名为索引器的特殊“属性”,能够用引用数组元素的方式来引用对象。
索引器又被称为带参数的属性,它的声明方式与属性的声明十分相似。索引器的基本声明形式为:
『特性』
『修饰符』【类型】 this[【参数表】]  
{  
      『get { 【get访问器体】 }』
      『set { 【set访问器体】 }』
}
注意,这里与属性声明不同的地方在于:索引器的名称必须是关键字this,this后面一定要跟一对方括号([和]),在方括号之间指定索引的参数表,其中至少必须有一个参数;索引器不能被定义为静态成员,而只能是实例成员。其他的定义都与属性完全一样,也是通过get访问器来取值,通过set访问器来赋值。
索引器使用方式不同于属性的使用方式,需要使用元素访问运算符[]、并在其中指定参数来进行引用。
下面的示例声明了一个Group类,该类实现了一个索引器,用于访问组中的各个成员:
// Indexer.cs
// 索引器示例
using System;
class Group
{
    public const int MaxNum = 8;
    private string[] member;
    public string this[int idx]         // 定义索引器
    {
        get
        {
            return member[idx];           // 取值
        }
        set
        {
            member[idx] = value;          // 赋值
        }
    }
    public Group()
    {
        member = new string[MaxNum];
    }
    public static void Main()
    {
        Group group = new Group();
        group[0] = "张三";             // 赋值
        group[1] = "李四";
        group[2] = "王五";
        group[3] = "钱六";
        group[4] = "赵七";
        group[5] = "孙八";
        group[6] = "周九";
        group[7] = "吴十";
        for (int i = 0; i < Group.MaxNum; ++i)
            Console.WriteLine(group[i]);    // 取值
    }
}
下面的示例演示了一个矩阵类,使用了两个属性和一个带两个参数的索引器:
// Matrix.cs
// 索引器示例
using System;
class Matrix
{
    private double[,] elements;
    private int row = 1;
    private int col = 1;
    public int Row
    {
        get
        {
            return row;
        }
    }
    public int Col
    {
        get
        {
            return col;
        }
    }
    public double this[int r, int c]          // 定义索引器
    {
        get                                 // get访问器
        {
            if (r >= row || r < 0 ||
                c >= col || c < 0)              // 如果越界
            {
                throw new IndexOutOfRangeException (); // 引发一个异常
            }
            return elements[r, c];             // 取值
        }
        set                           // set访问器
        {
            if (r >= row || r < 0 ||
                c >= col || c < 0)              // 如果越界
            {
                throw new IndexOutOfRangeException(); // 引发一个异常
            }
            elements[r, c] = value;            // 赋值
        }
    }
    public Matrix()
    {
        row = 1;
        col = 1;
        elements = new double[row, col];
    }
    public Matrix(int row, int col)
    {
        this.row = row;
        this.col = col;
        elements = new double[row, col];
    }
    public static void Main()
    {
        const int ROW = 8;
        const int COL = 8;
        Matrix m = new Matrix(ROW, COL);                // 创建对象
        for (int i = 0; i < m.Row; ++i)                 // 赋值
        {
            for (int j = 0; j < m.Col; ++j)
            {
                m[i, j] = i * j;
          }
        }
        for (int i = 0; i < m.Row; ++i)                 // 打印
        {
            for (int j = 0; j < m.Col; ++j)
            {
                Console.Write("{0,4} ", m[i, j]);
            }
            Console.WriteLine();
        }
    }
}
上述程序的输出为:
0    0    0    0    0    0    0    0
0    1    2    3    4    5    6    7
0    2    4    6    8   10   12   14
0    3    6    9   12   15   18   21
0    4    8   12   16   20   24   28
0    5   10   15   20   25   30   35
0    6   12   18   24   30   36   42
0    7   14   21   28   35   42   49
4.7.15. C#修饰符
用于限定类型以及类型成员的申明,c#中有13种修饰符,按功能可分为三部分:存取修饰符,类修饰符和成员修饰符.
存取修饰符:
public:存取不受限制.
private:只有包含该成员的类可以存取.
internal:只有当前工程可以存取.
protected:只有包含该成员的类以及继承的类可以存取.
类修饰符:
abstract:可以被指示一个类只能作为其它类的基类.
sealed:指示一个类不能被继承.
成员修饰符:
abstract:指示该方法或属性没有实现.
const:指定域或局部变量的值不能被改动
event:声明一个事件.
extern:指示方法在外部实现.
override:对由基类继承成员的新实现.
readonly:指示一个域只能在声明时以及相同类的内部被赋值.
static:指示一个成员属于类型本身,而不是属于特定的对象.
virtual:指示一个方法或存取器的实现可以在继承类中被覆盖

4.7.16. final, finally, finalize
final—修饰符(关键字)如果一个类被声明为final,意味着它不能再派生出新的子类,不能作为父类被继承。因此一个类不能既被声明为 abstract的,又被声明为final的。将变量或方法声明为final,可以保证它们在使用中     不被改变。被声明为final的变量必须在声明时给定初值,而在以后的引用中只能读取,不可修改。被声明为     final的方法也同样只能使用,不能重载
finally—再异常处理时提供 finally 块来执行任何清除操作。如果抛出一个异常,那么相匹配的 catch 子句就会     执行,然后控制就会进入 finally 块(如果有的话)。
finalize—方法名。Java 技术允许使用 finalize() 方法在垃圾收集器将对象从内存中清除出去之前做必要的清理     工作。这个方法是由垃圾收集器在确定这个对象没有被引用时对这个对象调用的。它是在 Object 类中定义的     ,因此所有的类都继承了它。子类覆盖 finalize() 方法以整理系统资源或者执行其他清理工作。finalize()      方法是在垃圾收集器删除对象之前对这个对象调用的。
4.7.17. Sealed
sealed 修饰符可以应用于类、实例方法和属性。密封类不能被继承。密封方法会重写基类中的方法,但其本身不能在任何派生类中进一步重写。当应用于方法或属性时,sealed 修饰符必须始终与 override一起使用。

5. 5数据库学习
5.1. 基础理论
5.1.1. 数据库的隔离级别
ANSI/ISO SQL92标准定义了一些数据库操作的隔离级别:
未提交读(read uncommitted)
提交读(read committed)
重复读(repeatable read)
序列化(serializable)
也就是隔离级别,0,1,2,3。ANSI/ISO SQL92标准有很详细的说明,可是这个说明详细是详细,就是看不明白。今天经高人指点,茅厕顿开。
隔离级别0与事务无关,并且不加锁,也就是说例如select * from t1,系统扫描过和读取的每一行都不加锁。
隔离级别1与事务无关,只对正在取数的行加锁,取完数马上开锁,也就是说,begin tran 然后select * from t1即使没有commit,锁也会自动打开。
隔离级别2与事务有关,对扫描过的地方加锁。例如,select * from t1,系统从第1行开始扫描,扫描到第5行的时候,1到5行都处于锁定状态,直到commit,这些锁才解开。
隔离级别3与事务有关,对全表加锁。

在一个程序中,依据事务的隔离级别将会有三种情况发生。
l 脏读:一个事务会读进还没有被另一个事务提交的数据,所以你会看到一些最后被另一个事务回滚掉的数据。
l 读值不可复现:一个事务读进一条记录,另一个事务更改了这条记录并提交完毕,这时候第一个事务再次读这条记录时,它已经改变了。
l 幻影读:一个事务用Where子句来检索一个表的数据,另一个事务插入一条新的记录,并且符合Where条件,这样,第一个事务用同一个where条件来检索数据后,就会多出一条记录。
一个事务所选择的隔离级别影响着数据库怎样对待其他事务拥有的锁,你所选择的隔离级别依赖于你的系统和商务逻辑。例如,一个银行在客户取钱之前会检查它的帐户余额,这种情况下,就需要一个隔离级别为可序列化的事务,这样另外一个取钱动作在这次完成之前将不能执行。如果它们仅需要提供一个帐户余额,“读提交的“将是合适的级别,因为他们仅需要查询余额的值,级别的降低会使事务运行更快。

5.1.2. Transaction执行时是否可以共享读

5.1.3. 数据库范式
第一范式(1NF)
    在任何一个关系数据库中,第一范式(1NF)是对关系模式的基本要求,不满足第一范式(1NF)的数据库就不是关系数据库。
    所谓第一范式(1NF)是指数据库表的每一列都是不可分割的基本数据项,同一列中不能有多个值,即实体中的某个属性不能有多个值或者不能有重复的属性。如果出现重复的属性,就可能需要定义一个新的实体,新的实体由重复的属性构成,新实体与原实体之间为一对多关系。在第一范式(1NF)中表的每一行只包含一个实例的信息。例如,对于图3-2 中的员工信息表,不能将员工信息都放在一列中显示,也不能将其中的两列或多列在一列中显示;员工信息表的每一行只表示一个员工的信息,一个员工的信息在表中只出现一次。简而言之,第一范式就是无重复的列
第二范式(2NF)
    第二范式(2NF)是在第一范式(1NF)的基础上建立起来的,即满足第二范式(2NF)必须先满足第一范式(1NF)。第二范式(2NF)要求数据库表中的每个实例或行必须可以被惟一地区分。为实现区分通常需要为表加上一个列,以存储各个实例的惟一标识。如
图3-2 员工信息表中加上了员工编号(emp_id)列,因为每个员工的员工编号是惟一的,因此每个员工可以被惟一区分。这个惟一属性列被称为主关键字或主键、主码。
    第二范式(2NF)要求实体的属性完全依赖于主关键字。所谓完全依赖是指不能存在仅依赖主关键字一部分的属性,如果存在,那么这个属性和主关键字的这一部分应该分离出来形成一个新的实体,新实体与原实体之间是一对多的关系。为实现区分通常需要为表加上一个列,以存储各个实例的惟一标识。简而言之,第二范式就是非主属性非部分依赖于主关键字。
第三范式(3NF)
    满足第三范式(3NF)必须先满足第二范式(2NF)。简而言之,第三范式(3NF)要求一个数据库表中不包含已在其它表中已包含的非主关键字信息。例如,存在一个部门信息表,其中每个部门有部门编号(dept_id)、部门名称、部门简介等信息。那么在图3-2
的员工信息表中列出部门编号后就不能再将部门名称、部门简介等与部门有关的信息再加入员工信息表中。如果不存在部门信息表,则根据第三范式(3NF)也应该构建它,否则就会有大量的数据冗余。简而言之,第三范式就是属性不依赖于其它非主属性。

第四范式
第四范式禁止主键列和非主键列一对多关系不受约束
第五范式
第五范式将表分割成尽可能小的块,为了排除在表中所有的冗余.

5.1.4. 索引
索引是一个数据结构,用来快速访问数据库表格或者视图里的数据。在SQL Server里,它们有两种形式:聚集索引和非聚集索引。聚集索引在索引的叶级保存数据。这意味着不论聚集索引里有表格的哪个(或哪些)字段,这些字段都会按顺序被保存在表格。由于存在这种排序,所以每个表格只会有一个聚集索引。非聚集索引在索引的叶级有一个行标识符。这个行标识符是一个指向磁盘上数据的指针。它允许每个表格有多个非聚集索引。
5.1.5. NULL
NULL这个值表示UNKNOWN(未知):它不表示“”(空字符串)。假设您的SQL Server数据库里有ANSI_NULLS,当然在默认情况下会有,对NULL这个值的任何比较都会生产一个NULL值。您不能把任何值与一个 UNKNOWN值进行比较,并在逻辑上希望获得一个答案。您必须使用IS NULL操作符。
5.1.6. 主键与外键
主键是表格里的(一个或多个)字段,只用来定义表格里的行;主键里的值总是唯一的。外键是一个用来建立两个表格之间关系的约束。这种关系一般都涉及一个表格里的主键字段与另外一个表格(尽管可能是同一个表格)里的一系列相连的字段。那么这些相连的字段就是外键。 
5.1.7. 触发器
触发器是一种专用类型的存储过程,它被捆绑到SQL Server 2000的表格或者视图上。在SQL Server 2000里,有INSTEAD-OF和AFTER两种触发器。INSTEAD-OF触发器是替代数据操控语言(Data Manipulation Language,DML)语句对表格执行语句的存储过程。例如,如果我有一个用于TableA的INSTEAD-OF-UPDATE触发器,同时对这个表格执行一个更新语句,那么INSTEAD-OF-UPDATE触发器里的代码会执行,而不是我执行的更新语句则不会执行操作。 
AFTER触发器要在DML语句在数据库里使用之后才执行。这些类型的触发器对于监视发生在数据库表格里的数据变化十分好用。 
5.1.8. Check限制
确保表格里的字段只接受特定范围里的值
5.1.9. 返回参数和OUTPUT参数
返回参数总是由存储过程返回,它用来表示存储过程是成功还是失败。返回参数总是INT数据类型。 
OUTPUT参数明确要求由开发人员来指定,它可以返回其他类型的数据,例如字符型和数值型的值。(可以用作输出参数的数据类型是有一些限制的。)您可以在一个存储过程里使用多个OUTPUT参数,而您只能够使用一个返回参数。
5.1.10. 相关子查询
相关子查询是一种包含子查询的特殊类型的查询。查询里包含的子查询会真正请求外部查询的值,从而形成一个类似于循环的状况。
5.1.11. 获得最后更新的事务号
给定表 table1 中有两个字段 ID、LastUpdateDate,ID表示更新的事务号, LastUpdateDate表示更新时的服务器时间
Select ID FROM table1 Where LastUpdateDate = (Select MAX(LastUpdateDate) FROM table1)
Select @@Identity

5.1.12. 取出表A中第31到第40记录
1:  select top 10 * from A where id not in (select top 30 id from A) 
2:  select top 10 * from A where id > (select max(id) from (select top 30 id from A as A)
5.1.13. 如何处理几十万5.1.14. 条并发数据
用存储过程或事务。取得最大标识的时候同时更新..注意主键不是自增量方式这种方法并发的时候是不会有重复主键的..取得最大标识要有一个存储过程来获取.
5.1.15. SQL注入
利用sql关键字对网站进行攻击。过滤关键字'等
6. 项目管理
6.1. 软件开发模型
6.1.1. 边做边改模型(Build-and-Fix Model)
  遗憾的是,许多产品都是使用"边做边改"模型来开发的。在这种模型中,既没有规格说明,也没有经过设计,软件随着客户的需要一次又一次地不断被修改.
    在这个模型中,开发人员拿到项目立即根据需求编写程序,调试通过后生成软件的第一个版本。在提供给用户使用后,如果程序出现错误,或者用户提出新的要求,开发人员重新修改代码,直到用户满意为止。
  这是一种类似作坊的开发方式,对编写几百行的小程序来说还不错,但这种方法对任何规模的开发来说都是不能令人满意的,其主要问题在于:
  (1) 缺少规划和设计环节,软件的结构随着不断的修改越来越糟,导致无法继续修改;
  (2) 忽略需求环节,给软件开发带来很大的风险;
  (3) 没有考虑测试和程序的可维护性,也没有任何文档,软件的维护十分困难。
6.1.2. 瀑布模型(Waterfall Model)
     1970年Winston Royce提出了著名的"瀑布模型",直到80年代早期,它一直是唯一被广泛采用的软件开发模型。
   瀑布模型将软件生命周期划分为制定计划、需求分析、软件设计、程序编写、软件测试和运行维护等六个基本活动,并且规定了它们自上而下、相互衔接的固定次序,如同瀑布流水,逐级下落。
     在瀑布模型中,软件开发的各项活动严格按照线性方式进行,当前活动接受上一项活动的工作结果,实施完成所需的工作内容。当前活动的工作结果需要进行验证,如果验证通过,则该结果作为下一项活动的输入,继续进行下一项活动,否则返回修改。
   瀑布模型强调文档的作用,并要求每个阶段都要仔细验证。但是,这种模型的线性过程太理想化,已不再适合现代的软件开发模式,几乎被业界抛弃,其主要问题在于:
  (1)各个阶段的划分完全固定,阶段之间产生大量的文档,极大地增加了工作量;
  (2)由于开发模型是线性的,用户只有等到整个过程的末期才能见到开发成果,从而增加了开发的风险;
  (3)早期的错误可能要等到开发后期的测试阶段才能发现,进而带来严重的后果。
  我们应该认识到,"线性"是人们最容易掌握并能熟练应用的思想方法。当人们碰到一个复杂的"非线性"问题时,总是千方百计地将其分解或转化为一系列简单的线性问题,然后逐个解决。一个软件系统的整体可能是复杂的,而单个子程序总是简单的,可以用线性的方式来实现,否则干活就太累了。线性是一种简洁,简洁就是美。当我们领会了线性的精神,就不要再呆板地套用线性模型的外表,而应该用活它。例如增量模型实质就是分段的线性模型,螺旋模型则是接连的弯曲了的线性模型,在其它模型中也能够找到线性模型的影子。
6.1.3. 快速原型模型(Rapid Prototype Model)
  快速原型模型的第一步是建造一个快速原型,实现客户或未来的用户与系统的交互,用户或客户对原型进行评价,进一步细化待开发软件的需求。通过逐步调整原型使其满足客户的要求,开发人员可以确定客户的真正需求是什么;第二步则在第一步的基础上开发客户满意的软件产品。
  显然,快速原型方法可以克服瀑布模型的缺点,减少由于软件需求不明确带来的开发风险,具有显著的效果。
  快速原型的关键在于尽可能快速地建造出软件原型,一旦确定了客户的真正需求,所建造的原型将被丢弃。因此,原型系统的内部结构并不重要,重要的是必须迅速建立原型,随之迅速修改原型,以反映客户的需求。
6.1.4. 增量模型(Incremental Model)
  与建造大厦相同,软件也是一步一步建造起来的。在增量模型中,软件被作为一系列的增量构件来设计、实现、集成和测试,每一个构件是由多种相互作用的模块所形成的提供特定功能的代码片段构成.
  增量模型在各个阶段并不交付一个可运行的完整产品,而是交付满足客户需求的一个子集的可运行产品。整个产品被分解成若干个构件,开发人员逐个构件地交付产品,这样做的好处是软件开发可以较好地适应变化,客户可以不断地看到所开发的软件,从而降低开发风险。但是,增量模型也存在以下缺陷:
  (1) 由于各个构件是逐渐并入已有的软件体系结构中的,所以加入构件必须不破坏已构造好的系统部分,这需要软件具备开放式的体系结构。
  (2) 在开发过程中,需求的变化是不可避免的。增量模型的灵活性可以使其适应这种变化的能力大大优于瀑布模型和快速原型模型,但也很容易退化为边做边改模型,从而是软件过程的控制失去整体性。
    在使用增量模型时,第一个增量往往是实现基本需求的核心产品。核心产品交付用户使用后,经过评价形成下一个增量的开发计划,它包括对核心产品的修改和一些新功能的发布。这个过程在每个增量发布后不断重复,直到产生最终的完善产品。
  例如,使用增量模型开发字处理软件。可以考虑,第一个增量发布基本的文件管理、编辑和文档生成功能,第二个增量发布更加完善的编辑和文档生成功能,第三个增量实现拼写和文法检查功能,第四个增量完成高级的页面布局功能。
6.1.5. 螺旋模型(Spiral Model)
  1988年,Barry Boehm正式发表了软件系统开发的"螺旋模型",它将瀑布模型和快速原型模型结合起来,强调了其他模型所忽视的风险分析,特别适合于大型复杂的系统。
  螺旋模型沿着螺线进行若干次迭代,图中的四个象限代表了以下活动:
  (1) 制定计划:确定软件目标,选定实施方案,弄清项目开发的限制条件;
  (2) 风险分析:分析评估所选方案,考虑如何识别和消除风险;
  (3) 实施工程:实施软件开发和验证;
  (4) 客户评估:评价开发工作,提出修正建议,制定下一步计划。
  螺旋模型由风险驱动,强调可选方案和约束条件从而支持软件的重用,有助于将软件质量作为特殊目标融入产品开发之中。但是,螺旋模型也有一定的限制条件,具体如下:
  (1) 螺旋模型强调风险分析,但要求许多客户接受和相信这种分析,并做出相关反应是不容易的,因此,这种模型往往适应于内部的大规模软件开发。
  (2) 如果执行风险分析将大大影响项目的利润,那么进行风险分析毫无意义,因此,螺旋模型只适合于大规模软件项目。
  (3) 软件开发人员应该擅长寻找可能的风险,准确地分析风险,否则将会带来更大的风险
     一个阶段首先是确定该阶段的目标,完成这些目标的选择方案及其约束条件,然后从风险角度分析方案的开发策略,努力排除各种潜在的风险,有时需要通过建造原型来完成。如果某些风险不能排除,该方案立即终止,否则启动下一个开发步骤。最后,评价该阶段的结果,并设计下一个阶段。
6.1.6. 演化模型(incremental model)
      主要针对事先不能完整定义需求的软件开发。用户可以给出待开发系统的核心需求,并且当看到核心需求实现后,能够有效地提出反馈,以支持系统的最终设计和实现。软件开发人员根据用户的需求,首先开发核心系统。当该核心系统投入运行后,用户试用之,完成他们的工作,并提出精化系统、增强系统能力的需求。软件开发人员根据用户的反馈,实施开发的迭代过程。第一迭代过程均由需求、设计、编码、测试、集成等阶段组成,为整个系统增加一个可定义的、可管理的子集。
      在开发模式上采取分批循环开发的办法,每循环开发一部分的功能,它们成为这个产品的原型的新增功能。于是,设计就不断地演化出新的系统。 实际上,这个模型可看作是重复执行的多个“瀑布模型”。
      “演化模型”要求开发人员有能力把项目的产品需求分解为不同组,以便分批循环开发。这种分组并不是绝对随意性的,而是要根据功能的重要性及对总体设计的基础结构的影响而作出判断。有经验指出,每个开发循环以六周到八周为适当的长度。
6.1.7. 喷泉模型(fountain model, (面向对象的生存期模型, OO模型))
      喷泉模型与传统的结构化生存期比较,具有更多的增量和迭代性质,生存期的各个阶段可以相互重叠和多次反复,而且在项目的整个生存期中还可以嵌入子生存期。就像水喷上去又可以落下来,可以落在中间,也可以落在最底部。
6.1.8. 智能模型(四代技术(4GL))
      智能模型拥有一组工具(如数据查询、报表生成、数据处理、屏幕定义、代码生成、高层图形功能及电子表格等),每个工具都能使开发人员在高层次上定义软件的某些特性,并把开发人员定义的这些软件自动地生成为源代码。这种方法需要四代语言(4GL)的支持。4GL不同于三代语言,其主要特征是用户界面极端友好,即使没有受过训练的非专业程序员,也能用它编写程序;它是一种声明式、交互式和非过程性编程语言。4GL还具有高效的程序代码、智能缺省假设、完备的数据库和应用程序生成器。目前市场上流行的4GL(如Foxpro等)都不同程度地具有上述特征。但4GL目前主要限于事务信息系统的中、小型应用程序的开发。
6.1.9. 混合模型(hybrid model)
      过程开发模型又叫混合模型(hybrid model),或元模型(meta-model),把几种不同模型组合成一种混合模型,它允许一个项目能沿着最有效的路径发展,这就是过程开发模型(或混合模型)。实际上,一些软件开发单位都是使用几种不同的开发方法组成他们自己的混合模型。
6.2. 软件开发流程
6.2.1. 文档管理
 
 
 

6.2.2. 角色管理
 
6.2.3. 流程图
 
 
6.3. RUP
RUP(Rational Unified Process,统一软件开发过程,统一软件过程)是一个面向对象且基于网络的程序开发方法论。根据Rational(Rational Rose和统一建模语言的开发者)的说法,好像一个在线的指导者,它可以为所有方面和层次的程序开发提供指导方针,模版以及事例支持。 RUP和类似的产品--例如面向对象的软件过程(OOSP),以及OPEN Process都是理解性的软件工程工具--把开发中面向过程的方面(例如定义的阶段,技术和实践)和其他开发的组件(例如文档,模型,手册以及代码等等)整合在一个统一的框架内。
6.3.1. 六大经验
迭代式开发。在软件开发的早期阶段就想完全、准确的捕获用户的需求几乎是不可能的。实际上,我们经常遇到的问题是需求在整个软件开发工程中经常会改变。迭代式开发允许在每次迭代过程中需求可能有变化,通过不断细化来加深对问题的理解。迭代式开发不仅可以降低项目的风险,而且每个迭代过程以可以执行版本结束,可以鼓舞开发人员。
管理需求。确定系统的需求是一个连续的过程,开发人员在开发系统之前不可能完全详细的说明一个系统的真正需求。RUP描述了如何提取、组织系统的功能和约束条件并将其文档化,用例和脚本的使用以被证明是捕获功能性需求的有效方法。
基于组件的体系结构。组件使重用成为可能,系统可以由组件组成。基于独立的、可替换的、模块化组件的体系结构有助于管理复杂性,提高重用率。RUP描述了如何设计一个有弹性的、能适应变化的、易于理解的、有助于重用的软件体系结构。
可视化建模。RUP往往和UML联系在一起,对软件系统建立可视化模型帮助人们提供管理软件复杂性的能力。RUP告诉我们如何可视化的对软件系统建模,获取有关体系结构于组件的结构和行为信息。
 验证软件质量。在RUP中软件质量评估不再是事后进行或单独小组进行的分离活动,而是内建于过程中的所有活动,这样可以及早发现软件中的缺陷。
控制软件变更。迭代式开发中如果没有严格的控制和协调,整个软件开发过程很快就陷入混乱之中,RUP描述了如何控制、跟踪、监控、修改以确保成功的迭代开发。RUP通过软件开发过程中的制品,隔离来自其他工作空间的变更,以此为每个开发人员建立安全的工作空间。
6.3.2. 统一软件开发过程RUP的二维开发模型
RUP软件开发生命周期是一个二维的软件开发模型。横轴通过时间组织,是过程展开的生命周期特征,体现开发过程的动态结构,用来描述它的术语主要包括周期(Cycle)、阶段(Phase)、迭代(Iteration)和里程碑(Milestone);纵轴以内容来组织为自然的逻辑活动,体现开发过程的静态结构,用来描述它的术语主要包括活动(Activity)、产物(Artifact)、工作者(Worker)和工作流(Workflow)。
6.3.3. 统一软件开发过程RUP核心概念
      RUP中定义了一些核心概念,
      角色:描述某个人或者一个小组的行为与职责。RUP预先定义了很多角色。
      活动:是一个有明确目的的独立工作单元。
      工件:是活动生成、创建或修改的一段信息。
6.3.4. 统一软件开发过程RUP裁剪
      RUP是一个通用的过程模板,包含了很多开发指南、制品、开发过程所涉及到的角色说明,由于它非常庞大所以对具体的开发机构和项目,用RUP时还要做裁剪,也就是要对RUP进行配置。RUP就像一个元过程,通过对RUP进行裁剪可以得到很多不同的开发过程,这些软件开发过程可以看作RUP的具体实例。RUP裁剪可以分为以下几步:
1) 确定本项目需要哪些工作流。RUP的9个核心工作流并不总是需要的,可以取舍。
2) 确定每个工作流需要哪些制品。
3) 确定4个阶段之间如何演进。确定阶段间演进要以风险控制为原则,决定每个阶段要那些工作流,每个工作流执行到什么程度,制品有那些,每个制品完成到什么程度。
4) 确定每个阶段内的迭代计划。规划RUP的4个阶段中每次迭代开发的内容。
5) 规划工作流内部结构。工作流涉及角色、活动及制品,他的复杂程度与项目规模即角色多少有关。最后规划工作流的内部结构,通常用活动图的形式给出。
6.3.5. 开发过程中的各个阶段和里程碑
  RUP中的软件生命周期在时间上被分解为四个顺序的阶段,分别是:初始阶段(Inception)、细化阶段(Elaboration)、构造阶段(Construction)和交付阶段(Transition)。每个阶段结束于一个主要的里程碑(Major Milestones);每个阶段本质上是两个里程碑之间的时间跨度。在每个阶段的结尾执行一次评估以确定这个阶段的目标是否已经满足。如果评估结果令人满意的话,可以允许项目进入下一个阶段。
1. 初始阶段
  初始阶段的目标是为系统建立商业案例并确定项目的边界。为了达到该目的必须识别所有与系统交互的外部实体,在较高层次上定义交互的特性。本阶段具有非常重要的意义,在这个阶段中所关注的是整个项目进行中的业务和需求方面的主要风险。对于建立在原有系统基础上的开发项目来讲,初始阶段可能很短。 初始阶段结束时是第一个重要的里程碑:生命周期目标(Lifecycle Objective)里程碑。生命周期目标里程碑评价项目基本的生存能力。
2. 细化阶段
  细化阶段的目标是分析问题领域,建立健全的体系结构基础,编制项目计划,淘汰项目中最高风险的元素。为了达到该目的,必须在理解整个系统的基础上,对体系结构作出决策,包括其范围、主要功能和诸如性能等非功能需求。同时为项目建立支持环境,包括创建开发案例,创建模板、准则并准备工具。 细化阶段结束时第二个重要的里程碑:生命周期结构(Lifecycle Architecture)里程碑。生命周期结构里程碑为系统的结构建立了管理基准并使项目小组能够在构建阶段中进行衡量。此刻,要检验详细的系统目标和范围、结构的选择以及主要风险的解决方案。
3. 构造阶段
  在构建阶段,所有剩余的构件和应用程序功能被开发并集成为产品,所有的功能被详细测试。从某种意义上说,构建阶段是一个制造过程,其重点放在管理资源及控制运作以优化成本、进度和质量。 构建阶段结束时是第三个重要的里程碑:初始功能(Initial Operational)里程碑。初始功能里程碑决定了产品是否可以在测试环境中进行部署。此刻,要确定软件、环境、用户是否可以开始系统的运作。此时的产品版本也常被称为“beta”版。
4. 交付阶段
  交付阶段的重点是确保软件对最终用户是可用的。交付阶段可以跨越几次迭代,包括为发布做准备的产品测试,基于用户反馈的少量的调整。在生命周期的这一点上,用户反馈应主要集中在产品调整,设置、安装和可用性问题,所有主要的结构问题应该已经在项目生命周期的早期阶段解决了。 在交付阶段的终点是第四个里程碑:产品发布(Product Release)里程碑。此时,要确定目标是否实现,是否应该开始另一个开发周期。在一些情况下这个里程碑可能与下一个周期的初始阶段的结束重合。
6.3.6. 统一软件开发过程RUP的核心工作流
  RUP中有9个核心工作流,分为6个核心过程工作流(Core Process Workflows)和3个核心支持工作流(Core Supporting Workflows)。尽管6个核心过程工作流可能使人想起传统瀑布模型中的几个阶段,但应注意迭代过程中的阶段是完全不同的,这些工作流在整个生命周期中一次又一次被访问。9个核心工作流在项目中轮流被使用,在每一次迭代中以不同的重点和强度重复。
1. 商业建模(Business Modeling)
      商业建模工作流描述了如何为新的目标组织开发一个构想,并基于这个构想在商业用例模型和商业对象模型中定义组织的过程,角色和责任。
2. 需求(Requirements)
  需求工作流的目标是描述系统应该做什么,并使开发人员和用户就这一描述达成共识。为了达到该目标,要对需要的功能和约束进行提取、组织、文档化;最重要的是理解系统所解决问题的定义和范围。
3. 分析和设计(Analysis & Design)
  分析和设计工作流将需求转化成未来系统的设计,为系统开发一个健壮的结构并调整设计使其与实现环境相匹配,优化其性能。分析设计的结果是一个设计模型和一个可选的分析模型。设计模型是源代码的抽象,由设计类和一些描述组成。设计类被组织成具有良好接口的设计包(Package)和设计子系统(Subsystem),而描述则体现了类的对象如何协同工作实现用例的功能。 设计活动以体系结构设计为中心,体系结构由若干结构视图来表达,结构视图是整个设计的抽象和简化,该视图中省略了一些细节,使重要的特点体现得更加清晰。体系结构不仅仅是良好设计模型的承载媒介,而且在系统的开发中能提高被创建模型的质量。
4. 实现(Implementation)
  实现工作流的目的包括以层次化的子系统形式定义代码的组织结构;以组件的形式(源文件、二进制文件、可执行文件)实现类和对象;将开发出的组件作为单元进行测试以及集成由单个开发者(或小组)所产生的结果,使其成为可执行的系统。
5. 测试(Test)
测试工作流要验证对象间的交互作用,验证软件中所有组件的正确集成,检验所有的需求已被正确的实现, 识别并确  认缺陷在软件部署之前被提出并处理。RUP提出了迭代的方法,意味着在整个项目中进行测试,从而尽可能早地发现缺陷,从根本上降低了修改缺陷的成本。测试类似于三维模型,分别从可靠性、功能性和系统性能来进行。
6. 部署(Deployment)
  部署工作流的目的是成功的生成版本并将软件分发给最终用户。部署工作流描述了那些与确保软件产品对最终用户具有可用性相关的活动,包括:软件打包、生成软件本身以外的产品、安装软件、为用户提供帮助。在有些情况下,还可能包括计划和进行beta测试版、移植现有的软件和数据以及正式验收。
7. 配置和变更管理(Configuration & Change Management)
  配置和变更管理工作流描绘了如何在多个成员组成的项目中控制大量的产物。配置和变更管理工作流提供了准则来管理演化系统中的多个变体,跟踪软件创建过程中的版本。工作流描述了如何管理并行开发、分布式开发、如何自动化创建工程。同时也阐述了对产品修改原因、时间、人员保持审计记录。
8. 项目管理(Project Management)
  软件项目管理平衡各种可能产生冲突的目标,管理风险,克服各种约束并成功交付使用户满意的产品。其目标包括:为项目的管理提供框架,为计划、人员配备、执行和监控项目提供实用的准则,为管理风险提供框架等。
9. 环境(Environment)
  环境工作流的目的是向软件开发组织提供软件开发环境,包括过程和工具。环境工作流集中于配置项目过程中所需要的活动,同样也支持开发项目规范的活动,提供了逐步的指导手册并介绍了如何在组织中实现过程。
6.3.7. RUP的迭代开发模式
RUP中的每个阶段可以进一步分解为迭代。一个迭代是一个完整的开发循环,产生一个可执行的产品版本,是最终产品的一个子集,它增量式地发展,从一个迭代过程到另一个迭代过程到成为最终的系统。 传统上的项目组织是顺序通过每个工作流,每个工作流只有一次,也就是我们熟悉的瀑布生命周期。这样做的结果是到实现末期产品完成并开始测试,在分析、设计和实现阶段所遗留的隐藏问题会大量出现,项目可能要停止并开始一个漫长的错误修正周期。
  一种更灵活,风险更小的方法是多次通过不同的开发工作流,这样可以更好的理解需求,构造一个健壮的体系结构,并最终交付一系列逐步完成的版本。这叫做一个迭代生命周期。在工作流中的每一次顺序的通过称为一次迭代。软件生命周期是迭代的连续,通过它,软件是增量的开发。一次迭代包括了生成一个可执行版本的开发活动,还有使用这个版本所必需的其他辅助成分,如版本描述、用户文档等。因此一个开发迭代在某种意义上是在所有工作流中的一次完整的经过,这些工作流至少包括:需求工作流、分析和设计工作流、实现工作流、测试工作流。其本身就像一个小型的瀑布项目。
与传统的瀑布模型相比较,迭代过程具有以下优点:
  降低了在一个增量上的开支风险。如果开发人员重复某个迭代,那么损失只是这一个开发有误的迭代的花费。
  降低了产品无法按照既定进度进入市场的风险。通过在开发早期就确定风险,可以尽早来解决而不至于在开发后期匆匆忙忙。
  加快了整个开发工作的进度。因为开发人员清楚问题的焦点所在,他们的工作会更有效率。
  由于用户的需求并不能在一开始就作出完全的界定,它们通常是在后续阶段中不断细化的。因此,迭代过程这种模式使适应需求的变化会更容易些。
6.3.8. 统一软件开发过程RUP的十大要素
1. 开发前景
2. 达成计划
3. 标识和减小风险
4. 分配和跟踪任务
5. 检查商业理由
6. 设计组件构架
7. 对产品进行增量式的构建和测试
8. 验证和评价结果
9. 管理和控制变化
10. 提供用户支持
让我们逐一的审视这些要素,看一看它们什么地方适合RUP,找出它们能够成为十大要素的理由。
1. 开发一个前景
      有一个清晰的前景是开发一个满足涉众真正需求的产品的关键。 前景抓住了RUP需求流程的要点:分析问题,理解涉众需求,定义系统,当需求变化时管理需求。 前景给更详细的技术需求提供了一个高层的、有时候是合同式的基础。正像这个术语隐含的那样,它是软件项目的一个清晰的、通常是高层的视图,能被过程中任何决策者或者实施者借用。它捕获了非常高层的需求和设计约束,让前景的读者能理解将要开发的系统。它还提供了项目审批流程的输入,因此就与商业理由密切相关。最后,由于前景构成了“项目是什么?”和“为什么要进行这个项目?”,所以可以把前景作为验证将来决策的方式之一。 对前景的陈述应该能回答以下问题,需要的话这些问题还可以分成更小、更详细的问题: ? 关键术语是什么?(词汇表) ? 我们尝试解决的问题是什么?(问题陈述) ? 涉众是谁?用户是谁?他们各自的需求是什么? ? 产品的特性是什么? ? 功能性需求是什么?(Use Cases) ? 非功能性需求是什么? ? 设计约束是什么?
2. 达成计划
        “产品的质量只会和产品的计划一样好。” (2) 在RUP中,软件开发计划(SDP)综合了管理项目所需的各种信息,也许会包括一些在先启阶段开发的单独的内容。SDP必须在整个项目中被维护和更新。 SDP定义了项目时间表(包括项目计划和迭代计划)和资源需求(资源和工具),可以根据项目进度表来跟踪项目进展。同时也指导了其他过程内容(原文:process components)的计划:项目组织、需求管理计划、配置管理计划、问题解决计划、QA计划、测试计划、评估计划以及产品验收计划。
      在较简单的项目中,对这些计划的陈述可能只有一两句话。比如,配置管理计划可以简单的这样陈述:每天结束时,项目目录的内容将会被压缩成ZIP包,拷贝到一个ZIP磁盘中,加上日期和版本标签,放到中央档案柜中。 软件开发计划的格式远远没有计划活动本身以及驱动这些活动的思想重要。正如Dwight D.Eisenhower所说:“plan什么也不是,planning才是一切。” “达成计划”—和列表中第3、4、5、8条一起—抓住了RUP中项目管理流程的要点。项目管理流程包括以下活动:构思项目、评估项目规模和风险、监测与控制项目、计划和评估每个迭代和阶段。
3. 标识和减小风险
      RUP的要点之一是在项目早期就标识并处理最大的风险。项目组标识的每一个风险都应该有一个相应的缓解或解决计划。风险列表应该既作为项目活动的计划工具,又作为确定迭代的基础。
4. 分配和跟踪任务
      有一点在任何项目中都是重要的,即连续的分析来源于正在进行的活动和进化的产品的客观数据。在RUP中,定期的项目状态评估提供了讲述、交流和解决管理问题、技术问题以及项目风险的机制。团队一旦发现了这些障碍物(篱笆),他们就把所有这些问题都指定一个负责人,并指定解决日期。进度应该定期跟踪,如有必要,更新应该被发布。(原文:updates should be issued as necessary。) 这些项目“快照”突出了需要引起管理注意的问题。随着时间的变化/虽然周期可能会变化(原文:While the period may vary。),定期的评估使经理能捕获项目的历史,并且消除任何限制进度的障碍或瓶颈。
5. 检查商业理由
      商业理由从商业的角度提供了必要的信息,以决定一个项目是否值得投资。商业理由还可以帮助开发一个实现项目前景所需的经济计划。它提供了进行项目的理由,并建立经济约束。当项目继续时,分析人员用商业理由来正确的估算投资回报率(ROI,即return on investment)。 商业理由应该给项目创建一个简短但是引人注目的理由,而不是深入研究问题的细节,以使所有项目成员容易理解和记住它。在关键里程碑处,经理应该回顾商业理由,计算实际的花费、预计的回报,决定项目是否继续进行。
6. 设计组件构架
      在RUP中,件系统的构架是指一个系统关键部件的组织或结构,部件
之间通过接口交互,而部件是由一些更小的部件和接口组成的。即主要的部分是什么?他们又是怎样结合在一起的? RUP提供了一种设计、开发、验证构架的很系统的方法。在分析和设计流程中包括以下步骤:定义候选构架、精化构架、分析行为(用例分析)、设计组件。 要陈述和讨论软件构架,你必须先创建一个构架表示方式,以便描述构架的重要方面。在RUP中,构架表示由软件构架文档捕获,它给构架提供了多个视图。每个视图都描述了某一组涉众所关心的正在进行的系统的某个方面。涉众有最终用户、设计人员、经理、系统工程师、系统管理员,等等。这个文档使系统构架师和其他项目组成员能就与构架相关的重大决策进行有效的交流。
7. 对产品进行增量式的构建和测试
      在RUP中实现和测试流程的要点是在整个项目生命周期中增量的编码、构建、测试系统组件,在先启之后每个迭代结束时生成可执行版本。在精化阶段后期,已经有了一个可用于评估的构架原型;如有必 要,它可以包括一个用户界面原型。然后,在构建阶段的每次迭代中,组件不断的被集成到可执行、经过测试的版本中,不断地向最终产品进化。动态及时的配置管理和复审活动也是这个基本过程元素(原文:essential process element)的关键。
8. 验证和评价结果
      顾名思义,RUP的迭代评估捕获了迭代的结果。评估决定了迭代满足评价标准的程度,还包括学到的教训和实施的过程改进。 根据项目的规模和风险以及迭代的特点,评估可以是对演示及其结果的一条简单的纪录,也可能是一个完整的、正式的测试复审记录。 这儿的关键是既关注过程问题又关注产品问题。越早发现问题,就越没有问题。(原文:The sooner you fall behind, the more time you will have to catch up.)
9. 管理和控制变化
      RUP的配置和变更管理流程的要点是当变化发生时管理和控制项目的规模,并且贯穿整个生命周期。其目的是考虑所有的涉众需求,尽可能的满足,同时仍能及时的交付合格的产品。 用户拿到产品的第一个原型后(往往在这之前就会要求变更),他们会要求变更。重要的是,变更的提出和管理过程始终保持一致。 在RUP中,变更请求通常用于记录和跟踪缺陷和增强功能的要求,或者对产品提出的任何其他类型的变更请求。变更请求提供了相应的手段来评估一个变更的潜在影响,同时记录就这些变更所作出的决策。他们也帮助确保所有的项目组成员都能理解变更的潜在影响。
10. 提供用户支持
      在RUP中,部署流程的要点是包装和交付产品,同时交付有助于最终用户学习、使用和维护产品的任何必要的材料。 项目组至少要给用户提供一个用户指南(也许是通过联机帮助的方式提供),可能还有一个安装指南和版本发布说明。 根据产品的复杂度,用户也许还需要相应的培训材料。最后,通过一个材料清单(BOM表,即Bill of Materials)清楚地记录应该和产品一起交付哪些材料。 关于需求 有人看了我的要素清单后,可能会非常不同意我的选择。例如,他会问,需求在哪儿呢?他们不重要吗?我会告诉他我为什么没有把它们包括进来。有时,我会问一个项目组(特别是内部项目的项目组):“你们的需求是什么?”,而得到的回答却是:“我们的确没有什么需求。” 刚开始我对此非常惊讶(我有军方的宇航开发背景)。他们怎么会没有需求呢?当我进一步询问时,我发现,对他们来说,需求意味着一套外部提出的强制性的陈述,要求他们必须怎么样,否则项目验收就不能通过。但是他们的确没有得到这样的陈述。尤其是当项目组陷入了边研究边开发的境地时,产品需求从头到尾都在演化。 因此,我接着问他们另外一个问题:“好的,那么你们的产品的前景是什么呢?”。这时他们的眼睛亮了起来。然后,我们非常顺利的就第一个要素(“开发一个前景”)中列出的问题进行了沟通,需求也自然而然的流动着(原文:and the requirements just flow naturally.)。 也许只有对于按照有明确需求的合同工作的项目组,在要素列表中加入“满足需求”才是有用的。请记住,我的清单仅仅意味着进行进一步讨论的一个起点。
6.3.9. 总结
  RUP具有很多长处:提高了团队生产力,在迭代的开发过程、需求管理、基于组件的体系结构、可视化软件建模、验证软件质量及控制软件变更等方面,针对所有关键的开发活动为每个开发成员提供了必要的准则、模板和工具指导,并确保全体成员共享相同的知识基础。它建立了简洁和清晰的过程结构,为开发过程提供较大的通用性。但同时它也存在一些不足: RUP只是一个开发过程,并没有涵盖软件过程的全部内容,例如它缺少关于软件运行和支持等方面的内容;此外,它没有支持多项目的开发结构,这在一定程度上降低了在开发组织内大范围实现重用的可能性。可以说RUP是一个非常好的开端,但并不完美,在实际的应用中可以根据需要对其进行改进并可以用OPEN和OOSP等其他软件过程的相关内容对RUP进行补充和完善。
7. 软件测试
 
7.1. 测试方法
7.1.1. 软件测试的基本过程
软件测试是一个极为复杂的过程。如图一所示,一个规范化的软件测试过程通常须包括以下基本的测试活动。
  ·拟定软件测试计划
  ·编制软件测试大纲
  ·设计和生成测试用例
  ·实施测试
  ·生成软件问题报告
   对整个测试过程进行有效的管理实际上,软件测试过程与整个软件开发过程基本上是平行进行的。测试计划早在需求分析阶段即应开始制定,其它相关工作,包括测试大纲的制定、测试数据的生成、测试工具的选择和开发等也应在测试阶段之前进行。充分的准备工作可以有效地克服测试的盲目性,缩短测试周期,提高测试效率,并且起到测试文档与开发文档互查的作用。
   此外,软件测试的实施阶段是由一系列的测试周期(Test Cycle)组成的。在每个测试周期中,软件测试工程师将依据预先编制好的测试大纲和准备好的测试用例,对被测软件进行完整的测试。测试与纠错通常是反复交替进行的。当使用专业测试人员时,测试与纠错甚至是平行进行的,从而压缩总的开发时间。更重要的是,由于专业测试人员丰富的测试经验、所采用的系统化的测试方法、全时的投入,特别是独立于开发人员的思维,使得他们能够更有效地发现许多单靠开发人员很难发现的错误和问题。
   软件测试大纲是软件测试的依据。它明确详尽地规定了在测试中针对系统的每一项功能或特性所必须完成的基本测试项目和测试完成的标准。无论是自动测试还是手动测试,都必须满足测试大纲的要求。
   一般而言,测试用例是指为实施一次测试而向被测系统提供的输入数据、操作或各种环境设置。测试用例控制着软件测试的执行过程,它是对测试大纲中每个测试项目的进一步实例化。已有许多著名的论著总结了设计测试用例的各种规则和策略。从工程实践的角度讲有几条基本准则:
  1.测试用例的代表性:能够代表各种合理和不合理的、合法的和非法的、边界和越界的,以及极限的输入数据、操作和环境设置等;
  2.测试结果的可判定性:即测试执行结果的正确性是可判定的或可评估的;
  3.测试结果的可再现性:即对同样的测试用例,系统的执行结果应当是相同的。
7.1.2. 测试计划
工欲善其事,必先利其器”。专业的测试必须以一个好的测试计划作为基础。尽管测试的每一个步骤都是独立的,但是必定要有一个起到框架结构作用的测试计划。测试的计划应该作为测试的起始步骤和重要环节。一个测试计划应包括:产品基本情况调研、测试需求说明、测试策略和记录、测试资源配置、计划表、问题跟踪报告、测试计划的评审、结果等等。
     产品基本情况调研:
      这部分应包括产品的一些基本情况介绍,例如:产品的运行平台和应用的领域,产品的特点和主要的功能模块,产品的特点等。对于大的测试项目,还要包括测试的目的和侧重点。
      具体的要点有:
      目的:重点描述如何使测试建立在客观的基础上,定义测试的策略,测试的配置, 粗略的估计测试大致需要的周期和最终测试报告递交的时间。
      变更:说明有可能会导致测试计划变更的事件。包括测试工具改进了,测试的环境改变了,或者是添加了新的功能。
      技术结构:可以借助画图,将要测试的软件划分成几个组成部分,规划成一个适用于测试的完整的系统,包括数据是如何存储的,如何传递的(数据流图),每一个部分的测试是要达到什么样的目的。每一个部分是怎么实现数据更新的。还有就是常规性的技术要求,比如运行平台、需要什么样的数据库等等。
      产品规格:就是制造商和产品版本号的说明。
      测试范围:简单的描述如何搭建测试平台以及测试的潜在的风险。
      项目信息:说明要测试的项目的相关资料,如:用户文档,产品描述,主要功能的举例说明。
      测试需求说明:
      这一部分要列出所有要测试的功能项。凡是没有出现在这个清单里的功能项都排除在测试的范围之外。万一有一天你在一个没有测试的部分里发现了一个问题,你应该很高兴你有这个记录在案的文档,可以证明你测了什么没测什么。具体要点有:
      功能的测试:理论上是测试是要覆盖所有的功能项,例如:在数据库中添加、编辑、删除记录等等,这会是一个浩大的工程,但是有利于测试的完整性。
      设计的测试:对于一些用户界面、菜单的结构还有窗体的设计是否合理等的测试。
      整体考虑:这部分测试需求要考虑到数据流从软件中的一个模块流到另一个模块的过程中的正确性。
      测试的策略和记录:
      这是整个测试计划的重点所在,要描述如何公正客观地开展测试,要考虑:模块、功能、整体、系统、版本、压力、性能、配置和安装等各个因素的影响。要尽可能的考虑到细节,越详细越好,并制作测试记录文档的模板,为即将开始的测试做准备,测试记录重要包括的部分具体说明如下:
      公正性声明:要对测试的公正性、遵照的标准做一个说明,证明测试是客观的,整体上,软件功能要满足需求,实现正确,和用户文档的描述保持一致。
      测试案例:描述测试案例是什么样的,采用了什么工具,工具的来源是什么,如何执行的,用了什么样的数据。测试的记录中要为将来的回归测试留有余地,当然,也要考虑同时安装的别的软件对正在测试的软件会造成的影响。
      特殊考虑:有的时候,针对一些外界环境的影响,要对软件进行一些特殊方面的测试。
      经验判断:对以往的测试中,经常出现的问题加以考虑。
      设想:采取一些发散性的思维,往往能帮助你找的测试的新途径。
      测试资源配置:
      项目资源计划:制定一个项目资源计划,包含的是每一个阶段的任务、所需要的资源,当发生类似到了使用期限或者资源共享的事情的时候,要更新这个计划。
      计划表:
      测试的计划表可以做成一个多个项目通用的形式,根据大致的时间估计来制作,操作流程要以软件测试的常规周期作为参考,也可以是根据什么时候应该测试哪一个模块来制定。
      问题跟踪报告:
      在测试的计划阶段,我们应该明确如何准备去做一个问题报告以及如何去界定一个问题的性质,问题报告要包括问题的发现者和修改者、问题发生的频率、用了什么样的测试案例测出该问题的,以及明确问题产生时的测试环境。
      问题描述尽可能是定量的,分门别类的列举,问题有几种:
      1、严重问题:严重问题意味着功能不可用,或者是权限限制方面的失误等等,也可能是某个地方的改变造成了别的地方的问题。
      2、一般问题:功能没有按设计要求实现或者是一些界面交互的实现不正确。
     3、建议问题:功能运行得不象要求的那么快,或者不符合某些约定俗成的习惯,但不影响系统的性能,界面先是错误,格式不对,含义模糊混淆的提示信息等等。
      测试计划的评审:
      又叫测试规范的评审,在测试真正实施开展之前必须要认真负责的检查一遍,获得整个测试部门人员的认同,包括部门的负责人的同意和签字。
      结果:
      计划并不是到这里就结束了,在最后测试结果的评审中,必须要严格验证计划和实际的执行是不是有偏差,体现在最终报告的内容是否和测试的计划保持一致,然后,就可以开始着手制作下一个测试计划了。
7.1.3. 各种测试的比较
测试方式
白盒测试:关心软件内部设计和程序实现,主要测试依据是设计文档
黑盒测试:不关心软件内部,只关心输入输出,主要测试依据是需求文档
测试阶段
单元测试、集成测试、系统测试、验收测试。是“从小到大”、“由内至外”、“循序渐进”的测试过程,体现了“分而治之”的思想。
单元测试的粒度最小,一般由开发小组采用白盒方式来测试,主要测试单元是否符合“设计”。
集成测试界于单元测试和系统测试之间,起到“桥梁作用”,一般由开发小组采用白盒加黑盒的方式来测试,既要验证“设计”又要验证“需求”。
系统测试的粒度最大,一般由独立测试小组采用黑盒方式来测试,主要测试系统是否符合“需求规格说明书”。
验收测试与系统测试非常相似,主要区别是测试人员不同,验收测试由用户执行。
7.1.4. 黑盒测试
  黑盒测试也称功能测试或数据驱动测试,它是在已知产品所应具有的功能,通过测试来检测每个功能是否都能正常使用,在测试时,把程序看作一个不能打开的黑盆子,在完全不考虑程序内部结构和内部特性的情况下,测试者在程序接口进行测试,它只检查程序功能是否按照需求规格说明书的规定正常使用,程序是否能适当地接收输入数锯而产生正确的输出信息,并且保持外部信息(如数据库或文件)的完整性。黑盒测试方法主要有等价类划分、边值分析、因果图、错误推测等,主要用于软件确认测试。 “黑盒”法着眼于程序外部结构、不考虑内部逻辑结构、针对软件界面和软件功能进行测试。“黑盒”法是穷举输入测试,只有把所有可能的输入都作为测试情况使用,才能以这种方法查出程序中所有的错误。实际上测试情况有无穷多个,人们不仅要测试所有合法的输入,而且还要对那些不合法但是可能的输入进行测试。
7.1.5. 白盒测试
  白盒测试也称结构测试或逻辑驱动测试,它是知道产品内部工作过程,可通过测试来检测产品内部动作是否按照规格说明书的规定正常进行,按照程序内部的结构测试程序,检验程序中的每条通路是否都有能按预定要求正确工作,而不顾它的功能,白盒测试的主要方法有逻辑驱动、基路测试等,主要用于软件验证。
  “白盒”法全面了解程序内部逻辑结构、对所有逻辑路径进行测试。“白盒”法是穷举路径测试。在使用这一方案时,测试者必须检查程序的内部结构,从检查程序的逻辑着手,得出测试数据。贯穿程序的独立路径数是天文数字。但即使每条路径都测试了仍然可能有错误。第一,穷举路径测试决不能查出程序违反了设计规范,即程序本身是个错误的程序。第二,穷举路径测试不可能查出程序中因遗漏路径而出错。第三,穷举路径测试可能发现不了一些与数据相关的错误。
7.1.6. ALAC(Act-like-a-customer)测试
ALAC测试是一种基于客户使用产品的知识开发出来的测试方法。ALAC测试是基于复杂的软件产品有许多错误的原则。最大的受益者是用户,缺陷查找和改正将针对哪些客户最容易遇到的错误。
7.2. 单元测试
7.2.1. 单元测试的基本方法
单元测试的对象是软件设计的最小单位模块。单元测试的依据是详细设描述,单元测试应对模块内所有重要的控制路径设计测试用例,以便发现模块内部的错误。单元测试多采用白盒测试技术,系统内多个模块可以并行地进行测试。

7.2.2. 单元测试任务
  单元测试任务包括:1 模块接口测试;2 模块局部数据结构测试;3 模块边界条件测试;4 模块中所有独立执行通路测试;5 模块的各条错误处理通路测试。
  模块接口测试是单元测试的基础。只有在数据能正确流入、流出模块的前提下,其他测试才有意义。测试接口正确与否应该考虑下列因素:
   1 输入的实际参数与形式参数的个数是否相同;
   2 输入的实际参数与形式参数的属性是否匹配;
   3 输入的实际参数与形式参数的量纲是否一致;
   4 调用其他模块时所给实际参数的个数是否与被调模块的形参个数相同;
   5 调用其他模块时所给实际参数的属性是否与被调模块的形参属性匹配;
   6调用其他模块时所给实际参数的量纲是否与被调模块的形参量纲一致;
   7 调用预定义函数时所用参数的个数、属性和次序是否正确;
   8 是否存在与当前入口点无关的参数引用;
   9 是否修改了只读型参数;
   10 对全程变量的定义各模块是否一致;
   11是否把某些约束作为参数传递。
  如果模块内包括外部输入输出,还应该考虑下列因素:
   1 文件属性是否正确;
   2 OPEN/CLOSE语句是否正确;
   3 格式说明与输入输出语句是否匹配;
   4缓冲区大小与记录长度是否匹配;
   5文件使用前是否已经打开;
   6是否处理了文件尾;
   7是否处理了输入/输出错误;
   8输出信息中是否有文字性错误;
  检查局部数据结构是为了保证临时存储在模块内的数据在程序执行过程中完整、正确。局部数据结构往往是错误的根源,应仔细设计测试用例,力求发现下面几类错误:
   1 不合适或不相容的类型说明;
   2变量无初值;
   3变量初始化或省缺值有错;
   4不正确的变量名(拼错或不正确地截断);
   5出现上溢、下溢和地址异常。
  除了局部数据结构外,如果可能,单元测试时还应该查清全局数据(例如FORTRAN的公用区)对模块的影响。
  在模块中应对每一条独立执行路径进行测试,单元测试的基本任务是保证模块中每条语句至少执行一次。此时设计测试用例是为了发现因错误计算、不正确的比较和不适当的控制流造成的错误。此时基本路径测试和循环测试是最常用且最有效的测试技术。计算中常见的错误包括:
   1 误解或用错了算符优先级;
   2混合类型运算;
   3变量初值错;
   4精度不够;
   5表达式符号错。
比较判断与控制流常常紧密相关,测试用例还应致力于发现下列错误:
   1不同数据类型的对象之间进行比较;
   2错误地使用逻辑运算符或优先级;
   3因计算机表示的局限性,期望理论上相等而实际上不相等的两个量相等;
   4比较运算或变量出错;
   5循环终止条件或不可能出现;
   6迭代发散时不能退出;
   7错误地修改了循环变量。
  一个好的设计应能预见各种出错条件,并预设各种出错处理通路,出错处理通路同样需要认真测试,测试应着重检查下列问题:
   1输出的出错信息难以理解;
   2记录的错误与实际遇到的错误不相符;
   3在程序自定义的出错处理段运行之前,系统已介入;
   4异常处理不当;
   5错误陈述中未能提供足够的定位出错信息。
  边界条件测试是单元测试中最后,也是最重要的一项任务。众的周知,软件经常在边界上失效,采用边界值分析技术,针对边界值及其左、右设计测试用例,很有可能发现新的错误。

7.2.3. 单元测试过程
  一般认为单元测试应紧接在编码之后,当源程序编制完成并通过复审和编译检查,便可开始单元测试。测试用例的设计应与复审工作相结合,根据设计信息选取测试数据,将增大发现上述各类错误的可能性。在确定测试用例的同时,应给出期望结果。
  应为测试模块开发一个驱动模块(driver)和(或)若干个桩模块(stub),下图显示了一般单元测试的环境。驱动模块在大多数场合称为“主程序”,它接收测试数据并将这些数据传递到被测试模块,被测试模块被调用后,“主程序”打印“进入-退出”消息。
  驱动模块和桩模块是测试使用的软件,而不是软件产品的组成部分,但它需要一定的开发费用。若驱动和桩模块比较简单,实际开销相对低些。遗憾的是,仅用简单的驱动模块和桩模块不能完成某些模块的测试任务,这些模块的单元测试只能采用下面讨论的综合测试方法。
提高模块的内聚度可简化单元测试,如果每个模块只能完成一个,所需测试用例数目将显著减少,模块中的错误也更容易发现。
7.3. 集成测试
7.3.1. 基本方法
 时常有这样的情况发生,每个模块都能单独工作,但这些模块集成在一起之后却不能正常工作。主要原因是,模块相互调用时接口会引入许多新问题。例如,数据经过接口可能丢失;一个模块对另一模块可能造成不应有的影响;几个子功能组合起来不能实现主功能;误差不断积累达到不可接受的程度;全局数据结构出现错误,等等。综合测试是组装软件的系统测试技术,按设计要求把通过单元测试的各个模块组装在一起之后,进行综合测试以便发现与接口有关的各种错误。
某设计人员习惯于把所有模块按设计要求一次全部组装起来,然后进行整体测试,这称为非增量式集成。这种方法容易出现混乱。因为测试时可能发现一大堆错误,为每个错误定位和纠正非常困难,并且在改正一个错误的同时又可能引入新的错误,新旧错误混杂,更难断定出错的原因和位置。与之相反的是增量式集成方法,程序一段一段地扩展,测试的范围一步一步地增大,错误易于定位和纠正,界面的测试亦可做到完全彻底。下面讨论两种增量式集成方法。
1 自顶向下集成
  自顶向下集成是构造程序结构的一种增量式方式,它从主控模块开始,按照软件的控制层次结构,以深度优先或广度优先的策略,逐步把各个模块集成在一起。深度优先策略首先是把主控制路径上的模块集成在一起,至于选择哪一条路径作为主控制路径,这多少带有随意性,一般根据问题的特性确定。以下图为例,若选择了最左一条路径,首先将模块M1,M2,M5和M8集成在一起,再将M6集成起来,然后考虑中间和右边的路径。广度优先策略则不然,它沿控制层次结构水平地向下移动。仍以下图为例,它首先把M2、M3和M4与主控模块集成在一起,再将M5和M6 和其他模块集资集成起来。
自顶向下综合测试的具体步骤为:
1 以主控模块作为测试驱动模块,把对主控模块进行单元测试时引入的所有桩模块用实际模块替代;
2 依据所选的集成策略(深度优先或广度优先),每次只替代一个桩模块;
3 每集成一个模块立即测试一遍;
4 只有每组测试完成后,才着手替换下一个桩模块;
 5 为避免引入新错误,须不断地进行回归测试(即全部或部分地重复已做过的测试)。
 从第二步开始,循环执行上述步骤,直至整个程序结构构造完毕。下图中,实线表示已部分完成的结构,若采用深度优先策略,下一步将用模块M7替换桩模块S7,当然M7本身可能又带有桩模块,随后将被对应的实际模块一一替代。
自顶向下集成的优点在于能尽早地对程序的主要控制和决策机制进行检验,因此较早地发现错误。缺点是在测试较高层模块时,低层处理采用桩模块替代,不能反映真实情况,重要数据不能及时回送到上层模块,因此测试并不充分。解决这个问题有几种办法,第一种是把某些测试推迟到用真实模块替代桩模块之后进行,第二种是开发能模拟真实模块的桩模块;第三种是自底向上集成模块。第一种方法又回退为非增量式的集成方法,使错误难于定位和纠正,并且失去了在组装模块时进行一些特定测试的可能性;第二种方法无疑要大大增加开销;第三种方法比较切实可行,下面专门讨论。
2自底向上集成
自底向上测试是从“原子”模块(即软件结构最低层的模块)开始组装测试,因测试到较高层模块时,所需的下层模块功能均已具备,所以不再需要桩模块。
自底向上综合测试的步骤分为:
 1 把低层模块组织成实现某个子功能的模块群(cluster);
 2 开发一个测试驱动模块,控制测试数据的输入和测试结果的输出;
 3 对每个模块群进行测试;
 4 删除测试使用的驱动模块,用较高层模块把模块群组织成为完成更大功能的新模块群。
从第一步开始循环执行上述各步骤,直至整个程序构造完毕。
下图说明了上述过程。首先“原子”模块被分为三个模块群,每个模块群引入一个驱动模块进行测试。因模块群1、模块群2中的模块均隶属于模块Ma,因此在驱动模块D1、D2去掉后,模块群1与模块群2直接与Ma接口,这时可对MaD3被去掉后,M3与模块群3直接接口,可对Mb进行集成测试,最后Ma、Mb和 Mc全部集成在一起进行测试。
自底向上集成方法不用桩模块,测试用例的设计亦相对简单,但缺点是程序最后一个模块加入时才具有整体形象。它与自顶向综合测试方法优缺点正好相反。因此,在测试软件系统时,应根据软件的特点和工程的进度,选用适当的测试策略,有时混和使用两种策略更为有效,上层模块用自顶向下的方法,下层模块用自底向上的方法。
此外,在综合测试中尤其要注意关键模块,所谓关键模块一般都具有下述一或多个特征:①对应几条需求;②具有高层控制功能;③复杂、易出错;④有特殊的性能要求。关键模块应尽早测试,并反复进行回归测试。

7.4. 确认测试
 通过综合测试之后,软件已完全组装起来,接口方面的错误也已排除,软件测试的最后一步确认测试即可开始。确认测试应检查软件能否按合同要求进行工作,即是否满足软件需求说明书中的确认标准。
1. 确认测试标准
实现软件确认要通过一系列墨盒测试。确认测试同样需要制订测试计划和过程,测试计划应规定测试的种类和测试进度,测试过程则定义一些特殊的测试用例,旨在说明软件与需求是否一致。无是计划还是过程,都应该着重考虑软件是否满足合同规定的所有功能和性能,文档资料是否完整、准确人机界面和其他方面(例如,可移植性、兼容性、错误恢复能力和可维护性等)是否令用户满意。
确认测试的结果有两种可能,一种是功能和性能指标满足软件需求说明的要求,用户可以接受;另一种是软件不满足软件需求说明的要求,用户无法接受。项目进行到这个阶段才发现严重错误和偏差一般很难在预定的工期内改正,因此必须与用户协商,寻求一个妥善解决问题的方法。
2. 配置复审
确认测试的另一个重要环节是配置复审。复审的目的在于保证软件配置齐全、分类有序,并且包括软件维护所必须的细节。
3. α、β测试
事实上,软件开发人员不可能完全预见用户实际使用程序的情况。例如,用户可能错误的理解命令,或提供一些奇怪的数据组合,亦可能对设计者自认明了的输出信息迷惑不解,等等。因此,软件是否真正满足最终用户的要求,应由用户进行一系列“验收测试”。验收测试既可以是非正式的测试,也可以有计划、有系统的测试。有时,验收测试长达数周甚至数月,不断暴露错误,导致开发延期。一个软件产品,可能拥有众多用户,不可能由每个用户验收,此时多采用称为α、β测试的过程,以期发现那些似乎只有最终用户才能发现的问题。
α测试是指软件开发公司组织内部人员模拟各类用户行对即将面市软件产品(称为α版本)进行测试,试图发现错误并修正。α测试的关键在于尽可能逼真地模拟实际运行环境和用户对软件产品的操作并尽最大努力涵盖所有可能的 用户操作方式。经过α测试调整的软件产品称为β版本。紧随其后的β测试是指软件开发公司组织各方面的典型用户在日常工作中实际使用β版本,并要求用户报告异常情况、提出批评意见。然后软件开发公司再对β版本进行改错和完善。
7.5. 系统测试
 计算机软件是基于计算机系统的一个重要组成部分,软件开发完毕后应与系统中其它成分集成在一起,此时需要进行一系列系统集成和确认测试。对这些测试的详细讨论已超出软件工程的范围,这些测试也不可能仅由软件开发人员完成。在系统测试之前,软件工程师应完成下列工作:
 (1)为测试软件系统的输入信息设计出错处理通路;
 (2)设计测试用例,模拟错误数据和软件界面可能发生的错误,记录测试结果,为系统测试提供经验和帮助;
 (3)参与系统测试的规划和设计,保证软件测试的合理性。
系统测试应该由若干个不同测试组成,目的是充分运行系统,验证系统各部件是否都能政党工作并完成所赋予的任务。下面简单讨论几类系统测试。
1、恢复测试
恢复测试主要检查系统的容错能力。当系统出错时,能否在指定时间间隔内修正错误并重新启动系统。恢复测试首先要采用各种办法强迫系统失败,然后验证系统是否能尽快恢复。对于自动恢复需验证重新初始化(reinitialization)、检查点(checkpointing mechanisms)、数据恢复(data recovery)和重新启动 (restart)等机制的正确性;对于人工干预的恢复系统,还需估测平均修复时间,确定其是否在可接受的范围内。
2、安全测试
安全测试检查系统对非法侵入的防范能力。安全测试期间,测试人员假扮非法入侵者,采用各种办法试图突破防线。例如,①想方设法截取或破译口令;②专门定做软件破坏系统的保护机制;③故意导致系统失败,企图趁恢复之机非法进入;④试图通过浏览非保密数据,推导所需信息,等等。理论上讲,只要有足够的时间和资源,没有不可进入的系统。因此系统安全设计的准则是,使非法侵入的代价超过被保护信息的价值。此时非法侵入者已无利可图。
3、强度测试
强度测试检查程序对异常情况的抵抗能力。强度测试总是迫使系统在异常的资源配置下运行。例如,①当中断的正常频率为每秒一至两个时,运行每秒产生十个中断的测试用例;②定量地增长数据输入率,检查输入子功能的反映能力;③运行需要最大存储空间(或其他资源)的测试用例;④运行可能导致虚存操作系统崩溃或磁盘数据剧烈抖动的测试用例,等等。
4、 性能测试
对于那些实时和嵌入式系统,软件部分即使满足功能要求,也未必能够满足性能要求,虽然从单元测试起,每一测试步骤都包含性能测试,但只有当系统真正集成之后,在真实环境中才能全面、可靠地测试运行性能系统性能测试是为了完成这一任务。性能测试有时与强度测试相结合,经常需要其他软硬件的配套支持。
7.6. 软件测试工具
7.6.1. Purify
Purify是原PureAtria公司(现已经与美国Rational公司合并,改名为美国Rational公司)于90年代初率先推出的专门用于检测程序中种种内存使用错误的软件工具。几乎所有使用过C语言开发软件的程序员都会有这样的体会,C语言中使用极为灵活的指针给程序员带来了很大便利,但同时也制造了许多的麻烦。由于指针使用不当而引起的错误通常是最难发现的,同时也是最难定位的一类错误。而Purify对多种常见的内存使用错误的检错能力和准确的定位,受到广大软件开发人员的青睐。
    Purify可以自动识别出二十多种内存使用错误,包括
  ·未初始化的局部变量
  ·未申请的内存
  ·使用已释放的内存
  ·数组越界
  ·内存丢失
  ·文件描述问题
  ·栈溢出问题
  ·栈结构边界错误等
  在下面的例子中,暗藏着两个内存使用错误。第一行为指针数组pp申请的空间尺寸不对。这类错误往往不易发现,因为在C语言中,一些"轻微"的内存越界可能被系统所容忍。但这往往是导致更严重错误的根源。例如,可能破坏其它数据区等。最后一行的错误是在释放pp 之前没有释放赋予它的字符串空间,从而把它们"丢失"了。这类错误犹如慢性自杀,它会逐渐消耗掉内存,降低系统的运行效率,直到完全崩溃。而真正的问题在于,这些程序中的"恶性肿瘤"用常规的测试手段和调试工具是极难发现和加以定位的。Purify则在此充分显示了它的强大功效,所到之处,即对所测试过的情况,上述各种常见的内存错误都可以被一一揭露出来,并且准确地指出错误的类型和位置。从而大大地提高了测试和纠错的效率,提高了软件的可靠性。
  …/"to get 10 words and print them out"/
  if(!(pp=(char**)malloc(10))){
       /*Size should be 10*sizeof(char*)*/
       printf("Out of memory.\n");
       exit(-1);
  }
  for(i=0;i<10;i++){
  scanf("%s",buffer);
  if(!(pp[i]=(char*)malloc(strlen(buffer)+1))){
  print("Out of Memory.\ n");
  exit(-1);
  }
  strcpy(pp[i],buffer);
  printf(pp[i]);
  }
  free(pp);/*all the strings pointed by it are lost!*/
  ……
   今年以来,原PureAtria公司陆续推出了其系列产品Pure,包括支持内存检测的Purify ,支持路径覆盖的PureCoverage,支持多线程应用程序性能测试的Quantify,以及用以提高测试期间连接编译被测程序效率的PureLink等。Pure系列现已支持C、C++、FORTRAN语言,以及UNIX和Window NT等操作系统,如Sun OS、Solaris 2.3,HP-UX,Windows NT Server以及IBM A/ X等。

 

 

 

posted on 2007-08-28 22:11  沧海-重庆  阅读(3322)  评论(0编辑  收藏  举报