堆栈和静态储区

值类型和引用类型

概念

  • 值类型:
  • 值类型变量直接包含它们的数据。当你声明一个值类型变量时,这个变量本身存储了实际的值。例如,C# 中的基本数据类型(如int、double、bool等)都是值类型。如果有一个int变量num = 5;,变量num存储的就是数字5这个值。值类型在内存中的存储相对简单,通常存储在栈(Stack)上。

  • 引用类型:
  • 引用类型变量存储的是一个引用,这个引用指向存储数据的实际内存位置(通常是堆 - Heap)。例如,在 C# 中,类(Class)、接口(Interface)、委托(Delegate)等都是引用类型。当你创建一个类的对象时,例如MyClass myObj = new MyClass();,变量myObj存储的是指向MyClass对象在堆内存中实际存储位置的引用。

    内存分配和管理

  • 值类型:
  • 如前面提到的,值类型一般存储在栈上。当一个值类型变量进入作用域(例如在一个方法内部声明)时,它会在栈上分配内存,当变量离开作用域时,其占用的栈内存会被自动释放。例如:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    class Program
    {
    static void Main()
    {
    int num1 = 10;
    int num2 = num1;
    num2 = 20;
    Console.WriteLine(num1);
    // 输出10,因为num1和num2是两个独立的值类型变量,num2的修改不影响num1
    }
    }

    在这个例子中,num1和num2都是int值类型变量,它们在栈上有各自独立的存储空间。当num2被赋值为20时,num1的值不受影响。

  • 引用类型:
  • 引用类型的对象本身存储在堆中,而引用变量存储在栈上。当你创建一个引用类型的对象时,会在堆中分配内存来存储对象的数据,同时在栈上创建一个引用变量来指向堆中的对象。例如:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    class MyClass
    {
    public int Value;
    }
    class Program
    {
    static void Main()
    {
    MyClass obj1 = new MyClass();
    obj1.Value = 10;
    MyClass obj2 = obj1;
    obj2.Value = 20;
    Console.WriteLine(obj1.Value);
    // 输出20,因为obj1和obj2指向同一个堆中的对象,修改obj2会影响obj1
    }
    }

    在这里,obj1和obj2是引用变量,它们指向同一个MyClass对象在堆中的存储位置。当通过obj2修改Value属性时,obj1所指向的对象的属性也被修改了,因为它们引用的是同一个对象。

    参数传递

  • 值类型:
  • 当一个值类型变量作为参数传递给一个方法时,实际上是将变量的值复制一份传递给方法。方法内部对参数的修改不会影响原始变量的值。例如:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    class Program
    {
    static void ModifyValue(int num)
    {
    num = 20;
    }
    static void Main()
    {
    int originalNum = 10;
    ModifyValue(originalNum);
    Console.WriteLine(originalNum);
    // 输出10,因为在ModifyValue方法中修改的是复制后的num,而不是originalNum
    }
    }
  • 引用类型:
  • 当一个引用类型变量作为参数传递给一个方法时,传递的是引用的副本。这意味着方法内部可以通过这个引用访问和修改原始对象。例如:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    class MyClass
    {
    public int Value;
    }
    class Program
    {
    static void ModifyObject(MyClass obj)
    {
    obj.Value = 20;
    }
    static void Main()
    {
    MyClass originalObj = new MyClass();
    originalObj.Value = 10;
    ModifyObject(originalObj);
    Console.WriteLine(originalObj.Value);
    // 输出20,因为在ModifyObject方法中通过引用修改了originalObj指向的对象的值
    }
    }

    性能差异

  • 值类型:
  • 由于值类型数据存储在栈上,访问速度通常比较快。而且值类型的内存管理相对简单,在变量离开作用域时自动释放内存,不需要像引用类型那样进行复杂的垃圾回收(Garbage Collection)操作。

  • 引用类型:
  • 引用类型的存储和访问涉及到堆和栈的交互。从栈上的引用变量访问堆中的对象可能会稍微慢一些。并且,引用类型对象的生命周期由垃圾回收机制管理,当对象不再被引用时,垃圾回收器会在合适的时间回收对象占用的堆内存,这个过程可能会带来一定的性能开销。不过,引用类型的灵活性使得它们在面向对象编程中非常重要,例如用于实现复杂的数据结构和对象之间的关系。


    栈内存

    栈的基本概念

    在 C# 中,栈(Stack)是一种用于存储数据的数据结构,它主要用于存储方法(函数)调用的相关信息,包括局部变量、方法参数、返回地址等。栈的操作遵循 “后进先出”(LIFO - Last In First Out)原则,就像一摞盘子,最后放上去的盘子最先被拿下来。

    栈在内存中有一个固定的大小(这个大小由操作系统和编译器等因素决定),并且是自动管理内存的。当一个方法被调用时,系统会在栈上为这个方法分配一块内存区域,用于存储该方法所需的信息。当方法执行结束后,这块内存区域会被自动释放,栈顶指针会相应地调整,使得栈恢复到之前的状态。

    栈在内存中的存储方式

    栈通常存储在内存的较高地址区域。它的内存布局是从高地址向低地址增长的。例如,当一个新的方法被调用时,新的栈帧(Stack Frame)会在栈顶创建。栈帧包含了这个方法的所有局部变量和调用信息。

    假设一个简单的 C# 程序,在Main方法中调用了另一个方法SomeMethod,当SomeMethod被调用时,一个新的栈帧会在栈顶被创建,用于存储SomeMethod的局部变量和其他相关信息。当SomeMethod执行完毕,这个栈帧会被销毁,栈顶指针会回到Main方法栈帧的位置。

    栈在方法调用中的作用

    参数传递:

    当一个方法被调用时,方法的参数会被压入栈中。例如,对于一个方法void MyMethod(int a, int b),当调用MyMethod(3, 5)时,值3和5会按照从右到左的顺序(在 C# 中一般是这个顺序)被压入栈中。这是因为在方法内部,参数的访问通常是从左到右的,先压入b的值5,再压入a的值3,这样在方法内部就可以按照正确的顺序访问参数。

    局部变量存储:

    方法内部的局部变量也存储在栈中。例如,在一个方法void AnotherMethod()中有一个局部变量int localVar = 10,这个localVar变量会在AnotherMethod的栈帧中分配一个存储位置,用于存放值10。

    返回地址存储:

    当一个方法被调用时,调用该方法的指令的下一条指令的地址(即返回地址)会被存储在栈中。这样,当被调用的方法执行完毕后,系统可以根据这个返回地址回到调用者继续执行后面的指令。例如,在Main方法中调用SomeFunction,当SomeFunction被调用时,Main方法中调用SomeFunction指令的下一条指令的地址会被存储在栈中,当SomeFunction执行结束后,程序会根据这个地址回到Main方法继续执行。

    访问速度:

    一般来说,栈的访问速度相对较快。因为栈的内存布局比较规整,而且不需要像堆那样进行复杂的内存管理操作(如垃圾回收)来查找和释放内存。数据在栈上的存储和访问比较直接,而堆中的对象可能因为垃圾回收等操作而导致存储位置发生变化,从而影响访问速度。


    堆内存

    在 C# 中,堆(Heap)是用于动态内存分配的重要内存区域,主要用于存储引用类型的对象。下面将从堆的基本概念、堆内存分配、垃圾回收机制、堆与栈的对比等方面详细介绍。

    基本概念

    堆是一块较大的内存区域,它与栈不同,栈主要用于存储局部变量和方法调用信息,遵循后进先出(LIFO)原则。而堆用于存储引用类型的对象,其内存分配和释放由垃圾回收器(Garbage Collector,GC)负责管理,不遵循 LIFO 原则。

    堆内存分配

    当使用 new 关键字创建一个引用类型的对象时,系统会在堆上为该对象分配内存。例如:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    class Person
    {
    public string Name;
    public int Age;
    }

    class Program
    {
    static void Main()
    {
    // 在堆上创建一个 Person 对象
    Person person = new Person();
    person.Name = "Alice";
    person.Age = 25;
    }
    }

    在上述代码中,new Person() 语句会在堆上分配一块内存,用于存储 Person 对象的 Name 和 Age 字段。同时,栈上的 person 变量存储的是指向堆上该 Person 对象的引用。

    垃圾回收机制

    垃圾回收器(GC)负责自动回收堆上不再被引用的对象所占用的内存。其主要工作流程如下:
    标记阶段:GC 会从根对象(如全局变量、静态变量、当前执行栈中的变量等)开始遍历所有可达的对象,并将这些对象标记为 “存活”。
    清除阶段:未被标记为 “存活” 的对象被视为垃圾对象,GC 会回收这些对象所占用的内存。
    压缩阶段:为了减少内存碎片,GC 可能会对堆上的存活对象进行移动和整理,使它们在内存中连续存储。

  • 例如:
  • 1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    class MyClass
    {
    public int Value;
    }

    class Program
    {
    static void Main()
    {
    MyClass obj1 = new MyClass();
    obj1.Value = 10;
    // 现在 obj1 指向一个新的对象,原来的对象不再被引用
    obj1 = new MyClass();
    obj1.Value = 20;
    // 之前创建的第一个 MyClass 对象成为了垃圾,等待垃圾回收
    }
    }

    垃圾回收器会在合适的时机(如堆空间不足、手动触发等)对这些垃圾对象进行回收。

    堆与栈的对比

    对比项
    存储内容 引用类型的对象 值类型的变量、方法调用信息(如局部变量、返回地址等)
    内存管理 由垃圾回收器自动管理 自动分配和释放,方法执行结束后,栈上的内存自动回收
    分配和访问速度 相对较慢,因为涉及复杂的内存管理和对象查找 相对较快,内存分配和释放简单直接
    内存布局 不遵循特定顺序,可能会产生内存碎片 遵循后进先出(LIFO)原则,内存布局较为规整

    堆的性能影响因素

    象创建频率:频繁创建和销毁对象会增加垃圾回收的负担,影响性能。可以考虑对象池技术来复用对象,减少对象创建和销毁的次数。
    大对象分配:大对象(如大型数组)会被分配到特殊的大对象堆(LOH)中,大对象堆的垃圾回收机制与普通堆不同,频繁分配大对象可能会导致内存碎片化和性能问题。
    垃圾回收策略:不同的垃圾回收模式(如工作站模式、服务器模式)适用于不同的应用场景,选择合适的垃圾回收模式可以提高性能。
    综上所述,了解 C# 中堆的工作原理和特点,有助于编写高效、稳定的代码。


    静态类型

    C# 中,静态类型是一个重要的概念,它贯穿于语言的多个方面,下面从静态类型的定义、静态成员、静态类以及静态类型检查几个方面详细介绍:

    静态类型的定义

    静态类型指的是在编译时就确定变量、表达式或参数的类型。在 C# 里,当你声明一个变量时,必须明确指定它的类型,编译器会在编译阶段进行类型检查,确保代码中所有操作都符合该类型的定义。

    1
    2
    3
    4
    // 声明一个 int 类型的变量
    int number = 10;
    // 声明一个 string 类型的变量
    string message = "Hello, World!";

    在上述代码中,number 被明确指定为 int 类型,message 被指定为 string 类型,编译器在编译时会根据这些类型信息来验证对它们的操作是否合法。例如,不能直接将一个字符串赋值给 int 类型的变量,否则会产生编译错误。

    静态成员

    静态字段

    静态字段属于类本身,而不是类的实例。无论创建多少个类的实例,静态字段在内存中只有一份副本。可以通过类名直接访问静态字段。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    class MyClass
    {
    // 静态字段
    public static int StaticField = 10;
    }

    class Program
    {
    static void Main()
    {
    // 通过类名访问静态字段
    Console.WriteLine(MyClass.StaticField);
    }
    }

    静态方法

    静态方法同样属于类本身,不能通过类的实例来调用,只能通过类名调用。静态方法只能直接访问类的静态成员,不能直接访问实例成员。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    class Calculator
    {
    // 静态方法
    public static int Add(int a, int b)
    {
    return a + b;
    }
    }

    class Program
    {
    static void Main()
    {
    // 通过类名调用静态方法
    int result = Calculator.Add(3, 5);
    Console.WriteLine(result);
    }
    }

    静态属性

    静态属性和静态字段类似,也是属于类本身。可以通过类名来访问静态属性,并且可以在属性的访问器中添加自定义的逻辑。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    class Counter
    {
    private static int count = 0;

    // 静态属性
    public static int Count
    {
    get { return count; }
    set { count = value; }
    }
    }

    class Program
    {
    static void Main()
    {
    // 通过类名访问静态属性
    Counter.Count = 5;
    Console.WriteLine(Counter.Count);
    }
    }

    静态类

    静态类是一种特殊的类,它只能包含静态成员,不能被实例化。静态类常用于创建工具类,提供一组相关的静态方法和属性。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    // 静态类
    public static class MathUtils
    {
    // 静态方法
    public static double Square(double num)
    {
    return num * num;
    }

    // 静态属性
    public static double Pi { get { return 3.14159; } }
    }

    class Program
    {
    static void Main()
    {
    // 通过类名调用静态方法
    double result = MathUtils.Square(4);
    Console.WriteLine(result);

    // 通过类名访问静态属性
    Console.WriteLine(MathUtils.Pi);
    }
    }

    静态类型检查

    静态类型检查是 C# 编译器在编译阶段进行的一项重要工作。它会检查代码中变量的使用是否符合其声明的类型,确保类型安全。例如:

    1
    2
    3
    int num = 10;
    // 下面这行代码会产生编译错误,因为不能将 int 类型赋值给 string 类型的变量
    // string str = num;

    编译器会在编译时发现上述代码的类型不匹配问题,并给出相应的错误信息,从而避免在运行时出现类型相关的异常。这种静态类型检查机制有助于提高代码的可靠性和可维护性。