Csharp中的值类型和应用类型的值传递和引用传递
为了巩固Csharp中的基础知识,为快要到来的面试打基础的过程中,我发现了个奇怪的事情,就是我对这个值类型和引用类型,以及值类型、引用类型的各种传递的定义以及表现表示各种不理解以及一脸懵,我作为要作为一个使用C#作为语言进行软件开发的准软件开发工程师来说,是很有必要的。
堆和栈
要搞明白啥是值类型啥是引用类型,我首先想到,这玩意拿过来就是要用的类似物品一样的东西,我不去引用更多的名词和定义以免自己回看的时候让自己一脸懵,但是首先,既然值类型和引用类型都是“东西”,或者是要传递的“物品”,现实生活中的快递啥的,都是需要一个仓库甚至是车厢去存放快件,所以这里面即将引入的概念就是用于存放这个值类型和引用类型的“物品”的这么一个地方——堆和栈
堆
这玩意字面上很好理解,就是一个空白的地方,往上垒东西就行,但是语文稍微好点或者语感稍微准确点的读者可能会发现(没有说看不起语感不好的人哦哈哈哈哈哈哈),咱这个堆,听起来给人第一感觉就是一堆存放在某块区域里的什么什么东西。举个例子——一堆杂乱无章的书,等等等等。
堆是干嘛用的?
対辽!这个C#中,堆的作用就是用来存放C#中的实例对象,当然,谁没事干写个项目一天传递的“东西”都是实例对象?当然也要存储数据!而且是大量数据!(不要问我这个大量是多大量,emmmm)
堆有啥特点?
我在整理资料的时候,发现我问自己最多的问题,就是这个。
从它的中文名称来思考,堆是形容杂乱无章的(如果是有序的,我更倾向于用组来形容)如果这么考虑,对辽!这就是第一个特点。
- 存储数据或者实例对象是无序的。
- 既然已经没有先后顺序了,你当然可以随意存取你想要存的数据或者是实例对象啦!
- 还有就是可以动态分配存储空间。
那么堆有啥不好的地方呢?
有是有,就是在读取速度上比较慢和不能自动回收过期的实例对象(当然.net有GC但是C++选手就稍微惨的一……)。
因为是没有先后顺序,你在向系统发出请求,我要找某某某数据!系统就会从堆中找你所要找的东西,就像是你在书堆里找自己喜欢的漫画书一样。所以时间就会慢一点。
栈
说了堆了,我们来说说栈这个名词。
栈相对于堆是啥样的?
简单来说,你可以把栈看成一个“乐事的薯片桶”。
那么栈有啥特点呢?
栈相对于堆来说,就显得十分智能了。
你拿起一个“乐事的薯片桶”在后部有底的情况下,你往里面放薯片是需要一片一片的拿起来再放进去,对吧?如果第一片“薯片”你放进去的是可乐味,第二片你放进去的是青柠黄瓜味,第三片是烤肉味,而且你突然想到,诶还没吃过可乐味的薯片,这时候,你想要拿出来。所以你要先把上面的两个薯片拿出来,再拿出来最下面的薯片。
特点就是!
- 栈只能在一端进行操作!也就是我们说的“薯片桶的口”,不过我们一般叫它栈顶,储存方式就像是“乐事薯片桶”的存薯片方式一样遵循“先进后出,先出后进”的原则。
- 栈是一种内存自我管理的结构,数据进栈自动分配内存,出栈自动清空所占内存。
- 虽然看起来一个栈中,存取数据的过程很麻烦,但是!他在多数据中的表现效率是快过堆这个存储方式的。
栈方式储存数据没有什么不好的地方吗?
有啊!万事都有两面性!
- 栈中的内存不能动态请求,就是,只能根据数据大小来分配,灵活性没有堆高。
- 如同“乐事薯片桶”中的薯片一样,只能存储一定量的数据,因此我们在使用的时候要考虑数据因为大小带来的影响。
值类型和引用类型在栈和堆中的分配
这里其实有两个规律!
-
在创建引用类型的时候,内存管理会分配一块空间在堆里,然后分配一块内存给栈,用来存储指堆中的内存地址!(没错就是指针。。。。。。。。。。)
-
在创建值类型数据时,内存管理会为他分配一块内存,然后这个内存是放在值类型数据被创建的地方。
这句话说实话我一开始也没懂,尤其是被创建的地方这句话,但是我思考了一下(万能的语文)分为以下两种情况: -
如果这个值类型数据是在方法内部创建的,那么他的内存地址就会跟随方法放到栈中。
-
如果这个值类型数据是引用类型的成员变量,那么他的内存地址就是会跟随引用类型存在堆中。
值类型
啥是值类型?我也没有啥定义啥的,但是搜集的资料中明确指出:byte,short,int,long,float,double,decimal,char,bool 和 struct这几个兄弟被统称为值类型。
引用类型
C#中的引用类型有:class和string
值类型和引用类型的本质区别:
- 值类型分配在线程堆栈上(管理由操作系统负责),引用类型分配在托管堆上(管理由垃圾回收器GC负责)。这里的管理指的是,内存的分配和回收。
- 值类型继承自valueType,valueType继承自System.Object;引用类型直接继承自System.Object。
- 值类型在作用域内结束时,会被操作系统自释放,减少托管堆压力;引用类型则靠GC。因此值类型在性能上由优势。
值传递
用值传递看看吧!
值类型的值传递
啥也不说!直接上代码!
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using static System.Console;
namespace Test3
{
class Program
{
static int pass(int PassNum, int PassNum2)
{
int MaxNumber = PassNum + PassNum2;
return MaxNumber;
}
static void Main(string[] args)
{
int Maxnumber1 = 6;
int Maxnumber2 = 7;
int ShowMax = pass(Maxnumber1, Maxnumber2);
WriteLine(ShowMax);
ReadLine();
}
}
}
运行结果为:
可以看到运行结果是13。
其中的参数的传递就是值类型传递。值类型复制数据本身,形成独立的数据块。
引用类型的值传递:
继续上代码!
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using static System.Console;
namespace Test4
{
class Program
{
static void ChangeArray(int[] CArray)
{
CArray[0] = 888;
CArray = new int[5] { -3, -2, -1, -3, -4 };
WriteLine("在这个方法里面,第一个元素是,{0}", CArray[0]);
}
static void Main(string[] args)
{
int[] CArray = { 1, 2, 3, 4, 5 };
WriteLine("在Main内部,ChangeArray方法调用前,数组的第一个元素是,{0}", CArray[0]);
ChangeArray(CArray);
WriteLine("在Main内部,ChangeArray方法调用后,数组的第一个元素是,{0}", CArray[0]);
ReadLine();
}
}
}
其实我在写这个例子的时候,就很迷在调用ChangeArray这个函数后,怎么参数里的数组是{888,2,3,4,5},在我想象里,应该是{888,-2,-1, -3, -4}:
同样,程序一开始,Main方法给值:
将引用类型值传递给方法后地址:
new的一个数组 更改了传入方法中的数组指针。
调用方法后,相同地址的数组改变了首位数值,所以首位数值为888.因为从数组的地址指针能看到,调用方法后的CArray数组实际上就是一开始Main声明的CArray,而不是后面在方法中new并赋值的CArray。
运行结果:
从此可以看出值传递引用类型数据时,虽然是复制但是会从新开辟地址,将一个数组实际上会变成两个地址,分别储存不同的值,用以调用。
引用传递
值类型的引用传递:
上代码!
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using static System.Console;
namespace Test3
{
class Program
{
static int pass(ref int PassNum,ref int PassNum2)
{
int MaxNumber = PassNum + PassNum2;
return MaxNumber;
}
static void Main(string[] args)
{
int Maxnumber1 = 6;
int Maxnumber2 = 7;
int ShowMax = pass(ref Maxnumber1, ref Maxnumber2);
WriteLine(ShowMax);
ReadLine();
}
}
}
和上文相比区别在于啥?就是ref!
有了这个“前缀”,你要明白:
在这个例子中,传递的不是Maxnumber1和Maxnumber2本身,而是他们的引用。在方法中的参数PassNum和PassNum2不是int类型,而是对int类型的引用。
引用类型的引用传递:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using static System.Console;
namespace Test4
{
class Program
{
static void ChangeArray(ref int[] CArray)
{
CArray[0] = 888;
CArray = new int[5] { -3, -2, -1, -3, -4 };
WriteLine("在这个方法里面,第一个元素是,{0}", CArray[0]);
}
static void Main(string[] args)
{
int[] CArray = { 1, 2, 3, 4, 5 };
WriteLine("在Main内部,ChangeArray方法调用前,数组的第一个元素是,{0}", CArray[0]);
ChangeArray( ref CArray);
WriteLine("在Main内部,ChangeArray方法调用后,数组的第一个元素是,{0}", CArray[0]);
ReadLine();
}
}
}
这倒是没有迷,直接改变了地址。
引用传递引用类型实例的main方法给值:
进入方法后,因为new的原因变了数组地址和数组的值:
直到运行结束,地址都没有再改变。
运行结果如下:
可以看到,引用的方法传递引用参数后,没有复制数组新增数组地址指针,就在原本的新建的数组中进行改值,所以输出的就是-3而不是888。
用return代替引用传递:
其实这个是老师给我留的作业啦!
上代码!
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using static System.Console;
namespace Test4
{
class Program
{
static int[] ChangeArray( int[] CArray)
{
CArray[0] = 888;
CArray = new int[5] { -3, -2, -1, -3, -4 };
WriteLine("在这个方法里面,第一个元素是,{0}", CArray[0]);
return CArray;
}
static void Main(string[] args)
{
int[] CArray = { 1, 2, 3, 4, 5 };
WriteLine("在Main内部,ChangeArray方法调用前,数组的第一个元素是,{0}", CArray[0]);
CArray = ChangeArray(CArray);
WriteLine("在Main内部,ChangeArray方法调用后,数组的第一个元素是,{0}", CArray[0]);
ReadLine();
}
}
}
本来是个值传递,非要获得引用传递的效果:我们就将方法中的数组用return传出,并在mian方法中创建一个变量进行接受即可,当然 变量名可以不同,以表示传出的数组是值传递后的复制的数组。
运行结果如下:
到此结束!谢谢大家!