Qt开发 | C++lambda函数 | Qt信号槽的五种写法 | 自定义信号、信号emit、信号参数注册 | connect函数 | Qt信号槽与moc | Qt内存管理机制 | Qt中文乱码

一、C++ lambda函数详解

详细内容可参考:https://liujie.blog.csdn.net/article/details/139213292

  C++ lambda表达式的本质就是重载了operator(),lambda表达式会被编译器翻译成类进行处理,在调用时会进行编译展开,因此lambda表达式对象其实就是一个匿名的函数对象,所以lambda表达式也叫做匿名函数对象。Qt槽函数可以使用lambda函数来写

C++ lambda表达式的构成

[caputer list] (parameters) mutable throw() -> return type{ statement }

参数意义:

  • caputer list是捕获列表

    用于捕获外部变量,捕获的变量可以在函数体中使用,可以省略,即不捕获外部变量。一共有三种捕获方式:值捕获、引用捕获和隐式捕获。

  • parameters是参数列表

    与普通函数的参数列表一致。如果不需要参数传递,则可以连同括号“()”一起省略,即无参数列表

  • mutable关键字

    默认情况下Lambda函数总是一个const函数,mutable可以取消其常量性。加上mutable关键字后,可以修改传递进来的拷贝,在使用该修饰符时,参数列表不可省略(即使参数为空)。

  • throw是异常说明

    用于Lamdba表达式内部函数抛出异常。

  • return type是返回值类型。

    追踪返回类型形式声明函数的返回类型。我们可以在不需要返回值的时候也可以连同符号”->”一起省略。此外,在返回类型明确的情况下,也可以省略该部分,让编译器对返回类型进行推导。

  • statement是Lambda表达式的函数体

    内容与普通函数一样,不过除了可以使用参数之外,还可以使用所有捕获的变量。

注意:可以忽略参数列表和返回类型,但必须永远包含捕获列表和函数体

  下面介绍三种捕获方式并举例

  • 值捕获:不能在lambda表达式中修改捕获变量的值

    值捕获意味着捕获的变量通过值传递给lambda表达式,即在lambda表达式中创建了这些变量的副本。因此,lambda表达式中使用的是这些变量的副本,而不是原始变量本身。由于被捕获变量的值是在创建时拷贝,因此随后对其求改不会影响到 lambda 内对应的值。

  • 引用捕获:使用引用捕获一个外部变量,需要在捕获列表变量前面加上一个引用说明符&

    引用捕获意味着捕获的变量通过引用传递给lambda表达式,即lambda表达式中使用的是原始变量的引用,这意味着可以在lambda内部修改这些变量的值。

  • 隐式捕获

    • =为值捕获
    • &为引用捕获
    • 当我们混合使用了隐式捕获和显式捕获时,捕获列表中的第一个元素必须是一个 & 或 =。此符号指定了默认捕获方式为引用或值。

示例:

// ch1_9_lambda.cpp : 此文件包含 "main" 函数。程序执行将在此处开始并结束。
//

#include <iostream>
using namespace std;

int main()
{
    //值捕获
    int value = 100;
    auto f = [value](int a, int b)->int {
        return a + b + value;
    };

    cout << f(1, 2) << endl;  //103

    //引用捕获
    auto f2 = [&value](int a, int b)->int {
        value++;
        return a + b;
    };
    cout << f2(1, 3) << endl;
    cout << "value =" << value << endl;

    //隐式捕获,=值捕获,&引用捕获
    int age = 123;
    auto f3 = [&](int a, int b)->int {
        value++;
        age++;
        return a + b;
    };
    cout << "age =" << age << endl;
    return 0;
}

运行结果:

image-20240620114210267

二、信号槽函数的五种写法

  要使用信号槽,必须在类的定义中包含Q_OBJECT宏。Q_OBJECT宏使得类能够使用信号和槽机制。在编译过程中,Qt的元对象编译器(moc)会读取带有Q_OBJECT宏的类的定义,并生成额外的代码来支持信号和槽的连接和调用。

元对象编译器(moc):moc是一个工具,它读取带有Q_OBJECT宏的源文件,并生成一个额外的源文件,这个文件包含了元对象代码,包括信号和槽的实现。

  • 第一种写法:Qt4写法

    connect(ui->btnOpnen, SIGNAL(clicked), this, SLOT(open()));
    

    不推荐这种写法,如果SIGNAL写错了,或者信号名字、槽函数名字写错了,编译器检查不出来,导致程序无响应,引起不必要的误解。

  • 第二种写法:Qt5写法

    connect(ui.btnOpnen, &QPushButton::clicked, this, &Widget::open);
    

    推荐使用这种写法,信号名字、槽函数名字写错了,编辑器会直接报错。

  • 第三种写法:lambda表达式写法

    connect(ui.btnOpnen, &QPushButton::clicked, [=](){
        //具体代码
    });
    

    适用于slot代码(槽函数代码)比较少的情况

  • 第四种写法:牵线法

    image-20240620121921471

    需要在代码中实现槽函数slot2()

    不推荐使用这种写法,非常不建议使用,如果空间太多,而且跨很多层界面,这种基本处理不了。

  • 第五种写法:槽函数名称为on_控件名字_信号名()

    这种写法不用connect,Qt自动连接。这种用法挺常见,推荐这样使用。

三、自定义信号、信号emit、信号参数注册

1.如何自定义信号

  • 使用signals声明

  • 返回值是void

    image-20240620160755455

  • 在需要发送的地方使用

    emit 信号名字(参数);
    

    进行发送

    image-20240620161852426

  • 在需要链接的地方使用connect进行链接

    image-20240620161937785

2.跨UI发送信号

  以“跨UI发送信号”为例,使用自定义信号。

.pro文件

QT       += core gui

greaterThan(QT_MAJOR_VERSION, 4): QT += widgets

CONFIG += c++17

# You can make your code fail to compile if it uses deprecated APIs.
# In order to do so, uncomment the following line.
#DEFINES += QT_DISABLE_DEPRECATED_BEFORE=0x060000    # disables all the APIs deprecated before Qt 6.0.0

SOURCES += \
    main.cpp \
    setdialog.cpp \
    widget.cpp

HEADERS += \
    setdialog.h \
    widget.h

FORMS += \
    setdialog.ui \
    widget.ui

# Default rules for deployment.
qnx: target.path = /tmp/$${TARGET}/bin
else: unix:!android: target.path = /opt/$${TARGET}/bin
!isEmpty(target.path): INSTALLS += target

setdialog.h文件

#ifndef SETDIALOG_H
#define SETDIALOG_H

#include <QDialog>

namespace Ui {
class SetDialog;
}

class SetDialog : public QDialog
{
    Q_OBJECT

public:
    explicit SetDialog(QWidget *parent = nullptr);
    ~SetDialog();

private slots:
    void on_btnAdd_clicked();

signals:
    void signal_addOne(int value);

private:
    Ui::SetDialog *ui;
};

#endif // SETDIALOG_H

widget.h文件

#ifndef WIDGET_H
#define WIDGET_H

#include <QWidget>

QT_BEGIN_NAMESPACE
namespace Ui {
class Widget;
}
QT_END_NAMESPACE

class Widget : public QWidget
{
    Q_OBJECT

public:
    Widget(QWidget *parent = nullptr);
    ~Widget();

private slots:
    void on_btnOpen_clicked();

private:
    Ui::Widget *ui;
};
#endif // WIDGET_H

setdialog.cpp文件

#include "setdialog.h"
#include "ui_setdialog.h"

SetDialog::SetDialog(QWidget *parent)
    : QDialog(parent)
    , ui(new Ui::SetDialog)
{
    ui->setupUi(this);
}

SetDialog::~SetDialog()
{
    delete ui;
}


void SetDialog::on_btnAdd_clicked()
{
    static int score = 90;
    emit signal_addOne(score++);
}

widget.cpp文件

#include "widget.h"
#include "ui_widget.h"
#include "setdialog.h"

Widget::Widget(QWidget *parent)
    : QWidget(parent)
    , ui(new Ui::Widget)
{
    ui->setupUi(this);
}

Widget::~Widget()
{
    delete ui;
}

void Widget::on_btnOpen_clicked()
{
    SetDialog dlg;
    //使用connect进行链接
    connect(&dlg,&SetDialog::signal_addOne, [=](int value){
        ui->lineEdit->setText(QString::number(value));
    });

    dlg.exec(); //事件循环会阻塞UI
}

main.cpp文件

#include "widget.h"

#include <QApplication>

int main(int argc, char *argv[])
{
    QApplication a(argc, argv);
    Widget w;
    w.show();
    return a.exec();
}

3.跨线程发送信号

  以“跨线程发送信号”为例,使用自定义信号,在子线程上向UI线程上发送信号。

注意:Qt的子线程无法直接修改UI,需要发送信号到UI线程进行修改

.pro文件

QT       += core gui

greaterThan(QT_MAJOR_VERSION, 4): QT += widgets

CONFIG += c++17

# You can make your code fail to compile if it uses deprecated APIs.
# In order to do so, uncomment the following line.
#DEFINES += QT_DISABLE_DEPRECATED_BEFORE=0x060000    # disables all the APIs deprecated before Qt 6.0.0

SOURCES += \
    childthred.cpp \
    main.cpp \
    widget.cpp

HEADERS += \
    childthred.h \
    widget.h

FORMS += \
    widget.ui

# Default rules for deployment.
qnx: target.path = /tmp/$${TARGET}/bin
else: unix:!android: target.path = /opt/$${TARGET}/bin
!isEmpty(target.path): INSTALLS += target

childthread.h文件

#ifndef CHILDTHRED_H
#define CHILDTHRED_H

#include <QThread>
#include <string>
using namespace std;

struct Score
{
    string name;
    int id;
    int age;
};

class ChildThred : public QThread
{
    Q_OBJECT
public:
    explicit ChildThred(QObject *parent = nullptr);

protected:
    void run() override;

signals:
    void signal_SendToUI(Score score);
};

#endif // CHILDTHRED_H

widget.h文件

#ifndef WIDGET_H
#define WIDGET_H

#include <QWidget>
#include "childthred.h"

QT_BEGIN_NAMESPACE
namespace Ui {
class Widget;
}
QT_END_NAMESPACE

class Widget : public QWidget
{
    Q_OBJECT

public:
    Widget(QWidget *parent = nullptr);
    ~Widget();

private slots:
    void on_btnUpdate_clicked();
    void showInfo(Score score);

private:
    Ui::Widget *ui;
};
#endif // WIDGET_H

childthread.cpp文件

#include "childthred.h"
#include <QDebug>

ChildThred::ChildThred(QObject *parent)
    : QThread{parent}
{
    //非基础类型参数注册
    qRegisterMetaType<Score>("Score");
}


void ChildThred::run()
{
    // qDebug() << "run thread id = " << QThread::currentThreadId();
    while(1)
    {
        Score s;
        s.name = "wanfeng";
        s.id = 1001;
        s.age = 27;

        //发射信号
        emit signal_SendToUI(s);
    }
}

widget.cpp文件

#include "widget.h"
#include "ui_widget.h"
#include "childthred.h"
#include <QDebug>

Widget::Widget(QWidget *parent)
    : QWidget(parent)
    , ui(new Ui::Widget)
{
    ui->setupUi(this);
}

Widget::~Widget()
{
    delete ui;
}

void Widget::on_btnUpdate_clicked()
{
    ChildThred *ch = new ChildThred();

    //链接子线程的信号与UI线程设置文本框
    //lamda槽函数的函数体实际运行在子线程中
    // connect(ch, &ChildThred::signal_SendToUI, [=](Score s){
    //     string info = s.name + " id =" + to_string(s.id) + " age = " + to_string(s.age);
    //     ui->lineEdit->setText(QString::fromStdString(info));
    //    qDebug() << "slot thread id = " << QThread::currentThreadId();
    // });

    //槽函数的函数体实际运行在UI线程中,
    //出现问题:QObject::connect: Cannot queue arguments of type 'Score'(Make sure 'Score' is registered using qRegisterMetaType().)
    //涉及到非基础类型参数注册
    connect(ch, &ChildThred::signal_SendToUI, this, &Widget::showInfo);
    ch->start();
    // qDebug() << "ui thread id = " << QThread::currentThreadId();
}

void Widget::showInfo(Score score)
{
    string info = score.name + " id =" + to_string(score.id) + " age = " + to_string(score.age);
    ui->lineEdit->setText(QString::fromStdString(info));
}

main.cpp文件

#include "widget.h"

#include <QApplication>

int main(int argc, char *argv[])
{
    QApplication a(argc, argv);
    Widget w;
    w.show();
    return a.exec();
}

4.非基础类型参数注册

  当子线程中发射信号的参数类型为非基础类型时,需要进行非基础类型参数注册

会出现的问题:

QObject::connect: Cannot queue arguments of type 'Score'
(Make sure 'Score' is registered using qRegisterMetaType().)

解决办法:

ChildThred::ChildThred(QObject *parent)
    : QThread{parent}
{
    //非基础类型参数注册
    qRegisterMetaType<Score>("Score");
}

5.槽函数的参数和信号参数的关系

  Qt中的槽函数与信号参数之间的关系需要遵循以下规则:

  • 参数数量:槽函数的参数数量可以比它所连接的信号的参数数量少(即忽略部分信号参数),但是不能比信号的参数多。
  • 参数类型:槽函数的参数类型必须与信号的参数类型完全匹配。参数类型包括基本数据类型、自定义类型、指针、引用等。
  • 参数顺序:槽函数的参数顺序必须与信号的参数顺序一致。Qt的信号和槽连接是基于参数的类型和顺序的。
  • 参数名称:参数的名称在信号和槽之间不需要匹配。Qt在连接信号和槽时,只关心参数的类型和顺序,不关心参数的名称。
  • 可变参数:信号可以有可变参数(使用...语法),但是槽函数不能有可变参数。
  • 默认参数:信号和槽都不能使用默认参数。这是因为信号的调用是动态的,而默认参数是编译时的概念。
  • 重载的槽函数:如果一个类中有多个重载的槽函数,只有参数列表与信号完全匹配的那个槽会被连接。如果信号连接到了一个参数不匹配的槽,编译时会报错。
  • 信号的重载:信号可以被重载,即同一个信号名可以对应多个具有不同参数列表的信号。但是,每个信号都必须有唯一的参数列表。
  • const修饰符:信号可以是const的,这意味着它们可以在const成员函数中被调用。但是,槽函数不能是const的,因为槽可能会修改对象的状态。
  • 指针和引用:信号可以传递指针或引用作为参数,槽函数也必须相应地使用指针或引用。例如,如果信号有一个int*参数,槽函数也必须有一个int*参数。
  • 值传递:信号和槽都支持值传递。如果信号传递一个值(例如int),槽函数也必须接收一个int类型的参数。
  • 信号的连接:在连接信号和槽时,Qt会检查参数的匹配性。如果参数不匹配,连接会失败,并且编译器会报错。

6.信号重名(重载)如何处理

  例如:QComboBox的信号

Q_SIGNALS:
    void currentIndexChanged(int index);
	void currentIndexChanged(const QString &);

对于这种重载的信号,用Qt4的connect来写是没问题的,但如果使用Qt5的写法那就无法编译通过,即

connect(ui->comboBox, &QComboBox::currentIndexChanged, this, &MainWindow::on_IndexChanged);

需要使用泛型解决信号重载问题

connect(ui->comboBox, QOverload<int>::of(&QComboBox::currentIndexChanged), this, &MainWindow::on_IndexChanged);

示例:

.pro文件

QT       += core gui

greaterThan(QT_MAJOR_VERSION, 4): QT += widgets

CONFIG += c++17

# You can make your code fail to compile if it uses deprecated APIs.
# In order to do so, uncomment the following line.
#DEFINES += QT_DISABLE_DEPRECATED_BEFORE=0x060000    # disables all the APIs deprecated before Qt 6.0.0

SOURCES += \
    main.cpp \
    widget.cpp

HEADERS += \
    widget.h

FORMS += \
    widget.ui

# Default rules for deployment.
qnx: target.path = /tmp/$${TARGET}/bin
else: unix:!android: target.path = /opt/$${TARGET}/bin
!isEmpty(target.path): INSTALLS += target

widget.h文件

#ifndef WIDGET_H
#define WIDGET_H

#include <QWidget>

QT_BEGIN_NAMESPACE
namespace Ui {
class Widget;
}
QT_END_NAMESPACE

class Widget : public QWidget
{
    Q_OBJECT

public:
    Widget(QWidget *parent = nullptr);
    ~Widget();

private slots:
    void onIndex(int index);
    void onIndexStr(const QString& index);

private:
    Ui::Widget *ui;
};
#endif // WIDGET_H

widget.cpp文件

#include "widget.h"
#include "ui_widget.h"
#include <QDebug>

Widget::Widget(QWidget *parent)
    : QWidget(parent)
    , ui(new Ui::Widget)
{
    ui->setupUi(this);

    ui->comboBox->addItem("10");
    ui->comboBox->addItem("11");
    ui->comboBox->addItem("12");
    ui->comboBox->addItem("13");
    ui->comboBox->addItem("14");
    ui->comboBox->addItem("15");
    ui->comboBox->addItem("16");

    // connect(ui->comboBox, &QComboBox::currentTextChanged, this, &Widget::onIndex);
    //使用泛型解决信号重载问题
    connect(ui->comboBox, QOverload<int>::of(&QComboBox::currentIndexChanged),
            this, &Widget::onIndex);

    connect(ui->comboBox, QOverload<const QString&>::of(&QComboBox::currentIndexChanged),
            this, &Widget::onIndexStr);
}

Widget::~Widget()
{
    delete ui;
}

void Widget::onIndex(int index)
{
    qDebug() << "int = " << index;
}

void Widget::onIndexStr(const QString& index)
{
    qDebug() << "const QString = " << index;
}

main.cpp文件

#include "widget.h"

#include <QApplication>

int main(int argc, char *argv[])
{
    QApplication a(argc, argv);
    Widget w;
    w.show();
    return a.exec();
}

四、connect函数详解

  connect函数的原型为

template <typename Func1, typename Func2>
static inline QMetaObject::Connection connect(
    const typename QtPrivate::FunctionPointer<Func1>::Object *sender, 
    Func1 signal,
    const typename QtPrivate::FunctionPointer<Func2>::Object *receiver, 
    Func2 slot,
    Qt::ConnectionType type = Qt::AutoConnection)
{
    //...
}
  • sender:发送者

  • signal:信号函数

  • receiver:接收者

  • slot:槽函数

  • type:连接类型

    enum ConnectionType {
        AutoConnection,
        DirectConnection,
        QueuedConnection,
        BlockingQueuedConnection,
        UniqueConnection =  0x80
    };
    
    • AutoConnection:

      默认的连接方式。在Qt中默认是用Qt::AutoConnection,所以平时写信号槽都是4个参数。

      如果接收方在发出信号的线程中,使用Qt::DirectConnection;否则使用Qt::QueuedConnection。在发出信号时确定连接类型。

    • DirectConnection:

      当发出信号时,插槽立即被调用。槽在发送信号的线程中执行。

    • QueuedConnection:

      当控制返回到接收方线程的事件循环时调用槽,槽在接收方的线程中执行。

    • BlockingQueuedConnection:

      与Qt::QueuedConnection相同,只是发送信号的线程会阻塞,直到槽返回。如果接收方存在于发送信号的线程中,则不能使用此连接,否则应用程序将死锁。

    • UniqueConnection:

      这是一个可以使用按位OR与上述任何一种连接类型组合的标志,当Qt::UniqueConnection被设置时,如果连接已经存在,QObject::connect()将失败(例如:如果相同的信号已经连接到相同的对象对的插槽)

五、Qt信号槽与moc

  moc全称是Meta-Object Compiler,也就是“元对象编译器”。Qt程序在交由标准编译器编译之前,先要使用moc分析C++源文件。如果它发现在一个头文件中包含了宏Q_OBJECT,则会生成另一个C++源文件,这个源文件中包含了Q_OBJECT宏的实现代码,这个新的文件名字将会是原文件名前面加上moc_构成,这个新的文件同样将进入编译系统,最终被链接到二进制代码中去。因此我们可以知道,这个新的文件不是“替换”掉旧的文件,而是与原文件一起参与编译。另外,我们还可以看出一点,moc的执行是在预处理器之前。因为预处理器执行之后,Q_OBJECT宏就不存在了。可以这么理解,moc把Qt中一些不是C++的关键字做了解析,让C++编译器可以认识,例如:slots、signals、emit等,moc会把这些重新编译解析。

注意:这一段介绍需要熟悉,面试时可能会问到

  运行moc指令,可以将带有Q_OBJECT宏的头文件进行代码翻译

D:\Project\QtProject\ch1_13_moc> moc widget.h -o moc_widget.cpp

会生成moc_widget.cpp文件。

六、C++模版技术实现Qt信号槽机制

问题:只有Qt有信号槽吗?其他项目不能使用信号槽吗?

  信号槽(Signals and Slots)是Qt框架中特有的一种对象通信机制,它使得对象之间的通信更加灵活和动态。然而,信号槽机制并不是Qt独有的,其他编程语言和框架也可能有类似的模式或机制,尽管它们可能有不同的名称和实现方式

例如:sigslot-C++ Signal/Slot Libary是一个纯C++信号槽,链接为:https://sigslot.sourceforge.net/

七、Qt内存管理机制

  C++派生类的构造顺序与析构顺序为

  • 构造顺序:先执行基类的构造函数,再执行派生类的构造函数
  • 析构顺序:先执行派生类的析构函数,在执行基类的析构函数

  Qt框架使用半自动内存管理机制,这种机制结合了手动内存管理的灵活性和自动内存管理的便利性。

  • QObject及其派生类的对象,如果其parent非0,那么其parent析构时会析构该对象
  • QWidget及其派生类的对象,可以设置Qt::WA_DeleteOnClose标志位,当close时会调用QWidgetPrivate::close_helper,进而调用deleteLater析构该对象

关键特点

  • 智能指针:Qt提供了智能指针类,如QPointer<T>QScopedPointer<T>,用于自动管理对象的生命周期。QPointer<T>在对象被删除时自动将指针设置为nullptr,而QScopedPointer<T>则确保在其作用域结束时自动删除所指向的对象。
  • 所有权系统:Qt使用所有权概念来管理内存。QSharedPointer<T>是一个引用计数的智能指针,当多个QSharedPointer<T>指向同一个对象时,对象的生命周期会根据引用计数来管理。当最后一个引用被销毁时,对象也会被自动删除。
  • 内存分配器:Qt提供了一个高效的内存分配器QMalloc,它用于分配和释放内存。这个分配器优化了内存分配和释放的性能,尤其是在频繁分配和释放小块内存时。
  • 垃圾收集:Qt没有使用传统的垃圾收集机制,而是依赖于C++的析构函数来释放资源。当对象超出作用域或其引用计数降到零时,析构函数会被调用。
  • 手动内存管理:虽然Qt提供了智能指针和内存分配器,但开发者仍然可以选择手动管理内存,使用newdelete操作符来分配和释放内存。
  • 内存泄漏检测:Qt提供了内存泄漏检测工具,如Valgrind和Qt Creator的内存检查工具,帮助开发者发现和修复内存泄漏问题。
  • 对象复制和赋值:Qt的类通常使用复制构造函数和赋值运算符来管理对象的复制。开发者需要注意深拷贝和浅拷贝的区别,并适当地重载这些函数。
  • 析构函数:Qt的类通常需要定义析构函数来释放资源,如关闭文件、释放动态分配的内存等。
  • 资源管理:Qt鼓励使用RAII(Resource Acquisition Is Initialization)原则来管理资源,即在对象构造时获取资源,在析构时释放资源。

八、Qt中文乱码如何解决

  Qt对中文的支持不是很好,使用QtCreator会出现各种乱七八糟的中文乱码问题,如何处理这种问题?

  • 粘贴别人的代码时,先在记事本里“过一遍”,再贴到QtCreator;

  • 使用u8

    ui->pushButton->setText(u8"你好");
    
  • 不使用QtCreator开发,直接使用vs2019

其他设置:

  • QtCreator — 选项 — 文本编辑器 — UTF8 BOM 总是删除

  • #pragma execution_character_set("utf-8")

    预处理器指令,用于指定源文件的执行字符集为UTF-8

上述方法不一定解决中文乱码问题。最好的解决办法是使用qt翻译文件,才不会出现中文乱码问题。
具体参考:https://blog.csdn.net/zwcslj/article/details/140335946?spm=1001.2014.3001.5501

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值