今天我在BBS的VC版上转悠,看到由个哥们出了这样的问题:
说他在编写MFC的数据库程序(ODBC)的时候出现了错误,再插入新记录后调用Update的时候出现了Assert,由于再BBS上,我和他通过信息交流了一下,发现他在AddNew和Update之间调用了Format。直觉告诉我问题出在这里。
于是分析了一下。这个是我在BBS上发的帖子。
这个问题我仔细看了一下,问题出在MFC内部:下面所述仅适用于VC6带的MFC4.2
我们在使用ODBC进行数据库的插入操作时,都是这么一个流程:
AddNew()
//给成员赋值
Update()
而在MFC的源文件dbcore.cpp 1040行,有这样一行注释:
// Buffer address must not change - ODBC's SQLBindCol depends upon this
由于MFC在进行默认的数据源绑定时,使用CString绑定字符串型的成员,而CString使用的是动态的内存管理方式,因此这个缓冲区地址其实是可以改变的,因此,在dbcore.cpp的1041行开始便是这样几句:
void* pvBind;
pvBind = value.GetBuffer(0);
value.ReleaseBuffer();
if (pvBind != pInfo->m_pvBindAddress)
{
TRACE1("Error: CString buffer (column %u) address has changed!/n",
nField);
ASSERT(FALSE);
}
因此,如果你在调用AddNew和Update之间把CString的缓冲区移动了,对不起,你必须收到一个ASSERT。(Nickshen好像就是这里的问题吧)
这样问题就很清楚了,就是在你调用AddNew和Update之间不能移动缓冲区。但是据我和nickshen私下讨论的结果,他只是在其中调用了CString的成员Format,想要把一个浮点数转换成字符串,如果这么做就会有问题,但是直接赋值就不会,难道Format会移动CString的缓冲区?
于是我跟踪了一下CString的Format函数,发现在被Format函数调用的FormatV函数的流程是这样的:先根据格式串算出大约格式化之后的字符串要占多大的空间,然后就是看是否分配新的缓冲区,然后sprintf。这个学过数据结构的都可以理解。远离很简单,但是有这么一个问题:在FormatV函数中,有这么一段
(strex.cpp, 659行起)
case 'f':
va_arg(argList, DOUBLE_ARG);
nItemLen = 128; // width isn't truncated
// 312 == strlen("-1+(309 zeroes).")
// 309 zeroes == max precision of a double
nItemLen = max(nItemLen, 312+nPrecision);
break;
这个是Format对于格式串中的%f的处理,在一个switch块中。
switch之后,
// adjust nMaxLen for output nItemLen
nMaxLen += nItemLen;
...
GetBuffer(nMaxLen);
看到了没?如果你使用了%f,MFC会很保守地认为你的一个%f会占用312的字符的位置(的确够保守的,至于为何时312,注释说得很清楚),于是用这个巨大的数调用GetBuffer。
然后是CString的operator=(LPCTSTR),这个就简单多了,不用保守的计算,源字符串有多少个字符就分配多少个字节,同样通过GetBuffer。
在GetBuffer的实现中,简单的说就是看看原来的长度够不够,不够重新分配一块够大的,然后memcpy,于是,缓冲区移动了。
慢着!
如果说长度不够就要移动缓冲区,而且两种操作都会移动缓冲区,那么为何只有Format会出错,赋值不会?
谁说不会?你尝试赋给你的变量一个长度超过256的字符串试试,肯定出错,我试过了。
那么,这个256又是何处来的?你在用一个RecordSet第一步一定是Open吧。跟踪一下发现,Open中有一步是BindFieldToColumns (dbcore.cpp 3854),经过一系列的分发,程序到了dbrfx.cpp 777:
case CFieldExchange::BindFieldToColumn:
...
// Constrain to user specified max length, subject to 256 byte min
if (cbColumn > (UINT)nMaxLength || cbColumn < 256)
cbColumn = nMaxLength;// Set up binding addres
void* pvData;
value.GetBufferSetLength(cbColumn+1);
pvData = value.LockBuffer(); // will be overwritten if UNICODE
那么这个nMaxLength是多少呢?这个看看AfxDB.h中对于RFX_Text的声明,255!
明白了?
这么一说事情就很清楚了,所有的一切都是由于MFC内部造成的,由于我们大多数时候都不会像数据库中插入一个长度超过255的字符串(事实上Access和SQL Server都只支持最多255个字符),因此不会有问题,但是偏偏MFC的工程师们在做Format函数的时候保守了,于是,只要你用了%f格式符,就有问题无疑了。
知道了原因,解决方案就很简单了:
1、如果你可以改数据库,不妨把那个string(varchar)类型的字段改成double。
2、如果你没有这个权限,或者数据太多已经不能改了,那么只有退而求其次,先定义一个buffer,sprintf一下,然后赋值给CString。
的确很麻烦。
同样的程序,再VC7下调试没有问题,有空再跟踪一下看看吧。MFC7.1的CString已
经完全重写了...