一个软件首先要规定能处理的数据类型, 其次要实现三项最基本的功能——引用管理、内存管理和异常管理。在 OCC 中,这三项功能分别对应基础类中的句柄、内存管理器和异常类。
1 异常类
1. 1 异常类的定义
异常处理机制实现了正常程序逻辑与错误处理的分离, 提高了程序的可阅读性和执行效率。为了转移程序运行的控制流,异常处理的模式通常有无条件转移模式、 重试模式、 恢复模式和终止模式[5]。 与 C++一样, OCC 采用的是终止模式。
为了实现这种异常处理机制, OCC 提供了一套异常类。所有异常类都是基于它们的根类——Failure 类的。异常类描述了函数运行期间可能发生的异常情况。发生异常时,程序将不能正常运行。对这种情况的响应被称为异常处理。
1. 2 异常类的使用
OCC 使用异常的语法与 C++使用异常的语法相似。要产生一个确定类型的
异常,需用到相应异常类的 Raise()方法,如例 3.10 所示。
例 3.10:
DomainError::Raise(“Cannot cope with this condition”);
这样就产生了一个 DomainError 类型的异常,同时伴有相应的提示信息“Cannot cope with this condition”。这信息可以是任意的。该异常可以被某种 DomainError 类型(DomainError 类型派生了好些类型)的句柄器捕获,如例 3.11 所示。
例 3.11:
try
{
OCC_CATCH_SIGNALS
// try 块。
}
catch (DomainError)
{
// 处理 DomainError 异常。
}
不能把异常类的使用当作一种编程技巧, 例如用异常类代替“goto”。 应该把异常类的使用作为方法的一种保护方式(防止被错误使用),即保证方法调用者遇到的问题是方法能处理的。在程序正常运行期间,不该产生任何异常。
在使用异常类的时候,需要用一个方法来保护另外一个可能出现异常的方
法。 这样能通过外层方法来检查内层方法的调用是否有效。 例如需要用三个方法(用于检查元素的 Value 函数、 用于检查数组下边界的 Lower 函数和用于检查数组上边界的 Upper 函数)使用 TCollection_ Array1 类,那么, Value 函数可以如例 3.12 那样被实现:
例 3.12:
Item TCollection_Array1::Value (const Standard_Integer&index) const
{
// 下面的 r1 和 r2 是数组的上下边界。
if(index < r1 || index > r2)
{
OutOfRange::Raise(“Index out of range in Array1::Value”);
}
return contents[index];
}
在此, OutOfRange::Raise(“Index out of range in Array1::Value”)异常用 Lower函数和 Upper 函数检查索引是否有效,以保护 Value 函数的调用。
一般地, 在 Value()函数调用前, 程序员已确定索引在有效区间内了。 这样,上面 Value()函数的实现就不是最优的了,因为检查既费时又冗余。在软件开发中有这样一种广泛的应用方式, 即将一些保护措施置于反汇编构件而非优化构件中。为了支持这种应用, OCC 为每一个异常类提供了相应的宏
Raise_if():
<ErrorTypeName>_Raise_if(condition, “Error message”)
这里 ErrorTypeName 是异常类型, condition 是产生异常的逻辑表达式,而 Error message 则是相关的错误信息。可以在编译的时候,通过 No_Exception 或者 No_两种预处理声明之一解除异常的调用,如例 3.13 所示:
例 3.13:
#define No_Exception /*解除所有的异常调用*/
使用这构造语句, Value 函数变为:
例 3.14:
Item TCollection_Array1::Value (const Standard_Integer&index) const
{
OutOfRange_Raise_if(index < r1 || index > r2,
“index out of range in Array1::Value”);
return contents[index];
}
1. 3 异常处理
异常发生时, 控制点将转移到调用堆栈中离当前执行点最近的指定类型的句柄器上。该句柄器具有如下特征:
(1)它的 try 块刚刚被进入还没有被退出;
(2)它的类型与异常类型匹配。
(3) T 类型异常句柄器与 E 类型异常匹配,即 T 类型和 E 类型相同,或者T类型是 E 类型的超类型。
OCC 的异常处理机制还可以将系统信号当作异常处理。为此,需要在相关代码的开端嵌入宏 OCC_CATCH_SIGNALS。建议将这个宏放在 try {}块中的第一位置。 例如, 有这样四个异常: NumericError 类型异常、 Overflow 类型异常、Underflow 类型异常和 ZeroDivide 类型异常, 其中 NumericError 类型是其它三种类型的超类型,那么,异常处理过程如例 3.15 所示。
例 3.15:
void f(1)
{
try
{
OCC_CATCH_SIGNALS
// try 块
}
catch(Standard_Overflow)
{ // 第一个句柄器
// ...
}
catch(Standard_NumericError)
}
在这个例子中,第一个句柄器将捕获 Overflow 类型异常;第二个句柄器将
捕获 NumericError 类型异常及其派生异常,包括 Underflow 类型异常 和Zerodivide 类型异常。 异常发生时, 系统将从最近的 try 块到最远的 try 块逐一检查句柄器,直到找到一个在形式上与产生的异常相匹配的为止。
在 try 块中, 如果将基类异常的句柄器置于派生类异常的句柄器之前, 则将发生错误。因为那样会导致后者永远不会被调用,如例 3.16 所示。
例 3.16:
void f(1)
{
int i = 0;
try
{
OCC_CATCH_SIGNALS
g(i);
// i 是可接受的。
}
// 在这放执行语句会导致编译错误!
catch(Standard_NumericError)
{
// 依据 i 值处理异常。
}
// 在这放执行语句可能导致不可预料的影响。
}
由异常类形成的树状体系与用户定义的类完全无关。 该体系的根类是 Failure异常。 因此, Failure 异常句柄器可以捕获任何 OCC 异常。 建议将 Failure 异常句柄器设置在主路径中,如例 3.17 所示。
例 3.17:
#include <Standard_ErrorHandler.hxx>
#include <Standard_Failure.hxx>
#include <iostream.h>
int main (int argc, char* argv[ ])
{
Try
{
OCC_CATCH_SIGNALS
//主块
return 0;
}
catch(Standard_Failure)
{
Handle(Standard_Failure) error = Failure::Caught ();
cout << error << end1;
return 1;
}
}
这里的 Caught 函数是 Failure 类的一个静态成员, 能返回一个含有异常错误信息的异常对象。这种接收对象的方法(通过 catch 的参数接收异常)代替了通常的 C++语句。
尽管标准的 C++处理法则和语法在 try 块和句柄器中同样适用, 但在一些平台上, OCC 能以一种兼容模式被编译(此时异常支持长转移)。在这种模式中,要求句柄器前后没有执行语句。因此,强烈建议将 try 块置于{}中。此外,这种模式也要求 Standard_ErrorHandler.hxx 头文件包含在程序中(置于 try 块前), 否则将不能处理 OCC 异常。再有, catch()语句不允许将一个异常对象作为参数来传递。
为了使程序能够像捕获其它异常那样捕获系统信号(如除零),在程序运行时要使用 OSD::SetSignal()方法安装相应的信号句柄器。通常,该方法在主函数
开端处被调用。
为了能真正的将系统信号转换成 OCC 异常, OCC_CATCH_ SIGNALS 宏
应该被嵌入到源代码中。典型的,将该宏置于捕获异常的 try{}块的开端处。OCC 的异常处理机制依据不同的宏预处理 NO_CXX_EXCE- PTIONS 和OCC_CONVERT_SIGNALS 有不同的实现。 这些预处理将被 OCC 或者用户程序的编译程序连贯定义。在 Windows 和 DEC 平台上,这些宏不是以默认值被定义的, 并且所有类都支持 C++异常, 包括从句柄器中抛掷异常。 因此, 异常的处理与 C++异常处理一样。
2 内存管理器
2. 1 使用内存管理器的原因
标准的内存分配有三种方式: 静态分配、 栈分配和堆分配。 静态分配是最简单的内存分配策略。 程序中的所有名字在编译时绑定在某个存储位置上; 这些绑定不会在运行时改变。块结构语言通过在栈上分配内存,克服了静态分配的一些限制。
每次过程调用时,一个活动记录或是帧被压入系统栈,并在返回时弹出。堆分配与栈所遵循的后进先出的规律不同,堆中的数据结构能够以任意次序分配与释放。
建模程序在运行期间,需要构造和析构大量的动态对象。在这种情况下, 标准的内存分配函数可能无法胜任工作。 因此, OCC 采用了特殊的内存管理器(在Standard 包中实现)。
2.2 内存管理器的用法
在 C 代码中使用 OCC 内存管理器分配内存,只需用 Standard::Allocate()方法代替 malloc() 函数, Standard::Free() 方法代替 free() 函数,以 及
Standard::Reallocate()代替 realloc()函数。
在 C++中, 可以将类的 new()操作定义为使用 Standard::Allocate()方法进行内存分配, 而将类的 delete()操作定义为使用 Standard::Allocate()方法进行内存释放。这样就可以使用 OCC 内存管理器为所有对象和所有类分配内存。就是用这种方式, CDL 提取器为所有用 CDL 声明的类定义了 new()函数和 delete()函数。 因此,除了异常类,所有 OCC 类都使用 OCC 内存管理器进行存储分配。
因为 new()函数和 delete()函数是被继承的,所以对于所有 OCC 类的派生类(比如, Standard_Transient 类的派生类), new()函数和 delete()函数同样适用。
2.3 内存管理器的配置
OCC 内存管理器可以适用于不同的内存优化技术(不同的内存块采用不同的优化技术, 这主要依据内存块的大小而定)。 或者, 用 OCC 内存管理器, 甚至可以不采用任何优化技术而直接使用 C 函数 malloc() 和 free()。
内存管理器的配置由下面几个环境变量值定义:
(1) MMGT_OPT。如果值设为 1(默认值),则内存管理器将如下面的描述那样对内存进行优化。 如果值设为 0, 则每个内存块将直接分配(通过 malloc()和 free()函数)在 C 内存堆里。在第二种情况下, 所有异常(不包括 MMGT_CLEAR异常)都将被忽略。
(2) MMGT_CLEAR。 如果值设为 1(默认值), 则每一个已分配的内存块都将被清零。如果值设为 0,则内存块正常返回。
(3) MMGT_CELLSIZE。 它定义了大内存池中内存块的最大空间。 默认值是 200 字节。
(4) MMGT_NBPAGES。它定义了页面中由小内存块构成的内存组件(内存池的大小(由操作系统决定)。默认值是 1000 字节。
(5) MMGT_THRESHOLD。它定义了内存块(能直接在 OCC 内部被循环使用)的最大空间。默认值是 40000 字节。
(6) MMGT_MMAP。 当值设为 1(默认值) 时, 使用操作系统的映射函数对大内存块进行分配。 当值设为 0 时, 大内存块将被 malloc()分配在 C 内存堆里。
(7) MMGT_REENTRANT。 当值设为 1 时, 所有对内存优化管理器的调用都将被响应, 以保证不同的线程能同时访问内存管理器。 在多线程程序中, 这个变量值应该设置为 1。这里所说的多线程程序是指那些使用 OCC 内存管理器,并且可能有不止一个调用 OCC 函数的线程的程序。默认值是 0。
在此提一个注意事项: 当多线程程序使用 OCC 以达到最佳内存优化性能时,需要检查两组变量。 其中一组是 MMGT_OPT=0; 另一组则是 MMGT_OPT=1 和
MMGT_REENTRANT=1。
2.4 内存管理器的实现
当且仅当 MMGT_OPT=1 时, 才用到 OCC 的特殊的内存优化技术。 这些技术有:
(1) 小内存块( 空间比由 MMGT_CELLSIZE 设定的值小) 不能单独分配,而是分配在大内存池(大小由变量 MMGT_NBPAGES 决定)中。每一个内存块分配在当前内存池的空闲部分。若内存池被完全占据,则使用下一个内存池。 在当前版本中,在进程结束前,内存池不能返回操作系统。然而,那些由 Standard::Free()释放的内存块被记忆在释放列表中。 当需要下一个内存块(与列表中某个块大小相同) 时, 相应的被释放的那个内存块所占空间可以被新的内存块占用,这也叫内存块的循环使用。
( 2)对于中等大小的内存块(比 MMGT_CELLSIZE 大,但比 MMGT_THRESHOLD 小),它们是被直接分配(通过使用 malloc() 和 free())在C内存堆里。 这些块要是被 Standard::Free()方法释放的话, 可以像小内存块那样被循环使用。 然而, 与小内存块不同, 那些被记录在释放列表中的可循环使用的中内存块(由持有内存管理器的程序释放)可以被 Standard::Purge()方法返回到 C 内存堆中。
( 3)大内存块(大小比 MMGT_THRESHOLD 大,包括用来分配小内存块的内存池) 的分配取决于 MMGT_MMAP 的值。 如果该值是 0, 则这些大块被分配在 C 堆里。否则,它们被操作系统映射函数分配在内存映射文件中。 当Standard::Free()被调用时,大块立即被返回到操作系统中去。
2.5 内存管理器的优缺点
OCC 内存管理器的优点主要体现在小块和中块的循环使用上。当程序需要连续分配和释放大小差不多的内存块时, 这个优点能加速程序的执行。 实际应用中,这种提升幅度可以高达 50%。
相应的, OCC 内存管理器的主要缺点是:程序运行时,被循环使用的内存块不能返回到操作系统中。 这可能导致严重的内存消耗, 甚至会被操作系统误认为内存泄露。为了减少这种影响,在频繁地对内存进行操作后, OCC 系统将调用 Standard::Purge()方法。
另外, OCC 内存管理器会带来额外的开销,它们有:
(1)舍入后,每个被分配的内存块的大小高达 8 字节。当 MMGT_OPT=0时,舍入值由 CRT 决定;对 32 位平台而言,典型值是 4 字节。
( 2) 在每个内存块的开端需要额外的 4 字节以记录该内存块的大小( 或者,当内存块被记录在释放列表时, 这 4 字节用来记录下一个内存块的地址)。 注意:只有在 MMGT_OPT=1 时,才需要这 4 字节。
需要注意的是: 由 OCC 内存管理器带来的额外开销可能比由 C 内存堆管理器带来的额外开销大, 或者小。 因此, 整体而言, 很难说到底是优化模式的内存消耗大还是标准模式的内存消耗大——这得视情况而定。
通常, 编程人员自己也会采用一种优化技术——在内存里面划出一些重要的块。 这样就可以将一些连续的数据存于这些块中, 使内存页面管理器对这些块的处理变得更容易。
在多线程模式(MMGT_REENTRANT=1) 中, OCC 内存管理器使用互斥机制以锁定程序对释放列表的访问。 因此, 当不同的线程经常同时调用内存管理器时,优化模式的性能不如标准模式的性能好。原因是: malloc() 函数和 free()函数在实现的过程中开辟了几个分配空间——这样就避免了由互斥机制带来的延迟。