C# 语言入门(二)内存机制、类型特性

本篇 核心知识点 :C# 内存划分(栈 / 堆)、unsafe 指针使用规范、值类型 & 引用类型、装箱拆箱、类型转换(隐式 / 显式)、变量、常量、readonly 只读、字符串转义与 @原生字符串、运算符(算术 / 关系 / 逻辑 / 位 / 赋值 / 三元)、浮点精度坑、短路运算、流程控制(if/switch/for/while/do-while)

一、C# 内存分区与指针相关

1. 栈内存、堆内存划分

概念
  1. 栈 (Stack) :自动分配释放,函数执行结束自动回收,存放值类型变量、局部变量、函数形参

  2. 堆 (Heap) :手动 GC 垃圾回收管理,存放引用类型对象

特性

值类型:变量直接存储数据本体,存栈;

引用类型:变量仅存堆内存地址,真实数据存堆;

多个引用变量存同一地址,修改一处所有变量同步变化。

2. unsafe 不安全代码与指针

概念

C# 默认安全托管环境,禁止直接操作原生指针;如需使用 C/C++ 风格指针,必须开启不安全代码。

使用步骤
  1. 项目设置:VS 项目属性→生成→勾选「允许不安全代码」;

  2. 代码用unsafe修饰代码块 / 函数;

特性
  1. 语法与 C++ 指针完全一致,支持*解引用、&取地址;

  2. sizeof获取类型字节大小,仅能放在 unsafe 块内

  3. 日常开发极少使用,面试常提问「C# 是否支持指针」。

代码示例
复制代码
unsafe void TestPointer()
{
    int a = 10;
    int* p = &a; // 取地址
    Console.WriteLine(*p); // 解引用取值
    Console.WriteLine(sizeof(int)); // int占4字节
}

二、值类型 & 引用类型

2.1 值类型

概念

派生自System.ValueType,数据直接存储在栈内存,赋值时完整拷贝一份数据副本。

包含类型

bool、char、byte/sbyte、short/ushort、int/uint、long/ulong、float (32 位单精度)、double (64 位双精度)、enum 枚举、struct 结构体。

特性

1 赋值互不干扰,修改副本不影响原变量;

2 函数传参默认值拷贝,内部修改不会影响外部实参;

3 内存自动回收,无 GC 开销。

2.2 引用类型

概念

变量仅保存堆内存地址,真实数据存堆,赋值仅拷贝地址,多个变量指向同一块数据。

包含类型

object、string、自定义 class、dynamic 动态类型、数组、集合 List 等。

特性

1 赋值共享同一份堆数据,一处修改全部同步;

2 依靠垃圾回收 GC 释放堆内存;

3 string 特殊:字符串不可变,修改会生成新堆对象。

3. 装箱 & 拆箱(面试高频)

1)装箱

概念:栈内值类型 拷贝到堆,转为object引用类型,隐式自动转换

内存变化:栈复制数据→堆开辟 object 包装。

复制代码
int num = 10;
object obj = num; // 自动装箱
2)拆箱

概念:堆中 object 转回栈内值类型,必须强制显式转换,转换类型不匹配报错。

复制代码
int res = (int)obj; // 强制拆箱
拓展

频繁装箱拆箱会产生大量堆临时对象,GC 压力大,高性能游戏项目尽量避免。

三、类型转换

3.1 隐式转换(自动转换)

概念

编译器自动完成,无需强转;小范围 / 低精度 → 大范围 / 高精度,无数据丢失风险。

示例规则

int → long / int → double;

易错点

double num = 8.0; float f = num; 编译报错,double 精度高于 float,无法隐式转 float;

浮点常量默认 double,单精度必须后缀ffloat f = 5.5f;

3.2 显式强制转换

概念 大范围→小范围,手动强制转换,存在精度丢失、数值溢出风险
复制代码
double d = 3.14;
int i = (int)d; // 强制截断小数

3.3 内置转换工具类

Convert.ToBoolean() / Convert.ToByte() / Convert.ToInt32()等,可实现字符串、任意基础类型互转;

字符串转数字必须用Convert.ToInt32("123"),C# 无 JS/TS 隐式数字转换。

四、变量、常量、只读 readonly

4.1 变量

概念

内存空间别名,必须显式指定类型(强类型),运行中可修改值。

作用域(由{}代码块决定)

1 局部变量:方法 / 循环 /if 内部,仅当前代码块有效,出块销毁;

2 成员变量:写在类内部、方法外部,整个类所有方法可访问;

3 静态 static 变量:类变量,不依赖对象,类名直接访问。

左值 / 右值

左值:可放在赋值号左边(变量);

右值:仅能读取、不可赋值(常量、表达式结果)。

4.2 常量 const

概念

编译期确定固定值,定义时必须初始化,全程不可修改。

特性

1 关键字const修饰;

2 仅支持基础值类型;

3 类中常量直接类名访问,无需实例;

复制代码
const int MAX_HP = 1000;

4.3 readonly 只读

概念

运行时赋值(构造函数内可改),构造完毕后永久只读。

区别 const

const 编译期定值;readonly 运行期赋值,支持对象类型,单例模式高频使用。

五、字符串两种写法与转义字符

1 普通双引号字符串

支持\n换行、\t制表、\\反斜杠等转义字符。

复制代码
string str = "D:\\Game\\test.txt";

2 @原生字符串

前缀@所有反斜杠不再转义,路径、SQL 语句专用。

复制代码
string path = @"D:\Game\test.txt";

拓展

字符串是引用类型,但具备值语义,修改会新建堆对象,不会修改原字符串。

六、运算符全分类与易错点

6.1 算术运算符 + - * / % ++ --

1 自增++i先加再运算,i++先运算后加;

2 取模%仅整数适用;

3 浮点除法:5 / 2.0结果 2.5;整数相除5/2直接截断得 2。

6.2 关系运算符 > < >= <= == !=

重点易错:浮点数不能直接==判等

浮点数存储存在精度误差,如 3.4 实际存储为 3.3999999,直接==大概率判断失败。

标准写法

取绝对值,差值小于极小阈值即判定相等:

复制代码
double a = 3.4;
double b = 3.4;
if(Math.Abs(a - b) < 1e-6)
    Console.WriteLine("相等");

6.3 逻辑运算符 && || ! 短路运算

短路规则

1 &&:左边条件 false,右侧代码完全不执行;

2 ||:左边条件 true,右侧代码完全不执行;

作用:减少无意义运算,提升性能。

6.4 位运算符 & | ^ ~ << >>

游戏高频用途:RGBA 颜色拆分、碰撞分组标记

1 &按位与:提取指定字节(如取 RGB 红色通道 color & 0xFF);

2 |按位或:合并标记、叠加颜色通道;

3 <<左移、>>右移:等价乘 2 / 除 2,效率极高。

示例:32 位 int 存储 RGBA 四色通道,单字节 0~255。

6.5 赋值运算符 = += -= *= /= %=

等价简写:a += 5a = a + 5

Lua 等脚本无复合赋值运算符,后期学习注意区分。

6.6 三元条件运算符 条件 ? 真值 : 假值

唯一三目运算符,简化 if-else 取值,常用于取最大 / 最小值

复制代码
int min = a < b ? a : b;

6.7 运算符优先级

乘除 > 加减 > 位运算 > 关系 > 逻辑 > 三元 > 赋值;

复杂表达式建议括号提升可读性。

七、流程控制语句

7.1 if / else if / else

易错规范

if 条件后禁止加分号,分号代表空语句,逻辑失效;

复制代码
// 错误写法
if(a > 10); 
{ }

7.2 switch 分支

规则

1 case 后必须是常量,不支持变量;

2 支持 int、char、字符串 string 常量;

3 每个 case 末尾建议加break,无 break 会穿透到下一分支;

4 多个 case 执行同一段代码可堆叠 case;

5 default所有分支不匹配时执行,可选写;

6 case 内定义变量需包裹{}代码块,防止重定义报错。

复制代码
int key = 2;
switch(key){
    case 1:
    case 2: // 1、2执行相同逻辑
        Console.WriteLine("选中");
        break;
    default:
        Console.WriteLine("无匹配");
        break;
}

7.3 循环语句 for /while/do-while

1 for 循环

格式for(初始化;条件;迭代);三部分用分号分隔;

循环变量仅循环内有效,外部无法访问;

循环头禁止加分号。

2 while 循环

先判断条件,不成立一次都不执行;

初始化写循环外部,迭代写循环体内。

3 do-while 循环

先执行一次循环体,再判断条件;至少执行 1 次

末尾必须加分号do {} while(条件);

八、拓展工程 & 面试考点

1 内存区分:值栈引用堆、装箱拆箱原理必考题;

2 浮点数判等坑,笔试高频改错;

3 短路运算、位运算游戏开发实际用途;

4 unsafe 指针使用前提(项目设置 + 代码块修饰);

5 const 与 readonly 核心区别;

6 switch、if、for 通用代码书写规范,规避语法陷阱。