浅议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++ 类成员变量默认初始值的实现

《c++类成员变量默认初始值的实现》本文主要介绍了c++类成员变量默认初始值,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一起学习学习吧... 目录C++类成员变量初始化c++类的变量的初始化在C++中,如果使用类成员变量时未给定其初始值,那么它将被

C++中NULL与nullptr的区别小结

《C++中NULL与nullptr的区别小结》本文介绍了C++编程中NULL与nullptr的区别,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编... 目录C++98空值——NULLC++11空值——nullptr区别对比示例 C++98空值——NUL

C++ Log4cpp跨平台日志库的使用小结

《C++Log4cpp跨平台日志库的使用小结》Log4cpp是c++类库,本文详细介绍了C++日志库log4cpp的使用方法,及设置日志输出格式和优先级,具有一定的参考价值,感兴趣的可以了解一下... 目录一、介绍1. log4cpp的日志方式2.设置日志输出的格式3. 设置日志的输出优先级二、Window

从入门到精通C++11 <chrono> 库特性

《从入门到精通C++11<chrono>库特性》chrono库是C++11中一个非常强大和实用的库,它为时间处理提供了丰富的功能和类型安全的接口,通过本文的介绍,我们了解了chrono库的基本概念... 目录一、引言1.1 为什么需要<chrono>库1.2<chrono>库的基本概念二、时间段(Durat

C++20管道运算符的实现示例

《C++20管道运算符的实现示例》本文简要介绍C++20管道运算符的使用与实现,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一起学习学习吧... 目录标准库的管道运算符使用自己实现类似的管道运算符我们不打算介绍太多,因为它实际属于c++20最为重要的

Visual Studio 2022 编译C++20代码的图文步骤

《VisualStudio2022编译C++20代码的图文步骤》在VisualStudio中启用C++20import功能,需设置语言标准为ISOC++20,开启扫描源查找模块依赖及实验性标... 默认创建Visual Studio桌面控制台项目代码包含C++20的import方法。右键项目的属性:

c++中的set容器介绍及操作大全

《c++中的set容器介绍及操作大全》:本文主要介绍c++中的set容器介绍及操作大全,本文通过实例代码给大家介绍的非常详细,对大家的学习或工作具有一定的参考借鉴价值,需要的朋友参考下吧... 目录​​一、核心特性​​️ ​​二、基本操作​​​​1. 初始化与赋值​​​​2. 增删查操作​​​​3. 遍历方

解析C++11 static_assert及与Boost库的关联从入门到精通

《解析C++11static_assert及与Boost库的关联从入门到精通》static_assert是C++中强大的编译时验证工具,它能够在编译阶段拦截不符合预期的类型或值,增强代码的健壮性,通... 目录一、背景知识:传统断言方法的局限性1.1 assert宏1.2 #error指令1.3 第三方解决

C++11委托构造函数和继承构造函数的实现

《C++11委托构造函数和继承构造函数的实现》C++引入了委托构造函数和继承构造函数这两个重要的特性,本文主要介绍了C++11委托构造函数和继承构造函数的实现,具有一定的参考价值,感兴趣的可以了解一下... 目录引言一、委托构造函数1.1 委托构造函数的定义与作用1.2 委托构造函数的语法1.3 委托构造函

C++11作用域枚举(Scoped Enums)的实现示例

《C++11作用域枚举(ScopedEnums)的实现示例》枚举类型是一种非常实用的工具,C++11标准引入了作用域枚举,也称为强类型枚举,本文主要介绍了C++11作用域枚举(ScopedEnums... 目录一、引言二、传统枚举类型的局限性2.1 命名空间污染2.2 整型提升问题2.3 类型转换问题三、C