首页 > 系统相关 >从C++看C#托管内存与非托管内存

从C++看C#托管内存与非托管内存

时间:2024-07-31 22:41:23浏览次数:14  
标签:IntPtr C# 托管 C++ 内存 Student new

进程的内存

一个exe文件,在没有运行时,其磁盘存储空间格式为函数代码段+全局变量段。加载为内存后,其进程内存模式增加为函数代码段+全局变量段+函数调用栈+堆区。我们重点讨论堆区。

进程内存
函数代码段
全局变量段
函数调用栈
堆区

托管堆与非托管堆

  • C#
    int a=10这种代码申请的内存空间位于函数调用栈区

    var stu=new Student();
    GC.Collect();
    

    new运算符申请的内存空间位于堆区。关键在于new关键字。在C#中,这个关键字是向CLR虚拟机申请空间,因此这个内存空间位于托管堆上面,如果没有对这个对象的引用,在我们调用GC.Collect()后,或者CLR主动收集垃圾,申请的这段内存空间就会被CLR释放。这种机制简化了内存管理,我们不能直接控制内存的释放时机。不能精确指定释放哪个对象占用的空间。

    我不太清楚CLR具体原理,但CLR也只是运行在操作系统上的一个程序。假设它是C++写的,那么我们可以想象,CLR调用C++new关键字后向操作系统申请了一个堆区空间,然后把这个变量放在一个全局列表里面。然后记录我们运行在CLR上面的C#托管程序堆这个对象的引用。当没有引用存在之后,CLR从列表中删除这个对象,并调用delete xxx把内存释放给操作系统。

    但是非托管堆呢?

  • C++
    在C++中也有new关键字,比如

    Student* stu=new Student();
    delete stu;
    //引发异常
    cout >> stu->Name >> stu->Age;
    

    申请的内存空间也位于堆区。但又C++没有虚拟机,所以C++中的new关键字实际上是向操作系统申请内存空间,在进程关闭后,又操作系统释放。但是C++给了另一个关键字deletedelete stu可以手动释放向操作系统申请的内存空间。之后访问这个结构体的字段会抛出异常

  • C
    C语言中没有new关键字,但却有两个函数,mallocfree

    int* ptr = (int *)malloc(5 * sizeof(int));
    free(ptr);
    

    他们起到了和C++中new关键字相同的作用。也是向操作系统申请一块在堆区的内存空间。

C#通过new关键字向CLR申请的内存空间位于托管堆。C++通过new关键字向操作系统申请的内存空间位于非托管堆。C语言通过mallocfree向操作系统申请的内存空间也位于非托管堆。C#的new关键字更像是对C++的new关键字的封装。

C#如何申请位于非托管堆的内存空间

C#本身的new运算符申请的是托管堆的内存空间,要申请非托管堆内存空间,目前我知道的只有通过调用C++的动态链接库实现。在.net8以前,使用DllImport特性在函数声明上面。在.net8,使用LibraryImport特性在函数声明上面

C++部分

新建一个C++动态链接库项目
image

然后添加.h头文件和.cpp源文件

//Student.h

#pragma once
#include <string>
using namespace std;

extern struct Student
{
	wchar_t* Name;// 使用 char* 替代 std::string 以保证与C#兼容
	int Age;
};

//__declspec(xxx)是MSC编译器支持的关键字,dllexport表示导出后面的函数
/// <summary>
/// 创建学生
/// </summary>
/// <param name="name">姓名</param>
/// <returns>学生内存地址</returns>
extern "C" __declspec(dllexport) Student* CreateStudent(const wchar_t* name);

/// <summary>
/// 释放堆上的内存
/// </summary>
/// <param name="student">学生地址</param>
extern "C" __declspec(dllexport) void FreeStudent(Student* student);
//Student.cpp

//pch.h在项目属性中指定,pch.cpp必需
#include "pch.h"

#include "Student.h"
#include <cstring>

Student* CreateStudent(const wchar_t* name)
{
	//new申请堆空间
	Student* student = new Student;
	student->Age = 10;
	//new申请名字所需要的堆空间
	//wcslen应对unicode,ansi的话,使用strlen和char就够了
	student->Name = new wchar_t[wcslen(name) + 1];
	//内存赋值
	wcscpy_s(student->Name, wcslen(name) + 1, name);
	return student;
}

void FreeStudent(Student* student)
{
	// 假设使用 new 分配
	delete[] student->Name;//释放数组形式的堆内存
    delete student; 
}

生成项目后,在解决方案下的x64\Debug中可以找到DLL

C#部分

由于C++动态链接库不符合C#动态链接库的规范。所以没法在C#项目的依赖中直接添加对类库的引用。只需要把DLL放在项目根目录下,把文件复制方式改为总是复制,然后代码中导入。

[DllImport("Student.dll", //指定DLL
CharSet=CharSet.Unicode//指定字符串编码
)]
public static extern IntPtr CreateStudent(string name);

[DllImport("Student.dll")]
private static extern IntPtr FreeStudent(IntPtr stu);
		
public static void Main()
{
    string studentName = "John";
    //用IntPtr接收C++申请空间的起始地址
    IntPtr studentPtr = CreateStudent(studentName);

    // 在C#中操作Student结构体需要进行手动的内存管理,如下
    // 从地址所在内存构建C#对象或结构体,类似于指针的解引用
    Student student = Marshal.PtrToStructure<Student>(studentPtr);

    // 访问学生信息
    //Marshal.PtrToStringUni(student.Name)将一段内存解释为unicode字符串,直到遇见结束符'\0'
    Console.WriteLine($"Student Name: {Marshal.PtrToStringUni(student.Name)}, Age: {student.Age}");

    // 记得释放分配的内存
    FreeStudent(studentPtr);
}

// 定义C++的Student结构体
[StructLayout(LayoutKind.Sequential)]
public struct Student
{
    // IntPtr对应C++中的 char*
    public IntPtr Name;
    public int Age;
}

调用结果如下

image

非托管类释放非托管内存空间

如果我们把C++代码的调用封装成类,那么可以实现IDisposable接口。在Dispose方法中释放资源,然后使用using语句块来确保Dispose方法被调用。这样使得内存泄漏可能性降低。

继承IDisposable接口后按下alt+enter,选择通过释放模式实现接口可以快速生成代码

/// <summary>
/// 非托管类
/// </summary>
public class Student:IDisposable
{
    // 定义C++的Student结构体
    [StructLayout(LayoutKind.Sequential)]
    private struct _Student
    {
        public IntPtr Name;
        public int Age;
    }

    // IntPtr对应C++中的 char*
    //需要在Dispose中手动释放
    private IntPtr _this;
    private IntPtr name;

    public string Name => Marshal.PtrToStringUni(name);
    public int Age;

    private bool disposedValue;

    public Student(string name)
    {
        _this=CreateStudent(name);
        _Student layout = Marshal.PtrToStructure<_Student>(_this);
		//记住要释放的内存起始地址
        this.Age = layout.Age;
        this.name = layout.Name;
    }

    [DllImport("Student.dll", CharSet = CharSet.Unicode)]
    private static extern IntPtr CreateStudent(string name);

    [DllImport("Student.dll")]
    private static extern IntPtr FreeStudent(IntPtr stu);

    protected virtual void Dispose(bool disposing)
    {
        if (!disposedValue)
        {
            if (disposing)
            {
                // TODO: 释放托管状态(托管对象)
            }

            // TODO: 释放未托管的资源(未托管的对象)并重写终结器
            if (_this != IntPtr.Zero)
            {
                FreeStudent(_this);
                //设置为不可访问
                _this = IntPtr.Zero;
                name = IntPtr.Zero;
            }
            // TODO: 将大型字段设置为 null
            disposedValue = true;
        }
    }

    // // TODO: 仅当“Dispose(bool disposing)”拥有用于释放未托管资源的代码时才替代终结器
    // ~Student()
    // {
    //     // 不要更改此代码。请将清理代码放入“Dispose(bool disposing)”方法中
    //     Dispose(disposing: false);
    // }

    public void Dispose()
    {
        // 不要更改此代码。请将清理代码放入“Dispose(bool disposing)”方法中
        Dispose(disposing: true);
        GC.SuppressFinalize(this);
    }
}

然后在Main中创建对象

string studentName = "John";
using (Student stu=new Student(studentName))
{
    Console.WriteLine($"Student Name: {stu.Name}, Age: {stu.Age}");
}
return;

结果

image

代码确实执行到了这里。

  • 单步调试执行流程,using->Console->Dispose()->Dispose(bool disposing)->FreeStudent(_this);

image

事实上可以在FreeStudent(_this);之后加一句代码Console.WriteLine(Name);,你将会看到原本的正常属性变成了乱码

image

其实代码有点重复。如果我把_Student layout = Marshal.PtrToStructure<_Student>(_this);中的layout定义为Student的私有成员,那么Student中的那两个私有指针就不需要了,完全可以从layout中取得。

标签:IntPtr,C#,托管,C++,内存,Student,new
From: https://www.cnblogs.com/ggtc/p/18333486

相关文章

  • 开源语音合成库 coqui TTS 使用记录
    1介绍功能:可以克隆声音;可以转换声音。支持多语言。GitHubhttps://github.com/coqui-ai/TTS在线试玩(效果不如本地demo)https://huggingface.co/spaces/coqui/xtts2本地搭建demo搭建环境condacreate-ncoquipython=3.10condaactivatecoquipipinstallTTS(可以自动......
  • 02.Nginx的安装与Dockerfile的编写
    获取nginx安装包地址:https://nginx.org/en/download.htmlMainlineversion:Mainline是Nginx目前主力在做的版本,可以说是开发版Stableversion:最新稳定版,生产环境上建议使用的版本Legacyversions:遗留的老版本的稳定版我们下载linux上的安装包tar.gz结尾的,右键复制链接即......
  • cisp证书含金量怎么样?值不值得考?
    随着中国经济的发展,网络已经渗透到了社会各个领域,企业信息化水平也得到了很大的提高,对网络安全人才的需求也在不断增加。但目前我国网络安全专业人才非常紧缺,主要原因是大多数企业没有重视这一方面的人才培养,企业对信息系统安全管理还没有一个统一的标准和规范。所以,掌握信......
  • 基于N32L40x CmBacktrace mdk5平台下的移植测试
    首先感谢大神提供的开源库CmBacktrace开源地址:https://github.com/armink/CmBacktrace/releases/latesthttps://gitee.com/Armink/CmBacktraceCmBacktrace是什么CmBacktrace一款针对ARMCortex-M系列MCU的错误代码自动追踪、定位,错误原因自动分析的开源库CmBac......
  • Magic-PDF:端到端PDF文档解析神器 构建高质量RAG必备!
    项目结构流程解析预处理的作用是判断文档内容是否需要进行OCR识别,如果是普通可编辑的PDF文档,则使用PyMuPDF库提取元信息。模型层除了常规的OCR、版面结构分析外,还有公式检测模型,可提取公式内容,用于后续把公式转化为Latex格式。但是目前暂无表格内容识别,官方预计1个月之内会放......
  • js中数组方法reduce的妙用
            在JavaScript中,reduce()是数组的一个方法,它用于将数组中的所有元素按照指定的函数进行合并并返回一个结果。使用reduce()方法,可以对数组进行求和、累加等操作。        我一直陷入固有思维,一直只使用reduce来进行累加操作。今天在做项目时在要进行复......
  • 理解 Unix/Linux 中的 Terminal、Shell、TTY 和 Console
    文章目录1Terminal1.1传统意义上的Terminal1.2现代的Terminal2TTY2.1TTY的起源2.2Linux中的TTY2.3虚拟终端2.3.1虚拟终端为什么是虚拟的?2.4伪终端2.4.1伪终端的组成2.4.2伪终端的工作原理2.4.3伪终端的应用3Console3.1Console的定义3.2Linux中......
  • leetcode20.有效的括号、华为OD机试-(C卷,100分)- 表达式括号匹配
    leetcode20.有效的括号题目描述给定一个只包括‘(’,‘)’,‘{’,‘}’,‘[’,‘]’的字符串s,判断字符串是否有效。有效字符串需满足:左括号必须用相同类型的右括号闭合。左括号必须以正确的顺序闭合。每个右括号都有一个对应的相同类型的左括号。示例1:输入:s=“()......
  • 【并发】ThreadLocal常用场景
    ThreadLocal经常被用到,尤其是在以下场景中:数据库连接管理:每个线程使用自己的数据库连接,以避免连接被多个线程同时使用而导致的并发问题。可以在多线程环境下确保每个线程使用的数据库连接是独立的,避免了连接池中的连接被多个线程共享的风险。会话管理:每个线程都有自......
  • 【并发】ThreadLocal是什么
    ThreadLocal是Java中用于实现线程局部变量的类。它为每个使用该变量的线程提供独立的副本,因此每个线程都可以独立地改变它自己的副本,而不会影响其他线程的副本。这对于需要在线程之间保持独立状态的数据非常有用,例如用户会话、数据库连接等。主要方法ThreadLocal<T>构......