语义的需要

断言

    断言准确的说应该算是一门语言无关的技术,不过其在代码的编写中占有重要地位,不能不提。断言就是在代码的调试版时会由于一表达式的值而弹出警告对话框,但是在释放版时不造成任何影响。断言表示在其所在位置,对应表达式的值应该满足的条件,不是可以满足的条件。
    断言的一般用途很多地方都说成是帮助调试。其实其用途和const变量及const成员函数一样,“帮助调试”只是其附加效果,它的真正目的是为了表现代码编写人的逻辑。就如前面const成员函数中所说的一样,如果一个程序员编写的代码断言失败,则表示他逻辑混乱,或者是对断言保护的代码(即断言后面的语句)进行了错误的使用,或者是断言的设置是不合逻辑的。
    在每一个类的公共成员函数的最开头都应该断言一下this的有效性(前面的Get/Set函数中我就使用了MFC中的ASSERT_VALID断言宏,后面的断言都使用MFC中的断言宏)。因为类的公共成员函数是由外界调用的,外界可能如下书写:
CAbcList *pL = NULL;
pL->GetCount(); // GetCount里断言失败
    这表示在每个公共成员函数执行以前都假定了自己的this是有效的指针,这是逻辑上的假定,因此需要用断言表示出来。但是保护及私有就不用这样保护了,因为它们一定是被自己或派生类调用,是辅助用的,而它们能够被调用,就表示调用者是有效的,逻辑上不需要前面的假定。
    CAbcList有个保护成员函数DWORD SortElement( DWORD index ) const,给定的index表示第几个元素,而返回的DWORD表示按照内定的排序方法,这个元素的位置。在SortElement里,第一句话就应该是断言index的有效性:ASSERT( index < m_Count )。这里又有个隐晦的假设,就是index一定是小于元素个数的,因此应该用断言将其表示出来。同样的,在这个方法的客户端(某个成员函数中),在使用 SortElement的返回值之前,应断言以确保其小于元素个数。
    因此断言是非常重要的,其不是为了调试而写的,而是为了表现逻辑而写的,养成书写断言的习惯,将会使代码的可读性大有提高。


小结

    C++提供的语义很正常地不止上面提到的那几个,还有许多都具有很重要的语义,如虚函数,重载函数、友员及explicit关键字等,上面的只是经常会被忽视而已。最后举一个关于语义的应用例子以说明其重要性和对代码编写的指导作用。
    有个对话框的简单程序,其从COM口不断获得数据并将数据画成曲线实时显示出来。这是一个很普遍的模型(如将COM口换成编译器,就成为从编译器那里不断获得编译进度,然后再在主窗口的状态栏上实时显示出编译进度)。其有两个线程,一个工作线程负责不断获得数据并通知对话框显示数据,一个界面线程负责对话 框的显示。
    现在假设使用MFC编写,对话框上有个编辑控件,用于不断显示最新的数据的真实值(如水温)。则对话框派生类中就将一个float和那个编辑控件通过DDX绑定,并存有一个数组,记录历史数据以用来画曲线。这里就产生问题了。如下的线程函数:
DWORD WINAPI ThreadProc( void *pData ) // 线程函数,用于从COM口获取数据
{
    // 数据获取循环
    // 数据获得后放在变量i中
    CAbcDialog *pDialog = reinterpret_cast< CAbcDialog* >( pData );
    ASSERT( pDialog );
    pDialog->m_Data = i;
    pDialog->UpdateData( FALSE ); // UpdateData内部ASSERT_VALID( this )断言失败
}
    注释中的调用语句断言失败,不过这并不是这个方法的失败之处,下面从语义上来分析其为什么失败,至于为什么断言失败,如果有兴趣,看参考我写的另一篇文章——《MFC界面包装类》。
    一个线程代表一个具有能动性的物体,即可以自动完成一系列任务的物体,比如自动提款机,自动流水线等。而人具有主观能动性,因此也就具备能动性,因此可以被看作一个线程。在设计一个多线程的程序时,最好就是将每个线程看成是一个人,然后就能很自然的分配工作了。
    前面的工作线程假设用A表示,界面线程用B表示。则A不断的询问COM口,是否有数据,有则将数据记录到一张卡片上,然后通过电话或按铃等手段通知B,接着继续询问COM口(这里假设没有使用COM口的中断功能)。B则在等待,一有电话或铃声就立刻看卡片(可能通过管道传到B手上,更可能的应该是使用类似电子邮件的业务),然后将数据画在看板上。
    发现问题了吗?那个卡片就相当于上面的pDialog->m_Data(准确的说应该是相当于i,这里假设i的存在仅仅是为了缓冲,而不是传输), 问题就在于m_Data是B的成员,而卡片应该是A和B共同使用的,而变成B的成员后就表示A每次都需要跑到B所在的地方,将卡片填好,再回去。或者卡片仍像刚才那样用管道传递,但问题的关键是这个卡片是由A负责的,不是B,因此如果B把它弄丢了或者损坏了将由A负责再找个新的(但是在代码上没有任何问题,因为程序员不用考虑内存坏掉的情况,这种情况属于异常,而且线程永远不会闹脾气),长久下去A和B都会觉得很不公平(A:既然我负责为什么他能用? B:为什么不是我负责?)。这是正宗的权责模糊。这看起来牛角尖钻得很严重,毕竟要实现上面的算法可以使用的方法有很多,都完成同样的功能,但哪个更好 呢?如果你看重语义,那么这样分析一下是值得的。
    因此上面的问题就是m_Data是CAbcDialog的公共成员,应该将其变为一静态全局变量(其只在CAbcDialog和ThreadProc之间使用。如果将ThreadProc换成CAbcDialog的静态成员函数,这将是一个大失误,这样逻辑和界面混在一起,属于不好的设计)或者将 m_Data变为CAbcDialog的私有,ThreadProc只能通过一Set函数对其进行操作(削掉ThreadProc的读取权限),然后通过 SendMessage将消息发个B,上面使用UpdateData来做这件事情,是不对的,表示看板也由A负责画了,则B实际什么都不干(顶多干些打杂的活,搬运看板等)。下面再来看使用语义的进一步优化。
    考虑到将数据画到看板上很轻松,这样B将长时间不干事,而A则闲不下来(要监视COM口的动向),因此想到让A每次写完卡片后不用按铃或打电话(这节约了 电铃和电话的成本),而是让B闲着的时候就去看看卡片的数据是否更新了,更新了就拿回来将数据画在看板上。但如果B不是很闲,即走的不是很频繁则可能丢失数据,因此决定使用3个卡片,表示B不可能忙得会错过3个时间段。
    现在就变成m_Data[3]了,然后在B的CWinApp的派生类的OnIdle中调用pDialog->UpdateData来更新曲线。这节 约了SendMessage的开销(发生线程切换,至少1000个CPU周期以上,A将挂起等待B把消息确认,而此时就将依靠COM口的缓冲来缓冲数 据),将COM口的缓冲减小,因为A不会因为SendMessage的关系而错过数据(注意,不是中断模式)。如果界面的工作比较多(如界面上有个小动 画,如Office的帮手),相当于看板很复杂,B并不是很闲,则错过的卡片数会增多,反而加大了代码的复杂程度,且不稳定。此时应该像原来那样使用 SendMessage,不过为了消除SendMessage的开销,应该使用PostMessage来代替。
    因此当在设计多线程程序时,将每个线程看成是一个人,而代码只是人所具备的技能,然后就是自己水平的表现了。
    上面的例子只是为了说明语义在代码设计时的应用,语义准确说属于程序员的编程修养(就像编的时间多了就会使用一套自己的变量命名规则和代码书写风格),但是在代码中体现出语义不仅能使他人更易阅读代码,还能帮助自己理清思路,这在大型系统的编写中尤为重要。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值