C++对象布局

转自 https://segmentfault.com/a/1190000023098876

C++对象布局

什么是多态?

多态可分为编译时多态和运行时多态。

  • 编译时多态:基于模板和函数重载方式,在编译时就已经确定对象的行为,也称为静态绑定。
  • 运行时多态:面向对象的一大特色,通过继承方式使得程序在运行时才会确定相应调用的方法,也称动态绑定,它的实现主要是依赖于虚函数表。

如何查看对象的布局

gcc中可以使用如下命令查看对象布局:

g++ -fdump-class-hierarchy model.cc后查看生成的文件

clang中可以使用如下命令:

clang -Xclang -fdump-record-layouts -stdlib=libc++ -c model.cc
// 查看对象布局
clang -Xclang -fdump-vtable-layouts -stdlib=libc++ -c model.cc
// 查看虚函数表布局

1.普通类对象的布局

struct Base {
    Base() = default;
    ~Base() = default;
    
    void Func() {}

    int a;
    int b;
};

int main() {
    Base a;
    return 0; 
}

// 使用clang -Xclang -fdump-record-layouts -stdlib=libc++ -c model.cc查看

输出如下:

*** Dumping AST Record Layout
         0 | struct Base
         0 |   int a
         4 |   int b
           | [sizeof=8, dsize=8, align=4,
           |  nvsize=8, nvalign=4]

*** Dumping IRgen Record Layout

画出图如下:

在这里插入图片描述

2.带虚函数的类对象的布局

struct Base {
    Base() = default;
    virtual ~Base() = default;
    
    void FuncA() {}

    virtual void FuncB() {
        printf("FuncB\n");
    }

    int a;
    int b;
};

int main() {
    Base a;
    return 0; 
}

// 这里可以查看对象的布局和相应虚函数表的布局
clang -Xclang -fdump-record-layouts -stdlib=libc++ -c model.cc
clang -Xclang -fdump-vtable-layouts -stdlib=libc++ -c model.cc

对象布局如下:

*** Dumping AST Record Layout
         0 | struct Base
         0 |   (Base vtable pointer)
         8 |   int a
        12 |   int b
           | [sizeof=16, dsize=16, align=8,
           |  nvsize=16, nvalign=8]

*** Dumping IRgen Record Layout

这个含有虚函数的结构体大小为16,在对象的头部,前8个字节是虚函数表的指针,指向虚函数的相应函数指针地址,a占4个字节,b占4个字节,总大小16字节。

虚函数表布局:

Vtable for 'Base' (5 entries).
   0 | offset_to_top (0)
   1 | Base RTTI
       -- (Base, 0) vtable address --
   2 | Base::~Base() [complete]
   3 | Base::~Base() [deleting]
   4 | void Base::FuncB()

画出对象布局图如下:

在这里插入图片描述

offset_to_top(0):表示当前这个虚函数表地址距离对象顶部地址的偏移量,因为对象的头部就是虚函数表的指针,所以偏移量为0。

RTTI指针:指向存储运行时类型信息type_info的地址,用于运行时类型识别,用于typeiddynamic_cast

RTTI下面就是虚函数表指针真正指向的地址,存储了类里面所有的虚函数。

3.单继承下不含有覆盖函数的类对象的布局

struct Base {
    Base() = default;
    virtual ~Base() = default;
    
    void FuncA() {}

    virtual void FuncB() {
        printf("Base FuncB\n");
    }

    int a;
    int b;
};

struct Derive : public Base{
};

int main() {
    Base a;
    Derive d;
    return 0; 
}

子类对象布局

*** Dumping AST Record Layout
         0 | struct Derive
         0 |   struct Base (primary base)
         0 |     (Base vtable pointer)
         8 |     int a
        12 |     int b
           | [sizeof=16, dsize=16, align=8,
           |  nvsize=16, nvalign=8]

*** Dumping IRgen Record Layout

和上面相同,这个含有虚函数的结构体大小为16,在对象的头部,前8个字节是虚函数表的指针,指向虚函数的相应函数指针地址,a占4个字节,b占4个字节,总大小为16字节。

子类虚函数表布局:

Vtable for 'Derive' (5 entries).
   0 | offset_to_top (0)
   1 | Derive RTTI
       -- (Base, 0) vtable address --
       -- (Derive, 0) vtable address --
   2 | Derive::~Derive() [complete]
   3 | Derive::~Derive() [deleting]
   4 | void Base::FuncB()

画图如下

在这里插入图片描述

这个和上面也是相同的,注意下虚函数表这里的FuncB()函数,还是Base类中的FuncB,因为在子类中没有重写这个函数。

4.单继承下含有覆盖函数的类对象的布局

struct Base {
    Base() = default;
    virtual ~Base() = default;
    
    void FuncA() {}

    virtual void FuncB() {
        printf("Base FuncB\n");
    }

    int a;
    int b;
};

struct Derive : public Base{
    void FuncB() override {
        printf("Derive FuncB \n");
    }
};

int main() {
    Base a;
    Derive d;
    return 0; 
}

子类对象布局:

*** Dumping AST Record Layout
         0 | struct Derive
         0 |   struct Base (primary base)
         0 |     (Base vtable pointer)
         8 |     int a
        12 |     int b
           | [sizeof=16, dsize=16, align=8,
           |  nvsize=16, nvalign=8]

*** Dumping IRgen Record Layout

依旧和上面相同,这个含有虚函数的结构体大小为16;在对象的头部,前8个字节是虚函数表的指针,指向虚函数的相应函数指针地址,a占4个字节,b占4个字节,总大小为16字节。

子类虚函数表布局:

Vtable for 'Derive' (5 entries).
   0 | offset_to_top (0)
   1 | Derive RTTI
       -- (Base, 0) vtable address --
       -- (Derive, 0) vtable address --
   2 | Derive::~Derive() [complete]
   3 | Derive::~Derive() [deleting]
   4 | void Derive::FuncB()

在这里插入图片描述

注意这里虚函数表中的FuncB函数已经是Derive中的FuncB了,因为在子类中重写了父类的这个函数。

再注意这里的RTTI中有了两项,表示BaseDerive的虚表地址是相同的,Base类中的虚函数和Derive类中的虚函数都在这个链条下。

5.多继承下不含有覆盖函数的类对象的布局

struct BaseA {
    BaseA() = default;
    virtual ~BaseA() = default;
    
    void FuncA() {}

    virtual void FuncB() {
        printf("BaseA FuncB\n");
    }

    int a;
    int b;
};

struct BaseB {
    BaseB() = default;
    virtual ~BaseB() = default;
    
    void FuncA() {}

    virtual void FuncC() {
        printf("BaseB FuncC\n");
    }

    int a;
    int b;
};

struct Derive : public BaseA, public BaseB{
};

int main() {
    BaseA a;
    Derive d;
    return 0; 
}

类对象布局:

*** Dumping AST Record Layout
         0 | struct Derive
         0 |   struct BaseA (primary base)
         0 |     (BaseA vtable pointer)
         8 |     int a
        12 |     int b
        16 |   struct BaseB (base)
        16 |     (BaseB vtable pointer)
        24 |     int a
       28 |     int b
          | [sizeof=32, dsize=32, align=8,
          |  nvsize=32, nvalign=8]

Derive大小为32,注意这里有了两个虚表指针,因为Derive是多继承,一般情况下继承了几个带有虚函数的类,对象布局中就有几个虚表指针,并且子类也会继承基类的数据。一般来说,不考虑内存对齐的话,子类(继承父类)的大小=子类(不继承父类)的大小+所有父类的大小

虚函数表布局:

Vtable for 'Derive' (10 entries).
   0 | offset_to_top (0)
   1 | Derive RTTI
       -- (BaseA, 0) vtable address --
       -- (Derive, 0) vtable address --
   2 | Derive::~Derive() [complete]
   3 | Derive::~Derive() [deleting]
   4 | void BaseA::FuncB()
   5 | offset_to_top (-16)
   6 | Derive RTTI
       -- (BaseB, 16) vtable address --
   7 | Derive::~Derive() [complete]
       [this adjustment: -16 non-virtual]
   8 | Derive::~Derive() [deleting]
       [this adjustment: -16 non-virtual]
   9 | void BaseB::FuncC()

对象布局图如下:

在这里插入图片描述

offset_to_top(0):表示当前这个虚函数表(BaseA, Derive)地址距离对象顶部地址的偏移量,因为对象的头部就是虚函数表的指针,所以偏移量为0。

这里的RTTI中有了两项,表示BaseADerive的虚表地址是相同的,BaseA类里面的虚函数和Derive类里的虚函数都在这个链条下,截止到 offset_to_top(-16) 之前都是BaseADerive的虚函数表。

offset_to_top(-16):表示当前这个虚函数表(BaseB)地址距离对象顶部地址的偏移量,因为对象的头部就是虚函数表的指针,所以偏移量为-16,这里用于this指针偏移。

6.多继承下含有覆盖函数的类对象的布局

struct BaseA {
    BaseA() = default;
    virtual ~BaseA() = default;
    
    void FuncA() {}

    virtual void FuncB() {
        printf("BaseA FuncB\n");
    }

    int a;
    int b;
};

struct BaseB {
    BaseB() = default;
    virtual ~BaseB() = default;
    
    void FuncA() {}

    virtual void FuncC() {
        printf("BaseB FuncC\n");
    }

    int a;
    int b;
};

struct Derive : public BaseA, public BaseB{
    void FuncB() override {
        printf("Derive FuncB \n");
    }

    void FuncC() override {
        printf("Derive FuncC \n");
    }
};

int main() {
    BaseA a;
    Derive d;
    return 0; 
}

对象布局

*** Dumping AST Record Layout
         0 | struct Derive
         0 |   struct BaseA (primary base)
         0 |     (BaseA vtable pointer)
         8 |     int a
        12 |     int b
        16 |   struct BaseB (base)
        16 |     (BaseB vtable pointer)
        24 |     int a
        28 |     int b
           | [sizeof=32, dsize=32, align=8,
           |  nvsize=32, nvalign=8]

*** Dumping IRgen Record Layout

类大小仍然是32,和上面一样。

虚函数表布局:

Vtable for 'Derive' (11 entries).
   0 | offset_to_top (0)
   1 | Derive RTTI
       -- (BaseA, 0) vtable address --
       -- (Derive, 0) vtable address --
   2 | Derive::~Derive() [complete]
   3 | Derive::~Derive() [deleting]
   4 | void Derive::FuncB()
   5 | void Derive::FuncC()
   6 | offset_to_top (-16)
   7 | Derive RTTI
       -- (BaseB, 16) vtable address --
   8 | Derive::~Derive() [complete]
       [this adjustment: -16 non-virtual]
   9 | Derive::~Derive() [deleting]
       [this adjustment: -16 non-virtual]
  10 | void Derive::FuncC()
       [this adjustment: -16 non-virtual]

在这里插入图片描述

offset_to_top(0):表示当前这个虚函数表(BaseA, Derive)地址距离对象顶部地址的偏移量,因为对象的头部就是虚函数表的指针,所以偏移量为0。

这里的RTTI中有了两项,表示BaseADerive的虚表地址是相同的,BaseA类里面的虚函数和Derive类里的虚函数都在这个链条下,截止到 offset_to_top(-16) 之前都是BaseADerive的虚函数表。

offset_to_top(-16):表示当前这个虚函数表BaseB地址距离对象顶部地址的偏移量,因为对象的头部就是虚函数表指针,所以偏移量为-16。当基类BaseB的引用或指针base实际接受的是Derive类型的对象,执行base->FuncC()时候,由于FuncC()已经被重写,而此时的this指针指向的是BaseB类型的对象,需要对this指针进行调整,就是offset_to_top(-16),所以this指针向上调整了16字节,之后调用FuncC(),就调用到了被重写后的Derive虚函数表中的FuncC()函数。这些带adjustment标记的函数都是需要进行指针调整的。

7.多重继承不同的继承顺序导致的类对象的布局相同么?

struct BaseA {
    BaseA() = default;
    virtual ~BaseA() = default;
    
    void FuncA() {}

    virtual void FuncB() {
        printf("BaseA FuncB\n");
    }

    int a;
    int b;
};

struct BaseB {
    BaseB() = default;
    virtual ~BaseB() = default;
    
    void FuncA() {}

    virtual void FuncC() {
        printf("BaseB FuncC\n");
    }

    int a;
    int b;
};

struct Derive : public BaseB, public BaseA{
    void FuncB() override {
        printf("Derive FuncB \n");
    }

    void FuncC() override {
        printf("Derive FuncC \n");
    }
};

int main() {
    BaseA a;
    Derive d;
    return 0; 
}

对象布局:

*** Dumping AST Record Layout
         0 | struct Derive
         0 |   struct BaseB (primary base)
         0 |     (BaseB vtable pointer)
         8 |     int a
        12 |     int b
        16 |   struct BaseA (base)
        16 |     (BaseA vtable pointer)
        24 |     int a
        28 |     int b
           | [sizeof=32, dsize=32, align=8,
           |  nvsize=32, nvalign=8]

*** Dumping IRgen Record Layout

这里可见,对象布局和上面的不相同,BaseB的虚函数表指针和数据在上面,BaseA的虚函数表指针和数据在下面,以A,B的顺序继承,对象的布局就是A在上B在下,以B,A的顺序继承,对象的布局就是B在上A在下。

虚函数表布局:

Vtable for 'Derive' (11 entries).
   0 | offset_to_top (0)
   1 | Derive RTTI
       -- (BaseB, 0) vtable address --
       -- (Derive, 0) vtable address --
   2 | Derive::~Derive() [complete]
   3 | Derive::~Derive() [deleting]
   4 | void Derive::FuncC()
   5 | void Derive::FuncB()
   6 | offset_to_top (-16)
   7 | Derive RTTI
       -- (BaseA, 16) vtable address --
   8 | Derive::~Derive() [complete]
       [this adjustment: -16 non-virtual]
   9 | Derive::~Derive() [deleting]
       [this adjustment: -16 non-virtual]
  10 | void Derive::FuncB()
       [this adjustment: -16 non-virtual]

对象布局图如下:

在这里插入图片描述

虚函数表的布局也有所不同,BaseBDerive共用一个虚表地址,在整个虚表布局的上方,而布局的下半部分是BaseA的虚表,可见继承顺序不同,子类的虚表布局也有所不同。

8.虚继承的布局

struct Base {
    Base() = default;
    virtual ~Base() = default;
    
    void FuncA() {}

    virtual void FuncB() {
        printf("BaseA FuncB\n");
    }

    int a;
    int b;
};

struct Derive : virtual public Base{
    void FuncB() override {
        printf("Derive FuncB \n");
    }
};

int main() {
    Base a;
    Derive d;
    return 0; 
}

对象布局:

*** Dumping AST Record Layout
         0 | struct Derive
         0 |   (Derive vtable pointer)
         8 |   struct Base (virtual base)
         8 |     (Base vtable pointer)
        16 |     int a
        20 |     int b
           | [sizeof=24, dsize=24, align=8,
           |  nvsize=8, nvalign=8]

*** Dumping IRgen Record Layout

虚继承下,这里的对象布局和普通单继承有所不同,普通单继承下子类和基类公用一个虚表地址,而在虚继承下,子类和虚基类分别有一个虚表地址的指针,两个指针的大小一共是16字节,再加上a和b的大小8,总共为24字节。

虚函数表:

Vtable for 'Derive' (13 entries).
   0 | vbase_offset (8)
   1 | offset_to_top (0)
   2 | Derive RTTI
       -- (Derive, 0) vtable address --
   3 | void Derive::FuncB()
   4 | Derive::~Derive() [complete]
   5 | Derive::~Derive() [deleting]
   6 | vcall_offset (-8)
   7 | vcall_offset (-8)
   8 | offset_to_top (-8)
   9 | Derive RTTI
       -- (Base, 8) vtable address --
  10 | Derive::~Derive() [complete]
       [this adjustment: 0 non-virtual, -24 vcall offset offset]
  11 | Derive::~Derive() [deleting]
       [this adjustment: 0 non-virtual, -24 vcall offset offset]
  12 | void Derive::FuncB()
       [this adjustment: 0 non-virtual, -32 vcall offset offset]

对象布局图如下:
在这里插入图片描述

vbase_offset(8):对象在对象布局中与指向虚基类虚函数表的指针地址的偏移量。

vcall_offset(-8):当虚基类Base的引用或指针base实际接受的是Derive类型的对象,执行base->FuncB()时候,由于FuncB()已经被重写,而此时的this指针指向的是Base类型的对象,需要对this指针进行调整,就是vcall_offset(-8),所以this指针向上调整了8字节,之后调用FuncB(),就调用到了被重写后的FuncB()函数。

9.虚继承带未覆盖函数的对象布局

struct Base {
    Base() = default;
    virtual ~Base() = default;
    
    void FuncA() {}

    virtual void FuncB() {
        printf("Base FuncB\n");
    }

    virtual void FuncC() {
        printf("Base FuncC\n");
    }

    int a;
    int b;
};

struct Derive : virtual public Base{
    void FuncB() override {
        printf("Derive FuncB \n");
    }
};

int main() {
    Base a;
    Derive d;
    return 0; 
}

对象布局:

*** Dumping AST Record Layout
         0 | struct Derive
         0 |   (Derive vtable pointer)
         8 |   struct Base (virtual base)
         8 |     (Base vtable pointer)
        16 |     int a
        20 |     int b
           | [sizeof=24, dsize=24, align=8,
           |  nvsize=8, nvalign=8]

*** Dumping IRgen Record Layout

和上面虚继承情况下相同,普通单继承下子类和基类共用一个虚表地址,而在虚继承下,子类和虚基类分别有一个虚表地址的指针,两个指针大小总和为16,再加上a和b的大小8,为24字节。

虚函数表布局:

Vtable for 'Derive' (15 entries).
   0 | vbase_offset (8)
   1 | offset_to_top (0)
   2 | Derive RTTI
       -- (Derive, 0) vtable address --
   3 | void Derive::FuncB()
   4 | Derive::~Derive() [complete]
   5 | Derive::~Derive() [deleting]
   6 | vcall_offset (0)
   7 | vcall_offset (-8)
   8 | vcall_offset (-8)
   9 | offset_to_top (-8)
  10 | Derive RTTI
       -- (Base, 8) vtable address --
  11 | Derive::~Derive() [complete]
       [this adjustment: 0 non-virtual, -24 vcall offset offset]
  12 | Derive::~Derive() [deleting]
       [this adjustment: 0 non-virtual, -24 vcall offset offset]
  13 | void Derive::FuncB()
       [this adjustment: 0 non-virtual, -32 vcall offset offset]
  14 | void Base::FuncC()

对象布局图如下:

在这里插入图片描述

vbase_offset(8):对象在对象布局中与指向虚基类虚函数表的指针地址的偏移量。

vcall_offset(-8):当虚基类Base的引用或指针base实际接受的是Derive类型的对象,执行base->FuncB()时候,由于FuncB()已经被重写,而此时的this指针指向的是Base类型的对象,需要对this指针进行调整,就是vcall_offset(-8),所以this指针向上调整了8字节,之后调用FuncB(),就调用到了被重写后的FuncB()函数。

vcall_offset(0):当Base的引用或指针base实际接受的是Derive类型的对象,执行base->FuncC()时候,由于Func()没有被重写,所以不需要对this指针进行调整,就是vcall_offset(0),之后调用FuncC()

10.菱形继承下类对象的布局

struct Base {
    Base() = default;
    virtual ~Base() = default;
    
    void FuncA() {}

    virtual void FuncB() {
        printf("BaseA FuncB\n");
    }

    int a;
    int b;
};

struct BaseA : virtual public Base {
    BaseA() = default;
    virtual ~BaseA() = default;
    
    void FuncA() {}

    virtual void FuncB() {
        printf("BaseA FuncB\n");
    }

    int a;
    int b;
};

struct BaseB : virtual public Base {
    BaseB() = default;
    virtual ~BaseB() = default;
    
    void FuncA() {}

    virtual void FuncC() {
        printf("BaseB FuncC\n");
    }

    int a;
    int b;
};

struct Derive : public BaseB, public BaseA{
    void FuncB() override {
        printf("Derive FuncB \n");
    }

    void FuncC() override {
        printf("Derive FuncC \n");
    }
};

int main() {
    BaseA a;
    Derive d;
    return 0; 
}

类对象布局:

*** Dumping AST Record Layout
         0 | struct Derive
         0 |   struct BaseB (primary base)
         0 |     (BaseB vtable pointer)
         8 |     int a
        12 |     int b
        16 |   struct BaseA (base)
        16 |     (BaseA vtable pointer)
        24 |     int a
        28 |     int b
        32 |   struct Base (virtual base)
        32 |     (Base vtable pointer)
        40 |     int a
        44 |     int b
           | [sizeof=48, dsize=48, align=8,
           |  nvsize=32, nvalign=8]

*** Dumping IRgen Record Layout

虚函数表:

Vtable for 'Derive' (20 entries).
   0 | vbase_offset (32)
   1 | offset_to_top (0)
   2 | Derive RTTI
       -- (BaseB, 0) vtable address --
       -- (Derive, 0) vtable address --
   3 | Derive::~Derive() [complete]
   4 | Derive::~Derive() [deleting]
   5 | void Derive::FuncC()
   6 | void Derive::FuncB()
   7 | vbase_offset (16)
   8 | offset_to_top (-16)
   9 | Derive RTTI
       -- (BaseA, 16) vtable address --
  10 | Derive::~Derive() [complete]
       [this adjustment: -16 non-virtual]
  11 | Derive::~Derive() [deleting]
       [this adjustment: -16 non-virtual]
  12 | void Derive::FuncB()
       [this adjustment: -16 non-virtual]
  13 | vcall_offset (-32)
  14 | vcall_offset (-32)
  15 | offset_to_top (-32)
  16 | Derive RTTI
       -- (Base, 32) vtable address --
  17 | Derive::~Derive() [complete]
       [this adjustment: 0 non-virtual, -24 vcall offset offset]
  18 | Derive::~Derive() [deleting]
       [this adjustment: 0 non-virtual, -24 vcall offset offset]
  19 | void Derive::FuncB()
       [this adjustment: 0 non-virtual, -32 vcall offset offset]

对象布局图如下:
在这里插入图片描述

vbase_offset(32)

vbase_offset(16):对象在对象布局中与指向虚基类虚函数表的指针地址的偏移量。

offset_to_top(0)

offset_to_top(-16)

offset_to_top(-32):指向虚函数表的地址与对象顶部地址的偏移量。

vcall_offset(-32):当虚基类Base的引用或指针base实际接受的是Derive类型的对象,执行base->FuncB()时候,由于FuncB()已经被重写,而此时的this指针指向的是Base类型的对象,需要对this指针进行调整,就是vcall_offset(-32),所以this指针向上调整了32字节,之后调用FuncB(),就调用到了被重写后的FuncB()函数。

为什么要虚继承?

在这里插入图片描述
非虚继承时,显然D会继承两次A,内部就会存储两份A的数据,浪费空间,而且还有二义性,D调用A的方法是,由于有两个A,究竟是调用哪个A的方法呢,编译器也不知道,就会报错,所以有了虚继承,解决了空间浪费以及二义性问题。在虚继承下,只有一个共享的基类子对象被继承,而无论该基类在派生层次中出现了多少次。共享的基类子对象被称为虚基类。在虚继承下,基类子对象的复制以及由此引起的二义性都被消除了。

为什么虚函数表中有两个析构函数?

前面的代码输出中我们可以看到虚函数表中有两个析构函数,一个标志为deleting,一个标志为complete,因为对象有两种构造方式,栈构造和堆构造,所以对应的实现上,对象也有两种析构方式,其中堆上对象的析构和栈上对象的析构不同之处在于,栈内存的析构不需要执行delete函数,会自动被回收。

总结

offset_to_top:对象在对象布局中与对象顶部地址的偏移量。

RTTI指针:指向存储运行时类型信息type_info的地址,用于运行时类型识别,用于typeiddynamic_cast

vbase_offset:对象在对象布局中与指向虚基类虚函数表的指针地址的偏移量。

vcall_offset:父类引用或指针指向子类对象,调用被子类重写的方法时,用于对虚函数执行指针地址调整,方便成功调用被重写的方法。

thunk:表示上面虚函数表中带有adjustment字段的函数调用需要先进行this指针调整,才可以调用到被子类重写的函数。

对象在Linux中的布局

A* a = new Derive();	//A为Derive的基类

如图:

在这里插入图片描述

a作为对象指针存储在栈中,指向在堆中的类A的实例内存,其中实例内存布局中有虚函数表指针,指针指向的虚函数表存放在数据段中,虚函数表中的各个函数指针指向的函数在代码段中。
在这里插入图片描述

虚表结构大体如上图所示,正常的虚表结构中都含有后三项,当有虚继承情况下会有前两个表项。

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值