学习笔记- 认识值类型与引用类型

  认识值类型与引用类型

万变不离其宗,只要搞清楚值类型和引用类型的原理,上面所有题目就都迎刃而解了。

 基本概念

下图清晰了展示了.NET中类型分类,值类型主要是一些简单的、基础的数据类型,引用类型主要用于更丰富的、复杂的、复合的数据类型。

https://pic002.cnblogs.com/img/fan0136/200902/2009020510331710.jpg

 内存结构

值类型和引用类型最根源的区别就是其内存分配的差异,在这之前首先要理解CLR的内存中两个重要的概念:

Stack :线程栈,由操作系统管理,存放值类型、引用类型变量(就是引用对象在托管堆上的地址)。栈是基于线程的,也就是说一个线程会包含一个线程栈,线程栈中的值类型在对象作用域结束后会被清理,效率很高。

GC Heap托管堆:进程初始化后在进程地址空间上划分的内存空间,存储.NET运行过程中的对象,所有的引用类型都分配在托管堆上,托管堆上分配的对象是由GC来管理和释放的。托管堆是基于进程的,当然托管堆内部还有其他更为复杂的结构,有兴趣的可以深入了解。

结合下图理解,变量a及其值3都是存储在栈上面。变量b在栈上存储,其值指向字符串“123”的托管堆对象地址(字符串是引用类型,字符串对象是存储在托管堆上面。字符串是一个特殊的引用类型,后面文章会专门探讨)”

 

 

值类型一直都存储在栈上面吗?所有的引用类型都存储在托管堆上面吗?

1.单独的值类型变量,如局部值类型变量都是存储在栈上面的;

2.当值类型是自定义class的一个字段、属性时,它随引用类型存储在托管堆上,此时她是引用类型的一部分;

4.所有的引用类型肯定都是存放在托管堆上的。

5.还有一种情况,同上面题目12,结构体(值类型)中定义引用类型字段,结构体是存储在栈上,其引用变量字段只存储内存地址,指向堆中的引用实例。

 

 对象的传递

将值类型的变量赋值给另一个变量(或者作为参数传递),会执行一次值复制。将引用类型的变量赋值给另一个引用类型的变量,它复制的值是引用对象的内存地址,因此赋值后就会多个变量指向同一个引用对象实例。理解这一点非常重要,下面代码测试验证一下:

int v1 = 0;
            int v2 = v1;
            v2 = 100;
            Console.WriteLine("v1=" + v1); //输出:v1=0
            Console.WriteLine("v2=" + v2); //输出:v2=100

            User u1=new User();
            u1.Age = 0;
            User u2 = u1;
            u2.Age = 100;
            Console.WriteLine("u1.Age=" + u1.Age); //输出:u1.Age=100
            Console.WriteLine("u2.Age=" + u2.Age); //输出:u2.Age=100,因为u1/u2指向同一个对象

当把对象作为参数传递的时候,效果同上面一样,他们都称为按值传递,但因为值类型和引用类型的区别,导致其产生的效果也不同。

参数-按值传递:

private void DoTest(int a)
        {
            a *= 2;
        }

        private void DoUserTest(User user)
        {
            user.Age *= 2;
        }

        [NUnit.Framework.Test]
        public void DoParaTest()
        {
            int a = 10;
            DoTest(a);
            Console.WriteLine("a=" + a); //输出:a=10
            User user = new User();
            user.Age = 10;
            DoUserTest(user);
            Console.WriteLine("user.Age=" + user.Age); //输出:user.Age=20
        }

上面的代码示例,两个方法的参数,都是按值传递

  • 对于值类型(int a) :传递的是变量a的值拷贝副本,因此原本的a值并没有改变。
  • 对于引用类型(User user) :传递的是变量user的引用地址(User对象实例的内存地址)拷贝副本,因此他们操作都是同一个User对象实例。

参数-按引用传递:

按引用传递的两个主要关键字:out  ref不管值类型还是引用类型,按引用传递的效果是一样的,都不传递值副本,而是引用的引用(类似c++的指针的指针)。out  ref告诉编译器方法传递额是参数地址,而不是参数本身,理解这一点很重要。

代码简单测试一下,如果换成out效果是相同的

private void DoTest( ref int a)
        {
            a *= 2;
        }

        private void DoUserTest(ref User user)
        {
            user.Age *= 2;
        }

        [NUnit.Framework.Test]
        public void DoParaTest()
        {
            int a = 10;
            DoTest(ref a);
            Console.WriteLine("a=" + a); //输出:a=20 ,a的值改变了
            User user = new User();
            user.Age = 10;
            DoUserTest(ref user);
            Console.WriteLine("user.Age=" + user.Age); //输出:user.Age=20
        }

out  ref的主要异同

  • out  ref都指示编译器传递参数地址,在行为上是相同的;
  • 他们的使用机制稍有不同,ref要求参数在使用之前要显式初始化,out要在方法内部初始化;
  • out  ref不可以重载,就是不能定义Method(ref int a)Method(out int a)这样的重载,从编译角度看,二者的实质是相同的,只是使用时有区别;

 

常见问题

https://images.cnblogs.com/cnblogs_com/gaoyuchuanit/%D6%B5%C0%E0%D0%CD%BA%CD%D2%FD%D3%C3%C0%E0%D0%CD%C7%F8%B1%F0.jpg

C#中sealed关键字
1. sealed关键字
    当对一个类应用 sealed 修饰符时,此修饰符会阻止其他类从该类继承。类似于Java中final关键字。
    在下面的示例中,类 B 从类 A 继承,但是任何类都不能从类 B 继承。
2. sealed 修饰方法或属性
    能够允许类从基类继承,并防止它们重写特定的虚方法或虚属性。
    1)sealed是对虚方法或虚属性,也就是同override一起使用,如果不是虚方法或虚属性会报出错误:cannot be sealed because it is not an override

在C#中无法显示的重写Finalize方法,只能通过析构函数语法形式来实现。Finalize方法不能被继承或重载。 析构函数不能加任何修饰符,不能带参数,也不能被显示调用,唯一的例外是在子类重写时,通过base调用父类Finalize方法,而且这种方式也被隐式封装在析构函数中。

C# 可空类型(Nullable)
C# 提供了一个特殊的数据类型,nullable 类型(可空类型),可空类型可以表示其基础值类型正常范围内的值,再加上一个 null 值。
 

文件转载至:http://www.cnblogs.com/anding/p/5229756.html

 

 

 

猜你喜欢

转载自blog.csdn.net/qq_25744257/article/details/81745280