C++基本功和 Design Pattern系列(7-9)

======================================================
大家请把我的文章当参考,详细内容 还请参照 权威书籍如果文中
有错误和遗漏, 请指出,Aear会尽力更正, 谢谢!
Aear Blog: http://blog.sina.com.cn/u/1261532101
======================================================

不知不觉已经写了6章。今天的内容是Leaf Class Design, 是讲的非继承类的设计。基本上是前面6章内容的综合运用。

所谓的Leaf Class, 就是在类层次中最低下一层,不能被其他任何的类所继承的。在Java中可以用final来表示。实际上这种类型的class又可以叫做singleton class. 为了简化这一章的内容,我们不考虑template的情况,以及不考虑从其他的类继承而得到的leaf class的情况,这样就不用考虑overloading和 virtual的问题。另外,inline的使用在不在今天的文章范围内。关于template,inline和overloading,将在以后的内容中介绍。

==================== Design Principle ====================


设计一个class,有一个最主要的宗旨,那就是:容易使用,而不容易犯错误。

相信大部分的的programmer都是从独自编码开始的,这样就造成了一个问题:因为对于所有的实现细节都非常熟悉,所以在设计上也不太关心是不是有防止其他使用者犯错误的机制。因此,根据Aear的个人经验,在设计class的时候,要假设这个class的使用者不是你自己。实际上在真正的团队开发中,你永远无法估计别人如何使用你的类。你只有尽量的使用良好的设计来防止别人错误的使用你的代码。

==================== Access Control ====================

对于类的访问控制,由3个关键字来管理: public, protected, and private。下面是public, protected, and private 的作用范围:

public: 可以被任何人访问
protected: 可以被继承类,继承类的friends,以及类成员和friends访问
private: 只可以被类成员和friends访问

这里值得注意的是protected和private区别仅仅在继承类上,由于我们讨论的是Leaf Class, Leaf class 不会被其他类继承。因此,Leaf Class 不需要protected. 所以我们讨论的范围缩小到 public 和 private.

关于public其实很简单,就是谁都可以访问,相当于structure,这里就不详细说明了。关于private有必要说下。首先,private起到的是访问保护, 而不是可见行保护. 也就是说,一个private member对程序来说是可见的,但是是不可访问的。举个简单的例子:

class Test {
public:
    Test(double i);
private:
    Test(float i);
}

Test k(1.0f); // Error, Test(float i) is private

对于Test有2个constructor,其中一个public,一个private,当我们运行k(1.0f)的时候,compiler并不直接去进行类型转换把float装成double,而是尝试调用Test(float i),为什么呢? 因此compiler在编译的时候,先会进行overload resolution,根据k(1.0f)的结构,使用的function是Test(float i)。在此之后,compiler才会进行access check. 这样很容易造成让人迷惑的情况,所以:不要把相同的overloaded methods 放在不同的访问控制域中

其次, private只是在编译的过程中进行访问控制检查,并无法在程序运行中进行控制。因此,即使是private member,也能够通过对内存操作进行修改。所以在写程序的时候,尽量不要做类似的操作,这样很容易破坏数据的一致性。

下面让我们来看看什么需要放在public里,什么需要放在private里。首先要说明的是,所有的data member,也就是attribute,需要放在private中。举个例子来说明这么做的重要性:

class String {
public:
    ...
    char * pStrBuf;
private:
    int StrLength;   

}

String TempStr;

TempStr. pStrBuf = "123123";

这里我们可以访问pStrBuf并改变其内容,但是数据的一致性就破坏了。为了有效的保护private中的数据,还需要注意以下2条:




1. 不要在类的method中返回private member的非const reference



2. 不要在类的method中返回private member的非const pointer

这样就基本上能保证client无法通过正常的途径破坏class内部数据的一致性了。

==================== 防止被继承 ====================

在Java中,我们可以通过Final来使一个class无法被其他class继承,但是C++中不提供final的关键字,我们可以通过其他途径来防止类被继承。

首先,在写类的时候,使用non-virtual destructor,就是明确的告诉使用者,这个类是不应该被继承的。当然,别人也可以继承你的类,但是如果你只有public和private两种access control,继承你的类和不继承你的类没有大的区别。这就是为什么leaf class不需要protected member的原因。如果你有protected member,那么别人可以通过继承你的类,来进行一些你不希望的操作,从而破坏内部数据的一致性。

但是,我们也有另外一种类方法,来硬性的使得继承类是非法的。其代码如下:

class Test {
public:
       static Test* CreateInstance() { return new Test(); };
       ~Test() {};

private:
       Test() {};
       Test(const Test &);
       Test& operator= (const Test &);
};
我们看到Test() 是private,因此,如果我们编译下列代码,会得到错误:

       Test P; // Error, private constructor cannot be called

同样,如果我们从Test Derive一个class出来,也会发生错误。在这个类声明里,需要注意的是Destructor ~Test()是public,因为我们通过CreateInstance new 一个Test(),需要通过client来delete掉它,要不就会发生memory leakage.所以下面的代码是合法的:
     
       Test * TempObj = Test::CreateInstance();
       delete TempObj;

使用CreateInstance有很多好处,比如防止被继承,而且可以有效的控制instance的数目,在优化的时候还可以使用object pool从而提高速度,等等。在Test里,copy constructor和operator= 一般是要放在private里边的,这样可以防止很多不通过CreateInstance创建Test的途径,比如:

       Test * TempObj = Test::CreateInstance();
       Test P(*TempObj);          // Error
       Test P = *TempObj;        // Error

==================== Explicit Constructor ====================

关于Explicit Type Conversion和Implicit Type Conversion已经在前几章讲过了,对于Class的constructor来说,只要有可能引起implicit type conversion的constructor,在大部分情况下,都应该使用explicit关键字,从而避免implicit type conversion引起的麻烦。比如:

class Test {
public:
    Test();
    explicit Test(int i);
    explicit Test(float k);
    explicit Test(int i, float k = 20.5);
    explicit Test(int i = 10, float k = 20.5);
};

上面是几种比较常见的情况,其他的情况需要大家自己判断决定。

==================== Operator Overloading ====================

对于大多数的leaf class来说,只有operator overloading能成为Class的friends。 通常为了效率考虑,下列的operator都是可以通过friend来实现:

+ - * / 等2元operator

几乎所有的一元operator都应该是member function.

如果data member中有pointer,那么就必须提供 copy constructor和 operator=.

==================== const & reference ====================

正如第一章所讲到的,尽量使用const 来保证你的类的使用者不犯错误,尽量使用reference来提高程序的效率。

==================== static data member ====================

如果使用了static data member, 那么需要注意的是,尽量附初值,并且尽量不要在constructor和初始化列表(initialize list)中使用,因为C++ standard并没有规定static data member应该在什么时候被初始化。

最后祝愿大家过的愉快,有空来我的Blog坐坐: http://blog.sina.com.cn/u/1261532101, 下次见!

 

 

 

======================================================
大家请把我的文章当参考,详细内容 还请参照 权威书
籍如果文中有错误和遗漏, 请指出,Aear会尽力更正,
谢谢!
Aear Blog: http://blog.sina.com.cn/u/1261532101
======================================================

今天讲的是inline. 其实大家都知道,inline并不是c++独有的特性。其实Aear的个人观点,inline根本就不应该是语言标准的一部分。因为inline的具体实现,跟compiler有很大的关系,把inline说成compiler的一个开关更合适一些。今天我们就说下在什么情况下可以使用inline, 使用inline的好处和坏处。

================= Inline能做什么 =================

关于inline能做什么,什么原理,相信很多人都知道。Aear再在这里重复一下:

1. inline能够去掉function call的overhead
2. inline能够帮助函数调用时候常量参数进行优化

举个例子来说明一:

void function(void)
{

}

function();

在调用function()前,一般会进行下面的操作(根据操作系统和compiler来决定,并不一定按照这个顺序):
    保存IP FP SP等寄存器的值(压栈)
    把参数压栈
    建立新的 IP FP SP等以便进行函数调用
    函数有可能还需要保存一些寄存器数据到内存。

在完成function()调用后,也会进行一系列的操作:
    如果有返回值,一般在一个寄存器中,需要进行一系列的处理工作
    恢复IP FP SP等保存的寄存器值。

整个过程大约需要花费20-150个CPU clock cycle.具体要根据参数的多少,还有函数的具体实现来决定。使用inline function,可以去掉这些函数调用前和调用后的工作,从而节省CPU clock cycle,提高程序的运行效率。

关于2的情况,例子如下:
    float test = cos(0);

如果cos不是inline,compiler有可能会生成cos()的调用代码,计算cos(0),然后返回给test.如果cos被inline了,由于cos(0) == 1,所以经过优化的代码可是直接生成test = 1.

================= Inline 坏处 =================

Aear个人认为,inline只在特定的情况下起到正面的作用,在大部分情况下,起到负面的作用,也就是说,坏处多过好处。使用inline的缺点包括:

1. 不容易debug,并不是所有的debugger都支持在inline function中设置断点,不过幸运的是,VS .net支持。

2. 很容易增加代码的长度,在很多情况下,代码长度的增加直接导致了代码执行速度的减慢,特别是在循环中,因为整个循环的长度超过了cache的容量,从而使得cache miss的几率增大

3. 使得编译的时间过长。如果一个inline function被更改,所有调用这个程序的代码都要重新编译。对于一般的游戏代码,整个编译时间可能会有1,2个小时。

4. 暴露了代码实现细节。由于inline function在大多数情况下必须在header file中定义(不过目前由于compiler越来越好,也可以在.cpp中定义inline了),所以导致使用者能够看到你的代码实现细节。

Inline并不是一个好东西,也不是一个万能药,在很多时候,它会给你带来无穷无尽的麻烦。所以在inline的时候,最好慎重慎重。

================= 如何使用 Inline =================

下面给出几种inline的用法。由于个人的代码结构,平台,实现细节的不同,请参考相关资料自行决定.

========= Platform Inline =========
首先要指出的是,inline并不是一条命令,而是对compiler的建议。compiler会具体分析你的代码,如果合适inline,才会进行inline的工作。在VS .net下,一共有3个关于inline的定义:

inline
__inline (__inline__ for GCC)
__forceinline

inline和__inline相同, 只有compiler认为inline合适,才会进行。__forceinline是强制compiler进行inline. 比如:

__forceinline void f(void);

如果你使用了inline或者__inline, 并且打开了编译器的 /Ob开关,那么compiler就会告诉你一个function是不是被真正的inline了。

========= Macro Inline =========

使用一个macro来控制inline,我们可以做如下定义:

#ifdef _DEBUG
#define COND_INLINE
#else
#define COND_INLINE inline
#endif

// inline function declaration
COND_INLINE function();

这样我们在debug模式下,就不会有inline带来的烦恼了。只有在release version,编译器才会进行真正的inline.


========= Inline Condition =========

一般来讲,根据程序员主观感觉inline得到的很有可能就是负面的效果。下面的一些情况,能够帮大家决定是否inline一个函数:

1. 使用profiler分析的结果告诉你需要inline一个function。 这个是最有把握的。windows下比较流行的profiler是vTune

2. class的属性访问函数(Accessor Methods)。一般来讲,这些是可以inline的,而且很多compiler都会帮你自动inline这些函数。

3. 小函数。这个需要举个例子说明,代码如下:

int Cal(int x, int y, int z)
{
    return x+y-z;
}

我们可以看到 Cal的代码很少,就是计算 x+y-Z,最多也就3,4条汇编代码。但是如果不inline, Cal函数的调用和返回过程所产生的代码,都比cal实际运行的代码多。因此Cal最好inline,通过消除Cal的调用和返回的代码,可以减少可执行程序的大小,同时加快运行的速度。

因此,一般来讲,5行内逻辑结构简单,没有if else的代码,可以比较放心的inline,大过5行的代码,就需要仔细的斟酌了。

================= 不能Inline的情况 =================

1. 包含Static Member的function. 很多compiler不能inline有static member的function.或者即使inline了,也会生成错误的代码(把static当成local处理)。所以最好先测试下compiler支持不支持static inline.测试代码可以这么写:

inline void f(void)
{
    static int k = 1;
    k++;
    std::cout<<k<<endl;
}

f();
f();

2. virtual member function.  Virtual Member Function在很多情况下是不能被inline的(根据compiler而定),因为具体virtual的哪个版本被调用,要在运行时候才能决定。不过我们可以通过创建一个新的可以inline的non-virtual member function,来提高运行速度。


================= 结论 =================

总的来说,inline的坏处多过好处,请大家inline的时候,慎重慎重再慎重,并不是所有的函数都适合inline的。

 

 

======================================================
大家请把我的文章当参考,详细内容 还请参照 权威书
籍如果文中有错误和遗漏, 请指出,Aear会尽力更正,
谢谢!
Aear Blog: http://blog.sina.com.cn/u/1261532101
======================================================

关于virtual 一般有3种用法: virtual function, pure virtual function, 和 virtual inheritance. 今天就分别来讲讲这几种virtual.

================ virtual function ================
virtual函数是在类里边的一种特殊的函数,至于具体的含义,相信大家都知道,我就不多说了。而virtual最基本的思想,就是OO语言中多态的体现。把函数从编译时的棒定,转移到运行时的动态绑定。

virtual的好处很多,比如能使程序结构更加清晰,代码的重复利用率更高, 但是也是有代价的。让我们来看下代码:

class Test {
    ...
    virtual void VirtualFunc(void) { cout<<"Test1"<<endl; };
};

class Test2 : public Test {
    ...
    virtual void VirtualFunc(void) { cout<<"Test2"<<endl; };
};

Test * pTest = new Test2();
pTest->VirtualFunc();

上面代码的输出结果是"Test2"。也就是说,虽然你是通过Test * 进行的调用,但真正执行的代码是你创建的Test2的VirtualFunc. 这样的效果是通过在编译的时候创建virtual pointer: _vptr 和 virtual table: _vtbl 来实现的(大部分编译器的实现是通过_vptr & _vtbl)。对于每个有virtual function的class,compiler创建一个_vtbl,用来记录所有的virtual function的地址。同时在所有这个class的instance(实例)里,都有一个_vptr,指向_vtbl。因此, pTest->VirtualFunc() 这段代码等同于:

    (*pTest->_vptr[X])()

其中X是VirtualFunc在_vtbl中对应的位置。(其实对于不同的compiler,都有不同的实现,不同的类层次结构,_vptr和_vtbl的数目也不一定相同。)

从上面的代码考虑,virtual 和 non-virtual的class对比,在性能上有3个方面受到影响:
    1. _vptr是在constructor中由compiler生成的代码隐性的初始化,占用了一定的时间。
    2. virtual function是通过指针间接调用的,比直接调用需要更多的时间,而且在有的情况下会导致CPU的指令缓存失效,从而浪费更多的时间。
    3. 由于virtual function是在运行时进行动态绑定的,所以无法进行inline.

对于第一条,如果是使用 virtual function,那是无可避免的,对于第2条,有通过显式的调用virtual function来提高速度,代码如下:

class Test2 : public Test {
    ...
    virtual void VirtualFunc(void) { cout<<"Test2"<<endl; };
    void VirtualFuncFast(void) { Test2::VirtualFunc(); };
};

由于Test2::VirtualFunc指定了调用函数的版本,所以是在编译时候就绑定了,免去了指针的间接调用过程,而且VirtualFuncFast本身也是可以inline的。当然了,这样做也是有坏处的,就是VirtualFuncFast本身是Test2的函数,而不是Test的,所以不能通过Test的指针掉用。因此只有在确定是Test2的情况下,通过static_cast才能掉用VirtualFuncFast.

同理,在一个virtual function里调用另外一个virtual function的时候,使用显试的调用也能提高一定的速度,比如:

class Test2 : public Test {
    ...
    virtual void VirtualFunc(void) { cout<<"Test2"<<endl; };
    virtual void VirtualFunc2(void) { Test2::VirtualFunc(); };
};


对于第3种情况,我们可以通过用Template代替Virtual Function来获得性能上的提高,举个简单的例子:

========== virtual 实现 ==========
class Base{
public:
    virtual void VirtualFunc(void);
};

class Derive1 : public Base{
public:
    virtual void VirtualFunc(void);
};

class Derive2 : public Base{
public:
    virtual void VirtualFunc(void);
};

class SomeClass {
public:
    void test(Base * pBase) { pBase->VirtualFunc(); };
};

// 用法
Test * pTest1 = new Derive1();
Test * pTest2 = new Derive2();
SomeClass Temp1.test(pTest1);
SomeClass Temp2.test(pTest2);

========== 对应的 Template 实现 ==========

class D1{
public:
    // inline here
    void VirtualFunc(void) {};
};

class D2{
public:
    // inline here
    void VirtualFunc(void) {};
};

template <class DCLASS>
class SomeClass {
public:
    // inline here
    void test( void ) { Temp.TestVirtualFunc(); };
private:
    DCLASS Temp;
};

// 用法
SomeClass <D1> Temp1;
SomeClass <D2> Temp2;
Temp1.test();
Temp2.test();


========== 如何选择 ==========

对于到底是使用 Virtual ,还是使用Template或者Virtual的优化形式, 在大多数情况下不难做出决定。具体选择的过程,只需要问问自己到底是程序的速度重要,还是其他重要,也就是,要在: 速度,程序结构,灵活性,易用易维护性,代码的复杂度,代码大小 这些中间做出选择。

如果速度排第一位,那么就使用template或者优化,如果其他排在速度的前面,应该尽量的使用virtual。


================ pure virtual function ================

pure virtual function主要是用在abstract class里。有pure virtual function的class,就是abstract class,是不能被实例化的。因此,pure virtual function的代码只有在指定的情况下才能被调用。 例如:

class Test {
    virtual void VirtualFunc(void) = 0 { cout<<"Test"<<endl; };
};


class Test2 : public Test {
    virtual void VirtualFunc(void) { Test::VirtualFunc(); };
};

================ virtual inheritance ================

其实Virtual Inheritance只有可能在Multiple Inheritance中使用。对于Muliple Inheritance, Aear是坚决反对反对再反对的。Aear个人认为,single inheritance是足够用的,Java就是最好的例子。 MI实在是太让人头疼了,所以Aear不会在这个系列中讲MI,这里只说说 Virtual Inheritance和普通的Inheritance的区别。

class Base {
    virtual ~Base();
};

class Derived1 : public Base {
};

class Derived2 : virtual public Base {
};

这里:

sizeof(Derived1) == 4
sizeof(Derived2) == 8

sizeof(Derived1) == 4 是因为 Derived1继承 Base以后,使用Derived1的 _vptr

sizeof(Derived1) == 8 是因为由于在所有的vritual inheritance里,Base作为基类在内存中只能出现一次。所以必须把Base和Derived2的附加部分单独对待。因此有2个_vptr,一个是Base的,一个是Derived2的。

virtual inheritance的直接结果就是大大增加了指令缓存失效的可能性,同时降低了类内部数据的访问速度。因为Base 和 Derived2的内部数据不再是放在连续的内存中了。如果virtual inheritance的层次越多,对运行速度的影响就越大。

所以Aear在这里极度不推荐MI和Virtual Inheritance.

================ virtual 的一些用法 ================
下面是virtual的一些用法。

========== virtual destructor ==========

这个比较简单,就是所有的Base Class,都应该是 public virtual destructor 或者是protected non-virtual destructor,例如:

class Base {
public:
    virtual ~Base();
};

或者

class Base {
protected:
    ~Base();
};

========== virtual constructor ==========

其实本没有virtual constructor,不过C++中有特殊的实现,实现类似virtual constructor 和 virtual copy constructor的。代码如下:

class Base {
    virtual Base * construct (void) { return new Base(); };
    virtual Base * clone(void) { return new Base(*this); };
};

class Derived : public Base {
    virtual Base * construct (void) { return new Derived(); };
    virtual Base * clone(void) { return new Derived(*this); };
};

========== pure virtual destructor ==========

如果我们想设置base class 为abstract class,而又只有一个destructor作为唯一的成员函数,可以把它设成为pure virtual

class Base {
public:
    virtual ~Base() = 0;
};

========== protected & private virtual ==========

对于virtual function是不是应该放在public里边,学术结论和现实中的代码往往不能统一。虽然很多人认为virtual function 不应该放在public里边,但是这样的设计经常会造成编码中的不方便,

实际上对与Java来说,protected 或 private 所有 virtual function,几乎是不太可能的。因此,Aear个人观点,是不是要protected & private virtual function,要根据情况而定。下面是个private virtual的例子:

class Base {
public:
    // this is interface
    void print(void) { output() };
private:
    virtual void output (void) { cout<<"Base"<<endl; };  
};

实际上这样的处理,要比public virtual速度上慢点。

========== 一些要点 ==========

1. 不建议使用MI (Mission Impossible?), 在大多数情况下,它引入的问题,要比它解决的问题多的多的多。
2. 类层次结构越多,类成员和函数的访问效率越低。
3. Virtual Inheritance的类成员访问效率要低于 Non-virtual Inheritance.

今天说了好多东西,下次见!祝大家过的愉快。

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
目 录 序言 前言 读者指南 第1章 引言 1 1.1 什么是设计模式 2 1.2 Smalltalk MVC中的设计模式 3 1.3 描述设计模式 4 1.4 设计模式的编目 5 1.5 组织编目 7 1.6 设计模式怎样解决设计问题 8 1.6.1 寻找合适的对象 8 1.6.2 决定对象的粒度 9 1.6.3 指定对象接口 9 1.6.4 描述对象的实现 10 1.6.5 运用复用机制 13 1.6.6 关联运行时刻和编译时刻的 结构 15 1.6.7 设计应支持变化 16 1.7 怎样选择设计模式 19 1.8 怎样使用设计模式 20 第2章 实例研究:设计一个文档编 辑器 22 2.1 设计问题 23 2.2 文档结构 23 2.2.1 递归组合 24 2.2.2 图元 25 2.2.3 组合模式 27 2.3 格式化 27 2.3.1 封装格式化算法 27 2.3.2 Compositor和Composition 27 2.3.3 策略模式 29 2.4 修饰用户界面 29 2.4.1 透明围栏 29 2.4.2 Monoglyph 30 2.4.3 Decorator 模式 32 2.5 支持多种视感标准 32 2.5.1 对象创建的抽象 32 2.5.2 工厂和产品 33 2.5.3 Abstract Factory模式 35 2.6 支持多种窗口系统 35 2.6.1 我们是否可以使用Abstract Factory 模式 35 2.6.2 封装实现依赖关系 35 2.6.3 Window和WindowImp 37 2.6.4 Bridge 模式 40 2.7 用户操作 40 2.7.1 封装一个请求 41 2.7.2 Command 及其子 41 2.7.3 撤消和重做 42 2.7.4 命令历史记录 42 2.7.5 Command 模式 44 2.8 拼写检查和断字处理 44 2.8.1 访问分散的信息 44 2.8.2 封装访问和遍历 45 2.8.3 Iterator及其子 46 2.8.4 Iterator模式 48 2.8.5 遍历和遍历过程中的动作 48 2.8.6 封装分析 48 2.8.7 Visitor 及其子 51 2.8.8 Visitor 模式 52 2.9 小结 53 第3章 创建型模式 54 3.1 Abstract Factory(抽象工厂)— 对象创建型模式 57 3.2 Builder(生成器)—对象创建型 模式 63 3.3 Factory Method(工厂方法)— 对象创建型模式 70 3.4 Prototype(原型)—对象创建型 模式 87 3.5 Singleton(单件)—对象创建型 模式 84 3.6 创建型模式的讨论 89 第4章 结构型模式 91 4.1 Adapter(适配器)—对象结构型 模式 92 4.2 Bridge(桥接)—对象结构型 模式 100 4.3 Composite(组成)—对象结构型 模式 107 4.4 Decorator(装饰)—对象结构型 模式 115 4.5 FACADE(外观)—对象结构型 模式 121 4.6 Flyweight(享元)—对象结构型 模式 128 4.7 Proxy(代理)—对象结构型 模式 137 4.8 结构型模式的讨论 144 4.8.1 Adapter与Bridge 144 4.8.2 Composite、Decorator与Proxy 145 第5章 行为模式 147 5.1 CHAIN OF RESPONSIBIL ITY(职责链) —对象行为型模式 147 5.2 COMMAND(命令)—对象行为型 模式 154 5.3 INTERPRETER(解释器)—行为型 模式 162 5.4 ITERATOR(迭代器)—对象行为型 模式 171 5.5 MEDIATOR(中介者)—对象行为型 模式 181 5.6 MEMENTO(备忘录)—对象行为型 模式 188 5.7 OBSERVER(观察者)—对象行为型 模式 194 5.8 STATE(状态)—对象行为型模式 201 5.9 STRATEGY(策略)—对象行为型 模式 208 5.10 TEMPLATE METHOD(模板方法) —行为型模式 214 5.11 VISIT
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值