【区块链安全 | 第十七篇】类型之引用类型(一)

文章目录

  • 引用类型
    • 数据存储位置
      • 分配行为
    • 数组
      • 特殊数组:bytes 和 string 类型
      • bytes.concat 和 string.concat 的功能
      • 分配 memory 数组
      • 数组字面量(Array Literals)
      • 二维数组字面量
      • 数组成员(Array Members)
      • 悬空引用(Dangling References)到存储数组元素

在这里插入图片描述

引用类型

引用类型的值可以通过多个不同的名称进行修改。这与值类型形成对比,在值类型中,每当使用一个值类型的变量时,都会获得一个独立的副本。因此,引用类型比值类型需要更谨慎地处理。目前,引用类型包括结构体(structs)、数组(arrays)和映射(mappings)。如果使用引用类型,必须明确提供数据存储的位置:memory(其生命周期仅限于外部函数调用期间)、storage(存储状态变量的位置,其生命周期与合约的生命周期一致)或 calldata(一个特殊的数据存储区域,其中包含函数参数)。

如果赋值或类型转换导致数据存储位置发生变化,则会自动触发复制操作,而在同一数据存储位置内部进行赋值时,仅在某些情况下会触发复制(对于 storage 类型)。

数据存储位置

每个引用类型都有一个额外的注释,即“数据存储位置”,用于指明其存储位置。数据存储位置包括 memory、storage 和 calldata。calldata 是一个不可修改、不可持久化的区域,其中存储了函数参数,其行为大多数情况下类似于 memory。

注意
transient 作为引用类型的数据存储位置目前尚不受支持。 如果可能,尽量使用 calldata 作为数据存储位置,因为这样可以避免复制,同时确保数据不可修改。具有 calldata 存储位置的数组和结构体可以作为函数的返回值,但无法直接分配此类类型。

注意
在函数体中声明或作为返回参数的 calldata 位置的数组和结构体必须在使用或返回之前进行赋值。在某些使用非平凡控制流的情况下,编译器可能无法正确检测初始化。在这些情况下,一个常见的解决方法是先将受影响的变量赋值给自身,然后再进行正确的初始化。

注意
在 0.6.9 版本之前,外部函数的引用类型参数数据存储位置仅限于 calldata,公共函数为 memory,内部和私有函数则可以是 memory 或 storage。而现在,所有可见性(visibility)的函数都允许使用 memory 和 calldata。

注意
构造函数的参数不能使用 calldata 作为数据存储位置。

注意
在 0.5.0 版本之前,数据存储位置可以省略,并且会根据变量类型、函数类型等默认使用不同的位置。但从 0.5.0 版本开始,所有复杂类型都必须显式指定数据存储位置。

分配行为

数据存储位置不仅与数据的持久性相关,还会影响赋值的语义:

  • 在 storage 和 memory(或 calldata)之间的赋值总是会创建一个独立的副本。
  • 在 memory 之间的赋值仅创建引用。这意味着对一个 memory 变量的修改会影响所有引用同一数据的 memory 变量。
  • 从 storage 赋值给本地 storage 变量时,也只是赋值引用。
  • 其他所有对 storage 的赋值都会进行复制。例如,对状态变量的赋值,或对 storage 结构体类型的本地变量的成员赋值,即使本地变量本身只是一个引用。

举个例子:

// SPDX-License-Identifier: GPL-3.0
pragma solidity >=0.5.0 <0.9.0;contract C {// x 的数据存储位置是 storage。// 这是唯一可以省略数据存储位置的地方。uint[] x;// memoryArray 的数据存储位置是 memory。function f(uint[] memory memoryArray) public {x = memoryArray; // 可以执行,会复制整个数组到 storageuint[] storage y = x; // 可以执行,赋值的是指针,y 的数据存储位置是 storagey[7]; // 合法,返回第 8 个元素y.pop(); // 合法,通过 y 修改 xdelete x; // 合法,清空数组,同时影响 y// 以下操作无法执行,因为它需要在 storage 中创建一个新的临时/匿名数组,// 但 storage 是静态分配的:// y = memoryArray;// 同样,“delete y” 也是不合法的,因为对指向 storage 对象的本地变量的赋值// 只能来自已有的 storage 对象。// 它会“重置”指针,但没有合理的位置可以指向。// 更多细节请参考“delete” 运算符的文档。// delete y;g(x); // 调用 g,传递 x 的引用h(x); // 调用 h,创建一个独立的临时副本存储在 memory}function g(uint[] storage) internal pure {}function h(uint[] memory) public pure {}
}

数组

数组可以是编译时固定大小的,也可以是动态大小的。

固定大小为 k,元素类型为 T 的数组写作 T[k],而动态大小的数组写作 T[]

例如,一个包含 5 个 uint 动态数组的数组写作 uint[][5]。该表示法与某些其他语言相反。在 Solidity 中,X[3] 始终是一个包含 3 个 X 类型元素的数组,即使 X 本身是一个数组。而在 C 语言等其他语言中,这种情况可能不同。

索引从 0 开始,访问顺序与声明顺序相反。

例如,如果有一个变量 uint[][5] memory x,要访问第三个动态数组中的第七个 uint,应使用 x[2][6],而访问第三个动态数组则使用 x[2]。同样,如果有一个数组 T[5] a,其中 T 也可以是数组,则 a[2] 的类型始终为 T

数组元素可以是任何类型,包括 mappingstruct。但一般的类型限制仍然适用,例如 mapping 只能存储在 storage 数据位置,并且 public 可见性的函数参数必须是 ABI 类型。

可以将状态变量数组标记为 public,Solidity 会自动为其创建一个 getter。数值索引会成为 getter 的必填参数。

访问超出数组末尾的索引会导致断言失败(Assertion Failure)。

动态大小数组的 push()push(value) 方法可用于在数组末尾追加新元素:

  • .push() 追加一个零初始化的元素,并返回对该元素的引用。
  • .push(value) 追加指定值的元素。

注意
动态数组只能在 storage 中调整大小。在 memory 中,此类数组可以是任意大小,但一旦分配后,其大小就无法更改。

特殊数组:bytes 和 string 类型

bytesstring 类型是特殊的数组。bytes 类型类似于 bytes1[],但在 calldatamemory 中会进行紧密打包(packed)。string 等同于 bytes,但不允许使用长度或索引访问。

Solidity 不提供字符串操作函数,但可以使用第三方字符串库。也可以通过 keccak256(abi.encodePacked(s1)) == keccak256(abi.encodePacked(s2)) 来比较两个字符串的哈希值,或者使用 string.concat(s1, s2) 来连接两个字符串。

应优先使用 bytes 而非 bytes1[],因为 bytes 的开销更小。使用 bytes1[]memory 中存储时,每个元素之间会填充 31 个字节的填充数据(padding),而 storage 中由于紧密打包不存在填充。通常,bytes 适用于任意长度的原始字节数据,而 string 适用于任意长度的字符串(UTF-8 编码)。如果可以限制长度,应使用 bytes1bytes32 这样的值类型,因为它们的成本更低。

注意
如果想要访问字符串 s 的字节表示,可使用 bytes(s).length 获取长度,或 bytes(s)[7] = 'x'; 进行修改。但这样访问的是 UTF-8 编码的底层字节,而不是独立的字符。

bytes.concat 和 string.concat 的功能

string.concat 可用于连接任意数量的 string 值。该函数返回一个 memory 位置的 string,其中包含所有参数的内容,不包含填充(padding)。如果参数的类型不能隐式转换为 string,需要先进行转换。

类似地,bytes.concat 可用于连接任意数量的 bytesbytes1bytes32 类型的值。该函数返回一个 memory 位置的 bytes,其中包含所有参数的内容,不包含填充。如果参数是 string 或其他不能隐式转换为 bytes 的类型,需要先转换为 bytesbytes1bytes32

示例如下:

// SPDX-License-Identifier: GPL-3.0
pragma solidity ^0.8.12;contract C {string s = "Storage";function f(bytes calldata bc, string memory sm, bytes16 b) public view {// 连接多个字符串string memory concatString = string.concat(s, string(bc), "Literal", sm);assert((bytes(s).length + bc.length + 7 + bytes(sm).length) == bytes(concatString).length);// 连接多个字节数组bytes memory concatBytes = bytes.concat(bytes(s), bc, bc[:2], "Literal", bytes(sm), b);assert((bytes(s).length + bc.length + 2 + 7 + bytes(sm).length + b.length) == concatBytes.length);}
}

如果调用 string.concat()bytes.concat() 时不传递任何参数,则返回一个空数组。

分配 memory 数组

可以使用 new 关键字创建动态长度的 memory 数组。与 storage 数组不同,memory 数组无法调整大小(例如,push() 方法不可用)。因此,必须在创建时确定所需大小,或创建一个新数组并复制所有元素。

与 Solidity 中的所有变量一样,新分配的数组元素总是初始化为默认值。

// SPDX-License-Identifier: GPL-3.0
pragma solidity >=0.4.16 <0.9.0;contract C {function f(uint len) public pure {uint ; // 创建一个长度为 7 的 uint 数组bytes memory b = new bytes(len); // 创建一个长度为 len 的 bytes 数组assert(a.length == 7);assert(b.length == len);a[6] = 8; // 赋值}
}

数组字面量(Array Literals)

数组字面量是用 [...] 包裹的一个或多个逗号分隔的表达式。例如:[1, a, f(3)]

数组字面量的类型遵循以下规则:

  • 它总是一个 静态大小的 memory 数组,其长度等于表达式的个数。
  • 其基本类型是列表中 第一个表达式 的类型,并且其他所有表达式都必须能隐式转换为该类型。如果无法转换,则会报错。
  • 仅仅存在一个可以转换的共同类型是不够的,必须有一个元素的原始类型就是该类型。

示例:

// SPDX-License-Identifier: GPL-3.0
pragma solidity >=0.4.16 <0.9.0;contract C {function f() public pure {g([uint(1), 2, 3]); // 显式转换第一个元素的类型}function g(uint[3] memory) public pure {// ...}
}

在上例中,[1, 2, 3] 的类型是 uint8[3] memory,因为 123 默认都是 uint8 类型。如果想让其成为 uint[3] memory,需要将第一个元素转换为 uint

无效示例

[1, -1] // 无效,1 是 uint8,-1 是 int8,无法隐式转换

有效示例

[int8(1), -1] // 有效,所有元素都是 int8

二维数组字面量

不同类型的定长 memory 数组之间无法相互转换,即使它们的基本类型可以转换。因此,在使用二维数组字面量时,必须显式指定共同的基本类型。

// SPDX-License-Identifier: GPL-3.0
pragma solidity >=0.4.16 <0.9.0;// 声明合约 C
contract C {/*** @dev 返回一个 4 行 2 列的 `uint24` 类型的二维静态数组* @return x 返回的数组 `uint24[2][4] memory`*/function f() public pure returns (uint24[2][4] memory) {// 定义一个 `uint24[2][4]` 类型的二维静态数组,并初始化uint24[2][4] memory x = [[uint24(0x1), 1],      // 第一行: 0x1(16 进制)转换为 uint24,第二个元素为 1[0xffffff, 2],         // 第二行: 0xffffff(最大 uint24 值),第二个元素为 2[uint24(0xff), 3],     // 第三行: 0xff(255),第二个元素为 3[uint24(0xffff), 4]    // 第四行: 0xffff(65535),第二个元素为 4];// 返回二维数组return x;}
}

如果没有显式指定 uint24,如下代码会 报错

uint[2][4] memory x = [[0x1, 1], [0xffffff, 2], [0xff, 3], [0xffff, 4]];

固定大小的 memory 数组不能赋值给动态大小的 memory 数组,示例如下:

// SPDX-License-Identifier: GPL-3.0
pragma solidity >=0.4.0 <0.9.0;// 这段代码无法编译
contract C {function f() public {uint[] memory x = [uint(1), 3, 4]; // 无法将 `uint[3] memory` 赋值给 `uint[] memory`}
}

未来可能会移除此限制,但由于 ABI 传递数组的方式,此限制目前仍然存在。

如果要初始化动态大小的数组,则必须分配各个元素:

// 使用 new 关键字创建动态数组并手动赋值
// SPDX-License-Identifier: GPL-3.0
pragma solidity >=0.4.16 <0.9.0;contract C {function f() public pure {uint[] memory x = new uint[](3);x[0] = 1;x[1] = 3;x[2] = 4;}
}

数组成员(Array Members)

length:
数组有一个 length 成员,它包含数组的元素数量。
内存(memory)数组的长度在创建后是固定的(但在运行时可以动态确定)。

push():

  • 动态存储(storage)数组bytes(但不包括 string)有一个 push() 成员函数,
    你可以使用它在数组末尾追加一个 零初始化 的元素。
  • 它返回对新元素的引用,因此可以像 x.push().t = 2x.push() = b 这样使用。

push(x):

  • 动态存储(storage)数组bytes(但不包括 string)有一个 push(x) 成员函数,
    你可以使用它在数组末尾追加指定的元素。
  • 该函数不返回任何值。

pop():

  • 动态存储(storage)数组bytes(但不包括 string)有一个 pop() 成员函数,
    你可以使用它从数组末尾移除一个元素。
  • 这也会 隐式调用 delete 来清除被移除的元素。
  • 该函数不返回任何值。

注意
通过调用 push() 增加存储数组的长度具有 恒定的 gas 费用,因为存储在 Solidity 中默认会被初始化为零。

但通过 pop() 减少存储数组的长度的费用 取决于被移除元素的大小。如果被移除的元素是 数组,则会非常昂贵,因为它的删除行为类似于调用 delete 来显式清除所有被移除的元素。

注意
要在external(而不是(public))函数中使用数组的数组(arrays of arrays),你需要启用 ABI 编码器 v2(ABI coder v2)。

在Byzantium 之前的 EVM 版本,无法访问从函数调用返回的动态数组。如果你的函数返回动态数组,请确保你的 EVM 版本设置为Byzantium 或更高版本。

悬空引用(Dangling References)到存储数组元素

在操作存储数组时,需要小心避免悬空引用。悬空引用是指指向某个已经不存在或者已被移动但没有更新引用的元素的引用。

例如,悬空引用可能发生在您将一个数组元素的引用存储到一个局部变量中,然后对包含该元素的数组进行 .pop() 操作时:

// SPDX-License-Identifier: GPL-3.0
pragma solidity >=0.8.0 <0.9.0;contract C {uint[][] s;function f() public {// 存储对 s 最后一个数组元素的指针。uint[] storage ptr = s[s.length - 1];// 删除 s 的最后一个数组元素。s.pop();// 尝试对不再数组中的元素进行写操作。ptr.push(0x42);// 之后对 s 进行 push 操作时,不会添加一个空数组,// 而是会导致 s 的最后一个元素长度为 1,且其第一个元素是 0x42。s.push();assert(s[s.length - 1][0] == 0x42);}
}

在上述代码中,ptr.push(0x42) 不会回滚,尽管 ptr 已不再指向一个有效的 s 数组元素。因为编译器假设未使用的存储区域始终被零化,因此后续的 s.push() 操作不会显式地将零写入存储空间,导致 s 的最后一个元素在该 push() 后的长度为 1,并且第一个元素是 0x42。

需要注意的是,Solidity 不允许声明对值类型(如 uintbool 等)的存储引用。这类显式的悬空引用仅限于嵌套引用类型。但是,在使用复杂表达式进行元组赋值时,暂时也可能会产生悬空引用:

// SPDX-License-Identifier: GPL-3.0
pragma solidity >=0.8.0 <0.9.0;contract C {uint[] s;uint[] t;constructor() {// 向存储数组中推送一些初始值。s.push(0x07);t.push(0x03);}function g() internal returns (uint[] storage) {s.pop();return t;}function f() public returns (uint[] memory) {// 以下代码首先会评估 `s.push()` 作为对新元素索引 1 的引用,// 然后调用 `g` 函数将此新元素弹出,导致左侧的元组元素成为悬空引用。// 尽管如此,赋值仍然会进行,并且会写入 `s` 数据区外。(s.push(), g()[0]) = (0x42, 0x17);// 随后对 `s` 进行 push 操作时,会暴露上一条语句写入的值,// 即函数结束时 `s` 的最后一个元素的值为 0x42。s.push();return s;}
}

在编写代码时,为了安全起见,最好每次赋值时只操作一次存储,并避免在赋值语句的左侧使用复杂表达式。

当操作字节数组(bytes array)元素的引用时,您需要特别小心,因为对字节数组执行 .push() 操作时,可能会导致存储布局从短格式转换为长格式。

// SPDX-License-Identifier: GPL-3.0
pragma solidity >=0.8.0 <0.9.0;// 这段代码会报告警告
contract C {bytes x = "012345678901234567890123456789";function test() external returns(uint) {(x.push(), x.push()) = (0x01, 0x02);return x.length;}
}

在这里,当第一次执行 x.push() 时,x 仍然以短格式存储,因此 x.push() 返回的是 x 第一个存储槽中的元素的引用。然而,第二次执行 x.push() 时,字节数组的存储格式会变成长格式。此时,x.push() 所引用的元素已经被移动到数组的数据区域,而引用仍指向原始位置(即长度字段)。因此,赋值操作会有效地破坏 x 的长度字段。

为了安全起见,建议在单次赋值语句中,只增加字节数组最多一个元素,并且不要同时在同一语句中进行数组的索引访问。

虽然上述描述的是当前版本编译器中的悬空存储引用的行为,但任何包含悬空引用的代码都应被视为具有未定义行为。因此,需确保在编写代码时避免悬空引用。

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

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

相关文章

selenium和pytessarct提取古诗文网的验证码(python爬虫)

代码实现的主要功能&#xff1a; 浏览器自动化控制 验证码图像获取与处理 OCR验证码识别 表单自动填写与提交 登录状态验证 异常处理与资源清理 1. 浏览器初始化与页面加载 driver webdriver.Chrome() driver.get("https://www.gushiwen.cn/user/login.aspx?fro…

【输入某年某日,判断这是这一年的第几天】

for语句和switch语句分别实现 文章目录 前言 一、用switch做 二、用for循环做 ​编辑 总结 前言 用两种不同的方法求解【输入某年某日&#xff0c;判断这是这一年的第几天】 一、用switch做 代码如下&#xff08;示例&#xff09;&#xff1a; int main() {int y, m, d, cou…

香港理工视觉语言模型赋能智能制造最新综述!基于视觉语言模型的人机协作在智能制造中的应用

作者&#xff1a;Junming FAN 1 ^{1} 1, Yue YIN 1 ^{1} 1, Tian WANG 1 ^{1} 1, Wenhang DONG 1 ^{1} 1, Pai ZHENG 1 ^{1} 1, Lihui WANG 2 ^{2} 2单位&#xff1a; 1 ^{1} 1香港理工大学工业及系统工程系&#xff0c; 2 ^{2} 2瑞典皇家理工学院论文标题&#xff1a; Vision-…

大智慧前端面试题及参考答案

如何实现水平垂直居中? 在前端开发中,实现元素的水平垂直居中是一个常见的需求,以下是几种常见的实现方式: 使用绝对定位和负边距:将元素的position设置为absolute,然后通过top、left属性将其定位到父元素的中心位置,再使用负的margin值来调整元素自身的偏移,使其水平垂…

算法基础_基础算法【高精度 + 前缀和 + 差分 + 双指针】

算法基础_基础算法【高精度 前缀和 差分 双指针】 ---------------高精度---------------791.高精度加法题目介绍方法一&#xff1a;代码片段解释片段一&#xff1a; 解题思路分析 792. 高精度减法题目介绍方法一&#xff1a;代码片段解释片段一&#xff1a; 解题思路分析 7…

OkHttpHttpClient

学习链接 okhttp github okhttp官方使用文档 SpringBoot 整合okHttp okhttp3用法 Java中常用的HTTP客户端库&#xff1a;OkHttp和HttpClient&#xff08;包含请求示例代码&#xff09; 深入浅出 OkHttp 源码解析及应用实践 httpcomponents-client github apache httpclie…

DoDAF科普

摘要 DoDAF&#xff08;Department of Defense Architecture Framework&#xff0c;美国国防部架构框架&#xff09;是一种专门为复杂系统设计的标准化框架&#xff0c;广泛应用于军事和国防项目。它通过提供一致的架构描述方法&#xff0c;确保跨组织、跨国界的系统集成和互操…

搭建qemu环境

1.安装qemu apt install qemu-system2.编译内核 设置gcc软链接sudo ln -s arm-linux-gnueabihf-gcc arm-linux-gccsudo ln -s arm-linux-gnueabihf-ld arm-linux-ldsudo ln -s arm-linux-gnueabihf-nm arm-linux-nmsudo ln -s arm-linux-gnueabihf-objcopy arm-linux-objc…

使用Claude Desktop和MCP工具创建个人编程助手

最近我在Claude Desktop上试用了MCP工具,体验过程令人兴奋不已。 我花时间测试了多个用于编程场景的MCP服务器——而Claude本就擅长编程,这一组合可谓相得益彰。 这些工具赋予Claude强大的自主任务执行能力,比如仅通过聊天就能实现Vibe编程。当然,必须谨慎控制其访问权限…

K8S集群搭建 龙蜥8.9 Dashboard部署(2025年四月最新)

一、版本兼容性和服务器规划 组件版本/配置信息备注操作系统Anolis OS 8.9基于 Linux 5.10.134-17.3.an8.x86_64内核版本Linux 5.10.134-17.3.an8.x86_64与 Kubernetes 1.29 兼容架构x86-64Kubernetes 版本v1.29.5最新稳定版&#xff0c;兼容 Linux 5.10 内核Docker 版本24.0.…

项目6——前后端互通的点餐项目

一、项目介绍 1、有哪些需求需要连接后台完成功能? 前台传给后台 后台返回给前台 注册: 用户名 密码 操作是否成功 登录: 用户名 密码 操作是否成功 下单: 用户名 菜名 操作是否成功 Request : 前端发送给后台的所有数据的载体 Res…

Go和Golang语言简介

李升伟 整理 Go 和 Golang 实际上指的是同一种编程语言&#xff0c;只是名称不同。 Go 名称&#xff1a;Go 是该编程语言的正式名称。 起源&#xff1a;由 Google 的 Robert Griesemer、Rob Pike 和 Ken Thompson 于 2007 年开始设计&#xff0c;2009 年正式发布。 设计目…

GitHub二次验证登录2FA(Enable two-factor authentication )

不用下载app&#xff0c;点击二维码下面的setup key获取到secret并且保存好 接下来几行代码就可以解析了。 添加依赖 <dependency><groupId>com.amdelamar</groupId><artifactId>jotp</artifactId><version>1.3.0</version> </d…

RabbitMQ技术方案分析

方案分析 在上一篇文档中&#xff0c;详细讲述了如何通过CanalMQ实现对分库分表的数据库和数据表进行数据同步&#xff0c;而在这个方案中&#xff0c;还有一个关键点是需要注意的&#xff1a;首先&#xff0c;数据增删改的信息是保证写入binlog的&#xff0c;Canal解析出增删…

node.js版本管理

概述 遇到了版本升级后&#xff0c;以前项目不兼容的问题。 下载一个node.js的版本管理工具&#xff0c;官网下载地址&#xff0c;可以选择版本下载&#xff0c;我选择的1.11.1版本的。下载完成后点击安装&#xff0c;分别选择nvm安装目录和nodejs的安装目录&#xff0c;点击安…

leetcode-热题100(3)

leetcode-74-搜索二维矩阵 矩阵最后一列升序排序&#xff0c;在最后一列中查找第一个大于等于target的元素 然后在该元素所在行进行二分查找 bool searchMatrix(int** matrix, int matrixSize, int* matrixColSize, int target) {int n matrixSize;int m matrixColSize[0];in…

在HarmonyOS NEXT 开发中,如何指定一个号码,拉起系统拨号页面

大家好&#xff0c;我是 V 哥。 《鸿蒙 HarmonyOS 开发之路 卷1 ArkTS篇》已经出版上市了哈&#xff0c;有需要的朋友可以关注一下&#xff0c;卷2应用开发篇也马上要出版了&#xff0c;V 哥正在紧锣密鼓的写鸿蒙开发实战卷3的教材&#xff0c;卷3主要以项目实战为主&#xff0…

音视频入门基础:MPEG2-TS专题(26)——通过FFmpeg命令使用RTP发送TS流

音视频入门基础&#xff1a;MPEG2-TS专题系列文章&#xff1a; 音视频入门基础&#xff1a;MPEG2-TS专题&#xff08;1&#xff09;——MPEG2-TS官方文档下载 音视频入门基础&#xff1a;MPEG2-TS专题&#xff08;2&#xff09;——使用FFmpeg命令生成ts文件 音视频入门基础…

(六)窗口表面

这节主要三部分 一&#xff0c;窗口表面的创建和销毁 二&#xff0c;呈现队列 三&#xff0c;与以前实现的图形能力队列的兼容 一&#xff0c;窗口表面 1&#xff0c;创建 VkSurfaceKHR surface; //创建窗口表面 glfwCreateWindowSurface(instance, //vkInstance对象window, …

【零基础入门unity游戏开发——2D篇】SpriteEditor图片编辑器

考虑到每个人基础可能不一样,且并不是所有人都有同时做2D、3D开发的需求,所以我把 【零基础入门unity游戏开发】 分为成了C#篇、unity通用篇、unity3D篇、unity2D篇。 【C#篇】:主要讲解C#的基础语法,包括变量、数据类型、运算符、流程控制、面向对象等,适合没有编程基础的…