对IO流关闭的思考

流必须要关闭的原因

java相对C,C++来说不需要手动释放内存,在对象引用被消除之后,正常情况下内存资源是会被垃圾回收,那么在使用完IO流之后为什么需要手动关闭.
这是为了回收系统资源,比如释放占用的端口,文件句柄,网络操作数据库应用等.对Unix系统来说,所有的资源都可以抽象成文件,所以可以通过lsof来观察。

看下面这个例子,我们创建许多的IO流但是不关闭

public class Hello {
    public static void main(String[] args) throws Exception {
        for (int i = 0; i < 100; i++) {
            FileInputStream is = new FileInputStream("/tmp/data/data"+i);
            byte[] bs = new byte[512];
            int len;
            while ((len = is.read(bs)) != -1) ;
            Thread.sleep(1000L);
        }
        Thread.sleep(10000L);
    }
}

运行这个程序之后,使用losf命令查看相关的文件句柄,会发现文件句柄始终在增长,当积累到一定时间之后会出现too many open files错误

如何关闭流

在java7以前流的关闭比较繁琐,我们使用try-finally块进行关闭操作,同时还要考虑流关闭过程中可能的异常

InputStream is = null;
OutputStream os = null;
try{
   // ...
}
finally{
  if(is != null)
     try{
       is.close();
     }
     catch(IOException e){}

  if(os != null)
     try{
       os.close()
     }
     catch(IOException e){}
}

在java7以后,我们可以使用try-with-resources,将需要关闭的流对象放在try的()中创建,需要注意的是只有实现Closeable接口的对象才可以放在这里创建

try (BufferedReader reader = new BufferedReader(new InputStreamReader(System.in));) {
    String s;
    while ((s = reader.readLine()) != null) {
        if (s.equalsIgnoreCase("quit")){
            break;
        }
        System.out.println(s.toUpperCase());
    }
} catch (Exception e) {
    e.printStackTrace();
}

包装流的关闭

包装流关闭的时候会自动调用被包装的流的关闭方法

看下面这个例子
打开的文件句柄必须要关闭,但是这个例子是例外,从控制台中读取字符串并打印

try (BufferedReader reader = new BufferedReader(new InputStreamReader(System.in));) {
    String s;
    while ((s = reader.readLine()) != null) {
        if (s.equalsIgnoreCase("quit")){
            break;
        }
        System.out.println(s.toUpperCase());
    }
} catch (Exception e) {
    e.printStackTrace();
}

这里使用了try-with-resources的写法(后面会具体的讲到)自动关闭reader对象,但是这里需要注意,BufferedReader包装的是System.in流,这个流是java的标准流用于接收键盘数据,如果这里讲System.in流关闭,那么其它的程序可能会出错,那么我们究竟需要关闭哪些流呢?统一的原则是 谁创建谁销毁,如果我们必须要在关闭不是自己创建的流对象,一定要通过文档告知接口的调用方

关闭包装流与被包装流的时候有没有顺序视情况而定

我们在使用缓存输出流的情况下,必须要优先关闭缓存输出流否则抛出异常.原因很简单,看下面这个例子,这里为了方便没有规范的关闭流

 FileOutputStream fos = new FileOutputStream(filepath);
 OutputStreamWriter osw = new OutputStreamWriter(fos, "UTF-8");
 BufferedWriter bw = new BufferedWriter(osw);
 bw.write("java IO close test");
 // 从内带外顺序顺序会报异常
 fos.close();
 osw.close();
 bw.close();

缓存输出流的作用是将输出数据先缓存起来,等待缓存区满了之后一次性输出来提高通道利用率,在调用缓存输出流的close方法的时候,会调用被装饰对象的write方法,如果被装饰对象被提前关闭了,那么自然就抛出异常

所以上面的例子修改为

bw.close();
fos.close();
osw.close();

如果没有涉及到缓存输出流那么就无所谓关闭顺序了

FileOutputStream fileOutputStream = new FileOutputStream("c:\\a.txt");
BufferedOutputStream bufferedOutputStream = new BufferedOutputStream(fileOutputStream);

bufferedOutputStream.write("test write something".getBytes());
bufferedOutputStream.flush();

fileOutputStream.close();//先关闭被包装流
bufferedOutputStream.close();
<think>好的,用户现在想了解如何用标准IO实现cp程序。首先,我需要回顾之前用户的问题。之前他们问过用标准IO实现ls -l,现在转向cp,这可能是在学习系统编程中的文件操作部分。 用户可能是在学习C语言的文件操作,或者想了解如何用标准库函数进行文件复制。他们可能已经知道系统调用如open和read,但这里需要标准IO的函数,比如fopen、fread、fwrite等。需要确定用户是否熟悉这些函数,但作为回答,应该详细解释。 接下来,我需要考虑标准IO和系统调用的区别。标准IO提供了缓冲机制,可能更高效,尤其是在处理大文件时。而cp程序的核心就是读取源文件并写入目标文件,同时处理可能的错误,比如文件存在,权限足等。 需要分步骤思考:打开源文件,打开目标文件,读取内容,写入内容,关闭文件。同时需要注意模式,比如二进制模式,避免在Windows下出现问题,但用户可能是在Linux环境下,所以可能需要考虑平台差异。 错误处理是关键部分。每个步骤都可能失败,比如fopen失败时,需要输出错误信息。还要处理fread和fwrite的返回值,确保实际读取和写入的字节数正确。如果中间出错,可能需要删除已经创建的目标文件,避免留下完整文件。 另外,需要考虑文件权限的问题。使用fopen的"w"模式会覆盖已有文件,但可能需要保留源文件的权限。过标准IO可能无法直接复制权限,这时候可能需要结合系统调用如chmod,但用户要求的是标准IO实现,可能这一步可以忽略,或者提示用户。但之前的例子中,cp通常会保留所有元数据,除非使用特定选项。 用户可能还想知道如何逐块读取,使用缓冲区提高效率。比如设置一个合适大小的缓冲区,比如4096字节,这样减少系统调用次数,提高性能。 可能还需要处理特殊情况,比如源文件和目标文件相同,这时候应该报错。或者目标文件是目录,应该如何处理。这些情况在实现中需要考虑,但基本的cp可能需要处理所有情况,但作为一个示例,可能需要简单提及。 最后,确保代码示例清晰,有注释,并且说明每个步骤的作用。同时比较与系统调用的区别,指出标准IO的优点和可能的足,比如缓冲带来的延迟写入问题,或者大文件处理时的内存使用。 总结,回答应包括代码结构、关键函数的使用、错误处理、缓冲区管理,以及可能的注意事项。同时,需要检查代码是否正确,比如是否正确关闭文件,是否处理所有错误路径,避免资源泄漏。</think>以下是用C标准I/O库实现`cp`命令的核心代码及实现原理分析: ```c #include <stdio.h> #include <stdlib.h> #include <stdbool.h> #define BUFFER_SIZE 4096 // 推荐使用4K的缓冲区大小 int main(int argc, char *argv[]) { if (argc != 3) { fprintf(stderr, "用法: %s <源文件> <目标文件>\n", argv[0]); return EXIT_FAILURE; } FILE *src = fopen(argv[1], "rb"); if (!src) { perror("无法打开源文件"); return EXIT_FAILURE; } // 检查源文件是否可读 if (fseek(src, 0, SEEK_SET) != 0) { perror("源文件可访问"); fclose(src); return EXIT_FAILURE; } FILE *dst = fopen(argv[2], "wb"); if (!dst) { perror("无法创建目标文件"); fclose(src); return EXIT_FAILURE; } char buffer[BUFFER_SIZE]; size_t bytes_read; bool error_occurred = false; while ((bytes_read = fread(buffer, 1, sizeof(buffer), src)) > 0) { size_t bytes_written = fwrite(buffer, 1, bytes_read, dst); if (bytes_written != bytes_read) { perror("写入失败"); error_occurred = true; break; } } // 检查读取是否出错(非EOF导致的结束) if (ferror(src)) { perror("读取文件错误"); error_occurred = true; } // 关闭文件前检查是否需要删除目标文件 if (fclose(dst) != 0) { perror("关闭目标文件失败"); error_occurred = true; } if (fclose(src) != 0) { perror("关闭源文件失败"); error_occurred = true; } // 出错时删除完整的目标文件 if (error_occurred) { remove(argv[2]); return EXIT_FAILURE; } return EXIT_SUCCESS; } ``` **关键实现原理:** 1. **文件模式选择** - 使用`"rb"`和`"wb"`模式: - `b`表示二进制模式(避免Windows下的换行符转换) - `r`/`w`分别表示只读/只写模式 2. **缓冲机制** - 标准I/O自带缓冲区(默认全缓冲) - 使用4K缓冲区可平衡内存使用和I/O效率 - 与直接系统调用相比减少上下文切换次数 3. **错误处理机制** - 检查每个关键操作的返回值 - 使用`ferror()`检测文件错误状态 - 出错时清理已创建的目标文件 4. **数据传输程** ```mermaid graph TD A[打开源文件] --> B[打开目标文件] B --> C{是否可读/写?} C --> |是| D[分配缓冲区] D --> E[读取数据块] E --> F{读取成功?} F --> |是| G[写入数据块] G --> H{写入完整?} H --> |是| E H --> |否| I[标记错误] F --> |否| J[结束读取] J --> K[关闭文件] K --> L{存在错误?} L --> |是| M[删除目标文件] ``` 5. **与系统调用实现的差异** | 特性 | 标准I/O实现 | 系统调用实现 | |---------------|--------------------------|---------------------| | 缓冲机制 | 自动缓冲(用户可调大小) | 无缓冲/手动缓冲 | | 性能 | 适合小文件 | 适合大文件 | | 错误处理 | 通过返回值+`ferror` | 直接通过errno | | 可移植性 | 跨平台统一 | 需处理平台差异 | | 元数据保留 | 仅内容复制 | 可保留权限/时间戳 | **扩展实现建议:** 1. **进度显示**:通过文件大小计算复制百分比 ```c fseek(src, 0L, SEEK_END); long file_size = ftell(src); rewind(src); ``` 2. **大文件支持**:使用`fseeko()`和`ftello()`处理超过2GB的文件 3. **保留元数据**(需结合系统调用): ```c #include <sys/stat.h> struct stat st; stat(src_path, &st); chmod(dst_path, st.st_mode); ``` 4. **稀疏文件处理**:检测全零块并跳过写入 **典型测试用例:** ```bash # 复制文本文件 ./mycp test.txt backup.txt # 复制二进制文件(验证二进制模式) ./mycp image.jpg copy.jpg # 错误处理测试(存在的源文件) ./mycp not_exist.txt dummy.txt # 应报错 ``` **注意事项:** 1. 目标文件存在时会直接覆盖(与`cp`命令默认行为一致) 2. 保留文件的ACL、扩展属性等高级元数据 3. 对符号链接会复制链接目标的内容(需`lstat`判断处理) 4. 内存使用与缓冲区大小直接相关(建议4K-1MB之间) 这个实现可作为学习标准I/O操作的典型案例,实际系统级`cp`命令会包含更多优化(如内存映射、异步I/O等),但核心原理与此一致。
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值