2006年7月27日
#
wParam和lParam
这两个是Win16系统遗留下来的产物,
在Win16API中WndProc有两个参数:
一个是WORD类型的16位整型变量;
另一个是LONG类型的32位整型变量。
因此根据匈牙利命名法,16位的变量就被命名为wParam,
而32位的变量就被命名为lParam。
到了Win32API中,原来的16位变量也被扩展为32位,
因此wParam和lParam的大小完全相同。
在Win32API的早期,为了保证和Win16API的代码可移植性
MS定义了WPARAM和LPARAM两个宏。
当时保留了w前缀的原因一方面是由于WPARAM宏也已W开头,
还有也因为要提醒程序员注意到可移植性,当然到了现在
Win16早已退出历史舞台,这个前缀也就约定俗成的沿用下来了。
首先说明一下使用const的好处:
使用const的好处在于它允许指定一种语意上的约束------某种对象不能被修改--------编译器具体来实
施这种约束。通过const,你可以通知编译器和其他程序员某个值要保持不变。只要是这种情况,你就要
明确地使用const ,因为这样做就可以借助编译器的帮助确保这种约束不被破坏。
(一)
首先解释一下const与指针的关系:
const在指针的声明中有一下三种形式:
const char *p = "hello"; // 非const指针,
// const数据,就是说p指向的那个内存空间的数据是不可
变的,但p还可以指向新的内存地址。
char * const p = "hello"; // const指针,
// 非const数据,就是说这个指针p一旦赋值或初始化,就
不能在指向其他位置了,但其指向的位置的数据值是可变的。
const char * const p = "hello"; // const指针,
// const数据,这个就很明显了,集上述两家之长处(也
可能是短处哦,),上述两者都不可变。
一般来说,你可以在头脑里画一条垂直线穿过指针声明中的星号(*)位置,如果const出现在线的左边,
指针指向的数据为常量;如果const出现在线的右边,指针本身为常量;如果const在线的两边都出现,二
者都是常量。
恩,差点忘了,还有一种形式:
char const * p = "hello";
这其实与上边的情形一是一样的,只是由于个人习惯的不同,二者都是对的。
(二)
在一个函数声明中,const可以指的是函数的返回值,或某个参数;对于成员函数,还可以指的是整个函
数。
const(1) int fun(int const(2)& )const(3)
{
int temp;
retrun temp;
}
参数的 const属性(上例2处)一般用引用传递,是为了保证该参数在函数中不允许被修改,一旦修改,
编译器会报错。
而返回值的const属性(上例1处)是保证函数的返回值不被修改,也许你会质疑这种可能性,但是这种可
能性确实存在,
详细情形如下:(摘自effective c++)
const rational operator*(const rational& lhs,
const rational& rhs);
很多程序员第一眼看到它会纳闷:为什么operator*的返回结果是一个const对象?因为如果不是这样,用
户就可以做下面这样的坏事:
rational a, b, c;
...
(a * b) = c; // 对a*b的结果赋值
我不知道为什么有些程序员会想到对两个数的运算结果直接赋值,但我却知道:如果a,b和c是固定类型
,这样做显然是不合法的。一个好的用户自定义类型的特征是,它会避免那种没道理的与固定类型不兼容
的行为。对我来说,对两个数的运算结果赋值是非常没道理的。声明operator*的返回值为const可以防止
这种情况,所以这样做才是正确的。
呵呵,象Scott Meyers这样的大师见地就是不一般吧
接下来说明函数的const属性:(上例3处)
当然喽,一般用于成员函数了,它有以下属性:
(1)const成员函数不被允许修改它所在对象的任何一个数据成员。
(2)const成员函数能够访问对象的const成员,而其他成员函数不可以。
(三)尽量使用 const代替define 吧,因为const是类型安全的。
应该使用
const double pi = 3.1415926;
而不要用#define pi 3.1415926
后者是宏,仅仅是对程序中的pi用3.1415926代替,会让你对于一些编译时的错误很难定位。
什么是引用?
对象的别名(另一个名称)。
引用经常用于“按引用传递(pass-by-reference)”:
void swap(int& i, int& j)
{
int tmp = i;
i = j;
j = tmp;
}
int main()
{
int x, y;
// ...
swap(x,y);
}
此处的 i 和 j 分别是main中的 x 和 y。换句话说,i 就是 x —— 并非指向 x 的指针,也不是 x 的拷贝,而是 x 本身。对 i 的任何改变同样会影响 x,反之亦然。
OK,这就是作为一个程序员所认知的引用。现在,给你一个不同的角度,这可能会让你更糊涂,那就是引用是如何实现的。典型的情况下,对象 x 的引用 i 是 x 的机器地址。但是,当程序员写 i++ 时,编译器产生增加 x 的代码。更详细的来说,编译器用来寻找 x 的地址位并没有被改变。C 程序员将此认为好像是 C 风格的按指针传递,只是句法不同 (1) 将 & 从调用者移到了被调用者处,(2)消除了*s。换句话说,C 程序员会将 i 看作为宏 (*p),而 p 就是指向 x 的指针(例如,编译器自动地将潜在的指针解除引用;i++被改变为 (*p)++;i = 7 被自动地转变成 *p = 7)。
很重要:请不要将引用看作为指向一个对象的奇异指针,即使引用经常是用汇编语言下的地址来实现的。引用就是对象。不是指向对象的指针,也不是对象的拷贝,就是对象。
--------------------------------------------------------------------------------
给引用赋值,意味着什么?
改变引用的“指示物”(引用所指的对象)。
请记住: 引用就是它的指示物,所以当改变引用的值时,也会改变其指示物的值。以编译器编写者的行话来说,引用是一个“左值”(它可以出现在赋值运算符左边)。
--------------------------------------------------------------------------------
返回一个引用,意味着什么?
意味着该函数调用可以出现在赋值运算符的左边。
最初这种能力看起来有些古怪。例如,没有人会认为表达式 f() = 7 有意义。然而,如果 a 是一个 Array 类,大多数人会认为 a = 7 有意义,即使 a 实际上是一个函数调用的伪装(它调用了 如下的 Array 类的 Array::operator[](int))。
class Array {
public:
int size() const;
float& operator[] (int index);
// ...
};
int main()
{
Array a;
for (int i = 0; i < a.size(); ++i)
a = 7; // 这行调用了 Array::operator[](int)
}
--------------------------------------------------------------------------------
object.method1().method2() 是什么意思?
连接这些方法的调用,因此被称为方法链
第一个被执行的是 object.method1()。它返回对象,可能是对象的引用(如,method1()可能以 return *this 结束),或可能是一些其他对象。我们姑且把返回的对象称为objectB。然后objectB成为method2()的this对象。
方法链最常用的地方是iostream库。例如,cout << x << y 可以执行因为 cout << x是一个返回cout.的函数
虽然使用的较少,但仍然要熟练掌握的是在命名参数法(Named Parameter Idiom)中使用方法链。
--------------------------------------------------------------------------------
如何能够使一个引用重新指向另一个对象?
不行。
你无法让引用与其指示物分离。
和指针不同,一旦引用和对象绑定,它无法再被重新指向其他对象。引用本身不是一个对象(它没有标识; 当试图获得引用的地址时,你将的到它的指示物的地址;记住:引用就是它的指示物)。
从某种意义上来说,引用类似 int* const p 这样的const指针(并非如 const int* p 这样的指向常量的指针)。不管有多么类似,请不要混淆引用和指针;它们完全不同。
--------------------------------------------------------------------------------
何时该使用引用, 何时该使用指针?
尽可能使用引用,不得已时使用指针。
当你不需要“重新指向(reseating)”时,引用一般优先于指针被选用。这通常意味着引用用于类的公有接口时更有用。引用出现的典型场合是对象的表面,而指针用于对象内部。
上述的例外情况是函数的参数或返回值需要一个“临界”的引用时。这时通常最好返回/获取一个指针,并使用 NULL 指针来完成这个特殊的使命。(引用应该总是对象的别名,而不是被解除引用的 NULL 指针)。
注意:由于在调用者的代码处,无法提供清晰的的引用语义,所以传统的 C 程序员有时并不喜欢引用。然而,当有了一些 C++ 经验后,你会很快认识到这是信息隐藏的一种形式,它是有益的而不是有害的。就如同,程序员应该针对要解决的问题写代码,而不是机器本身。
--------------------------------------------------------------------------------
什么是对象的句柄?它是指针吗?它是引用吗?它是指向指针的指针?它是什么?
[Recently created (on 4/01). Click here to go to the next FAQ in the "chain" of recent changes.]
句柄术语一般用来指获取另一个对象的方法——一个广义的假指针。这个术语是(故意的)含糊不清的。
含糊不清在实际中的某些情况下是有用的。例如,在早期设计时,你可能不准备用句柄来表示。你可能不确定是否将一个简单的指针或者引用或者指向指针的指针或者指向引用的指针或者整型标识符放在一个数组或者字符串(或其它键)以便能够以哈希表(hash-table)(或其他数据结构)或数据库键或者一些其它的技巧来查询。如果你只知道你会需要一些唯一标识的东西来获取对象,那么这些东西就被称为句柄。
因此,如果你的最终目标是要让代码唯一的标识/查询一个Fred类的指定的对象的话,你需要传递一个Fred句柄这些代码。句柄可以是一个能被作为众所周知的查询表中的键(key)来使用的字符串(比如,在std::map<std::string,Fred> 或 std::map<std::string,Fred*>中的键),或者它可以是一个作为数组中的索引的整数(比如,Fred* array = new Fred[maxNumFreds]),或者它可以是一个简单的 Fred*,或者它可以是其它的一些东西。
初学者常常考虑指针,但实际上使用未初始化的指针有底层的风险。例如,如果Fred对象需要移动怎么办?当Fred对象可以被安全删除时我们如何获知?如果Fred对象需要(临时的)连续的从磁盘获得怎么办?等等。这些时候的大多数,我们增加一个间接层来管理位置。例如,句柄可以是Fred**,指向Fred*的指针可以保证不会被移动。当Fred对象需要移动时,你只要更新指向Fred*的指针就可以了。或者让用一个整数作为句柄,然后在表或数组或其他地方查询Fred的对象(或者指向Fred对象的指针)。
重点是当我们不知道要做的事情的细节时,使用句柄。
使用句柄的另一个时机是想要将已经完成的东西含糊化的时候(有时用术语magic cookie也一样,就像这样,“软件传递一个magic cookie来唯一标识并定位适当的Fred对象”)。将已经完成的东西含糊化的原因是使得句柄的特殊细节或表示物改变时所产生的连锁反应最小化。举例来说,当将一个句柄从用来在表中查询的字符串变为在数组中查询的整数时,我们可不想更新大量的代码。
当句柄的细节或表示物改变时,维护工作更为简单(或者说阅读和书写代码更容易),因此常常将句柄封装到类中。这样的类常重载operator-> 和 operator*算符(既然句柄的效果象指针,那么它可能看起来也象指针)。
VC小技巧15个
作者:i_like_cpp
一、 一次只运行一个程序实例
下列两种方式都可以实现,建议采用第二种方式:
1、 if( FindWindow(NULL,"程序标题"))
exit(0);
2、BOOL CDemoTBarEApp::InstanceIsRun()
{
HANDLE m_hMutex;
m_hMutex = ::CreateMutex(NULL, TRUE, _T("YourApplication"));
ASSERT(m_hMutex);
if (GetLastError() == ERROR_ALREADY_EXISTS)
{
m_hMutex = NULL;
return TRUE;//实例已经运行
}
return FALSE;//实例未运行
}
二、 装载光标
SetCursor(AfxGetApp()->LoadStandardCursor(IDC_WAIT));
其中::SetCursor()是全局函数,用来设置整个例程的光标参数是宏定义光标句柄。AfxGetApp ()是一个系统函数,它返回当前的一个CWinApp对象。其成员函数LoadStandardCursor()用来读取一个系统指针,每一种系统指针的具体宏定义如下:
IDC_APPSTARTING 带小沙漏的标准箭头
IDC_ARROW 标准箭头
IDC_CROSS 十字光标(用于定位)
IDC_HAND Windows 2000:手型
IDC_HELP 带问号的箭头
IDC_IBEAM I型标
IDC_ICON Obsolete for applications marked version 4.0 or later.
IDC_NO 禁止符号
IDC_SIZE Obsolete for applications marked version 4.0 or later. Use IDC_SIZEALL.
IDC_SIZEALL 十字箭头
IDC_SIZENESW 指向东北和西南的双向箭头
IDC_SIZENS 指向南和北的双向箭头
IDC_SIZENWSE 指向西北和东南的双向箭头
IDC_SIZEWE 指向东西的双向箭头
IDC_UPARROW 上箭头
IDC_WAIT 沙漏
三、获得主框架:
CMainFrame * pMainframe = (CMainFrame *) AfxGetApp()->m_pMainWnd;
.获取应用程序的实例句柄:
Example: HANDLE hInstance=AfxGetInstanceHandle();
获得应用程序主窗口的指针:
Example: AfxGetMainWnd() ->ShowWindow(SW_SHOWMAXMIZED); //使程序最大化
四、重新建立字体的代码
if(m_fontLogo.m_hObject)
m_fontLogo.Detach();
m_fontLogo.CreateFont(nHeight, 0, 0, 0, nWeight, bItalic, bUnderline,0,0,0,0,0,0, Name);
五、用指定颜色填充区域
dc.FillSolidRect(rect, ::GetSysColor(COLOR_3DFACE));
六、绘制立体字体效果的字体,很值得一看
void CTestView::OnPaint()
{
CPaintDC dc(this); // device context for painting
CRect rect;
GetWindowRect(rect);
CFont m_fontLogo;
m_fontLogo.CreateFont(24, 0, 0, 0, FW_BOLD, true,
FALSE,0,0,0,0,0,0, "Arial");
CString m_LogoText;
m_LogoText=_T("Benlux Pro3D System");
dc.SetBkMode(TRANSPARENT);
CFont * OldFont = dc.SelectObject(&m_fontLogo);
// draw text in DC
COLORREF OldColor = dc.SetTextColor( ::GetSysColor( COLOR_3DHILIGHT));
rect.right = rect.Width();
rect.bottom = rect.Height();
rect.left = rect.top = 0;
dc.FillSolidRect(rect, ::GetSysColor(COLOR_3DFACE));
dc.DrawText( m_LogoText, rect + CPoint(1,1), DT_SINGLELINE | DT_LEFT | DT_VCENTER);
dc.SetTextColor( ::GetSysColor( COLOR_3DSHADOW));
dc.DrawText( m_LogoText, rect, DT_SINGLELINE | DT_LEFT | DT_VCENTER);
// restore old text color
dc.SetTextColor( OldColor);
// restore old font
dc.SelectObject(OldFont);
// Do not call CView::OnPaint() for painting messages
}
七、简单的消息检索和抽取函数,能够让系统响应其它操作
BOOL PeekAndPump()
{
static MSG msg;
while (::PeekMessage(&msg,NULL,0,0,PM_NOREMOVE)) {
if (!AfxGetApp()->PumpMessage()) {
::PostQuitMessage(0);
return FALSE;
}
}
return TRUE;
}
八、在你的程序中用动画光标替换默认的等待光标 (ANI光标的使用)
HCURSOR m_hAniCursor=NULL;
BeginWaitCursor(); //begin wait cursor for api function
//load ani cursor from file in current path
TCHAR cursorPath[MAX_PATH]; GetModuleFileName(NULL,cursorPath,MAX_PATH);
char drive[_MAX_DRIVE];
char dir[_MAX_DIR];
char fname[_MAX_FNAME];
char ext[_MAX_EXT];
_splitpath(cursorPath, drive, dir, fname, ext );
sprintf(cursorPath,"%s%swait.ani",drive,dir); //ani cursor file name is wait.ani
m_hAniCursor= LoadCursorFromFile(cursorPath);
HCURSOR oldCursor;
if(m_hAniCursor != NULL)
oldCursor=SetCursor(m_hAniCursor);
for(long i=0;i<1000;i++)
Sleep(5);
oldCursor=NULL;
m_hAniCursor=NULL;
EndWaitCursor(); //end wait cursor for api function
九、如何限制编辑框中的准许字符
如果用户在编辑控件中只允许接收数字,可以使用一个标准的编辑控件并指
定新的创建标志ES_NUMBERS,它是Windows 95新增加的标志,该标志限制 编辑控
件只按收数字字符。
如果用户需要复杂的编辑控件,可以使用Microsoft 的屏蔽编辑控件,它是一个很有用的OLE定制控件。
如果希望不使用OLE 定制控件自己处理字符,可以派生一个CEdit 类并处理WM_CHAR消息,然后从编辑控件中过滤出特定的字符。首先,使用ClassWizard 建立一个 CEdit的派生类,其次,在对话类中指定一个成员变量将编辑控件分类在OnInitdialog 中调用CWnd: : SubclassDlgItem .
//In your dialog class declaration (.H file )
private :
CMyEdit m_wndEdit ; // Instance of your new edit control .
//In you dialog class implementation (.CPP file )
BOOL CSampleDialog : : OnInitDialog ( )
{
//Subclass the edit lontrod .
m_wndEdit .SubclassDlgItem (IDC_EDIT,this );
…
}
使用ClassWizard处理WM_CHAR消息,计算nChar参量并决定所执行的操作,用户可以确定是否修改、传送字符。下例说明了如何显示字母字符,如果字符是字母字符,则调用CWnd ; OnChar,否则不调用OnChar.
//Only display alphabetic dharacters .
void CMyEdit : : OnChar (UINT nChar , UINT nRepCnt , UITN nFlags )
{
//Determine if nChar is an alphabetic character .
if (: : IsCharAlpha ( ( TCHAR) nChar ) )
CEdit : : OnChar (nChar, nRepCnt , nFlags );
}
如果要修改字符,则不能仅仅简单地用修改过的nChar调用CEdit : : OnChar。要修改一个字符,需要首先修改nChar,然后用修改过的nChar调用CWnd: : DefWindowProc。下例说明了如何将字符转变为大写:
//Make all characters uppercase
void CMyEdit : : OnChar (UINT nChar , UINT nRepCnt , UINT nFlags )
{
//Make sure character is uppercase .
if (: : IsCharAlpha ( .( TCHAR) nChar)
nChar=: : CharUpper (nChar ) ;
//Bypass default OnChar processing and directly call default window proc.
DefWindProc (WM_CHAR, nChar , MAKELPARAM (nRepCnt , nFlags )) ;
}
十、串太长时如何在其末尾显示一个省略号
调用CDC:: DrawText并指定DT_END_ELLIPSIS标志,这样就可以用小略号取代串末尾的字符使其适合于指定的边界矩形。如果要显示路径信息,指定DT_END_ELLIPSIS标志并省略号取代串中间的字符。
void CSampleView:: OnDraw (CDC* pDC)
{
CTestDoc* pDoc=GetDocument ();
ASSERT_VALID (pDoc);
//Add ellpsis to end of string if it does not fit
pDC->Drawtext (CString ("This is a long string"),
CRect (10, 10, 80, 30), DT_LEFT | DT_END_ELLIPSIS);
//Add ellpsis to middle of string if it does not fit
pDC->DrawText (AfxgetApp () ->m_pszhelpfilePath,
CRect (10, 40, 200, 60), DT_LEFT | DT_PATH_ELLIPSIS);
}
十一、如何实现一个橡皮区矩形(具有踪迹矩形并可移动、缩放的矩形)
CRectTracker是一个很有用的类,可以通过调用CRectTracker:: TrackRubberBand响应WM_LBUTTONDOWN消息来创建一个橡皮区矩形。下例表明使用CRectTracker移动和重置视窗中的蓝色椭圆的大小是很容易的事情。
首先,在文档类中声明一个CRectTracker数据成员:
class CTestDoc: Public CDocument
{…
public:
CRectTracker m_tracker;
…
};
其次,在文档类的构造函数中初始化CRectTracker 对象:
CTestDoc::CTestDoc()
{
m_tracker.m_rect.SetRect (10, 10, 300, 300);
m_tracker.m_nStyle=CRectTracker:: resizeInside |
CRectTracker:: dottedLine;
}
然后,在视图类的OnDraw函数中画椭圆和踪迹矩形:
void CTestView::OnDraw(CDC* pDC)
{
CTestDoc* pDoc = GetDocument();
ASSERT_VALID(pDoc);
//Select blue brush into device context.
CBrush brush (RGB (0, 0, 255));
CBrush* pOldBrush=pDC->SelectObject (&brush);
//draw ellipse in tracking rectangle.
CRect rcEllipse;
pDoc->m_tracker.GetTrueRect (rcEllipse);
pDC->Ellipse (rcEllipse);
//Draw tracking rectangle.
pDoc->m_tracker.Draw (pDC);
//Select blue brush out of device context.
pDC->SelectObject(pOldBrush);
}
最后,视图类中处理WM_LBUTTONDOWN消息,并增加下述代码。该段代码根据鼠标击键情况可以拖放、移动或者重置椭圆的大小。
void CTestView::OnLButtonDown(UINT nFlags, CPoint point)
{
//Get pointer to document.
CTestDoc* pDoc=GetDocument();
ASSERT_VALID (pDoc);
//If clicked on ellipse, drag or resize it. Otherwise create a
//rubber-band rectangle nd create a new ellipse.
BOOL bResult=pDoc->m_tracker.HitTest (point)!=
CRectTracker::hitNothing;
//Tracker rectangle changed so update views.
if (bResult)
{
pDoc->m_tracker.Track (this,point,TRUE);
pDoc->SetModifiedFlag ();
pDoc->UpdateAllViews (NULL);
}
else
pDoc->m_tracker.TrackRubberBand (this,point,TRUE);
CView::OnLButtonDown(nFlags, point);
}
十二、如何在临时目录创建一个临时文件
如果你要在临时目录下创建临时文件,下面的代码能帮到你的忙。
bool GetuniqueTempName (CString& strTempName)
{
strTempName="";
//Get the temporary files directory.
TCHAR szTempPath [MAX_PATH];
DWORD dwResult=:: GetTempPath (MAX_PATH, szTempPath);
if (dwResult==0)
return false;
//Create a unique temporary file.
TCHAR szTempFile[MAX_PATH];
UINT nResult=GetTempFileName (szTempPath, _T ("~ex"),0,szTempFile);
if (dwResult==0)
return false;
strTempName=szTempFile;
return true;
}
十三、如何限制窗口的最小范围
要限制窗体的大小,下面的代码能帮到你的忙。
在CMainFrame中增加WM_GETMAXMININFO消息的处理函数,然后在这个函数中写代码如下:
//限制主窗体的最小高度和宽度
void CMainFrame::OnGetMinMaxInfo(MINMAXINFO FAR* lpMMI)
{
lpMMI->ptMinTrackSize.x=600;
lpMMI->ptMinTrackSize.y=400;
CNewFrameWnd::OnGetMinMaxInfo(lpMMI);
}
十四、怎样删除文件到回收站中
要删除文件到回收站,很简单。只要用SHFileOperation函数就行了,下面的代码我将为你演示了这一个函数的用法。当然你可以直接拷贝到你的项目中。
//删除文件到回收站中
//pszPath : 待删除的全路径文件名
//bDelete : TRUE 删除,不移到回收站,FALSE:移到回收站
一、 //返回 : TRUE 删除成功 FALSE 删除失败
BOOL CDelFileToRecycleDlg::Recycle(LPCTSTR pszPath, BOOL bDelete/*=FALSE*/)
{
SHFILEOPSTRUCT shDelFile;
memset(&shDelFile,0,sizeof(SHFILEOPSTRUCT));
shDelFile.fFlags |= FOF_SILENT; // don''''t report progress
shDelFile.fFlags |= FOF_NOERRORUI; // don''''t report errors
shDelFile.fFlags |= FOF_NOCONFIRMATION; // don''''t confirm delete
// Copy pathname to double-NULL-terminated string.
//
TCHAR buf[_MAX_PATH + 1]; // allow one more character
_tcscpy(buf, pszPath); // copy caller''''s pathname
buf[_tcslen(buf)+1]=0; // need two NULLs at end
// Set SHFILEOPSTRUCT params for delete operation
shDelFile.wFunc = FO_DELETE; // REQUIRED: delete operation
shDelFile.pFrom = buf; // REQUIRED: which file(s)
shDelFile.pTo = NULL; // MUST be NULL
if (bDelete)
{ // if delete requested..
shDelFile.fFlags &= ~FOF_ALLOWUNDO; // ..don''''t use Recycle Bin
}
else
{ // otherwise..
shDelFile.fFlags |= FOF_ALLOWUNDO; // ..send to Recycle Bin
}
return SHFileOperation(&shDelFile); // do it!
}
十五、内存泄漏检查
也许你已经知道,在C++和C语言中指针问题也就是内存申请与释放是一个令人头疼的事情,假如你申请了内存,但没有释放,并且你的程序需要长时间地运行,那么,系统的资源将逐渐减少,当系统的资源全部被用完时,系统将会崩溃。所以在开发程序的过程中一定要保证资源的完全释放。下面我们来介绍内存漏洞的检查。
示例如下:
// do your memory allocations and deallocations...
CString s = "This is a frame variable";
#ifdef _DEBUG
CMemoryState oldMemState, newMemState, diffMemState;
oldMemState.Checkpoint();
#endif
// the next object is a heap object
CString* p = new CString( "Smith Alan 581_0215" );
delete p;
p=NULL;
#ifdef _DEBUG
newMemState.Checkpoint();
BOOL b=diffMemState.Difference(oldMemState, newMemState);
if (b)
{
AfxMessageBox( "Memory leaked! " );
}
#endif
根据试验,由于我们无法释放掉象int CString char 申请的变量。只能释放指针型的变量。而检测内存时,照样会出现内存泄漏现象。所以,这种内存检测方式局限性还是很大。因为我们无法释放非指针型变量
WinMain()函数
DOS下的C语言从main()开始,Windows下的C语言从WinMain()开始,一个空的WinMain()函数是这样的:
int WINAPI WinMain(HINSTANCE hinstance, HINSTANCE hPrevInstance, LPSTR lpCmdLine, int nCmdShow)
{
return(0);
}
一个函数即使什么也没做,也应该返回一个值。是的,有好多东西我们不熟悉。首先的首先,WINAPI是个什么声明?WINAPI是在windows.h头文件中定义的一个宏,它把函数调用翻译成正确的调用约定。当我们在程序中需要用到汇编语言的时候,我们在来深究它好了,记住,如果要用WinMain(),就必须要有WINAPI。
下一步让我们来看看括号里的四个参数:
◎ HINSTANCE hinstance:HINSTANCE是一个句柄类型的标识符。变量hinstance是一个整数,用于标识程序实例。Windows设置这个参数的值,并把它传递给你的程序代码。很多Windows函数都要用到它。
◎ HINSTANCE hPreInstance:你不用担心这个参数,它已经被废掉了。它只是为古老的Windows版本服务的。你将还会看到类似的情况。
◎ LPSTR lpCmdLine:是一个指向字符串的指针,它仅在程序名是从DOS命令行输入或是从Run对话框中输入时才起作用。因此,很少被程序代码所用。
◎ int nCmdShow:决定了窗口在初始显示时的状态。Windows通常给这个参数分配一个值。通常是SW_打头的一个常量。例如SW_SHOWNORMAL表示默认状态,SW_MAXINIZE或SW_MINIMIZE分别表示最大和最小模式等等。
memset用来对一段内存空间全部设置为某个字符。
memcpy用来做内存拷贝,你可以拿它拷贝任何数据类型的对象。
strcpy就只能拷贝字符串了,它遇到'\0'就结束拷贝。
strcpy
原型:extern char *strcpy(char *dest,char *src);
用法:#include <string.h>;
功能:把src所指由NULL结束的字符串复制到dest所指的数组中。
说明:src和dest所指内存区域不可以重叠且dest必须有足够的空间来容纳src的字符串。
返回指向dest的指针。
memcpy
原型:extern void *memcpy(void *dest, void *src, unsigned int count);
用法:#include <string.h>;
功能:由src所指内存区域复制count个字节到dest所指内存区域。
说明:src和dest所指内存区域不能重叠,函数返回指向dest的指针。
memset
原型:extern void *memset(void *buffer, int c, int count);
用法:#include <string.h>;
功能:把buffer所指内存区域的前count个字节设置成字符c。
说明:返回指向buffer的指针。
首先要谈论一点C++的知识,因为要想建立一个窗口,你就得先建立一个窗口类。窗口类包含所有的有关窗口的信息,如用什么样的鼠标符号,菜单样式等等。开发任何一个窗口程序,都离不开窗口类的建立。为了达到此目的,你必须填写WNDCLASSEX结构。EX的意思是“扩充”的意思,因为有一个老的结构叫作WNDCLASS,这里,我们将使用WNDCLASSEX结构,它的样子如下:
typedef struct _WNDCLASSEX {
UINT cbSize;
UINT style;
WNDPROC lpfnWndProc;
int cbClsExtra;
int cbWndExtra;
HANDLE hInstance;
HICON hIcon;
HCURSOR hCursor;
HBRUSH hbrBackground;
LPCTSTR lpszMenuName;
LPCTSTR lpszClassName;
HICON hIconSm;
} WNDCLASSEX;
这个结构有不少成员,讨厌的是,你必须为窗口类设置每一个成员。莫发愁,纸老虎一个。让我们来个速成。
※ UINT cbSize:指定了以字节为单位的结构的大小。这个成员是通过sizeof(WNDCLASSEX)实现的。你将会经常看到它,尤其是你使用了DirectX。
※ UINT style:指定了窗口的风格。它经常被以CS_打头的符号常量定义。两种或两种以上的风格可以通过C语言中的“或”(|)运算符加以组合。大多数情况我们只应用四种风格,出于对文章长度的考虑,我们只列出这四种。若你还需要其它的,到MSDN里找一下好了。当然前提是你使用的是Visual C++。
◎ CS_HREDRAW:一旦移动或尺寸调整使客户区的宽度发生变化,就重新绘制窗口。
◎ CS_VREDRAW:一旦移动或尺寸调整使客户区的高度发生变化,就重新绘制窗口。
◎ CS_OWNDC:为该类中的每一个窗口分配一个唯一的设备上下文。
◎ CS_DBLCLKS:当用户双击鼠标时向窗口过程发送双击消息。
※ WNDPROC lpfnWndProc:是指向窗口过程的指针。一般都指向CALLBACK函数。如果你没有用过函数指针,简单理解为函数的地址就是函数的名字,名字后面别带括号。
※ int cbClsExtra:它是为类保留的额外信息 。大多数程序员不用它,你在在写游戏程序时也不太可能用它,所以,设为0好了。
※ int cbWndExtra:同上一个差不多,设为0好了。
※ HANDLE hInstance:是指向窗口过程实例的句柄。同时也是WinMain()函数的参数之一。应该设置为hinstance。
※ HICON hIcon:指向窗口图标的句柄,它通常被LoadIcon()函数设置。在你学会如何在你的程序中使用资源前,你先设置成如下样子:LoadIcon(NULL,IDI_WINLOGO)。当然,还有一些其它的IDI_打头的符号常量,可以自己去帮助文件里寻找。
※ HCURSOR hCursor:指向窗口光标的句柄,它通常被LoadCursor()函数设置,在你学会如何在你的程序中使用资源前,你先用Windows默认的吧,LoadCursor(NULL,IDC_ARROW)。
※ HBRUSH hbrBackground:当你的窗口过程得到消息,要求刷新(或重画)窗口时,至少要用一种纯色或“brush”(画刷)重画窗口区域,画刷是由参数确定的。你可以使用GetStockObject()函数调用几种常备的画刷,如BLACK_BRUSH, WHITE_BRUSH, GRAY_BRUSH等。现在,你就用GetStockObject(BLACK_BRUSH)吧。也许你觉得我说的太简单了,是因为不想把开始弄得太复杂。在以后的篇幅里会详细讲述的。
※ LPCTSTR lpszMenuName:如果你想建立一个有下拉菜单的窗口,你得给这个参数赋一个菜单名称(这涉及到资源),由于你还不知道怎么创建菜单,你就先用NULL设置成一个没有菜单的窗口吧。
※ LPCSTR lpszClassName:很显然,你需要给类起个名字,随你便,如“**”。要用双引号引上。
※ HICON hIconSm:指向小图标的句柄。小图标用来显示在窗口的标题栏里。要用到LoadIcon()函数,现在,先用Windows默认的吧,LoadIcon(NULL,IDI_WINLOGO)。
好了,现在你关于WNDCLASSEX结构知道的已经差不多了,你可以自己设置它了。下面是一个例子:
WNDCLASSEX sampleClass; // declare structure variable
sampleClass.cbSize = sizeof(WNDCLASSEX); // always use this!
sampleClass.style = CS_DBLCLKS | CS_OWNDC | CS_HREDRAW | CS_VREDRAW; // standard settings
sampleClass.lpfnWndProc = MsgHandler; // we need to write this!
sampleClass.cbClsExtra = 0; // extra class info, not used
sampleClass.cbWndExtra = 0; // extra window info, not used
sampleClass.hInstance = hinstance; // parameter passed to WinMain()
sampleClass.hIcon = LoadIcon(NULL, IDI_WINLOGO); // Windows logo
sampleClass.hCursor = LoadCursor(NULL, IDC_ARROW); // standard cursor
sampleClass.hbrBackground = (HBRUSH)GetStockObject(BLACK_BRUSH); // a simple black brush
sampleClass.lpszMenuName = NULL; // no menu
sampleClass.lpszClassName = "Sample Class" // class name
sampleClass.hIconSm = LoadIcon(NULL, IDI_WINLOGO); // Windows logo again
我想,现在你已经有点儿不太崇拜Windows程序员了吧。言归正传,有一点我得提醒你,注意函数GetStockObject()前的(HBRUSH)类型配置,这是因为GetStockObject()可以调用其它的对象,不仅仅是“brush”,所以你需要一个HBRUSH类型配置。在Visual C++旧版本里不用配置,但新的6.0版本需要它,否则会编译出错。
下一件事是注册这个窗口类,只有这样,你才能创建新的窗口。十分简单,你只需要调用一个RegisterClassEX()函数,它只有一个参数,就是你的窗口类的地址(名字),根据我上面给的例子,这里应该这样:
RegisterClassEx(&sampleClass);
我们的窗口类创建完了,我们可以用它创建一个窗口了。
2006年7月13日
#
C++编译器如何实现异常处理
原著:Vishal Kochhar
翻译:局部变量
原文出处:How a C++ compiler implements exception handling
译者注:本文在网上已经有几个译本,但都不完整,所以我决定自己把它翻译过来。虽然力求信、雅、达,但鉴于这是我的第一次翻译经历,不足之处敬请谅解并指出。
与传统语言相比,C++的一项革命性创新就是它支持异常处理。传统的错误处理方式经常满足不了要求,而异常处理则是一个极好的替代解决方案。它将正常代码和错误处理代码清晰的划分开来,程序变得非常干净并且容易维护。本文讨论了编译器如何实现异常处理。我将假定你已经熟悉异常处理的语法和机制。本文还提供了一个用于VC++的异常处理库,要用库中的处理程序替换掉VC++提供的那个,你只需要调用下面这个函数:
install_my_handler();
之后,程序中的所有异常,从它们被抛出到堆栈展开(stack unwinding),再到调用catch块,最后到程序恢复正常运行,都将由我的异常处理库来管理。
与其它C++特性一样,C++标准并没有规定编译器应该如何来实现异常处理。这意味着每一个编译器的提供商都可以用它们认为恰当的方式来实现它。下面我会描述一下VC++是怎么做的,但即使你使用其它的编译器或操作系统①,本文也应该会是一篇很好的学习材料。VC++的实现方式是以windows系统的结构化异常处理(SEH)②为基础的。
结构化异常处理—概述
在本文的讨论中,我认为异常或者是被明确的抛出的,或者是由于除零溢出、空指针访问等引起的。当它发生时会产生一个中断,接下来控制权就会传递到操作系统的手中。操作系统将调用异常处理程序,检查从异常发生位置开始的函数调用序列,进行堆栈展开和控制权转移。Windows定义了结构“EXCEPTION_REGISTRATION”,使我们能够向操作系统注册自己的异常处理程序。
struct EXCEPTION_REGISTRATION
{
EXCEPTION_REGISTRATION* prev;
DWORD handler;
};
注册时,只需要创建这样一个结构,然后把它的地址放到FS段偏移0的位置上去就行了。下面这句汇编代码演示了这一操作:
mov FS:[0], exc_regp
prev字段用于建立一个EXCEPTION_REGISTRATION结构的链表,每次注册新的EXCEPTION_REGISTRATION时,我们都要把原来注册的那个的地址存到prev中。
那么,那个异常回调函数长什么样呢?在excpt.h中,windows定义了它的原形:
EXCEPTION_DISPOSITION (*handler)(
_EXCEPTION_RECORD *ExcRecord,
void* EstablisherFrame,
_CONTEXT *ContextRecord,
void* DispatcherContext);
不要管它的参数和返回值,我们先来看一个简单的例子。下面的程序注册了一个异常处理程序,然后通过除以零产生了一个异常。异常处理程序捕获了它,打印了一条消息就完事大吉并退出了。
#include <iostream>
#include <windows.h>
using std::cout;
using std::endl;
struct EXCEPTION_REGISTRATION
{
EXCEPTION_REGISTRATION* prev;
DWORD handler;
};
EXCEPTION_DISPOSITION myHandler(
_EXCEPTION_RECORD *ExcRecord,
void * EstablisherFrame,
_CONTEXT *ContextRecord,
void * DispatcherContext)
{
cout << "In the exception handler" << endl;
cout << "Just a demo. exiting..." << endl;
exit(0);
return ExceptionContinueExecution; //不会运行到这
}
int g_div = 0;
void bar()
{
//初始化一个EXCEPTION_REGISTRATION结构
EXCEPTION_REGISTRATION reg, *preg = ®
reg.handler = (DWORD)myHandler;
//取得当前异常处理链的“头”
DWORD prev;
_asm
{
mov EAX, FS:[0]
mov prev, EAX
}
reg.prev = (EXCEPTION_REGISTRATION*) prev;
//注册!
_asm
{
mov EAX, preg
mov FS:[0], EAX
}
//产生一个异常
int j = 10 / g_div; //异常,除零溢出
}
int main()
{
bar();
return 0;
}
/*-------输出-------------------
In the exception handler
Just a demo. exiting...
---------------------------------*/
注意EXCEPTION_REGISTRATION必须定义在栈上,并且必须位于比上一个结点更低的内存地址上,Windows对此有严格要求,达不到的话,它就会立刻终止进程。
函数和堆栈
堆栈是用来保存局部对象的连续内存区。更明确的说,每个函数都有一个相关的栈桢(stack frame)来保存它所有的局部对象和表达式计算过程中用到的临时对象,至少理论上是这样的。但现实中,编译器经常会把一些对象放到寄存器中以便能以更快的速度访问。堆栈是一个处理器(CPU)层次的概念,为了操纵它,处理器提供了一些专用的寄存器和指令。
图1是一个典型的堆栈,它示出了函数foo调用bar,bar又调用widget时的情景。请注意堆栈是向下增长的,这意味着新压入的项的地址低于原有项的地址。
通常编译器使用EBP寄存器来指示当前活动的栈桢。本例中,CPU正在运行widget,所以图中的EBP指向了widget的栈桢。编译器在编译时将所有局部对象解析成相对于栈桢指针(EBP)的固定偏移,函数则通过栈桢指针来间接访问局部对象。举个例子,典型的,widget访问它的局部变量时就是通过访问栈桢指针以下的、有着确定位置的几个字节来实现的,比如说EBP-24。
上图中也画出了ESP寄存器,它叫栈指针,指向栈的最后一项。在本例中,ESP指着widget的栈桢的末尾,这也是下一个栈桢(如果它被创建的话)的开始位置。
处理器支持两种类型的栈操作:压栈(push)和弹栈(pop)。比如,
pop EAX
的作用是从ESP所指的位置读出4字节放到EAX寄存器中,并把ESP加上(记住,栈是向下增长的)4(在32位处理器上);类似的,
push EBP
的作用是把ESP减去4,然后将EBP的值放到ESP指向的位置中去。
编译器编译一个函数时,会在它的开头添加一些代码来为其创建并初始化栈桢,这些代码被称为序言(prologue);同样,它也会在函数的结尾处放上代码来清除栈桢,这些代码叫做尾声(epilogue)。
一般情况下,序言是这样的:
Push EBP ; 把原来的栈桢指针保存到栈上
Mov EBP, ESP ; 激活新的栈桢
Sub ESP, 10 ; 减去一个数字,让ESP指向栈桢的末尾
第一条指令把原来的栈桢指针EBP保存到栈上;第二条指令通过让EBP指向主调函数的EBP的保存位置来激活被调函数的栈桢;第三条指令把ESP减去了一个数字,这样ESP就指向了当前栈桢的末尾,而这个数字是函数要用到的所有局部对象和临时对象的大小。编译时,编译器知道函数的所有局部对象的类型和“体积”,所以,它能很容易的计算出栈桢的大小。
尾声所做的正好和序言相反,它必须把当前栈桢从栈上清除掉:
Mov ESP, EBP
Pop EBP ; 激活主调函数的栈桢
Ret ; 返回主调函数
它让ESP指向主调函数的栈桢指针的保存位置(也就是被调函数的栈桢指针指向的位置),弹出EBP从而激活主调函数的栈桢,然后返回主调函数。
一旦CPU遇到返回指令,它就要做以下两件事:把返回地址从栈中弹出,然后跳转到那个地址去。返回地址是主调函数执行call指令调用被调函数时自动压栈的。Call指令执行时,会先把紧随在它后面的那条指令的地址(被调函数的返回地址)压入栈中,然后跳转到被调函数的开始位置。图2更详细的描绘了运行时的堆栈。如图所示,主调函数把被调函数的参数也压进了堆栈,所以参数也是栈桢的一部分。函数返回后,主调函数需要移除这些参数,它通过把所有参数的总体积加到ESP上来达到目的,而这个体积可以在编译时知道:
Add ESP, args_size
当然,也可以把参数的总体积写在被调函数的返回指令的后面,让被调函数去移除参数,下面的指令就在返回主调函数前从栈中移去了24个字节:
Ret 24
取决于被调函数的调用约定(call convention),这两种方式每次只能用一个。你还要注意的是每个线程都有自己独立的堆栈。
C++和异常
回忆一下我在第一节中介绍的EXCEPTION_REGISTRATION结构,我们曾用它向操作系统注册了发生异常时要被调用的回调函数。VC++也是这么做的,不过它扩展了这个结构的语义,在它的后面添加了两个新字段:
struct EXCEPTION_REGISTRATION
{
EXCEPTION_REGISTRATION* prev;
DWORD handler;
int id;
DWORD ebp;
};
VC++会为绝大部分函数③添加一个EXCEPTION_REGISTRATION类型的局部变量,它的最后一个字段(ebp)与栈桢指针指向的位置重叠。函数的序言创建这个结构并把它注册给操作系统,尾声则恢复主调函数的EXCEPTION_REGISTRATION。id字段的意义我将在下一节介绍。
VC++编译函数时会为它生成两部分数据:
a)异常回调函数
b)一个包含函数重要信息的数据结构,这些信息包括catch块、这些块的地址和这些块所关心的异常的类型等等。我把这个结构称为funcinfo,有关它的详细讨论也在下一节。
图3是考虑了异常处理之后的运行时堆栈。widget的异常回调函数位于由FS:[0]指向的异常处理链的开始位置(这是由widget的序言设置的)。异常处理程序把widget的funcinfo结构的地址交给函数__CxxFrameHandler,__CxxFrameHandler会检查这个结构看函数中有没有catch块对当前的异常感兴趣。如果没有的话,它就返回ExceptionContinueSearch给操作系统,于是操作系统会从异常处理链表中取得下一个结点,并调用它的异常处理程序(也就是调用当前函数的那个函数的异常处理程序)。
这一过程将一直进行下去——直到处理程序找到一个能处理当前异常的catch块为止,这时它就不再返回操作系统了。但是在调用catch块之前(由于有funcinfo结构,所以知道catch块的入口,参见图3),必须进行堆栈展开,也就是清理掉当前函数的栈桢下面的所有其他的栈桢。这个操作稍微有点复杂,因为:异常处理程序必须找到异常发生时生存在这些栈桢上的所有局部对象,并依次调用它们的析构函数。后面我将对此进行详细介绍。
异常处理程序把这项工作委托给了各个栈桢自己的异常处理程序。从FS:[0]指向的异常处理链的第一个结点开始,它依次调用每个结点的处理程序,告诉它堆栈正在展开。与之相呼应,这些处理程序会调用每个局部对象的析构函数,然后返回。此过程一直进行到与异常处理程序自身相对应的那个结点为止。
由于catch块是函数的一部分,所以它使用的也是函数的栈桢。因此,在调用catch块之前,异常处理程序必须激活它所隶属的函数的栈桢。
其次,每个catch块都只接受一个参数,其类型是它希望捕获的异常的类型。异常处理程序必须把异常对象本身或者是异常对象的引用拷贝到catch块的栈桢上,编译器在funcinfo中记录了相关信息,处理程序根据这些信息就能知道到哪去拷贝异常对象了。
拷贝完异常并激活栈桢后,处理程序将调用catch块。而catch块将把控制权下一步要转移到的地址返回来。请注意:虽然这时堆栈已经展开,栈桢也都被清除了,但它们占据的内存空间并没有被覆盖,所有的数据都还好好的待在栈上。这是因为异常处理程序仍在执行,象其他函数一样,它也需要栈来存放自己的局部对象,而其栈桢就位于发生异常的那个函数的栈桢的下面。catch块返回以后,异常处理程序需要“杀掉”异常对象。此后,它让ESP指向目标函数(控制权要转移到的那个函数)的栈桢的末尾——这样就把(包括它自己的在内的)所有栈桢都删除了,然后再跳转到catch块返回的那个地址去,就胜利的完成整个异常处理任务了。但它怎么知道目标函数的栈桢末尾在哪呢?事实上它没法知道,所以编译器把这个地址保存到了栈桢上(由前言来完成),如图3所示,栈桢指针EBP下面第16个字节就是。
当然,catch块也可能抛出新异常,或者是将原来的异常重新抛出。处理程序必须对此有所准备。如果是抛出新异常,它必须杀掉原来的那个;而如果是重新抛出原来的异常,它必须能继续传播(propagate)这个异常。
这里我要特别强调一点:由于每个线程有自己独立的堆栈,所以每个线程也都有自己独立的、由FS:[0]指向的EXCEPTION_REGISTRATION链。
C++和异常—2 图4是funcinfo的布局,注意这里的字段名可能与VC++编译器实际使用的不完全一致,而且我也只给出了和我们的讨论相关的字段。堆栈展开表(unwind table)的结构留到下节再讨论。
异常处理程序在函数中查找catch块时,它首先要判断异常发生的位置是否在当前函数(发生异常的那个函数)的一个try块中。是则查找与此try块相关的catch块表,否则直接返回。
先来看看它怎样找try块。编译时,编译器给每个try块都分配了start id和end id。通过funcinfo结构,异常处理程序可以访问这两个id,见图4。编译器为函数中的每个try块都生成了相关的数据结构。
上一节中,我说过VC++给EXCEPTION_REGISTRATION结构加上了一个id字段。回忆一下图3,这个结构位于函数的栈桢上。异常发生时,处理程序读出这个值,看它是否在try块的两个id确定的区间[start id,end id]中。是的话,异常就发生在这个try块中;否则继续查看try块表中的下一个try块。
谁负责更新id的值,它的值又应该是什么呢?原来,编译器会在函数的多个位置安插代码来更新id的值,以反应程序的实时运行状态。比如说,编译器会在进入try块的地方加上一条语句,把try块的start id写到栈桢上。
找到try块后,处理程序就遍历与其关联的catch块表,看是否有对当前异常感兴趣的catch块。在try块发生嵌套时,异常将既源于内层try块,也源于外层try块。这种情况下,处理程序应该按先内后外的顺序查找catch块。但它其实没必要关心这些,因为,在try块表中,VC++总是把内层try块放在外层try块的前面。
异常处理程序还有一个难题就是“如何根据catch块的相关数据结构判断这个catch块是否愿意处理当前异常”。这是通过比较异常的类型和catch块的参数的类型来完成的。例如下面这个程序:
void foo()
{
try
{
throw E();
}
catch(H)
{
//.
}
}
如果H和E的类型完全相同的话,catch块就要捕获这个异常。这意味着处理程序必须在运行时进行类型比较,对C等语言来说,这是不可能的,因为它们无法在运行时得到对象的类型。C++则不同,它有了运行时类型识别(runtime type identification,RTTI),并提供了运行时类型比较的标准方法。C++在标准头文件中定义了type_info类,它能在运行时代表一个类型。catch块数据结构的第二个字段(ptype_info,见图4)是一个指向type_info结构的指针,它在运行时就代表catch块的参数类型。type_info也重载了==运算符,能够指出两种类型是否完全相同。这样,异常处理程序只要比较(调用==运算符)catch块参数的type_info(可以通过catch块的相关数据结构来访问)和异常的type_info是否相同,就能知道catch块是不是愿意捕获当前异常了。
catch块的参数类型可以通过funcinfo结构得到,但异常的type_info从哪来呢?当编译器碰到
throw E();
这条语句时,它会为异常生成一个excpt_info结构,如图5所示。还是要提醒你注意这里用的名字可能与VC++使用的不一致,而且仍然只有与我们的讨论相关的字段。从图中可以看出,异常的type_info可以通过excpt_info结构得到。由于异常处理程序需要拷贝异常对象(在调用catch块之前),也需要消除掉它(在调用catch块之后),所以编译器在这个结构中同时提供了异常的拷贝构造函数、大小和析构函数的信息。
在catch块的参数是基类,而异常是派生类时,异常处理程序也应该调用catch块。然而,这种情况下,比较它们的type_info绝对是不相等,因为它们本来就不是相同的类型。而且,type_info类也没有提供任何其他函数或运算符来指出一个类是另一个类的基类。但异常处理程序还必须得去调用catch块!为了解决这个问题,编译器只能为处理程序提供更多的信息:如果异常是派生类,那么etypeinfo_table(通过excpt_info访问)将包含多个指向etype_info(扩展了type_info,这个名字是我启的)的指针,它们分别指向了各个基类的etype_info。这样,处理程序就可以把catch块的参数和所有这些type_info比较,只要有一个相同,就调用catch块。
在结束这一部分之前,还有最后一个问题:异常处理程序是怎么知道异常和excpt_info结构的?下面我就要回答这个问题。
VC++会把throw语句翻译成下面的样子:
//throw E(); //编译器会为E生成excpt_info结构
E e = E(); //在栈上创建异常
_CxxThrowException(&e, E_EXCPT_INFO_ADDR);
__CxxThrowException会把控制权连带它的两个参数都交给操作系统(控制权转移是通过软件中断实现的,请参见RaiseException)。而操作系统,在为调用异常回调函数做准备时,会把这两个参数打包到一个_EXCEPTION_RECORD结构中。接着,它从EXCEPTION_REGISTRATION链表的头结点(由FS:[0]指向)开始,依次调用各节点的异常处理程序。而且,指向当前EXCEPTION_REGISTRATION结构的指针也会作为异常处理程序的第二个参数出现。前面已经说过,VC++中的每个函数都在栈上创建并注册了EXCEPTION_REGISTRATION结构。所以传递这个参数可以让处理程序知道很多重要信息,比如说:EXCEPTION_REGISTRATION的id字段(用于查找catch块)、函数的栈桢(用于清理栈桢)和EXCEPTION_REGISTRATION结点在异常链表中的位置(用于堆栈展开)等。第一个参数是指向_EXCEPTION_RECORD结构的指针,通过它可以找到异常和它的excpt_info结构。下面是excpt.h中定义的异常回调函数的原型:
EXCEPTION_DISPOSITION (*handler)(
_EXCEPTION_RECORD* ExcRecord,
void* EstablisherFrame,
_CONTEXT *ContextRecord,
void* DispatcherContext);
后两个参数和我们的讨论关系不大。函数的返回值是一个枚举类型(也在excpt.h中定义),我前面已经说过,如果处理程序找不到catch块,它就会向系统返回ExceptionContinueSearch,对本文而言,我们只要知道这一个返回值就行了。_EXCEPTION_RECORD结构是在winnt.h中定义的:
struct _EXCEPTION_RECORD
{
DWORD ExceptionCode;
DWORD ExceptionFlags;
_EXCEPTION_RECORD* ExcRecord;
PVOID ExceptionAddress;
DWORD NumberParameters;
DWORD ExceptionInformation[15];
}EXCEPTION_RECORD;
ExceptionInformation数组中元素的个数和类型取决于ExceptionCode字段。如果是C++异常(异常代码是0xe06d7363,源于throw语句),那么数组中将包含指向异常和excpt_info结构的指针;如果是其他异常,那数组中基本上就不会有什么内容,这些异常包括除零溢出、访问违例等,你可以在winnt.h中找到它们的异常代码。
ExceptionFlags字段用于告诉异常处理程序应该采取什么操作。如果它是EH_UNWINDING(见Except.inc),那是说堆栈正在展开,这时,处理程序要清理栈桢,然后返回。否则处理程序应该在函数中查找catch块并调用它。清理栈桢意味着必须找到异常发生时生存在栈桢上的所有局部对象,并调用其析构函数,下一节我们将就此进行详细讨论。
清理栈桢 C++标准明确指出:堆栈展开工作必须调用异常发生时所有生存的局部对象的析构函数。如下面的代码:
int g_i = 0;
void foo()
{
T o1, o2;
{
T o3;
}
10/g_i; //这里会发生异常
T o4;
//...
}
foo有o1、o2、o3、o4四个局部对象,但异常发生时,o3已经“死亡”,o4还未“出生”,所以异常处理程序应该只调用o1和o2的析构函数。
前面已经说过,编译器会在函数的很多地方安插代码来记录当前的运行状态。实际上,编译器在函数中设置了一些关键区域,并为它们分配了id,进入关键区域时要记录它的id,退出时恢复前一个id。try块就是一个例子,其id就是start id。所以,在try块的入口,编译器会把它的start id记到栈桢上去。局部对象从创建到销毁也确定了一个关键区域,或者,换句话说,编译器给每个局部对象分配了唯一的id,例如下面的程序:
void foo()
{
T t1;
//.
}
编译器会在t1的定义后面(也就是t1创建以后),把它的id写到栈桢上:
void foo()
{
T t1;
_id = t1_id; //编译器插入的语句
//.
}
上面的_id是编译器偷偷创建的局部变量,它的位置与EXCEPTION_REGISTRATION的id字段重叠。类似的,在调用对象的析构函数前,编译器会恢复前一个关键区域的id。
清理栈桢时,异常处理程序读出id的值(通过EXCEPTION_REGISTRATION结构的id字段或栈桢指针EBP下面的4个字节来访问)。这个id可以表明,函数在运行到与它相关联的那个点之前没有发生异常。所有在这一点之前定义的对象都已初始化,应该调用这些对象中的一部分或全部对象的析构函数。请注意某些对象是属于子块(如前面代码中的o3)的,发生异常时可能已经销毁了,不应该调用它们的析构函数。
编译器还为函数生成了另一个数据结构——堆栈展开表(unwindtable,我启的名字),它是一个unwind结构的数组,可通过funcinfo来访问,如图4所示。函数的每个关键区域都有一个unwind结构,这些结构在展开表中出现的次序和它们所对应的区域在函数中的出现次序完全相同。一般unwind结构也会关联一个对象(别忘了,每个对象的定义都开辟了关键区域,并有id与其对应),它里面有如何销毁这个对象的信息。每当编译器碰到对象定义,它就生成一小段代码,这段代码知道对象在栈桢上的地址(就是它相对于栈桢指针的偏移),并能销毁它。unwind结构中有一个字段用于保存这段代码的入口地址:
typedef void (*CLEANUP_FUNC)();
struct unwind
{
int prev;
CLEANUP_FUNC cf;
};
try块对应的unwind结构的cf字段是空值NULL,因为没有与它对应的对象,所以也没有东西需要它去销毁。通过prev字段,这些unwind结构也形成了一个链表。异常处理程序清理栈桢时,会读取当前的id值,以它为索引取得展开表中对应的项,并调用其第二个字段指向的清理代码,这样,那个与之关联的对象就被销毁了。然后,处理程序将以当前unwind结构的prev字段为索引,继续在展开表中找下一个unwind结构,调用其清理代码。这一过程将一直重复,直到链表的结尾(prev的值是-1)。图6画出了本节开始时提到的那段代码的堆栈展开表。
现在把new运算符也加进来,对于下面的代码:
T* p = new T();
系统会首先为T分配内存,然后调用它的构造函数。所以,如果构造函数抛出了异常,系统就必须释放这些内存。因此,动态创建那些拥有“有为的构造函数”的类型时,VC++也为new运算符分配了id,并且堆栈展开表中也有与其对应的项,其清理代码将释放分配的内存空间。调用构造函数前,编译器把new运算符的id存到EXCEPTION_REGISTRATION结构中,构造函数顺利返回后,它再把id恢复成原来的值。
更进一步说,构造函数抛出异常时,对象可能刚刚构造了一部分,如果它有子成员对象或子基类对象,并且发生异常时它们中的一部分已经构造完成的话,就必须调用这些对象的析构函数。和普通函数一样,编译器也给构造函数生成了相关的数据来帮助完成这个任务。
展开堆栈时,异常处理程序调用的是用户定义的析构函数,这一点你必须注意,因为它也有可能抛出异常!C++标准规定堆栈展开过程中,析构函数不能抛出异常,否则系统将调用std::terminate。
实现
本节我们讨论其他三个有待详细解释的问题:
a)如何安装异常处理程序
b)catch块重新抛出异常或抛出新异常时应该如何处理
c)如何对所有线程提供异常处理支持
随同本文,有一个演示项目,查看其中的readme.txt文件可以得到一些编译方面的帮助①。
第一项任务是安装异常处理程序,也就是把VC++的处理程序替换掉。从前面的讨论中,我们已经清楚地知道__CxxFrameHandler函数是VC++所有异常处理工作的入口。编译器为每个函数都生成一段代码,它们在发生异常时被调用,把相应的funcinfo结构的指针交给__CxxFrameHandler。
install_my_handler()函数会改写__CxxFrameHandler的入口处的代码,让程序跳转到my_exc_handler()函数。不过,__CxxFrameHandler位于只读的内存页,对它的任何写操作都会导致访问违例,所以必须首先用VirtualProtectEx把该内存页的保护方式改成可读写,等改写完毕后,再改回只读。写入的数据是一个jmp_instr结构。
//install_my_handler.cpp
#include <windows.h>
#include "install_my_handler.h"
//C++默认的异常处理程序
extern "C"
EXCEPTION_DISPOSITION __CxxFrameHandler(
struct _EXCEPTION_RECORD* ExceptionRecord,
void* EstablisherFrame,
struct _CONTEXT* ContextRecord,
void* DispatcherContext
);
namespace
{
char cpp_handler_instructions[5];
bool saved_handler_instructions = false;
}
namespace my_handler
{
//我的异常处理程序 EXCEPTION_DISPOSITION
my_exc_handler(
struct _EXCEPTION_RECORD *ExceptionRecord,
void * EstablisherFrame,
struct _CONTEXT *ContextRecord,
void * DispatcherContext
) throw();
#pragma pack(push, 1)
struct jmp_instr
{
unsigned char jmp;
DWORD offset;
};
#pragma pack(pop)
bool WriteMemory(void* loc, void* buffer, int size)
{
HANDLE hProcess = GetCurrentProcess();
//把包含内存范围[loc,loc+size]的页面的保护方式改成可读写
DWORD old_protection;
BOOL ret = VirtualProtectEx(hProcess, loc, size, PAGE_READWRITE, &old_protection);
if(ret == FALSE)
return false;
ret = WriteProcessMemory(hProcess, loc, buffer, size, NULL);
//恢复原来的保护方式
DWORD o2;
VirtualProtectEx(hProcess, loc, size, old_protection, &o2);
return (ret == TRUE);
}
bool ReadMemory(void* loc, void* buffer, DWORD size)
{
HANDLE hProcess = GetCurrentProcess();
DWORD bytes_read = 0;
BOOL ret = ReadProcessMemory(hProcess, loc, buffer, size, &bytes_read);
return (ret == TRUE && bytes_read == size);
}
bool install_my_handler()
{
void* my_hdlr = my_exc_handler; void* cpp_hdlr = __CxxFrameHandler;
jmp_instr jmp_my_hdlr;
jmp_my_hdlr.jmp = 0xE9;
//从__CxxFrameHandler+5开始计算偏移,因为jmp指令长5字节
jmp_my_hdlr.offset = reinterpret_cast(my_hdlr) - (reinterpret_cast(cpp_hdlr) + 5);
if(!saved_handler_instructions)
{
if(!ReadMemory(cpp_hdlr, cpp_handler_instructions, sizeof(cpp_handler_instructions)))
return false;
saved_handler_instructions = true;
}
return WriteMemory(cpp_hdlr, &jmp_my_hdlr, sizeof(jmp_my_hdlr));
}
bool restore_cpp_handler()
{
if(!saved_handler_instructions)
return false;
else
{
void* loc = __CxxFrameHandler;
return WriteMemory(loc, cpp_handler_instructions, sizeof(cpp_handler_instructions));
}
}
}
编译指令#pragma pack(push, 1)告诉编译器不要在jmp_instr结构中填充任何用于对齐的空间。没有这条指令,jmp_instr的大小将是8字节,而我们需要它是5字节。
现在重新回到异常处理这个主题上来。调用catch块时,它可能重新抛出异常或抛出新异常。前一种情况下,异常处理程序必须继续传播(propagate)当前异常;后一种情况下,它需要在继续之前销毁原来的异常。此时,处理程序要面对两个难题:“如何知道异常是源于catch块还是程序的其他部分”和“如何跟踪原来的异常”。我的解决方法是:在调用catch块之前,把当前异常保存在exception_storage对象中,并注册一个专用于catch块的异常处理程序——catch_block_protector。调用get_exception_storage()函数,就能得到exception_storage对象:
exception_storage* p = get_exception_storage();
p->set(pexc, pexc_info);
注册 catch_block_protector;
调用catch块; //....
这样,当catch块(重新)抛出异常时,程序将会执行catch_block_protector。如果是抛出了新异常,这个函数可以从exception_storage对象中分离出前一个异常并销毁它;如果是重新抛出原来的异常(可以通过ExceptionInformation数组的前两个元素知道是新异常还是旧异常,后一种情况下着两个元素都是0,参见下面的代码),就通过拷贝ExceptionInformation数组来继续传播它。下面的代码就是catch_block_protector()函数的实现。
//-------------------------------------------------------------------
// 如果这个处理程序被调用了,可以断定是catch块(重新)抛出了异常。
// 异常处理程序(my_handler)在调用catch块之前注册了它。其任务是判断
// catch块抛出了新异常还是重新抛出了原来的异常,并采取相应的操作。
// 在前一种情况下,它需要销毁传递给catch块的前一个异常对象;在后一种
// 情况下,它必须找到原来的异常并将其保存到ExceptionRecord中供异常
// 处理程序使用。
//-------------------------------------------------------------------
EXCEPTION_DISPOSITION catch_block_protector(
_EXCEPTION_RECORD* ExceptionRecord,
void* EstablisherFrame,
struct _CONTEXT *ContextRecord,
void* DispatcherContext
) throw ()
{
EXCEPTION_REGISTRATION *pFrame;
pFrame= reinterpret_cast<EXCEPTION_REGISTRATION*>(EstablisherFrame);
if(!(ExceptionRecord->ExceptionFlags & (_EXCEPTION_UNWINDING | _EXCEPTION_EXIT_UNWIND)))
{
void *pcur_exc = 0, *pprev_exc = 0;
const excpt_info *pexc_info = 0, *pprev_excinfo = 0;
exception_storage* p = get_exception_storage();
pprev_exc = p->get_exception();
pprev_excinfo = p->get_exception_info();
p->set(0, 0);
bool cpp_exc = ExceptionRecord->ExceptionCode == MS_CPP_EXC;
get_exception(ExceptionRecord, &pcur_exc);
get_excpt_info(ExceptionRecord, &pexc_info);
if(cpp_exc && 0 == pcur_exc && 0 == pexc_info) //重新抛出
{
ExceptionRecord->ExceptionInformation[1] = reinterpret_cast<DWORD>(pprev_exc);
ExceptionRecord->ExceptionInformation[2] = reinterpret_cast<DWORD>(pprev_excinfo);
}
else
{
exception_helper::destroy(pprev_exc, pprev_excinfo);
}
}
return ExceptionContinueSearch;
}
下面是get_exception_storage()函数的一个实现:
exception_storage* get_exception_storage()
{
static exception_storage es;
return &es;
}
在单线程程序中,这是一个完美的实现。但在多线程中,这就是个灾难了,想象一下多个线程访问它,并把异常对象保存在里面的情景吧。由于每个线程都有自己的堆栈和异常处理链,我们需要一个线程安全的get_exception_storage实现:每个线程都有自己单独的exception_storage,它在线程启动时被创建,并在结束时被销毁。Windows提供的线程局部存储(thread local storage,TLS)可以满足这个要求,它能让每个线程通过一个全局键值来访问为这个线程所私有的对象副本,这是通过TlsGetValue()和TlsSetValue这两个API来完成的。
Excptstorage.cpp中给出了get_exception_storage()函数的实现。它会被编译成动态链接库,因为我们可以籍此知道线程的创建和退出——系统在这两种情况下都会调用所有(当前进程加载的)dll的DllMain()函数,这让我们有机会创建特定于线程的数据,也就是exception_storage对象。
//excptstorage.cpp
#include "excptstorage.h"
#include <windows.h>
namespace
{
DWORD dwstorage;
}
namespace my_handler
{
__declspec(dllexport) exception_storage* get_exception_storage() throw ()
{
void * p = TlsGetValue(dwstorage);
return reinterpret_cast <exception_storage*>(p);
}
}
BOOL APIENTRY DllMain( HANDLE hModule, DWORD ul_reason_for_call, LPVOID lpReserved )
{
using my_handler::exception_storage;
exception_storage *p;
switch (ul_reason_for_call)
{
case DLL_PROCESS_ATTACH:
//主线程(第一个线程)不会收到DLL_THREAD_ATTACH通知,所以,
//与其相关的操作也放在这了
dwstorage = TlsAlloc();
if (-1 == dwstorage)
return FALSE;
p = new exception_storage();
TlsSetValue(dwstorage, p);
break ;
case DLL_THREAD_ATTACH:
p = new exception_storage();
TlsSetValue(dwstorage, p);
break;
case DLL_THREAD_DETACH:
p = my_handler::get_exception_storage();
delete p;
break ;
case DLL_PROCESS_DETACH:
p = my_handler::get_exception_storage();
delete p;
break ;
}
return TRUE;
}
结论综上所述,异常处理是在操作系统的协助下,由C++编译器和运行时异常处理库共同完成的。
注释和参考资料- ① 本文写作期间,微软发布了Visual Studio 7.0。本文的异常处理库主要是在运行于奔腾处理器的windows2000上使用VC++6.0编译和测试的。但我也在VC++5.0和VC++7.0 beta版上测试过。6.0和7.0之间有一些差别,6.0先把异常(或其引用)拷贝到catch块的栈桢上,然后在调用catch块之前进行堆栈展开;7.0则先进行堆栈展开。在这方面,我的库代码的行为比较接近6.0版。
- ② 参见Matt Pietrek发表在MSDN上的文章《structured exception handling》。
- ③ 如果一个函数既不含try块,也没有定义任何具有“有为的析构函数”的对象,那么编译器将不为它生成用于异常处理的数据。
2006年7月12日
#
这几年各种新的技术井喷一样的涌现出来。身处这样一个时代的我们,难免就有一种拔剑四顾,心下茫然的感觉。在某一方面的做到精专已属不易,全才似不可能,那么究竟又该何去何从?这篇文章目的是探讨编程过程中一些本质的东西,已期对大家的学习有所帮助。
个人以为,从编程这一职业诞生开始一直至今,其本质就没有改变过。对概念的抽象对逻辑的描述始终都是编程过程中最核心的东西。
程序为解决某一问题而存在。而问题可拆分成某些概念和逻辑关系。而结构化程序设计和面向对象程序设计不过是对概念和逻辑进行表达的不同方式。而程序中逻辑关系的复杂程度随程序的规模而增加。
程序的本质不是各种技巧。就算你把EffectiveC++,MoreEffectiveC++中的每一条款都应用到了你的C++程序中,如果没有对逻辑关系的良好组织,你的程序也可能糟不可言。可能高耦合低内聚,可能不可扩充―――。
上面这段文字提到了四个概念,他们分别是:问题、概念、逻辑和技巧。
我来举个例子来说明这四个概念都代表些什么。
现在假设我们需要根据已知的一个文件名产生一个临时文件名。如果输入为 prog.dat,mydir,hello.,oops.tmp,end.dat那么相应的输出为
prog.dat=>prog.tmp
mydir=>mydir.tmp
hello.=>hello.tmp
oops.tmp=>oops.xxx
end.dat=>end.tmp
(这里使用了TheC++StandardLibrary中第11章的例子)
什么都不做的程序是不存在的,程序总要解决客观世界中的某些问题。这里的问题就是要为一个已知的文件名产生相应的临时文件名。
解决这个问题时涉及的概念主要有两个:字符串和扩展名。扩展名可以用字符串进行表示。
解决这个问题的逻辑之一可用下面的伪代码来表述:
if(输入文件名没有扩展名)
{
用输入文件名、'.'号和"tmp"生成临时文件名;
}
else
{
得到输入文件名的扩展名;
if(扩展名为空)
{
用输入文件名和"tmp"生成临时文件名;
}
elseif(扩展名为"tmp")
{
把"tmp"替换为"xxx";
}
else
{
把文件名'.'以后的部分替换为"tmp";
}
}
当然你还可以有其他的实现逻辑,而这种逻辑总是可以独立于语言、操作系统的。
至于你是用C++标准库、还是MFC的Cstring类或者.net中的相应类或者不依靠类库自己用某种语言来表述这种逻辑那就是技巧。
上面虽然是个小例子,但我认为完成任何程序:从上层应用直到底层驱动,都会涉及到这四个概念,。
上面所说编程时必须的四个要素中,我个人认为逻辑和概念是编程中更本质的东西,直接的原因是他们在更大的程度上决定着程序的质量。不要忘了评价软件质量的几个指标:
健壮性、可重用性、易扩充性、容错性等。
而这些指标并不是你选择了某个语言或者某个框架他们就达到了。而要通过对解决的问题的逻辑进行提炼和精化才可能达到。不要说这是系统分析的责任,程序每一部分都有自己要解决的问题,怎么可能每一部分程序质量的保证都是系统分析的责任。
此外当你要把原来用软件实现的功能换做用硬件来实现的时候,这点就分外的明显--软件其实就是一组逻辑关系。
但眼下主流意识形态似乎并不认同这点,各种招聘广告可为明证。
下面是一个招聘软件工程师的广告:
1.BSdegreeoraboveincomputerscience.
2.Atleast2yearsofdriverdevelopmentexperience.
3.ExperienceinC,C++,windowsSDK/DDKorLinuxsystemdevelopment.
4.Familiarwithcomputernetworkingortelecommunication802.3,802.11,TCP/IPprotocolisaplus.
5.Familiarwithembeddedsystemdesign.
6.FamiliarwithBluetoothisaplus.
如果把编程与磨刀砍柴做类比的化,那么
当某个人熟悉某种语言、某个框架、某个IDE、某个协议某个开发包的时候我们可以认为这个人拥有一把较锋利的柴刀。但这和这个人最终擅不擅长砍柴并没有然的关系。夫欲善其事,必先利其器。但利其器了未必就会善其事。所以说上面的广告一定程度上相当于想找一个会砍柴的人,但看人的时候却更注重他有没有一把锋利的刀子。(更可悲的是挣扎在生存边缘的我们,纵然不愿却也不得不迎合这种要求)
未避免矫枉过正,这里要强调一点,不是说基本技能不重要,没有对语言、框架、IDE的一定认识,根本就产生不了有用的程序。但当我们执迷于对不同语言、框架、IDE的优缺点进行比较的时候,不要忘了他们并不对程序的健壮性、可重用性、易扩充性、容错性等最终衡量程序优劣的诸多方面起决定作用。起决定作用的是你对概念的定义方式和对各种复杂逻辑关系的描述方式。
语言是什么?语言是逻辑的载体和描述的工具。当你试图表述逻辑关系的时候大多语言应该是满足这一要求的,要不然他早被淘汰了。框架是什么?框架是对逻辑和概念的一种封装。框架把某一领域通用的概念和逻辑封装起来,进而使程序员不必做一些重复性的工作。而实际上对这个领域的这些通用概念并不只有这么一种表述方式,也就是说并不只有一种逻辑表述。你选择某个框架的同时也就意味这你从n种方式中选择了一种,舍弃了其他。没有那个框架是完美的,选择的同时自然也就继承了这种不完美。所以善用他们,他们很重要,但不会再重要了。
当我意识到这点时,我发现虽然编程许久,但在对逻辑表述方面的能力却没有寸进,因为把90%以上的时间都投在了技巧的提升上了。JeffreyRichter和MattPietrek两位堪称Windows编程界中的技巧大师。可我现在敢说即使你把《Windows核心编程》、《ProgrammingServer-SideApplicationforMicorsoftWindows2000》还有MSDN杂志的UndertheHood专栏倒背如流,你可能还是写不出高质量的程序。这些东西是必须的,但远不是全部。
林锐写过一本《高质量C++/C编程指南》,此书在网上流传甚广,我想原因有二:一是此书对C/C++语言中的容易造成模糊的地方进行了详细说明(真的很有用);二是林锐笔法不错。但也正因其流传甚广,我觉得在这里必须指出,此书严重的文不对题。按照书中所说那些就能设计出高质量的C/C++程序么?那些都是编程所需要的技巧,不过是告诉你怎么能磨出一把锋利的刀而已。如果读过此书的人都把这些技巧和高质量的程序相等价,那么学习时恐怕就难免会误入歧途了。
这个题目太大,我是姑妄言之。这里不过是刚刚开始一种探讨而远非结论。欢迎大家一起讨论,还望诸君有以教我!
高质量C++/C编程指南
原文链接http://www.osprg.org/modules/wfsection/article.php?articleid=8,感谢林锐博士.
前言
软件质量是被大多数程序员挂在嘴上而不是放在心上的东西!
除了完全外行和真正的编程高手外,初读本书,你最先的感受将是惊慌:"哇!我以前捏造的C++/C程序怎么会有
那么多的毛病?"
别难过,作者只不过比你早几年、多几次惊慌而已。
请花一两个小时认真阅读这本百页经书,你将会获益匪浅,这是前面N-1个读者的建议。
一、编程老手与高手的误区
自从计算机问世以来,程序设计就成了令人羡慕的职业,程序员在受人宠爱之后容易发展成为毛病特多却常能自
我臭美的群体。
如今在Internet上流传的"真正"的程序员据说是这样的:
(1) 真正的程序员没有进度表,只有讨好领导的马屁精才有进度表,真正的程序员会让领导提心吊胆。
(2) 真正的程序员不写使用说明书,用户应当自己去猜想程序的功能。
(3) 真正的程序员几乎不写代码的注释,如果注释很难写,它理所当然也很难读。
(4) 真正的程序员不画流程图,原始人和文盲才会干这事。
(5) 真正的程序员不看参考手册,新手和胆小鬼才会看。
(6) 真正的程序员不写文档也不需要文档,只有看不懂程序的笨蛋才用文档。
(7) 真正的程序员认为自己比用户更明白用户需要什么。
(8) 真正的程序员不接受团队开发的理念,除非他自己是头头。
(9) 真正的程序员的程序不会在第一次就正确运行,但是他们愿意守着机器进行若干个30小时的调试改错。
(10)真正的程序员不会在上午9:00到下午5:00之间工作,如果你看到他在上午9:00工作,这表明他从昨晚一直干
到现在。
......
具备上述特征越多,越显得水平高,资格老。所以别奇怪,程序员的很多缺点竟然可以被当作优点来欣赏。就象
在武侠小说中,那些独来独往、不受约束且带点邪气的高手最令人崇拜。我曾经也这样信奉,并且希望自己成为
那样的"真正"的程序员,结果没有得到好下场。
我从读大学到博士毕业十年来一直勤奋好学,累计编写了数十万行C++/C代码。有这样的苦劳和疲劳,我应该称
得上是编程老手了吧?
我开发的软件都与科研相关(集成电路CAD和3D图形学领域),动辄数万行程序,技术复杂,难度颇高。这些软
件频频获奖,有一个软件获得首届中国大学生电脑大赛软件展示一等奖。在1995年开发的一套图形软件库到2000
年还有人买。罗列出这些"业绩",可以说明我算得上是编程高手了吧?
可惜这种个人感觉不等于事实。
读博期间我曾用一年时间开发了一个近10万行C++代码的3D图形软件产品,我内心得意表面谦虚地向一位真正的
软件高手请教。他虽然从未涉足过3D图形领域,却在几十分钟内指出该软件多处重大设计错误。让人感觉那套软
件是用纸糊的华丽衣服,扯一下掉一块,戳一下破个洞。我目瞪口呆地意识到这套软件毫无实用价值,一年的心
血白化了,并且害死了自己的软件公司。
人的顿悟通常发生在最心痛的时刻,在沮丧和心痛之后,我作了深刻反省,"面壁"半年,重新温习软件设计的基
础知识。补修"内功"之后,又觉得腰板硬了起来。博士毕业前半年,我曾到微软中国研究院找工作,接受微软公
司一位资深软件工程师的面试。他让我写函数strcpy的代码。
太容易了吧?
错!
这么一个小不点的函数,他从三个方面考查:
(1)编程风格;
(2)出错处理;
(3)算法复杂度分析(用于提高性能)。
在大学里从来没有人如此严格地考查过我的程序。我化了半个小时,修改了数次,他还不尽满意,让我回家好好
琢磨。我精神抖擞地进"考场",大汗淋漓地出"考场"。这"高手"当得也太窝囊了。我又好好地反省了一次。
我把反省后的心得体会写成文章放在网上传阅,引起了不少软件开发人员的共鸣。我因此有幸和国产大型IT企业
如华为、上海贝尔、中兴等公司的同志们广泛交流。大家认为提高质量与生产率是软件工程要解决的核心问题。
高质量程序设计是非常重要的环节,毕竟软件是靠编程来实现的。
我们心目中的老手们和高手们能否编写出高质量的程序来?
不见得都能!
就我的经历与阅历来看,国内大学的计算机教育压根就没有灌输高质量程序设计的观念,教师们和学生们也很少
自觉关心软件的质量。勤奋好学的程序员长期在低质量的程序堆中滚爬,吃尽苦头之后才有一些心得体会,长进
极慢,我就是一例。
现在国内IT企业拥有学士、硕士、博士文凭的软件开发人员比比皆是,但他们在接受大学教育时就"先天不足",
岂能一到企业就突然实现质的飞跃。试问有多少软件开发人员对正确性、健壮性、可靠性、效率、易用性、可读
性(可理解性)、可扩展性、可复用性、兼容性、可移植性等质量属性了如指掌?并且能在实践中运用自如?。
"高质量"可不是干活小心点就能实现的!
我们有充分的理由疑虑:
(1)编程老手可能会长期用隐含错误的方式编程(习惯成自然),发现毛病后都不愿相信那是真的!
(2)编程高手可以在某一领域写出极有水平的代码,但未必能从全局把握软件质量的方方面面。
事实证明如此。我到上海贝尔工作一年来,陆续面试或测试过近百名"新""老"程序员的编程技能,质量合格率大
约是10%。很少有人能够写出完全符合质量要求的if语句,很多程序员对指针、内存管理一知半解,......。
领导们不敢相信这是真的。我做过现场试验:有一次部门新进14名硕士生,在开欢迎会之前对他们进行"C++/C编
程技能"摸底考试。我问大家试题难不难?所有的人都回答不难。结果没有一个人及格,有半数人得零分。竞争
对手公司的朋友们也做过试验,同样一败涂地。
真的不是我"心狠手辣"或者要求过高,而是很多软件开发人员对自己的要求不够高。
要知道华为、上海贝尔、中兴等公司的员工素质在国内IT企业中是比较前列的,倘若他们的编程质量都如此差的
话,我们怎么敢期望中小公司拿出高质量的软件呢?连程序都编不好,还谈什么振兴民族软件产业,岂不胡扯。
我打算定义编程老手和编程高手,请您别见笑。
定义1:能长期稳定地编写出高质量程序的程序员称为编程老手。
定义2:能长期稳定地编写出高难度、高质量程序的程序员称为编程高手。
根据上述定义,马上得到第一推论:我既不是高手也算不上是老手。
在写此书前,我阅读了不少程序设计方面的英文著作,越看越羞惭。因为发现自己连编程基本技能都未能全面掌
握,顶多算是二流水平,还好意思谈什么老手和高手。希望和我一样在国内土生土长的程序员朋友们能够做到:
(1)知错就改;
(2)经常温故而知新;
(3)坚持学习,天天向上。
二、本书导读
首先请做附录B的C++/C试题(不要看答案),考查自己的编程质量究竟如何。然后参照答案严格打分。
(1)如果你只得了几十分,请不要声张,也不要太难过。编程质量差往往是由于不良习惯造成的,与人的智力
、能力没有多大关系,还是有药可救的。成绩越差,可以进步的空间就越大,中国不就是在落后中赶超发达资本
主义国家吗?只要你能下决心改掉不良的编程习惯,第二次考试就能及格了。
(2)如果你考及格了,表明你的技术基础不错,希望你能虚心学习、不断进步。如果你还没有找到合适的工作
单位,不妨到上海贝尔试一试。
(3)如果你考出85分以上的好成绩,你有义务和资格为你所在的团队作"C++/C编程"培训。希望你能和我们多多
交流、相互促进。半年前我曾经发现一颗好苗子,就把他挖到我们小组来。
(4)如果你在没有任何提示的情况下考了满分,希望你能收我做你的徒弟。
编程考试结束后,请阅读本书的正文。
本书第一章至第六章主要论述C++/C编程风格。难度不高,但是细节比较多。别小看了,提高质量就是要从这些
点点滴滴做起。世上不存在最好的编程风格,一切因需求而定。团队开发讲究风格一致,如果制定了大家认可的
编程风格,那么所有组员都要遵守。如果读者觉得本书的编程风格比较合你的工作,那么就采用它,不要只看不
做。人在小时候说话发音不准,写字潦草,如果不改正,总有后悔的时候。编程也是同样道理。
第七章至第十一章是专题论述,技术难度比较高,看书时要积极思考。特别是第七章"内存管理",读了并不表示
懂了,懂了并不表示就能正确使用。有一位同事看了第七章后觉得"野指针"写得不错,与我切磋了一把。可是过
了两周,他告诉我,他忙了两天追查出一个Bug,想不到又是"野指针"出问题,只好重读第七章。
光看本书对提高编程质量是有限的,建议大家阅读本书的参考文献,那些都是经典名著。
如果你的编程质量已经过关了,不要就此满足。如果你想成为优秀的软件开发人员,建议你阅读并按照CMMI规范
做事,让自己的综合水平上升一个台阶。上海贝尔的员工可以向网络应用事业部软件工程研究小组索取CMMI有关
资料,最好能参加培训。
三、版权声明
本书的大部分内容取材于作者一年前的书籍手稿(尚未出版),现整理汇编成为上海贝尔网络应用事业部的一个
规范化文件,同时作为培训教材。
由于C++/C编程是众所周知的技术,没有秘密可言。编程的好经验应该大家共享,我们自己也是这么学来的。作
者愿意公开本书的电子文档。
版权声明如下:
(1)读者可以任意拷贝、修改本书的内容,但不可以篡改作者及所属单位。
(2)未经作者许可,不得出版或大量印发本书。
(3)如果竞争对手公司的员工得到本书,请勿公开使用,以免发生纠纷。
预计到2002年7月,我们将建立切合中国国情的CMMI 3级解决方案。届时,包括本书在内的约1000页规范将严格
受控。
第1章 文件结构
每个C++/C程序通常分为两个文件。一个文件用于保存程序的声明(declaration),称为头文件。另一个文件用
于保存程序的实现(implementation),称为定义(definition)文件。
C++/C程序的头文件以".h"为后缀,C程序的定义文件以".c"为后缀,C++程序的定义文件通常以".cpp"为后缀(
也有一些系统以".cc"或".cxx"为后缀)。
1.1 版权和版本的声明
版权和版本的声明位于头文件和定义文件的开头(参见示例1-1),主要内容有:
(1)版权信息。
(2)文件名称,标识符,摘要。
(3)当前版本号,作者/修改者,完成日期。
(4)版本历史信息。
/*
* Copyright (c) 2001,上海贝尔有限公司网络应用事业部
* All rights reserved.
*
* 文件名称:filename.h
* 文件标识:见配置管理计划书
* 摘 要:简要描述本文件的内容
*
* 当前版本:1.1
* 作 者:输入作者(或修改者)名字
* 完成日期:2001年7月20日
*
* 取代版本:1.0
* 原作者 :输入原作者(或修改者)名字
* 完成日期:2001年5月10日
*/
示例1-1 版权和版本的声明
1.2 头文件的结构
头文件由三部分内容组成:
(1)头文件开头处的版权和版本声明(参见示例1-1)。
(2)预处理块。
(3)函数和类结构声明等。
假设头文件名称为 graphics.h,头文件的结构参见示例1-2。
l 【规则1-2-1】为了防止头文件被重复引用,应当用ifndef/define/endif结构产生预处理块。
l 【规则1-2-2】用 #include <filename.h> 格式来引用标准库的头文件(编译器将从标准库目录开始搜索)。
l 【规则1-2-3】用 #include "filename.h" 格式来引用非标准库的头文件(编译器将从用户的工作目录开始搜
索)。
2 【建议1-2-1】头文件中只存放"声明"而不存放"定义"
在C++ 语法中,类的成员函数可以在声明的同时被定义,并且自动成为内联函数。这虽然会带来书写上的方便,
但却造成了风格不一致,弊大于利。建议将成员函数的定义与声明分开,不论该函数体有多么小。
2 【建议1-2-2】不提倡使用全局变量,尽量不要在头文件中出现象extern int value 这类声明。
// 版权和版本声明见示例1-1,此处省略。
#ifndef GRAPHICS_H // 防止graphics.h被重复引用
#define GRAPHICS_H
#include <math.h> // 引用标准库的头文件
...
#include "myheader.h" // 引用非标准库的头文件
...
void Function1(...); // 全局函数声明
...
class Box // 类结构声明
{
...
};
#endif
示例1-2 C++/C头文件的结构
1.3 定义文件的结构
定义文件有三部分内容:
(1) 定义文件开头处的版权和版本声明(参见示例1-1)。
(2) 对一些头文件的引用。
(3) 程序的实现体(包括数据和代码)。
假设定义文件的名称为 graphics.cpp,定义文件的结构参见示例1-3。
// 版权和版本声明见示例1-1,此处省略。
#include "graphics.h" // 引用头文件
...
// 全局函数的实现体
void Function1(...)
{
...
}
// 类成员函数的实现体
void Box::Draw(...)
{
...
}
示例1-3 C++/C定义文件的结构
1.4 头文件的作用
早期的编程语言如Basic、Fortran没有头文件的概念,C++/C语言的初学者虽然会用使用头文件,但常常不明其
理。这里对头文件的作用略作解释:
(1)通过头文件来调用库功能。在很多场合,源代码不便(或不准)向用户公布,只要向用户提供头文件和二
进制的库即可。用户只需要按照头文件中的接口声明来调用库功能,而不必关心接口怎么实现的。编译器会从库
中提取相应的代码。
(2)头文件能加强类型安全检查。如果某个接口被实现或被使用时,其方式与头文件中的声明不一致,编译器
就会指出错误,这一简单的规则能大大减轻程序员调试、改错的负担。
1.5 目录结构
如果一个软件的头文件数目比较多(如超过十个),通常应将头文件和定义文件分别保存于不同的目录,以便于
维护。
例如可将头文件保存于include目录,将定义文件保存于source目录(可以是多级目录)。
如果某些头文件是私有的,它不会被用户的程序直接引用,则没有必要公开其"声明"。为了加强信息隐藏,这些
私有的头文件可以和定义文件存放于同一个目录。
第2章 程序的版式
版式虽然不会影响程序的功能,但会影响可读性。程序的版式追求清晰、美观,是程序风格的重要构成因素。
可以把程序的版式比喻为"书法"。好的"书法"可让人对程序一目了然,看得兴致勃勃。差的程序"书法"如螃蟹爬
行,让人看得索然无味,更令维护者烦恼有加。请程序员们学习程序的"书法",弥补大学计算机教育的漏洞,实
在很有必要。
2.1 空行
空行起着分隔程序段落的作用。空行得体(不过多也不过少)将使程序的布局更加清晰。空行不会浪费内存,虽
然打印含有空行的程序是会多消耗一些纸张,但是值得。所以不要舍不得用空行。
l 【规则2-1-1】在每个类声明之后、每个函数定义结束之后都要加空行。参见示例2-1(a)
l 【规则2-1-2】在一个函数体内,逻揖上密切相关的语句之间不加空行,其它地方应加空行分隔。参见示例2-1
(b )
// 空行
void Function1(...)
{
...
}
// 空行
void Function2(...)
{
...
}
// 空行
void Function3(...)
{
...
}
// 空行
while (condition)
{
statement1;
// 空行
if (condition)
{
statement2;
}
else
{
statement3;
}
// 空行
statement4;
}
示例2-1(a) 函数之间的空行 示例2-1(b) 函数内部的空行
2.2 代码行
l 【规则2-2-1】一行代码只做一件事情,如只定义一个变量,或只写一条语句。这样的代码容易阅读,并且方
便于写注释。
l 【规则2-2-2】if、for、while、do等语句自占一行,执行语句不得紧跟其后。不论执行语句有多少都要加{}
。这样可以防止书写失误。
示例2-2(a)为风格良好的代码行,示例2-2(b)为风格不良的代码行。
int width; // 宽度
int height; // 高度
int depth; // 深度
int width, height, depth; // 宽度高度深度
x = a + b;
y = c + d;
z = e + f;
X = a + b; y = c + d; z = e + f;
if (width < height)
{
dosomething();
}
if (width < height) dosomething();
for (initialization; condition; update)
{
dosomething();
}
// 空行
other();
for (initialization; condition; update)
dosomething();
other();
示例2-2(a) 风格良好的代码行 示例2-2(b) 风格不良的代码行
2 【建议2-2-1】尽可能在定义变量的同时初始化该变量(就近原则)
如果变量的引用处和其定义处相隔比较远,变量的初始化很容易被忘记。如果引用了未被初始化的变量,可能会
导致程序错误。本建议可以减少隐患。例如
int width = 10; // 定义并初绐化width
int height = 10; // 定义并初绐化height
int depth = 10; // 定义并初绐化depth
2.3 代码行内的空格
l 【规则2-3-1】关键字之后要留空格。象const、virtual、inline、case 等关键字之后至少要留一个空格,否
则无法辨析关键字。象if、for、while等关键字之后应留一个空格再跟左括号‘(',以突出关键字。
l 【规则2-3-2】函数名之后不要留空格,紧跟左括号‘(',以与关键字区别。
l 【规则2-3-3】‘('向后紧跟,‘)'、‘,'、‘;'向前紧跟,紧跟处不留空格。
l 【规则2-3-4】‘,'之后要留空格,如Function(x, y, z)。如果‘;'不是一行的结束符号,其后要留空格,
如for (initialization; condition; update)。
l 【规则2-3-5】赋值操作符、比较操作符、算术操作符、逻辑操作符、位域操作符,如"="、"+=" ">="、"<="
、"+"、"*"、"%"、"&&"、"||"、"<<","^"等二元操作符的前后应当加空格。
l 【规则2-3-6】一元操作符如"!"、"~"、"++"、"--"、"&"(地址运算符)等前后不加空格。
l 【规则2-3-7】象"[]"、"."、"->"这类操作符前后不加空格。
2 【建议2-3-1】对于表达式比较长的for语句和if语句,为了紧凑起见可以适当地去掉一些空格,如for (i=0;
i<10; i++)和if ((a<=b) && (c<=d))
void Func1(int x, int y, int z); // 良好的风格
void Func1 (int x,int y,int z); // 不良的风格
if (year >= 2000) // 良好的风格
if(year>=2000) // 不良的风格
if ((a>=b) && (c<=d)) // 良好的风格
if(a>=b&&c<=d) // 不良的风格
for (i=0; i<10; i++) // 良好的风格
for(i=0;i<10;i++) // 不良的风格
for (i = 0; I < 10; i ++) // 过多的空格
x = a < b ? a : b; // 良好的风格
x=a<b?a:b; // 不好的风格
int *x = &y; // 良好的风格
int * x = & y; // 不良的风格
array[5] = 0; // 不要写成 array [ 5 ] = 0;
a.Function(); // 不要写成 a . Function();
b->Function(); // 不要写成 b -> Function();
示例2-3 代码行内的空格
2.4 对齐
l 【规则2-4-1】程序的分界符‘{'和‘}'应独占一行并且位于同一列,同时与引用它们的语句左对齐。
l 【规则2-4-2】{ }之内的代码块在‘{'右边数格处左对齐。
示例2-4(a)为风格良好的对齐,示例2-4(b)为风格不良的对齐。
void Function(int x)
{
... // program code
}
void Function(int x){
... // program code
}
if (condition)
{
... // program code
}
else
{
... // program code
}
if (condition){
... // program code
}
else {
... // program code
}
for (initialization; condition; update)
{
... // program code
}
for (initialization; condition; update){
... // program code
}
While (condition)
{
... // program code
}
while (condition){
... // program code
}
如果出现嵌套的{},则使用缩进对齐,如:
{
...
{
...
}
...
}
示例2-4(a) 风格良好的对齐 示例2-4(b) 风格不良的对齐
2.5 长行拆分
l 【规则2-5-1】代码行最大长度宜控制在70至80个字符以内。代码行不要过长,否则眼睛看不过来,也不便于
打印。
l 【规则2-5-2】长表达式要在低优先级操作符处拆分成新行,操作符放在新行之首(以便突出操作符)。拆分
出的新行要进行适当的缩进,使排版整齐,语句可读。
if ((very_longer_variable1 >= very_longer_variable12)
&& (very_longer_variable3 <= very_longer_variable14)
&& (very_longer_variable5 <= very_longer_variable16))
{
dosomething();
}
virtual CMatrix CMultiplyMatrix (CMatrix leftMatrix,
CMatrix rightMatrix);
for (very_longer_initialization;
very_longer_condition;
very_longer_update)
{
dosomething();
}
第3章 命名规则
比较著名的命名规则当推Microsoft公司的"匈牙利"法,该命名规则的主要思想是"在变量和函数名中加入前缀以
增进人们对程序的理解"。例如所有的字符变量均以ch为前缀,若是指针变量则追加前缀p。如果一个变量由ppch
开头,则表明它是指向字符指针的指针。
"匈牙利"法最大的缺点是烦琐,例如
int i, j, k;
float x, y, z;
倘若采用"匈牙利"命名规则,则应当写成
int iI, iJ, ik; // 前缀 i表示int类型
float fX, fY, fZ; // 前缀 f表示float类型
如此烦琐的程序会让绝大多数程序员无法忍受。
据考察,没有一种命名规则可以让所有的程序员赞同,程序设计教科书一般都不指定命名规则。命名规则对软件
产品而言并不是"成败悠关"的事,我们不要化太多精力试图发明世界上最好的命名规则,而应当制定一种令大多
数项目成员满意的命名规则,并在项目中贯彻实施。
3.1 共性规则
本节论述的共性规则是被大多数程序员采纳的,我们应当在遵循这些共性规则的前提下,再扩充特定的规则,如
3.2节。
l 【规则3-1-1】标识符应当直观且可以拼读,可望文知意,不必进行"解码"。
标识符最好采用英文单词或其组合,便于记忆和阅读。切忌使用汉语拼音来命名。程序中的英文单词一般不会太
复杂,用词应当准确。例如不要把CurrentValue写成NowValue。
l 【规则3-1-2】标识符的长度应当符合"min-length && max-information"原则。
几十年前老ANSI C规定名字不准超过6个字符,现今的C++/C不再有此限制。一般来说,长名字能更好地表达含义
,所以函数名、变量名、类名长达十几个字符不足为怪。那么名字是否越长约好?不见得! 例如变量名maxval就
比maxValueUntilOverflow好用。单字符的名字也是有用的,常见的如i,j,k,m,n,x,y,z等,它们通常可用作函数
内的局部变量。
l 【规则3-1-3】命名规则尽量与所采用的操作系统或开发工具的风格保持一致。
例如Windows应用程序的标识符通常采用"大小写"混排的方式,如AddChild。而Unix应用程序的标识符通常采用"
小写加下划线"的方式,如add_child。别把这两类风格混在一起用。
l 【规则3-1-4】程序中不要出现仅靠大小写区分的相似的标识符。
例如:
int x, X; // 变量x 与 X 容易混淆
void foo(int x); // 函数foo 与FOO容易混淆
void FOO(float x);
l 【规则3-1-5】程序中不要出现标识符完全相同的局部变量和全局变量,尽管两者的作用域不同而不会发生语
法错误,但会使人误解。
l 【规则3-1-6】变量的名字应当使用"名词"或者"形容词+名词"。
例如:
float value;
float oldValue;
float newValue;
l 【规则3-1-7】全局函数的名字应当使用"动词"或者"动词+名词"(动宾词组)。类的成员函数应当只使用"动
词",被省略掉的名词就是对象本身。
例如:
DrawBox(); // 全局函数
box->Draw(); // 类的成员函数
l 【规则3-1-8】用正确的反义词组命名具有互斥意义的变量或相反动作的函数等。
例如:
int minValue;
int maxValue;
int SetValue(...);
int GetValue(...);
2 【建议3-1-1】尽量避免名字中出现数字编号,如Value1,Value2等,除非逻辑上的确需要编号。这是为了防止
程序员偷懒,不肯为命名动脑筋而导致产生无意义的名字(因为用数字编号最省事)。
3.2 简单的Windows应用程序命名规则
作者对"匈牙利"命名规则做了合理的简化,下述的命名规则简单易用,比较适合于Windows应用软件的开发。
l 【规则3-2-1】类名和函数名用大写字母开头的单词组合而成。
例如:
class Node; // 类名
class LeafNode; // 类名
void Draw(void); // 函数名
void SetValue(int value); // 函数名
l 【规则3-2-2】变量和参数用小写字母开头的单词组合而成。
例如:
BOOL flag;
int drawMode;
l 【规则3-2-3】常量全用大写的字母,用下划线分割单词。
例如:
const int MAX = 100;
const int MAX_LENGTH = 100;
l 【规则3-2-4】静态变量加前缀s_(表示static)。
例如:
void Init(...)
{
static int s_initValue; // 静态变量
...
}
l 【规则3-2-5】如果不得已需要全局变量,则使全局变量加前缀g_(表示global)。
例如:
int g_howManyPeople; // 全局变量
int g_howMuchMoney; // 全局变量
l 【规则3-2-6】类的数据成员加前缀m_(表示member),这样可以避免数据成员与成员函数的参数同名。
例如:
void Object::SetValue(int width, int height)
{
m_width = width;
m_height = height;
}
l 【规则3-2-7】为了防止某一软件库中的一些标识符和其它软件库中的冲突,可以为各种标识符加上能反映软
件性质的前缀。例如三维图形标准OpenGL的所有库函数均以gl开头,所有常量(或宏定义)均以GL开头。
3.3 简单的Unix应用程序命名规则