第十八讲:C++多态性与虚函数

第十八讲:C++多态性与虚函数

    * 掌握:多态性的概念;虚函数的定义及应用。
    * 理解:对象与类函数的静态关联与动态关联。
    重点、难点: :多态性的概念;虚函数的定义及应用。

一、多态性的概念

定义:
   
多态性(polymorphism)是面向对象程序设计的一个重要特征。利用多态性可以设计和实现一个易于扩展的系统。多态性是指具有不同功能的函数可以用同一个函数名,这样就可以用一个函数名调用不同内容的函数。在面向对象方法中一般是这样表述多态性的:向不同的对象发送同一个消息,不同的对象在接收时会产生不同的行为(即方法)。也就是说,每个对象可以用自己的方式去响应共同的消息。所谓消息,就是调用函数,不同的行为就是指不同的实现,即执行不同的函数。

分类:静态多态性和动态多态性
   静态多态性,在程序编译时系统就能决定调用的是哪个函数,因此静态多态性又称编译时的多态性。静态多态性是通过函数的重载实现的(运算符重载实质上也是函数重载)。
   动态多态性是在程序运行过程中才动态地确定操作所针对的对象。它又称运行时的多态性。动态多态性是通过虚函数(virtual function)实现的:

问题提出:
   在本章中主要介绍动态多态性和虚函数。要研究的问题是:当一个基类被继承为不同的派生类时,各派生类可以使用与基类成员相同的成员名,如果在运行时用同一个成员名调用类对象的成员,会调用哪个对象的成员?也就是说,通过继承而产生了相关的不同的派生类,与基类成员同名的成员在不同的派生类中有不同的含义。也可以说,多态性是“一个接口,多种方法”。

二、一个典型的例子

   下面的例子。一方面它是有关继承和运算符重载内容的综合应用的例子,另一方面又是作为讨论多态性的一个基础用例。

例1:先建立一个Point(点)类,包含数据成员x,y(坐标点)。以它为基类,派生出一个Circle(圆)类,增加数据成员r(半径),再以Circle类为直接基类,派生出一个Cylinder(圆柱体)类,再增加数据成员h(高)。要求编写程序,重载运算符“<<” 和“>>”,使之能用于输出以上类对象。

   这个题目难度并不大,但程序比较长。对于一个比较大的程序,应当分成若干步骤进行,先声明基类,再声明派生类,逐级进行,分步调试。

1、声明基类Point类

可写出声明基类Point的部分如下:
#include <iostream>
using namespace std;
class Point          //声明类Point
   { public:
      Point(float=0,float=0);    //有默认参数的构造函数
      void setPoint(float,float); //设置坐标
      float getX() const {return x;}//读x坐标
      float getY() const {return y;}//读y坐标
      friend ostream & operator<<(ostream &,const Point &);//重载运算符
     protected: //受保护成员
      float x,y; };

//定义Point类的成员函数
//Point的构造函数

Point::Point(float a,float b) //对XY初始化
  {  x=a;y=b;}           //设置x和y的坐标值
void Point::setPoint(float a,float b) //为XY赋新值
  { x=a;y=b;}             //输出点的坐标
ostream & operator<<(ostream &output,const Point &p)
  { output<<"["<<p.x<<","<<p.y<<"]"<<endl;
    return output; }

int main()
 { Point p(3.5S,6.4);   //建立Point类对象p
   coot<<"x="<<p.gex()<<",y="<<p.getY()<<eadl; //输出p的坐标值
   p.setPoint(8.5,6.8); //重新设置P的坐标值
   cout<<"p(new):"<<p<<endl; //用重载运算符“<<”输出p点坐标

程序编译通过,运行结果为:
  x=3.5,y=6.4
  p(new):[8.5,6.8]


  测试程序检查了基类中各函数的功能,以及运算符重载的作用,证明程序是正确的。

2、声明派生类Circle

在上面的基础上,再写出声明派生类Circle的部分;

class Circle:public Point //circle是Point类的公用派生类
   { Public:
      Circle(float x=O,float y=O,float r=O); //构造函数
      void setRadius(float);    //没置半径值
      float getRadius() const; //读取半径值
      float area() cont;      //计算圆面积
      fnend ostreatm &Operatord<<ostream &,const Circle &);//重载运算符“<<”
    Private:
      float radius;}
//定义构造函数,对圆心坐标和半径初始化
Circle::Circle(float a,float b,float r):Point(a,b),radius(r){}
//设置半径值
void Circle::setRadius(float r)
   { radius=r }
//读取半径值
float Circle::getRadius() const{return radius;}
//计算圆面积
float Circle::area()const
  { retum 3.14159*radius*radius;}
//重载运算符“<<”,使之按规定的形式输出圆的信息
ostream &operator<<(ostrearn &output,const Circle &c)
{ output<<"Center={"<<c.x<<","<<c.y<<"],r="<<c.radius<<<<",area="<<c.area()
<<endl;
rcturn output;}

为了测试以上Circle类的定义,可以写出下面的主函数:
int main()
 { Circle c(3.5,6.4,5.2); //建立Circle类对象c,并给定圆心坐标和半径
  coat<<"original circle:\nx="<<c.getX()<<",y="<<c.getY()<<",r="<<c.getRadius()
<<",area="<<c.area()<<endl; //输出圆心坐标、半径和面积
  c.seiRadius(7.5);       //设置半径值
  c.setPoint(5.5);         //设置圆心坐标值x,y
  cout<<"flew circle:\n"<<c;//用重载运算符“<<”输出圆对象的信息
  Point &pgef=c;          //pRef是Point类的引用变量,被c初始化
  cout<<“pRef:"<<pRef;    //输出pRef的信息
  return 0;}

程序编译通过,运行结果为
   original circle: (输出原来的圆的数据)
   x=2.5,y=6.4,r=5.2,area=84.9486
   new circle: 
(输出修改后的圆的数据)
   Ceater=[5,5],r=7.55,area=176.714
   pRef:[5,5] 
(输出圆的圆心“点”的数据)

3、声明Circle的派生类Cylinder

前面已从基类Point派生出Circle类,现在再从Circle派生出CyUnder类。
class Cylinder:public Circle    //(yliader是Cirle的公用派生类
   { public:
      Cylinder(gloat x=O,float y=O,float r=O,float h=O); //构造函数
      void setHeight(float); //设置圆柱高
      float getHeight() const;//读取圆柱高
      float area() const;    //计算圆表面积
      float volume() const{}const;//计算圆柱体积
      friend ostream&operato<<(ostrram&const Cylinder&); //重载运算符“<<”
     protected:
      float height;} //圆柱高
//定义构造函数
Cytinder::Cylider(float a,float b,float r ,float h);
Circle(a,b,r).height(h){}
//设置圆柱高
void Cylinder::setHeight(float h){height;}
//读取圆柱高
float Cylinder::getHeight() const{return height;}
//计算圆柱表面积
noat Cylinder::area() const
{rehirn 2*Circle::area( )+2*3.14159*radiuis*height;}
//计算圆柱体积
float Cylinder::volume() const
{return Circle::area()*height;}
//重载运算符“<<”
ostream &operator<<(ostream &output,const Cylinder &cy)
{ output<<"Center=["<<cy.x<<","<<cy.y<<"],r="<<cy.radius<<",h="<<cy.height
<<"\narea="<<cy.area( )<<",volum="<<cy.volume( )<<endl;
return output;}

可以写出下面的主函数:
int main()
  { Cylinder cyl(3.5,6.4,5.3,10); //定义Cylinder类对象cyl
   cout<<"\norlginal cylinder=\nx="<<cyl.getX()<<",y="<<cyl.getY()<<",r="
<<cyl.getRadius()<<:,h="<<cyl.getHeight()<<"\narea="<<cyl.area()
<<",volume="<<cyl.volume()<<endl;//用系统定义的运算符“<<”输出cyl的数据
   cyl.setHeight(15): //设置圆柱高
   cyl.setRadius(7.5); //设置圆半径
   cyl.setPoint(5,5); //设置圆心坐标值x.y
   cout<<:\nnew cylinder:\n"<<cyl; //用重载运算符“<<”输出cyl的数据
   Point&pRef=cyl; //pRef是Point类对象的引用变量
   cout<<"\npRef as a Point:"<<pRef;//pRef作为一个“点”输出
   Circle &cRef=cyl; //cRef是Circle类对象的引用变量
   cout<<"\ncRef as a Circle:"<<cRef;//cRef作为一个“圆”输出
   return 0 }

运行结果如下:
original cylinder: (输出cyl的初始值)
x=3.5,y=6.4,r=5.2,h=10 
(圆心坐标x,y。半径r,高h)
area=496.623,volume=849.486 
(圆柱表面积area和体积volume)
new cylinder:
 (输出cyl的新值)
Center=[5.5],r=7.5,h=15 
(以[5,5]形式输出同心坐标)
area=1060.29,volume=2650.72 
(圆柱表面积area和体积volume)
pRef as a Point:[5,5] 
(pRef作为一个“点”输出)
cRef as a Circle:Center=[5,5],r=7.5,area=176.714 
(cRef作为一个“圆”输出)

说明:
   在本例中存在静态多态性,这是运算符重载引起的(注意3个运算符函数是重载而不是同名覆盖,因为有一个形参类型不同)。可以看到,在编译时编译系统即可以判定应调用哪个重载运算符函数。稍后将在此基础上讨论动态多态性问题。

三、虚函数

1、虚函数的作用

问题提出:
   在同一类中是不能定义两个名字相同、参数个数和类型都相同的函数的,否则就是“重复定义”。但是在类的继承层次结构中,在不同的层次中可以出现名字相同、参数个数和类型都相同而功能不同的函数。因为它们不在同一个类中。编译系统按照同名覆盖的原则决定调用的对象。用这种方法来区分两个同名的函数。但是这样做很不方便。

解决方案:

   在基类中声明同名函数时,在最左面加一个关键字virtual,即virtual 函数类型 函数名;这样就把函数声明为虚函数。
   人们提出这样的设想,能否用同一个调用形式,既能调用派生类的函数又能调用基类的同名函数。在程序中不是通过不同的对象名去调用不同派生层次中的同名函数,而是 通过指针调用它们。
   例如,用同一个语句"pt->display();” 可以调用不同派生层次中的display函数,只需在调用前给指针变量Pt赋以不同的值(使之指向不同的类对象)即可。


虚函数的作用
   允许在派生类中重新定义与基类同名的函数,并且可以通过基类指针或引用来访问基类和派生类中的同名函数。

例2 基类与派生类中有同名函数。

在下面的程序中Student是基类,Oraduate是派生类,它们都有display这个同名的函数。
#include <iostream>
#include <string>
using namespace std;
class Student
   { public:
      Student(int,string,float); //声明构造函数
      void display();    //声明输出函数
     protected:         //受保护成员,派生类可以访问
      int num;
      string name;
      float score; };

Student::Student(int n,string nam,float s)//定义构造函数
   { num=n;name=nam;score=s;}

void Student::display()    //定义输出函数 
  { cout<<"num:"<<num<<"\nname:"<<name<<"\nscore:"<<score<<"\n\n";}

class Graduate:public Student
   { public:
      Graduate(int,string,float,float); //声明构造函数
      void display(); //声明输出函数
     private:
      float pay; };

void Graduate::display() //定义输出函数 
  { cout<<"num:"<<num<<"\nname:"<<name<<"\nscore:"<<score<<"\npay="<<pay<<endl;}

Graduate::Graduate(int n,string nam,float s,float p):Student(n,nam,s),pay(p){}

int main()
 { Student stud1(1001,"Li",87.5); //定义Student类对象studl 
   Graduate grad1(2001,"Wang",98.5,563.5); //定义Graduate类对象gradl
   Student *pt=&stud1; //定义指向基类对象的指针变量pt
   pt->display();
   pt=&grad1;
   pt->display();
   return 0; }

num:1001 (studl的数据)
name:Li
score:87.5 
num:2001 (gradl中基类部分的数据)
name:wang
score:98.5

? 假如想输出gradl的全部数据成员,当然也可以采用这样的方法:

1、通过对象名调用 display函数,如gradl.display()。
2、定义一个指向Graduate类对象的指针变量ptr,然 后使ptr指向gradl,再用ptr->display()调用。

   用虚函数就能顺利地解决这个问题。

   对程序作一点修改,在Student类中声明display函数时,在最左面加一个关键字virtual,即virtual void display();这样就把Student类的display函数声明为虚函数。程序其他部分都不改动。再编译和运行程序,请注意分析运行结果:

num:1001 (studl的数据)
name:Li
score:87.5
num:2001 (gradl中基类部分的数据)
name:wang
score:98.5
pay=1200 (这一项以前是没有的)

   1 、现在用同一个指针变量(指向基类对象的指针变量),不但输出了学生studl的全部数据,而且还输出了研究生gradl的全部数据,说明已调用了gradl的display函数。用同一种调用形式"pt—>display()”,而且pt是一个基类指针,可以调用同一类族中不同类的虚函数。这就是多态性,对同一消息,不同对象有不同的响应方式。
   2、基类指针是用来指向基类对象的,如果用它指向派生类对象,则进行指针类型转换,将派生类对象的指针先转换为基类的指针,所以基类指针指向的是派生类对象中的基类部分。在程序修改前,是无法通过基类指针去调用派生类对象中的成员函数的。虚函数突破了这一限制,在派生类的基类部分中,派生类的虚函数取代了基类原来的虚函数,因此在使基类指针指向派生类对象后,调用虚函数时就调用了派生类的虚函数。要注意的是,只有用virtual声明了虚函数后才具有以上作用。如果不声明为虚函数,企图通过基类指针调用派生类舶非虚函数是不行的。
   3、虚函数的以上功能是很有实用意义的。在面向对象的程序设计中,经常会用到类的继承,目的是保留基类的特性,以减少新类开发的时间。但是,从基类继承来的某些成员函数不完全适应派生类的需要,例如在例2中,基类的display函数只输出基类的数据,而派生类的display函数需要输出派生类的数据。过去我们曾经使派生类的输出函数与基类的输出函数不同名(如display和displayl),但如果派生的层次多,就要起许多不同的函数名,很不方便。如果采用同名函数,又会发生同名覆盖。

结论:
   利用虚函数就很好地解决了这个问题。可以看到:当把基类的某个成员函数声明为虚函数后,允许在其派生类中对该函数重新定义,赋予它新的功能,并且可以通过指向基类的指针指向同一类族中不同类的对象,从而调用其中的同名函数。由虚函数实现的动态多态性就是:同一类族中不同类的对象,对同一函数调用作出不同的响应。虚函数的使用方法是:

   (1)在基类用virtual声明成员函数为虚函数。这样就可以在派生类中重新定义此函数,为它赋予新的功能,并能方便地被调用。在类外定义虚函数时,不必再加virtual。
   (2)在派生类中重新定义此函数,要求函数名、函数类型、函数参数个数和类型全部与基类的虚函数相同,并根据派生类的需要重新定义函数体。
    C++规定,当一个成员函数被声明为虚函数后,其派生类中的同名函数都自动成为虚函数。因此在派生类重新声明该虚函数时,可以加virtual,也可以不加,但习惯上一般在每一层声明该函数时都加virtual,使程序更加清晰。
   如果在派生类中没有对基类的虚函数重新定义,则派生类简单地继承其直接基类的虚函数。
   (3)定义一个指向基类对象的指针变量,并使它指向同一类族中的某一对象。
   (4)通过该指针变量调用此虚函数,此时调用的就是指针变量指向的对象的同名函数。

   通过虚函数与指向基类对象的指针变量的配合使用,就能方便地调用同一类族中不同类对象的同名函数,只要先用基类指针指向该对象即可。如果指针不断地指向同一类族中不同类的对象,就能不断地调用这些对象中的同名函数。这就如同前面说的,不断地告诉出租车司机要去的目的地,然后司机把你送到你要去的地方。

需要说明;有时在基类中定义的非虚函数会在派生类中被重新定义(如例6.1中的area函数),如果用基类指针调用该成员函数,则系统会调用对象中基类部分的成员函数;如果用派生类指针调用该成员函数,则系统会调用派生类对象中的成员函数,这并不是多态性行为(使用的是不同类型的指针),没有用到虚函数的功能。

   以前介绍的函数重载处理的是同一层次上的同名函数问题,而虚函数处理的是不同派生层次上的同名函数问题,前者是横向重载,后者可以理解为纵向重载。但与重载不同的是:同一类族的虚函数的首部是相同的,而函数重载时函数的首部是不同的(参数个数或类型不同)。

2、静态关联与动态关联

问题提出
   
下面进一步探讨C++是怎样实现多态性的。同一个display函数在不同对象中有不同的作用,呈现了多态。要区分就多态性必须要有相关的信息,编译系统要根据已有的信息,对同名函数的调用作出判断。例如函数的重载,系统是根据参数的个数和类型的不同去找与之匹配的函数的。对于调用同一类族中的虚函数,应当在调用时用一定的方式告诉编译系统,你要调用的是哪个类对象中的函数。

关联定义:
   确定调用的具体对象的过程称为关联(binding)。binding原意是捆绑或连接,即把两样东西捆绑(或连接)在一起。在这里是指把一个函数名与一个类对象捆绑在一起,建立关联。一般地说,关联指把一个标识符和一个存储地址联系起来。在计算机辞典中可以查到,所谓关联,是指计算机程序中不同的部分互相连接的过程。有些书中把binding译为联编、编联、束定,或兼顾音和意,称之为绑定。作者认为:从意思上说,关联比较确切,也好理解。意思是建立两者之间的关联关系。但目前在有些书刊中用了联编这个术语。

静态关联(static binding)
   前面所提到的函数重载和通过对象名调用的虚函数,在编译时即可确定其调用的虚函数属于哪一个类,其过程称为静态关联(static binding),由于是在运行前进行关联的,故又称为早期关联(earlybinding)。函数重载属静态关联。

动态关联(dynamic binding)/滞后关联(1atebinding)
   在上一小节程序中看到了怎样使用虚函数,在调用虚函数时并没有指定对象名,那么系统是怎样确定关联的呢?读者可以看到,是通过基类指针与虚函数的结合来实现多态性的。先定义了一个指向基类的指针变量,并使它指向相应的类对象,然后通过这个基类指针去调用虚函数(例如"pt->display()”)。显然,对这样的调用方式,编译系统在编译该行时是无法确定应调用哪一个类对象的虚函数的。因为编译只作静态的语法检查,光从语句形式(例如"pt->display();”)是无法确定调用对象的。
在这样的情况下,编译系统把它放到运行阶段处理,在运行阶段确定关联关系。在运行阶段,基类指针变量先指向了某一个类对象,然后通过此指针变量调用该对象中的函数。此时调用哪一个对象的函数无疑是确定的。例如,先使pt指向gradl,再执行“pt->display()”,当然是调用gradl中的display函数。由于是在运行阶段把虚函数和类对象“绑定”在一起的,因此,此过程称为动态关联(dynamic binding)。这种多态性是动态的多态性,即运行阶段的多态性。
   在运行阶段,指针可以先后指向不同的类对象,从而调用同一类族中不同类的虚函数。由于动态关联是在编译以后的运行阶段进行的,因此也称为滞后关联(1atebinding)。

3、在什么情况下应当声明虚函数

使用虚函数时,有两点要注意:

    1、只能用virtual声明类的成员函数,使它成为虚函数,而不能将类外的普通函数声 明为虚函数。因为虚函数的作用是允许在派生类中对基类的虚函数重新定义。显然,它只能用于类的继承层次结构中。
   2、 一个成员函数被声明为虚函数后,在同一类族中的类就不能再定义一个非virtual的但与该虚函数具有相同的参数(包括个数和类型)和函数返回值类型的同名 函数。

根据什么考虑是否把一个成员函数声明为虚函数呢?主要考虑以下几点:

   1、首先看成员函数所在的类是否会作为基类。然后看成员函数在类的继承后有无可能被更改功能,如果希望更改其功能的,一般应该将它声明为虚函数。
   2、如果成员函数在类被继承后功能不需修改,或派生类用不到该函数,则不要把它声明为虚函数。不要仅仅考虑到要作为基类而把类中的所有成员函数都声明为虚函数。
   3、应考虑对成员函数的调用是通过对象名还是通过基类指针或引用去访问,如果是通过基类指针或引用去访问的,则应当声明为虚函数。
   4、有时,在定义虚函数时,并不定义其函数体,即函数体是空的。它的作用只是定义了一个虚函数名,具体功能留给派生类去添加。在6.4节中将详细讨论此问题。
   需要说明的是:使用虚函数,系统要有一定的空间开销。当一个类带有虚函数时,编译系统会为该类构造一个虚函数表(virtual function table,vtable),它是一个指针数组,存放每个虚函数的人口地址。系统在进行动态关联时的时间开销是很少的,因此,多态性是高效的。

4、虚析构函数

   析构函数的作用是在对象撤销之前做必要的“清理现场”的工作。当派生类的对象从内存中撤销时一般先调用派生类的析构函数,然后再调用基类的析构函数。但是,如果用new运算符建立了临时对象,若基类中有析构函数,并且定义了一个指向该基类的指针变量。在程序用带指针参数的delete运算符撤销对象时,会发生一个情况:系统会只执行基类的析构函数,而不执行派生类的析构函数。

例3 派生类对象析构函数时的执行情况。

为简化程序,只列出最必要的部分
#include <iostream>
using namespace std;
class Point//定义基类Point类
   { public:
      Point(){} //Point类构造函数
      ~Point(){cout<<"executing Point destructor"<<endl;} };//Point类析构函数

class Circle:public Point   //定义派生类Circle类
   { public:
      Circle(){}       //Circle类构造函数
      ~Circle(){cout<<"executing Circle destructor"<<endl;}//Circle类析构函数
    private:
      int radus; };

int main()
 { Point *p=new Circle;    //用new开辟Circle类对象的动态存储字间
   delete p;             //用delete释放动态存储空间 
   return 0; }

   这只是一个示意的程序。P是指向基类Point的指针变量,指向new开辟的动态存储字间。今希望用detele释放p所指向的空间。但运行结果为: executmg Point destmctor表示只执行了基类Point的析构函数,而没有执行派生类Circle的析构函数。原因是以前介绍过的。如果希望能执行派生类Circle的析构函数,可以将基类的析构函数声明为虚析构函数, 如:
    virtual ~Point(){cout<<”executing Pointdestructoff'<<endl;}
程序其他部分不改动,再运行程序,结果为:
   executmg Circle destructor
   executmg Point destructor

先调用了派生类的析构函数,再调用了基类的析构函数,符合人们的愿望。当基类的析构 函数为虚函数时,无论指针指的是同一类族中的哪一个类对象,当对象撤销时,系统会采用动态关联,调用相应的析构函数,对该对象进行清理工作。

   如果将基类的析构函数声明为虚函数,由该基类所派生的所有派生类的析构函数也都自动成为虚函数(即使派生类的析构函数与基类的析构函数名字不相同)。

   在程序中最好把基类的析构函数声明为虚函数。这将使所有派生类的析构函数自动成为虚函数。这样,如果程序中用delete运算符准备删除一个对象,而delete运算符的操作对象是指向派生类对象的基类指针,则系统会调用相应类的析构函数。

   虚析构函数的概念和用法很简单,但它在面向对象程序设汁中却是很重要的技巧。专业人员一般都习惯声明虚析构函数,即使基类并不需要析构函数,也显式地定义一个函数体为空的虚析构函数,以保证在撤销动态存储空间时能得到正确的处理。

   构造函数不能声明为虚函数。这是因为在执行构造函数时类对象还未完成建立过程,当然谈不上函数与类对象的关联。

 

 

 

 

 

 

 

 

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
机器学习模型机器学习模型机器学习模型机器学习模型机器学习模型机器学习模型机器学习模型机器学习模型机器学习模型机器学习模型机器学习模型机器学习模型机器学习模型机器学习模型机器学习模型机器学习模型机器学习模型机器学习模型机器学习模型机器学习模型机器学习模型机器学习模型机器学习模型机器学习模型机器学习模型机器学习模型机器学习模型机器学习模型机器学习模型机器学习模型机器学习模型机器学习模型机器学习模型机器学习模型机器学习模型机器学习模型机器学习模型机器学习模型机器学习模型机器学习模型机器学习模型机器学习模型机器学习模型机器学习模型机器学习模型机器学习模型机器学习模型机器学习模型机器学习模型机器学习模型机器学习模型机器学习模型机器学习模型机器学习模型机器学习模型机器学习模型机器学习模型机器学习模型机器学习模型机器学习模型机器学习模型机器学习模型机器学习模型机器学习模型机器学习模型机器学习模型机器学习模型机器学习模型机器学习模型机器学习模型机器学习模型机器学习模型机器学习模型机器学习模型机器学习模型机器学习模型机器学习模型机器学习模型机器学习模型机器学习模型机器学习模型机器学习模型机器学习模型机器
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值