使用 C++23 从零实现 RISC-V 模拟器(2):内存和总线

👉🏻 文章汇总「从零实现模拟器、操作系统、数据库、编译器…」:https://okaitserrj.feishu.cn/docx/R4tCdkEbsoFGnuxbho4cgW2Yntc

内存和总线

上一部分将内存全部放到了 CPU 里面,总线的概念是隐含着的。这一部分将内存拆分出来,再引入总线的概念,CPU 通过总线连接内存。

完整代码可以查看这个分支:https://github.com/weijiew/crvemu/tree/lab2-memory

实际上可以直接看代码,文章作为补充,这部分内容很简单。后续内容并没有完全将代码的所有修改列出来,建议快速浏览下面的内容有一个整体的认识后再结合代码学习。

1. CPU、内存和总线之间的关系

下面展示了 CPU、内存和总线之间的关系:

                    +-----+| CPU |+-----+|+-------+-------+|       |       |控制总线  数据总线 地址总线|       |       |v       v       v+--------------------------------+|             总线               |+--------------------------------+|       |v       v+-----+       +-----+| 内存 |<---->| I/O  |+-----+       +-----+

在这个简化模型中:

  • CPU:作为计算中心,它执行程序代码,处理数据。

  • 总线:分为控制总线、数据总线和地址总线,连接 CPU 和内存以及 I/O 设备。

    • 控制总线:CPU 通过它发送控制信号,如读写请求。
    • 数据总线:实际数据在 CPU 和内存之间的传输通道。
    • 地址总线:指定数据来源或目标位置的内存地址。
  • 内存:存储指令和数据,供 CPU 直接访问。

这个表示强调了 CPU 通过不同类型的总线与内存进行通信的方式,体现了它们之间的关系。

2. 内存 Dram

上一节已经实现的部分中内存是放在了 CPU 中,下面要将内存单独拆分出来作为一个类名为 Dram 的类(“Dynamic Random Access Memory” 动态随机访问内存)。随后再实现一个类名为 bus 的类来表示总线, CPU 通过总线 bus 读写内存 Dram ,接下来先实现 Dram 。

在实现之前要先定义几个参数来表示从内存中哪里开始读取,在 Qemu 中不是从物理地址 0 开始读取的,而是定义了一个具体的数字,下面的内容会详细讲解。

2.1 参数

上面的代码中涉及到了一些参数还没有定义,接下来定义一下参数。

// param.cpp
#include <cstddef> // 引入定义 std::size_t 的头文件// 定义DRAM的基地址
constexpr std::size_t DRAM_BASE = 0x8000'0000;// 定义DRAM的大小,128MB
constexpr std::size_t DRAM_SIZE = 1024 * 1024 * 128;// 定义DRAM的结束地址
constexpr std::size_t DRAM_END = DRAM_SIZE + DRAM_BASE - 1;

这三个参数是在计算机内存管理上下文中定义的,用于指定特定内存区域(在本例中是 DRAM,即动态随机访问内存)的基本属性。

  • DRAM_BASE 定义了 DRAM 内存区域的起始物理地址。

qemu 中定义了这个变量,这个地址是一个十六进制数,从 0x8000'0000 处开始执行,即内存区域的开始点。

  • DRAM_SIZE 定义了 DRAM 区域的总大小。

这个变量指定了从DRAM_BASE开始,可以用于存储数据的内存量。这个大小是以字节为单位的,对于内存大小的计算通常使用字节作为基本单位。DRAM_SIZE被定义为1024 * 1024 * 128字节,即 128MB。这是通过计算 1024 字节(1KB)乘以 1024(即 1MB)再乘以 128 得到的,即 DRAM 区域有 128MB 的存储容量。

  • DRAM_END定义了 DRAM 内存区域的结束地址。

基于DRAM_BASEDRAM_SIZE计算得出,指出了 DRAM 区域的最后一个字节的地址。这个地址用于界定 DRAM 区域的范围,对于确定内存访问是否越界很有帮助。

总结来说,这三个参数共同定义了 DRAM 内存区域的物理位置和大小,是计算机内存管理的基本组成部分。通过这些参数,操作系统和应用程序可以正确地定位和管理内存资源。

下面是涉及到现代 C++ 语法层面的解释:

  1. 使用std::size_t替换uint64_t用于表示大小

虽然在上述代码中使用uint64_t对于定义 DRAM 大小和地址范围是合适的,但在 C++中,表示大小或基于内存的索引时通常推荐使用std::size_t。这是因为std::size_t是一个无符号整数类型,其大小是为了能够安全地表示对象的大小,以及对象最大可能的索引,这样可以增强代码的可移植性和安全性。

  1. 使用constexpr确保编译时常量

代码已经正确使用了constexpr来声明编译时常量,这是现代 C++推荐的做法,因为它可以在编译时而不是运行时解析这些值,从而提高效率。没有需要修改的地方。关于 constexpr 可以进一步阅读这篇文章。

  1. 使用单引号(')作为数字分隔符

这个特性自 C++14 起被引入,允许开发者在数字字面量中加入单引号来分隔数字,使得长数字序列更容易被阅读。对于DRAM_BASE的定义,我们可以这样改写来增加其可读性:

constexpr std::size_t DRAM_BASE = 0x8000'0000;

这里,0x8000'00000x80000000在编译时是完全相同的,但加入分隔符后,数字更易于阅读,尤其是对于较长的十六进制或十进制数值。这种写法没有改变原有的数值,只是使得数值的表示更为友好。使用这种方式,你可以使代码更加清晰和易于维护。

2.2 实现 Dram

接下来讲解如何实现 Dram ,下面是一个最简的形式,简单来说用一个 vector 来表示内存,Dram 初始化的时候需要将指令写入内存中。

// dram.cpp
// 定义一个名为Dram的类,用于模拟DRAM(动态随机访问内存)的行为。
class Dram {
public:// 类的构造函数,接受一个包含机器码(即初始化代码)的vector作为参数。Dram(const std::vector<uint8_t>& code) {// 将dram成员变量的大小调整为DRAM_SIZE,并将所有元素初始化为0。// 这里DRAM_SIZE应该是一个在类外部定义的常量,表示DRAM的总容量(字节数)。dram.resize(DRAM_SIZE, 0); // 使用0初始化DRAM。// 将传入的code(机器码)复制到dram向量的开始位置。// std::copy是标准库算法,用于复制一个范围内的元素到另一个范围。// code.begin()和code.end()分别指向传入vector的开始和结束,指定了要复制的数据范围。// dram.begin()指定了目标范围的开始位置。std::copy(code.begin(), code.end(), dram.begin());}private:// 类的私有成员变量,用std::vector<uint8_t>表示DRAM存储的数据。// uint8_t是8位无符号整数类型,代表DRAM中每个存储单元可以存储的数据范围(0-255)。// 使用vector是因为它是一个动态数组,可以灵活地调整大小,并提供随机访问能力。std::vector<uint8_t> dram;
};

接下来添加loadstore成员函数,这些函数将模拟从 DRAM 加载和向 DRAM 存储数据的行为。

2.3 实现 Dram Load 方法

接下来要实现 Dram Load 方法,即从内存中读取指定长度的数据,输入参数为 addr 表示内存地址,size 表示需要读取的长度。目前 size 只能读取 8 位、16 位、32 位或 64 位 。

内存用 vector 来表示,其中一个位置表示 8 bit 所以需要计算 size 对应的比特数,即读取 vector 中多少个位置。随后使用 | 运算符将读取到的数据拼接起来。

下面是具体的代码:

class Dram {
public:// ...// 模拟从DRAM加载数据uint64_t load(uint64_t addr, uint64_t size) {if (size != 8 && size != 16 && size != 32 && size != 64) {throw std::runtime_error("LoadAccessFault");}uint64_t nbytes = size / 8;std::size_t index = (addr - DRAM_BASE);if (index + nbytes > dram.size()) {throw std::out_of_range("Address out of range");}uint64_t value = 0;for (uint64_t i = 0; i < nbytes; ++i) {value |= static_cast<uint64_t>(dram[index + i]) << (i * 8);}return value;}// ...private:std::vector<uint8_t> dram;};

2.4 实现 Dram store 方法

这部分实现写入内存的方法,输入参数需要给定读取对应的内存地址 addr ,待读取的长度 size 和返回值 value 。

和之前读取的方法类似,依旧是计算出来对应的索引然后将数据拼接起来。

class Dram {
public:// ...// 模拟向DRAM存储数据void store(uint64_t addr, uint64_t size, uint64_t value) {if (size != 8 && size != 16 && size != 32 && size != 64) {throw std::runtime_error("StoreAMOAccessFault");}uint64_t nbytes = size / 8;std::size_t index = (addr - DRAM_BASE);if (index + nbytes > dram.size()) {throw std::out_of_range("Address out of range");}for (uint64_t i = 0; i < nbytes; ++i) {dram[index + i] = (value >> (i * 8)) & 0xFF;}}private:std::vector<uint8_t> dram;
};

3. 总线 Bus

Bus 是用来将不同的设备衔接起来,用于在不同组件之间传输数据的通信系统。总线在计算机架构中起到了重要的桥梁作用,连接了各个硬件组件,如处理器、内存、输入/输出设备等。

目前只需要将内存 Dram 连接起来即可,下面是 bus 头文件的定义:

// bus.h
class Bus {
public:Bus(const std::vector<uint8_t>& code);uint64_t load(uint64_t addr, uint64_t size);void store(uint64_t addr, uint64_t size, uint64_t value);private:Dram dram;
};

其中 load 用于同 Dram 交互读取数据,而 store 用于写入数据。接下来实现 load 和 store 方法。

3.1 Bus load store

下面是代码是对 Dram 的包装,首先要检验地址是否合法随后调用 Dram 的方法,反之报错。

Bus::Bus(const std::vector<uint8_t>& code) : dram(code) {}uint64_t Bus::load(uint64_t addr, uint64_t size) {if (addr >= DRAM_BASE && addr <= DRAM_END) {return dram.load(addr, size);} else {throw std::runtime_error("LoadAccessFault at address " + std::to_string(addr));}
}void Bus::store(uint64_t addr, uint64_t size, uint64_t value) {if (addr >= DRAM_BASE && addr <= DRAM_END) {dram.store(addr, size, value);} else {throw std::runtime_error("StoreAMOAccessFault at address " + std::to_string(addr));}
}

4. CPU

上面已经将 Dram、Bus 剥离出来的,接下来需要修改 cpu.cpp 部分的代码,在其中增加 Bus 成员变量,通过 bus 调用 dram 进行读写。

随后删除 std::vector<uint8_t> dram; 成员变量,再提供对应的 store 和 load 方法同 dram 读写。

class Cpu {
public:// ... 其他Bus bus;uint64_t load(uint64_t addr, uint64_t size);void store(uint64_t addr, uint64_t size, uint64_t value);uint32_t fetch();
};

4.1 load 和 store

接下来实现 load 方法:

uint64_t Cpu::load(uint64_t addr, uint64_t size) {try {return bus.load(addr, size);} catch (const Exception& e) {std::cerr << "Exception load: " << e << std::endl;}
}

直接调去 bus 即可,两个参数分别为对应的地址和要读取数据的长度。

store 同上

void Cpu::store(uint64_t addr, uint64_t size, uint64_t value) {try {bus.store(addr, size, value);} catch (const Exception& e) {std::cerr << "Exception store: " << e << std::endl;}
}

4.2 fetch

fetch 即获取 32 位长度的指令。

uint32_t Cpu::fetch() {try {bus.load(pc, 32);} catch (const Exception& e) {std::cerr << "Exception fetch: " << e << std::endl;}
}

目前先解析 32 位,后续再进一步扩展。

不是所有的 RISC-V 指令都是固定的 32 位长度。RISC-V(Reduced Instruction Set Computing - V)是一种基于精简指令集(RISC)的开放标准架构,它提供了多种指令长度的选项,以适应不同的需求。

RISC-V 支持的指令长度包括 32 位、64 位和 128 位。最常见的是 RV32I、RV64I 和 RV128I,它们分别表示 32 位、64 位和 128 位的整数基本指令集。

例如,RV32I 指令是固定长度为 32 位的整数指令集,而 RV64I 则是 64 位的整数指令集。此外,RISC-V 还提供了扩展指令集,如 M 扩展用于整数乘法和除法,A 扩展用于原子操作,F 和 D 扩展用于浮点运算等。

总的来说,RISC-V 的灵活性使得它可以适应不同的应用领域,并且可以选择不同长度的指令集来平衡性能和资源的需求。

5. main

接下来更新 main 函数,读取指令的二进制形式随后执行。

int main(int argc, char* argv[]) {if (argc != 2) {std::cout << "Usage:\n"<< "- ./program_name <filename>\n";return 0;}std::ifstream file(argv[1], std::ios::binary);if (!file) {std::cerr << "Cannot open file: " << argv[1] << std::endl;return 1;}std::vector<uint8_t> code(std::istreambuf_iterator<char>(file), {});Cpu cpu(code); // 假设Cpu类的构造函数接受指令代码的vectortry {while (true) {uint32_t inst = cpu.fetch();auto new_pc = cpu.execute(inst);if (new_pc.has_value()) {cpu.pc = new_pc.value();} else {break;}}} catch (const Exception& e) {std::cerr << "Exception main: " << e << std::endl;}// 使用cpu对象进行操作cpu.dump_registers(); // 打印寄存器状态cpu.dump_pc();return 0;
}

将汇编编译为二进制的形式

$ riscv64-unknown-elf-gcc -Wl,-Ttext=0x0 -nostdlib -o add-addi add-addi.s
$ riscv64-unknown-elf-objcopy -O binary add-addi add-addi.bin

编译并执行指令,运行并测试是否正确:

mkdir -p build && cd build && cmake .. && make && ./crvemu ../add-addi.bin

6. 测试

此外本节内容引入了单元测试,将上面手动测试的过程封装为函数:

$ riscv64-unknown-elf-gcc -Wl,-Ttext=0x0 -nostdlib -o add-addi add-addi.s
$ riscv64-unknown-elf-objcopy -O binary add-addi add-addi.bin

下面是最终的单元测试:

// 消除警告: warning: cannot find entry symbol _start; defaulting to 0000000000000000
const std::string start = ".global _start \n _start:";// Test addi instruction
TEST(RVTests, TestAddi) {std::string code = start + "addi x31, x0, 42 \n";Cpu cpu = rv_helper(code, "test_addi", 1);EXPECT_EQ(cpu.regs[31], 42) << "Error: x31 should be 42 after ADDI instruction";
}// Test add instruction
TEST(RVTests, TestAdd) {std::string code = ".global _start \n _start:""addi x2, x0, 10 \n"   // 将 10 加载到 x2 中"addi x3, x0, 20 \n"   // 将 20 加载到 x3 中"add x1, x2, x3 \n";  // x1 = x2 + x3Cpu cpu = rv_helper(code, "test_add", 3);// 验证 x1 的值是否正确EXPECT_EQ(cpu.regs[1], 30) << "Error: x1 should be the result of ADD instruction";
}

5.1 rv_helper

通过 rv_helper 函数实现了将字符串转为汇编、二进制再放入 CPU 中执行。

三个参数分别为汇编代码的字符串形式,测试对应的名称,待测试的指令个数。

Cpu rv_helper(const std::string& code, const std::string& testname, size_t n_clock) {std::string filename = testname + ".s";// 创建并写入汇编文件std::ofstream file(filename);if (!file.is_open()) {throw std::runtime_error("Failed to create assembly file.");}file << code;file.close();// 生成目标文件和二进制文件generate_rv_obj(filename.c_str());generate_rv_binary(testname.c_str());// 读取二进制文件内容std::string binFilename = testname + ".bin";std::ifstream file_bin(binFilename, std::ios::binary);if (!file_bin.is_open()) {throw std::runtime_error("Failed to open binary file.");}std::vector<uint8_t> binaryCode((std::istreambuf_iterator<char>(file_bin)), std::istreambuf_iterator<char>());// 初始化CPU并执行指令Cpu cpu(binaryCode);for (size_t i = 0; i < n_clock; ++i) {try {uint64_t inst = cpu.fetch();auto new_pc = cpu.execute(inst);if (new_pc.has_value()) {cpu.pc = new_pc.value();} else {break;}} catch (const std::exception& e) {std::cerr << "CPU execution error: " << e.what() << std::endl;break;}}return cpu;
}

5.2 generate_rv_obj

此函数为 riscv64-unknown-elf-gcc -Wl,-Ttext=0x0 -nostdlib -o add-addi add-addi.s 对应的处理过程:

void generate_rv_obj(const std::string& assembly) {// 使用C++的字符串处理能力来获取不含扩展名的文件名size_t dotPos = assembly.find_last_of(".");std::string baseName = (dotPos == std::string::npos) ? assembly : assembly.substr(0, dotPos);std::string command = "riscv64-unknown-elf-gcc -Wl,-Ttext=0x0 -nostdlib -o " + baseName + " " + assembly;// 执行命令int result = std::system(command.c_str());// 检查命令执行结果if (result != 0) {std::cerr << "Failed to generate RV object from assembly: " << assembly << std::endl;}
}

5.2 generate_rv_obj

此函数为 riscv64-unknown-elf-gcc -Wl,-Ttext=0x0 -nostdlib -o add-addi add-addi.s 对应的处理过程:

void generate_rv_obj(const std::string& assembly) {// 使用C++的字符串处理能力来获取不含扩展名的文件名size_t dotPos = assembly.find_last_of(".");std::string baseName = (dotPos == std::string::npos) ? assembly : assembly.substr(0, dotPos);std::string command = "riscv64-unknown-elf-gcc -Wl,-Ttext=0x0 -nostdlib -o " + baseName + " " + assembly;// 执行命令int result = std::system(command.c_str());// 检查命令执行结果if (result != 0) {std::cerr << "Failed to generate RV object from assembly: " << assembly << std::endl;}
}

5.3 generate_rv_binary

此函数为 riscv64-unknown-elf-objcopy -O binary add-addi add-addi.bin 对应的处理过程:

void generate_rv_binary(const std::string& obj) {// 构建llvm-objcopy命令行字符串std::string command = "riscv64-unknown-elf-objcopy -O binary " + obj + " " + obj + ".bin";// 执行命令int result = std::system(command.c_str());// 检查命令执行结果if (result != 0) {std::cerr << "Failed to generate RV binary from object: " << obj << std::endl;}
}

运行并测试是否正确:

mkdir -p build && cd build && cmake .. && make && ./crvemu ../add-addi.bin
~/crvemu/build$ ./crvemu ../add-addi.bin
--------------------------------------------------------------------------------
x0(zero) = 0000000000000000 000x1(ra) = 0000000000000000 000x2(sp) = 0000000007ffffff 000x3(gp) = 0000000000000000
000x4(tp) = 0000000000000000 000x5(t0) = 0000000000000000 000x6(t1) = 0000000000000000 000x7(t2) = 0000000000000000
000x8(s0) = 0000000000000000 000x9(s1) = 0000000000000000 000xa(a0) = 0000000000000000 000xb(a1) = 0000000000000000
000xc(a2) = 0000000000000000 000xd(a3) = 0000000000000000 000xe(a4) = 0000000000000000 000xf(a5) = 0000000000000000
000x10(a6) = 0000000000000000 000x11(a7) = 0000000000000000 000x12(s2) = 0000000000000000 000x13(s3) = 0000000000000000
000x14(s4) = 0000000000000000 000x15(s5) = 0000000000000000 000x16(s6) = 0000000000000000 000x17(s7) = 0000000000000000
000x18(s8) = 0000000000000000 000x19(s9) = 0000000000000000 000x1a(s10) = 0000000000000000 000x1b(s11) = 0000000000000000
000x1c(t3) = 0000000000000000 000x1d(t4) = 0000000000000005 000x1e(t5) = 0000000000000025 000x1f(t6) = 000000000000002a

6. 总结

综上,这一章节将 dram 拆分出来作为一个单独的类,为了链接 dram 又引入了 bus 。并且将手动编译的过程改成函数,避免了手动执行,后续可以很方便的测试更多的指令。

下一节会将解析指令的过程单独拆分为一个类,然后进一步的解析更多的指令。

👉🏻 文章汇总「从零实现模拟器、操作系统、数据库、编译器…」:https://okaitserrj.feishu.cn/docx/R4tCdkEbsoFGnuxbho4cgW2Yntc

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/680727.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

业务流程

一、需求分析和设计&#xff1a; 在项目启动阶段&#xff0c;需要与业务人员和产品经理充分沟通&#xff0c;了解业务需求&#xff0c;并根据需求进行系统设计和数据库设计。这一阶段的输出通常是需求文档、系统架构设计、数据库设计等。 1.需求文档 需求文档是一份非常重要…

java中事务的使用

文章目录 前言一、同一张表1.业务代码2.测试代码3.测试结果 二、不同表1.业务代码2.测试代码3.测试结果 总结 前言 本文将介绍在springboot中使用Transactional注解来完成对数据库事务的操作&#xff0c;保证数据一致性。 一、同一张表 1.业务代码 Controller Controller p…

【Web】CVE-2021-31805 s2-062漏洞复现学习

目录 Struts2介绍 漏洞概况 OGNL与Struts2 简单原理 漏洞复现 正向rce 反弹shell payload分析 Struts2介绍 Struts 2 是一个流行的用于构建 Java Web 应用程序的开源 Web 应用程序框架。它是 Apache 软件基金会下的一个顶级项目&#xff0c;是 Struts 框架的升级版本。…

使用 devc++ 开发 easyx 实现 Direct2D 交互

代码为 codebus 另一先生的 文案 EasyX 的三种绘图抗锯齿方法 - CodeBus 这里移植到 devc 移植操作如下&#xff1a; 调用dev 的链接库方式&#xff1a; project -> project option -> 如图所示 稍作修改的代码。 #include <graphics.h> #include <d2d1.…

计算机的大脑—中央处理单元(CPU)(重点认识运算器和控制器)

计算机的中央处理单元&#xff08;CPU&#xff09;是计算机的主要部件之一&#xff0c;负责解释和执行大部分计算机指令&#xff0c;是计算机的大脑。CPU执行的操作包括算术运算、数据传输和指令的解释执行。现代CPU的性能取决于多种因素&#xff0c;包括其核心数量、时钟速度、…

临睡之际的生死思索与生命哲学的启示

在人类生存体验中&#xff0c;有一种独特而深邃的感受——当人们准备进入梦乡时&#xff0c;会担忧第二天醒来是否还能感知到生命的律动。这种“入睡即未知”的心理状态&#xff0c;既是生命无常的深刻体现&#xff0c;也是对个体生命价值、生活态度及人生哲学的一种深度拷问。…

BUUCTF-Real-[Jupyter]notebook-rce

1、简介 Jupyter Notebook&#xff08;此前被称为 IPython notebook&#xff09;是一个交互式笔记本&#xff0c;支持运行 40 多种编程语言。 如果管理员未为Jupyter Notebook配置密码&#xff0c;将导致未授权访问漏洞&#xff0c;游客可在其中创建一个console并执行任意Pytho…

Android 10.0 锁屏壁纸 LockscreenWallpaper

前言 一、设置壁纸 通过系统设置进行锁屏壁纸和桌面壁纸的设置。 Setting 部分的代码&#xff1a; packages/apps/WallpaperPicker2/src/com/android/wallpaper/module/DefaultWallpaperPersister.java private int setStreamToWallpaperManagerCompat(InputStream inputStre…

第6个-滚动动画

Day 6 - Scroll Animation 1. 演示效果 2. 分析思路 布局 所有的内容进行水平垂直居中&#xff0c;可以使用**margin:0 auto;&#xff0c;也可以使用flex**布局&#xff1a; body {background-color: #efedd6;display: flex;flex-direction: column;justify-content: center…

树结构 严蔚敏 数据结构代码

一&#xff0c;树顺序和链式存储结构的定义 //树用儿子-兄弟表示法&#xff0c;就成了二叉树//一般二叉树用顺序存储浪费空间&#xff0c;所以大都用链式存储//特殊的二叉树有完美 或 满二叉树、完全树 可以用顺序存储//严#define MAXSIZE 100 //二叉树的最大结点数typedef TE…

前端架构: 本地调试脚手架的2种方式

一、 调试简单的脚手架方式 假定脚手架名称是 xxx 1 &#xff09;方式1 在xxx脚手架项目目录的上一级&#xff0c;执行 npm i -g xxx这时候&#xff0c;就可以本地调试脚手架&#xff0c;在前文中已经说明软链的作用参考&#xff1a;https://blog.csdn.net/Tyro_java/article…

【C语言】实现栈

目录 &#xff08;一&#xff09;栈 &#xff08;二&#xff09;头文件 &#xff08;三&#xff09;功能实现 &#xff08;1&#xff09;初始化栈 &#xff08;2&#xff09; 栈的销毁 &#xff08;3&#xff09;压栈 &#xff08;4&#xff09; 出栈 &#xff08;5&a…

【html学习笔记】1.概念

1.概念 1.1 HTML标准格式 <html><body><p>Hello World</p></body> </html>1.2 编辑方式 新建一个笔记本文件&#xff0c;将html语法格式的内容写入。保存后将记事本的.txt后缀换成.html,就可以在浏览器里运行了 1.3 中文问题 为了避…

前端架构: 从vue-cli探究脚手架原理

从使用角度理解什么是脚手架 脚手架本质是一个操作系统的客户端 在终端中去执行一个命令&#xff0c;这个命令本身它就是一个客户端我们其实可以把脚手架理解为操作系统的一个客户端通过命令去执行它的时候&#xff0c;这个命令往往是这样的一个构造&#xff0c;如下 比如&…

PoW算法,请出示你的证明

口信消息型拜占庭问题的解可以防止 (n - 1) / 3 个坏人 (其中 n 为节点数)作恶&#xff0c;这样一来也是可以通过不断增加节点数来突破 (n - 1) / 3 的限制。为了防止这一行为可以使用区块链技术中的工作量证明&#xff08;Proof of Work&#xff09;算法。 原理 PoW算法&…

devc++跑酷小游戏1.2.5

更新了在关卡中的复位和地图的刷新z或Z&#xff0c;存档还是没写出来&#xff0c;文件操作好难&#xff01;&#xff01;&#xff01;&#xff01;&#xff01;&#xff01;&#xff01;&#xff01;&#xff01;&#xff01;&#xff01;&#xff01;&#xff01;&#xff01;…

【leetcode热题100】解码方法

难度&#xff1a; 中等通过率&#xff1a; 21.5%题目链接&#xff1a;力扣&#xff08;LeetCode&#xff09;官网 - 全球极客挚爱的技术成长平台 题目描述 一条包含字母 A-Z 的消息通过以下方式进行了编码&#xff1a; A -> 1 B -> 2 ... Z -> 26给定一个只包含数字…

continue、break、return的区别

continue、break、return的区别 continuebreakreturn continue 作用&#xff1a;跳过本次循环&#xff0c;继续接下来的循环 for(int i 1;i < 10;i){if(i4){continue;}System.out.print(i " "); } //输出结果为&#xff1a;1 2 3 5 6 7 8 9 10 //4的输出被跳过…

C#中 Combine 静态方法

在C#中&#xff0c;Combine是System.IO.Path类的一个静态方法&#xff0c;用于将多个路径片段组合成一个完整的路径。 Combine方法的详细解释如下&#xff1a; public static string Combine(string path1, string path2);参数&#xff1a; path1&#xff1a;要组合的第一个…

【手写数据库toadb】数据字典的内容结构,它的生成,避免鸡生蛋蛋生鸡的问题,高频访下的性能应对

411 数据字典的作用 ​专栏内容: 手写数据库toadb 本专栏主要介绍如何从零开发,开发的步骤,以及开发过程中的涉及的原理,遇到的问题等,让大家能跟上并且可以一起开发,让每个需要的人成为参与者。 本专栏会定期更新,对应的代码也会定期更新,每个阶段的代码会打上tag,方…