dotNET面试(二)

值类型与引用类型

1.值类型和引用类型的区别?

  • 值类型包括简单类型、结构体类型和枚举类型,引用类型包括自定义类、数组、接口、委托等。
  • 赋值方式:将一个值类型变量赋给另一个值类型变量时,将复制包含的值。这与引用类型变量的赋值不同,引用类型变量的赋值只复制对象的引用(即内存地址,类似C++中的指针),而不复制对象本身。
  • 继承:值类型不可能派生出新的类型,所有的值类型均隐式派生自 System.ValueType。但与引用类型相同的是,结构也可以实现接口。
  • null:与引用类型不同,值类型不可能包含 null 值。然而,可空类型功能允许将 null 赋给值类型。
  • 每种值类型均有一个隐式的默认构造函数来初始化该类型的默认值,值类型初始会默认为0,引用类型默认为null。
  • 值类型存储在栈中,引用类型存储在托管堆中。

2. 结构和类的区别?
结构体是值类型,类是引用类型,主要区别如上述。其他的区别:
结构不支持无参构造函数,不支持析构函数,并且不能有protected修饰
结构常用于数据存储,类class多用于行为
class需要用new关键字实例化对象,struct可以不使用new关键字
class可以为抽象类,struct不支持抽象

3. delegate是引用类型还是值类型?enum、int[]和string呢?
enum枚举是值类型,其他都是引用类型。

4. 堆和栈的区别?
线程堆栈:简称栈 Stack
托管堆: 简称堆 Heap
值类型大多分配在栈上,引用类型都分配在堆上;
栈由操作系统管理,栈上的变量在其作用域完成后就被释放,效率较高,但空间有限。堆受CLR的GC控制;栈是基于线程的,每个线程都有自己的线程栈,初始大小为1M。堆是基于进程的,一个进程分配一个堆,堆的大小由GC根据运行情况动态控制。

5.“结构”对象可能分配在堆上吗?什么情况下会发生,有什么需要注意的吗?
结构是值类型,有两种情况会分配在堆上面:
结构作为class的一个字段或属性,会随class一起分配在堆上面;
装箱后会在堆中存储,尽量避免值类型的装箱,值类型的拆箱和装箱都有性能损失

6. 理解参数按值传递?以及按引用传递?
按值传递:对于值类型传递它的值拷贝副本,而引用类型传递的是引用变量的内存地址,他们还是指向的同一个对象。
按引用传递:通过关键字out和ref传递参数的内存地址,值类型和引用类型的效果是相同的。

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

8.有几种方法可以判定值类型和引用类型?
简单来说,继承自System.ValueType的是值类型,反之是引用类型。

9. C#支持哪几个预定义的值类型?支持哪些预定义的引用类型?
值类型:整数、浮点数、字符、bool和decimal
引用类型:Object,String

10. 说说值类型和引用类型的生命周期?
值类型在作用域结束后释放,引用类型由GC垃圾回收器回收。

11.如果结构体中定义引用类型,对象在内存中是如何存储的?例如下面结构体中的class类 User对象是存储在栈上,还是堆上?

public struct MyStruct
{
public int Index;
public User User;
}

MyStruct存储在栈中,其字段User的实例存储在堆中,MyStruct.User字段存储指向User对象的内存地址。

 装箱与拆箱

1.什么是拆箱和装箱?装箱就是值类型转换为引用类型,拆箱就是引用类型(被装箱的对象)转换为值类型。
2.什么是箱子?就是引用类型对象。
3.箱子放在哪里?托管堆上。
4.装箱和拆箱有什么性能影响?装箱和拆箱都涉及到内存的分配和对象的创建,有较大的性能影响。
5.如何避免隐身装箱?编码中,多使用泛型、显示装箱。
6.箱子的基本结构?
上面说了,箱子就是一个引用类型对象,因此它的结构,主要包含两部分:
a.值类型字段值;
b.引用类型的标准配置,引用对象的额外空间:TypeHandle和同步索引块。
7.装箱的过程?

  1. 在堆中申请内存,内存大小为值类型的大小,再加上额外固定空间(引用类型的标配:TypeHandle和同步索引块);
  2. 将值类型的字段值(x=1023)拷贝新分配的内存中;
  3. 返回新引用对象的地址(给引用变量object o)

8.拆箱的过程?

  1. 检查实例对象(object o)是否有效,如是否为null,其装箱的类型与拆箱的类型(int)是否一致,如检测不合法,抛出异常;
  2. 指针返回,就是获取装箱对象(object o)中值类型字段值的地址;
  3. 字段拷贝,把装箱对象(object o)中值类型字段值拷贝到栈上,意思就是创建一个新的值类型变量来存储拆箱后的值。

string与字符串操作

1.字符串是引用类型类型还是值类型?引用类型。
2.在字符串连加处理中,最好采用什么方式,理由是什么?
少量字符串连接,使用String.Concat,大量字符串使用StringBuilder,因为StringBuilder的性能更好,如果string的话会创建大量字符串对象。
3.使用 StringBuilder时,需要注意些什么问题?
少量字符串时,尽量不要用,StringBuilder本身是有一定性能开销的;
大量字符串连接使用StringBuilder时,应该设置一个合适的容量。

类与接口

1. 所有类型都继承System.Object吗?
基本上是的,所有值类型和引用类型都继承自System.Object,接口是一个特殊的类型,不继承自System.Object。
2. 解释virtual、sealed、override和abstract的区别

  • virtual申明虚方法的关键字,说明该方法可以被重写;
  • sealed说明该类不可被继承;
  • override重写基类的方法;
  • abstract申明抽象类和抽象方法的关键字,抽象方法不提供实现,由子类实现,抽象类不可实例化。

3. 接口和类有什么异同?
不同点:
1、接口不能直接实例化。
2、接口只包含方法或属性的声明,不包含方法的实现。
3、接口可以多继承,类只能单继承。
4、类有分部类的概念,定义可在不同的源文件之间进行拆分,而接口没有(接口也可分部)。
5、表达的含义不同,接口主要定义一种规范,统一调用方法,也就是规范类,约束类,类是方法功能的实现和集合。
相同点:
1、接口、类和结构都可以从多个接口继承。
2、接口类似于抽象基类:继承接口的任何非抽象类型都必须实现接口的所有成员。
3、接口和类都可以包含事件、索引器、方法和属性。
4. 抽象类和接口有什么区别?
1、继承:接口支持多继承;抽象类不能实现多继承。
2、表达的概念:接口用于规范,更强调契约,抽象类用于共性,强调父子。抽象类是一类事物的高度聚合,那么对于继承抽象类的子类来说,对于抽象类来说,属于"Is A"的关系;而接口是定义行为规范,强调“Can Do”的关系,因此对于实现接口的子类来说,是"行为需要按照接口来完成"。
3、方法实现:对抽象类中的方法,即可以给出实现部分,也可以不给出;而接口的方法(抽象规则)都不能给出实现部分,接口中方法不能加修饰符。
4、子类重写:继承类对于两者所涉及方法的实现是不同的。继承类对于抽象类所定义的抽象方法,可以不用重写,也就是说,可以延用抽象类的方法;而对于接口类所定义的方法或者属性来说,在继承类中必须重写,给出相应的方法和属性实现。
5、新增方法的影响:在抽象类中,新增一个方法的话,继承类中可以不用作任何处理;而对于接口来说,则需要修改继承类,提供新定义的方法。
6、接口可以作用于值类型(枚举可以实现接口)和引用类型,抽象类只能作用于引用类型。
7、接口不能包含字段和已实现的方法,接口只包含方法、属性、索引器、事件的签名,抽象类可以定义字段、属性、包含有实现的方法。
5. 重载与覆盖的区别?
重载:当类包含两个名称相同但签名不同(方法名相同,参数列表不相同)的方法时发生方法重载。用方法重载来提供在语义上完成相同而功能不同的方法。
覆写:在类的继承中使用,通过覆写子类方法可以改变父类虚方法的实现。
主要区别:
1、方法的覆盖是子类和父类之间的关系,是垂直关系;方法的重载是同一个类中方法之间的关系,是水平关系。
2、覆盖只能由一个方法,或只能由一对方法产生关系;方法的重载是多个方法之间的关系。
3、覆盖要求参数列表相同;重载要求参数列表不同。
4、覆盖关系中,调用那个方法体,是根据对象的类型来决定;重载关系,是根据调用时的实参表与形参表来选择方法体的。
6. 在继承中new和override相同点和区别?看下面的代码,有一个基类A,B1和B2都继承自A,并且使用不同的方式改变了父类方法Print()的行为。测试代码输出什么?为什么?

 1 class Program
 2 {
 3     static void Main(string[] args)
 4     {
 5         B1 b1 = new B1();
 6         B2 b2 = new B2();
 7         b1.Print();
 8         b2.Print();//输出 B1、B2
 9         A ab1 = new B1();
10         A ab2 = new B2();
11         ab1.Print();
12         ab2.Print();//输出 B1、A
13         Console.ReadKey();
14     }
15 }
16 public class A
17 {
18     public virtual void Print()
19     {
20         Console.WriteLine("A");
21     }
22 }
23 public class B1 : A
24 {
25     public override void Print()
26     {
27         Console.WriteLine("B1");
28     }
29 }
30 public class B2 : A
31 {
32     public new void Print()
33     {
34         Console.WriteLine("B2");
35     }
36 }
View Code

7.class中定义的静态字段是存储在内存中的哪个地方?为什么会说它不会被GC回收?
随类型对象存储在内存的加载堆上,因为加载堆不受GC管理,其生命周期随AppDomain,不会被GC回收。

常量、字段、属性、特性与委托

1. const和readonly有什么区别?
const关键字用来声明编译时常量,readonly用来声明运行时常量。都可以标识一个常量,主要有以下区别:
1、初始化位置不同。const必须在声明的同时赋值;readonly即可以在声明处赋值,也可以在构造方法里赋值。
2、修饰对象不同。const即可以修饰类的字段,也可以修饰局部变量;readonly只能修饰类的字段 。
3、const是编译时常量,在编译时确定该值,且值在编译时被内联到代码中;readonly是运行时常量,在运行时确定该值。
4、const默认是静态的;而readonly如果设置成静态需要显示声明 。
5、支持的类型时不同,const只能修饰基元类型或值为null的其他引用类型;readonly可以是任何类型。

 1 namespace ConsoleApp
 2 {
 3     class Program
 4     {
 5         public readonly int PORT;
 6         static void Main(string[] args)
 7         {
 8             B a = new B();
 9             Console.WriteLine(a.PORT);
10             Console.WriteLine(B.PORT2);
11             Console.WriteLine(a.PORT3.ccc);
12             Console.ReadKey();
13         }
14     }
15     class B
16     {
17         //readonly即可以在声明处赋值,也可以在构造方法里赋值
18         public readonly int PORT;
19         //const必须在声明的同时赋值
20         public const int PORT2 = 10;
21         //public const A PORT3 = new A() error const只能修饰基元类型或值为null的其他引用类型
22         //readonly可以是任何类型
23         public readonly A PORT3 = new A();
24         public B()
25         {
26             PORT = 11;
27         }
28     }
29    class A
30    {
31        public string ccc = "aaaaa";
32    }
33 }
View Code

2. 字段与属性有什么异同?
•属性提供了更为强大的,灵活的功能来操作字段
•出于面向对象的封装性,字段一般不设计为Public
•属性允许在set和get中编写代码
•属性允许控制set和get的可访问性,从而提供只读或者可读写的功能 (逻辑上只写是没有意义的)
•属性可以使用override 和 new
3. 静态成员和非静态成员的区别?
•静态变量使用 static 修饰符进行声明,静态成员在加类的时候就被加载(上一篇中提到过,静态字段是随类型对象存放在Load Heap上的),通过类进行访问。
•不带有static 修饰符声明的变量称做非静态变量,在对象被实例化时创建,通过对象进行访问 。
•一个类的所有实例的同一静态变量都是同一个值,同一个类的不同实例的同一非静态变量可以是不同的值 。
静态函数的实现里不能使用非静态成员,如非静态变量、非静态函数等。
4. 特性是什么?如何使用?
特性与属性是完全不相同的两个概念,只是在名称上比较相近。Attribute特性就是关联了一个目标对象的一段配置信息,本质上是一个类,其为目标元素提供关联附加信息,这段附加信息存储在dll内的元数据,它本身没什么意义。运行期以反射的方式来获取附加信息。
5. C#中的委托是什么?事件是不是一种委托?
什么是委托?简单来说,委托类似于C或C++中的函数指针,允许将方法作为参数进行传递。
•C#中的委托都继承自System.Delegate类型;
•委托类型的声明与方法签名类似,有返回值和参数;
•委托是一种可以封装命名(或匿名)方法的引用类型,把方法当做指针传递,但委托是面向对象、类型安全的;
事件可以理解为一种特殊的委托,事件内部是基于委托来实现的。

GC与内存管理

1. 简述一下一个引用对象的生命周期?
•new创建对象并分配内存
•对象初始化
•对象操作、使用
•资源清理(非托管资源)
•GC垃圾回收
2. GC进行垃圾回收时的主要流程是?
① 标记:先假设所有对象都是垃圾,根据应用程序根Root遍历堆上的每一个引用对象,生成可达对象图,对于还在使用的对象(可达对象)进行标记(其实就是在对象同步索引块中开启一个标示位)。
② 清除:针对所有不可达对象进行清除操作,针对普通对象直接回收内存,而对于实现了终结器的对象(实现了析构函数的对象)需要单独回收处理。清除之后,内存就会变得不连续了,就是步骤3的工作了。
③ 压缩:把剩下的对象转移到一个连续的内存,因为这些对象地址变了,还需要把那些Root跟指针的地址修改为移动后的新地址。
3. GC在哪些情况下回进行回收工作?
•内存不足溢出时(0代对象充满时)
•Windwos报告内存不足时,CLR会强制执行垃圾回收
•CLR卸载AppDomian,GC回收所有
•调用GC.Collect
•其他情况,如主机拒绝分配内存,物理内存不足,超出短期存活代的存段门限
4. using() 语法是如何确保对象资源被释放的?如果内部出现异常依然会释放资源吗?
using() 只是一种语法形式,其本质还是try…finally的结构,可以保证Dispose始终会被执行。
5. 解释一下C#里的析构函数?为什么有些编程建议里不推荐使用析构函数呢?
C#里的析构函数其实就是终结器Finalize,因为长得像C++里的析构函数而已。
有些编程建议里不推荐使用析构函数要原因在于:第一是Finalize本身性能并不好;其次很多人搞不清楚Finalize的原理,可能会滥用,导致内存泄露,因此就干脆别用了。
6. Finalize() 和 Dispose() 之间的区别?
Finalize() 和 Dispose()都是.NET中提供释放非托管资源的方式,他们的主要区别在于执行者和执行时间不同:
•finalize由垃圾回收器调用;dispose由对象调用。
•finalize无需担心因为没有调用finalize而使非托管资源得不到释放,而dispose必须手动调用。
•finalize不能保证立即释放非托管资源,Finalizer被执行的时间是在对象不再被引用后的某个不确定的时间;而dispose一调用便释放非托管资源。
•只有class类型才能重写finalize,而结构不能;类和结构都能实现IDispose。
另外一个重点区别就是终结器会导致对象复活一次,也就说会被GC回收两次才最终完成回收工作,这也是有些人不建议开发人员使用终结器的主要原因。
7. Dispose和Finalize方法在何时被调用?
•Dispose一调用便释放非托管资源;
•Finalize不能保证立即释放非托管资源,Finalizer被执行的时间是在对象不再被引用后的某个不确定的时间;
8. .NET中的托管堆中是否可能出现内存泄露的现象?
是的,可能会。比如:
•不正确的使用静态字段,导致大量数据无法被GC释放;
•没有正确执行Dispose(),非托管资源没有得到释放;
•不正确的使用终结器Finalize(),导致无法正常释放资源;
•其他不正确的引用,导致大量托管对象无法被GC释放;
9. 在托管堆上创建新对象有哪几种常见方式?
•new一个对象;
•字符串赋值,如string s1=”abc”;
•值类型装箱。

多线程编程与线程同步

1. 描述线程与进程的区别?
•一个应用程序实例是一个进程,一个进程内包含一个或多个线程,线程是进程的一部分;
•进程之间是相互独立的,他们有各自的私有内存空间和资源,进程内的线程可以共享其所属进程的所有资源;
2. lock为什么要锁定一个参数,可不可锁定一个值类型?这个参数有什么要求?
lock的锁对象要求为一个引用类型。它可以锁定值类型,但值类型会被装箱,每次装箱后的对象都不一样,会导致锁定无效。
对于lock锁,锁定的这个对象参数才是关键,这个参数的同步索引块指针会指向一个真正的锁(同步块),这个锁(同步块)会被复用。
3. 多线程和异步有什么关系和区别?
多线程是实现异步的主要方式之一,异步并不等同于多线程。实现异步的方式还有很多,比如利用硬件的特性、使用进程或纤程等。在.NET中就有很多的异步编程支持,比如很多地方都有Begin***、End***的方法,就是一种异步编程支持,它内部有些是利用多线程,有些是利用硬件的特性来实现的异步编程。
4. 线程池的优点有哪些?又有哪些不足?
优点:减小线程创建和销毁的开销,可以复用线程;也从而减少了线程上下文切换的性能损失;在GC回收时,较少的线程更有利于GC的回收效率。
缺点:线程池无法对一个线程有更多的精确的控制,如了解其运行状态等;不能设置线程的优先级;加入到线程池的任务(方法)不能有返回值;对于需要长期运行的任务就不适合线程池。
5.Mutex和lock有何不同?一般用哪一个作为锁使用更好?
Mutex是一个基于内核模式的互斥锁,支持锁的递归调用,而Lock是一个混合锁,一般建议使用Lock更好,因为lock的性能更好。

 

posted @ 2019-04-16 11:51  Polo西柚  阅读(211)  评论(0编辑  收藏  举报