Boost 库中的实用工具类使用入门

来源:developerWorks 中国 作者:Arpan Sen
  
实用工具类开发和维护会耗费程序员的时间。Boost 通过提供几个高质量的库,从而简化了此过程,您可以将这些库轻松集成到现有的代码库中。本文简单概述一些较流行的 Boost 实用工具类,并帮助您了解如何将它们投入使用。

实用工具类(utility classes)在几乎任何适当规模的 C++ 项目中都是必需的,但是不存在满足此需求的标准方法。通常,团队根据他们的需求编写实用工具类代码,但是由于缺少重要的接口信息,使得相同组织中的其他项目团队无法重用那些类。建议的标准模板库(Standard Template Library,STL)只具有诸如 hash、stack 和 vector 等少数基本类,因此无法有效地用于取代遗留实用工具库。

本文将介绍几个 Boost 实用工具类,包括 tuple、static_assert、pool、random 和 program_options。您需要对标准 STL 具备一定的了解才能充分理解本文的内容。本文中的所有代码都已使用 Boost 1.35 来进行了测试并使用 gcc-3.4.4 来进行了编译。

boost::tuple 类

有时,您希望 C++ 函数返回多个不相关的值。在推出 STL 之前,实现此目的的方法是创建所有不相关变量的结构,并以指针或引用的形式返回它们或作为参数传递给函数——但是任一种方法都不是表达程序员意图的方法。STL 引入了 pair,可将其用于聚合不相关的数据部分,但它一次只能处理两个数据对象。为了使用 int、char 和 float 的元组(tuple ),您可以按如下方式返回 pair:

make_pair<int, pair<char, float> > (3, make_pair<char, float> ('a', 0.9));
			

随着您添加更多的元素,创建元组结构将变得越来越困难。Boost tuple 类型派上了用场。要使用 boost::tuple,您必须包括头文件 tuple.hpp。要执行元组比较和元组 I/O,您需要分别包括 tuple_comparison.hpp 和 tuple_io.hpp。

第一个使用元组的程序

清单 1 使用 int、char 和 float 的元组并打印内容。


清单 1. 创建 Boost 元组并打印内容
                
#include <iostream>
#include <tuple.hpp>
#include <tuple_comparison.hpp>
#include <tuple_io.hpp>
using namespace boost;

int main ( )
  {
  tuple<int, char, float> t(2, 'a', 0.9);
  std::cout << t << std::endl;
  return 0;
  }
		  

此代码的输出为 (2 a 0.9)。请注意,<< 运算符重载 std::ostream,以便通过转储每个单独的 tuple 元素来输出元组。

与元组相关的重要事实

在使用元组时,务必牢记以下事实:

  • 能够形成元组的元素数量目前仅限于 10 个。
  • 元组可以包含用户定义的类类型,但是您必须负责确保那些类已经定义了正确的构造函数和拷贝构造函数 (copy constructor)。清单 2 显示了产生编译时错误的代码部分,因为该拷贝构造函数是私有的。

    清单 2. 用于元组的类必须具有正确的拷贝构造函数
                            
    #include <tuple.hpp>
    #include <tuple_comparison.hpp>
    #include <tuple_io.hpp>
    #include <iostream>
    using namespace std;
    
    class X
      {
      int x;
      X(const X& u) { x = u.x; }
      public:
        X(int y=5) : x(y) { }
      };
    
    int main ( )
      {
      boost::tuple<int, X> t(3, X(2));
      return 0;
      }
    		  

  • 与 STL 提供的 make_pair 函数非常类似,Boost 提供了 make_tuple 例程。要从函数返回元组,您必须调用 make_tuple。可以创建具有临时元素的元组;清单 3 的输出为 (4 0)。

    清单 3.使用 make_tuple 来从函数返回元组
                            
    #include <tuple.hpp>
    #include <tuple_comparison.hpp>
    #include <tuple_io.hpp>
    #include <iostream>
    using namespace std;
    
    boost::tuple<int, int>
    divide_and_modulo(int a, int b)
      {
      return boost::make_tuple<int, int> (a/b, a%b);
      }
    
    int main ( )
      {
      boost::tuple<int, int> t = divide_and_modulo(8, 2);
      cout << t << endl;
      return 0;
      }
    		  		  

  • 要访问元组的各个元素,您可以使用 get 例程。此例程具有两种变体,如清单 4 所示。请注意,还可以使用 get 例程来设置元组的各个元素,虽然有些编译器可能不支持此功能。

    清单 4. 使用 boost::get 例程
                            
    #include <tuple.hpp>
    #include <tuple_comparison.hpp>
    #include <tuple_io.hpp>
    #include <iostream>
    using namespace std;
    
    boost::tuple<int, int>
    divide_and_modulo(int a, int b)
      {
      return boost::make_tuple<int, int> (a/b, a%b);
      }
    
    int main ( )
      {
      boost::tuple<int, int> t = divide_and_modulo(8, 2);
      cout << t.get<0>() << endl; // prints 4
      cout << boost::get<1>(t) << endl; // prints 0
    
      boost::get<0>(t) = 9; // resets element 0 of t to 9
      ++boost::get<0>(t);   // increments element 0 of t
      cout << t.get<1>() << endl; // prints 10
      return 0;
      }

  • 可以使用 const 限定符来声明元组,在这种情况下,用于访问特定元素的 get 调用将返回对 const 的引用。不能对以这种方式访问的元素进行赋值(请参见清单 5)。

    清单 5. 使用 const 限定符来声明的元组不可修改
                            
    #include <tuple.hpp>
    #include <tuple_comparison.hpp>
    #include <tuple_io.hpp>
    #include <iostream>
    using namespace std;
    
    int main ( )
      {
      const boost::tuple<int, char*> t(8, "Hello World!");
      t.get<1>()[0] = "Y"; // error!
      boost::get<0>(t) = 9; // error!
      return 0;
      }

  • 可以使用关系运算符 ==、!=、<、>、<= 和 >= 对相同长度的元组进行比较。比较不同长度的元组会产生编译时错误。这些运算符的工作原理是从左到右地比较两个参与元组的每个单独的元素(请参见清单 6)。

    清单 6. 关系运算符与元组
                            
    #include <tuple.hpp>
    #include <tuple_comparison.hpp>
    #include <tuple_io.hpp>
    #include <iostream>
    #include <string>
    using namespace std;
    
    int main ( )
      {
      boost::tuple<int, string> t(8, string("Hello World!"));
      boost::tuple<int, string> t2(8, string("Hello World!"));
      cout << (t == t2) << endl;
    
      boost::tuple<int, string> r(9, string("Hello World!"));
      boost::tuple<int, string> r2(8, string("Hello World!"));
      cout << (r > r2) << endl;
    
      boost::tuple<string, string> q(string("AA"), string("BB"));
      boost::tuple<string, string> q2(string("AA"), string("CC"));
      cout << (q < q2) << endl;
    
      return 0;
      }

    清单 6 的输出为 1 1 1。请注意,如果您不是使用 string 或 int,而是使用没有定义 ==、!= 等运算符的用户定义的随机类,则会产生编译错误。




Boost 静态断言

断言是 C/C++ 中的防错性程序设计的一部分。最常见的用法如下:

assert(<some expression you expect to be true at this point in code>);
		    

assert 例程仅在调试模式下有效。在发布模式下,通常使用预处理器宏 ¨CDNDEBUG 来编译代码,其效果相当于 assert 不存在。静态断言建立在这个基本概念之上,只不过静态断言仅在编译时有效。此外,静态断言不生成任何代码。

例如,假设您在一个整型变量中执行某个位操作,并预期其大小为 4:这并非在所有操作系统平台上都是如此(请参见清单 7)。


清单 7. 使用 Boost 静态断言来验证变量的大小
                
#include <boost/static_assert.hpp>
int main ( )
  {
  BOOST_STATIC_ASSERT(sizeof(int) == 4);
   // … other code goes here
  return 0;
  }
		  

要使用 BOOST_STATIC_ASSERT 宏,您必须包括 static_assert.hpp 头文件。不需要诸如 DNDEBUG 等特定于编译器的选项,并且您不需要向链接器提供库——单凭该头文件就足够了。

如果断言有效,则代码将顺利编译。但是如果该假设无效,在某些 64 位平台上就可能是如此,则编译器将生成错误消息并停止。使用 g++-3.4.4 进行编译时的典型消息如下:

assert.cc: In function `int main()':
assert.cc:8: error: incomplete type `boost::STATIC_ASSERTION_FAILURE< false>' 
    used in nested name specifier
		  		  

这肯定不是最详细的错误消息,但是它指出了具有错误假设的函数和确切行号。

下面是一些典型的现实情景,您应该在其中考虑使用静态断言:

  • 静态声明的数组的边界检查
  • 验证原始和用户定义的变量的大小
  • 允许模板类或函数仅使用某些数据类型来进行实例化

Boost 静态断言的行为

您可以在类、函数或命名空间范围中使用 Boost 静态断言;还可以与模板一起使用它们。清单 8 中的示例阐明了概念。


清单 8. 使用 Boost 静态断言来限制类实例化
                
#include <iostream>
#include <static_assert.hpp>
using namespace std;
using namespace boost;

template<class T>
class A
  {
  private:
    T x, y;
    BOOST_STATIC_ASSERT(numeric_limits<T>::is_signed);
  public:
    A(T x1, T y1) : x(x1), y(y1) { }
  };

int main ( )
  {
  A<unsigned long> a(2, 1);
  return 0;
  }
		  

在清单 8 中,仅当 T 有符号时,模板类 A 才能进行实例化。类 numeric_limits 是标准命名空间的一部分;它检查基本类型在给定操作系统平台上的属性。在无符号(unsigned )的 long 类型的情况下,专用变体 numeric_limits<unsigned int> 的 is_signed 标志为 false。当您在类范围中使用 BOOST_STATIC_ASSERT 时,它是私有的、受保护的还是公开的并不重要。

清单 9 将 BOOST_STATIC_ASSERT 与函数结合在一起使用。该代码确保在函数 f1 中处理的类型只能是 A 类型或其派生类型。通过使用 Boost 的静态断言宏和 is_convertible 例程(在 boost/type_traits/is_convertible.hpp 中定义),此代码确保不希望的类型不会最终调用此例程。


清单 9. 将函数限制为仅处理特定的数据类型
                
#include <iostream>
#include <static_assert.hpp>
#include <boost/type_traits/is_convertible.hpp>
using namespace std;
using namespace boost;

struct A
  {
  int a;
  float b;
  };

struct B : public A
  {
  };

template <typename T>
int f1 (T y)
  {
  BOOST_STATIC_ASSERT((is_convertible<T, A*>::value));
  return 0;
  }

int main ( )
  {
  f1<B*> (new B);
  return 0;
  }
  





使用 Boost 库生成随机数

随机数生成用于各种各样的计算机应用,例如安全和游戏。UNIX 系统一般附带了随机数生成例程 rand 和 srand。通常,srand 使用新的种子值来初始化 rand(请参见清单 10)。


清单 10. 用于在传统 UNIX 中生成随机数的代码
                
#include <stdlib.h>
#include <stdio.h>

int main ( )
  {
  srand(time(NULL)); // this introduces randomness
  for (int i=0; i<10; i++)
    printf("%d\n", rand());
  return 0;
  }
		  		  		  

rand 例程返回一个介于 0 和 stdlib.h 中定义的 RAND_MAX 之间的数字。要了解 srand 所做的工作,可以在将 srand 例程注释掉的情况下编译清单 11。当您这样做时,您将观察到 rand 并不真正是随机的——可执行代码每次打印同一组值。为了在代码中引入随机性,您可以使用 srand,此例程使用种子值来初始化 rand。由于每次调用程序时的时间值是不同的,因此对于不同的调用,代码打印的值不同。

传统随机数生成器的问题
传统随机数生成器具有几个问题。它们无法生成在用户指定范围内均匀分布的随机数,或者符合高斯或二项式分布的随机数。本文仅在可将随机数映射到某个范围内的上下文中讨论随机数。

使用 Boost 随机数生成器

Boost 随机数生成器位于 boost/random 文件夹中。此外,为方便起见,boost/ 目录中的 random.hpp 头文件包括了 boost/random 文件夹中的所有其他头文件。

Boost 随机接口划分为两个部分:随机数生成器和随机数必须位于其中的分布。本文讨论 uniform_int 和 uniform_real random-number 分布以及 mt19937 随机数生成器。清单 11 使用了 uniform_int 和 uniform_real 分布。


清单 11. 将 variate_generator 与 mt19937 引擎和 uniform_int 分布一起使用
                
#include <iostream>
#include <boost/random.hpp>
using namespace std;
using namespace boost;

int main ( )
  {
  uniform_int<> distribution(1, 100) ;
  mt19937 engine ;
  variate_generator<mt19937, uniform_int<> > myrandom (engine, distribution);

  for (int i=0; i<100; ++i)
    cout << myrandom() << endl;

  return 0;
  }

此代码生成介于 1 和 100 之间(包括 1 和 100)的随机数;用于实现随机化的基础引擎是 mt19937。variate_generator 为您组合了该引擎和分布。

清单 12 使用了另一个引擎: kreutzer1986.


清单 12:组合 uniform_real 分布和 kreutzer1986 引擎
                
#include <iostream>
#include <boost/random.hpp>
using namespace std;
using namespace boost;

int main ( )
  {
  uniform_real<> distribution(1, 2) ;
  kreutzer1986 engine ;
  variate_generator<kreutzer1986, uniform_real<> > myrandom (engine, distribution);

  for (int i=0; i<100; ++i)
    cout << myrandom() << endl;

  return 0;
  }
		  

除了 uniform_int 和 uniform_real 分布以外,Boost 还提供了几个其他分布,包括二项式、泊松和正态分布。





boost::pool 库概述

Boost pool 库引入了可用于实现快速内存分配的工具。正确的内存块对齐可以得到保证。

根据 Boost 文档所述,当您分配和释放许多小型对象时,建议使用池。使用池的另一个不太明显的优点在于,作为程序员,您不必担心内存泄露:内存由 Boost 库在内部自动进行管理。要使用 pool 库,您不必在链接时提供特定的库——单凭头文件就足以完成链接了。

有多个接口对 pool 库可用:

  • 池接口——替代 malloc 进行工作的普通接口。要使用此接口,需要包括 boost/pool 文件夹中的 pool.hpp 头文件。
  • 对象池接口——有对象意识的接口,在对象创建和删除过程中分别相应地调用构造函数和析构函数。还可以使用此接口创建普通对象,而不调用它们的构造函数。接口定义是在位于 boost/pool 目录中的 object_pool.hpp 头文件中提供的。清单 13 引入了 pool 和 object_pool 接口。请注意以下几点:
    • pool 接口需要知道每个单独的元素而不是类型的大小,因为它是一个 malloc 风格的分配程序,不会调用构造函数。
    • pool 接口中的 malloc 例程返回 void*。
    • object-pool 接口需要类型信息,因为要调用构造函数。
    • object-pool 接口中的 malloc/construct 例程返回指向类型的指针。malloc 例程不调用构造函数,但是 construct 要调用构造函数。
    • 使用 pool 接口或 object-pool 接口来创建的元素的范围与从中创建它们的池的范围相同。
    • 要从池接口中释放内存,可以调用 purge_memory 方法。该方法释放您先前创建的内存块,并使得从分配程序例程返回的所有指针失效。
    • 要释放各个元素,可以调用 pool 接口中的 free 例程。例如,如果 t 是使用 pool 接口来创建的池,并且 m 是从 t 分配的指针,则 t.free(m) 将把内存返回给 t(将其添加到 t 的空闲内存列表)。

      清单 13. pool 和 object_pool 接口
                                  
      #include <iostream>
      #include <boost/pool/pool.hpp>
      #include <boost/pool/object_pool.hpp>
      using namespace std;
      using namespace boost;
      
      class A
        {
        public: A( ) { cout << "Declaring A\n"; }
               ~A( ) { cout << "Deleting A\n"; }
        };
      
      int main ( )
        {
        cout << "Init pool...\n";
      
        pool<> p(10 * sizeof(A));
        for (int i=0; i<10; ++i)
          A* a = (A*) p.malloc(); // Always returns sizeof(A)
        p.purge_memory();
      
        cout << "Init object pool...\n";
      
        object_pool<A> q;
        for (int i=0; i<10; ++i)
          A* a = q.construct(); // Calls A's constructor 10 times
      
        return 0;
        }
        

  • singleton_pool 接口——与 pool 接口几乎相同,但是用作独立池。独立池的底层结构具有为 malloc、free 等声明的静态成员函数,并且构造函数是私有的。独立池声明中的第一个参数称为标记——它允许存在不同的独立池集(例如,用于 int 的多个池,其中每个池服务于不同的目的)。必须包括 singleton_pool.hpp 头文件才能使用此接口。请参见清单 14。

    清单 14. singleton_pool 接口
                            
    #include <iostream>
    #include <boost/pool/singleton_pool.hpp>
    using namespace std;
    using namespace boost;
    
    struct intpool {  };
    struct intpool2 {  };
    
    typedef boost::singleton_pool<intpool, sizeof(int)> ipool1;
    typedef boost::singleton_pool<intpool2, sizeof(int)> ipool2;
    
    int main ( )
      {
      cout << "Init singleton pool...\n";
      for (int i=0; i<10; ++i) {
        int* q1 = (int*) ipool1::malloc();
        int* q2 = (int*) ipool2::malloc();
      }
    
      ipool1::purge_memory();
      ipool2::purge_memory();
      return 0;
      }
    		  

  • pool_alloc 接口——通常与 STL 容器结合在一起使用。请考虑以下代码片段:
    #include <boost/pool/pool_alloc.hpp>
    
    std::vector<int, boost::pool_allocator<int> > v;
    std::list<double, boost::fast_pool_allocator<double> > L;
    

    存在两个分配程序:pool_allocator 和 fast_pool_allocator。第一个分配程序是通用分配,可以满足针对任何数量的连续内存块的请求。fast_pool_allocator 最适合于一次请求单个(通常较大)块,但是也适用于通用分配,不过具有一些性能缺点。




boost::program_options 简介

命令行处理是另一个难点,开发人员通常不会采用结构化的方式来解决。其结果是从头到尾维护开销。Boost program_options 库提供了简化命令行处理的例程和数据结构。

清单 15 详细描述了 boost::program_options 的使用。这是建议在您的代码中使用的标准模板。


清单 15. 使用 boost::program_options
                
#include <string>
#include <iostream>
#include <boost/program_options.hpp>
using namespace std;

int main (int ac, char* av[])
  {
  boost::program_options::options_description options("command line options");
  options.add_options() ("help", "Use -h or --help to list all arguments")
                                      ("file", boost::program_options::value<string>(),
                                       "Provide input file name");
  boost::program_options::variables_map vmap;
  boost::program_options::store(
      boost::program_options::parse_command_line(ac, av, options), vmap);
  boost::program_options::notify(vmap);

  if (vmap.count("help")) {
      cout << options << endl;
  }

  return 0;
  }
		  

您必须包括 program_options.hpp 头文件。清单 15 的工作方式如下:

  1. options_description 类声明所有的有效命令行选项。
  2. 使用方法 add_options,您可以注册命令和跟在命令后面的参数类型。在此例中,help 选项不需要任何参数,但是 file 选项需要一个字符串参数。
  3. variables_map 类在运行时存储命令行选项及其参数。
  4. Boost 的 parse_command_line 例程解析 argc 和 argv 参数。store 和 notify 方法帮助存储 vmap 对象中的数据。
  5. 当您检查 help 是否为程序的恰当命令行选项(这是 vmap.count("help") 所做的工作)时,options 对象将被转储到 cout。这意味着运算符 << 是为 options_description 类定义的。

下面是来自清单 15 的输出:

[user@/home/user1] ./a.out --help
command line options:
  --help                Use -h or --help to list all arguments
  --file arg            Provide input file name
  

当您遇到其他选项时,可以采取进一步的操作。例如,下面的代码片段经过了修改,以打印您输入的文件名:

…
if (vmap.count("file")) {
     cout << "Setting input file to " << vmap["file"].as<string>() << ".\n";
 } else {
     cout << "No file specified\n";
 }
…

请注意,variable_map 类在许多方面与哈希表非常相似。例如,要检索 file 参数,您可以调用 vmap["file"]。





提供多个参数和缩写的命令选项

命令行处理通常同时需要同一个命令选项的短名称和长名称。此外,您通常必须多次使用某个选项,以便收集该选项的所有参数。例如,您可能希望使用 ¨Ch 和 ¨Chelp 来打印可用的命令。清单 16 演示了这些功能。


清单 16. 使用较短的选项变体并允许多次调用命令选项
                
#include <string>
#include <iostream>
#include <boost/program_options.hpp>
using namespace std;

int main (int ac, char* av[])
  {
  boost::program_options::options_description options("command line options");
  options.add_options() ("help,h", "Use -h or --help to list all arguments")
                    ("file", boost::program_options::value<vector<string> >( ),
                         "Provide input file name");
  boost::program_options::variables_map vmap;
  boost::program_options::store(
      boost::program_options::parse_command_line(ac, av, options), vmap);
  boost::program_options::notify(vmap);

  if (vmap.count("help")) {
      cout << options << endl;
  }

  if (vmap.count("file")) {
      vector<string> ifiles(vmap["file"].as< vector<string> > ());
      vector<string>::iterator vI;
      cout << "Number of input files: " << ifiles.size() << endl;
      cout << "Input file list: " << endl;
      for(vI = ifiles.begin(); vI != ifiles.end(); ++vI)
          cout << "\t" << *vI << endl;
  } else {
      cout << "No file specified\n";
  }

  return 0;
  }
		  

在使用 add_options 来添加命令选项时,较长和较短的选项之间使用逗号进行分隔。请注意,较长的选项 (help) 必须在较短的选项 (h) 之前,代码才能正常工作。与使用单个字符串不同,file 选项现在是使用一个字符串向量来定义的。如果指定了 ¨Cfile 选项多次,则会将在所有指定中收集到的命令选项参数存储在关联的 vector<string> 中。下面是使用不同的参数来多次指定 ¨Ch 和 ¨Cfile 所获得的输出:

[user@/home/user1] ./a.out -h
command line options:
  -h [ --help ]         Use -h or --help to list all arguments
  --file arg            Provide input file name

No file specified
[user@/home/user1] ./a.out --file abc --file pqr
Number of input files: 2
Input file list:
        abc
        pqr





解析位置选项

带输入参数但是不带命令行选项来调用某个程序是非常普遍的。您预期参数和命令行选项之间自动存在某种神奇关联。这种行为由 boost::program_options 提供支持。

请考虑清单 17。第一个参数转换为 --file=<first parameter>,第二个参数转换为 --do-file=<second parameter>。


清单 17. 将位置参数与命令行选项相关联
                
#include <string>
#include <iostream>
#include <boost/program_options.hpp>
using namespace std;

int main (int ac, char* av[])
  {
  boost::program_options::options_description options("command line options");
  options.add_options() ("help,h", "Use -h or --help to list all arguments")
                        ("file", boost::program_options::value<string>(),
                         "Provide input file name")
                        ("do-file", boost::program_options::value<string>(),
                         "Specify commands file");

  boost::program_options::variables_map vmap;
  boost::program_options::positional_options_description poptd;
  poptd.add("file", 1);
  poptd.add("do-file", 2);

  boost::program_options::store(
      boost::program_options::command_line_parser(ac, av).
      options(options).positional(poptd).run(), vmap);
  boost::program_options::notify(vmap);

  if (vmap.count("file")) {
     cout << "file: " << vmap["file"].as<string> ( ) << endl;
  }

  if (vmap.count("do-file")) {
     cout << "do-file: " << vmap["do-file"].as<string> ( ) << endl;
  }

  return 0;
  }
		  

下面是输出内容:

[user@/home/user1] ./a.out file1 dofile1
file: file1
do-file: dofile1

清单 15 中使用的某些 API 在清单 17 中已发生更改。清单 17 引入了新的类 positional_options_description。该类的 add 方法(add("command option", N))将位置 N 处的输入参数与命令行选项 "command option" 相关联。因此,./a.out file1 在内部解析为 ./a.out ¨Cfile=file1。另一个区别在于调用 program_options::store 方法的方式。与使用 parse_command_line 例程不同,Boost 库要求您将 command_line_parser 例程与 store 方法结合在一起使用。

请注意,仍然可以使用 ¨Cfile 和 ¨Cdo-file 选项来调用该程序。最后,若要将所有的输入参数与同一个命令行选项相关联,您需要使用值 -1 将该命令行选项添加到 positional_options_description 对象。下面是代码:

…
boost::program_options::positional_options_description poptd;
poptd.add("file", -1);
...

(责任编辑:A6)


时间:2008-09-09 14:47 来源:developerWorks 中国 作者:Arpan Sen 原文链接

好文,顶一下
(0)
0%
文章真差,踩一下
(0)
0%
------分隔线----------------------------


把开源带在你的身边-精美linux小纪念品
无觅相关文章插件,快速提升流量