Unity面经-List底层原理、如何基于数组、如何扩容、List存储泛型、List有关在内存中的结构

学习来源

一、 核心

动态数组

二、先理解数组

a. 数组在创建的时候就固定长度。

b. 数组在内存中是 连续分布的一段区域

elem0\]\[elem1\]\[elem2\]\[elem3\]\[elem4\] ...

c. 内存中数组中的每个元素大小相同。

d. 数组通过指针偏移计算内存地址:

元素地址 = 起始地址 + (i × 单个元素大小)

所以访问 array[i] 只需要:

  1. 取数组起始地址(已保存)

  2. 做一次乘法 + 加法

  3. 直接读取目标位置的值

不需要遍历,也不依赖数组长度

无论访问第 0 个、第 10 个、第 100 万个元素,耗时都一样

因此时间复杂度 = O(1)(常数时间)

三、 再理解动态

数组是固定长度,List为了解决数组不能扩容的问题,动态的意思就是让数组可以扩容,而List就是可以扩容的数组。

  1. 首先,List中存放元素的核心数据结构还是数组。
cs 复制代码
// 简化的List<T>核心字段定义
public class List<T> 
{
    private T[] _items;
    // 先省略其他关键字段
}
  1. 其次,Capacity属性代表List中数组的最大长度。Count属性代表List中数组有效元素的数量。

创建List的时候可以指定初始长度100,100是对Capacity的初始化:

cs 复制代码
var list = new List<int>(100);
  1. 当数组满时候出发扩容,扩容的大小默认是对容量进行翻倍。

  2. 扩容在内存的表现

在堆中开辟一块新的内存,内存大小是原来的2倍,通过Array.Copy将原来的数据复制到新内存的位置,将老的指针指向新的List内存。

所以通常建议在创建数组的时候要设定初始长度,长度是开发者估算使用的,可以有效的避免因为扩容导致的内存移动的消耗。

  1. Add向尾部新加元素

当数组长度小于Capacity容量的时候添加元素,就不扩容,效率极快。

触发扩容就走4的扩容逻辑。

  1. Insert插入元素

当数组长度小于Capacity容量的时候添加元素,利用Array.Copy将Insert位置后的所有元素向后一位复制,如果需要扩容就走扩容逻辑,然后再位置放入插入元素。

插入的时间复杂度是O(n),为什么?因为如果插入的位置是第一个位置,那么所有的元素都要向后移动一格,操作的次数就是数组的长度n,所以是O(n)。

四、 删除元素和删除List

List是引用类型,GC只管引用。

  1. 删除一个元素,元素如果没有被其他类引用,这个元素会等待GC回收。

2.如果List中所有元素都被删除没有被引用,但是List存在引用,List不会被回收。

3.如果List没有被任何引用,List就会被回收。List中的元素和List无关。

五、 对于值类型存储在内存栈上的误区

很多面试题会说值类型存储在栈,引用类型存储在堆,这是不严谨的。

就像一个包含int的Point对象的类型的List,

cs 复制代码
struct Point { public float x, y; }
List<Point> list = new List<Point>();
list.Add(new Point { x = 1, y = 2 });
list.Add(new Point { x = 3, y = 4 });

内存布局如下:

栈(stack)

┌──────────────────────────┐

│ list 变量(引用)────────────┐

└──────────────────────────┘ │

堆(heap)

┌──────────────────────────────────────────┐

│ List<Point> 对象 │

│ _size = 2 │

│ _items ───────────────────────────────┐ │

│ _version = ... │ │

└──────────────────────────────────────────┘ │

堆(heap)

┌──────────────────────────────────────────┐

│ _items: Point[] 动态数组 │

│ ┌──────┬──────┬──────────────┬──────────┐ │

│ │ P0 │ P1 │ (空位) │ (空位)│ │

│ └──────┴──────┴──────────────┴──────────┘ │

│ P0 = {x = 1, y = 2} │

│ P1 = {x = 3, y = 4} │

└──────────────────────────────────────────┘

那么int值是直接存放在堆中的。

那么这个过程是否存在装箱?

答案是不存在,List存储的是泛型,整个过程没有发生Object和int类型的转化。

相关推荐
czxyvX2 小时前
010-C++之List
开发语言·c++·list
PfCoder3 小时前
C# async / await 用法以及和Task的关系
c#·多线程·winform·async、await
吃饺子不吃馅3 小时前
面试过别人后,我对面试祛魅了
前端·面试·github
uhakadotcom3 小时前
fastapi的最新版本,提供了哪些新api可供使用
前端·面试·github
小高0074 小时前
别再滥用 Base64 了——Blob 才是前端减负的正确姿势
前端·javascript·面试
进击的野人4 小时前
JavaScript原型与原型链:深入理解面向对象编程的基石
前端·javascript·面试
程序员爱钓鱼4 小时前
Python编程实战:综合项目 —— Flask 迷你博客
后端·python·面试
程序员爱钓鱼4 小时前
Python编程实战:综合项目 —— 迷你爬虫项目
后端·python·面试
Dream it possible!5 小时前
LeetCode 面试经典 150_二叉树_二叉树中的最大路径和(77_124_C++_困难)(DFS)
c++·leetcode·面试·二叉树