qt中的对象树与生命期

1.为什么要使用对象树: GUI 程序通常是存在父子关系的,比如一个对话框之中含有按钮、列表等部件,按钮、列表、对话框等部件其实就是一个类的对象(注意是类的对象,而非类),很明显这些对象之间是存在父子关系的,因此一个 GUI 程序通常会由一个父对象维护着一系列的子对象列表,这样更方便对部件的管理,比如当按下 tab 键时,父对象会依据子对象列表令各子对象依次获得焦点。当关闭对话框时,父象依据子对象列表,找到每个子对象,然后删除它们。 在 Qt 中,对对象的管理,使用的是树形结构,也就是对象树

组合模式与对象树

1、组合模式指的是把类的对象组织成树形结构,这种树形结构也称为对象树, Qt 使用对象树来管理 QObject 及其子类的对象。 注意:这里是指的类的对象而不是类。把类组织成树形结构只需使用简单的继承机制便可实现。
2、使用组合模式的主要作用是可以通过根节点对象间接调用子节点中的虚函数,从而可以间接的对子节点对象进行操作。
3、组合模式的基本思想是使用父类类型的指针指向子类对象,并把这个指针存储在一个数组中(使用容器更方便),然后每创建一个类对象就向这个容器中添加一个指向该对象的指针。

示例:打印出组合模式中的各对象的名称

在这里插入图片描述

#include "mainwindow.h"
#include <QApplication>
#include<iostream>
#include<QVector>
#include<QDebug>
using namespace std;
class A{
public:
    //也可使用A* m[11] 这种方式的数组代替,但数组
    //无法自动扩容
    QVector<A*> v;
    void add(A* ma)
    {
        v.push_back(ma);
    }
    QString name;
    A(){}
    A(QString n){name = n;}
    virtual QString g() {return name;}
    void f(){
        if(!v.isEmpty())
        {
            qDebug()<<name<<"==";
            for(int i= 0;i<v.size();i++)
            {
                qDebug()<<v[i]->g()<<",";
            }
            qDebug()<<"------------this is A-------------";
        }
    }
    //该函数会被递归调用,用以输出整个对象树中对象的名称
    virtual void pt()
    {
        qDebug()<<"--------this is-A--~--:"<<name;
        f();
        pt是虚函数,若v[i]的类型为子类型B。会调用B::pt()
        for(int i= 0;i<v.size();i++)
        {
            v[i]->pt();
        }
    }
};
class B: public A{     //需要继承A
public:
    QString name;
    B(QString str) {name = str;}

    void add(A *ma)
    {
        v.push_back(ma);
    }
    virtual QString g() {return name;}
    void f(){
        if(!v.isEmpty())
        {
            qDebug()<<name<<"==";
            for(int i= 0;i<v.size();i++)
            {
                qDebug()<<v[i]->g()<<",";
            }
            qDebug()<<"------------this is B-------------";
        }
    }
    void pt()
    {
        qDebug()<<"--------this is---B~--:"<<name;
        f();
        pt是虚函数,若v[i]的类型为子类型B。会调用B::pt()
        for(int i= 0;i<v.size();i++)
        {
            v[i]->pt();
        }
    }
};
//需要继承自类A,该类无add函数,也就是说该类的对象不能有子对象。
class C: public A{
public:
    QString name;
    C(QString str) {name = str;}
    QString g() {return name;}
    void f(){
        if(!v.isEmpty())
        {
            qDebug()<<name<<"==";
            for(int i= 0;i<v.size();i++)
            {
                qDebug()<<v[i]->g()<<",";
            }
            qDebug()<<"---------this is C----------------";
        }
    }
    void pt()
    {
        qDebug()<<"--------this is ~~~c :"<<name;
        f();
        for(int i= 0;i<v.size();i++)
        {
            v[i]->pt();
        }
    }
};
int main(int argc, char *argv[])
{
    QApplication a(argc, argv);
    MainWindow w;
    A ma("ma"),ma1("ma1"),ma2("ma2"),ma3("ma3"),ma4("ma4");
    B mb("mb"),mb1("mb1"),mb2("mb2"),mb3("mb3"),mb4("mb4");
    C mc("mc"),mc1("mc1"),mc2("mc2"),mc3("mc3"),mc4("mc4");
    ma.add(&mb); //ma.v[0]=&mb;
    mb.add(&mb1); //mb.v[0]=&mb1;
    mb.add(&mb2); //mb.v[1]=&mb2;
    ma.add(&mb1); //ma.v[1]=&mb1;
    mb1.add(&mb3); //mb1.v[0]=&mb3;
    mb1.add(&mb4); //mb1.v[1]=&mb4;
    mb2.add(&mc1); //mb2.v[0]=&mc1;
    mb2.add(&mc2); //mb2.v[1]=&mc2;
    mb2.add(&ma1); //mb2.v[2]=&ma1;
    qDebug()<<QString::fromLocal8Bit("各对象拥有的子对象")<<endl;
    ma.f(); mb.f(); mb1.f(); mb2.f();
    qDebug()<<endl<<QString::fromLocal8Bit("整个对象中结构")<<endl;
    ma.pt();
    w.show();
    return a.exec();
}

输出:

"ma" ==
"mb" ,
"mb1" ,
------------this is A-------------
"mb" ==
"mb1" ,
"mb2" ,
------------this is B-------------
"mb1" ==
"mb3" ,
"mb4" ,
------------this is B-------------
"mb2" ==
"mc1" ,
"mc2" ,
"ma1" ,
------------this is B-------------

"整个对象中结构" 

--------this is-A--~--: "ma"
"ma" ==
"mb" ,
"mb1" ,
------------this is A-------------
--------this is---B~--: "mb"
"mb" ==
"mb1" ,
"mb2" ,
------------this is B-------------
--------this is---B~--: "mb1"
"mb1" ==
"mb3" ,
"mb4" ,
------------this is B-------------
--------this is---B~--: "mb3"
--------this is---B~--: "mb4"
--------this is---B~--: "mb2"
"mb2" ==
"mc1" ,
"mc2" ,
"ma1" ,
------------this is B-------------
--------this is ~~~c : "mc1"
--------this is ~~~c : "mc2"
--------this is-A--~--: "ma1"
--------this is---B~--: "mb1"
"mb1" ==
"mb3" ,
"mb4" ,
------------this is B-------------
--------this is---B~--: "mb3"
--------this is---B~--: "mb4"

使用父节点对象删除子结构对象

代码如下:

#include "mainwindow.h"
#include <QApplication>
#include<iostream>
#include<QVector>
#include<QDebug>
using namespace std;
class A{
public:
    QVector<A*> v;
    QString name;
    A(){}
    A(QString n)
    {
        qDebug()<<"~~~this is construct:A(QString n)";
        name = n;
    }
    A(A *ma,QString n)
    {
        qDebug()<<"~~~this is construct:  A(A *ma,QString n)";
        name = n;ma->v.push_back(this);
    }
    virtual QString g() {return name;}
    virtual ~A()
    {
        qDebug()<<"~A()";
        if(!v.isEmpty())
        {
            qDebug()<<"~~~~~~~~start~~~~~~~~";
            for(int i= 0;i<v.size();i++)
            {
                qDebug()<<"~~~object= "<<i<<" is:"<<v[i]->g()<<endl;
                delete v[i];
            }
            qDebug()<<"~~~~~~~~end~~~~~~~~";
        }
    }
};
class B: public A{
public:
    QString name;
    B(QString str)
    {
        qDebug()<<"~~~this is construct:B(QString n)";
        name = str;
    }
    B(A *ma,QString n):A(ma,n)
    {
        qDebug()<<"~~~this is construct: B(A *ma,QString n):A(ma,n)";
        name =n;
    }
    ~B()
    {
        qDebug()<<" ~B()~~name:"<<g();
    }
    virtual QString g() {return name;}
};

class C: public A{
public:
    QString name;
    C(A* ma, QString str):A(ma,str)
    {
        qDebug()<<"~~~this is construct: C(A* ma, QString str)";
        name = str;
    }
    QString g() {return name;}
    ~C()
    {
        qDebug()<<"~C()~~name:"<<g();
    }
};
class D: public B{
public:
    QString name;
    D(A* ma, QString str):B(ma,str)
    {
        qDebug()<<"~~~this is construct: D(A* ma, QString str)";
        name = str;
    }
    QString g() {return name;}
    ~D()
    {
        qDebug()<<"~D()~~name:"<<g();
    }
};
int main(int argc, char *argv[])
{
    QApplication a(argc, argv);
    MainWindow w;
    A ma("ma");
    B *pb = new B(&ma,"pb");
    B *pb1 = new B(&ma,"pb1");
    B *pb2=new B(pb,"pb2");
    C *pc1=new C(pb,"pc1");
    A* pa1=new A(pb1,"pa1");
    B* pb3=new B(pb1,"pb3");
    C* pc2=new C(pb1,"pc2");
    D* pd1=new D(pc1,"pd1");
    D* pd2=new D(pc1,"pd2");
    w.show();
    qDebug()<<"---------------------------------------------------";
    return a.exec();
}

输出说明:

//A ma("ma");
~~~this is construct:A(QString n)
// B *pb = new B(&ma,"pb");  先A的构造后B的,因为B的基类是A
~~~this is construct:  A(A *ma,QString n)
~~~this is construct: B(A *ma,QString n):A(ma,n)
//B *pb1 = new B(&ma,"pb1");   先A的构造后B的
~~~this is construct:  A(A *ma,QString n)
~~~this is construct: B(A *ma,QString n):A(ma,n)
//B *pb2=new B(pb,"pb2");    先A的构造后B的
~~~this is construct:  A(A *ma,QString n)
~~~this is construct: B(A *ma,QString n):A(ma,n)
// C *pc1=new C(pb,"pc1");    先A后C
~~~this is construct:  A(A *ma,QString n)
~~~this is construct: C(A* ma, QString str)
//A* pa1=new A(pb1,"pa1");
~~~this is construct:  A(A *ma,QString n)
//B* pb3=new B(pb1,"pb3");  先A后B , pb1已经构造好的对象,传参走B的构造
~~~this is construct:  A(A *ma,QString n)
~~~this is construct: B(A *ma,QString n):A(ma,n)
//C* pc2=new C(pb1,"pc2");    先A后C
~~~this is construct:  A(A *ma,QString n)
~~~this is construct: C(A* ma, QString str)
// D* pd1=new D(pc1,"pd1");  D继承B,B继承A,因此构造顺序 A B D
~~~this is construct:  A(A *ma,QString n)
~~~this is construct: B(A *ma,QString n):A(ma,n)
~~~this is construct: D(A* ma, QString str)
// D* pd2=new D(pc1,"pd2"); D继承B,B继承A,因此构造顺序 A B D 
~~~this is construct:  A(A *ma,QString n)
~~~this is construct: B(A *ma,QString n):A(ma,n)
~~~this is construct: D(A* ma, QString str)1//函数中有指针,临时变量,因此优先析构临时变量,指针需要手动delete 否则由程序自动释放
~A()
~~~~~~~~start~~~~~~~~
//v中有pb,pb1,先析构pb
~~~object=  0  is: "pb" 

 ~B()~~name: "pb"
 //要析构pb 先析构pb的子对象,pb2和pc1
~A()
~~~~~~~~start~~~~~~~~
~~~object=  0  is: "pb2" 

 ~B()~~name: "pb2"
 //析构pc1
~A()
~~~object=  1  is: "pc1" 
//pc1又有子对象pd1,和pd2
~C()~~name: "pc1"
//析构pd1  D继承B,B继承A ,因此析构顺序 D B A
~A()
~~~~~~~~start~~~~~~~~
~~~object=  0  is: "pd1" 
//
~D()~~name: "pd1"
 ~B()~~name: "pd1"
 //析构pd2
~A()
~~~object=  1  is: "pd2" 

~D()~~name: "pd2"
 ~B()~~name: "pd2"
~A()
~~~~~~~~end~~~~~~~~ //pd2析构完成
~~~~~~~~end~~~~~~~~//pb析构完成
//开始析构pb1
~~~object=  1  is: "pb1" 
//pb1包含 pa1,pb3,pc2
 ~B()~~name: "pb1"
~A()
~~~~~~~~start~~~~~~~~
~~~object=  0  is: "pa1" 

~A()
~~~object=  1  is: "pb3" 

 ~B()~~name: "pb3"
~A()
~~~object=  2  is: "pc2" 

~C()~~name: "pc2"
~A()
~~~~~~~~end~~~~~~~~
~~~~~~~~end~~~~~~~~

QObject类、对象树、生命期

  1. QObject 类是所有 Qt 对象的基类,是 Qt 对象模型的核心,所有 Qt 部件都继承自 QObject。
  2. QObject 及其派生类的单形参构造函数应声明为 explicit,以避免发生隐式类型转换。
  3. QObject 类既没有复制构造函数也没有赋值操作符函数(实际上它们被声明为私有的),因此无法通过值传递的方式向函数传递一个 QObject 对象。
  4. Qt 库中的 QObject 对象是以树状结构组织自已的,当创建一个 QObject 对象时,可以为其设置父对象,新创建的对象会被加入到父对象的子对象列表中(可通过QObject::children()函数查询), 因为 Qt 的部件类,都是以 QObject 为基类,因此, Qt 的所有部件类都具有对象树的特性。
  5. 对象树的组织规则:
    ①、 每一个 QObject 对象只能有一个父 QObject 对象,但可以有任意数量的子 QObject 对象。 比如
    A ma; B mb; C mc;
    ma.setParent(&mb); //将对象 ma 添加到 mb 的子对象列表中,
    ma.setParent(&mc); //该语句会把 ma 从 mb 的子对象列表中移出,并将其添加到mc 的子对象列表中。 setParent 函数见后文。
    ②、 QObject 对象会把指向各个子对象地址的指针放在 QObjectList 之中。 QObjectList 是QList<QObject*>的别名, QList 是 Qt 的一个列表容器。
  6. 对象删除规则(注意:以下规则并非 C++语法规则,而是 Qt 的对象树规则):
    ①、 基本规则: 父对象会自动删除子对象。 父对象拥有对象的所有权, 在父对象被删除时会在析构函数中自动删除其子对象。
    ②、 手动删除子对象:当手动删除子对象时,会把该子对象从父对象的列表中移除, 以避免父对象被删除时该子对象被再次删除。 总之 QObject 对象不会被删除两次。
    ③、当一个对象被删除时,会发送一个 destroyed()信号,可以捕捉该信号以避免对 QObject对象的悬垂引用。
  7. 对象创建规则
    ①、 子对象通常应创建在堆中(使用 new 创建), 此时就不再需要使用 delete 将其删除了,当父对象被删除时,会自动删除该子对象。
    ②、 对于 Qt 程序,父对象通常创建在栈上,不应创建在堆上(使用 new 创建)
    ③、 子对象不应创建在栈中,因为若父对象比子对象更早的结束生命期(即父对象创建于子对象之后),则子对象会被删除两次,第一次发生在父对象生命期结束时, 由 Qt 对象树的规则,使用父对象删除子对象,第二次发生在子对象生命期结束时, 由 C++规
    则删除子对象。 这种错误可使用先创建父对象后创建子对象的方法解决,依据 C++规则,子对象会先被删除, 由 Qt 对象树规则知, 此时子对象会从父对象的列表中移除,当父对象结束生命期时,就不会再次删除子对象了。
  8. 其他规则:应确保每一个 QObject 对象在 QApplication 之后创建,在 QApplication 销毁之
    前销毁,因此 QObject 对象不能是 static 存储类型的,因为 static 对象将在 main()返回之后
    才被销毁,其销毁时间太迟了。
  9. 对象的名称:可以为每个对象设置一个对象名称,其主要作用是方便对对象树进行查询
    和管理。对象名称和对象是不同的,比如 A ma; 其中 ma 是对象,若为 ma 设置一个名称
    为“SSS”,则对象 ma 的对象名称为“SSS”
  10. 设置父对象的方法
    ①、 创建对象时,在构造函数中指定父对象, QObject 类及其子类都有一个形如 QObject
    *parent=0 的形参的构造函数,因此我们可以在创建对象时在构造函数中直接指定父
    对象。
    ②、使用 void QObject::setParent(QObject *parent)函数为该对象设置父对象。
  11. 设置对象名称:
    对象名称由 QObject 的 objectName 属性指定(默认值为空字符串),该属性的读取函数分别如下所示(注:对象的类名可通过 QMetaObject::className()查询。)
    Qstring objectName() const //读取该对象名称
    void setObjectName(const QString &name); //设置该对象的名称为 name

示例:对象树与对象的删除

#include "mainwindow.h"
#include <QApplication>
#include<iostream>
#include<QVector>
#include<QDebug>
using namespace std;
class A:public QObject{
public:
    A(){}
    A(QObject *p):QObject (p){}
    ~A()
    {
        qDebug()<<objectName()<<"=~A";
    }
};
class B:public QObject{
public:
    B(){}
    B(QObject *p):QObject (p){}
    ~B()
    {
        qDebug()<<objectName()<<"=~B";
    }
};
int main(int argc, char *argv[])
{
    QApplication a(argc, argv);
    MainWindow w;
    A ma;   // 父对象通常创建在栈上
    A *pa1 = new A(&ma);    //在堆上指定父对象
    A *pa2 = new A(&ma);
    B *pb1 = new B(pa1);
    B *pb2 = new B(pa2);
    ma.setObjectName("ma");
    pa1->setObjectName("pa1");
    pa2->setObjectName("pa2");
    pb1->setObjectName("pb1");
    pb2->setObjectName("pb2");
    A ma1;
    B mb1;
    mb1.setParent(&ma1); //在栈上把ma1 指定为mb1的父对象,此处父对象创建于子对象之前
    ma1.setObjectName("ma1");
    mb1.setObjectName("mb1");

    //下述错误,栈上指定父对象,父对象应创建于子对象之前
    //此处会导致子对象mb2,被析构两次,导致程序崩溃
    B  mb2;
    A ma2;
    //mb2.setParent(&ma2);

    w.show();
    qDebug()<<"---------------------------------------------------";
    return a.exec();
}

输出:

在这里插入图片描述

"" =~A    //ma2的析构
"" =~B     //mb2的析构
"mb1" =~B   //mb1的析构
"ma1" =~A     //ma1的析构
"ma" =~A       //ma的析构。其余是堆上创建的,系统自己回收
"pa1" =~A       //各对象负责删除自己的子对象
"pb1" =~B
"pa2" =~A
"pb2" =~B
  1. 查询对象树的信息,可使用以下 QObject 类中的成员函数
    ①、 QObject* parent() const 返回一个指向父对象的指针。
    ②、 const QObjectList& children() const
     作用:返回指向父对象中全部子对象的指针列表,新添加的子对象位于列表的最后(某些特定操作可改变该顺序,例如提升或降低 QWidget 子对象)。其中QObjectList 类型如下
    typedef QList<QObject*> QObjectList; //QList 是 Qt 的列表容器。
    ③、 QList findChildren ( const QString& name=QString(),Qt::FindChildOptions options=Qt::FindChildrenRecursively) const
     作用: 返回能转换为类型 T,且名称为 name 的所有子对象,若没有此对象,则返回空列表,若 name 为默认值,则会匹配所有对象的名称。该函数是按递归方式执行的。
     该函数与下面介绍的 findChild 的主要用途是可以通过父对象(或父部件)获取指向子对象(或子部件)的指针。
     name 参数:该参数是由 QObject::setObjectName 函数设置的名称。
     options 参数:该参数用于指定查询子对象的方式,该参数需要指定一个FindChildOption 类型的枚举值, FindChildOptions(注意,后面多了一个 s)是由Flags使用 typedef 重命名后的结果。 该参数可取以下两个FindChildOption 类型的枚举值
     Qt::FindDirectChlidrenOnly:表示查找该对象的直接子对象。
     Qt::FindChildrenRecursively:表示按递归方式查询该对象的所有子对象。
     注:经测试,当该对象没有名称为 name 的子对象时,该函数才返回空。
    ④、 T findChild(const QString& name=QString(),
    Qt::FindChildOptions options=Qt::FindChildrenRecursively) const
    该函数的作用与 findChildren 相同,但是该函数只能返回单个的子对象,
    ⑤、 void dumpObjectTree()
    该函数可在调试模式(debug)下输出某个对象的整个对象树结构。该函数在发布模式(release)下不会执行任何操作

示例:查询对象树

#include "mainwindow.h"
#include <QApplication>
#include<iostream>
#include<QVector>
#include<QDebug>
using namespace std;
class A:public QObject
{
public: A(){}
    A(QObject *p):QObject(p){}
    void f(){qDebug()<<"AF"; }
};
class B:public QObject
{
public:
    B(){}
    B(QObject *p):QObject(p){}
    void f(){qDebug()<<"BF";}
};
class C:public QObject
{
public: int c;
    C(){}
    C(QObject *p):QObject(p){}
    void f(){qDebug()<<"CF";}
};
int main(int argc, char *argv[])
{
    QApplication a(argc, argv);
    MainWindow w;

    A ma; A *pa1=new A(&ma); A *pa2=new A(&ma);
    B *pb1=new B(pa1); B *pb2=new B(pa1);
    C *pc1=new C(pb1); C *pc2=new C(pb1);
    ma.setObjectName("ma"); pa1->setObjectName("pa1"); pa2->setObjectName("pa2");
    pb1->setObjectName("pb1"); pb2->setObjectName("pb2");
    pc1->setObjectName("pc1"); pc2->setObjectName("pc2");
    pc2->c=2; pc1->c=1;
    QObjectList st= ma.children();
    qDebug()<<"~~~~~~~~~~~~~~~~~~~~~~~~~~";
    qDebug()<<"ma="<<ma.children();
    //以上输出: ma= (QObject(0x82d638, name = "pa1"), QObject(0x82d478, name = "pa2"))
    qDebug()<<"pb1="<<pb1->children();
    //以上输出: pb1= (QObject(0x840f38, name = "pc1"), QObject(0x840bf0, name = "pc2"))
    qDebug()<<"\n##### dumpObjectTree #####";
    ma.dumpObjectTree(); //输出见下图
    qDebug()<<"############";
    pb1->dumpObjectTree(); //输出见下图
    qDebug()<<"\n###### findChild #####";
    C* p1=ma.findChild<C*>("pc1"); //通过父对象 ma 获取指向子对象 pc1 的指针。
    p1->f(); //输出 CF
    qDebug()<<p1->c; //输出 1
    C* p2=ma.findChild<C*>("pc2",Qt::FindDirectChildrenOnly); /*获取ma的直接子对象中名称为pc2
    的对象的指针,因为 pc2 不是 ma 的直接子类,所以该处返回 NULL。 */
    //qDebug()<<p2->c; //错误,此时 p2指向的是NULL
    qDebug()<<"\n###### findChildren #####";
    QList<C*> s=ma.findChildren<C*>("pc1"); //Qt::FindDirectChildrenOnly
    qDebug()<<s; //输出: (QObject(0x840f38, name = "pc1"))
    qDebug()<<s[0]->c; //输出 1
    QList<C*> s1=ma.findChildren<C*>("pc2",Qt::FindDirectChildrenOnly);
    qDebug()<<s1; //输出一个空圆括号,因为 pc2 不是 ma 的直接子对象。
    //查找 ma 的直接子对象
    QList<C*> s2=ma.findChildren<C*>(QString(),Qt::FindDirectChildrenOnly);
    qDebug()<<s2; //输出: (QObject(0x82d638, name = "pa1"), QObject(0x82d478, name = "pa2"))
    QList<C*> s3=ma.findChildren<C*>(); //获取 ma 的整个对象树结构的列表
    qDebug()<<s3;

    w.show();
    qDebug()<<"---------------------------------------------------";
    return a.exec();
}

输出:
在这里插入图片描述

ma= (QObject(0x29041c74840, name = "pa1"), QObject(0x29041c74ca0, name = "pa2"))
pb1= (QObject(0x29043fb2b60, name = "pc1"), QObject(0x29043fb31c0, name = "pc2"))

##### dumpObjectTree #####
QObject::ma 
    QObject::pa1 
        QObject::pb1 
            QObject::pc1 
            QObject::pc2 
        QObject::pb2 
    QObject::pa2 
############
QObject::pb1 
    QObject::pc1 
    QObject::pc2 

###### findChild #####
CF
1

###### findChildren #####
(QObject(0x29043fb2b60, name = "pc1"))
1
()
(QObject(0x29041c74840, name = "pa1"), QObject(0x29041c74ca0, name = "pa2"))
(QObject(0x29041c74840, name = "pa1"), QObject(0x29041c74f20, name = "pb1"), QObject(0x29043fb2b60, name = "pc1"), QObject(0x29043fb31c0, name = "pc2"), QObject(0x29041c74890, name = "pb2"), QObject(0x29041c74ca0, name = "pa2"))
  • 3
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值