C# - 值类型和引用类型

值类型和引用类型(Value Type & Reference Type)html

.NET使用两种不一样的物理内存来存储数据,数据类型可简单分为值类型和引用类型。当声明一个值类型的变量后,系统会在栈(stack)中分配适当的内存来存储值类型的数据。而引用类型的变量虽然也利用栈,但栈上的地址是对堆(heap)地址的引用,引用类型的数据都存储在托管堆上,栈存储的是引用类型在托管堆上的地址的一个引用。数据库

 

赋值

值类型的赋值

int x = 100;
x = 200;//x指向的数据在栈上会被擦除同时在这个位置上从新填充200的二进制数。

int x = 100;
int y = x;//将x的数据拷贝给x,两个变量互不相干

值类型之间的相互赋值会产生副本,若是将100赋值给x,那么当你把x赋值给y时就会发生一次值拷贝。这样,x和y都有一个相同的值,但两个变量指向栈上的地址是不同的。也即y是拷贝了x的副本,它们各自有本身的版本。即便你把x传递给一个方法,在方法体内部改变这个x,原来的x也并未改变,方法接收的这个x将是原来那个x的一份拷贝。数组

string类型的赋值

string比较特殊,自己string一旦建立就不能被改变,当为它赋予一个新字符时,原来那个字符还驻留在内存中,只不过再也不有变量指向原来那个字符在堆上的地址而已。即便更改一个string的大小写,也不会真的改变这个字符,这只会在堆上建立一个新字符。为了下降内存占用,C#的字符串还有留用机制,假设某个字符串在内存中已经有了地址,那么假如两个变量的值都是该字符串,则两个字符串的引用地址是相等的。但字符串留用机制只针对常量,因此下面第二个测试引用相等性的输出是false。安全

string a = "sam";
a = "korn"; //a指向了新的地址,但sam还在内存中,未被擦除,若是频繁使用string类型就会形成内存浪费,建议使用StringBuilder建立字符串

string key = "a";
string key1 = "aaa";
string key2 = "aaa";
Console.WriteLine(object.ReferenceEquals(key1, key2)); //true
Console.WriteLine(object.ReferenceEquals($"{key}{key1}", $"{key}{key2}")); //false

引用类型的赋值

Animal a = new Animal();
Animal b = a;//将a赋值给b时,a是将栈上存储的对自身在堆上地址的引用赋值给了b,这样,b对自身在堆上地址的引用被擦除,从新填充了一个指向a指向的地址。但b原来指向的那个地址上的对象并未被擦除。

方法中的参数赋值

将值类型变量传递给方法,方法会创建值类型变量的拷贝,将引用类型变量传递给方法,方法会创建引用类型变量的地址的拷贝。也即值类型参数在方法中是独立的,与外部的那个变量没有关系,在方法内部改变这个变量不会影响外部那个变量,而引用类型由于传递的是引用地址,因此在方法中改变该变量会同时改变外部的那个变量。ide

数组的赋值 

将一个数组赋值给另外一个数组时,是将前一个数组的引用传递给另外一个数组,但传递一个数组给某个方法时,发生的是值拷贝。函数

int[] a = { 1, 2, 3 };
int[] b = a;
b[0] = 10;
b[1] = 20;
b[2] = 30;
Console.WriteLine($"{a[0]}{a[1]}{a[2]}"); //print 十、20、30

从占用内存空间上考虑,值类型的释放明显快于引用类型,由于存储在栈中的数据一旦离开做用域(块)就会被马上销毁而不用等待垃圾收集器来完成销毁的工做,试考虑在一个方法中定义了一个值类型的数据,一旦方法执行结束,该值会被马上清除,又假设在方法中定义一个引用类型x,方法中调用了另外一个方法,假设另外一个方法也调用了其它方法而且每个方法都引用了x,那么x是不可能立刻被销毁的,由于引用类型不创建拷贝,堆上的数据被改变,那么引用这个地址的变量都会被改变。从执行效率上考虑,当拷贝发生时,引用类型比值类型更高效,执行效率更快,由于它不须要副本,只须要拷贝一个堆地址的引用。值类型却须要在栈上分配内存空间存储副本数据。针对不一样的状况应采起不一样的方式处理这个问题。性能

 

与类型相关的null、Nullable<T>和void

null能够赋值给引用类型的变量,它表明的含义是未指向堆上的任何地址。若是x="",则x指向了""在堆上存储的地址,因此null!=""。学习

值类型不能够被赋值为null,但数据库表的值类型却能够为null,从表里查询的数据若是是null则没办法赋值给C#的值类型,为了解决这个问题,从2.0开始可使用Nullable<T>来表示一个能够为空的值类型。可使用可空修饰符?来代表任意类型是能够被赋值为null的。测试

void是在声明方法时使用,代表该方法没有任何返回类型。大数据

 

字面量

直接写出的一个值就是字面量(值)。如12345六、"aa"。但string x="aa"则不是。

 

值类型(Value Type)

值类型分类

值类型分为枚举(Enum)和结构(Struct)。 

2-1.值类型

内置的值类型就是struct类型,从小到大依次为:sbyte<short<int<long<float<double<decimal

2-1-1.整数 

byte整数(System.Byte)(无符号)
sbyte整数(System.SByte )(带符号)
以上两个最大存储8位二进制整数
byte称为字节,一个byte[]数组每一个元素只存储1B(1个字节),因此byte[]的length就是字节总数,1024B=1KB(千字节),1024KB=1MB,单位转换时小转大用除法,大转小用乘法便可

ushort整数(System.UInt16)(无符号)
short整数(System.Int16)(带符号)
以上两个最大存储16位二进制整数

uint整数(System.UInt32)(无符号)  
int整数(System.Int32)(带符号)  
以上两个最大存储32位二进制整数
9999999999的二进制数是1001 0101 0000 0010 1111 1000 1111 1111 11,该二进制数有34个bit位,int就不能存储该数。 

ulong整数(System.UInt64)(无符号)  
long整数(System.Int64)(带符号)  
以上两个最大存储64位二进制整数
 
decimal(System.Decimal)128位十进制数

2-1-2.浮点数 

float小数(System.Single)(最大存储32位)单精度类型,精确到小数点后6-7位。
double小数(System.Double)(最大存储64位)双精度类型,精确到小数点后15-16位。

2-1-3.字符数 

char(System.Char)(最大存储16个位)

字符将被自动转换为其对应的UTF-16编码,一个英文字符对应的UTF-16编码占一个字节,一个中文字符对应的UTF-16编码占两个字节。char类型的字符不能使用双引号,只能使用单引号括起来。

char.IsWhiteSpace(str, index)
//指定索引处是否为空字符串

char.IsPunctuation(charStr)
//参数是不是标点符号

2-3.自定义结构 

全部值类型或自定义的结构类型都派生自System.ValueType类。 

2-2.布尔型 

bool(System.Boolean)(存储8个位),实际上只须要一个位就能够存储布尔值,但它实际占用8个位 

注意

C#编译器默认整数类型的字面量是int类型,若是int存储不了该值则默认是long类型,浮点数则被默认是double类型。即便你把1赋值给一个byte类型的变量,该值也是Int32类型。

当你用一个byte类型的变量存储整数值时,该值被默认为是int,但实际存储的只有8个位。若是该变量参与数学运算,则它的值又会被当作int,在C#中整数类型都是以int或long进行计算,C#并无为byte等类型重写任何数学运算符。另外,整数类型相除若是预期结果有小数,小数不会保留,计算这样的结果应使用浮点数类型。

byte x = 3//字面量3默认是int类型,但x存储它只用8个位
byte y = 2//字面量2默认是int类型,但y存储它只用8个位
byte z = ( byte ) ( x + y ); //整数计算时若是值在栈上的存储不满32个位则以0填充,待满32个位后才会进行计算。因此此处x + y是两个int相加,结果是int,int转byte属于大转小,因此显示转换一下才行。

字面量后缀

能够为值类型的字面量指定后缀以转换该值被C#默认为的类型,可用的后缀(不区分大小写)有:m、d、f、u、l、ul,分别表示:decimal、double、float、uint、long、ulong。

decimal x = 100m//默认int被转为了decimal
uint y = 100u;
float z = 0.1f;
show ( 1.234566666654333 ); //1.234566666654333默认是double类型,它会丢失一个精度,最后一个3不会输出。
show ( 1.234566666654333M ); //将其当作decimal输出

值类型的方法 

int c = int.MaxValue;
int z = int.MinValue;
int h = int.Parse("1");
int result=0;
bool k = int.TryParse("123"out result);

值类型的转换

隐式转换

小转大就是隐式转换。也即隐式转换老是发生于位数小的类型转位数大的类型。

强制转换

大转小就是强制转换,也即位数大的类型转位数小的类型可能会丢失精度(sbyte转int没问题,int转flota没问题,倒过来则是错误的),编译器会及时提示错误。需考虑强转。

值类型变量在运行时的内存分配

计算机以数字的二进制形式进行存储。当声明了一个值类型的变量时,系统会根据它可存储的bit位数来进行内存分配(划分)。下图是cpu的栈位,0-7是8个位,8个位=1个字节。内存编号是存储数据的地址,变量标识符(变量名)指向了数据存储的物理地址(内存编号)。

数值的存储规则

1.该数的二进制数的位占不满内存划分的位时,系统会把0放置在该二进制数以前进行填充直到占满为止。

根据你声明的值类型的可存储最大位数,cpu自动为其划分对应位数的栈,下图涂色区域是可存储8位的栈,其它以此类推。

如今假设咱们要声明一个int类型的变量来存储3。

数字3的二进制数是11,该数只占2个bit位,2个位占不满int所声明的32个位,因此11前面会被填充30个0:

0000 0000 0000 0000 0000 0000 0000 0011(看下图)

补0时从内存编号的高位开始补起,下图中能够看到是从10000003的区域开始补0:

999999999的二进制数是1110 1110 0110 1011 0010 0111 1111 11,该数只占30个位,30个位占不满int所声明的32个位,因此该数前面会被填充2个0:

0011 1011 1001 1010 1100 1001 1111 1111(看下图,3的二进制数已占满从内存编号10000000开始到10000003的区域,因此999999999的二进制数将划分在后面,灰色部分)

补0(补码)时从内存编号的高位开始补起,下图中能够看到是从10000007的区域开始补0:

2.负数的存储是把当前数字的绝对值的满位后的二进制数按位取反再+1的形式来表示,流程是:1.取绝对值。2.转化为二进制数。3.不满位数则以0补位。4.按位取反:1变0,0变1。5.用结果数+1。6.用结果数逢二进一。按位取反称为反码,+1称为补码。

假设如今要用short存储数字-1000,绝对值1000的二进制数是1111 1010 00,该数只有10位,前面要补6个0获得0000 0011 1110 1000,每一个位取相反数(1的相反数是0)获得:1111 1100 0001 0111,1111 1100 0001 0111+1=1111 1100 0001 0112,逢二进一获得:1111 1100 0001 1000,首位的1会被计算机识别为负号,负号占了1个位。以下图:

写个程序检测一下:

static void Main( string [ ] args ) 
{
    short i = 1000;
    string s = Convert.ToString ( i, 2); //将i转换为二进制的字符表示
    Console.WriteLine ( s );
}

  

引用类型(Reference Type)

引用类型分为三种

1.类(Class)

2.接口(Interface)

3.委托(Delegate)

引用类型变量在运行时的内存分配

计算机以数字的二进制形式进行存储。当声明了一个引用类型的变量时,系统会在栈上默认为其划分32个位用于存储该标识符对该对象地址的引用。这个分配内存的流程以下:

public class Animal { public int ID; public short NameCode  }
class Program
{
    static void Main(string[] args)
    {
        Animal animal;
    }
}

在Main中声明了一个Animal类型的变量时,系统在栈上份内存并把每一个位所有都刷成0,如图:

接着你new一个对象

static void Main(string[] args)
{
    Animal animal;
    animal = new Animal();
}

此时系统会扫描该对象的成员,上面咱们在该对象的类里定义了一个32位的ID和一个16位的NameCode ,计算后获得48个位,系统就在堆上面为其划分48个位用来存储该对象。

从30000001开始先分配32个位,接着分配16个位。完成后,须要把对象在堆上的起始地址(内存编号)30000001转换为二进制数,这个二进制数会被填充到栈上,栈就完成了对堆的引用。30000001转换为二进制数获得:

1110 0100 1110 0001 1100 0000 1  (4*6=24位,不够32位,因此在其高位补足7个0)获得:

0000 0001 1100 1001 1100 0011 1000 0001 (恰好32位)

这个数字会被填充到刚才被刷成0的栈上,这样,栈就完成了对实例对象的引用,30000001的二进制数就成为了指向Animal对象的真正地址。30000001的二进制数填充到栈后如图:

这样animal这个变量在栈上的数据就被刷成了一个内存上的物理地址,这个地址指向了该变量所对应的对象的真正数据。

如今假设你要把animal赋值给另外一个变量,如图:

Animal animal2 = animal;

此时,系统会把animal在栈上的内存编号所引用的地址(30000001的二进制表示)copy、填充到animal2在栈上的内存编号所占用的位,假设此时10000004到10000007已经被其它数据占满,那么这个copy会在10000008处开始填充,如图:

若是类型里有引用类型的成员,好比一个string类型的成员,那么系统一样会在堆上(而非栈上)为该成员变量分配32个位用来存储能指向它数据的地址,而后在堆上另辟一块区域去存储它的值。

public class Student
{
    uint ID;
    string Name;
}

方法运行时的内存分配

方法在执行时,系统会在栈的高(内存编号从高到低,从下往上为函数划份内存)位上为该方法分配一个stack frame的空间。分配完成后stack frame以下图,栈帧用于存储函数做用域并执行。做用域中为方法的变量分配栈空间,一条规则是这样的:在哪一个方法中声明哪些变量,那么那些变量就由那个方法负责为它们划份内存。因此在如下在A方法的做用域中声明了两个byte类型的变量x和y,因此在栈帧包含的栈上为x和y划分了两个字节,A方法还接收了两个参数,由于参数是byte类型,因此还会发生值拷贝,这样,A方法还须要为i和z划份内存,Main方法中声明了i和z,因此在Main方法的栈帧上会为i和z划份内存,Main方法还接收一个string类型的args参数,因此还须要为args在堆上划份内存,再把堆地址填充到Main的栈帧所包含的栈上。

static void Main(string[] args)
{
    byte i = 1;
    byte z = 3;
    A(i,z);
}

static public byte A(byte i, byte z)
{
    byte x = 4;
    byte y = 5;
    return (byte)(i + z);
}

以上的Main方法是caller,因此调用的A方法的两个参数i和z的内存分配就划归给Main管理。看图:

栈溢出(stack overflow)

栈溢出(stack overflow)就是由于运行时,方法的stack frame空间是由高位向低位划份内存,若是方法有返回值,return后函数终止,它区块内的全部变量就会当即被销毁,但若是一直没有rentun,好比无限递归,这会形成一直向上划份内存,直到低位的区块被完全占满,最终就会致使栈溢出。 

结语

最后咱们须要知道,程序运行时,不管是值类型抑或引用类型,当程序执行到声明这些变量的代码的时候,就会为其划分对应的内存,而后将每个位都刷成0,直到赋值后才会有数据。这就是为何当声明一个变量却不使用它时,编译器会提示你还未使用过该变量,由于当程序运行起来后未使用的变量会浪费内存资源。

 

装箱与拆箱(Boxing&UnBoxing)

咱们能够把栈当作小盒子,把堆当作大箱子。装箱拆箱是指不一样数据类型之间的转换

装箱(小盒子装进大箱子,值类型变引用类型)

//声明了int类型的变量x,当即在栈上划分32个位,填充100的二进制数
int x = 100;
//声明了引用类型的变量obj,当即在栈上划分32个位
//计算定义在object类型中的成员须要占多少空间,再在堆上划分对应大小的空间
//将x赋值给obj,将x指向的值拷贝一份往堆上存储,再把这个值在堆上的地址存储到栈上
//转换的过程就是这么麻烦,因此大量的装箱操做就会发生性能损耗
object obj = x;

拆箱(大箱子拆成小盒子,引用类型变值类型)

int x = 100;
object obj = x;
//声明了int类型的变量y,当即在栈上划分32个位
//将obj指向的堆上的数据拷贝一份往栈上存储
int y =(int)obj;

由于值类型较小而引用类型较大,把小数据装进大箱子是装得下的,因此装箱是属于隐式进行。把大数据装进小盒子不必定装得下,对象装进小盒子就有可能拆掉大箱子后都装不下,因此拆箱是属于显示或强制进行,系统不会自动为你转换,这须要你本身手动显示或强制转换,装箱拆箱须要一个装或拆的过程,因此大量装和拆就会形成性能损耗。

 

对象的浅拷贝与深拷贝(Shallow Copy & Deep Copy)

假设有一个Animal类型的变量,若是直接把这个变量赋值给另外一个Animal变量,这个行为不叫拷贝,应叫作赋值,这会使两个Animal变量指向同一个堆上的地址。如今假设Animal有一个int类型的ID字段和一个Person类型的person字段,当拷贝Animal对象时,你有两个选择:

1.只拷贝Animal对象的ID,只拷贝Animal对象的person指向的堆地址,此为浅拷贝。

2.拷贝Animal对象的ID,拷贝Animal对象的person的数据,此为深拷贝。

实现浅拷贝

你可使用Object的MemberwiseClone方法建立对某对象的浅拷贝。MemberwiseClone是一个受保护的方法,只能在Object的派生类的类代码块中使用,该方法返回一个浅拷贝的对象,该对象只拷贝了源对象的值类型的成员,而引用类型的成员则只有一个堆引用地址。

//部门
public class Department
{
    public string Name;
    public Department()
    {
    }
}

//人员
public class Person
{
    public Department Department { get; set; }

    public Person() { }
    public Person GetCopy()
    {
        return MemberwiseClone() as Person;
    }
}
class Program
{
    static void Main(string[] args)
    {
        Person p = new Person();
        p.Department = new Department();
        p.Department.Name = "科技部";
        Person p2 = p.GetCopy(); //将p浅拷贝赋给p2
        p2.Department.Name = "开发部"; //p2.Department拷贝的是p.Department在堆上的地址
        Console.WriteLine(p.Department.Name); //print 开发部
    }
}

实现深拷贝

从上面代码的结果可知,改变p2的成员department,则p的department也会跟着被改变。由于p2的department修改了堆上的值,而深度拷贝能够解决这个问题。

// 利用二进制序列化和反序列实现深拷贝
public static T DeepCopyWithBinarySerialize<T>(T obj)
{
    object retval;
    using (MemoryStream ms = new MemoryStream())
    {
        BinaryFormatter bf = new BinaryFormatter();
        // 序列化成流
        bf.Serialize(ms, obj);
        ms.Seek(0, SeekOrigin.Begin);
        // 反序列化成对象
        retval = bf.Deserialize(ms);
        ms.Close();
    }
    return (T)retval;
}

转换

对于值类型来讲,小转大,大能存储小,因此隐式转换就能够完成。 大转小,小不能存储大,不被容许,因此必须显示甚至强制转换。对于引用类型来讲,子类转父类/基类,子派生自父类/基类,因此隐式转换就能够完成。父类/基类转小,父类/基类并不从子类派生,因此不存在转换问题。 

隐式转换

隐式转换:直接赋值 

 相同类型之间,小转大,可隐式转换。隐式转换就是编译器自动进行转换,不须要你亲自动手。

sbyte x = 10;
short y = x;//8位存入16位,小转大,可隐式转换
namespace Test
{
    public class Animal
    {
        public void Eat( ) { }
    }

    public class Person : Animal
    {
        public void Job( ) { }
    }

    class Program
    {
        static void Main( string [ ] args )
        {
            Person p = new Person ( );
            p.Job ( );//具备job方法
            Animal a = p;//子类转换为父类/基类对象后(a),a会丢失子类的Job()方法
        }
    }
}

显示转换

显示转换:(类型)变量 

相同类型之间,大转小,精度丢失,编译器会提示错误,此时须要你亲自动手显示转换。

short h;
int z = 10;
= z; //提示错误,32位存入16位,精度丢失,不可隐式转换,需显示转换
= (short)z; //显示转换

强制转换

强制转换:Convert.Toxxx( )方法 | Parse()方法

不一样类型之间进行转换时才须要强转,编译器没法推测转换结果,这种转换若是出错只能在运行时抛出异常。也即强制转换就是告诉编译器不要插手个人逻辑,我对个人行为负责。一般状况都是须要将一个object类型转换为其它类型时使用强转。

安全强制转换

安全强制转换:TryParse()方法 | as操做符

这是最保险的方法,TryPrase方法是值类型的方法,它接受两个参数,一个是被转换的操做数,另外一个是out类型的操做数。该方法测试操做数是否可被转换,并返回一个bool值,若是结果为真,就把转换结果给out变量,为假则不。as操做符是引用类型的操做符,它测试当前操做数的类型是否能够转换为目标类型,若是不能则返回null,该操做符不会由于转换失败抛出异常。

非转换

非转换:toString()

任何类型都继承了Object类,它提供了toString()方法,既然是任何类型,则结构类型一样可使用toString(),但null由于没有指向堆上的地址,因此为null的变量使用该方法会抛错。

static void Main(string[] args)
{
    int x = 100;
    x.ToString();//并未发生装箱,不存在转换操做,由于此方法是从Object继承
}
View Code

自定义转换

自定义显示转换:关键字explicit

namespace Test
{
    public class A
    {
        public static explicit operator A(B obj)
        {
            A a = new A();
            return a;//建立对象返回给须要转换的变量obj。
        }
        public void Show()
        {
            Console.WriteLine("转换成功");
        }
    }
    public class B { }
    class Program
    {
        static void Main(string[] args)
        {
            B b = new B();
            A newObj = (A)b;
            newObj.Show();
        }
    }
}
View Code

自定义隐式转换

自定义隐式转换:关键implicit 

public static implicit operator A(B obj)
//……
B b = new B();
A newObj = b;//隐式转换
View Code

 

 

内存栈堆.下载!

C# - 学习总目录

相关文章
相关标签/搜索