浅议C++ /CLI的gcnew关键字及C++ /CLI的引用类型

2024-03-09 04:48

本文主要是介绍浅议C++ /CLI的gcnew关键字及C++ /CLI的引用类型,希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!

C++/CLI中使用gcnew关键字表示在托管堆上分配内存,并且为了与以前的指针区分,用^来替换* ,就语义上来说他们的区别大致如下:


  1.     gcnew返回的是一个句柄(Handle),而new返回的是实际的内存地址.
2.     gcnew创建的对象由虚拟机托管,而new创建的对象必须自己来管理和释放.
当然,从程序员的角度来说,管它是句柄还是什么其他的东西,总跑不掉是对某块内存地址的引用,实际上我们都可以理解成指针.下面我们就写一段代码来测试一下好了.
using namespace System;
ref class Foo
{
public:
Foo()
{
System::Console::WriteLine("Foo::Foo");
}
~Foo()
{
System::Console::WriteLine("Foo::~Foo");
}
public:
int m_iValue;
};
int _tmain()
{
int* pInt = new int;
int^ rInt = gcnew int;
Foo^ rFoo = gcnew Foo;
delete rFoo;
delete rInt;
delete pInt;
}
我把调试的时候JIT编译的汇编代码择录了部分如下显示(请注意红色部分):

int* pInt = new int;
0000004c  mov         ecx,4
00000051  call        dword ptr ds:[03B51554h]
00000057  mov         esi,eax
00000059  mov         dword ptr [esp+18h],esi
int^ rInt = gcnew int;
0000005d  mov         ecx,788EF9D8h
00000062  call        FCFAF66C
00000067  mov         esi,eax
00000069  mov         dword ptr [esi+4],0
00000070  mov         edi,esi
Foo^ rFoo = gcnew Foo;
00000072  mov         ecx,3B51768h
00000077  call        FCFAF66C
0000007c  mov         esi,eax
0000007e  mov         ecx,esi
00000080  call        dword ptr ds:[03B517ACh]
00000086  mov         dword ptr [esp+1Ch],esi
delete rFoo;
0000008a  mov         ebx,dword ptr [esp+1Ch]
0000008e  test        ebx,ebx
00000090  je          000000A4
00000092  mov         ecx,ebx
00000094  call        dword ptr ds:[03FD0028h]
0000009a  mov         dword ptr [esp+14h],0
000000a2  jmp         000000AC
000000a4  mov         dword ptr [esp+14h],0
delete rInt;
000000ac  mov         edx,edi
000000ae  mov         ecx,788F747Ch
000000b3  call        FC8D20FD
000000b8  mov         ebp,eax
000000ba  test        ebp,ebp
000000bc  je          000000D0
000000be  mov         ecx,ebp
000000c0  call        dword ptr ds:[03FD0020h]
000000c6  mov         dword ptr [esp+10h],0
000000ce  jmp         000000D8
000000d0  mov         dword ptr [esp+10h],0
delete pInt;
000000d8  mov         ecx,dword ptr [esp+18h]
000000dc  call        dword ptr ds:[03B51540h]
我们先看分配内存这部分的代码
1.调用new方式分配
int* pInt = new int;
0000004c  mov         ecx,4
00000051  call        dword ptr ds:[03B51554h]

  可以看到,和以前在vc6中一样,分配内存的步骤如下:
1.  首先把sizeof(int) = 4 放到ecx中
2.  调用operator new 去分配4个字节
3.  调用构造函数等等......(这里不是我们的重点)

  成功分配后,会把返回地址放在eax中。
2.调用gcnew方式分配
int^ rInt = gcnew int;
0000005d  mov         ecx,788EF9D8h
00000062  call        FCFAF66C
。。。
Foo^ rFoo = gcnew Foo;
00000072  mov         ecx,3B51768h
00000077  call        FCFAF66C

  可以看到gcnew也是通过把一个参数放到ecx中,然后再调用一个函数来完成分配的操作,显然0x788EF9D8应该是一个地址,而不可能是一个数值。我们可以看到这里gcnew创建两个不同类型的变量,调用的函数地址却都是0xFCFAF66C,而存放到ecx中的两个地址就不一样。究竟这几个地址代表什么呢?
和new一样gcnew也是把返回地址放在eax中。我们直接从内存窗口看eax指向的内存块好了。Aha,看到了没有?

  这次的eax = 0x00F73404  对应的内存块为
0x00F73404  d8 f9 8e 78 00 00 00 00 。。。
这个不就是 mov 到 ecx中的值么?再回忆昨天写的分析Object对象布局的文章,可以肯定这个就是 MethodTable地址了,对于这个int来说,后面的4个字节对应的就是存放它的RawData,比如如果你初始化为 4 那么内存对应的就变化为 d8 f9 8e 79 04 00 00 00
分析清楚存放到ecx中的是 MethodTable指针,我们再分析那个对应的call函数,从vm的代码可以看出,有三个全局函数用来根据MethodTable创建对象,同时MethodTable本身也提供一个成员函数Allocate(),只不过这个成员函数也是调用的下面的函数:

OBJECTREF AllocateObject( MethodTable *pMT )
OBJECTREF AllocateObjectSpecial( MethodTable *pMT )
OBJECTREF FastAllocateObject( MethodTable *pMT )
其中AllocateObject又是调用AllocateObjectSpecial来完成工作。那么我们调用的应该就是AllocateObject或者FastAllocateObject了。

  在我们的例子里面两个call的地址都一样,但是你如果写下代码 double ^ pDouble = gcnew double;这个时候的地址是多少?它和int 的一样么?

  目前我还没有仔细去研究这个地址到底对应的是该类型的MethodTable::Allocate()或是上面的这三个全局函数,如果对应MethodTable::Allocate(),那么2.0中应该有个MethodTable::FastAllocate()吧,否则应该就是对应的全局函数AllocateObject 以及FastAllocateObject了。过几天一定要抽空再好好研究一下。
下面看对应的delete函数。
delete pInt;
000000d8  mov         ecx,dword ptr [esp+18h]
000000dc  call        dword ptr ds:[03B51540h]
比较简单,就是传入地址,然后调用operator delete来释放类存,会调用析构函数
对应的,释放gcnew创建的对象的代码如下:
delete rInt;
000000ac  mov         edx,edi
000000ae  mov         ecx,788F747Ch
000000b3  call        FC8D20FD

  这个也相对简单,它对应vm里面的一个函数:
void  CallFinalizer(Thread* FinalizerThread, Object* fobj)

  那么也就是
fobjà edx
FinalizerThread à ecx
Call CallFinalizer
但是,请注意!!!!!!!一个类包含析构函数和不包含析构函数,它对应的delete代码是不一样的,这点可以通过汇编代码比较得到,我这里就不多说了。

 

ref class R
{
private:
   int x;
public:
   R(int xx): x(xx) {}
};

R^ o = gcnew R(3);  //在托管堆
R os(3);   //也在托管堆
o 和 os 之间的区别在它们的生存期上,或者说得更加具体一些,是对它们生存期的控制力。
 如果编写的是托管代码,你可能不会介意放弃对内存的控制权,反而愿意信任运行库和垃圾回收器为你管理内存。
但是开发人员仍然需要操心与内存无关的清除工作: 比如关闭文件或者连接。 垃圾回收本身不足以处理你在应用程序中使用的所有资源。
 在 C++ 中,这种与内存无关的清除通常是在析构函数中进行的。

托管堆中的对象是通过句柄 o 访问的,当控制达到带有 gcnew 的那一行时对象就开始存在。
未来某个时候, o 将超出控制范围。 可能控制已经超过用 return 或者 exit 语句声明它的代码块,
可能代码块是 if, for, 或者 while 语句的谓词而且控制已经以通常的方式离开,或者出现了异常。
无论原因如何, o 都将超出范围。 这时候,事情变得有些复杂。 如果任何代码都有句柄的副本,
副本将到处都是,然后只要范围中有句柄,对象就将继续在托管堆中存在。 如果句柄对象应该回收了,
但是回收的准确时间并不知道,因此何时运行析构函数是未知的。 这取决于应用程序施加的内存压力数量等等因素。

对于堆栈中的对象 os ,情况就大大不同了。 在超出范围后(按照使 o 超出范围的同样情况),对象的一切就结束了。
它的析构函数,如果有的话,将在 os 离开范围后立即运行。 你可以准确地知道与内存无关的清除何时发生,而且能够尽快发生。 这就是所谓确定性析构。

顺便提及, os 实例(我们认为它在堆栈中)实际上使用的是托管堆上的内存(依然是由垃圾回收器托管的)。
析构函数并不回收该实例使用的内存;它关心的是与内存无关的清除。 引用类型只能模拟为在堆栈中。
 如果你已经习惯不管内存管理,并信任垃圾回收器处理一切,这种模拟是非常理想的。

    -----------《C/C++ 应用程序路线图》


p96
R^ o = gcnew R(3);
句柄o存储在栈上,gcnew的R实际对象存储在托管堆上。

当一个托管引用被声明为存在于堆栈上时,编译器实际上还会在托管堆上对其进行实例化
    -----------《使用 Visual C++ 2005 的现代语言功能编写更快的代码》
os是栈中对象,但编译器将其在托管堆中实例化。

引用类型永远分配到托管堆中,值类型相比引用类型小得多,被分配在线程栈上。值类型不受垃圾回收控制。
在文档中...class 都是引用类型,如System.Object class, the System.Exception class, the
System.IO.FileStream class, and the System.Random class
在文档中...struct或...enum都是值类型,如System.Boolean structure, the System.Decimal structure, the
System.TimeSpan structure, the System.DayOfWeek enumeration, the System.IO.FileAttributes, enumeration, and the System.−Drawing.FontStyle enumeration
    -----------《Microsoft.NET Framework Programming》p114
R是引用类型,因此无论是gcnew的对象,还是对象os,都是存储在托管堆上。

 

值类型默认存储在栈中,但是可以使用gcnew使其分配在托管堆中。
    -----------《Beginning Visual C++ 2005》p199
引用类型只保存在托管堆中
值类型有双重属性,其装箱后保存在托管堆中,正常时保存在栈中。
    -----------《C++CLIRationale》p26
值类型是不支持垃圾回收的,但如果gcnew分配后,就支持垃圾回收了??

int^ value = 99;在托管堆中分配内存。
    -----------《Beginning Visual C++ 2005》p200

 

我的理解::引用类型对象都在托管堆,^句柄所指的内存都是托管堆,^句柄在栈中

 

o 和 os 之间的区别的例子。o可以不随着范围的结束而清除。os随着范围的结束而清除。
 R^ o;
 dd = 1;
 while(dd==1){
  o = gcnew R(3);   //在托管堆
  R os(3);   //也在托管堆
  R^ nn = o;
//  R^ n1 = %os;
  dd--;
 }
由此可以知道,引用类型不管是不是使用了gcnew创建的对象,都分配在托管堆中。
引用类型由CLR支持,对象包含元数据信息。
int^ aa;
也分配到托管堆中。是在托管堆中存储的句柄。
int* pM;在堆栈中的指针pM,无法指向托管堆中的内存。如
 int ^a5 = gcnew int(5);
 int *pM;
 pM = a5;
不会通过编译。
 int ^a5;
 int *pM;
 pM = a5;
即使不使用gcnew分配内存,a5仍然是托管堆中的句柄,pM仍然无法指向它。也不会通过编译。


将无元数据信息的数据类型,转化为包含元数据信息的引用类型,这一过程称为装箱。即把简单的对象放到箱子里,使之成为复杂对象。
相反的过程称为拆箱。

 

最后,对引用类型、值类型、和非托管类的内存分配位置作了一个表格

 

ref class

value class

class

T *t = new T;

-----

本地堆

本地堆

T ^t = gcnew T; 

托管堆

托管堆

-----

T t;

栈(托管堆)

 

 

 

 

 

http://west263.com/info/html/chengxusheji/C-C--/20080224/9240.html

 

 

这篇关于浅议C++ /CLI的gcnew关键字及C++ /CLI的引用类型的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/789553

相关文章

Redis的Zset类型及相关命令详细讲解

《Redis的Zset类型及相关命令详细讲解》:本文主要介绍Redis的Zset类型及相关命令的相关资料,有序集合Zset是一种Redis数据结构,它类似于集合Set,但每个元素都有一个关联的分数... 目录Zset简介ZADDZCARDZCOUNTZRANGEZREVRANGEZRANGEBYSCOREZ

C++中使用vector存储并遍历数据的基本步骤

《C++中使用vector存储并遍历数据的基本步骤》C++标准模板库(STL)提供了多种容器类型,包括顺序容器、关联容器、无序关联容器和容器适配器,每种容器都有其特定的用途和特性,:本文主要介绍C... 目录(1)容器及简要描述‌php顺序容器‌‌关联容器‌‌无序关联容器‌(基于哈希表):‌容器适配器‌:(

SpringBoot项目中Maven剔除无用Jar引用的最佳实践

《SpringBoot项目中Maven剔除无用Jar引用的最佳实践》在SpringBoot项目开发中,Maven是最常用的构建工具之一,通过Maven,我们可以轻松地管理项目所需的依赖,而,... 目录1、引言2、Maven 依赖管理的基础概念2.1 什么是 Maven 依赖2.2 Maven 的依赖传递机

C#使用yield关键字实现提升迭代性能与效率

《C#使用yield关键字实现提升迭代性能与效率》yield关键字在C#中简化了数据迭代的方式,实现了按需生成数据,自动维护迭代状态,本文主要来聊聊如何使用yield关键字实现提升迭代性能与效率,感兴... 目录前言传统迭代和yield迭代方式对比yield延迟加载按需获取数据yield break显式示迭

c# checked和unchecked关键字的使用

《c#checked和unchecked关键字的使用》C#中的checked关键字用于启用整数运算的溢出检查,可以捕获并抛出System.OverflowException异常,而unchecked... 目录在 C# 中,checked 关键字用于启用整数运算的溢出检查。默认情况下,C# 的整数运算不会自

IDEA如何将String类型转json格式

《IDEA如何将String类型转json格式》在Java中,字符串字面量中的转义字符会被自动转换,但通过网络获取的字符串可能不会自动转换,为了解决IDEA无法识别JSON字符串的问题,可以在本地对字... 目录问题描述问题原因解决方案总结问题描述最近做项目需要使用Ai生成json,可生成String类型

redis-cli命令行工具的使用小结

《redis-cli命令行工具的使用小结》redis-cli是Redis的命令行客户端,支持多种参数用于连接、操作和管理Redis数据库,本文给大家介绍redis-cli命令行工具的使用小结,感兴趣的... 目录基本连接参数基本连接方式连接远程服务器带密码连接操作与格式参数-r参数重复执行命令-i参数指定命

C++中实现调试日志输出

《C++中实现调试日志输出》在C++编程中,调试日志对于定位问题和优化代码至关重要,本文将介绍几种常用的调试日志输出方法,并教你如何在日志中添加时间戳,希望对大家有所帮助... 目录1. 使用 #ifdef _DEBUG 宏2. 加入时间戳:精确到毫秒3.Windows 和 MFC 中的调试日志方法MFC

深入理解C++ 空类大小

《深入理解C++空类大小》本文主要介绍了C++空类大小,规定空类大小为1字节,主要是为了保证对象的唯一性和可区分性,满足数组元素地址连续的要求,下面就来了解一下... 目录1. 保证对象的唯一性和可区分性2. 满足数组元素地址连续的要求3. 与C++的对象模型和内存管理机制相适配查看类对象内存在C++中,规

Mysql 中的多表连接和连接类型详解

《Mysql中的多表连接和连接类型详解》这篇文章详细介绍了MySQL中的多表连接及其各种类型,包括内连接、左连接、右连接、全外连接、自连接和交叉连接,通过这些连接方式,可以将分散在不同表中的相关数据... 目录什么是多表连接?1. 内连接(INNER JOIN)2. 左连接(LEFT JOIN 或 LEFT