嵌入式全栈开发学习笔记---C++(多态)

目录

多态polymorphic

多态成立的三个条件

1、要有继承

2、要有虚函数重写

3、用父类指针(父类引用)指向子类对象

重载与重写区别

动态联编和静态联编

多态原理

构造函数中调用虚函数能否实现多态?

用父类指针指向子类数组

虚析构函数

动态类型识别

第一种方法:自定义类型

第二种方法:dynamic_cast动态转换

第三种方法:typeid获取类型信息

抽象类

抽象类注意事项

抽象类在多继承中的使用

纯虚函数的应用

多态案例


上节学习了继承和派生,这节开始学习多态!

多态polymorphic

所有的面向对象编程都具备这几个特征:抽象、封装、继承、多态

多态是面向对象编程的最后一个特征。

多态可以理解为多种形态。

先看这样一个代码:

很多人觉得通过这个p调用的是派生类Child里的show,但结果调用的是基类里面的show

为什么?

这就涉及到静态联编,编译的时候编译器发现p是基类类型,所以通过p调用的就是基类里面的show。

也就是说在编译期间(还不是运行的时候)的链接过程(编译分为四个步骤:预处理,编译,汇编和链接,这个在C语言的时候写过了,忘记的自己去翻翻看)中,直接把p->show()后面的这个“show”符号链接成基类的show函数的地址。

这就是所谓的静态联编,“静态”指的是在编译的时候,运行是属于“动态”,联编就是编译与链接。

但是这显然不是我们想要的结果,所以我们需要做一点修改,在基类的show函数前加上“virtual”。

派生类里面这里加不加都行

加上之后结果就不一样了,调用的是派生类里面的show

此时用基类指针指向基类对象,调用的就是基类里面的show

所以多态可以理解为:同一条语句有不同的执行结果,取决于指针指向谁。

这也就是一开始我们说的:“所谓多态就是多种形态”。

之前我们讲过一个概念叫“隐藏”,比如基类和派生类里面函数同名时,会把派生类继承过来的同名函数隐藏掉,所以调用的时候默认调用的是派生类里面的这个同名函数。

现在我们来学习一下“重写”的概念。

多态成立的三个条件

这就涉及到多态成立的三个条件:

1、要有继承

2、要有虚函数重写

虚函数就是刚刚被我们用virtual修饰的这个成员函数

所谓虚函数重写的意思是一定基类里面有这个虚函数

在派生类里面的这个show前面可以加virtual,也可以不加。而且这两个show的原型必须是一致的,一样的返回值,参数个数是也是一致的。

注:加上这个virtual之后,之前讲的“隐藏”的概念还是成立的。

3、用父类指针(父类引用)指向子类对象

多态一定是跟指针有关系。

以上三个条件只要缺少一个就不叫多态。

重载与重写区别

如果派生类定义了和基类相同原型的函数会怎么样?编译器支持这种写法,并且叫做函数重写。

函数重写。

函数重写就是在派生类中定义与基类原型相同的函数。函数重写只发生在派生类和基类之间。

重载与重写区别:

重载:同一个作用域;

          派生类无法重载基类函数,基类同名函数将被覆盖;

重载是在编译期间根据参数类型和个数决定;

         

重写:发生于基类、派生类之间;

          父类和子类函数有相同的函数原型;

          使用virtual关键字声明后能够产生多态;

          运行期间根据具体对象类型决定调用的函数。(因为new申请的空间是动态内存)

也就是说使用了virtual后,这条语句中p到底指向谁在编译期间(静态)编译器是不知道的,只有在运行的时候,这条语句真的执行了,才在堆空间申请一块内存,然后p才知道指向的是派生类对象,然后才调用派生类的函数,所以加上virtual后,这里属于动态联编。

动态联编和静态联编

1、联编是指一个程序模块、代码之间互相关联的过程。

2、静态联编(static binding),是程序的匹配、连接在编译阶段实现,也称为早期匹配。

    重载函数使用静态联编。

3、动态联编是指程序联编推迟到运行时进行,所以又称为晚期联编(迟绑定)switch 语句和 if 语句是动态联编的例子(比如得在运行时才能获取键值决定走哪条分支的情况)

C++与C相同,是静态编译型语言;

在编译时,编译器自动根据指针的类型判断指向的是一个什么样的对象;所以编译器认为父类指针指向的是父类对象;

由于程序没有运行,所以不可能知道父类指针指向的具体是父类对象还是子类对象

从程序安全的角度,编译器假设父类指针只指向父类对象,因此编译的结果为调用父类的成员函数。这种特性就是静态联编。

多态原理

当类中声明虚函数时,编译器会在类中生成一个虚函数表。虚函数表是一个存储类成员函数指针的数据结构。虚函数表是由编译器自动生成与维护的。

virtual成员函数会被编译器放入虚函数表中,当存在虚函数时,每个对象中都有一个指向虚函数表的指针(C++编译器给父类对象、子类对象提前布局vptr指针;当进行函数调用时,C++编译器不需要区分子类对象或者父类对象,只需要再base指针中,找vptr指针即可。)

VPTR一般作为类对象的第一个成员 。

直接上代码理解:

我们来研究一下Parent这个类占几个字节,现在这个类是个空类,在内存中空类是占一个字节,就只有一个占位字符在里面

那如果有了多态在里面,如下面这段代码,Parent这个类占几个字节?

结果是占了4个字节

所以其实多态就是靠这4个字节实现的,这4个字节是一个指针(我这个是32位系统,如果是64位系统,指针占8个字节)。而且这个指针是存放在类的前面4个字节。

接下来我们在每个类中加上一个成员变量再看看这两个类的大小

看看一下地址分布情况

内存布局:

这个指针我们就称为虚函数表指针。

凡是有虚函数的类,都会有一个虚函数表(里面保存的是函数的地址)

接下来我们以这个代码为例讲解多态的实现原理,从这两句代码开始讲起

首先p指向了Child,

接下来通过p指向show函数的步骤是:先通过p找到Child的对象,访问对象的前4个字节里面的虚函数指针,虚函数指针里面存放的是0x700,是Child虚函数表的地址。

然后再通过虚函数表找到show这个函数的地址是0x200,然后程序才跳到show函数所在空间,然后执行show。

注意:在多态中,子类的重写的show前面加不加virtual都一样,都是虚函数,子类继承了父类的虚函数和虚函数表,按理来说Child虚函数表里面应该是有一个0x100,但是被隐藏了,非要调用,函数名前面可以加作用域。

虽然把所有成员函数都声明为虚函数,也就是都加上Virtual也没事,但是不要把所有的成员函数声明成虚函数,因为这样调用的效率太低了。

比如父类中有个成员函数print,子类中没有这个函数的重写,但是我们也把virtual加在print前面让它成为虚函数,这样编译也不会报错,但是不建议这样做。

那接下来我们让基类指针指向基类对象的话,实现原理又是怎样的?

此时p指向parent的对象的前4个字节,里面的虚函数指针存放的parent虚函数表的地址,根据这个地址找到虚函数表,然后再根据虚函数表上的写的show的地址找到show,再执行。

以上就是多态实现的原理。

构造函数中调用虚函数能否实现多态?

构造的顺序是先构造父类、再构造子类

当调用父类的构造函数的时候,虚函数指针vfptr 指向父类的虚函数表;

当父类构造完,调用子类的构造函数的时候,虚函数指针 vfptr 指向子类的虚函数表

结论:构造函数中无法实现多态

直接上代码理解

这里是基类指针指向派生类对象,之前我们说过,创建派生类对象的时候,再初始化时要先调用基类的构造函数初始化,再调用派生类的构造函数。所以在这段代码中创建派生类对象后,会调用Parent里面的无参构造函数Parent(),而这个无参构造函数里面调用了虚函数show,那此时这个show是子类里面的show还是基类里面的show?

结果是调用父类Parent里面的show。

为什么这里不会调用子类的show?

因为调用基类构造函数的时候,派生类对象还没有形成,不存在多态。因为多态的形成一定是通过它最前面的那个指针实现的。

用父类指针指向子类数组

指针也是一种数据类型,C++类对象的指针p++/--,仍然可用。

指针运算是按照指针所指的类型进行的。

父类p++与子类p++步长不同;不要混搭,不要用父类指针++方式操作子类对象数组

直接上代码

也可以通过指针p来访问数组吗?

结果是不可以

因为成员变量形成不了多态,编译器发现p是Parent*类型,当时基类里面存在b。

那是否可以通过这个指针p来访问函数呢?

结果是不可以

首先p[2]是数组c的第三个元素,是一个子类对象,调用这个子类对象中的show()函数就出现了段错误。

结论就是:不要用基类指针指向派生类数组,因为步长不一样。

首先一个派生类对象的大小是4104个字节,c[2]是c这个地址往后走了4104*2个字节,而一个parent基类的大小是8个字节,p是parent*类指针,p[2]是p这个地址向后走了8*2个字节,也就是说c[2]和p[2]根本不是一个意思。P[2]就相当于是p+2,如果我们对p+2用*取值的话,取出来的就不是一个合法的对象了,再如果我们想通过p[2]去访问show的话,它根本就找不到这个show在哪,可能访问了一个非法的地址,程序就出现了段错误。

虚析构函数

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

构造函数不能是虚函数。建立一个派生类对象时,必须从类层次的根开始,沿着继承路径逐个调用基类的构造函数

析构函数可以是虚的。虚析构函数用于指引 delete 运算符正确析构动态对象

虚析构函数:通过父类指针释放子类对象

注:之前我们讲过普通成员函数的隐形参数this指针,现在再补充一点,在 C++ 中,this 是一个指向当前对象的指针,我们可以通过 this 来访问当前对象的所有成员。this 实际上是当前类类型的指针,例如,对于类 Box 的成员函数,this 是 Box* 类型的指针。

this 指针可以在类的所有非静态成员函数中使用,包括构造函数和析构函数。我们可以使用 this 指针来访问成员变量和成员函数。

直接上代码讲解:

以上这段代码没有函数重写,所以不涉及多态,因此属于静态联编。当通过p访问show的时候,编译器发现p是Person类型,而Person里面没有show,所以编译就报错了。

因此我们要在Person里面加上show函数并且声明成virtual,这样才能构成虚函数重写

这一次它就能把这个学生信息打印出来了,因为已经构成了多态。

但是你会发现还有一个问题没有解决,那就是它只调用了构造函数,没有调用析构函数。也就是我们只申请了空间,但是没有释放空间。

那么我们直接在main函数中手动释放空间可以吗?

结果还是不可以

因为这里delete p释放的是Person*类型的指针,它只会调用Person里面的析构函数。

怎么办?

我们可以把Person里面的析构函数写出来,然后声明为虚析构函数

这样就可以了,为什么?

因为虚析构函数可以通过基类指针释放派生类对象。

也就是说我们这里写的是delete p,但是编译器会调用p指向的那个对象(派生类对象)的析构函数。

所以基类里面的析构函数我们尽量写成虚析构函数(派生类里面的析构函数加不加virtual都可以)

那构造函数要不要也加上virtual变成虚函数呢?

结果报错了

结论:构造函数不能被声明为虚函数。

因为调用构造函数的时候,对象还没有初始化完成,对象的最前面的4个字节是不是指向虚函数表还不一定。

接下来再看一个跟多态相关的问题

动态类型识别

直接上代码讲解

在这段代码中,现在如果把test(&p)改成test(&c)的话,那test函数的参数要改成Child*类型吗?

其实是不需要的,有了多态之后,这里不需要改成Child*类型。

因为在多态里面,基类指针可以指向基类对象,也可以指向派生类对象,而且指向不同的对象就调用不同的成员函数。

所以当我们有很多的派生类,不知道派生类的具体类型的情况下,我们就可以懵懂地定义一个基类指针,反正运行的时候它会根据指针指向谁就调用谁的成员函数,这就是多态的好处和意义。

现在我们在test函数里面将p强转成child*类型,然后赋值给c,再通过c访问派生类里面的数组b的最后一个元素。

这段代码目前编译正常,但是我们分析一下就有问题了,一开始test传的是基类对象p的地址,然后基类指针p指向的是基类的对象p,但是在test函数体内,我们将基类指针p强转为child*类型(此时编译器看c->b[1024000-1]=1;这句里面的c的确是有数组b,所以语法上就通过了,所以编译没有问题),但是p指针里面的值是没有改变的,也就是说p指针里面存的还是基类对象p的地址,当Child*c=(Child*)p;这句一旦执行完毕,就是派生类指针c指向了基类对象p,而在基类里面没有数组b,这时如果我们通过c访问数组b的话,就出现了段错误。

结论:强制类型转换有的时候能转换,有的时候不能转换,如果p指向派生类对象,可以转换,如果指向基类对象,就不能转换。

要想拯救这段代码,那就得用多态来解决。

C++为了能够在运行时正确判断一个对象确切的类型,加入了RTTI(Run-Time Type Identification)。

RTTI提供了以下两个非常有用的操作符:

 

(1)typeid操作符,返回指针和引用所指的实际类型。

 

(2)dynamic_cast操作符,将基类类型的指针或引用安全地转换为派生类型的指针或引用。

 

三种办法可以解决C++动态类型识别问题

第一种方法:自定义类型

C++如何得到动态类型?

C++中的多态根据实际的对象类型调用对应的函数

1、可以在基类中定义虚函数返回具体的类型信息

2、所有的派生类都必须实现类型相关的虚函数

3、每个类中的类型虚函数都需要不同的实现

使用虚函数进行动态类型识别的缺陷

1、必须从基类开始提供类型虚函数

2、所有派生类都必须重写类型虚函数

3、每个派生类的ID必须唯一

直接上代码讲解:

刚刚说过如果指针p指向派生类对象,可以转换,如果指向基类对象,就不能转换。这主要取决于指针p到底指向的是谁,因为它指向谁就调用谁的函数,那要想实现这个效果,我们就得形成多态。

以下这段代码中想形成多态就差函数重写这一个条件

那虚函数我们要怎么写呢?既然说自定义类型,那我们用数字表示也可以,直接用枚举

既然是虚函数重写,我们就每个类都写一个getID函数

那在这种情况下只有getID返回1的情况可以强转

如果换成这样就是不能强转

所以我们就可以通过一个数字ID的方式来判定指针到底指向的是谁,进而我们就知道这个类型到底能不能强转。

既然是自定义,那么我们不一定要把这个“ID”定义成枚举类型,也可以定义成int类型。

最后这里是可以强转成这样的:

虽然这种方法能有效解决问题,但是我们需要写很多辅助代码,比较麻烦。

第二种方法:dynamic_cast动态转换

dynamic_cast是C++里面强转的关键字

新的关键字 dynamic_cast

1、dynamic_cast是C++中的新型关键字

2、dynamic_cast用于基类和派生类之间的转换

3、dynamic_cast要求使用的目标类型是多态

  即要求所在类族至少有一个虚函数

  用于指针转换时,转换失败返回空指针

  用于引用转换时,转换失败将引发bad_cast异常

dynamic_cast的优势

1、不用显示的声明和定义虚函数

2、不用为类族中的每个类分配类型ID

dynamic_cast的缺陷

1、只能用于有虚函数的类族

直接上代码讲解:

在C语言里面这样强转的,但是这个什么时候能转什么时候不能转我们不知道,dynamic_cast就可以解决这个问题

dynamic_cast强转

如果p这个指针原本指向的就是Chinese的对象的话,那么这条语句才会强转成功,否则强转失败,失败返回空指针,所以我们只要判断指针c是不是空的。

注意,dynamic_cast所强转的类型的类里面必须要有虚函数才行,也就是能形成多态的地方才允许这么使用。

第三种方法:typeid获取类型信息

如果获取一个变量的类型?

C++提供了typeid关键字用于动态获取类型信息

1、typeid关键字返回对应参数的类型信息

2、typeid关键字返回一个type_info类对象的常引用(别名))

  当typeid参数为NULL时,抛出bad_typeid异常

3、type_info类的使用需要包含typeinfo头文件

直接上代码讲解

打印出来的结果并不是完整的类型表示符,而是这个类型在编译器里面的编号,比如int就i,char就是c,float就是f,我们自定义的三个类型前面加了数字。不同的编译器打印出来可能不一样。

现在我们用typeid来解决我们之前的问题

这里能不能强转就取决于*p是什么类型,因为p指向一个对象,*p就是一个对象。

再来看一下多态里面的一个概念

抽象类

纯虚函数是一个在基类中说明的虚函数,在基类中没有定义,要求任何派生类都定义自己的版本;

纯虚函数为各派生类提供一个公共界面(接口的封装和设计、软件的模块功能划分)

纯虚函数的说明形式:

virtual 类型 函数名(参数列表) = 0;

一个具有纯虚函数的类称为抽象类。

直接上代码:

抽象类示例:图形、圆、矩形

这段看上去没毛病,可是运行后的结果是

因为目前这个代码没有构成多态,因为没有虚函数重写,没有构成多态,所以只是静态联编的过程。

基类指针指向派生类对象我们这里写的是没毛病的

但是编译通过ps去Shape里面找的时候发现没有get_s这个函数,所以就报错了。

所以我们要把虚函数写上,构成虚函数重写,形成多态

但是由于Shape这个类表示的只是很多图形的大类,没有具体的图形,所以不好在get_s()函数中写具体的函数体,按按理这个get_s在基类里面就不应该存在,但是为了形成多态,它又必须存在,怎么办?

如果我们就这样空着函数体,编译就会报错,因为它比如有返回值,返回double类型,如果空着函数体,就会默认返回int类型

我们可以这样写:

这种为了形成多态不得不写的函数,而函数体又没法写,那么可以直接让它等于0(它既然不得不存在,那就让它等于0,留一个接口给它,类似于声明),我们称这个函数为纯虚函数。

含有纯虚函数的类,我们称为抽象类;抽象类派生出来的类可以创建对象。

这样写就没有问题了

此时如果我们通过基类指针创建一个基类对象,编译就会报错

结论:抽象类不能创建对象

因为如果我们创建了一个抽象类的基类对象,之后它想要调用get_s函数的时候,根本没办法执行,因为get_s是一个纯虚函数

完整代码:

#include <iostream>

using namespace std;

class Shape
{
protected:
	int m_a;
	int m_b;
public:
	Shape(int a,int b=0)//如果是圆的话只需要半径一个参数,所以b设为默认参数
	{
		m_a=a;
		m_b=b;
	}
	virtual double get_s()=0;  //纯虚函数
};

class Rectangle:public Shape
{
public:
	Rectangle(int a,int b):Shape(a,b)  //创建Rectangle的对象时,调用Rectangle构造函数,然后调用Shape构造函数初始化,这里将a和b传给Shape()
	{
		
	}
	double get_s()//计算矩形面积
	{
		return m_a*m_b;
	}
};

class Circle:public Shape
{
public:
	Circle(int r):Shape(r)//创建Circle这个类时,传一个参数r给Shape,Shape的另外一个参数默认是0
	{

	}
	double get_s()
	{
		return m_a*m_a*3.14;
	}
	
};

int main()
{
	Shape *ps=new Rectangle(1,2);
	cout<<ps->get_s()<<endl;
	delete ps;

	ps=new Circle(1);//ps被释放后重新申请一块内存给它
	cout<<ps->get_s()<<endl;
	delete ps;
	
	//ps=new Shape(1,2);//抽象类不能创建对象
	//ps->get_s();

	return 0;
}

抽象类注意事项

1、抽象类不能用于直接创建对象实例,可以声明抽象类的指针和引用;

2、可使用指向抽象类的指针支持运行时多态性

3、派生类中必须实现基类中的纯虚函数,否则它仍将被看作一个抽象类

抽象类在多继承中的使用

C++中没有Java中的接口概念,抽象类可以模拟Java中的接口类。(接口和协议)

工程上的多继承

被实际开发经验抛弃的多继承

工程开发中真正意义上的多继承是几乎不被使用的

多重继承带来的代码复杂性远多于其带来的便利

多重继承对代码维护性上的影响是灾难性的

在设计方法上,任何多继承都可以用单继承代替

纯虚函数的应用

C++中没有接口的概念

C++中可以使用纯虚函数实现接口

接口类中只有函数原型定义。

实际工程经验证明

多重继承接口不会带来二义性和复杂性等问题  

多重继承可以通过精心设计用单继承和接口来代替

接口类只是一个功能说明,而不是功能实现。

子类需要根据功能说明定义功能实现。

多态案例

公司员工工号从1号开始,每来一个员工工号加1;

总经理底薪10000;

工程师每小时100元;

销售月薪是销售额的10%;

销售经理底薪5000+部门销售额的5%;

用代码表示每个人每月的薪资。

把继承和多态的一些知识点综合起来练习一下

完整代码:

Employee.h

#ifndef EMPLOYEE_H
#define EMPLOYEE_H

//抽象基类
class Employee
{
protected:
	int id;//工号
	static int num;//员工的个数
public:
	Employee();//可在外部实现
	virtual void print_salary()=0;//纯虚函数在派生类中实现
	virtual ~Employee();//因为最后要通过基类指针释放派生类对象,所以要虚析构函数
};

//总经理
class Manager:virtual public Employee
{
protected:
	int base;//基本工资
public:
	Manager(int b);//把基本工资传过来初始化
	void print_salary();//计算并打印工资
};

//工程师
class Engineer:public Employee
{
private:
	int hour;//工时
public:
	Engineer(int h);//把工时传过来初始化
	void print_salary();//计算并打印工资
};

//销售
class SalePerson:virtual public Employee
{
protected:
	int sales;//销售额
	static int sum;//总的销售额
public:
	SalePerson(int m=0);//把销售额传过来初始化;创建销售经理对象时要调用这个构造函数,但是销售经理不需要传销售额,所以把m设为默认参数
	void print_salary();//计算并打印工资
};

//销售经理
class SaleManager:public Manager,public SalePerson  //使用Base和sum;为了让id只继承一份过来,所以把上面Manager的SalePerson改成虚继承Employee
{
public:
	SaleManager(int b);//把基本工资传过来初始化
	void print_salary();//计算并打印工资
};



#endif

Employee.cpp

#include "employee.h"
#include <iostream>

using namespace std;

int Employee::num=0;//静态成员变量在全局初始化,它比主函数先执行
int SalePerson::sum=0;//总的销售额


Employee::Employee()
{
	num++;//人数累计,静态成员变量不能被构造函数的this指针访问,因为它是共享的变量,不属于这里
	this->id=num;//用数字当工号
}

Employee::~Employee()
{
	
}

Manager::Manager(int b)
{
	this->base=b;
}

void Manager:: print_salary()
{
	cout<<"职位:经理    工号:"<<this->id<<"   工资:";//先不换行
	cout<<base<<endl;//换行
}

Engineer::Engineer(int h)
{
	hour=h;
}

void Engineer::print_salary()
{
	cout<<"职位:工程师    工号:"<<this->id<<"   工资:";//先不换行
	cout<<100*hour<<endl;//换行
	
}

SalePerson::SalePerson(int m)
{
	sales=m;
	sum+=sales;
}

void SalePerson::print_salary()
{
	cout<<"职位:销售    工号:"<<this->id<<"   工资:";//先不换行
	cout<<sales*0.1<<endl;//换行
}

SaleManager::SaleManager(int b):Manager(b)  //派生类的对象要先调用基类构造函数初始化,所以用初始化列表给基类传参
{
	//base=b;
}

void SaleManager::print_salary()
{
	cout<<"职位:销售经理    工号:"<<this->id<<"   工资:";//先不换行
	cout<<base+sum*0.05<<endl;//换行
}

Main.cpp

#include "employee.h"
#include <iostream>
#include <time.h>
#include <stdlib.h>

using namespace std;

int main()
{
	//要创建20个员工
	Employee *e[20]={0};//基类指针指向派生类对象
	int idx=0;//对象数组元素下标

	//创建一个总经理
	e[idx++]=new Manager(10000);

	srand(time(NULL));
	//创建10个工程师,工时随机
	for(int i=0;i<10;i++)
	{
		e[idx++]=new Engineer(rand()%100+120);//工时范围120-219
	}

	//创建8个销售,销售额随机
	for(int i=0;i<8;i++)
	{
		e[idx++]=new SalePerson(rand()%20000+100000);//销售额范围100000-299999
	}

	//创建一个销售经理
	e[idx++]=new SaleManager(5000);

	for(int i=0;i<20;i++)
	{
		e[i]->print_salary();
		delete e[i];
	}

	return 0;
}

运行结果:

下节开始学习运算符重载!

如有问题可评论区或者私信留言,如果想要进扣扣交流群请私信!

  • 20
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Vera工程师养成记

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值