数据结构中的内存变化:变量赋值与引用的深度解析

我们将从数据结构的角度深入探讨内存管理机制,重点解析变量赋值和引用的具体表现。通过对不同数据类型的分析,将展示它们在堆和栈内存中的具体变化

计算机基础知识 :lua

常见到的描述:

a={1,2,3} b=a b[1]=2 print(b) 

a="aaaa" b=a b="bbbb" print(b)

a=10 b=a b=11 print(b)

1:常用的值类型和引用类型堆栈的变化

表(Table)的情况

a = {1, 2, 3}
b = a
b[1] = 2
print(b[1])
  1. a = {1, 2, 3}

    • 在内存中创建一个表对象 {1, 2, 3}
    • 变量 a 指向这个表对象的内存地址。
  2. b = a

    • 变量 b 被赋值为 a 的值,即表对象的内存地址。
    • 现在 ab 都指向同一个表对象 {1, 2, 3}
  3. b[1] = 2

    • 通过 b 修改表中的第一个元素,将其值从 1 改为 2
    • 由于 ab 指向同一个表对象,所以表对象的内容被修改为 {2, 2, 3}
  4. print(b[1])

    • 输出 b 中第一个元素的值,即 2

在这段代码中,ab 指向同一个表对象,修改 b 的内容会影响到 a

字符串的情况

a = "aaaa"
b = a
b = "bbbb"
print(b)
  1. a = "aaaa"

    • 在内存中创建一个字符串对象 "aaaa"
    • 变量 a 指向这个字符串对象的内存地址。
  2. b = a

    • 变量 b 被赋值为 a 的值,即字符串对象的内存地址。
    • 现在 ab 都指向同一个字符串对象 "aaaa"
  3. b = "bbbb"

    • 在内存中创建一个新的字符串对象 "bbbb"
    • 变量 b 被重新赋值为新的字符串对象 "bbbb" 的内存地址。
    • 变量 a 仍然指向原来的字符串对象 "aaaa" 的内存地址。
  4. print(b)

    • 输出 b 指向的字符串,即 "bbbb"

在这段代码中,重新赋值 b"bbbb" 时,b 指向一个新的字符串对象,而 a 仍然指向原来的字符串对象。

数字的情况

a = 10
b = a
b = 11
print(b)
  1. a = 10

    • 变量 a 被赋值为整数 10
  2. b = a

    • 变量 b 被赋值为 a 的值,即 10
    • 现在 ab 都是 10
  3. b = 11

    • 变量 b 被重新赋值为整数 11
    • 变量 a 仍然是 10
  4. print(b)

    • 输出 b 的值,即 11

在这段代码中,ab 都是简单的数字,重新赋值 b 不会影响 a 的值。

内存总结

  • 表的情况

    • ab 指向同一个表对象的内存地址。
    • 修改 b 的内容会影响 a,因为它们指向同一个对象。
  • 字符串的情况

    • ab 最初指向同一个字符串对象的内存地址。
    • 重新赋值 b 为新的字符串对象时,b 指向新的内存地址,而 a 保持不变。
  • 数字的情况

    • ab 都是简单的数字值,重新赋值 b 不会影响 a

Lua 变量在内存中存储的是对实际数据对象的引用(对于表和字符串),而对于数字和布尔值等简单数据类型,变量直接存储值本身。这些示例展示了 Lua 中变量赋值和对象引用的内存管理行为。

2:对于值类型数据堆栈内存的误解(class和结构体)

 比如现在c# 有这样一个情况,当我们声明一个类,当类中声明一个结构体。堆栈数据如何分配的呢?

关于结构体和类的内存存储位置的问题:

1:值类型和引用类型的存储位置

  1. 值类型(Value Types)

    • 值类型包括基本数据类型(如整数、浮点数、布尔值等)以及用户自定义的结构体(struct)。
    • 值类型的实例通常存储在栈上,而不是堆上。栈上的分配和释放速度较快,适合存储较小的数据。
  2. 引用类型(Reference Types)

    • 引用类型包括类(class)、接口、委托和数组等。
    • 引用类型的实例存储在堆上,而不是栈上。堆上的分配和释放比栈上的操作更复杂,但是能够处理更大和更灵活的对象。
public class MyClass
{
    private struct MyStruct
    {
        public int x;
        public float y;
    }

    private MyStruct structInstance;

    // 其他成员和方法
}
内存结构和存储地址分析
  1. 类的实例化

    • 当创建 MyClass 的实例时,系统会为这个类分配内存空间,包括其声明的结构体成员 structInstance
  2. 结构体 MyStruct 的存储

    • MyStruct 在内存中的布局和存储位置通常是作为类的成员之一。具体来说,它会与类的其他成员一起存储在相同的内存块中。
  3. 内存地址分配

    • MyClass 的实例的内存地址(通常是在堆上分配,具体地址由系统动态分配)。
    • structInstance 的内存地址相对于 MyClass 实例的地址是固定的偏移量。例如,假设 structInstanceMyClass 中的偏移量是 0x04,则 structInstance 的内存地址就是 MyClass 实例地址 + 0x04
  4. 结构体成员的访问

    • 可以通过类的实例来访问结构体的成员变量,例如 myClassInstance.structInstance.xmyClassInstance.structInstance.y

2:特性和注意事项

  • 封装性:结构体在类内部声明时,可以利用类的访问控制符进行封装,控制结构体的成员变量的访问权限。
  • 内存布局:结构体的成员变量和类的其他成员一起存储在相同的内存块中,这种方式有助于优化内存访问和管理。
  • 效率和灵活性:使用结构体作为类的成员可以提高代码的效率,特别是当需要定义轻量级的数据类型时,结构体比类更适合

然后抛出一个问题:

我们常说结构体是值类型,值类型一般存储在栈上为啥当结构体在class里面声明的时候内存是在堆上的呢

3:为什么结构体在类中声明时存储在堆上?

  1. 在 C# 中,当结构体作为类的成员时,它的存储位置通常是和类实例一起存储在堆上,而不是栈上的主要原因有:

  2. 对象生命周期和作用域

    • 类的实例(对象)的生命周期由程序的执行流程决定,通常在堆上分配,直到垃圾回收器确定其不再需要时才被释放。
    • 结构体作为类的一部分,其生命周期和所属类实例相同,因此也存储在堆上。
  3. 引用类型的特性

    • 类是引用类型,其实例(对象)存储在堆上,而不是栈上。即使类的字段(包括结构体)是值类型,它们也是类的一部分,因此跟随类实例一起存储在堆上。
  4. 性能和内存管理

    • 将结构体存储在堆上可以更好地管理内存,特别是在处理类对象时,结构体的复制和管理更加方便和统一。

在 C# 中,类可以内部声明结构体作为其成员,这种方式利用了结构体的值类型特性和类的封装性,同时遵循类似于其他编程语言的内存管理和访问规则。这种设计能够帮助开发人员更好地组织和管理复杂的数据结构,从而提高代码的效率和可维护性。

4:啥情况下声明的值类型数据是在栈上

在 C# 中,值类型数据通常存储在栈上,但具体存储位置的决定因素并不是简单地由值类型的声明来决定,而是由值类型的使用方式和上下文环境决定的

    1 局部变量

  • 当值类型作为局部变量声明时,通常存储在调用栈上。例如:
  • void MyMethod() {
        int x = 10; // x 存储在栈上
    }
    

    2 方法的参数

  • 方法的参数如果是值类型,也会存储在调用栈上。例如:
  • void MyMethod(int a) {
        // a 存储在栈上
    }
    

    3 值类型数组的元素

  • 如果值类型是数组的元素,且数组本身是局部变量或者存储在栈上的对象中,则数组元素通常也会存储在栈上。例如:
void MyMethod() {
    int[] arr = new int[3]; // arr 存储在栈上
    arr[0] = 1; // arr[0] 存储在栈上
}

      4 结构体作为局部变量或字段

  • 当结构体作为局部变量或者字段声明时,其实例也通常存储在栈上。例如:
  • struct MyStruct {
        public int x;
    }
    
    void MyMethod() {
        MyStruct s = new MyStruct(); // s 存储在栈上
        s.x = 10; // s.x 存储在栈上
    }
    

    需要注意的是,尽管值类型数据通常存储在栈上,但具体实现依赖于编译器的优化和执行环境的具体实现。在某些情况下,编译器或运行时环境可能会选择将值类型数据存储在堆上,例如当值类型数据作为引用类型的字段或数组元素,或者在需要进行装箱(boxing)和拆箱(unboxing)的情况下

3:什么情况发啥装箱拆箱操作 ?

在 C# 中,装箱(boxing)和拆箱(unboxing)是将值类型数据转换为引用类型数据(装箱)和将引用类型数据转换为值类型数据(拆箱)的操作

装箱(Boxing)

装箱是将值类型数据转换为引用类型数据的过程。具体来说,当将值类型数据赋值给 object 类型或者其它接口类型时,就会发生装箱操作。例如:

int i = 42;
object obj = i; // 装箱操作,将 int 值类型转换为 object 引用类型

在这个例子中,i 是一个值类型的变量,存储在栈上,而 obj 是一个引用类型的变量,它在堆上分配内存来存储装箱后的 int 值。

拆箱(Unboxing)

拆箱是将引用类型数据转换为值类型数据的过程。具体来说,当从 object 类型或者其它接口类型中获取值类型数据时,就会发生拆箱操作。例如:

object obj = 42;
int i = (int)obj; // 拆箱操作,将 object 引用类型转换为 int 值类型

在这个例子中,obj 是一个引用类型的变量,它存储了一个装箱后的 int 值。当执行拆箱操作时,运行时系统会检查 obj 中实际存储的类型,并将其转换为 int 类型的值,这个值存储在栈上的 i 变量中。

什么情况会发生装箱拆箱操作?

  1. 将值类型赋值给 object 类型变量或者接口类型变量

    • 当将值类型数据赋值给 object 类型、System.ValueType 类型或者任何接口类型时,会发生装箱操作。
  2. object 类型变量或者接口类型变量中获取值类型数据

    • 当从 object 类型、System.ValueType 类型或者任何接口类型的变量中获取值类型数据时,如果变量实际存储的是装箱后的值类型数据,会发生拆箱操作。
  3. 作为参数传递给接受 object 类型或者接口类型的方法

    • 如果一个方法的参数类型是 object 或者某个接口类型,而传入的是值类型数据,那么在传递过程中可能会发生装箱操作。

性能和注意事项

  • 性能影响:装箱和拆箱操作都会引入额外的性能开销,包括内存分配和数据复制。频繁的装箱拆箱操作可能会影响应用程序的性能,特别是在高性能需求的场景下需要谨慎使用。

  • 数据类型转换:装箱拆箱操作是类型转换的一种形式,因此在使用时需要注意类型安全和数据的正确性。

  • 避免不必要的装箱拆箱:尽量避免不必要的装箱拆箱操作,可以通过泛型、值类型的特化方法(例如 Nullable<T>)等方式来避免或减少这些操作。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值