C# 指针、内存管理与公共语言基础设施详解
指针与地址相关要点
栈空间是非常宝贵的资源,虽然其容量较小,但一旦栈空间耗尽,程序将会崩溃。通常程序的栈空间小于 1MB,甚至可能更少。所以,要格外注意避免在栈上分配任意大小的缓冲区。
指针解引用
若要访问指针所指向类型变量中存储的数据,就需要对指针进行解引用操作,即在表达式前加上间接运算符。例如:
byte data = *pData;
此代码对 pData 所指向的字节位置进行解引用,并返回该位置的单个字节。
在不安全代码中运用此原理,可实现对“不可变”字符串的非常规修改,如下所示:
string text = "S5280ft";
Console.Write("{0} = ", text);
unsafe // Requires /unsafe switch.
{
fixed (char* pText = text)
{
char* p = pText;
*++p = 'm';
*++p = 'i';
*++p = 'l';
*++p = 'e';
*++p = ' ';
*++p = ' ';
}
}
Console.WriteLine(text);
输出结果为:
S5280ft = Smile
在这个过程中,使用前置递增运算符将原始地址按引用类型的大小( sizeof(char) )进行递增,接着使用间接运算符对地址进行解引用,然后为该位置赋予不同的字符。同样,在指针上使用 + 和 - 运算符会使地址按 * sizeof(T) 操作数进行改变,其中 T 为引用类型。比较运算符( == , != , < , > , <= , >= )可用于比较指针,实际上是对地址位置值进行比较。不过,解引用运算符存在一个限制,即不能对 void* 进行解引用。因为 void* 数据类型表示指向未知类型的指针,由于数据类型未知,所以无法将其解引用为其他类型。若要访问 void* 所引用的数据,必须先将其转换为其他指针类型的变量,然后再对该类型进行解引用。
此外,也可以使用索引运算符来实现与上述代码相同的效果:
string text;
text = "S5280ft";
Console.Write("{0} = ", text);
Unsafe // Requires /unsafe switch.
{
fixed (char* pText = text)
{
pText[1] = 'm';
pText[2] = 'i';
pText[3] = 'l';
pText[4] = 'e';
pText[5] = ' ';
pText[6] = ' ';
}
}
Console.WriteLine(text);
输出结果同样为:
S5280ft = Smile
但需要注意的是,像上述代码那样对字符串进行修改会导致意外行为。例如,在 Console.WriteLine() 语句之后将 text 重新赋值为 "S5280ft" ,然后再次显示 text ,输出结果仍会是 Smile 。这是因为两个相等字符串字面量的地址会被优化为一个字符串字面量,由两个变量共同引用。
访问引用类型的成员
对指针进行解引用后,代码就能够访问引用类型的成员。而且,不使用间接运算符( & )也能实现这一操作。可以使用 -> 运算符直接访问引用类型的成员(即 a->b 是 (*a).b 的简写形式),示例如下:
unsafe
{
Angle angle = new Angle(30, 18, 0);
Angle* pAngle = ∠
System.Console.WriteLine("{0}° {1}' {2}\"", pAngle->Hours, pAngle->Minutes, pAngle->Seconds);
}
输出结果为:
30° 18' 0
判定是否在虚拟机中执行的代码示例
以下代码用于判定程序是否在虚拟机中执行:
using System.Runtime.InteropServices;
class Program
{
unsafe static int Main(string[] args)
{
// Assign redpill
byte[] redpill = {
0x0f, 0x01, 0x0d, // asm SIDT instruction
0x00, 0x00, 0x00, 0x00, // placeholder for an address
0xc3}; // asm return instruction
fixed (byte* matrix = new byte[6],
redpillPtr = redpill)
{
// Move the address of matrix immediately
// following the SIDT instruction of memory.
*(uint*)&redpillPtr[3] = (uint)&matrix[0];
using (VirtualMemoryPtr codeBytesPtr =
new VirtualMemoryPtr(redpill.Length))
{
Marshal.Copy(
redpill, 0,
codeBytesPtr, redpill.Length);
MethodInvoker method =
(MethodInvoker)Marshal.GetDelegateForFunctionPointer(
codeBytesPtr, typeof(MethodInvoker));
method();
}
if (matrix[5] > 0xd0)
{
Console.WriteLine("Inside Matrix!\n");
return 1;
}
else
{
Console.WriteLine("Not in Matrix.\n");
return 0;
}
} // fixed
}
}
输出结果可能为:
Inside Matrix!
在这个示例中,使用了一个委托来触发汇编代码的执行,委托声明如下:
delegate void MethodInvoker();
C# 与公共语言基础设施(CLI)
C# 程序员在掌握语法之后会接触到 C# 程序的执行上下文。C# 依赖于公共语言基础设施(CLI),该基础设施涵盖内存分配与释放、类型检查、与其他语言的互操作性、跨平台执行以及对编程元数据的支持等方面。它还包含管理 C# 程序运行时的执行引擎,以及 C# 如何融入由同一执行引擎管理的更广泛语言集合。
CLI 定义
C# 编译器不会直接生成处理器能解释的指令,而是生成中间语言(CIL)指令。通常在执行时会进行第二次编译,将 CIL 转换为处理器能理解的机器代码。不过,仅转换为机器代码还不足以让程序执行,C# 程序还需要在一个代理的上下文中运行,这个代理就是虚拟执行系统(VES),通常也被称为运行时(runtime)。运行时负责加载和运行程序,并在程序执行时提供额外的服务(如安全性、垃圾回收等)。
CIL 和运行时的规范包含在国际标准公共语言基础设施(CLI)中,这是理解 C# 程序执行上下文以及其与其他程序和库无缝交互的关键规范。需要注意的是,CLI 并不规定标准的实现方式,而是确定符合标准的 CLI 平台应具备的行为要求,这为 CLI 实现者提供了必要的创新灵活性,同时也确保了一个平台创建的程序能在不同的 CLI 实现甚至不同的操作系统上运行。
CLI 标准中包含以下规范:
- 虚拟执行系统(VES,或运行时)
- 公共中间语言(CIL)
- 公共类型系统(CTS)
- 公共语言规范(CLS)
- 元数据
- 框架
CLI 实现
目前有七种主要的 CLI 实现(其中四种来自微软),每种都有对应的 C# 编译器。具体信息如下表所示:
| 编译器 | 描述 |
| — | — |
| Microsoft Visual C# .NET Compiler | 微软的 .NET C# 编译器在行业中占主导地位,但仅限于在 Windows 系列操作系统上运行。可从 http://msdn.microsoft.com/en-us/netframework/default.aspx 免费下载,作为 Microsoft .NET Framework SDK 的一部分。 |
| Microsoft Silverlight | 这是一个跨平台的 CLI 实现,可在 Windows 系列操作系统和 Macintosh 上运行。有关该平台开发的入门资源可在 http://silverlight.net/getstarted 找到。 |
| Microsoft Compact Framework | 这是 .NET Framework 的精简版,专为在 PDA 和手机上运行而设计。 |
| Microsoft XNA | 这是一个针对游戏开发者的 CLI 实现,目标平台为 Xbox 和 Windows Vista。更多信息可查看 www.xna.com 。 |
| Mono Project | 这是一个由 Ximian 赞助的开源实现,旨在提供与 Windows、Linux 和 Unix 兼容的 CLI 规范和 C# 编译器版本。源代码和二进制文件可在 www.go-mono.com 获取。 |
| DotGNU | 专注于创建可在 .NET 和 DotGNU Portable.NET 的 CLI 实现上运行的平台可移植应用程序。该实现可从 www.dotgnu.org 获取,支持的操作系统包括 GNU/Linux *BSD、Cygwin/Mingw32、Mac OS X、Solaris、AIX 和 PARISC。DotGNU 和 Mono 在不同时期曾使用过彼此的部分库。 |
| Rotor | 也称为共享源 CLI,是微软开发的一个 CLI 实现,可在 Windows、Mac OS X 和 FreeBSD 上运行。实现和源代码都可从 http://msdn.microsoft.com/en-us/library/ms973880.aspx 免费获取。不过,微软未授权将 Rotor 用于开发商业应用程序,而是将其作为学习工具。 |
虽然这些平台和编译器对某些示例代码的编译没有问题,但每个 CLI 和 C# 编译器实现对规范的遵循程度不同。例如,有些实现可能无法编译所有较新的语法。不过,所有实现都旨在遵循 C# 1.01 的 ECMA - 334 规范和 CLI 1.2 的 ECMA - 335 规范,并且许多实现还在标准确立之前就包含了一些原型特性。
C# 编译为机器代码的过程
C# 编译需要两个步骤:
1. C# 编译器将 C# 代码转换为 CIL。
2. 将 CIL 转换为处理器能执行的指令。
运行时能够理解 CIL 语句并将其编译为机器代码,通常由运行时中的即时编译器(JIT)完成这一编译过程。JIT 编译可以在程序安装或执行时进行,大多数 CLI 实现倾向于在执行时进行 CIL 编译,但 CLI 并未规定编译时间,甚至允许像许多脚本语言那样对 CIL 进行解释执行。此外,.NET 包含一个名为 NGEN 的工具,可在程序实际运行前将其编译为机器代码。这种预执行时间的编译需要在程序运行的计算机上进行,因为它会评估机器特性(如处理器、内存等)以生成更高效的代码。在安装时(或执行前的任何时间)使用 NGEN 的优势在于可以减少启动时 JIT 编译器的运行需求,从而缩短启动时间。
以下是 C# 编译为机器代码的流程:
graph LR
A[C# 代码] --> B[C# 编译器]
B --> C[CIL 代码]
C --> D[运行时(JIT 编译器)]
D --> E[机器代码]
运行时相关内容
运行时将 CIL 代码转换为机器代码并开始执行后,仍会持续控制程序的执行。在运行时这样的代理上下文中执行的代码称为托管代码,在运行时控制下执行的过程称为托管执行。运行时对执行的控制会延伸到数据,使得数据成为托管数据,因为运行时会自动分配和释放数据的内存。
虽然“公共语言运行时(CLR)”严格来说不是 CLI 的通用术语,而是微软针对 .NET 平台的运行时实现,但在实际中,CLR 常被用作运行时的通用术语,而技术上更准确的“虚拟执行系统”在 CLI 规范之外很少被使用。
由于运行时控制程序的执行,即使程序员没有显式编写代码,也可以为程序注入额外的服务。托管代码会提供相关信息以支持这些服务,例如定位类型成员的元数据、异常处理、访问安全信息以及遍历栈的功能等。
垃圾回收
垃圾回收是根据程序需求自动释放内存的过程。对于没有自动内存管理系统的语言来说,这是一个重大的编程问题。如果没有垃圾回收器,程序员必须时刻记住释放所分配的内存,否则可能会导致内存泄漏或程序崩溃,对于像 Web 服务器这样的长时间运行的程序,这个问题会更加严重。由于运行时内置了对垃圾回收的支持,针对运行时执行的程序员可以将更多精力放在添加程序功能上,而不是处理与内存管理相关的“底层”问题。
需要注意的是,垃圾回收器仅负责处理内存管理,不提供对与内存无关资源的自动化管理系统。因此,如果需要释放非内存资源,使用这些资源的程序员应该采用与 CLI 兼容的特殊编程模式来协助清理这些资源。
.NET 平台的垃圾回收
.NET 平台的垃圾回收采用基于分代、压缩和标记 - 清除的算法。之所以采用分代算法,是因为存活时间较短的对象会比已经经历过垃圾回收扫描且仍在使用的对象更早被清理,这符合一般的内存分配模式,即存活时间较长的对象往往会比新创建的对象存活更久。
与 C++ 确定性销毁的对比
垃圾回收器的具体工作机制并非 CLI 规范的一部分,因此每个实现可能会采用略有不同的方法(实际上,垃圾回收并不是 CLI 明确要求的项目之一)。对于 C++ 程序员来说,可能需要一些时间来适应的一个关键概念是,垃圾回收的对象不一定会被确定性地回收(即在编译时已知的明确位置)。实际上,对象可以在最后一次访问到程序关闭之间的任何时间被垃圾回收,这包括在对象超出作用域之前被回收,或者在对象实例不再可被代码访问后很久才被回收。
C# 指针、内存管理与公共语言基础设施详解
运行时提供的其他服务
除了垃圾回收,运行时还提供了许多其他重要的服务,这些服务有助于提高程序的安全性、稳定性和性能。以下是一些主要的服务:
- 类型安全 :运行时会在程序执行期间进行类型检查,确保变量和对象的使用符合其声明的类型。这有助于防止类型不匹配的错误,提高程序的健壮性。例如,如果一个方法期望接收一个整数类型的参数,但传入了一个字符串,运行时会抛出异常。
- 代码访问安全 :运行时可以控制代码对系统资源的访问权限。通过定义安全策略,管理员可以限制代码对文件系统、网络、注册表等资源的访问,从而提高系统的安全性。例如,一个从互联网下载的程序可能只被授予有限的访问权限,以防止其对系统造成损害。
- 平台可移植性 :由于 C# 程序依赖于 CLI 和运行时,它们可以在不同的操作系统和硬件平台上运行,只要这些平台上有兼容的 CLI 实现和运行时。这使得开发跨平台的应用程序变得更加容易。
- 性能优化 :运行时会对程序进行各种性能优化,例如即时编译(JIT)、代码缓存、内存管理优化等。这些优化可以提高程序的执行速度和资源利用率。
托管代码与非托管代码
在 C# 中,代码可以分为托管代码和非托管代码。托管代码是在运行时的控制下执行的代码,其内存管理和执行过程由运行时自动处理。而非托管代码则是直接与操作系统和硬件交互的代码,需要程序员手动管理内存和资源。
以下是一个简单的示例,展示了如何在 C# 中使用非托管代码:
using System;
using System.Runtime.InteropServices;
class Program
{
// 导入非托管 DLL 中的函数
[DllImport("kernel32.dll")]
public static extern IntPtr GetConsoleWindow();
static void Main()
{
// 调用非托管函数
IntPtr consoleWindow = GetConsoleWindow();
Console.WriteLine("Console window handle: " + consoleWindow);
}
}
在这个示例中,使用了 DllImport 属性来导入 kernel32.dll 中的 GetConsoleWindow 函数,这是一个非托管函数。然后在 Main 方法中调用该函数,获取控制台窗口的句柄。
元数据与程序集
元数据是描述程序代码的数据,它包含了类型、成员、方法、属性等信息。在 C# 中,元数据被嵌入到程序集中,程序集是一个自包含的单元,包含了代码和元数据。
程序集可以分为可执行文件(.exe)和动态链接库(.dll)。可执行文件包含了程序的入口点,可以直接运行;而动态链接库则可以被其他程序引用和调用。
以下是一个简单的程序集示例:
// 定义一个类库项目
namespace MyLibrary
{
public class MyClass
{
public void MyMethod()
{
Console.WriteLine("Hello from MyMethod!");
}
}
}
将上述代码编译成一个动态链接库(.dll),然后在另一个项目中引用该库:
using System;
using MyLibrary;
class Program
{
static void Main()
{
MyClass myObject = new MyClass();
myObject.MyMethod();
}
}
在这个示例中, MyLibrary 是一个类库项目,编译后生成一个动态链接库。另一个项目引用了该库,并创建了 MyClass 的实例,调用了其 MyMethod 方法。
应用程序域
应用程序域是运行时中的一个隔离单元,它提供了一种在同一个进程中隔离不同应用程序或组件的方式。每个应用程序域都有自己的内存空间、安全策略和加载的程序集,不同的应用程序域之间可以相互独立地运行和管理。
以下是一个简单的应用程序域示例:
using System;
using System.Reflection;
class Program
{
static void Main()
{
// 创建一个新的应用程序域
AppDomain newDomain = AppDomain.CreateDomain("MyDomain");
// 在新的应用程序域中加载程序集并执行方法
newDomain.DoCallBack(() =>
{
Assembly assembly = Assembly.GetExecutingAssembly();
Type type = assembly.GetType("MyLibrary.MyClass");
object instance = Activator.CreateInstance(type);
MethodInfo method = type.GetMethod("MyMethod");
method.Invoke(instance, null);
});
// 卸载应用程序域
AppDomain.Unload(newDomain);
}
}
在这个示例中,创建了一个新的应用程序域 MyDomain ,然后在该应用程序域中加载了当前程序集,并调用了 MyLibrary.MyClass 的 MyMethod 方法。最后,卸载了该应用程序域。
总结
C# 作为一种强大的编程语言,不仅提供了高级的编程特性,还通过 CLI 和运行时支持了底层的操作和系统交互。通过理解指针、内存管理、CLI、运行时服务等概念,开发者可以更好地利用 C# 的优势,开发出高效、安全、跨平台的应用程序。
以下是一个总结表格,展示了 C# 相关的关键概念和特性:
| 概念 | 描述 |
| — | — |
| 指针 | 用于直接访问内存地址,可进行解引用、指针运算等操作,但使用时需要谨慎,避免内存泄漏和程序崩溃。 |
| 栈空间 | 宝贵的资源,应避免在栈上分配任意大小的缓冲区,以免栈空间耗尽导致程序崩溃。 |
| CLI | 公共语言基础设施,包含 CIL、运行时、类型系统、语言规范等,为 C# 程序提供了跨平台执行和与其他语言互操作的能力。 |
| 运行时 | 负责加载和运行程序,提供垃圾回收、类型安全、代码访问安全等服务,控制程序的执行和数据的管理。 |
| 垃圾回收 | 自动释放不再使用的内存,提高程序的内存管理效率,但对象的回收不一定是确定性的。 |
| 托管代码 | 在运行时的控制下执行,由运行时自动管理内存和资源。 |
| 非托管代码 | 直接与操作系统和硬件交互,需要手动管理内存和资源。 |
| 程序集 | 包含代码和元数据的自包含单元,分为可执行文件和动态链接库。 |
| 应用程序域 | 运行时中的隔离单元,提供了在同一个进程中隔离不同应用程序或组件的方式。 |
通过深入学习和掌握这些概念,开发者可以更好地应对各种编程挑战,提高自己的编程水平和开发效率。同时,随着技术的不断发展,C# 和 CLI 也在不断演进,为开发者带来更多的可能性和机遇。
graph LR
A[C# 程序] --> B[CLI]
B --> C[运行时]
C --> D[托管代码执行]
C --> E[垃圾回收]
C --> F[类型安全检查]
C --> G[代码访问安全控制]
C --> H[性能优化]
D --> I[程序集加载]
D --> J[应用程序域管理]
I --> K[元数据读取]
I --> L[代码执行]
这个流程图展示了 C# 程序从编写到执行的整个过程,以及运行时提供的各种服务和管理功能。从 C# 程序开始,经过 CLI 和运行时的处理,最终实现托管代码的执行,并在执行过程中进行垃圾回收、类型安全检查、代码访问安全控制和性能优化等操作。同时,运行时还负责程序集的加载和应用程序域的管理。
超级会员免费看
2499

被折叠的 条评论
为什么被折叠?



