C++继承和多态总结

一、继承的本质和原理

在这里插入图片描述
总结:

  1. 外部只能访问对象public成员,protected和private的成员无法直接访问
  2. 在继承结构中,派生类可以继承基类的private成员,但是无法直接访问
  3. protected和private的区别:在基类中定义的成员,想被派生类访问但是不想被外部访问,那把基类中的相关成员定义成protected。如果派生类和外部都不访问,那就把基类中的相关成员定义成peivate
  4. 使用class定义派生类,默认继承方式是private;使用struct定义派生类,默认继承方式是public

二、派生类的构造过程

派生类怎么初始化从基类继承来的成员变量?

  1. 派生类可以继承所有成员(包括方法和变量),除了构造函数和析构函数。通过调用基类相应的构造函数来初始化
  2. 派生类的构造和析构函数,负责初始化和清理派生类部分;派生类从基类继承来的成员的初始化以及清理部分由基类的构造和析构函数负责。

派生类对象构造和析构的过程

  1. 派生类调用基类的构造函数,初始化从基类继承的成员
  2. 派生类调用自己的构造函数,初始化派生类自己特有的成员
  3. 当对象作用域到期时,调用派生类的析构函数,释放派生类成员可能占用的外部资源(堆内存、文件)
  4. 调用基类的析构函数,释放派生类内存中,从基类继承类的成员可能占用的外部资源(堆内存、文件)
class Base {
public:
	Base(int data) :ma(data) {
		cout << "Base()" << endl;
	}
	~Base() {
		cout << "~Base()" << endl;
	}

protected:
	int ma;
};

class Derive :public Base {
public:
	// 错误写法:Derive(int data):ma(data), mb(data)
	Derive(int data) :Base(data), mb(data){
		cout << "Derive()" << endl;
	}

	~Derive() {
		cout << "~Derive()" << endl;
	}

private:
		int mb;
};

三、重载、隐藏

重载: 同一作用域,函数名相同,参数列表不同,和返回值无关,virtual可有可无(同一作用域,函数名相同,参数列表不同)

void fun(int a) {}
void fun(double d){}
void fun(string str){}

覆盖: 在继承结构中,派生类中有和基类函数名相同,参数相同,基类函数必须有virtual(针对继承结构、针对virtual)

class Base {
public:
    virtual void fun(int a) {
        cout << a << endl;
    }
};

class Derive : public Base{
public:
    void fun(int a) {
        a += 1;
        cout << a << endl;
    }
};

int main() {
    Derive d;
    d.fun(1);
    return 0;
}

隐藏: 在继承结构中,如果派生类的函数与基类的函数同名,但是参数不同,此时无论有无virtual,基类的函数都被隐藏(针对继承结构,不针对virtual),被隐藏后,不能直接调用,但是可以通过类作用域访问

class Base {
public:
    void fun(int a) {
        cout << a << endl;
    }
};

class Derive : public Base{
public:
    void fun(string str) {
        cout << str << endl;
    }
};

int main() {
    Derive d;
    d.fun(1);       // fun(int)被隐藏了,无法直接访问,error
    d.Base::fun(1); // 正确
    return 0;
}
#include<iostream>

using namespace std;

class Base {
public:
	Base(int data) :ma(data) {
		cout << "Base()" << endl;
	}

	void show() {
		cout << "Base::show()" << endl;
	}

	void show(int) {
		cout << "Base::show(int)" << endl;
	}

	~Base() {
		cout << "~Base()" << endl;
	}

protected:
	int ma;
};

class Derive :public Base {
public:
	Derive(int data) :Base(data), mb(data){
		cout << "Derive()" << endl;
	}

	void show() {
		cout << "Derive::show()" << endl;
	}

	~Derive() {
		cout << "~Derive()" << endl;
	}

private:
		int mb;
};

int main() {

#if 0
	Derive d(20);
	d.show();
	// d.show(10);//Derive::show: 函数不接受 1 个参数(隐藏)
	d.Base::show(10);
#endif

	Base base(10);
	Derive derive(20);
	
	base = derive;

	// error
	derive = base;

	// 由于是Base的指针,该指针只指向Base大小的空间,只能访问Base的方法和成员,除非强转
	Base* ptr_b = &derive;
	
	// error,由于是Derive的指针,该指针指向Derive大小的空间,
	// 但是实际可以访问的空间就只有Base那么大,剩下的空间属于非法访问
	Derive* ptr_d = &base;
	return 0;
}

总结: 指针的类型决定指针的能力,但实际能访问的区域由指向的对象决定。如果指针的能力大于实际允许访问的空间(Derive* ptr_d = &base),则是非法访问(直接编译不过)。如果指针的能力小于实际指向的空间(Base* ptr_b = &derive;),则只能访问指针指向范围内的数据。

四、静态绑定和动态绑定

静态绑定(编译期间)

	Base base(10);
	Derive derive(20);
	Base* ptr_b = &derive;

	//Base::show(),静态(编译时期)绑定(函数调用),call Base::show (0D71037h)  
	ptr_b->show();    
	//Base::show(int),静态(编译时期)绑定(函数调用),call Base::show (0D712FDh)
	ptr_b->show(10); 

	// 4
	cout << sizeof(Base) << endl;  
	//8
	cout << sizeof(Derive) << endl;
	//class Base*
	cout << typeid(ptr_b).name() << endl;
	//class Base
	cout << typeid(*ptr_b).name() << endl;

类添加虚函数后,有什么影响?

  1. 如果类里面定义了虚函数,编译器在编译阶段给这个类类型产生一个唯一的vftable虚函数表,虚函数表中存储的内容就是RTTI指针和虚函数地址。
  2. 程序运行时,每张虚函数表都会加载到内存的.rodata区,用户无法通过指针修改虚函数表。
  3. 如果类里面定义了虚函数,程序运行时,这个类对应的对象对应的内存开始部分会多存储一个vfptr虚函数指针,指向相应类型的虚函数表vftable。一个类型定义的所有对象的vfptr都会指向同一张虚函数表。
  4. 一个类里虚函数的个数不影响对象占用内存的大小(共用一个vfptr),影响的是虚函数表vftable的大小
  5. 如果派生类中的方法和基类继承来的某个方法,返回值、函数名、参数列表都相同,而且基类的方法是virtual的,那派生类中的方法直接被处理成虚函数,也即覆盖(注意,只有方法会覆盖,数据成员不会覆盖,而是存储多个,可以通过作用域访问)。

动态绑定(运行期间)

调用的不是类名加作用域的方法,而是调用寄存器内存放的地址,而这个地址只有在运行时期才能确定

#include<iostream>
#include<typeinfo>

using namespace std;

class Base {
public:
	Base(int data):ma(data) {
		cout << "Base()" << endl;
	}

	virtual void show() {
		cout << "Base::show()" << endl;
	}

	virtual void show(int) {
		cout << "Base::show(int)" << endl;
	}

	~Base() {
		cout << "~Base()" << endl;
	}
	
protected:
	int ma;
};

class Derive :public Base {
public:
	Derive(int data) :Base(data), mb(data){
		cout << "Derive()" << endl;
	}

	void show() {
		cout << "Derive::show()" << endl;
	}

	~Derive() {
		cout << "~Derive()" << endl;
	}
	
private:
	int mb;
};

int main() {
	Base base(10);
	Derive derive(20);
	Base* ptr_b = &derive;

	/*
		查看ptr_b的类型,是Base;查看Base::show;
		1. 如果Base::show是普通函数,就进行静态绑定,call Base::show 
		2. Base::show是虚函数,就进行动态(运行时)绑定(函数调用,从虚函数表中获取)
		mov eax, dword ptr[ptr_b]  ; 根据vfptr获取虚函数表的地址
		mov ecx, dword ptr[eax]    ; 从虚函数表中拿到虚函数的地址
		call ecx                   ; 调用虚函数
	*/
	ptr_b->show();    //Derive::show()
	ptr_b->show(10); //Base::show(int)

	// 4 + 4(vfptr) = 8
	cout << sizeof(Base) << endl;  
	// 8 + 4(vfptr) = 12 
	cout << sizeof(Derive) << endl;
	// 指针类型就是class Base*(编译时期直接确定,不会改变)
	cout << typeid(ptr_b).name() << endl;
	/*
	首先ptr_b的类型是Base,然后查看Base是否有虚函数
	1. 没有虚函数,*ptr_b就是编译时期(静态)的类型,*ptr_b就是class Base
	2. 如果有虚函数,*ptr_b就是运行时期(动态)的类型,即RTTI类型
	*/
	cout << typeid(*ptr_b).name() << endl;  // class Derive
}

在这里插入图片描述

由于DeriveBase继承了虚函数,RTTI(Run-Time Type Identification) 指向的是一个RTTI类型的指针,理解成一个对象名字符串即可。本来存放的是&Base::show()(继承而来),但是由于Derive中重写了虚函数,于是就用&Derive::show()覆盖了&Base::show()

覆盖: 如果派生类中的方法和基类继承来的某个方法,返回值、函数名、参数列表都相同,而且基类的方法是virtual的,那派生类中的方法直接被处理成虚函数。那么派生类的虚函数表中原有的方法则被覆盖。
在这里插入图片描述
理解虚函数

虚函数依赖:

  1. 虚函数可以在vftable中产生函数地址
  2. 对象必须存在(vfptr–>vftable–>虚函数地址)

哪些函数不能实现成虚函数?

  1. 构造函数不能实现成virtual,构造函数构造完成,对象才产生,才有vfptr。
  2. 构造函数中调用虚函数,也不会发生动态绑定,调用任何函数都是静态绑定的。(动态绑定需要调用派生类的方法,然而派生类对象还没有构造完成)
  3. static静态成员方法的调用不依赖对象,不存在于vftable,不需要通过vfptr访问vftable进行调用

虚析构函数

class Base {
public:
	Base(int data):ma(data) {
		cout << "Base()" << endl;
	}

	virtual void show() {
		cout << "Base::show()" << endl;
	}

	virtual void show(int) {
		cout << "Base::show(int)" << endl;
	}

	~Base() {
		cout << "~Base()" << endl;
	}
	
protected:
	int ma;
};

class Derive :public Base {
public:
	Derive(int data) :Base(data), mb(data), ptr(new int(data)){
		cout << "Derive()" << endl;
	}

	~Derive() {
		delete ptr;
		cout << "~Derive()" << endl;
	}
	
private:
	int mb;
	int* ptr;
};

int main() {
	Base* ptr_b = new Derive(10);
	ptr_b->show();
	delete ptr_b; 
}

在这里插入图片描述
没有调用派生类的析构函数,在本代码中导致内存泄露

delete ptr_b执行过程说明:
查看ptr_b的类型,是Base*;接着查看Base::~Base()是普通函数还是虚函数
1. 是普通函数,对于析构函数的调用就是静态绑定,汇编代码为:call Base::~Base
2. 是虚函数,对于析构函数的调用就是动态绑定。
Derive的vftable中用&Derive::~Derive替换了基类的析构函数(只要基类中的析构函数是virtual,派生类的析构函数也是virtual)

代码改动如下:

virtual ~Base(){
	cout << "~Base()" << endl;
}

在这里插入图片描述
什么时候必须把Base的析构函数实现成虚函数?

基类的指针指向堆上的派生类对象时,delete ptr_base调用析构函数的时候,由于必须要调用到派生类对象的析构函数,所以必须是动态绑定,此时需要把Base的析构函数实现成virtual。若是静态绑定,则直接根据指针的类型,调用析构函数,无法调用派生类的析构函数。

实例对象、指针、引用分别调用虚函数

int main() {
	Base b;
	Derive d;
	// 通过对象访问,肯定是静态绑定
	b.show();
	d.show();

	// 动态绑定(必须是由指针调用),指向哪个对象就查哪个对象的虚函数表
	Base* ptr_b1 = &b;
	ptr_b1->show(); 
	
	Base* ptr_b2 = &d;
	ptr_b2->show();

	// 引用也同指针, call eax
	Base& r_b1 = b;
	r_b1.show();

	Base& r_b2 = d;
	r_b2.show();

	/*
		p的类型是Derive*,所以查看Derive里的show,发现是virtual,
		于是查指向对象的前4字节,从虚函数表中取出调用函数的地址
	*/
	Derive* p = (Derive*)&b;
	p->show();     // Base::show()
}

面试官:怎么理解多态?

分为静态(编译时期)多态和动态(运行时期)多态。

  1. 静态多态的表现形式包括函数重载类模板
  2. 继承结构中,Base类指针(引用)指向Derive类对象,通过该指针(引用)调用同名覆盖方法(虚函数),该指针指向哪个Derive对象,就调用哪个Derive类方法。Base指针指向哪个对象,就访问谁的vfptr,调用对应的方法

面试官:继承的好处有哪些?

  1. 可以做代码复用
  2. 在基类中提供统一的虚函数接口,让派生类重写,就可以实现多态了

五、抽象类

当我们不希望一个class去实例化一个对象时,我们可以将这个类写成抽象类。拥有纯虚函数的类叫做抽象类,抽象类不能实例化对象,但是可以定义指针和引用变量。而纯虚函数指的是没有函数体,必须重写的那种,例如:virtual void fun() = 0


class Animal {
public:
	Animal(string name) :_name(name) {

	}
	// 纯虚函数,必须重写
	virtual void bark() = 0;
protected:
	string _name;
};

class Cat : public Animal{
public:
	Cat(string name) :Animal(name) {

	}

	void bark() {
		cout << _name << " miao miao" << endl;
	}
private:

};

class Dog : public Animal {
public:
	Dog(string name) :Animal(name) {

	}

	void bark() {
		cout << _name << " wang wang" << endl;
	}
private:

};

int main() {
	Cat cat("cat");
	Cat dog("dog");
	Animal* animal1 = &cat;
	Animal* animal2 = &dog;
	animal1->bark();
	animal2->bark();
}

六、笔试题总结

笔试题1:交换vfptr

void fun(){
	Animal* p1 = new Cat("cat");
	Animal* p2 = new Dog("dog");
	// 强转为int*
	int* p11 = (int*)p1;
	int* p22 = (int*)p2;
	// 实际上这里是交换了对象的前4个字节,也就是vfptr
	int tmp = p11[0];
	p11[0] = p22[0];
	p22[0] = tmp;
	// 根据交换以后的vfptr访问虚函数表
	p1->bark();
	p2->bark();
}

在这里插入图片描述

笔试题2:编译时期就确定压栈哪个默认参数

#include<iostream>
#include<string>

using namespace std;

class Base {
public:
	virtual void show(int i = 10) {
		cout << "Base::show() i = "<< i << endl;
	}
};

class Derive :public Base {
public:
	void show(int i = 20) {
		cout << "Derive::show() i = "<< i << endl;
	}
};

int main() {
	Base* p = new Derive();
	p->show();
	delete p;
	return 0;
}

在这里插入图片描述

在这里插入图片描述
解析:汇编指令在编译时期确定,参数压栈和访问权限也是在编译时期确定。虽然动态绑定执行的是Derive::show,但由于pBase*,而Baseshow方法的默认值是10,编译时期直接就将参数默认值10压栈。压栈以后,虽然是动态绑定call eax,调用的是Derive的方法,但在编译时期就确定好将10作为参数压栈,打印的值也就是编译时期确定的10

笔试题3:编译时期确定访问权限

#include<iostream>

using namespace std;

class Base {
public:
	virtual void show() {
		cout << "Base::show()"<< endl;
	}
};

class Derive :public Base {
private:
	// show方法改成了private
	void show() {
		cout << "Derive::show()"<< endl;
	}
};

int main() {
	Base* p = new Derive();
	p->show();  // 正常动态绑定,访问Derive::show()
	delete p;
	return 0;
}

解析:访问权限是编译时期确定的,编译时期用Base指针进行访问,而在编译时期编译器只能看见Base类里的show方法是public的,所以在编译时期认为这个show方法是可以访问的。而编译派生类的时候,直接将派生类show方法的地址放入vftable,动态绑定的时候(call eax),也就可以正常从vftable中取出重写的show方法地址。同理,下面代码直接无法通过编译

class Base {
// 访问权限在编译时期确定
private:
	virtual void show() {
		cout << "Base::show()"<< endl;
	}
};

class Derive :public Base {
public:
	void show() {
		cout << "Derive::show()"<< endl;
	}
};

int main() {
	Base* p = new Derive();
	p->show();  // 编译时期确定Base::show是private,无法访问
	delete p;
	return 0;
}

笔试题4:构造函数栈帧初始化完成后就将vftable地址写入vfptr,然后执行初始化列表,最后执行函数体

#include<iostream>

using namespace std;

class Base {
public:
	Base() {
		/*
			push ebp        
			mov ebp, esp  ; 开辟栈帧
			sub esp, 4Ch
			rep stos      ; windows会做内存初始化
			最后将&Base::vftable写入vfptr
		*/
		cout << "Base::Base()" << endl;
		clear();
	}
	
	virtual void show() {
		cout << "Base::show()"<< endl;
	}

	void clear() {
		// 当前对象被清0
		memset(this, 0, sizeof(*this));
	}
};

class Derive :public Base {
public:
	Derive() {
		/*	
			每一个函数进来,都有如下4行代码
			push ebp
			mov ebp, esp  ; 开辟栈帧
			sub esp, 4Ch
			rep stos      ; windows会做内存初始化

			最后将&Derive::vftable写入vfptr        ; 在这里将vftable的地址写入vfptr
		*/
		cout << "Derive::Derive()" << endl;
	}

	void show() {
		cout << "Derive::show()" << endl;
	}
};

void fun1() {
	Base* p1 = new Base();
	p1->show();   // 动态绑定,报错
	delete p1;
}

void fun2() {
	Base* p2 = new Derive();
	p2->show();  // 动态绑定,正常访问vftable
	delete p2;
}

int main() {
	fun1();
	fun2();
	return 0;
}

fun1分析:
在这里插入图片描述
fun2分析:

派生类对象需要首先调用Base(),执行Base()时,先开辟栈帧,在执行语句前会将&Base::vftable写入vfptr,当前对象内存被清0。然后调用Derive(),开辟栈帧,在执行语句前将&Derive::vftable的写入vfptr,此时派生类虚函数表可以正常访问。

在这里插入图片描述

七、理解虚继承和虚基类

virtual可以修饰方法,也可以修饰继承方式。修饰继承方式的时候,则是虚继承。虚继承会将Base类的数据搬到Derive类对象最末尾,然后再对象首地址补上一个vbptr

在这里插入图片描述

在这里插入图片描述

class A {};   // sizeof(A) = 1
class B : public A{};  // sizeof(B) = 1
class A {};   // sizeof(A) = 1
class B :virtual public A{};  // sizeof(B) = 4,包括一个vbptr
// sizeof(A) = 4,包括vfptr
class A {
public:		
	virtual void fun() {}
};
/* 
sizeof(B) = 8,包括vfptr和vbptr,vfptr直接继承A的(而不是派生类有一个单独的vfptr)
               如果是虚继承,基类的数据全部存放在派生类对象内存最后面
class B size(8):
        +---
 0      | {vbptr}
        +---
        +--- (virtual base A)
 4      | {vfptr}
        +---

B::$vbtable@:
 0      | 0
 1      | 4 (Bd(B+0)A)

B::$vftable@:
        | -4
 0      | &A::fun
vbi:       class  offset o.vbptr  o.vbte fVtorDisp
               A       4       0       4 0
*/
class B :virtual public A{};

注:

  1. windows查看类空间命令cl <源文件名> /d1reportSingleClassLayout<类名>
  2. vfptr由派生类继承得到,当派生类有自己单独的虚函数时,也有自己的vfptrvfptr指向vftablevftable存放vfptr在内存中的偏移量、RTTI信息以及虚函数地址。如果派生类没有virtual关键字的函数,那派生类对象的vfptr就在基类作用域下
  3. vbptr指向vbtablevbtable存放的vbptr和虚基类数据在派生类内存中的偏移量

关于虚继承导致的delete错误

#include<iostream>
#include<typeinfo>
#include<string>

using namespace std;

class A {
public:		
	virtual void fun() {
		cout << "call A::fun()" << endl;
	}
	
	void operator delete(void* ptr){
		cout << "operator delete p:" << ptr <<endl;
		free(ptr);
	}
private:
	int ma;
};

class B :virtual public A{
public:
	void fun() {
		cout << "call B::fun()" << endl;
	}

	void* operator new(size_t size){
		void* p = malloc(size);
		cout << "operator new p:" << p <<endl;
		return p;
	}
private:
	int mb;
};

int main() {
	// 基类指针指向派生类对象,永远是指向派生类对象中基类数据的起始地址
	// 这里如果是栈上对象,编译器会自动释放空间,不会报错
	A* p = new B();
	p->fun();
	delete p; // 释放空间error
	return 0;
}

在这里插入图片描述
基类指针指向派生类对象,永远是指向派生类对象中基类数据的起始地址,由于虚继承将基类数据放在了派生类对象后面,所以基类指针p指向的也是派生类对象的中间部分,普通继承则把基类数据放在派生类对象内存最前面,基类指针p可以正常指向派生类对象首地址。

free释放空间时,需要往前偏移查看块头,从指针p处开始偏移,无法正确读取块头,则报错

八、菱形继承问题

用虚继承解决多重继承中,继承重复数据的问题,一般来说,在设计比较好的开源代码中很少见到多重继承,Java中就直接禁止了多重继承
在这里插入图片描述

有重复继承的问题,D的对象存放两份ma,用虚继承(重复数据替换为vbptr)解决这种重复继承的问题(谁的数据重复,谁就需要被虚继承

在这里插入图片描述

此时,注意A已经和D一样需要自己进行构造(结构图最靠左),而不是需要构造别人提前构造自己(被迫构造),需要手动对A进行初始化。通过构造函数打印结果,也可以看到此时没有重复构造A,把A的数据都通过vbptr替换,并把A的数据放到内存最后

使用普通继承时,A的数据是在B和C作用域下的,所以A的构造函数由B和C的构造函数调用,D不用负责A数据的初始化

// 虚继承前
D(int data) :B(data), C(data), md(data){}

使用如上图所示的继承方式,B和C都是虚继承A,此时会在B和C作用域下将A的数据都用vbptr替换,将A的数据搬到D对象空间的末尾,A的数据直接就在D作用域下,所以初始化D时也需要负责A数据的初始化。如果还按照普通继承的方式不调用A的构造函数,就会报错

在这里插入图片描述

// 虚继承后
D(int data) :A(data),B(data), C(data), md(data){}
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
C++中的继承多态和虚函数是面向对象编程的重要概念。 继承是指一个类可以从另一个类继承属性和方法。子类可以继承父类的公有成员和保护成员,但不能继承私有成员。通过继承,子类可以重用父类的代码,并且可以添加自己的特定功能。继承可以实现代码的重用和层次化的设计。 多态是指同一个函数可以根据不同的对象调用不同的实现。多态可以通过虚函数来实现。虚函数是在基类中声明为虚拟的函数,它可以在派生类中被重写。当通过基类指针或引用调用虚函数时,实际调用的是派生类中的实现。这样可以实现动态绑定,即在运行时确定调用的函数。 虚函数的原理是通过虚函数表来实现的。每个包含虚函数的类都有一个虚函数表,其中存储了虚函数的地址。当调用虚函数时,编译器会根据对象的类型在虚函数表中查找对应的函数地址并调用。 综上所述,C++中的继承多态和虚函数是实现面向对象编程的重要机制,它们可以提高代码的灵活性和可扩展性。 #### 引用[.reference_title] - *1* *3* [C++多态之 虚函数和虚函数表](https://blog.csdn.net/weixin_46053588/article/details/121231465)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v91^control_2,239^v3^insert_chatgpt"}} ] [.reference_item] - *2* [c++多态及虚函数表内部原理实战详解](https://blog.csdn.net/bitcarmanlee/article/details/124830241)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v91^control_2,239^v3^insert_chatgpt"}} ] [.reference_item] [ .reference_list ]

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

bugcoder-9905

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

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

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

打赏作者

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

抵扣说明:

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

余额充值