• c++模板


    C++模板

      模板是C++支持参数化多态的工具,使用模板可以使用户为类或者函数声明一种一般模式,使得类中的某些数据成员或者成员函数的参数、返回值取得任意类型。

      模板是一种对类型进行参数化的工具;

      通常有两种形式:函数模板类模板

      函数模板针对仅参数类型不同的函数

      类模板针对仅数据成员成员函数类型不同的类。

      使用模板的目的就是能够让程序员编写与类型无关的代码。比如编写了一个交换两个整型int 类型的swap函数,这个函数就只能实现int 型,对double,字符这些类型无法实现,要实现这些类型的交换就要重新编写另一个swap函数。使用模板的目的就是要让这程序的实现与类型无关,比如一个swap模板函数,即可以实现int 型,又可以实现double型的交换。模板可以应用于函数和类。下面分别介绍。

      注意:模板的声明或定义只能在全局,命名空间或类范围内进行。即不能在局部范围,函数内进行,比如不能在main函数中声明或定义一个模板。

    一、函数模板通式


    1、函数模板的格式:

        template <class 形参名class 形参名,......> 返回类型 函数名(参数列表)

       {

          函数体

       }

      其中templateclass是关见字,class可以用typename 关见字代替,在这里typename 和class没区别,<>括号中的参数叫模板形参,模板形参和函数形参很相像,模板形参不能为空一但声明了模板函数就可以用模板函数的形参名声明类中的成员变量和成员函数,即可以在该函数中使用内置类型的地方都可以使用模板形参名。模板形参需要调用该模板函数时提供的模板实参来初始化模板形参,一旦编译器确定了实际的模板实参类型就称他实例化了函数模板的一个实例。比如swap的模板函数形式为

          template <class T> void swap(T& a, T& b){},

    当调用这样的模板函数时类型T就会被被调用时的类型所代替,比如swap(a,b)其中abint 型,这时模板函数swap中的形参T就会被int 所代替,模板函数就变为swap(int &a, int &b)。而当swap(c,d)其中cddouble类型时,模板函数会被替换为swap(double &a, double &b),这样就实现了函数的实现与类型无关的代码。

      2、注意:对于函数模板而言不存在 h(int,int) 这样的调用,不能在函数调用的参数中指定模板形参的类型,对函数模板的调用应使用实参推演来进行,即只能进行 h(2,3) 这样的调用,或者int a, b; h(a,b)

      函数模板的示例演示将在下文中涉及!

    二、类模板通式


      1、类模板的格式为:

        template<class 形参名class 形参名> class 类名

        { ... };

      类模板和函数模板都是以template开始后接模板形参列表组成,模板形参不能为空,一但声明了类模板就可以用类模板的形参名声明类中的成员变量和成员函数,即可以在类中使用内置类型的地方都可以使用模板形参名来声明。比如

        template<class T> class A{public: T a; T b; T hy(T c, T &d);};

    在类A中声明了两个类型为T的成员变量ab,还声明了一个返回类型为T带两个参数类型为T的函数hy

      2、类模板对象的创建:比如一个模板类A,则使用类模板创建对象的方法为A<int> m;在类A后面跟上一个<>尖括号并在里面填上相应的类型,这样的话类A中凡是用到模板形参的地方都会被int 所代替。当类模板有两个模板形参时创建对象的方法为A<int, double> m;类型之间用逗号隔开。

      3、对于类模板,模板形参的类型必须在类名后的尖括号中明确指定。比如A<2> m;用这种方法把模板形参设置为int是错误的(编译错误:error C2079: 'a' uses undefined class 'A<int>'),类模板形参不存在实参推演的问题。也就是说不能把整型值2推演为int 型传递给模板形参。要把类模板形参调置为int 型必须这样指定A<int> m

      4、在类模板外部定义成员函数的方法为:

        template<模板形参列表> 函数返回类型 类名<模板形参名>::函数名(参数列表){函数体},

    比如有两个模板形参T1T2的类A中含有一个void h()函数,则定义该函数的语法为:

        template<class T1,class T2> void A<T1,T2>::h(){}。

    注意:当在类外面定义类的成员时template后面的模板形参应与要定义的类的模板形参一致。

      5、再次提醒注意:模板的声明或定义只能在全局,命名空间或类范围内进行。即不能在局部范围,函数内进行,比如不能在main函数中声明或定义一个模板。

    三、模板的形参


    有三种类型的模板形参:类型形参,非类型形参和模板形参。

      1、类型形参

        1.1 、类型模板形参:类型形参由关见字class或typename后接说明符构成,如template<class T> void h(T a){};其中T就是一个类型形参,类型形参的名字由用户自已确定。模板形参表示的是一个未知的类型。模板类型形参可作为类型说明符用在模板中的任何地方,与内置类型说明符或类类型说明符的使用方式完全相同,即可以用于指定返回类型,变量声明等。

        作者原版:1.2、 不能为同一个模板类型形参指定两种不同的类型,比如template<class T>void h(T a, T b){},语句调用h(2, 3.2)将出错,因为该语句给同一模板形参T指定了两种类型,第一个实参2把模板形参T指定为int,而第二个实参3.2把模板形参指定为double,两种类型的形参不一致,会出错。(针对函数模板)

        作者原版:1.2针对函数模板是正确的,但是忽略了类模板。下面将对类模板的情况进行补充。

        本人添加1.2补充版(针对于类模板)、当我们声明类对象为:A<int> a,比如template<class T>T g(T a, T b){},语句调用a.g(2, 3.2)在编译时不会出错,但会有警告,因为在声明类对象的时候已经将T转换为int类型,而第二个实参3.2把模板形参指定为double,在运行时,会对3.2进行强制类型转换为3。当我们声明类的对象为:A<double> a,此时就不会有上述的警告,因为从intdouble是自动类型转换。

    演示示例1:

      TemplateDemo.h

    复制代码
     1 #ifndef TEMPLATE_DEMO_HXX
     2 #define TEMPLATE_DEMO_HXX
     3 
     4 template<class T> class A{
     5     public:
     6         T g(T a,T b);
     7         A();
     8 };
     9 
    10 #endif
    复制代码

      TemplateDemo.cpp

    复制代码
     1 #include<iostream.h>
     2 #include "TemplateDemo.h"
     3 
     4 template<class T> A<T>::A(){}
     5 
     6 template<class T> T A<T>::g(T a,T b){
     7     return a+b;
     8 }
     9 
    10 void main(){
    11     A<int> a;
    12     cout<<a.g(2,3.2)<<endl;
    13 }
    复制代码

      编译结果:

    复制代码
    1 --------------------Configuration: TemplateDemo - Win32 Debug--------------------
    2 Compiling...
    3 TemplateDemo.cpp
    4 G:C++CDaimaTemplateDemoTemplateDemo.cpp(12) : warning C4244: 'argument' : conversion from 'const double' to 'int', possible loss of data
    5 
    6 TemplateDemo.obj - 0 error(s), 1 warning(s)
    复制代码

      运行结果:      5  

      我们从上面的测试示例中可以看出,并非作者原作中的那么严密!此处仅是本人跟人测试结果!请大家本着实事求是的态度,自行验证!

      2、非类型形参

        2.1 、非类型模板形参:模板的非类型形参也就是内置类型形参,如template<class T, int a> class B{};其中int a就是非类型的模板形参。

        2.2、 非类型形参在模板定义的内部是常量值,也就是说非类型形参在模板的内部是常量。

        2.3、 非类型模板的形参只能是整型,指针和引用,像double,String, String **这样的类型是不允许的。但是double &,double *,对象的引用或指针是正确的。

        2.4、 调用非类型模板形参的实参必须是一个常量表达式,即他必须能在编译时计算出结果。

        2.5 、注意:任何局部对象,局部变量,局部对象的地址,局部变量的地址都不是一个常量表达式,都不能用作非类型模板形参的实参。全局指针类型,全局变量,全局对象也不是一个常量表达式,不能用作非类型模板形参的实参。

        2.6、 全局变量的地址或引用,全局对象的地址或引用const类型变量是常量表达式,可以用作非类型模板形参的实参

        2.7 、sizeof表达式的结果是一个常量表达式,也能用作非类型模板形参的实参。

        2.8 、当模板的形参是整型时调用该模板时的实参必须是整型的,且在编译期间是常量,比如template <class T, int a> class A{};如果有int b,这时A<int, b> m;将出错,因为b不是常量,如果const int b,这时A<int, b> m;就是正确的,因为这时b是常量。

        2.9 、非类型形参一般不应用于函数模板中,比如有函数模板template<class T, int a> void h(T b){},若使用h(2)调用会出现无法为非类型形参a推演出参数的错误,对这种模板函数可以用显示模板实参来解决,如用h<int, 3>(2)这样就把非类型形参a设置为整数3。显示模板实参在后面介绍。

        2.10、 非类型模板形参的形参和实参间所允许的转换
          1、允许从数组到指针,从函数到指针的转换。如:template <int *a> class A{}; int b[1]; A<b> m;即数组到指针的转换
          2、const修饰符的转换。如:template<const int *a> class A{}; int b; A<&b> m; 即从int *到const int *的转换。
          3、提升转换。如:template<int a> class A{}; const short b=2; A<b> m; 即从short到int 的提升转换
          4、整值转换。如:template<unsigned int a> class A{}; A<3> m; 即从int 到unsigned int的转换。
          5、常规转换。

    非类型形参演示示例1:

      由用户自己亲自指定栈的大小,并实现栈的相关操作。

      TemplateDemo.h

    复制代码
     1 #ifndef TEMPLATE_DEMO_HXX
     2 #define TEMPLATE_DEMO_HXX
     3 
     4 template<class T,int MAXSIZE> class Stack{//MAXSIZE由用户创建对象时自行设置
     5     private:
     6         T elems[MAXSIZE];    // 包含元素的数组
     7         int numElems;    // 元素的当前总个数
     8     public:
     9         Stack();    //构造函数
    10         void push(T const&);    //压入元素
    11         void pop();        //弹出元素
    12         T top() const;    //返回栈顶元素
    13         bool empty() const{     // 返回栈是否为空
    14             return numElems == 0;
    15         }
    16         bool full() const{    // 返回栈是否已满
    17             return numElems == MAXSIZE;
    18         }
    19 };
    20 
    21 template <class T,int MAXSIZE> 
    22 Stack<T,MAXSIZE>::Stack():numElems(0){     // 初始时栈不含元素
    23     // 不做任何事情
    24 }
    25 
    26 template <class T,int MAXSIZE>
    27 void Stack<T, MAXSIZE>::push(T const& elem){
    28     if(numElems == MAXSIZE){
    29         throw std::out_of_range("Stack<>::push(): stack is full");
    30     }
    31     elems[numElems] = elem;   // 附加元素
    32     ++numElems;               // 增加元素的个数
    33 }
    34 
    35 template<class T,int MAXSIZE>
    36 void Stack<T,MAXSIZE>::pop(){
    37     if (numElems <= 0) {
    38         throw std::out_of_range("Stack<>::pop(): empty stack");
    39     }
    40     --numElems;               // 减少元素的个数
    41 }
    42 
    43 template <class T,int MAXSIZE>
    44 T Stack<T,MAXSIZE>::top()const{
    45     if (numElems <= 0) {
    46         throw std::out_of_range("Stack<>::top(): empty stack");
    47     }
    48     return elems[numElems-1];  // 返回最后一个元素
    49 }
    50 
    51 #endif
    复制代码

      TemplateDemo.cpp

    复制代码
     1 #include<iostream.h>
     2 #include <iostream>
     3 #include <string>
     4 #include <cstdlib>
     5 #include "TemplateDemo.h"
     6 
     7 int main(){
     8     try {
     9         Stack<int,20>  int20Stack;  // 可以存储20个int元素的栈
    10         Stack<int,40>  int40Stack;  // 可以存储40个int元素的栈
    11         Stack<std::string,40> stringStack; // 可存储40个string元素的栈
    12 
    13         // 使用可存储20个int元素的栈
    14         int20Stack.push(7);
    15         std::cout << int20Stack.top() << std::endl;    //7
    16         int20Stack.pop();
    17 
    18         // 使用可存储40个string的栈
    19         stringStack.push("hello");
    20         std::cout << stringStack.top() << std::endl;    //hello
    21         stringStack.pop();    
    22         stringStack.pop();    //Exception: Stack<>::pop<>: empty stack
    23         return 0;
    24     }
    25     catch (std::exception const& ex) {
    26         std::cerr << "Exception: " << ex.what() << std::endl;
    27         return EXIT_FAILURE;  // 退出程序且有ERROR标记
    28     }
    29 }
    复制代码

      运行结果:

        

    非类型形参演示示例2:

      TemplateDemo01.h

    复制代码
     1 #ifndef TEMPLATE_DEMO_O1
     2 #define TEMPLATE_DEMO_01
     3 
     4 template<typename T> class CompareDemo{
     5     public:
     6         int compare(const T&, const T&);
     7 };
     8 
     9 template<typename T> 
    10 int CompareDemo<T>::compare(const T& a,const T& b){
    11     if((a-b)>0)
    12         return 1;
    13     else if((a-b)<0)
    14         return -1;
    15     else
    16         return 0;
    17 }
    18 
    19 #endif
    复制代码

      TemplateDemo01.cpp

    复制代码
    1 #include<iostream.h>
    2 #include "TemplateDemo01.h"
    3 
    4 void main(){
    5     CompareDemo<int> cd;
    6     cout<<cd.compare(2,3)<<endl;
    7 }
    复制代码

      运行结果:     -1 

    复制代码
    1 #include<iostream.h>
    2 #include "TemplateDemo01.h"
    3 
    4 void main(){
    5     CompareDemo<double> cd;
    6     cout<<cd.compare(3.2,3.1)<<endl;
    7 }
    复制代码

      运行结果:      1  

      TemplateDemo01.h 改动如下:

    复制代码
     1 #ifndef TEMPLATE_DEMO_O1
     2 #define TEMPLATE_DEMO_01
     3 
     4 template<typename T> class CompareDemo{
     5     public:
     6         int compare(T&, T&);
     7 };
     8 
     9 template<typename T> 
    10 int CompareDemo<T>::compare(T& a,T& b){
    11     if((a-b)>0)
    12         return 1;
    13     else if((a-b)<0)
    14         return -1;
    15     else
    16         return 0;
    17 }
    18 
    19 #endif
    复制代码

      TempalteDemo01.cpp

    复制代码
    1 #include<iostream.h>
    2 #include "TemplateDemo01.h"
    3 
    4 void main(){
    5     CompareDemo<int> cd;
    6     int a=2,b=3;
    7     cout<<cd.compare(a,b)<<endl;
    8 }
    复制代码

    非类型形参演示示例3:

      TemplateDemo02.cpp

    复制代码
     1 #include<iostream.h>
     2 
     3 template<typename T>
     4 const T& max(const T& a,const T& b){
     5     return a>b ? a:b;
     6 }
     7 
     8 void main(){
     9     cout<<max(2.1,2.2)<<endl;//模板实参被隐式推演成double
    10     cout<<max<double>(2.1,2.2)<<endl;//显示指定模板参数。
    11     cout<<max<int>(2.1,2.2)<<endl;//显示指定的模板参数,会将函数函数直接转换为int。
    12 }
    复制代码

      运行结果:

           

    cout<<max<int>(2.1,2.2)<<endl;//显示指定的模板参数,会将函数函数直接转换为int。此语句会出现警告:
    复制代码
    1 --------------------Configuration: TemplateDemo02 - Win32 Debug--------------------
    2 Compiling...
    3 TemplateDemo02.cpp
    4 G:C++CDaimaTemplateDemo02TemplateDemo02.cpp(11) : 
      warning C4244: 'argument' : conversion from 'const double' to 'const int', possible loss of data 5 G:C++CDaimaTemplateDemo02TemplateDemo02.cpp(11) :
      warning C4244: 'argument' : conversion from 'const double' to 'const int', possible loss of data 6 7 TemplateDemo02.obj - 0 error(s), 2 warning(s)
    复制代码
     
     
     

    1. 模板的概念。

    我们已经学过重载(Overloading),对重载函数而言,C++的检查机制能通过函数参数的不同及所属类的不同。正确的调用重载函数。例如,为求两个数的最大值,我们定义MAX()函数需要对不同的数据类型分别定义不同重载(Overload)版本。

    //函数1.

    int max(int x,int y);
    {return(x>y)?x:y ;}

    //函数2.
    float max(
    float x,float y){
    return (x>y)? x:y ;}

    //函数3.
    double max(
    double x,double y)
    {return (c>y)? x:y ;}

    但如果在主函数中,我们分别定义了 char a,b; 那么在执行max(a,b);时 程序就会出错,因为我们没有定义char类型的重载版本。

    现在,我们再重新审视上述的max()函数,它们都具有同样的功能,即求两个数的最大值,能否只写一套代码解决这个问题呢?这样就会避免因重载函数定义不 全面而带来的调用错误。为解决上述问题C++引入模板机制,模板定义:模板就是实现代码重用机制的一种工具,它可以实现类型参数化,即把类型定义为参数, 从而实现了真正的代码可重用性。模版可以分为两类,一个是函数模版,另外一个是类模版。

    2. 函数模板的写法

    函数模板的一般形式如下:

    Template <class或者也可以用typename T>

    返回类型 函数名(形参表)
    {//函数定义体 }

    说明: template是一个声明模板的关键字,表示声明一个模板关键字class不能省略,如果类型形参多余一个 ,每个形参前都要加class <类型 形参表>可以包含基本数据类型可以包含类类型.

    请看以下程序:

    //Test.cpp

    #include <iostream>

    using std::cout;

    using std::endl;

    //声明一个函数模版,用来比较输入的两个相同数据类型的参数的大小,class也可以被typename代替,

    //T可以被任何字母或者数字代替。

    template <class T>

    T min(T x,T y)

    { return(x<y)?x:y;}

    void main( )

    {

    int n1=2,n2=10;

    double d1=1.5,d2=5.6;

    cout<< "较小整数:"<<min(n1,n2)<<endl;

    cout<< "较小实数:"<<min(d1,d2)<<endl;

    system("PAUSE");

    }

    程序运行结果:

     

    程序分析:main()函数中定义了两个整型变量n1 , n2 两个双精度类型变量d1 , d2然后调用min( n1, n2); 即实例化函数模板T min(T x, T y)其中T为int型,求出n1,n2中的最小值.同理调用min(d1,d2)时,求出d1,d2中的最小值.

    3. 类模板的写法

    定义一个类模板:

    Template < class或者也可以用typename T >
    class类名{
    //类定义......
    };

    说明:其中,template是声明各模板的关键字,表示声明一个模板,模板参数可以是一个,也可以是多个。

    例如:定义一个类模板:

    // ClassTemplate.h
    #ifndef ClassTemplate_HH

    #define ClassTemplate_HH

    template<typename T1,typename T2>

    class myClass{

    private:

    T1 I;

    T2 J;

    public:

    myClass(T1 a, T2 b);//Constructor

    void show();

    };

    //这是构造函数

    //注意这些格式

    template <typename T1,typename T2>

    myClass<T1,T2>::myClass(T1 a,T2 b):I(a),J(b){}

    //这是void show();

    template <typename T1,typename T2>

    void myClass<T1,T2>::show()

    {

    cout<<"I="<<I<<", J="<<J<<endl;

    }

    #endif

    // Test.cpp

    #include <iostream>

    #include "ClassTemplate.h"

    using std::cout;

    using std::endl;

    void main()

    {

    myClass<int,int> class1(3,5);

    class1.show();

    myClass<int,char> class2(3,'a');

    class2.show();

    myClass<double,int> class3(2.9,10);

    class3.show();

    system("PAUSE");

    }

    最后结果显示:

    4.非类型模版参数

    一般来说,非类型模板参数可以是常整数(包括枚举)或者指向外部链接对象的指针。

    那么就是说,浮点数是不行的,指向内部链接对象的指针是不行的。


    template<typename T, int MAXSIZE>

    class Stack{

    Private:

    T elems[MAXSIZE];

    };

    Int main()

    {

    Stack<int, 20> int20Stack;

    Stack<int, 40> int40Stack;

    };

    5.使用模板类型

    有时模板类型是一个容器或类,要使用该类型下的类型可以直接调用,以下是一个可打印STL中顺序和链的容器的模板函数

    template <typename T>
    void print(T v)
    {
    T::iterator itor;
    for (itor = v.begin(); itor != v.end(); ++itor)
    {
    cout << *itor << " ";
    }
    cout << endl;
    }

    void main(int argc, char **argv){
    list<int> l;
    l.push_back(1);
    l.push_front(2);
    if(!l.empty())
    print(l);
    vector<int> vec;
    vec.push_back(1);
    vec.push_back(6);
    if(!vec.empty())
    print(vec);
    }

    打印结果

    类型推导的隐式类型转换
    在决定模板参数类型前,编译器执行下列隐式类型转换:

    左值变换
    修饰字转换
    派生类到基类的转换

    见《C++ Primer》([注2],P500)对此主题的完备讨论。

    简而言之,编译器削弱了某些类型属性,例如我们例子中的引用类型的左值属性。举例来说,编译器用值类型实例化函数模板,而不是用相应的引用类型。

    同样地,它用指针类型实例化函数模板,而不是相应的数组类型。

    它去除const修饰,绝不会用const类型实例化函数模板,总是用相应的非 const类型,不过对于指针来说,指针和 const 指针是不同的类型。

    底线是:自动模板参数推导包含类型转换,并且在编译器自动决定模板参数时某些类型属性将丢失。这些类型属性可以在使用显式函数模板参数申明时得以保留。


    6. 模板的特化

    如果我们打算给模板函数(类)的某个特定类型写一个函数,就需要用到模板的特化,比如我们打算用 long 类型调用 max 的时候,返回小的值(原谅我举了不恰当的例子):
    template<> // 这代表了下面是一个模板函数
    long max<long>( long a, long b ) // 对于 vc 来说,这里的 <long> 是可以省略的
    {
    return a > b ? b : a;
    }
    实际上,所谓特化,就是代替编译器完成了对指定类型的特化工作,现代的模板库中,大量的使用了这个技巧。
    对于偏特化,则只针对模板类型中部分类型进行特化,如

    template<T1, T2>

    class MyClass;

    template<T1, T2>

    class MyCalss<int, T2>//偏特化
    7. 仿函数
    仿函数这个词经常会出现在模板库里(比如 STL),那么什么是仿函数呢?
    顾名思义:仿函数就是能像函数一样工作的东西,请原谅我用东西这样一个代词,下面我会慢慢解释。
    void dosome( int i )
    这个 dosome 是一个函数,我们可以这样来使用它: dosome(5);
    那么,有什么东西可以像这样工作么?
    答案1:重载了 () 操作符的对象,因此,这里需要明确两点:
      1 仿函数不是函数,它是个类;
      2 仿函数重载了()运算符,使得它的对你可以像函数那样子调用(代码的形式好像是在调用比如:
    struct DoSome
    {
    void operator()( int i );
    }
    DoSome dosome;
    这里类(对 C++ 来说,struct 和类是相同的) 重载了 () 操作符,因此它的实例 dosome 可以这样用 dosome(5); 和上面的函数调用一模一样,不是么?所以 dosome 就是一个仿函数了。

    实际上还有答案2:
    函数指针指向的对象。
    typedef void( *DoSomePtr )( int );
    typedef void( DoSome )( int );
    DoSomePtr *ptr=&func;
    DoSome& dosome=*ptr;

    dosome(5); // 这里又和函数调用一模一样了。
    当然,答案3 成员函数指针指向的成员函数就是意料之中的答案了。

    8. 仿函数的用处
    不管是对象还是函数指针等等,它们都是可以被作为参数传递,或者被作为变量保存的。因此我们就可以把一个仿函数传递给一个函数,由这个函数根据需要来调用这个仿函数(有点类似回调)。
    STL 模板库中,大量使用了这种技巧,来实现库的“灵活”。
    比如:
    for_each, 它的源代码大致如下:
    template< typename Iterator, typename Functor >
    void for_each( Iterator begin, Iterator end, Fucntor func )
    {
    for( ; begin!=end; begin++ )
    func( *begin );
    }

    这个 for 循环遍历了容器中的每一个元素,对每个元素调用了仿函数 func,这样就实现了 对“每个元素做同样的事”这样一种编程的思想。

    特别的,如果仿函数是一个对象,这个对象是可以有成员变量的,这就让 仿函数有了“状态”,从而实现了更高的灵活性。

  • 相关阅读:
    改造vant日期选择
    css3元素垂直居中
    npm综合
    (转)网页加水印方法
    Mac下IDEA自带MAVEN插件的全局环境配置
    隐藏注册控件窗口
    High performance optimization and acceleration for randomWalk, deepwalk, node2vec (Python)
    How to add conda env into jupyter notebook installed by pip
    The Power of WordNet and How to Use It in Python
    背单词app测评,2018年
  • 原文地址:https://www.cnblogs.com/hdk1993/p/5857171.html
Copyright © 2020-2023  润新知