持续更新
一 基础外设
1.为何要设置栈?栈的作用?
栈的作用为:(1)保存现场/上下文;(2)传递参数:汇编调用C函数进行传参;(3)保存临时变量
2.CPU工作的核心是什么?
时钟,对于CPU而言,可理解为由逻辑门组成,在特定输入下,会有相应的输出。逻辑运算为了保证操作顺序的稳定,
会将时钟作用于寄存器上,从而实现运算的控制,实现同步
3.单片机执行过程?
关看门狗:防止程序重启
设置时钟:设置机器运行频率
初始化SDRAM:初始化内存
重定位
4.Nor 与 Nand区别?
Nor的接口与RAM一致,可随意访问任意地址数据;Nor 容量比Nand小;代码能够给直接在Nor上运行;Nor擦除速度比Nand慢;Nand更适合存储数据
5.同步与异步?
同步指发送方发出数据后,等待收方发回响应以后才发下一个数据包的通讯方式
异步指发送方发出数据后,不等接收方响应,接着发下个数据包的通讯方式
6.单工、半双工、全双工?
单工数据传输只支持数据在一个方向上传输;
半双工数据传输允许数据在两个方向上传输,但是,在某一时刻,只允许数据在一个方向上传输,它实际上是一种切换方向的单工通信;
全双工数据通信允许数据同时在两个方向上传输,因此,全双工通信是两个单工通信方式的结合,它要求发送设备和接收设备都有独立的接收和发送能力。
7.UART、I2C、SPI?
UART:全双工,异步通信,不需要时钟线,通过起始位与停止位及波特率进行数据识别,传输速率较低
I2C:半双工,包含时钟线与数据线,支持多主机多从机,速度比SPI慢
具体请看:https://blog.youkuaiyun.com/MoLiYw/article/details/101103224
SPI:全双工,同步串行传输,传输速率高,只支持单主机
具体请看:https://blog.youkuaiyun.com/MoLiYw/article/details/101106543
8.ADC原理?
- 采样
- 保持
- 量化
- 编码
9.LCD原理?
请看文章:https://blog.youkuaiyun.com/MoLiYw/article/details/101108969
10.触摸屏原理?
11.为何要关闭cache?
使用cache是为了提高系统性能,但由于cache的使用可能会改变访问主存的数量、类型和时间,因此bootloader不需要
12.什么叫位置无关码?
13.UART工作原理?
- UART设计采用模块化思想,分为数据发送模块、数据接收模块、波特率发生器控制模块。CPU将并行数据写入UART,发送模块由并行输入到串行输出,接收则相反,波特率用于时钟频率,控制数据位时间。
- 在串口异步通信中,数据通过移位寄存器逐位发送,且每次为一字节,这需要发送端与接收端按相同的字节帧格式与波特率通信。字节帧中规定了起始位、数据位、奇偶校验位、停止位。
二 ARM体系结构
1.ARM内核工作模式?
7种:
- usr(用户)
- sys(系统模式)
- svc(管理)
- irq(中断)
- abt(终止)
- und(未定义模式)
- fiq(快中断)
2.ARM对异常的处理?
保护现场->处理异常->恢复现场 利用CPSR寄存器
3.常用汇编指令?
属性 | 指令 | 介绍 |
---|---|---|
跳转 | B | 绝对跳转指令 |
BL | 相对跳转指令 | |
数据处理 | MOV | 数据传送 |
CMP | 比较 | |
ADD | 加法 | |
SUB | 减法 | |
AND | 逻辑与 | |
EOR | 逻辑异或 | |
ORR | 逻辑或 | |
乘法 | MUL | 32位乘 |
状态寄存器访问 | MRS | 状态寄存器到通用寄存器传送 |
内存访问 | LDR | 字数据读取 |
STR | 字数据写入 | |
异常中断 | SWI | 软中断指令 |
ARM协处理器 | MRC | 协处理器到ARM寄存器数据传送 |
MCR | ARM寄存器到协处理器寄存器数据传送 |
三 操作系统
1.进程标识?
- PID(Process ID 进程 ID号)
- TGID(Thread Group ID 线程组 ID号)
- PGID(Process Group ID 进程组 ID号)
- PPID( Parent process ID 父进程 ID号)
- SID(Session ID 会话ID)
2.进程与线程之间关系?
- 进程是资源分配的最小单位,线程是CPU调度的最小单位。
- 进程有自己独立空间;线程没有独立空间
多进程与多线程:
对比 | 进程 | 线程 | 总结 |
---|---|---|---|
数据共享,同步 | 共享复杂,IPC;数据分开,同步简单 | 共享简单;同步复杂 | 各有优势 |
内存,CPU | 占用内存多,CPU利用率低 | 占用内存少,CPU利用率高 | 线程占优 |
切换 | 创建销毁、切换复杂;速度慢 | 创建销毁、切换简单;速度快 | 线程占优 |
可靠性 | 进程间不会影响 | 一个线程挂掉将导致整个进程挂掉 | 进程占优 |
分布式 | 适用于多核,多机分布式 | 适用于多核分布式 | 进程占优 |
3.进程与线程的选择?
- 需要频繁创建销毁的优先用线程;
- 需要进行大量计算的优先使用线程;
- 强相关的处理用线程,弱相关的处理用进程;
- 可能要扩展到多机分布的用进程,多核分布的用线程;
4.进程状态与IPC通信?
进程状态:
就绪、运行、阻塞
IPC:
- 管道:由内核管理的一个缓存区;半双工
- 信号:用于一个或几个进程之间传递异步信号
- 消息队列:消息队列是内核地址空间中的内部链表
- 共享内存:共享内存是在多个进程之间共享内存区域的一种进程间的通信方式
- 信号量:信号量是一种计数器,用于控制对多个进程共享的资源进行的访问
- 套接字:套接字机制不但可以单机的不同进程通信,而且使得跨网机器间进程可以通信
IPC | 优点 | 缺点 |
---|---|---|
管道 | 自身具备同步机制 | 只能用于有亲缘关系进程之间的通信;只支持单向数据流 |
信号 | 异步通信 | 可携带信息少,不具备同步机制 |
消息队列 | 提供有格式字节流,消息有类型或优先级 | 速度相比较慢,在新的应用程序中不应当再使用 |
共享内存 | 最快,效率高 | 自身不具备同步机制 |
信号量 | 速度快于记录锁 | 复杂 |
socket | 实现简单,同步机制 |
5.临界区?
临界区是一个访问共用资源的程序片段,而这些资源又无法被多个线程同时访问的特性。
- 如果有若干进程要求进入空闲的临界区,一次仅允许一个进程进入;
- 任何时候,处于临界区内的进程不可多于一个。如已有进程进入自己的临界区,则其它所有试图进入临界区的进程必须等待;
- 进入临界区的进程要在有限时间内退出,以便其它进程能及时进入自己的临界区;
- 如果进程不能进入自己的临界区,则应让出CPU,避免进程出现“忙等”现象。
对于多线程对临界区访问,可以用线程同步来解决
互斥量(加锁)
信号量
6.线程同步方式?
互斥量、信号量、事件(信号)
7.死锁?
资源数量有限、锁和信号量错误使用。
死锁条件:
- 互斥使用(资源独占):一个资源每次只能给一个进程使用
- 占有且等待(请求和保持,部分分配):进程在申请新的资源的同时保持对原有资源的占有。
- 不可抢占(不可剥夺):资源申请者不能强行的从资源占有着手中多去资源,资源只能由占有着自愿释放
- 循环等待:若干进程之间形成一种头尾相接的环形等待资源关系
死锁预防:
防止条件中任何一个发生
8.并发设计?
8.1 简单介绍并发编程?
逻辑流在时间上重叠。在访问慢速I/O设备、人机交互、推迟工作以降低延迟、服务多个网络客户端等需要。
8.2 构建并发编程?
- 进程(内核调度与维护,虚拟地址空间独立,采用IPC通信);
- I/O多路复用(应用程序在一个进程的上下文显示调度逻辑流,共享地址空间);
- 线程(共享虚拟地址空间)。
8.3 比较
方式 | 优点 | 缺点 | 场合 |
---|---|---|---|
多进程 | 最简单,有独立地址空间 | 共享信息复杂,速率慢 | 目标子动能交互少,如果资源和性能许可,可以设计由多个子应用程序来组合完成目的 |
I/O多路复用 | 更多对程序行为的控制 | 编码复杂,不能充分利用多核处理器;每个逻辑流都能够访问该进程全部地址空间 | 客户端要处理多个socket;客户端同时处理连接和用户输入;TCP同时监听socket和连接socket;服务器同时处理TCP和UDP;服务器监听多个端口 |
多线程 | 逻辑控制简单,共享内存,消耗低 | 一个线程崩溃导致整个程序崩溃,CPU占用高 | 常见的浏览器、web服务;需要频繁创建销毁的优先用线程;需要进行大量计算的优先使用线程; |
8.4 多路复用select、poll、epoll
select O(n)
- 单个进程能监视的文件描述符数量有限;采用轮询扫描,性能差
- 内核/用户空间拷贝,需复制大量句柄数据结构,巨大开销
- 返回整个句柄的数组,需要遍历整个数组才能发现哪些句柄发生事件
- 水平触发,应用程序没有完成对一个已经就绪的文件描述符进行I/O操作,那么之后每次select调用还是会将这些文件描述符通知进程
poll O(n)
- poll采用链表保存文件描述符,没有了监视文件数量的限制,但其他三个缺点存在
epoll O(1)
- epoll避免以上缺点,通过红黑树和双链表数据结构,并结合回调机制,造就其高效性
9.分页与分段差别?
- 段式存储管理是一种符合用户视角的内存分配管理方案。在段式存储管理中,将程序的地址空间划分为若干段(segment),
如代码段,数据段,堆栈段;这样每个进程有一个二维地址空间,相互独立,互不干扰。
段式管理的优点是:没有内碎片(因为段大小可变,改变段大小来消除内碎片)。
但段换入换出时,会产生外碎片(比如4k的段换5k的段,会产生1k的外碎片) - 页式存储管理方案是一种用户视角内存与物理内存相分离的内存分配管理方案。
在页式存储管理中,将程序的逻辑地址划分为固定大小的页(page),而物理内存划分为同样大小的帧,
程序加载时,可以将任意一页放入内存中任意一个帧,这些帧不必连续,从而实现了离散分离。
页式存储管理的优点是:没有外碎片(因为页的大小固定),但会产生内碎片(一个页可能填充不满)。
10.操作系统进程调度策略?
- FCFS(先来先服务,队列实现,非抢占的):先请求CPU的进程先分配到CPU
- SJF(最短作业优先调度算法):平均等待时间最短,但难以知道下一个CPU区间长度
- 优先级调度算法(可以是抢占的,也可以是非抢占的):优先级越高越先分配到CPU,相同优先级先到先服务,存在的主要问题是:低优先级进程无穷等待CPU,会导致无穷阻塞或饥饿;解决方案:老化
- 时间片轮转调度算法(可抢占的):队列中没有进程被分配超过一个时间片的CPU时间,除非它是唯一可运行的进程。如果进程的CPU区间超过了一个时间片,那么该进程就被抢占并放回就绪队列。
- 多级队列调度算法:将就绪队列分成多个独立的队列,每个队列都有自己的调度算法,队列之间采用固定优先级抢占调度。其中,一个进程根据自身属性被永久地分配到一个队列中。
- 多级反馈队列调度算法:与多级队列调度算法相比,其允许进程在队列之间移动:若进程使用过多CPU时间,那么它会被转移到更低的优先级队列;在较低优先级队列等待时间过长的进程会被转移到更高优先级队列,以防止饥饿发生。
11.进程同步?
- 原子操作
- 信号量机制
- 自旋锁管程
- 会合
- 分布式系统
四 网络编程
1.模型?
OSI | TCP | 举例 | |
---|---|---|---|
应用层 | 应用层 | 应用层 | Telnet(远程登录协议)、FTP(文件传输协议)、SMTP(简单邮件传输协议)、DNS(域名解析)、SNMP(简单网络管理协议)、HTTP(超文本传输协议)、DHCP(动态主机分配协议)、SSH(安全外壳协议) |
表示层 | |||
会话层 | |||
传输层 | 传输层 | 传输层 | TCP(传输控制协议)、UDP(用户数据报协议) |
网络层 | 网络层 | 网络层 | IP(Internet协议)、ARP(地址解析协议)、ICMP(Internet控制报文协议) |
数据链路层 | 网络接口层 | 数据链路层 | 802.11、以太网、Wi-Fi、帧中继、GPRS |
物理层 | 物理层 | 以太网物理层、调制解调器 |
传输层:端对端接口
- 传输控制协议(TCP):进程间通信(监听输入对话建立请求;请求另一网络站点对话;可靠的发送和接收数据;适度的关闭对话)
- 用户数据报文协议(UDP):不可靠的非连接型传输层服务
网络层:控制子网运行
- Internet协议(IP):源和目的之间提供非连接型传递服务;
- 数据传送、寻址、路由选择、数据报文分析
- IP主要目的是为数据输入/输出网络提供基本算法,为高层协议提供无连接的传送服务。
- 只是封装和传递数据,不向发送/接收者报告包的状态,不处理所遇到的故障。 - 网络控制报文协议(ICMP):报告网络状态,测试和错误
- 地址解析协议(ARP):32位IP地址和48位物理地址之间执行翻译
数据链路层:物理寻址
TCP/IP协议族:
- Internrt协议
- 传输控制协议(TCP)和用户数据报协议(UDP)
- 处于TCP与UDP之上的一组应用协议:TELNET、文件传送协议(FTP)、域名服务(DNS)、简单邮件传送程序(SMTP)
2.TCP三次握手?
字段 | 含义 |
---|---|
ACK | 确认号是否有效,一般置为1 |
PSH | 提示接收端应用程序立即从TCP缓冲区把数据读走 |
RST | 对方要求重新建立连接,复位 |
SYN | 请求建立连接 |
FIN | 释放一个连接 |
2.1 TCP三次握手是什么流程?四次挥手呢?
三次握手:
- 第一次握手:建立连接时,客户端发送syn(同步序列编号,syn=j)包到服务器,并进入SYN_SEND状态,等待服务器确认
- 第二次握手:服务器收到syn包,必须确认客户的SYN(ack=j+1),同时自己也发出一个SYN(syn=k),即SYN+ACK包,此时服务器进入SYN_RECY状态
- 第三次握手:客户端收到服务器的SYN+ACK包,向服务器发送确认包ACK(ack=k+1),发送完毕,C/S进入ESTABLISHED状态
四次挥手:
- 第一次挥手:Client发送一个FIN,用来关闭Client到Server的数据传送,Client进入FIN_WAIT_1状态。
- 第二次挥手:Server收到FIN后,发送一个ACK给Client,确认序号为收到序号+1(与SYN相同,一个FIN占用一个序号),Server进入CLOSE_WAIT状态。
- 第三次挥手:Server发送一个FIN,用来关闭Server到Client的数据传送,Server进入LAST_ACK状态。
- 第四次挥手:Client收到FIN后,Client进入TIME_WAIT状态,接着发送一个ACK给Server,确认序号为收到序号+1,Server进入CLOSED状态,完成四次挥手。
2.2 为什么建立连接是三次握手,而关闭连接是四次挥手?
这是因为Server在LISTEN状态下,收到Client建立连接请求的SYN报文后,会将ACK和SYN放在一个报文中发送给Client,其中ACK报文用来应答,SYN报文用来同步。而关闭连接时,当Server收到FIN报文时,因TCP是全双工,此时的Server可能还有数据没有发送给Client,所以Server只能先回复一个ACK报文表示已经接收到了Client的FIN报文,等待Server将所有数据全部发送后才发送FIN报文标识同意关闭连接。即ACK和FIN一般都会分开发送。所以需要四次握手。
3.UDP可靠传输?
3.1 为什么要可靠?
在保证通信的时延和质量的条件下尽量降低成本
3.2 方法?
① 添加seq/ack机制,确保数据发送到对端
② 添加发送和接收缓冲区,主要是用户超时重传
③ 添加超时重传机制
RUDP:可靠用户数据报协议
RTP:实时协议
UDT:互联网数据传输协议
3.3 设计程序实现UDP可靠传输?
五 程序语言
1 程序错误判别
1.1 无符号类型数据?
int main(void)
{
unsigned char c = 10;
while(c-- >= 0)
{
}
}
error:while条件会一直为真,持续循环
Problem:无符号字符型范围为0-255;当c=0时,再次执行一次循环后,c=-1,在计算机中会采取补码形式存储,即11111111,也就是255,所以会继续执行
Solution:去掉条件中的"="号
2 内存管理
2.1 strcpy、sprintf及memcpy之间差别?
strcpy:把src开始以\0结尾的字符串复制到以dest为开始的地址空间,当源字符串的大小大于目的字符串的最大存储空间后,执行该操作会出现段错误
sprintf:把格式化字符串写入某个字符串,对写入buffer的字符数没有限制,存在溢出可能
memcpy:从源src所指的内存地址的起始位置开始拷贝n个字节到目标dest所指的内存地址的起始位置中,src和dest有可能出现空间重叠,它可以复制任何内容
void* mymemcpy(void* desc, void* src, int size)
{
if(NULL == desc && NULL == src)
return NULL;
unsigned char* desc1 = (unsigned char *)desc;
unsigned char* src1 = (unsigned char *)src;
if(desc > src && desc1 < (src1 + size)) // memory overlop
{
for(int i = size-1; i <= 0; i--)
{
*desc1++ = *src1++;
}
}
else
{
for(int i = 0; i < size; i++)
{
*desc1++ = *src1++;
}
}
return desc;
}
2.2 野指针?
- 指针定义时未被初始化:指针在被定义的时候,如果程序不对其进行初始化的话,它会指向随机区域,因为任何指针变量(除了static修饰的指针变量)在被定义的时候是不会被置空的,它的默认值是随机的。
- 指针被释放时没有被置空:我们在用malloc开辟内存空间时,要检查返回值是否为空,如果为空,则开辟失败;如果不为空,则指针指向的是开辟的内存空间的首地址。指针指向的内存空间在用free()或者delete(注意delete只是一个操作符,而free()是一个函数)释放后,如果程序员没有对其置空或者其他的赋值操作,就会使其成为一个野指针。
- 指针操作超越变量作用域:不要返回指向栈内存的指针或引用,因为栈内存在函数结束的时候会被释放
2.3 内存越界?
int main(void)
{
char a;
char* c = &a;
strcpy(c,"hello");
return 0;
}
error:内存越界
Problem:字符串"hello"占6byte,但a为1byte
2.4 内存泄漏
会发生内存泄漏的内存就是堆上的内存,也就是说由malloc系列函数或new操作符分配的内存。如果用完之后没有及时free或delete,这块内存就无法释放,直到整个程序终止。
2.5 kmalloc、vmalloc、malloc?
- kmalloc和vmalloc是分配的是内核的内存,malloc分配的是用户的内存
- kmalloc保证分配的内存在物理上是连续的,内存只有在要被DMA访问的时候才需要物理上连续,malloc和vmalloc保证的是在虚拟地址空间上的连续
- kmalloc能分配的大小有限,vmalloc和malloc能分配的大小相对较大
- vmalloc比kmalloc要慢。尽管在某些情况下才需要物理上连续的内存块,但是很多内核代码都用kmalloc来获得内存,而不是vmalloc。这主要是出于性能的考虑。vmalloc函数为了把物理内存上不连续的页转换为虚拟地址空间上连续的页,必须专门建立页表项。糟糕的是,通过vmalloc获得的页必须一个个地进行映射,因为它们物理上是不连续的,这就会导致比直接内存映射大得多的TLB抖动,vmalloc仅在不得已时才会用–典型的就是为了获得大块内存时。
6 程序设计
1 排序算法?
/*********************交换排序*********************/
/* 1.1 冒泡排序 */
void BubbleSort(int data[], int length)
{
int i,j,tmp;
bool swap; /* 添加标志位优化:前一轮有序则循环终止 */
for(j=length-1; j>0; j++)
{
swap = flase;
for(i=0; i<j; i++)
{
if(data[i] > data[i+1]) // 由小到大排序
{
tmp = data[i];
data[i] = data[i+1];
data[i+1] = tmp;
swap = true;
}
}
if(flase == swap)
{
break;
}
}
}
/* 1.2 快速排序 */
void QuicksSort(int data[], int low, int high)
{
if(low > high)
return;
int pivot = findpivot(data, low, high);
QuicksSort(data, low, pivot-1); //递归排序左子数组
QuicksSort(data, pivot+1, high); //递归排序右子数组
}
int findpivot(int data[], int low, int high)
{
int pivot = data[low];
while(low<high)
{
while((low < high) && (data[high] >= pivot))
{
high--;
}
data[low] = data[high];
while((low < high) && (data[low] <= pivot))
{
low++;
}
data[high] = data[low];
}
data[low] = pivot;
return low;
}
/****************************二 插入排序 *****************************************/
/* 2.1 直接插入 */
void InsertSort(int data[], int length)
{
int i;
for(i=1; i<length; i++)
{
int value = data[i];
int position = i;
while((position > 0) && (data[position-1] > value))
{
data[position] = data[position-1];
position--;
}
data[position] = value;
}
}
/* 2.2 希尔排序 */
void ShellSort_On2(int a[], int length)
{
int temp,delta,i,j;
for(delta = length/2; delta>=1; delta/=2)
{
for(i=delta; i<length; i++)
{
for(j=i; (j>=delta)&&(data[j]<data[j-delta]); j-=delta)
{
temp = data[j-delta];
data[j-delta] = data[j];
data[j] = temp;
}
}
}
}
/********************** 三 选择排序**********************************/
/* 3.1 直接选择 */
void SelectionSort(int a[], int length)
{
int temp,i,j;
int min = 0;
for(i=0; i<length-1; i++)
{
min = i;
for(j=i+1; j<length; j++)
{
if(data[min] > data[j])
{
min = j;
}
}
if(min != j)
{
temp = data[i];
data[i] = data[min];
data[min] = temp;
}
}
}
/************************** 四 归并排序 ***************************************/
void MergeSort(int data[], int length)
{
int temp[length];
InternalMergeSort(data, temp, 0, length-1);
}
void InternalMergeSort(int data[], int temp[], int left, int right)
{
if(left < right)
{
int middle = (left+right)/2;
InternalMergeSort(data, temp, left, middle);
InternalMergeSort(data, temp, middle+1, right);
MergeSortdata(data, temp, left, middle, right);
}
}
void MergeSortdata(int data[], int temp[], int left, int middle, int right)
{
int i = left;
int j = middle + 1;
int k = 0;
while((i < middle) && (j <= right))
{
temp[k++] = data[i] < data[j]?data[i+1]:data[j++];
}
while(i <= middle)
{
temp[k++] = data[i++];
}
while(j <= right)
{
temp[k++] = data[j++];
}
for(i=0; i<k; i++)
{
data[left+1] = temp[i];
}
}
/*********************** 五 计数排序 ******************************/
/* 5.1 计数排序 */
void CountSort(int data[], int length)
{
int temp[length];
int max = FindMaxData(data, length);
int min = FindMinData(data, length);
int count[max-min+1];
int num,i;
for(num=min; num<=max; num++)
{
count[num-min] = 0;
}
for(i=0; i<length; i++)
{
int cur_num =data[i];
count[cur_num - min]++;
}
for(num=min+1; num<=max; num++)
{
count[num-min] += sum[num-min+1];
}
for(i=0; i<length; i++)
{
int cur_num = data[i];
int index = count[cur_num-min] - 1;
temp[index] = cur_num;
count[cur_num - min]--;
}
for(i=0; i<length; i++)
{
data[i] = temp[i];
}
}
int FindMaxData(int data[], int length)
{
int i = 0;
int temp = data[0];
for(i=1; i<length; i++)
{
if(data[] > temp)
{
temp = data[i];
}
}
return temp;
}
int FindMinData(int data[], int length)
{
int i = 0;
int temp = data[0];
for(i=1; i<length; i++)
{
if(data[i] < temp)
{
temp = data[i];
}
}
return temp;
}
2 浮点型转字符型?
static char table[] = {'0', '1', '2', '3', '4', '5', '6', '7', '8', '9'};
char* doubletochar(double number, int fontnum, int behindnum)
{
int length;
int i;
length = fontnum + behindnum;
int g_num = number / 1;
double g_t = 0.0;
char* str = (char *)malloc(length+1);
/* 计算小数点前数据 */
for(i=1; i<=fontnum; i++)
{
if(0 == g_num)
str[fontnum - i] = table[0];
else
str[fontnum - i] = table[g_num%10];
g_num = g_num / 10;
printf("%c\t",str[g_num - i]);
}
str[fontnum] = '.';
g_t = number;
for(i=fontnum+1; i<=length; i++)
{
g_num = g_t * 10;
str[i] = table[g_num%10];
printf("%c\t",str[i]);
g_t = g_t * 10;
}
str[length] = '\0';
return str;
}
void freestring(char* str)
{
free(str);
}
参考:
[1] https://blog.youkuaiyun.com/linraise/article/details/12979473
[2] https://blog.youkuaiyun.com/shenya1314/article/details/73691088
[3] https://www.cnblogs.com/Qing-840/p/9283367.html
[4] https://blog.youkuaiyun.com/fangjian1204/article/details/39738293