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

 

 

  • 2
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
如果你想在 Visual Studio 中使用 C++/CLI 编写 C++ 项目,并且引用 C# 中的 System::Collections,可以按照以下步骤进行: 1. 创建一个 C++ 项目 首先,你需要在 Visual Studio 中创建一个 C++ 项目。可以选择 Windows 桌面向导中的“空项目”模板,或者选择“Windows 桌面向导”模板,并在下一步中取消选中“预先配置的组件”。 2. 添加对 C# 程序集的引用 右键单击项目,选择“属性”,在“常规”选项卡中,找到“引用”选项卡。在这里,你可以添加对 C# 程序集的引用。 如果你需要引用的是 .NET Framework 中的程序集,可以点击“添加新引用”按钮,然后选择“程序集”选项卡,找到你需要引用的程序集并添加它。 如果你需要引用的是自己编写的 C# 类库,可以点击“添加新引用”按钮,然后选择“浏览”选项卡,找到你的类库并添加它。 3. 在 C++/CLI 代码中使用 C# 类型 现在,你可以在 C++/CLI 代码中使用 C# 类型了。例如,如果你想使用 C# 中的 List<T> 类,可以按照以下步骤进行: - 在 C++/CLI 代码中添加以下代码: ```cpp #using <mscorlib.dll> using namespace System::Collections::Generic; ``` - 在代码中创建一个 List 对象,并使用 Add 方法向其中添加元素: ```cpp List<int>^ myList = gcnew List<int>(); myList->Add(10); myList->Add(20); myList->Add(30); ``` - 使用 for each 循环遍历 List 中的元素: ```cpp for each (int i in myList) { Console::WriteLine(i); } ``` 需要注意的是,由于 C++/CLI 是一种混合语言,因此它可以同时使用 C++ 和 C# 语言特性。因此,在 C++/CLI 代码中可以直接使用 C# 类型,并且不需要使用 COM 或 P/Invoke 技术。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值