c# 内存的具体表现- 通用类型系统 深拷贝 浅拷贝 函数传参
c# 通用类型系统 及变量在 深拷贝 浅拷贝 函数传参 中的深层次的表现
在编程中遇到了一些想不到的异常,跟踪发现,自己对于c#变量在内存上的表现理解有偏差,系统的学习并通过代码实验梳理了各种情况下,变量在内存级的表现情况,对以后的coding应该有些帮助。在此记录以免忘记了。。。
1. 通用类型系统
先来一张图:
通用数据类型分为了值类型和引用类型。
我们定义一个int型实际上是一个system.int32的实例,在语法上我们像使用其他的类对象一样,但是,存储的的仍然是基本类型。这种把基本类型和引用类型在语法上统一起来,所以可以称之为
通用类型系统。其实每一个值类型都有一个.net相对应的system.***。
引用类型,string是引用类型。在很多人看来string的表现与值类型一样,但是他是引用类型,只是string类的很多成员函数返回的重新生成的string对象。我们应该把引用类型当做对象指针。
在学习c++时,我们知道变量分配在堆或者栈上。在c#讨论是我们不在区分堆栈,现在统一称为堆栈。c#引入了一个新的内存空间,称之为托管堆。
引用一张别人的图,图右侧的堆其实应该是托管堆。
值类型和引用类型的对象指针分配在堆栈上,而引用类型的具体对象分配在托管堆上。
2. 变量赋值操作
值类型赋值:值类型的赋值很简单,即在堆栈申请空间,将值写入申请的空间。int a =10;即堆栈上给a分配4字节空间,4字节空间保存了10的二进制;int b =a,即堆栈上给b分配4字节空间,4字节空间保存了a的值即10的二进制.a与b是完全独立的,b只是使用了a的值对自己的空间进行了赋值。
引用类型赋值:引用类型声明时即在堆栈分配了4字节的空间(一个指针的大小),初始值为0x00000000。如object boxed,此时boxed分配了4字节的空间,空间内为0x00000000;boxed= new object(),此时在托管堆分配了空间存放了一个object的对象,boxed在堆栈的4个字节的空间存放了对象在托管堆栈的首地址。当时使用object a =b时,堆栈上分配了a的4字节的空间,其值是b的4字节的空间的值。即a,b保存了同样的地址值,指向了同样的托管堆中的一个对象。a,b只是同一个对象的不同“指针”。
3. 引用类型的浅拷贝和深拷贝
浅拷贝和深拷贝问题出现在对象包含一个引用类型的成员这种情况下。说白了,浅拷贝和深拷贝就是有没有对对象的引用类型成员重新分配空间。引用类型对象存储在托管堆中,若对象本身包含一个引用类型的成员时,此成员在对象中存储的是引用类型的成员对象的地址。理解为“我”包含一个指针,指向了一个“他”,你要浅拷贝我时,我把这个指针给你,你也指向了“他”;你要深拷贝我时,创建一个“他”的弟弟即使用new,你指向“他”的弟弟即new的返回值(地址)。
示例说明下:
class Program
{
static void Main(string[] args)
{ Test a = new Test("aaa",);
Test b = a;
Test c = a.Clone();
c.myInt = ;
c.myStr = "bbb";
c.myIntClass.myInt = ;
Console.WriteLine(a.myInt +" "+ a.myStr+" "+a.myIntClass.myInt);
Console.WriteLine(b.myInt + " " + b.myStr + " " + b.myIntClass.myInt);
Console.WriteLine(c.myInt + " " + c.myStr + " " + c.myIntClass.myInt); Console.ReadLine(); }
}
public class Test:Object
{
public string myStr { get; set; }
public int myInt { get; set; } public IntClass myIntClass { get; set; }
public Test Clone()
{
return (Test)this.MemberwiseClone();
}
public Test(string _str,int _int)
{
myStr = _str;
myInt = _int;
myIntClass = new IntClass(_int);
}
}
public class IntClass
{
public int myInt { get; set; }
public IntClass(int _int)
{
myInt = _int;
}
}
a和b是指向同一个Test对象的引用,c是a的一个浅拷贝;即c指向的对象内有一个int,一个指向string的指针(与a指向string的指针的值相同),一个指向IntClass的指针(与a指向IntClass的指针的值相同)。当改变c的int时,a不受影响;当改变c的intClass时,其实是改变了a,c指向的同一个对象,故a,b,c的IntClass 同时改变了。这里有个有趣的现象,即string也是引用类型,而c改变了其myStr,a竟然没收受到影响。这要特殊说明string是一个特殊的引用类型,
c.myStr = "bbb";其实是重新创建了一个string对象,c的mystr指向了新的对象,故a不受影响。string对象是不会被改变值的,对string的任何操作都是在一个新的对象上进行,然后返回新的对象引用。所以很多时候string表现的更像一个值类型,虽然它的确是一个引用类型。
4. 函数传参
无论是值类型还是引用类型,函数默认传递方式都是值传递,形参都“copy”了实参的值。 对于值类型很好理解,即堆栈上给形参分配了新的空间,把实参的值copy进新分配的空间。对于引用类型,堆栈上给形参分配了新的空间,把实参的值--实参在堆栈保持的4字节的对象地址 copy进了新分配的空间,注意,并没有在托管堆创建对象。所有,函数传递的引用类型,实参与形参在堆栈上是不同的空间,但其所指向的对象是同一个。
示例:
Test a = new Test("123", 10);
Console.WriteLine(a.myInt + " " + a.myStr + " " + a.myIntClass.myInt);
int b = 100;
Change(a, b);
Console.WriteLine(a.myInt + " " + a.myStr + " " + a.myIntClass.myInt);
Console.WriteLine(b);
public static void Change(Test _test,int _int)
{
_test.myInt = _int;
_int *= 10;
}
经过函数change之后对象a的myInt变为了100,而int类型的b的值并没有变化还是100。
可以简单的认为函数的值传递仅限于堆栈上的分配空间和对于新空间的赋值。
out 和 ref :
c#函数有out和ref2个关键字修饰。先讲讲ref,ref相当于c++中的&传参。ref在堆栈上也没有分配空间,形参和实参在堆栈上是一个空间。所有对形参的任何改变都会体现在实参上,甚至,对于形参指向对象的改变也会体现在实参上。示例如下:
Test a = new Test("", );
Console.WriteLine(a.myInt + " " + a.myStr + " " + a.myIntClass.myInt);
int b = ;
Change(ref a, ref b);
Console.WriteLine(a.myInt + " " + a.myStr + " " + a.myIntClass.myInt);
Console.WriteLine(b); public static void Change(ref Test _test,ref int _int)
{
_test = new Test("new", _int);
_int *= ;
}
change传递的是ref关键字修饰的参数,在change内部对于形参的任何操作相当于对实参的操作。我们与不带ref关键字的对比下:
Test a = new Test("", );
Console.WriteLine(a.myInt + " " + a.myStr + " " + a.myIntClass.myInt);
int b = ;
Change( a, b);
Console.WriteLine(a.myInt + " " + a.myStr + " " + a.myIntClass.myInt);
Console.WriteLine(b); public static void Change( Test _test, int _int)
{
_test = new Test("new", _int);
_int *= ;
}
不带ref关键字时,对于引用类型的赋值操作不会体现在实参上,这是由于我们把形参的堆栈上的地址值赋值为了在托管堆新分配的对象。而实参堆栈上的地址值依然指向旧的对象。不带ref实参,形参在堆栈是不同的2块空间。 out关键字与ref一样,形参与实参是同一块堆栈地址,不同的时,函数退出前必须对out参数赋值(即在托管堆分配新的对象)。 总结函数传参: 默认是指传递,在堆栈上分配空间并把实参在堆栈上相应值复制到新分配的空间。对于形参堆栈的操作(值类型的赋值,引用类型的赋值)不会体现到实参上。对于托管堆的操作(改变引用类型的成员)会体现到实参上。
ref传递,在堆栈上没有分配新的空间,形参和实参是同一块堆栈地址。对于形参堆栈的操作(值类型的赋值,引用类型的赋值)和 对于托管堆的操作(改变引用类型的成员)都会体现到实参上。
out传递,在堆栈上没有分配新的空间,形参和实参是同一块堆栈地址。函数返回前必须对形参赋值。(若转载请注明博客园源地址)
c# 内存的具体表现- 通用类型系统 深拷贝 浅拷贝 函数传参的更多相关文章
- “通用类型系统”(CTS)
一.什么是“通用类型系统”(CTS) 描述类型的定义和行为 二.CTS规范 一个类型可以包含零个或者多个成员1,成员①字段(Field)作为对象状态一部分的数据变量.字段根据名称和类型来区分②方法(M ...
- python集合增删改查,深拷贝浅拷贝
集合 集合是无序的,不重复的数据集合,它里面的元素是可哈希的(不可变类型),但是集合本身是不可哈希(所以集合做不了字典的键)的.以下是集合最重要的两点: 去重,把一个列表变成集合,就自动去重了. 关系 ...
- JavaScript之深拷贝&浅拷贝
深拷贝&浅拷贝,说起来都明白,但是说不出所以然.今天就系统的整理下思绪,一点点的将其分析出所以然 废话不多说 浅拷贝 简单的说就是一个值引用,学生时代接触过编程的人都应该了解过指针,浅拷贝可以 ...
- 【04】Python 深拷贝浅拷贝 函数 递归 集合
1 深拷贝浅拷贝 1.1 a==b与a is b的区别 a == b 比较两个对象的内容是否相等(可以是不同内存空间) a is b 比较a与b是否指向同一个内存地址,也就是a与b的id是否相 ...
- java中深拷贝浅拷贝简析
Java中对象的创建 clone顾名思义就是复制, 在Java语言中, clone方法被对象调用,所以会复制对象.所谓的复制对象,首先要分配一个和源对象同样大小的空间,在这个空间中创建一个新的对象.那 ...
- 【opencv】imread 赋值 深拷贝浅拷贝
import cv2 import copy import os def filter_srcimg(dstimg): ss=3 srcimg=copy.deepcopy(dstimg) #aa=5 ...
- Java基础 深拷贝浅拷贝
Java基础 深拷贝浅拷贝 非基本数据类型 需要new新空间 class Student implements Cloneable{ private int id; private String na ...
- JS Object Deep Copy & 深拷贝 & 浅拷贝
JS Object Deep Copy & 深拷贝 & 浅拷贝 https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Refe ...
- iOS内存管理(二)之深拷贝和浅拷贝
对象拷贝(复制对象) 1.复制对象顾名思义,复制一个对象作为副本,它会开辟一块新的一块内存(堆内存)来存储副本对象,就像复制文件一样.即源对象和副本对象是两块不同的内存区域. 2.NSObject ...
随机推荐
- supervisor的集中化管理搭建
1.supervisor很不错,可惜是单机版,所以上github上找了个管理工具supervisord-monitor. github地址: https://github.com/mlazarov/s ...
- HTTP协议(四)
第一步:新建一个header.php页 <?php header('Location:http://www.baidu.com');//默认是302重定向 ?> 第二步:分析 如何制定重定 ...
- 关于 Python generator(生成器)的类比
Python 的生成器运用仿佛是最完美的 xing爱,生成器本身和循环代表男女,结束代表同时达到高潮,不是很精准,但很有趣啊!哈哈哈,一下记住了
- 【Electron】Electron开发入门(一):开发环境搭建
刚接触Electron+js开发PC端桌面应用程序的时候,简直一头雾水,搜了网上很多教程,有的要么讲的零零碎碎,要么就是版本太低,很多API语法都不能用了:现在我把一些有用的教程归纳一下,并把目前最新 ...
- CSS3学习笔记(4)-CSS3函数
p{ font-size: 15px; text-indent: 2em; } .alexrootdiv>div{ background: #eeeeee; border: 1px solid ...
- laravel安装插件laravel-ide-helper
1.插件位置laravel-ide-helper https://github.com/barryvdh/laravel-ide-helper 2.首先改变镜像源为国内的镜像源 P { margin- ...
- NDK 线程同步
使用场景 对底层代码进行 HOOK, 不可避免的要考虑多线程同步问题, 当然也可以写个类似 java 的线程本地变量来隔离内存空间. 死锁分析 恩, 道理其实大家都懂的, 毕竟大学就学了操作系统,理论 ...
- MongoDB基础教程系列--第八篇 MongoDB 副本集实现复制功能
为什么用复制 为什么要使用复制呢?如果我们的数据库只存在于一台服务器,若这台服务器宕机了,那对于我们数据将会是灾难,当然这只是其中一个原因,若数据量非常大,读写操作势必会影响数据库的性能,这时候复制就 ...
- T-SQL编程语句
书接上回 一起学习下SQL中的表连接 一般情况下咱们会使用鼠标去进行表连接操作,那针对于像我比较懒的并且眼盲的,不喜欢来回切换,咱们就用到了点T-SQL表连接语句 一般情况咱们从两个表中查出来相似的内 ...
- CSAcademy Beta Round #5 Long Journey
题目链接:https://csacademy.com/contest/arhiva/#task/long_journey/ 大意是有一张无向不带权的图,两个人同时从s点出发,分别前往a点和b点,且每个 ...