答案
计算机组成原理
1、为什么计算的补数(补码)=反码+1?
二进制的减法可以转化成加法运算。利用mod=|负数|+补数 ,计算机的mod =2字长 ,计算的字长是固定的,计算机加法结果超过字长的部分会舍弃,留下部分就是余数。
例如 :一个字长为8的 计算机,表示数字范围是0~28-1 ,一共28种(mod), 28 =反码+|原码|+1,补码=反码+1, +1后才能进位表示mod 长。
编程
题目一 、答案:0,10,5,50
题目考察对运行时常量和编译时常量的了解。
C#中用Const定义的常量 是编译时常量,在编译时值已经确定了,在源代码编译成IL的时候,编译器就把常量C都替换成 5,而static readonly定义的字段 运行时常量 在运行时才确定值,在类第一次应用的时候就是调用静态构造函数初始化readonly定义的常量。代码是按顺序执行的,运行到static readonly int A = C * D;此时的D并未定义,clr就用类型默认值 初始化为0;所以A=5*0 结果是0.
扩展 1.1 答案:x=1 Y=5 ;
类的初始化顺序:静态字段=》静态构造函数
此时程序的入口函数main在B类中,所B最先初始化。
public static int Y = A.X + 1; 在类B初始化过程中要引用类A的静态字段X,A.X表示对类A完成初始化后的调用 。所以必须先完成类A初始化,在初始化类A过程中有引用类B 静态字段Y。
此时Y为初始化 所以默认是0。所以类A初始化后的X字段值是1。类B根据类A.X=1,等到 Y=5
扩展 1.2 答案: x=5 Y=4 ;
类的初始化顺序:静态字段=》静态构造函数
程序主函数main 在program类中,所以 program最先初始化。
Console.WriteLine("X={0},Y={1}", A.X, B.Y)这个语句设计到类A 、类B 的静态字段、静态构造函数的初始化顺序。
静态字段只在类第一次引用的时候初始化,在类初始化完成后,静态字段的值就不会变了。
A.X是对A类初始化完成后的调用,A初始化的过程中涉及到类B的初始化。
所以当类A初始化完成后,类B的值也确定了。
然后再初始化A类 静态构造函数。 X = B.Y + 1;B类初始化顺序也是
扩展1.3 仔细看题目 考察对类初始化过程的了解
答:1个对象
这是因为在编译期间,应用了编译器优化中一种被称为常量折叠(Constant Folding)的技术,会将编译期常量的加减乘除的运算过程在编译过程中折叠。编译器通过语法分析,会将常量表达式计算求值,并用求出的值来替换表达式,而不必等到运行期间再进行运算处理,从而在运行期间节省处理器资源。
字符串为什么做引用类型?为什么字符串赋值 ,做参数都和值类型一样?
答:值类型和引用类型最根源的区别就是其存储位置的差异
Stack 栈:线程栈,由操作系统管理,存放值类型、引用类型变量(就是引用对象在托管堆上的地址)栈是基于线程的,也就是说一个线程会包含一个线程栈,线程栈中的值类型在对象作用域结束后会被清理,效率很高。
GC Heap托管堆:进程初始化后在进程地址空间上划分的内存空间,存储.NET运行过程中的对象,所有的引用类型都分配在托管堆上,托管堆上分配的对象是由GC来管理和释放的。托管堆是基于进程的,当然托管堆内部还有其他更为复杂的结构,
值类型通常比较小,直接存储在栈上,在使用完就直接销毁了。而引用类型通常比较大,回收时间不确定。所以放在托管堆上由GC负责统一管理他。字符串就是因为容量大,所以作为引用类型,由GC负责回收。
题目9:答案
这个看起来,与上面的做了同样的事,但为什么WriteLine返回的是False?
首先,需要说明一下ToString的工作方式,它总是返回它自身的引用。o是一个指向“abc”的变量,调用ToString返回的就是这个引用。所以,对于上面的内容,可以这样解释:
- 开始,变量a指向字符串对象“abc”(#1),变量o指向另一个字符串对象(#2),也包含“abc”。
- 调用
String.Intern(o.ToString())
将对象#2的引用添加到内部池中。 - 现在#2对象已经存在池中了,任何时候,使用“abc”调用String.Intern都将返回#2的引用(o指向了这个对象)。
- 所以,当你使用ReferenceEquals比较o和String.Intern(a)时,返回True。因为
String.Intern(a)
返回的是#2的引用。 - 现在我们创建一个新的变量o2,使用String.Copy(a)创建一个新的对象#3,它也包含“abc”。
- 调用
String.Intern(o2.ToString())
没有向内部池中添加任何内容,因为“abc”已经存在(#2)。 - 所以,此时调用Intern返回的是对象#2的引用。注意,这里并没有使用类似o2 = String.Intern(o2.ToString())这样的代码。
- 这就是为什么最后一行WriteLine打印的False的原因,因为我们在尝试比较#3与#2的引用。如果如7中所说,添加
o2 = String.Intern(o2.ToString())
这样的代码,WriteLine返回的就是True。
9.1扩展题目答案
第一个WriteLine打印的是“not interned”,因为“xyz”还没有存在于内部池中;第二个WriteLine打印了“xyz”因为现在内部池中有了“xyz”;第三个WriteLine打印True,因为对象引用的就是内部池中的“xyz”。
常量字符串自动被加入内部池
改变最后一行代码为:
1
|
Console.WriteLine( object .ReferenceEquals( "xyz" , s)); |
你会发现,奇怪的事情发生了,这些代码不再输出“not interned”了,并且最后的两个WriteLine输出的是False!发生了什么?
原因就是这个最后添加的那行代码中的常量“xyz”,CLR会将程序中使用的字符常量自动添加到内部池中。所以,当最后一行被添加之后,“xyz”在程序“运行之前”(避免严谨,这里用引号)就已经存在于内部池中。所以,当调用String.IsInterned的时候,返回的不再是null,而是指向“xyz”的引用。这也解释了,为什么后面的ReferenceEquals返回False,因为s从来没有被加到内部池中,其指向也不是内部池的"xyz"。
编译器比你想象的要聪明
改变最后一行代码为:
1
|
Console.WriteLine( object .ReferenceEquals( "x" + "y" + "z" , s)); |
运行一下,你会发现运行结果和直接使用“xyz”一样。但这里使用了+运算符啊?编译器不应该知道”x“+"y"+"z"最终的结果吧?
实际上,如果你将”x“+"y"+"z"替换为String.Format("{0}{1}{2}",'x','y','z'),结果确实就不一样了。某种原因,CLR会将使用+运算符链接的字符串视为常量,而String.Format却需要在运行时才能知道结果。为什么?看一下下面的代码:
1
2
3
4
5
6
7
|
using System; class Program { public static void Main() { Console.WriteLine( "x" + "y" + "z" ); } } |
这段代码编译之后,使用Ildasm.exe查看,会看到:
看到了吧,编译器足够聪明,将”x“+"y"+"z"替换为”xyz“
9.2:答案 考察是对匿名函数和委托相等的了解。委托列表相等要求,同一个类同一个方法或者同一个实例同一方法。
Action a = () => Console.WriteLine("a"); Action b = () => Console.WriteLine("a"); Console.WriteLine(a == b); // output: False Console.WriteLine(a + b == a + b); // output: True Console.WriteLine(b + a == a + b); // output: False
同匿名函数内部我们就可以知道,委托绑定了不同的方法。所以Console.WriteLine(a == b); // output: False
9.3题目的答案:考察对==和实例equal的区别
equal是object类型提供函数,==是机器操作码,由cpu负责运行,可以重载
10、答案
【个人的理解】
C# 实例有默认值的规范,而这个默认值从编译角度都是为0,引用类型默认都是null,值类型应该默认都是0。
默认值的初始化通常是通过使内存管理器或垃圾回收器将全部内存初始化为零来完成的,然后再将其分配给使用---msdn,然后在调用构造函数完成默认值初始化 。
内存清空了,对于引用类型存在堆栈中地址都没了,自然就为null,类可以实例字段设置初始值,因为那些都是保持托管堆中的,不影响初始化。
内存清空了,对于引用值类型相应的内容就没了,值类型是存在托堆栈中的。
而struct型是实实在在内容保存在堆栈中,要保持默认值,就必须保证堆栈的内容都为空。
为了实现 值类型堆栈内容都为0,C# 就将struct 的无参构造函数要隐藏起来并且不允许在struct定义无参构造函数,保证了值类型初始化为系统默认值。
当我在程序中调用deaflut(),默认值的初始化通常是通过使内存管理器或垃圾回收器将全部内存初始化为零来完成的,然后再将其分配给使用,然后在调用构造函数完成默认值初始化。
【msdn 官方的解释】
默认值的初始化通常是通过使内存管理器或垃圾回收器将全部内存初始化为零来完成的,然后再将其分配给使用。 出于此原因,可以方便地使用所有位数表示空引用。
11、运行时
值类型存储在堆栈上,值类型的实例中引用实例保存在托管堆上。
引用类型存储在托管堆上,类类型的实例中的值类型字段和属性装箱后随实例存储在托管堆上。
局部变量被匿名函数和迭代器捕获后回变成 匿名类型的字段,存储在托管堆上。因此
局部值类型变量会 随函数 保存在调用栈的帧栈上
12、 软件是如何实现跨平台的?
程序是什么?
程序就是一系列的指令的集合
cpu是什么
cpu就是 执行一系列指令集合的框架
跨平台是如何实现的?
微框架是固定的无法通过软件进行变更,指令集是可以变更的。所以要实现跨平台,就必须引进中间平台将一套指令解释成适用不同微框架下的指令集。
因此微软引入了CLR和IL实现跨平台。具体执行过程是1、软件源码经过编译后生成IL中间代码。2、让IL代码运行在CLR上,CLR根据不同的微框架 ,将IL代码解释成适用该微框架的指令集。
跨平台就是让一套程序源码,通过 虚拟机 解释成适用于不同微框架的指令集。
IL是一套指令集,这套指令集是对所有微框架进行抽象形成,他可以通过虚拟机 解释成适用于不同微框架的指令集。
13、Win32平台是什么与.net平台有关系
win32=Windows API
win32是以前对Windows API的称呼。
Windows API这一叫法实际上是多个Windows平台上相似接口的统称,这些接口也拥有各自的名字,如Win32 API、GDI+、GUi。几乎所有的Windows应用程序都在与 Windows API 进行交互。
Windows的现代64位版本实现了本机64位API,它称为“ Win32”。它与32位API 兼容,因此保留了相同的名称,但是它是本机64位实现,对于64位库将其自身称为“ Win32”是很有意义的。
Win64 API环境与Win32 API环境几乎相同,与从Win16到Win32的重大转变不同。现在将Win32和Win64 API合并在一起,称为Windows API。使用Windows API,您可以编译相同的源代码以在32位Windows或64位Windows上本地运行。
Win32和WinRT是指Windows操作系统的底层API(应用编程接口),在Windows系统上运行的应用程序(各种软件)通过这些系统级接口与硬件(例如显示器、键鼠等)交互。Win32在Windows 7及以前的系统(包括Windows XP)中被广泛运用。WinRT是微软公司在Windows 8中引入的一组新的应用编程接口,允许开发者使用更加现代化的语言特性高效开发具有现代风格的应用程序。在Windows 8及以后的操作系统(包括Windows 10)中都包含有Win32和WinRT两套API
C是Windows API(Win32)的主要编程语言。Windows 8提供的Windows API和WinRT API就是用C++[3]实现的,并且在设计上也是面向对象的[3]。
两者之间的关系
.NET和Win32的关系并非并列,不严格的说可以认为.NET是运行在Win32之上。那都有了Win32为什么还要有.NET?前面说了这是一个编程平台。原始的Win32接口调用非常麻烦,支持的编程语言也有限,而.NET可以看成对Win32的封装,牺牲少量的运行效率换取更好的程序容错性、更好的开发效率。并且现在还有了.NET Core,可以让代码不依赖.NET Framework,这样程序在运行的时候就不需要目标机器必须安装有.NET Framework了。非托管代码主要是基于win 32平台开发的DLL,activeX的组件,托管代码是基于.net平台开发的。
14、答案
x = 1, a = 1, y = 1, b = 0
x = 1, a = 0 , y = 1, b = 1
x = 1, a = 0 , y = 1, b = 1
x = 1, a = 0 , y = 1, b = 0 这个才是考察重点
揭开易失性关键字|的神秘面纱奥努尔·古姆斯的博客 (onurgumus.github.io)
15、指针和句柄有啥区别?
总结:
1、句柄就是进程句柄表中的索引。
2、句柄是对进程范围内一个内核对象地址的引用,一个进程的句柄传给另一个进程是无效的。一个内核对象可用有多个句柄。
Windows之所以要设立句柄,根本上源于内存管理机制的问题,即虚拟地址。简而言之数据的地址需要变动,变动以后就需要有人来记录、管理变动,因此系统用句柄来记载数据地址的变更。
16、 16、异步操作一定要开辟新线程吗?
这里需要纠正一个误区,那就是Control类上的异步调用BeginInvoke并没有开辟新的线程完成委托任务,而是让界面控件的所属线程完成委托任务的。看来异步操作就是开辟新线程的说法不一定准确。
17、finally内的代码一定会执行吗?
不一定
string str = "app异常写入window日志"; try { if (str == "app异常写入window日志") { Environment.FailFast(str);//引发System.ExecutionEngineException:“Exception_WasThrown”导致finadlly后的代码无法执行 //执行后就终止程序了 } } finally { Console.WriteLine("其实这个代码不会运行"); }
18、如何修改 ,以下代码,完成任务
主要考察垃圾回收
目的是:每个2s继续一次垃圾回收
发生错误:只执行一次
原因:t被垃圾回收了。 当第一次执行垃圾回收时候GC发现 t 声明后再无引用。所以就被回收了。
源代码
public static void Main() {
Timer t = new Timer(TimerCallback, null, 0, 2000);
Console.ReadLine();
t = null;
}
修改后
public static void Main() { Timer t = new Timer(TimerCallback, null, 0, 2000); Console.ReadLine(); t = null; //错误写法,这样会被编译器优化了。有写等于没写 t.Dispose();//正确写法,把变量t 延续到方法结束 }
19、为什么我们需要析构函数?
答:要销毁非托管资源。例如关闭本机资源 文件 mutex实例。这些对象实例是保存本机资源的句柄通过句柄访问本机资源,所有必须在销毁实例之后通过句柄关闭本机资源。然后GC在回收改托管实列。
20、AOT,JIT是什么?即Just-in-time,动态(即时)编译,边运行边编译;AOT,Ahead Of Time,指运行前编译,是两种程序的编译方式。AOT应用在CoreRT中。CoreCLR 和 CoreRT 都是.NET Core的运行时(Runtime),CoreRT 和 CoreCLR 不同的是,CoreRT 提供了一套AOT 的机制,可以将.NET Core程序编译成原生代码,不依赖 .NET 运行时而运行在宿主机器上。除此之外两个运行时大部分功能代码是共享的,比如GC。AOT的优化带来不少好处:
21、元数据在.net 中的应用有哪些?反射、垃圾回收、序列化、CLR类型验证、窗体设计时候拖放控件、加载程序集,编译器中智能感知也是应用元数据。.net元数据油3中表构成:定义表、引用表、清单表具体查CLR via C#P35
22、数组初始值设定项 例如:A[] a=new A{a,s,f}。为什么要省略 new A从而变成A[] a={a,s,f}
答案: 数组初始值设定项依赖的是Array的Add()的方法而不是对象Add()的方法,因此A[] a=new A{a,s,f}中的new A要省略掉 写成A[] a= {a,s,f}。集合对象初始值设定项 依赖的是集合对象 的Add方法例如:A a=new A{a,s,v},因此对象new A不能省略。
23、 为什么C# jsonserializeOption中默认不识别尾部逗号、默认不包含字段、默认不skip注释。
因为System.Text.Json C# json遵守的RFC 8259标准 改标准没有这些东西。而JSON5这个超集规范通过明确地添加便利性特征(如注释、备选引号、无引号字符串、尾部逗号)来增强官方规范。
24、xml为什么要写<?xml version='1.0'?>。因为标明xml版本,不同版本xmlns属性和命名空间不一样
Namespaces in XML 1.1(<?xml version="1.1" encoding="US-ASCII"?>)
1、容许取消绑定已经绑定的前缀,默认命名空间可以取消
2、命名空间使用IRI格式命名(URL, URN, URI, IRI 的区别)
Namespaces in XML 1.0(<?xml version="1.0" encoding="US-ASCII"?>)
1、 不容许取消绑定已经绑定的前缀,默认命名空间可以取消
2、命名空间使用URI格式命名(URL, URN, URI, IRI 的区别)
25、Encoding.UTF8 与 new UTF8Encoding(false) 有什么区别
System.Text.Encoding.UTF8 是一个静态实例,它省略了 BOM,而 new UTF8Encoding(false) 创建的实例是含有 BOM 的。
BOM,即 Byte Order Mark,也即字节流标记,它是用来让应用程序识别所用的编码的。UTF-8 的 BOM 是 0xEFBBBF。
public UTF8Encoding(bool encoderShouldEmitUTF8Identifier),可以看出,如果我们指定参数为 false,表示不省略 BOM;如果为 true,则和 Encoding.UTF8 一样了。
26、数组的元素访问时间复杂度是多少。为什么数组可以通过索引来访问?
数组的访问:
数组在访问操作方面有着独特的性能优势,因为数组是支持随机访问的,也就是说我们可以通过下标随机访问数组中任何一个元素,其原理是因为数组元素的存储是连续的,所以我们可以通过数组内存空间的首地址加上元素的偏移量计算出某一个元素的内存地址,如下:
array[n]的地址 = array数组内存空间的首地址 + 每个元素大小*n
通过上述公式可知:数组中通过下标去访问数据时并不需要遍历整个数组,因此数组的访问时间复杂度是 O(1),当然这里需要注意,如果不是通过下标去访问,而是通过内容去查找数组中的元素,则时间复杂度不是O(1),极端的情况下需要遍历整个数组的元素,时间复杂度可能是O(n),当然通过不同的查找算法所需的时间复杂度是不一样的。
数组的插入与删除:
同样是因为数组元素的连续性要求,所以导致数组在插入和删除元素的时候效率比较低。
如果要在数组中间插入一个新元素,就必须要将要相邻的后面的元素全部往后移动一个位置,留出空位给这个新元素。还是拿上面那图举例,如果需要在下标为2的地方插入一个新元素11,那就需要将原有的2、3、4、5几个下标的元素依次往后移动一位,新元素再插入下标为2的位置,最后形成新的数组是:
23、4、11、6、15、5、7
如果新元素是插入在数组的最开头位置,那整个原始数组都需要向后移动一位,此时的时间复杂度为最坏情况即O(n),如果新元素要插入的位置是最末尾,则无需其它元素移动,则此时时间复杂度为最好情况即O(1),所以平均而言数组插入的时间复杂度是O(n)
数组的删除与数组的插入是类似的。
27、O(log2n)
28\关于算法的时间复杂度很多都用包含O(logN)这样的描述,但是却没有明确说logN的底数究竟是多少。
解答:
算法中log级别的时间复杂度都是由于使用了分治思想,这个底数直接由分治的复杂度决定。
如果采用二分法,那么就会以2为底数,三分法就会以3为底数,其他亦然。
不过无论底数是什么,log级别的渐进意义是一样的。
也就是说该算法的时间复杂度的增长与处理数据多少的增长的关系是一样的。
我们先考虑O(logx(n))和O(logy(n)),x!=y,我们是在考虑n趋于无穷的情况。
求当n趋于无穷大时logx(n)/logy(n)的极限可以发现,极限等于lny/lnx,也就是一个常数,
也就是说,在n趋于无穷大的时候,这两个东西仅差一个常数。
所以从研究算法的角度log的底数不重要。
最后,结合上面,我也说一下关于大O的定义(算法导论28页的定义),
注意把这个定义和高等数学中的极限部分做比较,
显然可以发现,这里的定义正是体现了一个极限的思想,
假设我们将n0取一个非常大的数字,
显然,当n大于n0的时候,我们可以发现任意底数的一个对数函数其实都相差一个常数倍而已。
所以书上说写的O(logn)已经可以表达所有底数的对数了,就像O(n^2)一样。
没有非常严格的证明,不过我觉得这样说比较好理解,如果有兴趣证明,完全可以参照高数上对极限趋于无穷的证明。
29、
二、这货还是不是平衡二叉树?
判断一棵平衡二叉树(AVL树)有如下必要条件:
条件一:必须是二叉搜索树。
条件二:每个节点的结点的平衡因子(左子树高-右子树高) 的高度差至多为1。
30、为什么要发明红黑树?
平衡二叉树AVL:插入/删除很容易破坏“平衡”特性,需要频繁调整树的形态。如:(插入操作导致不平衡,则需要先计算平衡因子,找到最小不平衡子树(时间开销大),再进行LL/RR/LR/RL调整。
红黑树RBT:插入/删除很多时候不会破坏“红黑”特性,无需频繁调整树的形态。即便需要调整,一般都可以在常数级时间内完成
平衡官叉树:适用于以查为主、很少插入/删除的场景红黑树:适用于频繁插入、删除的场景,实用性更强
31、快速排序法quicksort 最好时间复杂度=O(nlog2n),最坏时间复杂度=O(n2)。日常数据都是随机的,所有快速排序法基本都是处于最好的时间复杂度。所以一般情况下是最好的排序法。
32、什么是堆?
堆是完全二叉树,虽然堆的典型实现方法是数组,但从逻辑的角度上讲,堆实际上是一种树结构。
小根堆:完全二叉树中,根≤左、右
大根堆:完全二叉树中,根≥左、右
33、 为啥减法运算可以转化成加法运算,原理是什么?
答案:因为原理就是取模运算,计算机加法器是一个mod为2字长-1,超过的部分会被舍弃。因此可以用负数转化成他的补数。参与加法计算。涉及到概念什么是余数 a=qm+r。
34、原理Decimal是由4个int组成的,它的内部最长也是int。所以上述MyStruct结构中占内存最大的成员是double,对齐长度是8。
//计算结果是32个字节=8+16+8
35、C#结构体的内存对齐(边界对齐)
对结构体的内存结构进行布局规则如下
规则1、每个特定平台上的编译器都有自己的默认“对齐系数”(也叫对齐模数)。程序员可以通过[StructLayout(LayoutKind.Sequential, Pack =n)],n=1,2,4,8,16来改变这一系数,其中的n就是你要指定的“对齐系数”。
C#对齐系数 n=0,1,2,4,8,16, 0表示默认的平台对齐系数,就是操作系统的字长字长/8。
规则2、数据成员对齐 :结构体的第一个字段的偏移量(offset)为0以后每一个成员相对于结构体首字段的offset都是该成员大小与有效对其值中较小的那个的整数倍,不满足条件,自动填补字节。
找最大数据长度时,如果结构体T有复杂类型成员A的,该A成员的长度为该复杂类型成员A的最大成员长度,例如 demail 由4个int 组成,它的最大成员是int,decimal a;a应是int 而不是decimal
备注:对齐单位(有效对齐值): 系统给定的对齐系数和结构体内最长数据长度,之中较小的那一个。有效对其值也叫做对齐单位。
规则3、结构的整体对齐:结构体总体size mod 对齐单位=0 ,结构体的总的大小时对齐单位的整数倍,不满足将自动填充。
36、为什么依赖比关联的耦合度低?
依赖对应对象方法的局部变量,关联对应对象的成员变量。
成员变量和对象具有相同的生命周期,即类A一直和类B存在关联关系
局部变量只有在方法被调用时,类A才会和类B存在依赖关系
因此从关系存在的时间长短可以推断出依赖比关联耦合度低
37、C# 为什么不支持多重继承?
软件质量衡量的指标 就是复用性和维护性,因为多重继承会会使得类过庞大,复用性低,维护性差 。从设计模式的角度说,多重继承违背第一职责原则,用桥接模式取代,它能极大的减少子类的个数。
38、判断偶数,只有组后一位是0的数都是偶数
for (int i = 0; i < 10; i++)
{
if((i&1)==0)
Console.WriteLine(i);
}