0%

从fscanf/fprintf到fstream

  1. 1. C中的文件读写示例:使用fscanf和fprintf
  2. 2. ifstream/ofstream/fstream使用示例
  3. 3. 总结
  4. 4. 改进:既不要_CRT_SECURE_NO_WARNINGS,又要格式化输入输出

在C语言中,使用fscanffprintf操作由fopen打开的文件指针,进行文件读写,其格式化读写方式使用起来相当便利。但是在C++中,文件读写通常使用fstream方式,对于C里面的两个函数以及fopen开始强调安全性问题,推荐使用fopen_sfscanf_sfprintf_s代替,在最新的编译器立面通常需要在“项目-属性-C/C++-预处理器-预处理器定义”中加入_CRT_SECURE_NO_WARNINGS才能保证不报错。那么如何使文件流的方式用起来和格式化读写一样便利呢?本文进行探究。

1. C中的文件读写示例:使用fscanf和fprintf

例如有一些点保存在数组中,使用fscanf和fprintf可以很方便地按照一定的格式(一行保存两个坐标)将它们保存到文本文件中,如下所示。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
/* 保存点 */
void savePoints(vector<Point>& pts, string fileName)
{
FILE* fp;
fp = fopen(fileName.c_str(), "w");

if (!fp)
{
cout << "Open file failed!" << endl;
return;
}

for (size_t i = 0; i < pts.size(); i++)
{
fprintf(fp, "%d %d\n", pts[i].x, pts[i].y);
}

if (pts.size() == 0)
{
cout << "[WARNING] point number is 0" << endl;
}

fclose(fp);
}

/* 读取点 */
void readPoints(vector<Point>& pts, string fileName)
{
FILE* fp;
fp = fopen(fileName.c_str(), "r");

if (!fp)
{
cout << "Open file failed!" << endl;
return;
}

while (!feof(fp))
{
Point pt;
fscanf(fp, "%d %d\n", &pt.x, &pt.y);
pts.push_back(pt);
}

if (pts.size() == 0)
{
cout << "[WARNING] point number is 0" << endl;
}

fclose(fp);
}

2. ifstream/ofstream/fstream使用示例

C++ 文件和流 | 菜鸟教程

将读写点的代码用fstream改写,大致如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
/* 保存点 */
void savePoints(vector<Point>& pts, string fileName)
{
fstream file;
file.open(fileName, ios::out | ios::trunc); // ios::trunc截断,强行覆盖已有结果

for (size_t i = 0; i < pts.size(); i++)
{
file << pts[i].x << " " << pts[i].y << endl;
}

file.close();
}

/* 读取点 */
void readPoints(vector<Point>& pts, string fileName)
{
fstream file;
file.open(fileName, ios::in);

Point pt;
char ch;
while (!file.eof())
{
file >> pt.x >> pt.y;
pts.push_back(pt);
}

file.close();
}

其中open()函数的第二个参数可以参考下表。

模式标志 描述
ios::app 追加模式。所有写入都追加到文件末尾。
ios::ate 文件打开后定位到文件末尾。
ios::in 打开文件用于读取。
ios::out 打开文件用于写入。
ios::trunc 如果该文件已经存在,其内容将在打开文件之前被截断,即把文件长度设为 0。

但是用改写的代码执行后存在一个问题,由于采用的是流的方式进行读写,因此更偏向于单个字符或单个数字的操作,读入时会自动跳过空格换行符,当输出到文件时使用file << pt.x << " " << pt.y << endl;,那么对应输入时只需要用file >> pt.x >> pt.y;即可。这就导致两个问题:

  1. 无法自定义分隔符。如果使用逗号或多个逗号作为分隔符,那么必须将其作为单个字符读入到变量中:file >> pt.x >> ch >> pt.y;,或者已知分隔符的长度,控制文件指针跳转(用到seek get):file >> pt.x; file.seekg(n, ios::cur); file >> pt.y;
  2. 文件尾难以判断。假设文件中保存了1000个点,那么在保存时,通常会记录1000行的x、y坐标,最后一行为空。在使用fscanf格式化输入时,会自动跟到数据和换行符(例如fscanf(fp, "%d %d\n")),而在使用文件流时,最后一个换行符的存在导致读取完最后一个数据行之后,file.eof()函数返回值仍为false,因此会额外执行一次循环,而此时file >> pt.x >> pt.y根本无法读入数据,因此会导致最后一个数据项在数组的末尾出现两次。因此,需要设计额外的逻辑来判断真实的文件尾。

3. 总结

fstream采用流的方式读取文件,更加安全,也符合读写逻辑,但损失了格式化的便捷性。

针对流的操作只能用流的方式来应对,改进一下上面的例子,当数据之间的分隔符为空格时,可以不考虑分隔符直接读入;在确保数据文件的最后一项数据之后有一个换行符的前提下,可以采用读完数据再判断是否到达文末的方式打断循环,这种方法可以解决当前问题,但是如果最后一行没有换行符,那么这种方法就会少读取一行数据。而相对地,fscanf只会提示错误,而不会导致数据缺失。综合来看,格式化输入输出要比纯粹的流方法来得方便,流方式无法达到格式化输入的便利性

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
/* 保存点 */
void savePoints(vector<Point>& pts, string fileName)
{
fstream file;
file.open(fileName, ios::out | ios::trunc); // ios::trunc截断,强行覆盖已有结果

for (size_t i = 0; i < pts.size(); i++)
{
file << pts[i].x << " " << pts[i].y << endl;
}

file.close();
}

/* 读取点 */
void readPoints(vector<Point>& pts, string fileName)
{
fstream file;
file.open(fileName, ios::in);

Point pt;
while (true)
{
file >> pt.x >> pt.y;
if (file.eof()) break;
pts.push_back(pt);
}

file.close();
}

4. 改进:既不要_CRT_SECURE_NO_WARNINGS,又要格式化输入输出

解决方案:使用fopen_sfscanf_sfprintf_s。使用上的区别主要在fopen_s上,三个函数的使用如下所示。使用这样的代码就可以避免VS报错和警告。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
/* 保存点 */
void savePoints(vector<Point>& pts, string fileName)
{
FILE* fp;
if (!fopen_s(&fp, fileName.c_str(), "w") != 0 || !fp)
{
cout << "File open failed!" << endl;
return;
}

for (size_t i = 0; i < pts.size(); i++)
{
fprintf_s(fp, "%d %d\n", pts[i].x, pts[i].y);
}
fclose(fp);
}

/* 读取点 */
void readPoints(vector<Point>& pts, string fileName)
{
FILE* fp;
if (fopen_s(&fp, fileName.c_str(), "r") != 0 || !fp)
{
cout << "File open failed!" << endl;
return;
}

Point pt;
while (!feof(fp))
{
fscanf_s(fp, "%d %d\n", &pt.x, &pt.y);
pts.push_back(pt);
}

fclose(fp);
}