weitom1982

向各位技术前辈学习,学习再学习.
posts - 299, comments - 79, trackbacks - 0, articles - 0
  IT博客 :: 首页 :: 新随笔 :: 联系 :: 聚合  :: 管理


原著:Michael Dunn

作者:Chengjie Sun


原文出处:CodeProject:The Complete Guide to C++ Strings, Part II



引言

  因为C语言风格的字符串容易出错且不易管理,黑客们甚至利用可能存在的缓冲区溢出bug把C语言风格的字符串作为攻击目标,所以出现了很多字符串封装类。不幸的是,在某些场合下我们不知道该使用哪个字符串类,也不知道怎样把一个C风格的字符串转换成一个字符串封装类。
  这篇文章将介绍所有在Win32 API, MFC, STL, WTL 和 Visual C++ 运行库中出现的字符串类型。我将描述每一个类的用法,告诉大家怎样创建每一个类的对象以及怎样把一个类转换成其他类。受控字符串和Visual C++ 7中的类两部分是Nish完成的。
  为了更好的从这篇文章中受益,你必须要明白不同的字符类型和编码,这些内容我在第一部分中介绍过。

Rule #1 of string classes

  使用cast来实现类型转换是不好的做法,除非有文档明确指出这种转换可以使用。
促使我写这两篇文章的原因是字符串类型转换中经常遇到的一些问题。当我们使用cast把字符串从类型X转换到类型Z的时候,我们不知道为什么代码不能正常工作。各种各样的字符串类型,尤其是BSTR,几乎没有在任何一个地方的文档中被明确的指出可以用cast来实现类型转换。所以我想一些人可能会使用cast来实现类型转换并希望这种转换能够正常工作。
  除非源字符串是一个被明确指明支持转换操作符的字符串包装类,否则cast不对字符串做任何转换。对常量字符串使用cast不会起到任何作用,所以下面的代码:

void SomeFunc ( LPCWSTR widestr );
main()
{
  SomeFunc ( (LPCWSTR) "C:\\foo.txt" );  // WRONG!
}      
  肯定会失败。它可以被编译,因为cast操作会撤消编译器的类型检查。但是,编译可以通过并不能说明代码是正确的。
  在下面的例子中,我将会指明cast在什么时候使用是合法的。
C-style strings and typedefs

  正如我在第一部分中提到的,windows APIs 是用TCHARs来定义的,在编译时,它可以根据你是否定义_MBCS或者_UNICODE被编译成MBCS或者Unicode字符。你可以参看第一部分中对TCHAR的完整描述,这里为了方便,我列出了字符的typedefs

TypeMeaning
WCHARUnicode character (wchar_t)
TCHARMBCS or Unicode character, depending on preprocessor settings
LPSTR string of char (char*)
LPCSTRconstant string of char (const char*)
LPWSTR string of WCHAR (WCHAR*)
LPCWSTR constant string of WCHAR (const WCHAR*)
LPTSTR string of TCHAR (TCHAR*)
LPCTSTR constant string of TCHAR (const TCHAR*)

  一个增加的字符类型是OLETYPE。它表示自动化接口(如word提供的可以使你操作文档的接口)中使用的字符类型。这种类型一般被定义成wchar_t,然而如果你定义了OLE2ANSI预处理标记,OLECHAR将会被定义成char类型。我知道现在已经没有理由定义OLE2ANSI(从MFC3以后,微软已经不使用它了),所以从现在起我将把OLECHAR当作Unicode字符。
这里给出你将会看到的一些OLECHAR相关的typedefs:

TypeMeaning
OLECHAR Unicode character (wchar_t)
LPOLESTR string of OLECHAR (OLECHAR*)
LPCOLESTR constant string of OLECHAR (const OLECHAR*)

  还有两个用于包围字符串和字符常量的宏定义,它们可以使同样的代码被用于MBCS和Unicode builds :

Type Meaning
_T(x)Prepends L to the literal in Unicode builds.
OLESTR(x)Prepends L to the literal to make it an LPCOLESTR.

  在文档或例程中,你还会看到好多_T的变体。有四个等价的宏定义,它们是TEXT, _TEXT, __TEXT和__T,它们都起同样的做用。

COM 中的字符串 —— BSTR 和 VARIANT

  很多自动化和COM接口使用BSTR来定义字符串。BSTRs中有几个"陷阱",所以这里我用单独的部分来说明它。
  BSTR 是 Pascal-style 字符串(字符串长度被明确指出)和C-style字符串(字符串的长度要通过寻找结束符来计算)的混合产物。一个BSTR是一个Unicode字符串,它的长度是预先考虑的,并且它还有一个0字符作为结束标记。下面是一个BSTR的示例:

 

06 00 00 0042 006F 0062 0000 00
--length--BobEOS

  注意字符串的长度是如何被加到字符串数据中的。长度是DWORD类型的,保存了字符串中包含的字节数,但不包括结束标记。在这个例子中,"Bob"包含3个Unicode字符(不包括结束符),总共6个字节。字符串的长度被预先存储好,以便当一个BSTR在进程或者计算机之间被传递时,COM库知道多少数据需要传送。(另一方面,一个BSTR能够存储任意数据块,而不仅仅是字符,它还可以包含嵌入在数据中的0字符。然而,由于这篇文章的目的,我将不考虑那些情况)。
  在 C++ 中,一个 BSTR 实际上就是一个指向字符串中第一个字符的指针。它的定义如下:

BSTR bstr = NULL;
  bstr = SysAllocString ( L"Hi Bob!" ); 
  if ( NULL == bstr )
    // out of memory error 
  // Use bstr here...
 SysFreeString ( bstr );      
自然的,各种各样的BSTR封装类为你实现内存管理。
  另外一个用在自动化接口中的变量类型是VARIANT。它被用来在无类型(typeless)语言,如Jscript和VBScript,来传递数据。一个VARIANT可能含有很多不同类型的数据,例如long和IDispatch*。当一个VARIANT包含一个字符串,字符串被存成一个BSTR。当我后面讲到VARIANT封装类时,我会对VARIANT多些介绍。

字符串封装类

  到目前为止,我已经介绍了各种各样的字符串。下面,我将说明封装类。对于每个封装类,我将展示怎样创建一个对象及怎样把它转换成一个C语言风格的字符串指针。C语言风格的字符串指针对于API的调用,或者创建一个不同的字符串类对象经常是必需的。我不会介绍字符串类提供的其他操作,比如排序和比较。
  重复一遍,除非你确切的明白结果代码将会做什么,否则不要盲目地使用cast来实现类型转换。

CRT提供的类

_bstr_t
  _bstr_t是一个对BSTR的完整封装类,实际上它隐藏了底层的BSTR。它提供各种构造函数和操作符来访问底层的C语言风格的字符串。然而,_bstr_t却没有访问BSTR本身的操作符,所以一个_bstr_t类型的字符串不能被作为输出参数传给一个COM方法。如果你需要一个BSTR*参数,使用ATL类CComBSTR是比较容易的方式。
  一个_bstr_t字符串能够传给一个接收参数类型为BSTR的函数,只是因为下列3个条件同时满足。首先,_bstr_t有一个向wchar_t*转换的转换函数;其次,对编译器而言,因为BSTR的定义,wchar_t*和BSTR有同样的含义;第三,_bstr_t内部含有的wchar_t*指向一片按BSTR的形式存储数据的内存。所以,即使没有文档说明,_bstr_t可以转换成BSTR,这种转换仍然可以正常进行。
// Constructing
_bstr_t bs1 = "char string";       // construct from a LPCSTR
_bstr_t bs2 = L"wide char string"; // construct from a LPCWSTR
_bstr_t bs3 = bs1;                 // copy from another _bstr_t
_variant_t v = "Bob";
_bstr_t bs4 = v;                   // construct from a _variant_t that has a string
 
// Extracting data
LPCSTR psz1 = bs1;              // automatically converts to MBCS string
LPCSTR psz2 = (LPCSTR) bs1;     // cast OK, same as previous line
LPCWSTR pwsz1 = bs1;            // returns the internal Unicode string
LPCWSTR pwsz2 = (LPCWSTR) bs1;  // cast OK, same as previous line
BSTR    bstr = bs1.copy();      // copies bs1, returns it as a BSTR
 
  // ...
SysFreeString ( bstr );      
  注意_bstr_t也提供char*和wchar_t*之间的转换操作符。这是一个值得怀疑的设计,因为即使它们是非常量字符串指针,你也一定不能使用这些指针去修改它们指向的缓冲区的内容,因为那将破坏内部的BSTR结构。

_variant_t
  _variant_t是一个对VARIANT的完整封装,它提供很多构造函数和转换函数来操作一个VARIANT可能包含的大量的数据类型。这里,我将只介绍与字符串有关的操作。
// Constructing
_variant_t v1 = "char string";       // construct from a LPCSTR
_variant_t v2 = L"wide char string"; // construct from a LPCWSTR
_bstr_t bs1 = "Bob";
_variant_t v3 = bs1;                 // copy from a _bstr_t object
 
// Extracting data
_bstr_t bs2 = v1;           // extract BSTR from the VARIANT
_bstr_t bs3 = (_bstr_t) v1; // cast OK, same as previous line      
注意
  如果类型转换不能被执行,_variant_t方法能够抛出异常,所以应该准备捕获_com_error异常。

还需要注意的是
  没有从一个_variant_t变量到一个MBCS字符串的直接转换。你需要创建一个临时的_bstr_t变量,使用提供Unicode到MBCS转换的另一个字符串类或者使用一个ATL转换宏。
  不像_bstr_t,一个_variant_t变量可以被直接作为参数传递给一个COM方法。_variant_t
  继承自VARIANT类型,所以传递一个_variant_t来代替VARIANT变量是C++语言所允许的。

STL 类
  STL只有一个字符串类,basic_string。一个basic_string管理一个以0做结束符的字符串数组。字符的类型是basic_string模般的参数。总的来说,一个basic_string类型的变量应该被当作不透明的对象。你可以得到一个指向内部缓冲区的只读指针,但是任何写操作必须使用basic_string的操作符和方法。
  basic_string有两个预定义的类型:包含char的string类型和包含wchar_t的wstring类型。这里没有内置的包含TCHAR的类型,但是你可以使用下面列出的代码来实现。
// Specializations
typedef basic_string tstring; // string of TCHARs
 
// Constructing
string str = "char string";         // construct from a LPCSTR
wstring wstr = L"wide char string"; // construct from a LPCWSTR
tstring tstr = _T("TCHAR string");  // construct from a LPCTSTR
 
// Extracting data
LPCSTR psz = str.c_str();    // read-only pointer to str''s buffer
LPCWSTR pwsz = wstr.c_str(); // read-only pointer to wstr''s buffer
LPCTSTR ptsz = tstr.c_str(); // read-only pointer to tstr''s buffer
  不像_bstr_t,一个basic_string变量不能在字符集之间直接转换。然而,你可以传递由c_str()返回的指针给另外一个类的构造函数(如果这个类的构造函数接受这种字符类型)。例如:
// Example, construct _bstr_t from basic_string
_bstr_t bs1 = str.c_str();  // construct a _bstr_t from a LPCSTR
_bstr_t bs2 = wstr.c_str(); // construct a _bstr_t from a LPCWSTR      
ATL 类

CComBSTR
  CComBSTR 是 ATL 中的 BSTR 封装类,它在某些情况下比_bstr_t有用的多。最引人注意的是CComBSTR允许访问底层的BSTR,这意味着你可以传递一个CComBSTR对象给COM的方法。CComBSTR对象能够替你自动的管理BSTR的内存。例如,假设你想调用下面这个接口的方法:
// Sample interface:
struct IStuff : public IUnknown
{
  // Boilerplate COM stuff omitted...
  STDMETHOD(SetText)(BSTR bsText);
  STDMETHOD(GetText)(BSTR* pbsText);
};      
  CComBSTR有一个操作符--BSTR方法,所以它能直接被传给SetText()函数。还有另外一个操作--&,这个操作符返回一个BSTR*。所以,你可以对一个CComBSTR对象使用&操作符,然后把它传给需要BSTR*参数的函数。
CComBSTR bs1;
CComBSTR bs2 = "new text";
 
  pStuff->GetText ( &bs1 );       // ok, takes address of internal BSTR
  pStuff->SetText ( bs2 );        // ok, calls BSTR converter
  pStuff->SetText ( (BSTR) bs2 ); // cast ok, same as previous line      
  CComBSTR有和_bstr_t相似的构造函数,然而却没有内置的向MBCS字符串转换的函数。因此,你需要使用一个ATL转换宏。
// Constructing
CComBSTR bs1 = "char string";       // construct from a LPCSTR
CComBSTR bs2 = L"wide char string"; // construct from a LPCWSTR
CComBSTR bs3 = bs1;                 // copy from another CComBSTR
CComBSTR bs4;

  bs4.LoadString ( IDS_SOME_STR );  // load string from string table
// Extracting data
BSTR bstr1 = bs1;        // returns internal BSTR, but don''t modify it!
BSTR bstr2 = (BSTR) bs1; // cast ok, same as previous line
BSTR bstr3 = bs1.Copy(); // copies bs1, returns it as a BSTR
BSTR bstr4;
  bstr4 = bs1.Detach();  // bs1 no longer manages its BSTR
  // ...
  SysFreeString ( bstr3 );
  SysFreeString ( bstr4 );      
  注意在上个例子中使用了Detach()方法。调用这个方法后,CComBSTR对象不再管理它的BSTR字符串或者说它对应的内存。这就是bstr4需要调用SysFreeString()的原因。
  做一个补充说明:重载的&操作符意味着在一些STL容器中你不能直接使用CComBSTR变量,比如list。容器要求&操作符返回一个指向容器包含的类的指针,但是对CComBSTR变量使用&操作符返回的是BSTR*,而不是CComBSTR*。然而,有一个ATL类可以解决这个问题,这个类是CAdapt。例如,你可以这样声明一个CComBSTR的list:
std::list< CAdapt > bstr_list;

  CAdapt提供容器所需要的操作符,但这些操作符对你的代码是透明的。你可以把一个bstr_list当作一个CComBSTR的list来使用。

CComVariant
  CComVariant是VARIANT的封装类。然而,不像_variant_t,在CComVariant中VARIANT没有被隐藏。事实上你需要直接访问VARIANT的成员。CComVariant提供了很多构造函数来对VARIANT能够包含的多种类型进行处理。这里,我将只介绍和字符串相关的操作。

// Constructing
CComVariant v1 = "char string";       // construct from a LPCSTR
CComVariant v2 = L"wide char string"; // construct from a LPCWSTR
CComBSTR bs1 = "BSTR bob";
CComVariant v3 = (BSTR) bs1;          // copy from a BSTR
 
// Extracting data
CComBSTR bs2 = v1.bstrVal;            // extract BSTR from the VARIANT      
  不像_variant_t,这里没有提供针对VARIANT包含的各种类型的转换操作符。正如上面介绍的,你必须直接访问VARIANT的成员并且确保这个VARIANT变量保存着你期望的类型。如果你需要把一个CComVariant类型的数据转换成一个BSTR类型的数据,你可以调用ChangeType()方法。
CComVariant v4 = ... // Init v4 from somewhere
CComBSTR bs3;
 
  if ( SUCCEEDED( v4.ChangeType ( VT_BSTR ) ))
    bs3 = v4.bstrVal;      
  像_variant_t一样,CComVariant也没有提供向MBCS字符串转换的转换操作。你需要创建一个_bstr_t类型的中间变量,使用提供从Unicode到MBCS转换的另一个字符串类,或者使用一个ATL的转换宏。

ATL转换宏

  ATL:转换宏是各种字符编码之间进行转换的一种很方便的方式,在函数调用时,它们显得非常有用。ATL转换宏的名称是根据下面的模式来命名的[源类型]2[新类型]或者[源类型]2C[新类型]。据有第二种形式的名字的宏的转换结果是常量指针(对应名字中的"C")。各种类型的简称如下:
A: MBCS string, char* (A for ANSI)
W: Unicode string, wchar_t* (W for wide)
T: TCHAR string, TCHAR*
OLE: OLECHAR string, OLECHAR* (in practice, equivalent to W)
BSTR: BSTR (used as the destination type only)

  所以,W2A()宏把一个Unicode字符串转换成一个MBCS字符串。T2CW()宏把一个TCHAR字符串转转成一个Unicode字符串常量。
  为了使用这些宏,需要先包含atlconv.h头文件。你甚至可以在非ATL工程中包含这个头文件来使用其中定义的宏,因为这个头文件独立于ATL中的其他部分,不需要一个_Module全局变量。当你在一个函数中使用转换宏时,需要把USES_CONVERSION宏放在函数的开头。它定义了转换宏所需的一些局部变量。
  当转换的目的类型是除了BSTR以外的其他类型时,被转换的字符串是存在栈中的。所以,如果你想让字符串的生命周期比当前的函数长,你需要把这个字符串拷贝到其他的字符串类中。当目的类型是BSTR时,内存不会自动被释放,你必须把返回值赋给一个BSTR变量或者一个BSTR封装类以避免内存泄漏。
  下面是一些各种转换宏的使用例子:

// Functions taking various strings:
void Foo ( LPCWSTR wstr );
void Bar ( BSTR bstr );
// Functions returning strings:
void Baz ( BSTR* pbstr );
#include 
main()
{
using std::string;
USES_CONVERSION;    // declare locals used by the ATL macros
// Example 1: Send an MBCS string to Foo()
LPCSTR psz1 = "Bob";
string str1 = "Bob";
 
  Foo ( A2CW(psz1) );
  Foo ( A2CW(str1.c_str()) );
 
// Example 2: Send a MBCS and Unicode string to Bar()
LPCSTR psz2 = "Bob";
LPCWSTR wsz = L"Bob";
BSTR bs1;
CComBSTR bs2;
 
  bs1 = A2BSTR(psz2);         // create a BSTR
  bs2.Attach ( W2BSTR(wsz) ); // ditto, assign to a CComBSTR 
  Bar ( bs1 );
  Bar ( bs2 );
 
  SysFreeString ( bs1 );      // free bs1 memory
  // No need to free bs2 since CComBSTR will do it for us.
 
// Example 3: Convert the BSTR returned by Baz()
BSTR bs3 = NULL;
string str2;
  Baz ( &bs3 );          // Baz() fills in bs3
  str2 = W2CA(bs3);      // convert to an MBCS string
  SysFreeString ( bs3 ); // free bs3 memory
}      
  正如你所看见的,当你有一个和函数所需的参数类型不同的字符串时,使用这些转换宏是非常方便的。

MFC类

CString
  因为一个MFC CString类的对象包含TCHAR类型的字符,所以确切的字符类型取决于你所定义的预处理符号。大体来说,CString 很像STL string,这意味着你必须把它当成不透明的对象,只能使用CString提供的方法来修改CString对象。CString有一个string所不具备的优点:CString具有接收MBCS和Unicode两种字符串的构造函数,它还有一个LPCTSTR转换符,所以你可以把CString对象直接传给一个接收LPCTSTR的函数而不需要调用c_str()函数。
// Constructing
CString s1 = "char string";  // construct from a LPCSTR
CString s2 = L"wide char string";  // construct from a LPCWSTR
CString s3 ( '' '', 100 );  // pre-allocate a 100-byte buffer, fill with spaces
CString s4 = "New window text";
 
  // You can pass a CString in place of an LPCTSTR:
  SetWindowText ( hwndSomeWindow, s4 );
 
  // Or, equivalently, explicitly cast the CString:
  SetWindowText ( hwndSomeWindow, (LPCTSTR) s4 );        
  你可以从你的字符串表中装载一个字符串,CString的一个构造函数和LoadString()函数可以完成它。Format()方法能够从字符串表中随意的读取一个具有一定格式的字符串。     
// Constructing/loading from string table
CString s5 ( (LPCTSTR) IDS_SOME_STR );  // load from string table
CString s6, s7; 
  // Load from string table.
  s6.LoadString ( IDS_SOME_STR );
 
  // Load printf-style format string from the string table:
  s7.Format ( IDS_SOME_FORMAT, "bob", nSomeStuff, ... );  
  第一个构造函数看起来有点奇怪,但是这实际上是文档说明的装入一个字符串的方法。 注意,对一个CString变量,你可以使用的唯一合法转换符是LPCTSTR。转换成LPTSTR(非常量指针)是错误的。养成把一个CString变量转换成LPTSTR的习惯将会给你带来伤害,因为当你的程序后来崩溃时,你可能不知道为什么,因为你到处都使用同样的代码而那时它们都恰巧正常工作。正确的得到一个指向缓冲区的非常量指针的方法是调用GetBuffer()方法。下面是正确的用法的一个例子,这段代码是给一个列表控件中的项设定文字:
CString str = _T("new text");
LVITEM item = {0};
  item.mask = LVIF_TEXT;
  item.iItem = 1;
  item.pszText = (LPTSTR)(LPCTSTR) str; // WRONG!
  item.pszText = str.GetBuffer(0);      // correct
 
  ListView_SetItem ( &item );
str.ReleaseBuffer();  // return control of the buffer to str      
  pszText成员是一个LPTSTR变量,一个非常量指针,因此你需要对str调用GetBuffer()。GetBuffer()的参数是你需要CString为缓冲区分配的最小长度。如果因为某些原因,你需要一个可修改的缓冲区来存放1K TCHARs,你需要调用GetBuffer(1024)。把0作为参数时,GetBuffer()返回的是指向字符串当前内容的指针。
  上面划线的语句可以被编译,在这种情况下,甚至可以正常起作用。但这并不意味着这行代码是正确的。通过使用非常量转换,你已经破坏了面向对象的封装,并对CString的内部实现作了某些假定。如果你有这样的转换习惯,你终将会陷入代码崩溃的境地。你会想代码为什么不能正常工作了,因为你到处都使用同样的代码而那些代码看起来是正确的。
  你知道人们总是抱怨现在的软件的bug是多么的多吗?软件中的bug是因为程序员写了不正确的代码。难道你真的想写一些你知道是错误的代码来为所有的软件都满是bug这种认识做贡献吗?花些时间来学习使用CString的正确方法让你的代码在任何时间都正常工作把。
  CString 有两个函数来从一个 CString 创建一个 BSTR。它们是 AllocSysString() 和SetSysString()。
// Converting to BSTR
CString s5 = "Bob!";
BSTR bs1 = NULL, bs2 = NULL;
  bs1 = s5.AllocSysString();
  s5.SetSysString ( &bs2 );
  SysFreeString ( bs1 );
  SysFreeString ( bs2 );      
COleVariant
  COleVariant和CComVariant.很相似。COleVariant继承自VARIANT,所以它可以传给接收VARIANT的函数。然而,不像CComVariant,COleVariant只有一个LPCTSTR构造函数。没有对LPCSTR 和LPCWSTR的构造函数。在大多数情况下这不是一个问题,因为不管怎样你的字符串很可能是LPCTSTRs,但这是一个需要意识到的问题。COleVariant还有一个接收CString参数的构造函数。
// Constructing
CString s1 = _T("tchar string");
COleVariant v1 = _T("Bob"); // construct from an LPCTSTR
COleVariant v2 = s1; // copy from a CString      
  像CComVariant一样,你必须直接访问VARIANT的成员。如果需要把VARIANT转换成一个字符串,你应该使用ChangeType()方法。然而,COleVariant::ChangeType()如果失败会抛出异常,而不是返回一个表示失败的HRESULT代码。
// Extracting data
COleVariant v3 = ...; // fill in v3 from somewhere
BSTR bs = NULL;
  try
    {
    v3.ChangeType ( VT_BSTR );
    bs = v3.bstrVal;
    }
  catch ( COleException* e )
    {
    // error, couldn''t convert
    }
  SysFreeString ( bs );      

WTL 类

CString
  WTL的CString的行为和MFC的 CString完全一样,所以你可以参考上面关于MFC的 CString的介绍。

CLR 和 VC 7 类

  System::String是用来处理字符串的.NET类。在内部,一个String对象包含一个不可改变的字符串序列。任何对String对象的操作实际上都是返回了一个新的String对象,因为原始的对象是不可改变的。String的一个特性是如果你有不止一个String对象包含相同的字符序列,它们实际上是指向相同的对象的。相对于C++的使用扩展是增加了一个新的字符串常量前缀S,S用来代表一个受控的字符串常量(a managed string literal)。
// Constructing
String* ms = S"This is a nice managed string";      
  你可以传递一个非受控的字符串来创建一个String对象,但是样会比使用受控字符串来创建String对象造成效率的微小损失。这是因为所有以S作为前缀的相同的字符串实例都代表同样的对象,但这对非受控对象是不适用的。下面的代码清楚地阐明了这一点:
String* ms1 = S"this is nice";
String* ms2 = S"this is nice";
String* ms3 = L"this is nice";
  Console::WriteLine ( ms1 == ms2 ); // prints true
  Console::WriteLine ( ms1 == ms3);  // prints false      
正确的比较可能没有使用S前缀的字符串的方法是使用String::CompareTo()
  Console::WriteLine ( ms1->CompareTo(ms2) );
  Console::WriteLine ( ms1->CompareTo(ms3) );      
  上面的两行代码都会打印0,0表示两个字符串相等。 String和MFC 7 CString之间的转换是很容易的。CString有一个向LPCTSTR的转换操作,而String有两个接收char* 和 wchar_t*的构造函数,因此你可以把一个CString变量直接传给一个String的构造函数。
CString s1 ( "hello world" );
String* s2 ( s1 );  // copy from a CString      
反方向的转换也很类似
String* s1 = S"Three cats";
CString s2 ( s1 );      
  这也许会使你感到一点迷惑,但是它确实是起作用的。因为从VS.NET 开始,CString 有了一个接收String 对象的构造函数。
  CStringT ( System::String* pString );      
对于一些快速操作,你可能想访问底层的字符串:
String* s1 = S"Three cats";
  Console::WriteLine ( s1 );
const __wchar_t __pin* pstr = PtrToStringChars(s1);
  for ( int i = 0; i < wcslen(pstr); i++ )
    (*const_cast<__wchar_t*>(pstr+i))++;
  Console::WriteLine ( s1 );      
  PtrToStringChars()返回一个指向底层字符串的const __wchar_t* ,我们需要固定它,否则垃圾收集器或许会在我们正在管理它的内容的时候移动了它。

在 printf-style 格式函数中使用字符串类

  当你在printf()或者类似的函数中使用字符串封装类时你必须十分小心。这些函数包括sprintf()和它的变体,还有TRACE和ATLTRACE宏。因为这些函数没有对添加的参数的类型检查,你必须小心,只能传给它们C语言风格的字符串指针,而不是一个完整的字符串类。
  例如,要把一个_bstr_t 字符串传给ATLTRACE(),你必须使用显式转换(LPCSTR) 或者(LPCWSTR):
_bstr_t bs = L"Bob!";
ATLTRACE("The string is: %s in line %d\n", (LPCSTR) bs, nLine);

  如果你忘了使用转换符而把整个_bstr_t对象传给了函数,将会显示一些毫无意义的输出,因为_bstr_t保存的内部数据会全部被输出。

所有类的总结

  两个字符串类之间进行转换的常用方式是:先把源字符串转换成一个C语言风格的字符串指针,然后把这个指针传递给目的类型的构造函数。下面这张表显示了怎样把一个字符串转换成一个C语言风格的字符串指针以及哪些类具有接收C语言风格的字符串指针的构造函数。

Class  string type convert to char*? convert to const char*? convert to wchar_t*? convert to const wchar_t*? convert to BSTR? construct from char*? construct from wchar_t*?
_bstr_tBSTRyes cast1yes castyes cast1yes castyes2yesyes
_variant_tBSTRnononocast to
_bstr_t3
cast to
_bstr_t3
yesyes
stringMBCSnoyes c_str() methodnononoyesno
wstringUnicodenononoyes c_str() methodnonoyes
CComBSTRBSTRnononoyes cast to BSTRyes castyesyes
CComVariantBSTRnononoyes4yes4yesyes
CString TCHARno6in MBCS
builds, cast
no6in Unicode
builds, cast
no5yesyes
COleVariantBSTRnononoyes4yes4in MBCS
builds
in Unicode
builds
  • 1、即使 _bstr_t 提供了向非常量指针的转换操作符,修改底层的缓冲区也会已引起GPF如果你溢出了缓冲区或者造成内存泄漏。
  • 2、_bstr_t 在内部用一个 wchar_t* 来保存 BSTR,所以你可以使用 const wchar_t* 来访问BSTR。这是一个实现细节,你可以小心的使用它,将来这个细节也许会改变。
  • 3、如果数据不能转换成BSTR会抛出一个异常。
  • 4、使用 ChangeType(),然后访问 VARIANT 的 bstrVal 成员。在MFC中,如果数据转换不成功将会抛出异常。
  • 5、这里没有转换 BSTR 函数,然而 AllocSysString() 返回一个新的BSTR。
  • 6、使用 GetBuffer() 方法,你可以暂时地得到一个非常量的TCHAR指针。

  • 作者简介

    Michael Dunn:
      
    Michael Dunn居住在阳光城市洛杉矶。他是如此的喜欢这里的天气以致于想一生都住在这里。他在4年级时开始编程,那时用的电脑是Apple //e。1995年,在UCLA获得数学学士学位,随后在Symantec公司做QA工程师,在 Norton AntiVirus 组工作。他自学了 Windows 和 MFC 编程。1999-2000年,他设计并实现了 Norton AntiVirus的新界面。
      Michael 现在在 Napster(一个提供在线订阅音乐服务的公司)做开发工作,他还开发了UltraBar,一个IE工具栏插件,它可以使网络搜索更加容易,给了 googlebar 以沉重打击;他还开发了 CodeProject SearchBar;与人共同创建了 Zabersoft 公司,该公司在洛杉矶和丹麦的 Odense 都设有办事处。
      他喜欢玩游戏。爱玩的游戏有 pinball, bike riding,偶尔还玩 PS, Dreamcasth 和 MAME 游戏。他因忘了自己曾经学过的语言:法语、汉语、日语而感到悲哀。

    Nishant S(Nish)
      Nish是来自印度 Trivandrum,的 Microsoft Visual C++ MVP。他从1990年开始编码。现在,Nish为作为合同雇员在家里为 CodeProject 工作。   
      他还写了一部浪漫戏剧《Summer Love and Some more Cricket》和一本编程书籍《Extending MFC applications with the .NET Framework》。他还管理者MVP的一个网站http://www.voidnish.com/ 。在这个网站上,你可以看到他的很多关于编程方面的思想和文章。
    Nish 还计划好旅游,他希望自一生中能够到达地球上尽可能多的地方。

    posted @ 2006-03-20 16:38 高山流水 阅读(209) | 评论 (0)编辑 收藏

     

    原著:Michael Dunn

    翻译:Chengjie Sun



    原文出处:CodeProject:The Complete Guide to C++ Strings, Part I

    引言

      毫无疑问,我们都看到过像 TCHAR, std::string, BSTR 等各种各样的字符串类型,还有那些以 _tcs 开头的奇怪的宏。你也许正在盯着显示器发愁。本指引将总结引进各种字符类型的目的,展示一些简单的用法,并告诉您在必要时,如何实现各种字符串类型之间的转换。
      在第一部分,我们将介绍3种字符编码类型。了解各种编码模式的工作方式是很重要的事情。即使你已经知道一个字符串是一个字符数组,你也应该阅读本部分。一旦你了解了这些,你将对各种字符串类型之间的关系有一个清楚地了解。
      在第二部分,我们将单独讲述string类,怎样使用它及实现他们相互之间的转换。

    字符基础 -- ASCII, DBCS, Unicode

      所有的 string 类都是以C-style字符串为基础的。C-style 字符串是字符数组。所以我们先介绍字符类型。这里有3种编码模式对应3种字符类型。第一种编码类型是单子节字符集(single-byte character set or SBCS)。在这种编码模式下,所有的字符都只用一个字节表示。ASCII是SBCS。一个字节表示的0用来标志SBCS字符串的结束。
      第二种编码模式是多字节字符集(multi-byte character set or MBCS)。一个MBCS编码包含一些一个字节长的字符,而另一些字符大于一个字节的长度。用在Windows里的MBCS包含两种字符类型,单字节字符(single-byte characters)和双字节字符(double-byte characters)。由于Windows里使用的多字节字符绝大部分是两个字节长,所以MBCS常被用DBCS代替。
      在DBCS编码模式中,一些特定的值被保留用来表明他们是双字节字符的一部分。例如,在Shift-JIS编码中(一个常用的日文编码模式),0x81-0x9f之间和 0xe0-oxfc之间的值表示"这是一个双字节字符,下一个子节是这个字符的一部分。"这样的值被称作"leading bytes",他们都大于0x7f。跟随在一个leading byte子节后面的字节被称作"trail byte"。在DBCS中,trail byte可以是任意非0值。像SBCS一样,DBCS字符串的结束标志也是一个单字节表示的0。
      第三种编码模式是Unicode。Unicode是一种所有的字符都使用两个字节编码的编码模式。Unicode字符有时也被称作宽字符,因为它比单子节字符宽(使用了更多的存储空间)。注意,Unicode不能被看作MBCS。MBCS的独特之处在于它的字符使用不同长度的字节编码。Unicode字符串使用两个字节表示的0作为它的结束标志。
      单字节字符包含拉丁文字母表,accented characters及ASCII标准和DOS操作系统定义的图形字符。双字节字符被用来表示东亚及中东的语言。Unicode被用在COM及Windows NT操作系统内部。
      你一定已经很熟悉单字节字符。当你使用char时,你处理的是单字节字符。双字节字符也用char类型来进行操作(这是我们将会看到的关于双子节字符的很多奇怪的地方之一)。Unicode字符用wchar_t来表示。Unicode字符和字符串常量用前缀L来表示。例如:

    wchar_t wch = L''1''; // 2 bytes, 0x0031
    wchar_t* wsz = L"Hello"; // 12 bytes, 6 wide characters

    字符在内存中是怎样存储的

      单字节字符串:每个字符占一个字节按顺序依次存储,最后以单字节表示的0结束。例如。"Bob"的存贮形式如下:

    42 6F 62 00
    B o b BOS

    Unicode的存储形式,L"Bob"

    42 00 6F 00 62 00 00 00
    B o b BOS

    使用两个字节表示的0来做结束标志。

      一眼看上去,DBCS 字符串很像 SBCS 字符串,但是我们一会儿将看到 DBCS 字符串的微妙之处,它使得使用字符串操作函数和永字符指针遍历一个字符串时会产生预料之外的结果。字符串" " ("nihongo")在内存中的存储形式如下(LB和TB分别用来表示 leading byte 和 trail byte)

    93 FA 96 7B 8C EA 00
    LB TB LB TB LB TB EOS
    EOS

    值得注意的是,"ni"的值不能被解释成WORD型值0xfa93,而应该看作两个值93和fa以这种顺序被作为"ni"的编码。

    使用字符串处理函数

      我们都已经见过C语言中的字符串函数,strcpy(), sprintf(), atoll()等。这些字符串只应该用来处理单字节字符字符串。标准库也提供了仅适用于Unicode类型字符串的函数,比如wcscpy(), swprintf(), wtol()等。
      微软还在它的CRT(C runtime library)中增加了操作DBCS字符串的版本。Str***()函数都有对应名字的DBCS版本_mbs***()。如果你料到可能会遇到DBCS字符串(如果你的软件会被安装在使用DBCS编码的国家,如中国,日本等,你就可能会),你应该使用_mbs***()函数,因为他们也可以处理SBCS字符串。(一个DBCS字符串也可能含有单字节字符,这就是为什么_mbs***()函数也能处理SBCS字符串的原因)
      让我们来看一个典型的字符串来阐明为什么需要不同版本的字符串处理函数。我们还是使用前面的Unicode字符串 L"Bob":

    42 00 6F 00 62 00 00 00
    B o b BOS

      因为x86CPU是little-endian,值0x0042在内存中的存储形式是42 00。你能看出如果这个字符串被传给strlen()函数会出现什么问题吗?它将先看到第一个字节42,然后是00,而00是字符串结束的标志,于是strlen()将会返回1。如果把"Bob"传给wcslen(),将会得出更坏的结果。wcslen()将会先看到0x6f42,然后是0x0062,然后一直读到你的缓冲区的末尾,直到发现00 00结束标志或者引起了GPF。
      到目前为止,我们已经讨论了str***()和wcs***()的用法及它们之间的区别。Str***()和_mbs**()之间的有区别区别呢?明白他们之间的区别,对于采用正确的方法来遍历DBCS字符串是很重要的。下面,我们将先介绍字符串的遍历,然后回到str***()与_mbs***()之间的区别这个问题上来。

    正确的遍历和索引字符串

      因为我们中大多数人都是用着SBCS字符串成长的,所以我们在遍历字符串时,常常使用指针的++-和-操作。我们也使用数组下标的表示形式来操作字符串中的字符。这两种方式是用于SBCS和Unicode字符串,因为它们中的字符有着相同的宽度,编译器能正确的返回我们需要的字符。
      然而,当碰到DBCS字符串时,我们必须抛弃这些习惯。这里有使用指针遍历DBCS字符串时的两条规则。违背了这两条规则,你的程序就会存在DBCS有关的bugs。

  • 1.在前向遍历时,不要使用++操作,除非你每次都检查lead byte;
  • 2.永远不要使用-操作进行后向遍历。
  •   我们先来阐述规则2,因为找到一个违背它的真实的实例代码是很容易的。假设你有一个程序在你自己的目录里保存了一个设置文件,你把安装目录保存在注册表中。在运行时,你从注册表中读取安装目录,然后合成配置文件名,接着读取该文件。假设,你的安装目录是C:\Program Files\MyCoolApp,那么你合成的文件名应该是C:\Program Files\MyCoolApp\config.bin。当你进行测试时,你发现程序运行正常。
      现在,想象你合成文件名的代码可能是这样的:

    bool GetConfigFileName ( char* pszName, size_t nBuffSize )
    {
        char szConfigFilename[MAX_PATH];
     
        // Read install dir from registry... we''ll assume it succeeds.
     
        // Add on a backslash if it wasn''t present in the registry value.
        // First, get a pointer to the terminating zero.
        char* pLastChar = strchr ( szConfigFilename, '''' );
     
        // Now move it back one character.
        pLastChar--;  
     
        if ( *pLastChar != ''\\'' )
            strcat ( szConfigFilename, "\\" );
     
        // Add on the name of the config file.
        strcat ( szConfigFilename, "config.bin" );
     
        // If the caller''s buffer is big enough, return the filename.
        if ( strlen ( szConfigFilename ) >= nBuffSize )
            return false;
        else
            {
            strcpy ( pszName, szConfigFilename );
            return true;
            }
    }      
      这是一段很健壮的代码,然而在遇到 DBCS 字符时它将会出错。让我们来看看为什么。假设一个日本用户使用了你的程序,把它安装在 C:\。下面是这个名字在内存中的存储形式:
     
    433A5C83 8883 4583 5283 5C00
       LB TB LB TB LB TB LB TB  
    C:\EOS

      当使用 GetConfigFileName() 检查尾部的''\\''时,它寻找安装目录名中最后的非0字节,看它是等于''\\''的,所以没有重新增加一个''\\''。结果是代码返回了错误的文件名。
      哪里出错了呢?看看上面两个被用蓝色高量显示的字节。斜杠''\\''的值是0x5c。'' ''的值是83 5c。上面的代码错误的读取了一个 trail byte,把它当作了一个字符。
      正确的后向遍历方法是使用能够识别DBCS字符的函数,使指针移动正确的字节数。下面是正确的代码。(指针移动的地方用红色标明)

    bool FixedGetConfigFileName ( char* pszName, size_t nBuffSize )
    {
        char szConfigFilename[MAX_PATH];
     
        // Read install dir from registry... we''ll assume it succeeds.
     
        // Add on a backslash if it wasn''t present in the registry value.
        // First, get a pointer to the terminating zero.
        char* pLastChar = _mbschr ( szConfigFilename, '''' );
     
        // Now move it back one double-byte character.
        pLastChar = CharPrev ( szConfigFilename, pLastChar );
     
        if ( *pLastChar != ''\\'' )
            _mbscat ( szConfigFilename, "\\" );
     
        // Add on the name of the config file.
        _mbscat ( szConfigFilename, "config.bin" );
    
         // If the caller''s buffer is big enough, return the filename.
        if ( _mbslen ( szInstallDir ) >= nBuffSize )
            return false;
        else
            {
            _mbscpy ( pszName, szConfigFilename );
            return true;
            }
    }
    
      上面的函数使用CharPrev() API使pLastChar向后移动一个字符,这个字符可能是两个字节长。在这个版本里,if条件正常工作,因为lead byte永远不会等于0x5c。
      让我们来想象一个违背规则1的场合。例如,你可能要检测一个用户输入的文件名是否多次出现了'':''。如果,你使用++操作来遍历字符串,而不是使用CharNext(),你可能会发出不正确的错误警告如果恰巧有一个trail byte它的值的等于'':''的值。
    与规则2相关的关于字符串索引的规则:
    2a. 永远不要使用减法去得到一个字符串的索引。

    违背这条规则的代码和违背规则2的代码很相似。例如,

    char* pLastChar = &szConfigFilename [strlen(szConfigFilename) - 1];

    这和向后移动一个指针是同样的效果。

    回到关于str***()和_mbs***()的区别

      现在,我们应该很清楚为什么_mbs***()函数是必需的。Str***()函数根本不考虑DBCS字符,而_mbs***()考虑。如果,你调用strrchr("C:\\ ", ''\\''),返回结果可能是错误的,然而_mbsrchr()将会认出最后的双字节字符,返回一个指向真的''\\''的指针。
      关于字符串函数的最后一点:str***()和_mbs***()函数认为字符串的长度都是以char来计算的。所以,如果一个字符串包含3个双字节字符,_mbslen()将会返回6。Unicode函数返回的长度是按wchar_t来计算的。例如,wcslen(L"Bob")返回3。

    Win32 API中的MBCS和Unicode

    两组 APIs:
      尽管你也许从来没有注意过,Win32中的每个与字符串相关的API和message都有两个版本。一个版本接受MBCS字符串,另一个接受Unicode字符串。例如,根本没有SetWindowText()这个API,相反,有SetWindowTextA()和SetWindowTextW()。后缀A表明这是MBCS函数,后缀W表示这是Unicode版本的函数。
      当你 build 一个 Windows 程序,你可以选择是用 MBCS 或者 Unicode APIs。如果,你曾经用过VC向导并且没有改过预处理的设置,那表明你用的是MBCS版本。那么,既然没有 SetWindowText() API,我们为什么可以使用它呢?winuser.h头文件包含了一些宏,例如:

    BOOL WINAPI SetWindowTextA ( HWND hWnd, LPCSTR lpString );
    BOOL WINAPI SetWindowTextW ( HWND hWnd, LPCWSTR lpString );
     
    #ifdef UNICODE
    #define SetWindowText  SetWindowTextW
    #else
    #define SetWindowText  SetWindowTextA
    #endif      
    当使用MBCS APIs来build程序时,UNICODE没有被定义,所以预处理器看到:
    #define SetWindowText SetWindowTextA

      这个宏定义把所有对SetWindowText的调用都转换成真正的API函数SetWindowTextA。(当然,你可以直接调用SetWindowTextA() 或者 SetWindowTextW(),虽然你不必那么做。)
      所以,如果你想把默认使用的API函数变成Unicode版的,你可以在预处理器设置中,把_MBCS从预定义的宏列表中删除,然后添加UNICODE和_UNICODE。(你需要两个都定义,因为不同的头文件可能使用不同的宏。) 然而,如果你用char来定义你的字符串,你将会陷入一个尴尬的境地。考虑下面的代码:

    HWND hwnd = GetSomeWindowHandle();
    char szNewText[] = "we love Bob!";
    SetWindowText ( hwnd, szNewText );

    在预处理器把SetWindowText用SetWindowTextW来替换后,代码变成:

    HWND hwnd = GetSomeWindowHandle();
    char szNewText[] = "we love Bob!";
    SetWindowTextW ( hwnd, szNewText );

      看到问题了吗?我们把单字节字符串传给了一个以Unicode字符串做参数的函数。解决这个问题的第一个方案是使用 #ifdef 来包含字符串变量的定义:

    HWND hwnd = GetSomeWindowHandle();
    #ifdef UNICODE
    wchar_t szNewText[] = L"we love Bob!";
    #else
    char szNewText[] = "we love Bob!";
    #endif
    SetWindowText ( hwnd, szNewText );

    你可能已经感受到了这样做将会使你多么的头疼。完美的解决方案是使用TCHAR.

    使用TCHAR

      TCHAR是一种字符串类型,它让你在以MBCS和UNNICODE来build程序时可以使用同样的代码,不需要使用繁琐的宏定义来包含你的代码。TCHAR的定义如下:

    #ifdef UNICODE
    typedef wchar_t TCHAR;
    #else
    typedef char TCHAR;
    #endif

    所以用MBCS来build时,TCHAR是char,使用UNICODE时,TCHAR是wchar_t。还有一个宏来处理定义Unicode字符串常量时所需的L前缀。

    #ifdef UNICODE
    #define _T(x) L##x
    #else
    #define _T(x) x
    #endif

      ##是一个预处理操作符,它可以把两个参数连在一起。如果你的代码中需要字符串常量,在它前面加上_T宏。如果你使用Unicode来build,它会在字符串常量前加上L前缀。

    TCHAR szNewText[] = _T("we love Bob!");

      像是用宏来隐藏SetWindowTextA/W的细节一样,还有很多可以供你使用的宏来实现str***()和_mbs***()等字符串函数。例如,你可以使用_tcsrchr宏来替换strrchr()、_mbsrchr()和wcsrchr()。_tcsrchr根据你预定义的宏是_MBCS还是UNICODE来扩展成正确的函数,就像SetWindowText所作的一样。
      不仅str***()函数有TCHAR宏。其他的函数如, _stprintf(代替sprinft()和swprintf()),_tfopen(代替fopen()和_wfopen())。 MSDN中"Generic-Text Routine Mappings."标题下有完整的宏列表。

    字符串和TCHAR typedefs

      由于Win32 API文档的函数列表使用函数的常用名字(例如,"SetWindowText"),所有的字符串都是用TCHAR来定义的。(除了XP中引入的只适用于Unicode的API)。下面列出一些常用的typedefs,你可以在msdn中看到他们。

    type Meaning in MBCS builds Meaning in Unicode builds
    WCHARwchar_twchar_t
    LPSTR zero-terminated string of char (char*)zero-terminated string of char (char*)
    LPCSTR constant zero-terminated string of char (const char*)constant zero-terminated string of char (const char*)
    LPWSTRzero-terminated Unicode string (wchar_t*) zero-terminated Unicode string (wchar_t*)
    LPCWSTRconstant zero-terminated Unicode string (const wchar_t*)constant zero-terminated Unicode string (const wchar_t*)
    TCHAR/TD> char/TD> wchar_t/TD>
    LPTSTRzero-terminated string of TCHAR (TCHAR*) zero-terminated string of TCHAR (TCHAR*)
    LPCTSTR constant zero-terminated string of TCHAR (const TCHAR*)constant zero-terminated string of TCHAR (const TCHAR*)

    何时使用 TCHAR 和 Unicode

      到现在,你可能会问,我们为什么要使用Unicode。我已经用了很多年的char。下列3种情况下,使用Unicode将会使你受益:

  • 1.你的程序只运行在Windows NT系统中。
  • 2. 你的程序需要处理超过MAX_PATH个字符长的文件名。
  • 3. 你的程序需要使用XP中引入的只有Unicode版本的API.
  •   Windows 9x 中大多数的 API 没有实现 Unicode 版本。所以,如果你的程序要在windows 9x中运行,你必须使用MBCS APIs。然而,由于NT系统内部都使用Unicode,所以使用Unicode APIs将会加快你的程序的运行速度。每次,你传递一个字符串调用MBCS API,操作系统会把这个字符串转换成Unicode字符串,然后调用对应的Unicode API。如果一个字符串被返回,操作系统还要把它转变回去。尽管这个转换过程被高度优化了,但它对速度造成的损失是无法避免的。
      只要你使用Unicode API,NT系统允许使用非常长的文件名(突破了MAX_PATH的限制,MAX_PATH=260)。使用Unicode API的另一个优点是你的程序会自动处理用户输入的各种语言。所以一个用户可以输入英文,中文或者日文,而你不需要额外编写代码去处理它们。
      最后,随着windows 9x产品的淡出,微软似乎正在抛弃MBCS APIs。例如,包含两个字符串参数的SetWindowTheme() API只有Unicode版本的。使用Unicode来build你的程序将会简化字符串的处理,你不必在MBCS和Unicdoe之间相互转换。
      即使你现在不使用Unicode来build你的程序,你也应该使用TCHAR及其相关的宏。这样做不仅可以的代码可以很好地处理DBCS,而且如果将来你想用Unicode来build你的程序,你只需要改变一下预处理器中的设置就可以实现了。

    作者简介
      Michael Dunn:居住在阳光城市洛杉矶。他是如此的喜欢这里的天气以致于想一生都住在这里。他在4年级时开始编程,那时用的电脑是Apple //e。1995年,在 UCLA 获得数学学士学位,随后在Symantec 公司做 QA 工程师,在 Norton AntiVirus 组工作。他自学了 Windows 和 MFC 编程。1999-2000年,他设计并实现了 Norton AntiVirus 的新界面。 
      Michael 现在在 Napster(一个提供在线订阅音乐服务的公司)做开发工作,他还开发了UltraBar,一个IE工具栏插件,它可以使网络搜索更加容易,给了 googlebar 以沉重打击;他还开发了 CodeProject SearchBar;与人共同创建了 Zabersoft 公司,该公司在洛杉矶和丹麦的 Odense 都设有办事处。
      他喜欢玩游戏。爱玩的游戏有 pinball, bike riding,偶尔还玩 PS, Dreamcasth 和 MAME 游戏。他因忘了自己曾经学过的语言:法语、汉语、日语而感到悲哀。

    posted @ 2006-03-20 16:36 高山流水 阅读(157) | 评论 (0)编辑 收藏

     

    事件和回调


    Ken Bergmenn
    MSDN开发组

    什么时候使用一个事件(或连接点)接口以及什么时候使用特定的回调接口是很难理解的。他们有点类似,但在很多方面还是有很大的差异。下面将概述一些基本问题,这些基本问题在试图决定如何继续进行你的组件时是应该考虑到的。

    我认识你吗?

    事件接口和回调接口之间关键的概念差异是事件被设计得更象一个匿名广播,而回调函数则更象一个"握手"联络方式。虽然其他重要问题也需要阐明,但是关于使用哪一个技术关键决定于匿名性。

    开始熟悉

    任意对一个事件源有访问的对象都能通过简单地把那个索引放进WithEvents变量来处理源提出的事件。在C++中处理连接点与之有相当的关联,但是前提是一样的。在任意一种情况下,事件源对什么对象可能正在处理自己的事件没有任何概念。可能有许多个对象正与之密切相连但也可能一个也没有。关键之处是事件源盲目地通知每一个被连接的客户机通过它的事件连接点接口,但并不知道是否客户机正在执行处理事件。

    相反地,一个设计用来执行回调的服务器必须有一个明显的对每个需要通知的对象的访问,它必须连接和管理这些访问,而且最后它还必须执行通知。基本上,服务器必须清楚地知道有多少个客户机以及如何与他们中的每一个连接和交互。

    事件顺序

    在Microsoft Visual Basic事件接口中,一个事件源不能控制客户机接受他们的事件的顺序。这话反之也是正确的。客户机不能肯定与别的客户机相比用一种特定的顺序接受通知。即使你要在C++中滚动你自己的连接点,后者也是正确的。

    另一方面,一个回调服务器必须控制它调回客户机的顺序。当然,在一个回调服务器上做这些工作会有好处。比如,一个源服务器可能被设计用来给某些客户机高优先级的通知,或它可能执行一些特定的基于回调结果的动作。这个灵活性在下一点中将更有意义。

    谁负责

    当事件服务器提出一个事件,它的所有的客户机都得到了在事件服务器能重新得到控制之间处理事件的机会。所以,如果许多客户机都在听候事件,通知处理将花费难以预知的时间。另一方面,一个回调服务器,因为它对执行通知处理负责,所以在它对客户机做的每个调用它都能重新得到控制。

    因为控制级别在这里是有效的,回调可以采用比事件更灵活的技术来进行客户通知。

    现在,让我们来考虑一下事件的参数会发生什么变化。因为事件服务器直到所有的客户机都已经处理了特定的事件后才能重新得到控制,所有特定客户机的ByRef参数的变化已经丢失了。只有最后一个客户机参数发生的变化能被事件服务器看见。当考虑到哪一个客户机被通知的顺序不能得到保证的事实时,这确实变得不方便。当然,对于回调服务器,服务器对这个处理负责,所有每一个客户机的反馈都能被独立地分析。事实上,一个回调服务器可能希望在通知每一个客户机时传递新值。

    处理错误

    两种途径之间最终的编译差异在于错误的处理方式。如果在一个客户机的事件处理器中发生了错误,则事件源不会被通知。处理事件的客户机甚至可能非常可怕地崩溃掉,而事件源并不知道,当然,客户机的致命错误也只能是使服务器崩溃(如果它是一个处理中的元件)。在这种情况中,事件服务器将不知道为什么崩溃,甚至不知道发生了错误。

    当然,在任意执行问题中,测量你自己的需要的唯一办法是实验,基准和测试。你自己可以试试重排列,并作作数学演算。如果你真的想得到绝对的最后速度冲刺,那么做测试是得到保证的唯一方法,而不能相信你曾经得到什么样的承诺。

    posted @ 2006-03-20 16:32 高山流水 阅读(374) | 评论 (0)编辑 收藏

    作者: 黄森堂(vcmfc)

    1.检测程序中的括号是否匹配
     把光标移动到需要检测的括号(如大括号{}、方括号[]、圆括号()和尖括号<>)前面,键入快捷键“Ctrl+]”。如果括号匹配正确,光标就跳到匹配的括号处,否则光标不移动,并且机箱喇叭还会发出一声警告声。

    2.查看一个宏(或变量、函数)的宏定义
     把光标移动到你想知道的一个宏上,就比如说最常见的DECLARE_MAP_MESSAGE上按一下F12(或右键菜单中的Go To Defition Of …),如果没有建立Browse files,会出现提示对话框,确定,然后就会跳到定义那些东西的地方。

    3.格式化一段乱七八糟的源代码
     选中那段源代码,按ATL+F8。

    4.在编辑状态下发现成员变量或函数不能显示
     删除该项目扩展名为.ncb文件,重新打开该项目。

    5.如何整理ClassView视图中大量的类
     可以在classview 视图中右键新建文件夹(new folder),再把具有相近性质的类拖到对应的文件夹中,使整个视图看上去清晰明了.

    6.定位预处理指定
    在源文件中定位光标到对称的#if, #endif,使用Ctrl+K.

    7.如何添加系统中Lib到当前项目
     在Project | Settings | Link | Object/library modules:输入Lib名称,不同的Lib之间用空格格开.

    8.如何添加系统中的头文件(.h)到当前项目.
     #include ,告诉编译到VC系统目录去找;使用#include "FileName.h",告诉编译在当前目录找.

    9.如何在Studio使用汇编调试
     在WorkBench的Debugger状态下按CTRL+F7.

    10.怎样处理ClassZiard找不到的系统消息
     如果要在ClassWizard中处理WM_NCHITTEST等系统消息,请在ClassWizard中Class Info页中将Message filter改为Window就有了.

    11.如何干净的删除一个类
     先从Workspace中的FileView中删除对应的.h和.cpp文件,再关闭项目,从实际的文件夹中删除对应的.h和.cpp文件与.clw文件。

    12.如果让控制台应用程序支持mfc类库
     可以在控制台应用程序中include 来引入mfc库,但是控制台应用程序缺省是单线程的,mfc是多线程的,为解决该矛盾,在project setting->c/c++ 选项,选择code generation,在use run-time library 下拉框中选择debug multithread。

    13.如何汉化只有可执行代码的.exe 文件
     在nt 下利用vc open file 以resources方式打开*.exe 文件,直接修改资源文件,然后保存即可。

    附:VC项目文件说明
    .opt 工程关于开发环境的参数文件。如工具条位置等信息;

    .aps (AppStudio File),资源辅助文件,二进制格式,一般不用去管他.

    .clw ClassWizard信息文件,实际上是INI文件的格式,有兴趣可以研究一下.有时候ClassWizard出问题,手工修改CLW文件可以解决.如果此文件不存在的话,每次用ClassWizard的时候绘提示你是否重建.

    .dsp (DeveloperStudio Project):项目文件,文本格式,不过不熟悉的话不要手工修改.DSW(DeveloperStudio Workspace)是工作区文件,其他特点和DSP差不多.

    .plg 是编译信息文件,编译时的error和warning信息文件(实际上是一个html文件),一般用处不大.在Tools->Options里面有个选项可以控制这个文件的生成.

    .hpj (Help Project)是生成帮助文件的工程,用microsfot  Help Compiler可以处理.

    .mdp (Microsoft DevStudio Project)是旧版本的项目文件,如果要打开此文件的话,会提示你是否转换成新的DSP格式.

    .bsc 是用于浏览项目信息的,如果用Source Brower的话就必须有这个文件.如果不用这个功能的话,可以在Project Options里面去掉Generate Browse Info File,可以加快编译速度.

    .map 是执行文件的映像信息纪录文件,除非对系统底层非常熟悉,这个文件一般用不着.

    .pch (Pre-Compiled File)是预编译文件,可以加快编译速度,但是文件非常大.

    .pdb (Program Database)记录了程序有关的一些数据和调试信息,在调试的时候可能有用.

    .exp 只有在编译DLL的时候才会生成,记录了DLL文件中的一些信息.一般也没什么用.

    .ncb 无编译浏览文件(no compile browser)。当自动完成功能出问题时可以删除此文件。build后会自动生成。

    posted @ 2006-03-20 16:31 高山流水 阅读(111) | 评论 (0)编辑 收藏

    使用线程

    Greg Ewing
    Clarity Consulting Inc.


    2002 年 3 月

    摘要:本文论述了各种模式的线程(单线程、单元线程和自由线程)以及每种模式的使用方法。同时,还提供了一个使用线程的 C# 语言代码示例,以帮助您编写使用线程的应用程序。本文还讨论了多线程代码中的一些重要问题。

    下载(英文)示例文件。(请注意,在示例文件中,程序员的注释使用的是英文,本文中将其译为中文是为了便于读者进行理解。)

    目录

    简介
    线程背景
    示例应用程序
    多线程代码问题
    总结

    简介

    编写多线程 Microsoft® 消息队列 (MSMQ) 触发器应用程序向来是一件让人畏惧的事情。不过,.NET 框架线程和消息类的出现使这项工作变得比以前容易了。这些类允许您使用任何适用于 .NET 框架的语言来编写多线程应用程序。以前,像 Microsoft Visual Basic® 之类的工具对线程的支持十分有限。因此不得不使用 C++ 来编写多线程代码,通过 Visual Basic 构建由多个过程或 ActiveX DLL 组成的解决方案(这种解决方案一点也不理想),或者干脆完全放弃多线程。使用 .NET 框架,您可以构建各种多线程应用程序,而不用考虑选择使用哪种语言。

    本文将逐步介绍构建侦听并处理来自 Microsoft 消息队列的多线程应用程序的过程。本文将着重讨论两个名称空间 System.ThreadingSystem.Messaging。示例代码是用 C# 语言编写的,但您可以轻松地将其转换为您所使用的语言。

    线程背景

    在 Win32 环境中,线程有三种基本模式:单线程、单元线程和自由线程。

    单线程

    您最初编写的某些应用程序很可能是单线程应用程序,仅包含与应用程序进程对应的线程。进程可以被定义为应用程序的实例,拥有该应用程序的内存空间。大多数 Windows 应用程序都是单线程的,即用一个线程完成所有工作。

    单元线程

    单元线程是一种稍微复杂的线程模式。标记用于单元线程的代码可以在其自己的线程中执行,并限制在自己的单元中。线程可以被定义为进程所拥有的实体。处理时将调度该进程。在单元线程模式中,所有线程都在主应用程序内存中各自的子段范围内运行。此模式允许多个代码实例同时但独立地运行。例如,在 .NET 之前,Visual Basic 仅限于创建单元线程组件和应用程序。

    自由线程

    自由线程是最复杂的线程模式。在自由线程模式中,多个线程可以同时调用相同的方法和组件。与单元线程不同,自由线程不会被限制在独立的内存空间。当应用程序必须进行大量相似而又独立的数学计算时,您可能需要使用自由线程。在这种情况下,您需要生成多个线程使用相同的代码示例来执行计算。可能 C++ 开发人员是仅有的编写过自由线程应用程序的应用程序开发人员,因为像 Visual Basic 6.0 这样的语言几乎不可能编写自由线程应用程序。

    使用线程模式

    为了使您对线程模式有一定的概念,我们可以将其想象为从一所屋子搬到另一所屋子。如果您采用单线程方法,则需要您自己完成从打包到扛箱子再到拆包的所有工作。如果您使用单元线程模式,则表示您邀请了好朋友来帮忙。每个朋友在一个单独的房间里工作,并且不能帮助在其他房间工作的人。他们各自负责自己的空间和空间内的物品搬运。如果您采用自由线程方法,您仍然邀请相同的朋友来帮忙,但是所有朋友可以随时在任何一个房间工作,共同打包物品。与此类似,您的房子就是运行所有线程的进程,每个朋友都是一个代码实例,搬运的物品为应用程序的资源和变量。

    本示例解释了不同线程模式的优点和缺点。单元线程比单线程要快,因为有多个组件实例在工作。在某些情况下,自由线程比单元线程更快更有效,这是因为所有事情同时发生,并且可以共享所有资源。但是,当多线程更改共享资源时,这可能会出现问题。假设一个人开始使用箱子打包厨房用具,此时另一个朋友进来了,要使用同一个箱子打包浴室的东西。第一个朋友在箱子上贴上了“厨房用具”,另一个朋友用“洗漱用品”标签覆盖了原标签。结果,当您拆包时,就会发生将厨房用品搬到浴室的情况。

    示例应用程序

    第一步是要检查示例应用程序的设计。应用程序将生成多个线程,每个线程都侦听来自 MSMQ 队列的消息。本示例使用两个类,主 Form 类和自定义 MQListen 类。Form 类将处理用户界面并创建,管理和破坏辅助线程。MQListen 类包含所有代码,包括辅助线程运行所需的消息队列因素。

    准备应用程序

    • 要启动应用程序,请打开 Visual Studio .NET 并创建一个名为 MultiThreadedMQListener 的新 C# Windows 应用程序。打开窗体的属性,将其命名为 QueueListenerForm。画出初始窗体后,将两个标签、两个按钮、一个状态栏和两个文本框拖放到窗体上。将第一个文本框命名为 Server,第二个文本框命名为 Queue。将第一个按钮命名为 StartListening,第二个按钮命名为 StopListening。可以保留状态栏的默认名称 statusBar1
    • 下一步,单击 Project(项目)菜单并单击 Add Reference(添加引用),以向 System.Messaging 名称空间添加一个引用。在 .NET 组件列表中找到并选择 System.Messaging.Dll。名称空间包含与 MSMQ 队列通信所使用的类。
    • 下一步,单击 File(文件)菜单,然后单击 Add New Item(添加新项),以在项目中添加一个新类。选择 Class(类)模板并将其命名为 MQListen。在类的顶部添加下列 using 语句:
      // C#
      using System.Threading;
      using System.Messaging;

      System.Threading 名称空间允许您访问所有必要的线程功能,在本例中,您可以访问 Thread 类和 ThreadInterruptException 构造函数。该名称空间还包括许多其他高级功能,本文不作详细讨论。System.Messaging 名称空间允许您访问 MSMQ 功能,包括向队列发送消息和接收队列消息。在本例中,您将使用 MessageQueue 类来接收消息。还必须在主窗体代码中添加 using System.Threading

    所有引用就位后,您就可以开始编写代码了。

    辅助线程

    首先需要构建封装所有线程工作的 MQListen 类。将下列代码插入 MQListen 中。

    // C#
    public class MQListen
    {   
       private string m_MachineName;
       private string m_QueueName;
          
       // 构造函数接收必要的队列信息。
       public MQListen(string MachineName, string QueueName)
       {
          m_MachineName = MachineName;
          m_QueueName = QueueName;
       }
        
    
       // 每个线程用来侦听 MQ 消息的一种唯一方法
       public void Listen()
       {
          // 创建一个 MessageQueue 对象。
          System.Messaging.MessageQueue MQ  = new 
    System.Messaging.MessageQueue();
    
          // 设置 MessageQueue 对象的路径属性。
          MQ.Path = m_MachineName + "\\private$\\" + m_QueueName;
                
          // 创建一个 Message 对象。
    System.Messaging.Message Message = new    
    System.Messaging.Message();        
          // 重复上述步骤,直到收到中断。
          while (true)
          {
             try
             {
    // 休眠以在中断发出时捕捉中断。
    System.Threading.Thread.Sleep(100);
    // 将 Message 对象设置为与接收函数的结果相等。
    // 持续时间(天、小时、分钟、秒)。
                            Message = MQ.Receive(new TimeSpan(0, 0, 0, 1));
                                 
                // 显示已接收消息的标签
    System.Windows.Forms.MessageBox.Show(" Label: " + Message.Label);
                      
             }
             catch (ThreadInterruptedException e)
             {
    // 从主线程捕捉 ThreadInterrupt 并退出。
                Console.WriteLine("Exiting Thread");
                Message.Dispose();
                MQ.Dispose();
                break;
             }
             catch (Exception GenericException)
             {
                // 捕捉接收过程中抛出的所有异常。
                Console.WriteLine(GenericException.Message);
             }
          }
       }
    }

    代码讨论

    MQListen 类包含一个不同于构造函数的函数。该函数封装每个辅助线程要执行的所有工作。在主线程中,您向线程构造函数传递一个对此函数的引用,以便在启动线程时执行该函数。

    Listen 所做的第一件事情是设置一个消息队列对象。MessageQueue 构造函数通过三种实现进行重载。第一种实现使用两个参数:一个字符串参数,指定侦听队列的位置;一个布尔值参数,指示是否为访问队列的第一个应用程序赋予独占读取队列的权限。第二种实现只使用队列路径参数,第三种实现不使用参数。为了简便起见,您可以使用第三种实现,在下一行分配路径。

    如果您引用了队列,则必须创建一个消息对象。消息构造函数也有三种实现方式。如果您想将消息写入队列,则可以使用前两种实现。这两种实现采用两个对象:一个是位于消息正文中的对象;一个是定义如何将对象序列化到消息正文的 IMessageFormatter 对象。在本例中,您将从队列中读取数据,以初始化空的消息对象。

    初始化对象后,您需要输入执行所有工作的主循环。然后,当主线程调用 Interrupt 终止这些线程时,则只有在线程处于等待、睡眠或连接状态下才会被中断。如果没有处于上述三种状态,则要等到下次进入这三种状态中的一种时才会被中断。要确保辅助线程进入等待、睡眠或连接状态,请调用位于 System.Threading 名称空间的 Sleep 方法。对于使用过 Windows API 睡眠函数的 C++ 和 Visual Basic 开发人员而言,Sleep 方法并不陌生。它只使用一个参数:线程处于睡眠状态的毫秒数。如果您从未调用过 Sleep,辅助线程将永远不会进入可以接收中断请求的状态,而会无限制地继续下去,除非您手动关闭进程。

    MQ Receive 方法有两种实现。第一种实现不使用参数,将一直等待接收消息。第二种实现(本例使用这种实现)使用 TimeSpan 对象指定一个超时值。TimeSpan 构造函数包含四个参数:日、小时、分钟和秒。在本例中,Receive 方法在超时和返回前将等待一秒种。

    收到的消息将被分配给先前创建的消息对象,然后,便可以对其进行处理了。本例打开一个带有标签的消息框,并删除了此消息。如果您想在实际使用中采用此代码,则可以在此处放置任何消息处理代码。

    当辅助线程收到 Interrupt 请求后,将发出一个 ThreadInterruptedException 异常。要捕捉此异常,请在 try-catch 块中包含 SleepReceive 函数。您应当指定两个捕获:第一个用于捕获中断异常,第二个用于处理捕获到的错误异常。捕获到中断异常时,请首先将其写入线程正在退出的调试窗口。下一步,对队列对象和消息对象调用 Dispose 方法,以保证所有内存都被清空并发送到内存回收器。最后,中断 while 循环。

    函数退出 while 循环后,关联的线程也将立即结束,代码为 0。在调试窗口,您将看到一则消息,例如“The thread '<name>' (0x660) has exited with code 0 (0x0)”(线程 '<name>' (0x660) 已经退出,代码为 0 (0x0))。现在,线程已经退出该环境,并已自动被破坏。主线程和辅助线程都不需要执行专门的清除操作。

    主窗体

    下一步是向窗体添加代码以生成辅助线程并针对各辅助线程启动 MQListen 类。首先,请向窗体添加下列函数:

    // C#
    private void StartThreads()
    {
       int LoopCounter; // 线程计数
       StopListeningFlag = false; // 跟踪辅助线程是否应当
                                   // 终止的标志。
    
       // 将一个包含 5 个线程的数组声明为辅助线程。
       Thread[] ThreadArray = new Thread[5];
    
    // 声明包含辅助线程的所有代码的类。
       MQListen objMQListen = new
    MQListen(this.ServerName.Text,this.QueueName.Text); 
    
       for (LoopCounter = 0; LoopCounter < NUMBER_THREADS; LoopCounter++)
       {
          // 创建一个 Thread 对象。
          ThreadArray[LoopCounter] = new Thread(new
    ThreadStart(objMQListen.Listen));
          // 启动线程将调用 ThreadStart 委托。
          ThreadArray[LoopCounter].Start();
       }
    
       statusBar1.Text = LoopCounter.ToString() + " listener threads started";
    
       while (!StopListeningFlag)
       {
          // 等待用户按下停止按钮。
          // 在等待过程中,让系统处理其他事件。
          System.Windows.Forms.Application.DoEvents();
       }
    
       statusBar1.Text = "Stop request received, stopping threads";
       // 向每个线程发送一个中断请求。
       for (LoopCounter = 0;LoopCounter < NUMBER_THREADS; LoopCounter++)
       {      
          ThreadArray[LoopCounter].Interrupt();
       }
    
       statusBar1.Text = "All Threads have been stopped";
    }

    代码讨论

    要启动此函数,请创建一个包含 5 个项目的线程数组。此数组将保持对所有线程的引用,以备将来使用。

    MQListen 类的构造函数使用两个参数:包含消息队列的计算机名以及要侦听的队列的名称。构造函数使用文本框中的值来为这两个参数赋值。

    要创建线程,您需要进入循环以初始化每个线程对象。Thread 构造函数要求您向其传递一个委托,该委托在调用线程的 Start 方法时指向要调用的函数。您希望线程开始使用 MQListen.Listen 函数,但该线程并不是一个委托。为了满足线程构造函数的要求,您必须传递一个 ThreadStart 对象,该对象将创建一个给定函数名称的委托。此时,请向 ThreadStart 对象传递一个对 MQListen.Listen 函数的引用。由于该数组元素已被初始化,请立即调用 Start 来开始线程。

    所有线程开始后,请用相应的消息来更新窗体中的状态栏。随着线程的运行和侦听队列,主线程将等待用户请求应用程序停止侦听。为此,主线程将进入一个 while 循环,直至您单击 StopListening 按钮更改 StopListeningFlag 的值。在此等待循环中,将允许应用程序使用 Forms.Application.DoEvents 方法处理其他需要处理的工作。对于熟悉 Visual Basic 的读者来说,这一点与旧的 DoEvents 方法相同。对于熟悉 C++ 的读者来说,这等于编写一个 MSG 泵。

    当用户单击 StopListening 按钮时,该循环将退出并进入线程关闭代码。要关闭所有线程,代码必须检查线程数组,并向每个线程发送一个中断信号。在此循环内部,请对数组中的每个线程调用 Interrupt 方法。调用此方法之前,MQListen 类中的代码将继续正常执行。因此,您可以对每个辅助线程调用 Interrupt,而不必考虑线程是否正在处理其他事件。完成后,线程类将处理所有线程的清除。最后,请在退出前更新主窗体中的状态栏。

    现在,您需要在按钮后添加代码。请向 StartListening 按钮的 Click 事件添加以下代码:

    // C#
    statusBar1.Text = "Starting Threads";
    StartThreads();

    这将更新状态栏并调用 StartThreads 方法。对于 StopListening 按钮,您只需使用以下代码将 StopListeningFlag 设置为 True

    // C#
    StopListeningFlag = true;

    最后一步是为 StopListeningFlag 添加窗体级的变量。请在窗体代码的顶部添加以下行:

    // C#
    private bool StopListeningFlag = false;

    要测试应用程序,您可以下载 MQWrite,这是一个写入消息队列的示例应用程序。

    多线程代码问题

    您已经完成了示例代码,因此您已经具备编写自己的多线程应用程序所需的工具。线程可以显著提高某些应用程序的性能和可伸缩性。在功能增强的同时,您还必须了解线程有危险的一面。使用线程可能会破坏您的应用程序,这样的情况确实存在。线程可能会阻止运行,造成无法预料的后果,甚至会导致应用程序停止运行。

    如果您有多个线程,请确保它们之间不存在互相等待以到达某一点或完成的情况。如果操作错误,可能会导致死锁状态,两个线程都无法完成,因为它们都在相互等待。

    如果多线程要求访问不能轻易共享的资源(如软盘驱动器、串行端口或红外线端口),您可能需要避免使用线程或需要使用一种更高级的线程工具(如 synclocks 或 mutexes)来管理并发性。如果两个线程试图同时访问这些资源,其中一个线程将无法获得资源,或者会导致数据损坏。

    使用线程的另一个常见问题是竞争状态。如果一个线程正在将数据写入文件,而另一个线程正在从该文件中读取数据,您将无法知道哪个线程先完成。这种情况称为竞争状态,因为两个线程都在竞相到达文件末尾。如果读取线程快于写入线程,则将返回无法预料的结果。

    使用线程时,还应当考虑所有线程是否都能够完全独立地进行工作。如果确实需要来回传递数据,在数据相对简单的情况下,只要小心操作即可。传递复杂对象时,来回移动这些对象的封送代价将十分可观。这将导致操作系统管理的额外开销并且会降低总体性能。

    另一个问题是将代码转交给其他开发人员的传递成本。虽然 .NET 确实使线程变得容易,但请注意,维护您代码的下一位开发人员必须了解要使用的线程。尽管这不是避免使用线程的理由,但是它充分说明了应该提供足够的代码注释。

    这些问题本身并不能打消您使用线程的热情,但您在设计应用程序和决定是否使用线程时应该考虑到这些问题。遗憾的是,本文无法详细论述某些避免这些问题的方法。如果您已决定使用线程但遇到了上述某些问题,请检查 synclocks 或 mutexes 看是否能解决问题或引导您使用其他解决方案。

    总结

    有了上述信息,您就可以编写使用线程的应用程序。不过,在编写过程中,请记住上面提到的问题。如果使用得当,那么,与单线程相比,多线程应用程序将具有更好的性能和可伸缩性。但是,如果使用不当,使用线程会适得其反,并且会导致应用程序不稳定。



    -----------------------------------------------------------------------------------------------------------------------------------------------------------------

     

    线程

    线程是一个能独立于程序的其他部分运行的作业。线程属于一个过程,获得自己的CPU时间片。基于WIN32的应用程序可以使用多个可执行的线程,称为多线程。Windows 3.x不能提供一种机制天然地支持多线程应用程序,但是一些为Windows 3.x编写应用程序的公司使用他们自己的线程安排。

    基于WIN32的应用软件能在给定的过程中产生多个线程。依靠生成多个线程,应用程序能够完成一些后台操作,例如计算,这样程序就能运行得更快。当线程运行时,用户仍能继续影响程序。正如前面谈到的,当一个应用程序运行时,就产生了一个相应的过程。那么应用程序就能有一个单独的线程等待键盘输入或执行一个操作,例如脱机打印或计算电子表格中各项的总数。

    在网络世界中,当你试图调整你站点的服务器的性能时,就要运行线程。如果你使用的是IIS,你可以在服务器上设置对于每个处理器所能创建的线程的最大数目。这样,就能在处理器间更均匀地分配工作,从而加速你的站点。

    线程模式

    现在,为了让你知道线程是什么和在哪能使用他们,让我们看一下使用线程时你可能要运行的应用程序:ActiveX组件。ActiveX组件是独立于其他代码运行,基于COM的代码。这听起来是不是很熟悉?当你使用ActiveX组件时,必须在操作系统中注册。其中的一条注册信息就是,这个ActiveX组件是否支持多个线程,如果支持怎样支持。这就是线程模式。

    组件支持的基本线程模式有:单线程,单元线程,组合线程。下面几个部分将谈谈每一种模式对组件来说意味着什么。

    单线程

    如果组件被标记(即注册)为单线程组件,这就意味着所有可执行函数(称作方法)都将在组件的一个共享线程中运行。这就类似于没有生成独立的可执行线程的应用程序。单线程组件的缺点是一次只能运行一个方法。如果多次调用组件,例如调用组件中的存储方法,就会产生瓶颈,因为一次只能有一个调用。

    如果你正在创建或使用一个ActiveX组件,建议不要使用单线程组件。

    单元线程

    如果一个组件被标记为单元线程,那么每个可执行的方法都将在一个和组件相联系的线程上运行。之所以成为单元线程是因为,每个新生成的组件实例都有一个相应的线程单元,每个正在运行的组件都有它自己的线程。单元线程组件要比单线程组件要好,因为多个组件可以在各自的单元中同时运行方法。

    自由线程

    一个自由线程组件是一个支持多线程单元的多线程组件。这意味着多个方法调用可同时运行,因为每个调用都有自己的运行线程。这能使你的组件运行快得多,但也有一些缺点。运行在同一单元中的单元组件可以在单元中直接调用其他组件的方法,这是一个非常快的操作。但是,自由线程组件必须从一个单元向另一个单元调用。为了实现这一操作,WIN32生成了一个代理,用来通过单元界线。这对于每个需要的功能调用来说就产生了系统开销,从而减低了系统的速度。每一个访问自由组件的调用都有一个相应的代理。既然代理调用比直接调用慢,那么自然会有性能方面的降低。

    关于自由线程组件另一个需要注意的是:他们不是真正自由的。如果你创建了一个自由线程组件。你仍必须确保组件中的线程完全同步。这不是一件容易的事。只是简单地把你的组件标记为是自由线程的,并不能使你的组件支持多线程,你仍要去做使你的组件自由线程化的工作。如果你不做这个工作,你的共享数据可能被破坏。这里说明一下为什么:让我们假定你有一个方法计算某个数然后把它写到某个变量中。此方法被传入一个初始值例如是4,在随后的计算中这个变量的值增长为5。在方法结束时这个最后的值被写入到变量中。如果一次只有一个计算过程的话,所有这些会工作得很好。然而,当数据正在被改变时,另一个线程试图访问它,那么重新得到的数据就有可能是错误的。下面的图表说明了这一点。

    为了修正这一错误,开发者为对象提供了线程同步。线程同步是在正在运行你想保护的某一其他代码时运行的代码。操作系统并不先占这个代码,直到获得一个可以中断的信号。如果你想了解更多的有关线程同步对象的详细内容,你不应该阅读Geek Speak column!我的意思是,“注意看一下本文后面列出的参考阅读文献”。

    图二,共享数据被多线程访问搞乱了

     

    组合线程

    读到这,你也许会想既然每种形式的线程都有自己的优点和缺点,为什么不把不同的线程模式结合起来使用呢?组合线程模式也许符合你的要求。一个被标记为组合线程的组件既有单元线程组件的特性又有自由线程组件的特性。当一个组件被标记为组合线程时,这个组件将总是在和生成它的对象所在单元相同的单元中创建。如果组件是被一个标记为单线程的对象创建的,那么这个组件的行为将和一个单元线程组件一样,并且它将在线程单元中创建。这就意味着,组件和创建它的对象之间的调用,不需要一个为通信提供的代理调用。

    如果新组件是被自由线程组件创建的,那么这个组件将表现得像一个自由线程组件,但是它将在同一单元中运行,因此新组件能够直接访问创建它的对象(既不需代理调用)。切记,如果你打算把你的组件标记为组合线程,你必须提供线程同步保护你的线程数据。

    线程

    线程是一个能独立于程序的其他部分运行的作业。线程属于一个过程,获得自己的CPU时间片。基于WIN32的应用程序可以使用多个可执行的线程,称为多线程。Windows 3.x不能提供一种机制天然地支持多线程应用程序,但是一些为Windows 3.x编写应用程序的公司使用他们自己的线程安排。

    基于WIN32的应用软件能在给定的过程中产生多个线程。依靠生成多个线程,应用程序能够完成一些后台操作,例如计算,这样程序就能运行得更快。当线程运行时,用户仍能继续影响程序。正如前面谈到的,当一个应用程序运行时,就产生了一个相应的过程。那么应用程序就能有一个单独的线程等待键盘输入或执行一个操作,例如脱机打印或计算电子表格中各项的总数。

    在网络世界中,当你试图调整你站点的服务器的性能时,就要运行线程。如果你使用的是IIS,你可以在服务器上设置对于每个处理器所能创建的线程的最大数目。这样,就能在处理器间更均匀地分配工作,从而加速你的站点。

    线程模式

    现在,为了让你知道线程是什么和在哪能使用他们,让我们看一下使用线程时你可能要运行的应用程序:ActiveX组件。ActiveX组件是独立于其他代码运行,基于COM的代码。这听起来是不是很熟悉?当你使用ActiveX组件时,必须在操作系统中注册。其中的一条注册信息就是,这个ActiveX组件是否支持多个线程,如果支持怎样支持。这就是线程模式。

    组件支持的基本线程模式有:单线程,单元线程,组合线程。下面几个部分将谈谈每一种模式对组件来说意味着什么。

    单线程

    如果组件被标记(即注册)为单线程组件,这就意味着所有可执行函数(称作方法)都将在组件的一个共享线程中运行。这就类似于没有生成独立的可执行线程的应用程序。单线程组件的缺点是一次只能运行一个方法。如果多次调用组件,例如调用组件中的存储方法,就会产生瓶颈,因为一次只能有一个调用。

    如果你正在创建或使用一个ActiveX组件,建议不要使用单线程组件。

    单元线程

    如果一个组件被标记为单元线程,那么每个可执行的方法都将在一个和组件相联系的线程上运行。之所以成为单元线程是因为,每个新生成的组件实例都有一个相应的线程单元,每个正在运行的组件都有它自己的线程。单元线程组件要比单线程组件要好,因为多个组件可以在各自的单元中同时运行方法。

    自由线程

    一个自由线程组件是一个支持多线程单元的多线程组件。这意味着多个方法调用可同时运行,因为每个调用都有自己的运行线程。这能使你的组件运行快得多,但也有一些缺点。运行在同一单元中的单元组件可以在单元中直接调用其他组件的方法,这是一个非常快的操作。但是,自由线程组件必须从一个单元向另一个单元调用。为了实现这一操作,WIN32生成了一个代理,用来通过单元界线。这对于每个需要的功能调用来说就产生了系统开销,从而减低了系统的速度。每一个访问自由组件的调用都有一个相应的代理。既然代理调用比直接调用慢,那么自然会有性能方面的降低。

    关于自由线程组件另一个需要注意的是:他们不是真正自由的。如果你创建了一个自由线程组件。你仍必须确保组件中的线程完全同步。这不是一件容易的事。只是简单地把你的组件标记为是自由线程的,并不能使你的组件支持多线程,你仍要去做使你的组件自由线程化的工作。如果你不做这个工作,你的共享数据可能被破坏。这里说明一下为什么:让我们假定你有一个方法计算某个数然后把它写到某个变量中。此方法被传入一个初始值例如是4,在随后的计算中这个变量的值增长为5。在方法结束时这个最后的值被写入到变量中。如果一次只有一个计算过程的话,所有这些会工作得很好。然而,当数据正在被改变时,另一个线程试图访问它,那么重新得到的数据就有可能是错误的。下面的图表说明了这一点。

    为了修正这一错误,开发者为对象提供了线程同步。线程同步是在正在运行你想保护的某一其他代码时运行的代码。操作系统并不先占这个代码,直到获得一个可以中断的信号。如果你想了解更多的有关线程同步对象的详细内容,你不应该阅读Geek Speak column!我的意思是,“注意看一下本文后面列出的参考阅读文献”。

    图二,共享数据被多线程访问搞乱了

     

    组合线程

    读到这,你也许会想既然每种形式的线程都有自己的优点和缺点,为什么不把不同的线程模式结合起来使用呢?组合线程模式也许符合你的要求。一个被标记为组合线程的组件既有单元线程组件的特性又有自由线程组件的特性。当一个组件被标记为组合线程时,这个组件将总是在和生成它的对象所在单元相同的单元中创建。如果组件是被一个标记为单线程的对象创建的,那么这个组件的行为将和一个单元线程组件一样,并且它将在线程单元中创建。这就意味着,组件和创建它的对象之间的调用,不需要一个为通信提供的代理调用。

    如果新组件是被自由线程组件创建的,那么这个组件将表现得像一个自由线程组件,但是它将在同一单元中运行,因此新组件能够直接访问创建它的对象(既不需代理调用)。切记,如果你打算把你的组件标记为组合线程,你必须提供线程同步保护你的线程数据。

    posted @ 2006-03-20 16:29 高山流水 阅读(153) | 评论 (0)编辑 收藏

    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-03-20 16:27 高山流水 阅读(98) | 评论 (0)编辑 收藏

    译者按]  Bjarne Stroustrup博士,1950年出生于丹麦,先后毕业于丹麦阿鲁斯大学和英国剑桥大学,AT&T大规模程序设计研究部门负责人,AT&T、贝尔实验室和ACM成员。1979年,B. S开始开发一种语言,当时称为“C with Class”,后来演化为C++。1998年,ANSI/ISO C++标准建立,同年,B. S推出了其经典著作The C++ Programming Language的第三版。C++的标准化标志着B. S博士倾20年心血的伟大构想终于实现。但是,计算技术的发展一日千里,就在几年前人们还猜想C++最终将一统天下,然而随着Internet的爆炸性增长,类似Java、C#等新的、现代感十足的语言咄咄逼人,各种Script语言更是如雨后春笋纷纷涌现。在这种情况下,人们不禁有些惶恐不安。C++是不是已经过时了呢?其前景如何?标准C++有怎样的意义?应该如何学习?我们不妨看看B. S对这些问题的思考。以下文字是译者从Stroustrup1998年之后发表的若干文章、谈话笔记中精选出来的,由于出处不一,内容多有重复,为保持完整,亦一并译出。

    以下内容选自B. S在自己主页上发表的FAQ
    1. 请谈谈C++书。
    没有,也不可能有一本书对于所有人来说都是最好的。不过对于那些真正的程序员来说,如果他喜欢从“经典风格”的书中间学习一些新的概念和技术,我推荐我的The C++ Programming Language, 1998年的第三版和特别版。那本书讲的是纯而又纯的C++,完全独立于平台和库(当然得讲到标准库)。该书面向那些有一定经验的程序员,帮助他们掌握C++,但不适合毫无经验的初学者入门,也不适合那些临时程序员品尝C++快餐。所以这本书的重点在于概念和技术,而且在完整性和精确性上下了不少功夫。如果你想知道为什么C++会变成今天的模样,我的另一本书The Design and Evolution of C++ 能给你满意的答案。理解设计的原则和限制能帮助你写出更好的程序。www.accu.com是最好的书评网站之一,很多有经验的程序员在此仗义执言,不妨去看看。

    2. 学习C++要花多长时间?
    这要看你说的“学习”是什么意思了。如果你是一个Pascal程序员,你应该能很快地使你的C++水平达到与Pascal相近的程度;而如果你是一个C程序员,一天之内你就能学会使用C++进行更出色的C风格编程。另一方面,如果你想完全掌握C++的主要机制,例如数据抽象,面向对象编程,通用编程,面向对象设计等等,而此前又对这些东西不很熟悉的话,花上个一两年是不足为奇的。那么是不是说这就是学习C++所需要的时间呢?也许再翻一番,我想打算成为更出色的设计师和程序员最起码也要这么长的时间。如果学习一种新的语言不能使我们的工作和思想方式发生深刻的变革,那又何苦来哉?跟成为一个钢琴家或者熟练掌握一门外语相比,学习一种新的、不同的语言和编程风格还算是简单的。

    3. 了解C是学习C++的先决条件吗?
    否!C++中与C相近的子集其实比C语言本身要好学,类型方面的错误会少一些,也不像C那样绕圈子,还有更好的支持库。所以应该从这个子集开始学习C++。

    4. 要想成为真正的OO程序员,我是不是得先学习Smalltalk
    否。如果你想学Smalltaok,尽管去学。这种语言很有趣,而且学习新东西总是一个好主意。但是Smalltalk不是C++,而且把Smalltalk的编程风格用在C++里不会有什么好结果。如果你想成为一个出色的C++程序员,而且也没有几个月的时间百无聊赖,请你集中力量学好C++以及其背后的思想。

    5. 我如何开始学习C++?
    这取决于你的基础和学习动机。如果你是个初学者,我想你最好找个有经验的程序员来帮助你,要不然你在学习和实践中不可避免的犯下的种种错误会大大地打击你的积极性。另外,即使你的编译器配备了充足的文档资料,一本C++书籍也永远是必不可少的,毕竟文档资料不是学习编程思想的好教材。
    选择书籍时,务必注意该书是不是从一开始就讲授标准C++,并且矢志不渝地使用标准库机制。例如,从输入中读取一个字符串应该是这样的:
     string s; // Standard C++ style
     cin >> s;
    而不是这样的:
     char s[MAX];  /* Standard C style */
     scanf("%s",s);
    去看看那些扎实的C++程序员们推荐的书吧。记住,没有哪本书对所有人来说都是最好的。
    另外,要写地道的C++程序,而避免用C++的语法写传统风格的程序,新瓶装旧酒没多大意义。
    (遗憾的是,目前在市面上的中文C++教材中,符合B. S的这个标准的可以说一本都没有,大家只好到网上找一些英文的资料来学习了。——译者)

    6. 怎样改进我的C++程序?
    不好说。这取决于你是怎么使用该语言的。大多数人低估了抽象类和模板的价值,反过来却肆无忌惮地使用造型机制(cast)和宏。这方面可以看看我的文章和书。抽象类和和模板的作用当然是提供一种方便的手段建构单根的类层次或者重用函数,但更重要的是,它们作为接口提供了简洁的、逻辑性的服务表示机制。

    7. 语言的选择是不是很重要?
    是,但也别指望奇迹。很多人似乎相信某一种语言能够解决他们在系统开发中遇到的几乎所有问题,他们不断地去寻找完美的编程语言,然后一次次的失败,一次次的沮丧。另外一些人则将编程语言贬为无关紧要的细节,把大把大把的银子放在开发流程和设计方法上,他们永远都在用着COBOL, C和一些专有语言。一种优秀的语言,例如C++,能帮助设计者和程序员做很多事情,而其能力和缺陷又能够被清楚地了解和对待。

    8. ANSI/ISO标准委员会是不是糟蹋了C++?
    当然不是!他们(我们)的工作很出色。你可以在一些细节上找些歪理来挑刺,但我个人对于这种语言以及新的标准库可是欣欣然。ISO C++较之C++的以前版本更出色更有条理。相对于标准化过程刚刚开始之初,你今天可以写出更优雅、更易于维护的C++程序。新的标准库也是一份真正的大礼。由于标准库提供了strings, lists, vectors, maps以及作用于其上的基本算法,使用C++的方式已经发生了巨大的变化。

    9. 你现在有没有想删除一些C++特性?
    没有,真的。问这些问题的人大概是希望我回答下面特性中的一个:多继承、异常、模板和RTTI。但是没有它们,C++就是不完整的。在过去的N年中,我已经反复考虑过它们的设计,并且与标准委员会一起改进了其细节,但是没有一个能被去掉又不引起大地震。
    从语言设计的角度讲,我最不喜欢的部分是与C兼容的那个子集,但又不能把它去掉,因为那样对于在现实世界里工作的程序员们来说伤害太大了。C++与C兼容,这是一项关键的设计决策,绝对不是一个叫卖的噱头。兼容性的实现和维护是十分困难的,但确实使程序员们至今受益良多。但是现在,C++已经有了新的特性,程序员们可以从麻烦多多的C风格中解脱出来。例如,使用标准库里的容器类,象vector, list, map, string等等,可以避免与底层的指针操作技巧混战不休。

    10. 如果不必和C兼容,你所创造的语言是不是就会是Java?
    不是,差得远。如果人们非要拿C++和Java来作比较,我建议他们去阅读The Design and Evolution of C++,看看C++为什么是今天这个样子,用我在设计C++时遵从的原则来检验这两种语言。这些原则与SUN的Java开发小组所持的理念显然是不同的。除了表面语法的相似性之外,C++与Java是截然不同的语言。在很多方面,Java更像Smalltalk(译者按:我学习Java时用的是Sun的培训教材,里面清楚地写道:Java在设计上采用了与C++相似的语法,与Smalltalk相似的语义。所以可以说Java与C++是貌合神离,与Smalltalk才是心有灵犀)。Java语言相对简单,这部分是一种错觉,部分是因为这种语言还不完整。随着时间的推移,Java在体积和复杂程度上都会大大增长。在体积上它会增长两到三倍,而且会出现一些实现相关的扩展或者库。这是一条每个成功的商业语言都必须走过的发展之路。随便分析一种你认为在很大范围内取得了成功的语言,我知道肯定是无有例外者,而且实际上这非常有道理。
    上边这段话是在Java 1.1推出之前写的。我确信Java需要类似模板的机制,并且需要增强对于固有类型的支持。简单地说,就是为了基本的完整性也应该做这些工作。另外还需要做很多小的改动,大部分是扩展。1998年秋,我从James Gosling(Java语言的创始人——译者)那里得到一份建议书,说是要在Java中增加固有类型、操作符重载以及数学计算支持。还有一篇论文,是数学分析领域的世界级大师,伯克利大学的W. Kahan教授所写的How Java’s Floating-Point Hurts Everyone Everywhere(“且看Java的浮点运算如何危害了普天下的芸芸众生”——译者),揭露了Java的一些秘密。
    我发现在电视和出版物中关于Java的鼓吹是不准确的,而且气势汹汹,让人讨厌。大肆叫嚣凡是非Java的代码都是垃圾,这是对程序员的侮辱;建议把所有的保留代码都用Java重写,这是丧心病狂,既不现实也不负责任。Sun和他的追随者似乎觉得为了对付微软罪恶的“帝国时代”,就必须如此自吹自擂。但是侮辱和欺诈只会把那些喜欢使用不同编程语言的程序员逼到微软阵营里去。
    Java并非平台无关,它本身就是平台。跟Windows一样,它也是一个专有的商业平台。也就是说,你可以为Windows/Intel编写代码,也可以为Java/JVM编写代码,在任何一种情况下,你都是在为一个属于某个公司的平台写代码,这些代码都是与该公司的商业利益扯在一起的。当然你可以使用任何一种语言,结合操作系统的机制来编写可供JVM执行的程序,但是JVM之类的东西是强烈地偏向于Java语言的。它一点也不像是通用的、公平的、语言中立的VM/OS。
    私下里,我会坚持使用可移植的C++作大部分工作,用不同的语言作余下的工作。
    (”Java is not platform-independent, it is the platform”,B. S的这句评语对于C++用户有着很大的影响,译者在国外的几个新闻组里看到,有些C++高手甚至把这句话作为自己的签名档,以表明对Java的态度和誓死捍卫C++的决心。实际上有很多程序员不光是把自己喜爱的语言当成一种工具,更当成一种信仰。——译者)

    11. 您怎么看待C#语言?
    就C#语言本身我没什么好说的。想让我相信这个世界还需要另外一个专有的语言可不是一件容易的事,而且这个语言还是专门针对某一个专有操作系统的,这就更让我难以接受。直截了当地说,我不是一个专有语言的痴迷者,而是一个开放的正式标准的拥护者。

    12. 在做大项目时,您是不是真的推荐Ada,而不是C++?
    当然不是。我不知道这是谁传出来的谣言,肯定是一个Ada信徒,要么是过分狂热,要么是不怀好意。

    13. 你愿不愿意将C++与别的语言比较?
    抱歉,我不愿意。你可以在The Design and Evolution of C++的介绍性文字里找到原因。
    有不少书评家邀请我把C++与其它的语言相比,我已经决定不做此类事情。在此我想重申一个我很久以来一直强调的观点:语言之间的比较没什么意义,更不公平。主流语言之间的合理比较要耗费很大的精力,多数人不会愿意付出这么大的代价。另外还需要在广泛的应用领域有充分经验,保持一种不偏不倚、客观独立的立场,有着公正无私的信念。我没时间,而且作为C++的创造者,在公正无私这一点上我永远不会获得完全的信任。
    人们试图把各种语言拿来比较长短,有些现象我已经一次又一次地注意到,坦率地说我感到担忧。作者们尽力表现的公正无私,但是最终都是无可救药地偏向于某一种特定的应用程序,某一种特定的编程风格,或者某一种特定的程序员文化。更糟的是,当某一种语言明显地比另一种语言更出名时,一些不易察觉的偷梁换柱就开始了:比较有名的语言中的缺陷被有意淡化,而且被拐弯抹角地加以掩饰;而同样的缺陷在不那么出名的语言里就被描述为致命硬伤。类似的,有关比较出名的语言的技术资料经常更新,而不太出名的语言的技术资料往往是几年以前的,试问这种比较有何公正性和意义可言?所以我对于C++之外的语言的评论严格限制在一般性的特别特定的范畴里。
    换言之,我认为C++是大多数人开发大部分应用程序时的最佳选择。

    14. 别人可是经常拿他们的语言与C++比来比去,这让你感到不自在了吗?
    当这些比较不完整或者出于商业目的时,我确实感觉不爽。那些散布最广的比较性评论大多是由某种语言,比方说Z语言的拥护者发表的,其目的是为了证明Z比其它的语言好。由于C++被广泛地使用,所以C++通常成了黑名单上的头一个名字。通常,这类文章被夹在Z语言的供货商提供的产品之中,成了其市场竞争的一个手段。令人震惊的是,相当多的此类评论引用那些在开发Z语言的公司中工作的雇员的文章,而这些经不起考验文章无非是想证明Z是最好的。特别是在这些比较中确实有一些零零散散的事实,(所以更具欺骗性——译者),毕竟没有一种语言在任何情况下都是最好的。C++当然不完美,不过请注意,特意选择出来的事实虽然好像正确,但有时是完全的误导。
    以后再看到语言比较方面的文章时,请留心是谁写的,他的表述是不是以事实为依据,以公正为准绳,特别是评判的标准是不是对于所引述的每一种语言来说都公平合理。这可不容易做到。

    15. 在做小项目时,C优于C++吗?
    我认为非也。除了由于缺乏好的C++编译器而导致的问题之外,我从没有看到哪个项目用C会比用C++更合适。
    (不过现在C++编译器导致的问题还是不可忽略的,当你看到同样功能的C++程序可执行代码体积比C大一倍而且速度慢得多时,会对此有所感触的。——译者)

    以下内容来自Visual C++ Developer’s Journal主编Elden Nelson 2000年3月对B. S的专访
    16. 如果您现在有机会从头设计C++语言,您会做些什么不同的事情?
    当然,你永远都不可能重新设计一种语言,那没有意义,而且任何一种语言都是它那个时代的产物。如果让我今天再设计一种语言,我仍然会综合考虑逻辑的优美、效率、通用性、实现的复杂程度和人们的喜好。要知道人们的习惯对于他们的喜好有着巨大的影响。
    现在,我会寻找一种简单得多的语法,我会把类型系统的冲突问题限制在很少的几种情况里,而且你能很容易的发现这些问题。这样就能够很容易的禁止不安全的操作。
    (B. S的原则是:对于糟糕的代码,就算是不能完全禁止,至少也要让它大白于天下,而不是藏在阴暗的角落里暗箭伤人。C++实际上已经提供了这样的机制,例如如果你使用象reinterpret_cast<int>(pointer)这样的很明显是非常糟糕的表达式进行造型,别人会很容易地找到问题所在。只不过C++仍然允许你使用传统的、C风格的造型机制,而又有不少人一直使用这种老式的风格,所以才引来麻烦多多。B. S的意思是说,要是现在能够禁止老式的风格该有多好!作为语言设计者的他,恐怕是没有这个机会了,但是作为语言使用者的我们,却还有很大的希望去改进自己的代码。何去何从,应该是我们深思的时候了。——译者)
    我还会把核心语言的体积尽可能搞得小一些,包括类和模板的关键的抽象特性,而把很多其它的语言特性放在库里来解决。当然我也会保证核心语言足够的强大,使得那些库本身也足以用这个核心语言来产生。我可不希望标准库的创建需要用到什么不属于该语言本身的神秘机制。另外我会让这个核心语言的定义更加精确。(有不少新的语言在建库时就使用了一些“不属于该语言本身的神秘机制”,比如VB和JAVA。从理论上讲,这是近乎无赖的行径,所以B. S不以为然。不过从实用出发倒也无伤大雅。——译者)
    最重要的是,我会在该语言被广泛使用之前尽可能维持一个很长的酝酿期,这样我可以以其他人的反馈为基础进行改进。这可能是最困难的,因为一旦有什么东西是明显出色和有前途的,大家就会蜂拥而至的来使用它,此后作任何不兼容的修正都会是非常困难的。
    我相信这些思想与我当初设计C++时的理念是非常类似的,同样也是这些思想指引着一二十年来C++的不断演化。当然,我认为现在还没有什么东西能让我觉得像是“完美的语言”。
    17. 您预期C++做哪些增强,会不会删掉一些东西?
    很不幸,虽然有一些东西很应该扔掉,但恐怕很难真的删掉任何东西。第一个应该抛弃的东西就是C风格的造型机制和类型截断转换。就算不禁止,编译器的作者们至少也应该对这种行为给与强烈的警告。我希望能用类似vector的东西彻底取代数组,但这显然是不肯能的。不过如果程序员们能主动使用vector来代替数组,就会立刻受益匪浅。关键是你不必再使用C++中最复杂难缠的技巧了,现在有优秀得多的替代方案。
    至于主要的特性,我没想去掉任何东西。特别是那些把C++与C区别开来的主要特性恐怕没法风平浪静的被抛掉。通常问这些问题的人是希望我挑出诸如多继承、异常、模板等机制来接受批判。所以在这我想大声讲清楚,我认为多继承机制对于静态类型语言实现继承性来说是必需的,异常机制是在大系统中对付错误的正确方法,模板机制是进行类型安全的、精致的和高效的程序设计的灵丹妙药。我们可以在小的细节上对于这些机制挑挑刺,但在大的方面,这些基本的概念都必须坚持。
    现在我们仍在学习标准C++,也正在标准所提供的特性基础上发展出更新的、更有趣的编程技术。特别是人们刚刚开始使用STL和异常机制,还有很多高效强大的技术鲜为人知,所以大可不必急匆匆的跑去增加什么新的机制。
    我认为当前的重点是提供很多新的、比以前更加精致的、更有用的库,这方面潜力巨大。例如,如果有一个能被广泛使用的、更精致的支持并发程序设计的库,那将是一大福音——C风格的线程库(例如Pthread——译者)实在不够好。我们也就可以与各种其他的系统,例如SQL以及不同的组件模型更好地契合起来。数值计算领域的人们在这方面好象已经走在了前面,类似像Blitz++、POOMA、MTL之类的高效而精致的库的开发已经取得了非凡的成就。(译者在Internet上造访了Blitz++和POOMA的主页,前者是一个高性能数学库,据称其性能与Fortran 77不相上下,同时又支持大量的C++特性。我想凡是对于数值计算领域有所了解的人都知道这有多么伟大的意义。POOMA则是一个专门研究C++并行数学算法的项目,它的前景更加不可限量。译者非常认同B. S的这个观念。——译者)
    有了足够的经验之后,我们就能更好的决定应该对标准做些什么调整。

    18. 显然,这几年世界变了,正在走向一个以Web为中心、分布式计算为主流的时代。那么您觉得C++还能维持其地位吗?程序员们可不可能把若干种专用语言(比如Perl、Javascript)综合运用以彻底取代某一种通用语言?(C++就是这样的通用语言——译者)为了配合新的计算模式,C++及其标准库应该做怎样的调整?
    从来没有哪一种语言能适合所有的工作,我恐怕以后也不会有。实际系统通常是用多种语言和工具构造起来的。C++只是想成为若干语言和工具中的一个,当某些专用语言在其领域里特别突出时,它们可以与C++互为补充。也就是说,我觉得如果大多数现在的专用语言能借助特定领域的C++库共同工作的话,它们会表现得更出色。脚本语言通常导致难以维护的代码,而且也没有给程序的结构、可扩展性和可维护性的优化留下什么余地。
    我不敢肯定未来的代码是否真的会是以Web为中心的。就算是直接处理Web的系统也主要是由处理本地资源,如IP连接之类的程序模块构成的。
    地理上的分布性以及服务器软件对于并发机制的高度依赖对于系统的建造者来说的确是个挑战。有些针对上述问题的库已经出现,也许我们将会看到它们最终得以标准化。当然,一些原操作和保证规则应该被加到核心语言中以提供对这些库的更佳支持。
    总的来说,对于Web和网络,我们非常需要一个真正的系统/网络级的安全模型。指望JavaScript之类的脚本语言实现这个模型无异于白日做梦。注意,我也没说C++提供了这个问题的解决方式。C++的重心是高效的访问系统资源,而不是反欺诈。

    19. 您看C++未来的走向如何?在接下来的10年里它会衰落吗?或者是基本保持现在的形式?或者发展变化呈不同的形式?
    C++有着最美好的未来。用它你能写出伟大的代码。除了故意进行恶意欺诈,C++仍将是开发高性能、高复杂度系统的最好语言。据我所知,没有那种语言能在通用性、效率和精致三方面的统一上可与C++相题并论。
    我没看到C++有衰落的征兆。在我能预见的未来里,它的用途还会不断增长。当然,在未来的十年里我们会看到一些变化,但不会像你想得那么显著。跟每一种语言一样,C++也会发展变化。“语言专家们”要求改进的喧嚣声震耳欲聋,但是系统开发者们的基本请求是保持稳定。
    C++会改进,但是这些改进将主要是为了反映从实践中得来的经验教训,而不会是为了追风尚赶时髦。为了更高效地使用一些新的编程技术,比如通用编程技术,可能会增加一些小的特性。会有大量的库涌现,我预期会出现一种崭新的、更出色的库支持机制。我希望新的扩展主要集中在支持抽象方面的一般特性,而不是为支持某些特殊任务的特定机制。
    例如,“属性”这个概念是很有用的,但我不认为在一种通用编程语言中有它的容身之地。用标准C++的一组类可以很容易地支持这一概念。如果我们感觉那族类对于“属性”这一概念的支持不尽如人意,也不会立刻跑去在语言里增加属性机制,而是仔细考虑如何改进类和模

    posted @ 2006-03-20 16:16 高山流水 阅读(78) | 评论 (0)编辑 收藏

     
    ------专访C++之父Bjarne Stroustrup博士------
    《C++程序设计语言(特别版)》试读
    感谢C++ View让我们转载它的这篇专访(2000-2002 C-View.ORG All Rights Reserved),未得C++ View同意,任何人请勿将此文再做转载。采访:虫虫,翻译:ALNG Bjarne Stroustrup博士,C++语言的设计者和最初实现者,现任AT&T实验室的大型程序设计研究部的主管。著有《C++程序设计语言》(1985年第1版,1991年第2版,1997年第3版,2000年特别版)、《The Annotated C++ Reference Manual》和《C++语言的设计与演化》。Bjarne于1950年生于丹麦美丽的港口城市奥尔胡斯市,在奥尔胡斯大学取得硕士学位,在英国著名的剑桥大学完成了他的博士学业......更多>>>
    专访索引
      1、C++的标准化进程
    2、C++的模板函数
      3、经典流 4、C++、Java与C#
      5、Bjarne看C++的机制 6、STL与C++的GUI
      7、在C++中相得益彰的GP和OO 8、今后C++将支持分布开发
      9、爱好广泛的Bjarne 10、Bjarne的中国观
    1、C++的标准化进程                              TOP
    记者:C++的ANSI/ISO标准化标志着C++的成熟。能告诉我们在这个标准化的过程中,您感到最难忘、最快乐以及最遗憾的事分别是什么吗?
    Bjarne Stroustrup标准化进程其实是一项极具价值的重大活动,但是人们对它认识太不足了,而且整个进程也是荆棘满途。实际上,通过标准化活动,C++语言显得越发成熟和完善了,还因此而获得了有着惊人表达能力的标准库。编译器的厂商老想束缚住他们的用户,而正式的标准化活动,则是用户们为数不多的自卫手段之一。
      很难说哪一件事是最特别的。在委员会中,大多数的工作都是发现、提炼和建立信任的这样一个过程,这都需要花费大量的时间。不过最重要的事莫过于以下两件事,其一是1990年基于《The C++ Programming Language》第2版的参考手册(有模板和异常处理机制的那一版)进行C++标准化的那第一次的投票,其二则是1998年批准ISO标准的最终表决。毋庸置疑,在这两件大事当中,将STL接纳为标准库一部分的投票是一件最令人欢欣鼓舞的快事。
      可以说,没有任何负面或者遗憾的事情能与这些具有进步意义的投票相提并论。说到"遗憾",要么是一些十分微小的技术细节,要么就是那些(暂时)分化了委员会而使进展缓慢的讨论。例如,我本来是反对C风格的强制类型转换,也不想引入仅允许整型的静态常量成员在类中初始化的机制。不过,这都是些无关痛痒的小节。

      我正期待着另外一次关键的表决。明年(2002年)的某个时候,委员会将决定ISO C++的未来方向,这可是头等大事啊!

    2、C++的模板函数                              TOP
    记者:Alexander Stepanov说有一次他曾经与你争论。因为他认为C++的模板函数应该像Ada通用类一样显式实例化,而你坚持认为函数应使用重载机制隐式实例化。正是由于您的坚持,这一技术后来在STL中发挥了重要作用。能跟我们具体谈谈吗?
    Bjarne Stroustrup:对此,我已经没有多少可补充的了。在模板成为C++的一部分之前,Alex和我曾经花了一些时间去讨论语言特性。从我的角度来看,当时的Ada经验给他施加了过大的影响,而Alex有着自己的优势--泛型编程的宝贵实践经验,这恰恰是我的不足。他强化了我对不牺牲效率和内限制表达能力或牺牲效率的实现方法。尤其是过去我对能否把模板参数限制在继承层次持怀疑态度,如今我态度依然。联的偏好。我们都讨厌宏而喜欢类型安全。他本来想要更强的模板参数的静态类型检验,我也是这么想的,不过还没有找到可以不
      后来Alex创造性地使用了我所设计的模板特性,这就导致了STL的诞生,使得目前人们开始重视泛型及生成编程。跟Alex争论很有意思!关于我对他风格的印象,参看http://www.stlport.org/resources/StepanovUSA.html【记者注:这是一篇STL之父Alexander Stepanov的访谈录,内容相当激进,心脏不好的人请做好一切必要准备^_^。Alex在GP上有极深的造诣,这篇访谈颠覆性不小,甚至可以看到他对OO的批判!也许彻底抛弃OO很难,但Alex的话确实富有启发性,值得一看】。
      我曾经试验过多种在不使用语言扩展的情况下约束模板参数的方式。个人早期的想法在《The Design and Evolution of C++》(《C++语言的设计与演化》的中文版和影印版均已由机械工业出版社引进出版)一书中已有详述,其后期的变体如今成为了普遍使用的约束和概念检查的一部分。这些系统在表现力和弹性上比在其他语言中的常见内建设施要强很多。如果要举例的话,可以参阅我的C++ Style and Technique FAQ(http://www.research.att.com/~bs/bs_faq2.html#constraints)。

    3、经典流                                  TOP
    记者:Jerry Schwarz在Standard C++ IOStream and Locales一书的前言中回顾了IOStream的历史。我想在从经典流到标准IOStream的转变过程间一定有很多趣事,您能给我们讲一些呢?
    Bjarne Stroustrup我不想替这次转变再多说些什么了。然而,我想强调的是原来我所设计的流库简单且高效,我在两个月内就完成了设计和建构。
      那次关键的决策把格式与缓冲分离开来,并使用了类型安全的表达式语法(依赖于<<和>>运算符)。与AT&T贝尔实验室的同事Doug McIlroy进行了一番探讨之后,我最终做出了这些决策。实验表明,诸如<、>、逗号和=都不太合适,后来我选择了使?lt;<和>>。类型安全使得在编译时就可以决定一些原本在C风格库中需要在运行时才决定的事情,因而其性能非同一般。在我刚开始使用流以后的不久,Dave Presotto就把我的实现的缓冲部分替换成一个更出色的部分,我一直都没有注意到这一点,直到他后来告诉我。
      目前的IO流肯定小不了。不过我坚信,在许多通常没有使用IO流全部通用性的情况下,借助于强力的优化,我们可以重获原来的效率。注意,IO流之所以如此复杂,大部分的原因是为了满足那些我原来所设计的经典流没能考虑到的需求。例如,带本地化的标准IO流就可以处理经典流力不能及的汉字和汉字串。

    4、C++、Java与C#                             TOP
    记者:有人说Java是纯粹面向对象的,而C#更胜一筹。而还有很多人说它们纯粹是面向金钱的。以您之见呢?
    Bjarne Stroustrup我喜欢"面向金钱"这个词 :-) 还有Andrew Koenig的成语"面向大话"我也喜欢。 不过,C++可不面向这两个东东。
      对这点我还想指出,我认为纯粹性并不是优点。C++的强项恰恰在于它支持多种有效的编程风格(多种的思维模型,如果你一定要这么说)以及他们之间的相互组合。最优雅、最有效也最容易维护的解决方案常常涉及不止一种的风格(编程模型)。如果一定要用吸引人的字眼,可以这么说,C++是一种多思维模型的语言。在软件开发的庞大领域之中,需求千变万化,需要至少一种支持多种编程风格的通用语言,而且很可能需要一种以上。再说,世界之大,总能容得下好几种编程语言吧?那种认为一种语言对所有应用和每个程序员都是最好的看法,本质上就是荒谬的。【注:paradigm的中文翻译似乎没有约定。有人偏好"典范"或者"范式",记者则喜欢侯捷先生使用的"思维模式"或者"思维模型"。总之,paradigm的大概意思是an example or pattern,大家理解就行。】
      Java和C#的主要力量源自于其所有者的支持。这意味着低价(为取得市场份额免费发放实现和库),集约和不择手段的营销(欺骗宣传),以及由于缺乏替代厂商而产生的表面上的标准。当然,就Java的情形而言,当其他厂商和修改版本出现后,版本、兼容性和移植问题也会像其他语言一样,重新冒出来。
      不被语言所有者操纵的开放进程所产生的正式标准是最好的。如果用户不想看到这种语言为了其发起者或所谓"一般用户"的利益,而不顾经济上无足轻重的"少数派"反对而来回折腾,像ISO这样正式的标准进程,则是他们唯一的希望了。
      C++本可以更简单或更易用些(更纯粹,如果你认为这是必要的),不过这样就无法支持那些有着"不同寻常"的需求的用户了。我个人很关注他们,他们要构建可靠性、运行效率以及可维护性远高于行业平均水准的系统。我的猜测是,在10年内大多数的程序员都将面临"不同寻常"的技术要求,他们可以从C++的多思维模型结构中受益,而Java和C#之类"简化"语言则力有不逮了。
      我认为模板和泛型编程是现代C++的核心,是无损效率、类型安全代码的关键。然而它们并不适合经典的面向对象编程思维模型。

    5、Bjarne看C++的机制                           TOP
    记者:Ian Joyner在C++: A Critique of C++ and Programming and Language Trends of the 1990s一书中比较了C++和Java并批评了C++的许多机制。你赞成他的观点吗?尤其是多数新语言都有垃圾收集机制,C++中会加入吗?
    Bjarne Stroustrup:Ian Joyner对C++的观点,我不敢苟同。撇开这点,垃圾收集可能算是有价值的技术,不过并不是万能丹,它也会带来问题。对C++而言,自动垃圾收集是一个有效的实施技术,有许多为C++设计的不错的垃圾收集器(商业支持和免费的都有),而且也被广泛地使用(参看我的C++页面上的链接)。然而C++中垃圾收集机制应该是可选的,这样在不适合垃圾收集的地方,如严格的实时应用程序,可以免受其累。关于垃圾收集,我的《The C++ Programming Language》(《C++程序设计语言》)一书和我的主页上都用评注,可以参看。
      我期望在下一个C++标准中能体现出我的意见,并做出明确的声明。就此而论,C++可以优雅地处理一般的资源,而不仅仅局限于内存。尤其是"resource acquisition is initialization(资源获得就是初始化)"技术(参看D&E、TC++PL和我的技术FAQ)支持对任意资源进行简单并且符合异常安全(exception-safe)要求的管理。没有析构函数的Java不可能支持这一技术。

    6、STL与C++的GUI                               TOP
    记者:STL是一个超凡脱俗的跨平台架构。有没有考虑在其他方面,比如GUI(图形用户接口),设计这样的标准架构?
    Bjarne Stroustrup:很自然地,很多人会想如何在其他领域借鉴STL的成功。比如在数值运算和图论方面都有了许多有趣的工作。相关链接可以参看我的网页。

      标准GUI价值极大,不过我怀疑其政治上的可行性。太多有钱的大公司在维持其专有GUI上有着重大的商业利益,而且要求用户放弃现在所使用的GUI库也殊非易事。【注:有朋友可能奇怪,一个GUI库怎么扯出"政治(politically)"来了?西方人口中的"政治",在中文里并没有真正对应的词语。这里的意思是of concerning public affairs,跟中文里的"政治"无关。下一段就是对这个所谓"政治上的可行性"的详细解释。】
      这里我所说的可行性是就商业和技术而言。现在有好几种广泛使用的GUI,即使标准委员会提供一个替代方案,它们也不会就此退出。其所有者和用户──常常有充分理由──会只是忽略新标准。更糟的情况:某些公司或群体会积极反对这样的标准,因为他们认为标准不如他们已有的库,或者因为差异太大而使得转换到新GUI不可行。必须理解,如果标准不能充分服务于其目标用户,用户会视而不见。许多ISO标准因为无人理会而变得无关紧要。C++标准可不想成为其中一员──把现有实施拉近到一起,标准就功德无量了──我们不希望将来ISO C++标准被人忽略。
      注意STL成功的一个主要原因在于它是一个技术突破。它可不单是"又一个容器库",因此它不需要和许多现有的容器库(其中几个品质卓著)直接竞争。我猜想C++要有一个标准GUI,我们需要技术突破,加上好运多多。
      不过我还是怀疑委员会有由必需的专业技术和资源来构建一个可以成为真实世界中真正标准的GUI。
      我对标准库的想法倾向于修补现有库的遗漏(如hash_map和正则表达式),以及通过更广泛的运行时间类型信息和并发库来支持分布运算(可选)。
      有时大家忘了,库不是非得成为标准的一部分才有用。有成千上万有用的C++库。例如,参见C++库FAQ(我的C++网页有链接)。

    7、在C++中相得益彰的GP和OO                          TOP
    记者:泛型编程是C++特殊的编程思维模型。你是怎样看GP(泛型编程)和OO(面向对象)的?将来C++会提供更强大的机制来支持GP吗?有没有考虑引入其他思维模型,比如面向模式?
    Bjarne Stroustrup我认为,在C++中面向对象和泛型编程相得益彰,我所写的许多最优美的代码段都是两者的结合。也就是说,那些认为OOP和GP水火不容的观点是错误的。它们是应该组合使用的技巧,语言应该支持这样的组合──C++正是如此。
      我觉得C++相当好地支持了泛型编程,所以只需要细微的增加。模板化的typedef是个显而易见的例子。我们要谨慎地扩展语言,仅当扩展对要表述的内容提供重大的便利时,我们才这样做。比如我不支持对模板参数约束检查提供直接语言支持的想法。通过约束/概念检查模板,我们已经可以比用为C++和相似的语言提议的各种各样的语言扩展做得更多。
      谈起"思维模型"和"新的思维模型"让我很为难,只有很少的想法佩得上这样美妙的字眼。我也担心对新观念过于直接的支持,可能会限制和跟不上我们的观念和技术的进一步演化。理想的情况是,语言设施应有效地支持非常通用的观念,这样大家可以使用这些设施用各种风格来编写代码。至于C++能优雅地支持哪些模式概念,能和不能通过与已有风格的组合,还有待观察。我认为,只需要很少新的特定语言概念来支持模式。
    8、今后C++将支持分布开发                          TOP
    记者:今后C++会支持分布开发吗?对RTTI和多线程的进一步支持呢?
    Bjarne Stroustrup:对。如果事情进展能如我所愿,C++标准的下一次修订会通过提供扩展的类型信息和并发支持库来支持分布计算。我觉得这不需要特别的语言扩展。不过在存在并发的情况下现有语言设施实施需要做出额外的保证。
      我没有太多可说,因为围绕下一标准应该和不该包含哪些的讨论才刚刚开始。我的看法是C++需要一个无缝地支持线程(在同一地址空间内)、进程(在不同地址空间)及远端进程(可能有重大的通讯延时而且网络可能暂时分离)的标准库。支持这点会需要超越简单的Unix或Windows线程的设施。但是我并不认为这需要设计新的语言元件。

    9、爱好广泛的Bjarne                            TOP
    记者:据说大人物年轻时就会表现出与常人的差异,请问您在大学就读时表现过什么与众不同的地方?
    Bjarne Stroustrup并不清楚是否有人觉得我真的与众不同。我猜想自己可能比多数人天真和显得理想主义那么一点点。此外,比起大多数人,我花在解决现实问题的时间会多一点吧──我要挣钱以免债台高筑。我可不能如此,因为家里并不富有,我一直被告诫要勤奋工作。另一方面,我喜欢学习自己感兴趣的许多东西(包括哲学和历史),而不仅仅是那些直接有助于我取得学位和提高成绩的东西。
    10、Bjarne的中国观                             TOP
    记者:喜欢安徒生的童话吗?在《夜莺》里他写到了中国。您对中国、中华文化和中国人的印象如何?以前去过中国吗?2008年来中国看奥运会可能是个不错的主意。
    Bjarne Stroustrup
    :作为一名丹麦人,我当然知道安徒生童话。恰好我也很喜欢这些故事。在《夜莺》里描绘的中国纯属虚构,与当时的中国可能有也可能没有任何联系。安徒生创造的那个"中国",是用来泛指许多个国家及其统治者的。
      中国是个巨大的概念Bjarne Stroustrup,很难能对之有一个总体的印象。我所遇到的中国人大都是程序员或者工程师,因此我对中国人的看法可能会过于狭隘。纵使是类似于我的本国丹麦这样的小国和文化体也是十分复杂的,不是单个人能够完全理解的──丹麦只有500万人口。我对历史很感兴趣,因此也看了数本有关中国历史和文化题材的书籍。不过这可能意味着我头脑里的中国会比较古老,与现在的中国并不能相提并论。我在台湾进行过一个星期的讲学,那里挺不错的,不过目前我尚没有机会访问大陆。
      关于中国历史和文化的书我看过不少。中国历史悠久、幅员辽阔,因此书的内容就集中于早期的事件、人和传统,几乎没有描绘近10年或者20年的中国。尽管从新闻和中国朋友那里得知中国已经发生了巨大的变化,但是我对今日的中国还是相当无知(但是可能比大多数人对远方国度的那种无知要强一些),比如我对当今中国的文学和音乐一无所知。因而一旦想起中国,我就可能想起很多严重过时的东西──尽管自己极力去避免此类的错误。这里顺便说一句,我对主要从书本上获知的世界其他地区也都有类似的问题。
      我对大型人群和有组织的群体事件不太热心,因此我会远离2008年奥运会,就象我远离那些原本可以参与的各届奥运会一样。我希望能找个除此之外的机会访问中国。

    Bjarne著作参考                              TOP

    http://www.china-pub.com/computers/bookinfo/zfbs.htm

    posted @ 2006-03-20 16:11 高山流水 阅读(102) | 评论 (0)编辑 收藏



    引言

    自从Dennis M.Ritchie于1973年设计并实现C语言以来,计算机程序设计领域已经发生了巨大的变化。以C语言为根基的C++、Java和C#等面向对象语言相继诞生,并在各自领域大获成功。今天,C及其后裔几乎统治了计算机程序设计世界。可以这么说,C语言的诞生是现代程序语言革命的起点,它改变了程序设计语言发展的轨迹,是程序设计语言发展史中的一个里程碑。

    然而,C并不仅仅是其他语言的起点和基础,否则它早已成为程序设计语言发展过程中又一个被遗弃者。和当初发明时一样,C语言今天依然至关重要。它的表达力、效率、简练、紧凑、对机器的最终控制以及跨平台的高度移植性,使其在系统编程、嵌入式编程等领域一直占据着统治地位,而C99标准的制订则再一次使C语言焕发出新的活力。下文介绍C程序设计领域中的几本好书,其中一些堪称经典。

    1. Brian W.Kernighan, Dennis M.Ritchie,《C程序设计语言》,机械工业出版社

    这是迄今为止在所有程序设计语言书籍中最广受尊敬的一部经典,是任何一名C程序员的必读之作。因为出自C语言的设计者Dennis M.Ritchie和著名的计算机科学家Brian W.Kernighan之手,它被昵称为“K&R C”。是它首先引入了“Hello World!”程序,这个程序几乎成了后来任何一本入门性程序设计语言书籍中的第一个例子。

    如同C语言本身简洁紧凑而极具威力一样,这本书轻薄短小而极富张力。通过简洁的描述和典型的示例,它全面、系统、准确地讲述了C语言的各个特性以及C程序设计的基本方法,内容涵盖基本概念、类型和表达式、控制流、函数与程序结构、指针与数组、结构、输入与输出、UNIX系统接口以及标准库等内容。

    简洁清晰是这本书最大的特色。这本小书可以教给你许多比它厚几倍的“大部头”的知识。我认为那些动辄洋洋洒洒拼凑出好几百页乃至上千页的技术作者应该好好向K&R学一学。对于中、高级程序员而言,如果希望迅速获得C语言的严肃知识而又不愿意多花费哪怕一丁点时间,这本书就是首选。

    顺便说一句,这本书的索引制作非常出色,极具实用价值,这可能首先要归功于正文部分的简明扼要。此外,尽管它看上去很像一本教程,但其实更是一本写给专业程序员的指南。如果你不具备任何其他语言程序设计背景或基本的C语言知识,这本书也许并不适合用作你的C语言启蒙读物。

    2. Perter Van Der LinDen,《C专家编程》,人民邮电出版社

    C语言是严肃的程序员语言,但这并不意味着C语言书籍必须板着面孔说教。在这本被C程序员昵称为“鱼书”(封面上有一条“丑陋的”腔棘鱼)的著作中,作为SUN公司编译器和操作系统核心开发组成员之一,Peter淋漓尽致地展示了其深厚的技术沉淀、丰富的项目经验、高超的写作技巧以及招牌式的幽默。在这部作品中,作者以流畅的文字、诙谐的笔法将逸闻典故、智慧和幽默自然地融入技术描述中,读来宛若一本小说,极富趣味。

    本书讲述了C语言的历史、语言特性、声明、数组、指针、连接、运行时以及内存使用等知识,揭示了C语言中许多隐晦之处,尤其深入解析了声明、数组和指针、内存使用等方面的细节。要想成为一名专家级C程序员,这些内容都是必须掌握的。和其他满是抽象例子的C语言书籍不同,这本书充满了大量的来自真实世界的C程序设计实例,它们对C程序员具有很高的参照价值。另外,每一章都以极富趣味的“轻松一下”收尾,而附录A“程序员工作面试的秘密”则是任何语言的程序员在应聘工作前增强自信的好材料。

    我怀疑真正的C专家可能用不着看这本书 - 从内容到组织方式到行文风格都决定了这是一本轻松愉快的“从菜鸟到高手”的进阶读本,所以,它理应拥有更广泛的读者群。初级程序员往往更需要热情的鼓励,在阅读这本书的过程中,你定会深深地被作者对编程的激情所感染。

    世间并无完美。我认为这本书的缺陷在于,和大多数平庸的C语言书籍一样,它画蛇添足地加入了一章关于C++的描述。在今天看来,这个描述既不全面也有失公允。不过,鉴于作者是在1994年从一名C程序员的角度去观察C++,这一点也就不足为奇了。

    3. Samuel P. Harbison, Guy L. Steele,《C语言参考手册(第五版)》(影印版),人民邮电出版社

    在C语言参考手册类书籍里,Samuel P. Harbison 和Guy L. Steele合著的《C: A Reference Manual》是非常出色的一本。这本手册的第五版新增了对C99标准的介绍,以便满足新时期C语言学习的需要。全书共分为两大部分,第一部分专注于C语言特性,第二部分则全面讨论了C标准库。本书涵盖C99、C89、传统的C、所有版本的C运行库以及编写与C++兼容的C代码等一切知识。

    这本手册只是中等厚度,但它比“比它更厚”的其他参考手册更清晰地描述了C语言的现在和过去的方方面面。整本手册技术细节描述精确,组织条理清楚,内容完备详尽而又简明扼要。可以这么说,它在广度、深度和精度方面都是出类拔萃的。对于中、高级C程序员而言,这本手册值得常备案头,它几乎肯定要比K&R的著作使用频率更高。

    2004年2月1日补充:我手头有这本书的中文版:《C语言参考手册》(机械工业出版社出版)。它在很大程度上减轻了我的查阅负担,不过偶尔也增加了理解上的困难。

    4. David Hanson,《C语言接口与实现:创建可重用软件的技术》,机械工业出版社

    C语言能够历经三十多年而不衰,一个重要的原因在于它的适应能力。在这“复用”、“面向对象”、“组件”、“异常处理”等先进机制漫天飞舞的年代,C语言仍然能够凭借它小而优雅的语言特性,在相当程度上满足现代软件体系架构提出的要求。只不过,想要达到这个程度,必须要在C的应用功力上达到最高层次。在嵌入式、系统软件以及对性能要求极高的系统开发中,开发人员必须达到这样的层次,熟练掌握C语言的高级特性,才能够同时满足效率和灵活性、复用性的要求。可惜,虽然C语言技术图书汗牛充栋,但是关注这个峰顶之域的作品却是屈指可数。David Hanson的《C Interfaces and Implementations》就是个中翘楚。

    David Hanson是业内大名鼎鼎的自由编译器lcc的合作者。在这个项目中,他负责提供高度可复用的基础架构。在不断的实践中,他完全使用ANSI C形成了一整套可复用组件库。这套组件库架构清晰,性能优异,而且提供了很多高级的特性,比如类Win32 SEH的异常处理机制,可移植的线程库,高性能的内存池,丰富的可复用数据结构组件。David Hanson把他在创作这些组件的过程中所积累的心得以及对其源码的精致剖析原原本本地写在了这本书里。这样的著作,当然堪称C语言领域里的铭心绝品。难怪已故著名技术作家Richard Stevens对此书赞不绝口,他说:“这本书中的技术,对于大部分C程序员来说,已经遗忘得太久了。”对于希望能在C语言应用上达到最高层次的核心程序员而言,这本书是难得的必读之作。

    其他

    除了以上四本书以外,我还乐意推荐Andrew Koenig的著作《C陷阱和缺陷》(人民邮电出版社)和Deitel父子合著的《C How to Program》两本书。

    Andrew Koenig是世界上屈指可数的C++专家,他的这本书可能是最薄的一本C语言经典。它简明扼要地讲述了C程序设计中的陷阱和缺陷,包括词法陷阱、语法陷阱、语义陷阱、连接、库函数、预处理器以及可移植性缺陷等,最后一章还给出了关于如何减少程序错误的建议以及前面各章问题的参考答案。尽管这个小册子成书于C89标准制定之前,然而,即使到了C99早已颁布的今天,书中提到的大多数陷阱和缺陷一如十五年前那样使我们警醒。

    Deitel父子合著的《C How to Program》一直是非常好的C语言入门教程,我手头的中译本名为《C程序设计教程》(机械工业出版社出版,原书第二版)。除了对技术的正规描述(辅以许多简明扼要的例子)外,每一章后面都带有小结、术语、常见的程序设计错误、良好的程序设计习惯、性能忠告、可移植性忠告、软件工程评述、自我测验练习及答案等。整书内容清晰,组织良好,易于阅读和理解。值得一提的是,有许多入门书读完一遍即可扔掉,而这一本是个例外。

    posted @ 2006-03-20 16:07 高山流水 阅读(89) | 评论 (0)编辑 收藏

    一:说明
    在本文章中使用精通、熟练、熟悉、了解标志你对某技术的掌握程度。
    精通:能够掌握此技术的85%技术要点以上,使用此技术时间超过两年,并使用此
    技术成功实施5个以上的项目。能使用此技术优化性能或代码,做到最大可能的重用。
    熟练:能够掌握此技术的60%技术要点以上,使用此技术时间超过一年,并使用此
    技术成功实施3个以上的项目。能使用此技术实现软件需求并有经验的积累在实现之前
    能做优化设计尽可能的实现模块或代码的重用。
    熟悉:能够掌握此技术的50%技术要点以上,使用此技术时间超过半年上,并使用此
    技术成功实施1个以上的项目。能使用此技术实现软件需求。
    了解:可以在实际需要时参考技术文档或帮助文件满足你的需要,基本知道此项技术在
    你运用是所起的作用,能够调用或者使用其根据规定提供给你的调用方式。
    二:基本要求
    1:html 掌握程度:熟练。原因:不会html你可能写JSP?
    2:javascript/jscript:掌握程度:熟悉。原因:client端的数据校验、一些页面处理需要你使用脚本。
    3:css 掌握程度:熟悉。原因:实现页面风格的统一通常会使用css去实现。
    4:java基础编程 掌握程度:熟练。原因:不会java你能写JSP?开玩笑吧。还有你必须非常熟悉以下几个包java.lang;java.io;java.sql;java.util;java.text;javax.sevrlet;
    javax.servlet.http; javax.mail;等。
    5:sql 掌握程度:熟练。原因:如果你不使用数据库的话你也许不需要掌握sql。同时你必须对以下几种数据库中的一种以上的sql比较熟悉。Oracle,DB2,Mysql,Postgresql.
    6:xml 掌握程度:了解 原因:AppServer的配置一般是使用XML来实现的。
    7:ejb 掌握程度:了解 原因:很多项目中商业逻辑是由ejb来实现的,所以呢。。。
    8:以下几种AppServer(engnier) 你需要了解一个以上。
    a:)Tomcat 
    b:)WebLogic
    c:)WebSphere
    d:)JRun
    e:)Resin
    原因:你的jsp跑在什么上面啊?

    三:选择要求(因项目而定)
    1:LDAP 掌握程度:了解 原因:LADP越来越多的运用在权限控制上面。
    2:Struts 掌握程度:熟练 原因:如果符合MVC设计通常会使用Struts实现C。
    3:Xsp 掌握程度:根据需要而定很多时候是不使用的,但在不需要使用ejb但
    jsp+servlet+bean实现不了的时候Xsp是一个非常不错的选择。
    4:Linux 掌握程度:熟悉 原因:如果你的运用跑在Linux/Unix上你最少要知道
    rm ,mv,cp,vi,tar gzip/gunzip 是用来做什么的吧。
    四:工具的使用
    1:UltraEdit(EditPlus)+jakarta-ant+jakarta-log4j;
    2:Jubilder4-6
    3:Visual Age For Java
    4:VCafe
    以上的工具你选择你自己熟悉的吧。不过强烈建议你用log4j做调试工具。

    五:成长之路
    1:html 学习时间,如果你的智商在80以上,15天时间应该够用了。至少你能手写出一个页面来。
    2:jacascript/jscript学习时间,这真的不好说,比较深奥的东西,够用的话一个礼拜可以学写皮毛。
    3:css 学习时间,三天的时间你应该知道如何使用css了,不要求你写,一般是美工来写css。
    4:java 学习时间,天才也的三个月吧。慢满学吧。如果要精通,那我不知道需要多少时间了。用来写
    jsp,四个月应该够了。

    5:sql 学习时间,只需要知道insert ,delete ,update ,select,create/drop table的话一天你应该知道了。
    6:xml 学习时间,我不知道我还没有学会呢。呵呵。不过我知道DTD是用来做什么的。
    7:ejb 学习时间,基本的调用看3天你会调用了。不过是建立在你学会java的基础上的。
    8:熟悉AppServer,Tomcat四天你可以掌握安装,配置。把jsp跑起来了。如果是WebLogic也够了,但要使用ejb那不关你的事情吧。SA做什么去了。
    9:熟悉Linux那可得需要不少时间。慢慢看man吧。
    10:Struts如果需要你再学习。

    六:结束语
    我是闲的无聊,所以花了半个小时写了写,如果你觉得简直是一堆Shit,自己知道就行了,不用告诉我,呵呵。
    如果对你还有点帮助,别忘了夸我两句。如果需要联系我:bingo_ge@hotmail.com

    posted @ 2006-03-08 14:50 高山流水 阅读(101) | 评论 (0)编辑 收藏

    仅列出标题
    共30页: First 21 22 23 24 25 26 27 28 29 Last