洛谷P4911 河童重工的计算机

题面见 https://www.luogu.org/problemnew/show/P4911

作为出题人,我有责任写一篇题解(逃

这题的初衷就是一个庞大的大模拟,刚刚见到未来程序改这道题有着一种自然的熟悉感呢(笑)

但是不像那道题,这道题没有什么语义树之类的高深的东西呢,只有对代码能力的考察,像我这种初中三年荒废在搞项目的人,代码功底还可以说是十分扎实的呢(笑)

关心一下注释的格式:

注释是可以嵌套的,像[[] putchar 65; ]这样的根本不会有什么输出呢

标程中,我是用std::function存下每一个指令对应的处理函数,然后解释调用

第一步,先把程序分拆成tokens,去掉注释,记录行号和function定义位置,替换为set指令
第二步,把tokens转换成指令和数据源,同时替换callfunc为call指令

最后别忘了,call和ret时栈中要压入调用处指令地址和当前的%line寄存器的值这两个值呢

以下是标程(八月就定稿了

/* Ktx-65 Assembly Language Interpreter */
/* Ktx-65 汇编语言解释器 */
/* Copyright (c) Kawasiro Nitori & Moriya Suwano of Kappa Heavy Industries Co.,Ltd. */
/* 版权所有 河童重工业会社 河城荷取 洩矢诹访子 */

/* 版本:审议稿 */
/* 平成三十年 八月 十一日 */

#include <cstdlib>
#include <iostream>
#include <stack>
#include <functional>
#include <map>
#include <vector>
#include <initializer_list>
#include <string>
#include <fstream>
#include <cstring>
using namespace std;

bool compileErrorLevel = false;
void compileError(int line, string message) {
    // TODO Compile Error
    cout << "[ERROR] Interpreter encountered a Compile Error on line " << line << endl;
    cout << "    Message: " << message << endl;
    compileErrorLevel = true;
}

#define compileAssert(line, statement, message) \
(!!(statement) || [](int l, string state, string m)->bool {compileError(l, "Compile Assertation Failed: " + m + ", Statement: " + state); return false; }(line, #statement, message))

void toupper(string& str) { for (char& c : str)c = toupper(c); }
string touppers(const string& str) { string a = str; toupper(a); return a; }

class AssemblyProgram {
public:

    enum Command {
        Undefined = 0,  // Undefined
        Halt = 1,       // Stops the program
        NoOp,           // Does nothing
        // Memory Access
        Set,            // Read value from <0> into <1>
        // Streamline control
        Jump,           // Jump to <0>(absolute line number, begins from 1)
        JumpIf,         // Jump to <0> if <1> is not zero (takes rFlag if <1> doesn't exist)
        Call,           // push current linenum+1 to sAddr and jump to <0>(begins from 1)
        Return,         // jump to sAddr.top() and sAddr.pop()
        // Arithmetic operations
        // rVal if not exist↓
        Inverse,        // <1> = -<0>
        Add,            // <2> = <0> + <1>
        Minus,          // <2> = <0> - <1>
        Multiply,       // <2> = <0> * <1>
        IntDivide,      // <2> = <0> / <1>
        Modulo,         // <2> = <0> % <1>
        LeftShift,      // <2> = <0> << <1>
        RightShift,     // <2> = <0> >> <1>
        BitAnd,         // <2> = <0> & <1>
        BitOr,          // <2> = <0> | <1>
        BitXor,         // <2> = <0> ^ <1>
        // Logical Operations
        // rFlag if not exist↓
        Greater,        //  <2> = <0> > <1>
        Less,           //  <2> = <0> < <1>
        GreaterEqual,   //  <2> = <0> >= <1>
        LessEqual,      //  <2> = <0> <= <1>
        Equal,          //  <2> = <0> == <1>
        LogicalAnd,     //  <2> = <0> && <1>
        LogicalOr,      //  <2> = <0> || <1>
        // Console I/O
        //        rVal if not exist ¡ý
        ReadInt,        //  cin >> <0>
        ReadChar,       // getchar(<0>)
        WriteInt,       // cout << <0>
        WriteChar,      // putchar(<0>)
        // Keep at last - command count
        CommandCount
    };

    enum VariableType {
        Constant,
        Pointer,
        Memory
    };

    struct DataSource {
        int* valptr;
        int valconst;
        int* valmemidptr;
        AssemblyProgram* prog;
        VariableType type;
        int& operator ()() {
            if (type == Constant)
                return valconst;
            else if (type == Pointer)
                return *valptr;
            else {
                int id = *valmemidptr;
                if (id >= memsize || id < 0) {
                    prog->runtimeError("Memory acess out of range");
                    valconst = 0;
                    return valconst;
                }
                return prog->mem[id];
            }
        }
    };

    void halt() {
        // TODO Halt
        running = false;
    }

    void runtimeError(string message) {
        // TODO Runtime Error
        cout << "[ERROR] Interpreter encountered a Runtime Error on instruction " << nextptr + 1 << ", clock cycle " << clockCounter << endl;
        cout << "    Message: " << message << endl;
        running = false;
    }

private:
    // Exception-Safe Wrappers
    void pushAddr(int val) { if (sAddr.size() >= stackSize)runtimeError("sAddr Stack Overflow"); sAddr.push(val); }
    int topAddr() { if (sAddr.size() <= 0) { runtimeError("sAddr Access Error(size = 0)"); return 0; } else return sAddr.top(); }
    void popAddr() { if (sAddr.size() <= 0)runtimeError("sAddr Stack Underflow"); else sAddr.pop(); }
    int popAddrRet() { if (sAddr.size() <= 0) { runtimeError("sAddr Stack Underflow"); return 0; } else { int val = sAddr.top(); sAddr.pop(); return val; } }
    int readMem(int id) { if (id >= memsize || id < 0) { runtimeError("Memory Access Error(Read)"); return 0; } else return mem[id]; }
    void writeMem(int id, int val) { if (id >= memsize || id < 0)runtimeError("Memory Access Error(Write)"); else mem[id] = val; }
    int lineProgramId(int line) { auto i = commandLines.find(line); if (i == commandLines.end()) { runtimeError("Jump Line Number Invalid"); return 0; } else return i->second; }

private:
    // Command Handlers
    void funcSet(vector<DataSource>& l) { l[1]() = l[0](); nextptr++; }
    void funcJmp(vector<DataSource>& l) { nextptr = lineProgramId(l[0]() + rLine); }
    void funcJIf(vector<DataSource>& l) { if (l.size() <= 1) { if (rFlag)nextptr = l[0]() + lineProgramId(rLine); else nextptr++; } else { if (l[1]())nextptr = l[0]() + lineProgramId(rLine); else nextptr++; } }
    void funcCall(vector<DataSource>& l) { pushAddr(rLine); pushAddr(nextptr + 1); nextptr = lineProgramId(l[0]()); }
    void funcRet(vector<DataSource>& l) { if (l.size() > 0)rRet = l[0](); nextptr = popAddrRet(); rLine = popAddrRet(); }
    void funcInv(vector<DataSource>& l) { int* p;  if (l.size() <= 1)p = &rVal; else p = &l[1](); (*p) = -l[0](); nextptr++; }
    void funcAdd(vector<DataSource>& l) { int* p;  if (l.size() <= 2)p = &rVal; else p = &l[2](); (*p) = l[0]() + l[1](); nextptr++; }
    void funcSub(vector<DataSource>& l) { int* p;  if (l.size() <= 2)p = &rVal; else p = &l[2](); (*p) = l[0]() - l[1](); nextptr++; }
    void funcMult(vector<DataSource>& l) { int* p; if (l.size() <= 2)p = &rVal; else p = &l[2](); (*p) = l[0]() * l[1](); nextptr++; }
    void funcIDiv(vector<DataSource>& l) { int* p; if (l.size() <= 2)p = &rVal; else p = &l[2](); (*p) = l[0]() / l[1](); nextptr++; }
    void funcMod(vector<DataSource>& l) { int* p;  if (l.size() <= 2)p = &rVal; else p = &l[2](); (*p) = l[0]() % l[1](); nextptr++; }
    void funcLSft(vector<DataSource>& l) { int* p; if (l.size() <= 2)p = &rVal; else p = &l[2](); (*p) = l[0]() << l[1](); nextptr++; }
    void funcRSft(vector<DataSource>& l) { int* p; if (l.size() <= 2)p = &rVal; else p = &l[2](); (*p) = l[0]() >> l[1](); nextptr++; }
    void funcBAnd(vector<DataSource>& l) { int* p; if (l.size() <= 2)p = &rVal; else p = &l[2](); (*p) = l[0]() & l[1](); nextptr++; }
    void funcBOr(vector<DataSource>& l) { int* p;  if (l.size() <= 2)p = &rVal; else p = &l[2](); (*p) = l[0]() | l[1](); nextptr++; }
    void funcBXor(vector<DataSource>& l) { int* p; if (l.size() <= 2)p = &rVal; else p = &l[2](); (*p) = l[0]() ^ l[1](); nextptr++; }
    void funcLGr(vector<DataSource>& l) { int* p;  if (l.size() <= 2)p = &rFlag; else p = &l[2](); (*p) = l[0]() > l[1](); nextptr++; }
    void funcLLs(vector<DataSource>& l) { int* p;  if (l.size() <= 2)p = &rFlag; else p = &l[2](); (*p) = l[0]() < l[1](); nextptr++; }
    void funcLGE(vector<DataSource>& l) { int* p;  if (l.size() <= 2)p = &rFlag; else p = &l[2](); (*p) = l[0]() >= l[1](); nextptr++; }
    void funcLLE(vector<DataSource>& l) { int* p;  if (l.size() <= 2)p = &rFlag; else p = &l[2](); (*p) = l[0]() <= l[1](); nextptr++; }
    void funcLEql(vector<DataSource>& l) { int* p; if (l.size() <= 2)p = &rFlag; else p = &l[2](); (*p) = l[0]() == l[1](); nextptr++; }
    void funcLAnd(vector<DataSource>& l) { int* p; if (l.size() <= 2)p = &rFlag; else p = &l[2](); (*p) = l[0]() && l[1](); nextptr++; }
    void funcLOr(vector<DataSource>& l) { int* p;  if (l.size() <= 2)p = &rFlag; else p = &l[2](); (*p) = l[0]() || l[1](); nextptr++; }
    void funcRInt(vector<DataSource>& l) { if (l.size() <= 0)cin >> rVal; else cin >> l[0](); nextptr++; }
    void funcRCh(vector<DataSource>& l) { if (l.size() <= 0)rVal = getchar(); else l[0]() = getchar(); nextptr++; }
    void funcWInt(vector<DataSource>& l) { if (l.size() <= 0)cout << rVal; else cout << l[0](); nextptr++; }
    void funcWCh(vector<DataSource>& l) { if (l.size() <= 0)putchar(rVal); else putchar(l[0]()); nextptr++; }

public:
    void initalaize() {
#define REGISTER_RAW(command, name) \
commands[name] = command; names[command] = name
#define REGISTER_HANDLER(command, handler, minParamCount, name)    \
handlers[command] = [this](vector<DataSource>& l) {if (l.size() < minParamCount)runtimeError("Paramater Count Mismatch"); else handler(l); }; \
/*handlers[command] = bind(&AssemblyProgram::handler, this, placeholders::_1);*/ \
commands[name] = command; names[command] = name
        REGISTER_RAW(Undefined, "UDEF");
        handlers[Undefined] = [this](vector<DataSource>&) { runtimeError("Undefined Command Called"); };
        REGISTER_RAW(Halt, "HLT");
        handlers[Halt] = [this](vector<DataSource>&) { halt(); };
        REGISTER_RAW(NoOp, "NOP");
        handlers[NoOp] = [this](vector<DataSource>&) {};
        REGISTER_HANDLER(Set, funcSet, 2, "SET");
        REGISTER_HANDLER(Jump, funcJmp, 1, "JMP");
        REGISTER_HANDLER(JumpIf, funcJIf, 1, "JIF");
        REGISTER_HANDLER(Call, funcCall, 1, "CALL");
        REGISTER_HANDLER(Return, funcRet, 0, "RET");
        REGISTER_HANDLER(Inverse, funcInv, 1, "INV");
        REGISTER_HANDLER(Add, funcAdd, 2, "ADD");
        REGISTER_HANDLER(Minus, funcSub, 2, "SUB");
        REGISTER_HANDLER(Multiply, funcMult, 2, "MULT");
        REGISTER_HANDLER(IntDivide, funcIDiv, 2, "IDIV");
        REGISTER_HANDLER(Modulo, funcMod, 2, "MOD");
        REGISTER_HANDLER(LeftShift, funcLSft, 2, "LSFT");
        REGISTER_HANDLER(RightShift, funcRSft, 2, "RSFT");
        REGISTER_HANDLER(BitAnd, funcBAnd, 2, "BAND");
        REGISTER_HANDLER(BitOr, funcBOr, 2, "BOR");
        REGISTER_HANDLER(BitXor, funcBXor, 2, "BXOR");
        REGISTER_HANDLER(Greater, funcLGr, 2, "LGR");
        REGISTER_HANDLER(Less, funcLLs, 2, "LLS");
        REGISTER_HANDLER(GreaterEqual, funcLGE, 2, "LGE");
        REGISTER_HANDLER(LessEqual, funcLLE, 2, "LLE");
        REGISTER_HANDLER(Equal, funcLEql, 2, "LEQL");
        REGISTER_HANDLER(LogicalAnd, funcLAnd, 2, "LAND");
        REGISTER_HANDLER(LogicalOr, funcLOr, 2, "LOR");
        REGISTER_HANDLER(ReadInt, funcRInt, 0, "RINT");
        REGISTER_HANDLER(ReadChar, funcRCh, 0, "RCH");
        REGISTER_HANDLER(WriteInt, funcWInt, 0, "WINT");
        REGISTER_HANDLER(WriteChar, funcWCh, 0, "WCH");
#undef REGISTER_RAW
#undef REGISTER_HANDLER
        program.clear();
        nextptr = 0;
    }

    bool pushCommand(int fileline, string name, vector<DataSource> param) {
        toupper(name);
        auto i = commands.find(name);
        if (i == commands.end()) {
            compileError(fileline, "Invaild command \"" + name + "\"");
            return false;
        }
        else {
            program.push_back(make_pair(i->second, param));
            if (commandLines.find(fileline) == commandLines.end())
                commandLines[fileline] = program.size() - 1;
            return true;
        }
    }

    int* getRegister(int fileline, string name) {
        toupper(name);
        if (name == "R1")
            return &rR1;
        else if (name == "R2")
            return &rR2;
        else if (name == "R3")
            return &rR3;
        else if (name == "R4")
            return &rR4;
        else if (name == "E1")
            return &rE1;
        else if (name == "E2")
            return &rE2;
        else if (name == "E3")
            return &rE3;
        else if (name == "E4")
            return &rE4;
        else if (name == "FLAG")
            return &rFlag;
        else if (name == "VAL")
            return &rVal;
        else if (name == "RET")
            return &rRet;
        else if (name == "LINE")
            return &rLine;
        else {
            compileError(fileline, "Invaild register name \"" + name + '\"');
            return nullptr;
        }
    }

    int* getMemory(int fileline, int id) {
        if (id >= memsize || id < 0) {
            compileError(fileline, "Memory access statically out of range");
            return 0;
        }
        else return mem + id;
    }

    void run() {
        rR1 = rR2 = rR3 = rR4 = rE1 = rE2 = rE3 = rE4 = rFlag = rVal = rRet = rLine = 0;
        memset(mem, 0, sizeof(mem));
        while (!sAddr.empty())
            sAddr.pop();

        clockCounter = 0;
        nextptr = 0;
        running = true;
        while (running) {
            if (nextptr >= program.size() || nextptr < 0)
                runtimeError("Program Pointer Invaild");
            else
                handlers[program[nextptr].first](program[nextptr].second);
            clockCounter++;
        }
    }

    int getClock() { return clockCounter; }

    bool valid;
private:

    friend class Compiler;

    int rR1, rR2, rR3, rR4;
    int rE1, rE2, rE3, rE4;
    int rFlag, rVal, rRet, rLine;
    static constexpr int stackSize = 512 * 1024;
    stack<int> /*sVal,*/ sAddr;

    vector<pair<Command, vector<DataSource>>> program;
    int nextptr;
    static constexpr int memsize = 16 * 1024 * 1024;
    int mem[memsize];

    int clockCounter;
    bool running;

    function<void(vector<DataSource>&)> handlers[CommandCount];
    map<int, int> commandLines;
    map<string, Command> commands;
    string names[CommandCount];
};

class Tokenizer {
public:
    enum Type {
        Unknown,
        Identifier,
        Number,
        Symbol
    };

    struct Token {
        Type type;
        string word;
        int line;
    };

    static vector<Token> parse(const string& str) {
        vector<Token> tokens;
        Token buffer{ Unknown, "", 1 };
        int commentLayers = 0;
        int curline = 1;
        for (char c : str) {
            if (c == '[')
                commentLayers++;
            else if (c == ']') {
                if (commentLayers > 0)
                    commentLayers--;
            }
            else if (commentLayers <= 0) {
                if (!(isblank(c) || iscntrl(c) || c == ',')) {
                    if ((buffer.type == Number && !isdigit(c)) ||
                        (buffer.type == Identifier && (!isalnum(c) && c != '_')) ||
                        (buffer.type == Symbol && (isalnum(c) || c == '_' || c == '-'))) {
                        buffer.line = curline;
                        tokens.push_back(buffer);
                        buffer = Token{ Unknown, "" };
                    }
                    if (buffer.type == Unknown) {
                        // Check new object type
                        if (isalpha(c) || c == '_')
                            buffer.type = Identifier;
                        else if (isdigit(c) || c == '-')
                            buffer.type = Number;
                        else
                            buffer.type = Symbol;
                    }
                    buffer.word.push_back(c);
                }
                else {
                    if (buffer.type != Unknown) {
                        buffer.line = curline;
                        tokens.push_back(buffer);
                        buffer = Token{ Unknown, "" };
                    }
                }
            }
            if (c == '\n')
                curline++;
        }
        if (buffer.type != Unknown) {
            buffer.line = curline;
            tokens.push_back(buffer);
            buffer = Token{ Unknown, "" };
        }
        return tokens;
    }

    void parseTotal(string total) {
        tokens = parse(total);
        nextid = 0;
    }

    void parse(istream& in) {
        int c;
        string str;
        while (c = in.get()) {
            if (in.eof())
                break;
            str.push_back(c);
        }
        parseTotal(str);
    }

    Token next() {
        if (nextid >= tokens.size())
            return Token{ Unknown, "" };
        else
            return tokens[nextid++];
    }

    Token offset(int offset) {
        int id = nextid - 1 + offset;
        if (id >= tokens.size() || id < 1)
            return Token{ Unknown };
        else
            return tokens[id];
    }

private:
    vector<Token> tokens;
    int nextid;
};

/*
Basic Syntax
COMMAND %Register @Address Value @%AddrAtRegister;
*/
class Compiler {
public:

    struct Command {
        int line;
        string command;
        vector<Tokenizer::Token> sources;
    };

    void compile(Tokenizer& tokens) {
        valid = false;
        bool ok = true;
        while (ok) {
            Tokenizer::Token commt;
            while (ok && (commt = tokens.next()).type != Tokenizer::Identifier)
                if (commt.type == Tokenizer::Unknown)
                    ok = false;
            string command = commt.word;
            vector<Tokenizer::Token> sources;
            Tokenizer::Token cur;
            bool pushCommand = true;
            while (ok && (cur = tokens.next()).word != ";") {
                if (cur.type == Tokenizer::Unknown)
                    ok = false;
                //if (cur.type == Tokenizer::Identifier&&tokens.offset(-1).type != Tokenizer::Symbol)
                //    compileError(cur.line, "\";\" expected before a new identifier");
                else if (cur.type == Tokenizer::Symbol&&cur.word == "$"&&touppers(command) == "FUNCTION") {
                    Tokenizer::Token func = tokens.next();
                    compileAssert(func.line, func.type == Tokenizer::Identifier, "\"function $\" not followed by a identifier as function name");
                    compileAssert(func.line, funcStartLines.find(func.word) == funcStartLines.end(), "Redefinition of function \"" + func.word + "\"");
                    funcStartLines.insert(make_pair(func.word, commt.line));

                    auto vec = Tokenizer::parse("#LINE %LINE");
                    for (auto& i : vec) { i.line = commt.line; }
                    commands.push_back(Command{ commt.line, "SET", vec });
                    pushCommand = false;
                }
                sources.push_back(cur);
            }
            if (compileErrorLevel)
                break;
            if (ok&&pushCommand)
                commands.push_back(Command{ commt.line, command, sources });
        }
        if (compileErrorLevel) {
            compileErrorLevel = false;
            valid = false;
        }
        else
            valid = true;
    }

    void generateCode(AssemblyProgram& prog) {
        if (!valid)
            return;
        prog.initalaize();
        prog.valid = false;

        bool ok = true;

        for (auto i = commands.begin(); ok&&i != commands.end(); i++) {
            string& command = i->command;
            vector<AssemblyProgram::DataSource> sources;
            bool pushCommand = true;

            auto j = i->sources.begin();
            auto nextToken = [&]()->Tokenizer::Token {
                j++;
                if (j == i->sources.end())
                    return Tokenizer::Token{ Tokenizer::Unknown };
                else
                    return *j;
            };
            for (; ok&&j != i->sources.end(); nextToken()) {
                auto& cur = *j;
                if (cur.type == Tokenizer::Number)
                    sources.push_back(AssemblyProgram::DataSource{ nullptr, atoi(cur.word.c_str()), nullptr, nullptr, AssemblyProgram::Constant });
                else if (cur.type == Tokenizer::Symbol) {
                    if (cur.word == "%") {
                        Tokenizer::Token reg = nextToken();
                        compileAssert(reg.line, reg.type == Tokenizer::Identifier, "% not followed by identifier");
                        sources.push_back(AssemblyProgram::DataSource{ prog.getRegister(reg.line, reg.word), 0, nullptr, nullptr, AssemblyProgram::Pointer });
                    }
                    else if (cur.word == "@") {
                        Tokenizer::Token val = nextToken();
                        compileAssert(val.line, val.type == Tokenizer::Number, "@ not followed by a number");
                        sources.push_back(AssemblyProgram::DataSource{ prog.getMemory(val.line, atoi(val.word.c_str())), 0, nullptr, nullptr, AssemblyProgram::Pointer });
                    }
                    else if (cur.word == "@%") {
                        Tokenizer::Token reg = nextToken();
                        compileAssert(reg.line, reg.type == Tokenizer::Identifier, "@% not followed by identifier");
                        sources.push_back(AssemblyProgram::DataSource{ nullptr, 0, prog.getRegister(reg.line, reg.word), &prog, AssemblyProgram::Memory });
                    }
                    else if (cur.word == "#") {
                        Tokenizer::Token def = nextToken();
                        compileAssert(def.line, def.type == Tokenizer::Identifier, "# not followed by identifier");
                        toupper(def.word);
                        if (def.word == "LINE")
                            sources.push_back(AssemblyProgram::DataSource{ nullptr, def.line, nullptr, nullptr, AssemblyProgram::Constant });
                        else
                            compileError(def.line, "Compile-time constant identifier \"" + def.word + "\" undefined");
                    }
                    else if (cur.word == "$") {
                        if (command == "callfunc") {
                            Tokenizer::Token func = nextToken();
                            compileAssert(func.line, func.type == Tokenizer::Identifier, "\"callfunc $\" not followed by a identifier as function name");
                            auto it = funcStartLines.find(func.word);
                            if (it == funcStartLines.end())
                                compileError(func.line, "Function \"" + func.word + "\" undefined");
                            else {
                                prog.pushCommand(i->line, "CALL", {
                                    AssemblyProgram::DataSource{ nullptr, it->second, nullptr, nullptr, AssemblyProgram::Constant }
                                                 });
                            }
                            pushCommand = false;
                        }
                        else
                            compileError(cur.line, "$ not preceeded by keyword \"function\" or \"callfunc\"");

                    }
                }
                if (compileErrorLevel)
                    ok = false;
            }
            if (ok&&pushCommand)
                prog.pushCommand(i->line, i->command, sources);
        }

        if (compileErrorLevel) {
            compileErrorLevel = false;
            prog.valid = false;
        }
        else
            prog.valid = true;
    }

private:

    vector<Command> commands;
    map<string, int> funcStartLines;
    bool valid;

};

AssemblyProgram prog;
Tokenizer tokens;
Compiler compiler;

int main(int argc, char* argv[]) {

    int n;
    cin >> n;

    string str;
    string total;
    while (n > 0 && getline(cin, str)) {
        if (str == "")
            continue;
        n--;
        total += str + '\n';
    }

    tokens.parseTotal(total);
    compiler.compile(tokens);
    compiler.generateCode(prog);
    prog.run();

    return 0;
}

话又说回来,要是像未来程序改那样的题真的出现在了省选里,又有多少人会做出来呢……

发表回复

您的电子邮箱地址不会被公开。 必填项已用*标注