C#面试题及详细答案120道(11-20)-- 面向对象编程(OOP)
《前后端面试题
》专栏集合了前后端各个知识模块的面试题,包括html,javascript,css,vue,react,java,Openlayers,leaflet,cesium,mapboxGL,threejs,nodejs,mangoDB,SQL,Linux… 。
文章目录
- 一、本文面试题目录
- 11. 简述面向对象的三大特性(封装、继承、多态)
- 12. 什么是接口(interface)?接口与抽象类(abstract class)的区别
- 13. 什么是密封类(sealed class)?使用场景是什么?
- 14. 方法重载(Overload)和方法重写(Override)的区别
- 15. 什么是虚方法(virtual method)?如何实现?
- 16. 什么是隐藏方法(new关键字)?与重写的区别
- 17. 什么是构造函数?静态构造函数的特点
- 18. 什么是析构函数?与IDisposable接口的区别
- 19. 什么是索引器(Indexer)?如何实现?
- 20. 什么是部分类(partial class)?使用场景
- 二、120道C#面试题目录列表
一、本文面试题目录
11. 简述面向对象的三大特性(封装、继承、多态)
面向对象编程(OOP)的三大核心特性是封装、继承和多态,它们共同构成了面向对象设计的基础:
1. 封装(Encapsulation)
- 原理:将数据(字段)和操作数据的方法捆绑在一起,隐藏内部实现细节,仅通过公共接口暴露必要功能。
- 作用:提高安全性(防止数据被随意修改)、简化使用(只需关注接口而非实现)。
- 示例:
public class Person
{// 私有字段(封装数据)private string _name;// 公共属性(提供访问接口)public string Name{get => _name;set {if (string.IsNullOrWhiteSpace(value))throw new ArgumentException("姓名不能为空");_name = value;}}
}
2. 继承(Inheritance)
- 原理:允许一个类(子类)继承另一个类(父类)的属性和方法,实现代码复用和扩展。
- 作用:减少代码重复,建立类之间的层次关系。
- 示例:
// 父类
public class Animal
{public void Eat() => Console.WriteLine("吃东西");
}// 子类继承父类
public class Dog : Animal
{// 扩展父类功能public void Bark() => Console.WriteLine("汪汪叫");
}// 使用
var dog = new Dog();
dog.Eat(); // 继承的方法
dog.Bark(); // 自己的方法
3. 多态(Polymorphism)
- 原理:同一操作作用于不同对象可产生不同结果,允许使用基类引用指向派生类对象。
- 作用:提高代码灵活性和扩展性,支持"开闭原则"。
- 示例:
public class Shape
{public virtual void Draw() => Console.WriteLine("绘制形状");
}public class Circle : Shape
{public override void Draw() => Console.WriteLine("绘制圆形");
}public class Square : Shape
{public override void Draw() => Console.WriteLine("绘制正方形");
}// 使用多态
Shape[] shapes = { new Shape(), new Circle(), new Square() };
foreach (var shape in shapes)
{shape.Draw(); // 根据实际类型执行不同实现
}
12. 什么是接口(interface)?接口与抽象类(abstract class)的区别
接口(interface) 是一种契约,定义了类或结构必须实现的成员(方法、属性、事件等),但不包含实现细节。
语法示例:
public interface IVehicle
{void Start();void Stop();int Speed { get; set; }
}// 实现接口
public class Car : IVehicle
{public int Speed { get; set; }public void Start() => Console.WriteLine("汽车启动");public void Stop() => Console.WriteLine("汽车停止");
}
接口与抽象类的区别:
特性 | 接口 | 抽象类 |
---|---|---|
成员实现 | 只能定义成员,不能有实现 | 可以有抽象成员(无实现)和具体成员(有实现) |
继承限制 | 一个类可以实现多个接口 | 一个类只能继承一个抽象类(单继承) |
构造函数 | 不能有构造函数 | 可以有构造函数 |
访问修饰符 | 成员默认为public(不能显式指定其他修饰符) | 成员可以有各种访问修饰符 |
字段 | 不能包含字段(可包含自动属性) | 可以包含字段 |
多态用途 | 定义跨层次结构的功能契约 | 用于同层次结构的代码复用 |
版本兼容 | 新增成员不影响现有实现(C# 8.0+支持默认实现) | 新增抽象成员会破坏现有继承类 |
适用场景:
- 接口:定义不相关类的共同行为(如
IDisposable
)、实现多继承效果、制定API契约。 - 抽象类:在相关类之间共享代码、定义基础行为并允许派生类扩展。
13. 什么是密封类(sealed class)?使用场景是什么?
密封类(sealed class) 是不允许被继承的类,使用sealed
关键字修饰。
语法示例:
public sealed class MathHelper
{public static int Add(int a, int b) => a + b;
}// 错误:无法继承密封类
// public class AdvancedMath : MathHelper { }
密封方法(sealed method):
密封类中的方法默认是密封的,也可以在非密封类中用sealed
修饰重写的方法,防止进一步重写:
public class BaseClass
{public virtual void Method() { }
}public class DerivedClass : BaseClass
{// 密封此方法,防止子类重写public sealed override void Method() { }
}public class GrandchildClass : DerivedClass
{// 错误:无法重写密封方法// public override void Method() { }
}
使用场景:
- 安全保护:防止核心类被恶意继承和修改(如加密算法类)。
- 性能优化:JIT编译器可对密封类进行优化,调用方法时无需检查虚方法表。
- 设计完整性:当类的设计不适合扩展时(如工具类、数据传输对象)。
- 防止破坏封装:避免子类修改基类的关键行为导致逻辑错误。
注意:密封类不能同时是抽象类(抽象类必须被继承才有意义)。
14. 方法重载(Overload)和方法重写(Override)的区别
方法重载(Overload) 和方法重写(Override) 是实现多态的两种方式,但机制完全不同:
特性 | 方法重载 | 方法重写 |
---|---|---|
定义 | 同一类中,方法名相同,参数列表不同 | 父子类中,方法名、参数、返回值完全相同 |
关键字 | 不需要特殊关键字 | 需要virtual (基类)和override (派生类) |
目的 | 提供相同功能的不同实现方式 | 子类重新定义父类的方法实现 |
绑定时机 | 编译时绑定(静态多态) | 运行时绑定(动态多态) |
返回值 | 可以不同(但不能仅靠返回值区分) | 必须相同(或协变返回类型) |
访问修饰符 | 可以不同 | 不能比基类方法更严格 |
示例代码:
// 方法重载示例
public class Calculator
{// 重载1:两个int参数public int Add(int a, int b) => a + b;// 重载2:三个int参数public int Add(int a, int b, int c) => a + b + c;// 重载3:两个double参数public double Add(double a, double b) => a + b;
}// 方法重写示例
public class Animal
{// 基类虚方法public virtual void MakeSound() => Console.WriteLine("动物发出声音");
}public class Cat : Animal
{// 重写基类方法public override void MakeSound() => Console.WriteLine("猫喵喵叫");
}// 使用
Animal animal = new Cat();
animal.MakeSound(); // 输出"猫喵喵叫"(运行时多态)
使用场景:
- 重载:同一操作有不同参数需求(如
Console.WriteLine
支持多种参数类型)。 - 重写:子类需要提供与父类不同的实现(如不同动物的叫声不同)。
15. 什么是虚方法(virtual method)?如何实现?
虚方法(virtual method) 是在基类中声明的、允许派生类重写(override)其实现的方法,是实现多态的基础。
实现步骤:
- 在基类中用
virtual
关键字声明方法 - 在派生类中用
override
关键字重写方法 - 通过基类引用调用方法时,会根据实际对象类型执行相应实现
示例代码:
// 基类定义虚方法
public class Document
{public string Title { get; set; }// 虚方法:提供默认实现public virtual void Print(){Console.WriteLine($"打印文档: {Title}");}
}// 派生类重写虚方法
public class PdfDocument : Document
{// 重写基类方法public override void Print(){// 可以调用基类实现base.Print();// 增加PDF特有的打印逻辑Console.WriteLine("使用PDF打印机打印");}
}public class WordDocument : Document
{// 重写基类方法public override void Print(){Console.WriteLine($"打印Word文档: {Title}(包含格式)");}
}// 使用虚方法实现多态
public static void Main()
{Document[] documents = {new Document { Title = "普通文档" },new PdfDocument { Title = "报告.pdf" },new WordDocument { Title = "合同.docx" }};foreach (var doc in documents){doc.Print(); // 根据实际类型调用不同实现}
}
输出结果:
打印文档: 普通文档
打印文档: 报告.pdf
使用PDF打印机打印
打印Word文档: 合同.docx(包含格式)
注意事项:
- 虚方法必须有实现(哪怕是空实现)
- 静态方法、密封方法、私有方法不能声明为虚方法
- 派生类可以选择是否重写虚方法(不重写则使用基类实现)
16. 什么是隐藏方法(new关键字)?与重写的区别
隐藏方法是指在派生类中用new
关键字声明与基类同名的方法,从而隐藏基类的方法实现,而非重写。
语法示例:
public class BaseClass
{public void ShowMessage(){Console.WriteLine("这是基类方法");}
}public class DerivedClass : BaseClass
{// 使用new关键字隐藏基类方法public new void ShowMessage(){Console.WriteLine("这是派生类方法");}
}
隐藏与重写的区别:
特性 | 隐藏方法(new) | 重写方法(override) |
---|---|---|
机制 | 创建新的方法,隐藏基类方法 | 替换基类虚方法的实现 |
关键字 | 使用new | 需要基类virtual 和派生类override |
多态行为 | 不支持多态,调用取决于引用类型 | 支持多态,调用取决于实际对象类型 |
基类方法要求 | 基类方法可以是任意方法 | 基类方法必须是virtual 、abstract 或override 的 |
行为对比示例:
public static void Main()
{// 隐藏方法的行为BaseClass baseObj = new DerivedClass();DerivedClass derivedObj = new DerivedClass();baseObj.ShowMessage(); // 输出"这是基类方法"(取决于引用类型)derivedObj.ShowMessage(); // 输出"这是派生类方法"(取决于引用类型)// 重写方法的行为(对比)BaseClass baseVirtual = new DerivedVirtual();baseVirtual.VirtualMethod(); // 输出"派生类重写的方法"(取决于实际对象类型)
}// 重写对比示例
public class BaseVirtual
{public virtual void VirtualMethod() => Console.WriteLine("基类虚方法");
}public class DerivedVirtual : BaseVirtual
{public override void VirtualMethod() => Console.WriteLine("派生类重写的方法");
}
使用建议:
- 隐藏方法会破坏多态性,一般不推荐使用
- 必须使用时应显式添加
new
关键字(避免编译器警告) - 优先使用重写(override)实现多态行为
17. 什么是构造函数?静态构造函数的特点
构造函数是一种特殊的方法,用于在创建对象时初始化对象,与类同名且无返回值。
实例构造函数示例:
public class Person
{public string Name { get; set; }public int Age { get; set; }// 无参构造函数public Person(){Name = "未知";Age = 0;}// 带参构造函数public Person(string name, int age){Name = name;Age = age;}
}// 使用
var person1 = new Person(); // 调用无参构造函数
var person2 = new Person("张三", 30); // 调用带参构造函数
静态构造函数是用于初始化类的静态成员的特殊构造函数,具有以下特点:
- 声明方式:使用
static
关键字,无参数,无访问修饰符 - 调用时机:第一次访问类(创建实例或访问静态成员)时自动调用,仅调用一次
- 用途:初始化静态字段、注册事件、加载资源等
静态构造函数示例:
public class Logger
{// 静态字段public static string LogFilePath;// 静态构造函数static Logger(){Console.WriteLine("静态构造函数调用");LogFilePath = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "log.txt");// 可以执行初始化文件、注册事件等操作}// 实例构造函数public Logger(){Console.WriteLine("实例构造函数调用");}
}// 使用
public static void Main()
{// 第一次访问类,触发静态构造函数Logger.LogFilePath = "test.log";// 创建实例,静态构造函数不会再次调用var logger1 = new Logger();var logger2 = new Logger();
}
输出结果:
静态构造函数调用
实例构造函数调用
实例构造函数调用
注意事项:
- 静态构造函数不能被直接调用
- 若未定义静态构造函数,编译器会自动生成默认的
- 静态构造函数中发生的异常会导致类型初始化失败,该类型将无法使用
18. 什么是析构函数?与IDisposable接口的区别
析构函数(Destructor)是用于在对象被垃圾回收前释放非托管资源的特殊方法。
语法与特点:
- 名称与类名相同,前缀为
~
- 无参数、无返回值、无访问修饰符
- 不能显式调用,由垃圾回收器自动调用
- 仅适用于引用类型(类)
析构函数示例:
public class FileHandler
{private FileStream _stream;public FileHandler(string filePath){_stream = new FileStream(filePath, FileMode.Open);}// 析构函数~FileHandler(){// 释放非托管资源_stream?.Dispose();Console.WriteLine("析构函数执行,释放资源");}
}
IDisposable接口用于显式释放托管和非托管资源,定义了Dispose()
方法供手动调用。
IDisposable实现示例:
public class ResourceHandler : IDisposable
{private bool _disposed = false;private FileStream _fileStream; // 托管资源private IntPtr _unmanagedResource; // 非托管资源public void Dispose(){Dispose(true);// 告诉GC不需要调用析构函数GC.SuppressFinalize(this);}// 核心释放逻辑protected virtual void Dispose(bool disposing){if (_disposed) return;// 释放托管资源if (disposing){_fileStream?.Dispose();}// 释放非托管资源if (_unmanagedResource != IntPtr.Zero){// 调用非托管释放函数CloseHandle(_unmanagedResource);_unmanagedResource = IntPtr.Zero;}_disposed = true;}// 析构函数:仅释放非托管资源~ResourceHandler(){Dispose(false);}// 外部非托管函数[DllImport("kernel32.dll")]private static extern bool CloseHandle(IntPtr hObject);
}
析构函数与IDisposable的区别:
特性 | 析构函数 | IDisposable接口 |
---|---|---|
调用方式 | 由GC自动调用,不可控 | 手动调用或通过using语句自动调用 |
释放内容 | 仅适合释放非托管资源 | 可释放托管和非托管资源 |
执行时机 | 不确定(取决于GC) | 确定(调用时立即执行) |
性能影响 | 会延长对象生命周期(进入终结队列) | 可及时释放资源,提高性能 |
使用场景 | 作为释放非托管资源的后备机制 | 主要的资源释放方式 |
最佳实践:
- 实现
IDisposable
接口进行主动资源释放 - 使用
using
语句确保Dispose()
被调用 - 析构函数仅作为最后防线,释放未被手动释放的非托管资源
19. 什么是索引器(Indexer)?如何实现?
索引器(Indexer) 允许类或结构像数组一样通过索引访问,使对象可以模拟数组的行为。
实现语法:
public 返回类型 this[参数列表]
{get { /* 获取逻辑 */ }set { /* 设置逻辑 */ }
}
示例:自定义集合类
public class StringCollection
{private List<string> _items = new List<string>();// 索引器实现public string this[int index]{get{// 验证索引有效性if (index < 0 || index >= _items.Count)throw new IndexOutOfRangeException("索引超出范围");return _items[index];}set{// 支持动态扩展if (index >= _items.Count){_items.AddRange(Enumerable.Repeat("", index - _items.Count + 1));}_items[index] = value;}}// 还可以实现其他索引类型(如字符串索引)public int this[string item]{get => _items.IndexOf(item);}public int Count => _items.Count;
}// 使用索引器
public static void Main()
{var collection = new StringCollection();// 像数组一样赋值collection[0] = "第一个元素";collection[1] = "第二个元素";// 像数组一样访问Console.WriteLine(collection[0]); // 输出"第一个元素"// 使用字符串索引Console.WriteLine(collection["第二个元素"]); // 输出 1
}
索引器的特点:
- 可以重载(定义多个不同参数的索引器)
- 支持任意类型的索引参数(不局限于int)
- 可以像属性一样使用访问修饰符和验证逻辑
- 接口中也可以定义索引器(无实现)
与数组的区别:
- 索引器可以使用非整数索引(如字符串、枚举)
- 索引器的访问器可以有复杂逻辑(验证、计算等)
- 索引器没有
Length
属性,需自行实现
常见应用:
- 自定义集合类(如
List<T>
内部实现了索引器) - 字典类(如
Dictionary<TKey, TValue>
的键索引) - 包装复杂数据结构,提供简化访问方式
20. 什么是部分类(partial class)?使用场景
部分类(partial class) 允许将一个类的定义拆分到多个源文件中,编译时会合并为一个完整的类。
语法示例:
文件1:Person.cs
public partial class Person
{public string Name { get; set; }public int Age { get; set; }public void Introduce(){Console.WriteLine($"我叫{Name},今年{Age}岁");}
}
文件2:Person_Additional.cs
public partial class Person
{public string Address { get; set; }public void Move(string newAddress){Address = newAddress;Console.WriteLine($"搬到了{newAddress}");}
}
编译后,这两个文件会被视为一个完整的Person
类:
// 等效于合并后的类
public class Person
{public string Name { get; set; }public int Age { get; set; }public string Address { get; set; }public void Introduce() { /* 实现 */ }public void Move(string newAddress) { /* 实现 */ }
}
部分方法(partial method):
部分类中可以定义部分方法,在一个文件中声明,在另一个文件中实现:
// 声明
partial class DataProcessor
{// 部分方法:只有签名,没有实现partial void OnDataProcessed(string data);
}// 实现(可选)
partial class DataProcessor
{partial void OnDataProcessed(string data){Console.WriteLine($"数据处理完成: {data}");}
}
使用场景:
- 大型项目协作:多人同时编辑同一个类的不同部分。
- 代码生成:工具生成的代码与手动编写的代码分离(如ORM实体类、Windows Forms设计器代码)。
- 功能拆分:按功能模块拆分类的定义(如数据访问、业务逻辑、事件处理)。
- 条件编译:不同平台或配置的代码放在不同文件中。
注意事项:
- 所有部分类必须使用
partial
关键字 - 所有部分类必须在同一命名空间
- 所有部分类必须有相同的访问修饰符
- 若部分类是抽象的或密封的,整个类就是抽象的或密封的
- 部分方法必须是private,且返回类型为void
二、120道C#面试题目录列表
文章序号 | C#面试题120道 |
---|---|
1 | C#面试题及详细答案120道(01-10) |
2 | C#面试题及详细答案120道(11-20) |
3 | C#面试题及详细答案120道(21-30) |
4 | C#面试题及详细答案120道(31-40) |
5 | C#面试题及详细答案120道(41-50) |
6 | C#面试题及详细答案120道(51-60) |
7 | C#面试题及详细答案120道(61-75) |
8 | C#面试题及详细答案120道(76-85) |
9 | C#面试题及详细答案120道(86-95) |
10 | C#面试题及详细答案120道(96-105) |
11 | C#面试题及详细答案120道(106-115) |
12 | C#面试题及详细答案120道(116-120) |