求知 文章 文库 Lib 视频 iPerson 课程 认证 咨询 工具 讲座 Modeler   Code  
会员   
 
  
 
 
     
   
分享到
规范化的C++编程方法备忘录
 
作者 unituniverse2,火龙果软件    发布于 2014-02-17
 

先说一些题外话...

其实并没有“Win64编程”这一说法。只有64位Windows、64位平台、(应用程序的)64位版本。Win32PlatformSDK自从7.0版发布以来就做了修订。支持32/64位一体化代码。微软保证:没有特殊说明的API在32位和64位上都有相同的形式(仅针对库文件而言)。64位的API也叫Win32API(的64位版本)。没有Win64API一说。

当然不是所有的API两平台都有,例如AWE memory APIs就只有32位版本。

聪明的Windows设计者们并没有因为对象化管理就提供了完全面向对象的编程接口。相反,Windows提供的API很多是基于C的。尽管.NET已经发布并升级了多次,设计者甚至提出了用Windows SDK(基于.net的WinFX开发包)取代Win32PlatformSDK成为核心主件,但似乎并没有放弃C接口的API作为系统API的意思。毕竟让用户能够随便修改系统级的保护数据成员可不是个好主意。而且放弃了能包装成多种编程语言的能力会让微软冒着“无法灵活地扩展编程接口、仅支持自家编程语言”,从而失去部分市场的风险。

有一点比较重要:对字符串尽量不要用strxxx一族的标准库函数。尽量使用multi-bytes或Unicode处理字符串。(multi-bytes != chars)

- 如果你要提供给别人编程接口,使用C++可以做到高于C的性能(如果你开发的是用户模式应用)

不要以为C比C++快。当前的C++编译器已经比较完善。事实上,C++的性能很大程度上取决于用户的编程风格。这里提到的是指在完善编程的情况下能达到的。

例如我们创建一个对象,然后处理该对象并得到想要的结果。这里假设不考虑对象管理问题:

C:
LpTagAlian pobj;

pobj = CreateAlian();
if(!pobj)
return(FALSE);
fbRes = AlyAlian(&oResult, pobj);
...
DeleteAlian(pobj);

首先在这里CreateAlian分配一个TagAlian所需内存,然后经过适当处理形成一个有效的TagAlian对象。

然后调用AlyAlian。要命的是因为AlyAlian要公开给用户(例如作为DLL的导出函数),因此AlyAlian内部必须在作所需处理前先检查第二个参数中内容的有效性(除了检查pobj!=NULL外)。

C++:
CAlian obj;

obj.Create();
if(!obj.IsValid())
return(FALSE);
fbRes = obj.Aly(oResult);
...

考虑到有效性基于obj对象的私有属性成员,Aly内部就可以不检查该属性成员指向的堆内的实际对象的有效性。注意这样编码的缺点是,用户可能修改库文件来编程访问私有成员。

另外,通过上述分析可知,用C++包装已有API是无法像这样提高性能的(反而有所降低)。因为原有的“额外”检查并没有减少。

- 对象的连续引用问题

>相信你不会写出类似下面的代码:

LPDATA lpDEntry = GetData(hWndThis);

if(!lpDEntry)
return(0);
delete lpDEntry; // Frees memory referenced by lpDEntry;
lpDEntry->DoSomething(); // Quotes the 'object' that referenced by lpDEntry!
lpDEntry->msgpost = 1; // Writes 1 to the 'object' that has been deleted!
...

>但是现在我们把它改一下形式,成为:

LPDATA lpDEntry = GetData(hWndThis);

if(!lpDEntry)
return(0);
Foo(lpDEntry);
lpDEntry->DoSomething();
lpDEntry->msgpost = 1;
...

其中函数Foo的内容为:

void Foo(LPDATA lpDEntry)
{
delete lpDEntry; // Frees memory referenced by lpDEntry;
}

>然后我们可以干得再隐蔽些,成为:

LPDATA lpDEntry = GetData(hWndThis);
LPFOO lpFuncPrcss;

if(!lpDEntry)
return(0);
lpFuncPrcss = SelectFunc(userrequest);
(*lpFuncPrcss)(lpDEntry);
lpDEntry->DoSomething();
lpDEntry->msgpost = 1;
...

其中LPFOO被声明为:

typedef void (* LPFOO)(LPDATA);

SelectFunc为一个函数,视传入参数的值决定具体的返回函数,其中包括Foo。

>然而,我们还可以干得更隐蔽:

LPDATA lpDEntry = GetData(hWndThis);
LPFOO lpFuncPrcss;

if(!lpDEntry)
return(0);
SendMessage(hWndParent, WM_COMMAND, thisID, hWndThis);
lpDEntry->DoSomething();
lpDEntry->msgpost = 1;
...

不要忘了其他人在hWndParent的过程响应函数里可以干任何事情,包括调用Foo,如果hWndParent的过程响应函数是那个人编写的话。

>现在即使其他人调用不到Foo也一样糟糕,如果你在hWndThis过程函数里删除*lpDEntry,Foo这个函数甚至根本不存在。

>让我们把问题更简单化些:

设以下代码是你写的hWndThis窗口的过程响应函数中的几行:

RECT rc;

DestroyWindow(hWndThis);
if(!GetClientRect(hWndThis, &rc))
return(0);
if(rc.right - rc.left >= 16)
UseMyFrame(hWndThis, ...

你当然不会这样写,但有可能DestroyWindow处换成了一个SendMessage(hWndParent)。

这样的Win32 Users API还真不少:SendMessage系列、SetFocus、SetCapture、SetWindowPos、GetMessage、PeekMessage、...,及它们的调用者们(如MoveWindow调用了SetWindowPos)。

>杜绝此类故障的方法:

注意在C++等面向对象程序中可看不到类似在成员函数内部删除自己的例子(违反规则的程序不能被编译或运行时引起bug),因为设计决定了非静态成员函数就是对象的组成部分(如果这个类可以创建对象的话)。

但是你却可以把回调函数看作对象(严格地说不属于任何对象)的静态成员函数(其实根本就是全局的)。问题是很多回调函数被从某个函数内部激发,各种可能事件都会发生。

1.最简单但无本质帮助的方法就是把禁忌编程规范写到你的公开给用户(用你开发的接口开发的人)的文档中。

例如微软就让开发人员不要对DialogBox调用DestroyWindow而应当用EndDialog(其实只是设置退出标志)来结束模态对话框。
这样做的坏处就是对有嵌套的用户回调调用可能的用户程序码中,很容易因考虑不周和系统版本升级等因素引发跨越多极的这种故障。而且让用户很难调试跟踪出问题代码来。

2.避免在有内部的用户回调调用可能的函数其后使用对象。如果非要,该点无效。

3.避免这样线性地编程,将能延迟或提前的处理过程尽量分离掉。

例如把第一例改成

LPDATA lpDEntry = GetData(hWndThis);
LPFOO lpFuncPrcss;

if(!lpDEntry)
return(0);
lpDEntry->DoSomething();
lpDEntry->msgpost = 1;
SendMessage(hWndParent, WM_COMMAND, thisID, hWndThis);
...

但如果DoSomething和msgpost的处理想要依赖于SendMessage的结果,该点就无效。

4.使用对象销毁通告设置并检查状态。

在第一例中,可以定义一个全局类对象来管理*lpDEntry们。但这种方法的前提是对象数量必须比较少且最好能确定。
但对于动态数量的对象,则存在下面的分析:

-设成全局对象,必须有足够的容量容纳所有的*lpDEntry。予先分配静态数量的空间不合适(要么不足要么浪费),二分配动态数量的空间又存在遍历查询的效率问题。

-我们不大可能在堆中放跟踪标志,因为标志所占的内存本身也可能分配失败。

-难道用类来管理lpDEntry吗?仔细分析发现类根本无能为力。况且如果要类化lpDEntry,类本身也得放到堆中。不要忘了,你可以精巧设计使对象状态检查完美——除非对象已经被删除了...

在第二例中,在调用了SendMessage后调用IsWindow行吗?其实根本行不通!IsWindow比较慢,而且其他线程也可能创建或删除窗口,某个刚创建的窗口可能就使用了已经删除的窗口一样的句柄值。如果可能,SendMessage的接受者可以造一个一样的窗口来,后面的代码就会使用该窗口...

对象销毁通告可以是一个含标志的数据类型,必须存在于栈中。

每一次调用含有用户回调调用行为的函数前,设置对象销毁通告中的标志为“正常”,待函数返回后检查该标志。如果标志被改成了“已删除”,则所有的对被保护对象的操作都应当被跳过。被保护对象也得保留一个对对象销毁通告结构的指针。该指针默认为空。在一个对象销毁通告对象创建时被设置成指向该对象。但如果有嵌套调用而要同时引用多个对象销毁通告对象怎么办呢?幸而栈是后进先出的,从而避免了出现乱序的可能。这样我们就可以用链表处理该问题。当然,该链表的元素除了那个Data obj外其余的都只存在于栈中。注意当一个销毁通告对象(其实每次只可能是最靠近Data obj的那个)销毁时,要将自己的标志拷贝到前一个对象里面。紧接着,如果标志仍为“正常”就读取前一个对象的指针值,并拷贝到被保护对象Data obj的相应保留指针位置。

结果如图:

相关文章

深度解析:清理烂代码
如何编写出拥抱变化的代码
重构-使代码更简洁优美
团队项目开发"编码规范"系列文章
相关文档

重构-改善既有代码的设计
软件重构v2
代码整洁之道
高质量编程规范
相关课程

基于HTML5客户端、Web端的应用开发
HTML 5+CSS 开发
嵌入式C高质量编程
C++高级编程
 
分享到
 
 
   
Visual C++编程命名规则
任何时候都适用的20个C++技巧
C语言进阶
串口驱动分析
轻轻松松从C一路走到C++
C++编程思想
更多...   


C++并发处理+单元测试
C++程序开发
C++高级编程
C/C++开发
C++设计模式
C/C++单元测试


北京 嵌入式C高质量编程
中国航空 嵌入式C高质量编程
华为 C++高级编程
北京 C++高级编程
丹佛斯 C++高级编程
北大方正 C语言单元测试
罗克韦尔 C++单元测试
更多...