55、.NET 内存管理与非安全代码深度解析

.NET 内存管理与非安全代码深度解析

1. .NET 中的 GC 类

在 .NET 开发中,开发者可借助 GC 类作为垃圾回收器(Garbage Collector,简称 GC)的接口。利用该类,能够对垃圾回收操作进行操控。不过,通常情况下,垃圾回收在不受开发者干预时能更好地运行。以下是 GC 类的静态成员列表:
| 成员名称 | 描述 |
| — | — |
| MaxGeneration | 获取系统当前支持的最大代数。 |
| AddMemoryPressure | 强制 GC 识别非托管资源的内存分配。 |
| Collect | 对指定代数执行垃圾回收。有两种重载形式:无参数时对所有代数进行回收;带一个参数时指定要回收的最老代数。 |
| CollectionCount | 返回指定代数的垃圾回收周期数。 |
| GetGeneration | 返回指定对象的代数。 |
| GetTotalMemory | 返回当前为托管堆分配的总字节数。布尔参数设为 true 时,该方法在系统执行垃圾回收和对象终结操作时,会等待一小段时间再返回结果。 |
| KeepAlive | 引用指定对象,使其在当前例程开始到调用该方法的位置期间不被垃圾回收。 |
| RemoveMemoryPressure | 移除为非托管资源预留的部分内存压力。 |
| ReRegisterForFinalize | 为复活的对象重新关联终结器。 |
| SuppressFinalize | 取消指定对象的未来终结操作。 |
| WaitForPendingFinalizers | 挂起线程,直到终结队列清空。 |

2. 非安全代码概述

非安全代码能够访问超出公共语言运行时(Common Language Runtime,简称 CLR)控制范围的非托管内存,而安全代码只能访问由 CLR 在 GC 支持下管理的托管堆。使用托管堆的代码本质上更安全,因为 CLR 会自动释放未使用的对象,进行类型检查和其他内存管理操作,让开发者能更专注于应用程序开发,间接提高用户的生产力和满意度。

在非安全代码中,会使用指向非托管内存的指针。这些指针与非托管内存一样,不受 CLR 管理,指向非托管内存中的固定位置,而引用类型指向托管内存中的可移动位置。CLR 管理引用类型,开发者无需手动释放引用类型分配的内存。在 C 和 C++ 应用开发中,开发者需特别关注内存管理操作,但不当的指针管理常导致非托管环境中的非安全代码问题,如内存泄漏、无效内存访问、指针删除错误和边界错误等。将指针管理和操作封装在引用类型中,使托管代码更加安全。不过,在特定情况下,仍有必要编写非安全代码,以下是一些适用场景:
- 非托管代码通常严重依赖指针,在向 C# 代码迁移时,非安全代码是一种可选方案。
- 实现某些软件算法时,根据设计需要使用指针,可能需要非安全代码。
- 调用非托管函数时,可能需要函数指针。
- 处理二进制内存中的数据结构时,指针可能是必要的。
- 在某些情况下,非托管指针可提高代码的性能和效率。

3. 非安全代码的调用与互操作性

有时开发者需要在托管应用中调用非托管代码。尽管 .NET Framework 的类库(FCL)几乎涵盖了开发 .NET 应用所需的所有代码,但仍可能需要调用操作系统库中的函数(API)以实现 FCL 未提供的功能。此外,一些专有软件或商业软件可能没有托管代码版本。同样,也可以在非托管模块中调用托管代码,如回调。

平台调用服务(PInvoke)在托管执行和非托管执行之间搭建了双向桥梁。在托管代码中,PInvoke 负责定位、加载和执行非托管模块中的函数。大多数跨平台调用由互操作性封送器进行封送处理,它负责在非托管和托管环境的可识别格式之间转换参数和返回类型。幸运的是,并非所有类型都需要转换,避免了不必要的开销。

还可以在托管代码和包含非托管代码的 COM 组件之间建立桥梁。运行时可调用包装器(Runtime Callable Wrapper,简称 RCW)简化了在托管代码中调用 COM 组件的过程,COM 可调用包装器(COM Callable Wrapper,简称 CCW)使托管组件看起来像 COM 组件,从而间接让 COM 客户端可以访问托管组件。COM 组件也可通过 PInvoke 访问,但 CCW 和 RCW 更实用,在大多数场景中是推荐的解决方案。

4. 非安全代码的限制与编译选项

非安全代码无法获得批准,因为它不受代码访问安全的约束,不进行类型检查以防止缓冲区溢出攻击等操作,代码可靠性难以确定。因此,在托管代码中调用非安全代码需要非常高的权限。包含非安全代码的托管应用必须使用 /unsafe 选项进行编译。在 Microsoft Visual Studio 2005 中,可通过以下步骤设置:
1. 从解决方案资源管理器中打开项目的上下文菜单,选择“属性”。
2. 在“生成”选项卡中,勾选“允许使用不安全代码”选项。

5. “unsafe”关键字的使用

“unsafe”关键字用于界定非安全代码的上下文,避免意外插入非安全代码。处于该上下文中的代码可以声明和使用非托管指针。该关键字可应用于类、结构体、接口或委托,当应用于某个类型时,该类型的所有成员都被视为非安全的,也可应用于类型的特定成员。例如:

public struct ZStruct {
    public unsafe int *fielda;
    public unsafe double *fieldb;
}

public unsafe struct ZStruct {
    public int *fielda;
    public double *fieldb;
}

也可以通过“unsafe”语句创建非安全代码块,块内的所有代码都将在非安全上下文中执行。示例如下:

public static void Main(){ 
    int number = 296; 
    byte b; 
    unsafe { 
        b = MethodA(&number); 
    } 
    Console.WriteLine(b); 
} 

public unsafe static byte MethodA(int *pI) { 
    byte *temp = (byte*) pI; 
    return *temp; 
}

基类的非安全特性不会被派生类继承,除非派生类显式指定为“unsafe”,否则它是安全的。在非安全上下文中,派生类可以使用基类中可访问的非安全成员。例如:

public unsafe class ZClass { 
    protected int *fielda; 
} 

public class YClass: ZClass { 
    protected int *fieldb; // 编译错误 
}

要解决上述编译错误,需在 fieldb 前显式添加“unsafe”关键字。

6. 指针的使用

非安全代码主要涉及直接访问指针,指针指向内存中的固定位置,可用于传统的指针操作,如间接寻址、指针计算等。指针不受 GC 控制,必要时开发者需自行管理其生命周期。

C# 不会自动暴露指针,需要在非安全上下文中才能使用。在该语言中,指针通常通过引用进行操作,引用将指针转换为托管堆内存中的引用,该引用和关联的内存由 GC 管理且可能会移动,因此引用不能直接用于操作指针。

指针的声明语法如下:

typenonmanagé* identificateur;
typenonmanagé* identificateur = initialiseur;

可以在同一语句中声明多个指针,用分号分隔。注意,其语法与 C 或 C++ 略有不同,例如:

int *pA, pB, pC; // C++: int *pA, *pB, *pC;

非托管类型(托管类型的子集)包括 sbyte、byte、short、ushort、int、uint、long、ulong、char、float、double、decimal、bool 和 enum 等,像 string 等某些托管类型不在此列。可以为用户定义的结构体创建指针,前提是结构体的所有字段都是非托管类型。指针类型不继承自 System.Object ,因此不能与 System.Object 进行转换。

空指针(void 指针)是允许的,但非常危险。它是无类型指针,可模拟任何其他类型的指针,任何类型的指针都可隐式转换为空指针,这种不可预测性使其极不安全。除空指针外,指针在类型层面有不同程度的安全性。虽然不同类型的指针之间不能进行隐式转换,但大多数情况下允许显式转换。例如:

int val = 5; 
float* pA = &val; // 编译错误

可以使用值的地址或另一个指针来初始化指针,示例如下:

public unsafe static void Main() { 
    int ival = 5; 
    int *p1 = &ival; // 间接寻址 
    int *p2 = p1; // 指针赋值
}

以下是指针使用中各符号的含义:
| 符号 | 描述 |
| — | — |
| 指针声明 ( ) | 用于声明新的指针变量,如 int *pA; |
| 间接寻址 (
) | 对指针进行间接寻址操作,返回指针所指内存地址存储的值。例如: int val = 5; int *pA = &val; Console.WriteLine(*pA); 会输出 5。但不能对空指针进行间接寻址。 |
| 取地址 (&) | 返回变量的内存地址,用于初始化指针,如 int *pA = &val; |
| 成员访问 (->) | 对位于内存位置的类型成员进行间接寻址。例如,对于结构体 ZStruct 和其成员 fielda ,可使用 ZStruct obj = new ZStruct(5); ZStruct *pObj = &obj; int val1 = pObj->fielda; 访问成员,也可用 int val2 = (*pObj).fielda; 实现相同功能。 |
| 指针元素 ([]) | 表示从指针内存地址开始的偏移量。例如, p[2] 表示偏移量为 2,偏移量根据指针类型的大小递增。若 p int 类型指针, p[2] 表示偏移 8 个字节。假设 ZStruct 包含两个连续的 int 字段 fielda fieldb ,可通过 ZStruct obj = new ZStruct(5); int *pA = &obj.fielda; Console.WriteLine(pA[1]); 访问 fieldb 。 |
| 指针的指针 (**) | 指向包含另一个指针地址的内存位置。可以使用双星号 ** 对其进行间接寻址,也可分多次使用单星号进行操作。例如:

int val = 5; 
int *pA = &val; 
int **ppA = &pA; 
Console.WriteLine((int)ppA); // 输出 ppA 存储的地址(即 pA 的地址)
Console.WriteLine((int)*ppA); // 输出 pA 存储的地址
Console.WriteLine((int)**ppA); // 输出 pA 所指地址存储的值(即 5)
``` |

综上所述,在 .NET 开发中,合理运用 GC 类和谨慎使用非安全代码及指针,能帮助开发者更好地管理内存和实现特定功能,但同时也需要充分了解其潜在风险并采取相应的防范措施。

#### 7. 指针操作示例与流程分析

为了更清晰地理解指针的操作,下面通过一个更复杂的示例来详细说明。假设我们要实现一个简单的数组反转功能,使用指针来完成这个任务。以下是具体的代码实现:

```csharp
using System;

class Program
{
    public unsafe static void ReverseArray(int* arr, int length)
    {
        int* left = arr;
        int* right = arr + length - 1;

        while (left < right)
        {
            int temp = *left;
            *left = *right;
            *right = temp;

            left++;
            right--;
        }
    }

    public unsafe static void Main()
    {
        int[] array = { 1, 2, 3, 4, 5 };
        fixed (int* arrPtr = array)
        {
            ReverseArray(arrPtr, array.Length);
        }

        foreach (int num in array)
        {
            Console.Write(num + " ");
        }
        Console.WriteLine();
    }
}

下面是这个示例的流程分析:
1. 定义反转函数 ReverseArray 函数接受一个 int 类型的指针 arr 和数组的长度 length 作为参数。
- 初始化两个指针 left right ,分别指向数组的起始位置和末尾位置。
- 使用 while 循环,只要 left 指针小于 right 指针,就交换它们所指向的值。
- 每次交换后, left 指针向后移动一位, right 指针向前移动一位。
2. 主函数中调用
- 创建一个整数数组 array
- 使用 fixed 语句固定数组,获取数组的指针 arrPtr
- 调用 ReverseArray 函数,传入数组指针和数组长度。
3. 输出结果 :使用 foreach 循环遍历反转后的数组,并输出每个元素。

这个示例展示了如何使用指针进行数组操作,同时也体现了指针在处理内存时的灵活性。

8. 非安全代码的性能考量

在某些情况下,非安全代码可以显著提高性能,但这并不意味着可以随意使用。下面是使用非安全代码提高性能的一些场景和注意事项:

场景
- 内存密集型操作 :当需要处理大量数据时,使用非安全代码可以减少内存分配和垃圾回收的开销。例如,在图像处理、数据压缩等领域,直接操作内存可以提高处理速度。
- 底层系统交互 :与操作系统或硬件进行交互时,非安全代码可以直接访问底层资源,避免了托管代码的中间层开销。

注意事项
- 代码复杂度 :非安全代码通常比安全代码更复杂,容易引入错误。在编写非安全代码时,需要仔细考虑边界条件和内存管理,以避免出现内存泄漏、访问越界等问题。
- 可维护性 :非安全代码的可读性和可维护性较差,对于团队开发来说,可能会增加开发和维护的难度。因此,在使用非安全代码时,需要权衡性能提升和可维护性之间的关系。

9. 非安全代码的调试与测试

由于非安全代码的特殊性,调试和测试非安全代码需要额外的注意。以下是一些调试和测试非安全代码的建议:

调试
- 使用调试工具 :利用 Visual Studio 等开发工具的调试功能,设置断点、查看变量值和内存状态。在调试非安全代码时,需要特别注意指针的指向和内存的使用情况。
- 检查边界条件 :非安全代码容易出现访问越界等问题,因此在调试时需要仔细检查边界条件,确保指针不会超出合法范围。

测试
- 单元测试 :编写单元测试用例,对非安全代码的各个功能进行测试。在测试时,需要考虑各种可能的输入和边界情况,确保代码的正确性。
- 压力测试 :进行压力测试,模拟大量数据和高并发场景,检查非安全代码在极端情况下的性能和稳定性。

10. 总结

非安全代码在 .NET 开发中是一把双刃剑,它可以提供对非托管内存的直接访问,从而实现一些特定的功能和提高性能,但同时也带来了安全和维护方面的挑战。在使用非安全代码时,需要遵循以下原则:

  • 谨慎使用 :非安全代码应该作为最后的手段,只有在确实需要时才使用。在大多数情况下,尽量使用安全代码来完成开发任务。
  • 充分测试 :在使用非安全代码之前,需要进行充分的测试和调试,确保代码的正确性和稳定性。
  • 遵循规范 :编写非安全代码时,需要遵循相关的规范和最佳实践,避免出现常见的错误和安全隐患。

通过合理运用非安全代码和指针,开发者可以在保证代码安全和可维护性的前提下,实现更高效的内存管理和更强大的功能。同时,不断学习和掌握非安全代码的使用技巧,也有助于提升开发者的技术水平和解决问题的能力。

以下是一个总结非安全代码使用要点的表格:
| 要点 | 说明 |
| — | — |
| 使用场景 | 非托管代码迁移、特定算法实现、底层系统交互等 |
| 性能考量 | 提高内存密集型操作和底层交互性能,但增加代码复杂度和降低可维护性 |
| 调试测试 | 使用调试工具,检查边界条件;进行单元测试和压力测试 |
| 使用原则 | 谨慎使用,充分测试,遵循规范 |

通过以上内容,我们对 .NET 中的内存管理、非安全代码和指针的使用有了更深入的了解。在实际开发中,需要根据具体需求和场景,合理选择使用安全代码和非安全代码,以达到最佳的开发效果。

已经博主授权,源码转载自 https://pan.quark.cn/s/053f1da40351 在计算机科学领域,MIPS(Microprocessor without Interlocked Pipeline Stages)被视作一种精简指令集计算机(RISC)的架构,其应用广泛存在于教学实践和嵌入式系统设计中。 本篇内容将深入阐释MIPS汇编语言中涉及数组处理的核心概念与实用操作技巧。 数组作为一种常见的数据结构,在编程中能够以有序化的形式储存及访问具有相同类型的数据元素集合。 在MIPS汇编语言环境下,数组通常借助内存地址与索引进行操作。 以下列举了运用MIPS汇编处理数组的关键要素:1. **数据存储**: - MIPS汇编架构采用32位地址系统,从而能够访问高达4GB的内存容量。 - 数组元素一般以连续方式存放在内存之中,且每个元素占据固定大小的字节空间。 例如,针对32位的整型数组,其每个元素将占用4字节的存储空间。 - 数组首元素的地址被称为基地址,而数组任一元素的地址可通过基地址加上元素索引乘以元素尺寸的方式计算得出。 2. **寄存器运用**: - MIPS汇编系统配备了32个通用寄存器,包括$zero, $t0, $s0等。 其中,$zero寄存器通常用于表示恒定的零值,$t0-$t9寄存器用于暂存临时数据,而$s0-$s7寄存器则用于保存子程序的静态变量或参数。 - 在数组处理过程中,基地址常被保存在$s0或$s1寄存器内,索引则存储在$t0或$t1寄存器中,运算结果通常保存在$v0或$v1寄存器。 3. **数组操作指令**: - **Load/Store指令**:这些指令用于在内存与寄存器之间进行数据传输,例如`lw`指令用于加载32位数据至寄存器,`sw`指令...
根据原作 https://pan.quark.cn/s/cb681ec34bd2 的源码改编 基于Python编程语言完成的飞机大战项目,作为一项期末学习任务,主要呈现了游戏开发的基本概念和技术方法。 该项目整体构成约500行代码,涵盖了游戏的核心运作机制、图形用户界面以及用户互动等关键构成部分。 该项目配套提供了完整的源代码文件、相关技术文档、项目介绍演示文稿以及运行效果展示视频,为学习者构建了一个实用的参考范例,有助于加深对Python在游戏开发领域实际应用的认识。 我们进一步研究Python编程技术在游戏开发中的具体运用。 Python作为一门高级编程语言,因其语法结构清晰易懂和拥有丰富的库函数支持,在开发者群体中获得了广泛的认可和使用。 在游戏开发过程中,Python经常与Pygame库协同工作,Pygame是Python语言下的一款开源工具包,它提供了构建2D游戏所需的基础功能模块,包括窗口系统管理、事件响应机制、图形渲染处理、音频播放控制等。 在"飞机大战"这一具体游戏实例中,开发者可能运用了以下核心知识点:1. **Pygame基础操作**:掌握如何初始化Pygame环境,设定窗口显示尺寸,加载图像和音频资源,以及如何启动和结束游戏的主循环流程。 2. **面向对象编程**:游戏中的飞机、子弹、敌人等游戏元素通常通过类的设计来实现,利用实例化机制来生成具体的游戏对象。 每个类都定义了自身的属性(例如位置坐标、移动速度、生命值状态)和方法(比如移动行为、碰撞响应、状态更新)。 3. **事件响应机制**:Pygame能够捕获键盘输入和鼠标操作事件,使得玩家可以通过按键指令来控制飞机的移动和射击行为。 游戏会根据这些事件的发生来实时更新游戏场景状态。 4. **图形显示与刷新**:...
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值