实例解析C++/CLI的输入与输出

实例解析C++/CLI 的输入与输出
——有关C++/CLI程序读写的ABC
 
 
         当使用标准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;复制读取内容,把余下的字符输出直至文件结尾。最后,格式化输出一行文本。
 
插1:输入与对应的输出
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操作在默认情况下是同步执行的,但也可以异步的方式来执行,对此的讨论,已超出了本文的范围。愿大家编程愉快!
 
 
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值