C++程序设计案例实训教程第15章

来源:互联网 发布:java和php的优缺点 编辑:程序博客网 时间:2024/06/06 02:41

15章  输入输出与文件第四篇  输入输出处理

    

                第15章  输入输出与文件

(本资料qq讨论群112133686)

C语言中,用printf()scanf()进行输入输出,往往不能保证所输入输出的数据是可靠的、安全的。C++语言的输入输出优于C语言中的printf()scanf()C++通过输入输出I/O类库来实现丰富的I/O功能,但是比较复杂有许多细节需要掌握。C++的输入输出流是指由若干字节组成的字节序列,这些字节中的数据按顺序从一个对象传送到另一对象。流表示了信息从源到目的端的流动。在C++中,输入输出流被定义为类,C++I/O库中的类称为流类(stream class)。用流类定义的对象称为流对象。coutcin并不是C++语言中提供的语句,它们是iostream类的对象,C++对文件的输入输出需要用ifstreamofstream类,ifstream支持对文件的输入操作,ofstream支持对文件的输出操作。

本章举的实例都是C++输入输出相关的例子,如怎样用流类库输出一个文件、对二进制文件读写(和文本文件读写)、获取文件长度和随机写入数据,以及输入输出库函数例子,通过这些实例让读者加深对输入输出的理解。本章重点是文件输入和输出的实例,这些对于初学者反复多实践,加深理解,举一反三。

15.1  输入输出概述

案例15-1  使用流类库输出一个文件

【案例描述】

本章开始演示C++输入输出系统的实例,程序的输入指的是在输入文件中把数据传送给程序,程序的输出指的是从程序把数据传送给输出文件。本实例创建个文件,写入数据,然后读取显示。本例效果如图15-1所示。

【实现过程】

Hello world!”字符串输出到“example.txt”文本文件中。以生成的“example.txt”文件作为输入文件,通过两次fin >> str;,读取内容,并在屏幕上输出。代码如下:

#include<iostream>

#include <fstream>

#include <string>

using namespace std;

int main() {   

cout <<"流类库输出文件演示:"<<endl;

ofstream fout("example.txt"); //磁盘文件的输出

if(!fout) //不能打开文件

{ cerr << "打开文件错误!" << endl; //输出出错信息

exit(1); //结束程序

}

fout << "Hello world!"<< endl; //写入文件内容

fout.close(); //关闭磁盘文件

ifstream fin("example.txt"); //磁盘文件的输入

if(!fin) { //不能打开文件

cerr << "打开文件错误!" << endl;

exit(1);}

string str; //定义string类字符串

fin >> str; //读取文件内容

cout << str << " ";

fin >> str; //读取文件内容

cout << str << endl;                      //打印输出内容

system("pause");

return 0;

}

【案例分析】

1ostream类提供了格式化和无格式化的输出功能。包括:

— 用插入操作符(<<)输出标准类型数据;

— put成员函数输出字符;

— write成员函数实现无格式输出。

最常用的输出方法是在cout上用插入操作符(<<),插入操作符可以接受任何标准类型的实参,包括const char *、标准库stringcomplex等类型。实参可以是任何表达式包括函数调用,只要其结果是能被插入操作符能接受的数据类型即可。

2C++中的I/O是以流(stream)的形式出现的。

流的输入操作是字节从外部设备(包括键盘、磁盘、网络连接)输入到内存,是字节从设备到内存的流动。输出操作是从内存输出到外部设备(如显示器、打印机、磁盘、网络连接),是字节从内存到外部设备的流动。

 

提示:流实际上是一个处于传输状态的字节序列,是字节在对象之间的“流动”,流的操作包括输入与输出。

案例15-2  如何获得文件长度

【案例描述】

实际编程中常需要知道一个文件内容长度,如读取图像数据时知道文件有多长,以便初始化缓冲区。本例演示输入一个文件求文件长度的功能,效果如图15-2所示。

 

15-2  如何获得文件长度

【实现过程】

打开一个文件example.txt,取得文件头的位置begin和取得文件尾的位置end,它们的差值(end-begin)就是文件的长度。代码如下:

#include <iostream>

#include <fstream>

using namespace std;

int main () {

  long begin,end;

  ifstream myfile ("example.txt"); //磁盘文件的输入

  begin = myfile.tellg(); //返回读指针相对于文件头的位置

  myfile.seekg (0, ios::end); //输出文件中的指针从文件尾

  end = myfile.tellg(); //返回读指针相对于文件头的位置

  myfile.close(); //关闭磁盘文件

  cout << "example.txt size is: " << (end-begin) << " bytes.\n";

  return 0;

}

【案例分析】

1)上述代码long tellg()回读指针相对于文件头的位置。

2stream& seekg(long off, ios::seek_dir dir ),设定读取指针到距离文件某一特定位置off个字节的位置。特定位置由dir确定,dirios::begios::curios::end之一。当特定位置为ios::end时,off应为负数。

案例15-3  使用system()函数使屏幕停止

【案例描述】

如果调试控制台程序中,很多时候点击“启动调试”后是一闪而过,此时可有两种方法让cmddos调试屏幕暂停,一个是代码加上system("pause"),对这个读者很熟悉;另一个是在主函数main()return 0前加上两句:cin.get()。本例效果如图15-3所示。

 

15-3  使用system()函数使屏幕停止

【实现过程】

定义一个函数simon(),输入的是整数,主函数中输入整数,调用simon()显示输入整数,cin.get()屏幕暂停,让程序等待键击。代码如下:

#include <tchar.h>

#include<iostream>

void simon(int);

using namespace std; //这是一个使用std;名称空间的例子

int _tmain(int argc, _TCHAR* argv[])

{

simon(18);

cout<<"请输入一个整数: ";

int count;

cin>>count; //输入取得的整数

simon(count); //显示输入整数

cout<<"完成!"<<endl;

cin.get(); //这两行是使调试屏幕暂停,不会一闪而过

cin.get(); //让程序等待键击

return 0; //退出主函数

}

void simon(int n)

{

cout<<"现在整数是: "<<n<<"测试!"<<endl;

}

【案例分析】

1)代码中调用了一个getch()库函数,程序中的作用使屏幕暂停,当程序执行到此函数时就暂停,等待输入一个任意字符后,程序继续向下执行。这样做的好处是,可以使用户看清封面和主子菜单

2cin.get(字符数组名,接收字符数目)用来接收一行字符串,可以接收空格。

 

提示:VC++下,cin.get()如果不包含using namespace std指令,那么必须使用std::前缀std::cin.get

15.2  高层I/O

案例15-4  读写二进制文件

【案例描述】

二进制文件是指含ASCII码字符外的数据的文件,它不能由文本编辑软件打开。在实际应用中,大多数文件都是二进制文件,如图像文件、影像文件等。这是一个二进制文件读写的例子,效果如图15-4所示。

 

15-4  读写二进制文件

【实现过程】

创建ofstream流对象out对应于二进制文件example.asc,打开文件,以二进制形式写入浮点数字符数组fnum,以二进制形式按格式fnum读出数据并显示读出数据。代码如下:

#include <iostream>

#include <fstream>

using namespace std;

int main(void){

   float fnum[4] = {11.22, -33.44, 55.66, 77.88};//定义浮点数字符数组

   int i;

   //创建ofstream流对象out

   ofstream out("example.asc", ios::out | ios::binary);

   if(!out){ //不能打开文件

      cout << "Cannot open file.";

      exit (1); //结束程序

   }

   out.write((char *) &fnum, sizeof(fnum)); //以二进制形式写入numbers.asc

   out.close(); //关闭流

   for (i=0; i<4; i++)

      fnum[i] = 0.0;

   //创建ifstream流对象in,以便从numbers.asc中读取信息

   ifstream in("example.asc", ios::in | ios::binary);

   if(!in) { //不能打开文件

      cout << "Cannot open file.";

      exit (1); //结束程序

   }

   in.read((char *) &fnum, sizeof(fnum)); //以二进制形式读入数据

   cout << in.gcount() << " bytes read." << endl; //显示读入数据字符数

   for (i=0; i<4; i++)

      cout << fnum[i] << " "; //标准输出,屏幕显示

   in.close(); //关闭流

   system("pause");

}

【案例分析】

1)在代码中,二进制文件example.asc的读写利用istream类的成员函数readwrite来实现。这两个成员函数的原型为:

istream& read(char *buffer,int len);

ostream& write(const char * buffer,int len); 

字符指针buffer指向内存中一段存储空间,len是读写字节数。调用的方式如:

a. write(p1,50); b. read(p2,30);。

2)输出二进制文件的方法是使用write()成员函数。输入二进制文件使用成员函数read()每次读取固定长度的数据,同样用eof()判断是否到达文件尾。

 

提示:C++语言把每个文件看成是一个有序的字节流。文件打开时,就创建一个对象,并将这个对象和某个流关联起来。包含<iostream>时,会自动生成cincoutcerrclog这四个对象,与这些对象关联的流提供与文件通信的方法(文件操作)。

案例15-5  读写记事本

【案例描述】

文本文件是以ASCII表示的文件如记事本。在文件操作前,需要将程序与被操作的文件联系起来,使程序可以“引用”文件。在程序内定义一个文件类的对象,由该对象与文件发生联系,程序内所有的与文件的操作都是对该对象的操作。本实例举文本文件读写操作,效果如图15-5所示。

【实现过程】

键盘读入一行字符再输出到“example.txt”文本文件中。以生成的“example.txt”文件作为输入文件,通过两次infile>>line;,读取内容,并在屏幕上输出。代码如下:

#include <iostream>

#include <fstream>

#include  <fstream>

#include <string>

using namespace std;

void main() {

char line[180];

fstream myfile; //建立文件流

myfile.open("example.txt", ios::out); //定义文件流对象,打开磁盘文件

if(!myfile) { //如果打开失败

cerr<<"File open or create error!"<<endl;

exit(1);

}

     cin.getline(line,80); //从键盘读入一行字符

 

myfile<<line; //向磁盘文件输出数据

myfile.close(); //关闭磁盘文件

 

      ifstream infile; //定义输入文件流对象

infile.open("example.txt"); //以输入方式打开磁盘文件

if (infile.is_open())

{

while (! infile.eof() )

{

infile>>line; //从磁盘文件读入

cout <<"读取文件内容为:"<<line << endl; //在显示器上显示

}

infile.close(); //关闭磁盘文件

}else //如果打开失败

cout << "Unable to open file";

system("pause");

}

【案例分析】

1C++的文件I/O模式分为两种文本模式与二进制模式,默认模式为文本模式。当使用文本模式时,输出到文件的内容为ASCII码字符(包括回车、换行)。也就是说,文本文件中只能存储ASCII码字符。如整数123与浮点数234.5在文本文件中分别存储为:“123”与“234.5”。

2)文本文件输出可用插入操作符<<与成员函数write()

3)文件输出的步骤一般为:

— 创建输出文件流(对象),将建立的文件连接到文件流上。此步需要对文件是否创建成功进行判断,如果文件创建错误,则退出。

— 向输出文件流输出内容。

— 关闭文件(文件流对象消失时也会自动关闭文件)。

 

提示:文本文件通常以.txt为后缀,C++的源程序文件也属于文本文件。文本文件能用Windows的记事本打开。

案例15-6  搜索文本

【案例描述】

在实际编程中可实现对一个打开的文件进行处理。如打开记事本,对记事本的内容进行查找、替换等操作,本实例举个最简单的统计某个字符串在文本中出现次数的实例。本例效果如图15-6所示。

 

15-6  搜索文本

【实现过程】

输入文件名称,打开文件,对输入关键词查找,输出出现次数的累加。代码如下:

int main()

{

char filename[30];

cout<<"输入文件名称:"<<endl;

cin>>filename;

fstream fin(filename, ios::in);

if(!fin)

{  cerr << "cannot open file " << endl; //文件打开出错

return -1;

}

string s;

cout << "请输入要查找的字符串:";

cin >> s; //关键词的处理

string t;

int num = 0;

string::size_type i;

while(!fin.eof()) //文件最后面

{  

fin >> t; //开始读字符,直到遇到空白符,说明找到一个词

if(t.size() > s.size()) //找到一个词大于关键词

{   

for(i = 0; i != s.size(); i++)   

{    

if(t[i] != s[i]) //不相等关键词的处理   

break;  

}   

if(i == s.size())    num++; //出现次数累加

}  

else if (t == s)   num++; //相等出现次数累加

}

fin.close(); //关闭打开文件

cout << "该字符串共出现了" << num << "" << endl;

system("pause");

return 0;}

【案例分析】

1)在一个已知的文本文件中查找字符串,统计该字符串在文章中出现次数,字符串由用户给定。

2fstream可同时读写操作的文件类,为输入(读)而打开文件。eof()如果读文件到达文件末尾,返回true

 

提示:string::size_type中定义与负数进行运算时自动转化为signed,而unsigned没有这个功能。

案例15-7  猜灯谜

【案例描述】

猜灯谜是一个互动游戏,产生随机的谜面在规定次数猜出正确的谜底。本例涉及随机算法,抽取打开题库中的随机的谜面,等待猜谜者输入谜底。本例效果如图15-7所示。

【实现过程】

打开题库文件riddle.dat,srand()随机产生序号,读取猜谜信息,取得输入谜底的信息,程序判断是否正确,最后程序判定猜谜的成绩。其代码如下:

void main()

{ FILE *stream;

char line[1000];

CString mimian; //谜面

CString tishi; //提示

CString midi; //谜底

CString fstring;

char ch;

    srand(time(NULL));

int Win = rand() % 90 + 10; //随机赋值

if( (stream = fopen( "riddle.dat", "r" )) != NULL )

{

     if( fgets( line, 100, stream ) == NULL)

printf( "fgets error\n" );

        else

{

         while(!feof(stream))  {

  for(int i=0;i<Win;i++) //随机赋值

  fgets( line, 1000, stream );

          if( fgets( line, 1000, stream ) == NULL)

 line[strlen(line)-2]='\0';

  fstring.Format("%s",line);

  break;    }   }   }

      char str[21000];

  strcpy(str,fstring); //复制字符串

      const char * split = " ";

      char * p;

  CString word;

   int i=0;

      p = strtok (str,split); //分解字符串为一组字符串

      while(p!=NULL) {

  word.Format("%s",p);

  if(i==0)  mimian= word; //谜面

  if(i==1)  tishi= word; //提示

  if(i==2) midi= word; //谜底

          i++;

          p = strtok(NULL,split);   }

    printf("%s%s%s\n",mimian,"  请猜一",tishi);

i = 0;   string n;

printf("输入谜底:\n");

cin >>n; //取得输入的谜底

string a=midi;

while(1)

{

if(a != n) //猜的谜底不对

{ printf("你猜的谜底不对\n");

i++;

cin >>n; //取得输入的谜底

}

if(a == n) //正确

{

i++;

printf("you guees is right\n");

printf("你现在想退出吗? 输入a结束\n");

scanf("%c",&ch); //取得一字符

if(ch == 'a')

break;

continue;

}

if(i > 10) //机会用完了  

//代码略

【案例分析】

1strtok (str,split)分解一个字符串为一组字符串。str为要分解的字符串,split为分隔符字符串。

2fgets()函数的调用格式如下:fgetsstrnfp),fp是文件指针,str是存储字符串的起始地址,n是一个int类型变量。

 

提示:函数fgets()优于函数gets():读取指定大小的数据,避免gets()函数从stdin接收字符串而不检查它所复制的缓存的容积而导致缓存溢出的问题。

15.3  流类库

案例15-8  移动文件指针现实文件中部写入数据

【案例描述】

在文件尤其是二进制文件中,每一笔数据(记录)存储内容都是一个接着一个连续排列。文件中记录的排列与内存中的数组一样。前面例子对文件的读写都是从头到尾,是否可以像读取数组一样随机读取文件中某个记录?有了文件指针后,就可以直接跳到指针处,读写指针处的记录。本例是移动文件指针到指定位置读取数据,效果如图15-8所示。

 

15-8  移动文件指针现实文件中部写入数据

【实现过程】

5个学生的数据,把这些数据存到外存磁盘文件中;将第3个学生的数据修改后存回磁盘文件中的原有位置;从磁盘文件读入修改后的5个学生的数据并显示出来。代码如下:

#include <fstream>

#include <iostream>

using namespace std;

struct student

{int num;

 char name[20];

 float score;

};

int main( )

{student stud[5]={101,"Li",83,102,"Fun",97.5,104,"Wang",54,

                  106,"Tan",76.5,110,"ling",96};

 fstream iofile("student.dat",ios::in|ios::out|ios::binary);  

//fstream类定义输入输出二进制文件流对象iofile

 if(!iofile)

  {

 cerr<<"open error!"<<endl;

     abort( );

  }

 for(int i=0;i<5;i++) //向磁盘文件输出5个学生的数据

   iofile.write((char *)&stud[i],sizeof(stud[i]));  

 student stud1[5]; //用来存放从磁盘文件读入的数据

 cout<<endl;

 stud[2].num=112; //修改第三个学生(序号为2)的数据

 strcpy(stud[2].name,"Wu");

 stud[2].score=62;

 iofile.seekp(2*sizeof(stud[0]),ios::beg);   //定位于第三个学生数据的开头

 iofile.write((char *)&stud[2],sizeof(stud[2])); //更新第三个学生数据

 iofile.seekg(0,ios::beg);                   //重新定位于文件开头

 for( i=0;i<5;i++)

   {iofile.read((char *)&stud[i],sizeof(stud[i])); //读入5个学生的数据

    cout<<stud[i].num<<" "<<stud[i].name<<" "<<stud[i].score<<endl;

   }

 iofile.close( );

 system("pause");

 return 0;

}

【案例分析】

1)磁盘文件中有一个文件指针,用来指明当前应进行读写的位置。对于二进制文件,允许对指针进行控制,使它按用户的意图移动到所需的位置,以便在该位置上进行读写。

2)文件流提供一些有关文件指针的成员函数。

3)“文件中的位置”和“位移量”已被指定为long型整数,以字节为单位。

 

提示:ios类中定义的3个特定的文件指针:ios::beg文件开头指针,ios::cur当前指针位置,ios::end文件尾指针。

15.4  输出类

案例15-9  输出高精度浮点数(cout高级应用实例)

【案例描述】

在输出数据时,有时希望数据按指定的格式输出。除了可以用控制符来控制输出格式外,还可以通过调用流对象cout,控制输出格式的成员函数来控制输出格式。本例效果如图15-9所示。

 

15-9  输出高精度浮点数(cout高级应用实例)

【实现过程】

通过precision、scientific、fixed、floatfield格式输出实数。代码中用流成员函数setf()和控制符setiosflags()括号中的参数表示格式状态,它是通过格式标志来指定的。代码如下:

#include <iostream>

using namespace std;

int main()

{

double pi=3.1415926535;

int num=200;

cout<<"缺省精度为:"<<cout.precision()<<""<<endl;

cout<<"缺省表达方式:"<<num<<","<<pi<<endl;

//按值大小,相当于printf函数的%g模式,6位有效数字

cout.setf(ios::scientific,ios::floatfield); //设定为科学计数法

cout<<"科学数表达方式:"<<num<<","<<pi<<endl;

cout.setf(ios::fixed,ios::floatfield);     //设为定点格式,取消科学计数方式

cout<<"定点表达方式:"<<num<<","<<pi<<endl;

cout.precision(8); //精度为8位,小数点后8

cout.setf(ios::scientific,ios::floatfield); //设定为科学计数法

cout<<"定点8位表达方式:"<<num<<","<<pi<<endl;

cout.setf(ios::fixed,ios::floatfield);     //设为定点格式,取消科学计数方式

cout<<"科学计数8位表达方式:"<<num<<","<<pi<<endl;

cout.unsetf(ios_base::floatfield); //设置:既非定点,也非科学计数法

cout<<"默认方式下:"<<num<<","<<pi<<endl;

system("pause");

return 0;

}

【案例分析】

— precision用来设置浮点数小数部分,包括小数点的位数,默认为6

— *fixed以小数形式显示浮点数,默认小数部分为6位(包括小数点)。

— scientific以科学计数法形式显示浮点数。cout.setf(ios::scientific,ios::floatfield);,表明浮点数用科学表示法输出。

— ios_base::floatfield设置输出时按浮点格式。

 

提示:对实数的输出,一旦指明按科学表示法输出实数,则输出均按科学表示法输出,直到指明以定点数输出为止。

案例15-10  设置输出域宽

【案例描述】

在输出数据时有两种方法实现数据按指定的格式输出分别是:使用控制符的方法,如*dec、hex由setf函数调用,使用流对象的有关成员函数。本实例讨论第二种方法实际应用,效果如图15-10所示。

 

15-10  设置输出域宽

【实现过程】

程序通过流的格式控制setfunsetfcout、width来设置输出域宽。代码实现如下:

#include <iostream>

using namespace std;

int main( )

{

 int a=21;

 cout.setf(ios::showbase); //显示基数符号(0x0)

 cout<<"dec:"<<a<<endl;          //默认以十进制形式输出a

 cout.unsetf(ios::dec);          //终止十进制的格式设置

 cout.setf(ios::hex);            //设置以十六进制输出的状态

 cout<<"hex:"<<a<<endl;          //以十六进制形式输出a

 cout.unsetf(ios::hex); //终止十六进制的格式设置

 cout.setf(ios::oct);            //设置以八进制输出的状态

 cout<<"oct:"<<a<<endl;          //以八进制形式输出a

//cout.unseft(ios::oct);

 char *pt="China";               //pt指向字符串“China

 cout.width(10);                 //指定域宽为10

 cout<<pt<<endl;                 //输出字符串

 cout.width(10);                 //指定域宽为10

 cout.fill('*'); //指定空白处以‘*’填充

 cout<<pt<<endl;                 //输出字符串

 double pi=22.0/7.0;             //输出pi

 cout.setf(ios::scientific); //指定用科学记数法输出

 cout<<"pi="; //输出“pi=

 cout.width(14); //指定域宽为14

 cout<<pi<<endl; //输出pi

 cout.unsetf(ios::scientific); //终止科学记数法状态

 cout.setf(ios::fixed); //指定用定点格式输出

 cout.width(12); //指定域宽为12

 cout.setf(ios::showpos); //正数输出“+”号

 cout.setf(ios::internal); //数符出现在左侧

 cout.precision(6); //保留6位小数

 cout<<pi<<endl; //输出pi,注意数符“+”的位置

 system("pause");

 return 0;

}

【案例分析】

除了可以用控制符来控制输出格式外,还可以通过调用流对象cout中用于控制输出格式的成员函数来控制输出格式。流成员函数setf()和控制符setiosflags括号中的参数表示格式状态,它是通过格式标志来指定的。设置dec是指用十进制方式进行输出设置,fix是指用普通计数法方式进行输出。还有一种是scientific用科学计数法方式输出。

int width(int nw)设置当前域宽。cout.setf(ios::showbase)显示基数符号(0x0)。long setf( long lFlags )设置流的格式位IFlags,返回以前的格式。long unsetf( long lFlags )清除Imask

15.5  输入类

案例15-11  使用getgetline函数读取C风格字符串

【案例描述】

编程中要读取C风格字符或字符串,这时可以用输入函数cin.get()cin.getline()。当要把输入的一行作为一个字符串送到字符数组中时用cin.getline()。本例效果如图15-11所示。

 

15-11  使用getgetline函数读取C风格字符串

【实现过程】

程序分别演示cin.get()读取C风格字符和cin.getline()函数读取C风格字符串。代码如下:

#include <iostream>

using namespace std;

const int Len=10; //定义常量Len,用于声明数组大小

int main()

{

char sz1[Len],sz2[Len];

cin.get(sz1,Len,'A');//从输入流中读取Len-1个字符,遇到指定的终止字符'A',则提前结束读取

cin.get(sz2,Len); //从输入流中读取Len-1个字符

cout<<"sz1="<<sz1<<endl;

cout<<"sz2="<<sz2<<endl;

cin.sync(); //清空输入缓冲区

cin.getline(sz1,Len,'A');//从输入流中读取一行字符,遇到指定的终止字符'A',则提前结束读取

cin.getline(sz2,Len); //从输入流中读取一行字符

cout<<"sz1="<<sz1<<endl;

cout<<"sz2="<<sz2<<endl;

    system("pause");

return 0;

}

【案例分析】

1)流成员函数get3种形式:无参数的、有1个参数的和有3个参数的。

— 不带参数的get函数其调用形式为cin.get(),从指定的输入流中提取一个字符,函数的返回值就是读入的字符。若遇到输入流中的文件结束符,则函数值返回文件结束标志EOF(End Of File)

— 有一个参数的get函数,其调用形式为cin.get(ch),从输入流中读取一个字符,赋给字符变量ch

— 3个参数的get函数,其调用形式为cin.get(字符数组,字符个数n,终止字符)或cin.get(字符指针,字符个数n,终止字符)。从输入流中读取n-1个字符,赋给指定的字符数组(或字符指针指向的数组),如果在读取n-1个字符之前遇到指定的终止字符,则提前结束读取。

2getline()函数的作用是从输入流中读取一行字符,其用法与带3个参数的get()函数类似。即cin.getline(字符数组(或字符指针),字符个数n,终止标志字符)。

 

提示:cin.getline(数组名,数组空间数),getline()函数的作用是从输入流中读取一行字符,其用法与带三个参数的get函数类似。

15.6  流状态

案例15-12  读取流状态

【案例描述】

每个流都有一个与之相关的状态字,出错和非标准条件下都是通过适当地设置和检测这个状态字来处理的。本实例输入一个浮点数,读取数据流当前的状态,效果如图15-12所示。

【实现过程】

输入一个浮点数,rdstate()函数返回数据流当前的状态进行判断,看是否输入正确或错误。其代码如下:

#include <iostream>

using namespace std;

int main()

{

        float x;

        cin>>x; //输入

        cout<<cin.rdstate()<<endl;   //返回数据流当前的状态位

        if(cin.rdstate() == ios::goodbit) //如果输入正确

        {

                cout<<"输入数据的类型正确,无错误!"<<endl;

        }

        if(cin.rdstate() == ios::failbit) //如果输入失败

        {

                cout<<"输入数据类型错误,非致命错误,可清除输入缓冲区挽回!"<<endl;

        }

        system("pause");

        return 0;

}

【案例分析】

流的这些状态以使用ios类提供的错误侦测函数来读取。流错误侦测函数:

— int rdstate() const返回数据流当前的状态位;

— ios::failbit输入(输出)流出现非致命错误,可挽回;

— ios::badbit输入(输出)流出现致命错误,不可挽回;

— ios::eofbit已经到达文件尾;

— ios::goodbit流状态完全正常。

 

提示:ios::failbitios::badbitios::eofbitios::goodbit均为常量,任何一个都代表了一种流状态,因此称为“输入状态标记位常量”。

案例15-13  设置状态字

【案例描述】

每一个输入/输出流对象都维护一个格式状态字,用其表示流对象当前的格式状态并控制流的格式。C++提供了使用操纵符修改格式状态字来控制流的格式,运用成员函数来控制流的格式的方法。两者实质上都是使用格式状态字。本例效果如图15-13所示。

 

15-13  设置状态字

【实现过程】

定义flags18个状态字,主函数输出18种状态字。其代码如下:

# include <iostream>

using namespace std;

struct fmtflags {

            long flag;

char flagname[12];

} flags[18]={{ios::hex,"hex"},

{ios::dec,"dec"},

{ios::oct,"oct"},

{ios::basefield,"basefield"}, //恢复到十进制输出

{ios::internal,"internal"}, //数字的符号在域中左对齐,数字在域中右对齐,填充字符加到中间

{ios::left,"left"}, //在域中左对齐,填充字符加到右边

{ios::right,"right"}, //在域中右对齐,填充字符加到左边

{ios::adjustfield,"adjustfield"},

                    {ios::fixed,"fixed"},

{ios::scientific,"scientific"},

{ios::basefield,"basefield"},

{ios::showbase,"showbase"}, //设置基指示符输出和数值中的字

{ios::showpoint,"showpoint"},

                          {ios::showpos,"showpos"},//正数前加+号

{ios::skipws,"skipws"},

{ios::uppercase,"uppercase"}, //设置基指示符输出和数值中的字

{ios::boolalpha,"boolalpha"}, //bool型默认输出01

{ios::unitbuf,"unitbuf"}

};

void main() {

   long IFlags;

   IFlags=cout.setf(0,cout.flags());

   cout.setf(ios::hex,ios::basefield); //恢复到十六进制输出

   cout<<"Default flag is:"<<IFlags<<endl; //默认按十进制输出

   for(int i=0;i<18;i++)

   cout<<flags[i].flag<<'\t'<<flags[i].flagname<<endl;

system("pause");

 }

【案例分析】

每一个输入/输出流对象都维护一个格式状态字,用其表示流对象当前的格式状态并控制流的格式,例如整型值的进制基数或浮点数的精度。C++提供了多种格式控制的方法,如使用操作符修改对象的格式状态字来控制流的格式,运用成员函数等。

 

提示:格式标志在类ios中被定义为枚举值,因此在引用这些格式标志时要在前面加上类名ios和域运算符“::”。

15.7  重载>><<

案例15-14  complex

【案例描述】

C++中,标准库本身已经对左移运算符<<和右移运算符>>分别进行了重载,使其能够用于不同数据的输入输出,但是输入输出的对象只能是 C++内置的数据类型(例如 boolintdouble等)和标准库所包含的类类型(例如 stringcomplexofstreamifstream等),效果如图15-14所示。

 

15-14  complex设计

【实现过程】

以全局函数的形式重载>>,使它能够读入两个 double 类型的数据,并分别赋值给复数的实部和虚部。另外,运算符重载函数中用到了 complex类的 private成员变量,必须在 complex类中将该函数声明为友元函数。同理,对输出运算符>>进行重载,让它能够输出复数。代码实现如下:

#include <iostream>

using namespace std;

class complex{

public:

    complex(double real = 0.0, double imag = 0.0): m_real(real), m_imag(imag){ };

public:

    friend complex operator+(const complex & A, const complex & B);

    friend complex operator-(const complex & A, const complex & B);

    friend complex operator*(const complex & A, const complex & B);

    friend complex operator/(const complex & A, const complex & B);

    friend istream & operator>>(istream & in, complex & A);

    friend ostream & operator<<(ostream & out, complex & A);

private:

    double m_real;  //实部

    double m_imag;  //虚部

};

//重载加法运算符

complex operator+(const complex & A, const complex &B){

    complex C;

    C.m_real = A.m_real + B.m_real;

    C.m_imag = A.m_imag + B.m_imag;

    return C;

}

 

//重载减法运算符

complex operator-(const complex & A, const complex &B){

    complex C;

    C.m_real = A.m_real - B.m_real;

    C.m_imag = A.m_imag - B.m_imag;

    return C;

}

//重载乘法运算符

complex operator*(const complex & A, const complex &B){

    complex C;

    C.m_real = A.m_real * B.m_real - A.m_imag * B.m_imag;

    C.m_imag = A.m_imag * B.m_real + A.m_real * B.m_imag;

    return C;

}

//重载除法运算符

complex operator/(const complex & A, const complex & B){

    complex C;

    double square = A.m_real * A.m_real + A.m_imag * A.m_imag;

    C.m_real = (A.m_real * B.m_real + A.m_imag * B.m_imag)/square;

    C.m_imag = (A.m_imag * B.m_real - A.m_real * B.m_imag)/square;

    return C;

}

//重载输入运算符

istream & operator>>(istream & in, complex & A){

    in >> A.m_real >> A.m_imag;

    return in;

}

//重载输出运算符

ostream & operator<<(ostream & out, complex & A){

    out << A.m_real <<" + "<< A.m_imag <<" i ";;

    return out;

}

int main(){

    complex c1, c2, c3;

    cin>>c1>>c2;

 

    c3 = c1 + c2;

    cout<<"c1 + c2 = "<<c3<<endl;

 

    c3 = c1 - c2;

    cout<<"c1 - c2 = "<<c3<<endl;

 

    c3 = c1 * c2;

    cout<<"c1 * c2 = "<<c3<<endl;

 

    c3 = c1 / c2;

    cout<<"c1 / c2 = "<<c3<<endl;

 

    return 0;

}

【案例分析】

1为了能够直接访问 complex类的 private成员变量,同样需要将该函数声明为 complex类的友元函数:friend ostream & operator<<(ostream &out, complex &A);。

2定义了一种新的数据类型,需要用输入输出运算符去处理,那么就必须对它们进行重载。本节以前面的 complex类为例来演示输入输出运算符的重载。

15.8  文件操作

案例15-15  设计一个简单的学生数据库类

【案例描述】

一般情况下读写是顺序进行的,即逐个字节进行读写。但是对于二进制数据文件来说,可以利用前面实例中提到的成员函数移动指针,随机地访问文件中任一位置上的数据,还可以修改文件中的内容。本实例实现学生数据管理方面的功能,即二进制数据文件随机读写,效果如图15-15所示。

 

15-51  设计一个简单的学生数据库类

【实现过程】

5个学生的数据,要求:把它们存到磁盘文件中;将磁盘文件中的第135个学生数据读入程序,并显示出来;将第三个学生的数据修改后存回磁盘文件中的原有位置;从磁盘文件读入修改后的5个学生的数据并显示出来。代码实现如下:

#include <fstream>

# include <iostream>

using namespace std;

struct student

{int num;

 char name[20];

 float score;

};

int main( )

{student stud[5]={1001,"Li",85,1002,"Fun",97.5,1004,"Wang",54,

                  1006,"Tan",76.5,1010,"ling",96};

 fstream iofile("student.dat",ios::in|ios::binary);  

//fstream类定义输入输出二进制文件流对象iofile

 if(!iofile)

  {

 cerr<<"open error!"<<endl;

     abort( );

  }

 for(int i=0;i<5;i++) //向磁盘文件输出5个学生的数据

   iofile.write((char *)&stud[i],sizeof(stud[i]));  

   student stud1[5]; //用来存放从磁盘文件读入的数据

 for(i=0;i<5;i=i+2)

   {iofile.seekg(i*sizeof(stud[i]),ios::beg);//定位于第024学生数据的开头

    iofile.read((char *)&stud1[i/2],sizeof(stud1[0]));

//先后读入3个学生的数据,存放在stud1[0],stud[1]stud[2]

    cout<<stud1[i/2].num<<" "<<stud1[i/2].name<<" "<<stud1[i/2].score<<endl;

//输出stud1[0]stud[1]stud[2]各成员的值

}

 cout<<endl;

 stud[2].num=1012; //修改第三个学生(序号为2)的数据

 strcpy(stud[2].name,"Wu");

 stud[2].score=60;

 iofile.seekp(2*sizeof(stud[0]),ios::beg);   //定位于第三个学生数据的开头

 iofile.write((char *)&stud[2],sizeof(stud[2])); //更新第三个学生数据

 iofile.seekg(0,ios::beg);                   //重新定位于文件开头

 for( i=0;i<5;i++)

   {iofile.read((char *)&stud[i],sizeof(stud[i])); //读入5个学生的数据

    cout<<stud[i].num<<" "<<stud[i].name<<" "<<stud[i].score<<endl;

   }

 iofile.close( );

 return 0;

}

【案例分析】

1)本程序把磁盘文件stud.dat指定为输入输出型的二进制文件。这样不仅可以向文件添加新的数据或读入数据,还可以修改(更新)数据。利用这些功能,可以实现比较复杂的输入输出任务。

2)由于同一个磁盘文件在程序中需要频繁地进行输入和输出,因此可将文件的工作方式指定为输入输出文件,即ios::in|ios::out|ios::binary。编程中,注意要正确计算好每次访问指针的定位的位置,即正确使用seekgseekp函数,正确进行文件中数据的重写(更新)。

 

提示:不能用ifstreamofstream类定义输入输出的二进制文件流对象,而应当用fstream类。

案例15-16  实现程序退出自动保存数据库内容到
磁盘文件

【案例描述】

案例15-1615-1715-18演示的同一个代码文件,代码都是在165.cpp中,都是学生管理的。本例编写一个函数把数据保存在磁盘中,效果如图15-16所示。

 

15-16  实现程序退出自动保存数据库内容到磁盘文件

【实现过程】

定义一个保存函数SaveAsFile(),定义文件流对象文件student.dat,遍历list容器it,取得数据,保存在文件student.dat中。代码实现如下:

void SaveAsFile()  

{  

system("cls");      //清屏

    ofstream ofile("student.dat",ios::out); //定义文件流对象,打开磁盘文件

    if (!ofile)

    {  

        cout<<"打开文件失败!"<<endl; //如果打开失败,返回

        return;  

    }  

    list<Student>::iterator it;

    for(it = lst.begin(); it != lst.end(); ++it) //返回第一个元素、下一位置的指针

{

   //向磁盘文件“student.dat”输出数据

        ofile<<it->name<<"#"<<it->ID<<"#"<<it->grade<<"\n";

}

    cout <<"保存完毕..."<< endl;

    ofile.close(); //关闭磁盘文件

    return ;  

}

【案例分析】

如果文件的每一个字节中均以ASCII代码形式存放数据,即一个字节存放一个字符,这个文件就是ASCII文件(或称字符文件)。程序可以从ASCII文件中读入若干个字符,也可以向其输出一些字符。对ASCII文件的读写操作可以用以下两种方法:

1)用流插入运算符“<<”和流提取运算符“>>”输入输出标准类型的数据;

2)用文件流的putgetgeiline等成员函数进行字符的输入输出。

 

提示:在向磁盘文件输出一个数据后,要输出一个(或几个)空格或换行符,以作为数据间的分隔;否则以后从磁盘文件读数据时,10个整型的数字连成一片无法区分。

案例15-17  实现程序启动时自动读取数据库

【案例描述】

代码见165.cpp,文件存入磁盘中,程序启动运行时需要从磁盘读取数据到内存中,如存入外存通讯录,需要查询的时候调入数据。本例作用是读取外存数据,复制到定义变量中,效果如图15-17所示。

 

15-17  实现程序启动时自动读取数据库

【实现过程】

定义了3个函数。inputData(),从文件student.dat中读取数据,利用for循环,分别读取姓名、学号和分数,复制到类Student的姓名、学号和分数,在容器后端增加元素。代码实现如下:

void inputData() //从文件中读取数据

{

cout<<"正在从文件读入数据..."<<endl;

ifstream ifile("student.dat");  

    if(!ifile) //如果打开失败,返回

    {  

        cout<<"student.dat cannot be opened!"<<endl;  

        return;  

    }

    char ch;  

    int i;

    for (i=0;i<MAX_STU;i++) //读取数目  

    {  

string s_name,s_id,s_grade;

        if(!ifile.get(ch))  

        {

            cout<<"文件已经读完!"<<endl;  

            return;  

        }  

        while (ch!='#') //读取姓名

        {  

            if (ch==' ') //跳过空格  

            {  

                ifile.get(ch);  

                continue;  

            }

            s_name+=ch;

            ifile.get(ch);

        }  

        ifile.get(ch);  

        while (ch!='#') //读取学号

        {  

            if (ch==' ')  

            {  

                ifile.get(ch); //跳过空格  

                continue;  

            }  

            s_id+=ch;  

            ifile.get(ch);  

        }  

        ifile.get(ch);  

        while(ch!='\n') //读取分数  

        {  

            if (ch==' ')  

            {  

                ifile.get(ch);  

                continue;  

            }  

            s_grade+=ch;  

if(!ifile.get(ch))  

{

cout<<"文件已经读完!"<<endl;  

return;  

}   

        }

Student temp;

strcpy(temp.name,s_name.c_str()); //复制姓名字符串

strcpy(temp.ID,s_id.c_str()); //复制学号字符串

temp.grade=atoi(s_grade.c_str()); //复制分数字符串

lst.push_back(temp); //在容器后端增加元素

    }  

    ifile.close(); //关闭磁盘文件

system("cls"); //清屏

}

【案例分析】

1)文件的输入是指从文件中读数数据到内存中,文本文件输入常用的是提取操作符>>,在文件输入中经常检查文件是否到达尾部,输入流的成员函数eof()用来侦测是否到达文件结尾,若读取到文件结尾时,返回true

2)输入二进制文件使用成员函数read(),每次读取固定长度的数据,同样用eof()判断是否到达文件尾。

 

提示:文件输出一般要经过下列3个步骤:

1、创建输入文件流(对象),以输入方式打开的文件连接到文件流上,此步需要对文件是否打开成功进行判断,如果文件打开错误,则退出;

2、从输入文件流中读取内容,此步需要对读文件是否成功进行判断,如果读入不成功或位置到文件尾,则读入结束;

3、关闭文件(文件流对象消失时也会自动关闭文件)。

案例15-18  开发一个完整的学生数据管理系统V1.0

【案例描述】

代码见165.cpp,这是个综合的实例,目的是加深对文件读写理解和综合应用,编写小型信息处理软件,就是综合利用C++先进特性,本实例是个完整的学生管理方面的软件,实现学生记录查询、排序、插入、删除功能,效果如图15-18所示。

 

15-18  开发一个完整的学生数据管理系统V1.0

【实现过程】

如果存在有学生记录(包括学号、姓名、成绩)的文件student.dat,那么程序启动后就会自动读取数据;否则要手工输入学生数据。本实例编程实现查询、排序、插入、删除诸多功能。代码实现如下:

#define MAX_STU 100 //读入学生的最大数目

class Student

{

public:

    char * name; //定义字符串指针

char *ID;

    int grade;

    Student() //构造函数

    {

        name = new char[strlen("Anonymous-----") + 1];//分配内存

   ID = new char[strlen("NoIdInput------") + 1];

        grade = 0;

    }

    //代码略

    //显示学生的信息

    void print()

    {

        cout << name <<" " <<ID << " " << grade << endl;

    }

    //构造函数

    ~Student()

    {

        delete []name; //析构函数

  delete []ID;

    }

};

list<Student> lst; //学生链表,用于存放学生数据

void print(list<Student> lst, char * name) //输入链表中所有的学生

{

    list<Student>::iterator it; //建立容器

    cout << name << ":" << endl;

    for(it = lst.begin(); it != lst.end(); ++it) //返回第一个元素、最后一个元素的下一位置的指针

        it->print(); // 显示学生的信息

    cout << endl;

}

void screenA() //显示屏幕操作A

{//代码略}

void screenB() //显示屏幕查询

{//代码略}

void searchByID() //按学号查找

{

cout<<"-------请输入学号ID"<<endl;

char tID[12];

memset(tID,0,12); //字符串清零

cin>>tID;

bool flag=false;

list<Student>::iterator it; //建立容器

    for(it = lst.begin(); it != lst.end(); ++it) //返回第一个元素、最后一个元素的下一位置的指针

{

if (strcmp(it->ID,tID)==0)

{

cout<<"----查找到,该学生信息如下:-----"<<endl;

it->print(); //显示学生的信息

flag=true;

break;

}

}

    if (flag==false)

    { cout<<"未找到!"<<endl;    }   }

void searchByName() //按名字查找

{

}

void searchByGrade() //按分数查找

{}

void sortByGrade() //按分数进行排序,由高到低

{

system("cls");

cout<<"-------按分数排序完毕,结果如下:"<<endl;

lst.sort( greater<Student>() );

list<Student>::iterator it;

    for(it = lst.begin(); it != lst.end(); ++it)

{

   it->print(); //显示学生的信息

}

}

void insertStudent() //插入一个学生

{//代码略}

void deleteStudent() //按要求删除一个学生

{

system("cls");

    cout<<"-------请输入要删除学生的学号ID"<<endl;

char tID[12];

memset(tID,0,12);

cin>>tID;

bool flag=false;

list<Student>::iterator it;

    for(it = lst.begin(); it != lst.end(); ++it)

{

if (strcmp(it->ID,tID)==0)

{

cout<<"----查找到,该学生信息如下:-----"<<endl;

it->print();

lst.erase(it);

cout<<"删除完毕!"<<endl;

flag=true;

break;

}

}

    if (flag==false)

    {

cout<<"未找到!"<<endl;

    }

}

void inputData() //从文件中读取数据

{  //代码略}

void SaveAsFile()  

{  //代码略}

int main()

{

inputData(); //从文件中读入数据

char ch;

screenA();

while (cin>>ch)

{

switch(ch)

//代码略

     return 0;

}}

【案例分析】

进入系统后,通过选择来确定要做哪一个操作。

— 若选1,按学号查询用二分法实现;按姓名查询用顺序法实现;按成绩查询实现查询成绩小于m分的学生;找到该学生就将学生的记录输出到屏幕,若查无此人,输出相关查无此人的信息。

— 若选2,即按成绩从大到小排序,姓名、学号顺序也随之调整。

— 若选3,将一个新学生记录按学号顺序插入,并把结果保存到文件student.dat中。

— 若选4,删除指定学生的记录,并把结果保存到文件student.dat中。

以上各个功能均编写成了子函数,由主函数调用实现。

 

提示:在此基础上可以编写一个按名字查找和按分数查找的函数。

案例15-19  开发一个完整的学生数据管理系统V2.0

【案例描述】

继续上面实例的话题,代码见165.cpp,编写一个按名字查找和按分数查找的函数。本实例是个完整的学生管理系统,本实例实现按名字查找学生信息的功能,效果如图15-19所示。

 

15-19  开发一个完整的学生数据管理系统V2.0

【实现过程】

输入姓名,取得输入的姓名,如果姓名相同,那么就显示学生的信息。代码实现如下:

void searchByName() //按名字查找

{

cout<<"-------请输入姓名:"<<endl;

char tname[12];

memset(tname,0,12); //字符串清零

cin>>tname;

bool flag=false;

list<Student>::iterator it; //建立容器

    for(it = lst.begin(); it != lst.end(); ++it) //返回第一个元素、最后一个元素的下一位置的指针

{

if (strcmp(it->name,tname)==0)

{

cout<<"----查找到,该学生信息如下:-----"<<endl;

it->print(); // 显示学生的信息

flag=true;

break;

}

}

    if (flag==false)

    {

cout<<"未找到!"<<endl;

    }

}

【案例分析】

整个函数的流程是:首先建立容器it,然后通过for循环返回第一个元素、最后一个元素的下一位置的指针,其中需要利用比较函数strcmp()来比较姓名。

 

提示:按分数查找函数和按名字查找函数的原理是一样的。

案例15-20  网络文件传输(TCP+算法)

【案例描述】

日常生活中,需常需要互联网来实时传输文件。实时传输文件,一般通过TCP/UDP协议进行传输,本实例采用TCP协议加算法,实现简单文件传输功能。本例效果如图15-20所示。

 

15-20  网络文件传输(TCP+算法)

【实现过程】

函数GetFile()读要发送文件的内容,以读的方式打开文件,读文件的内容,把文件的名称和长度ffnamelen赋值给DataPacket;在主函数中实现套接口初始化,创建套接口,读取输入的文件名调用函数GetFile(),调用函数TCPSendPacket()发送文件内容。客户端client代码实现如下:

DWORD GetFile(char *fname)

{

int    i;

FILE   *fp;

int    Filesize;

int    count;

int    total = 0;

char   buffer[100];

char   Senddata[MAX_FILESIZE]; //需发送文件内容

 

fp = fopen(fname,"r"); //以读的方式打开文件

if(fp == NULL)

{

printf("cannot open file\n");

return (0);

}

i = 0;

Filesize =0;

memset(Senddata,0,MAX_FILESIZE); //字符串清零

while(!feof(fp))

{

count = fread(buffer,sizeof(char),100,fp); //读文件内容

if(ferror(fp))

{

printf("read file error");

break;

}

Filesize += count;

 

if(Filesize > MAX_FILESIZE) //打开文件长度大于文件最大值

{

printf("your file is too big\n");

fclose(fp);

return (0);

}

memcpy(&Senddata[i],buffer,count); //拷贝count个字节

i += count;

}

fclose(fp); //关闭文件

Senddata[i] = '\0'; //加上结尾符

strcpy(DataPacket.ffname,fname); //复制

memcpy(DataPacket.ffdata,Senddata,Filesize);//拷贝Filesize个字节

DataPacket.len = Filesize;

printf("%s %d\n",DataPacket.ffname,DataPacket.len);

return (1);

}

DWORD TCPSendPacket(struct Filedata Packet) //发送文件内容

{

int length;

length = send(sock,(char*)&Packet,sizeof(DataPacket),0);  //发送数据

if(length <= 0)

{

printf("send Filedata error\n");

closesocket(sock); //关闭套接口

WSACleanup(); //终止Winsock 2 DLL (Ws2_32.dll)的使用

return (-1);

}

return (1);

}

int main()

{

char sendfilename[30];

StartSock(); //套接口初始化

CallServer(); //创建套接口

printf("connect ok!\n");

while(1)

{

printf("Input your filename to send:\n");

scanf("%s", sendfilename); //读取输入文件名

if(strcmp(sendfilename, "exit") == 0) //字符串比较

{

break;

}

if(GetFile(sendfilename) == 0) //读要发送文件内容

continue;

TCPSendPacket(DataPacket); //发送文件内容

}

closesocket(sock); //关闭套接口

return 0;   }

【案例分析】

1socketSend.Send(&FileLength, 4),把要发送的文件的长度传送给对方;socketSend.Send(fn,40),发送要传送的文件的文件名;data = new char[FileLength],分配一块和要传输的文件一样大小的内存空间;file.ReadHuge(data, FileLength),把文件中所有的数据一次性读入;datasocketSend.Send(data, FileLength),把data中的数据都发送出去。

2)函数ConnectProcess(),为处理接收文件的内容。socketReceive.Receive(&FileLength, 4),获取要接收的文件的长度;socketReceive.Receive(fn,40),获取要接收的文件的文件名;recv(),获取要接收的文件内容;WriteFile(),写文件内容。

案例15-21  断点续传

【案例描述】

在日常工作中经常要下载文件,会用到互联网流行的下载工具如具有断点续传功能的迅雷下载软件。是本实例用HTTP断点续传源码,用WinInet API编写的下载小程序。本例效果如图15-21所示。

【实现过程】

定义URL的结构crackedURL并赋初始值,启用HTTP协议建立HTTP连接,创建一个URL请求,读取文件大小,开辟缓冲区,然后读取文件内容。部分代码实现如下:

#define URL_STRING_TEST  http://eng.edu-edu.com.cn/audio/Onelove.mp3//下载文件地址

void main()

{

URL_COMPONENTS crackedURL; //下载文件地址

//这里是下载缓冲区大小 1KB大小缓冲写入一次

    TCHAR  szBuffer[1024];    

//代码略

FILE* file = fopen("Onelove.mp3", "wb"); //二进制写方式打开

    HINTERNET hInt,hConn,hReq;

//启用HTTP协议

hInt = InternetOpen("Microsoft Internet Explorer", INTERNET_OPEN_TYPE_DIRECT, NULL, NULL, 0);

//建立HTTP连接

    hConn = InternetConnect(hInt,crackedURL.lpszHostName,crackedURL.nPort,NULL, NULL,INTERNET_SERVICE_HTTP,0,0);

//创建一个URL请求

hReq = HttpOpenRequest(hConn, "GET", crackedURL.lpszUrlPath, NULL, "", NULL, 0, 0);

char buff[64];

DWORD dwContLen = 0;

DWORD  dwLen;

BOOL   bResult = FALSE;

    DWORD nBytesGet = 0;

BOOL   bEnd = FALSE;

DWORD dwRetCode = 0;

DWORD dwSizeOfRq = sizeof(DWORD);

dwSizeOfRq = sizeof(buff);

  if (HttpSendRequest(hReq,NULL,0,NULL,0) //HTTP 服务器发送指定的请求

&&HttpQueryInfo(hReq, HTTP_QUERY_STATUS_CODE|HTTP_QUERY_FLAG_NUMBER, &dwRetCode, &dwSizeOfRq, NULL)

&& dwRetCode < 400)

{ bResult = TRUE;//true;  

}

//查询文件大小

if (HttpQueryInfo(hReq, HTTP_QUERY_CONTENT_LENGTH, &buff, &dwSizeOfRq, NULL))

dwContLen = atol(buff);

  //这里更改发送的文件请求方法,请求的路径,版本

string strHeader; //设置接受数据类型

     strHeader += "Accept: */*\r\n";

   strHeader += "Pragma: no-cache\r\n"; //设置禁止用缓存和缓存控制

   strHeader += "Cache-Control: no-cache\r\n";

   strHeader += "Connection:Keep-Alive\r\n"; //设置连接

      string strRange = "Range: bytes=";     //设置请求文件的大小

   strRange+=IntToString(dwContLen/2);

   strRange+="-";

   strRange+=IntToString(dwContLen);

   strRange+"\r\n";

   strHeader+=strRange;

//打开了想要下载的文件

      hReq = HttpOpenRequest(hConn, "GET", crackedURL.lpszUrlPath, NULL, "", NULL, 0, 0);

       //添加一个或多个 HTTP请求报头到 HTTP请求句柄

bool bTrue = HttpAddRequestHeaders(hReq,strHeader.c_str(), -1,HTTP_ADDREQ_FLAG_ ADD|HTTP_ADDREQ_FLAG_REPLACE  );

//HTTP服务器发送指定的请求

    if (HttpSendRequest(hReq,NULL,0,NULL,0)

&& HttpQueryInfo(hReq, HTTP_QUERY_STATUS_CODE|HTTP_QUERY_FLAG_NUMBER, &dwRetCode, &dwSizeOfRq, NULL)

 && dwRetCode < 400)

  {

bResult = TRUE;//true;

}

while(TRUE) {

if (bResult) //开始读取文件

//代码略

fclose(file);

cout<<"Onelove.mp3 下载成功"<<endl; system("pause");

}

【案例分析】

HTTP超文本传输协议,工作在TCP/IP协议体系中的TCP协议的上层。主要特点:

1)支持客户/服务器模式。

2)简单快速:客户向服务器请求服务时,只需传送请求方法和路径。

3)灵活:HTTP允许传输任意类型的数据对象。

4)无连接:限制每次连接只处理一个请求。

5)无状态:HTTP协议是无状态协议。

wininet是微软提供的用来编写网络客户端程序的类库,它封装了winsock类的功能。编写的程序运行的时候,只要和服务器连接建立,就等于打开了想要的文件。HttpOpenRequest()HttpSenRequest()是在一起工作的,实现打开文件的功能。HttpOpenRequest()创建一个请求句柄并且把参数存储在句柄中,HttpOpenRequest()把请求参数送到HTTP服务器。

 

提示:WinInet(“Windows Internet”)API,帮助程序员使用3个常见的Internet协议,这3个协议是万维网的超文本传输协议(HTTPHypertext Transfer Protocol),文件传输协议(FTPFile Transfer Protocol)和称为Gopher的文件传输协议。WinInet函数的语法与常用的Win32 API函数的语法类似,这使得使用这些协议就像使用本地硬盘上的文件一样容易。

 

15.9  字符串流

案例15-22  输入输出流之字符串流

【案例描述】

字符串流以内存中用户定义的字符数组(字符串)作为输入输出对象。字符串流也有缓冲区,当缓冲区满了或遇到换行符,流缓冲的数据一起存入字符数组本例效果如图15-22所示。

【实现过程】

建立输入字符串流对象strin,建立输出字符串流对象strout,利用一个循环输入字符串,然后输出字符串的值。代码实现如下:

#include <strstream>  

#include <iostream>  

using namespace std;  

int main()  

{  

    char cStr[50] = "12 34 65 -23 -32 33 61 99 321 32";  

    int nArray[10];  

    cout << "Old cStr: " << cStr << endl;  

    // 以字符串cStr作为输入流,建立strincStr的关联,缓冲区大小为字符串cStr的大小  

    istrstream strin(cStr, sizeof(cStr));  

    int i, temp;  

    for (i = 0; i < 10; i++)  

        strin >> nArray[i];  

     // 输出读入的数组  

    cout << "nArray: ";  

    for (i = 0; i < 10; i++)  

        cout << nArray[i] << " ";  

    cout << endl;  

    for ( i = 0; i < 5; i++)  // 数组旋转  

    {  

        temp = nArray[i];  

        nArray[i] = nArray[9 - i];;  

        nArray[9-i] = temp;  

    }  

    ostrstream strout(cStr, sizeof(cStr));  

    for (i = 0; i < 10; i++)  

        strout << nArray[i] << " ";  

    strout << endl;             // 加入'\0'  

    cout << "New cStr: " << cStr << endl; // 输出最新的cStr  

    return 0;  

}  

【案例分析】

输入输出流之字符串流头文件<strstream>。函数具体介绍

1建立输出字符串流对象

ostrestream的构造函数:

ostrestream::ostrstream(char *buffer, int n, int mode = ios::out);buffer指字符串数组首元素的指针,n是流缓冲区的大小。iost::out是默认值建立输出字符串流对象与字符数组的关联:ostrstream strout(cStr1, 20)。

2建立输入字符串流对象:istrstream的构造函数:istrstream::istrstream(char *buffer)

istrstream::istrstream(char *buffer, int n)。其中buffer是指向字符数组首元素的指针,n是缓冲区的大小建立输入字符串流对象与字符数组的关联。istrstream strin(cStr2);cStr2字符数组中的全部数组作为输入字符串流的内容;istrstream strin(cStr2);cStr2字符数组中的全部数组作为输入字符串流的内容

3建立输入输出字符串流对象:strstream的构造函数strstream::strstream(char *buffer, int n, int mode)建立输入输出流对象鱼字符串流流的关联strstream strio(cStr3, sizeof(cStr3), ios::in|ios::out);以字符数组cStr3为输入输出对象,缓冲区大小与数组cStr3相同

15.10  本章练习

 

原创粉丝点击