
2月19日学习数组今天整理了一下数组相关的知识发现这玩意儿真的是编程里最基础又最能藏坑的东西。你说它简单声明、赋值、遍历三件套下来小学生都会。但真要往深了抠内存布局、类型推导、边界处理、跨语言差异每一层都能翻出不少东西。这两天搜资料的时候看到一堆关于数组的提问从“C语言二维数组怎么初始化”到“西门子1500PLC怎么获取数组索引”从“TypeScript数组有哪些方法”到“连续子数组乘积最大值怎么算”跨度大得离谱。但这也说明一个问题——数组这个东西不管你是搞嵌入式、写Web、做算法还是玩工业自动化都绕不开。这篇算是自己这段时间学习数组的一个系统性整理把常见操作、底层原理、不同语言的差异、还有实际开发中容易踩的坑都过一遍。新手可以当一份入门手册老手也能当个查漏补缺的清单。1. 数组的第一性原理内存布局与类型系统1.1 数组到底是什么数组本质上就是一段连续的内存空间里面按顺序存放着相同类型的元素。这句话看着简单但它是理解后面所有问题的基石。连续内存意味着什么意味着你可以通过“起始地址 索引 × 元素大小”直接算出任意元素的地址这就是O(1)随机访问的理论基础。相同类型意味着什么意味着每个元素占用的字节数一样编译器才能准确计算偏移量。举个例子一个int arr[5]在32位系统上每个int占4字节整个数组占用20字节内存布局长这样// 假设数组起始地址是 0x1000 // 0x1000 - 0x1003 arr[0] // 0x1004 - 0x1007 arr[1] // 0x1008 - 0x100B arr[2] // 0x100C - 0x100F arr[3] // 0x1010 - 0x1013 arr[4]网上很多人问“C语言数组变量的类型转换”其实就是没想明白这一点。数组名在这个语境下会退化为指向首元素的指针arr和arr[0]在数值上是一样的但类型不同——前者是int*后者是指向整个数组的指针int(*)[5]。这两个搞混了后面做指针运算的时候就会算出莫名其妙的结果。注意数组名不是指针数组名是数组名它是一个不可修改的左值只是在表达式中会隐式转换为指向首元素的指针。这两者的区别在sizeof运算中体现得最明显——sizeof(arr)返回的是整个数组占用的字节数而sizeof(ptr)返回的是指针本身的大小。1.2 各语言初始化方式的差异数组初始化是每个语言入门第一课但不同语言的初始化语法差异非常大背后的设计理念也完全不同。C语言传统数组// 完全初始化 int a[5] {1, 2, 3, 4, 5}; // 部分初始化剩余补0 int b[5] {1, 2, 3}; // b[3] 0, b[4] 0 // 省略长度由编译器推导 int c[] {1, 2, 3, 4, 5}; // 长度为5 // 指定初始化器C99 int d[10] {[0] 1, [5] 2, [9] 3};这里有个很多C语言新手容易忽略的点int b[5] {1, 2, 3};这种局部初始化剩余元素会被自动补零这其实是C标准规定的静态初始化语义。但如果你写的是int b[5];然后不初始化局部变量就成了未定义行为里面可能是任何垃圾值。我见过太多人在这个上面翻车声明了一个数组不初始化就直接拿来用结果跑出来一个随机数还以为是自己逻辑写错了。C的std::array和std::vector#include array #include vector // 定长数组替代C风格数组 std::arrayint, 5 arr {1, 2, 3, 4, 5}; // 动态数组长度可以运行时决定 std::vectorint vec {1, 2, 3}; vec.push_back(4);C里我基本不推荐原生数组除非是写底层或者有明确的性能要求。std::array在栈上分配性能和原生数组完全一致但自带边界检查at()方法和迭代器支持。std::vector是动态扩容的存堆上用起来最省心。Java数组// 声明并初始化 int[] arr new int[]{1, 2, 3, 4, 5}; // 动态创建默认值 int[] arr2 new int[5]; // 元素默认是0 // 数组赋值给另一个数组 int[] arr3 arr2.clone(); // 深拷贝新数组 int[] arr4 Arrays.copyOf(arr2, arr2.length); // 也是深拷贝看到那个热搜词“java数组赋值给另一个数组的方法”没这里必须说清楚直接int[] arr3 arr2;这不是赋值数组这是让arr3和arr2指向同一个数组对象。你改arr3[0]arr2[0]也跟着变。真要想复制内容要用clone()、Arrays.copyOf()或者System.arraycopy()。这个坑在Java面试里出现的频率极高。JavaScript的数组是动态的// 直接声明 let arr [1, 2, 3, 4, 5]; // 创建指定长度的空数组 let arr2 new Array(5); // 注意这里是长度5不是[5] // 添加元素 arr.push(6); // 末尾添加 arr.unshift(0); // 开头添加 arr.splice(2, 0, 99); // 在索引2处插入99JS数组本质上不是传统意义上的数组它是对象键是字符串形式的索引所以它可以不连续、可以混合类型、可以动态扩缩。这也解释了为什么JS数组的“性能”比不上真正的数组——因为它压根不是数组。这里顺带提一下热搜里有个“C 用unique_ptr智能指针生成动态char数组能用char*类型吗”答案是可以的但需要注意几点。std::unique_ptrchar[]的get()方法返回的是char*所以可以直接赋值给char*指针。但如果你对这个指针做了delete操作那就违背了智能指针的管理原则会导致双重释放或未定义行为。更合理的做法是std::unique_ptrchar[] buffer(new char[1024]); char* raw_ptr buffer.get(); // 没问题可以这样用 strcpy(raw_ptr, hello); // 不要 delete raw_ptr; // 这块内存归unique_ptr管1.3 指针数组、字符数组和二维数组热搜词里出现了很多“指针数组”“字符数组”“二维数组”这几个概念初学者最容易搞混但其实它们的本质区别非常清晰。指针数组是数组每个元素是一个指针。const char* fruits[] {apple, banana, cherry}; // 这是一个长度为3的数组每个元素类型是 const char*数组指针是指针指向整个数组。int arr[3][4]; // 二维数组 int (*p)[4] arr; // p是指向“长度为4的int数组”的指针二维字符数组本质是“字符串数组”。char names[3][20] { Alice, Bob, Charlie }; // 3行20列可以存3个最长19个字符的字符串热搜里还有个“C语言二维数组”以及“c语言 二维数组 指针”其实二维数组在内存里也是连续的是“数组的数组”。arr[2][3]这个二维数组在内存里就是连续的6个int元素。arr[i]的类型是int[3]arr的类型是int[2][3]。所以在函数传参的时候void func(int arr[][3])这样写是对的而void func(int arr[][])是错的因为编译器无法计算第二维的偏移量。2. 数组常用操作跨语言对比与底层逻辑2.1 获取数组索引热搜词里有一条“西门子1500tplc怎么获取数组的索引”这个放在工业自动化场景下其实很典型。PLC里数组用的也不少比如模拟量采集的缓存区、配方数据等。西门子1500系列支持SCL结构化控制语言获取数组索引的方式其实和通用编程差不多就是遍历查找// SCL示例在数组中找到值50的索引 FOR i : 0 TO 99 DO IF data[i] 50 THEN index : i; EXIT; // 找到就退出循环 END_IF; END_FOR;这里比较关键的一点是PLC里数组的下标默认是从0开始但有些老设备或者特定HMI组态软件里可能会从1开始这个一定要看清楚手册。还有就是PLC里数组越界访问会导致CPU进入STOP状态部分型号所以做边界检查比在普通PC上更重要。放在通用编程里不同语言的索引获取方式差异很大。Python有list.index()JavaScript有indexOf()Java有Arrays.binarySearch()要求有序C/C就得自己写循环。算法复杂度也要注意无序数组查找是O(n)有序数组用二分查找是O(log n)哈希表是O(1)平均——用哪种取决于你的场景不要无脑循环。2.2 数组转字符串“数组转字符串”在每种语言里都有对应的原生方法但细节差异不少。JavaScriptlet arr [1, 2, 3, 4, 5]; let str1 arr.toString(); // 1,2,3,4,5 let str2 arr.join(-); // 1-2-3-4-5 let str3 JSON.stringify(arr); // [1,2,3,4,5]join()是最灵活的可以指定分隔符。但要注意如果数组元素是对象或者嵌套数组toString()和join()都会调用元素的toString()方法结果可能和你预期的不一样。Javaint[] arr {1, 2, 3}; String str Arrays.toString(arr); // [1, 2, 3]用Arrays.toString()而不是直接arr.toString()——后者返回的是“类名哈希码”很多新手在这里懵了半天。Cstd::vectorint vec {1, 2, 3}; std::ostringstream oss; for (size_t i 0; i vec.size(); i) { if (i 0) oss ,; oss vec[i]; } std::string str oss.str(); // 1,2,3C标准库没有直接提供数组转字符串的接口要么自己写循环要么借助第三方库如fmtlib。不过C20以后有了std::format写起来会好一些。2.3 二维数组的各种操作二维数组本质上就是“数组的数组”它在实际开发中常用于矩阵运算、图像处理、表格数据存储等场景。我整理一下几个常见操作C语言遍历二维数组int matrix[3][4] { {1, 2, 3, 4}, {5, 6, 7, 8}, {9, 10, 11, 12} }; // 行优先遍历 for (int i 0; i 3; i) { for (int j 0; j 4; j) { printf(%d , matrix[i][j]); } }C语言二维数组在内存中是“行优先存储”的也就是说第0行的4个元素连续存放接下来是第1行的4个元素。所以在遍历时把最内层循环放在最后一维上即连续访问CPU缓存命中率会高很多性能差距在数据量大时非常明显。MATLAB取出多列% 取出矩阵的第2列到第3列 sub_matrix matrix(:, 2:3);MATLAB是列优先存储而且索引从1开始很多从C系语言转过来的人经常在这个地方出问题。matrix(:, 2:3)的含义是“所有行、第2到第3列”出来的结果是一个N×2的矩阵。Qt调试时查看整个二维数组热搜里有“qt5 debug 怎样设置可查看整个2维数组”这个我也用Qt踩过。在Qt Creator调试时直接点开一个int[3][4]类型的变量默认只显示第一层的指针信息看不到完整矩阵。解决办法是右键变量选择“Change Value Display Format”然后自定义格式为(*(int(*)[4])variable_name)这样就能以二维数组的形式展开查看。或者更简单的方法在“Locals and Expressions”窗口里手动添加表达式写成(int(*)[4])variable_name然后展开箭头也能看到完整数据。实测下来这个方法最省事。LabVIEW字节数组转二进制数组LabVIEW里有一个专门的转换方式字节数组U8数组转成布尔数组或数值数组用的是“Boolean Array To Number”或者通过“Type Cast”实现。这个场景通常出现在串口通信解析上收到的原始字节要按位拆开处理。2.4 动态数组与大小容量动态数组Dynamic Array是个比较宽泛的概念。在C里是std::vector在Java里是ArrayList在JavaScript和Python里数组本来就是动态的。不同语言处理“扩容”的策略不一样理解这个对写出高性能代码很有帮助。std::vector的扩容策略一般是“倍增”也就是当容量不够时分配一块新内存大小是原来的2倍然后把旧数据搬过去再释放旧内存。这个“搬迁”是O(n)的开销所以频繁push_back会导致多次扩容拷贝影响性能。解决方法是提前reserve()std::vectorint vec; vec.reserve(10000); // 提前预留容量 for (int i 0; i 10000; i) { vec.push_back(i); // 不会触发多次扩容 }热搜词里有个“C#中如何判断一维数组是否为空”这个也值得提一下因为“空数组”和“null数组”是两个完全不同的概念int[] arr1 new int[0]; // 空数组长度为0不是null int[] arr2 null; // null引用 // 判断是否为空 if (arr1 null || arr1.Length 0) { // 这样才是“空”或“null”的统一判断 }C#里new int[0]会返回一个长度为0的非null数组直接用arr.Length 0判断没问题但如果变量可能是null就得先判null再判Length否则会抛NullReferenceException。3. 数组算法与经典场景实战3.1 数组去重基础版到进阶版数组去重是面试中出现频率最高的数组算法题之一。它的解法非常多从最简单的双层循环到用哈希表再到有序数组的双指针复杂度从O(n²)一路降到O(n)。JavaScript一行流const arr [1, 2, 2, 3, 3, 4, 5, 5]; const unique [...new Set(arr)]; // [1, 2, 3, 4, 5]这个利用了Set的特性实现最简单。但要注意Set去重是严格相等SameValueZero所以1和1不会被合并{}和{}是两个不同的引用对象数组去重不能这么搞。对象数组去重热搜里有“对象数组去重”这个稍微麻烦一点。Set对引用类型默认按引用比较所以用Set去重对象数组是无效的除非你保证引用相同。通常的做法是const arr [ { id: 1, name: Alice }, { id: 2, name: Bob }, { id: 1, name: Alice } ]; // 按id去重 const unique Array.from( new Map(arr.map(item [item.id, item])).values() ); // 结果: [{ id: 1, name: Alice }, { id: 2, name: Bob }]这里用Map以id作为键后面的覆盖前面的从而实现去重。原理就是哈希表去重时间复杂度O(n)。有序数组去重双指针// C 版本原地去重 int removeDuplicates(std::vectorint nums) { if (nums.empty()) return 0; int slow 0; for (int fast 1; fast nums.size(); fast) { if (nums[fast] ! nums[slow]) { slow; nums[slow] nums[fast]; } } return slow 1; }双指针在有序数组去重中非常优雅fast负责探路slow负责记录“最后一个不重复元素”的位置一趟遍历O(n)搞定且不需要额外空间。如果不是有序数组就得先排序再双指针整体O(n log n)或者直接用哈希表O(n)空间换时间。3.2 树状数组Fenwick Tree与二分查找的结合树状数组是一个比较进阶的数据结构主要用来解决“单点修改 前缀和查询”的问题两个操作都是O(log n)。热搜里有“树状数组上二分”这个是在树状数组上做二分查找的技巧。树状数组本身支持“前缀和”查询但不支持“第k小”这种顺序统计量查询。不过由于树状数组每个节点的值表示的是一个区间的和所以可以通过“二进制拆位累加”的方式实现O(log n)的“求第k小”操作原理类似二分但利用了树状数组内部节点的层级关系。// 树状数组经典结构 int tree[MAXN]; // 单点修改 void update(int i, int delta) { while (i n) { tree[i] delta; i i (-i); } } // 前缀和 int query(int i) { int sum 0; while (i 0) { sum tree[i]; i - i (-i); } return sum; } // 在树状数组上找“前缀和 target”的第一个位置 int find_kth(int k) { int pos 0; for (int i LOG_N; i 0; i--) { int next_pos pos (1 i); if (next_pos n tree[next_pos] k) { pos next_pos; k - tree[next_pos]; } } return pos 1; }这个find_kth本质上就是“倍增法”从最高位开始尝试跳跃如果跳过去之后前缀和仍小于k就跳过去并减去这段区间的和。跑出来的效果就是O(log n)找到第k小的元素。3.3 连续子数组乘积最大值这个题目是“连续子数组乘积最大值”和常见的“最大子数组和”有异曲同工之处但难点在于乘法有个“负负得正”的问题。维护一个最大值是不够的因为你不知道下一个负数是会让“最小乘积”变成“最大乘积”还是让“最大乘积”变成负数。正确的做法是同时维护两个变量max_prod和min_prod分别表示以当前位置结尾的子数组乘积的最大值和最小值。int maxProduct(vectorint nums) { if (nums.empty()) return 0; int max_prod nums[0], min_prod nums[0], result nums[0]; for (int i 1; i nums.size(); i) { if (nums[i] 0) { swap(max_prod, min_prod); } max_prod max(nums[i], max_prod * nums[i]); min_prod min(nums[i], min_prod * nums[i]); result max(result, max_prod); } return result; }核心思想就是遇到负数时把当前最大和最小互换因为“最大的乘积 × 负数”会变成最小的而“最小的乘积 × 负数”反而可能变成最大的。这题在很多大厂面试中出现过属于经典的“多状态动态规划”问题。3.4 一个简单的成绩统计案例热搜里有个“输入10个学生的成绩求出平均分用数组”这个例子特别适合展示数组的基础应用。我用C语言写一个#include stdio.h int main() { float scores[10]; float sum 0, average; printf(请输入10个学生的成绩\n); for (int i 0; i 10; i) { printf(第%d个学生, i 1); scanf(%f, scores[i]); sum scores[i]; // 边输入边累加省一趟循环 } average sum / 10; printf(平均分%.2f\n, average); // 低于平均分的学生人数 int below 0; for (int i 0; i 10; i) { if (scores[i] average) below; } printf(低于平均分的人数%d\n, below); return 0; }这里有个小技巧输入的时候就顺便累加就可以少遍历一次数组。数组的意义在于如果你只需求平均分不存数组直接累加变量就行但如果后面还要统计“多少人低于平均分”那就必须把成绩存起来了。到底要不要用数组、用多大的数组取决于你是否需要二次访问这批数据。4. 多语言与多平台实战问题整理4.1 TypeScript与JavaScript的数组方法热搜里有一堆“typescript数组的方法”“ts 数组添加数据”“es6提取数组对象一部分”这里一起梳理掉。TypeScript是JavaScript的超集数组方法沿用JS的但类型系统让数组操作更安全。let arr: number[] [1, 2, 3, 4, 5]; let names: Arraystring [Alice, Bob]; // 添加数据 arr.push(6); // [1,2,3,4,5,6] arr [...arr, 7]; // 不可变更新返回新数组 arr [0, ...arr]; // 头部插入 // ES6 提取数组对象的一部分 interface Person { id: number; name: string; age: number; } const people: Person[] [ { id: 1, name: Alice, age: 20 }, { id: 2, name: Bob, age: 25 }, { id: 3, name: Charlie, age: 30 } ]; // 只要id和name字段 const result people.map(({ id, name }) ({ id, name })); // 或者排除某个字段 const withoutAge people.map(({ age, ...rest }) rest);// 坐标表达数组 const points [[1, 2], [3, 4], [5, 6]]; // 解构 for (const [x, y] of points) { console.log(x, y); }热搜里“javascript 坐标表达数组”其实就是用二维数组表示坐标集合这在Canvas绘图、游戏开发、地图处理里很常见。4.2 C的数组边界与多线程读写热搜有“c两个线程分别读写一个大数组”这个场景多线程编程中很经典。两个线程一个读一个写同一个大数组需要区分情况一个线程读、一个线程写不同位置不用加锁但前提是内存不冲突且写入的数据是完整可见的需要考虑内存序。一个线程读、一个线程写同一位置必须同步否则就是数据竞争data race是未定义行为。多个线程读没问题可以并发读。#include thread #include vector #include atomic // 例一读写不同位置安全 std::vectorint arr(1000000); std::thread writer([]() { for (int i 0; i 500000; i) { arr[i] i; } }); std::thread reader([]() { for (int i 500000; i 1000000; i) { std::cout arr[i] ; } }); // 例二读写同一位置有数据竞争需要用锁或原子变量 std::atomicint counter{0}; std::thread t1([]() { for (int i 0; i 100000; i) counter; }); std::thread t2([]() { for (int i 0; i 100000; i) counter; });C的坑在于如果你用原生数组而不是std::vector传递长度信息很容易出错。常见做法是传指针长度或者用std::spanC20引入来封装数组视图安全性和表达力都更好#include span void process(std::spanint arr) { for (int x : arr) { x * 2; } } int data[100]; process(data); // 自动推导长度4.3 JSON数组与YAML数组的解析热搜词里出现了“json数组”和“yamlcpp读取数组”这俩都在配置解析场景中高频出现。很多程序员把时间花在“到底怎么把配置文件里的数组读出来”这个问题上。我这里简单给个小例子JSON数组// JSON.parse 直接得到数组 const jsonStr [{id:1,name:Alice},{id:2,name:Bob}]; const arr JSON.parse(jsonStr); console.log(arr[0].name); // AliceC yaml-cpp 读取数组#include yaml-cpp/yaml.h #include vector #include string // YAML 内容: // fruits: // - apple // - banana // - cherry YAML::Node config YAML::LoadFile(config.yaml); std::vectorstd::string fruits; for (const auto node : config[fruits]) { fruits.push_back(node.asstd::string()); }这里要注意yaml-cpp遍历节点时如果节点是序列数组迭代器按顺序遍历。但如果节点是映射对象迭代器顺序是不确定的所以在读取数组时最好用明确的索引方式config[fruits][0].asstd::string()而不是依赖遍历顺序。4.4 工业场景PLC、LabVIEW与数组整个热搜词里最有意思的一类是工业现场的数组问题。我搜集到西门子PLC、LabVIEW和Qt调试的相关提问这些场景说明一个事实数组不只是互联网后端或算法面试的东西嵌入式开发、工控组态、测试测量里数组一样是核心数据结构。西门子1500 PLC获取数组索引SCL是1500系列的主要编程语言之一查找数组索引的通用写法前面已经提到了。这里补充一个经验在TIA Portal里数组越界会导致CPU进入STOP模式默认设置下。所以在PLC里写数组遍历时最好用LOWER_BOUND和UPPER_BOUND指令获取数组边界这样可以写出对变量长度自适应的代码避免写死上下标// SCL 动态边界遍历 FOR i : LOWER_BOUND(data) TO UPPER_BOUND(data) BY 1 DO // 处理data[i] END_FOR;LabVIEW 字节数组转二进制数组LabVIEW中处理串口数据时经常要用到这个。收到的是一个U8字节数组但业务逻辑需要按位读取。可以用“Boolean Array To Number”函数或者在“Numeric Conversion”里转换数据类型。如果要把字节数组按位拆成布尔数组可以先做位掩码运算// 伪代码把字节数组的第n位取出来 bit_value : (byte_array[i] n) 1LabVIEW的数组操作和文本语言差异很大它是基于“数据流”的图形化编程数组的索引、切片、替换都是在“程序框图”上用节点完成的。新手很容易找不到“取出某一行/某一列”的节点实际上在“数组”函数选板里有“Index Array”和“Array Subset”两个节点能解决绝大部分需求。Qt 调试查看二维数组这个前面章节已经提到过关键就是用类型转换表达式(int(*)[4])variable_name来强制让调试器按二维数组格式展开。还有一个经验是在Qt Creator里打断点后可以把鼠标悬停在变量名上等弹出提示框后右键选择“Preferences”把“Display array elements”的阈值调大这样大数组也能完整显示。5. 现实中我会怎么用一份数组避坑清单5.1 边界与初始化80%的数组Bug都出在这里我把自己踩过、同事踩过、看过别人踩过的坑做个清单整理按出现频率排个序基本就是这份避坑表问题类型典型错误正确做法涉及语言索引越界for (i0; in; i) arr[i]i lengthC/C/Java/C#未初始化int arr[5];直接用初始化或先填默认值C/C浅拷贝arr2 arr1后修改arr2clone()/copyOf()/Arrays.copyJava索引从0还是1MATLAB里写成arr(0)确认语言约定MATLAB数组变指针sizeof(arr)在函数内求数组大小用std::size或传长度参数C/C空数组vs null直接arr.Length没判null判null再判LengthC#/Java引用相等用比较两个数组内容Arrays.equals()/std::equal()Java/C最后一项值得多说两句。Java里比较数组比较的是引用是否相同不是内容是否相等。两个数组内容完全一样返回false。要比较内容Java用Arrays.equals(arr1, arr2)C用std::equal(std::begin(a), std::end(a), std::begin(b))JavaScript用every或者先把数组转字符串。5.2 数组处理小技巧最后分享几个自己平时攒的小技巧比较杂但实用性都不错交换数组元素——不引入临时变量时可以用异或操作arr[i] ^ arr[j]; arr[j] ^ arr[i]; arr[i] ^ arr[j];。注意在语言层面这是安全的但在生产代码里我还是推荐老老实实写临时变量可读性比那几微秒重要得多。动态扩容的代价——普通数组插入头部元素是O(n)操作因为后面所有元素都要往后挪。如果频繁在头部插入用std::dequeC、ArrayDequeJava、linked listC都比数组合适。一句话总结就是看你的访问模式。二维数组与缓存——C/C里按行遍历比按列遍历快因为内存是行优先的按行访问时CPU缓存命中率高。数据量大的时候这个差异能拉到10倍以上。这个在“c语言二维数组”相关代码优化中特别重要。判断数组相等或包含——不需要自己写双层循环。C用std::equal/std::findJava用Arrays.equals/List.containsJS用includes/every这些现成的方法比自己手写的循环少90%的Bug。把数组传参时的长度一起带上——不要指望在被调函数里用sizeof(arr)/sizeof(arr[0])来算数组长度数组在传参时已经退化成指针了。C里用std::vector或std::array或者传size参数或者用std::span都比裸指针隐含长度设计要好。5.3 后续学习方向说句实在话数组本身不是难点难的是和数组配套的那些东西。我今天花了大半天把这些知识串了一遍接下来准备往这几个方向深入一是树状数组和线段树的延伸这俩在区间查询类题目里是王牌结构我整理完树状数组上二分之后发现线段树的可持久化版本也值得啃一遍。二是多维数组在图像处理中的应用其实就是二维数组的实操版本卷积、滤波这些操作本质都是数组遍历和邻域运算等下次有空我把OpenCV里对Mat对象的遍历方式也整理一篇。三是C中数组和STL容器的性能对比。我目前的理解是std::vector比原生数组更安全但底层实现到底差在哪、什么时候该用std::array、什么时候该用vector、什么时候该客串一下裸数组这中间的取舍值得再做一组benchmark实测。数组这个东西就是这样——看起来谁都会但每个细节抠出来都有一篇千字文章能写。今天先记到这里下次继续。