Lecture 22 输入/输出(面向对象途径)
输入/输出(I/O)概述
输入/输出(简称I/O)是程序的一个重要组成部分:
-
程序运行所需要的数据往往要从外设(如:键盘、文件等)得到
-
程序的运行结果通常也要输出到外设(如:显示器、打印机、文件等)中去。
在C++中,输入/输出操作不是语言定义的成分,而是由具体的实现作为标准库的功能来提供。
在C++中,输入/输出是一种基于字节流的操作:
-
在进行输入操作时,可把输入的数据看成逐个字节地从外设流入到计算机内存。
-
在进行输出操作时,则把输出的数据看成逐个字节地从内存流出到外设。
在C++的标准库中,提供了:
-
基于字节的I/O操作
-
基于C++基本数据类型数据的I/O操作,在这些操作的内部实现了基本数据类型与字节流之间的转换。
另外,还可以对操作符“<<”和“>>”进行重载,以实现对自定义类型的数据(对象)进行输入/输出操作。
C++输入输出的实现途径
过程式——通过从C语言保留下来的函数库中的输入/输出函数来实现。
-
printf
-
scanf
-
…
面向对象——通过C++的I/O类库中的类/对象来实现。
-
ostream
、cout
,<<
-
istream
、cin
,>>
-
…
printf
、scanf
的缺陷
printf
和scanf
是两个带可变参数的函数。- 不是强类型:编译时刻不进行参数类型检查,会导致与类型相关的运行错误!(类型不安全)
例如,
int i;
double j;
scanf("i=%d,j=%lf",&i,&j); //i=12,j=34.5
printf("i=%d, j=%f\n",i,j); //i=12, j=34.5
当格式串描述与数据不一致时会导致运行时刻的错误:
scanf("i=%d,j=%d",&i,&j); //类型不一致
printf("i=%d, j=%d\n",i,j); //类型不一致
scanf("i=%d",&i,&j); //个数不一致
printf("i=%d, j=%f\n",i); //个数不一致
cout
、cin
的优势
不需要单独指定数据的类型和个数,编译时刻根据数据本身来决定操作的类型和个数,这样可避免与类型和个数相关的错误!
int i;
double j;
cout << "i=";
cin >> i;
cout << "j=";
cin >> j;
cout << "i=" << i << ",j=" << j << endl;
I/O的分类
面向控制台的I/O
-
从标准输入设备(如:键盘)获得数据
-
把程序结果从标准输出设备(如:显示器)输出
面向文件的I/O
-
从外存文件获得数据
-
把程序结果保存到外存文件中
面向字符串变量的I/O
-
从程序中的字符串变量中获得数据
-
把程序结果保存到字符串变量中
-
现在已被STL中的string替代
C++的I/O类库中基本的类
在进行输入/输出时,首先创建某个I/O类的对象,然后,可以调用该对象类的成员函数进行基于字节流的输入/输出操作。
istream
类和ostream
以及它们的派生类分别重载了操作符“>>”(抽取)和“<<”(插入),用它们可以进行基本类型数据的输入/输出操作。例如:
- 输入
istream in(...);
in >> x; //x是一个变量
in >> y; //y是一个变量
//或
in >> x >> y;
- 输出
ostream out(...);
out << e1; //e1是一个表达式
out << e2; //e2是一个表达式
//或
out << e1 << e2;
面向控制台的I/O
在I/O类库中预定义了四个I/O对象,可以直接利用这些对象进行控制台的输入/输出操作:
-
cin
(istream
类的对象):对应着计算机系统的标准输入设备。(通常为键盘) -
cout
(ostream
类的对象):对应着计算机系统的标准输出设备。(通常为显示器) -
cerr
和clog
(ostream
类的对象):对应着计算机系统用于输出特殊信息(如程序错误信息)的设备。(通常也对应着显示器,但不受输出重定向的影响)。cerr
为不带缓冲的,clog
为带缓冲的。
在进行控制台输入/输出时,程序中需要有下面的包含命令:
#include <iostream>
控制台输出操作
基于插入操作符(<<)的基本数据类型数据的输出
#include <iostream>
using namespace std;
......
int x;
float f;
char ch;
int *p=&x;
......
cout << x ; //输出x的值。
cout << f; //输出f的值。
cout << ch; //输出ch的值。
cout << "hello"; //输出字符串"hello"。
cout << p; //输出变量p的值,即,变量x的地址。
或
cout << x << f << ch << "hello" << p;
输出格式控制
为了对输出格式进行控制,可以通过输出一些操纵符(manipulator)来实现,例如:
#include <iostream>
#include <iomanip> //操纵符声明的头文件。
using namespace std;
.....
int x=10;
cout << hex << x << endl; //以十六进制输出x的值,然后换行。
常用输出操纵符
操纵符 | 含义 |
---|---|
endl | 输出换行符,并执行flush 操作 |
flush | 使输出缓存中的内容立即输出 |
dec | 十进制输出 |
oct | 八进制输出 |
hex | 十六进制输出 |
setprecision(int n) | 设置浮点数的精度(由输出格式决定是有效数字的个数还是小数点后数字的位数) |
setiosflags(long flags) / resetiosflags(long flags) | 设置/重置输出格式,flags的取值可以是:ios::scientific (以指数形式显示浮点数),ios::fixed (以小数形式显示浮点数),等等。 |
除了通过插入操作符进行基本数据类型数据的输出外,也可以用ostream
类的成员函数来进行基于字节的输出操作。
例如:
//输出一个字节。
ostream& ostream::put(char ch);
cout.put('A');
//输出p所指向的内存空间中count个字节。
ostream& ostream::write(const char *p,int count);
char info[100];
int n;
......
cout.write(info,n);
控制台输入操作
基于抽取操作符(>>)的基本数据类型数据的输入
#include <iostream>
using namespace std;
......
int x;
double y;
char str[10];
cin >> x; cin >> y; cin >> str;
//或者
cin >> x >> y >> str;
在输入的各个数据之间用空白符(空格
、\t
、\n
)分开:
-
输入前先跳过空白符
-
输入过程中碰到空白符或当前数据类型不允许的字符结束
可以通过一些操纵符来控制输入的行为,例如:
char str[10];
cin >> setw(10) >> str; //把输入的字符串和一个'\0'放入str中,最多输入9个字符。
除了抽取操作符“>>”外,还可以使用istream
类的成员函数来进行基于字节的输入操作。
例如:
//输入一个字节。
istream::get(char &ch);
//输入count个字节至p所指向的内存空间中。
istream::read(char *p,int count);
//输入一个字符串。输入过程直到输入了count-1个字符或
//遇到delim指定的字符为止,并自动加上一个'\0'字符。
istream::getline(char *p, int count, char delim=’\n’);
操作符“>>”和“<<”的重载
标准库中重载的操作符“>>”和“<<”只能对基本数据类型的数据进行输入/输出。
可以针对自定义的类进一步重载操作符“>>”和“<<”,从而实现能用它们对某个类的对象进行输入/输出操作。
例:插入操作符“<<”的重载
class A
{ int x,y;
public:
......
friend ostream& operator << (ostream& out, const A &a);
};
ostream& operator << (ostream& out, const A &a)
{ out << a.x << ',' << a.y;
return out;
}
.....
A a1,a2;
cout << a1 << endl << a2 << endl;
派生类输出操作符“<<”的实现
class A
{ int x,y;
public:
......
friend ostream& operator << (ostream& out, const A &a);
};
ostream& operator << (ostream& out, const A& a)
{ out << a.x << ',' << a.y;
return out;
}
class B: public A
{ double z;
public:
......
};
......
A a;
B b;
cout << a << endl
cout << b << endl; //只输出了b.x和b.y
class A
{ int x,y;
public:
......
friend ostream& operator << (ostream& out, const A &a);
};
ostream& operator << (ostream& out, const A& a)
{ out << a.x << ',' << a.y; return out;
}
class B: public A
{ double z;
public:
......
friend ostream& operator << (ostream&,const B&);
};
ostream& operator << (ostream& out, const B& b)
{ out << (A&)b << ',' << b.z; return out;
}
......
B b;
cout << b << endl; //OK
A *p=new B;
cout << *p << endl; //只输出了p->x和p->y
class A
{ int x,y;
public:
......
virtual void display(ostream& out) const
{ out << x << ',' << y ; }
};
ostream& operator << (ostream& out, const A& a)
{ a.display(out); //动态绑定到A类或B类对象的display。
return out;
}
class B: public A
{ double z;
public:
......
void display(ostream& out) const
{ A::display(out); out << ',' << z ; }
};
A a; B b;
cout << a << endl << b << endl; //OK
面向文件的I/O
需求:
-
程序运行结果有时需要永久性地保存起来,以供其它程序或本程序下一次运行时使用。
-
程序运行所需要的数据也常常要从其它程序或本程序上一次运行所保存的数据中获得。
用于永久性保存数据的设备称为外部存储器(简称:外存),如:
- 磁盘、磁带、光盘等。
在外存中保存数据的方式通常有两种:
-
文件
-
数据库
文件的基本概念
在C++中,把文件看成是由一系列字节所构成的字节串,对文件中数据的操作(输入/输出)通常是逐个字节顺序进行,因此称为流式文件。
每个打开的文件都有一个内部(隐藏)的位置指针,它指出文件的当前读写位置。
进行读/写操作时,每读入/写出一个字节,文件位置指针会自动往后移动一个字节的位置。
文件数据的存储方式
文本方式(text)
-
只包含可显示的字符和有限的几个控制字符(如:‘\r’、‘\n’、‘\t’等)的编码。
-
例如,以文本方式存储整数1234567 :
- 把字符1、2、3、4、5、6、7的ASCII码(共7个字节)依次写入文件。
-
一般用于存储具有“行”结构的文本数据。(可用记事本等软件打开察看)
二进制方式(binary)
-
包含任意的没有显式含义的纯二进制字节。
-
例如,以二进制方式存储整数1234567 :
- 把1234567的int型机内表示
00 12 D6 87
(共4个字节)依次写入文件。
- 把1234567的int型机内表示
-
一般用于存储任意结构的数据。
文件的读写过程
在外存(如磁盘)中,每个文件都有一个名字(文件名)(操作系统一般采用树型的目录结构来管理外存中的文件)。
对文件数据进行读写的过程:
-
打开文件:把程序内部的一个表示文件的变量/对象与外部的一个具体文件关联起来,并创建内存缓冲区。
-
文件读/写:存取文件中的内容。
-
关闭文件:把暂存在内存缓冲区中的内容写入到文件中,并归还打开文件时申请的内存资源(包括内存缓冲区)。
在利用I/O类库中的类进行文件的输入/输出时,程序中需要包含下面的头文件:
#include <iostream>
#include <fstream>
文件输出操作
打开文件:创建一个ofstream
类的对象,并建立与外部文件之间的联系。
-
直接方式:
ofstream out_file(<文件名> [,<打开方式>]); //例如: ofstream out_file("d:\\myfile.txt",ios::out);
-
间接方式:
ofstream out_file; out_file.open(<文件名> [,<打开方式>]); //例如: out_file.open("d:\\myfile.txt",ios::out);
打开方式
-
ios::out
- 打开一个外部文件用于写操作。
- 如果外部文件已存在,则首先把它的内容清除;否则,先创建该外部文件。
ios::out
是默认打开方式。
-
ios::app
- 打开一个外部文件用于添加(文件位置指针在末尾)操作。
- 如果外部文件不存在,则先创建该外部文件。
-
ios::out
或ios::app
分别与ios::binary
的按位或(|)ios::out | ios::binary
或ios::app | ios::binary
- 表示按二进制方式打开文件。(默认的是文本方式)
- 对以文本方式打开的文件,当输出的字符为’\n’时,在某些平台上(如:DOS和Windows平台)将自动转换成’\r’和’\n’两个字符写入外部文件。
判断打开操作是否成功
打开文件时,必须要对文件打开操作的成功与否进行判断。判断文件是否成功打开可以采用以下方式:
if (!out_file.is_open()) //或:out_file.fail()
//或:!out_file
{ ...... //失败处理
}
输出数据
文件成功打开后,可以使用插入操作符“<<”或ofstream
类的一些成员函数来进行文件数据的输出操作,例如:
int x=12;
double y=12.3;
......
//以文本方式输出数据
ofstream out_file("d:\\myfile.txt",ios::out);
if (!out_file) exit(-1);
out_file << x << ' ' << y << endl; //12 12.3
或:
//以二进制方式输出数据
ofstream out_file("d:\\myfile.dat",ios::out|ios::binary);
if (!out_file) exit(-1);
out_file.write((char *)&x,sizeof(x)); //4个字节
out_file.write((char *)&y,sizeof(y)); //8个字节
struct Student
{ int no;
char name[10];
int scores[5];
} s1={161220042,"张三",{90,95,85,75,95}};
//以文本方式输出数据
ofstream out_file("d:\\students.txt",ios::out);
if (!out_file) exit(-1);
out_file << s1.no << ' ' << s1.name;
for (int i=0; i<5; i++)
out_file << ' '<< s1.scores[i];
out_file << endl;
//以二进制方式输出数据
ofstream out_file("d:\\students.dat",ios::out|ios::binary);
out_file.write((char *)&s1,sizeof(s1));
文件输出操作结束时,要使用ofstream的成员函数close关闭文件:
out_file.close();
关闭文件的目的:
- 把文件内存缓冲区的内容写到磁盘文件中!
程序正常结束时,系统也会自动关闭打开的文件。
文件输入操作
打开文件:创建一个ifstream类的对象,并与外部文件建立联系。例如:
ifstream in_file(<文件名> [,<打开方式>]);
//或
ifstream in_file;
in_file.open(<文件名> [,<打开方式>]);
打开方式
-
ios::in
,打开一个外部文件用于读操作。(默认) -
也可以把
ios::in
与ios::binary
通过按位或操作(|)实现二进制打开方式。默认为文本方式。 -
对以文本方式打开的文件,当文件中的字符为连续的’\r’和’\n’时,在某些平台上(如:DOS和Windows平台)读入时将自动转换成一个字符’\n’输入。
-
打开文件时要判断打开是否成功。
- 判断方式与文件输出打开操作的判断一样。
文件成功打开后,可以使用抽取操作符“>>”或ifstream类的一些成员函数来进行文件输入操作,例如:
int x;
double y;
......
//以文本方式输入数据
ifstream in_file("D:\\myfile.txt",ios::in);
if (!in_file) exit(-1);
in_file >> x >> y;
//以二进制方式输入数据
ifstream in_file("D:\\myfile.dat",ios::in|ios::binary);
if (!in_file) exit(-1);
in_file.read((char *)&x,sizeof(x));
in_file.read((char *)&y,sizeof(y));
注意:从文件输入必须要知道文件中数据的存储方式和格式!
struct Student
{ int no;
char name[10];
int scores[5];
} s1;
//以文本方式输入数据
ifstream in_file("d:\\students.txt",ios::in);
if (!in_file) exit(-1);
in_file >> s1.no >> s1.name;
for (int i=0; i<5; i++)
in_file >> s1.scores[i];
//以二进制方式输入数据
ifstream in_file("d:\\students.dat",ios::in|ios::binary);
in_file.read((char *)&s1,sizeof(s1));
文件输入操作结束后,要使用ifstream
的一个成员函数close关闭文件:
in_file.close();
读取数据过程中有时需要判断是否正确读入了数据(尤其是在文件末尾处)。
判断是否正确读入了数据,可以调用ios类的成员函数fail来实现:
bool ios::fail() const;
- 该函数返回true表示文件操作失败;返回false表示操作成功。
例:从文件读入一系列整型数
//文件中的数据形式
1 2 3 4\n
------------------
1\n
2\n
3\n
4\n
------------------
1 2 3 4
------------------
1\n
2\n
3\n
4
......
ifstream in_file("d:\\myfile.txt",ios::in);
if (!in_file) exit(-1);
int x;
in_file >> x; //读入第一个整型数
while (!in_file.fail())
{ ...... //使用x的值
in_file >> x; //读入下一个整型数
}
in_file.close();
......
有关文件读写的几点注意
以文本方式输出的文件要以文本方式输入;以二进制方式输出的文件要以二进制方式输入!
以文本方式读写的文件要以文本方式打开;以二进制方式读写的文件要以二进制方式打开!
以二进制方式存取文件不利于程序的兼容性和可移植性。例如,
-
在不同计算机平台上,整型数的各字节在内存中的存储次序可能不一样。
-
在不同的编译环境下,同样的结构类型数据的尺寸(字节数)可能不一样。
-
…
打开既能输入、又能输出的文件
如果需要打开一个既能读入数据、也能输出数据的文件,则需要创建一个fstream
类的对象。
文件内部有两个位置指针,一个用于是读,另一个用于写。
在创建fstream
类的对象并建立与外部文件的联系时,文件打开方式应为下面之一:
-
ios::in|ios::out
(可在文件任意位置写) -
ios::in|ios::app
(只能在文件末尾写)
文件的随机存取
为了能够随机读写文件中的数据,可以显示地指出读写的位置。
下面的操作用来指定文件内部读指针的位置:
istream& istream::seekg(<位置>);//指定绝对位置
istream& istream::seekg(<偏移量>,<参照位置>); //指定相对位置
streampos istream::tellg(); //获得指针位置
下面的操作来指定文件内部写指针的位置:
ostream& ostream::seekp(<位置>);//指定绝对位置
ostream& ostream::seekp(<偏移量>,<参照位置>); //指定相对位置
streampos ostream::tellp(); //获得指针位置
<参照位置>可以是:ios::beg
(文件头),ios::cur
(当前位置)和ios::end
(文件尾)。