C++ && 汇编

7 篇文章 0 订阅

C++是一种面向对象的高级语言,但是由于其基于C语言发展而来,因此其内在原理和C语言如出一辙,于是就来看看C++程序翻译成汇编代码是啥样的(在x86 linux环境下,使用g++ 翻译得到的结果),采用了C++11新标准,因此可以顺路看一下C++11中的mov语义究竟是如何实现的

一、翻译的原cpp文件:

extern double sqrt(double x);
extern double abs(double x);

struct Position
{
public:
    Position(double px = 0 , double py = 0 , double pz = 0) : px(px) , py(py) , pz(pz){}
    double px , py , pz;
};

int distance(const Position &x , const Position &y)
{
    return abs(sqrt((x.px * x.px + x.py * x.py + x.pz * x.pz)) - sqrt((y.px * y.px + y.py * y.py + y.pz * y.pz)));
}

class Gun 
{
public:
    Gun(unsigned nbullet = 0) : nbullet(nbullet){}
    virtual bool shot(const Position &pfrom , const Position &pto);
    void set_bullet(unsigned n)
    {
        nbullet = n;
    }

    unsigned get_bullet()const
    {
        return nbullet;
    }

    virtual Gun* copy_self()const;
    virtual ~Gun(){}
private:
    unsigned nbullet;
};

Gun* Gun::copy_self()const
{
    return new Gun(*this);
}

bool Gun::shot(const Position &pfrom , const Position &pto)
{
    if(nbullet == 0)
        return false;
    else
        nbullet--;

    return true;
}

class HandGun : public Gun
{
public:
    HandGun(unsigned nbullet = 0 , double dist = 0) : Gun(nbullet) , dist(dist){}
    bool shot(const Position &px , const Position &py);
    HandGun* copy_self()const;

    void set_dist(double d)
    {
        dist = d;
    }

    double get_dist()const
    {
        return dist;
    }

private:
    double dist;
};

HandGun* HandGun::copy_self()const
{
    return new HandGun(*this);
}

bool HandGun::shot(const Position &px , const Position &py)
{
   if(!Gun::shot(px , py))
       return false;

   return distance(px , py) < dist;
}

class Soldior
{
public:
   Soldior(const Position &pos , const Gun *g = nullptr) : pos(pos)
   {
       if(g == nullptr)
           gun = nullptr;
       else
           gun = g->copy_self();
   }

   Soldior(const Soldior& s) 
   {
      if(s.gun == nullptr)
          gun = nullptr;
      else
          gun = s.gun->copy_self();
      pos = s.pos;
   }

   Soldior(Soldior &&s) noexcept
   {
       gun = s.gun;
       s.gun = nullptr;
       pos = s.pos;
   }

   bool shot(const Position &pt)
   {
       return gun->shot(pos , pt);
   }

   Soldior& operator=(const Soldior &s)
   {
      if(this == &s)
          return *this;

      if(gun != nullptr)
          delete gun;
      
      if(s.gun == nullptr)
         gun = nullptr;
      else
      	 gun = s.gun->copy_self();

      pos = s.pos;
      return *this;
   }

   Soldior& operator=(Soldior &&s)
   {
      if(this == &s)
          return *this;

      if(gun != nullptr)
          delete gun;

      gun = s.gun;
      s.gun = nullptr;
      pos = s.pos;
      return *this;
   } 

   ~Soldior()
   {
      if(gun != nullptr)
         delete gun;
   }

private:
   Gun *gun;
   Position pos;
};

 
extern  Soldior getSoldior();

int main(int argc , char *argv[])
{
    Gun *g = new Gun(100);
    Soldior *s = new Soldior(Position(20 , 30 , 50) , g);
    *s = getSoldior();
    delete g;
    delete s;
}

二、翻译得到的对应asm汇编文件:(只保留了对我们理解C++有用的部分)


_ZN8PositionC2Eddd:  ;constructor of Position
	pushq	%rbp
	movq	%rsp, %rbp ;save rbp 

	movq	%rdi, -8(%rbp) ;get this pointer
	movsd	%xmm0, -16(%rbp) ;get px
	movsd	%xmm1, -24(%rbp) ;get py 
	movsd	%xmm2, -32(%rbp) ;get pz

	movq	-8(%rbp), %rdx 
	movq	-16(%rbp), %rax
	movq	%rax, (%rdx) ;init px 

	movq	-8(%rbp), %rdx
	movq	-24(%rbp), %rax
	movq	%rax, 8(%rdx) ;init py

	movq	-8(%rbp), %rdx
	movq	-32(%rbp), %rax
	movq	%rax, 16(%rdx) ;init pz

	popq	%rbp ;recover rbp
	ret

_Z8distanceRK8PositionS1_: ;distance
	pushq	%rbp
	movq	%rsp, %rbp

	subq	$32, %rsp ;extend stack 

	movq	%rdi, -8(%rbp) ;get address of x
	movq	%rsi, -16(%rbp) ;get address of y

	movq	-8(%rbp), %rax
	movsd	(%rax), %xmm1 ;get x.px
	movq	-8(%rbp), %rax
	movsd	(%rax), %xmm0 ;get x.px
	mulsd	%xmm0, %xmm1 ;%xmm1 = x.px * x.px

	movq	-8(%rbp), %rax
	movsd	8(%rax), %xmm2
	movq	-8(%rbp), %rax
	movsd	8(%rax), %xmm0
	mulsd	%xmm2, %xmm0 ;%xmm0 = x.py * x.py

	addsd	%xmm0, %xmm1 ;%xmm1 += %xmm0

	movq	-8(%rbp), %rax
	movsd	16(%rax), %xmm2
	movq	-8(%rbp), %rax
	movsd	16(%rax), %xmm0
	mulsd	%xmm2, %xmm0 ;%xmm0 = x.pz * x.pz

	addsd	%xmm1, %xmm0 ;%xmm0 += %xmm1

	call	_Z4sqrtd ;call sqrt with argument %xmm0
	movsd	%xmm0, -24(%rbp) ;save return value of sqrt

	movq	-16(%rbp), %rax ;mov address value of y into %rax

	movsd	(%rax), %xmm1
	movq	-16(%rbp), %rax
	movsd	(%rax), %xmm0
	mulsd	%xmm0, %xmm1 ;%xmm1 = y.px * y.px

	movq	-16(%rbp), %rax
	movsd	8(%rax), %xmm2
	movq	-16(%rbp), %rax
	movsd	8(%rax), %xmm0 
	mulsd	%xmm2, %xmm0 ;%xmm0 = y.py * y.py

	addsd	%xmm0, %xmm1 ;%xmm1 += %xmm0

	movq	-16(%rbp), %rax
	movsd	16(%rax), %xmm2
	movq	-16(%rbp), %rax
	movsd	16(%rax), %xmm0
	mulsd	%xmm2, %xmm0 ;%xmm0 = y.pz * y.pz

	addsd	%xmm1, %xmm0 ;%xmm0 += %xmm1

	call	_Z4sqrtd ;call sqrt with argument %xmm0
	movsd	-24(%rbp), %xmm3 ;mov return value of first call to sqrt to %xmm3

	subsd	%xmm0, %xmm3 ;%xmm3 -= %xmm0
	movapd	%xmm3, %xmm0 ;save %xmm3 to %xmm0 for pass to call absd as argument

	call	_Z3absd ;call absd

	cvttsd2si	%xmm0, %eax 
	ret

_ZN3GunC2Ej: ;constructor of Gun
	pushq	%rbp
	movq	%rsp, %rbp

	movq	%rdi, -8(%rbp) ;get this pointer
	movl	%esi, -12(%rbp) ;get nbullet
	movq	-8(%rbp), %rax
	movq	$_ZTV3Gun+16, (%rax) ;pointer to virtual function table of class Gun

	movq	-8(%rbp), %rax
	movl	-12(%rbp), %edx
	movl	%edx, 8(%rax) ;init nbullet

	popq	%rbp
	ret

_ZN3GunD2Ev:   ;destructor of Gun
	pushq	%rbp
	movq	%rsp, %rbp

	subq	$16, %rsp ;extend stack storage

	movq	%rdi, -8(%rbp) ;get this pointer

	movq	-8(%rbp), %rax
	movq	$_ZTV3Gun+16, (%rax) ;get pointer to virtual function table of class Gun

	movl	$0, %eax
	testl	%eax, %eax
	je	.L5
	movq	-8(%rbp), %rax
	movq	%rax, %rdi
	call	_ZdlPv
.L5:
	leave
	ret

_ZN3GunC2ERKS_: ;copy constructor of Gun
	pushq	%rbp
	movq	%rsp, %rbp

	movq	%rdi, -8(%rbp) ;get this pointer
	movq	%rsi, -16(%rbp) ;get address of argument gun

	movq	-8(%rbp), %rax 
	movq	$_ZTV3Gun+16, (%rax) ;set pointer to virtual table with class Gun's virtual table address

	movq	-16(%rbp), %rax 
	movl	8(%rax), %edx ;%edx point to address of argument gun's nbullet

	movq	-8(%rbp), %rax
	movl	%edx, 8(%rax) ;init this->nbullet

	popq	%rbp
	ret

_ZNK3Gun9copy_selfEv: ;Gun::copy_self
	pushq	%rbp
	movq	%rsp, %rbp

	pushq	%rbx
	subq	$24, %rsp ;extend stack

	movq	%rdi, -24(%rbp) ;get this pointer
	movl	$16, %edi ;mov size of Gun to %edi

	call	_Znwm ;alloc memory on heap

	movq	%rax, %rbx ;this pointer to new gun
	movq	-24(%rbp), %rax
	movq	%rax, %rsi ;my own this pointer
	movq	%rbx, %rdi ;this pointer to new gun

	call	_ZN3GunC1ERKS_ ;call copy constructor of Gun

	movq	%rbx, %rax ;set return value as this pointer to new gun

	addq	$24, %rsp ;resave stack

	popq	%rbx
	popq	%rbp
	ret

_ZN3Gun4shotERK8PositionS2_: ;Gun::shot
	pushq	%rbp
	movq	%rsp, %rbp

	movq	%rdi, -8(%rbp) ;this pointer
	movq	%rsi, -16(%rbp) ;address of x
	movq	%rdx, -24(%rbp) ;address of y

	movq	-8(%rbp), %rax
	movl	8(%rax), %eax ;address of this->nbullet

	testl	%eax, %eax
	jne	.L14 ;nbullet != 0
	movl	$0, %eax
	jmp	.L13
.L14:
	movq	-8(%rbp), %rax
	movl	8(%rax), %eax ;address of this->nbullet

	leal	-1(%rax), %edx 
	movq	-8(%rbp), %rax
	movl	%edx, 8(%rax)
.L13:
	popq	%rbp
	ret

_ZN7HandGunC2Ejd: ;constructor of HandGun
	pushq	%rbp
	movq	%rsp, %rbp

	subq	$32, %rsp

	movq	%rdi, -8(%rbp)
	movl	%esi, -12(%rbp)
	movsd	%xmm0, -24(%rbp)

	movq	-8(%rbp), %rax
	movl	-12(%rbp), %edx
	movl	%edx, %esi ;set nbullet
	movq	%rax, %rdi ;set this pointer
	call	_ZN3GunC2Ej ;call constructor of Gun

	movq	-8(%rbp), %rax
	movq	$_ZTV7HandGun+16, (%rax) ;set virtual function pointer

	movq	-8(%rbp), %rdx
	movq	-24(%rbp), %rax
	movq	%rax, 16(%rdx) ;init dist

	leave
	ret

_ZN7HandGunC2ERKS_: ;copy constructor of HandGun
	pushq	%rbp
	movq	%rsp, %rbp

	subq	$16, %rsp

	movq	%rdi, -8(%rbp)
	movq	%rsi, -16(%rbp)

	movq	-16(%rbp), %rdx
	movq	-8(%rbp), %rax
	movq	%rdx, %rsi ;set arg gun's this pointer
	movq	%rax, %rdi ;set my own this pointer
	call	_ZN3GunC2ERKS_ ;call Gun's copy constructor

	movq	-8(%rbp), %rax
	movq	$_ZTV7HandGun+16, (%rax) ;set virtual function pointer

	movq	-16(%rbp), %rax
	movq	16(%rax), %rax ;get arg gun's dist
	movq	-8(%rbp), %rdx
	movq	%rax, 16(%rdx) ;init dist

	leave
	ret

_ZNK7HandGun9copy_selfEv: ;HandGun::copy_self
	pushq	%rbp
	movq	%rsp, %rbp

	pushq	%rbx
	subq	$24, %rsp

	movq	%rdi, -24(%rbp) ;my own this pointer
	movl	$24, %edi
	call	_Znwm
	movq	%rax, %rbx ;get address of new gun

	movq	-24(%rbp), %rax
	movq	%rax, %rsi ;set arg gun's address (my own this pointer)
	movq	%rbx, %rdi ;set this pointer (new gun's this pointer)
	call	_ZN7HandGunC1ERKS_ ;call copy constructor of HandGun

	movq	%rbx, %rax ;set new this pointer as return value
	addq	$24, %rsp ;resave stack

	popq	%rbx
	popq	%rbp
	ret

_ZN7HandGun4shotERK8PositionS2_: ;HandGun::shot
	pushq	%rbp
	movq	%rsp, %rbp

	subq	$32, %rsp

	movq	%rdi, -8(%rbp)
	movq	%rsi, -16(%rbp)
	movq	%rdx, -24(%rbp)

	movq	-8(%rbp), %rax ;this
	movq	-24(%rbp), %rdx ;addr of x
	movq	-16(%rbp), %rcx ;addr of y

	movq	%rcx, %rsi ;set y
	movq	%rax, %rdi ;set this
	call	_ZN3Gun4shotERK8PositionS2_ ;call Gun::shot

	xorl	$1, %eax ;check return value

	testb	%al, %al
	je	.L21 ;true

	movl	$0, %eax
	jmp	.L22
.L21:
	movq	-24(%rbp), %rdx
	movq	-16(%rbp), %rax
	movq	%rdx, %rsi ;set x
	movq	%rax, %rdi ;set y
	call	_Z8distanceRK8PositionS1_ ;call distance

	cvtsi2sd	%eax, %xmm0 
	movq	-8(%rbp), %rax
	movsd	16(%rax), %xmm1 
	ucomisd	%xmm0, %xmm1
	seta	%al
.L22:
	leave
	ret

_ZN7SoldiorC2ERK8PositionPK3Gun: ;constructor of Soldior
	pushq	%rbp
	movq	%rsp, %rbp

	subq	$32, %rsp

	movq	%rdi, -8(%rbp)
	movq	%rsi, -16(%rbp)
	movq	%rdx, -24(%rbp)

	movq	-8(%rbp), %rax ;this
	movq	-16(%rbp), %rdx ;addr of pos
	movq	(%rdx), %rcx
	movq	%rcx, 8(%rax) ;init pos.px

	movq	8(%rdx), %rcx
	movq	%rcx, 16(%rax) ;init pos.py

	movq	16(%rdx), %rdx
	movq	%rdx, 24(%rax) ;init pos.pz

	cmpq	$0, -24(%rbp) 
	jne	.L24 ;g != nullptr

	movq	-8(%rbp), %rax
	movq	$0, (%rax) ;init gun as nullptr

	jmp	.L23
.L24:
	movq	-24(%rbp), %rax ;this
	movq	(%rax), %rax ;pointer to Gun's virtual functions table

	addq	$8, %rax
	movq	(%rax), %rax ;second function pointer of g's virtual table

	movq	-24(%rbp), %rdx
	movq	%rdx, %rdi ;set g as 'this' argument

	call	*%rax ;call g->copy_self
	movq	-8(%rbp), %rdx
	movq	%rax, (%rdx) ;gun = g
.L23:
	leave
	ret

_ZN7SoldioraSEOS_: ;mov assignment
	pushq	%rbp
	movq	%rsp, %rbp

	subq	$16, %rsp

	movq	%rdi, -8(%rbp)
	movq	%rsi, -16(%rbp)

	movq	-8(%rbp), %rax
	cmpq	-16(%rbp), %rax 
	jne	.L27 ;if(this != &s)
	movq	-8(%rbp), %rax
	jmp	.L28
.L27:
	movq	-8(%rbp), %rax 
	movq	(%rax), %rax
	testq	%rax, %rax
	je	.L29 ;this->gun == nullptr

	movq	-8(%rbp), %rax
	movq	(%rax), %rax
	testq	%rax, %rax
	je	.L29

	movq	-8(%rbp), %rax
	movq	(%rax), %rax
	movq	(%rax), %rax
	addq	$24, %rax
	movq	(%rax), %rax
	movq	-8(%rbp), %rdx
	movq	(%rdx), %rdx
	movq	%rdx, %rdi
	call	*%rax ;call gun's destructor
.L29:
	movq	-16(%rbp), %rax
	movq	(%rax), %rdx
	movq	-8(%rbp), %rax
	movq	%rdx, (%rax) ;gun = s->gun

	movq	-16(%rbp), %rax
	movq	$0, (%rax) ;s->gun = nullptr

	movq	-8(%rbp), %rax
	movq	-16(%rbp), %rdx

	movq	8(%rdx), %rcx
	movq	%rcx, 8(%rax) ;pos.px = s->pos.px

	movq	16(%rdx), %rcx
	movq	%rcx, 16(%rax) ;pos.py = s->pos.py

	movq	24(%rdx), %rdx
	movq	%rdx, 24(%rax) ;pos.pz = s->pos.pz
 	movq	-8(%rbp), %rax ;return this
.L28:
	leave
	ret

_ZN7SoldiorD2Ev: ;destructor of Soldior
	pushq	%rbp
	movq	%rsp, %rbp 

	subq	$16, %rsp

	movq	%rdi, -8(%rbp)
	movq	-8(%rbp), %rax
	movq	(%rax), %rax ;%rax = gun

	testq	%rax, %rax 
	je	.L30 ;gun == nullptr

	movq	-8(%rbp), %rax
	movq	(%rax), %rax
	testq	%rax, %rax 
	je	.L30 ;gun == nullptr

	movq	-8(%rbp), %rax
	movq	(%rax), %rax
	movq	(%rax), %rax ;gun->vptr

	addq	$24, %rax ;third function pointer in gun's virtual table
	movq	(%rax), %rax

	movq	-8(%rbp), %rdx 
	movq	(%rdx), %rdx ;get gun
	movq	%rdx, %rdi ;set gun as 'this' argument
	call	*%rax ;call gun's destructor
.L30:
	leave
	ret

main:
	pushq	%rbp
	movq	%rsp, %rbp

	pushq	%r12
	pushq	%rbx
	subq	$80, %rsp

	movl	%edi, -68(%rbp)
	movq	%rsi, -80(%rbp)

	movl	$16, %edi ;set alloc size as Gun
	call	_Znwm ;alloc memory on heap

	movq	%rax, %rbx ;%rbx is this pointer of new gun

	movl	$100, %esi ;set nbullet
	movq	%rbx, %rdi ;set 'this' arg
	call	_ZN3GunC1Ej ;call constructor of Gun

	movq	%rbx, -64(%rbp) ;save %rbx

	movabsq	$4632233691727265792, %rcx ;constexpr accessed as static address
	movabsq	$4629137466983448576, %rdx
	movabsq	$4626322717216342016, %rax

	leaq	-48(%rbp), %rsi
	movq	%rcx, -88(%rbp)
	movsd	-88(%rbp), %xmm2 ;px
	movq	%rdx, -88(%rbp)
	movsd	-88(%rbp), %xmm1 ;py
	movq	%rax, -88(%rbp)
	movsd	-88(%rbp), %xmm0 ;pz

	movq	%rsi, %rdi ;set 'this' arg
	call	_ZN8PositionC1Eddd ;call constructor of Position

	leaq	-48(%rbp), %r12 
	movl	$32, %edi ;set alloc size as Soldior's size
	call	_Znwm

	movq	%rax, %rbx ;s
	movq	-64(%rbp), %rax

	movq	%rax, %rdx ;set g
	movq	%r12, %rsi ;set pos
	movq	%rbx, %rdi ;set 'this' arg
	call	_ZN7SoldiorC1ERK8PositionPK3Gun ;call Soldior's constructor

	movq	%rbx, -56(%rbp) ;save %rbx
	leaq	-48(%rbp), %rax
	movq	%rax, %rdi ;set return addr of next function

	call	_Z10getSoldiorv ;call getSoldior

	leaq	-48(%rbp), %rdx ;get return val
	movq	-56(%rbp), %rax  ;get s
	movq	%rdx, %rsi ;set 's' arg
	movq	%rax, %rdi ;set 'this' arg
	call	_ZN7SoldioraSEOS_ ;call mov assignment of Soldior

	leaq	-48(%rbp), %rax 
	movq	%rax, %rdi
	call	_ZN7SoldiorD1Ev ;delete return val of getSoldior

	movl	$0, %eax
	jmp	.L38
.L37:
	movq	%rax, %r12
	movq	%rbx, %rdi
	call	_ZdlPv
	movq	%r12, %rax
	movq	%rax, %rdi
	call	_Unwind_Resume
.L38:
	addq	$80, %rsp
	popq	%rbx
	popq	%r12
	popq	%rbp
	ret

_ZTV3Gun: ;Gun's virtual functions table
	.quad	0
	.quad	_ZTI3Gun ;some information of this table , we can ignore
	.quad	_ZN3Gun4shotERK8PositionS2_ 
	.quad	_ZNK3Gun9copy_selfEv
	.quad	_ZN3GunD1Ev
	.quad	_ZN3GunD0Ev
	.weak	_ZTI7HandGun
	.section	.rodata._ZTI7HandGun,"aG",@progbits,_ZTI7HandGun,comdat
	.align 16
	.type	_ZTI7HandGun, @object
	.size	_ZTI7HandGun, 24

_ZTV7HandGun: ;virtual function table of HandGun
	.quad	0
	.quad	_ZTI7HandGun
	.quad	_ZN7HandGun4shotERK8PositionS2_
	.quad	_ZNK7HandGun9copy_selfEv
	.quad	_ZN7HandGunD1Ev
	.quad	_ZN7HandGunD0Ev
	.section	.text._ZN7HandGunD2Ev,"axG",@progbits,_ZN7HandGunD5Ev,comdat
	.align 2
	.weak	_ZN7HandGunD2Ev
	.type	_ZN7HandGunD2Ev, @function
_ZN7HandGunD2Ev: ;destructor of HandGun
	pushq	%rbp
	movq	%rsp, %rbp

	subq	$16, %rsp

	movq	%rdi, -8(%rbp)
	movq	-8(%rbp), %rax
	movq	$_ZTV7HandGun+16, (%rax) ;set vptr

	movq	-8(%rbp), %rax
	movq	%rax, %rdi
	call	_ZN3GunD2Ev ;call Gun's destructor
	movl	$0, %eax
	testl	%eax, %eax
	je	.L39
	movq	-8(%rbp), %rax
	movq	%rax, %rdi
	call	_ZdlPv
.L39:
	leave
	ret

由此总结一下C++中虚函数的实现机制, 首先,C++中的每个定义了虚函数的类都实现了一个虚函数表,虚函数表中存储了这个类中每个虚函数的实际地址,C++为每个定义了虚函数的类的对象插入了一个隐式的成员:虚函数表指针,指向这个类对应的虚函数表。那么这个虚函数表指针是什么时候初始化的呢?答案是:在一个类的构造函数或析构函数执行的第一步就进行虚函数指针的初始化,这样就是实现了在构造函数和析构函数中调用虚函数始终是自己本身定义的函数。比如,一个子类对象开始构造,先执行父类的构造函数,父类的构造函数第一步先将虚函数指针指向了父类的虚函数表,从而在父类的构造函数中调用虚函数始终调用的是自己定义的虚函数,父类的构造函数执行完后执行子类的构造函数,子类的构造函数现将虚函数指针指向它本身所属类的虚函数表,此后直到子类的析构函数执行,都未在任何地方修改这个虚函数指针,从而在此期间任何对虚函数的调用都执行的是子类自己的虚函数。最后,该对象要被析构,首先调用子类的析构函数,子类的析构函数首先将他的虚函数指针指向他自己所属类的虚函数表(其实如果这个子类没有子类的话,这一步并未改变子类虚函数指针的指向,但是计算机没有人类那么聪明,所以这一步必须执行)。执行完子类的析构函数后调用父类的析构函数,父类的析构函数首先将虚函数指针指向父类的虚函数表,从而父类的构造函数中调用的永远是他自己定义的虚函数,父类的析构函数执行完毕后整个对象析构结束。


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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值