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# 数据类型的主要分类。掌握好这个框架,后续学习就会顺畅很多。
结束总结
今天你只需要记住两句话:
值类型是为了存储数据而设计,各变量独立,赋值就是复制。 引用类型是为了共享对象而设计,多变量共享,一改全改。
下节课,我们将正式迈入值类型的第一大分支——整数类型。
听懂不算会,敲出来才算。
如果你觉得有收获,记得点赞、收藏、关注。
我是村长,咱们下期见!
下期预告:《整数类型》