在 MFC 開發中,許多開發者會遇到 CArchive 和 CFile 文件指針位置不一致的問題。本文通過一個具體的學生信息管理系統代碼示例,深入分析這兩個類的文件指針關係,並解決讀寫循環只執行一次、寫入換行符導致讀取異常等常見問題。
void Add::OnClickedIdbAdd() {
UpdateData();
// ... 數據驗證代碼 ...
CFile cf;
if (!cf.Open(_T("D:\\SIMS.txt"), CFile::modeCreate | CFile::modeNoTruncate | CFile::modeReadWrite)) {
#ifdef _DEBUG
afxDump << "unable to open file" << "\n";
exit(1);
#endif
}
CArchive cas(&cf, CArchive::store); // 存檔對象(寫入)
CArchive cal(&cf, CArchive::load); // 加載對象(讀取)
CString strSno, strtemp;
// 嘗試用 CFile 的指針判斷文件結尾
while (cf.GetPosition() != cf.GetLength()) {
cal >> strSno;
if (cf.GetPosition() == cf.GetLength()) {
strtemp.Format(_T("等於:pos:%lu,len:%lu"), cf.GetPosition(), cf.GetLength());
MessageBox(strtemp);
}
MessageBox(strSno);
if (m_aSno == strSno) {
MessageBox(_T("該學號已存在,請重新輸入。"), _T("錯誤"));
m_aSno = ""; UpdateData(FALSE); return;
}
}
cf.SeekToEnd(); // 移動到文件末尾準備寫入
cas << m_aSno << m_aName << m_aMajor << m_aComp << m_aMath << m_aEng << m_aGym;
strtemp.Format(_T("\r\n"));
cas << strtemp; // 寫入換行符
cas.Flush();
cf.SeekToBegin(); // 回到文件開頭驗證
cal >> m_aSno >> m_aName;
MessageBox(m_aSno); MessageBox(m_aName);
// 清空界面並提示成功
m_aSno = ""; m_aName = ""; m_aMajor = "";
m_aComp = m_aEng = m_aGym = m_aMath = 0;
UpdateData(FALSE);
MessageBox(_T("添加成功"), _T("成功"));
cal.Close(); cas.Close(); cf.Close();
}
答案是肯定的。 CArchive 內部沒有獨立的文件指針,它直接操作關聯的 CFile 對象的指針。因此 cf.GetPosition() 和 cf.GetLength() 反映的就是 CArchive 讀寫後的實際位置。
代碼中同時創建了兩個 CArchive 對象:cas(存檔/寫入)和 cal(加載/讀取),它們共享同一個 CFile 對象 cf。這會導致嚴重的緩衝區衝突:
cal 讀取時會從文件預讀數據到內部緩衝區,導致 cf 的指針向前移動。cas 寫入時也會使用緩衝區,兩個緩衝區互相干擾,造成文件指針位置錯亂。cal >> strSno 讀取了一筆數據,緩衝區可能已讀取了後續內容,導致 cf.GetPosition() 直接跳到文件末尾,循環條件 cf.GetPosition() != cf.GetLength() 變為假,循環結束。\r\n 導致後續讀取異常的原因CArchive::ReadString 遇到換行符會停止讀取。代碼中寫入 \r\n 作為記錄分隔符,但讀取時使用的是 operator>>(針對 CString 會調用 ReadString)。第一次讀取正常,第二次讀取時遇到殘留的換行符或緩衝區數據錯亂,導致讀取到空字符串或亂碼,第三次關閉 CArchive 時因緩衝區不一致拋出異常。
// 1. 先純讀取檢查學號是否存在
CFile cfRead;
if (cfRead.Open(_T("D:\\SIMS.txt"), CFile::modeRead | CFile::shareDenyWrite)) {
CArchive cal(&cfRead, CArchive::load);
CString strSno;
while (cal.ReadString(strSno)) { // 使用 ReadString 按行讀取
// 解析每行數據(需約定格式,如字段用制表符分隔)
// 這裡簡單演示:假設每行第一個字段是學號
int pos = strSno.Find(_T('\t'));
CString curSno = (pos != -1) ? strSno.Left(pos) : strSno;
if (m_aSno == curSno) {
MessageBox(_T("學號已存在"));
cal.Close(); cfRead.Close(); return;
}
}
cal.Close(); cfRead.Close();
}
// 2. 追加寫入新記錄
CFile cfWrite;
cfWrite.Open(_T("D:\\SIMS.txt"), CFile::modeCreate | CFile::modeNoTruncate | CFile::modeWrite);
cfWrite.SeekToEnd();
CArchive cas(&cfWrite, CArchive::store);
cas << m_aSno << _T('\t') << m_aName << _T('\t') << m_aMajor << _T('\t')
<< m_aComp << _T('\t') << m_aMath << _T('\t') << m_aEng << _T('\t') << m_aGym << _T('\n');
cas.Flush(); cas.Close(); cfWrite.Close();
對於簡單的文本文件操作,直接用 CFile::Read/Write 或 CStdioFile 更直觀,避免序列化緩衝區問題。
CArchive 和 CFile 共享文件指針,但 CArchive 的內部緩衝機制使得同時進行讀寫操作極其危險。正確做法是將讀取和寫入分離到不同的時間點、不同的文件句柄中進行,並根據數據性質選擇合適的讀寫方式(文本用 CStdioFile,二進制結構化用 CArchive)。