深入探索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)
{
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)
{
*(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;
}
}
}
}
delegate void MethodInvoker();
运行结果为:
Inside Matrix!
公共语言基础结构(CLI)概述
C#编译器不会直接生成处理器可解释的指令,而是生成中间语言(CIL)指令。通常在执行时会进行第二次编译,将CIL转换为处理器能理解的机器代码。但仅转换为机器代码还不足以执行代码,C#程序需要在一个代理的上下文中执行,这个代理就是虚拟执行系统(VES),通常简称为运行时。运行时负责加载和运行程序,并在程序执行时提供额外服务,如安全、垃圾回收等。
CIL和运行时的规范包含在国际标准公共语言基础结构(CLI)中。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 | 这是一个针对Xbox和Windows Vista的游戏开发者的CLI实现。更多信息可查看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,是微软开发的一个在Windows、Mac OS X和FreeBSD上运行的CLI实现。实现和源代码可从http://msdn.microsoft.com/en-us/library/ms973880.aspx 免费获取。不过,微软未授权将Rotor用于开发商业应用程序,而是将其作为学习工具。 |
这些CLI和C#编译器实现对规范的遵循程度不同,有些可能无法编译较新的语法。但所有实现都旨在遵循C# 1.01的ECMA - 334规范和CLI 1.2.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[运行时]
D --> E[机器代码]
运行时相关特性
运行时将CIL代码转换为机器代码并开始执行后,仍会持续控制程序的执行。在运行时上下文下执行的代码称为托管代码,这种执行过程称为托管执行。运行时对执行的控制会延伸到数据,数据的内存由运行时自动分配和释放,因此称为托管数据。
“公共语言运行时(CLR)”严格来说不是CLI的通用术语,而是微软针对.NET平台的运行时实现。不过,CLR通常被用作运行时的通用术语,而技术上更准确的“虚拟执行系统”在CLI规范之外很少使用。
由于运行时控制程序执行,即使程序员没有明确编写代码,也可以为程序注入额外服务。托管代码提供了相关信息以支持这些服务的附加,例如定位类型成员的元数据、异常处理、访问安全信息以及遍历栈的功能等。以下是运行时和托管执行提供的一些附加服务:
垃圾回收
垃圾回收是根据程序需求自动释放内存的过程。对于没有自动内存管理系统的语言来说,这是一个重要的编程难题。没有垃圾回收器时,程序员必须记得手动释放所有分配的内存,否则可能会导致内存泄漏或程序崩溃,尤其是对于像Web服务器这样的长时间运行的程序。由于运行时内置了垃圾回收支持,针对运行时执行的程序员可以专注于添加程序功能,而不是处理内存管理相关的“底层”问题。
需要注意的是,垃圾回收器仅负责内存管理,不提供管理非内存资源的自动化系统。因此,如果需要释放非内存资源,使用这些资源的程序员应采用特殊的CLI兼容编程模式来帮助清理这些资源。
.NET平台的垃圾回收实现采用了基于分代、压缩和标记清除的算法。分代的原因是,存活时间较短的对象会比已经经历过多次垃圾回收扫描且仍在使用的对象更早被清理,这符合内存分配的一般模式,即存活时间较长的对象往往会比新创建的对象存活更久。
与C++不同,C#中垃圾回收的对象不一定会在确定的位置(如编译时已知的位置)被回收。实际上,对象可以在最后一次访问到程序关闭之间的任何时间被垃圾回收,包括在对象超出作用域之前或在对象实例不再可访问很久之后。
深入探索C#指针、地址与公共语言基础结构
垃圾回收机制的详细剖析
垃圾回收机制在C#编程中扮演着至关重要的角色,它极大地提升了编程效率,让开发者能够将更多的精力放在业务逻辑的实现上。下面详细剖析一下垃圾回收机制的工作原理和特点。
垃圾回收器采用了分代回收的策略,将对象分为不同的代(Generation)。通常分为三代:第0代、第1代和第2代。新创建的对象会被分配到第0代,当第0代的内存空间达到一定阈值时,会触发一次小规模的垃圾回收,主要清理第0代中不再使用的对象。如果某些对象在这次回收中存活下来,它们会被提升到第1代。当第1代的内存空间也达到阈值时,会进行一次范围更广的回收,包括第0代和第1代。同样,存活下来的对象会被提升到第2代。第2代的对象通常是一些长期存活的对象,垃圾回收的频率相对较低。
这种分代回收的策略基于一个经验法则:大多数对象的生命周期都很短。通过优先清理第0代的对象,可以快速释放大量的内存空间,减少垃圾回收的开销。
以下是一个简单的示例,展示了对象在不同代之间的提升过程:
using System;
class Program
{
static void Main()
{
// 创建一些第0代对象
object obj1 = new object();
object obj2 = new object();
// 触发一次垃圾回收
GC.Collect(0);
// 此时obj1和obj2如果存活,会被提升到第1代
// 创建更多第0代对象
object obj3 = new object();
object obj4 = new object();
// 触发一次第1代的垃圾回收
GC.Collect(1);
// 此时存活的对象会被提升到第2代
Console.WriteLine("垃圾回收演示完成");
}
}
在这个示例中,我们手动触发了不同代的垃圾回收,模拟了对象在不同代之间的提升过程。
运行时的其他重要特性
类型安全
运行时提供了强大的类型安全保障。在C#中,所有的变量和对象都有明确的类型,编译器会在编译时进行类型检查,确保代码中不会出现类型不匹配的错误。例如,不能将一个整数类型的变量赋值给一个字符串类型的变量,否则编译器会报错。
在运行时,即使代码通过了编译,运行时也会继续进行类型安全检查。例如,在进行方法调用时,会检查传递的参数类型是否与方法定义的参数类型一致。如果不一致,会抛出
InvalidCastException
或其他相关异常,避免程序出现不可预期的错误。
以下是一个类型安全检查的示例:
class Animal
{
public void Eat()
{
Console.WriteLine("动物正在进食");
}
}
class Dog : Animal
{
public void Bark()
{
Console.WriteLine("狗在汪汪叫");
}
}
class Program
{
static void Main()
{
Animal animal = new Dog();
animal.Eat();
// 尝试将Animal类型的对象转换为Dog类型
Dog dog = animal as Dog;
if (dog != null)
{
dog.Bark();
}
else
{
Console.WriteLine("转换失败");
}
}
}
在这个示例中,我们定义了一个
Animal
类和一个
Dog
类,
Dog
类继承自
Animal
类。在
Main
方法中,我们创建了一个
Dog
对象并将其赋值给一个
Animal
类型的变量。然后尝试将
Animal
类型的对象转换为
Dog
类型,使用
as
运算符进行安全转换。如果转换成功,就可以调用
Dog
类特有的
Bark
方法;如果转换失败,会输出相应的提示信息。
代码访问安全
代码访问安全(Code Access Security,CAS)是运行时提供的一项重要安全特性。它允许程序控制对系统资源的访问权限,确保代码只能访问其被授权访问的资源。
在C#中,可以通过使用安全策略和权限集来实现代码访问安全。例如,可以限制代码对文件系统、网络资源或注册表的访问权限。当代码尝试访问受保护的资源时,运行时会检查代码是否具有相应的权限,如果没有权限,会抛出
SecurityException
异常。
以下是一个简单的代码访问安全示例:
using System;
using System.Security.Permissions;
class Program
{
[FileIOPermission(SecurityAction.Demand, Read = @"C:\temp\test.txt")]
static void ReadFile()
{
try
{
string content = System.IO.File.ReadAllText(@"C:\temp\test.txt");
Console.WriteLine(content);
}
catch (Exception ex)
{
Console.WriteLine("读取文件时出错: " + ex.Message);
}
}
static void Main()
{
try
{
ReadFile();
}
catch (SecurityException ex)
{
Console.WriteLine("没有权限读取文件: " + ex.Message);
}
}
}
在这个示例中,我们使用
FileIOPermission
特性来要求调用
ReadFile
方法的代码必须具有读取指定文件的权限。如果没有权限,会抛出
SecurityException
异常。
平台可移植性
C#和CLI的设计使得程序具有良好的平台可移植性。由于C#编译器生成的是中间语言(CIL)代码,而不是特定平台的机器代码,因此可以在不同的操作系统和硬件平台上运行。只要目标平台上有兼容的CLI实现和运行时环境,C#程序就可以顺利执行。
例如,使用Mono Project实现的CLI,可以在Windows、Linux和Unix等多种操作系统上运行C#程序。这为开发者提供了更大的灵活性,可以将应用程序部署到不同的平台上,而无需对代码进行大量的修改。
性能优化
运行时还提供了一些性能优化的机制。例如,即时(JIT)编译器会在程序运行时将CIL代码编译为机器代码,并且会根据程序的运行情况进行优化。JIT编译器会分析代码的执行模式,对热点代码进行优化,提高程序的执行效率。
另外,垃圾回收器的分代回收策略也有助于提高性能。通过优先清理短期存活的对象,可以减少内存碎片,提高内存的利用率。
CLI的组件
CLI包含多个重要的组件,这些组件共同构成了C#程序运行的基础环境。
元数据
元数据是描述代码的数据,它包含了类型、方法、字段等信息。在C#中,元数据被嵌入到编译后的程序集中,运行时可以通过元数据来了解代码的结构和功能。例如,反射机制就是基于元数据实现的,它允许程序在运行时动态地获取类型信息、调用方法和访问字段。
以下是一个简单的反射示例:
using System;
using System.Reflection;
class Program
{
static void Main()
{
Type type = typeof(DateTime);
MethodInfo[] methods = type.GetMethods();
foreach (MethodInfo method in methods)
{
Console.WriteLine(method.Name);
}
}
}
在这个示例中,我们使用反射机制获取
DateTime
类型的所有方法,并输出它们的名称。
应用程序域
应用程序域(Application Domain)是一种隔离机制,它允许在一个进程中运行多个相互隔离的应用程序。每个应用程序域都有自己的内存空间、加载的程序集和安全策略,一个应用程序域中的错误不会影响其他应用程序域的运行。
以下是一个创建和使用应用程序域的示例:
using System;
using System.Reflection;
class Program
{
static void Main()
{
// 创建一个新的应用程序域
AppDomain newDomain = AppDomain.CreateDomain("NewDomain");
// 在新的应用程序域中执行代码
newDomain.DoCallBack(() =>
{
Console.WriteLine("在新的应用程序域中执行");
});
// 卸载应用程序域
AppDomain.Unload(newDomain);
}
}
在这个示例中,我们创建了一个新的应用程序域,并在其中执行了一段代码,最后卸载了该应用程序域。
程序集和清单
程序集是.NET中的一个重要概念,它是一个自描述的单元,包含了代码、元数据和资源。程序集可以是可执行文件(.exe)或动态链接库(.dll)。
清单是程序集的一部分,它包含了程序集的元数据信息,如程序集的名称、版本、依赖项等。运行时会根据清单来加载和管理程序集。
模块
模块是程序集的组成部分,一个程序集可以包含多个模块。模块可以包含类型定义、资源和其他代码元素。模块的使用可以提高代码的组织性和可维护性。
总结
C#作为一种强大的编程语言,通过指针和地址操作可以实现底层的内存管理,同时借助公共语言基础结构(CLI)提供的丰富功能,如运行时服务、垃圾回收、类型安全等,使得开发者可以专注于业务逻辑的实现,提高开发效率和程序的可靠性。CLI的多种实现和组件为C#程序的跨平台运行和性能优化提供了有力支持。无论是开发桌面应用、Web应用还是游戏,C#和CLI都能提供一个强大而灵活的开发环境。
通过深入了解C#指针、地址与公共语言基础结构,开发者可以更好地掌握C#编程的核心技术,编写出高质量、高性能的应用程序。
超级会员免费看
3万+

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



