• 输入输出


    C++语言本身并不包含输入输出功能,但C++标准库提供了一套用于输入和输出的类库

    在C++的输入输出系统中,最核心的对象是流,一个流就是一个字节序列,流的操作包括对流的读和写

     

    C++输入输出库定义了两个公共基类:

    ios_base,描述了流的基本性质,但不考虑流的字符集

    basic_ios,描述了流的基本性质,而且描述了流的字符集

    之所以为基本流操作设计两个类,是为了执行代码最小化(与模板类相比,非模板类将产生更小的代码)

    派生类basic_istream和basic_ostream都是从公共基类ios_base和basic_ios继承了一些成员

    在这些继承而来的数据成员中,大部分用来描述流的属性或特征。例如,其中一个数据成员用于描述流的格式属性,如是否输出进行左对齐;另一个数据成员存储量浮点数的精确位;还有一个数据成员用于描述流状态,比如是否遇到了文件结束标志。

    basic_istream增加了而在流中读取和移动数据的方法。并且对操作符>>就行了重载,用于内建数据类型的输入操作

    basic_ostream增加了在流中写入数据的方法,并且重载了<<

    basic_iostream对basic_istream和basic_ostream进行了多重派生,因而它既继承了读取流的操作,又继承了写入流的操作。basic_iostream没有增加额外的数据成员,而且除了构造和析构函数外,也没有增加其他成员函数

     

    输入输出类库提供的输入输出功能是  带缓冲的输入输出 ,也就是说,数据并不是直接的读入和写出,而是铜鼓偶一个 缓冲区 进行的

     

    通常换行符的出现,将带动一次清扫操作(将缓冲区中的额数据写的输出流中)

    basic_streambuf作为基类派生出一些特殊的缓冲类,如basic_filebuf和basic_stringbuf。它包含了读、写缓冲区及移动数据的方法,因为这种读写操作直接对缓冲区进行,它们将其称为 底层输入输出方法,而将basic_istream和basic_ostream中的输入输出方法称为 高层输入输出方法,因为这些方法不直接访问流,而是通过调用底层输入输出间接地访问流

     

    basic_streambuf包含了用于处理上溢的虚函数overflow和处理下溢的虚函数underflow,上溢发生于对已满的缓冲区进行些操作时,下溢发生在对已空的缓冲区进行读操作时。派生类应覆盖这两个方法,已完成上下溢的处理操作

     

    将缓冲类层次用流类层次独立出来的原因之一是使高层输入输出功能独立于底层输入输出功能。

    如果将这两个类层次合并为一个类层次,那么某些类就可能需要同时提供高层和底层的输入输出方法

     

    缓冲类和流类一起组成了C++输入输出类层次,从输入输出操作的对象来看,C++输入输出类层次又可分为三个部分:

    与操作对象无关的通用类集合   basic_streambuf、ios_base、basic_ios、basic_istream、basic_ostream和basic_iostream

    读写字符的字符流类集合        basic_stringbuf、basic_istringstream、basic_ostringstream和basic_stringstream

    读写文件的文件流类集合        basic_filebuf、basic_ifilestream、basic_ofilestream和basic_filestream

     

    basic_ifstream由basic_istream派生而来,增加了打开和关闭文件操作,这些函数具体实现依赖于basic_filebuf,所以basic_ifstream添加了一个basic_filebuf的数据成员

    同理basic_ofstream由basic_ostream派生,增加了打开关闭函数,添加了basic_filebuf成员

    basic_fstream由basic_iostream派生,增加了打开关闭函数,增加了basic_filebuf成员

     

     

    下面是摘录了一段iosfwd文件中的代码:

     

    任何数据集最终都可以编码为比特流,因此输入输出都可以看做是流

    C++输入输出类都拥有某个缓冲区对象作为其数据成员

     

     

     

    除缓冲区外,ios_base是所有其他标准输入输出类的直接或间接基类

     ios_base标志和成员:

     

    带一个参数的setf成员函数设置特定格式,而不改变其他标志:

       cout.setf(ios_base::showbase);不好,改变了其他标志

      cout.flags(cout.flags() | ios_base::showbase );

    设置单一标志:

     

     

    指定数据基数,并用大写字母十六进制表示:

     

     对齐:

     

    填充:

     

    float定点和科学输出:

    默认float精度为6

     

    清除指定的标志:

      eg: cin.unsetf(ios_base::skipws);

    清除skipws标志,使得不忽略空格(默认情况下忽略空格)。使用flags:

      cin.flags(cin.flags() & ~ios_base::skipws);

     

    流状态标志:

     

    打开流状态标志:

     

    ostream的构造函数预先设置了ios_base::out标志,其他用户指定的标志与这个标志连接起来:

    ostream fout("out.dat",ios_base::app);

     

    下面列出3个seek操作的标志:

     

    输出流basic_ostream定义了seekp方法。

    ios_base定义了一个 静态 成员函数 sync_with_stdio,用来同步标准C++输入输出库函数和C输入输出库函数

     

    basic_ios的一些成员函数:

     

    例子:

     

    在某些系统中,将一个控制字符作为文件结束标志。为了在收到EOF标志后仍能从标准输入接受数据,某些系统需要跳过EOF标志。

      cin.clear();

    清除所有的输入输入状态标志,包括EOF

     

    basic_ios重载了操作符!并提供了basic_ios到void *类型的转换

    template<class charT,class traints = char_trains<charT>>

    class basic_ios : public ios_base {

      // ...

      operator void *() const;

      bool operator!() const;

      // ...

    };

    如果设置了failbit或badbit,则operator void*() 返回0(false),否则返回非0(true)。

     

    ios_base类拥有成员函数fill,若不带参数调用fill,fill返回当前填充字符;若带参数则fill设置填充字符,然后返回原先的填充字符。

    basic_ios::char_type old_fill = cout.fill('0');

    // write to standard output

    cout.fill(old_fill);

     

    类basic_ios中的exceptions函数用于在输入输出操作时抛出例外,例外的抛出可依据某种情况进行,例如eofbit、badbit、failbit或它们的组合。当设置指定的标志时,抛出一个ios_base::failure类型的例外;可将goodbit作为exceptions的参数,用来解除输入输出例外抛出机制;如果调用exceptions时不带参数,exceptions返回当前的输入输出状态标志

     

     

     

     

     

     

    basic_istream是模板类,从basic_ios派生而来,它实现了get函数的多个重载版本,以支持不同的调用方式。

    其中之一:

      basic_istream<charT,traints> & get(char_type &c);

    调用该函数后,无论下一个字符是否是空格,都读入c,并返回更新后的流对象;如果字符没有读入,设置流对象的failbit标志

     

    另一个get重载是:

    int_type get();

    调用该函数时,同样读入下一个字符(包括空格),然后返回该字符,若没有读到字符,get函数返回该流定义的文件结束标志(若字符类型为char,则为EOF),并设置流对象的failbit标志,类似于C语言中的fgetc函数。int_type是用typedef定义的某种整数类型

     

    basic_istream还包含一个用来读入整行信息的成员getline:

    basic_istream<charT,trains> & getline(char_type *b,streamsize s,char_type d);

    getline有三个变量,读入信息放到字符数组b,希望读入的字符长度s,end-of-line标志d。如果默认,则将换行符作为end-of-line标志。streamsize用typedef定义的某种整数类型。

    getline函数读入字符直到下面三种情况之一:

    1,遇到文件结束标志     --->  设置流对象的eofbit标志

    2,遇到行结束标志       ------> 不会将该标志(行结束标志)存入数组

    3,已经读取了s-1个字符  -----> 设置failbit标志

    在读取完后,getline函数添加一个null结束符数组中,形成一个C语言风格的字符串然后返回流对象

    getline不会读入超过s-1个字符

    basic_istream还有以下:

    basic_istream<charT,traints>&read(char_type *a,streamsize n);

    int_type peek();   返回流中下一个字符

    basic_istream<charT,traits>& putback(char_type c); 将字符送入流中,(类似C中ungetc())

    basic_istream<charT,traits> &ignore(int count=1,int_type stop);  从流中移出count个字符或所有字符,直到遇到stop标志,stop默认为end-of-file,移走的字符不存储,就是简单的丢弃。

    streamsize  gcount() const 返回上一次用未格式化的输入成员函数读取的字符总数

    有两种不同的流位置标志,分别用于输入和输出。成员函数seekg和tellg定位到输入流的某个位置(seekg中的g是指get)或读取当前位置;而seekp和tellp定位到输出流的某个位置(seekp中p值普通)或读取当前位置。这些函数类似C中的fseek和ftell

    seekg成员又有两种形式,其中一个时:

    basic_istream<charT,traits>& seekg(off_type off,ios_base::seek_dir dir); (ios_base::beg   ios_base::cur  ios_base::end) 要以二进制打开

    pos_type tellg()       返回输入流的当前位置

    basic_istream<charT,traits>& seekg(pos_type pos);

    该函数返回到输入流的pos位置。

    basic_istream重载了>>操作符

    charT在模板参数中指字符类型

     

    basic_ostream

    basic_ostream<charT,traits>& put(char_type);

     

     

    basic_ostream<charT,traits> &write(const char_type *a,streamsize m);
    basic_ostream<charT,traits>&seekp(pos_type);
    basic_ostream<charT,traits>&seekp(off_type,ios_base::seekdir); 要以二进制打开
    pos_type tellp();
    basic_ostream<charT,traits> & flush();
    basic_ostream<charT,traits> & operator<<(short);


    以虚继承方式将basic_istream从basic_ios继承出来,是为了确保basic_ios中的成员在其派生类
    中只有一份。如果basic_ios不是basic_istream,basic_ios的virtual基类,则basic_iostream
    拥有basic_ios数据成员的两份拷贝

    操作器是一个函数,可以直接或间接地改变流。

     

    我们使用无参操纵器来表示hex或endl这样的操纵器

     

    设计无参操纵器:

    以endl为例,由于endl改变了输出流basic_ostream,故它应该带一个basic_ostream&类型的参数,并返回basic_ostream&。实际上

    因为语句 cout << endl;  <==> cout.operator<<(endl)

    因此我们需要重载operator<<,并将其参数类型指定为指向一个basic_ostream&类型函数并返回basic_ostream&类型的函数的指针。

    basic_ostream正好提供了一个:

    basic_ostream<charT,traits>& operator<<(basic_ostream<charT,traits>&(*f)(basic_ostream<charT,traits>&);

    实际上f是一个指向操纵器函数的指针

    template<class charT,class traits>
    basic_ostream<charT,traits>& basic_ostream<charT,traits>::operator<<(basic_ostream&(*f)(basic_ostream))
    {
     return f(*this);
    }

    对于某些特定类型的ostream,操作器endl可写成:
    ostream& endl(ostream & os) {
     os << '\n';
     return os.flush();
    }

    当代码 cout << endl;执行时,相当于进行如下调用:
     cout.operator<<(endl);
    而operator<<的函数体主要执行了语句:
     returne f(*this);
    该语句等同于:
     return endl(cout);
    最终,执行了代码:
     cout << ’\n';
     return cout.flush();


    带参数的操纵器设计:
    思想是为操纵器设计一个辅助类

    头文件iomanip中,模板类omanip的声明如下:
    template<class Typ,class charT,class traits = char_traits<charT>>
    struct omanip {
     Typ n;
     void (*f)(basic_ostream<charT,traits>&,Typ);
     omanip(void (*f1)(basic_ostream<charT,traits>&,Typ),Typ n1):f(f1),n(n1){}
    };

    相应的<<重载函数声明:
    template<class charT,class traits,class Typ>
    basic_ostream<charT,traits> & operator<<(basic_ostream<charT,traits>&os,const omanip<Typ,charT>&sman)
    {
     (sman.f)(os,sman.n);
     return os;
    }


    操纵器是一个顶层函数,可通过重载操作符<<或>>来操纵某个对象,设计一个操纵器不需要对类进行任何修改。成员函数是类的一部分,它也可以通过重载操作符<<或>>来操纵其

    所属的类对象。但设计成员函数必须修改类,但有时候无法做到这一点。比如,类的实现代码是放在一个库中

     

    头文件fsteam中声明了如下类:

    basic_filebuf        basic_ofstream      basic_ifstream             basic_fstream

    这些类提供了文件的输入和输出的高级接口

    basic_ofstream包含了将输出文件和basic_ofstream流关联起来的构造函数和成员函数

    explicit basic_ofstream(const char *filename,ios_base::openmode mode = ios_base::out);

    void open(const char *filename,ios_base::openmode mode= ios_base::out);

    bool  is_open();

     

    explicit basic_ifstream(const char *filename,ios_base::openmode mode = ios_base::in);

    void open(const char *filename,ios_base::openmode mode = ios_base::in);

     

    explicit  base_fstream(const char *filename,ios_base::openmode mode = ios_base::in | ios_base::out);

    void open(const char *filename,ios_base::openmode mode = ios_base::in | ios_base::out);

     

     例子:

    #include <iostream>
    #include <cstdio>
    #include <fstream>
    #include <cstring>
    #include <cstdlib>
    #include <cctype>
    using namespace std;

    const int header_size = 256;
    const char Taken = 'T';
    const char Free = 'F';
    const char Deleted = 'D';
    class frandom : public fstream {
    public:
       frandom();
       frandom(const char *);
       frandom(const char *,int,int,int);
       ~frandom();
       void open(const char *);
       void open(const char *,int,int,int);
       void close();
       long get_slots() const { return slots; }
       long get_record_size() const { return record_size; }
       long get_key_size() const { return key_size; }
       long get_total_bytes() const { return total_bytes; }
       long get_no_records() const { return no_records; }
       bool add_record(const char *);
       bool find_record(char *);
       bool remove_record(const char *);
    private:
       long slots;
       long record_size;
       long key_size;
       long total_bytes;
       long no_records;
       long loc_address;
       char *buffer;
       char *stored_key;
       long get_address(const char *) const;
       bool locate(const char *);
    };

    frandom::~frandom() {
       if(is_open()) {
          delete [] stored_key;
          delete [] buffer;
          char buff[header_size];
          for(int i = 0 ;i < header_size ;i ++)
             buff[i] = ' ';
          sprintf(buff,"%ld %ld %ld %ld",slots,record_size,key_size,no_records);
          seekp(0,ios_base::beg);
          write(buff,header_size);
       }
    }

    frandom::frandom() : fstream() {
       buffer = stored_key = 0;
       slots = record_size = key_size = 0;
       total_bytes = no_records = 0;
    }

    frandom::frandom(const char *filename) : fstream() {
       buffer = stored_key = 0;
       open(filename);
    }

    frandom::frandom(const char * filename,int sl,int actual_record_size,int ks) : fstream() {
       buffer = stored_key = 0;
       open(filename,sl,actual_record_size,ks);
    }

    void frandom::open(const char *filename) {
       fstream::open(filename,ios_base::in | ios_base::out | ios_base::binary);
       if(is_open()) {
          char buff[header_size];
          read(buff,header_size);
          sscanf(buff,"%ld%ld%ld%ld",&slots,&record_size,&key_size,&no_records);
          total_bytes = slots * record_size + header_size;
          stored_key = new char[key_size + 1];
          buffer = new char[record_size];
       }
    }

    void frandom::open(const char *filename,int sl,int actual_record_size,int ks) {
       fstream::open(filename,ios_base::in | ios_base::out | ios_base::binary );
       if(is_open()) {
          setstate(ios_base::failbit);
          fstream::close();
          return;
       }
       fstream::open(filename,ios_base::out | ios_base::binary);
       if(is_open())
          fstream::close();
       fstream::open(filename,ios_base::in | ios_base::out | ios_base::binary);
       if(is_open()) {
          clear();
          char buff[header_size];
          slots = sl;
          record_size = actual_record_size + 1;
          key_size = ks;
          total_bytes = slots * record_size + header_size;
          no_records = 0;
          stored_key = new char [key_size + 1];
          for(int i = 0 ; i < header_size ;i ++) 
          buff[i] = ' ';
          sprintf(buff,"%ld %ld %ld %ld",slots,record_size,key_size,no_records);
          write(buff,header_size);
          buffer = new char[record_size];
          for(i = 1;i < record_size ; i ++)
             buff[i] = ' ';
          buffer[0] = Free;
          for(i = 0 ;i < slots; i++)
             write(buffer,record_size);
       }
    }

    long frandom::get_address(const char *key) const {
       memcpy(stored_key,key,key_size);
       stored_key[key_size] = '\0';
       return (atol(stored_key) % slots) * record_size + header_size;
    }

    bool frandom::locate(const char *key) {
       long address,start_address,unocc_adderss;
       int delete_flag = false;
       address = get_address(key);
       unocc_adderss = start_address = address;
       do {
          seekg(address,ios_base::beg);
          switch(get()) {
          case Deleted:
             if(!delete_flag) {
                unocc_adderss = address;
              delete_flag = true;
             }
             break;
          case Free:
             loc_address = delete_flag ? unocc_adderss : address;
             return false;
          case Taken:
             seekg(address + 1,ios_base::beg);
             read(stored_key,key_size);
             if(memcmp(key,stored_key,key_size) == 0) {
                loc_address = address;
                return true;
             }
             break;
          }
          address += record_size;
          if(address >= total_bytes)
             address = header_size;
       }while(address != start_address);

       loc_address = unocc_adderss;

       return false;
    }

    bool frandom::add_record(const char *record) {
       if(no_records >= slots || locate(record) )
          return false;
       seekp(loc_address,ios_base::beg);
       write(&Taken,1);
       write(record,record_size - 1);
       no_records ++;
     
       return true;
    }

    bool frandom::find_record(char *record) {
       if(locate(record)) {
          seekg(loc_address + 1,ios_base::beg);
          read(record,record_size - 1);
      
          return true;
       }
       else
          return false;
    }

    bool frandom::remove_record(const char *key) {
       if(locate(key)) {
          -- no_records;
          seekp(loc_address,ios_base::beg);
          write(&Deleted,1);

          return true;
       }
       else
          return false;
    }

    void frandom::close() {
       if(is_open()) {
          delete[] stored_key;
          delete[] buffer;
          char buff[header_size];
          for(int i = 0 ;i < header_size ;i ++)
            buff[i] = ' ';
          sprintf(buff,"%ld %ld %ld %ld",slots,record_size,key_size,no_records);
          seekp(0,ios_base::beg);
          write(buff,header_size);
          fstream::close();
       }
    }

    int main() {
       char b[10],c;

       frandom finout;

       cout << "New file (Y/N)? ";

       cin >> c;

       if(toupper(c) == 'Y') {
          finout.open("data.dat",15,5,3);
          if(!finout) {
             cerr << "Couldn't open file\n";
             return EXIT_FAILURE;
          }
       }
       else {
          finout.open("data.dat");
          if(!finout) {
             cerr << "Couldn't open file\n";
             return EXIT_FAILURE;
          }
       }

       do {
          cout << "\n\n[A]dd\n[F]ind\n[R]emove\n[Q]uit?";
          cin >> c;
          switch(toupper(c)) {
          case 'A':
             cout << "Which record to add ? ";
             cin >> b;
             if(finout.add_record(b)) 
                cout << "Record added\n";
             else
                cout << "Record not added\n";
             break;
          case 'F':
             cout << "Key? ";
             cin >> b;
             if(finout.find_record(b)) {
              b[5] = '\0';
              cout << "Record found: " << b << endl;
             }
             else
                cout << "Record not found\n";
             break;
          case 'R':
             cout << "Key? ";
             cin >> b;
             if(finout.remove_record(b))
                cout << "Record removed\n";
             else
                cout << "Record not removed\n";
             break;
          case 'Q':
             break;
          default:
             cout << "Illegal choice\n";
             break;
          }
       }while(toupper(c) != 'Q');

       return 0;
    }

    结果:

    头文件sstream中声明的类:

    basic_stringbuf                            basic_ostringstream                       basic_istringbuf                        basic_stringstream

    提供字符流输入和输出的高级接口

    basic_ostringstream类用于将字符流写到内部缓冲区,该缓冲区的内容可拷贝到一个basic_string对象(用途上类似于C中的sprintf)

    explicit basic_ostringstream(ios_base::openmode = ios_base::out);

    explicit basic_ostreamstream(const basic_string(charT,traits,Allocator)&s,ios_base::openmode::out)  //将内部缓冲区初始化为s

    basic_string<charT,traits,Alloctor>str() const;

    void str(const basic_string<charT,traits,Alloctor>& s);

    basic_istringstream类用于从内部缓冲区读取字符流。缓冲区内的数据可以拷贝到basic_string对象(类似于C中sscanf())

    explicit  basic_istringstream( const basic_string<charT,traits,Alloctor>&s,ios_base::openmode = ios_base::in);

    basic_stringstream类用于读写内部缓冲区,该缓冲区中的数据可拷贝到basic_string对象中

    explicit basic_stringstream(ios_base::openmode = ios_base::in | ios_base::out);

    explicit basic_string(const basic_string(charT,traits,Alloctor>&s,ios_base::openmode = ios_base::in | ios_base::out);

    高层拷贝例子1:

    原始文件:

    程序:

    结果文件:

    高层拷贝例子2:

    原文件:

    程序:

    结果文件:

    编写一个高层非模板copy函数,将tab字符替换为适量的空格,该函数第三个参数指明用多少个空格替换tab:

    缓冲区类:
    basic_streambuf    basic_filebuf    basic_stringbuf

    basic_streambuf是类的基本缓冲区,它只有一个默认构造函数

    公有成员:

    basic_streambuf<char_type,traits>* pubsetbuf(char_type * buff,streamsize len);

    该函数调用了protected成员函数 virtual basic_streambuf<char_type,traits>* setbuf(char_type * buff,streamsize len);

    其他成员:

    streamsize in_avail()       返回输入缓冲区中所剩的字节数

    int_type sbumpc()      返回输入缓冲区当前字符,然后增加缓冲区位置标志,调用失败返回end-of-file标志

    int_type  sgetc()            返回缓冲区中下一个字符,但不改变缓冲区位置标识,调用失败返回end-of-file

    streamsize  sgetn(char_type *store,streamsize n)    返回缓冲区下n个字符并存储到store中,并将缓冲区位置后移n个字节

    int_type snextc()    将缓冲区位置后移一个字节,然后返回缓冲区当前位置字符,调用失败返回end-of-file标志

    int_type sputbackc(char_type c)   将字符c返回缓冲区,调用失败返回end-of-file标志

    int_type  sputc(char_type c)    将字符c写到输出缓冲区,调用失败返回end-of-file标志

    streamsize sputn(const char_type *store,streamsize n)  将n个字符写入缓冲区,返回真正放入缓冲区字符个数

    pos_type  pubseekoff(off_type off,ios_base::seek_dir dir,ios_base::openmode pt = ios_base::in | ios_base::out);    从dir开始移动输入或输出流位置off字节

    pos_type pubseekpos(pos_type off,ios_base::openmode pt = ios_base::in | ios_base::out);   将输入或输出流位置移到位置off

    basic_filebuf:

    basic_filebuf<charT,traits> * open(const char *filename,ios_bae::openmode mode);  以mode的形式打开文件并与缓冲区对象basic_filebuf关联

    basic_streambuf<charT,traits>* setbuf(char_type *b,streamsize len)   覆盖父类方法,为该缓冲区对象建立一个缓冲区b作为它的缓冲区

    basic_filebuf<charT,traits> * close();

    例子:

    原文件:

    程序:

    结果文件:

    当上面程序结束时,对象自动调用析构函数,析构函数关闭相应的文件,输出缓冲区清扫

    virtual int_type overflow(int_type stop)  清扫直到遇见stop标志

    virtual int_type underflow() ;将一批数据从源文件中读到缓冲区

    basic_stringbuf类:

    explicit basic_stringbuf(ios_base::openmode = ios_bae::in | ios_bae::out);

    explicit basic_stringbuf(const basic_string<charT,traits,Alloctor>& s,ios_base::openmode mode = ios_base::in | ios_base::out);

    basic_stringbuf<char T,traits> * setbuf(char_type *b,streamsize len);          指定缓冲区

    常见编程错误:

    1,当使用某些版本(前面有初始)的seekg和seekp成员函数时,应以二进制打开,否则不能正常工作

    2,当重载操作符>>用于读取字符串且域宽设为n != 0时,不是无论有没有空格就会读取并存储n个字符!!在默认情况下,读取操作符忽略空格,直到读入n-1个非空格字符或者

    遇到了下一个空格,然后在最后天剑一个null结束符

    3,如果文件未关闭,则文件输出缓冲区中的数据不会被清扫。有两种方式可以关闭文件:显示调用close关闭 , 经由析构函数间接调用

  • 相关阅读:
    Learn Goroutine
    Redis eviction policies
    Hungarian Algorithm
    Prime and Factors
    HDU 2642 Stars
    236. Lowest Common Ancestor of a Binary Tree
    Leetcode 96. Unique Binary Search Trees
    Search in Rotated Sorted Array
    ID Generator
    概率问题
  • 原文地址:https://www.cnblogs.com/lfsblack/p/2711491.html
Copyright © 2020-2023  润新知