Skip to main content

13.值类型 VS 引用类型

第 13 集:数据类型与内存

一、开场

大家好,我是村长。

上节课我们认识了类和对象。 对象是根据类创建出来的一个个独立的"实体"。

实体,就是实际存在的物体。那我们 new 出来的对象到底存在于哪里呢?

没错!第7课我们讲过:程序和数据都会进入内存。

对象也是数据,它需要占据实实在在的内存空间。

内存从逻辑上分为五个区:代码段、数据段、栈、堆,还有操作系统保留区。

不过今天,咱们不用记这么多,只需关注其中最重要的两个区:栈和堆。

听到这两个词,很多同学就开始紧张,觉得是高深莫测的底层概念。

其实没那么复杂。栈是用来管理“方法”的,堆是用来存储“对象”的。

想真正搞懂 C# 中的值类型和引用类型,这一步必须跨过去。

别担心,今天我用几个最简单的例子,带你彻底搞懂它们。

迈过这道坎,你对 C# 的理解,会再上一个台阶。

(LOGO 动画)

本期内容 一、什么是栈内存(Stack)? 二、什么是堆内存(Heap)? 三、数据类型简介 四、数据类型的分类

一、什么是栈内存(Stack)?

栈内存,简称"栈",是一种管理方法调用的内存空间。

打开上节课的 Cat 类文件,找到 Main 方法,

class Cat
{
// ========== 特征:编程里叫“字段” ==========
public string Color = "白色";
public int Eyes = 2;
public int Legs = 4;

// ========== 行为:编程里叫“方法” ==========
// 实例方法:必须由具体的猫对象来调用
public void Meow()
{
Console.WriteLine("喵喵喵");
}

public void CatchMouse()
{
Console.WriteLine("捉到老鼠了");
}
}
public static void Main()
{

Cat cat1 = new Cat();
Cat cat2 = new Cat();
Cat cat3 = new Cat();

cat1.CatchMouse();
}

当 Main 方法被调用时,从逻辑上讲,系统会在栈内存上为它分配一块专属空间,用来保存方法执行过程中需要的数据,这块空间叫做栈帧(Stack Frame)。

接下来,在 Main 方法里添加五行代码:

int a = 10;
int b = a;
b = 20;
Console.WriteLine(a);
Console.WriteLine(b);

这五行代码非常简单。 但是,它背后隐藏着 C# 中一个重要概念: 变量中的数据是如何存储的。

当执行:int a = 10;时,

系统会在 Main 方法对应的栈帧中,为变量 a 分配一块空间,然后把 10 存进去",同时,贴个标签叫 a。 (动画配图:创建方框,存储10,标注a)

当执行int b = a时,这里发生了一件重要的事情。

系统会读取变量 a 中存储的10。 然后复制一份,再存储到变量 b 的空间里。 此时,变量a 中存储的是10,变量b 中存储的也是10 数据虽然一样,但它们两个是完全独立的变量,毫无关系。 为什么?因为把变量a赋值给变量b,赋值的是数据的副本。

接着执行b = 20 这一步,就是修改 变量b 中保存的数据。 原来的 10 被新值 20 替换。 现在变量a 中存储的是 10。变量b 中存储的是 20。 最后打印变量 a,输出10。 打印变量 b ,输出20;

通过这五行代码,我们发现一个规律: 每个变量都有自己独立的数据空间。 变量之间的赋值,赋值的是数据本身,而不是共享同一份数据。

二、什么是内存堆(Heap)?

了解了栈,我们再来看堆。 堆,是用来存储对象的内存空间。

简单记:"栈"管理方法,"堆"存储对象。


Cat cat1 = new Cat();
Cat cat2 = new Cat();
Cat cat3 = new Cat();

cat1.CatchMouse();

比如,当执行new Cat() 时,实际上发生了两件事。

第一件事:在堆中创建一个 Cat 对象。

new Cat()告诉系统:“我要在堆上,创建一个 Cat 类型的对象实体。” 于是堆上分配了一块空间,假设空间地址是 0x123456, 里面存着cat对象的数据本身。

Color = "白色";
Eyes = 2,Legs = 4

这块空间,就是对象真正存在的地方。

第二件事:在栈中创建变量 cat1。 因为真正的Cat对象实体,存储在堆里。 所以cat1变量不存储cat对象实体,而是存储cat对象实体的引用。 这里的引用,本质上可以理解成一个能够定位cat对象实体的地址,比如0x123456,只不过,在 C#中,这个地址受到保护和限制,程序员不能像 在 C++语言中那样,直接操作它。 到这里,请记住:cat1 变量名存储在栈,但它指向的对象实体存储在堆。

接下来,在cat1.CatchMouse()这行代码下,添加一行代码,

Console.WriteLine(cat1.Color);  // 白色

打印cat1 的color 属性,

点击执行,结果是白色。请记住这个颜色。

Cat cat4 = cat1; // 把 cat1 的门牌号复制一份给 cat4
cat4.Color = "黑色";
Console.WriteLine(cat1.Color); // 猜猜输出什么?

然后再创建一个对象cat4,把cat1赋值给它。 这行代码意味着:复制了cat1变量中存储的引用给cat4。现在 两个变量中存储着相同的引用,同时指向堆中的Cat对象实体。这样,就实现了数据的共享。

接着我们验证一下,通过cat4 ,修改堆中Cat对象实体的Color 属性值为黑色, 因为 cat1 和 cat4 同时指向cat对象实体,所以通过 cat1 ,打印 Color 属性。 你会发现:输出的也是黑色。

这就是对象在堆上的存储方式——多个变量可以引用同一个对象实体,一改全改。

四、C#数据类型简介

搞懂了堆和栈,C#的数据类型就没什么难的了。

为了方便管理各种各样的数据,C#的数据类型主要分为两大类:值类型和引用类型。

它们最大的区别,不在于名字,而在于变量保存的数据不同。

维度值类型引用类型
数据不同变量存储数据本身变量存储对象的引用
设计目的存储“数据本身”共享“对象”
关注“数据是多少”“对象是谁?”(身份)
赋值效果复制数据副本复制引用地址
数据共享不共享,各自独立共享同一对象

旁白(表格数据逐行显示):

  • 值类型就是变量直接存储数据本身。引用类型就是变量存储对象的引用。
  • 值类型是为了存储“数据本身”而设计;引用类型是为了“共享对象”而设计。
  • 值类型关注的是:这个数据是多少?引用类型关注的是:这个对象是谁?
  • 值类型的赋值,传递的是数据副本。引用类型的赋值,传递的是对象的引用地址。简单记:一个传值,一个传址,地址的址。
  • 值类型的核心特点是数据独立、互不共享。引用类型的核心特点是:数据共享,多个变量可以共享同一个对象实体。
> ⚠️ 但是,这里必须敲黑板:
> 千万别把C#里的“引用”简单叫做“指针”。在C#里它就叫“引用”。
> 真正的指针,比如C++ 里的指针,可以做地址运算,非常危险;而 C#的引用,是受管控的安全引用,你不能拿它直接做加减乘除。请务必牢记。

五、数据类型的分类

为了帮你建立全局视野,我们先来看一张 C# 数据类型的分类图。

C# 的类型系统分为两大阵营:值类型和引用类型。

值类型往下又分为三大类:简单类型、枚举类型和结构类型。其中我们最熟悉的,就是简单类型,它包括整数、浮点数、高精度小数、字符和布尔值——这些就是最基础的数据单元。

引用类型则主要包括四大类:类、接口、委托和数组。而类是我们面向对象编程的绝对主角。特别要注意的是,我们天天打交道的 string(字符串) ,它本质上也是一个特殊的类,只是因为太常用,编译器给了它很多便利。

未来 80% 的场景,你只需要重点关注两类:值类型里的简单类型,和引用类型里的类。至于枚举、结构、接口、委托、数组,它们都是在你遇到特定需求时,自然会用上的进阶工具。

以上就是 C# 数据类型的主要分类。掌握好这个框架,后续学习就会顺畅很多。

结束总结

今天你只需要记住两句话:

值类型是为了存储数据而设计,各变量独立,赋值就是复制。 引用类型是为了共享对象而设计,多变量共享,一改全改。

下节课,我们将正式迈入值类型的第一大分支——整数类型。

听懂不算会,敲出来才算。
如果你觉得有收获,记得点赞、收藏、关注。
我是村长,咱们下期见!

下期预告:《整数类型》