當使用標准C++編程時,我們已開始接觸到兩個主要的I/O"工具":標准C頭文件cstdio和標准C++中與流相關的頭文件iostream,如果加上Windows的話,那麼還有Win32庫和MFC庫,另外,還有CLI/.NET。本文將要探討的,就是C++/CLI中的輸入與輸出。
簡介
日常,我們與文件或設備進行通訊的邏輯通道,稱為流。數據可以8位字節或16位Unicode字符形式進行讀寫,而兩者都有其自己的類集;另外,還有用於在字節與字符之間轉換的類。其中,字符流通過Stream類及其的派生類實現;字符流通過TextReader與TextWriter類及其的派生類實現。
在圖1中演示了標准I/O的類繼承關系。(帶有System命名空間前綴的類與I/O無關,但其卻是I/O類的基類。)
圖1:標准I/O類繼承關系
System::Object
System::Attribute
System::ComponentModel::MemberAttribute
System::ComponentModel::DescriptionAttribute
IODescriptionAttribute
System::ComponentModel::Component
FileSystemWatcher
System::Delegate
FileSystemEventHandler
RenamedEventHandler
System::EventArgs
FileSystemEventArgs
RenamedEventArgs
System::Exception
IOException
DirectoryNotFoundException
EndOfStreamException
FileNotFoundException
PathTooLongException
System::SystemException
InternalBufferOverflowException
BinaryReader
BinaryWriter
FileSystemEntry
Directory
File
Stream
BufferedStream
FileStream
MemoryStream
TextReader
StreamReader
StringReader
TextWriter
StreamWriter
StringWriter
System::ValueType
System::Enum
ChangedFilters
FileAccess
FileMode
FileShare
FileSystemAttributes
SeekOrigin
WatcherChangeTypes
WatcherTarget
WaitForChangedResult
每當一個程序運行時,會自動為我們打開三個流,分別是:
·標准輸入:一般來說,其被定向到鍵盤(可以使用Console::SetIn來進行重定向);可通過其類型為TextReader的Console::In字段來訪問。
·標准輸出:一般來說,其被定向到屏幕(可以使用Console::SetOut來進行重定向);可通過其類型為TextWriter的Console::Out的字段來訪問。標准輸出一般用於結果的顯示。
·標准錯誤:一般來說,其被定向到屏幕(可以使用Console::SetError來進行重定向);可通過其類型為TextWriter的Console::Error字段來訪問。標准錯誤一般用於錯誤信息的顯示。
在標准流中,可支持多種單字節與多字節字符編碼,例如,在大量的日文計算中,或許不會存儲為Unicode,但可使用占據一個或多個字節的多種編碼形式來存儲,如JIS、Shift-JIS、EUC;同樣地,在使用字母的"西方國家"中,大量的文本使用EBCDIC編碼來存儲,而使用UTF-8格式也在日漸增多。字符流隱藏了處理這些編碼的復雜性,它們中的某些類可允許指定某種特定的編碼形式。對字符編碼的詳細討論已經超過了本文的范圍,可參閱其他書籍。
基本I/O類
我們就從TextReader與TextWriter類開始,這兩個類提供了一些基本的原語,而所有其他的字符流I/O都是在其上構建的,在例1中演示了這些原語:
例1:
using namespace System;
/*1*/ using namespace System::IO;
void Copy(TextReader^ inStream, TextWriter^ outStream);
int main()
{
/*2*/ TextReader^ inStream = Console::In;
/*3*/ TextWriter^ outStream = Console::Out;
/*4*/ outStream->Write(static_cast<wchar_t>(inStream->Read()));
outStream->Write(static_cast<wchar_t>(inStream->Read()));
/*5*/ outStream->Flush();
array<wchar_t>^ buffer = {L'w', L'x', L'y', L'z'};
/*6*/ inStream->Read(buffer, 1, 2);
/*7*/ outStream->Write(buffer);
/*8*/ Copy(inStream, outStream);
/*9*/ outStream->Write("{0} * {1} = {2}\n", 10, 5, 10 * 5);
/*10*/ inStream->Close();
/*11*/ outStream->Close();
}
/*12*/
void Copy(TextReader^ inStream, TextWriter^ outStream)
{
/*13*/ int c;
while ((c = inStream->Read()) != -1)
{
outStream->Write(static_cast<wchar_t>(c));
}
}
在標記1中,我們通過引入命名空間System::IO,以使用標准I/O;在標記2與3中,定義了兩個變量:inStream與outStream,分別引用標准輸入與輸出流。
在標記4中,讀入一個字符,並直接回寫,接著再讀寫另一個。請留意寫操作中的顯示轉換,這是因為Read返回一個int而不是wchar_t。
輸出流支持緩沖區刷新--即轉儲清除,如標記5所示。
正如大家在標記6中所看到的,Read方法被重載了,我們之前使用的第一個版本讀取並返回一個寬字符,而這個新版本讀取一串給定數目的寬字符,並把它們以給定的偏移量,存儲在一個寬字符CLI數組中。在此,我們讀取了兩個字符,並把它們存儲在buffer[1]與buffer[2]中,而buffer[0]與buffer[3]未動。
在標記7中,我們使用了一個重載版本的Write輸出整個數組。
在標記8中,調用了Copy把輸入中的字符逐個復制到輸出中,直至文件結尾。緊接著,在標記9中,用了另一個重載版本的Write以進行格式化的輸出,最後,在標記10與11中,關閉了兩個流。
可在標記13中看到,我們讀取的字符存儲在一個int類型的變量中,而這樣做的原因是,我們需要返回一個代表了合法字符值的值,如文件結尾。通過返回一個int,Read可以返回-1作為文件結尾的值,而在此的Unicode輸入值可為范圍0-65535中的任意值。
代碼段1列出了一些輸入及與其對應的輸出。數字1與數字2被讀寫出來,接著數字3與數字4被讀取並存儲在4字符數組的第二個與第三個元素中。在輸出數組時,其包含了w、3、4、z;復制讀取內容,把余下的字符輸出直至文件結尾。最後,格式化輸出一行文本。
代碼段:輸入與對應的輸出
1234567
12w34z567
Hello there
Hello there
^Z
10 * 5 = 50
<end-of-file>
文件I/O
除我們上面使用的類之外,在文件I/O與標准流之間,其差異並不明顯。例2中的程序從命令行中接受它的輸入與輸出文件名字符串:
例2:
using namespace System;
using namespace System::IO;
void Copy(TextReader^ inStream, TextWriter^ outStream);
int main(array<String^>^ argv)
{
if (argv->Length != 2)
{
Console::WriteLine("需要兩個參數。");
/*1*/ Environment::Exit(1);
}
try
{
/*2a*/ FileStream^ inFile = gcnew FileStream(argv[0], FileMode::Open);
/*2b*/ StreamReader^ inStream = gcnew StreamReader(inFile);
/*2c*/// StreamReader^ inStream = File::OpenText(argv[0]);
Console::WriteLine("CanRead is {0}, CanWrite is {1}",inFile->CanRead, inFile->CanWrite);
/*3*/ StreamWriter^ outStream = File::CreateText(argv[1]);
/*4*/ Copy(inStream, outStream);
/*5*/ outStream->Write("{0} * {1} = {2}\n", 10, 5, 10 * 5);
inStream->Close();
outStream->Close();
}
/*6*/ catch (FileNotFoundException^ ex)
{
Console::WriteLine(ex->Message);
}
/*7*/ catch (IOException^ ex)
{
Console::WriteLine(ex);
}
}
/*8*/
void Copy(TextReader^ inStream, TextWriter^ outStream)
{
int c;
while ((c = inStream->Read()) != -1)
{
outStream->Write(static_cast<wchar_t>(c));
}
}
請留意main中的新符號,argv聲明為指向字符串數組的句柄,與標准C++中main不同,這個數組中不包含代表程序名的字符串,而是在argv[0]中代表了第一個命令行參數。
標記1中的Environment::Exit允許我們正常結束程序,並提供一個退出狀態碼(非零值在此表示沒有成功)。在標記2a中,定義了一個FileStream類型的inFile引用變量,它對應於輸入文件名,並指示創建一個新文件;接下來,在標記2b中,把這個FileStream對象映射到一個StreamReader對象上,這兩步也可合為一步,如標記2c中所示(其已經被注釋掉了)。另外,在File類中,提供了很多靜態的函數,我們在標記3中使用了一個函數創建一個StreamWriter對象。
正如上個例子一樣,Copy將把輸入中的每個字符復制到輸出中。因為StreamReader是從TextReader中派生,而StreamWriter是從TextWriter中派生的,所以Copy中的函數調用都是合法的,即使用同屬的I/O函數。
在標記5中,向輸出流進行了一個格式化的寫入,並關閉了兩個流。
請注意,標記6與7的catch塊中的順序是非常重要的,因為FileNotFoundException是從IOException中派生的,所以基類必須跟在派生類後面,否則,派生類的catch塊將永遠沒有機會執行。
除提供各種構造函數之外,StreamReader和StreamWriter也支持與TextReader和TextWriter相同的函數集。
字符串I/O
正像我們可以讀寫文件一樣,我們也能對字符串進行讀寫,請看例3中的例子,插2是程序輸出:
例3:
using namespace System;
using namespace System::Text;
using namespace System::IO;
void Copy(TextReader^ inStream, TextWriter^ outStream);
int main()
{
String^ str = "abcde";
/*1*/ StringReader^ inStream = gcnew StringReader(str);
/*2*/ StringWriter^ outStream = gcnew StringWriter;
/*3*/ StringBuilder^ sb = outStream->GetStringBuilder();
Console::WriteLine("Capacity is {0}", sb->Capacity);
/*4*/ outStream->Write(static_cast<wchar_t>(inStream->Read()));
//讀寫a
outStream->Write('!'); // write a !
outStream->Write(static_cast<wchar_t>(inStream->Read()));
//讀寫b
outStream->Flush();
outStream->Write("Result = {0,4:0.##}", 10.0/3);
//輸出格式化文本
/*5*/ Console::WriteLine(outStream); //調用StringWriter::ToString
/*6*/ Copy(inStream, outStream);
/*7*/ Console::WriteLine(outStream);
/*8*/ Console::WriteLine(sb); //調用StringBuilder::ToString
/*9*/ inStream->Close();
outStream->Close();
}
void Copy(TextReader^ inStream, TextWriter^ outStream)
{
int c;
while ((c = inStream->Read()) != -1)
{
outStream->Write(static_cast<wchar_t>(c));
}
}
代碼段2:例3的輸出
Capacity is 16
a!bResult = 3.33
a!bResult = 3.33cde
a!bResult = 3.33cde
正如大家在程序中所看到的,默認情況下,StringWriter的構造函數將創建一個未命名的StringBuilder用於進行文本的寫入,這個對象默認大小為16;另外也可在構造函數中使用一個現有的StringBuilder。在兩種情況下,底層的StringBuilder都是通過StringWriter::GetStringBuilder來訪問的。
其他類型的I/O
到目前為止,所有的示例都是處理字符I/O,當然,這對大多數程序來說,已是足夠了;但在某些程序中,卻需要以二進制形式處理不同的數據類型,請看例4中的代碼:
例4:
using namespace System;
using namespace System::IO;
int main()
{
/*1*/ Stream^ fs = File::Create("io04.dat");
/*2*/ BinaryWriter^ bw = gcnew BinaryWriter(fs);
/*3*/ bw->Write(true);
bw->Write(L'A');
bw->Write(0xabcd);
bw->Write(0x12345678LL);
bw->Write(123.456F);
bw->Write("Hello");
bw->Close();
fs->Close();
/*4*/ fs = File::Open("io04.dat", FileMode::Open);
/*5*/ BinaryReader^ br = gcnew BinaryReader(fs);
/*6*/ Console::WriteLine("bool: " + br->ReadBoolean());
Console::WriteLine("wchar_t: " + br->ReadChar());
Console::WriteLine("int: " + br->ReadInt32());
Console::WriteLine("long long: " + br->ReadInt64());
Console::WriteLine("float: " + br->ReadSingle());
Console::WriteLine("String: " + br->ReadString());
br->Close();
fs->Close();
}
一個BinaryWriter對象必須與某種形式的輸出流相關聯,因此,在標記1中,我們打開了一個磁盤文件,並在標記2中把文件流與BinaryWriter相關聯。而標記4與5中的情況也是一樣的。
標記3及後續的語句,調用了幾個Write函數,同樣地,在標記6中也有相應的Read函數。程序輸出見插3。
代碼段3:例4的輸出
bool: True
wchar_t: A
int: 43981
long long: 305419896
float: 123.456
String: Hello
隨機訪問I/O
在隨機訪問中,可以打開一個文件,並在同一時間,用讀寫流訪問它,或在文件中移動讀寫位置、保存當前位置以便返回、重新讀取一個文件區域、或進行覆寫,請看例5:
例5:
using namespace System;
using namespace System::IO;
int main()
{
/*1*/ Stream^ fs = gcnew FileStream("Io05.dat", FileMode::Create, FileAccess::ReadWrite);
BinaryWriter^ bw = gcnew BinaryWriter(fs);
BinaryReader^ br = gcnew BinaryReader(fs);
/*2*/ Console::WriteLine("CanRead is {0}, CanWrite is {1}, CanSeek is {2}",
fs->CanRead, fs->CanWrite, fs->CanSeek);
/*3*/ Console::WriteLine("Position at start is {0}", fs->Position);
bw->Write(true);
/*4*/ long long pos1 = fs->Position;
bw->Write(1234);
bw->Write(123.456);
Console::WriteLine("Position at end is {0}", fs->Position);
/*5*/ fs->Position = pos1;
bw->Write(5678); //把1234覆寫為5678
/*6*/ fs->Position = 0;
bw->Write(false); //把true覆寫為false
/*7*/ fs->Seek(0, SeekOrigin::Begin);
Console::WriteLine("bool: " + br->ReadBoolean());
/*8*/ fs->Seek(-1, SeekOrigin::Current);
Console::WriteLine("bool: " + br->ReadBoolean());
Console::WriteLine("int: " + br->ReadInt32());
/*9*/ fs->Seek(-8, SeekOrigin::End);
Console::WriteLine("double: " + br->ReadDouble());
bw->Close();
br->Close();
fs->Close();
}
在標記3中,顯示了當前文件位置,並在標記4中,通過FileStream::Position屬性把它保存在一個變量中。如果設置了相同的屬性值,就可以恢復當前位置,如標記5與6所示。在此可以保存任意數量的位置值。
還可以通過調用FileStream::Seek來確定文件的當前位置,如標記7、8、9中所示;此函數的第一個參數是一個與第二個參數指定位置相關的字節計數。
舉例來說,在標記7中,我們指定了從文件起始處的0字節偏移;在標記8中,我們指定了當前位置之前的1字節偏移--這也正位於我們讀取的布爾變量之前;在標記9中,指定了文件結尾前的8字節位置,並讀取此處的double變量。一般而言,最好設置Position為一個先前從屬性中獲取的值;另外,定位至文件的起始與結尾都是安全的,然而,定位至一個任意的字節位置也許會讓我們正巧位於一個多字節值當中,那麼之後進行的讀取將是毫無意義的,程序的輸出請見插4。
插4:例5的輸出
CanRead is True, CanWrite is True, CanSeek is True
Position at start is 0
Position at end is 13
boolean: False
boolean: False
int: 5678
double: 123.456
文件與目錄操作
File與Path類允許我們對文件及目錄名分別進行特定的操作。例6演示了有關此的一系列函數,而程序在Win32系統上的輸出請見插5。另外,Directory類也提供了一組與目錄相關的函數。 例6:
using namespace System;
using namespace System::IO;
int main()
{
String^ fName1 = "Io06";
if (!Path::HasExtension(fName1))
{
fName1 = Path::ChangeExtension(fName1, ".dat");
}
Console::WriteLine("fName1 is {0}", fName1);
StreamWriter^ outStream = File::CreateText(fName1);
outStream->Write("some text");
outStream->Close();
String^ fName2 = "Io06Copy.dat";
Console::WriteLine("File {0} exists is {1}", fName2, File::Exists(fName2));
File::Copy(fName1, fName2);
Console::WriteLine("File {0} exists is {1}", fName2, File::Exists(fName2));
File::Delete(fName2);
Console::WriteLine("File {0} exists is {1}\n", fName2,
File::Exists(fName2));
FileInfo^ f = gcnew FileInfo(fName1);
Console::WriteLine("fName1 is {0}", fName1);
Console::WriteLine("FullName: {0}", f->FullName);
Console::WriteLine("DirectoryName: {0}", f->DirectoryName);
Console::WriteLine("Name: {0}\n", f->Name);
Console::WriteLine("Attributes: {0}", f->Attributes);
Console::WriteLine("Length: {0}\n", f->Length);
Console::WriteLine("Creation Time: {0}", f->CreationTime);
Console::WriteLine("LastAccessTime: {0}", f->LastAccessTime);
Console::WriteLine("LastWriteTime: {0}", f->LastWriteTime);
}
代碼段5:例6的輸出
fName1 is Io06.dat
File Io06Copy.dat exists is False
File Io06Copy.dat exists is True
File Io06Copy.dat exists is False
fName1 is Io06.dat
FullName: e:\Seminars\C++\Ccli\Source\Io\Io06\Main\Io06.dat
DirectoryName: e:\Seminars\C++\Ccli\Source\Io\Io06\Main
Name: Io06.dat
Attributes: Archive
Length: 9
Creation Time: 6/12/2005 5:50:46 PM
LastAccessTime: 6/12/2005 5:53:44 PM
LastWriteTime: 6/12/2005 5:53:44 PM
其他話題
現在,越來越多的應用程序依賴於具有更持久化特性的信息--即外部文件,而不是在單次執行中產生的信息,例如,一個訪問存貨信息的程序,可能會查詢(或更新)一個或多個與此相關的數據文件;而信息"主文件"的生命期,往往可能會超過使用它的應用程序的生命期。另外,那些在不同程序間進行通訊的應用程序,如客戶端與服務端程序,當傳遞的信息其生命期大大短於數據庫記錄的生命期時,都會涉及到程序以外的某些數據格式。另外,數據記錄經常會包含一些簡單類型的對象,有關保存及恢復對象的過程,都可使用串行化機制來實現。
雖然,I/O操作在默認情況下是同步執行的,但也可以異步的方式來執行,對此的討論,已超出了本文的范圍。願大家編程愉快!