c++[1]

1 篇文章 0 订阅
回调函数
2007年05月27日 星期日 16:07

简介

  对于很多初学者来说,往往觉得回调函数很神秘,很想知道回调函数的工作原理。本文将要解释什么是回调函数、它们有什么好处、为什么要使用它们等等问题,在开始之前,假设你已经熟知了函数指针。

  什么是回调函数?

  简而言之,回调函数就是一个通过函数指针调用的函数。如果你把函数的指针(地址)作为参数传递给另一个函数,当这个指针被用为调用它所指向的函数时,我们就说这是回调函数。

  为什么要使用回调函数?

  因为可以把调用者与被调用者分开。调用者不关心谁是被调用者,所有它需知道的,只是存在一个具有某种特定原型、某些限制条件(如返回值为int)的被调用函数。

  如果想知道回调函数在实际中有什么作用,先假设有这样一种情况,我们要编写一个库,它提供了某些排序算法的实现,如冒泡排序、快速排序、shell排序、shake排序等等,但为使库更加通用,不想在函数中嵌入排序逻辑,而让使用者来实现相应的逻辑;或者,想让库可用于多种数据类型(int、float、string),此时,该怎么办呢?可以使用函数指针,并进行回调。

  回调可用于通知机制,例如,有时要在程序中设置一个计时器,每到一定时间,程序会得到相应的通知,但通知机制的实现者对我们的程序一无所知。而此时,就需有一个特定原型的函数指针,用这个指针来进行回调,来通知我们的程序事件已经发生。实际上,SetTimer() API使用了一个回调函数来通知计时器,而且,万一没有提供回调函数,它还会把一个消息发往程序的消息队列。

  另一个使用回调机制的API函数是EnumWindow(),它枚举屏幕上所有的顶层窗口,为每个窗口调用一个程序提供的函数,并传递窗口的处理程序。如果被调用者返回一个值,就继续进行迭代,否则,退出。EnumWindow()并不关心被调用者在何处,也不关心被调用者用它传递的处理程序做了什么,它只关心返回值,因为基于返回值,它将继续执行或退出。

  不管怎么说,回调函数是继续自C语言的,因而,在C++中,应只在与C代码建立接口,或与已有的回调接口打交道时,才使用回调函数。除了上述情况,在C++中应使用虚拟方法或函数符(functor),而不是回调函数。

  一个简单的回调函数实现

  下面创建了一个sort.dll的动态链接库,它导出了一个名为CompareFunction的类型--typedef int (__stdcall *CompareFunction)(const byte*, const byte*),它就是回调函数的类型。另外,它也导出了两个方法:Bubblesort()和Quicksort(),这两个方法原型相同,但实现了不同的排序算法。

void DLLDIR __stdcall Bubblesort(byte* array,int size,int elem_size,CompareFunction cmpFunc);

void DLLDIR __stdcall Quicksort(byte* array,int size,int elem_size,CompareFunction cmpFunc);

  这两个函数接受以下参数:

  ·byte * array:指向元素数组的指针(任意类型)。

  ·int size:数组中元素的个数。

  ·int elem_size:数组中一个元素的大小,以字节为单位。

  ·CompareFunction cmpFunc:带有上述原型的指向回调函数的指针。

  这两个函数的会对数组进行某种排序,但每次都需决定两个元素哪个排在前面,而函数中有一个回调函数,其地址是作为一个参数传递进来的。对编写者来说,不必介意函数在何处实现,或它怎样被实现的,所需在意的只是两个用于比较的元素的地址,并返回以下的某个值(库的编写者和使用者都必须遵守这个约定):

  ·-1:如果第一个元素较小,那它在已排序好的数组中,应该排在第二个元素前面。

  ·0:如果两个元素相等,那么它们的相对位置并不重要,在已排序好的数组中,谁在前面都无所谓。

  ·1:如果第一个元素较大,那在已排序好的数组中,它应该排第二个元素后面。

  基于以上约定,函数Bubblesort()的实现如下,Quicksort()就稍微复杂一点:

void DLLDIR __stdcall Bubblesort(byte* array,int size,int elem_size,CompareFunction cmpFunc)
{
 for(int i=0; i < size; i++)
 {
  for(int j=0; j < size-1; j++)
  {
   //回调比较函数
   if(1 == (*cmpFunc)(array+j*elem_size,array+(j+1)*elem_size))
   {
    //两个相比较的元素相交换
    byte* temp = new byte[elem_size];
    memcpy(temp, array+j*elem_size, elem_size);
    memcpy(array+j*elem_size,array+(j+1)*elem_size,elem_size);
    memcpy(array+(j+1)*elem_size, temp, elem_size);
    delete [] temp;
   }
  }
 }
}

  注意:因为实现中使用了memcpy(),所以函数在使用的数据类型方面,会有所局限。

  对使用者来说,必须有一个回调函数,其地址要传递给Bubblesort()函数。下面有二个简单的示例,一个比较两个整数,而另一个比较两个字符串:

int __stdcall CompareInts(const byte* velem1, const byte* velem2)
{
 int elem1 = *(int*)velem1;
 int elem2 = *(int*)velem2;

 if(elem1 < elem2)
  return -1;
 if(elem1 > elem2)
  return 1;

 return 0;
}

int __stdcall CompareStrings(const byte* velem1, const byte* velem2)
{
 const char* elem1 = (char*)velem1;
 const char* elem2 = (char*)velem2;
 return strcmp(elem1, elem2);
}

  下面另有一个程序,用于测试以上所有的代码,它传递了一个有5个元素的数组给Bubblesort()和Quicksort(),同时还传递了一个指向回调函数的指针。

int main(int argc, char* argv[])
{
 int i;
 int array[] = {5432, 4321, 3210, 2109, 1098};

 cout << "Before sorting ints with Bubblesort/n";
 for(i=0; i < 5; i++)
  cout << array[i] << '/n';

 Bubblesort((byte*)array, 5, sizeof(array[0]), &CompareInts);

 cout << "After the sorting/n";
 for(i=0; i < 5; i++)
  cout << array[i] << '/n';

 const char str[5][10] = {"estella","danielle","crissy","bo","angie"};

 cout << "Before sorting strings with Quicksort/n";
 for(i=0; i < 5; i++)
  cout << str[i] << '/n';

 Quicksort((byte*)str, 5, 10, &CompareStrings);

 cout << "After the sorting/n";
 for(i=0; i < 5; i++)
  cout << str[i] << '/n';

 return 0;
}

  如果想进行降序排序(大元素在先),就只需修改回调函数的代码,或使用另一个回调函数,这样编程起来灵活性就比较大了。

调用约定

  上面的代码中,可在函数原型中找到__stdcall,因为它以双下划线打头,所以它是一个特定于编译器的扩展,说到底也就是微软的实现。任何支持开发基于Win32的程序都必须支持这个扩展或其等价物。以__stdcall标识的函数使用了标准调用约定,为什么叫标准约定呢,因为所有的Win32 API(除了个别接受可变参数的除外)都使用它。标准调用约定的函数在它们返回到调用者之前,都会从堆栈中移除掉参数,这也是Pascal的标准约定。但在C/C++中,调用约定是调用者负责清理堆栈,而不是被调用函数;为强制函数使用C/C++调用约定,可使用__cdecl。另外,可变参数函数也使用C/C++调用约定。

  Windows操作系统采用了标准调用约定(Pascal约定),因为其可减小代码的体积。这点对早期的Windows来说非常重要,因为那时它运行在只有640KB内存的电脑上。

  如果你不喜欢__stdcall,还可以使用CALLBACK宏,它定义在windef.h中:

#define CALLBACK __stdcallor

#define CALLBACK PASCAL //而PASCAL在此被#defined成__stdcall

   作为回调函数的C++方法

  因为平时很可能会使用到C++编写代码,也许会想到把回调函数写成类中的一个方法,但先来看看以下的代码:

class CCallbackTester
{
 public:
 int CALLBACK CompareInts(const byte* velem1, const byte* velem2);
};

Bubblesort((byte*)array, 5, sizeof(array[0]),
&CCallbackTester::CompareInts);

  如果使用微软的编译器,将会得到下面这个编译错误:

error C2664: 'Bubblesort' : cannot convert parameter 4 from 'int (__stdcall CCallbackTester::*)(const unsigned char *,const unsigned char *)' to 'int (__stdcall *)(const unsigned char *,const unsigned char *)' There is no context in which this conversion is possible

  这是因为非静态成员函数有一个额外的参数:this指针,这将迫使你在成员函数前面加上static。当然,还有几种方法可以解决这个问题,但限于篇幅,就不再论述了。

 

仿函数,又叫做函数对象,是一个重载了"()"运算符的struct,是STL(标准模板库)六大组件(容器、配置器、迭代器、算法、配接器、仿函数)之一;仿函数虽然小,但却极大的拓展了算法的功能,几乎所有的算法都有仿函数版本。例如,查找算法find_if就是对find算法的扩展,标准的查找是两个元素向等就找到了,但是什么是相等在不同情况下却需要不同的定义,如地址相等,地址和邮编都相等,虽然这些相等的定义在变,但算法本身却不需要改变,这都多亏了仿函数。

看个简单的例子:
struct D {
 D(int i=0){num=i;}
 int num;
};
struct print_D{
 void operator()(const D* d)const{
      cout<<"I am D. my num="<<d->num<<endl;
    }
};

int main()
{
  vector<D*> V;

  V.push_back(new D(1));
  V.push_back(new D(2));
  V.push_back(new D);
  V.push_back(new D(3));

  for_each(V.begin(), V.end(), print_D());
}
编译输出:

I am D. my num=1
I am D. my num=2
I am D. my num=0
I am D. my num=3

如果使用mem_fun,会方便很多:

struct D {
  D(int i=0){num=i;}
  void print() { cout << "I'm a D. my num=" << num<< endl; }
  int num;
};

int main()
{
  vector<D*> V;

  V.push_back(new D(1));
  V.push_back(new D(2));
  V.push_back(new D);
  V.push_back(new D(3));

  for_each(V.begin(), V.end(), mem_fun(&D::print));
}

 

mem_fun对于一些多态的虚函数也十分有用

struct B {
 virtual void print() = 0;
};

struct D1 : public B {
 void print() { cout << "I'm a D1" << endl; }
};

struct D2 : public B {
 void print() { cout << "I'm a D2" << endl; }
};

int main()
{
 vector<B*> V;

 V.push_back(new D1);
 V.push_back(new D2);
 V.push_back(new D2);
 V.push_back(new D1);

  for_each(V.begin(), V.end(), mem_fun(&B::print));
}

 

仿函数之所以叫做函数对象,是因为仿函数都是定义了()函数运算操作符的类。例如,STL自带的仿函数equal_to<class Tp>定义为:

template <class _Tp>

struct equal_to : public binary_function<_Tp,_Tp,bool>

{

bool operator()(const_Tp&__x,const_Tp&__y) const { return __x==__y; }

};

在算法内部调用此操作符,如find_if:

template <class_RandomAccessIter,class_Predicate>

_STLP_INLINE_LOOP _RandomAccessIter __find_if(_RandomAccessIter __first, _RandomAccessIter __last,_Predicate __pred,const random_access_iterator_tag &)

{

_STLP_DIFFERENCE_TYPE(_RandomAccessIter) __trip_count = (__last - __first) >> 2;

 

for ( ; __trip_count > 0 ; --__trip_count) {

if (__pred(*__first)) return __first;

++__first;

//以下略

}

 

仿函数的可配接性

仿函数的可配接性是指仿函数能够与其它仿函数配接在一起实现新的功能,如不小于60,可以利用STL自带的not1<int>和less<int>配接而成:not1(bind2nd(less<int>(), 12))。

 

一般而言,通用函数也可以作为仿函数参数传递给算法,但其区别在于“通用函数不具有可配接性”。是否定义成仿函数都具有配接性了呢?也不尽然!只有从unary_function或者binary_funcion继承的仿函数才有配接性。这是为什么呢?

其奥妙在于模板类常见的类型定义,可配接性的关键就在于这些类型定义;如binary_function:

template <class _Arg1, class _Arg2, class _Result>

struct binary_function {

typedef _Arg1 first_argument_type;

typedef _Arg2 second_argument_type;

typedef _Result result_type;

};

在STL的适配器中会自动使用到这些类型定义,所以必须声明这些类型。

 

把通用函数转换为仿函数

STL的实现也考虑到会将通用函数作为仿函数来使用,为了保证这些函数的可配接性,即把这些函数转换为仿函数使用,STL也提供了相应的适配器ptr_fun1_base,ptr_fun2_base,其原理也是重载函数调用操作符,在仿函数对象构造时把通用函数作为参数传入,如:

template <class _Arg, class _Result>

class pointer_to_unary_function : public unary_function<_Arg, _Result>

{

protected:

//函数原型

_Result (*_M_ptr)(_Arg);

public:

pointer_to_unary_function() {}

//构造时把函数指针传入

explicit pointer_to_unary_function(_Result (*__x)(_Arg)) : _M_ptr(__x) {}

//()函数运算操作符重载,执行函数功能

_Result operator()(_Arg __x) const { return _M_ptr(__x); }

};

 

把类成员函数转换为仿函数

既然通用函数都能转换为仿函数,带有C++封装性的类的成员函数(当然要是public)也能否转换为仿函数?答案是肯定的,STL也提供了相应适配器。由于返回值和参数的个数不同,这类适配器的数目很多:_Void_mem_fun0_ptr、_Void_mem_fun1_ptr、_Void_const_mem_fun0_ptr、_Void_const_mem_fun1_ptr等。

例子中使用通用函数和成员函数作为仿函数配合STL算法使用。

class Numbers

{

public:

//用于显示

bool display()

{

      cout << *this;

      return true;

}

//用于查找

bool if_equal(int val)

{

     return val == m_val;

}

};

 

如下的语句验证了ptr_fun转换后的仿函数的可配接性:

vector<int>::iterator it = find_if(vNums.begin(), vNums.end(), bind2nd(ptr_fun(if_equal), val));

而for_each(vObjs.begin(), vObjs.end(), mem_fun(&Numbers::display));和vector<Numbers*>::iterator itObj=find_if(vObjs.begin(), vObjs.end(), bind2nd(mem_fun1(&Numbers::if_equal), 3));

说明了如何使用STL的适配器来转换类成员函数。需要说明的是,在转换成员函数时,有引用和指针两个版本,例子程序中使用的是指针版本,所以定义vector时定义元素类型尾Number*。这是因为这时适配器的函数操作符是通过指针形式调用的,如mem_fun1返回mem_fun1_t的内部实现为:

Ret operator()(_Tp* __p, _Arg __x) const { return (__p->*_M_f)(__x); }

 

定义自己的仿函数类型

定义可配接的仿函数,只需要从unary_function和binary_function派生即可,但是STL只定义了这两种类型;但我们有可能需要使用3个参数的仿函数,同时也更能体会可配接性的原理,这里给出了triple_function的函数原型,可以STL的作为一种扩展。

//用于方便提前类的类型定义

#define TRIPLE_ARG(Operation, Type) Operation::Type

//三元函数的类型定义

template<class Arg1, class Arg2, class Arg3, class Result>

struct triple_funcion

{

    //保证可配接性的类型定义

       typedef Arg1 first_argument_type;

       typedef Arg2 second_argument_type;

       typedef Arg3 third_argument_type;

       typedef Result result_type;

};

 

//三元函数的适配器,把第3个参数固定为特定值

template <class Operation>

class binder3rd : public binary_function<typename TRIPLE_ARG(Operation, first_argument_type),

typename TRIPLE_ARG(Operation, second_argument_type), typename TRIPLE_ARG(Operation, result_type)>

{

protected:

       Operation m_op;

       typename Operation::third_argument_type value;

public:

       binder3rd(const Operation& x, const typename Operation::third_argument_type y):m_op(x), value(y){}

    //通过固定第三个参数,把函数转换为binary_function

       typename Operation::result_type operator()(const Operation::first_argument_type& x,

              const Operation::second_argument_type& y) const

       {

              return m_op(x, y, value);

       }

      

};

 

//上层使用的包装类

template<class Operation, class Arg>

inline binder3rd<Operation> bind3rd(const Operation& fn, const Arg& x)

{

       typedef Operation::third_argument_type third_argument_type;

       return binder3rd<Operation>(fn, third_argument_type(x));

}

 

在例子中定义了一个三元仿函数:

class Qualified : public triple_funcion<Student, int, int, bool>

{

public:

       bool operator()(const Student& s, int math, int physics) const

       {

              return s.math > math && s.physics > physics;

       }

};

用于查找数学和物理两科成绩符合条件的学生。

查找时,通过bind3rd和bind2nd把数学和物理的成绩基线定下来:数学>40,物理>60。

it = find_if(it, students.end(), bind2nd(bind3rd(Qualified(), 40), 60));

 

仿函数小巧和作用大,原因是其可配接性和用于算法;可以根据需要把相关函数封装到类中,或者调用基本的函数库来减少开发量。只要知道了STL适配器内部机制,就能定义出符合要求的仿函数来。


本文来自CSDN博客,转载请标明出处:http://blog.csdn.net/armman/archive/2007/03/18/1532645.aspx

C++回调函数(callback)与仿函数(functor)异同

2009年下半年全国计算机等级考试你准备好了没?考计算机等级考试的朋友,2009年下半年全国计算机等级考试时间是2009年9月19日至23日。更多优质资料尽在考试大论坛 考试大在线题库
  回调函数(callback)与仿函数(functor)很多时候从用途上来看很相似,以致于我们经常将它们相提并论。例如:
  inline bool compare(int a, int b)
  {
  return a > b;
  }
  struct comparer {
  bool operator()(int a, int b) const {
  return a > b;
  }
  };
  void main()
  {
  std::vector<int> vec, vec2;
  std::sort(vec.begin(), vec.end(), compare);
  std::sort(vec2.begin(), vec2.end(), comparer());
  }
  仿函数(functor)之所以称为仿函数,是因为这是一种利用某些类对象支持operator()的特性,来达到模拟函数调用效果的技术。
  如果这里vec, vec2这两个vector的内容一样,那么从执行结果看,使用回调函数compare与使用仿函数comparer是一样的。
  那么,我们应该用回调,还是用仿函数?
  很多人都说用仿函数吧,回调函数是丑陋的,代码不太象C++风格。
  但其实问题的本质不是在代码风格上,仿函数与回调函数各有利弊,不能一概而论。
  仿函数(functor)的优点
  我的建议是,如果可以用仿函数实现,那么你应该用仿函数,而不要用回调。原因在于:
  仿函数可以不带痕迹地传递上下文参数。而回调技术通常使用一个额外的void*参数传递。这也是多数人认为回调技术丑陋的原因。
  更好的性能。
  仿函数技术可以获得更好的性能,这点直观来讲比较难以理解。你可能说,回调函数申明为inline了,怎么会性能比仿函数差?我们这里来分析下。我们假设某个函数func(例如上面的std::sort)调用中传递了一个回调函数(如上面的compare),那么可以分为两种情况:
  func是内联函数,并且比较简单,func调用最终被展开了,那么其中对回调函数的调用也成为一普通函数调用(而不是通过函数指针的间接调用),并且如果这个回调函数如果简单,那么也可能同时被展开。在这种情形下,回调函数与仿函数性能相同。
  func是非内联函数,或者比较复杂而无法展开(例如上面的std::sort,我们知道它是快速排序,函数因为存在递归而无法展开)。此时回调函数作为一个函数指针传入,其代码亦无法展开。而仿函数则不同。虽然func本身复杂不能展开,但是func函数中对仿函数的调用是编译器编译期间就可以确定并进行inline展开的。因此在这种情形下,仿函数比之于回调函数,有着更好的性能。并且,这种性能优势有时是一种无可比拟的优势(对于std::sort就是如此,因为元素比较的次数非常巨大,是否可以进行内联展开导致了一种雪崩效应)。
  仿函数(functor)不能做的?
  话又说回来了,仿函数并不能完全取代回调函数所有的应用场合。例如,我在std::AutoFreeAlloc中使用了回调函数,而不是仿函数,这是因为AutoFreeAlloc要容纳异质的析构函数,而不是只支持某一种类的析构。这和模板(template)不能处理在同一个容器中支持异质类型,是一个道理。

仿函数,就是使一个类的使用看上去象一个函数。其实现就是类中实现一个operator(),这个类就有了类似函数的行为,就是一个仿函数类了。

  在我们写代码时有时会发现有些功能的实现的代码,会不断的在不同的成员函数中用到,但是又不好将这些代码独立出来成为一个类的一个成员函数。但是又很想复用这些代码。写一个公共的函数,可以,这是一个解决方法,不过函数用到的一些变量,就可能成为公共的全局变量,再说为了复用这么一片代码,就要单立出一个函数,也不是很好维护。这时就可以用仿函数了,写一个简单类,除了那些维护一个类的成员函数外,就只是实现一个operator(),在类实例化时,就将要用的,非参数的元素传入类中。这样就免去了对一些公共变量的全局化的维护了。又可以使那些代码独立出来,以便下次复用。而且这些仿函数,还可以用关联,聚合,依赖的类之间的关系,与用到他们的类组合在一起,这样有利于资源的管理(这点可能是它相对与函数最显著的有点了)。如果在配合上模板技术和policy编程思想,那就更是威力无穷了,大家可以慢慢的体会。

1. 什么是仿函数?

仿函数,又或叫做函数对象,是STL(标准模板库)六大组件(容器、配置器、迭代器、算法、配接器、仿函数)之一



2. 为什么需要仿函数?


仿函数可以达到函数的功能,并且由于仿函数是编译期内联,性能要高

重要的是仿函数结合泛型编程,仿函数的泛化大大增强了它的应用性

非局部静态对象

  非局部静态对象指的是这样的对象:    定义在全局或名字空间范围内 (例如:theFileSystem 和tempDir ),    在一个类中被声明为 static,或,    在一个文件范围被定义为 static。    比如:   class Test{……}//定义一个类   Test test;//非局部静态对象   这种对象的初始化顺序是不定的,更详细的说明可以查看Effective C++ 中的条款47;

c++静态对象的初始化

程序设计语言 2009-04-21 09:59:34 阅读154 评论0   字号: 订阅

静态对象语义

所谓的静态对象,是指从产生一直持续到程序结束的那些对象,在这个过程中不会动态的消亡,所以被称为静态对象。包括global对象,定义于namespace的对象,在class 函数 file里的static对象。其中函数里的是local静态对象,其他都是non-local的,local与non-local的初始化时机不同。对于local的静态对象,初始化发生在函数被调用期间,首次碰到该定义时。而对于non-local的静态变量则在main函数的usercode之前进行初始化。

对于类中的静态变量和全局变量都是non-local静态变量,必要的初始化都是在main里的user code之前完成,作为类的成员而非对象成员,初始化时机与该类对象并无关系。

其中出现在类定义中的静态变量语句只是声明,对于要使用的类的静态成员变量,必须还要在类外进行定义,否则使用时会发生链接错误。声明并不会导致空间的分配,只有定义才会使其被生成。也就是如果你对类的静态成员进行了定义,那么它就肯定会被分配空间并初始化。就像全局变量一样。

静态成员变量初始化

有人可能提出这样的疑问:假如好多类都声明了自己大量的静态变量,但是这些类好多都没用,这样其不是浪费了好多空间,为啥不等到有类被使用的时候在申请空间呢?

首先如果你没有用这个东西,你可以不定义它。另一方面既然你声明了,说明你需要它,但你用根本没用它,那这个变量根本就是不需要的,那就是可以去掉了。另外从库的观点看,作为类的属性,一般应当与类同时被用到 。当然像你说的java的实现方法,也没有问题啊,同样都是很多因素下权衡下来的一个决定吧。

但是如果在c++中如果这样实现,意味着要改变变量定义的语义,而这个改变的影响是巨大的,就像一个全局变量我们定义了,能否不给他分配空间,而是等到第一次使用它时再搞呢?当然也许会问:能否通过编译器优化把这样定义了而没使用的变量优化掉呢?实际上这也是不可能的,因为虽然没有使用这样的变量,但是程序可能依赖与该变量的初始化动作,比如在它的构造函数中建立起执行环境,如果把它优化掉了,程序就是错误的了。

实现方法:

对于non-local静态变量的初始化,编译器实际上是这样实现的。对每个编译文件里需要初始化的静态变量,生成一系列的sti_开头的函数,在里面完成初始化调用语句,然后把这些sti_函数抽取出来,形成一个初始化函数表,然后在__main()函数里调用,然后把这个函数放到main里的开头。

而对于local静态变量,为了实现第一次碰到时初始化,附加了一个全局变量,标志该对象是否被初始化,同时在析构时检查这个变量。这样就保证了第一次碰到时初始化,同时只有在执行了构造函数的时候,才会在程序退出时执行对应的析构函数。

 

 

Microsoft特殊处

dllimport和dllexport存储类修饰符是C语言的Microsoft特殊处扩充。这些修饰显式定义了DLL的客户界面(可执行的文件或另外的DLL)。说明为dllexport的函数消除了一个模块定义(.DLL)文件的需要。你可以为数据和对象使用dllimport和dllexport修饰符。

dllimport和dllexport存储类修饰符必须与扩充的属性语法关键字__declspec一起使用,下面是这样的例子:

#define DllImport __declspec(dllimport)

#define DllExport __declspec(dllexport)

DllExport void func();

Dllexport int i = 10;

DllExport int j;

DllExport int n;

有关扩充的存储类修饰符的语法的指定信息,参见第3章“说明和类型”中的“扩充的存储类型属性”。

Microsoft特殊处结束

定义和说明

Microsoft特殊处

DLL界面指的是系统中某个程序中输出的所有已知项(函数和数据);也就是所有说明为dllimport或dllexport的所有项。包括在DLL界面中的所有说明必须指定为dllimport或dllexport属性。但该定义只能指定dllexport属性。例如,如下函数定义生成一个编译器错误:

#define DLLImport __declspec(dllimport)

#define DLLExport __declspec(dllexport)

DLLImport int func()/*错误:在定义中禁止dllimport*/

{
return 1;
}

下面代码也产生一个错误:

#define DllImport __declspec(dllimport)

#define DllExport __declspec(dllexport)

DllImport int i=10; /*错误:这是一个定义*/

但如下是正确的语法:

#define DllImport __declspec(dllimport)

#define DllExport __declspec(dllexport)

DllExport int i=10; /*正确:这是一个输出定义*/

dllexport的使用隐含一个定义,而dllimport隐含一个说明。你必须对dllexport使用extern关键字强制为一个说明;否则,隐含是一个定义。

#define DllImport __declspec(dllimport)

#define DllExport __declspec(dllexport)

extern DllImport int k; /*这是正确的并隐含一个说明*/

Dllimport int j;

Microsoft特殊处结束

用dllexport和dllimport定义联编函数

Microsoft特殊处

你可以用dllexport属性定义一个联编函数,在这种情况下,该函数总是被实例化和被输出,无论程序中的任何模块引用该函数。该函数假定是被另一程序输入。

你也可以用dllimport属性说明一个函数为联编函数,在这种情况下,该函数可以被伸展(从属于/Ob(联编)编译器选项规格)但不能被实例化。在特殊情况中,如果一个联编输入的函数的地址被占用,该函数的地址保留在返回的DLL中。这个行为和占用一个非联编输入的函数的地址相同。在联编函数中的静态局部数据和字符串在DLL和象在单个程序中似的客户(也就是,一个没有DLL界面的可执行文件)之间维护相同的标识符。

在进行提供输入的联编函数的练习时要小心,例如,如果你修改DLL,不要假设该客户使用该DLL的改变的版本。为了保证你加载适当的DLL版本,重新建立该DLL的客户。

Microsoft特殊处结束

dllimport/dllexport的规则和限制

Microsoft特殊处

* 如果你说明一个函数没有dllimport或dllexport属性,该函数不认为是DLL界面的部分。因此,该函数的定义必须出现在该模块中或相同程序的另一个模块中。为了使该函数成为DLL界面部分,必须在其它模块中以dllexport说明该函数的定义;否则,在建立客户时产生一个链接器错误。

* 如果你的程序的单个模块包含相同函数的dllimport和dllexport说明,那么dllexport属性的优先级比dllimport属性的优先级高。但编译器产生一个警告。例如:

#define DLLimport __declspec(dllimport)

#define DLLexport __declspec(dllexport)

DllImport void func1(void); DllExport void func1(void);/*警告:dllexport更优先*/

* 你不能用一个以dllimport属性说明的数据对象的地址初始化一个静态数据指针。

例如,如下代码产生一个错误:#define DllImport __declspec(dllimport)#define DllExport __declspec(dllexport) DllImport int i ; . . . int *pi=&i; /* 错误 */ void func2() { static int *pi=&i; /* 错误 */ }

* 用一个dllimport说明的函数的地址初始化一个静态函数指针,设置该指针为该DLL输入形实替换程序(一个转换控制到该函数的代码块)而不是该函数的地址。如下赋值不产生错误消息:

#define DllImport __declspec(dllimport)

#define DllExport __declspec(dllexport)

DllImport void func1(void)

. . . static void (*pf)(void)=&func1;/* 没有错误 */

void func2()

{

static void (*pf)(void)=&func1;/* 没有错误 */

}

* 因为在一个对象的说明中包括dllexport属性的程序必须提供这个对象的定义,你可以用一个dllexport函数的地址初始化一个全局或局部静态函数指针。类似地,你可以用一个dllexport数据对象的地址初始化一个全局或局部静态数据指针。例如:

#define DllImport __declspec(dllimport)

#define DllExport __declspec(dllexport)

DllImport void func1(void);

DllImport int i;

DllExport void func1(void);

DllExport int i;

. . .

int *pi=&i; /* 正确 */

static void(*pf)(void) = &func1;

/* 正确 */

void func2()
{
static int *pi=i; /* 正确 */
static void (*pf)(void) = &func1; /* 正确 */
}

 

DLL中导出函数的两种方式(dllexport与.def文件)

(2009-03-06 11:34:58)

DLL中导出函数的声明有两种方式:

一种方式是:在函数声明中加上__declspec(dllexport);
另外一种方式是:采用模块定义(.def)文件声明,(.def)文件为链接器提供了有关被链接程序的导出、属性及其他方面的信息。

方式一:在函数声明中加上__declspec(dllexport)
/// 在动态链接库程序中
/// 声明动态链接库(**.dll)的对外接口函数TestFuction
extern "C" __declspec(dllexport) int TestFuction(int nType,char *strPath,std::vector<string> &vecData)
{
   do anything here
   return 0;
}


/// 在外部希望调用动态链接库的程序中
/// 加载动态链接库(**.dll)并调用其对外接口TestFuction
void func()
{
  //typedef与函数TestFuction类型相同的函数指针为TESTDLL
  typedef int (_cdecl * TESTDLL)(int nType,char *strPath,std::vector<string> &vecData);
  HINSTANCE hmod;
  //加载动态链接库**.dll
  hmod =::LoadLibrary(_TEXT("dll相对路径//**.dll"));
  if(NULL == hmod)
  {
     TRACE("加载**.dll失败");
  }
  //定义一个与函数TestFuction类型相同的函数指针lpproc
  TESTDLL lpproc;
  //搜索**.dll中函数名为TestFuction的对外接口
  lpproc = (TESTDLL)GetProcAddress (hmod,"TestFuction");
  //如果搜索成功
  if(NULL != lpproc)
  {
     int nType = 0;
     char* strPath = "Data";
     std::vector<string> vecData;
     //通过函数指针lpproc调用**.dll的接口函数TestFuction
     int nResult = (*lpproc)(nType,strPath,vecData);
  }
  //...
  //在恰当的时候释放动态链接库**.dll
  FreeLibrary(hmod);
}


方式二:采用模块定义(.def)文件声明
首先创建 一个DLL程序(DllTestDef)
在*.cpp中
int __stdcall Add(int numa, int numb)
{
     return (numa + numb);
}

int __stdcall Sub(int numa, int numb)
{
     return (numa - numb);
}

然后创建一个.def的文件,在里面加上

;DllTestDef.lib : 导出DLL函数
;作者:----
LIBRARY DllTestDef
EXPORTS
Add @ 1
Sub @ 2

最后创建一个测试程序:.cpp文件如下:
#include <iostream>
#include <windows.h>

using namespace std;

typedef int (__stdcall *FUN)(int, int);
HINSTANCE hInstance;
FUN   fun;

int main()
{
       hInstance = LoadLibrary("DLLTestDef.dll");
       if(!hInstance)
           cout << "Not Find this Dll" << endl;
       fun = (FUN)GetProcAddress(hInstance, MAKEINTRESOURCE(1));
       if (!fun)
       {
              cout << "not find this fun" << endl;
       }
       cout << fun(1, 2) << endl;
       FreeLibrary(hInstance);
       return 0;
}

说明:
.def文件的规则为:

(1)LIBRARY语句说明.def文件相应的DLL;

(2)EXPORTS语句后列出要导出函数的名称。可以在.def文件中的导出函数名后加@n,表示要导出函数的序号为n(在进行函数调用时,这个序号将发挥其作用);

(3).def 文件中的注释由每个注释行开始处的分号 (;) 指定,且注释不能与语句共享一行。

(4)使用__declspec(dllexport)和使用.def文件是有区别的。

如果你的DLL是提供给VC用户使用的,你只需要把编译DLL时产生的.lib提供给用户,
它可以很轻松地调用你的DLL。但是如果你的DLL是供VB、PB、Delphi用户使用的,那么会产生一个小麻烦。
因为VC++编译器对于__declspec(dllexport)声明的函数会进行名称转换,如下面的函数:
__declspec(dllexport) int __stdcall Add()
会转换为Add@0,这样你在VB中必须这样声明:
Declare Function Add Lib "DLLTestDef.dll" Alias "Add@0" () As Long
@后面的数由于参数类型不同而可能不同。这显然不太方便。所以如果要想避免这种转换,就要使用.def文件方式导出函数了。

 

 

dllexport

默认分类 2010-08-18 14:04:02 阅读5 评论0   字号: 订阅



  虽然能用dll实现的功能都可以用com来替代,但dll的优点确实不少,它更容易创建。本文将讨论如何利用vc mfc来创建不同类型的dll,以及如何使用他们。

  一、dll的不同类型

  使用vc++可以生成两种类型的dll:mfc扩展dll和常规dll。常规dll有可以分为动态连接和静态连接。visual c++还可以生成win32 dll,但不是这里讨论的主要对象。

  1、mfc扩展dll

  每个dll都有某种类型的接口:变量、指针、函数、客户程序访问的类。它们的作用是让客户程序使用dll,mfc扩展dll可以有c++的接口。也就是它可以导出c++类给客户端。导出的函数可以使用c++/mfc数据类型做参数或返回值,导出一个类时客户端能创建类对象或者派生这个类。同时,在dll中也可以使用dll和mfc。

  visual c++使用的mfc类库也是保存在一个dll中,mfc扩展dll动态连接到mfc代码库的dll,客户程序也必须要动态连接到mfc代码库的dll。(这里谈到的两个dll,一个是我们自己编写的dll,一个装mfc类库的dll)现在mfc代码库的dll也存在多个版本,客户程序和扩展dll都必须使用相同版本的mfc代码dll。所以为了让mfc扩展dll能很好的工作,扩展dll和客户程序都必须动态连接到mfc代码库dll。而这个dll必须在客户程序运行的计算机上。

  2、常规dll

  使用mfc扩展dll的一个问题就是dll仅能和mfc客户程序一起工作, 相信熟悉星际争霸的fans都认识!,如果需要一个使用更广泛的dll,最好采用常规dll,因为它不受mfc的某些限制。常规dll也有缺点:它不能和客户程序发送指针或mfc派生类和对象的引用。一句话就是常规dll和客户程序的接口不能使用mfc,但在dll和客户程序的内部还是可以使用mfc。

  当在常规dll的内部使用mfc代码库的dll时,可以是动态连接/静态连接。如果是动态连接,也就是常规dll需要的mfc代码没有构建到dll中,这种情况有点和扩展dll类似,在dll运行的计算机上必须要mfc代码库的dll。如果是静态连接, 龙之谷无限疲劳外挂,常规dll里面已经包含了需要的mfc代码,这样dll的体积将比较大,但它可以在没有mfc代码库dll的计算机上正常运行。

  二、建立dll

  利用visual c++提供的向导功能可以很容易建立一个不完成任何实质任务的dll,这里就不多讲了,主要的任务是如何给dll添加功能,以及在客户程序中利用这个dll

  1、导出类

  用向导建立好框架后,就可以添加需要导出类的.cpp .h文件到dll中来,或者用向导创建c++ herder file/c++ source file。为了能导出这个类,在类声明的时候要加“_declspec(dllexport)”,如:

  class _declspec(dllexport) cmyclass

  {

  ...//声明

  }

  如果创建的mfc扩展dll,可以使用宏:afx_ext_class:

  class afx_ext_class cmyclass

  {

  ...//声明

  }

  这样导出类的方法是最简单的,也可以采用.def文件导出,这里暂不详谈。

  2、导出变量、常量、对象

  很多时候不需要导出一个类,可以让dll导出一个变量、常量、对象,导出它们只需要进行简单的声明:_declspec(dllexport) int myint;

  _declspec(dllexport) extern const colorref mycolor=rgb(0,0,0);

  _declspec(dllexport) crect rect(10,10,20,20);

  要导出一个常量时必须使用关键字extern,否则会发生连接错误。

  注意:如果客户程序识别这个类而且有自己的头文件,则只能导出一个类对象。如果在dll中创建一个类,客户程序不使用头文件就无法识别这个类。

  当导出一个对象或者变量时,载入dll的每个客户程序都有一个自己的拷贝。也就是如果两个程序使用的是同一个dll,一个应用程序所做的修改不会影响另一个应用程序。

  我们在导出的时候只能导出dll中的全局变量或对象,而不能导出局部的变量和对象,因为它们过了作用域也就不存在了,那样dll就不能正常工作。如:

  myfunction()

  {

  _declspec(dllexport) int myint;

  _declspec(dllexport) cmyclass object;

  }

  3、导出函数

  导出函数和导出变量/对象类似,只要把_declspec(dllexport)加到函数原型开始的位置:

  _declspec(dllexport) int myfunction(int);

  如果是常规dll,它将和c写的程序使用,声明方式如下:

  extern c _declspec(dllexport) int myfunction(int);

  实现:

  extern c _declspec(dllexport) int myfunction(int x)

  {

  ...//操作

  }

  如果创建的是动态连接到mfc代码库dll的常规dll,则必须插入afx_manage_state作为导出函数的首行,因此定义如下:

  extern c _declspec(dllexport) int myfunction(int x)

  {

  afx_manage_state(afxgetstaticmodulestate());

  ...//操作

  }

  有时候为了安全起见,在每个常规dll里都加上,也不会有任何问题,只是在静态连接的时候这个宏无效而已。这是导出函数的方法,记住只有mfc扩展dll才能让参数和返回值使用mfc的数据类型。

  4、导出指针

  导出指针的方式如下:

  _declspec(dllexport) int *pint;

  _declspec(dllexport) cmyclass object = new cmyclass;

  如果声明的时候同时初始化了指针,就需要找到合适的地方类释放指针。在扩展dll中有个函数dllmain()。(注意函数名中的两个l要是小写字母),可以在这个函数中处理指针:

  # include myclass.h

  _declspec(dllexport) cmyclass *pobject = new cmyclass;

  dllmain(hinstance hinstance,dword dwreason,lpvoid lpreserved)

  {

  if(dwreason == dll_process_attach)

  {

  .....//

  }

  else if(dwreason == dll_process_detach)

  {

  delete pobject;

  }

  }

  常规dll有一个从cwinapp派生的类对象处理dll的开和关,可以使用类向导添加initinstance/exitinstance函数。

  int cmydllapp::exitinstance()

  {

  delete pobject;

  return cwinapp::exitinstance();

  }

  三、在客户程序中使用dll

  编译一个dll时将创建两个文件.dll文件和.lib文件。首先将这两个文件复制到客户程序项目的文件夹里,这里需要注意dll和客户程序的版本问题,尽量使用相同的版本,都使用release或者都是debug版本。

  接着就需要在客户程序中设置lib文件,打开project settings--- >link--->object/library modules中输入lib的文件名和路径。如:debug/sampledll.lib。除了dll和lib文件外,客户程序需要针对导出类、函数、对象和变量的头文件,现在进行导入添加的关键字就是:_declspec(dllimport),如:

  _declspec(dllimport) int myfunction(int);

  _declspec(dllimport) int myint;

  _declspec(dllimport) cmyclass object;

  extern c _declspec(dllimport) int myfunction(int);

  在有的时候为了导入类,要把相应类的头文件添加到客户程序中,不同的是要修改类声明的标志:

  class _declspec(dllimport) cmyclass,如果创建的是扩展dll,两个位置都是:

  class afx_ext_class cmyclass。

  使用dll的一个比较严重的问题就是编译器之间的兼容性问题。不同的编译器对c++函数在二进制级别的实现方式是不同的。所以对基于c++的dll,如果编译器不同就有很麻烦的。如果创建的是mfc扩展dll,就不会存在问题,因为它只能被动态连接到mfc的客户应用程序。这里不是本文讨论的重点。

  一、重新编译问题

  我们先来看一个在实际中可能遇到的问题:

  比如现在建立好了一个dll导出了cmyclass类,客户也能正常使用这个dll,假设cmyclass对象的大小为30字节。如果我们需要修改dll中的cmyclass类,让它有相同的函数和成员变量,但是给增加了一个私有的成员变量int类型,现在cmyclass对象的大小就是34字节了。当直接把这个新的dll给客户使用替换掉原来30字节大小的dll,客户应用程序期望的是30字节大小的对象,而现在却变成了一个34字节大小的对象,糟糕,客户程序出错了。

  类似的问题,如果不是导出cmyclass类,而在导出的函数中使用了cmyclass,改变对象的大小仍然会有问题的。这个时候修改这个问题的唯一办法就是替换客户程序中的cmyclass的头文件,全部重新编译整个应用程序,让客户程序使用大小为34字节的对象。

  这就是一个严重的问题,有的时候如果没有客户程序的源代码,那么我们就不能使用这个新的dll了。

  二、解决方法

  为了能避免重新编译客户程序,这里介绍两个方法:(1)使用接口类。(2)使用创建和销毁类的静态函数。

  1、使用接口类

  接口类的也就是创建第二个类,它作为要导出类的接口,所以在导出类改变时,也不需要重新编译客户程序,因为接口类没有发生变化。

  假设导出的cmyclass类有两个函数functiona functionb。现在创建一个接口类cmyinterface,下面就是在dll中的cmyinterface类的头文件的代码:

  # include myclass.h

  class _declspec(dllexport) cmyinterface

  {

  cmyclass *pmyclass;

  cmyinterface();

  ~cmyinterface();

  public:

  int functiona(int);

  int functionb(int);

  };

  而在客户程序中的头文件稍不同,不需要include语句,因为客户程序没有它的拷贝。相反,使用一个cmyclass的向前声明,即使没有头文件也能编译:

  class _declspec(dllexport) cmyinterface

  {

  class cmyclass;//向前声明

  cmyclass *pmyclass;

  cmyinterface();

  ~cmyinterface();

  public:

  int functiona(int);

  int functionb(int);

  };

  在dll中的cmyinterface的实现如下:

  cmyinterface::cmyinterface()

  {

  pmyclass = new cmyclass();

  }

  cmyinterface::~cmyinterface()

  {

  delete pmyclass;

  }

  int cmyinterface::functiona()

  {

  return pmyclass->functiona();

  }

  int cmyinterface::functionb()

  {

  return pmyclass->functionb();

  }

  .....

  对导出类cmyclass的每个成员函数,cmyinterface类都提供自己的对应的函数。客户程序与cmyclass没有联系,这样任意改cmyclass也不会有问题,因为cmyinterface类的大小没有发生变化。即使为了能访问cmyclass中的新增变量而给cmyinterface类加了函数也不会有问题的。

  但是这种方法也存在明显的问题,对导出类的每个函数和成员变量都要对应实现,有的时候这个接口类会很庞大。同时增加了客户程序调用所需要的时间。增加了程序的开销。

  2、使用静态函数

  还可以使用静态函数来创建和销毁类对象。创建一个导出类的时候,增加两个静态的公有函数createme()/destroyme(),头文件如下:

  class _declspec(dllexport) cmyclass

  {

  cmyclass();

  ~cmyclass();

  public:

  static cmyclass *createme();

  static void destroyme(cmyclass *ptr);

  };

  实现函数就是:

  cmyclass * cmyclass::cmyclass()

  {

  return new cmyclass;

  }

  void cmyclass::destroyme(cmyclass *ptr)

  {

  delete ptr;

  }

  然后象其他类一样导出cmyclass类,这个时候在客户程序中使用这个类的方法稍有不同了。如若想创建一个cmyclass对象,就应该是:

  cmyclass x;

  cmyclass *ptr = cmyclass::createme();

  在使用完后删除:

  cmyclass::destroyme(ptr);

  引文来源 游戏推广 - VC,C++外挂技术 - VC++ MFC DLL动态链接库编写详

 

MSDN中的dllexport与dllimport定义

使用 __declspec(dllexport) 从 DLL 导出 
Microsoft 在 Visual C++ 的 16 位编译器版本中引入了 __export,使编译器得以自动生成导出名并将它们放到一个 .lib 文件中。然后,此 .lib 文件就可以像静态 .lib 那样用于与 DLL 链接。

在 32 位编译器版本中,可以使用 __declspec(dllexport) 关键字从 DLL 导出数据、函数、类或类成员函数。__declspec(dllexport) 会将导出指令添加到对象文件中,因此您不需要使用 .def 文件。

当试图导出 C++ 修饰函数名时,这种便利最明显。由于对名称修饰没有标准规范,因此导出函数的名称在不同的编译器版本中可能有所变化。如果使用 __declspec(dllexport),仅当解决任何命名约定更改时才必须重新编译 DLL 和依赖 .exe 文件。

许多导出指令(如序号、NONAME 和 PRIVATE)只能在 .def 文件中创建,并且必须使用 .def 文件来指定这些属性。不过,在 .def 文件的基础上另外使用 __declspec(dllexport) 不会导致生成错误。

若要导出函数,__declspec(dllexport) 关键字必须出现在调用约定关键字的左边(如果指定了关键字)。例如:

__declspec(dllexport) void __cdecl Function1(void);若要导出类中的所有公共数据成员和成员函数,关键字必须出现在类名的左边,如下所示:

class __declspec(dllexport) CExampleExport : public CObject
{ ... class definition ... };生成 DLL 时,通常创建一个包含正在导出的函数原型和/或类的头文件,并将 __declspec(dllexport) 添加到头文件中的声明中。若要提高代码的可读性,请为 __declspec(dllexport) 定义一个宏并对正在导出的每个符号使用该宏:

#define DllExport   __declspec( dllexport ) __declspec(dllexport) 将函数名存储在 DLL 的导出表中。如果希望优化表的大小,请参见按序号而不是按名称从 DLL 导出函数。

注意

将 DLL 源代码从 Win16 移植到 Win32 时,请用 __declspec(dllexport) 替换 __export 的每个实例。

作为参考,请在 Win32 Winbase.h 头文件中搜索。它包含 __declspec(dllimport) 的用法示例。

您希望做什么?
使用 .def 文件从 DLL 导出

使用 AFX_EXT_CLASS 导出和导入

导出 C++ 函数以用于 C 语言可执行文件

导出 C 函数以用于 C 或 C++ 语言可执行文件

确定要使用的导出方法

使用 __declspec(dllimport) 导入到应用程序中

初始化 DLL

您想进一步了解什么?
__declspec 关键字

导入和导出内联函数

相互导入

如何从 DLL 导出数据

如何与应用程序或其他 DLL 共享 DLL 中的数据

请参见
概念
从 DLL 导出

使用 __declspec(dllimport) 导入到应用程序中 
如果一个程序使用 DLL 定义的公共符号,就说该程序是在导入公共符号。为使用 DLL 生成的应用程序创建头文件时,在公共符号的声明上使用 __declspec(dllimport)。不论是用 .def 文件导出还是用 __declspec(dllexport) 关键字导出,__declspec(dllimport) 关键字均有效。

若要提高代码的可读性,请为 __declspec(dllimport) 定义一个宏,然后使用此宏声明每个导入的符号:

#define DllImport   __declspec( dllimport )

DllImport int  j;
DllImport void func();在函数声明上使用 __declspec(dllimport) 是可选操作,但如果使用此关键字,编译器将生成更有效的代码。但是,为使导入的可执行文件能够访问 DLL 的公共数据符号和对象,必须使用 __declspec(dllimport)。请注意,DLL 的用户仍然需要与导入库链接。

对 DLL 和客户端应用程序可以使用相同的头文件。为此,请使用特殊的预处理器符号来指示是生成 DLL 还是生成客户端应用程序。例如:

#ifdef _EXPORTING
   #define CLASS_DECLSPEC    __declspec(dllexport)
#else
   #define CLASS_DECLSPEC    __declspec(dllimport)
#endif

class CLASS_DECLSPEC CExampleA : public CObject
{ ... class definition ... };

 

本文来自CSDN博客,转载请标明出处:http://blog.csdn.net/cl_gamer/archive/2009/12/10/4978765.aspx

 

C++全局对象存储位置?生命生存期?

调用约定符?

智能指针

反汇编指令解读

堆栈生长结构和控制

 

 

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值