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

news2024/9/25 9:39:22

进程的内存

一个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中取得。

文章转载自:ggtc

原文链接:https://www.cnblogs.com/ggtc/p/18333486

体验地址:引迈 - JNPF快速开发平台_低代码开发平台_零代码开发平台_流程设计器_表单引擎_工作流引擎_软件架构

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1971284.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

(亲测)taro不是内部或外部命令,也不是可运行的程序 或批处理文件。

目录 报错 原因 解决方法&#xff08;亲测&#xff09; 报错 报错&#xff1a;taro不是内部或外部命令&#xff0c;也不是可运行的程序 或批处理文件。 原因 全局成功安装后&#xff0c;taro指令还是不能使用&#xff0c;此时需要手动添加环境变量。 解决方法&#xff08…

python字符串数据容器练习(replace,split,count)

一、要求 二、代码实现 str "itheima itcast boxuegu" num str.count("it") print(f"[num]") str1 str.replace(" " , "|") print(str1) str2 str1.split("|") print(str2) 三、知识点总结 Python字符串是由…

深入理解Vue slot的原理

文章目录 前言为什么需要插槽作用域插槽插槽的原理总结 前言 插槽是Vue中一个重要的特性&#xff0c;它有很多种用法&#xff1a;默认插槽、具名插槽、作用域插槽。尤其作用域插槽&#xff0c;还有一堆特性&#xff0c;比如解构prop&#xff0c;解构prop的时候还可以进行属性名…

[CISCN2019 华北赛区 Day2 Web1]Hack World1

试试数字1 2 3朝后都是 猜测为数字型注入 试试1 union select flag from flag 爆破发现都被过滤了&#xff0c;尝试用布尔盲注&#xff0c;用python编写脚本&#xff0c;得到flag

浅谈SPI

目录 前言JDK SPIJDBC SPIServiceLoader实现原理小结 SpringSpringBoot SPI实现原理Debug小结 Dubbo SPI如何使用实现原理 前言 SPI&#xff0c;英文全称是Service Provider Interface&#xff0c;直译是“服务提供接口”或“服务提供者接口”&#xff0c;是一种基于ClassLoad…

YOLOv8目标检测算法改进之融合SCconv的特征提取方法

引言 YOLO目标检测算法历经发展&#xff0c;目前已经成为了目标检测领域的经典算法。当前&#xff0c;YOLO目标检测算法已经更新到YOLOv10&#xff0c;但从大家的反映来看,YOLOv10的效果并不理想&#xff08;该算法的创新点是提升检测速度&#xff0c;并不提升精度&#xff0c…

JVM: 方法调用

文章目录 一、介绍二、方法调用的原理1、静态绑定2、动态绑定&#xff08;1&#xff09;介绍&#xff08;2&#xff09;原理 一、介绍 在JVM中&#xff0c;一共有五个字节码指令可以执行方法调用&#xff1a; invokestatic: 调用静态方法。invokespecial&#xff1a;调用对象…

大模型参与城市规划中的应用

人工智能咨询培训老师叶梓 转载标明出处 传统的城市规划往往依赖于专业规划师的经验和判断&#xff0c;耗时耗力&#xff0c;且难以满足居民多样化的需求。近年来&#xff0c;大模型&#xff08;LLMs&#xff09;的崛起为城市规划领域带来了新的机遇。清华大学电子工程系的Zhil…

微信小程序多端框架实现app内自动升级

多端框架生成的app&#xff0c;如果实现app内自动升级&#xff1f; 一、Android 实现app自动升级&#xff0c;华为应用市场 1、获取 应用市场地址 下载地址 2、在微信开放平台进行配置 应用下载地址&#xff1a;应用市场点击分享&#xff0c;里面有一个复制连接功能 应用市…

XMLDecoder反序列化

XMLDecoder反序列化 基础知识 就简单讲讲吧&#xff0c;就是为了解析xml内容的 一般我们的xml都是标签属性这样的写法 比如person对象以xml的形式存储在文件中 在decode反序列化方法后&#xff0c;控制台成功打印出反序列化的对象。 就是可以根据我们的标签识别是什么成分…

QT多媒体编程(一)——音频编程知识详解及MP3音频播放器Demo

目录 引言 一、QtMultimedia模块简介 主要类和功能 二、QtMultimedia相关类及函数解析 QAudioInput QAudioOutput QAudioFormat QMediaPlayer QMediaPlaylist QCamera 三、音频项目实战Demo UI界面 核心代码 运行结果 四、结论 引言 在数字时代&#xff0c;音频…

ArcGIS for js 分屏(vue项目)

一、引入依赖 import {onMounted, ref} from "vue"; import Map from "arcgis/core/Map"; import MapView from "arcgis/core/views/MapView"; import WebTileLayer from "arcgis/core/layers/WebTileLayer"; 二、页面布局 <tem…

22. Hibernate 性能之缓存

1. 前言 本节和大家一起聊聊性能优化方案之&#xff1a;缓存。通过本节学习&#xff0c;你将了解到&#xff1a; 什么是缓存&#xff0c;缓存的作用&#xff1b;HIbernate 中的缓存级别&#xff1b;如何使用缓存。 2. 缓存 2.1 缓存是什么 现实世界里&#xff0c;缓存是一个…

纪念二2024.07 federated-解决mysql跨库联表问题

若需要创建FEDERATED引擎表&#xff0c;则目标端实例要开启FEDERATED引擎。从MySQL5.5开始FEDERATED引擎默认安装 只是没有启用&#xff0c;进入命令行输入 show engines ; FEDERATED行状态为NO。 mysql安装配置文件 一、连接工具查看是否开启federated show engines 二、m…

VMware Workstation17 安装 CentOS7 教程

今天给伙伴们分享一下VMware Workstation17 安装 CentOS7 教程&#xff0c;希望看了有所收获。 我是公众号「想吃西红柿」「云原生运维实战派」作者&#xff0c;对云原生运维感兴趣&#xff0c;也保持时刻学习&#xff0c;后续会分享工作中用到的运维技术&#xff0c;在运维的路…

JS【详解】内存泄漏(含泄漏场景、避免方案、检测方法),垃圾回收 GC (含引用计数、标记清除、标记整理、分代式垃圾回收)

内存泄漏 在执行一个长期运行的应用程序时&#xff0c;应用程序分配的内存没有被释放&#xff0c;导致可用内存逐渐减少&#xff0c;最终可能导致浏览器崩溃或者应用性能严重下降的情况&#xff0c;即 JS 内存泄漏 可能导致内存泄漏的场景 不断创建全局变量未及时清理的闭包&…

Graylog 收集网络设备日志的详细配置指南

需求:网络日志接入到日志服务中,做日志的备份和查询。 交换机或是其它网络设备日志需要接入到graylog日志服务中进行备份和查询。 软件版本 graylog5.1 架构图 一、添加inputs 接受日志信息 二、编辑inputs 配置 第1个红框 title 代表通道的名称,您可以根据需要自由定义…

【CTF-Crypto】格密码基础(例题较多,非常适合入门!)

格密码相关 文章目录 格密码相关格密码基本概念&#xff08;属于后量子密码&#xff09;基础的格运算&#xff08;行列式运算&#xff09;SVP&#xff08;shortest Vector Problem&#xff09;最短向量问题CVP&#xff08;Closet Vector Problem&#xff09;最近向量问题 做题要…

浏览器用户文件夹详解 - ShortCuts(六)

1. Shortcuts简介 1.1 什么是Shortcuts文件&#xff1f; Shortcuts文件是Chromium浏览器中用于存储用户创建的快捷方式信息的一个重要文件。每当用户在浏览器中创建快捷方式时&#xff0c;这些信息都会被记录在Shortcuts文件中。通过这些记录&#xff0c;用户可以方便地快速访…

《小迪安全》学习笔记02

域名默认存放目录和IP默认存放目录不一样。 IP地址是WWW文件里的&#xff0c;域名访问是WWW里的一个子目录里的&#xff08;比如是blog&#xff09;。 Nmap: Web源码拓展 拿到一个网站的源码&#xff0c;要分析这几个方面↑。 不同类型产生的漏洞类型也不一样 在网站中&…