C++ (week4):Linux系统编程1:文件

news2024/11/18 11:37:12

文章目录

    • 一、文件:Linux文件操作
      • 1.基于文件指针的文件操作
      • 2.Linux目录操作
        • (1)目录路径
          • 0.error
          • 1.getcwd
          • 2.chdir
          • 3.创建目录:mkdir
          • 4.删除目录:rmdir
          • 5.unlink(路径名)
        • (2)目录流 DIR*
          • 0.模型
          • 1.opendir:打开目录流
          • 2.closedir:关闭目录流
          • 3.readdir:从目录流中读取下一个目录项
          • 4.seekdir:可以移动目录流中的位置
          • 5.telldir :返回目录流中现在的位置。
          • 6.rewinddir :重置目录流,即移动到目录流的起始位置。
        • (3)练习:递归处理目录
          • ①青春版tree命令
          • ②递归复制目录
          • ③递归删除目录
      • 3.基于文件描述符的文件操作
        • (1)文件描述符
        • (2)打开文件、创建文件、关闭文件
          • ①打开文件:open()
          • ②创建文件:open()、creat()
          • ③关闭文件:close()
        • (3)读写文件
          • ①读文件:read
          • 写文件:write
        • (4)改变文件大小:ftruncate
        • (5)内存映射:mmap
        • (6)文件定位:lseek
        • (7)获取文件的元数据信息:fstat
        • (8)文件描述符的复制:dup、dup2
        • (9)将脏页写入磁盘:fsync
        • (10)文件描述符 vs 文件流(文件指针)

一、文件:Linux文件操作

1.基于文件指针的文件操作

跳转链接:https://blog.csdn.net/Edward1027/article/details/138673572


2.Linux目录操作

(1)目录路径
0.error

NAME: glibc error reporting functions
SYNOPSIS:

#include <error.h>
#include <errno.h>
void error(int status, int errnum, const char *format, ...)

①第一个参数:为0不退出程序;非0退出程序,相当于exit(1)。
②第二个参数:为0,则不设置errno。若非0,则设置errno。
③第三个参数:相当于perror(" ")

举例:

//Window下的错误处理
perror("getcwd");
exit(1);

//linux下的错误处理
error(1, errno, "getcwd");    //非0退出,不需要换行符,会自动添加\n
error(1, 0, "prefix:");  	  //不设置errno

1.getcwd

(1)cwd:current working directory
(2)作用:获取当前工作目录的绝对路径
(3)参数
(4)返回值:成功 、失败
(5)特例:getcwd(NULL,0)

查阅man手册:name、synopsis、return value

笔记代码

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述


2.chdir

在这里插入图片描述

惯用法(Idiom):切换当前工作目录

if(chdir(argv[1]) == -1){
	error(1, errno, "chdir %s", argv[1]);
}

3.创建目录:mkdir

在这里插入图片描述
惯用法(Idiom):

if(mkdir(argv[1]) == -1){
	error(1, errno, "mkdir %s", argv[1]);
}
mode_t mode;
int err = mkdir(argv[1], mode);
if(err){
	error(1, errno, "mkdir %s", argv[1]);
}

如何查看mode_t究竟是什么类型?


4.删除目录:rmdir

在这里插入图片描述
惯用法(Idiom):

if(rmdir(argv[1]) == -1){
	error(1, errno, "rmdir %s", argv[1]);
}

5.unlink(路径名)

(2)目录流 DIR*

目录项:directory entry -> dirent

0.模型

在这里插入图片描述

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述


1.opendir:打开目录流

在这里插入图片描述


2.closedir:关闭目录流

惯用法(Idiom):

if(closedir(pdir) == -1) {
    error(1, errno, "closedir %s failed.", src);
}

在这里插入图片描述


3.readdir:从目录流中读取下一个目录项

在这里插入图片描述


目录项(directory entry, dirent) 结构体:inode编号、名字

#include <dirent.h>
#include <sys/types.h>

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

工作原理:
在这里插入图片描述



练习1:使用 readdir 函数读取并打印目录中的所有文件名:

#include <stdio.h>
#include <dirent.h>

int main() {
    // 打开目录
    DIR* dir = opendir("./test1");
    if (dir == NULL) {
    	error(1, errno, "opendir %s", path);
    }

    // 读取目录中的每一项
    struct dirent* entry;
    while ((entry = readdir(dir)) != NULL) {
        printf("%s\n", entry->d_name);
    }

    // 关闭目录
    closedir(dir);

    return 0;
}


4.seekdir:可以移动目录流中的位置
5.telldir :返回目录流中现在的位置。
6.rewinddir :重置目录流,即移动到目录流的起始位置。

(3)练习:递归处理目录
①青春版tree命令

在这里插入图片描述

//youth_tree.c
#include <stdio.h>
#include <string.h>
#include <error.h>
#include <errno.h>
#include <sys/types.h>
#include <dirent.h>

int directories = 0, files = 0;

void dfs_print(const char* path, int width){
    //打开目录流
    DIR* stream = opendir(path);
    if(stream == NULL){
        error(1, errno, "opendir %s failed.\n",path);
    }

    //遍历每一个目录项
    errno = 0;
    struct dirent* pdirent;
    while((pdirent = readdir(stream)) != NULL){
        char* filename = pdirent->d_name;
        //忽略.和..
        if(strcmp(filename, ".") == 0 || strcmp(filename, "..") == 0){
            continue;
        }
            
        //打印目录项的名字
        for(int i = 0; i < width; i++){
            putchar(' ');
        }
        puts(filename);

        if(pdirent->d_type == DT_DIR){
            //拼接路径
            char subpath[128];
            sprintf(subpath, "%s/%s", path, filename);
            directories++;
            dfs_print(subpath, width + 4);
        }else{
            files++;
        }
    } //pdirent == NULL 

    //关闭目录流
    closedir(stream);
    if(stream == NULL){
        error(1, errno, "closedir failed.");
    }
}

int main(int argc, char* argv[]){
    //参数校验
    if(argc != 2){
        error(1, 0, "参数错误。正确用法: ./youth_tree  dir");    
    }

    //输出根目录名
    puts(argv[1]);
    //递归打印目录项
    dfs_print(argv[1], 4);   //缩进4格
    //打印统计信息
    printf("\n%d directories, %d files.\n",directories, files);
    
    return 0;
}

②递归复制目录

在这里插入图片描述

// copyDir.c
#include <stdio.h>
#include <string.h>
#include <error.h>
#include <errno.h>
#include <sys/types.h>
#include <dirent.h>
#include <stdlib.h>
#include <sys/stat.h>

// 复制文件
void copyFile(const char* src, const char* dst) {
    //1.打开文件
	FILE* stream_src = fopen(src, "rb");
	if(stream_src == NULL) {
        error(1 , errno, "open src file %s failed.",src);
	}

    FILE* stream_dst = fopen(dst, "wb");
    if(!stream_dst){
        fclose(stream_src);
        error(1, errno, "open dst file %s failed.",dst);
    }

	//2.确定文件大小
	fseek(stream_src, 0, SEEK_END);
	long len = ftell(stream_src);
	char* content = (char*)malloc((len + 1) * sizeof(char));  // 1 for '\0'

	//3.读取文件
	rewind(stream_src);  //回到文件开头
	int bytes;
    while(bytes = fread(content, 1, len, stream_src) > 0){
        fwrite(content, 1, len, stream_dst);
    }
    content[bytes] = '\0';

	//4.关闭文件
	fclose(stream_src);
	fclose(stream_dst);
}

void copyDir(const char* src, const char* dst) {
    // 创建dst目录
    mkdir(dst,0777);
    // 打开src目录
    DIR* pdir = opendir(src);
    if(pdir == NULL){
        error(1, errno, "opendir %s failed.",src);
    }
    // 遍历目录流
    errno = 0;
    struct dirent* pdirent;
    while((pdirent = readdir(pdir)) != NULL) {
        // 忽略.和..
        char* filename = pdirent->d_name;
        if(strcmp(filename, ".") == 0 || strcmp(filename, "..") == 0){
            continue;
        }
        // 如果该目录项是目录,则调用copyDir递归复制
        char subsrc[128];
        char subdst[128];
            sprintf(subsrc, "%s/%s", src, filename);
            sprintf(subdst, "%s/%s", dst, filename);
        if(pdirent->d_type == DT_DIR){
            copyDir(subsrc, subdst);
        }
        // 如果该目录项是文件,则调用copyFile复制文件
        else{
            copyFile(subsrc, subdst);
        }
    }
    // 关闭目录流
    if(closedir(pdir) == -1){
        error(1, errno, "closedir %s failed.", src);
    }
}

int main(int argc, char* argv[]) {
    //参数校验  ./copyDir src dst
     if(argc != 3){
         error(1, 0, "参数错误。正确用法:%s src dst",argv[0]);
     }
     //递归复制目录
    copyDir(argv[1], argv[2]);
    return 0;
}

③递归删除目录

在这里插入图片描述

#include <func.h>
#include <string.h>

void deleteDir(const char* path) {
	// 打开目录
    DIR* pdir = opendir(path);
    if(pdir == NULL){
        error(1, errno, "opendir %s", path);
    }
     
    errno = 0;
    struct dirent* pdirent;
    // 遍历目录流,依次删除每一个目录项
    while ((pdirent = readdir(pdir)) != NULL) {
        // 忽略.和..
        if(strcmp(pdirent->d_name, ".") == 0 || strcmp(pdirent->d_name, "..") == 0){
            continue;
        }
        // 如果该目录项是目录,则调用deleteDir递归删除
        char subpath[1024];
        sprintf(subpath, "%s/%s", path, pdirent->d_name);  //拼接路径:上级路径/文件名
        if(pdirent->d_type == DT_DIR){
            deleteDir(subpath);
        }
        // 如果该目录项是文件,则调用unlink删除文件
        else {
            unlink(subpath);
        }
    } //pdirent == NULL

    if(errno){
        error(1, errno, "readdir");
    }
    
    // 目录为空了,可以删除该目录了
    if(rmdir(path) == -1){
        error(1, errno, "rmdir %s", path);
    }
    
    // 关闭目录流
    closedir(pdir);
    if(pdir == NULL){
        error(1, errno, "closedir failed.");
    }
}

int main(int argc, char *argv[])
{
    //参数校验
    if(argc != 2){
        error(1, 0, "参数错误。正确用法: ./rmdir_r  dir");
    }
    
    //递归删除目录
    deleteDir(argv[1]);

    return 0;
}


3.基于文件描述符的文件操作

(1)文件描述符

1.文件描述符 (file descriptor,fd) ,非负整数

2.内核用于管理文件的数据结构
①文件描述表:下标是文件描述符fd
②打开文件表:文件位置,即偏移量。有一个指针,指向vnode。
③vnode:是磁盘inode的复制

在这里插入图片描述


(2)打开文件、创建文件、关闭文件

【系统调用】

①打开文件:open()
int open(const char *pathname, int flags); //文件名 打开方式
int open(const char *pathname, int flags, mode_t mode);//文件名 打开方式 权限

返回值:
①执行成功,返回一个文件描述符,表示已经打开的文件
②执行失败,返回-1,并设置相应的errno

在这里插入图片描述

在这里插入图片描述

#include <func.h>

int main(int argc, char* argv[])
{
    // ./test_open file 
    if (argc != 2) {
        error(1, 0, "Usage: %s file", argv[0]);
    }

    // int fd = open(argv[1], O_RDWR);
    int fd = open(argv[1], O_RDWR | O_CREAT | O_TRUNC, 0666); //核心
    if (fd == -1) {
        error(1, errno, "open %s", argv[1]);
    }

    // 获取了文件描述符
    printf("fd = %d\n", fd);

    return 0;
}

②创建文件:open()、creat()
int creat(const char *pathname, mode_t mode); //文件名 权限
//creat现在已经不常用了,它等价于
open(pathname, O_WRONLY| O_CREAT | O_TRUNC, mode);

flags选项
在这里插入图片描述


③关闭文件:close()
int close(int fd);//fd表示文件描述词,是先前由open或creat创建文件时的返回值。

将reference count -1,若引用计数为0,则释放数据结构。


(3)读写文件

使用read和write来读写文件,它们统称为不带缓冲的IO。读写到内核缓冲区。

①读文件:read

1.函数原型

#include <unistd.h>
ssize_t read(int fd, void *buf, size_t count);//文件描述符、缓冲区(起始地址)、长度(最多读取长度)

需要定义一个缓冲区来接收数据

2.返回值

int n = read(fd, recvline, MAXSIZE);  //n为成功读取的字节数

成功返回读写的字节数目,出错返回-1,读到文件末尾EOF,read立刻返回0


写文件:write

系统调用 write

#include <unistd.h>

ssize_t write(int fd, const void *buf, size_t count);   //count是要写的字节数目

对于read和write函数,成功返回读写的字节数目,出错返回-1,读到文件末尾EOF,返回0



在这里插入图片描述

#include <func.h>

int main(int argc, char* argv[])
{
    // ./copy src dst
    if (argc != 3) {
        error(1, errno, "Usage: %s src dst", argv[0]);
    }
    // 1. 打开src和dst
    int src = open(argv[1], O_RDONLY);
    if (src == -1) {
        error(1, errno, "open %s", argv[1]);
    }
    int dst = open(argv[2], O_WRONLY | O_CREAT | O_TRUNC, 0666);
    if (dst == -1) {
        error(1, errno, "open %s", argv[2]);
    }
    // 2. 复制文件
    char buf[4096];
    int bytes;
    while ((bytes = read(src, buf, sizeof(buf))) > 0) {
        // 实际读了bytes个字节,所以写入bytes字节
        write(dst, buf, bytes);
    }
    // 3. 关闭文件
    close(src);
    close(dst);
    return 0;
}

补图。内核缓冲区



(4)改变文件大小:ftruncate

1.作用:
改变文件的大小:截断文件,扩大文件大小

2.头文件、参数

#include <unistd.h>
#include <sys/types.h>

int ftruncate(int fd, off_t length);

3.返回值:
执行成功则返回0,失败返回-1

在这里插入图片描述

文件空洞,全为’\0’的页,不会分配磁盘空间


(5)内存映射:mmap

将文件的一部分,映射到内存

mmap:内存映射(memory map),零拷贝复制技术

使用场景:复制大文件

优势:标准文件操作通常需要将文件内容从磁盘读入内核缓冲区,然后再从内核缓冲区拷贝到用户缓冲区。而使用 mmap 时,文件内容直接映射到用户空间,可以直接访问,避免了多次数据拷贝,从而提高了效率

mmap 是一种内存映射文件的方法,用于将文件内容映射到内存中,从而可以通过内存地址直接访问文件内容。它在处理大文件时非常有用,因为它不需要将整个文件一次性加载到内存中,而是按需加载部分内容。这种方法提高了文件访问的效率,并且可以通过内存操作来读写文件内容。

off_t 用于表示文件偏移量,实际类型是long或long long
size_t 用于表示对象大小或内存块大小,实际类型是 unsigned int 或 unsigned long
在这里插入图片描述

在这里插入图片描述


(6)文件定位:lseek

lseek,系统调用,改变文件的位置。一个系统调用,实现三个库函数。

在这里插入图片描述

#include <sys/types.h>
#include <unistd.h>
off_t lseek(int fd, off_t offset, int whence);//fd文件描述词
//whence 可以是下面三个常量的一个
//SEEK_SET 从文件头开始计算, SEEK_CUR 从当前指针开始计算, SEEK_END 从文件尾开始计算

(7)获取文件的元数据信息:fstat

stat、fstat

#include <sys/types.h>
#include <sys/stat.h>
#include <unistd.h>
int stat(const char *file_name, struct stat *buf); //文件名 stat结构体指针
int fstat(int fd, struct stat *buf); //文件描述词 stat结构体指针

(8)文件描述符的复制:dup、dup2
#include <unistd.h>
int dup(int oldfd);
int dup2(int oldfd, int newfd);

1.系统调用:dup

2.dup:返回最小可用的文件描述符
dup2:指定新的文件描述符

3.文件描述符表,fd复制,引用计数+1

在这里插入图片描述

4.作用:实现重定向


(9)将脏页写入磁盘:fsync

(10)文件描述符 vs 文件流(文件指针)

文件描述符:传输文件
文件流:对文件进行操作

补图11:02

1.fopen是库函数,open是系统调用
1.文件描述符(file descriptor)

int fd = open("file.txt", O_RDONLY);
read(fd, buffer, sizeof(buffer));
write(fd, buffer, sizeof(buffer));
close(fd);

2.文件指针(file pointer)

FILE *fp = fopen("file.txt", "r");
fread(buffer, sizeof(char), sizeof(buffer), fp);
fwrite(buffer, sizeof(char), sizeof(buffer), fp);
fclose(fp);

2.区别:文件描述符更底层,更高效。但没有缓冲机制,导致要进行多次I/O。

在这里插入图片描述

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1699912.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

MT3040 矩形覆盖

代码&#xff1a; #include <bits/stdc.h> using namespace std; typedef long long ll; const int N 3e5 10; int n, ans, d, w; stack<int> s; // 单调栈 // 如果楼高度类似121&#xff08;凸&#xff0c;两边相等&#xff0c;中间比两边的大&#xff09;&…

【C++】二叉树进阶(二叉搜索树)

目录 一、内容安排说明二、 二叉搜索树2.1 二叉搜索树概念2.2 二叉搜索树操作2.2.1 二叉搜索树的查找2.2.2 二叉搜索树的插入2.2.3 二叉搜索树的删除 2.3 二叉搜索树的代码实现2.3.1 二叉搜索树的节点设置2.3.2 二叉搜索树类的框架2.3.3 二叉搜索树的查找函数2.3.3.1 非递归方式…

如何使用多种算法解决LeetCode第135题——分发糖果问题

❤️❤️❤️ 欢迎来到我的博客。希望您能在这里找到既有价值又有趣的内容&#xff0c;和我一起探索、学习和成长。欢迎评论区畅所欲言、享受知识的乐趣&#xff01; 推荐&#xff1a;数据分析螺丝钉的首页 格物致知 终身学习 期待您的关注 导航&#xff1a; LeetCode解锁100…

企业档案管理系统软件都有哪些分类

企业档案管理系统软件可以根据其功能和特点进行分类。以下是一些常见的分类&#xff1a; 1. 全能类档案管理系统&#xff1a;提供文件存储和检索功能&#xff0c;并支持多种文件类型和格式的管理&#xff0c;如文本文件、图像文件、音频文件等。 2. 电子档案管理系统&#xff1…

源码编译安装LAMP(安装apeche mysql php 论坛 网站 巨详细版)

目录 一.LAMP架构相关概述 1.各组件作用 Linux&#xff08;平台&#xff09; Apache&#xff08;前台&#xff09; MySQL&#xff08;后台&#xff09; PHP/Perl/Python&#xff08;中间连接&#xff09; 总结 二.编译安装Apache httpd服务 1.关闭防火墙&#xff0c;将…

2024年【T电梯修理】考试内容及T电梯修理新版试题

题库来源&#xff1a;安全生产模拟考试一点通公众号小程序 2024年【T电梯修理】考试内容及T电梯修理新版试题&#xff0c;包含T电梯修理考试内容答案和解析及T电梯修理新版试题练习。安全生产模拟考试一点通结合国家T电梯修理考试最新大纲及T电梯修理考试真题汇总&#xff0c;…

UI控件与视图层次:探索界面的无限可能

[OC]UI学习笔记 文章目录 [OC]UI学习笔记视图和视图层次结构CGRectUILabelUIButtonUIView控件UIView的层级关系UIWindow定时器和视图移动UISwitch进度条和滑动条控件步进器和分栏控件警告对话框与等待指示器UITextField 视图和视图层次结构 Objective-C中的UI编程主要围绕视图…

IO系列(八) -浅析NIO工作原理

一、简介 现在使用 NIO 的场景越来越多&#xff0c;很多网上的技术框架或多或少的使用 NIO 技术&#xff0c;譬如 Tomcat、Jetty、Netty&#xff0c;学习和掌握 NIO 技术已经不是一个 Java 攻城狮的加分技能&#xff0c;而是一个必备技能。 那什么是 NIO 呢&#xff1f; NIO…

民国漫画杂志《时代漫画》第24期.PDF

时代漫画24.PDF: https://url03.ctfile.com/f/1779803-1248635000-177187?p9586 (访问密码: 9586) 《时代漫画》的杂志在1934年诞生了&#xff0c;截止1937年6月战争来临被迫停刊共发行了39期。 ps: 资源来源网络!

怎么理解直接程序控制和中断方式?

直接程序控制 看完之后是不是依然一头雾水&#xff1f;来看下面两个例子 无条件传送 假设你正在使用键盘打字。当你敲击键盘上的一个键时&#xff0c;键盘会立即产生一个信号&#xff08;即输入数据&#xff09;&#xff0c;并且这个信号会立即被电脑接收。在这个过程中&…

属于程序员的浪漫,一颗会跳动的心!!!

绘制一颗会跳动的心❤ 嘿嘿 可以说是程序员的专属浪漫了吧&#xff0c;就像点燃一颗LED灯一样&#xff1f;&#xff08;我瞎说的啊&#xff0c;大家别当真&#xff0c;我很菜的&#xff01;&#xff01;&#xff01;&#xff01;&#xff09; 程序就在下面啦&#xff0c;然…

Markdown魔法手册:解锁高效写作的新技能

边使用边更新0.0... 文章目录 一、如何在Markdown中插入表情&#xff1f;二、文字样式设置1.文本颜色设置2.文本字号设置3.文本字体设置4. 实战演练5.黄色高亮 一、如何在Markdown中插入表情&#xff1f; 在Markdown中插入表情&#xff08;emoji&#xff09;的方法取决于你使用…

C#--SVG矢量图画法示例

1.代码示例 <Viewbox Grid.Column"1" Grid.ColumnSpan"1" Grid.RowSpan"1" ><Path Name"ValveShape" Stroke"Black" Data"M 50,0 L 150,200 L 50,200 L 150,0 Z" Width"200" Height"…

网络安全等级保护:正确配置 Linux

正确配置 Linux 对Linux安全性的深入审查确实是一项漫长的任务。原因之一是Linux设置的多样性。用户可以使用Debian、Red Hat、Ubuntu或其他Linux发行版。有些可能通过shell工作&#xff0c;而另一些则通过某些图形用户界面&#xff08;例如 KDE 或 GNOME&#xff09;工作&…

uni-app微信小程序动态切换tabBar,根据不同用户角色展示不同的tabBar

前言 在UniApp的开发小程序过程中&#xff0c;为了针对不同角色用户登录后的个性化需求。通过动态权限配置机制&#xff0c;能够根据用户的角色展示不同的TabBar。此项目是通过Uni-App命令行的方式搭建的Vue3ViteTsPiniaUni-ui的小程序项目 最终效果 1、司机角色&#xff1a; …

Kubernetes(K8S) 集群环境搭建指南

Kubernetes&#xff08;简称K8s&#xff09;是一个开源的容器编排平台&#xff0c;旨在自动化部署、扩展和管理容器化应用。K8S环境搭建过程比较复杂&#xff0c;涉及到非常多组件安装和系统配置&#xff0c;本文将会详细介绍如何在服务器上搭建好Kubernetes集群环境。 在学习…

MybatisPlus中自定义sql

背景 在开发过程中&#xff0c;可能会出现除了where条件&#xff0c;其它sql比较复杂&#xff0c;这时候就需要用到自定义sql了。 问题 如&#xff1a;用户状态为正常的数据年龄加一&#xff08;所有用户年龄加一&#xff09; 数据库中sql&#xff1a; UPDATE USER SET…

Go GORM介绍

GORM 是一个功能强大的 Go 语言 ORM&#xff08;对象关系映射&#xff09;库&#xff0c;它提供了一种方便的方式来与 SQL 数据库进行交互&#xff0c;而不需要编写大量的 SQL 代码。 GORM的关键特性 全功能的ORM&#xff1a;支持几乎所有的ORM功能&#xff0c;包括模型定义、基…

【漏洞复现】英飞达医学影像存档与通信系统 WebJobUpload 任意文件上传漏洞

0x01 产品简介 英飞达医学影像存档与通信系统 Picture Archiving and Communicaton System&#xff0c;它是应用在医院影像科室的系统&#xff0c;主要的任务就是把日常产生的各种医学影像(包括核磁&#xff0c;CT&#xff0c;超声&#xff0c;各种X光机&#xff0c;各种红外仪…

如何编辑 PDF 中的文本?4个有效的编辑PDF方法

PDF 文件可以轻松打开和查看&#xff0c;但修改要复杂得多 - 尤其是在 PDF 中的文本编辑方面。 知道如何离线编辑 PDF 中的文本对于任何需要快速更改而无需在线加载文档或担心安全问题的人来说都非常有益。它使用户能够更好地控制他们的文档&#xff0c;并有更广泛的字体和图形…