C语言中的I/O操作与浮点数处理
1. 无缓冲I/O的使用场景
缓冲通常会让重复操作更高效,但在某些情况下,无缓冲I/O更适用。就像扔五个冰箱,你不会把它们都塞进垃圾袋再扔掉,而是一个一个单独扔掉。
2. 使用原始I/O复制文件
若要复制文件,可使用缓冲I/O系统,但这意味着要让其选择缓冲区大小。我们可自行设置缓冲区大小,以下是使用1024字节缓冲区的原始I/O复制文件的程序:
/**
* Copy one file to another.
*
* Usage:
* copy <from> <to>
*/
#include <stdio.h>
#include <stdbool.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#ifndef O_BINARY
#define O_BINARY 0 // Define O_BINARY if not defined.
#endif // O_BINARY
int main(int argc, char* argv[])
{
if (argc != 3) {
fprintf(stderr, "Usage is %s <infile> <outfile>\n",
argv[0]);
exit(8);
}
// The fd of the input file
int inFd = open(argv[1], O_RDONLY|O_BINARY);
if (inFd < 0) {
fprintf(stderr, "ERROR: Could not open %s for
input\n", argv[1]);
exit(8);
}
// The fd of the output file
int outFd = open(argv[2], O_WRONLY|O_CREAT|O_BINARY,
0666);
if (outFd < 0) {
fprintf(stderr, "ERROR: Could not open %s for
writing\n", argv[2]);
exit(8);
}
while (true)
{
char buffer[1024]; // Buffer to read and write
size_t readSize; // Size of the last read
readSize = read(inFd, buffer, sizeof(buffer));
if (readSize < 0) {
fprintf(stderr, "ERROR: Read error for file
%s\n", argv[1]);
exit(8);
}
if (readSize == 0)
break;
if (write(outFd, buffer, readSize) != readSize) {
fprintf(stderr, "ERROR: Write error for %s\n",
argv[2]);
exit(8);
}
}
close(inFd);
close(outFd);
return (0);
}
使用该程序时,需指定输入文件和输出文件,命令如下:
$ ./copy input-file output-file
程序执行步骤如下:
1. 检查参数数量是否正确。
2. 打开输入文件,
open
函数的一般形式为
file-descriptor = open(filename, flags)
,
O_RDONLY
表示只读,
O_BINARY
表示二进制文件。
3. 检查打开输入文件是否出错。
4. 打开输出文件,使用
O_WRONLY
(只写)和
O_CREAT
(若文件不存在则创建)标志,
0666
参数指定文件的保护模式。
5. 进行文件复制,
read
函数读取数据,
write
函数写入数据。
6. 检查读取和写入是否出错。
7. 关闭文件描述符。
3. 保护模式参数说明
0666
是一个八进制数,每个数字代表不同用户组的权限,每一位代表不同的权限类型:
| 权限类型 | 数值 |
| ---- | ---- |
| 读 | 4 |
| 写 | 2 |
| 执行 | 1 |
数字顺序为
<用户>, <组>, <其他>
,
0666
表示用户、组和其他用户都有读写权限。若使用
0640
,则用户有读写权限,组只有读权限,其他用户无权限。
4. 二进制模式的使用
不同操作系统使用不同字符表示行尾,文本文件在不同操作系统间不具有可移植性。例如,在Linux上使用
write(fd, "Hi\n", 3)
写入文件,文件包含三个字符
48 69 0a
;在Windows上执行相同代码,会写入四个字符
48 69 0d 0a
。
为避免这种转换,可使用
O_BINARY
标志。Linux不区分二进制和文本文件,没有
O_BINARY
标志,因此在代码中添加如下定义:
#ifndef O_BINARY
#define O_BINARY 0 // Define O_BINARY if not defined.
#endif // O_BINARY
若操作系统已定义
O_BINARY
,则
#define
不会被编译;若使用无
O_BINARY
的类Linux操作系统,
O_BINARY
将被赋值为0,不产生任何影响。
5. ioctl函数
原始I/O系统还提供了
ioctl
函数用于I/O控制,其一般形式为:
result = ioctl(fd, request, parameter);
其中,
fd
是文件描述符,
request
是设备特定的控制请求,
parameter
是请求的参数。大多数请求下,操作成功返回0,失败返回非零值。可使用
ioctl
函数弹出可移动介质、倒带或快进磁带驱动器、设置串行设备的速度和其他参数,以及设置网络设备的地址信息。
6. 浮点数的基本概念
浮点数是小数点位置可变的数,如
1.0
、
0.1
、
0.0001
或
1000.0
。严格来说,小数点后不一定要有数字,例如
1.0
和
1.
是同一个数,但小数点两边都有数字时更易读。也可使用指数表示法,如
1.0e33
表示
1.0 × 10^33
。
7. 浮点数类型
在C语言中,浮点数类型有
float
、
double
和
long double
。
double
类型的精度和范围是
float
(单精度)类型的两倍,
long double
的精度和范围更大。所有浮点常量默认为
double
类型,可通过添加
F
后缀将其变为单精度
float
,添加
L
后缀变为
long double
。
8. 自动转换
C语言会自动进行一些转换。若表达式的一个操作数是浮点数,C会自动将另一个操作数转换为浮点数。例如:
f = 1.0 / 3; // Bad form
这里的
3
会在除法运算前转换为
3.0
。若将浮点数赋值给整数,会将其转换为整数。
9. 浮点数的问题
浮点数存在不精确的问题,例如十进制浮点数中
1/3
是
0.333333
,无论使用多少位数字,都无法精确表示。计算机使用二进制浮点数,与十进制浮点数类似,只是指数和小数部分用二进制表示。
10. 舍入误差
1 + 1
等于
2
,但
1/3 + 1/3
不等于
2/3
。例如:
+3.333e-01 // 1/3 in our notation
+3.333e-01 // 1/3 in our notation
+6.666e-01
而
2/3
是
+6.667e-01
,这就是舍入误差的例子。可通过添加保护位来减少舍入误差。
11. 精度位数
单精度浮点数(
float
)理论上约有6.5位精度,但实际并非总是如此。例如计算
2/3 – 1/3 – 1/3
:
+6.667e-01 // 2/3
-3.333e-01 // 1/3
-3.333e-01 // 1/3
+0.001e-01 // Result unnormalized
+1.000e-04 // Result normalized
结果的第一位数字是
1
,而正确的第一位数字应该是
0
。
12. 特殊值
IEEE浮点数格式有一些特殊的位模式,代表特殊值:
-
无穷大
:
float f = 1.0 / 0.0;
结果为
INFINITY
,
float f = -1.0 / 0.0;
结果为
-INFINITY
。
-
非数字(NaN)
:当操作无法产生结果时会生成
NaN
,例如
float f = sqrt(-1.0);
。
13. 最小可表示数
在浮点数方案中,最小可表示数并非
+1.0000e-99
,还可以更小,如
+0.1000e-99
和
+0.0001e-99
。
综上所述,原始I/O系统能让我们更好地控制I/O操作,但需要我们自己处理更多细节;浮点数在使用时存在不精确等问题,需要我们在编程时格外注意。
14. 编程问题实践
为了更好地掌握上述知识,下面给出几个编程问题及分析。
14.1 输出问候语程序
编写一个程序,接受一个参数,即运行程序的人的名字,然后输出
Hello <name>
,若未提供参数,则输出
Hello stranger
。以下是示例代码:
#include <stdio.h>
int main(int argc, char* argv[]) {
if (argc == 2) {
printf("Hello %s\n", argv[1]);
} else {
printf("Hello stranger\n");
}
return 0;
}
操作步骤如下:
1. 编写上述代码并保存为
hello.c
文件。
2. 编译代码:
gcc hello.c -o hello
。
3. 运行程序:
- 提供参数:
./hello Fred
,输出
Hello Fred
。
- 不提供参数:
./hello
,输出
Hello stranger
。
14.2 模式判断程序
编写一个程序,扫描参数列表,若存在
-d
参数,则输出
Debug mode
,若
-d
缺失,则输出
Release mode
。还可以添加其他选项。示例代码如下:
#include <stdio.h>
#include <stdbool.h>
int main(int argc, char* argv[]) {
bool debug = false;
for (int i = 1; i < argc; i++) {
if (strcmp(argv[i], "-d") == 0) {
debug = true;
break;
}
}
if (debug) {
printf("Debug mode\n");
} else {
printf("Release mode\n");
}
return 0;
}
操作步骤如下:
1. 编写代码并保存为
mode.c
文件。
2. 编译代码:
gcc mode.c -o mode
。
3. 运行程序:
- 包含
-d
参数:
./mode -d
,输出
Debug mode
。
- 不包含
-d
参数:
./mode
,输出
Release mode
。
14.3 复制程序性能测试
测试前面提到的复制程序在不同缓冲区大小下的性能。我们可以通过修改缓冲区大小(如1、16384、17000),并使用系统自带的时间命令来测试程序运行时间。
# 测试原始缓冲区大小
time ./copy large-input-file large-output-file
# 修改缓冲区大小为1
# 修改代码中buffer数组大小为1
gcc copy.c -o copy
time ./copy large-input-file large-output-file
# 修改缓冲区大小为16384
# 修改代码中buffer数组大小为16384
gcc copy.c -o copy
time ./copy large-input-file large-output-file
# 修改缓冲区大小为17000
# 修改代码中buffer数组大小为17000
gcc copy.c -o copy
time ./copy large-input-file large-output-file
由于几乎所有磁盘以512字节块进行读写,当缓冲区大小接近512的倍数时,程序性能可能更好,因为可以减少磁盘读写次数。
14.4 使用
getopt
函数解析参数
getopt
函数可用于解析命令行参数。以下是使用
getopt
函数改写前面模式判断程序的示例:
#include <stdio.h>
#include <getopt.h>
int main(int argc, char* argv[]) {
int opt;
bool debug = false;
while ((opt = getopt(argc, argv, "d")) != -1) {
switch (opt) {
case 'd':
debug = true;
break;
default:
fprintf(stderr, "Usage: %s [-d]\n", argv[0]);
return 1;
}
}
if (debug) {
printf("Debug mode\n");
} else {
printf("Release mode\n");
}
return 0;
}
操作步骤如下:
1. 编写代码并保存为
getopt_mode.c
文件。
2. 编译代码:
gcc getopt_mode.c -o getopt_mode
。
3. 运行程序:
- 包含
-d
参数:
./getopt_mode -d
,输出
Debug mode
。
- 不包含
-d
参数:
./getopt_mode
,输出
Release mode
。
15. 总结
本文主要介绍了C语言中原始I/O系统和浮点数的相关知识,总结如下:
| 类别 | 特点 | 注意事项 |
| ---- | ---- | ---- |
| 原始I/O系统 | 提供对I/O操作的最佳控制,减少操作系统的编辑和干扰 | 需要自己处理更多细节,容易出错 |
| 浮点数 | 小数点位置可变,有
float
、
double
、
long double
类型 | 存在不精确、舍入误差等问题,使用时需谨慎 |
在实际编程中,我们可以根据具体需求选择合适的I/O方式和数据类型。例如,当需要精确控制I/O操作时,可使用原始I/O系统;当对精度要求不高且数据处理量较小时,可使用浮点数,但要注意其可能带来的问题。
16. 流程图总结
graph TD
A[开始] --> B{参数数量是否正确}
B -- 是 --> C[打开输入文件]
B -- 否 --> D[输出错误信息并退出]
C --> E{输入文件打开是否出错}
E -- 是 --> D
E -- 否 --> F[打开输出文件]
F --> G{输出文件打开是否出错}
G -- 是 --> D
G -- 否 --> H[进行文件复制]
H --> I{读取是否出错}
I -- 是 --> D
I -- 否 --> J{是否到达文件末尾}
J -- 是 --> K[关闭文件描述符]
J -- 否 --> L{写入是否出错}
L -- 是 --> D
L -- 否 --> H
K --> M[结束]
该流程图展示了文件复制程序的执行流程,帮助我们更好地理解程序的逻辑。
通过本文的学习,相信大家对C语言中的I/O操作和浮点数处理有了更深入的了解,在实际编程中能够更加熟练地运用这些知识。
超级会员免费看
1272

被折叠的 条评论
为什么被折叠?



