正点原子I.MX6U嵌入式Qt开发指南:第十章《多线程》
liebian365 2024-10-17 14:00 5 浏览 0 评论
今日头条/西瓜视频/抖音短视频 同名:正点原子
原子哥今日头条/西瓜视频/抖音短视频账号:正点原子-原子哥
感谢各位的关注和支持,你们的关注和支持是正点原子无限前进的动力。
第十章《多线程》
我们写的一个应用程序,应用程序跑起来后一般情况下只有一个线程,但是可能也有特殊情况。比如我们前面章节写的例程都跑起来后只有一个线程,就是程序的主线程。线程内的操作都是顺序执行的。恩,顺序执行?试着想一下,我们的程序顺序执行,假设我们的用户界面点击有某个操作是比较耗时的。您会发现界面点击完了,点击界面对应的操作还没有完成,所以就会冻结界面,不能响应,直到操作完成后,才返回到正常的界面里。如果我们的界面是这么设计的话,估计用户得发毛了。
这种情况我们一般是创建一个单独的线程来执行这个比较耗时的操作。比如我们使用摄像头拍照保存照片。恩,很多朋友问,这个不算耗时吧。对的在电脑上使用Qt拍照,处理起来非常快。根本也不需要开启一个线程来做这种事。但是我们是否考虑在嵌入式的CPU上做这种事情呢?嵌入式的CPU大多数都没有电脑里的CPU主频(几GHz)那么高,处理速度也不快。此时我们就需要考虑开多一个线程来拍照了。拍完照再与主线程(主线程即程序原来的线程)处理好照片的数据,就完成了一个多线程的应用程序了。
官方文档里说,QThread类提供了一种独立于平台的方法来管理线程。QThread对象在程序中管理一个控制线程。QThreads在run()中开始执行。默认情况下,run()通过调用exec()来启动事件循环,并在线程中运行Qt事件循环。您可以通过使用QObject::moveToThread()将worker对象移动到线程来使用它们。
QThread线程类是实现多线程的核心类。Qt有两种多线程的方法,其中一种是继承QThread的run()函数,另外一种是把一个继承于QObject的类转移到一个Thread里。Qt4.8之前都是使用继承QThread的run()这种方法,但是Qt4.8之后,Qt官方建议使用第二种方法。两种方法区别不大,用起来都比较方便,但继承QObject的方法更加灵活。所以Qt的帮助文档里给的参考是先给继承QObject的类,然后再给继承QThread的类。
另外Qt提供了QMutex、QMutexLocker、QReadLocker 和QWriteLocker等类用于线程之间的同步,详细可以看Qt的帮助文档。
本章介绍主要如何使用QThread实现多线程编程,讲解如何通过继承QThread和QObject的方法来创建线程。还会使用QMutexLocker正确的退出一个线程。本章的内容就是这么多,并不深入,所以不难,目的就是快速掌握Qt线程的创建,理解线程。
10.1继承QThread的线程
在第十章的章节开头说过了,继承QThread是创建线程的一个普通方法。其中创建的线程只有run()方法在线程里的。其他类内定义的方法都在主线程内。恩,这样不理解?我们画个图捋一捋。
通过上面的图我们可以看到,主线程内有很多方法在主线程内,但是子线程,只有run()方法是在子线程里的。run()方法是继承于QThread类的方法,用户需要重写这个方法,一般是把耗时的操作写在这个run()方法里面。
10.1.1应用实例
本例目的:快速了解继承QThread类线程的使用。
例05_qthread_example1,继承QThread类的线程(难度:一般)。项目路径为Qt/2/05_qthread_example1。本例通过QThread类继承线程,然后在MainWindow类里使用。通过点击一个按钮开启线程。当线程执行完成时,会发送resultReady(const QString &s)信号给主线程。流程就这么简单。
在头文件“mainwindow.h”具体代码如下。
mainwindow.h编程后的代码
/******************************************************************
Copyright ? Deng Zhimao Co., Ltd. 1990-2021. All rights reserved.
* @projectName 05_qthread_example1
* @brief mainwindow.h
* @author Deng Zhimao
* @email 1252699831@qq.com
* @net www.openedv.com
* @date 2021-04-06
*******************************************************************/
1 #ifndef MAINWINDOW_H
2 #define MAINWINDOW_H
3
4 #include <QMainWindow>
5 #include <QThread>
6 #include <QDebug>
7 #include <QPushButton>
8
9 /* 使用下面声明的WorkerThread线程类 */
10 class WorkerThread;
11
12 class MainWindow : public QMainWindow
13 {
14 Q_OBJECT
15
16 public:
17 MainWindow(QWidget *parent = nullptr);
18 ~MainWindow();
19
20 private:
21 /* 在MainWindow类里声明对象 */
22 WorkerThread *workerThread;
23
24 /* 声明一个按钮,使用此按钮点击后开启线程 */
25 QPushButton *pushButton;
26
27 private slots:
28 /* 槽函数,用于接收线程发送的信号 */
29 void handleResults(const QString &result);
30
31 /* 点击按钮开启线程 */
32 void pushButtonClicked();
33 };
34
35 /* 新建一个WorkerThread类继承于QThread */
36 class WorkerThread : public QThread
37 {
38 /* 用到信号槽即需要此宏定义 */
39 Q_OBJECT
40
41 public:
42 WorkerThread(QWidget *parent = nullptr) {
43 Q_UNUSED(parent);
44 }
45
46 /* 重写run方法,继承QThread的类,只有run方法是在新的线程里 */
47 void run() override {
48 QString result = "线程开启成功";
49
50 /* 这里写上比较耗时的操作 */
51 // ...
52 // 延时2s,把延时2s当作耗时操作
53 sleep(2);
54
55 /* 发送结果准备好的信号 */
56 emit resultReady(result);
57 }
58
59 signals:
60 /* 声明一个信号,译结果准确好的信号 */
61 void resultReady(const QString &s);
62 };
63
64 #endif // MAINWINDOW_H
65
第36行,声明一个WorkerThread的类继承QThread类,这里是参考Qt的QThread类的帮助文档的写法。
第47行,重写run()方法,这里很重要。把耗时操作写于此,本例相当于一个继承QThread类线程模板了。
在源文件“mainwindow.cpp”具体代码如下。
mainwindow.cpp编程后的代码
/******************************************************************
Copyright ? Deng Zhimao Co., Ltd. 1990-2021. All rights reserved.
* @projectName 05_qthread_example1
* @brief mainwindow.cpp
* @author Deng Zhimao
* @email 1252699831@qq.com
* @net www.openedv.com
* @date 2021-04-06
*******************************************************************/
1 #include "mainwindow.h"
2
3 MainWindow::MainWindow(QWidget *parent)
4 : QMainWindow(parent)
5 {
6 /* 设置位置与大小 */
7 this->setGeometry(0, 0, 800, 480);
8
9 /* 对象实例化 */
10 pushButton = new QPushButton(this);
11 workerThread = new WorkerThread(this);
12
13 /* 按钮设置大小与文本 */
14 pushButton->resize(100, 40);
15 pushButton->setText("开启线程");
16
17 /* 信号槽连接 */
18 connect(workerThread, SIGNAL(resultReady(QString)),
19 this, SLOT(handleResults(QString)));
20 connect(pushButton, SIGNAL(clicked()),
21 this, SLOT(pushButtonClicked()));
22 }
23
24 MainWindow::~MainWindow()
25 {
26 /* 进程退出,注意本例run()方法没写循环,此方法需要有循环才生效 */
27 workerThread->quit();
28
29 /* 阻塞等待2000ms检查一次进程是否已经退出 */
30 if (workerThread->wait(2000)) {
31 qDebug()<<"线程已经结束!"<<endl;
32 }
33 }
34
35 void MainWindow::handleResults(const QString &result)
36 {
37 /* 打印出线程发送过来的结果 */
38 qDebug()<<result<<endl;
39 }
40
41 void MainWindow::pushButtonClicked()
42 {
43 /* 检查线程是否在运行,如果没有则开始运行 */
44 if (!workerThread->isRunning())
45 workerThread->start();
46 }
第11行,线程对象实例化,Qt使用C++基本都是对象编程,Qt线程也不例外。所以我们也是用对象来管理线程的。
第24~33行,在MainWindow的析构函数里退出线程,然后判断线程是否退出成功。因为我们这个线程是没有循环操作的,直接点击按钮开启线程后,做了2s延时操作后就完成了。所以我们在析构函数里直接退出没有关系。
第41~46行,按钮点击后开启线程,首先我们得判断这个线程是否在运行,如果不在运行我们则开始线程,开始线程用start()方法,它会调用重写的run()函数的。
10.1.2程序运行效果
点击开启线程按钮后,延时2s后,Qt Creator的应用程序输出窗口打印出“线程开启成功”。在2s内多次点击按钮则不会重复开启线程,因为线程在这2s内还在运行。同时我们可以看到点击按钮没卡顿现象。因为这个延时操作是在我们创建的线程里运行的,而pushButton是在主线程里的,通过点击按钮控制子线程的运行。
当关闭程序后,子线程将在主线程的析构函数里退出。注意线程使用wait()方法,这里等待2s,因为我们开启的线和是延时2s就完成了。如果是实际的操作,请根据CPU的处理能力,给一个适合的延时,阻塞等待线程完成后,就会自动退出并打印“线程已经结束”。
10.2继承QObject的线程
在第10章章节开头已经说过,继承QThread类是创建线程的一种方法,另一种就是继承QObject类。继承QObject类更加灵活。它通过QObject::moveToThread()方法,将一个QObeject的类转移到一个线程里执行。恩,不理解的话,我们下面也画个图捋一下。
通过上面的图不难理解,首先我们写一个类继承QObject,通过QObject::moveToThread()方法将它移到一个QThread线程里执行。那么可以通过主线程发送信号去调用QThread线程的方法如上图的fun4(),fun5()等等。这些方法都是在QThread线程里执行的。
10.2.1应用实例
本例目的:快速了解继承QObject类线程的使用。
例06_qthread_example2,继承QObject类的线程(难度:一般)。项目路径为Qt/2/06_qthread_example2。本例通过QObject类继承线程,然后在MainWindow类里使用。通过点击一个按钮开启线程。另一个按钮点击关闭线程。另外通过加锁的操作来安全的终止一个线程。(我们可以通过QMutexLocker可以安全的使用QMutex以免忘记解锁。)
在我们谈谈为什么需要加锁来终止一个线程?因为quit()和exit()方法都不会中途终止线程。要马上终止一个线程可以用terminate()方法。但是这个函数存在非常不安全的因素,Qt官方文档说不推荐使用。
我们可以添加一个bool变量,通过主线程修改这个bool变量来终止,但是有可能引起访问冲突,所以需要加锁,例程里可能体现不是那么明确,当我们有doWork1(),doWork2…就能体现到bool变量加锁的作用了。但是加锁会消耗一定的性能,增加耗时。
下面的例子是仿照Qt官方写的,看似简单,但是流程大家可能不是很明白,所以画个了大体的流程图,给大伙瞧瞧。
在头文件“mainwindow.h”具体代码如下。
mainwindow.h编程后的代码
/******************************************************************
Copyright ? Deng Zhimao Co., Ltd. 1990-2021. All rights reserved.
* @projectName 06_qthread_example2
* @brief mainwindow.h
* @author Deng Zhimao
* @email 1252699831@qq.com
* @net www.openedv.com
* @date 2021-04-08
*******************************************************************/
1 #ifndef MAINWINDOW_H
2 #define MAINWINDOW_H
3
4 #include <QMainWindow>
5 #include <QThread>
6 #include <QDebug>
7 #include <QPushButton>
8 #include <QMutexLocker>
9 #include <QMutex>
10
11 /* 工人类 */
12 class Worker;
13
14 class MainWindow : public QMainWindow
15 {
16 Q_OBJECT
17
18 public:
19 MainWindow(QWidget *parent = nullptr);
20 ~MainWindow();
21
22 private:
23 /* 开始线程按钮 */
24 QPushButton *pushButton1;
25
26 /* 打断线程按钮 */
27 QPushButton *pushButton2;
28
29 /* 全局线程 */
30 QThread workerThread;
31
32 /* 工人类 */
33 Worker *worker;
34
35 private slots:
36 /* 按钮1点击开启线程 */
37 void pushButton1Clicked();
38
39 /* 按钮2点击打断线程 */
40 void pushButton2Clicked();
41
42 /* 用于接收工人是否在工作的信号 */
43 void handleResults(const QString &);
44
45 signals:
46 /* 工人开始工作(做些耗时的操作 ) */
47 void startWork(const QString &);
48 };
49
50 /* Worker类,这个类声明了doWork1函数,将整个Worker类移至线程workerThread */
51 class Worker : public QObject
52 {
53 Q_OBJECT
54
55 private:
56 /* 互斥锁 */
57 QMutex lock;
58
59 /* 标志位 */
60 bool isCanRun;
61
62 public slots:
63 /* 耗时的工作都放在槽函数下,工人可以有多份不同的工作,但是每次只能去做一份 */
64 void doWork1(const QString ?meter) {
65
66 /* 标志位为真 */
67 isCanRun = true;
68
69 /* 死循环 */
70 while (1) {
71 /* 此{}作用是QMutexLocker与lock的作用范围,获取锁后,
72 * 运行完成后即解锁 */
73 {
74 QMutexLocker locker(&lock);
75 /* 如果标志位不为真 */
76 if (!isCanRun) {
77 /* 跳出循环 */
78 break;
79 }
80 }
81 /* 使用QThread里的延时函数,当作一个普通延时 */
82 QThread::sleep(2);
83
84 emit resultReady(parameter + "doWork1函数");
85 }
86 /* doWork1运行完成,发送信号 */
87 emit resultReady("打断doWork1函数");
88 }
89
90 // void doWork2();...
91
92 public:
93 /* 打断线程(注意此方法不能放在槽函数下) */
94 void stopWork() {
95 qDebug()<<"打断线程"<<endl;
96
97 /* 获取锁后,运行完成后即解锁 */
98 QMutexLocker locker(&lock);
99 isCanRun = false;
100 }
101
102 signals:
103 /* 工人工作函数状态的信号 */
104 void resultReady(const QString &result);
105 };
106 #endif // MAINWINDOW_H
第51~105行,声明一个Worker的类继承QObject类,这里是参考Qt的QThread类的帮助文档的写法。将官方的例子运用到我们的例子里去。
第62~88行,我们把耗时的工作都放于槽函数下。工人可以有不同的工作,但是每次只能去做一份。这里不同于继承QThread类的线程run(),继承QThread的类只有run()在新线程里。而继承QObject的类,使用moveToThread()可以把整个继承的QObject类移至线程里执行,所以可以有doWork1(),doWork2…等等耗时的操作,但是这些耗时的操作都应该作为槽函数,由主线程去调用。
第67~80行,进入循环后使用互拆锁判断isCanRun变量的状态,为假即跳出while循环,直到doWork1结束。注意,虽然doWork1结束了,但是线程并没有退出(结束)。因为我们把这个类移到线程里了,直到这个类被销毁。或者使用quit()和exit()退出线程才真正的结束!
在源文件“mainwindow.cpp”具体代码如下。
mainwindow.cpp编程后的代码
/******************************************************************
Copyright ? Deng Zhimao Co., Ltd. 1990-2021. All rights reserved.
* @projectName 06_qthread_example2
* @brief mainwindow.cpp
* @author Deng Zhimao
* @email 1252699831@qq.com
* @net www.openedv.com
* @date 2021-04-08
*******************************************************************/
1 #include "mainwindow.h"
2
3 MainWindow::MainWindow(QWidget *parent)
4 : QMainWindow(parent)
5 {
6 /* 设置显示位置与大小 */
7 this->setGeometry(0, 0, 800, 480);
8 pushButton1 = new QPushButton(this);
9 pushButton2 = new QPushButton(this);
10
11
12 /* 设置按钮的位置大小 */
13 pushButton1->setGeometry(300, 200, 80, 40);
14 pushButton2->setGeometry(400, 200, 80, 40);
15
16 /* 设置两个按钮的文本 */
17 pushButton1->setText("开启线程");
18 pushButton2->setText("打断线程");
19
20 /* 工人类实例化 */
21 worker = new Worker;
22
23 /* 将worker类移至线程workerThread */
24 worker->moveToThread(&workerThread);
25
26 /* 信号槽连接 */
27
28 /* 线程完成销毁对象 */
29 connect(&workerThread, SIGNAL(finished()),
30 worker, SLOT(deleteLater()));
31 connect(&workerThread, SIGNAL(finished()),
32 &workerThread, SLOT(deleteLater()));
33
34 /* 发送开始工作的信号,开始工作 */
35 connect(this, SIGNAL(startWork(QString)),
36 worker, SLOT(doWork1(QString)));
37
38 /* 接收到worker发送过来的信号 */
39 connect(worker, SIGNAL(resultReady(QString)),
40 this, SLOT(handleResults(QString)));
41
42 /* 点击按钮开始线程 */
43 connect(pushButton1, SIGNAL(clicked()),
44 this, SLOT(pushButton1Clicked()));
45
46 /* 点击按钮打断线程 */
47 connect(pushButton2, SIGNAL(clicked()),
48 this, SLOT(pushButton2Clicked()));
49 }
50
51 MainWindow::~MainWindow()
52 {
53 /* 打断线程再退出 */
54 worker->stopWork();
55 workerThread.quit();
56
57 /* 阻塞线程2000ms,判断线程是否结束 */
58 if (workerThread.wait(2000)) {
59 qDebug()<<"线程结束"<<endl;
60 }
61 }
62
63 void MainWindow::pushButton1Clicked()
64 {
65 /* 字符串常量 */
66 const QString str = "正在运行";
67
68 /* 判断线程是否在运行 */
69 if(!workerThread.isRunning()) {
70 /* 开启线程 */
71 workerThread.start();
72 }
73
74 /* 发送正在运行的信号,线程收到信号后执行后返回线程耗时函数 + 此字符串 */
75 emit this->startWork(str);
76 }
77
78 void MainWindow::pushButton2Clicked()
79 {
80 /* 如果线程在运行 */
81 if(workerThread.isRunning()) {
82
83 /* 停止耗时工作,跳出耗时工作的循环 */
84 worker->stopWork();
85 }
86 }
87
88 void MainWindow::handleResults(const QString & results)
89 {
90 /* 打印线程的状态 */
91 qDebug()<<"线程的状态:"<<results<<endl;
92 }
第20行,工人类实例化。继承QObject的多线程类不能指定父对象。
第24行,工人类实例化后,工人类将自己移至workerThread线程里执行。
第29~32行,线程结束后,我们需要使用deleteLater来销毁worker对象和workerThread对象分配的内存。deleteLater会确认消息循环中没有这两个线程的对象后销毁。
10.2.2程序运行效果
点击开启线程按钮后,应用程序输出窗口每隔2秒打印“正在运行doWork1函数”,当我们点击打断线程按钮后,窗口打印出“打断doWork1函数”。点击打断线程,会打断doWork1函数的循环,doWork1函数就运行结束了。再点击开启线程,可以再次运行doWork1函数。本例界面简单,仅用了两个按钮和打印语句作为显示部分,但是对初学线程的朋友们友好,因为程序不长。我们可以结合程序的注释,一步步去理解这种线程的写法。重要的是掌握写法,最后才应用到花里胡哨的界面去吧!
相关推荐
- 快递查询教程,批量查询物流,一键管理快递
-
作为商家,每天需要查询许许多多的快递单号,面对不同的快递公司,有没有简单一点的物流查询方法呢?小编的回答当然是有的,下面随小编一起来试试这个新技巧。需要哪些工具?安装一个快递批量查询高手快递单号怎么快...
- 一键自动查询所有快递的物流信息 支持圆通、韵达等多家快递
-
对于各位商家来说拥有一个好的快递软件,能够有效的提高自己的工作效率,在管理快递单号的时候都需要对单号进行表格整理,那怎么样能够快速的查询所有单号信息,并自动生成表格呢?1、其实方法很简单,我们不需要一...
- 快递查询单号查询,怎么查物流到哪了
-
输入单号怎么查快递到哪里去了呢?今天小编给大家分享一个新的技巧,它支持多家快递,一次能查询多个单号物流,还可对查询到的物流进行分析、筛选以及导出,下面一起来试试。需要哪些工具?安装一个快递批量查询高手...
- 3分钟查询物流,教你一键批量查询全部物流信息
-
很多朋友在问,如何在短时间内把单号的物流信息查询出来,查询完成后筛选已签收件、筛选未签收件,今天小编就分享一款物流查询神器,感兴趣的朋友接着往下看。第一步,运行【快递批量查询高手】在主界面中点击【添...
- 快递单号查询,一次性查询全部物流信息
-
现在各种快递的查询方式,各有各的好,各有各的劣,总的来说,还是有比较方便的。今天小编就给大家分享一个新的技巧,支持多家快递,一次能查询多个单号的物流,还能对查询到的物流进行分析、筛选以及导出,下面一起...
- 快递查询工具,批量查询多个快递快递单号的物流状态、签收时间
-
最近有朋友在问,怎么快速查询单号的物流信息呢?除了官网,还有没有更简单的方法呢?小编的回答当然是有的,下面一起来看看。需要哪些工具?安装一个快递批量查询高手多个京东的快递单号怎么快速查询?进入快递批量...
- 快递查询软件,自动识别查询快递单号查询方法
-
当你拥有多个快递单号的时候,该如何快速查询物流信息?比如单号没有快递公司时,又该如何自动识别再去查询呢?不知道如何操作的宝贝们,下面随小编一起来试试。需要哪些工具?安装一个快递批量查询高手快递单号若干...
- 教你怎样查询快递查询单号并保存物流信息
-
商家发货,快递揽收后,一般会直接手动复制到官网上一个个查询物流,那么久而久之,就会觉得查询变得特别繁琐,今天小编给大家分享一个新的技巧,下面一起来试试。教程之前,我们来预览一下用快递批量查询高手...
- 简单几步骤查询所有快递物流信息
-
在高峰期订单量大的时候,可能需要一双手当十双手去查询快递物流,但是由于逐一去查询,效率极低,追踪困难。那么今天小编给大家分享一个新的技巧,一次能查询多个快递单号的物流,下面一起来学习一下,希望能给大家...
- 物流单号查询,如何查询快递信息,按最后更新时间搜索需要的单号
-
最近有很多朋友在问,如何通过快递单号查询物流信息,并按最后更新时间搜索出需要的单号呢?下面随小编一起来试试吧。需要哪些工具?安装一个快递批量查询高手快递单号若干怎么快速查询?运行【快递批量查询高手】...
- 连续保存新单号功能解析,导入单号查询并自动识别批量查快递信息
-
快递查询已经成为我们日常生活中不可或缺的一部分。然而,面对海量的快递单号,如何高效、准确地查询每一个快递的物流信息,成为了许多人头疼的问题。幸运的是,随着科技的进步,一款名为“快递批量查询高手”的软件...
- 快递查询教程,快递单号查询,筛选更新量为1的单号
-
最近有很多朋友在问,怎么快速查询快递单号的物流,并筛选出更新量为1的单号呢?今天小编给大家分享一个新方法,一起来试试吧。需要哪些工具?安装一个快递批量查询高手多个快递单号怎么快速查询?运行【快递批量查...
- 掌握批量查询快递动态的技巧,一键查找无信息记录的两种方法解析
-
在快节奏的商业环境中,高效的物流查询是确保业务顺畅运行的关键。作为快递查询达人,我深知时间的宝贵,因此,今天我将向大家介绍一款强大的工具——快递批量查询高手软件。这款软件能够帮助你批量查询快递动态,一...
- 从复杂到简单的单号查询,一键清除单号中的符号并批量查快递信息
-
在繁忙的商务与日常生活中,快递查询已成为不可或缺的一环。然而,面对海量的单号,逐一查询不仅耗时费力,还容易出错。现在,有了快递批量查询高手软件,一切变得简单明了。只需一键,即可搞定单号查询,一键处理单...
- 物流单号查询,在哪里查询快递
-
如果在快递单号多的情况,你还在一个个复制粘贴到官网上手动查询,是一件非常麻烦的事情。于是乎今天小编给大家分享一个新的技巧,下面一起来试试。需要哪些工具?安装一个快递批量查询高手快递单号怎么快速查询?...
你 发表评论:
欢迎- 一周热门
- 最近发表
- 标签列表
-
- wireshark怎么抓包 (75)
- qt sleep (64)
- cs1.6指令代码大全 (55)
- factory-method (60)
- sqlite3_bind_blob (52)
- hibernate update (63)
- c++ base64 (70)
- nc 命令 (52)
- wm_close (51)
- epollin (51)
- sqlca.sqlcode (57)
- lua ipairs (60)
- tv_usec (64)
- 命令行进入文件夹 (53)
- postgresql array (57)
- statfs函数 (57)
- .project文件 (54)
- lua require (56)
- for_each (67)
- c#工厂模式 (57)
- wxsqlite3 (66)
- dmesg -c (58)
- fopen参数 (53)
- tar -zxvf -c (55)
- 速递查询 (52)