llbird的MFC串口操作库CnComm断言问题探讨及vs2015修正

浏览: 106 发布日期: 2017-07-01 分类: c++

最近在读llbird的MFC串口操作库CnComm,在vs2015中使用了一下,感觉非常简洁精妙,好的地方就不说了,来谈谈问题。

首先是断言的使用问题,感觉作者的断言使用有点小小的问题,当然我说的也不一定对,欢迎大家来提意见!!

先看断言定义:

#ifndef CN_ASSERT
#define CN_2STR(L)		_T(#L)					//!< 将表达式L转换成字符串
#define CN_LINE(L)		CN_2STR(L)				//!< 将行号L转换成字符串
/*! 内部断言 启用异常将抛出异常 否则调试版将退出 发行版未启用异常将不做任何处理 */
#define CN_ASSERT(E)	((E) ? true : CnComm::Assert(_T("CN_ASSERT(") _T(#E) _T(") failed; CnComm(") CN_LINE(__LINE__) _T("); ")))
#endif
这个定义也些许问题,来看MFC的assert.h中关于assert的定义:
#ifdef NDEBUG
 #define assert(expression) ((void)0)
#else
 _ACRTIMP void __cdecl _wassert(
 _In_z_ wchar_t const* _Message,
 _In_z_ wchar_t const* _File,
 _In_ unsigned _Line
 );
 #define assert(expression) (void)( \
 (!!(expression)) || \
 (_wassert(_CRT_WIDE(#expression), _CRT_WIDE(__FILE__), (unsigned)(__LINE__)), 0) \
 )
#endif
//end

其 中 最关键的一行:

#ifdef NDEBUG
 #define assert(expression) ((void)0)
#else
//end

这一行至关重要,起什么作用呢?主要作用是在非debug模式下,编译器定义NDEBUG,将assert这一行代码替换为((void)0)。为什么说至关重要呢?因为这样一来,所有assert中的代码将不复存在,不管是函数调用还是条件判断!!!所以assert的使用有着至关重要的一条:

*不要用assert替代必需的条件判断!!!!!

嘿嘿,什么意思呢?就是对一些正常情况下可能出现的异常,不要使用assert。例如:

 FILE*fp=fopen(path,"r");
 assert(fp);
 fseek(fp,0L,SEEK_END);

这样的代码是完全错误的,用assert来代替必需的条件判断。也就是说,fopen正常情况下也可能失败,assert在debug模式下,暂时起到了条件判断作用,但是一旦在release的时候,上述assert便不复存在了,此时代码危险得很!!!正确写法是:

  FILE*fp=fopen(path,"r");
  assert(fp);
  if(fp)
  {
    fseek(fp,0L,SEEK_END);
  }
//end

好了,不说远了,来说CnComm。看完其断言定义,来看其使用:

void SetOption(DWORD dwOption)
{
   CN_ASSERT(!IsOpen());//! 打开状态下不可以设置参数

   dwOption_ = dwOption;
}
//end

由于断言定义不当,这么使用便造成了问题。即使用使用#define CN_ASSERT语句,也无法将其中的函数调用去调,也就是说,(!IsOpen())语句不管是debug还是release都会运行!!!这样对程序逻辑影响不大,但却是浪费了运行时间!!!更严复的问题出在Open函数中:

bool Open(DWORD dwPort)
	{
		if (!CN_ASSERT(dwPort >= 1 && dwPort <= 1024))
			return false;

		BindPort(dwPort);

		if (!CN_ASSERT(OpenPort()))
			return false;

		if (!CN_ASSERT(SetupPort()))
			return Close(), false;

		if ((dwOption_ & (EN_THREAD | EN_RX_THREAD | EN_TX_THREAD)) && !CN_ASSERT(BeginThread()))
			return Close(), false;

		return true;


嘿,注意 if (!CN_ASSERT(OpenPort()))这句!!问题倒是不大,主要问题在于:

对于串口的操作,出现串口打开失败,这是一种正常条件下的异常情况。当然你也可以使用断言来检测一下串口有没有打开,但串口没打开的情况下,程序依然需要有正确的处理逻辑,在CN_ASSERT关闭时,Open函数由于其定义,影响不大,但SetOption或多或少会受影响。很多情况下,我们需要用Open的方式来检测串口能否打开或存不存在,于是在CN_ASSERT打开时,你只能不断受到来自messagebox的骚扰!! 因此,既然串口打不开也需于正常情况,最好就是不要在Open时使用assert。在Open中使得assert便可以理解为:打开串口不允许失败!!

在一般情况下,串口打开失败的机率极高,因此Open函数应该考虑允许失败,将Open后的结果交由用户检测!!!!

首先修改断言定义:

#if !defined(CN_ASSERT) && defined(_DEBUG)
#define CN_2STR(L)		_T(#L)					//!< 将表达式L转换成字符串
#define CN_LINE(L)		CN_2STR(L)				//!< 将行号L转换成字符串
/*! 内部断言 启用异常将抛出异常 否则调试版将退出 发行版未启用异常将不做任何处理 */
#define CN_ASSERT(E)	((E) ? true : CnComm::Assert(_T("CN_ASSERT(") _T(#E) _T(") failed; CnComm(") CN_LINE(__LINE__) _T("); ")))
#else
#define CN_ASSERT(E)   ((void)0)
#endif
//end


其次,针对各个使用CN_ASSERT的地方进行修改:

       void SetOption(DWORD dwOption)
	{
		bool optTag = IsOpen();//2016.11.25,modify assert

		CN_ASSERT(!optTag);//! 打开状态下不可以设置参数

		if (optTag)//return while openning
		{
			return;
		}

		dwOption_ = dwOption;
//ce
#ifdef CN_COMM_FOR_CE

		optTag = IsOverlappedMode();
		CN_ASSERT(!optTag); //! WINCE不允许使用重叠IO 即EN_OVERLAPPED掩码
		if (optTag)
		{
			return;
		}

		dwOption_ &= (~EN_OVERLAPPED);
#endif
	}
//end
        


其它地方也依次修改,例如Open函数:

//! 打开串口 请注意与cnComm1~1.3的区别 cnComm1~1.3将使用9600, n, 8, 1配置端口 而1.5将只打开端口不配置波特率等参数  \param[in] dwPort 串口序号 1~1024
	bool Open(DWORD dwPort)
	{
		CN_ASSERT(dwPort >= 1);
		CN_ASSERT(dwPort <= 1024);

		if (dwPort == 0 || dwPort > 1024)
			return false;

		BindPort(dwPort);

		bool optTag = OpenPort();

		if (!optTag)
			return false;

		optTag = SetupPort();

		if (!optTag)
			{return Close(), false;}

		if ((dwOption_ & (EN_THREAD | EN_RX_THREAD | EN_TX_THREAD)))
		{
			optTag = BeginThread();
			CN_ASSERT(optTag);

			if (!optTag)
			{
				return Close(), false;
			}
		}


		return true;
	}
//end


原Open函数违背了assert的好几个原则,包括单项条件原则,也就是assert的条件中最好只有一个!!!

其次来说说vs2015中的修正,原1.51的代码在2015中编译会报error C4996,主要是关于函数安全的问题。

首先,对于_tcscpy函数,由于存在函数重载,直接改为_tcscpy_s即可;

其次,对于_sntprintf和_vsnprintf以及_vsnwprintf改为_sntprintf_s、_vsnprintf_s以及_vsnwprintf_s并在cout参数位置增加_TRUNCATE参数

最后,对于vsprintf和vswprintf,改为vsprintf_s和vswprintf_s,增加_vscprintf和_vscwprintf计数,代码如下:

//! 写串口 szBuffer 可以输出格式字符串 不检查缓冲区长度 小心溢出
	DWORD Write(char *szBuffer, char * szFormat, ...)
	{
		va_list va;
		va_start(va, szFormat);
		int len = _vscprintf(szFormat, va) + 1;
		vsprintf_s(szBuffer, len, szFormat, va);
		va_end(va);

		return Write(szBuffer);
	}
	//! 写串口 szBuffer 可以输出格式字符串 不检查缓冲区长度 小心溢出
	DWORD Write(wchar_t *szBuffer, wchar_t * szFormat, ...)
	{
		va_list va;
		va_start(va, szFormat);
		int len = _vscwprintf(szFormat, va) + 1;
		vswprintf_s(szBuffer, len, szFormat, va);//zhy@2016.11.24,change to 4 param,overflow danger!!!
		va_end(va);

		return Write(szBuffer);
	}
//end
注意,上述代码修改后,仍然可能溢出!!!

以上就是llbird的MFC串口操作库CnComm断言问题探讨及vs2015修正的全文介绍,希望对您学习和使用c++编程开发有所帮助.
返回顶部