C++Linux网络编程基础

动态库和静态库

当动态库和静态库同时存在的时候,会优先使用动态库

静态库

1. 制作静态库

g++ -c -o lib库名.a 源文件代码清单

-c表示只编译,-o则是说明需要指定文件名

2. 使用静态库
g++ 选项 源代码文件名清单 -l库名 -L库文件所在的目录名
3. 库文件的概念

程序在编译时,会将库文件的二进制代码链接到目标程序中,这种方式称为静态编译
如果多个程序中用到了同一个静态库中的函数,就会存在多份拷贝。

4. 静态库的特点
  • 静态库的链接是在编译时期完成的,执行的时候代码加载速度快。
  • 目标程序的可执行文件比较大,浪费空间
  • 程序的更新和发布不方便,如果某一个静态库更新了,所有使用它的程序都需要重新编译

动态库

1. 制作动态库
g++ -fPIC -shared -o lib库名.so 源代码文件清单
2. 使用动态库
g++ 选项 源代码文件名清单 -l库名 -L库文件所在的目录名

需要注意的是:运行可执行程序的时候,需要提前设置LD_LIBRARY_PATH环境变量。

3. 动态库的概念

程序在编译时不会把库的二进制代码链接到目标程序中,而是在运行的时候才被载入。
如果多个程序中用到了同一动态库中的函数,那么在内存中只有一份,避免了空间浪费问题。

4. 动态库的特点
  • 程序在运行的过程中,需要用到动态库的时候才把动态库的二进制代码载入内存
  • 可以实现进程之间的代码共享,因此动态库也称为共享库
  • 程序升级比较简单,不需要重新编译程序,只需要更新动态库就行

makefile

makefile是一个编译规则文件,用于实现自动化编译
[[…/杂项/Makefile|Makefile]]中有写

main函数的参数

main函数有三个参数,分别是argc、argv和envp:

int main(int argc, char* argv[], char* envp[]){ }
  • argc:存放了程序参数的个数,包括程序本身
  • argv:字符串数组,存放了每个参数的值,包括程序名本身
  • envp:字符串数组,存放了环境变量,数组的最后一个元素是空

什么叫包括程序本身?
在Linux中,我们想要运行这个程序,就需要在终端中使用指令:

./程序名

其实这就相当于将程序名作为一个参数传递给main函数,因此不管什么时候,argc最小都为1,但是我们在终端输入的时候可能还有别的情况:

./程序名 Hello World

此时这个main函数就接收了三个参数,即:argc = 3,此时的argv为:

argv[0] = "./程序名"
argv[1] = "hello"
argv[2] = "world"

操作环境变量

1. 设置环境变量

使用函数setenv():(这个函数是POSIX提供的,因此只能够在Linux系统中使用

int setenv(const char* name, const char* value, int overwrite);
  • name:环境变量名
  • value:环境变量的值
  • overwrite:这个变量的值有两种情况:0和非0
    • 0:如果环境变量不存在,则增加新的环境变量;如果环境变量已经存在,不替换它的值
    • 非0:如果环境变量不存在,则增加新的环境变量;如果环境变量已经存在,替换它的值
  • 返回值:0(成功),-1(失败)
注意事项

此函数设置的环境变量只对本进程有效,不会影响shell的环境变量
也就是说,如果执行了setenv()函数后关闭了该程序,上次的设置失效。

获取环境变量的值

char* getenv(const char* name);

这个函数就更简单了,好像也没什么好说的。
但是这个函数与setenv不同,getenv()是C/C++库提供的,在stdlib.h(cstdlib)中

gdb常用命令

gdb(GNU symbolic debugge)是C/C++最常用的调试工具,gdb通常需要手动安装。

1. 安装gdb

sudo apt install gdb

2. gdb常用命令

如果希望程序可调试,编译的时候需要添加-g(gdb的缩写)选项,并且不能够使用-O选项进行优化
在开始调试之前,需要输入指令:

gdb 目标程序
命令简写命令说明
set args设置程序运行的参数,例如:set args 需要输入的参数
breakb设置断点(可以有多个),例如:b 20,表示在第20行设置断点
runr开始运行程序,或在程序运行结束后重新开始执行
nextn执行当前行语句,如果该语句为函数调用,不会进入函数内部
steps执行当前行语句,如果该语句为函数调用,则会进入函数内部(有源码才能进)
print()p显示变量或者表达式的值,如果p后面是表达式,会执行这个表达式
continuec继续运行程序,遇到下一个断点停止,如果没有遇到断点,程序将会一直运行
set var设置变量的值
quitq退出gdb模式

Linux的时间操作

UNIX操作系统根据计算机产生的年代把1970年1月1日作为UNIX的纪元时间,1970年1月1日是时间的中间点,将从1970年1月1日起经过的秒数用一个整数存放

time_t

time_t用于表示事件类型,它是long类型的别名,在头文件time.h中定义,用于表示1970年1月1日到0时0秒到现在的秒数

time()

time函数用于获取操作系统的当前时间,需要使用头文件time.h
它有两种使用方法:

  1. 将空地址传给time(),并将time的返回值赋值给now:
    #include <time.h>time_t now = time(0);
    
  2. 将变量的地址作为参数传递给time():
    #include <time.h>
    time_t now;
    time(&now);
    

tm结构体,localtime()和mktime()

time_t是一个长整数,不符合人类的使用习惯,需要转换成tm结构体,tm结构体在头文件time.h中:

struct tm{int tm_year; // 年份:其值等于实际年份减去1970int tm_mon;  // 月份:取值区间为[0, 11]int tm_mday; // 日期:一个月中的日期,取值区间为[1, 31]int tm_hour; // 时:取值区间为[0, 23]int tm_min;  // 分:取值区间为[0, 59]int tm_sec;  // 秒:取值区间为[0, 59]int tm_wday; // 星期:取值区间为[0, 6],0是星期天,6是星期六int tm_yday; // 从每年的1月1日开始算起的天数,取值区间为[0, 365]int tm_isdst;// 夏令时标识符(没啥用)
}

想要将time_h转换为tm结构体,需要使用库函数localtime,需要使用头文件time.h
需要注意的是:loacaltime()不是线程安全的(因为它使用一个静态的结构来存储转换后的本地时间,并返回指向该结构的指针),而localtime_r()是线程安全的(它接受一个指向存储结构的指针作为参数,并将转换后的本地时间存储在该结构中,而不需要使用静态的存储)

struct tm *localtime(const time_t* timep);
struct tm *localtime_r(const time_t* timep, struct tm* result);

若是要将tm结构体转换成time_t,就需要使用库函数mktime,它也在time.h中:

time_t mktime(struct tm* tm);

该函数主要用于时间的计算

gettiemofday()

该函数用于获取1970年1月1日到现在的秒和当前秒钟已逝去的微妙数,可用于程序计时,该函数在头文件sys/time.h钟。

int gettimeofday(struct timeva* tv, struct timezone* tz);struct timeval{time_t        tv_sec;  // secondssusenconds    tv_usec; // microseconds
};struct timezone{           // 时区int tz_minuteswest;    // minutes west of Greenwichint tz_dsttime;        // type of DST correction
};

程序睡眠

如果需要将程序挂起一段时间,可以使用sleep()和usleep()两个库函数,需要使用头文件unistd.h

unsigned int sleep(unsigned int seconds); // 单位是秒
int usleep(useconds_t usec);              // 单位是微秒

目录操作函数

1. 获取当前目录函数getcwd()和get_current_dir_name()

getcwd()和get_current_dir_name(),这两个函数都在头文件unistd.h中:

char* getcwd(char* buf, size_t size);
char* get_current_dir_name(void);

这两个函数功能上没什么区别:

#include <iostream>
#include <unistd.h>
using namespace std;int main(){char path1[256];    // linux系统目录的最大长度嘶255getcwd(path1, 256);cout << "path1 = " << path1 << endl;char* path2 = get_current_dir_name();cout << "path2 = " << path2 << endl;free(path2);        // 注意释放内存
}

注意事项:get_currrent_dir_name()会动态分配内存,需要使用char*进行接收,并且这块内存需要我们进行手动释放,并且需要注意,get_current_dir_name()中使用的是malloc进行内存分配,因此我们在释放的时候也要使用freenew、delete、malloc、free不能混用!混用可能会导致问题

2. 切换工作目录chdir()、创建目录mkdir()和删除目录rmdir()

切换工作目录chdir()

切换工作目录函数chdir需要包含头文件unistd.h

#include <unistd.h>int chdir(const char* path);

若是返回值为0则表示切换成功,若非0则失败(目录不存在或没有权限)

创建目录mkdir()

创建目录的函数名就是Linux中创建目录的命令名mkdir,它需要使用头文件sys/stat.h

#include <sys/stat.h>int mkdir(const char* pathname, mode_t mode);

可以看到该函数有两个参数:

  • pathname:目录名
  • mode:访问权限的数字写法,如:0755(不能省略前置的0,因为权限数字是八进制

返回值和chdir()一样。

删除目录rmdir()

使用过rmdir()需要包含头文件unistd.h

int rmdir(const char* path);

path就是要删除的目录的路径,返回值和chdir()也是一样的。

获取目录中文件的列表

这一系列的操作都需要使用头文件dirent.h(dir event),一共有三个步骤:

步骤一:用opendir()打开目录
DIR* opendir(const char* pathname);

若是成功,返回目录的地址;若是失败,返回空地址。

步骤二:用readdir()读取目录
struct dirent* readdir(DIR* dirp);

若是成功过,返回struct dirent结构体的地址;若是失败,返回空地址。

步骤三:用closedir()关闭目录
int closedir(DIR* dirp);

相关的数据结构DIR

在上面的函数中,我们使用了目录指针DIR*,每调用一次readdir(),含税返回struct dirent的地址,存放了本次读取到的内容

struct dirent{long d_ino;                 // inode number索引节点号off_t d_off;                // offset to this dirent在目录文件中的偏移unsigned short d_reclen;    // length of this d_name文件长度名unsigned char d_type;       // the type of d_name文件类型char d_name[NAME_MAX + 1];  // file name文件名,最长255字符(因为是Linux系统)
};

重点在d_named_type

  • d_name是文件名或目录名
  • d_type是文件类型,有多种取值,这里我们只关注两种:
    • 8:常规文件
    • 4:目录

Linux的系统错误

在C++程序中,如果调用了库函数,可以通过函数的返回值判断调用是否成功。其实还有一个整型的全局变量errno,存放了函数调用过程中产生的错误代码。
如果调用库函数失败,可以通过errno的值来查找原因,这也是调试程序的一个重要方法。
使用errno需要包含头文件errno.h(或cerrno),配合strerror()和perror()两个库函数,可以差点出错的详细信息。

strerror()

strerror()在头文件string.h中声明,用于获取错误代码对应的详细信息。它有两个版本,一个线程安全,一个非线程安全:

char* strerror(int errnum);                             // 非线程安全
char* strerror_r(int errnum, char* buf, size_t buflen); // 线程安全

这里给出一段示例代码:

#include <string.h>
#include <iostream>
using namespace std;int main(){int ii;for(ii=0; ii<150; ii++){ // gcc 8.3.1 一共有133个错误代码cout << ii << ":" << strerror(ii) << endl;}
}

运行这段代码,能看到0133都是有语句输出的,其中:==0表示程序正常运行,1133是错误信息==。

perror()

perror()在头文件stdio.h中声明,用于在控制台显示最近一次系统错误的详细信息,在实际开发中,服务程序在后台运行,通过控制台显示错误信息意义不大:

void perror(const char* s);

注意事项

1. 调用库函数失败不一定会设置errno

并不是全部的库函数在调用失败时都会设置errno的值,以man手册为准(不属于系统调用的函数不会设置errno,即:操作系统(OS)提供的库才会设置errno

2. errno不能作为调用函数失败的标志

errno的值只有在库函数调用发生错误时才会被设置,当库函数调用成功时,errno的值不会被修改,不会主动得置为0。
在实际开发中,判断函数执行是否成功还得靠函数的返回值,只有在返回值是失败的情况下,才需要关注errno的值。

目录和文件的更多操作

access()

access()用于判断当前用户对目录或文件的存取权限,需要包含头文件unistd.h

int access(const char* pathname, int mode);
  • pathname:目录或文件名
  • mode:需要判断的存取权限,在unistd.h中存在如下宏定义:
      #define R_OK 4 // 判断是否有读权限#define W_OK 2 // 判断是否有写权限#define X_OK 1 // 判断是否有执行权限#define F_OK 0 // 判断是否存在
    
  • 返回值:若是pathname满足mode权限就返回0;不满足就返回-1,并设置errno(这也说明unistd.h是Linux提供的库

stat()与stat结构体

(略)

rename()

rename()函数在头文件stdio.h中,用于重命名目录或文件,相当于操作系统的mv命令

int rename(const char* oldpath, const char* newpath);
  • oldname:原目录或文件名
  • newpath:目标目录或文件名
  • 返回值:0(成功),-1(失败,并设置errno)

在实际开发中,access()主要用于判断目录或文件是否存在。

remove()

remove()函数在头文件stdio.h中,用于删除目录或文件,相当于操作系统的rm命令

#include <stdio.h>int remove(const char* pathname);
  • pathname:待删除的目录或文件名
  • 返回值:0(成功),-1(失败,并设置errno)

Linux中的信号

信号的基本概念

信号(signal)是软件中断,是进程之间相互传递消息的一种方法,用于通知进程发生了事件,但是,不能给进程传递任何数据。
信号产生的原因有很多,在Shell中,可以用killkillall命令发送信号:

kill -信号的类型 进程编号
killall -信号的类型 进程名

信号处理、

进程对信号的处理方法有三种:

  1. 对该信号的处理采用系统的默认操作,大部分的信号的默认操作是终止进程
  2. 设置终端的处理函数,收到信号后,由该函数来处理
  3. 忽略某个信号,对该信号不做处理,就像未发生过一样

主要是通过signal函数来设置对信号的处理方式,需要包含头文件signal.h

sighandler_t signal(int signum, sighander_t handler);
  • signum:信号的编号,在Linux中默认有64种编号(0~63,其中很大一部分属于自定义信号,默认是终止进程
  • handle:信号的处理方式
    • SIG_DFL:恢复参数signum信号的处理方式为默认行为
    • 一个自定义的处理很好的函数,函数的形参是信号的编号
    • SIG_IGN:忽略参数signum所指的信号
      这里给出一段示例:
// 如果接收到信号1,就执行func函数中的内容
signal(1, func);

发送信号

可以使用kill库函数发送信号:

int kill(pid_t pid, int sig);
  • pid:指定的进程
  • sig:所指定的需要发送的信号

其他内容后续再来补充

进程终止

一共有八种方式可以终止进程,其中5种为正常终止:

  1. main()中使用return返回
  2. 在任意函数中调用exit()
  3. 在任意函数中调用_exit()或_Exit()
  4. . 最后一个线程中其启动例程(线程主函数)用return返回
  5. 在最后一个线程中调用pthread_exit()返回

还有3种异常终止:

  1. 调用abort()终止
  2. 接收到一个信号
  3. 最后一个线程对取消请求做出响应

进程终止的状态

main()中,return的返回值即终止状态,如果没有return语句或调用exit(),那么该进程的终止状态是0
在Shell中,查看进程的终止状态:

echo &?

正常终止进程的三个函数:

  • exit()
  • _Exit()
  • _exit()

其中,前两个是ISO C说明的,_exit()是POSIX说明的:

void exit(int status);
void _exit(int status);
void _Exit(int status);

status即为进程终止的状态。
如果进程不是正常终止,打印的终止状态为非0

调用可执行程序

Linuz提供了system()和exec()函数族,在C++程序中,可以执行其他的程序(二进制文件,操作系统命令或Shell脚本)

system()

system()提供了一种简单的执行程序的方法,需要使用头文件stdlib.h,把需要执行的程序和参数用一个字符串传给system()就行了。

int system(const char* string);

system()的返回值比较麻烦:

  • 如果函数执行失败,system()返回值非0
  • 如果程序执行成功,并且被执行的程序终止状态是0,此函数的返回值即为0
注意事项

在使用此函数的时候,传递的参数最好使用全路径,这样可以避免环境变量的问题

exec函数族

exec函数族提供了另一种在进程中调用程序(可执行文件或Shell脚本)的办法:

int execl(const char* path, const char* arg, ...);
int execlp(const char* file, const char* arg, ...);
int execle(const char* path, const char* arg, ..., char* const envp[]);
int execcv(const char* path, char* const argv[]);
int execvp(const char* file, char* const argv[]);
int execvpr(const char* file, char* const argv[], char* const envp[]);

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

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

相关文章

conda国内加速

1、配置国内源 conda config --add channels https://mirrors.tuna.tsinghua.edu.cn/anaconda/pkgs/free/ conda config --add channels https://mirrors.tuna.tsinghua.edu.cn/anaconda/pkgs/main/ 2、显示源地址 conda config --set show_channel_urls yes

【Arduino】基于 I2C 模块通过 I2C 接口驱动 LCD1602 字符液晶显示模块显示文本:只需两个引脚即可控制 LCD1602 液晶屏

引言 LCD1602是一种16列2行的字符液晶显示模块&#xff0c;常用于Arduino等嵌入式系统的用户接口。为了简化连接和编程&#xff0c;我们将使用I2C接口&#xff0c;这只需要Arduino的两个模拟输入引脚。 步骤 安装 LiquidCrystal_I2C 库 为了在Arduino中使用I2C模块驱动LCD显…

Linux的Shell程序(全面超详细的介绍)

文章目录 前言1.Shell概述1.1概述 2.Shell解析器3.Shell脚本入门4.Shell中的变量4.1 系统变量4.2 自定义变量4.3 特殊变量&#xff1a;$n4.4 特殊变量&#xff1a;$#4.5 特殊变量&#xff1a;\$*、$4.6 特殊变量&#xff1a;$&#xff1f; 5.运算符6.条件判断7.流程控制7.1 if …

五种List集合的简单实现

五种List集合的简单实现 一、数组形式二、单向链表形式三、含哨兵节点的单向链表形式四、含哨兵节点的双向链表形式五、含哨兵节点的环形链表形式 本文是对不同形式List集合的增删改查实现&#xff0c;仅是对学习过程进行记录 一、数组形式 关键点&#xff1a; 有三个成员变量…

Hive-SQL语法大全

Hive SQL 语法大全 基于语法描述说明 CREATE DATABASE [IF NOT EXISTS] db_name [LOCATION] path; SELECT expr, ... FROM tbl ORDER BY col_name [ASC | DESC] (A | B | C)如上语法&#xff0c;在语法描述中出现&#xff1a; []&#xff0c;表示可选&#xff0c;如上[LOCATI…

面试高频知识点:1集合 1.2 ConcurentHashMap是如何实现线程安全的?(1.8之前后区别)

ConcurrentHashMap&#xff08;并发哈希表&#xff09;是Java集合框架中的一种实现Map接口的类&#xff0c;它专为多线程环境设计&#xff0c;以提供更好的性能和线程安全。在理解 ConcurrentHashMap 是如何实现线程安全的时候&#xff0c;我们可以分别探讨 JDK 1.8 之前和之后…

CGAL 网格法向量计算

文章目录 一、简介二、实现代码三、实现效果参考资料一、简介 在计算机图形三维曲面中,严格意义上来讲其实并不存在我们数学层面上所定义的曲面结构,因为计算机中的曲面图形一般都是有一个个三角形或者是多边形而组成的,因此所谓曲面的法向量其实就是在计算每一个三角形或多…

【操作系统和计网从入门到深入】(五)软硬链接和动静态库

前言 这个专栏其实是博主在复习操作系统和计算机网络时候的笔记&#xff0c;所以如果是博主比较熟悉的知识点&#xff0c;博主可能就直接跳过了&#xff0c;但是所有重要的知识点&#xff0c;在这个专栏里面都会提到&#xff01;而且我也一定会保证这个专栏知识点的完整性&…

Lombok:简化JavaBeans的神器

前言 Lombok 是一个 Java 库&#xff0c;它通过注解的方式帮助我们自动生成构造器、getter/setter、equals、hashCode、toString 等方法&#xff0c;极大地简化了 JavaBean 的编写。对于经常需要编写大量的样板代码的工作来说&#xff0c;Lombok 提供了一种优雅的解决方案。 …

Linux内核--网络协议栈(四)sk_buff介绍

目录 一、引言 二、sk_buff ------>2.1、skb介绍 ------>2.2、控制字段 ------>2.3、其他字段 ------>2.4、特定功能字段 ------>2.5、管理字段 ------>2.6、内存分配 ------>2.7、内存释放 ------>2.8、克隆和拷贝 ------>2.9、队列管理…

通信入门系列——连续卷积定理、循环卷积、离散卷积定理

本节目录 一、连续卷积定理 1、时域卷积定理 2、频域卷积定理 二、循环卷积 三、离散卷积定理本节内容 一、连续卷积定理 卷积定理在信号分析中占有重要的地位&#xff0c;包括时域卷积定理和频域卷积定理。在信号分析领域&#xff0c;通常采用基于卷积定理的时频域分析&#…

Zuul1.x 高并发下阻塞分析以及解决方案

背景 由于最近博主在压测接口的时候发现我接口出现卡死状态&#xff0c;最开始以为是我自己接口出现问题&#xff0c;单独压测我自己的服务&#xff08;不经过网关&#xff09;200/qps/10 次循环 是没问题&#xff0c;但是加上网关&#xff08;zuul 1.x&#xff09; 去发现 经…

编曲学习:Cubase12导入Cubasis工程的方法!

Steinberg 发布 Cubasis 3 项目导入器&#xff0c;可将 Cubasis 的项目导入到 Cubase 使用https://m.midifan.com/news_body.php?id35635 我偶然看到这个文章&#xff0c;不过发现Cubase12默认好像没有这个选项&#xff0c;心想着要是移动端能和PC端同步&#xff0c;感觉会挺…

【网站项目】基于jsp的199旅游景点管理系统

&#x1f64a;作者简介&#xff1a;多年一线开发工作经验&#xff0c;分享技术代码帮助学生学习&#xff0c;独立完成自己的项目或者毕业设计。 代码可以私聊博主获取。&#x1f339;赠送计算机毕业设计600个选题excel文件&#xff0c;帮助大学选题。赠送开题报告模板&#xff…

C++中的排序操作:sort与自定义排序(自定义排序函数、匿名函数、运算符重载)

在C编程中&#xff0c;排序是一项常见而又重要的操作。本文将深入介绍C标准库中的sort算法&#xff0c;以及如何利用其强大的自定义排序功能满足各种排序需求。 sort算法简介 C标准库提供了sort算法&#xff0c;能够在O(N log N)的时间内对容器中的元素进行排序。这一高效的排…

快速下载百度网盘的文件——使用motrix

问题描述 下载速度慢 上传速度快 解决方案&#xff1a; Motrix 在该开源程序里面 选windows选择zip 启动之后 &#xff0c;把百度网盘的链接转化成磁力链接。然后输入转化后的连接。转换的网页 每次设置下载认任务是选择高级选项里面的请求头 修改为LogStatistic 然后就能超…

“低绩效”指南

前言 一看这个标题&#xff0c;大家是否有个疑问&#xff0c;为啥是低绩效指南&#xff0c;而不是高绩效指南。可以这么说&#xff0c;能拿到高绩效的人大多有共通之处&#xff0c;然而获得低绩效的同学原因各不相同&#xff0c;针对笔者总结的一些经验或许能帮助大家反向操作…

Qt5编译MySQL数据驱动、部署MySQL服务器、Qt写代码连接MySQL数据库_案例介绍

一、前言 由于Qt 5在高版本中取消了对MySQL数据库的默认支持,要在QT里继续使用mysql需要自己编译库。本篇文章介绍Qt5(我用的Qt5.12.6)里如何编译MySQL的库文件,讲解在Linux下安装配置MySQL数据库,Qt编写代码连接上自己的MySQL数据库完成数据存储。 MySQL是一个开源的关…

Odrive 学习系列四:如何使用脚本自动初始化odrive配置

一、背景: 在学习markbase的教程后,发现odrive的初始化配置命令确实有点多。尽管odrive有自动补全: 且可以通过 ctrl + → 来快速补全: 但是对初学者而言,仍旧有比较大的工作量。 而针对于此,我们可以通过powershell脚本的方式来解决这个问题。 二、设计初始化…

接口测试 03 -- 接口自动化思维 Requests库应用

1. 接口自动化思维梳理 1.1接口自动化的优点 接口测试自动化&#xff0c;简单来讲就是功能测试用例脚本化然后执行脚本&#xff0c;产生一份可视化测试报告。不管什么样的测试方式&#xff0c;都是为了验证功能与发现 BUG。那为什么要做接口测试自动化呢&#xff1f;一句话概括…