浅议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

相关文章

C++使用printf语句实现进制转换的示例代码

《C++使用printf语句实现进制转换的示例代码》在C语言中,printf函数可以直接实现部分进制转换功能,通过格式说明符(formatspecifier)快速输出不同进制的数值,下面给大家分享C+... 目录一、printf 原生支持的进制转换1. 十进制、八进制、十六进制转换2. 显示进制前缀3. 指

C#中async await异步关键字用法和异步的底层原理全解析

《C#中asyncawait异步关键字用法和异步的底层原理全解析》:本文主要介绍C#中asyncawait异步关键字用法和异步的底层原理全解析,本文给大家介绍的非常详细,对大家的学习或工作具有一... 目录C#异步编程一、异步编程基础二、异步方法的工作原理三、代码示例四、编译后的底层实现五、总结C#异步编程

C++中初始化二维数组的几种常见方法

《C++中初始化二维数组的几种常见方法》本文详细介绍了在C++中初始化二维数组的不同方式,包括静态初始化、循环、全部为零、部分初始化、std::array和std::vector,以及std::vec... 目录1. 静态初始化2. 使用循环初始化3. 全部初始化为零4. 部分初始化5. 使用 std::a

MySQL 中查询 VARCHAR 类型 JSON 数据的问题记录

《MySQL中查询VARCHAR类型JSON数据的问题记录》在数据库设计中,有时我们会将JSON数据存储在VARCHAR或TEXT类型字段中,本文将详细介绍如何在MySQL中有效查询存储为V... 目录一、问题背景二、mysql jsON 函数2.1 常用 JSON 函数三、查询示例3.1 基本查询3.2

C++ vector的常见用法超详细讲解

《C++vector的常见用法超详细讲解》:本文主要介绍C++vector的常见用法,包括C++中vector容器的定义、初始化方法、访问元素、常用函数及其时间复杂度,通过代码介绍的非常详细,... 目录1、vector的定义2、vector常用初始化方法1、使编程用花括号直接赋值2、使用圆括号赋值3、ve

如何高效移除C++关联容器中的元素

《如何高效移除C++关联容器中的元素》关联容器和顺序容器有着很大不同,关联容器中的元素是按照关键字来保存和访问的,而顺序容器中的元素是按它们在容器中的位置来顺序保存和访问的,本文介绍了如何高效移除C+... 目录一、简介二、移除给定位置的元素三、移除与特定键值等价的元素四、移除满足特android定条件的元

Python获取C++中返回的char*字段的两种思路

《Python获取C++中返回的char*字段的两种思路》有时候需要获取C++函数中返回来的不定长的char*字符串,本文小编为大家找到了两种解决问题的思路,感兴趣的小伙伴可以跟随小编一起学习一下... 有时候需要获取C++函数中返回来的不定长的char*字符串,目前我找到两种解决问题的思路,具体实现如下:

C++ Sort函数使用场景分析

《C++Sort函数使用场景分析》sort函数是algorithm库下的一个函数,sort函数是不稳定的,即大小相同的元素在排序后相对顺序可能发生改变,如果某些场景需要保持相同元素间的相对顺序,可使... 目录C++ Sort函数详解一、sort函数调用的两种方式二、sort函数使用场景三、sort函数排序

Pydantic中Optional 和Union类型的使用

《Pydantic中Optional和Union类型的使用》本文主要介绍了Pydantic中Optional和Union类型的使用,这两者在处理可选字段和多类型字段时尤为重要,文中通过示例代码介绍的... 目录简介Optional 类型Union 类型Optional 和 Union 的组合总结简介Pyd

Java调用C++动态库超详细步骤讲解(附源码)

《Java调用C++动态库超详细步骤讲解(附源码)》C语言因其高效和接近硬件的特性,时常会被用在性能要求较高或者需要直接操作硬件的场合,:本文主要介绍Java调用C++动态库的相关资料,文中通过代... 目录一、直接调用C++库第一步:动态库生成(vs2017+qt5.12.10)第二步:Java调用C++