由一个小程序引发对 size_type类型的思考

本文通过对比动态数组与vector实现插入排序的过程,详细分析了vector在使用size_type类型作为索引变量时可能遇到的下标溢出问题,并给出了解决方案。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

今天第一天看“算法导论”,看了插入排序算法。想用c++练习一下,恰好最近也在复习动态数组以及vector,想分别用动态数组和vector实现上面的功能。

首先,用动态数组实现没事,程序如下:

//用动态数组实现数组元素从小到大的排序
#include<iostream>
using namespacestd;
int main()
{
    int i,j,key;
    int arr;
    cout<<"请输入vector对象的大小:";
    cin>>arr;
    int *p=new int[arr];
    cout<<endl;
    cout<<"请输入"<<arr<<"个数据:";
    for(i=0;i<arr;++i)
        cin>>p[i];
    cout<<"原始数据为:";
    for (int i=0; i<arr; i++)
    {
       cout<<"p["<<i<<"] ="<<p[i]<<endl;
    }
    for(j=1;j<arr;j++)
    {
        key=p[j];
        i=j-1;
        while((i>=0)&&(p[i]>key))
        {
            p[i+1]=p[i];
            i=i-1;
        }
        p[i+1]=key;
    }
    cout<<"输出数据为:";
    for (int i=0; i<arr; i++)
    {
       cout<<"p["<<i<<"] ="<<p[i]<<endl;
    }
    delete[] p;//delete[]释放数组内存
    system("pause");
    return 0;
}

  然后用vector实现,代码如下:


<span style="font-size:12px;"><span style="color:#008000;">//用动态数组实现数组元素从小到大的排序
#include <iostream>
#include <vector>
using namespace std;
int main()
{
	int key,word;
	vector<int> ivec;
	vector<int>::size_type i,j;
	</span><span style="color:#ff0000;"><strong>//int i,j;</strong></span><span style="color:#008000;">
	cout<<"请输入数组数据(Ctrl+Z to end):";
	while(cin>>word)
		ivec.push_back(word);
	cout<<"原始数据为:";
	for(vector<int>::iterator iter=ivec.begin();iter!=ivec.end();++iter)
		cout<<(*iter)<<" ";
	cout<<endl;
	for(j=1;j<ivec.size();j++)
	{
		key=ivec[j];
		i=j-1;
		while((i>=0)&&(ivec[i]>key))
		{
			ivec[i+1]=ivec[i];
			i=i-1;
		}
		ivec[i+1]=key;
	}
  cout<<"输出数据为:";
	for(vector<int>::iterator iter=ivec.begin();iter!=ivec.end();++iter)
		cout<<(*iter)<<" ";
	cout<<endl;
	system("pause");
	return 0;
}</span></span>


那么问题来了,最开始我输入的是1,2,3

输出没问题,接下来输入3,2,1问题就来了,出现以下报错:


也就是说,vector下标溢出了,我想上面两个程序,核心都一样,为什么第二个就不行了呢?

于是开始调程序,发现当断点设在while循环后的大括号时,i的值竟然是4294967295,明白了,是i有问题。由于我设的i是size_type类型,这种类型跟int类型类似,但它是无符号的(也就是unsigned int 类型),所以当首次执行while循环时,i=0;while循环体内,i=i-1;语句使得i为负值了,但因为我设的i是无符号的,所以i溢出了。分析了原因之后,我用int类型的i代替size_type的i(也就是用红色语句代替它上面那条语句),结果运行正常。

总结:c++中的size_type类型其实就是unsigned int 类型,但单独设为size_type类型,只是为了更好的兼容不同的操作系统而已。所以我们用int 类型来求vector下标时也是正确的。

#include "CodeGen.hpp" #include "CodeGenUtil.hpp" #include <algorithm> #include <cstdint> #include <cstring> #include <string> #include <sys/types.h> #include <utility> #include <vector> void CodeGen::allocate() { unsigned offset = PROLOGUE_OFFSET_BASE; // 为每个参数分配栈空间(如果没有被分配寄存器) for (auto &arg : context.func->get_args()) { auto size = arg.get_type()->get_size(); offset = ALIGN(offset + size, size); context.offset_map[&arg] = -static_cast<int>(offset); } // 为每条指令结果分配栈空间 for (auto &bb : context.func->get_basic_blocks()) { for (auto &instr : bb.get_instructions()) { // 每个非 void 的定值都分配栈空间 if (not instr.is_void()) { auto size = instr.get_type()->get_size(); offset = ALIGN(offset + size, size); context.offset_map[&instr] = -static_cast<int>(offset); } // 为数组 alloca 分配额外空间(不对齐) if (instr.is_alloca()) { auto *alloca_inst = static_cast<AllocaInst *>(&instr); auto alloc_size = alloca_inst->get_alloca_type()->get_size(); offset += alloc_size; context.array_start_offset[alloca_inst] = offset; } } } // 最终的帧大小对齐为 16 的倍数 context.frame_size = ALIGN(offset, PROLOGUE_ALIGN); } void CodeGen::gen_prologue() { makeSureInRange("addi", SP, SP, -context.frame_size, "add"); makeSureInRange("sd", RA_reg, SP, context.frame_size - 8, "stx"); makeSureInRange("sd", FP, SP, context.frame_size - 16, "stx"); makeSureInRange("addi", FP, SP, context.frame_size, "add"); // 将函数参数转移到栈帧上 int garg_cnt = 0; int farg_cnt = 0; for (auto &arg : context.func->get_args()) { if (arg.get_type()->is_float_type()) { store_from_freg(&arg, FReg::fa(farg_cnt++)); } else { // int or pointer store_from_greg(&arg, Reg::a(garg_cnt++)); } } } void CodeGen::gen_epilogue() { // TODO1:根据你的理解实现函数的 epilogue // 提示:可能包括的步骤:恢复ra、恢复s0、恢复sp、返回到调用方 // throw not_implemented_error{__FUNCTION__}; // 生成统一函数退出标签 std::string exit_label = func_exit_label_name(context.func); append_inst(exit_label, ASMInstruction::Label); // 标签必须在恢复指令前 // 恢复 ra 和 fp 寄存器 makeSureInRange("ld", RA_reg, SP, context.frame_size - 8, "ld"); makeSureInRange("ld", FP, SP, context.frame_size - 16, "ld"); // 恢复栈指针 makeSureInRange("addi", SP, SP, context.frame_size, "add"); // 返回调用方 append_inst("ret"); //TODO1-------------end } // 将一个值 val 加载到目标通用寄存器 reg 中 void CodeGen::load_to_greg(Value *val, const Reg &reg) { assert(val->get_type()->is_integer_type() || val->get_type()->is_pointer_type()); if (auto *constant = dynamic_cast<ConstantInt *>(val)) {// 如果 val 是一个常数整数 int32_t val = constant->get_value(); if (IS_IMM_12(val)) { append_inst(ADDI, {reg.print(), "zero", std::to_string(val)}); } else { load_large_int32(val, reg);// 如果常数太大,用 load_large_int32 处理 } } else if (auto *global = dynamic_cast<GlobalVariable *>(val)) { // 如果是全局变量,生成地址加载指令 append_inst(LOAD_ADDR, {reg.print(), global->get_name()}); } else { //剩余情况从栈中加载到寄存器 load_from_stack_to_greg(val, reg); } } // 加载一个 32 位大整数到寄存器(通常是伪指令 li 会被展开成 lui+addi) void CodeGen::load_large_int32(int32_t val, const Reg &reg) { append_inst(LI, {reg.print(), std::to_string(val)}); } // 加载一个 64 位整数到寄存器,先加载高 32 位并左移,再加载低 32 位 void CodeGen::load_large_int64(int64_t val, const Reg &reg) { auto low_32 = static_cast<int32_t>(val & LOW_32_MASK); // 提取低 32 位 auto high_32 = static_cast<int32_t>(val >> 32); // 提取高 32 位 load_large_int32(high_32, reg); append_inst(SLLI, {reg.print(), reg.print(), "32"}); // 加载高 32 位并左移 32 位 load_large_int32(low_32, reg);// 覆盖写入低 32 位 } // 从栈帧中加载某个变量 val 到通用寄存器 reg 中 void CodeGen::load_from_stack_to_greg(Value *val, const Reg &reg) { // 获取该变量在当前函数栈帧中的偏移 auto offset = context.offset_map.at(val); auto offset_str = std::to_string(offset); auto *type = val->get_type(); // 获取该变量的类型(用于确定加载指令) if (IS_IMM_12(offset)) { // 如果 offset 能够用 12 位立即数表示,可以直接使用 offset(fp) 格式访问内存 if (type->is_int1_type()) { append_inst(LOAD_BYTE, {reg.print(), "fp", offset_str}); } else if (type->is_int32_type()) { append_inst(LOAD_WORD, {reg.print(), "fp", offset_str}); } else { // Pointer append_inst(LOAD_DOUBLE, {reg.print(), "fp", offset_str}); } } else { // 如果偏移过大,不能直接编码到指令中,先将 offset 加载到寄存器 load_large_int64(offset, reg); // reg = offset append_inst(ADD, {reg.print(), "fp", reg.print()}); // reg = fp + offset if (type->is_int1_type()) { append_inst(LOAD_BYTE, {reg.print(), reg.print(), "0"}); } else if (type->is_int32_type()) { append_inst(LOAD_WORD, {reg.print(), reg.print(), "0"}); } else { // Pointer append_inst(LOAD_DOUBLE, {reg.print(), reg.print(), "0"}); } } } // 将通用寄存器 reg 中的值存储到 val 对应的栈上位置(以 fp 为基址) void CodeGen::store_from_greg(Value *val, const Reg &reg) { // 获取该变量在当前函数栈帧中的偏移 auto offset = context.offset_map.at(val); auto offset_str = std::to_string(offset); auto *type = val->get_type(); // 获取该变量的类型(用于确定加载指令) if (IS_IMM_12(offset)) { // 如果 offset 能够用 12 位立即数表示,可以直接使用 offset(fp) 格式访问内存 if (type->is_int1_type()) { append_inst(STORE_BYTE, {reg.print(), "fp", offset_str}); } else if (type->is_int32_type()) { append_inst(STORE_WORD, {reg.print(), "fp", offset_str}); } else { // Pointer append_inst(STORE_DOUBLE, {reg.print(), "fp", offset_str}); } } else { // 对于 offset 超出立即数范围的情况,需要通过地址计算访问 auto addr = Reg::s(11); // 使用临时寄存器 s11 作为中间地址计算(可更换) load_large_int64(offset, addr); append_inst(ADD , {addr.print(), "fp", addr.print()}); if (type->is_int1_type()) { append_inst(STORE_BYTE, {reg.print(), addr.print(), "0"}); } else if (type->is_int32_type()) { append_inst(STORE_WORD, {reg.print(), addr.print(), "0"}); } else { // Pointer append_inst(STORE_DOUBLE, {reg.print(), addr.print(), "0"}); } } } // 将一个浮点类型的 Value 加载到浮点寄存器 freg 中 void CodeGen::load_to_freg(Value *val, const FReg &freg) { assert(val->get_type()->is_float_type()); if (auto *constant = dynamic_cast<ConstantFP *>(val)) { // 若是浮点常量,加载立即数 float val = constant->get_value(); load_float_imm(val, freg); } else { // 从栈中加载浮点变量 auto offset = context.offset_map.at(val); auto offset_str = std::to_string(offset); if (IS_IMM_12(offset)) { append_inst(FLOAD_SINGLE, {freg.print(), "fp", offset_str}); } else { // 偏移过大,使用寄存器间接寻址 auto addr = Reg::s(11); // 临时通用寄存器 s11 load_large_int64(offset, addr); // 加载偏移 append_inst(ADD, {addr.print(), "fp", addr.print()}); // addr = fp + offset append_inst(FLOAD_SINGLE, {freg.print(), addr.print(), "0"}); // 从 addr 加载 } } } // 将 float 常量加载进浮点寄存器 freg void CodeGen::load_float_imm(float val, const FReg &r) { int32_t bytes = *reinterpret_cast<int32_t *>(&val); // 将 float 解释为 32 位整数(IEEE 754 bit pattern) load_large_int32(bytes, Reg::s(11)); append_inst("fmv.s.x", {r.print(), Reg::s(11).print()}); // 使用 fmv.s.x 指令将整数位模式转成 float 放入 freg } // 将浮点寄存器 r 中的值存储回栈中 val 对应的位置 void CodeGen::store_from_freg(Value *val, const FReg &r) { auto offset = context.offset_map.at(val); if (IS_IMM_12(offset)) { auto offset_str = std::to_string(offset); append_inst(FSTORE_SINGLE, {r.print(), "fp", offset_str}); } else { // 偏移过大,需要间接寻址 auto addr = Reg::s(11); load_large_int64(offset, addr); append_inst(ADD, {addr.print(), "fp", addr.print()}); // addr = fp + offset append_inst(FSTORE_SINGLE, {r.print(), addr.print(), "0"}); // 从 r 存到 addr } } void CodeGen::gen_ret() { // TODO2:函数返回操作,你需要思考如何处理返回值(a0/fa0),如何返回到调用者(可以使用j指令、context中或许有你需要的信息) // throw not_implemented_error{__FUNCTION__}; // 如果有返回值 if (!context.inst->get_operands().empty()) { Value *ret_val = context.inst->get_operand(0); // 处理浮点返回值 if (ret_val->get_type()->is_float_type()) { load_to_freg(ret_val, FReg::fa(0)); } // 处理整型返回值 else { load_to_greg(ret_val, Reg::a(0)); } } // 直接跳转到退出标签,后续指令不再执行 append_inst("j " + func_exit_label_name(context.func)); // TODO2----------------end } void CodeGen::gen_br() { auto *branchInst = static_cast<BranchInst *>(context.inst); if (branchInst->is_cond_br()) { // TODO6:补全条件跳转操作 // 提示: 根据条件表达式的结果(reg t1 != 0),选择跳转到 true 分支或 false 分支。 // 你可能会用到blt、j等指令 // throw not_implemented_error{__FUNCTION__}; auto *truebb = static_cast<BasicBlock *>(branchInst->get_operand(1)); auto *falsebb = static_cast<BasicBlock *>(branchInst->get_operand(2)); // 确保标签唯一且正确 append_inst("bnez t1, " + label_name(truebb)); append_inst("j " + label_name(falsebb)); // TODO6-------------------end } else { // 无条件跳转 auto *branchbb = static_cast<BasicBlock *>(branchInst->get_operand(0)); append_inst("j " + label_name(branchbb)); // 跳转到目标基本块 } } void CodeGen::gen_binary() { // 分别将左右操作数加载到 t0 t1 load_to_greg(context.inst->get_operand(0), Reg::t(0)); load_to_greg(context.inst->get_operand(1), Reg::t(1)); // 根据指令类型生成汇编 switch (context.inst->get_instr_type()) { case Instruction::add: output.emplace_back("add t2, t0, t1"); break; case Instruction::sub: output.emplace_back("sub t2, t0, t1"); break; case Instruction::mul: output.emplace_back("mul t2, t0, t1"); break; case Instruction::sdiv: output.emplace_back("div t2, t0, t1"); break; case Instruction::srem: output.emplace_back("remw t2, t0, t1"); break; default: assert(false); } // 将结果填入栈帧中 store_from_greg(context.inst, Reg::t(2)); } void CodeGen::gen_alloca() { auto *alloca_inst = static_cast<AllocaInst *>(context.inst); auto shuzu_offset = context.array_start_offset[alloca_inst]; std::string temp_reg = "t1"; // 加载偏移量到临时寄存器 load_large_int32(shuzu_offset, Reg::t(0)); // 计算栈地址:fp - shuzu_offset append_inst(SUB + string(" ") + temp_reg + ", fp, t0"); store_from_greg(context.inst, Reg::t(1)); } void CodeGen::gen_load() { auto ptr = context.inst->get_operand(0);//在指针类型auto*和auto没有任何区别 auto *type = context.inst->get_type(); load_to_greg(ptr, Reg::t(0)); std::string sreg ="t0"; if (type->is_float_type()) { std::string dest="ft0"; append_inst(FLOAD_SINGLE,{dest, sreg, "0"});//ft0=M[t0+0] store_from_freg(context.inst, FReg::ft(0)); } else { // TODO3: 补全load整型变量的情况,考虑int1 int32 int64 // throw not_implemented_error{__FUNCTION__}; // 根据变量类型选择加载指令 if (type->is_int1_type()) { append_inst("lb t1, 0(t0)"); } else if (type->is_int32_type()) { append_inst("lw t1, 0(t0)"); } else { // int64或指针 append_inst("ld t1, 0(t0)"); } store_from_greg(context.inst, Reg::t(1)); // TODO3----------------end } } void CodeGen::gen_store() { auto *type = context.inst->get_operand(0)->get_type();//怎么store取决于我们要存的数据是什么类型 auto *ptr = context.inst->get_operand(1);//位置 auto *data = context.inst->get_operand(0);//要存入的值 load_to_greg(ptr, Reg::t(1)); auto pst_reg=std::string("t1"); if (type->is_float_type()) { load_to_freg(data, FReg::ft(0)); append_inst(FSTORE_SINGLE ,{"ft0", pst_reg , "0"});//M[t1+0]=ft0 } else { if(type->is_int1_type()){ load_to_greg(data, Reg::t(0)); append_inst("sb "+std::string("t0")+", "+ "0("+pst_reg+")");//M[t1+0]=t0 }else if(type->is_int32_type()){ load_to_greg(data, Reg::t(0)); append_inst("sw "+std::string("t0")+", "+ "0("+pst_reg+")"); }else{ load_to_greg(data, Reg::t(0)); append_inst("sd "+std::string("t0")+", "+ "0("+pst_reg+")"); } } } void CodeGen::gen_icmp() { //这个指令有两个参数,就是两个参与运算的参数 auto sreg0=std::string("t0"); auto sreg1=std::string("t1"); load_to_greg(context.inst->get_operand(0), Reg::t(0)); // Operand 1 load_to_greg(context.inst->get_operand(1), Reg::t(1)); // Operand 2 auto dest_reg = std::string("t0"); // 根据指令类型生成汇编 switch (context.inst->get_instr_type()) { case Instruction::eq: append_inst("slt s11,"+sreg1+","+sreg0); append_inst("slt t0,"+sreg0+","+sreg1); append_inst("or t0,t0,s11"); append_inst("addi s11,zero,1"); append_inst("sub "+dest_reg+",s11,t0"); break; case Instruction::ne: append_inst("slt s11,"+sreg1+","+sreg0); append_inst("slt t0,"+sreg0+","+sreg1); append_inst("or "+dest_reg+",t0,s11"); break; case Instruction::gt: append_inst("slt "+dest_reg+","+sreg1+","+sreg0); break; case Instruction::ge: append_inst("slt "+dest_reg+","+sreg0+","+sreg1); append_inst("addi s11,zero,1"); append_inst("sub "+dest_reg+",s11,"+dest_reg); break; case Instruction::lt: append_inst("slt "+dest_reg+","+sreg0+","+sreg1); break; case Instruction::le: append_inst("slt "+dest_reg+","+sreg1+","+sreg0); append_inst("addi s11,zero,1"); append_inst("sub "+dest_reg+",s11,"+dest_reg); break; default: assert(false); } store_from_greg(context.inst,Reg::t(0)); } void CodeGen::gen_fcmp() { // TODO7: 补全各种浮点型变量比较的情况(对应的Light IR:feq/fne/fgt/fge/flt/fle) // 提示: 你可能会用到 feq.s、flt.s、fle.s、xori等指令 // throw not_implemented_error{__FUNCTION__}; load_to_freg(context.inst->get_operand(0), FReg::ft(0)); load_to_freg(context.inst->get_operand(1), FReg::ft(1)); switch (context.inst->get_instr_type()) { case Instruction::feq: append_inst("feq.s t0, ft0, ft1"); break; case Instruction::fne: append_inst("feq.s t0, ft0, ft1"); append_inst("xori t0, t0, 1"); break; case Instruction::fgt: append_inst("flt.s t0, ft1, ft0"); // a > b → b < a break; case Instruction::fge: append_inst("fle.s t0, ft1, ft0"); // a >= b → b <= a break; case Instruction::flt: append_inst("flt.s t0, ft0, ft1"); break; case Instruction::fle: append_inst("fle.s t0, ft0, ft1"); break; default: assert(false); } store_from_greg(context.inst, Reg::t(0)); // TODO7----------------end } void CodeGen::gen_float_binary() { auto sreg0=std::string("ft0"); auto sreg1=std::string("ft1"); load_to_freg(context.inst->get_operand(0), FReg::ft(0)); // Operand 1 load_to_freg(context.inst->get_operand(1), FReg::ft(1)); // Operand 2 auto dest_reg = std::string("ft0"); // 根据指令类型生成汇编 switch (context.inst->get_instr_type()) { case Instruction::fadd: output.emplace_back("fadd.s "+dest_reg+","+sreg0+","+sreg1); break; case Instruction::fsub: output.emplace_back("fsub.s "+dest_reg+","+sreg0+","+sreg1); break; case Instruction::fmul: output.emplace_back("fmul.s "+dest_reg+","+sreg0+","+sreg1); break; case Instruction::fdiv: output.emplace_back("fdiv.s "+dest_reg+","+sreg0+","+sreg1); break; default: assert(false); } // 将结果填入栈帧中 store_from_freg(context.inst,FReg::ft(0)); } void CodeGen::gen_zext() { auto sreg0=std::string("t0"); auto dest_reg = std::string("t0"); auto *type = context.inst->get_type(); if (type->is_float_type()) { sreg0=std::string("ft0"); load_to_freg(context.inst->get_operand(0), FReg::ft(0)); // Operand 1 dest_reg=std::string("ft0"); append_inst(GR2FR + string(" ")+sreg0+","+dest_reg);//放到合适的位置 } else { load_to_greg(context.inst->get_operand(0), Reg::t(0)); // Operand 1 if (type->is_int8_type()) { append_inst("andi " + dest_reg + ", " + sreg0 + ", 0xff"); } else if (type->is_int16_type()) { append_inst("andi " + dest_reg + ", " + sreg0 + ", 0xffff"); }else if(sreg0!=dest_reg){ append_inst("add "+dest_reg+", zero, "+sreg0); } } if(type->is_float_type()){ store_from_freg(context.inst,FReg::ft(0)); }else{ store_from_greg(context.inst,Reg::t(0)); } } void CodeGen::gen_call() { auto *callInst = static_cast<CallInst *>(context.inst); auto retType = callInst->get_function_type()->get_return_type(); int gregs = 0; // 通用寄存器参数计数器 int fregs = 0; // 浮点寄存器参数计数器 // 处理函数参数,按照类型加载到相应的寄存器 for (auto& arg : callInst->get_operands()) { auto argType = arg->get_type(); if (argType->is_float_type()) { load_to_freg(arg, FReg::fa(fregs++)); // 加载到浮点寄存器 } else if (argType->is_pointer_type() || argType->is_integer_type()) { load_to_greg(arg, Reg::a(gregs++)); // 加载到通用寄存器 } } // 生成函数调用指令 append_inst("jal " + callInst->get_operand(0)->get_name()); // 根据返回值类型选择寄存器存储返回值 if (retType->is_float_type()) { store_from_freg(callInst, FReg::fa(0)); // 浮点返回值 } else if (retType->is_integer_type()) { store_from_greg(callInst, Reg::a(0)); // 整数返回值 } } /* * %op = getelementptr [10 x i32], [10 x i32]* %op, i32 0, i32 %op //多维数组访问 * %op = getelementptr i32, i32* %op, i32 %op //一维数组/直接访问指针 * * Memory layout * - ^ * +-----------+ | 低地址 * | arg ptr |---+ | //arg ptr 是你传给 GEP 的起始指针(基准地址) * +-----------+ | | * | | | | * +-----------+ / | * | |<-- | * | | \ | * | | | | //Array 是连续内存的数组区域,GEP 会根据偏移量在这里面计算具体元素地址。 * | Array | | | * | | | | * | | | | * | | | | * +-----------+ | | * | Pointer |---+ | //Pointer 表示计算完地址后的结果,即 GEP 的结果(往往是你要访问或存储的内存地址)。 * +-----------+ | * | | | * +-----------+ | * | | | * +-----------+ | * | | | * +-----------+ | 高地址 * + */ void CodeGen::gen_gep() { auto *gepInst = static_cast<GetElementPtrInst *>(context.inst); int len=gepInst->get_num_operand(); // 操作数个数,包含指针 + 若干维度的下标 std::vector<Value *> ops=gepInst->get_operands(); // 获取所有操作数 //拿到基准地址->拿到值->基准地址修改一下->存回去 if(len>=3){ // TODO9: 完善多维数组地址计算,形如 a[i][j] 、 a[i][j][k]等形式的访问 // 提示:1. 操作数从第二个开始处理即可,第一个操作数是基准指针,后面的操作数都表示下标, // 2. 具体可以用context.inst->get_operand(j)获取第j+1个操作数。 // 3. 依次处理每一维度下标,将其乘以对应元素大小,累加偏移量。 // 需要考虑元素大小超过imm12范围的情况,比如int a[2][300][300];时, // 处理第一维时,每个 a[i] 是一个 300x300 的二维数组,共 360000 字节,超过imm12 // 4. 将偏移量加到基准指针上,得到最终地址。并存入当前指令目标变量对应的栈帧位置 // throw not_implemented_error{__FUNCTION__}; // 处理多维数组 load_to_greg(ops[0], Reg::t(1)); // 基地址 append_inst("addi t2, zero, 0"); // 初始化总偏移量 // 获取基类型(指针指向的类型Type* current_type = gepInst->get_element_type(); for (int j = 1; j < len; j++) { // 计算当前维度的步长(元素大小) int step = 0; // if (current_type->is_array_type()) { // step = current_type->get_array_element_type()->get_size(); // current_type = current_type->get_array_element_type(); // } else if (current_type->is_pointer_type()) { // step = current_type->get_pointer_element_type()->get_size(); // current_type = current_type->get_pointer_element_type(); // } else { // step = current_type->get_size(); // } if (current_type->is_array_type()) { int num_elements = current_type->get_array_element_type()->get_size(); Type* element_type = current_type->get_array_element_type(); step = element_type->get_size() * num_elements; current_type = element_type; } else if (current_type->is_pointer_type()) { step = current_type->get_pointer_element_type()->get_size(); current_type = current_type->get_pointer_element_type(); } else { step = current_type->get_size(); } load_to_greg(gepInst->get_operand(j), Reg::t(0)); append_inst("mul t0, t0, t3"); // 下标 × 步长 append_inst("add t2, t2, t0"); // 累加偏移 } append_inst("add t0, t1, t2"); // 基地址 + 总偏移 store_from_greg(context.inst, Reg::t(0)); // TODO9-------------------end }else{//形如a[i]的访问,或访问指针 auto dest_reg=std::string("t0"); auto *ptr = context.inst->get_operand(0); // 指针 auto ptr_reg=std::string("t1"); load_to_greg(ptr, Reg::t(1)); // 加载基准地址 auto *idx = context.inst->get_operand(1); // 下标 auto idx_reg=std::string("t0");//这个是常数,也就是数组下标 load_to_greg(idx, Reg::t(0)); // 加载下标值 // 以下三条指令实现乘以 4(即元素大小为 4 的简化情况): append_inst("add s11,"+idx_reg+" , "+idx_reg); // s11 = 2 * idx append_inst("add s11,s11,s11");// s11 = 4 * idx // t0 = ptr_reg + s11,即最终地址 = 原始地址 + 偏移 append_inst("add "+dest_reg+",s11,"+ptr_reg); //把t0里存的最终地址存回栈帧的对应位置 //比如当前IR是 %op0 = getelementptr xxxxxx ,那这里就是把计算得到的地址存到%op0在栈帧中的位置 store_from_greg(context.inst, Reg::t(0)); } } void CodeGen::gen_sitofp() { auto *itfInst = static_cast<SiToFpInst *>(context.inst); std::vector<Value *> ops=itfInst->get_operands(); auto sreg0=std::string("t0"); load_to_greg(context.inst->get_operand(0),Reg::t(0)); auto dest_reg= std::string("ft0"); append_inst(GR2FR ,{dest_reg, sreg0}); store_from_freg(context.inst,FReg::ft(0)); } void CodeGen::gen_fptosi() { // TODO8: 浮点数转向整数,注意向下取整(rtz),你可能会用到指令fcvt.w.s // throw not_implemented_error{__FUNCTION__}; load_to_freg(context.inst->get_operand(0), FReg::ft(0)); append_inst("fcvt.w.s t0, ft0"); store_from_greg(context.inst, Reg::t(0)); // TODO8--------------------end } void CodeGen::global_array_int(ConstantArray * init_val){//全局整型数组变量 /*示例输出 int a[5]={0,1,2,3,4}; .data .globl a .align 3 .type a, @object .size a, 20 a: .word 0 .word 1 .word 2 .word 3 .word 4 */ for (unsigned i = 0; i < init_val->get_size_of_array(); i++) {//获得这一层的大小 Constant *element = init_val->get_element_value(i); if (!dynamic_cast<ConstantArray *>(element)) {//这个元素已经不再是array了 auto *IntVal = static_cast<ConstantInt *>(element); append_inst(".word", {std::to_string(IntVal->get_value())}, ASMInstruction::Atrribute); }else{ //这个元素依然是array,递归下去 auto new_array=static_cast<ConstantArray *>(element); global_array_int(new_array); } } } void CodeGen::global_array_float(ConstantArray * init_val){ /*示例输出 float a[3]={1.01,4.11,13.99}; .data .globl a .align 3 .type a, @object .size a, 12 a: .word 1065437102 //float 1.01 .word 1082361119 //float 4.11 .word 1096800010 //float 13.99 */ // TODO5-2:完善浮点型全局数组变量初始化 // 提示:可以参考global_array_int的实现 // throw not_implemented_error{__FUNCTION__}; for (unsigned i = 0; i < init_val->get_size_of_array(); i++) { Constant *element = init_val->get_element_value(i); if (!dynamic_cast<ConstantArray *>(element)) { auto *FPVal = static_cast<ConstantFP *>(element); float val = FPVal->get_value(); // 安全的位模式转换 union { float f; int32_t i; } converter; converter.f = val; append_inst(".word", {std::to_string(converter.i)}, ASMInstruction::Atrribute); } else { global_array_float(static_cast<ConstantArray *>(element)); } } // TODO5-2------------------end } void CodeGen::run() { // 确保每个函数中基本块的名字都被设置好 m->set_print_name(); /* 使用 GNU 伪指令为全局变量分配空间 * 你可以使用 `la` 指令将标签 (全局变量) 的地址载入寄存器中, 比如 * 要将 `a` 的地址载入 t0, 只需要 `la t0, a` * 由于在IR自动化生成阶段,我们为无初始值的全局变量分配了0作为初始值,因此在目标代码生成阶段,全局变量都有初始值 */ if (!m->get_global_variable().empty()) { append_inst("Global variables", ASMInstruction::Comment); /* * 虽然可以使用 `.bss` 伪指令为未初始化数据分配空间, * 我们依然显式指定 `.data` 段,这是因为: * * - `.data` 更加通用,与标准 RISC-V 编译器行为一致; * - `.bss` 虽然常用于未初始化数据,但某些旧版本 GNU 汇编器对其支持不完善; * - 显式使用 `.data` 能更好地控制输出段结构。 */ append_inst(".text", ASMInstruction::Atrribute); append_inst(".data",ASMInstruction::Atrribute); for (auto &global : m->get_global_variable()) { //给全局变量分配空间 if(global.get_type()->get_pointer_element_type()->is_integer_type()){//处理整数型全局变量 auto *IntVal = static_cast<ConstantInt *>(global.get_init()); /* 输出形式示例: .globl a .align 2 .type a, @object .size a, 4 a: .word 5 */ auto size = global.get_type()->get_pointer_element_type()->get_size(); append_inst(".globl", {global.get_name()}, ASMInstruction::Atrribute); append_inst(".align 2", ASMInstruction::Atrribute); // 对齐到 4 字节 append_inst(".type", {global.get_name(), "@object"}, ASMInstruction::Atrribute); append_inst(".size", {global.get_name(), std::to_string(size)}, ASMInstruction::Atrribute); append_inst(global.get_name(), ASMInstruction::Label); append_inst(".word", {std::to_string(IntVal->get_value())}, ASMInstruction::Atrribute); }else if(global.get_type()->get_pointer_element_type()->is_array_type()){ //处理数组类型全局变量 /* 输出形式示例: .globl a .data .align 3 .type a, @object .size a, 20 a: .word 0 .word 1 .word 2 .word 3 .word 4 */ if(global.get_type()->get_pointer_element_type()->get_array_element_type()->is_integer_type()){ //整型数组 auto size = global.get_type()->get_pointer_element_type()->get_size(); append_inst(".globl", {global.get_name()}, ASMInstruction::Atrribute); append_inst(".align 3", ASMInstruction::Atrribute); // 对齐到 8 字节 append_inst(".type", {global.get_name(), "@object"}, ASMInstruction::Atrribute); append_inst(".size", {global.get_name(), std::to_string(size)}, ASMInstruction::Atrribute); append_inst(global.get_name(), ASMInstruction::Label); if(dynamic_cast<ConstantZero *>(global.get_init())){ // 初始化值为 0,使用 `.space` 节省空间 append_inst(".space", {std::to_string(size)}, ASMInstruction::Atrribute); }else{ //如果不是0 auto *IntVal = static_cast<ConstantArray *>(global.get_init()); global_array_int(IntVal); } }else{ //浮点型数组 // TODO5-1:完善浮点型全局数组变量声明及其初始化 // 提示:你可能需要将初始化值不为0的浮点型全局数组变量的处理逻辑封装到global_array_float函数中,因此可能需要完成TODO5-2,即填充global_array_float函数 // 当然你也可以直接在当前else分支内处理,弃用global_array_float函数(对应的TODO5-2也不用完成) // throw not_implemented_error{__FUNCTION__}; auto size = global.get_type()->get_pointer_element_type()->get_size(); append_inst(".globl", {global.get_name()}, ASMInstruction::Atrribute); append_inst(".align 3", ASMInstruction::Atrribute); // 8字节对齐 append_inst(".type", {global.get_name(), "@object"}, ASMInstruction::Atrribute); append_inst(".size", {global.get_name(), std::to_string(size)}, ASMInstruction::Atrribute); append_inst(global.get_name(), ASMInstruction::Label); if (dynamic_cast<ConstantZero *>(global.get_init())) { append_inst(".space", {std::to_string(size)}, ASMInstruction::Atrribute); } else { auto *FPVal = static_cast<ConstantArray *>(global.get_init()); global_array_float(FPVal); } // TODO5-1------------------end } }else if(global.get_type()->get_pointer_element_type()->is_float_type()){ //浮点型全局变量 /* 输出形式示例: float a=1.01; .globl a .align 2 .type a, @object .size a, 4 a: .word 1065437102 // float 1.01 */ // TODO4:完善浮点型全局变量声明及其初始化 // 提示:RISC-V 中没有 .float 指令,需手动将 float 转换为 int 再用 .word 表示原始比特位 // 可以使用 reinterpret_cast<int&>(float) 实现 float → int 的位级转换 // throw not_implemented_error{__FUNCTION__}; auto *FPVal = static_cast<ConstantFP *>(global.get_init()); float val = FPVal->get_value(); int32_t bytes = *reinterpret_cast<int32_t*>(&val); append_inst(".globl", {global.get_name()}, ASMInstruction::Atrribute); append_inst(".align 2", ASMInstruction::Atrribute); append_inst(".type", {global.get_name(), "@object"}, ASMInstruction::Atrribute); append_inst(".size", {global.get_name(), "4"}, ASMInstruction::Atrribute); append_inst(global.get_name(), ASMInstruction::Label); append_inst(".word", {std::to_string(bytes)}, ASMInstruction::Atrribute); // TODO4--------------------------end } } } // 函数代码段 append_inst(".text", ASMInstruction::Atrribute); append_inst(".align 2",ASMInstruction::Atrribute); for (auto &func : m->get_functions()) { if (not func.is_declaration()) { // 更新 context context.clear(); context.func = &func; // 函数信息 append_inst(".globl", {func.get_name()}, ASMInstruction::Atrribute); append_inst(".type", {func.get_name(), "@function"}, ASMInstruction::Atrribute); append_inst(func.get_name(), ASMInstruction::Label); // 分配函数栈帧 allocate(); // 生成 prologue gen_prologue(); //处理bb for (auto &bb : func.get_basic_blocks()) { context.bb = &bb; append_inst(label_name(context.bb), ASMInstruction::Label); for (auto &instr : bb.get_instructions()) { // For debug append_inst(instr.print(), ASMInstruction::Comment); context.inst = &instr; // 更新 context switch (instr.get_instr_type()) { case Instruction::ret: gen_ret(); break; case Instruction::br: gen_br(); break; case Instruction::add: case Instruction::sub: case Instruction::mul: case Instruction::sdiv: case Instruction::srem: gen_binary(); break; case Instruction::fadd: case Instruction::fsub: case Instruction::fmul: case Instruction::fdiv: gen_float_binary(); break; case Instruction::alloca: gen_alloca(); break; case Instruction::load: gen_load(); break; case Instruction::store: gen_store(); break; case Instruction::ge: case Instruction::gt: case Instruction::le: case Instruction::lt: case Instruction::eq: case Instruction::ne: gen_icmp(); break; case Instruction::fge: case Instruction::fgt: case Instruction::fle: case Instruction::flt: case Instruction::feq: case Instruction::fne: gen_fcmp(); break; case Instruction::phi: break; case Instruction::call: gen_call(); break; case Instruction::getelementptr: gen_gep(); break; case Instruction::zext: gen_zext(); break; case Instruction::fptosi: gen_fptosi(); break; case Instruction::sitofp: gen_sitofp(); break; default: assert(false && "Unhandled instruction type"); } } } // 生成 epilogue gen_epilogue(); } } } std::string CodeGen::print() const { std::string result; for (const auto &inst : output) { result += inst.format(); } auto sub = result.find("memset_int"); while (sub != string::npos) { result.replace(sub, 10, "memset"); sub = result.find("memset_int"); } sub = result.find("memset_float"); while (sub != string::npos) { result.replace(sub, 12, "memset"); sub = result.find("memset_float"); } return result; } 进阶实验TODO9的问题 在TODO9的gen_gep函数中有这样一行代码: int len = gepInst->get_num_operand(); // 操作数个数,包含指针 + 若干维度的下标 这行代码是老师你所给定的代码(不包含在TODO中),然后根据已有的逻辑:当len>=3(也就是访问形如a[i][j])是分支跳转到TODO9中,否则(也就是访问形如a[i])运行老师所给的代码。 目前问题是:当我sysy中访问a[i]类数组时,len的值为3,程序不会跳转到else代码块。然后一旦运行我所写的TODO9代码,程序将发生段错误。 所以我尝试将判断条件改成if(len>=4),那么这样程序将遇到新问题: 假如我声明了数组int w[2] = {6, 2}; 当我访问w[0]和w[1]时的返回值始终是6,甚至越界访问w[2]的返回值也是6。 所以还请老师看看问题出在了哪里? (其实我还有一个问题:我本地测试lv4/fcmp2时程序报错Error: unrecognized opcode `fne.s t0,ft0,ft1’,就是说我这里指令集没有包含fne,不知道是我环境问题还是什么。 ps:我测试了一下,这段代码能解决第一个问题: load_to_greg(ops[0], Reg::t(1)); // 基地址 for (size_t i = 2; i < ops.size(); ++i) { load_to_greg(ops[i], Reg::t(0)); auto elem_size = gepInst->get_element_type()->get_size(); load_large_int32(elem_size, Reg::s(11)); append_inst(“mul t0, t0, s11”); append_inst(“add t1, t1, t0”); } store_from_greg(context.inst, Reg::t(1)); 发表时间: 2025-05-23 03:55:52 回复 助教-编译原理Z 助教-编译原理Z 发表文章: 63 Re: 进阶实验TODO9的问题 针对问题1: 抱歉,这里注释有误,当len>=3(就是是访问数组,a[i]也包括在内),可以回顾一下Light IR,以 a[0] 为例,GEP 需要两个下标{0,0},所以对应了3个操作数(基地址、索引1、索引2),getelementptr [2 x i32], [2 x i32]* %op0, i32 0, i32 0 第一个 0 是:从 aAlloca 这个变量指针出发(GEP 要先“解一层指针”) sailor@sailor:~/Test/compiler-labs/tests/4-code-gen/autogen$ python3 eval_lab5.py ./testcases/ debug [info] Start testing, using testcase dir: /home/sailor/Test/compiler-labs/tests/4-code-gen/autogen/testcases [info] Answer path: /home/sailor/Test/compiler-labs/tests/4-code-gen/autogen/answers lv0/frame1…OK lv0/frame2…OK lv0/return…OK lv1/load1…OK lv1/load2…OK lv1/load3…OK lv1/load4…OK lv2/global_float1…OK lv2/global_float2…OK lv2/global_float_array…OK lv3/br1…CE lv3/br2…OK lv3/br3…CE lv4/fcmp1…CE lv4/fcmp2…OK lv4/fcmp3…OK lv4/fcmp4…CE lv4/fcmp5…OK lv4/fcmp6…CE lv4/fcmp7…CE lv5/array1…OK lv5/array2…OK lv5/array3…WA lv5/array4…OK lv5/array5…WA lv5/array6…WA sailor@sailor:~/Test/compiler-labs/tests/4-code-gen/autogen$ cat log.txt ==========/home/sailor/Test/compiler-labs/tests/4-code-gen/autogen/testcases/lv0/frame1.sysy========== [Output] Your Output: 6 [Output] Expected Output: 6 ==========/home/sailor/Test/compiler-labs/tests/4-code-gen/autogen/testcases/lv0/frame2.sysy========== [Output] Your Output: 13 [Output] Expected Output: 13 ==========/home/sailor/Test/compiler-labs/tests/4-code-gen/autogen/testcases/lv0/return.sysy========== [Output] Your Output: 3 [Output] Expected Output: 3 ==========/home/sailor/Test/compiler-labs/tests/4-code-gen/autogen/testcases/lv1/load1.sysy========== [Output] Your Output: 10 [Output] Expected Output: 10 ==========/home/sailor/Test/compiler-labs/tests/4-code-gen/autogen/testcases/lv1/load2.sysy========== [Output] Your Output: 5 [Output] Expected Output: 5 ==========/home/sailor/Test/compiler-labs/tests/4-code-gen/autogen/testcases/lv1/load3.sysy========== [Output] Your Output: 6 [Output] Expected Output: 6 ==========/home/sailor/Test/compiler-labs/tests/4-code-gen/autogen/testcases/lv1/load4.sysy========== [Output] Your Output: -6 10 [Output] Expected Output: -6 10 ==========/home/sailor/Test/compiler-labs/tests/4-code-gen/autogen/testcases/lv2/global_float1.sysy========== [Output] Your Output: 0x1.070a3ep+2 5 [Output] Expected Output: 0x1.070a3ep+2 5 ==========/home/sailor/Test/compiler-labs/tests/4-code-gen/autogen/testcases/lv2/global_float2.sysy========== [Output] Your Output: 0x1.070a3ep+2 0 [Output] Expected Output: 0x1.070a3ep+2 0 ==========/home/sailor/Test/compiler-labs/tests/4-code-gen/autogen/testcases/lv2/global_float_array.sysy========== [Output] Your Output: -0x1.bfae14p+3 248 [Output] Expected Output: -0x1.bfae14p+3 248 ==========/home/sailor/Test/compiler-labs/tests/4-code-gen/autogen/testcases/lv3/br1.sysy========== ==========/home/sailor/Test/compiler-labs/tests/4-code-gen/autogen/testcases/lv3/br2.sysy========== [Output] Your Output: 3 [Output] Expected Output: 3 ==========/home/sailor/Test/compiler-labs/tests/4-code-gen/autogen/testcases/lv3/br3.sysy========== ==========/home/sailor/Test/compiler-labs/tests/4-code-gen/autogen/testcases/lv4/fcmp1.sysy========== ==========/home/sailor/Test/compiler-labs/tests/4-code-gen/autogen/testcases/lv4/fcmp2.sysy========== [Output] Your Output: 0x1.070a3ep+2 7 [Output] Expected Output: 0x1.070a3ep+2 7 ==========/home/sailor/Test/compiler-labs/tests/4-code-gen/autogen/testcases/lv4/fcmp3.sysy========== [Output] Your Output: 0x1.070a3ep+2 5 [Output] Expected Output: 0x1.070a3ep+2 5 ==========/home/sailor/Test/compiler-labs/tests/4-code-gen/autogen/testcases/lv4/fcmp4.sysy========== ==========/home/sailor/Test/compiler-labs/tests/4-code-gen/autogen/testcases/lv4/fcmp5.sysy========== [Output] Your Output: 0x1.070a3ep+2 55 [Output] Expected Output: 0x1.070a3ep+2 55 ==========/home/sailor/Test/compiler-labs/tests/4-code-gen/autogen/testcases/lv4/fcmp6.sysy========== ==========/home/sailor/Test/compiler-labs/tests/4-code-gen/autogen/testcases/lv4/fcmp7.sysy========== ==========/home/sailor/Test/compiler-labs/tests/4-code-gen/autogen/testcases/lv5/array1.sysy========== [Output] Your Output: 1 2 [Output] Expected Output: 1 2 ==========/home/sailor/Test/compiler-labs/tests/4-code-gen/autogen/testcases/lv5/array2.sysy========== [Output] Your Output: 0x1.028f5cp+0 4 [Output] Expected Output: 0x1.028f5cp+0 4 ==========/home/sailor/Test/compiler-labs/tests/4-code-gen/autogen/testcases/lv5/array3.sysy========== [Output] Your Output: 3 [Output] Expected Output: 5 [WA] Output mismatch: /home/sailor/Test/compiler-labs/tests/4-code-gen/autogen/answers/lv5/array3.out vs output/lv5/array3.out ==========/home/sailor/Test/compiler-labs/tests/4-code-gen/autogen/testcases/lv5/array4.sysy========== [Output] Your Output: 0x1.a66666p+1 3 [Output] Expected Output: 0x1.a66666p+1 3 ==========/home/sailor/Test/compiler-labs/tests/4-code-gen/autogen/testcases/lv5/array5.sysy========== [Output] Your Output: 4 [Output] Expected Output: 8 [WA] Output mismatch: /home/sailor/Test/compiler-labs/tests/4-code-gen/autogen/answers/lv5/array5.out vs output/lv5/array5.out ==========/home/sailor/Test/compiler-labs/tests/4-code-gen/autogen/testcases/lv5/array6.sysy========== [Output] Your Output: 0x1.a66666p+1 5 [Output] Expected Output: 0x1.19999ap+2 5 [WA] Output mismatch: /home/sailor/Test/compiler-labs/tests/4-code-gen/autogen/answers/lv5/array6.out vs output/lv5/array6.out 第二个0是:访问数组的第 0 项,也就是a[0] 而else的情况对应数组解引用,不过Sysy中没有定义指针类型 至于问题2:“指令集没有包含fne”,RISC-V本来就是没有fne.s指令的,我在注释中是提示了feq.s、flt.s、fle.s、xori等指令,在文档《RISC-V汇编介绍》中也是介绍了feq.s / flt.s / fle.s,并未提及fne.s这个不存在的RISC-V指令,请仔细阅读文档。 发表时间: 2025-05-23 20:33:26 回复 1033220327 彭俊斓 发表文章: 18 Re: 进阶实验TODO9的问题 谢谢老师,这两个问题我成功解决了。 但是全局整型数组初始化无法正常执行,这不属于我们TODO该做的内容。 比如有这样一份待翻译代码: int arr[2][3] = { {1, 2, 3}, {4, 5, 6} }; int main() { return arr[1][2]; } 将其翻译成汇编代码时汇编代码没有.word来定义数组元素的值,这是对应.s汇编代码的一部分: .globl arr .align 3 .type arr, @object .size arr, 24 arr: 这导致最终的返回值不确定(我这里返回值是8)。 但是当arr为局部数组时: int main() { int arr[2][3] = { {1, 2, 3}, {4, 5, 6} }; return arr[1][2]; } 对应的汇编代码却能够正常初始化数组,也就是说程序能够正确返回出数组里面的六个值。 定位到CodeGen.cpp的run()函数,我发现对全局整型数组变量的初始化好像只考虑了一维的情况,事实上把数组定义为一维的运行结果也确实没问题。所以请老师帮我看看问题出在了哪里。 发表时间: 2025-05-24 04:14:29 回复 助教-编译原理Z 助教-编译原理Z 发表文章: 63 Re: 进阶实验TODO9的问题 这一处确实有点问题,在处理多维整型数组时实际进入的是else分支,整型数组分支只处理一维整型数组(因为global.get_type()->get_pointer_element_type()->get_array_element_type()是个array type) 不过其实在实现global_array_float也就是TODO5-2时,模仿global_array_int的逻辑,只需要在处理到具体数组元素时(!dynamic_cast<ConstantArray *>(element)为真时)改成显示转换(static_cast)成ConstantFP,但这并不会影响全局多维整型数组的创建的。你可以尝试一下修改你的global_array_float,把处理到数组元素的判断条件改成!dynamic_cast<ConstantArray *>(element)
最新发布
05-30
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值