条款14: 确定基类有虚析构函数
有时,一个类想跟踪它有多少个对象存在。一个简单的方法是创建一个静态类成员来统计对象的个数。这个成员被初始化为0,在构造函数里加1,析构函数里减1。(条款m26里说明了如何把这种方法封装起来以便很容易地添加到任何类中,“my article on counting objects”提供了对这个技术的另外一些改进)
设想在一个军事应用程序里,有一个表示敌人目标的类:
class enemytarget {
public:
enemytarget() { ++numtargets; }
enemytarget(const enemytarget&) { ++numtargets; }
~enemytarget() { --numtargets; }
static size_t numberoftargets()
{ return numtargets; }
virtual bool destroy(); // 摧毁enemytarget对象后
// 返回成功
private:
static size_t numtargets; // 对象计数器
};
// 类的静态成员要在类外定义;
// 缺省初始化为0
size_t enemytarget::numtargets;
这个类不会为你赢得一份政府防御合同,它离国防部的要求相差太远了,但它足以满足我们这儿说明问题的需要。
敌人的坦克是一种特殊的敌人目标,所以会很自然地想到将它抽象为一个以公有继承方式从enemytarget派生出来的类(参见条款35及m33)。因为不但要关心敌人目标的总数,也要关心敌人坦克的总数,所以和基类一样,在派生类里也采用了上面提到的同样的技巧:
class enemytank: public enemytarget {
public:
enemytank() { ++numtanks; }
enemytank(const enemytank& rhs)
: enemytarget(rhs)
{ ++numtanks; }
~enemytank() { --numtanks; }
static size_t numberoftanks()
{ return numtanks; }
virtual bool destroy();
private:
static size_t numtanks; // 坦克对象计数器
};
(写完以上两个类的代码后,你就更能够理解条款m26对这个问题的通用解决方案了。)
最后,假设程序的其他某处用new动态创建了一个enemytank对象,然后用delete删除掉:
enemytarget *targetptr = new enemytank;
...
delete targetptr;
到此为止所做的一切好象都很正常:两个类在析构函数里都对构造函数所做的操作进行了清除;应用程序也显然没有错误,用new生成的对象在最后也用delete删除了。然而这里却有很大的问题。程序的行为是不可预测的——无法知道将会发生什么。
c++语言标准关于这个问题的阐述非常清楚:当通过基类的指针去删除派生类的对象,而基类又没有虚析构函数时,结果将是不可确定的。这意味着编译器生成的代码将会做任何它喜欢的事:重新格式化你的硬盘,给你的老板发电子邮件,把你的程序源代码传真给你的对手,无论什么事都可能发生。(实际运行时经常发生的是,派生类的析构函数永远不会被调用。在本例中,这意味着当targetptr 删除时,enemytank的数量值不会改变,那么,敌人坦克的数量就是错的,这对需要高度依赖精确信息的部队来说,会造成什么后果?)
为了避免这个问题,只需要使enemytarget的析构函数为virtual。声明析构函数为虚就会带来你所希望的运行良好的行为:对象内存释放时,enemytank和enemytarget的析构函数都会被调用。
和绝大部分基类一样,现在enemytarget类包含一个虚函数。虚函数的目的是让派生类去定制自己的行为(见条款36),所以几乎所有的基类都包含虚函数。
如果某个类不包含虚函数,那一般是表示它将不作为一个基类来使用。当一个类不准备作为基类使用时,使析构函数为虚一般是个坏主意。请看下面的例子,这个例子基于arm(“the annotated c++ reference manual”)一书的一个专题讨论。
// 一个表示2d点的类
class point {
public:
point(short int xcoord, short int ycoord);
~point();
private:
short int x, y;
};
如果一个short int占16位,一个point对象将刚好适合放进一个32位的寄存器中。另外,一个point对象可以作为一个32位的数据传给用c或fortran等其他语言写的函数中。但如果point的析构函数为虚,情况就会改变。
实现虚函数需要对象附带一些额外信息,以使对象在运行时可以确定该调用哪个虚函数。对大多数编译器来说,这个额外信息的具体形式是一个称为vptr(虚函数表指针)的指针。vptr指向的是一个称为vtbl(虚函数表)的函数指针数组。每个有虚函数的类都附带有一个vtbl。当对一个对象的某个虚函数进行请求调用时,实际被调用的函数是根据指向vtbl的vptr在vtbl里找到相应的函数指针来确定的。
虚函数实现的细节不重要(当然,如果你感兴趣,可以阅读条款m24),重要的是,如果point类包含一个虚函数,它的对象的体积将不知不觉地翻番,从2个16位的short变成了2个16位的short加上一个32位的vptr!point对象再也不能放到一个32位寄存器中去了。而且,c++中的point对象看起来再也不具有和其他语言如c中声明的那样相同的结构了,因为这些语言里没有vptr。所以,用其他语言写的函数来传递point也不再可能了,除非专门去为它们设计vptr,而这本身是实现的细节,会导致代码无法移植。
所以基本的一条是,无故的声明虚析构函数和永远不去声明一样是错误的。实际上,很多人这样总结:当且仅当类里包含至少一个虚函数的时候才去声明虚析构函数。
这是一个很好的准则,大多数情况都适用。但不幸的是,当类里没有虚函数的时候,也会带来非虚析构函数问题。 例如,条款13里有个实现用户自定义数组下标上下限的类模板。假设你(不顾条款m33的建议)决定写一个派生类模板来表示某种可以命名的数组(即每个数组有一个名字)。
template<class t> // 基类模板
class array { // (来自条款13)
public:
array(int lowbound, int highbound);
~array();
private:
vector<t> data;
size_t size;
int lbound, hbound;
};
template<class t>
class namedarray: public array<t> {
public:
namedarray(int lowbound, int highbound, const string& name);
...
private:
string arrayname;
};
如果在应用程序的某个地方你将指向namedarray类型的指针转换成了array类型的指针,然后用delete来删除array指针,那你就会立即掉进“不确定行为”的陷阱中。
namedarray<int> *pna =
new namedarray<int>(10, 20, "impending doom");
array<int> *pa;
...
pa = pna; // namedarray<int>* -> array<int>*
...
delete pa; // 不确定! 实际中,pa->arrayname
// 会造成泄漏,因为*pa的namedarray
// 永远不会被删除
现实中,这种情形出现得比你想象的要频繁。让一个现有的类做些什么事,然后从它派生一个类做和它相同的事,再加上一些特殊的功能,这在现实中不是不常见。namedarray没有重定义array的任何行为——它继承了array的所有功能而没有进行任何修改——它只是增加了一些额外的功能。但非虚析构函数的问题依然存在(还有其他问题,参见m33)
最后,值得指出的是,在某些类里声明纯虚析构函数很方便。纯虚函数将产生抽象类——不能实例化的类(即不能创建此类型的对象)。有些时候,你想使一个类成为抽象类,但刚好又没有任何纯虚函数。怎么办?因为抽象类是准备被用做基类的,基类必须要有一个虚析构函数,纯虚函数会产生抽象类,所以方法很简单:在想要成为抽象类的类里声明一个纯虚析构函数。
这里是一个例子:
class awov { // awov = "abstract w/o
// virtuals"
public:
virtual ~awov() = 0; // 声明一个纯虚析构函数
};
这个类有一个纯虚函数,所以它是抽象的,而且它有一个虚析构函数,所以不会产生析构函数问题。但这里还有一件事:必须提供纯虚析构函数的定义:
awov::~awov() {} // 纯虚析构函数的定义
这个定义是必需的,因为虚析构函数工作的方式是:最底层的派生类的析构函数最先被调用,然后各个基类的析构函数被调用。这就是说,即使是抽象类,编译器也要产生对~awov的调用,所以要保证为它提供函数体。如果不这么做,链接器就会检测出来,最后还是得回去把它添上。
可以在函数里做任何事,但正如上面的例子一样,什么事都不做也不是不常见。如果是这种情况,那很自然地会想到将析构函数声明为内联函数,从而避免对一个空函数的调用所产生的开销。这是一个很好的方法,但有一件事要清楚。
因为析构函数为虚,它的地址必须进入到类的vtbl(见条款m24)。但内联函数不是作为独立的函数存在的(这就是“内联”的意思),所以必须用特殊的方法得到它们的地址。条款33对此做了全面的介绍,其基本点是:如果声明虚析构函数为inline,将会避免调用它们时产生的开销,但编译器还是必然会在什么地方产生一个此函数的拷贝。
posted @
2006-09-24 21:07 Jerry Cat 阅读(268) |
评论 (0) |
编辑 收藏
[C++基础]重载、覆盖、多态与函数隐藏
小结:
重载
overload
是根据函数的参数列表来选择要调用的函数版本,而多态是根据运行时对象的实际类型来选择要调用的虚
virtual
函数版本,多态的实现是通过派生类对基类的虚
virtual
函数进行覆盖
override
来实现的,若派生类没有对基类的虚
virtual
函数进行覆盖
override
的话,则派生类会自动继承基类的虚
virtual
函数版本,此时无论基类指针指向的对象是基类型还是派生类型,都会调用基类版本的虚
virtual
函数;如果派生类对基类的虚
virtual
函数进行覆盖
override
的话,则会在运行时根据对象的实际类型来选择要调用的虚
virtual
函数版本,例如基类指针指向的对象类型为派生类型,则会调用派生类的虚
virtual
函数版本,从而实现多态。
使用多态的本意是要我们在基类中声明函数为
virtual
,并且是要在派生类中覆盖
override
基类的虚
virtual
函数版本,注意,此时的函数原型与基类保持一致,即同名同参数类型;如果你在派生类中新添加函数版本,你不能通过基类指针动态调用派生类的新的函数版本,这个新的函数版本只作为派生类的一个重载版本。还是同一句话,重载只有在当前类中有效,不管你是在基类重载的,还是在派生类中重载的,两者互不牵连。如果明白这一点的话,在例
6
、例
9
中,我们也会对其的输出结果顺利地理解。
重载是静态联编的,多态是动态联编的。进一步解释,重载与指针实际指向的对象类型无关,多态与指针实际指向的对象类型相关。若基类的指针调用派生类的重载版本,
C++
编绎认为是非法的,
C++
编绎器只认为基类指针只能调用基类的重载版本,重载只在当前类的名字空间作用域内有效,继承会失去重载的特性,当然,若此时的基类指针调用的是一个虚
virtual
函数,那么它还会进行动态选择基类的虚
virtual
函数版本还是派生类的虚
virtual
函数版本来进行具体的操作,这是通过基类指针实际指向的对象类型来做决定的,所以说重载与指针实际指向的对象类型无关,多态与指针实际指向的对象类型相关。
最后阐明一点,虚
virtual
函数同样可以进行重载,但是重载只能是在当前自己名字空间作用域内有效
(
请再次参考例
6)
。
本文来源:http://blog.csdn.net/callzjy/archive/2004/01/04/20044.aspx
续:
重载与覆盖
成员函数被重载的特征:
(1)相同的范围(在同一个类中);
(2)函数名字相同;
(3)参数不同;
(4)virtual关键字可有可无。
覆盖是指派生类函数覆盖基类函数,特征是:
(1)不同的范围(分别位于派生类与基类);
(2)函数名字相同;
(3)参数相同;
(4)基类函数必须有virtual关键字。
“隐藏”是指派生类的函数屏蔽了与其同名的基类函数,
规则如下:
(1)如果派生类的函数与基类的函数同名,但是参数不同。
此时,不论有无virtual关键字,基类的函数将被隐藏(注意别与重载混淆)。
(2)如果派生类的函数与基类的函数同名,并且参数也相同,但是基类函数没有virtual关键字。
此时,基类的函数被隐藏(注意别与覆盖混淆)。
如下示例程序中:
(1)函数Derived::f(float)覆盖了Base::f(float)。
(2)函数Derived::g(int)隐藏了Base::g(float),而不是重载。
(3)函数Derived::h(float)隐藏了Base::h(float),而不是覆盖。
#include<iostream.h>
class Base{
public:
virtual void f(floatx){cout<<"Base::f(float)"<<x<<endl;}
void g(floatx){cout<<"Base::g(float)"<<x<<endl;
void h(floatx){cout<<"Base::h(float)"<<x<<endl;}
};
class Derived:publicBase{
public:
virtual void f(floatx){cout<<"Derived::f(float)"<<x<<endl;}
void g(intx){cout<<"Derived::g(int)"<<x<<endl;}
void h(floatx){cout<<"Derived::h(float)"<<x<<endl;}
};
void main(void){
Derived d;
Base *pb=&d;
Derived *pd=&d;
//Good:behavior depends solely on type of the object
pb->f(3.14f); //Derived::f(float)3.14
pd->f(3.14f); //Derived::f(float)3.14
//Bad:behavior depends on type of the pointer
pb->g(3.14f); //Base::g(float)3.14
pd->g(3.14f); //Derived::g(int)3(surprise!)
//Bad:behavior depends on type of the pointer
pb->h(3.14f); //Base::h(float)3.14(surprise!)
pd->h(3.14f); //Derived::h(float)3.14
posted @
2006-09-24 21:06 Jerry Cat 阅读(208) |
评论 (0) |
编辑 收藏
摘要: 1. 变量作用域
在vc7.1中, 如果一个变量定义在for语句的条件从句中,那么这个变量可以在for之后使用。但Vc8禁止这样,会报告一个C2065错误.
for
(
int
i
=
...
阅读全文
posted @
2006-09-24 21:04 Jerry Cat 阅读(567) |
评论 (1) |
编辑 收藏
1. 编译时出现:WINVER not defined. Defaulting to 0×0501 (Windows XP and Windows .NET Server)
这个问题是因为没有指定工程要使用的平台SDK的版本。
Minimum system required
|
Macros to define
|
Windows Server 2003 family
|
_WIN32_WINNT>=0×0502
|
Windows XP
|
_WIN32_WINNT>=0×0501
|
Windows 2000
|
_WIN32_WINNT>=0×0500
|
Windows NT 4.0
|
_WIN32_WINNT>=0×0400
|
Windows Me
|
_WIN32_WINDOWS=0×0490
|
Windows 98
|
_WIN32_WINDOWS>=0×0410
|
Internet Explorer 6.0
|
_WIN32_IE>=0×0600
|
Internet Explorer 5.01, 5.5
|
_WIN32_IE>=0×0501
|
Internet Explorer 5.0, 5.0a, 5.0b
|
_WIN32_IE>=0×0500
|
Internet Explorer 4.01
|
_WIN32_IE>=0×0401
|
Internet Explorer 4.0
|
_WIN32_IE>=0×0400
|
Internet Explorer 3.0, 3.01, 3.02
|
_WIN32_IE>=0×0300
|
解决办法:
属性,C/C++,命令行,附加项中添加 /D_WIN32_WINNT=0×0501 (因为我是在xp下工作的所以是0×0501)
2. Link时出现:LINK : warning LNK4075: 忽略”/EDITANDCONTINUE”(由于”/INCREMENTAL:NO”规范)
这个问题是因为在vc6中,工程使用的增量编译。
解决办法:
属性,链接器,常规,启动增量链接 选择 是(INCREMENTAL)
3. 编译时出现:
warning C4129: “U” : 不可识别的字符转义序列
error C3847: 通用字符中的错误符号;必须使用十六进制数字
原因:为开发全球通用的应用程序,.NET Framework 使用 Unicode UTF-16(Unicode 转换格式,16 位编码形式)来表示字符。在某些情况下,.NET Framework 在内部使用 UTF-8。引入通用字符名称的格式是 \u#### 或 \U########。
解决办法:
//#include MAKEPATH(MAIN_IMAGE_PATH, FunUtil\\Unit_star.txt)
#include “..\\ImageData\\ML128160\\FunUtil\\Unit_star.txt”
4. 链接时出现:LIBCMTD.lib(crt0dat.obj) : error LNK2005: _exit 已经在 MSVCRTD.lib(MSVCR71D.dll) 中定义 等类似错误
原因:
Run-Time Library
•Run-Time Library是编译器提供的标准库,提供一些基本的库函数和系统调用。
我们一般使用的Run-Time Library是C Run-Time Libraries。当然也有Standard C++ libraries。
C Run-Time Libraries实现ANSI C的标准库。VC安装目录的CRT目录有C Run-Time库的大部分源代码。 C Run-Time Libraries有静态库版本,也有动态链接库版本;有单线程版本,也有多线程版本;还有调试和非调试版本。
•动态链接库版本:
/MD Multithreaded DLL 使用导入库MSVCRT.LIB
/MDd Debug Multithreaded DLL 使用导入库MSVCRTD.LIB
•静态库版本:
/ML Single-Threaded 使用静态库LIBC.LIB
/MLd Debug Single-Threaded 使用静态库LIBCD.LIB
/MT Multithreaded 使用静态库LIBCMT.LIB
/MTd Debug Multithreaded 使用静态库LIBCMTD.LIB
若要使用此运行时库
|
请忽略这些库
|
单线程 (libc.lib)
|
libcmt.lib、msvcrt.lib、libcd.lib、libcmtd.lib、msvcrtd.lib
|
多线程 (libcmt.lib)
|
libc.lib、msvcrt.lib、libcd.lib、libcmtd.lib、msvcrtd.lib
|
使用 DLL 的多线程 (msvcrt.lib)
|
libc.lib、libcmt.lib、libcd.lib、libcmtd.lib、msvcrtd.lib
|
调试单线程 (libcd.lib)
|
libc.lib、libcmt.lib、msvcrt.lib、libcmtd.lib、msvcrtd.lib
|
调试多线程 (libcmtd.lib)
|
libc.lib、libcmt.lib、msvcrt.lib、libcd.lib、msvcrtd.lib
|
使用 DLL 的调试多线程 (msvcrtd.lib)
|
libc.lib、libcmt.lib、msvcrt.lib、libcd.lib、libcmtd.lib
|
解决方法:
属性,链接器,输入,忽略指定库 libc.lib、libcmt.lib、msvcrt.lib、libcd.lib、libcmtd.lib (这是我需要忽略的,你可以根据你工程的实际情况选择。)
update(20060205):
5. 链接是出现不能打开
mfc4xx.lib的错误时,这是因为VC7对MFC的dll进行了升级。
解决办法:
属性,链接器,输入,附加依赖项 中 添加mfc71d.lib。
posted @
2006-09-24 21:01 Jerry Cat 阅读(824) |
评论 (0) |
编辑 收藏
windows核心编程--内核对象
简单地说:
内核对象是系统的一种资源。系统对象一旦产生,任何应用程序都可以开启并且使用该对象。系统给内核对象一个计数值作为管理只用,内核对象包括:
event,mutex,semaphore,file,file-mapping,preocess,thread.
这些内核对象每次产生都会返回一个handle,作为标示,每使用一次,对应的计数值加1,调用CloseHandle可以结束内核对象的使用。
具体:
1. 内核对象:
1).符号对象
2).事件对象
3).文件对象
4).文件影象对象
5).I/O完成对象
6).作业对象
7).信箱对象
8).互斥对象
9).管道对象
10).进程对象
11).信标对象
12).线程对象
13).待计时器对象
等
2.内核对象只能由内核所拥有,而不是由进程拥有.(就是说进程没有了,内核还可以被其他进程使用)
3.内核对象的数据结构有计数器,进程调用时,计数器增1,调用结束,计数器减1,内核对象计数器为零时,销毁此内核对象.(系统来管理内核对象)
4.内核安全性,进程使用什么权限调用内核对象,由SECURITY_ATTRIBUTES结构的数据结构来指定.几乎所有的调用内核对象的函数都含有SECURITY_ATTRIBUTES结构的指针参数.(可以由这个参数来判断是不是内核对象哦)
typedef struct _SECURITY_ATTRIBUTES {
DWORD nLength; //结构体长度
LPVOID lpSecurityDescriptor; //安全性设置
BOOL bInheritHandle; //可继承性
} SECURITY_ATTRIBUTES, *PSECURITY_ATTRIBUTES;
5.进程的内核对象的句柄表,进程调用内核对象时,就会创建内核对象的句柄表,就是内核对象在进程中的索引,索引值就是调用内核对象函数返回的句柄.关闭所有的内核对象,使用CloseHandle();
6.跨越进程边界共享内核对象
MICROSOFT把句柄设计成进程句柄,不设计成系统句柄是为了实现句柄的健壮性和安全性。
1)
内核对象句柄的继承性。(为了实现内核的多个进程的共享)
作用:为了子进程实现对父进程创建的内核对象的访问。
步骤:首先,父进程创建内核对象时,初始化SECURITY_ATTRIBUTES结构的对象,让SECURITY_ATTRIBUTES结构体的成员变量bInheritHandle设置为TRUE。
然后,子进程创建后,生成自己的句柄表,句柄表遍历父进程的句柄表,找到有继承性的句柄,并复制一份到子进程的句柄表中,子进程的内核对象和父进程的内核对象使用相同的内存块指针,内核对象计数器在子进程中创建内核对象后增一,父进程调用CloseHandle()来关闭内核对象,确不影响子进程使用该内核对象。
2)改变句柄的标志
BOOL SetHandleInformation(
HANDLE hObject, // handle to object
DWORD dwMask, // flags to change
DWORD dwFlags // new values for flags
);
打开内核的可继承性标志
SetHandleInformation(hobj,HANDLE_FLAG_INHERIT,HANDLE_FLAG_INHERIT);
关闭内核的可继承性标志
SetHandleInformation(hobj,HANDLE_FLAG_INHERIT,0);
若想让内核对象不被关闭,设置HANDLE_FLAG_PROTECT_FROM_CLOSE。
获得句柄标志的函数
BOOL GetHandleInformation(
HANDLE hObject, // handle to object
LPDWORD lpdwFlags // handle properties
);
3)命名对象
作用:
让进程中的内核对象可以共享,让别的进程可以通过命名空间,跨进程来访问这个进程的内核对象。
创建对象和访问对象使用函数
创建对象Create*:如果命名的内核对象已经存在并具备安全访问权限,则参数被忽略,进程的句柄表复制一份内核对象的指针和标志到进程的句柄表,如果不存在,则马上创建内核对象。
例子:
HANDLE CreateMutex(
LPSECURITY_ATTRIBUTES lpMutexAttributes, // SD
BOOL bInitialOwner, // initial owner
LPCTSTR lpName // 对象名字
);
打开对象Open*:如果命名的内核对象已经存在并具备安全访问权限,进程的句柄表复制一份内核对象的指针和标志到进程的句柄表,如果不存在,则返回NULL,使用GetLassError(),得到返回值2。
4)终端服务的名字空间
每个客户程序会话都有自己的服务名字空间,一个会话无法访问另一个会话的对象,尽管他们具备相同的对象名字。
服务程序的名字空间对象总放在全局名字空间中。
5)复制对象句柄
DuplicateHandle函数来对另一个进程对象的句柄进行复制到调用此函数的进程句柄表中,实现进程间共享内核对象。
BOOL DuplicateHandle(
HANDLE hSourceProcessHandle, // handle to source process
HANDLE hSourceHandle, // handle to duplicate
HANDLE hTargetProcessHandle, // handle to target process
LPHANDLE lpTargetHandle, // duplicate handle
DWORD dwDesiredAccess, // requested access
BOOL bInheritHandle, // handle inheritance option
DWORD dwOptions // optional actions
);
posted @
2006-09-24 05:11 Jerry Cat 阅读(684) |
评论 (0) |
编辑 收藏
[转]DLL的进入点函数
一个D L L可以拥有单个进入点函数。系统在不同的时间调用这个进入点函数,这个问题将在下面加以介绍。这些调用可以用来提供一些信息,通常用于供D L L进行每个进程或线程的初始化和清除操作。如果你的D L L不需要这些通知信息,就不必在D L L源代码中实现这个函数。例如,如果你创建一个只包含资源的D L L,就不必实现该函数。如果确实需要在D L L中接受通知信息,可以实现类似下面的进入点函数:
BOOL WINAPI DllMain(HINSTANCE hinstDll, DWORD fdwReason, PVOID fImpLoad)
{
switch(fdwReason)
{
case DLL_PROCESS_ATTACH:
//The DLL is being mapped into the process's address space.
break;
case DLL_THREAD_ATTACH:
//A thread is being created.
break;
case DLL_THREAD_DETACH:
//A thread is exiting cleanly.
break;
case DLL_PROCESS_DETACH:
//The DLL is being unmapped from the process's address space.
break;
}
return(TRUE); // Used only for DLL_PROCESS_ATTACH
}
注意函数名D l l M a i n是区分大小写的。
参数h i n s t D l l包含了D L L的实例句柄。与( w ) Wi n M a i n函数的h i n s t E x e参数一样,这个值用于标识D L L的文件映像被映射到进程的地址空间中的虚拟内存地址。通常应将这个参数保存在一个全局变量中,这样就可以在调用加载资源的函数(如D i a l o g B o x和L o a d S t r i n g)时使用它。最后一个参数是f I m p L o a d,如果D L L是隐含加载的,那么该参数将是个非0值,如果D L L是显式加载的,那么它的值是0。
参数f d w R e a s o n用于指明系统为什么调用该函数。该参数可以使用4个值中的一个。这4个值是: D L L _ P R O C E S S _ AT TA C H、D L L _ P R O C E S S _ D E TA C H、D L L _ T H R E A D _ AT TA C H或D L L _ T H R E A D _ D E TA C H。这些值将在下面介绍。
注意必须记住,D L L使用D l l M a i n函数来对它们进行初始化。当你的D l l M a i n函数执行时,同一个地址空间中的其他D L L可能尚未执行它们的D l l M a i n函数。这意味着它们尚未初始化,因此你应该避免调用从其他D L L中输入的函数。此外,你应该避免从D l l M a i n内部调用L o a d L i b r a r y ( E x )和F r e e L i b r a r y函数,因为这些函数会形式一个依赖性循环。
DLL_PROCESS_ATTACH通知
当D L L被初次映射到进程的地址空间中时,系统将调用该D L L的D l l M a i n函数,给它传递参数f d w R e a s o n的值D L L _ P R O C E S S _ AT TA C H。只有当D L L的文件映像初次被映射时,才会出现这种情况。如果线程在后来为已经映射到进程的地址空间中的D L L调用L o a d L i b r a r y ( E x )函数,那么操作系统只是递增D L L的使用计数,它并不再次用D L L _ P R O C E S S _ AT TA C H的值来调用D L L的D l l M a i n函数。
当处理D L L _ P R O C E S S _ AT TA C H时,D L L应该执行D L L中的函数要求的任何与进程相关的初始化。例如, D L L可能包含需要使用它们自己的堆栈(在进程的地址空间中创建)的函数。
DLL_PROCESS_DETACH通知
D L L从进程的地址空间中被卸载时,系统将调用D L L的D l l M a i n函数,给它传递f d w R e a s o n的值D L L _ P R O C E S S _ D E TA C H。当D L L处理这个值时,它应该执行任何与进程相关的清除操作。例如, D L L可以调用H e a p D e s t r o y函数来撤消它在D L L _ P R O C E S S _ D E TA C H通知期间创建的堆栈。
DLL_THREAD_ATTACH通知
当在一个进程中创建线程时,系统要查看当前映射到该进程的地址空间中的所有D L L文件映像,并调用每个文件映像的带有D L L _ T H R E A D _ AT TA C H值的D l l M a i n函数。这可以告诉所有的D L L执行每个线程的初始化操作。新创建的线程负责执行D L L的所有D l l M a i n函数中的代码。只有当所有的D L L都有机会处理该通知时,系统才允许新线程开始执行它的线程函数。
DLL_THREAD_DETACH通知
让线程终止运行的首选方法是使它的线程函数返回。这使得系统可以调用E x i t T h r e a d来撤消该线程。E x i t T h r e a d函数告诉系统,该线程想要终止运行,但是系统并不立即将它撤消。相反, 它要取出这个即将被撤消的线程, 并让它调用已经映射的D L L 的所有带有D L L _ T H R E A D _ D E TACH 值的D l l M a i n函数。这个通知告诉所有的D L L执行每个线程的清除操作。
DllMain与C/C++运行期库
当编写一个D L L时,你需要得到C / C + +运行期库的某些初始帮助。例如,如果你创建的D L L包含一个全局变量,而这个全局变量是个C + +类的实例。在你顺利地在D l l M a i n函数中使用这个全局变量之前,该变量必须调用它的构造函数。这是由C / C + +运行期库的D L L启动代码来完成的。当你的D L L文件映像被映射到进程的地址空间中时,系统实际上是调用_ D l l M a i n C RTS t a r t u p函数,而不是调用D l l M a i n函数。
延迟加载DLL (但是怎么延迟那?^_^)
Microsoft Visual C++ 6.0提供了一个出色的新特性,它能够使D L L的操作变得更加容易。这个特性称为延迟加载D L L。延迟加载的D L L是个隐含链接的D L L,它实际上要等到你的代码试图引用D L L中包含的一个符号时才进行加载。延迟加载的D L L在下列情况下是非常有用的:
• 如果你的应用程序使用若干个D L L,那么它的初始化时间就比较长,因为加载程序要将所有需要的D L L映射到进程的地址空间中。解决这个问题的方法之一是在进程运行的时候分开加载各个D L L。延迟加载的D L L能够更容易地完成这样的加载。
• 如果调用代码中的一个新函数,然后试图在老版本的系统上运行你的应用程序,而该系统中没有该函数,那么加载程序就会报告一个错误,并且不允许该应用程序运行。你需要一种方法让你的应用程序运行,然后,如果(在运行时)发现该应用程序在老的系统上运行,那么你将不调用遗漏的函数。
函数转发器
函数转发器是D L L的输出节中的一个项目,用于将对一个函数的调用转至另一个D L L中的另一个函数。
DLL转移
M i c r o s o f t给Windows 2000增加了一个D L L转移特性。这个特性能够强制操作系统的加载程序首先从你的应用程序目录中加载文件模块。只有当加载程序无法在应用程序目录中找到该文件时,它才搜索其他目录。为了强制加载程序总是首先查找应用程序的目录,要做的工作就是在应用程序的目录中放入一个文件。该文件的内容可以忽略,但是该文件必须称为A p p N a m e . l o c a l。例如,如果有一个可执行文件的名字是S u p e r A p p . e x e ,那么转移文件必须称为S u p e r A p p . e x e . l o c a l。在系统内部, L o a d L i b r a r y ( E x )已经被修改,以便查看是否存在该文件。如果应用程序的目录中存在该文件,该目录中的模块就已经被加载。如果应用程序的目录中不存在这个模块,L o a d L i b r a r y ( E x )将正常运行。对于已经注册的C O M对象来说,这个特性是非常有用的。它使应用程序能够将它的C O M对象D L L放入自己的目录,这样,注册了相同C O M对象的其他应用程序就无法干扰你的操作。
posted @
2006-09-23 21:02 Jerry Cat 阅读(565) |
评论 (0) |
编辑 收藏
如何在C语言中巧用正则表达式
如果用户熟悉Linux下的sed、awk、grep或vi,那么对正则表达式这一概念肯定不会陌生。由于它可以极大地简化处理字符串时的复杂度,因此现在已经在许多Linux实用工具中得到了应用。千万不要以为正则表达式只是Perl、Python、Bash等脚本语言的专利,作为C语言程序员,用户同样可以在自己的程序中运用正则表达式。
标准的C和C++都不支持正则表达式,但有一些函数库可以辅助C/C++程序员完成这一功能,其中最著名的当数Philip Hazel的Perl-Compatible Regular Expression库,许多Linux发行版本都带有这个函数库。
编译正则表达式
为了提高效率,在将一个字符串与正则表达式进行比较之前,首先要用regcomp()函数对它进行编译,将其转化为regex_t结构:
int
regcomp(regex_t
*
preg,
const
char
*
regex,
int
cflags);
参数regex是一个字符串,它代表将要被编译的正则表达式;参数preg指向一个声明为regex_t的数据结构,用来保存编译结果;参数cflags决定了正则表达式该如何被处理的细节。
如果函数regcomp()执行成功,并且编译结果被正确填充到preg中后,函数将返回0,任何其它的返回结果都代表有某种错误产生。
匹配正则表达式
一旦用regcomp()函数成功地编译了正则表达式,接下来就可以调用regexec()函数完成模式匹配:
int
regexec(
const
regex_t
*
preg,
const
char
*
string
, size_t nmatch,regmatch_t pmatch[],
int
eflags);
typedef
struct
{
regoff_t rm_so;
regoff_t rm_eo;
}
regmatch_t;
参数preg指向编译后的正则表达式,参数string是将要进行匹配的字符串,而参数nmatch和pmatch则用于把匹配结果返回给调用程序,最后一个参数eflags决定了匹配的细节。
在调用函数regexec()进行模式匹配的过程中,可能在字符串string中会有多处与给定的正则表达式相匹配,参数pmatch就是用来保存这些匹配位置的,而参数nmatch则告诉函数regexec()最多可以把多少个匹配结果填充到pmatch数组中。当regexec()函数成功返回时,从string+pmatch[0].rm_so到string+pmatch[0].rm_eo是第一个匹配的字符串,而从string+pmatch[1].rm_so到string+pmatch[1].rm_eo,则是第二个匹配的字符串,依此类推。
释放正则表达式
无论什么时候,当不再需要已经编译过的正则表达式时,都应该调用函数regfree()将其释放,以免产生内存泄漏。
void
regfree(regex_t
*
preg);
函数regfree()不会返回任何结果,它仅接收一个指向regex_t数据类型的指针,这是之前调用regcomp()函数所得到的编译结果。
如果在程序中针对同一个regex_t结构调用了多次regcomp()函数,POSIX标准并没有规定是否每次都必须调用regfree()函数进行释放,但建议每次调用regcomp()函数对正则表达式进行编译后都调用一次regfree()函数,以尽早释放占用的存储空间。
报告错误信息
如果调用函数regcomp()或regexec()得到的是一个非0的返回值,则表明在对正则表达式的处理过程中出现了某种错误,此时可以通过调用函数regerror()得到详细的错误信息。
size_t regerror(
int
errcode,
const
regex_t
*
preg,
char
*
errbuf, size_t errbuf_size);
参数errcode是来自函数regcomp()或regexec()的错误代码,而参数preg则是由函数regcomp()得到的编译结果,其目的是把格式化消息所必须的上下文提供给regerror()函数。在执行函数regerror()时,将按照参数errbuf_size指明的最大字节数,在errbuf缓冲区中填入格式化后的错误信息,同时返回错误信息的长度。
应用正则表达式
最后给出一个具体的实例,介绍如何在C语言程序中处理正则表达式。
#include
<
stdio.h
>
;
#include
<
sys
/
types.h
>
;
#include
<
regex.h
>
;
/**/
/*
取子串的函数
*/
static
char
*
substr(
const
char
*
str, unsigned start, unsigned end)
{
unsigned n
=
end
-
start;
static
char
stbuf[
256
];
strncpy(stbuf, str
+
start, n);
stbuf[n]
=
0
;
return
stbuf;
}
/**/
/*
主程序
*/
int
main(
int
argc,
char
**
argv)
{
char
*
pattern;
int
x, z, lno
=
0
, cflags
=
0
;
char
ebuf[
128
], lbuf[
256
];
regex_t reg;
regmatch_t pm[
10
];
const
size_t nmatch
=
10
;
/**/
/*
编译正则表达式
*/
pattern
=
argv[
1
];
z
=
regcomp(
&
reg, pattern, cflags);
if
(z
!=
0
)
{
regerror(z,
&
reg, ebuf,
sizeof
(ebuf));
fprintf(stderr,
"
%s: pattern '%s' \n
"
, ebuf, pattern);
return
1
;
}
/**/
/*
逐行处理输入的数据
*/
while
(fgets(lbuf,
sizeof
(lbuf), stdin))
{
++
lno;
if
((z
=
strlen(lbuf))
>
;
0
&&
lbuf[z
-
1
]
==
'
\n
'
)
lbuf[z
-
1
]
=
0
;
/**/
/*
对每一行应用正则表达式进行匹配
*/
z
=
regexec(
&
reg, lbuf, nmatch, pm,
0
);
if
(z
==
REG_NOMATCH)
continue
;
else
if
(z
!=
0
)
{
regerror(z,
&
reg, ebuf,
sizeof
(ebuf));
fprintf(stderr,
"
%s: regcom('%s')\n
"
, ebuf, lbuf);
return
2
;
}
/**/
/*
输出处理结果
*/
for
(x
=
0
; x
<
nmatch
&&
pm[x].rm_so
!=
-
1
;
++
x)
{
if
(
!
x) printf(
"
%04d: %s\n
"
, lno, lbuf);
printf(
"
$%d='%s'\n
"
, x, substr(lbuf, pm[x].rm_so, pm[x].rm_eo));
}
}
/**/
/*
释放正则表达式
*/
regfree(
&
reg);
return
0
;
}
上述程序负责从命令行获取正则表达式,然后将其运用于从标准输入得到的每行数据,并打印出匹配结果。执行下面的命令可以编译并执行该程序:
#
gcc regexp.c -o regexp
# ./regexp 'regex[a-z]*' < regexp.c
0003
:
#
include <regex.h>;
$
0
=
'
regex
'
0027
:
regex_t reg;
$
0
=
'
regex
'
0054
:
z
=
regexec(
&
reg
,
lbuf
,
nmatch
,
pm
,
0
);
$
0
=
'
regexec
'
小结
对那些需要进行复杂数据处理的程序来说,正则表达式无疑是一个非常有用的工具。本文重点在于阐述如何在C语言中利用正则表达式来简化字符串处理,以便在数据处理方面能够获得与Perl语言类似的灵活性。
posted @
2006-09-23 02:29 Jerry Cat 阅读(300) |
评论 (0) |
编辑 收藏
Yahoo抛弃3721是明智之举
看到消息Yahoo中国最终将抛弃3721,觉得这真是明智之举。(参考:马云:"不再发展3721 不会选流氓作对手")
3721就是流氓的代名词, 虽然3721的流氓程度也许比起现在后辈的那些新流氓们要文雅一些,但基本就是五十步和百步的区别。 而3721可谓是这些铺天盖地的流氓软件的最大的罪魁祸首 --因为做流氓软件,不但没有被惩罚, 反而发了财 --这才导致更多聪明的脑袋不把力气往正路上去, 想尽办法去做流氓, 搞得全中国几千万台电脑中毒,搞得中国网民不敢随便下载。
有个和孔子有关的故事:
鲁国之法,鲁人为人臣妾於诸侯,有能赎之者,取其金於府。子贡赎鲁人於诸侯,来而让,不取其金。孔子曰:"赐失之矣。自今以往,鲁人不赎人矣。取其金,则无损於行;不取其金,则不复赎人矣。"《吕氏春秋 察微》
子路拯人于溺,其人谢之以牛,子路受之。孔子喜曰:自今鲁国多拯人于溺矣。
翻译如下:
第一则翻译成白话文:春秋时代,鲁国有这样一条法规:凡是鲁国人到其他国家去旅行,看到有鲁国人沦为奴隶,可以自己垫钱把他先赎回,待回鲁国后到官府去报销。官府用国库的钱支付赎金,并给予一定的奖励。子贡到国外去,恰好碰到有一个鲁国人在那里做奴隶,就掏钱赎出了他。回国以后这个学生既没有张扬,也没去报销所垫付的赎金。那个被赎回的人把情况讲给众人,人们都称赞这个学生仗义,人格高尚。一时间,街头巷尾都把这件事当作美谈。孔子知道了这件事,不仅没有表扬这个学生,还对他进行了严厉的批评,责怪他犯了一个有违社会大道的错误,是只为小义而不顾大道。
孔子指出:由于这个学生没有到官府去报销赎金而被人们称赞为品格高尚,那么其他的人在国外看到鲁国人沦为奴隶,就要对是否垫钱把他赎出来产生犹豫。因为垫钱把他赎出来再去官府报销领奖,人们就会说自己不仗义,不高尚;不去官府报销,自己的损失谁来补。于是,多一事不如少一事,只好假装没看见,从客观上讲,这个学生的行为妨碍了更多的在外国做奴隶的鲁国人被赎买回来。
第二则是讲,有人掉进水里,亲人在岸上喊,如果能救上他的,就送恩人一头牛以作报酬,子路听到马上跳下水救起那个人,高兴地接受了报酬。其他人觉得子路贪小利。孔子却表扬了他。说你为大家做了一个榜样,今后再有人遇到险情,大家都会奋不顾身,整个国家就会有许多人因此而得救。权利与义务是对等的,既然行善,没有必要害怕获得相应的权利。
明代的袁子凡对这两则故事有过精辟的论述:自俗眼观之,子贡不受金为优,子路之受牛为劣;孔子则取由而黜赐焉。乃知人之为善,不论现行而论流弊;不论一时而论久远;不论一身而论天下。现行虽善,而其流足以害人;则似善而实非也;现行虽不善,而其流足以济人,则非善而实是也;然此就一节论之耳。他如非义之义,非礼之礼,非信之信,非慈之慈,皆当抉择。
这个故事和流氓关系并不太大, 但说的是一件事情的社会效应。如果流氓总是得逞, 这个社会上流氓的倾向就越大; 反之,如果流氓被打击得越多, 流氓倾向就会越少。
所以是流氓就应该坚决去打击、去鄙视,不管他是有钱的流氓还是没钱的流氓,或者是已经宣称自己不是流氓的流氓,只有这样才能让流氓的习气最终在中国互联网界消失。Yahoo中国现在这种壮士断腕地抛弃3721, 绝对具有长远而深刻的意义。
”不和流氓为伍,不和流氓做对手“,这句话说得好! 支持一下!
posted @
2006-09-13 23:53 Jerry Cat 阅读(363) |
评论 (0) |
编辑 收藏
[转]Windows 语音编程初步
一、SAPI简介
软件中的语音技术包括两方面的内容,一个是语音识别(speech recognition) 和语音合成(speech synthesis)。这两个技术都需要语音引擎的支持。微软推出的应用编程接口API,虽然现在不是业界标准,但是应用比较广泛。
SAPI全称 The Microsoft Speech API.相关的SR和SS引擎位于Speech SDK开发包中。这个语音引擎支持多种语言的识别和朗读,包括英文、中文、日文等。
SAPI包括以下组件对象(接口):
(1)Voice Commands API。对应用程序进行控制,一般用于语音识别系统中。识别某个命令后,会调用相关接口是应用程序完成对应的功能。如果程序想实现语音控制,必须使用此组对象。
(2)Voice Dictation API。听写输入,即语音识别接口。
(3)Voice Text API。完成从文字到语音的转换,即语音合成。
(4)Voice Telephone API。语音识别和语音合成综合运用到电话系统之上,利用此接口可以建立一个电话应答系统,甚至可以通过电话控制计算机。
(5)Audio Objects API。封装了计算机发音系统。
SAPI是架构在COM基础上的,微软还提供了ActiveX控件,所以不仅可用于一般的windows程序,还可以用于网页、VBA甚至EXCEL的图表中。如果对COM感到陌生,还可以使用微软的C++ WRAPPERS,它用C++类封装了语音SDK COM对象。
二、安装SAPI SDK。
首先从这个站点下载开发包:
http://www.microsoft.com/speech/download/sdk51
Microsoft Speech SDK 5.1添加了Automation支持。所以可以在VB,ECMAScript等支持Automation的语言中使用。
版本说明:
Version: 5.1
发布日期: 8/8/2001
语音: English
下载尺寸: 2.0 MB - 288.8 MB
这个SDK开发包还包括了可以随便发布的英文和中文的语音合成引擎(TTS),和英文、中文、日文的语音识别引擎(SR)。
系统要求98以上版本。编译开发包中的例子程序需要vc6以上环境。
******下载说明******:
(1)如果要下载例子程序,说明文档,SAPI以及用于开发的美国英语语音引擎,需要下载SpeechSDK51.exe,大约68M。
(2)如果想要使用简体中文和日文的语音引擎,需要下载SpeechSDK51LangPack.exe。大约82M。
(3)如果想要和自己的软件一起发布语音引擎,需要下载SpeechSDK51MSM.exe,大约132M。
(在这个地址,我未能成功下载)。
(4)如果要获取XP下的 Mike 和 Mary 语音,下载Sp5TTIntXP.exe。大约3.5M。
(5)如果要获取开发包的文档说明,请下载sapi.chm。大约2.3M。这个在sdk51里面已经包含。
下载完毕后,首先安装SpeechSDK51.exe,然后安装中文语言补丁包SpeechSDK51LangPack,然后展开
msttss22l,自动将所需dll安装到系统目录。
三、配置vc环境
在vc6.0的环境下编译语音工程,首先要配置编译环境。假设sdk安装在d:\Microsoft Speech SDK 5.1\路径下,打开工程设置对话框,在c/c++栏中选择Preprocessor分类,然后在"附加包含路径"中输入
d:\Microsoft Speech SDK 5.1\include
告诉vc编译程序所需的SAPI头文件的位置。
然后切换到LINK栏,在Input分类下的附加库路径中输入:
d:\Microsoft Speech SDK 5.1\lib\i386
使vc在链接的时候能够找到sapi.lib。
四、语音合成的应用。即使用SAPI实现TTS(Text to Speech)。
1、首先要初始化语音接口,一般有两种方式:
ISpVoice* pVoice;
::CoInitialize(NULL);
HRESULT hr = CoCreateInstance(CLSID_SpVoice, NULL, CLSCTX_ALL, IID_ISpVoice,
(void **)&pVoice);
然后就可以使用这个指针调用SAPI函数了,例如
pVoice->SetVolume(50);//设置音量
pVoice->Speak(str.AllocSysString(),SPF_ASYNC,NULL);
另外也可以使用如下方式:
CComPtr<ISpVoice> m_cpVoice;
HRESULT hr = m_cpVoice.CoCreateInstance( CLSID_SpVoice );
在下面的例子中都用这个m_cpVoice变量。
CLSID_SpVoice的定义位于SPAI.H中。
2、获取/设置输出频率。
SAPI朗读文字的时候,可以采用多种频率方式输出声音,比如:
8kHz 8Bit Mono、8kHz 8Bit Stereo、44kHz 16Bit Mono、44kHz 16Bit Stereo等。在音调上有所差别。具体可以参考sapi.h。
可以使用如下代码获取当前的配置:
CComPtr<ISpStreamFormat> cpStream;
HRESULT hrOutputStream = m_cpVoice->GetOutputStream(&cpStream);
if (hrOutputStream == S_OK)
{
CSpStreamFormat Fmt;
hr = Fmt.AssignFormat(cpStream);
if (SUCCEEDED(hr))
{
SPSTREAMFORMAT eFmt = Fmt.ComputeFormatEnum();
}
}
SPSTREAMFORMAT 是一个ENUM类型,定义位于SPAI.H中。每一个值对应了不同的频率设置。例如 SPSF_8kHz8BitStereo = 5
通过如下代码设置当前朗读频率:
CComPtr<ISpAudio> m_cpOutAudio; //声音输出接口
SpCreateDefaultObjectFromCategoryId( SPCAT_AUDIOOUT, &m_cpOutAudio ); //创建接口
SPSTREAMFORMAT eFmt = 21; //SPSF_22kHz 8Bit Stereo
CSpStreamFormat Fmt;
Fmt.AssignFormat(eFmt);
if ( m_cpOutAudio )
{
hr = m_cpOutAudio->SetFormat( Fmt.FormatId(), Fmt.WaveFormatExPtr() );
}
else hr = E_FAIL;
if( SUCCEEDED( hr ) )
{
m_cpVoice->SetOutput( m_cpOutAudio, FALSE );
}
3、获取/设置播放所用语音。
引擎中所用的语音数据文件一般保存在SpeechEngines下的spd或者vce文件中。安装sdk后,在注册表中保存了可用的语音,比如英文的男/女,简体中文的男音等。位置是:
HKEY_LOCAL_MACHINE\Software\Microsoft\Speech\Voices\Tokens
如果安装在中文操作系统下,则缺省所用的朗读语音是简体中文。SAPI的缺点是不能支持中英文混读,在朗读中文的时候,遇到英文,只能逐个字母读出。所以需要程序自己进行语音切换。
(1) 可以采用如下的函数把当前SDK支持的语音填充在一个组合框中:
// SAPI5 helper function in sphelper.h
HWND hWndCombo = GetDlgItem( hWnd, IDC_COMBO_VOICES ); //组合框句柄
HRESULT hr = SpInitTokenComboBox( hWndCombo , SPCAT_VOICES );
这个函数是通过IEnumSpObjectTokens接口枚举当前可用的语音接口,把接口的说明文字添加到组合框中,并且把接口的指针作为LPARAM
保存在组合框中。
一定要记住最后程序退出的时候,释放组合框中保存的接口:
SpDestroyTokenComboBox( hWndCombo );
这个函数的原理就是逐个取得combo里面每一项的LPARAM数据,转换成IUnknown接口指针,然后调用Release函数。
(2) 当组合框选择变化的时候,可以用下面的函数获取用户选择的语音:
ISpObjectToken* pToken = SpGetCurSelComboBoxToken( hWndCombo );
(3) 用下面的函数获取当前正在使用的语音:
CComPtr<ISpObjectToken> pOldToken;
HRESULT hr = m_cpVoice->GetVoice( &pOldToken );
(4) 当用户选择的语音和当前正在使用的不一致的时候,用下面的函数修改:
if (pOldToken != pToken)
{
// 首先结束当前的朗读,这个不是必须的。
HRESULT hr = m_cpVoice->Speak( NULL, SPF_PURGEBEFORESPEAK, 0);
if (SUCCEEDED (hr) )
{
hr = m_cpVoice->SetVoice( pToken );
}
}
(5) 也可以直接使用函数SpGetTokenFromId获取指定voice的Token指针,例如:
WCHAR pszTokenId[] = L"HKEY_LOCAL_MACHINE\\Software\\Microsoft\\Speech\\Voices\\Tokens\\MSSimplifiedChineseVoice";
SpGetTokenFromId(pszTokenID , &pChineseToken);
4、开始/暂停/恢复/结束当前的朗读
要朗读的文字必须位于宽字符串中,假设位于szWTextString中,则:
开始朗读的代码:
hr = m_cpVoice->Speak( szWTextString, SPF_ASYNC | SPF_IS_NOT_XML, 0 );
如果要解读一个XML文本,用:
hr = m_cpVoice->Speak( szWTextString, SPF_ASYNC | SPF_IS_XML, 0 );
暂停的代码: m_cpVoice->Pause();
恢复的代码: m_cpVoice->Resume();
结束的代码:(上面的例子中已经给出了)
hr = m_cpVoice->Speak( NULL, SPF_PURGEBEFORESPEAK, 0);
5、跳过部分朗读的文字
在朗读的过程中,可以跳过部分文字继续后面的朗读,代码如下:
ULONG ulGarbage = 0;
WCHAR szGarbage[] = L"Sentence";
hr = m_cpVoice->Skip( szGarbage, SkipNum, &ulGarbage );
SkipNum是设置要跳过的句子数量,值可以是正/负。
根据sdk的说明,目前SAPI仅仅支持SENTENCE这个类型。SAPI是通过标点符号来区分句子的。
6、播放WAV文件。SAPI可以播放WAV文件,这是通过ISpStream接口实现的:
CComPtr<ISpStream> cpWavStream;
WCHAR szwWavFileName[NORM_SIZE] = L"";;
USES_CONVERSION;
wcscpy( szwWavFileName, T2W( szAFileName ) );//从ANSI将WAV文件的名字转换成宽字符串
//使用sphelper.h 提供的这个函数打开 wav 文件,并得到一个 IStream 指针
hr = SPBindToFile( szwWavFileName, SPFM_OPEN_READONLY, &cpWavStream );
if( SUCCEEDED( hr ) )
{
m_cpVoice->SpeakStream( cpWavStream, SPF_ASYNC, NULL );//播放WAV文件
}
7、将朗读的结果保存到wav文件
TCHAR szFileName[256];//假设这里面保存着目标文件的路径
USES_CONVERSION;
WCHAR m_szWFileName[MAX_FILE_PATH];
wcscpy( m_szWFileName, T2W(szFileName) );//转换成宽字符串
//创建一个输出流,绑定到wav文件
CSpStreamFormat OriginalFmt;
CComPtr<ISpStream> cpWavStream;
CComPtr<ISpStreamFormat> cpOldStream;
HRESULT hr = m_cpVoice->GetOutputStream( &cpOldStream );
if (hr == S_OK) hr = OriginalFmt.AssignFormat(cpOldStream);
else hr = E_FAIL;
// 使用sphelper.h中提供的函数创建 wav 文件
if (SUCCEEDED(hr))
{
hr = SPBindToFile( m_szWFileName, SPFM_CREATE_ALWAYS, &cpWavStream,
&OriginalFmt.FormatId(), OriginalFmt.WaveFormatExPtr() );
}
if( SUCCEEDED( hr ) )
{
//设置声音的输出到 wav 文件,而不是 speakers
m_cpVoice->SetOutput(cpWavStream, TRUE);
}
//开始朗读
m_cpVoice->Speak( szWTextString, SPF_ASYNC | SPF_IS_NOT_XML, 0 );
//等待朗读结束
m_cpVoice->WaitUntilDone( INFINITE );
cpWavStream.Release();
//把输出重新定位到原来的流
m_cpVoice->SetOutput( cpOldStream, FALSE );
8、设置朗读音量和速度
m_cpVoice->SetVolume((USHORT)hpos); //设置音量,范围是 0 - 100
m_cpVoice->SetRate(hpos); //设置速度,范围是 -10 - 10
hpos的值一般位于
9、设置SAPI通知消息。SAPI在朗读的过程中,会给指定窗口发送消息,窗口收到消息后,可以主动获取SAPI的事件,
根据事件的不同,用户可以得到当前SAPI的一些信息,比如正在朗读的单词的位置,当前的朗读口型值(用于显
示动画口型,中文语音的情况下并不提供这个事件)等等。
要获取SAPI的通知,首先要注册一个消息:
m_cpVoice->SetNotifyWindowMessage( hWnd, WM_TTSAPPCUSTOMEVENT, 0, 0 );
这个代码一般是在主窗口初始化的时候调用,hWnd是主窗口(或者接收消息的窗口)句柄。WM_TTSAPPCUSTOMEVENT
是用户自定义消息。
在窗口响应WM_TTSAPPCUSTOMEVENT消息的函数中,通过如下代码获取sapi的通知事件:
CSpEvent event; // 使用这个类,比用 SPEVENT结构更方便
while( event.GetFrom(m_cpVoice) == S_OK )
{
switch( event.eEventId )
{
。。。
}
}
eEventID有很多种,比如SPEI_START_INPUT_STREAM表示开始朗读,SPEI_END_INPUT_STREAM表示朗读结束等。
可以根据需要进行判断使用。
四、结束语
SAPI的功能很多,比如语音识别、使用语法分析等,由于条件和精力有限,我未能一一尝试,感兴趣的朋友可以自己安装一个研究一下。
另外提供一个简单例子程序的下载,位置是:
ftp://vckbase:vckbase@210.192.111.117/user/iwaswzq/Universe.rar
posted @
2006-09-13 00:45 Jerry Cat 阅读(3413) |
评论 (1) |
编辑 收藏
翻译者 : myan
出处: http://www.sgi.com/technology/stl
1995年3月,dr.dobb's journal特约记者, 著名技术书籍作家al stevens采访了stl创始人alexander stepanov. 这份访谈纪录是迄今为止对于stl发展历史的最完备介绍, 侯捷先生在他的stl有关文章里推荐大家阅读这篇文章. 因此我将该文全文翻译如下:
q: 您对于generic programming进行了长时间的研究, 请就此谈谈.
a: 我开始考虑有关gp的问题是在7o年代末期, 当时我注意到有些算法并不依赖于数据结构的特定实现,而只是依赖于该结构的几个基本的语义属性. 于是我开始研究大量不同的算法,结果发现大部分算法可以用这种方法从特定实现中抽象出来, 而且效率无损. 对我来说,效率是至关重要的, 要是一种算法抽象在实例化会导致性能的下降, 那可不够棒.
当时我认为这项研究的正确方向是创造一种编程语言. 我和我的两个朋友一起开始干起来.一个是现在的纽约州立大学教授deepak kapur, 另一个是伦塞里尔技术学院教授david musser.当时我们三个在通用电器公司研究中心工作. 我们开始设计一种叫tecton的语言. 该语言有一种我们称为"通用结构"的东西, 其实不过是一些形式类型和属性的集合体, 人们可以用它来描述算法. 例如一些数学方面的结构充许人们在其上定义一个代数操作, 精化之,扩充之, 做各种各样的事.
虽然有很多有趣的创意, 最终该项研究没有取得任何实用成果, 因为tecton语言是函数型语言. 我们信奉backus的理念,相信自己能把编程从von neumann风格中解放出来. 我们不想使用副效应, 这一点限制了我们的能力, 因为存在大量需要使用诸如"状态", "副效应"等观念的算法.
我在70年代末期在tecton上面所认识到了一个有趣的问题: 被广泛接受的adt观念有着根本性的缺陷. 人们通常认为adt的特点是只暴露对象行为特征, 而将实现隐藏起来. 一项操作的复杂度被认为是与实现相关的属性, 所以抽象的时候应予忽略. 我则认识到, 在考虑一个(抽象)操作时, 复杂度(或者至少是一般观念上的复杂度)必须被同时考虑在内. 这一点现在已经成了gp的核心理念之一.
例如一个抽象的栈stack类型, 仅仅保证你push进去的东西可以随后被pop出来是不够的,同样极端重要的是, 不管stack有多大, 你的push操作必须能在常数时间内完成. 如果我写了一个stack, 每push一次就慢一点, 那谁都不会用这个烂玩艺.
我们是要把实现和界面分开, 但不能完全忽略复杂度. 复杂度必须是, 而且也确实是横陈于模块的使用者与实现者之间的不成文契约. adt观念的引入是为了允许软件模块相互可替换. 但除非另一个模块的操作复杂度与这个模块类似, 否则你肯定不愿意实现这种互换.如果我用另外一个模块替换原来的模块, 并提供完全相同的接口和行为, 但就是复杂度不同, 那么用户肯定不高兴. 就算我费尽口舌介绍那些抽象实现的优点, 他肯定还是不乐意用. 复杂度必须被认为是接口的一部分.
1983年左右, 我转往纽约布鲁克林技术大学任教. 开始研究的是图的算法, 主要的合作伙伴是现在ibm的aaron kershenbaum. 他在图和网络算法方面是个专家, 我使他相信高序(high order)的思想和gp能够应用在图的算法中. 他支持我与他合作开始把这些想法用于实际的网络算法. 某些图的算法太复杂了, 只进行过理论分析, 从来没有实现过. 他企图建立一个包含有高序的通用组件的工具箱, 这样某些算法就可以实现了. 我决定使用lisp语言的一个变种scheme语言来建立这样一个工具箱. 我们俩建立了一个巨大的库, 展示了各种编程技术.网络算法是首要目标. 不久当时还在通用电器的david musser加了进来, 开发了更多的组件,一个非常大的库. 这个库供大学里的本科生使用, 但从未商业化. 在这项工作中, 我了解到副效应是很重要的, 不利用副效应, 你根本没法进行图操作. 你不能每次修改一个端点(vertex)时都在图上兜圈子. 所以, 当时得到的经验是在实现通用算法时可以把高序技术和副效应结合起来. 副效应不总是坏的, 只有在被错误使用时才是.
1985年夏, 我回到通用电器讲授有关高序程序设计的课程. 我展示了在构件复杂算法时这项技术的应用. 有一个听课的人叫陈迩, 当时是信息系统实验室的主任. 他问我是否能用ada语言实现这些技术, 形成一个工业强度的库, 并表示可以提供支持. 我是个穷助教, 所以尽管我当时对于ada一无所知, 我还是回答"好的". 我跟dave musser一起建立这个ada库. 这是很重要的一个时期, 从象scheme那样的动态类型语言(dynamically typed language)转向ada这样的强类型语言, 使我认识到了强类型的重要性. 谁都知道强类型有助于纠错. 我则发现在ada的通用编程中, 强类型是获取设计思想的有力工具. 它不仅是查错工具, 而且是思想工具.这项工作给了我对于组件空间进行正交分解的观念. 我认识到, 软件组件各自属于不同的类别.oop的狂热支持者认为一切都是对象. 但我在ada通用库的工作中认识到, 这是不对的. 二分查找就不是个对象, 它是个算法. 此外, 我还认识到, 通过将组件空间分解到几个不同的方向上, 我们可以减少组件的数量, 更重要的是, 我们可以提供一个设计产品的概念框架.
随后, 我在贝尔实验室c++组中得到一份工作, 专事库研究. 他们问我能不能用c++做类似的事.我那时还不懂c++, 但当然, 我说我行. 可结果我不行, 因为1987年时, c++中还没有模板, 这玩意儿在通用编程中是个必需品. 结果只好用继承来获取通用性, 那显然不理想.直到现在c++继承机制也不大用在通用编程中, 我们来说说为什么. 很多人想用继承实现数据结构和容器类, 结果几乎全部一败涂地. c++的继承机制及与之相关的编程风格有着戏剧性的局限. 用这种方式进行通用编程, 连等于判断这类的小问题都解决不了. 如果你以x类作为基类, 设计了一个虚函数operater==, 接受一个x类对象, 并由x派生类y, 那么y的operator==是在拿y类对象与x类对象做比较. 以动物为例, 定义animal类, 派生giraffe(长颈鹿)类. 定义一个成员函数mate(), 实现与另一个哺乳动物的交配操作, 返回一个animal对象. 现在看看你的派生类giraffe,它当然也有一个mate()方法, 结果一个长颈鹿同一个动物交配, 返回一个动物对象. 这成何体统?当然, 对于c++程序员来说, 交配函数没那么重要, 可是operator==就很重要了.
对付这种问题, 你得使用模板. 用模板机制, 一切如愿.
尽管没有模板, 我还是搞出来一个巨大的算法库, 后来成了unix system laboratory standard component library的一部分. 在bell lab, 我从象andy koenig, bjarne stroustrup(andrew koenig, 前iso c++标准化委员会主席; bjarne stroustrup, c++之父 -- 译者)这类专家身上学到很多东西. 我认识到c/c++的重要, 它们的一些成功之处是不能被忽略的. 特别是我发现指针是个好东东. 我不是说空悬的指针, 或是指向栈的指针. 我是说指针这个一般观念. 地址的观念被广泛使用着. 没有指针我们就没法描述并行算法.
我们现在来探讨一下为什么说c是一种伟大的语言. 通常人们认为c是编程利器并且获得如此成功,是因为unix是用c写的. 我不同意. 计算机的体系结构是长时间发展演变的结果, 不是哪一个聪明的人创造的. 事实上是广大程序员在解决实际问题的过程中提出的要求推动了那些天才提出这些体系. 计算机经过多次进化, 现在只需要处理字节地址索引的内存, 线性地址空间和指针. 这个进化结果是对于人们要求解决问题的自然反映. dennis ritchie天才的作品c, 正反映了演化了30年的计算机的最小模型. c当时并不是什么利器. 但是当计算机被用来处理各种问题时, 作为最小模型的c成了一种非常强大的语言, 在各个领域解决各种问题时都非常高效. 这就是c可移植性的奥秘, c是所有计算机的最佳抽象模型, 而且这种抽象确确实实是建立在实际的计算机, 而不是假想的计算机上的. 人们可以比较容易的理解c背后的机器模型, 比理解ada和scheme语言背后的机器模型要容易的多. c的成功是因为c做了正确的事, 不是因为at&t的极力鼓吹和unix.
c++的成功是因为bjarne stroustrup以c为出发点来改进c, 引入更多的编程技术, 但始终保持在c所定义的机器模型框架之内, 而不是闭门造车地自己搞出一个新的机器模型来. c的机器模型非常简单. 你拥有内存, 对象保存在那里面, 你又有指向连续内存空间的指针, 很好理解. c++保留了这个模型, 不过大大扩展了内存中对象的范畴, 毕竟c的数据类型太有限了, 它允许你建立新的类型结构, 但不允许你定义类型方法. 这限制了类型系统的能力. c++把c的机器模型扩展为真正类型系统.
1988年我到惠普实验室从事通用库开发工作. 但实际上好几年我都是在作磁盘驱动器. 很有趣但跟
gp毫不相关. 92年我终于回到了gp领域, 实验室主任bill worley建立了一个算法研究项目, 由我
负责. 那时候c++已经有模板了. 我发现bjarne的模板设计方案是非常天才的. 在bell lab时, 我参
加过有关模班设计的几个早期的讨论, 跟bjarne吵得很凶, 我认为c++的模板设计应该尽可能向ada的
通用方案看齐. 我想可能我吵得太凶了, 结果bjarne决定坚决拒绝我的建议. 我当时就认识到在c++
中设置模板函数的必要性了, 那时候好多人都觉得最好只有模板类. 不过我觉得一个模板函数在使用
之前必须先显式实例化, 跟ada似的. bjarne死活不听我的, 他把模板函数设计成可以用重载机制来
隐式实例化. 后来这个特别的技术在我的工作中变得至关重要, 我发现它容许我做很多在ada中不可能
的任务. 非常高兴bjarne当初没听我的.
q: 您是什么时候第一次构思stl的, 最初的目的是什么?
a: 92年那个项目建立时由8个人, 渐渐地人越来越少, 最后剩下俩, 我和李梦, 而且李小姐是这个领域的新手. 在她的专业研究中编译器是主要工作, 不过她接受了gp研究的想法, 并且坚信此项研究将带给软件开发一个大变化, 要知道那时候有这个信念的认可是寥寥无几. 没有她, 我可不敢想象我能搞定stl, 毕竟stl标着两个人的名字:stepanov和lee. 我们写了一个庞大的库, 庞大的代码量, 庞大的数据结构组件,函数对象, 适配器类, 等等. 可是虽然有很多代码, 却没有文档. 我们的工作被认为是一个验证性项目,其目的是搞清楚到底能不能在使算法尽可能通用化的前提下仍然具有很高的效率. 我们化了很多时间来比较, 结果发现, 我们算法不仅最通用, 而且要率与手写代码一样高效, 这种程序设计风格在性能上是不打折扣的! 这个库在不断成长, 但是很难说它是什么时候成为一个"项目"的. stl的诞生是好几件事情的机缘巧合才促成的.
q: 什么时候, 什么原因促使您决定建议使stl成为ansi/iso标准c++一部分的?
a: 1993年夏, andy koenig跑到斯坦福来讲c++课, 我把一些有关的材料给他看, 我想他当时确实是很兴奋.他安排我9月到圣何塞给c++标准委员会做一个演讲. 我演讲的题目是"c++程序设计的科学", 讲得很理论化, 要点是存在一些c++的基本元素所必须遵循的, 有关基本操作的原则. 我举了一些例子, 比如构造函数, 赋值操作, 相等操作. 作为一种语言, c++没有什么限制. 你可以用operator==()来做乘法. 但是相等操作就应该是相等操作. 它要有自反性, a == a; 它要有对称性, a == b 则 b == a; 它还要有传递性. 作为一个数学公理, 相等操作对于其他操作是基本的要素. 构造函数和相等操作之间的联系就有公理性的东西在里边. 你用拷贝构造函数生成了一个新对象, 那么这个对象和原来那个就应该是相等的. c++是没有做强行要求, 但是这是我们都必须遵守这个规则. 同样的, 赋值操作也必须产生相等的对象. 我展示了一些基本操作的"公理", 还讲了一点迭代子(iterator), 以及一些通用算法怎样利用迭代子来工作. 我觉得那是一个两小时的枯燥演讲, 但却非常受欢迎. 不过我那时并没有想把这个东西塞在标准里, 它毕竟是太过先进的编程技术, 大概还不适于出现在现实世界里, 恐怕那些做实际工作的人对它没什么兴趣.
我是在9月做这个演讲的, 直到次年(1994)月, 我都没往ansi标准上动过什么脑筋. 1月6日, 我收到andy koenig的一封信(他那时是标准文档项目编辑), 信中说如果我希望stl成为标准库的一部分, 可以在1月25日之前提交一份建议到委员会. 我的答复是:"andy, 你发疯了吗?", 他答复道:"不错, 是的我发疯了, 为什么咱们不疯一次试试看?"
当时我们有很多代码, 但是没有文档, 更没有正式的建议书. 李小姐和我每星期工作80小时, 终于在期限之前写出一份正式的建议书. 当是时也, 只有andy一个人知道可能会发生些什么. 他是唯一的支持者, 在那段日子里他确实提供了很多帮助. 我们把建议寄出去了, 然后就是等待. 在写建议的过程中我们做了很多事. 当你把一个东西写下来, 特别是想到你写的可能会成为标准, 你就会发现设计中的所有纰漏. 寄出标准后,我们不得不一段一段重写了库中间的代码, 以及几百个组件, 一直到3月份圣迭戈会议之前. 然后我们又重新修订了建议书, 因为在重新写代码的过程中, 我们又发现建议书中间的很多瑕疵.
q: 您能描述一下当时委员会里的争论吗? 建议一开始是被支持呢, 还是反对?
a: 我当时无法预料会发生些什么. 我做了一个报告, 反响很好. 但当时有许多反对意见. 主要的意见是:这是一份庞大的建议, 而且来得太晚, 前一次会议上已经做出决议, 不在接受任何大的建议. 而这个东西是有史以来最大的建议, 包括了一大堆新玩艺. 投票的结果很有趣, 压倒多数的意见认为应对建议进行再考虑, 并把投票推迟到下次会议, 就是后来众所周知的滑铁卢会议.
bjarne stroustrup成了stl的强有力支持者. 很多人都通过建议、更改和修订的方式给予了帮助。bjarne干脆跑到这来跟我们一起工作了一个礼拜。andy更是无时无刻的帮助我们。c++是一种复杂的语言,不是总能搞得清楚确切的含义的。差不多每天我都要问andy和bjarne c++能不能干这干那。我得把特殊的荣誉归于andy, 是他提出把stl作为c++标准库的一部分;而bjarne也成了委员会中stl的主要鼓吹者。其他要感谢的人还有:mike vilot,标准库小组的负责人; rogue wave公司的nathan myers(rogue wave是boland c++builder中stl方案的提供商 —— 译者),andersen咨询公司的larry podmolik。确实有好多人要致谢。
在圣迭戈提出的stl实际与当时的c++,我们被要求用新的ansi/iso c++语言特性重写stl,这些特性中有一些是尚未实现的。为了正确使用这些新的、未实现的c++特性,bjarne和andy花了无以计数的时间来帮助我们。
人们希望容器独立于内存模式,这有点过分,因为语言本身并没有包括内存模式。所以我们得要想出一些机制来抽象内存模式。在stl的早期版本里,假定容器的容积可以用size_t类型来表示,迭代子之间的距离可以用ptrdiff_t来表示。现在我们被告知,你为什么不抽象的定义这些类型?这个要求比较高,连语言本身都没有抽象定义这些类型,而且c/c++数组还不能被这些类型定义所限定。我们发明了一个机制称作"allocator",封装了内存模式的信息。这个机制深刻地影响了库中间的每一个组件。你可能疑惑:内存模式和算法或者容器类接口有什么关系?如果你使用size_t这样的东西,你就无法使用 t* 对象,因为存在不同的指针类型(t*, t huge *, 等等)。这样你就不能使用引用,因为内存模式不同的话,会产成不同的引用类型。这样就会导致标准库产生庞大的分支。
另外一件重要的事情是我们原先的关联类型数据结构被扩展了。这比较容易一些,但是最为标准的东西总是很困难的,因为我们做的东西人们要使用很多年。从容器的观点看,stl做了十分清楚的二分法设计。所有的容器类被分成两种:顺序的和关联的,就好像常规的内存和按内容寻址的内存一般。这些容器的语义十分清楚。
当我到滑铁卢以后,bjarne用了不少时间来安慰我不要太在意成败与否,因为虽然看上去似乎不会成功,但是我们毕竟做到了最好。我们试过了,所以应该坦然面对。成功的期望很低。我们估计大部分的意见将是反对。但是事实上,确实有一些反对意见,但不占上风。滑铁卢投票的结果让人大跌眼镜,80%赞成,20%反对。所有人都预期会有一场恶战,一场大论战。结果是确实有争论,但投票是压倒性的。
q: stl对于1994年2月发行的ansi/iso c++工作文件中的类库有何影响?
a: stl被放进了滑铁卢会议的工作文件里。stl文档被分解成若干部分,放在了文件的不同部分中。mike
vilot负责此事。我并没有过多地参与编辑工作,甚至也不是c++委员会的成员。不过每次有关stl的
建议都由我来考虑。委员会考虑还是满周到的。
q: 委员会后来又做了一些有关模板机制的改动,哪些影响到了stl?
a: 在stl被接受之前,有两个变化影响到了我们修订stl。其一是模板类增加了包含模板函数的能力。stl广泛地使用了这个特性来允许你建立各种容纳容器的容器。一个单独的构造函数就能让你建立一个能容纳list或其他容器的vector。还有一个模板构造函数,从迭代子构造容器对象,你可以用一对迭代子当作参数传给它,这对迭代子之间的元素都会被用来构造新的容器类对象。另一个stl用到的新特性是把模板自身当作模板参数传给模板类。这项技术被用在刚刚提到的allocator中。
q: 那么stl影响了模板机制吗?
a: 在弗基山谷的会议中,bjarne建议给模板增加一个“局部特殊化”(partial specialization)的特性。这个特性可以让很多算法和类效率更高,但也会带来代码体积上的问题。我跟bjarne在这个建议上共同研究了一段时间,这个建议就是为了使stl更高效而提出的。我们来解释一下什么是“局部特殊化”。你现在有一个模板函数 swap( t&, t& ),用来交换两个参数。但是当t是某些特殊的类型参数时,你想做一些特殊的事情。例如对于swap( int&, int& ),你想用一种特别的操作来交换数据。这一点在没有局部特殊化机制的情况下是不可能的。有了局部特殊化机制,你可以声明一个模板函数如下:
template <class t> void swap( vector<t>&, vector<t>& );
这种形式给vector容器类的swap操作提供了一种特别的办法。从性能的角度讲,这是非常重要的。如果你用通用的形式去交换vector,会使用三个赋值操作,vector被复制三次,时间复杂度是线性的。然而,如果我们有一个局部特殊化的swap版本专门用来交换两个vector,你可以得到一个时间复杂度为常数的,非常快的操作,只要移动vector头部的两个指针就ok。这能让vector上的sort算法运行得更快。没有局部特殊化,让某一种特殊的vector,例如vector<int>运行得更快的唯一办法是让程序员自己定一个特殊的swap函数,这行得通,但是加重了程序员的负担。在大部分情况下,局部特殊化机制能够让算法在某些通用类上表现得更高效。你有最通用的swap,不那么通用的swap,更不通用的swap,完全特殊的swap这么一系列重载的swap,然后你使用局部特殊化,编译器会自动找到最接近的那个swap。另一个例子copy。现在我们的copy就是通过迭代子一个一个地拷贝。使用模板特殊化可以定义一个模板函数:
template <class t> t** copy( t**, t**, t** );
这可以用memcpy高效地拷贝一系列指针来实现,因为是指针拷贝,我们可以不必担心构造对象和析构对象的开销。这个模板函数可以定义一次,然后供整个库使用,而且用户不必操心。我们使用局部特殊化处理了一些算法。这是个重要的改进,据我所知在弗基山谷会议上得到了好评,将来会成为标准的一部分。(后来的确成了标准的一部分 —— 译者)
q: 除了标准类库外,stl对那一类的应用程序来说最有用处?
a: 我希望stl能够引导大家学习一种新的编程风格:通用编程。我相信这种风格适用于任何种类的应用程序。这种风格就是:用最通用的方式来写算法和数据结构。这些结构所要求的语义特性应该能够被清楚地归类和分类,而这些归类分类的原则应该是任何对象都能满足的。理解和发展这种技术还要很长时间,stl不过是这个过程的起点。
我们最终会对通用的组件有一个标准的分类,这些组件具有精心定义的接口和复杂度。程序员们将不必在微观层次上编程。你再也不用去写一个二分查找算法。就是在现在,stl也已经提供了好几个通用的二分查找算法,凡是能用二分查找算法的场合,都可以使用这些算法。算法所要求的前提条件很少:你只要在代码里使用它。我希望所有的组件都能有这么一天。我们会有一个标准的分类,人们不用再重复这些工作。
这就是douglas mcilroy的梦想,他在1969年关于“构件工厂”的那篇著名文章中所提出来的东西。stl就是这种“构件工厂”的一个范例。当然,还需要有主流的力量介入这种技术的发展之中,光靠研究机构不行,工业界应该想程序员提供组件和工具,帮助他们找到所需的组件,把组件粘合到一起,然后确定复杂度是否达到预期。
q: stl没有实现一个持久化(persistent)对象容器模型。map和multimap似乎是比较好的候选者,它们可以把对象按索引存入持久对象数据库。您在此方向上做了什么工作吗,或者对这类实现有何评论?
a:很多人都注意到这个问题。stl没实现持久化是有理由的。stl在当时已经是能被接受的最巨大的库了。再大一点的话,我认为委员会肯定不会接受。当然持久化是确实是一些人提出的问题。在设计stl,特别是设计allocator时,bjarne认为这个封装了内存模式的组件可以用来封装持久性内存模式。bjarne的洞察秋毫非常的重要和有趣,好几个对象数据库公司正在盯着这项技术。1994年10月我参加了object database management group的一个会议,我做了一个关于演说。他们非常感兴趣,想让他们正在形成中的组件库的接口与stl一致,但不包括allocator在内。不过该集团的某些成员仔细分析了allocator是否能够被用来实现持久化。我希望与stl接口一致的组件对象持久化方案能在接下来的一年里出现。
q:set,multiset,map和multimap是用红黑树实现的,您试过用其他的结构,比如b*树来实现吗?
a:我不认为b*适用于内存中的数据结构,不过当然这件事还是应该去做的。应该对许多其他的数据结构,比如跳表(skip list)、伸展树(splay tree)、半平衡树(half-balanced tree)等,也实现stl容器的标准接口。应该做这样的研究工作,因为stl提供了一个很好的框架,可以用来比较这些结构的性能。结口是固定的,基本的复杂度是固定的,现在我们就可一个对各种数据结构进行很有意义的比较了。在数据结构领域里有很多人用各种各样的接口来实现不同的数据结构,我希望他们能用stl框架来把这些数据结构变成通用的。
(译者注:上面所提到的各种数据结构我以为大多并非急需,而一个stl没有提供而又是真正重要的数据结构是哈希结构。后来在stepanov和matt austern等人的sgi*stl中增补了hashset,hashmap和hashtable三种容器,使得这个stl实现才比较完满。众所周知,红黑树的时间复杂度为o(logn), 而理想hash结构为o(1)。当然,如果实现了持久化,b+树也是必须的。)
q:有没有编译器厂商跟您一起工作来把stl集成到他们的产品中去?
a:是的,我接到了很多厂家的电话。borland公司的peter becker出的力特别大。他帮助我实现了对应borland编译器的所有内存模式的allocator组件。symantec打算为他们的macintosh编译器提供一个stl实现。edison设计集团也很有帮助。我们从大多数编译器厂商都得到了帮助。
(译者注:以目前的stl版本来看,最出色的无疑是sgi*stl和ibm stl for as/390,所有windows下的的stl实现都不令人满意。根据测试数据,windows下最好的stl运行在piii 500mhz上的速度远远落后与在250mhz sgi工作站(irix操作系统)上运行的sgi*stl。以我个人经验,linux也是运行stl的极佳平台。而在windows的stl实现中,又以borland c++builder的rogue wave stl为最差,其效率甚至低于jit执行方式下的java2。visual c++中的stl是著名大师p. j. plauger的个人作品,性能较好,但其queue组件效率很差,慎用)
q:stl包括了对ms-dos的16位内存模式编译器的支持,不过当前的重点显然是在32位上线性内存模式(flat model)的操作系统和编译器上。您觉得这种面向内存模式的方案以后还会有效吗?
a:抛开intel的体系结构不谈,内存模式是一个对象,封装了有关指针的信息:这个指针的整型尺寸和距离类型是什么,相关的引用类型是什么,等等。如果我们想利用各种内存,比如持久性内存,共享内存等等,抽象化的工作就非常重要了。stl的一个很漂亮的特性是整个库中唯一与机器类型相关的部分——代表真实指针,真实引用的组件——被封装到大约16行代码里,其他的一切,容器、算法等等,都与机器无关(真是牛啊!)。从移植的观点看,所有及其相关的东西,象是地址记法,指针等,都被封装到一个微小的,很好理解的机制里面。这样一来,allocator对于stl而言就不是那么重要了,至少不像对于基本数据结构和算法的分解那么重要。
q:ansi/iso c标准委员会认为像内存模式这类问题是平台相关的,没有对此做出什么具体规定。c++委员会会不会采取不同的态度?为什么?
a:我认为stl在内存模式这一点上跟c++标准相比是超前的。但是在c和c++之间有着显著的不同。c++有构造函数和new操作符来对付内存模式问题,而且它们是语言的一部分。现在看来似乎让new操作符像stl容器使用allocater那样来工作是很有意义的。不过现在对问题的重要性不像stl出现之前那么显著了,因为在大多数场合,stl数据结构将让new失业。大部分人不再需要分配一个数组,因为stl在做这类事情上更为高效。要知道我对效率的迷信是无以复加的,可我在我的代码里从不使用new,汇编代码表明其效率比使用new时更高。随着stl的广泛使用,new会逐渐淡出江湖。而且stl永远都会记住回收内存,因为当一个容器,比如vector退出作用域时,它的析构函数被调用,会把容器里的所有东西都析构。你也不必再担心内存泄漏了。stl可以戏剧性地降低对于垃圾收集机制的需求。使用stl容器,你可以为所欲为,不用关心内存的管理,自有stl构造函数和析构函数来对付。
q:c++标准库子委员会正在制订标准名空间(namespace)和异常处理机制。stl类会有名空间吗,会抛出异
常吗?
a:是的。该委员会的几个成员正在考虑这件事,他们的工作非常卓越。
q:现在的stl跟最终作为标准的stl会有多大不同?委员会会不会干预某些变化,新的设计会不会被严格地控
制起来?
a:多数人的意见看起来是不希望对stl做任何重要的改变。
q:在成为标准之前,程序员们怎样的一些stl经验?
a:他们可以从butler.hpl.hp.com/stl当下stl头文件,在borland和ibm或其他足够强劲的的编译器中使用它。学习这种编程技术的唯一途径是编程,看看范例,试着用这种技术来编程。
q:您正在和p. j. plauger合作一本stl的书。那本书的重点是什么?什么时候面世?
a:计划95年夏天面世,重点是对stl实现技术的详解,跟他那本标准c库实现和标准c++库实现的书类似。他是
这本书的第一作者。该书可以作为stl的参考手册。我希望跟bjarne合作另写一本书,在c++/stl背景下介绍语言与库的交互作用。
好多工作都等着要做。为了stl的成功,人们需要对这种编程技术进行更多的试验性研究,更多的文章和书籍应该对此提供帮助。要准备开设此类课程,写一些入门指南,开发一些工具帮助人们漫游stl库。stl是一个
框架,应该有好的工具来帮助使用这个框架。
(译者注:他说这番话时,并没有预计到在接下来的几年里会发生什么。由于internet的大爆炸和java、vb、delphi等语言的巨大成功,工业界的重心一下子从经典的软件工程领域转移到internet上。再加上标准c++直到98年才制订,完全符合要求的编译器直到现在都还没有出现,stl并没有立刻成为人们心中的关注焦点。他提到的那本书也迟迟不能问世,直到前几天(2001年元旦之后),这本众人久已期盼的书终于问世,由p. j. plauger, alexander stepanov, meng lee, david musser四大高手联手奉献,prentice hall出版。不过该书主要关注的是stl的实现技术,不适用于普通程序员。
另外就p. j. plauger做一个简介:其人是标准c中stdio库的早期实现者之一,91年的一本关于标准c库的书使他名满天下。他现在是c/c++ use's journal的主编,与microsoft保持着良好的,甚至是过分亲密的关系,visual c++中的stl和其他的一些内容就是出自他的那只生花妙笔。不过由于跟ms的关系已经影响到了他的中立形象,现在有不少人对他有意见。
至于stepanov想象中的那本与stroustrup的书,起码目前是没听说。其实这两位都是典型的编程圣手,跟ken thompson和dennis ritchie是一路的,懒得亲自写书,往往做个第二作者。如果作为第一作者,写出来的书肯定是学院味十足,跟标准文件似的,不适合一般程序员阅读。在计算机科学领域,编程圣手同时又是写作高手的人是凤毛麟角,最著名的可能是外星人d. e. knuth, c++领域里则首推前面提到的andrew koenig。可惜我们中国程序员无缘看到他的书。)
q:通用编程跟oop之间有什么关系?
a:一句话,通用编程是oop基本思想的自然延续。什么是oop的基本思想呢?把组件的实现和接口分开,并且让组件具有多态性。不过,两者还是有根本的不同。oop强调在程序构造中语言要素的语法。你必须得继承,使用类,使用对象,对象传递消息。gp不关心你继承或是不继承,它的开端是分析产品的分类,有些什么种类,他们的行为如何。就是说,两件东西相等意味着什么?怎样正确地定义相等操作?不单单是相等操作那么简单,你往深处分析就会发现“相等”这个一般观念意味着两个对象部分,或者至少基本部分是相等的,据此我们就可以有一个通用的相等操作。再说对象的种类。假设存在一个顺序序列和一组对于顺序序列的操作。那么这些操作的语义是什么?从复杂度权衡的角度看,我们应该向用户提供什么样的顺序序列?该种序列上存在那些操作?那种排序是我们需要的?只有对这些组件的概念型分类搞清楚了,我们才能提到实现的问题:使用模板、继承还是宏?使用什么语言和技术?gp的基本观点是把抽象的软件组件和它们的行为用标准的分类学分类,出发点就是要建造真实的、高效的和不取决于语言的算法和数据结构。当然最终的载体还是语言,没有语言没法编程。stl使用c++,你也可以用ada来实现,用其他的语言来实现也行,结果会有所不同,但基本的东西是一样的。到处都要用到二分查找和排序,而这就是人们正在做的。对于容器的语义,不同的语言会带来轻微的不同。但是基本的区别很清楚是gp所依存的语义,以及语义分解。例如,我们决定需要一个组件swap,然后指出这个组件在不同的语言中如果工作。显然重点是语义以及语义分类。而oop所强调的(我认为是过分强调的)是清楚的定义类之间的层次关系。oop告诉了你如何建立层次关系,却没有告诉你这些关系的实质。
(这段不太好理解,有一些术语可能要过一段时间才会有合适的中文翻译——译者)
q:您对stl和gp的未来怎么看?
a:我刚才提到过,程序员们的梦想是拥有一个标准的组件仓库,其中的组件都具有良好的、易于理解的和标准的接口。为了达成这一点,gp需要有一门专门的科学来作为基础和支柱。stl在某种程度上开始了这项工作,它对于某些基本的组件进行了语义上的分类。我们要在这上面下更多的功夫,目标是要将软件工程从一种手工艺技术转化为工程学科。这需要一门对于基本概念的分类学,以及一些关于这些基本概念的定理,这些定理必须是容易理解和掌握的,每一个程序员即使不能很清楚的知道这些定理,也能正确地使用它。很多人根本不知道交换律,但只要上过学的人都知道2+5等于5+2。我希望所有的程序员都能学习一些基本的语义属性和基本操作:赋值意味着什么?相等意味着什么?怎样建立数据结构,等等。
当前,c++是gp的最佳载体。我试过其他的语言,最后还是c++最理想地达成了抽象和高效的统一。但是我觉得可能设计出一种语言,基于c和很多c++的卓越思想,而又更适合于gp。它没有c++的一些缺陷,特别是不会像c++一样庞大。stl处理的东西是概念,什么是迭代子,不是类,不是类型,是概念。说得更正式一些,这是bourbaki所说的结构类型(structure type),是逻辑学家所说的理念(theory),或是类型理论学派的人所说的种类(sort),这种东西在c++里没有语言层面上的对应物(原文是incarnation,直译为肉身——译者),但是可以有。你可以拥有一种语言,使用它你可以探讨概念,精化概念,最终用一种非常“程序化”(programmatic,直译为节目的,在这里是指符合程序员习惯的——译者)的手段把它们转化为类。当然确实有一些语言能处理种类(sorts),但是当你想排序(sort)时它们没什么用处。我们能够有一种语言,用它我们能定义叫做foward iterator(前向迭代子)的东西,在stl里这是个概念,没有c++对应物。然后我们可以从forword iterator中发展出bidirectional iterator(双向迭代子),再发展出random iterator。可能设计一种语言大为简化gp,我完全相信该语言足够高效,其机器模型与c/c++充分接近。我完全相信能够设计出一种语言,一方面尽可能地靠近机器层面以达成绝对的高效,另一方面能够处理非常抽象化的实体。我认为该语言的抽象性能够超过c++,同时又与底层的机器之间契合得天衣无缝。我认为gp会影响到语言的研究方向,我们会有适于gp的实用语言。从这些话中你应该能猜出我下一步的计划。
mengyan
译于2001年1月
posted @
2006-09-11 07:31 Jerry Cat 阅读(144) |
评论 (0) |
编辑 收藏