ATL offsetofclass 的工作原理

 ATL深入浅出[1]

作者:ErIM Creator
日期:2007-7-20
参考:http://www.codeproject.com/atl/atl_underthehood_.asp

在这一系列的文章里,我将和大家一起讨论ATL的底层工作和ATL所使用到的技术。如果你只想尝试写个普通的ATL控件,这篇文章对你一点帮助

都没有;如果你想更好的学会使用ATL,认真往下看把。

我们先来讨论一个类(Class)的存储配置。首先来写一个类,这个类没有任何数据成员,然后来看看他的内存结构。

程序1:
#include <iostream>

using namespace std;

class Class{
};

int main()
{
 Class objClass;
 
 cout << "Size of object is = " << sizeof(Class) << endl;
 cout << "Address of object is = " << &objClass << endl;

 return 0;
}

程序输出:
Size of object is = 1
Address of object is = 0012FF7C

现在,如果我们往类里添加数据成员,他的大小会将等于所有数据成员所占内存的和。在模版类(template class)中,也是这样。现在,让我

们来看看模版类Point。

程序2:
#include <iostream>

using namespace std;

template <typename T>
class CPoint{
 T m_x;
 T m_y;
};

int main()
{
 CPoint<int> objPoint;
 
 cout << "Size of object is = " << sizeof(objPoint) << endl;
 cout << "Address of object is = " << &objPoint << endl;

 return 0;
}
程序输出:
Size of object is = 8
Address of object is = 0012FF78

现在,我们也添加一个继承类到这个程序,我们用Point3D类来继承Point类,同时来看看程序的内存结构。

程序3:
#include <iostream>

using namespace std;

template <typename T>
class CPoint{
 T m_x;
 T m_y;
};

template <typename T>
class CPoint3D : public CPoint<T>
{
 T m_z;
};
int main()
{
 CPoint<int> objPoint;
 
 cout << "Size of Point is = " << sizeof(objPoint) << endl;
 cout << "Address of Point is = " << &objPoint << endl;

 CPoint3D<int> objPoint3D;

 cout << "Size of Point3D is = " << sizeof(objPoint3D) << endl;
 cout << "Address of Point3D is = " << &objPoint3D << endl;
 
 return 0;
}
程序输出:
Size of Point is = 8
Address of Point is = 0012FF78
Size of Point3D is = 12
Address of Point3D is = 0012FF6C

这个程序说明了派生类的内存结构。派生类所占用的内存总数是基类的所有数据成员和该派生类所有数据成员所占内存的和。


如果把虚函数也添加进来,那这个问题就更有趣了。我们来看看这个程序。
程序4:
#include <iostream>

using namespace std;

class Class
{
 virtual void fun()
 {
  cout << "Class::fun" << endl;
 }
};

void main()
{
 Class objClass;
 cout << "size of class = " << sizeof(objClass) << endl;
 cout << "Address of class = " << & objClass << endl;
}
程序输出:
size of class = 4
Address of class = 0012FF7C

如果我们添加一个以上的需函数,那么情况会变得更有去。

程序5:
#include <iostream>

using namespace std;

class Class
{
 virtual void fun1()
 {
  cout << "Class::fun" << endl;
 }
 virtual void fun2()
 {
  cout << "Class::fun2" << endl;
 }
 virtual void fun3()
 {
  cout << "Class::fun3" << endl;
 }
};

void main()
{
 Class objClass;
 cout << "size of class = " << sizeof(objClass) << endl;
 cout << "Address of class = " << & objClass << endl;
}
程序的输出和上面一样。我们来做多点实验,使我们能更好地理解他。

程序6:
#include <iostream>

using namespace std;

class CPoint
{
 int m_x;
 int m_y;
public:
 virtual ~CPoint(){
 };
};

void main()
{
 CPoint objPoint;
 cout << "size of Point = " << sizeof(objPoint) << endl;
 cout << "Address of Point = " << &objPoint << endl;
}

程序输出:
size of Point = 12
Address of Point = 0012FF68

这个程序的输出告诉我们,无论你在类里添加多少个虚函数,他的大小只增加一个int数据类型所占的内存空间,例如在Visual C++ 他增加4字

节。他表示有三块内存给这个类的整数,一个给m_x,一个给m_y,还有一个用来处理被调用的虚函数的虚指针。首先来看一看新的一块内存,也

就是虚函数指针所占内存,在该对象的最开始头(或者最后)。我们可以通过直接访问对象所占的内存块来调用虚函数。只要把对象的地址保

存到一个int类型指针,然后使用指针算法的魔术(The magic of pointer arithmetic)就能够调用他了。

程序7:
#include <iostream>

using namespace std;

class CPoint {
 
public:
 
 int m_ix;
 int m_iy;
 CPoint(const int p_ix = 0, const int p_iy = 0) :
 m_ix(p_ix), m_iy(p_iy) {
 }
 int getX() const {
  return m_ix;
 }
 int getY() const {
  return m_iy;
 }
 virtual ~CPoint() { };
};
int main() {
 
 CPoint objPoint(5, 10);

 int* pInt = (int*)&objPoint;
 *(pInt+0) = 100; // 打算改变 x 的值
 *(pInt+1) = 200; // 打算改变 y 的值

 cout << "X = " << objPoint.getX() << endl;
 cout << "Y = " << objPoint.getY() << endl;

 return 0;
}
这个程序最重要的地方是:
int* pInt = (int*)&objPoint;
*(pInt+0) = 100; // 打算改变 x 的值
*(pInt+1) = 200; // 打算改变 y 的值

这里我们把对象的地址保存到一个int类型的指针,然后把它当成整型指针。
程序输出:
X = 200
Y = 10
当然,这不是我们想要的结果!程序说明,这时的200是保存到x_ix,而不是x_iy。也就是说对象第一个数据成员是从内存的第二个地址开始的

,而不是第一个。换句话说,第一个内存地址保存的是虚函数的地址,然后其他保存的都是类的数据成员。我们来改一下下面两行代码。
int* pInt = (int*)&objPoint;
*(pInt+1) = 100; // 打算改变 x 的值
*(pInt+2) = 200; // 打算改变 y 的值

这时我们得到了所期待的结果。

程序8:
#include <iostream>

using namespace std;

class CPoint {
public:
         int m_ix;
         int m_iy;

         CPoint(const int p_ix = 0, const int p_iy = 0) :
                 m_ix(p_ix), m_iy(p_iy) {
         }

         int getX() const {
                 return m_ix;
         }
         int getY() const {
                 return m_iy;
         }

         virtual ~CPoint() { };

};

int main() {

         CPoint objPoint(5, 10);

         int* pInt = (int*)&objPoint;
         *(pInt+1) = 100; // 想要改变 x 的值
         *(pInt+2) = 200; // 想要改变 y 的值

         cout << "X = " << objPoint.getX() << endl;
         cout << "Y = " << objPoint.getY() << endl;

         return 0;

}

程序的输出:
X = 100
Y = 200

这里很明确的告诉我们,无论什么时候我们添加虚函数到类里面,虚指针都存放在内存的第一个位置。

现在问题出现了:虚指针里存放的是什么?来看看以下程序,我们就能知道它是什么概念。
程序9:
#include <iostream>

using namespace std;

class Class {

         virtual void fun() { cout << "Class::fun" << endl; }

};

int main() {

         Class objClass;

         cout << "Address of virtual pointer " << (int*)(&objClass+0) << endl;
         cout << "Value at virtual pointer " << (int*)*(int*)(&objClass+0) << endl;

         return 0;

}
程序输出:

Address of virtual pointer 0012FF7C
Value at virtual pointer 0046C060

虚指针保存一张叫做虚表的地址。而虚表保存着整个类的虚函数地址。换句话说,虚表是一个存放虚函数地址的数组。让我们看一下下面的程

序来理解这种思想。

程序10:
#include <iostream>

using namespace std;

 

class Class {

         virtual void fun() { cout << "Class::fun" << endl; }

};

 

typedef void (*Fun)(void);

 

int main() {

         Class objClass;

 

         cout << "Address of virtual pointer " << (int*)(&objClass+0) << endl;

         cout << "Value at virtual pointer i.e. Address of virtual table "

                  << (int*)*(int*)(&objClass+0) << endl;

         cout << "Value at first entry of virtual table "

                  << (int*)*(int*)*(int*)(&objClass+0) << endl;

 

         cout << endl << "Executing virtual function" << endl << endl;

         Fun pFun = (Fun)*(int*)*(int*)(&objClass+0);
         pFun();
         return 0;

}
这个程序有些不常用的间接类型转换。这个程序最重要的地方是

Fun pFun = (Fun)*(int*)*(int*)(&objClass+0);

这里 Fun 是一个typeded 函数指针。

typdef void (*Fun)(void):

我们来详细研究这种不常用的间接转换。
(int*)(&objClass+0)给出类第一个入口的虚函数的指针然后把他类型转换成int*.我们使用间接操作符(也就是 *)来获取这个地址的值然后再

次类型转换成 int* 也就是(int*)*(int*)(&objClass+0).这将会给出虚函数地址表的入口地址.获得这个位置的值,也就是得到类的第一个虚

函数的指针,再次使用间接操作符再类型转换成相应的函数指针类型。像这样:
Fun pFun = (Fun)*(int*)*(int*)(&objClass+0);
表示获取虚函数地址表第一个入口的值然后类换成Fun类型后保存到pFun.

添加多一个虚函数到类里面会怎么样。我们现在想要访问虚函数地址表里第二个成员。查看以下程序,看看虚函数地址表里的值。

程序11:
#include <iostream>

using namespace std;
class Class {
         virtual void f() { cout << "Class::f" << endl; }
         virtual void g() { cout << "Class::g" << endl; }
};

int main() {

         Class objClass;
         cout << "Address of virtual pointer " << (int*)(&objClass+0) << endl;
         cout << "Value at virtual pointer i.e. Address of virtual table "
                 << (int*)*(int*)(&objClass+0) << endl;

         cout << endl << "Information about VTable" << endl << endl;
         cout << "Value at 1st entry of VTable "
                 << (int*)*((int*)*(int*)(&objClass+0)+0) << endl;

         cout << "Value at 2nd entry of VTable "
                 << (int*)*((int*)*(int*)(&objClass+0)+1) << endl;

         return 0;
}
程序输出:
Address of virtual pointer 0012FF7C
Value at virtual pointer i.e. Address of virtual table 0046C0EC
Information about VTable
Value at 1st entry of VTable 0040100A
Value at 2nd entry of VTable 0040129E

现在我们心理产生一个问题。编译器怎么知道虚函数地址表的长度呢?答案是:虚函数地址表最后的入口等于NULL。把程序做些小改动来理解

它。

程序12:
#include <iostream>

using namespace std;

class Class {
         virtual void f() { cout << "Class::f" << endl; }
         virtual void g() { cout << "Class::g" << endl; }
};

int main() {
         Class objClass;

         cout << "Address of virtual pointer " << (int*)(&objClass+0) << endl;
         cout << "Value at virtual pointer i.e. Address of virtual table "
                  << (int*)*(int*)(&objClass+0) << endl;
         cout << endl << "Information about VTable" << endl << endl;
         cout << "Value at 1st entry of VTable "
                  << (int*)*((int*)*(int*)(&objClass+0)+0) << endl;
         cout << "Value at 2nd entry of VTable "
                  << (int*)*((int*)*(int*)(&objClass+0)+1) << endl;
         cout << "Value at 3rd entry of VTable "
                  << (int*)*((int*)*(int*)(&objClass+0)+2) << endl;
         cout << "Value at 4th entry of VTable "
                  << (int*)*((int*)*(int*)(&objClass+0)+3) << endl;
         return 0;
}

程序输出:
Address of virtual pointer 0012FF7C
Value at virtual pointer i.e. Address of virtual table 0046C134

Information about VTable

Value at 1st entry of VTable 0040100A
Value at 2nd entry of VTable 0040129E
Value at 3rd entry of VTable 00000000
Value at 4th entry of VTable 73616C43

这个程序的输出显示了虚函数地址表的最后一个入口等于NULL.我们用我们所了解的知识来调用虚函数

程序13:
#include <iostream>

using namespace std;

class Class {
         virtual void f() { cout << "Class::f" << endl; }
         virtual void g() { cout << "Class::g" << endl; }
};

typedef void(*Fun)(void);

int main() {

         Class objClass;

         Fun pFun = NULL;

         // calling 1st virtual function

         pFun = (Fun)*((int*)*(int*)(&objClass+0)+0);

         pFun();

         // calling 2nd virtual function

         pFun = (Fun)*((int*)*(int*)(&objClass+0)+1);

         pFun();

         return 0;
}

这个程序的输出是:

Class::f
Class::g

现在我们来看一下多层继承的情况。以下是个简单的多层继承的情况。

程序14:
#include <iostream>
using namespace std;
class Base1 {
public:
         virtual void f() { }

};
class Base2 {
public:
         virtual void f() { }
};
class Base3 {
public:
         virtual void f() { }
};
class Drive : public Base1, public Base2, public Base3 {
};
int main() {

         Drive objDrive;
         cout << "Size is = " << sizeof(objDrive) << endl;

         return 0;
}

程序输出:
Size is = 12

程序表明,当你的 drive 类具有一个以上的基类时,drive 类就具有所有基类的虚函数指针。
如果 drive 类也具有虚函数那会怎么样呢。我们来看看这个程序以便更好的了解多继承虚函数的概念。

程序15:
#include <iostream>

using namespace std;

 

class Base1 {

         virtual void f() { cout << "Base1::f" << endl; }

         virtual void g() { cout << "Base1::g" << endl; }

};

 

class Base2 {

         virtual void f() { cout << "Base2::f" << endl; }

         virtual void g() { cout << "Base2::g" << endl; }

};

 

class Base3 {

         virtual void f() { cout << "Base3::f" << endl; }

         virtual void g() { cout << "Base3::g" << endl; }

};

 

class Drive : public Base1, public Base2, public Base3 {

public:

         virtual void fd() { cout << "Drive::fd" << endl; }

         virtual void gd() { cout << "Drive::gd" << endl; }

};

 

typedef void(*Fun)(void);

 

int main() {

         Drive objDrive;

 

         Fun pFun = NULL;

 

         // calling 1st virtual function of Base1

         pFun = (Fun)*((int*)*(int*)((int*)&objDrive+0)+0);

         pFun();

        

         // calling 2nd virtual function of Base1

         pFun = (Fun)*((int*)*(int*)((int*)&objDrive+0)+1);

         pFun();

 

         // calling 1st virtual function of Base2

         pFun = (Fun)*((int*)*(int*)((int*)&objDrive+1)+0);

         pFun();

 

         // calling 2nd virtual function of Base2

         pFun = (Fun)*((int*)*(int*)((int*)&objDrive+1)+1);

         pFun();

 

         // calling 1st virtual function of Base3

         pFun = (Fun)*((int*)*(int*)((int*)&objDrive+2)+0);

         pFun();

 

         // calling 2nd virtual function of Base3

         pFun = (Fun)*((int*)*(int*)((int*)&objDrive+2)+1);

         pFun();

 

         // calling 1st virtual function of Drive

         pFun = (Fun)*((int*)*(int*)((int*)&objDrive+0)+2);

         pFun();

 

         // calling 2nd virtual function of Drive

         pFun = (Fun)*((int*)*(int*)((int*)&objDrive+0)+3);

         pFun();

 

         return 0;

}

程序输出:
Base1::f

Base1::g

Base2::f

Base2::f

Base3::f

Base3::f

Drive::fd

Drive::gd

这个程序显示 drive 的虚函数保存在 vptr 的第一个 虚函数地址表.
我们在 static_cast 的帮助下我门能够获取 Drive 类 vptr 的偏移量.我们看一下以下的程序来更好地理解他。

程序16:
#include <iostream>

using namespace std;

 

class Base1 {

public:

         virtual void f() { }

};

 

class Base2 {

public:

         virtual void f() { }

};

 

class Base3 {

public:

         virtual void f() { }

};

 

class Drive : public Base1, public Base2, public Base3 {

};

 

// any non zero value because multiply zero with any no is zero

#define SOME_VALUE        1

 

int main() {

         cout << (DWORD)static_cast<Base1*>((Drive*)SOME_VALUE)-SOME_VALUE << endl;

         cout << (DWORD)static_cast<Base2*>((Drive*)SOME_VALUE)-SOME_VALUE << endl;

         cout << (DWORD)static_cast<Base3*>((Drive*)SOME_VALUE)-SOME_VALUE << endl;

         return 0;

}

ATL 使用一个叫做offsetofclass 的宏来这么做,该宏定义在 ATLDEF.h 里。宏的定义是:
#define offsetofclass(base, derived) /
       ((DWORD)(static_cast<base*>((derived*)_ATL_PACKING))-_ATL_PACKING)
这个宏返回在 drive 类对象模型里的基类的vptr的偏移量。我们来看一个例子了解这个概念。
程序17:
#include <windows.h>

#include <iostream>

using namespace std;

 

class Base1 {

public:

         virtual void f() { }

};

 

class Base2 {

public:

         virtual void f() { }

};

 

class Base3 {

public:

         virtual void f() { }

};

 

class Drive : public Base1, public Base2, public Base3 {

};

 

#define _ATL_PACKING 8

 

#define offsetofclass(base, derived) /

         ((DWORD)(static_cast<base*>((derived*)_ATL_PACKING))-_ATL_PACKING)

 

int main() {

         cout << offsetofclass(Base1, Drive) << endl;

         cout << offsetofclass(Base2, Drive) << endl;

         cout << offsetofclass(Base3, Drive) << endl;

         return 0;

}

这是 drive 类的内存层次
程序输出是:
0
4
8
程序的输出显示,这个宏返回了所要的基类vptr的偏移量。在 Don Box 的《Essential COM》,他使用一个类似的宏来这么做。把程序做些小改动来用Box的宏取代ATL 的宏。
程序18:
#include <windows.h>

#include <iostream>

using namespace std;

 

class Base1 {

public:

         virtual void f() { }

};

 

class Base2 {

public:

         virtual void f() { }

};

 

class Base3 {

public:

         virtual void f() { }

};

 

class Drive : public Base1, public Base2, public Base3 {

};

 

#define BASE_OFFSET(ClassName, BaseName) /

         (DWORD(static_cast<BaseName*>(reinterpret_cast<ClassName*>/

         (0x10000000))) - 0x10000000)

 

int main() {

         cout << BASE_OFFSET(Drive, Base1) << endl;

         cout << BASE_OFFSET(Drive, Base2) << endl;

         cout << BASE_OFFSET(Drive, Base3) << endl;

         return 0;

}

程序的目的很输出和前面的程序一样。
我们用这个宏在我们的程序做些实用的事。事实上,我们可以通过获取在 drive的内存结构里基类的vptr来调用所要的基类的虚函数。

程序19:
#include <windows.h>

#include <iostream>

using namespace std;

 

class Base1 {

public:

         virtual void f() { cout << "Base1::f()" << endl; }

};

 

class Base2 {

public:

         virtual void f() { cout << "Base2::f()" << endl; }

};

 

class Base3 {

public:

         virtual void f() { cout << "Base3::f()" << endl; }

};

 

class Drive : public Base1, public Base2, public Base3 {

};

 

#define _ATL_PACKING 8

 

#define offsetofclass(base, derived) /

         ((DWORD)(static_cast<base*>((derived*)_ATL_PACKING))-_ATL_PACKING)

 

int main() {

         Drive d;

 

         void* pVoid = NULL;

 

         // call function of Base1

         pVoid = (char*)&d + offsetofclass(Base1, Drive);

         ((Base1*)(pVoid))->f();

 

         // call function of Base2

         pVoid = (char*)&d + offsetofclass(Base2, Drive);

         ((Base2*)(pVoid))->f();

 

         // call function of Base1

         pVoid = (char*)&d + offsetofclass(Base3, Drive);

         ((Base3*)(pVoid))->f();

 

         return 0;

}

这个程序的输出:

Base1::f()

Base2::f()

Base3::f()

在这个指南里,我设法解释 ATL 里 offsetofclass 宏的工作原理。我希望在下一篇文章探测其他神秘的ATL。

内容概要:本文档详细介绍了基于布谷鸟搜索算法(CSO)优化长短期记忆网络(LSTM)进行时间序列预测的项目实例。项目旨在通过CSO自动优化LSTM的超参数,提升预测精度和模型稳定性,降低人工调参成本。文档涵盖了项目背景、目标与意义、挑战及解决方案、模型架构、代码实现、应用领域、注意事项、部署与应用、未来改进方向及总结。特别强调了CSO与LSTM结合的优势,如高效全局搜索、快速收敛、增强泛化能力等,并展示了项目在金融、气象、能源等多个领域的应用潜力。 适合人群:具备一定编程基础,特别是对MATLAB有一定了解的研发人员和技术爱好者。 使用场景及目标:①提高时间序列预测精度,减少误差;②降低人工调参的时间成本;③增强模型泛化能力,确保对未来数据的良好适应性;④拓展时间序列预测的应用范围,如金融市场预测、气象变化监测、工业设备故障预警等;⑤推动群体智能优化算法与深度学习的融合,探索复杂非线性系统的建模路径;⑥提升模型训练效率与稳定性,增强实际应用的可操作性。 阅读建议:此资源不仅包含详细的代码实现,还涉及模型设计、优化策略、结果评估等内容,因此建议读者在学习过程中结合理论知识与实践操作,逐步理解CSO与LSTM的工作原理及其在时间序列预测中的应用。此外,读者还可以通过多次实验验证模型的稳定性和可靠性,探索不同参数组合对预测效果的影响。
内容概要:本文详细介绍了ArkUI框架及其核心组件Button在鸿蒙应用开发中的重要性。ArkUI框架作为鸿蒙系统应用界面的核心开发工具,提供了简洁自然的UI信息语法、多维状态管理和实时界面预览功能,支持多种布局方式和强大的绘制能力,满足了现代应用开发对于简洁性、高效性和灵活性的要求。Button组件作为ArkUI框架的重要组成部分,通过绑定onClick事件,实现了从简单的数据操作到复杂的业务流程处理,从页面间的无缝导航到各类功能的高效触发。此外,文章还探讨了Button组件在未来智能化、交互体验多样化以及跨设备应用中的潜力和发展趋势。 适合人群:具备一定编程基础,尤其是对鸿蒙应用开发感兴趣的开发人员和设计师。 使用场景及目标:①理解ArkUI框架的基本特性和优势;②掌握Button组件的使用方法,包括基本绑定、复杂逻辑处理和事件传参;③熟悉Button组件在表单提交、页面导航和功能触发等场景下的具体应用;④展望Button组件在智能化、虚拟现实、增强现实和物联网等新兴技术中的未来发展。 阅读建议:由于本文内容涵盖了从基础概念到高级应用的广泛主题,建议读者先了解ArkUI框架的基本特性,再逐步深入学习Button组件的具体使用方法。同时,结合实际案例进行实践操作,有助于更好地理解和掌握相关知识。
资源下载链接为: https://pan.quark.cn/s/d3128e15f681 罗技MX Master 2S是一款高端无线鼠标,凭借其卓越的性能和舒适性,深受专业设计师、程序员以及需要长时间使用鼠标的人群的喜爱。它在macOS平台上表现出色,功能丰富。而“LogiMgr Installer 8.20.233.zip”是该鼠标在macOS系统上对应的软件安装程序,版本号为8.20.233,主要功能如下: 驱动安装:该安装包可确保MX Master 2S在macOS系统中被正确识别和配置,发挥出最佳硬件性能,同时保证良好的兼容性。它会安装必要的驱动程序,从而启用鼠标的高级功能。 自定义设置:借助此软件,用户能够根据自己的工作习惯,对MX Master 2S的各个按钮和滚轮功能进行自定义。比如设置特定快捷键、调整滚动速度和方向等,以满足个性化需求。 Flow功能:罗技Flow是一项创新技术,允许用户在多台设备间无缝切换。只需在软件中完成设备配置,鼠标就能在不同电脑之间进行复制、粘贴操作,从而大幅提升工作效率。 电池管理:软件具备电池状态监控功能,可帮助用户实时了解MX Master 2S的电量情况,并及时提醒用户充电,避免因电量不足而影响工作。 手势控制:MX Master 2S配备独特的侧边滚轮和拇指按钮,用户可通过软件定义这些手势,实现诸如浏览页面、切换应用等操作,进一步提升使用便捷性。 兼容性优化:罗技的软件会定期更新,以适应macOS系统的最新变化,确保软件与操作系统始终保持良好的兼容性,保障鼠标在不同系统版本下都能稳定运行。 设备配对:对于拥有多个罗技设备的用户,该软件能够方便地管理和配对这些设备,实现快速切换,满足多设备使用场景下的需求。 在安装“LogiMgr Installer 8.20.233.app”时,用户需确保macOS系统满足软件的最低要求,并
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值