Boost.Bind用法详解(一)

Boost.Bind 为函数和函数对象提供了一致的语法,对于值语义和指针语义也一样。我们将从一些简单的例子开始,处理一些简单绑定的用法,然后再转移到通过嵌套绑定进行函数组合。弄明白如何使用 bind 的关键是,占位符的概念。占位符用于表示提供给结果函数对象的参数,Boost.Bind 支持最多九个参数。占位符被命名为 _1_2_3_4, 直至 _9, 你要把它们放在你原先放参数的地方。作为第一个例子,我们定义一个函数,nine_arguments, 它将被一个 bind 表达式调用。

#include <iostream>
#include "boost/bind.hpp"

void nine_arguments(
  int i1,int i2,int i3,int i4,
  int i5,int i6,int i7,int i8, int i9) {
  std::cout << i1 << i2 << i3 << i4 << i5
    << i6 << i7 << i8 << i9 << '\n';
}

int main() {
  int i1=1,i2=2,i3=3,i4=4,i5=5,i6=6,i7=7,i8=8,i9=9;
  (boost::bind(&nine_arguments,_9,_2,_1,_6,_3,_8,_4,_5,_7))
    (i1,i2,i3,i4,i5,i6,i7,i8,i9);
}

在这个例子中,你创建了一个匿名临时绑定器,并立即把参数传递给它的调用操作符来调用它。如你所见,占位符的顺序是被搅乱的,这说明参数的顺序被重新安排了。注意,占位符可以在一个表达式中被多次使用。这个程序的输出如下。

921638457

这表示了占位符对应于它的数字所示位置的参数,即 _1 被第一个参数替换,_2 被第二个参数替换,等等。接下来,你将看到如何调用一个类的成员函数。

调用成员函数

我们来看一下如何用 bind 调用成员函数。我们先来做一些可以用标准库来做的事情,这样可以对比一下用 Boost.Bind 的方法。保存某种类型的元素在一个标准库容器中,一个常见的需要是对某些或全部元素调用一个成员函数。这可以用一个循环来完成,通常也正是这样做的,但还有更好的方法。考虑下面这个简单的类,status, 我们将用它来示范 Boost.Bind 的易用性和强大的功能。

class status {
  std::string name_;
  bool ok_;
public:
  status(const std::string& name):name_(name),ok_(true) {}

  void break_it() {
    ok_=false;
  }

  bool is_broken() const {
    return ok_;
  }

  void report() const {
    std::cout << name_ << " is " <<
      (ok_ ? "working nominally":"terribly broken") << '\n';
  }
};

如果我们把这个类的实例保存在一个 vector, 并且我们需要调用成员函数 report, 我们可能会象下面这样做。

std::vector<status> statuses;
statuses.push_back(status("status 1"));
statuses.push_back(status("status 2"));
statuses.push_back(status("status 3"));
statuses.push_back(status("status 4"));

statuses[1].break_it();
statuses[2].break_it();

for (std::vector<status>::iterator it=statuses.begin();
  it!=statuses.end();++it) {
  it->report();
}

这个循环正确地完成了任务,但它是冗长、低效的(由于要多次调用 statuses.end()),并且不象使用标准库算法 for_each 那样清楚地表明意图。为了用 for_each 来替换这个循环,我们需要用一个适配器来对 vector 元素调用成员函数 report 。这时,由于元素是以值的方式保存的,我们需要的是适配器 mem_fun_ref.

std::for_each(
  statuses.begin(),
  statuses.end(),
  std::mem_fun_ref(&status::report));

这是一个正确、合理的方法,它非常简洁,非常清楚这段代码是干什么的。以下是使用Boost.Bind 完成相同任务的代码。[1]

[1] 要注意的是boost::mem_fn, 它也被接纳进入Library Technical Report, 它也可以在这种没有参数的情况下使用。 mem_fn 取代了 std::mem_fun 和 std::mem_fun_ref.

std::for_each(
  statuses.begin(),
  statuses.end(),
  boost::bind(&status::report,_1));

这个版本同样的清楚、明白。这是前面所说的占位符的第一个真正的使用,我们同时告诉编译器和代码的读者,_1 用于替换这个函数所调用的绑定器的第一个实际参数。虽然这段代码节省了几个字符,但在这种情况下标准库的 mem_fun_ref 和 bind 之间并没有太大的不同,但是让我们来重用这个例子并把容器改为存储指针。

std::vector<status*> p_statuses;
p_statuses.push_back(new status("status 1"));
p_statuses.push_back(new status("status 2"));
p_statuses.push_back(new status("status 3"));
p_statuses.push_back(new status("status 4"));

p_statuses[1]->break_it();
p_statuses[2]->break_it();

我们还可以使用标准库,但不能再用 mem_fun_ref. 我们需要的是适配器 mem_fun, 它被认为有点用词不当,但它的确正确完成了需要做的工作。

std::for_each(
  p_statuses.begin(),
  p_statuses.end(),
  std::mem_fun(&status::report));

虽然这也可以工作,但语法变了,即使我们想做的事情非常相似。如果语法可以与第一个例子相同,那就更好了,所以我们所关心的是代码要做什么,而不是如何去做。使用bind, 我们就无须关心我们处理的元素是指针了(这一点已经在容器类型的声明中表明了,对于现代的库来说,这样的冗余信息是不需要的)。

std::for_each(
  p_statuses.begin(),
  p_statuses.end(),
  boost::bind(&status::report,_1));

如你所见,这与我们前一个例子完全一样,这意味着如果我们之前已经明白了 bind ,那么我们现在也清楚它。现在,我们已决定换用指针了,我们要面对另一个问题,即生存期控制。我们必须手工释放 p_statuses 中的元素,这很容易出错,也无须如此。所以,我们可能决定开始使用智能指针,并(再次)修改我们的代码。

std::vector<boost::shared_ptr<status> > s_statuses;
s_statuses.push_back(
  boost::shared_ptr<status>(new status("status 1")));
s_statuses.push_back(
  boost::shared_ptr<status>(new status("status 2")));
s_statuses.push_back(
  boost::shared_ptr<status>(new status("status 3")));
s_statuses.push_back(
  boost::shared_ptr<status>(new status("status 4")));
s_statuses[1]->break_it();
s_statuses[2]->break_it();

现在,我们要用标准库中的哪个适配器呢?mem_fun 和 mem_fun_ref 都不适用,因为智能指针没有一个名为 report 的成员函数,所以以下代码编译失败。

std::for_each(
  s_statuses.begin(),
  s_statuses.end(),
  std::mem_fun(&status::report));

不巧,标准库不能帮我们完成这个任务[2]。因此,我们不得不采用我们正想要摆脱的循环,或者使用 Boost.Bind, 它不会抱怨任何事情,而且正确地完成我们想要的。

[2] 以后将可以这样做,因为 mem_fn 和 bind 都将成为未来的标准库的一部分。

std::for_each(
  s_statuses.begin(),
  s_statuses.end(),
  boost::bind(&status::report,_1));

再一次,这段代码与前面的例子完全一样(除了容器的名字不同)。使用绑定的语法是一致的,不论是用于值语义或是指针语义,甚至是用于智能指针。有时,使用不同的语法有助于理解代码,但在这里,不是这样的,我们的任务是对容器中的元素调用成员函数,没有更多的也没有更少的事情。语法一致的价值不应被低估,因为它对于编写代码的人,以及对于日后需要维护代码的人都是有帮助的(当然,我们并不真的是在写需要维护的代码,但为了这个主题,让我们假装是在写)。

这些例子示范了一个非常基本和常见的情形,在这种情形下 Boost.Bind 尤为出色。即使标准库也提供了完成相同工作的一些基本工具,但我们还是看到 Bind 既提供了一致的语法,也增加了标准库目前缺少的功能。

看一下门帘的后面

在你开始使用 Boost.Bind 后,这是无可避免的;你将开始惊讶它到底是如何工作的。这看起来就象是魔术,bind 可以推断出参数的类型和返回类型,它又是如何处理占位符的呢?我们将快速地看一下驱动这个东西的机制。它有助于知道一点 bind的工作原理,特别是在试图解释这惊人的简洁性以及编译器对最轻微的错误给出的直接的错误信息。我们将创建一个非常简单的绑定器,至少是部分地模仿 Boost.Bind 的语法。为了避免把这个离题的讨论搞成几页那么长,我们只支持一类绑定,即接受单个参数的成员函数。此外,我们不会对cv限定符进行处理;我们只处理最简单的情况。

首先,我们需要能够推断出我们要绑定的函数的返回类型、类的类型、和参数类型。我们用一个函数模板来做到这一点。

template <typename R, typename T, typename Arg>
simple_bind_t<R,T,Arg> simple_bind(
  R (T::*fn)(Arg),
  const T& t,
  const placeholder&) {
  return simple_bind_t<R,T,Arg>(fn,t);
}

这看起来有点可怕,毕竟这只是在定义整个机器的一部分。但是,这一部分的焦点在于类型推断在哪发生。你会注意到这个函数有三个模板参数,RT, 和 ArgR 是返回的类型,T是类的类型,而 Arg 是(单个)参数的类型。这些模板参数组成了我们的函数的第一个参数,即 R (T::*f)(Arg). 这样,传递一个带单个参数的成员函数给 simple_bind 将允许编译器推断出 R 为成员函数的返回类型,T 为成员函数的类,Arg 为成员函数的参数类型。simple_bind 的返回类型是一个函数对象,它使用与 simple_bind 相同的三个类型进行特化,其构造函数接受一个成员函数指针和一个对应类(T)的实例。 simple_bind 简单地忽略占位符(即函数的最后一个参数),我保留这个参数的原因是为了模仿 Boost.Bind 的语法。在一个更好的实现中,我们显然应该使用这个参数,但是现在让我们先不要管它。这个函数对象的实现相当简单。

template <typename R,typename T, typename Arg>
class simple_bind_t {
  typedef R (T::*fn)(Arg);
  fn fn_;
  T t_;
public:
  simple_bind_t(fn f,const T& t):fn_(f),t_(t) {}

  R operator()(Arg& a) {
    return (t_.*fn_)(a);
  }
};

从 simple_bind 的实现中我们可以看到,构造函数接受两个参数:第一个是指向成员函数的指针,第二个是一个 const T 引用,它会被复制并稍后用于给定一个用户提供的参数来调用其成员函数。最后,调用操作符返回 R, 即成员函数的返回类型,并接受一个 Arg 参数,即传给成员函数的那个参数的类型。调用成员函数的语法稍稍有点晦涩:

(t_.*fn_)(a);

.* 是成员指针操作符,它的第一个操作数是 class T; 另外还有一个成员指针操作符,->*, 它的第一个操作数是是一个 T 指针。剩下就是创建一个占位符,即用于替换实际参数的变量。我们可以通过在匿名名字空间中包含某种类型的变量来创建一个占位符;我们把它称为 placeholder:

namespace {
  class placeholder {};
  placeholder _1;
}

我们创建一个简单的类和一个小程序来测试一下。

class Test {
public:
  void do_stuff(const std::vector<int>& v) {
    std::copy(v.begin(),v.end(),
      std::ostream_iterator<int>(std::cout," "));
  }
};

int main() {
  Test t;
  std::vector<int> vec;
  vec.push_back(42);
  simple_bind(&Test::do_stuff,t,_1)(vec);
}

当我们用上述参数实例化函数 simple_bind 时,类型被自动推断;R 是 voidT 是 Test, 而Arg 是一个 const std::vector<int> 引用。函数返回一个simple_bind_t<void,Test,Arg> 的实例,我们立即调用它的调用操作符,并传进一个参数vec.

非常不错,simple_bind 已经给了你关于绑定器如何工作的一些想法。现在,是时候回到 Boost.Bind 了!

关于占位符和参数

第一个例子示范了 bind 最多可以支持九个参数,但了解多一点关于参数和占位符如何工作的情况,可以让我们更好地使用它。首先,很重要的一点是,普通函数与成员函数之间有着非常大的差异,在绑定一个成员函数时,bind 表达式的第一个参数必须是成员函数所在类的实例!理解这个规则的最容易的方法是,这个显式的参数将取替隐式的 this ,被传递给所有的非静态成员函数。细心的读者将会留意到,实际上这意味着对于成员函数的绑定器来说,只能支持八个参数,因为第一个要用于传递实际的对象。以下例子定义了一个普通函数 print_string 和一个带有成员函数 print_string 的类 some_class ,它们将被用于 bind 表达式。

#include <iostream>
#include <string>
#include "boost/bind.hpp"

class some_class {
public:
  typedef void result_type;
  void print_string(const std::string& s) const {
    std::cout << s << '\n';
  }
};

void print_string(const std::string s) {
  std::cout << s << '\n';
}

int main() {
  (boost::bind(&print_string,_1))("Hello func!");
  some_class sc;
  (boost::bind(&some_class::print_string,_1,_2))
    (sc,"Hello member!");
}

第一个 bind 表达式绑定到普通函数 print_string. 因为该函数要求一个参数,因此我们需要用一个占位符(_1)来告诉 bind 它的哪一个参数将被传递为 print_string 的第一个参数。要调用获得的函数对象,我们必须传递一个 string 参数给调用操作符。参数是一个const std::string&, 因此传递一个字面的字符串将引发一个 std::string 转型构造函数的调用。

(boost::bind(&print_string,_1))("Hello func!");

第二个绑定器用于一个成员函数,some_class 的 print_string 。bind 的第一个参数是成员函数指针。但是,一个非静态成员函数指针并不真的是一个指针[3]。我们必须要有一个对象才可以调用这个函数。这就是为什么这个 bind 表达式必须声明绑定器有两个参数,调用它时两个参数都必须提供。

[3] 是的,我知道这听起来很怪异。但它的确是真的。

boost::bind(&some_class::print_string,_1,_2);

要看看为什么会这样,就要考虑一下得到的这个函数对象要怎么使用。我们必须把一个some_class 实例和一个 print_string 用的参数一起传递给它。

(boost::bind(&some_class::print_string,_1,_2))(sc,"Hello member!");

这个调用操作符的第一个参数是 this ,即那个 some_class 实例。注意,这第一个参数可以是一个指针(智能的或裸的)或者是一个引用;bind 是非常随和的。调用操作符的第二个参数是那个成员函数要用的参数。这里,我们"延迟"了所有两个参数,即我们定义的这个绑定器,它的两个参数,对象本身及成员函数的参数,都要在调用操作符时才指定。我们不是一定非这样做不可。例如,我们可以创建一个绑定器,每次调用它时,都是对同一个对象调用 print_string ,就象这样:

(boost::bind(&some_class::print_string,some_class(),_1))
 ("Hello member!");

这次得到的函数对象已经包含了一个 some_class 实例,因此它的调用操作符只需要一个占位符(_1)和一个参数(一个string)。最后,我们还可以创建一个所谓的无参(nullary)函数,它连那个 string 也绑定了,就象这样:

(boost::bind(&some_class::print_string,
 some_class(),"Hello member!"))();

这些例子清楚地显示了 bind 的多功能性。它可用于延迟它所封装的函数的所有参数、部分参数、或一个参数也不延迟。它也可以把参数按照你所要的顺序进行重排;只要照你的需要排列占位符就行了。接下来,我们将看看如何用 bind 来就地创建排序用的谓词。

动态的排序标准

在对容器中的元素进行排序时,我们有时候需要创建一个函数对象以定义排序的标准,如果我们没有提供关系操作符,或者是已有的关系操作符不是我们想要的排序标准时,就需要这样做了。有些时候我们可以使用来自标准库的比较函数对象(std::greater,std::greater_equal, 等等),但只能对已有类型进行比较,我们不能就地定义一个新的。我们将使用一个名为 personal_info 的类来演示 Boost.Bind 如何帮助我们。personal_info 包含有 first name, last name, 和 age, 并且它没有提供任何的比较操作符。这些信息在创建以后就不再变动,并且可以用成员函数 namesurname, 和 age 来取出。

class personal_info {
  std::string name_;
  std::string surname_;
  unsigned int age_;

public:
  personal_info(
    const std::string& n,
    const std::string& s,
    unsigned int age):name_(n),surname_(s),age_(age) {}

  std::string name() const {
    return name_;
  }

  std::string surname() const {
    return surname_;
  }

  unsigned int age() const {
    return age_;
  }
};

我们通过提供以下操作符来让这个类可以流输出(OutputStreamable):

std::ostream& operator<<(
  std::ostream& os,const personal_info& pi) {
  os << pi.name() << ' ' <<
    pi.surname() << ' ' << pi.age() << '\n';
  return os;
}

如果我们要对含有类型 personal_info 元素的容器进行排序,我们就需要为它提供一个排序谓词。为什么开始的时候我们没有为 personal_info 提供关系操作符呢?一个原因是,因为有几种排序的可能性,而我们不知道对于不同的用户哪一种是合适的。虽然我们也可以选择为不同的排序标准提供不同的成员函数,但这样会加重负担,我们要在类中实现所有相关的排序标准,这并不总是可以做到的。幸运的是,我们可以很容易地用 bind 就地创建所需的谓词。我们先看看基于年龄(可以通过成员函数 age 取得)来进行排序。我们可以为此创建一个函数对象。

class personal_info_age_less_than :
  public std::binary_function<
  personal_info,personal_info,bool> {
public:
  bool operator()(
  const personal_info& p1,const personal_info& p2) {
    return p1.age()<p2.age();
  }
};

我们让 personal_info_age_less_than 公有派生自 binary_function. 从 binary_function派生可以提供使用适配器时所需的 typedef ,例如使用 std::not2. 假设有一个 vector,vec, 含有类型为 personal_info 的元素,我们可以象这样来使用这个函数对象:

std::sort(vec.begin(),vec.end(),personal_info_age_less_than());

只要不同的比较方式的数量很有限,这种方式就可以工作良好。但是,有一个潜在的问题,计算逻辑被定义在不同的地方,这会使得代码难以理解。利用一个较长的、描述清晰的名字可以解决这个问题,就象我们在这里做的一样,但是不是所有情况都会这样清晰,有很大可能我们需要为大于、小于或等于关系提供一堆的函数对象。

那么,Boost.Bind 有什么帮助呢?实际上,在这个例子中它可以帮助我们三次。如果我们要解决这个问题,我们发现有三件事情要做,第一件是绑定一个逻辑操作,如 std::less. 这很容易,我们可以得到第一部分代码。

boost::bind<bool>(std::less<unsigned int>(),_1,_2);

注意,我们通过把 bool 参数提供给 bind,显式地给出了返回类型。有时这是需要的,对于有缺陷的编译器或者在无法推断出返回类型的上下文时。如果一个函数对象包含typedefresult_type, 就不需要显式给出返回类型[4]。现在,我们有了一个接受两个参数的函数对象,两个参数的类型都是 unsigned int, 但我们还不能用它,因为容器中的元素的类型是 personal_info, 我们需要从这些元素中取出 age 并把它作为参数传递给std::less. 我们可以再次使用 bind 来实现。

[4] 标准库的函数对象都定义了 result_type ,因此它们可以与 bind 的返回类型推断机制共同工作。

boost::bind(
  std::less<unsigned int>(),
  boost::bind(&personal_info::age,_1),
  boost::bind(&personal_info::age,_2));

这里,我们创建了另外两个绑定器。第一个用主绑定器的调用操作符的第一个参数(_1)来调用 personal_info::age 。第二个用主绑定器的调用操作符的第二个参数(_2)来调用personal_info::age 。因为 std::sort 传递两个 personal_info 对象给主绑定器的调用操作符,结果就是对来自被排序的 vector 的两个 personal_info 分别调用personal_info::age 。最后,主绑定器传递两个新的、内层的绑定器的调用操作符所返回的 age 给 std::less. 这正是我们所需要的!调用这个函数对象的结果就是 std::less 的结果,这意味着我们有了一个有效的比较函数对象可以用来排序容器中的 personal_info 对象。以下是使用它的方法:

std::vector<personal_info> vec;
vec.push_back(personal_info("Little","John",30));
vec.push_back(personal_info("Friar", "Tuck",50));
vec.push_back(personal_info("Robin", "Hood",40));

std::sort(
  vec.begin(),
  vec.end(),
  boost::bind(
    std::less<unsigned int>(),
    boost::bind(&personal_info::age,_1),
    boost::bind(&personal_info::age,_2)));

我们可以简单地通过绑定另一个 personal_info 成员(变量或函数)来进行不同的排序,例如,按 last name 排序。

std::sort(
  vec.begin(),
  vec.end(),
  boost::bind(
    std::less<std::string>(),
    boost::bind(&personal_info::surname,_1),
    boost::bind(&personal_info::surname,_2)));

这是一种出色的技术,因为它提供了一个重要的性质:就地实现简单的函数。它使得代码易懂且易于维护。虽然技术上可以用绑定器实现基于复杂标准的排序,但那样做是不明智的。给 bind 表达式添加复杂的逻辑会很快失去它的清晰和简洁。虽然有时你想用绑定来做更多的事情,但最好是让绑定器与要维护它的人一样聪明,而不是更加聪明。

函数组合,Part I

一个常见的问题是,将一些函数或函数对象组合成一个函数对象。假设你需要测试一个int ,看它是否大于5且小于等于10。使用"常规"的代码,你将这样写:

if (i>5 && i<=10) {
  // Do something
}

如果是处理一个容器中的元素,上述代码只有放在一个单独的函数时才能工作。如果你不想这样,那么用一个嵌套的 bind 也可以获得相同的效果(注意,这时通常不能使用标准库的 bind1st 和 bind2nd)。如果我们对这个问题进行分解,我们会发现我们需要:逻辑与(std::logical_and), 大于(std::greater), 和小于等于(std::less_equal)。逻辑与看起来就象这样:

boost::bind(std::logical_and<bool>(),_1,_2);

然后,我们需要另一个谓词来回答 _1 是否大于5。

boost::bind(std::greater<int>(),_1,5);

然后,我们还需要另一个谓词来回答 _1 是否小于等于10。

boost::bind(std::less_equal<int>(),_1,10);

最后,我们需要把它们两个用逻辑与合起来,就象这样:

boost::bind(
  std::logical_and<bool>(),
  boost::bind(std::greater<int>(),_1,5),
  boost::bind(std::less_equal<int>(),_1,10));

这样一个嵌套的 bind 相对容易理解,虽然它是后序的。还有,任何人都可以逐字地阅读这段代码并弄清楚它的意图。我们用一个例子来测试一下这个绑定器。

std::vector<int> ints;

ints.push_back(7);
ints.push_back(4);
ints.push_back(12);
ints.push_back(10);

int count=std::count_if(
  ints.begin(),
  ints.end(),
  boost::bind(
    std::logical_and<bool>(),
    boost::bind(std::greater<int>(),_1,5),
    boost::bind(std::less_equal<int>(),_1,10)));

std::cout << count << '\n';

std::vector<int>::iterator int_it=std::find_if(
  ints.begin(),
  ints.end(),
  boost::bind(std::logical_and<bool>(),
    boost::bind(std::greater<int>(),_1,5),
    boost::bind(std::less_equal<int>(),_1,10)));

if (int_it!=ints.end()) {
  std::cout << *int_it << '\n';
}

使用嵌套的 bind 时,小心地对代码进行正确的缩入非常重要,因为如果一旦缩入错误,代码就会很难理解。想想前面那段清晰的代码,再看看以下这个容易混乱的例子。

std::vector<int>::iterator int_it=
  std::find_if(ints.begin(),ints.end(),
    boost::bind<bool>(
    std::logical_and<bool>(),
    boost::bind<bool>(std::greater<int>(),_1,5),
      boost::bind<bool>(std::less_equal<int>(),_1,10)));

当然,对于较长的代码行,这是一个常见的问题,但是在使用这里所描述的结构时更为明显,在这里长语句是合理的而不是个别例外。因此,请对你之后的程序员友好些,确保你的代码行正确缩入,这样可以让人更容易阅读。

本书的一位认真的审阅者曾经问过,在前面的例子中,为什么创建了两个相同的绑定器,而不是创建一个绑定器对象然后使用两次?答案是,因为我们不知道 bind 所创建的绑定器的精确类型(它是由实现定义的),我们没有方法为它声明一个变量。还有,这个类型通常都非常复杂,因为它的署名特征包括了函数 bind 中所有的类型信息(自动推断的)。但是,可以用另外一个工具来保存得到的函数对象,例如来自 Boost.Function 的工具。相关方法的详情请见 "Library 11Function 11"。

这里给出的函数组合的要点与标准库的一个著名的扩充相符,即来自SGI STL的函数compose2 ,它在 Boost.Compose 库(现在已经不用了)中也被称为 compose_f_gx_hx 。

  • 0
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值