C#基础练习题:从控制台到内存模型的实战拆解
简介这份C#基础练习题面向初学C#的程序员、计算机专业学生及自学者覆盖基本语法与数据类型、控制结构、函数与方法、数组与集合、类与对象、接口与抽象类、异常处理、枚举与委托、文件流、LINQ、异步编程与泛型等核心知识点通过实践帮助读者将理论转化为编码能力。资源共1670个文件压缩包约17.87MB以224个cs源文件、97个resx资源文件、94个csproj工程文件为主同时包含exe可执行程序、pdb调试信息、sln解决方案及jpg、ico、bmp等配套素材项目结构完整便于对照练习与编译运行。已有729人学习下载。读者可结合多类型工程案例理解值传递与引用传递、对象设计、文件读写、LINQ查询及异步编程等要点边写边调系统刷题自查。1. C#基础练习题从控制台题目到内存模型的一次实战拆解接手第一份开发工作时我原以为C#练习题只是应付考试的“小儿科”直到在某个内部培训上被一道「交换两个整数」的题问住——用Swap方法实现时传入的int实参压根没变。那一刻我才意识到C#基础练习题的真正价值从来不是语法本身而是透过变量、分支、循环、类与接口这些看似平实的题目把栈、堆、引用类型、值类型、垃圾回收这些CLR行为逐一暴露出来。这套练习题集适合两类人一类是初学者需要在真实可运行的例子里建立内存思维另一类是已经写过业务代码但不清楚底层逻辑的开发者想通过最短路径把C#基础能力补齐。这份资源最大的好处是没有框架封装每个题都是纯控制台程序能直接触摸代码每一行的实际行为。2. 项目全景与技术选型控制台为什么比WinForm更适合练底层2.1 这套题到底在练什么从变量、分支、循环到泛型与LINQ拿到习题集后不要急着敲代码先把题目的知识点分布扫一遍。这套C#基础练习题覆盖的范围大致集中在六个区块变量与数据类型、运算符与表达式、流程控制、数组与集合、面向对象类、继承、接口、多态、泛型与LINQ。最后两个区块会适度牵涉委托和事件但不深入异步编程定位非常清晰——它是为“把语言基础夯扎实”设计的不是为某个具体业务框架服务的。每个区块的题目数量有差异但整体呈现“前密后疏”的特点前面的变量、分支、循环题量最大后面的泛型和LINQ题量明显减少。这不是编排出问题而是基础题的常见设计逻辑——前面靠大量重复建立手感后面靠综合题串联知识。如果你只挑中间章节的数组题来做会漏掉最重要的部分就是第2章的运算符优先级和第6章的委托用法。我建议第一次过题时按顺序刷不要跳因为后面的题目会隐式引用前面的知识点。与之配套的工程结构是典型的“练习集”结构每道题对应一个独立的.cs文件文件名与题目编号一致比如Exercise_01.cs、Exercise_02.cs。没有复杂的解决方案分层没有Web项目没有数据库连接所有代码都依赖.NET内置的Console类完成输入输出。这也正是它适合入门的原因——把变量、表达式、控制流这些核心概念从层层封装里剥出来直接展示在屏幕前。2.2 目录结构解析从入口到测试的工程布局整个资源解压后是一个标准的.NET控制台项目目录结构大致如下CSharp-Practice/ ├── CSharpPractice.sln ├── CSharpPractice/ │ ├── CSharpPractice.csproj │ ├── Program.cs │ ├── Exercises/ │ │ ├── Exercise_01_Variable.cs │ │ ├── Exercise_02_Operator.cs │ │ ├── Exercise_03_IfSwitch.cs │ │ ├── Exercise_04_ForWhile.cs │ │ ├── Exercise_05_Array.cs │ │ ├── Exercise_06_Method.cs │ │ ├── Exercise_07_ClassObject.cs │ │ ├── Exercise_08_Inheritance.cs │ │ ├── Exercise_09_Interface.cs │ │ ├── Exercise_10_Generic.cs │ │ └── Exercise_11_Linq.cs │ └── Utils/ │ └── InputHelper.cs └── README.mdProgram.cs是入口文件只负责调用各练习题的运行方法Exercises目录存放全部题目Utils目录放了一个输入辅助类专门处理控制台整数读取避免每个题都重复写int.Parse(Console.ReadLine())的判空逻辑。这种组织方式值得学习——题目的业务逻辑与输入处理互相分离新手可以聚焦题目本身不用在输入转换上反复试错。InputHelper类在这里承担了“稳定输入”的职责它公开一个int ReadInt()方法内部用int.TryParse做转换非法输入会提示重输。这种做法很聪明因为练习题里大量涉及整数输入比如猜数字、求素数、排序如果每次都用裸int.Parse一旦用户输入非数字字符程序立即抛异常初学者的信心容易被击穿。辅助类先帮你屏蔽掉这层干扰等学到异常处理章节时再回头看它的实现正好能理解TryParse的设计意图。2.3 表达式、语句与CLR一个do-while背后隐藏的内存分配练习题里有一个特别值得深挖的地方同一个循环用for和while写看似等价但中间涉及的变量作用域和内存占用并不完全一样。比如这样一道题循环累加1到100的和。用for写时i的作用域被限制在循环体内用while写时i要在循环外先声明。对于普通练习题这种差异无伤大雅但如果你想彻底理解C#变量的生命周期这就是一个重要的观察窗口。常见做法是在循环体里直接打印变量地址虽然C#不允许直接操作地址但可以通过在unsafe上下文观察或者用GC.GetAllocatedBytesForCurrentThread()捕获内存分配量。C#练习题的隐藏价值就在这里题目不会直接告诉你“栈内存”和“堆内存”的区别但当你写完一个值类型的累加循环、再看一个字符串拼接的循环时内存分配量的差异会直观地告诉你发生了什么。从CLR角度看值类型变量如int在栈上分配引用类型变量如string的引用在栈上、对象在堆上。字符串拼接循环的每次都会创建新字符串对象旧对象等待垃圾回收。这套练习题中有好几道字符串题目比如“反转字符串”“统计字符出现次数”如果你只是老老实实用拼接答案正确但性能很差改成StringBuilder后内存分配就会明显变少。C#基础练习题表面是语法题实际上把“内存思维”藏在了所有题目的背后。3. 核心模块拆解从入门题到高阶题的推进路线3.1 顺序与分支先手写边界条件再谈语法这套练习题的第一道重头戏是“判断闰年”。题目描述很简单输入一个年份输出是否闰年。但如果直接开写十个人里有八个会在边界条件上翻车——公历闰年规则是“能被4整除但不能被100整除或者能被400整除”很多人只写了year % 4 0就收工。我建议的写法是把边界条件列为数组或列表先写测试数据再写实现代码// 判断闰年普通年能被4整除且不能被100整除为闰年 // 世纪年能被400整除才是闰年 public static bool IsLeapYear(int year) { // 先写边界负数或0没有闰年概念 if (year 0) return false; // 核心判断400整除一定闰4整除且非100整除才闰 return (year % 400 0) || (year % 4 0 year % 100 ! 0); }这段代码有两个要点。第一个要点是year 0的边界检查很多初学者完全忽略这个分支导致输入0或负数时得出错误结论。第二个要点是判断顺序——先用year % 400 0覆盖世纪闰年再判断普通闰年这个顺序逻辑清晰且不重复计算。参数year是输入年份方法返回布尔值调用方可以直接在if条件里使用。做完这道题后建议自己扩展两种变体改用switch表达式实现以及用三元运算符嵌套实现。对比三种写法的可读性差异比盯着语法书看十遍都有效。这套题里还有“三个数排序”“判断三角形类型”等分支题它们的共同套路都是先写边界再写逻辑最后做输出。保持这个节奏分支训练的目的就达到了——不是学会if而是学会“把所有可能的分支都想到”。3.2 循环与数组内存布局比语法更重要循环部分的经典题是“打印九九乘法表”和“冒泡排序”。前者是双重循环的启蒙后者是数组元素交换的入门。但我想特别提醒的是循环题最容易出问题的不是循环语法而是数组越界。以冒泡排序为例常见错误是这样// 经典的冒泡排序实现 public static void BubbleSort(int[] arr) { // 外层控制排序趟数内层控制每趟比较次数 for (int i 0; i arr.Length - 1; i) { // 注意每趟结束后最后i个元素已经有序无需再比较 for (int j 0; j arr.Length - 1 - i; j) { // 相邻元素比较逆序则交换 if (arr[j] arr[j 1]) { (arr[j], arr[j 1]) (arr[j 1], arr[j]); } } } }这段代码里最容易写错的是内层循环的边界条件j arr.Length - 1 - i。很多初学者会写成j arr.Length - 1这样程序也能运行但每一趟都会多比较已经排好的尾部元素效率降低更关键的是它会掩盖对“每趟之后末尾元素已就位”这个逻辑的理解。i在这里表示已经完成排序的元素个数j 1不能越界所以内层循环的上界必须减去i。代码里的元组交换(arr[j], arr[j 1]) (arr[j 1], arr[j])是C# 7以后引入的语法一行完成交换比传统三步交换temp a; a b; b temp;更简洁。如果想把循环练透建议把冒泡排序改成函数式写法用Array.Sort和自定义ComparisonT委托做一遍对比两种方案的代码量。这里要理解的是“排序逻辑与业务逻辑解耦”——练习题不一定要求最优解但一定要理解不同的抽象层级。3.3 方法、类与接口面向对象练习的正确顺序面向对象部分练习题的设计是个层层递进的“员工管理系统”系列先定义一个Employee类包含姓名、工号、薪水三个属性再定义Manager类继承它增加奖金属性再定义一个IWork接口让两个类都实现DoWork()方法。这个系列虽然简单却把继承、多态、接口三个核心概念串起来了。我摘一段Manager类重写方法的代码做讲解// 基类员工 public class Employee { // 三个基本属性采用属性初始化器简化赋值 public string Name { get; set; } public string EmployeeId { get; set; } public decimal Salary { get; set; } // 虚方法子类可重写 public virtual void DoWork() { Console.WriteLine(${Name} 正在执行基础工作); } } // 派生类经理 public class Manager : Employee { // 经理独有的奖金属性 public decimal Bonus { get; set; } // 重写基类方法体现多态 public override void DoWork() { // 先调用基类逻辑再扩展经理特有逻辑 base.DoWork(); Console.WriteLine(${Name} 正在协调团队管理项目进度); } }这里的关键点是virtual和override的配对使用。如果基类方法不加virtual派生类里用new关键字也能写同名方法但那叫隐藏不是重写多态就失效了。调用端如果用Employee e new Manager(); e.DoWork()new隐藏时调用的是基类版本override重写时调用的才是派生类版本。这套练习题里专门有几道题考察这个区别属于典型的“基础但不简单”的知识点。接口部分的题是让Employee和Manager都实现IWork接口再写一个WorkProcessor类接收IWork参数遍历调用DoWork。这道题的价值在于理解“面向接口编程”——WorkProcessor不关心传入的具体类型只依赖接口约定。这种设计在基础题里看似多余但它是理解依赖注入和单元测试的起点。4. 动手跑编译、除错与调试的完整流程4.1 环境准备用命令行编译代替IDE黑匣子拿到源码后第一步先把环境准备好。理论上装一个Visual Studio或Rider就能直接打开解决方案按F5运行但我强烈建议至少用一次命令行编译的方式跑通它理由很朴素IDE会自动帮你配置一堆东西初学者根本分不清哪些是项目本身需要的哪些是IDE附加的。用dotnet命令走一遍所有依赖和编译过程都是透明的。我的建议步骤是这样的先进入包含CSharpPractice.csproj的目录执行dotnet build看是否报错再执行dotnet run确认控制台输出正常。如果dotnet命令找不到说明当前环境变量没有配置好需要先安装.NET SDK。使用命令行时dotnet build的--verbosity参数可以调整日志详细程度调试依赖问题时用detailed能看清每一步操作。# 进入项目文件所在目录 cd CSharpPractice/CSharpPractice # 还原依赖并编译 dotnet build --verbosity minimal # 运行程序 dotnet rundotnet run默认会先编译再运行如果代码改动不大直接用它就行。如果只想编译不做其他操作dotnet build更合适。还有一点如果机器上同时装了多个SDK版本建议在项目目录里放一个global.json固定版本避免不同机器跑出不同行为。这套题的入口输出设计得比较直接Program.cs里维护一个while循环菜单显示题目编号用户输入编号后执行对应的Exercise方法。所以运行后屏幕会提示“请输入题目编号1-11”按数字键后显示该题的执行结果。每道题内部都做了输入提示不会让用户干等着。如果某个习题里用了不同的输入格式比如先输入数组长度再输入元素控制台里也会有明确提示。4.2 调试技巧断点和逐语句在练习题中的应用命令行跑通后接下来进入“看得懂每一行”的阶段。练习题虽然简单但有些递归题、循环题看代码很难立刻看出问题此时就要靠调试器逐行检查。不外乎两种方式一种是IDE里按F9打断点按F10逐语句执行另一种是在代码里临时加Console.WriteLine打印变量。练习题的代码量小第二种方式往往更快也更符合“从底层理解程序”的目标。以之前的冒泡排序为例如果你想看每一趟排序后的数组状态可以在内层循环结束后临时加一行for (int i 0; i arr.Length - 1; i) { for (int j 0; j arr.Length - 1 - i; j) { if (arr[j] arr[j 1]) { (arr[j], arr[j 1]) (arr[j 1], arr[j]); } } // 临时输出观察每趟排序后的数组状态 Console.WriteLine($第{i 1}趟排序结果: {string.Join(, , arr)}); }string.Join(, , arr)把数组元素拼接成字符串输出是调试数组问题时最常用的技巧。i 1是显示给用户看的趟数序号数组下标从0开始所以加1才对得上人类习惯。改成Console.WriteLine的好处是保留运行轨迹比断点更容易回看完整过程。输出之后你就能清楚看到每趟循环把哪个元素送到了末尾边界条件错了时误差会在哪一趟暴露出来。如果需要更细致的检查断点依然必要。比如在一个递归函数里观察参数如何变化断点配合“调用堆栈”窗口能看到每一层递归的入参。这套练习题里有“斐波那契数列”“汉诺塔”两道递归题非常适合用断点跟踪调用层级。4.3 单元测试写法把练习题变成可回归的用例练习题最好的用法不是“做完就扔”而是把关键方法改成可测试的形态再用单元测试项目把它们固定下来。具体操作是把IsLeapYear、BubbleSort、Factorial这类核心方法定义为public static然后新建一个测试项目引用主项目。不必引入复杂框架用内置的Microsoft.VisualStudio.TestTools.UnitTesting或轻量的xUnit都行。以闰年判断为例一个最小测试用例长这样[Fact] public void IsLeapYear_世纪闰年_返回True() { // 2000年是世纪闰年能被400整除 bool result Exercise_02_Operator.IsLeapYear(2000); // 断言结果为true Assert.True(result); }测试方法的命名规则一目了然被测方法_场景_预期结果。2000能被400整除所以预期是true。把这几十个测试用例跑通后再回头看练习题的解法你会发现自己对“边界条件”的敏感度大幅提升。相比直接跑控制台程序单元测试的收益是长期的以后学了重构改完代码一键测试就知道有没有破坏旧行为以后忘了某个边界条件的定义跑一遍测试就能回忆起来。这套练习题里有大量“输入校验”类题目特别适合改造成测试用例建议优先选这些题来练手。5. 常见问题排查五个最容易翻车的C#细节5.1 现象控制台输出乱码做“统计字符出现次数”这类含中文字符串的题目时控制台偶尔会打印出方块或乱码。原因Windows控制台默认代码页与项目文件编码不一致最常见的情况是项目文件是UTF-8而控制台代码页是GBK或反之。解决在Program.cs的Main方法第一行设置控制台编码Console.OutputEncoding System.Text.Encoding.UTF8;。如果还不行检查.cs文件本身的编码格式在IDE右下角可以切换保存编码统一改成UTF-8 with BOM。5.2 现象循环变量在多个线程或委托里全部变成同一个值做“泛型与委托”章节的题时有人在for循环里创建Task或Action运行时发现所有委托读取到的i都是循环结束后的最终值。原因在C# 5以前for循环变量在整个循环期间是一个变量闭包捕获的是同一个引用而非副本。C# 5以后foreach的循环变量已被特殊处理但for的循环变量行为没有自动修复。解决循环体内使用局部变量拷贝for (int i 0; i 5; i) { // 闭包捕获的是当前迭代的副本而不是共享同一个变量 int index i; actions.Add(() Console.WriteLine(index)); }index在每次迭代中是不同变量委托捕获的就是各自的值结果输出0到4而不是4到4。5.3 现象字符串内容相同用判断却得到false这道题出现频率不低特别是从Java转C#的人更容易踩。string是引用类型但C#对做了重载值相等时返回true所以一般情况下用没问题。问题出在对象做了object类型转换之后。原因如果把两个string变量转成object类型再比较就不再调用字符串重载版本而是执行引用比较自然返回false。解决始终用string.Equals(a, b)或a.Equals(b)做字符串内容比较。练习题里有一道“回文判断”的题我建议实现时直接用Equals顺便把这个坑埋进注释里。5.4 现象折半查找写对了边界但始终死循环数组章节的“二分查找”题有人把while (left right)里的误写成导致搜索单个元素时循环根本不进去有人把mid (left right) / 2写成mid right / 2查找范围没有缩半直接死循环。原因二分查找的本质是通过缩小搜索区间逼近结果left和right的更新必须建立在mid的基础上任何脱离mid的更新都会破坏收敛性。解决mid必须由left和right计算得到并且分支里left mid 1、right mid - 1才能保证区间真正缩小。写完代码后用数组长度1、2、3分别测边界是最快的验证方式。另外(left right) / 2在数组极大时可能整数溢出严谨的写法是left (right - left) / 2。5.5 现象IDE里能跑命令行编译直接提示没有Main入口点某次在一台新机器上编译这套练习题dotnet build直接报CS5001错误提示”Program does not contain a static ‘Main’ method suitable for an entry point”。原因项目文件里的OutputType被改成了Library或Program.cs没有被包含进编译项解决方案被打开时IDE自动修复了包含关系但命令行模式下没有这个修复过程。解决检查.csproj文件确认OutputTypeExe/OutputType存在且ItemGroup Compile IncludeProgram.cs / /ItemGroup如果没有该节点手动补上或右键Program.cs选择“包含于编译”。这个现象提醒我命令行编译是检验工程文件健康度的好方式IDE的“自动修复”能力反而会掩盖问题。6. 二次开发给基础题加上性能剖析与内存观察的改造思路这套练习题全部跑熟之后下一步不是换成框架项目继续学而是回头改造习题集本身。项目源码里藏着大量可扩展的“开关”我第一次实践是在练习题的类库里加一个性能观察器让每道题输出结果的同时附带耗时和内存分配信息。思路是这样的在Exercises里建一个PerformanceTracker.cs封装Stopwatch和GC.GetAllocatedBytesForCurrentThread()。核心代码如下public static class PerformanceTracker { // 记录方法执行耗时与内存分配 public static void Run(string title, Action action) { // 强制一次垃圾回收让数据更干净 GC.Collect(); GC.WaitForPendingFinalizers(); // 步进计时器 Stopwatch sw Stopwatch.StartNew(); long startBytes GC.GetAllocatedBytesForCurrentThread(); action(); long endBytes GC.GetAllocatedBytesForCurrentThread(); sw.Stop(); // 输出统一格式的性能数据 Console.WriteLine(${title}: {sw.ElapsedMilliseconds}ms, {endBytes - startBytes} bytes); } }改造的验证方式是把“字符串反转”分别用拼接和StringBuilder各封装成方法放进PerformanceTracker.Run里执行对比输出的内存差异。实际操作中差异会非常明显拼接方案分配的内存可能高出几十倍这个数字比任何教材里的理论都更有说服力。从此以后每种新解法我都会先跑一遍性能数据再决定是否保留。类似地你还可以加一个“代码覆盖率检查器”配合单元测试或者把题目的输入参数抽象成配置文件去掉所有写死的常量。这已经不只是在做练习而是把练习题当成一个小型框架来维护。从那以后我每次拿到新项目都强制走一遍“先控制台观察、再单测固定、最后加监控输出”的流程这套加工习惯就是从这个项目里养出来的。希望帮到你。本文还有配套的精品资源点击获取