C#基础面试题集

2023-12-13 12:28:12

C#基础

1. 简述值类型和引用类型有什么区别

1.1介绍

值类型:int,bool,float,char,struct,enum。
引用类型:string,object,delegate,interface,class,array。

1.2 区别

值类型存储在栈中,引用类型存储在堆中。 值类型存储快,引用类型存储慢。 值类型表示实际数据,引用类型表示指向在内存堆中的指针和引用。
值类型在栈中可以自动释放,引用类型在堆中需要GC来释放 值类型继承于 System.ValueType,(System.ValueType继承于System.Object),引用类型继承于System.Object。
值类型在栈中存储的是直接的值,引用类型数据本身实在堆中,栈中存放的是一个引用的地址。

1.3 底层

1.引用类型在实例化时,先在栈内开辟空间,用于存储堆中对象的地址,然后在堆内开辟空间,存储引用对象。
2.而值类型直接在栈中开辟空间存储对象。值类型也有引用地址,但都在栈内的同一空间。
3.在参数对象进入方法体内,实则是在栈中开辟了新的临时空间。(也就是参数对象的副本)栈内值类型的修改,由于栈中地址不同,所以值类型不会影响到主体。而引用类型的存储数据是一个堆内的地址,所以对于引用类型的修改是直接修改堆内的对象。
4.值类型对象中的引用类型在堆中(struct中定义的string等),引用类型对象中的值类型也在堆中(class中的int等)
C#中所有引用类型的基类是什么? 引用类型的基类是System.Object值类型的基类是 System.ValueType 同时,值类型也隐式继承自System.Object

2. C# String类型比 stringBuilder 类型的优势是什么?

2.1 介绍
string的修改,实则是new 一个新的string,在堆内新开辟空间。而此时栈内的副本也会指向堆内新对象。因此string改变是新建的对象,和本体没有联系。

2.2 解决
当频繁堆一个字符串进行修改时,利用StringBuilder代替String

2.3 StringBuilder的底层实现?
StringBuilder 是支持扩容的(char类型)数组,在每次空间不足时,会开辟原先数组大小的容量,类似于链表,新建的数组指向上一个已经用完的数组,本身不会产生gc。

2.4 扩展:
StringBuffer是线程安全,一般用于多线程(C#端不存在)
StringBuilder是非线程安全,所以性能略好,一般用于单线程

2.5 用StringBuilder拼接字符串就一定比string要好吗?
答:极少拼接(或者短字符串)的情况下 String甚至优于StringBuilder,因为String是公用API,通用性好,用途广泛,读取性能高,占用内存较小,Stringbuilder初始化花费时间更大。

如果是处理字符串的话,用string中的方法每次都需要创建一个新的字符串对象并且分配新的内存地址,而 stringBuilder是在原来的内存里对字符串进行修改,所以在字符串处理方面还是建议用stringBuilder这样比较节约内存。但是 string类的方法和功能仍然还是比 stringBuilder 类要强。

string类由于具有不可变性(即对一个 string 对象进行任何更改时,其实都是创建另外一个 string
类的对象),所以当需要频繁的对一个 string 类对象进行更改的时候,建议使用StringBuilder 类,StringBuilder类的原理是首先在内存中开辟一定大小的内存空间,当对此 StringBuilder 类对象进行更改时, 如果内存空间大小不够,会对此内存空间进行扩充,而不是重新创建一个对象,这样如果对一个字符串对象进行频繁操作的时候,不会造成过多的内存浪费,其实本质上并没有很大区别,都是用来存储和操作字符串的,唯一的区别就在于性能上。

String主要用于公共 API,通用性好、用途广泛、读取性能高、占用内存小。
StringBuilder主要用于拼接 String,修改性能好。
不过现在的编译器已经把String的 + 操作优化成 StringBuilder 了, 所以一般用String 就可以了
String是不可变的,所以天然线程同步。
StringBuilder可变,非线程同步。

2.6 字符串池

字符串池有什么用,原理是什么?
字符串池是CLR一种针对于反复修改字符串对象的优化措施,作用能够一定程度减少内存消耗。原理是内部开辟容器通过键值对的形式注册字符串对象,键是字符串对象的内容,值是字符串在托管堆上的引用。这样当新创建的时候,会去检查,如果不存在就在这个容器中开辟空间存放字符串。

3.面向对象的三大特点

继承

提高代码重用度,增强软件可维护性的重要手段,符合开闭原则。继承最主要的作用就是把子类的公共属性集合起来,便与共同管理,使用起来也更加方便。你既然使用了继承,那代表着你认同子类都有一些共同的特性,所以你把这些共同的特性提取出来设置为父类。继承的传递性:传递机制 a?b; b?c; c具有a的特性 。继承的单根性:在C#中一个类只能继承一个类,不能有多个父类。

封装

封装是将数据和行为相结合,通过行为约束代码修改数据的程度,增强数据的安全性,属性是C#封装实现的最好体现。就是将一些复杂的逻辑经过包装之后给别人使用就很方便,别人不需要了解里面是如何实现的,只要传入所需要的参数就可以得到想要的结果。封装的意义在于保护或者防止代码(数据)被我们无意中破坏。

多态性

多态性是指同名的方法在不同环境下,自适应的反应出不同得表现,是方法动态展示的重要手段。多态就是一个对象多种状态,子类对象可以赋值给父类型的变量。 例如叫声,在鸟这个类中是“鸣啼”在狗这个类中是“犬吠”。

4.请简述private,public,protected,internal的区别

public:对任何类和成员都公开,无限制访问
private:仅对该类公开
protected:对该类和其派生类公开
internal:只能在包含该类的程序集中访问该类
protected internal: protected + internal

5.结构体和类

区别

  • 结构体是值类型,类是引用类型。结构体存在栈中,类存在堆中。
  • 值类型存取快,引用类型存取慢。
  • 值类型表示实际数据,引用类型表示指向存储在内存堆中的数据的指针和引用。
  • 栈的内存是自动释放的,堆内存是.NET 中会由 GC 来自动释放。
  • 值类型继承自 System.ValueType,引用类型继承自 System.Object。
  • 结构体变量和类对象进行类型传递时,结构体变量进行的就是值传递,而类对象进行的是引用传递,或者说传递的是指针,这样在函数中改变参数值,结构体对象的值是不变的,而类对象的值是变化了。
  • 在C#中结构体类型定义时,成员是不能初始化的,这样就导致了,定义结构体变量时,变量的所有成员都要自己赋值初始化。但对于类,在定义类时,就可以初始化其中的成员变量,所以在定义对象时,对象本身就已经有了初始值,你可以自己在重新给个别变量赋值。(注意在C++中,类的定义中是不能初始化的,初始化要放在构造函数中)
  • 结构体不能申明无参的构造函数,而类可以。
  • 声明了结构类型后,可以使用new运算符创建构造对象,也可以不使用new关键字。如果不使用new,那么在初始化所有字段之前,字段将保持未赋值状态且对象不可用。
  • 结构体申明有参构造函数后,无参构造不会被顶掉。
  • 结构体不能申明析构函数,而类可以。
  • 结构体不能被继承,而类可以。
  • 结构体需要在构造函数中初始化所有成员变量,而类随意。
  • 结构体不能被静态static修饰(不存在静态结构体),而类可以。

使用环境
结构体

  • 结构是值类型在栈中,栈的存取速度比堆快,但是容量小,适合轻量级的对象,比如点、矩形、颜色。
  • 如果对象时数据集合时,优先考虑接结构体(位置,坐标)
  • 在变量传值的时候,希望传递对象的是拷贝,而不是对象的引用地址,这个时候就可以使用结构体。

  • 类是引用类型,存储在堆中,堆的容量大,适合重量级的对象,栈的空间不大,大量的对应当存在于堆中。
  • 如果对象需要继承和多态特征,用类(玩家、怪物)。

什么时候用结构体呢?

结构体在堆栈中创建,是值类型,而类是引用类型。
每当需要一种经常使用的类型,而且大多数情况下该类型只是一些数据时,使用结构体能比使用类获得更佳性能。

6.请描述Interface与抽象类之间的不同

区别

1.接口不是类 不能实例化 抽象类可以间接实例化
2.接口是完全抽象 抽象类为部分抽象
3.接口可以多继承 抽象类是单继承

  • 接口不是类(无构造函数和析构函数) ,不能被实例化,抽象类可以间接实例化(可以被继承,有构造函数,可以实例化子类的同时间接实例化抽象类这个父类)。
  • 接口只能做方法申明,抽象类中可以做方法申明,也可以做方法实现。
  • 抽象类中可以有实现成员,接口只能包含抽象成员。因此接口是完全抽象,抽象类是部分抽象。
  • 抽象类要被子类继承,接口要被类实现。
  • 抽象类中所有的成员修饰符都能使用,接口中的成员都是对外的,所以不需要修饰符修饰。
  • 接口可以实现多继承,抽象类只能实现单继承,一个类只能继承一个类但可以实现多个接口。
  • 抽象方法要被实现,所以不能是静态的,也不能是私有的。

使用环境:

使用抽象类是为了代码的复用,而使用接口的动机是为了实现多态性。
抽象类适合用来定义某个领域的固有属性,也就是本质,接口适合用来定义某个领域的扩展功能。

抽象类

1.当2个或多个类中有重复部分的时候,我们可以抽象出来一个基类,如果希望这个基类不能被实例化,就可以把这个基类设计成抽象类。
2.当需要为一些类提供公共的实现代码时,应优先考虑抽象类。因为抽象类中的非抽象方法可以被子类继承下来,使实现功能的代码更简单。

接口

当注重代码的扩展性跟可维护性时,应当优先采用接口。
①接口与实现它的类之间可以不存在任何层次关系,接口可以实现毫不相关类的相同行为,比抽象类的使用更加方便灵活;
②接口只关心对象之间的交互的方法,而不关心对象所对应的具体类。接口是程序之间的一个协议,比抽象类的使用更安全、清晰。一般使用接口的情况更多。

7.在类的构造函数前加上static会报什么错?为什么?

介绍

  • 静态构造函数既没有访问修饰符,也没有参数。
  • 在创建第一个类实例或任何静态成员被引用时,.NET将自动调用静态构造函数来初始化类。
  • 一个类只能有一个静态构造函数。
  • 无参数的构造函数可以与静态构造函数共存。
  • 最多只运行一次。
  • 静态构造函数不可以被继承。
  • 如果没有写静态构造函数,而类中包含带有初始值设定的静态成员,那么编译器会自动生成默认的静态构造函数。
  • 如果静态构造函数引发异常,运行时将不会再次调用该构造函数,并且在程序运行所在的应用程序域的生存期内,类型将保持未初始化。

构造函数格式为public+类名如果加上 static会报错(静态构造函数不能有访问、型的对象,静态构造函数只执行一次;
运行库创建类实例或者首次访问静态成员之前,运行库调用静态构造函数;
静态构造函数执行先于任何实例级别的构造函数;显然也就无法使用this和base 来调用构造函数。

8.虚函数实现原理

每个虚函数都会有一个与之对应的虚函数表,该虚函数表的实质是一个指针数组,存放的是每一个对象的虚函数入口地址。对于一个派生类来说,他会继承基类的虚函数表同时增加自己的虚函数入口地址,如果派生类重写了基类的虚函数的话,那么继承过来的虚函数入口地址将被派生类的重写虚函数入口地址替代。那么在程序运行时会发生动态绑定,将父类指针绑定到实例化的对象实现多态。

9.指针和引用的区别

  1. 引用不能为空,即不存在对空对象的引用,指针可以为空,指向空对象。
  2. 引用必须初始化,指定对哪个对象的引用,指针不需要。
  3. 引用初始化后不能改变,指针可以改变所指对象的值。
  4. 引用访问对象是直接访问,指针访问对象是间接访问。
  5. 引用的大小是所引用对象的大小,指针的大小,是指针本身大小,通常是4字节。
  6. 引用没有const,指针有const
  7. 引用和指针的+自增运算符意义不同。
  8. 引用不需要分配内存空间,指针需要。

10.C#中ref和out关键字有什么区别?

ref和out的作用
解决值类型和引用类型在函数内部改值或者重新申明能够影响外部传入的变量让其也被修改。

使用
就是在申明参数的时候前面加上ref和out的关键字即可,传入参数时同上。

区别

ref修饰参数,表示进行引用传递,
out修饰参数也表示进行引用传递,但传递的引用只为带回返回值
ref又进又出 out不进只出
ref传入的变量必须初始化但是在内部可改可不改。
out传入的变量不用初始化但是在内部必须修改该值(必须赋值)。

11.请简述关键字Sealed用在类声明和函数声明时的作用

关键字sealed,类声明时可防止其他类继承此类,在方法中声明则可防止派生类重写此方法。与override一起使用。

12.C#中委托和接口有什么区别?各用在什么场合?

委托介绍
委托是约束集合中的一个类,而不是一个方法,相当于一组方法列表的引用,可以便捷的使用委托对这个方法集合进行操作。委托是对函数指针的封装。

委托和接口的区别
接口介绍
接口是约束类应该具备功能的集合,约束了类应该具备哪些功能,使类从复杂的逻辑中解脱出来,方便类的管理和拓展,同时解决类的单继承问题。

使用情况

接口:无法继承的场所、完全抽象的场所、多人协作的场所
委托:多由于事件的处理

C#中委托和事件的区别

委托和事件的区别

  • 事件可以看做成委托中的一个变量。
  • 事件是基于委托的存在,事件是委托的安全包裹 让委托的使用更具有安全性。

委托可以用“=”来赋值,事件不可以。
委托可以在声明它的类外部进行调用,而事件只能在类的内部进行调用。
委托是一个类型,事件修饰的是一个对象。

13. .Net与 Mono 的关系?

.Net是一个语言平台,Mono为.Net提供集成开发环境,集成并实现了.NET的编译器、CLR 和基础类库,使得.Net既可以运行在windows也可以运行于 linux,Unix,Mac OS 等。

14.请简述GC(垃圾回收)产生的原因,并描述如何避免?

GC为了避免内存溢出而产生的回收机制
如何避免:

1)减少 new 产生对象的次数
2)使用公用的对象(静态成员)
3)将 String 换为 StringBuilder

15.协变与逆变

协变(out):
和谐、自然的变化

里式替换原则中,父类容器可以装载子类对象,子类可以转换成父类。比如string转object,感受是和谐的。
逆变(in):
逆常规、不正常的变化

里式替换原则中,父类容器可以装载子类对象,但是子类对象不能装载父类。所以父类转换为子类,比如object转string,感受是不和谐的。

协变和逆变是用来修饰泛型的,用于泛型中修饰字母,只有泛型接口和泛型委托能使用.
作用:

//1.返回值与参数
 
//用out修饰的泛型,只能作为返回值
 
delegate T Testout<out T>();
 
//用in修饰的泛型,只能作为参数
 
delegate T TestIn<in T>(T t);

16.重载和重写的区别

  1. 封装、继承、多态所处位置不同,重载在同类中,重写在父子类中。
  2. 定义方式不同,重载方法名相同参数列表不同,重写方法名和参数列表都相同。
  3. 调用方式不同,重载使用相同对象以不同参数调用,重写用不同对象以相同参数调用。
  4. 多态时机不同,重载时编译时多态,重写是运行时多态。

17.C#函数 Func(string a, string b)用 Lambda 表达式怎么写?

(a,b) => {};

18.数列1,1,2,3,5,8,13…第 n 位数是多少?用 C#递归算法实现

public int CountNumber(int num) {

       if (num == 1 || num == 2) {
           return 1;
       } else {
           return CountNumber(num -1) + CountNumber(num-2);
       }
       
  }

19.冒泡排序(手写代码)

public static void BubblingSort(int[]array) {

      for (int i = 0; i < array.Length; i++){

          for (int j = array.Length - 1; j > 0; j--){

              if (array[j] < array[i]) {

                  int temp = array[j];

                  array[j] = array[j-1];

                  array[j - 1] = temp;

              }

          }

      }

  }

C#中的排序方式有哪些?
选择排序,冒泡排序,快速排序,插入排序,希尔排序,归并排序
排序算法里,快速排序时间复杂度多少,稳不稳定?冒泡排序呢?插入排序呢?

(排序算法的时间复杂度,空间复杂度,和稳定性,常见的几种排序算法) 快排复杂度是 nlogn,不稳定,插入排序和冒泡排序,复杂度都是 n^2都稳定。
“最近情绪不稳定,快(快速排序)些(希尔排序)选(直接选择排序)堆(堆排序)朋友聊聊天”

20.C#中有哪些常用的容器类,各有什么特点。

List,HashTable,Dictionary,Stack,Queue

List:索引泛型容器 访问速度快 修改速度慢

HashTable/Dictionary:散列表格式 查询效率高 空间占用较大

Stack:后进先出

Queue:先进先出

21.C#中常规容器和泛型容器有什么区别,哪种效率高?

不带泛型的容器需要装箱和拆箱操作速度慢所以泛型容器效率更高数据类型更安全

22.有哪些常见的数值类?

简单值类型–包括 整数类型、实数类型、字符类型、布尔类型
复合值类型–包括 结构类型、枚举类型

23.反射的实现原理?

可以在加载程序运行时,动态获取和加载程序集,并且可以获取到程序集的信息反射即在运行期动态获取类、对象、方法、对象数据等的一种重要手段

主要使用的类库:System.Reflection

核心类:

1.Assembly描述了程序集

2.Type描述了类这种类型

3.ConstructorInfo描述了构造函数

4.MethodInfo描述了所有的方法

5.FieldInfo描述了类的字段

6.PropertyInfo描述类的属性

//反射个人认为,就是得到程序集中的属性和方法。
//实现步骤:
1,导入using System.Reflection;
2,Assembly.Load("程序集")加载程序集,返回类型是一个Assembly
3foreach (Type type in assembly.GetTypes())
            { string t = type.Name;  }   //得到程序集中所有类的名称
4,Type type = assembly.GetType("程序集.类名");//获取当前类的类型
5,Activator.CreateInstance(type); //创建此类型实例
6,MethodInfo mInfo = type.GetMethod("方法名");//获取当前方法
7,mInfo.Invoke(null,方法参数);

通过以上核心类可在运行时动态获取程序集中的类,并执行类构造产生类对象,动态获取对象的字段或属性值,更可以动态执行类方法和实例方法等。

反射面向对象体现

之前了解的面向对象是基于类实现,而反射中就是基于程序集实现,只不过把类再用程序集包裹了一下,封装是把一些属性方法封装到一个类中,限制其数据修改的程度,那多加一层皮(程序集 ) 就是一个道理了,继承多态就是和类一样,把类换成程序集去理解。

  • 优点:

允许在运行时发现并使用编译时还不了解的类型以及成员。

  • 缺点:

1.根据目标类型的字符串搜索扫描程序集的元数据的过程耗时。
2.反射调用方法或属性比较耗时。(首先必须将实参打包成数组,在内部,反射必须将这些实参解包到线程栈上。可以使用多态避免反射操作)

通过反射去获取对象的一个实例
反射可以直接访问类的构造,直接通过getConstructor,去访问这个构造函数,然后通过不同的参数列表,就可以具体的定位到哪一个构造的重载,通过这个方法,去得到类的实例,把对象就拿到了。

24.C#中unsafe关键字是用来做什么的?什么场合下使用?

非托管代码才需要这个关键字一般用在带指针操作的场合

25.请简述ArrayList和 List的主要区别

ArrayList不带泛型 数据类型丢失 需要装箱拆箱 (不丢需)
List带泛型 数据类型不丢失 不需要装箱拆箱 (带不不)

26.想要在for循环中删除List(或者vector,都行)中的元素时,有可能出现什么问题,如何避免?

当删除遍历节点后面的节点时,会导致List.Count进行变化,删除元素后,当根据i++,遍历到删除的节点会发生异常。

处理

可以从后往前元素元素,即删除在访问的前面

27.For,foreach,Enumerator.MoveNext的使用,与内存消耗情况

for循环可以通过索引依次进行遍历,foreach和Enumerator.MoveNext通过迭代的方式进行遍历。内存消耗上本质上并没有太大的区别。但是在Unity中的Update中,一般不推荐使用foreach 因为会遗留内存垃圾。

28.函数中多次使用string的+=处理,会产生大量内存垃圾(垃圾碎片),有什么好的方法可以解决。

通过StringBuilder那进行append,这样可以减少内存垃圾

29.当需要频繁创建使用某个对象时,有什么好的程序设计方案来节省内存?

设计单例模式进行创建对象或者使用对象池

30.JIT和AOT区别

Just-In-Time -实时编译

执行慢安装快占空间小一点

Ahead-Of-Time -预先编译

执行快安装慢占内存占外存大

31.给定一个存放参数的数组,重新排列数组

void SortArray(Array arr){Array.Sort(arr);}

32.Foreach循环迭代时,若把其中的某个元素删除,程序报错,怎么找到那个元素?以及具体怎么处理这种情况?(注:Try…Catch捕捉异常,发送信息不可行)

foreach不能进行元素的删除,因为迭代器会锁定迭代的集合,解决方法:记录找到索引或者key值,迭代结束后再进行删除。

33.GameObject a=new GameObject() GameObject b=a 实例化出来了A,将A赋给B,现在将B删除,问A还存在吗?

存在,b删除只是将它在栈中的内存删除,而A对象本身是在堆中,所以A还存在

34.你拥有A块钱,一瓶水B块钱,每瓶水可以得到一个瓶盖,每C个瓶盖可以换一瓶水请写出函数求解上面题目,上面题目ABC为参数

public static int Buy(int a,int b,int c) {
 
    return a/b + ForCap(c,a/b);
 
}
 
public static int ForCap(int c,int d) {
 
    if (d)???
 
    return 0;
 
    } else {
 
    return d/c + ForCap(c,d/c + d%c);
 
    }
 
}

35.有一排开关,第一个人把所有的开关打开,第二个人按2的倍数的开关,第三个人按3的倍数的开关,以此类推,现在又n个开关,k个人,写函数求最后等两者的开关,输入参数n和k

static void Main(string[] args) {
 
       int n = int.Parse(Console.ReadLine());
 
       int k = int.Parse(Console.ReadLine());
 
       Function(100,100);
 
  }
 
  static void Function(int n, int k) {
 
       int i, j = 0;
 
bool[] a = new bool[1000]; //初始false:关灯,true:开灯 
 
for (i = 1; i <= k; i++)      //k个人
 
for (j = 1; j <= n; j++)  //n个灯
 
               if (j % i == 0)
 
a[j] = !a[j]; //取反,false变true,原来开变关,关变开
 
for (i = 1; i <= n; i++) //最后输出a[i]的值就可以了
 
if (a[i]) //灯亮着
 
                   Console.WriteLine(i);
 
}

36.数制转换,将任意整数转换成8进制形式

static void Main(string[] args) {
 
      int n;
 
      n =int.Parse(Console.ReadLine());
 
Console.WriteLine("输入的10进制为:{0}",n);
 
Console.Write("转换为8进制数为: ");
 
      d2o(n);
 
}
 
static void d2o(int n) {
 
     if (n > 7) {
 
          d2o(n / 8);
 
     }
 
     Console.Write(n%8);
 
}

37.找出200以内的素数。

static void Main(string[] args) {
 
    int count = 0;
 
    for (int i = 1; i < 200; i++) {//外层循环:要判断的数
 
        for (int j = 2; j <=i; j++){
 
            if (i % j == 0&& i!=j) {
 
                 break;
 
             }
 
             if (j == i ) {//结束的条件:最后一个数还没有被整除 
 
                  count++;
 
                  Console.WriteLine(i);
 
             }
 
        }
 
    }
 
    Console.WriteLine(count);
 
}

38.打印杨辉三角形

public static void YHSJ(){
 
    int [][]a= new int[7][] ;
 
    a[0] = new int[1];  //a[0][0]=1;
 
    a[1] = new int[2] ;
 
    for (int i = 0; i < 7; i++) {
 
        a[i] = new int[i+1] ;  
 
        a[i][0] =1;
 
        a[i][i]=1;
 
        if(i>1) {//求出中间的数据
 
    for(int j=1;j
 
                a[i][j]= a[i-1][j-1]+a[i-1][j];
 
            }
 
        }
 
     }
 
     for (int i=0; i
 
         for (int k = 0; k < a.Length-1-i; k++) {
 
             Console.Write("");
 
         }
 
         for(int j=0;j
 
             Console.Write(a[i][j] + "");
 
         }
 
         Console.WriteLine();
 
     }
 
}

39.中国有句俗话“三天打鱼两天晒网”,某人从2000年1月1日起开始“三天打鱼两天晒网”,问这个人在今后的某天中“打鱼”还是”晒网”

public static void Compute(){
 
Console.WriteLine ((DateTime.Now - DateTime.Parse("2000-01-01")).Days%5<3?"打鱼":"晒网");
 
}

40.假设当前市场价一只鸡10元,一只鸭12元5角。请写一个函数ShowPrice,输入参数分别为鸡和鸭的个数(非负整型),功能为显示出总价钱,精确到分。例如调用ShowPrice(5,10)后输出175.00。请注意程序的可读性和易于维护性。

static void ShowPrice(int num_chicken, int num_duck)  {
 
      float totalPrice = 0.00f;
 
      float price_chicken = 10f;
 
      float price_duck = 12.5f;
 
      totalPrice = num_chicken * price_chicken + num_duck * price_duck;
 
Console.WriteLine("总价钱为:{0:0.00}", totalPrice);
 
  }

41.请写一个函数,用于返回n!(阶乘)结果末尾连续0的个数,如GetZeroCount(5)返回1,因为5! = 120,末尾连续1个0

  static void Main(string[] args) {
 
        int fac = Factorial(5);
 
        Console.WriteLine(CountZero(fac));
 
    }
 
    public static int Factorial(int n) {
 
        if (n == 1) {
 
            return 1;
 
        } else {
 
            return n * jiecheng(n - 1);
 
        }
 
    }
 
//求连续的0的个数
 
    public static int CountZero(int num) {
 
int result = 0; //最后的结果
 
        String numStr = num.ToString();
 
        for (int i = numStr.Length - 1; i >= 0; i--) {
 
            if (numStr[i] == '0') {
 
               result ++;
 
            } else {
 
                break;
 
            }
 
        }
 
        return result;
 
    }

42、C#中 委托和事件的区别

委托是一个类,该类内部维护着一个字段,指向一个方法。
事件可以被看作一个委托类型的变量,通过事件注册、取消多个委托或方法。

通过委托执行方法

class Program 
{
     static void Main(string[] args)
     {
         Example example = new Example();
         example.Go(); 
         Console.ReadKey();
     }
 }
 
public class Example
{
    public delegate void DoSth(string str);
    
    internal void Go()
    {
        //声明一个委托变量,并把已知方法作为其构造函数的参数
        DoSth d = new DoSth(Print);  
        string str = "Hello,World"; 
        
        //通过委托的静态方法Invoke触发委托 
        d.Invoke(str);
    } 

    void Print(string str)
    {
        Console.WriteLine(str);
    }

}

以上在CLR运行时,委托DoSth实际上就一个类,该类有一个参数类型为方法的构造函数,并且提供了一个Invoke实例方法,用来触发委托的执行。

  • 委托DoSth定义了方法的参数和返回类型
  • 通过委托DoSth的构造函数,可以把符合定义的方法赋值给委托
  • 调用委托的实例方法Invoke执行了方法

但,实际上让委托执行方法还有另外一种方式,那就是:委托变量(参数列表)

public class Example  
{ 
     public delegate void DoSth(object sender, EventArgs e); 
     internal void Go() 
     { 
         //声明一个委托变量,并把已知方法作为其构造函数的参数
         DoSth d = new DoSth(Print); 
         object sender = 10; 
         EventArgs e = new EventArgs(); 
         d(sender, e); 
     }
      
     void Print(object sender, EventArgs e) 
     { 
         Console.WriteLine(sender); 
     } 
 }

以上,

  • 委托DoSth的参数列表和方法Print的参数列表还是保持一致
  • 委托DoSth中的参数object sender通常用来表示动作的发起者,EventArgs e用来表示动作所带的参数。

而实际上,委托变量(参数列表),事件就是采用这种形式执行方法的。
通过事件执行方法

public class Example  
{ 
     public delegate void DoSth(object sender, EventArgs e); 
     public event DoSth myDoSth; 
     internal void Go() 
     { 
         //声明一个委托变量,并把已知方法作为其构造函数的参数
         DoSth d = new DoSth(Print); 
         object sender = 10; 
         EventArgs e = new EventArgs(); 
         myDoSth += new DoSth(d); 
         myDoSth(sender, e);
     }

     void Print(object sender, EventArgs e) 
     { 
         Console.WriteLine(sender); 
     } 
 }

以上,

  • 声明了事件myDoSth,事件的类型是DoSth这个委托
  • 通过+=为事件注册委托
  • 通过DoSth委托的构造函数为事件注册委托实例
  • 采用委托变量(参数列表)这种形式,让事件执行方法
    而且,通过+=还可以为事件注册多个委托。
public class Example  
{ 
    public delegate void DoSth(object sender, EventArgs e); 
    public event DoSth myDoSth;

    internal void Go() 
    { 
        //声明一个委托变量,并把已知方法作为其构造函数的参数 
        DoSth d = new DoSth(Print); 
        DoSth d1 = new DoSth(Say); 
        object sender = 10; 
        EventArgs e = new EventArgs(); 
      
        //为事件注册多个委托 
        myDoSth += new DoSth(d); 
        myDoSth += new DoSth(d1); 
        myDoSth(sender, e); 
    }

    void Print(object sender, EventArgs e) 
    { 
        Console.WriteLine(sender); 
    }

    void Say(object sender, EventArgs e) 
    { 
        Console.WriteLine(sender); 
    } 
}

以上,通过+=为事件注册1个或多个委托实例,实际上,还可以为事件直接注册方法。

public class Example 
{
    public delegate void DoSth(object sender, EventArgs e); 
    public event DoSth myDoSth;  
    internal void Go()
    { 
        object sender = 10; 
        EventArgs e = new EventArgs();  

        //为事件注册多个委托 
        myDoSth += Print; 
        myDoSth += Say; 
        myDoSth(sender, e); 
    }

    void Print(object sender, EventArgs e) 
    { 
        Console.WriteLine(sender); 
    }

    void Say(object sender, EventArgs e) 
    {
         Console.WriteLine(sender); 
    }

}   

通过EventHandler执行方法
先来看EventHandler的源代码。
在这里插入图片描述
可见,EventHandler就是委托。现在就使用EventHandler来执行多个方法。

 
 
public class Example  
{ 
    public event EventHandler myEvent; 
    internal void Go() 
    { 
        object sender = 10; 
        EventArgs e = new EventArgs(); 
        //为事件注册多个委托 
        myEvent += Print; 
        myEvent += Say;
        myEvent(sender, e); 
    }

    void Print(object sender, EventArgs e) 
    { 
        Console.WriteLine(sender); 
    }

    void Say(object sender, EventArgs e) 
    {
         Console.WriteLine(sender); 
    } 
}

总结:

  • 委托就是一个类,也可以实例化,通过委托的构造函数来把方法赋值给委托实例
  • 触发委托有2种方式: 委托实例.Invoke(参数列表),委托实例(参数列表)
  • 事件可以看作是一个委托类型的变量
  • 通过+=为事件注册多个委托实例或多个方法
  • 通过-=为事件注销多个委托实例或多个方法
  • EventHandler就是一个委托

1.事件的声明只是在委托前面加一个event关键词,虽然你可以定义一个public,但是有了event关键词后编译器始终会把这个委托声明为private,然后添加1组add,remove方法。add对应+=,remove对应-=。这样就导致事件只能用+=,-=来绑定方法或者取消绑定方法。而委托可以用=来赋值,当然委托也是可以用+=,-=来绑定方法的

2.委托可以在外部被其他对象调用,而且可以有返回值(返回最后一个注册方法的返回值)。而事件不可以在外部调用,只能在声明事件的类内部被调用。我们可以使用这个特性来实现观察者模式。大概就是这么多。下面是一段测试代码。

namespace delegateEvent
{
    public delegate string deleFun(string word);
 
    public class test
    {
        public event deleFun eventSay;
        public deleFun deleSay;
        public void doEventSay(string str)
        {
            if (eventSay!=null)
                eventSay(str);
        }
    }
 
 
    class Program
    {
        static void Main(string[] args)
        {
            test t = new test();
            t.eventSay += t_say;
            t.deleSay += t_say;
            t.deleSay += t_say2;
            //t.eventSay("eventSay"); 错误 事件不能在外部直接调用
            t.doEventSay("eventSay");//正确 事件只能在声明的内部调用
            string str = t.deleSay("deleSay");//正确 委托可以在外部被调用 当然在内部调用也毫无压力 而且还能有返回值(返回最后一个注册的方法的返回值)
            Console.WriteLine(str);
            Console.Read();
        }
 
        static string t_say(string word)
        {
            Console.WriteLine(word);
 
            return "return "+word;
        }
 
        static string t_say2(string word)
        {
            Console.WriteLine(word);
 
            return "return " + word + " 2";
        }
    }
}

43、协同程序的执行代码是什么?有何用处,有何缺点?

function Start() { 
    // 协同程序WaitAndPrint在Start函数内执行,可以视同于它与Start函数同步执行.
    StartCoroutine(WaitAndPrint(2.0)); 
    print ("Before WaitAndPrint Finishes " + Time.time );
}

function WaitAndPrint (waitTime : float) {
    // 暂停执行waitTime秒
    yield WaitForSeconds (waitTime);
    print ("WaitAndPrint "+ Time.time );
}

作用:一个协同程序在执行过程中,可以在任意位置使用yield语句。yield的返回值控制何时恢复协同程序向下执行。协同程序在对象自有帧执行过程中堪称优秀。协同程序在性能上没有更多的开销。
缺点:协同程序并非真线程,可能会发生堵塞。

45、什么是里氏代换元则?

里氏替换原则(Liskov Substitution Principle LSP)面向对象设计的基本原则之一。
通俗点:就是子类对象可以赋值给基类对象,基类对象不能赋值给子类对象

46、编写一个函数,输入一个32位整数,计算这个整数有多少个bit为1

uint BitCount (uint n)
{
    uint c = 0; // 计数器
    while (n > 0) {
        if ((n & 1) == 1) // 当前位是1
            ++c; // 计数器加1
        n >>= 1; // 移位
    }
    return c;
}

47、简述static和const关键字的作用

static 关键字至少有下列几个作用:
(1)函数体内static 变量的作用范围为该函数体,不同于auto 变量,该变量的内存只被分配一次,因此其值在下次调用时仍维持上次的值;
(2)在模块内的static 全局变量可以被模块内所用函数访问,但不能被模块外其它函数访问;
(3)在模块内的static 函数只可被这一模块内的其它函数调用,这个函数的使用范围被限制在声明它的模块内;
(4)在类中的static 成员变量属于整个类所拥有,对类的所有对象只有一份拷贝;
(5)在类中的static 成员函数属于整个类所拥有,这个函数不接收this 指针,因而只能访问类的static 成员变量。
const 关键字至少有下列几个作用:
(1)欲阻止一个变量被改变,可以使用const 关键字。在定义该const 变量时,通常需要对它进行初始化,因为以后就没有机会再去改变它了;
(2)对指针来说,可以指定指针本身为const,也可以指定指针所指的数据为const,或二者同时指定为const;
(3)在一个函数声明中,const 可以修饰形参,表明它是一个输入参数,在函数内部不能改变其值;
(4)对于类的成员函数,若指定其为const 类型,则表明其是一个常函数,不能修改类的成员变量;
(5)对于类的成员函数,有时候必须指定其返回值为const 类型,以使得其返回值不为“左值”。

48、计算 s = 1!+2!+3!+…+num!的代码。num为输入,s为输出。(!代表阶乘 3!= 1 * 2 * 3)

Console.ReadLine(num)
int s = 0;
for(int i = 1; i <= num; i++)
{
    s += JieCheng(num);
}
public int JieCheng(int num)
{
    if(num < 0)
    {
        Console.WriteLine("error");
        return;
    }
    if(num <=1)
    {
        return 1;
    }
    else {
        return num * JieCheng(num - 1)
    }
}

49、用你熟悉的语言从一个字符串中去掉相连的重复字符,例如原字符串“adffjkljaalkjhl”变为“adfjkljalkjhl”

int GetResult(char[] input, char[] output)  
{  
    int i, j, k = 0;  
    int flag;  
    int length;  
    if(input == NULL || output == NULL)  
    {  
        return -1;  
    }  
    length=strlen(input);//求数组的长度  
    for(i = 0; i<length; i++)  
    {  
        flag = 1;  
        for(j = 0; j < i; j++)  
        {  
            if(output[j] == input [i])  
            flag = 0;  
        }  
        if(flag)  
        output[k++] = input[i];  
    }  
    printf("最终的字符串为:");  
    output[k] = '\0';  
    for(int m = 0; m < output.Length; m++)
    {
        print (output [m]);
    } 
    return 0;  
}  

50、在一个单链表中,如何判断是否有环?

第一种,对链表遍历的同时,记录已访问过的结点,如果遍历过程中发现有结点是此前已经访问过的结点,说明有环;第二种方法,快慢指针法,快指针每次前进两个,慢指针每次前进一个,如果快慢指针相遇了,说明有环。

51、有限状态机和行为树的区别

附录:

C#常用的数据结构详解 :Array,ArrayList,List,LinkedList,Queue,Stack,Dictionary,t>

1、数组

特点:

数组存储在连续的内存上。 数组的内容都是相同类型。 数组可以直接通过下标访问。

数组创建

  • int size = 10;
  • int[] test = new int[size];

创建一个新的数组时将在 CLR 托管堆中分配一块连续的内存空间,来盛放数量为size,类型为所声明类型的数组元素。
如果类型为值类型,则将会有size个未装箱的该类型的值被创建。
如果类型为引用类型,则将会有size个相应类型的引用被创建。
优点:
由于是在连续内存上存储的,所以它的索引速度非常快,访问一个元素的时间是恒定的也就是说与数组的元素数量无关,而且赋值与修改元素也很简单。

string[] test2 = new string[3];

**//赋值**

test2[0] = "chen";
test2[1] = "j";
test2[2] = "d";

**//修改**

test2[0] = "chenjd";

缺点

  1. 由于是连续存储,所以在两个元素之间插入新的元素就变得不方便。
  2. 声明一个新的数组时,必须指定其长度,这就会存在一个潜在的问题,那就是当我们声明的长度过长时,显然会浪费内存,当我们声明长度过短的时候,则面临这溢出的风险。

针对这种缺点,下面隆重推出ArrayList。

2、ArrayList:

为了解决数组创建时必须指定长度以及只能存放相同类型的缺点而推出的数据结构。ArrayList是System.Collections命名空间下的一部分,所以若要使用则必须引入System.Collections。正如上文所说,ArrayList解决了数组的一些缺点。

不必在声明ArrayList时指定它的长度,这是由于ArrayList对象的长度是按照其中存储的数据来动态增长与缩减的。
ArrayList可以存储不同类型的元素。这是由于ArrayList会把它的元素都当做Object来处理。因而,加入不同类型的元素是允许的。

ArrayList的操作:
ArrayList test3 = new ArrayList();

//新增数据
test3.Add("i");
test3.Add("j");
test3.Add("d");
test3.Add("is");
test3.Add(25);
test3[4] = 26;

test3.RemoveAt(4);

缺点
那是因为ArrayList可以存储不同类型数据的原因是由于把所有的类型都当做Object来做处理,也就是说ArrayList的元素其实都是Object类型的,辣么问题就来了。

  1. ArrayList不是类型安全的。因为把不同的类型都当做Object来做处理,很有可能会在使用ArrayList时发生类型不匹配的情况。

如上文所诉,数组存储值类型时并未发生装箱,但是ArrayList由于把所有类型都当做了Object,所以不可避免的当插入值类型时会发生装箱操作,在索引取值时会发生拆箱操作。这能忍吗?

注:为何说频繁的没有必要的装箱和拆箱不能忍呢?
装箱 (boxing):就是值类型实例到对象的转换
拆箱:就是将引用类型转换为值类型

//装箱,将值类型转成引用类型
int  info = 1989;  
object obj=(object)info;  

//拆箱,引用类型转换成值类型
object obj = 1;
int info = (int)obj;

显然,从原理上可以看出,装箱时,生成的是全新的引用对象,这会有时间损耗,也就是造成效率降低
那么为了解决ArrayList不安全类型与装箱拆箱的缺点,所以出现了泛型 的概念,作为一种新的数组类型引入。也是工作中经常用到的数组类型。和ArrayList很相似,长度都可以灵活的改变,最大的不同在于在声明List集合时,我们同时需要为其声明List集合内数据的对象类型,这点又和Array很相似,其实List内部使用了Array来实现。

3、List (泛型)

List<string> test4 = new List<string>(); 

//新增数据

test4.Add(“Fanyoy”); 

test4.Add(“Chenjd”);  

//修改数据 

test4[1] = “murongxiaopifu”; 

//移除数据 

test4.RemoveAt(0);

这么做最大的好处就是:

(1)即确保了类型安全;
(2)也取消了装箱和拆箱的操作;
(3)它融合了Array可以快速访问的优点以及ArrayList长度可以灵活变化的优点。

也就是链表了。
和上述的数组最大的不同之处

就是在于链表在内存存储的排序上可能是不连续的。
这是由于链表是通过上一个元素指向下一个元素来排列的,所以可能不能通过下标来访问。如图

在这里插入图片描述

既然链表最大的特点就是存储在内存的空间不一定连续,那么链表相对于数组最大优势和劣势就显而易见了。

向链表中插入或删除节点无需调整结构的容量。因为本身不是连续存储而是靠各对象的指针所决定,所以添加元素和删除元素都要比数组要有优势

链表适合在需要有序的排序的情境下增加新的元素,这里还拿数组做对比,例如要在数组中间某个位置增加新的元素,则可能需要移动移动很多元素,而对于链表而言可能只是若干元素的指向发生变化而已。

缺点

由于其在内存空间中不一定是连续排列,所以访问时候无法利用下标,而是必须从头结点开始,逐次遍历下一个节点直到寻找到目标。所以当需要快速访问对象时,数组无疑更有优势。

综上,链表适合元素数量不固定,需要经常增减节点的情况。

4、Queue

“先进先出”(FIFO—first in first out)的线性表。通过使用Enqueue和Dequeue这两个方法来实现对 Queue 的存取。

默认情况下,Queue的初始容量为32, 增长因子为2.0。

当使用Enqueue时,会判断队列的长度是否足够,若不足,则依据增长因子来增加容量,例如当为初始的2.0时,则队列容量增长2倍。

5、Stack

后进先出顺序(LIFO)的数据结构

默认容量为10。

使用pop和push来操作。

6、Dictionary<K,T>

提到字典就不得不说Hashtable哈希表以及Hashing(哈希,也有叫散列的),因为字典的实现方式就是哈希表的实现方式,只不过字典是类型安全的,也就是说当创建字典时,必须声明key和item的类型,这是第一条字典与哈希表的区别

哈希

简单的说就是一种将任意长度的消息压缩到某一固定长度,比如某学校的学生学号范围从0000099999,总共5位数字,若每个数字都对应一个索引的话,那么就是100000个索引,但是如果我们使用后3位作为索引,那么索引的范围就变成了000999了,当然会冲突的情况,这种情况就是哈希冲突(Hash Collisions)了。

回到Dictionary<K,T>
劣势就是空间
以空间换时间,通过更多的内存开销来满足我们对速度的追求

在创建字典时,我们可以传入一个容量值,但实际使用的容量并非该值。而是使用“不小于该值的最小质数来作为它使用的实际容量,最小是3。”,当有了实际容量之后,并非直接实现索引,而是通过创建额外的2个数组来实现间接的索引,即int[]buckets和Entry[]entries两个数组(即buckets中保存的其实是entries数组的下标),这里就是第二条字典与哈希表的区别,还记得哈希冲突吗?对,第二个区别就是处理哈希冲突的策略是不同的!
字典会采用额外的数据结构来处理哈希冲突,这就是刚才提到的数组之一buckets桶了,buckets的长度就是字典的真实长度,因为buckets就是字典每个位置的映射,然后buckets中的每个元素都是一个链表,用来存储相同哈希的元素,然后再分配存储空间。

在这里插入图片描述

因此,我们面临的情况就是,即便我们新建了一个空的字典,那么伴随而来的是2个长度为3的数组。所以当处理的数据不多时,还是慎重使用字典为好,很多情况下使用数组也是可以接受的。

在这里插入图片描述

爱你不跪的模样
爱你对峙过绝望不肯哭一场
你将造你的城邦在废墟之上

文章来源:https://blog.csdn.net/Lareinaba/article/details/134898388
本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。