第 2 部分:枚举、数组与方法(day04--day06)
本章面试题覆盖三元表达式、常量、枚举、结构体、内存结构(栈与堆)、数组、删除数组中元素、数组中元素反转、冒泡排序、多维数组、方法(含 ref / out / params)以及飞行棋项目实战。
三元表达式与常量
Q:什么是三元表达式?它的语法格式是什么?
考察点: 三元表达式的定义与语法
参考答案:
- 三元表达式是一种特殊的条件表达式,由三部分组成:条件、表达式1(条件满足时的返回值)、表达式2(条件不满足时的返回值)。
- 语法格式:
条件 ? 表达式1 : 表达式2 - 特点:
- 简洁的条件判断:可以减少代码行数,更加紧凑简洁。
- 提高代码可读性:能够直观地表示条件判断的结果,使代码更易于理解和维护。
- 优化代码性能:由于求值过程较为简单,执行效率稍高。
- 适用场景:简单的"二选一"取值,例如判断成绩是否及格:
csharp
string result = score >= 60 ? "及格" : "不及格";
Q:使用三元表达式判断一个整数是奇数还是偶数。
考察点: 三元表达式与取模运算
参考答案:
csharp
int num = Convert.ToInt32(Console.ReadLine());
string result = num % 2 == 0 ? "偶数" : "奇数";
Console.WriteLine(result);
Q:使用三元表达式求两个数字中的最大值。
考察点: 三元表达式求最大值
参考答案:
csharp
int a = Convert.ToInt32(Console.ReadLine());
int b = Convert.ToInt32(Console.ReadLine());
int max = a > b ? a : b;
Console.WriteLine("最大值为:" + max);
Q:使用三元表达式比较两个数字的大小关系(大于、等于、小于)。
考察点: 三元表达式的嵌套使用
参考答案:
- 三元表达式的值可以是另一个三元表达式,即支持嵌套使用。
csharp
int a = Convert.ToInt32(Console.ReadLine());
int b = Convert.ToInt32(Console.ReadLine());
string result = a > b ? "大于" : (a == b ? "等于" : "小于");
Console.WriteLine(result);
Q:什么是常量?它的语法格式是什么?
考察点: 常量的定义与语法
参考答案:
- 常量可以被当作常规的变量,只是它们的值在定义后不能被修改。
- 常量可以是整数常量、浮点常量、字符常量、字符串常量、枚举常量等。
- 使用场景:创建时必须赋值,而且值不能再发生变化,比如 PI、星期等。
- 语法格式:
const 数据类型 常量名 = 常量值; - 注意事项:常量名通常为全大写。
csharp
const double PI = 3.14159;
Q:定义一年有多少天(365)、多少周(52)、多少月(12)的常量并输出。
考察点: 常量的定义与使用
参考答案:
csharp
const int DAY = 365;
const int WEEK = 52;
const int MONTH = 12;
Console.WriteLine($"一年有{DAY}天、{WEEK}周、{MONTH}个月");
Q:常量与变量有什么区别?
考察点: 常量与变量的对比
参考答案:
- 变量:在程序运行过程中可以随时重新赋值,值是可变的。
- 常量:创建(定义)时必须赋值,而且值在定义后不能被修改,相当于"只读"的变量。
- 命名约定:常量名通常为全大写;常量适用于"创建时必须赋值,而且值不能再发生变化"的数据,如 PI、星期等。
- 使用场景:需要固定的、不会变化的数据时优先使用常量,防止被意外修改。
Q:三元表达式和 if-else 语句有什么区别?如何选择?
考察点: 三元表达式与 if-else 的对比
参考答案:
- 三元表达式是一个表达式,必须返回两个值之一,适合简单的"二选一"取值;if-else 是语句,可以执行任意多条代码。
- 三元表达式代码更简洁、紧凑;当逻辑复杂(多分支或多条语句)时,应使用 if-else 提高可读性,避免嵌套三元表达式影响理解。
- 选择原则:简单条件判断用三元表达式,复杂逻辑用 if-else。
枚举与结构体
Q:什么是枚举?枚举属于什么类型?
考察点: 枚举的定义与类型
参考答案:
- 枚举为自定义的值类型,由一组命名常量组成。
- 枚举类型同样用来存储数据,只是存储的值为整型常量值,常量值不允许被改变。
- 使用场景:
- 需要重复使用固定字符串集合时,如"星期一~星期天",避免"周一""星期一""Monday"等写法不一致的问题。
- 游戏中的种类,如"切水果"游戏中定义"水果"枚举存放香蕉、苹果、橘子、西瓜等。
- 王者荣耀中的职业,如定义"职业"枚举存放"战士""法师""射手"等。
Q:枚举的语法格式是什么?定义位置有什么要求?
考察点: 枚举的语法与定义位置
参考答案:
- 语法格式:
csharp
enum 枚举名
{
常量名,
常量名,
...
}
- 注意:枚举类型中定义的内容是固定的整型常量;固定的整型常量值可以使用默认值,也可以手动指定。
- 定义位置:枚举通常使用位置为命名空间的中、类的上面,表示这个命名空间下所有类都可以访问这个枚举。
csharp
enum Sex
{
Female,
Male
}
Q:如何使用枚举?
考察点: 枚举的使用
参考答案:
- 使用形式:
枚举类型 变量名 = 枚举名.常量名;
csharp
Sex female = Sex.Female;
Sex male = Sex.Male;
Console.WriteLine(female);
Console.WriteLine(male);
Q:如何将枚举转换为整数?
考察点: 枚举与整数的转换
参考答案:
- 使用强制类型转换
(int)枚举变量即可将枚举转换为整数类型。 - 枚举常量的整数值可以使用默认值(从 0 开始递增),也可以手动指定。
csharp
Sex female = Sex.Female;
Sex male = Sex.Male;
Console.WriteLine((int)female); //0
Console.WriteLine((int)male); //1
Q:什么是结构体?结构体与类有什么关系?
考察点: 结构体的概念
参考答案:
- 结构体是从 C/C++ 时代迁移过来的经典,结构体与类相似。
- 结构体通常在面向过程中使用;C# 是面向对象的语言,使用类比较多,所以结构体作为了解即可。
- 结构类型是一种可封装数据和提供相关功能的值类型,它使得一个单一变量可以存储各种数据类型的相关数据。
Q:结构体的语法格式是什么?定义位置有什么要求?
考察点: 结构体的语法与定义位置
参考答案:
- 注意:结构体中的内容称为字段。
- 语法格式:
csharp
struct 结构体名
{
public 数据类型 字段名;
public 数据类型 字段名;
...
}
- 定义位置:结构体使用位置为命名空间的下面、类的上面,表示这个命名空间下所有类都可以访问这个结构体。
csharp
struct Student
{
public string name;
public int age;
public string addr;
}
Q:如何使用结构体?
考察点: 结构体的使用
参考答案:
- 先声明结构体类型的变量,再通过"变量名.字段名"给字段赋值和取值。
csharp
Student student;
student.name = "光头强";
student.age = 32;
student.addr = "北京";
Console.WriteLine(student.name);
Console.WriteLine(student.age);
Console.WriteLine(student.addr);
Q:定义一个结构类型 Person,包含姓名、性别、年龄三个成员,并声明两个 Person 变量表示"张三 男 18岁"和"小兰 女 16岁"。
考察点: 结构体的综合练习
参考答案:
csharp
struct Person
{
public string name;
public string sex;
public int age;
}
//使用结构体
Person p1;
p1.name = "张三";
p1.sex = "男";
p1.age = 18;
Person p2;
p2.name = "小兰";
p2.sex = "女";
p2.age = 16;
Console.WriteLine($"{p1.name} {p1.sex} {p1.age}岁");
Console.WriteLine($"{p2.name} {p2.sex} {p2.age}岁");
Q:枚举和结构体有什么相同点和不同点?
考察点: 枚举与结构体的对比
参考答案:
- 相同点:
- 都是自定义的值类型。
- 都定义在命名空间的下面(中)、类的上面,命名空间下所有类都可以访问。
- 不同点:
- 枚举由一组命名整型常量组成,存储的值为整型常量值,常量值不允许被改变,不能定义字段/方法。
- 结构体与类类似,可以封装各种数据类型的数据(字段),是"单一变量存储多种相关数据"的载体。
- 关键字不同:枚举用 enum,结构体用 struct。
内存结构
Q:栈区存放什么内容?由谁管理?
考察点: 栈区的特点
参考答案:
- 栈区中:
- 值类型:存储实际的值。
- 引用类型:存储引用地址(指针)。
- 栈区由编译器自动分配释放。
Q:堆区(托管堆)存放什么内容?由谁管理?
考察点: 堆区的特点
参考答案:
- 堆区存储引用类型的数据(实际的对象内容)。
- 在 C# 中由 .NET 平台的垃圾回收机制(GC)管理。
Q:值类型和引用类型在内存中是如何分配的?
考察点: 值类型与引用类型的内存分配
参考答案:
- 值类型(如 int、double、bool、char、枚举、结构体等):存储在栈区,直接存储实际的值。
- 引用类型(如数组、string、类等):栈区中存放的是引用地址(指针),实际数据存储在堆区(托管堆),由 GC 管理。
- 赋值行为不同:值类型赋值是复制值本身;引用类型赋值复制的是地址,多个变量可能指向同一块堆内存。
Q:值类型的赋值是什么含义?请说明下面代码的输出结果。
考察点: 值类型赋值的内存含义
参考答案:
csharp
int a = 1;
int b = a;
b = 2;
Console.WriteLine(a); //1
Console.WriteLine(b); //2
- 将一个 int 类型的变量赋值给另一个变量,实际上是在内存中创建了一个新的 int 值的副本。
- 执行赋值后,变量 a 和变量 b 都包含值 1,但它们是两个不同的变量。
- 如果此后改变了变量 b 的值,变量 a 的值不会改变,因为它们是两个不同的变量。
Q:值类型是如何比较的?
考察点: 值类型的比较
参考答案:
- 值类型比较的是两个变量的值。
csharp
int a = 1;
int b = 1;
bool c = a == b; //true,比较的是值
- 在这个例子中,两个变量的值都是 1,所以表达式
a == b的结果是 true。
Q:string 在内存中的存储有什么特殊之处?什么是驻留池?
考察点: string 的特殊存储与驻留池
参考答案:
- string 是引用类型,但它的存储方式很特殊。
- 在 C# 内存的堆空间中有一块特殊的区域,该区域为驻留池。
- 字符串存储的为字面量值时,将字符串的值存储在驻留池中,例如
string name = "张三";。 - 字符串存储的为动态的值时,在堆空间中申请多块空间存储变化的数据,例如
string name = "张三" + "李四" + "王五";。
Q:string 字面量的存储过程是怎样的?这样做有什么好处?
考察点: 驻留池的存储过程
参考答案:
- 用
string str = "xxx"的方式创建 string 变量时,编译器会先判断字符串值是否在驻留池中出现过。 - 如果没有出现过:在驻留池中申请新的空间将字符串值存储在该空间,将地址返回,存储在栈空间。
- 如果出现过:不会再在驻留池中使用新的空间来存放一个相同的内容,而是将之前的地址返回,存储在栈空间。
- 这样做可以提高内存的使用效率,并减少了创建字符串的次数。
Q:请看下面的字符串比较代码,说明输出结果。
考察点: string 的引用赋值与不可变性
参考答案:
csharp
string A = "ABCDEFG";
string B = A;
Console.WriteLine(A == B); //True
A = "ABCD";
Console.WriteLine(A); //ABCD
Console.WriteLine(B); //ABCDEFG
Console.WriteLine(A == B); //False
- 最初 A 和 B 都指向同一个字符串,所以
A == B为 True。 - string 存储的值不允许被改变,
A = "ABCD"实际上是让 A 指向另一个字符串,B 仍指向原来的 "ABCDEFG"。 - 所以 A 输出 "ABCD",B 输出 "ABCDEFG",最后
A == B为 False。
Q:null 字符串和空字符串 "" 有什么区别?
考察点: null 与空字符串的区别
参考答案:
- null 字符串表示字符串变量没有分配任何字符串实例。
- 空字符串 "" 表示分配了一个长度为 0 的字符串实例。
- 它们代表不同的概念,比较结果为 False:
csharp
string str1 = null;
string str2 = "";
Console.WriteLine(str1 == str2); //False
Q:string 作为方法参数时,在方法内重新赋值会影响外部变量吗?为什么?
考察点: 特殊引用类型 string 作为参数
参考答案:
- 不会影响。
- string 存储的值不允许被改变,方法内
str = "bcd"只是让形参指向新的字符串,外部变量仍指向原来的字符串。
csharp
static void Main(string[] args)
{
string str = "abc";
Test(str);
Console.WriteLine(str); //abc
}
public static void Test(string str)
{
str = "bcd";
}
数组
Q:为什么使用数组?数组能解决什么问题?
考察点: 数组的作用
参考答案:
- 录入商品时,每个商品需要名称和价格两个变量;商品数量多时需要定义大量变量(10个商品需要20个变量,100个商品需要200个变量),显然比较麻烦复杂。
- 数组提供一种"一个变量存放多个值"的结构,一个数组变量即可存放多个同类型的数据,简化了代码。
Q:什么是数组?数组有哪些特点?
考察点: 数组的定义与特点
参考答案:
- 数组是一种可以存放固定个数的相同类型数据的有序集合,数组是引用数据类型。
- 特点:
- 数组一旦被创建,它的大小就是不可以改变的。
- 创建了数组,立刻在内存中分配连续的空间,每个元素占用的空间大小是相同的。
- 同一个数组其元素的类型必须是相同类型,不允许出现混合类型。
- 数组类型中存储的数据可以是任何数据类型,包括基本类型和引用类型。
- 数组属于引用数据类型。
Q:数组的内存结构是怎样的?为什么栈中只存放一个地址?
考察点: 数组内存结构
参考答案:
- 因为数组是一块连续的空间,虽然元素的地址是连续的,但是因为有多个元素,那就有多个地址,但是栈空间中只能存放一个地址引用。
- 所以数组对象中存放的是数组空间中第一个元素的地址。
- 栈中的数组变量通过"第一个元素的地址 + 脚标偏移"访问数组中的每一个元素。
Q:创建数组有哪几种方式?
考察点: 数组的创建语法
参考答案:
- 方式一:仅分配空间:
数据类型[] 数组名 = new 数据类型[长度]; - 方式二:创建并指定元素:
数据类型[] 数组名 = {值1, 值2, ...}; - 方式三:
数据类型[] 数组名 = new 数据类型[]{值1, 值2, ...}; - 方式四:指定长度和元素:
数据类型[] 数组名 = new 数据类型[长度]{值1, 值2, ...};(指定长度时,长度和值的个数必须相同)
csharp
int[] ints3 = new int[5];
int[] ints = { 1, 2, 3 };
int[] ints2 = new int[] { 7, 8 };
int[] ints4 = new int[3] { 4, 5, 6 };
Q:如何获取和修改数组中的元素?脚标有什么限制?
考察点: 数组元素的操作
参考答案:
- 通过脚标(索引)操作元素:
数组名[脚标]。 - 获取数组中元素时:0 <= 脚标 <= 数组长度-1;向数组中添加元素时同样要求 0 <= 脚标 <= 数组长度-1。
- "添加"实际是往已分配的空间位置写入值,"修改"是对已有值的位置重新赋值。
csharp
int[] ints1 = { 1, 2, 3 };
Console.WriteLine(ints1[0]); //获取元素
int[] ints2 = new int[5];
ints2[0] = 20; //添加元素
ints2[0] = 10; //修改元素
Q:如何遍历数组?for 循环和 foreach 有什么区别?
考察点: 数组的遍历
参考答案:
- 遍历数组:使用循环获取数组中的每一个元素。
- 获取数组长度:
数组名.Length。 - for 循环遍历:
csharp
int[] ints1 = { 1, 2, 3 };
for (int i = 0; i < ints1.Length; i++)
{
Console.WriteLine(ints1[i]);
}
- foreach 遍历:
csharp
int[] ints1 = { 1, 2, 3 };
foreach (int i in ints1)
{
Console.WriteLine(i);
}
- 区别:for 通过脚标访问,可以灵活控制遍历(倒序、跳步、按脚标修改元素);foreach 直接取出每个元素,语法简洁,但不能通过脚标修改元素。
Q:数组中元素的默认值是什么?
考察点: 数组中元素默认值
参考答案:
- 值类型:
- 数字类型:0
- bool 类型:false
- char 类型:'\0'(空字符)
- 引用类型:null
- 例如:
int[] ints = new int[5];的元素默认值为 0;string[] names = new string[2];的元素默认值为 null。
Q:输入 5 个整数,输出这 5 个数字、它们的和以及平均数。
考察点: 数组的综合运用
参考答案:
csharp
int[] ints = new int[5];
for (int i = 0; i < 5; i++)
{
Console.WriteLine($"请输入第 {i + 1} 个数字");
ints[i] = Convert.ToInt32(Console.ReadLine());
}
//输出五个数字
Console.Write("\n输入的5个数字为:");
for (int i = 0; i < ints.Length; i++)
{
Console.Write(ints[i]);
if (i != ints.Length - 1)
{
Console.Write(",");
}
}
//输出五个数字的和
int sum = 0;
for (int i = 0; i < ints.Length; i++)
{
sum += ints[i];
}
Console.WriteLine("\n\n5个数字的和为:" + sum);
//输出五个数字的平均数
Console.WriteLine("\n5个数字的平均数为:" + sum / ints.Length);
Q:简易超市管理系统中,是如何用数组实现商品录入和查看的?
考察点: 数组在项目中的综合应用
参考答案:
- 使用两个数组分别存储商品名称和价格,并用一个计数器记录已录入的商品数量:
csharp
string[] names = new string[200]; //商品名称数组
double[] prices = new double[200]; //商品价格数组
int count = 0; //添加商品时的脚标,添加完成后的计数器
- do-while + switch 实现菜单循环:1.录入商品 2.查看商品 3.退出系统。
- 录入商品:输入名称和价格存入
names[count]、prices[count],然后count++;输入 y 继续录入。 - 查看商品:for 循环遍历
i < count,输出names[i]和prices[i]。 - 退出系统:
started = false结束 do-while 循环。 - 关键点:查看时循环条件用
i < count而不是i < names.Length,因为只有前 count 个位置存放了有效数据,其余位置是默认值(null / 0)。
Q:为什么说数组是引用数据类型?一个数组变量为什么能存放多个值?
考察点: 数组的引用类型本质
参考答案:
- 创建数组时,会在堆中申请一块连续空间用于存放数组中的值;栈中的数组变量保存的是这块空间第一个元素的地址(引用),所以数组属于引用数据类型。
- 数组是"一个变量存放多个值"的结构:变量本身是引用,指向堆中的整块连续空间,通过脚标访问其中的每个元素。
- 一旦定义好数组长度就不能改变(堆中空间大小固定)。
数组操作:删除元素、反转与多维数组
Q:数组中删除元素的思路是什么?为什么不能直接把元素"删掉"?
考察点: 删除数组中元素
参考答案:
- 数组在堆内存中是一块连续空间,且长度一旦定义就不能改变,所以数组中并没有真正的"删除"操作。
- 删除思路:
- 将删除的元素修改为默认值;
- 将该元素后面的元素依次向前赋值;
- 将最后一个元素设置为默认值。
- 例如删除字符数组 {'a','b','c','d'} 中的元素 b(脚标1):先把
arr[1] = '\0',再把脚标2及之后的元素依次前移,最后arr[3] = '\0'。
Q:写出删除数组中脚标为 1 的元素的完整代码。
考察点: 删除数组中元素代码实现
参考答案:
csharp
//定义初始值为a b c d,长度为4的数组
char[] arr = { 'a', 'b', 'c', 'd' };
//删除脚标为1的元素
arr[1] = '\0';
//从脚标为2的元素开始移动,直到最后一个元素
for (int i = 2; i < arr.Length; i++)
{
arr[i - 1] = arr[i];
}
//将最后一个元素设置为空
arr[3] = '\0';
//遍历数组,查看结果
foreach (char c in arr)
{
Console.WriteLine(c); //a c d (最后一个为空字符)
}
Q:超市管理系统中的"下架商品"功能是如何实现的?
考察点: 删除元素在项目中的应用
参考答案:
- 输入要下架的序号 index;
- 从 index+1 开始到 count-1,把后面的元素依次向前赋值(名称数组和价格数组都要移动):
csharp
for (int i = index + 1; i < count; i++)
{
names[i - 1] = names[i];
prices[i - 1] = prices[i];
}
//最后一个有效位置清空为默认值
names[count - 1] = null;
prices[count - 1] = 0.0;
count--; //计数器减一
- 注意:必须先完成元素前移,再清空最后一位,最后 count--,顺序不能颠倒。
Q:数组中元素反转的思路是什么?为什么只需要交换 长度/2 次?
考察点: 数组中元素反转思路
参考答案:
- 元素反转本质为元素位置的交换,元素原有位置发生改变。
- 从第一个元素开始,和最后一个元素交换;然后把第二个元素和倒数第二个元素交换......依次类推。
- 元素个数 4 除以 2 等于 2,所以长度为 4 的数组只需要交换 2 次;一般地,对称的两两交换只需要 数组长度/2 次。
- 脚标交换规则:
i <---> 数组长度 - 1 - i
Q:写出数组元素反转的完整代码。
考察点: 数组中元素反转代码实现
参考答案:
csharp
char[] arr = { 'a', 'b', 'c', 'd' };
for (int i = 0; i < arr.Length / 2; i++)
{
//两个变量的值之间的交换,借助一个临时变量
//arr[i] <-> arr[arr.Length - 1 - i]
char temp = arr[i];
arr[i] = arr[arr.Length - 1 - i];
arr[arr.Length - 1 - i] = temp;
}
//结果为 d c b a
Q:如何交换两个变量的值?
考察点: 交换两个变量的值
参考答案:
- 借助一个临时变量完成交换:
csharp
string str1 = "可乐";
string str2 = "橙汁";
string temp = str1;
str1 = str2;
str2 = temp;
Console.WriteLine("str1:" + str1);
Console.WriteLine("str2:" + str2);
- 三步:先把 str1 存入临时变量 temp,再把 str2 赋给 str1,最后把 temp 赋给 str2。
Q:"查看最新上架商品"为什么用倒序循环?如何实现?
考察点: 数组的倒序遍历
参考答案:
- 最新上架的商品存放在数组的末尾(脚标最大),所以从 count-1 到 0 倒序遍历即可,仅完成倒序查看即可(不改变数组中元素的位置)。
csharp
Console.WriteLine("商品角标\t商品名称\t商品价格");
for (int i = count - 1; i >= 0; i--)
{
Console.WriteLine($"{i}\t\t{names[i]}\t{prices[i]}");
}
Q:什么是多维数组?如何创建和操作?
考察点: 多维数组的概念与操作
参考答案:
- 多维数组的本质为数组中的每个元素依然为数组。
- 创建:
int[,] ints = new int[2, 3];其中 2 是第一维长度,3 是第二维长度。 - 操作:通过逗号分隔的多维脚标访问元素
ints[行, 列]。
csharp
int[,] ints = new int[2, 3];
ints[0, 0] = 1;
ints[0, 1] = 2;
ints[0, 2] = 3;
ints[1, 0] = 4;
ints[1, 1] = 5;
ints[1, 2] = 6;
//添加元素后的数组 {{1,2,3}, {4,5,6}}
Q:如何遍历多维数组?
考察点: 多维数组的遍历
参考答案:
- 使用
GetLength(0)获取第一维长度,GetLength(1)获取第二维长度,双重 for 循环遍历:
csharp
for (int i = 0; i < ints.GetLength(0); i++)
{
for (int j = 0; j < ints.GetLength(1); j++)
{
Console.Write(ints[i, j] + " ");
}
Console.WriteLine();
}
Q:双色球案例中为什么使用 new int5, 8?
考察点: 多维数组在实际案例中的应用
参考答案:
- 福彩双色球由 6 位 1 到 33 的整数(红球),加 1 位 1 到 16 的整数(蓝球)组成。
new int[5, 8]表示最多购买 5 组,每一组中有 6 个红球号码、1 个蓝球号码、1 个倍数,共 8 列:- 第一维 5:最多购买 5 组;
- 第二维 8:每组 6 个红球(脚标 0~5)+ 1 个蓝球(脚标 6)+ 1 个倍数(脚标 7)。
冒泡排序与算法
Q:排序是什么?常见的排序方式和排序算法有哪些?
考察点: 排序的基本概念
参考答案:
- 排序主要是针对数字类型进行的。
- 常见的排序方式:升序排序(从小到大排列)、降序排序(从大到小排列)。
- 排序算法:冒泡排序、快速排序、插入排序、选择排序等。每一种排序实现的方式又有所不同,但是最终的结果都是一样的,区别在于时间和空间的不同。
Q:冒泡排序的思路是什么?为什么叫"冒泡"?
考察点: 冒泡排序思路
参考答案:
- 冒泡排序实现思路和现实生活中水中吐泡泡一样:在水中吐泡泡以后,会逐渐地往上升。
- 规则:从脚标 0 的元素开始,每两个元素依次进行比较:
- 前一个元素大于后一个元素,交换位置;
- 前一个元素不大于后一个元素,不交换位置。
- 每轮比较都会把本轮的最大(小)值移动到最后面,就像最大的"泡泡"逐渐浮到水面。
Q:冒泡排序比较的轮数是多少?为什么?
考察点: 冒泡排序比较轮数
参考答案:
- 比较的轮数为:数组长度 - 1 轮。
- 因为每轮比较都会把本轮比较的最大(小)值移动到最后面,最后剩下的为最大(最小),不需要再比较。
Q:冒泡排序中第 i 轮需要比较多少次?
考察点: 冒泡排序每轮比较次数
参考答案:
- 假设当前是第 i 轮(i 从 0 开始的值),当第 i 轮比较时,比较的次数为:长度 - 1 - i 次。
- 第一轮 i = 0:需比较 (length-1) - 0 次(之所以减 1,因为最后一次比较的是最后两个,也就是说比较到倒数第二个就可以了)。
- 第二轮 i = 1:需比较 (length-1) - 1 次,因为本轮最后 1 个值不需要比较了。
- 第三轮 i = 2:需比较 (length-1) - 2 次。
- 第四轮 i = 3:需比较 (length-1) - 3 次,只需要比较 1 次。
Q:写出冒泡排序升序的完整代码。
考察点: 冒泡排序代码实现
参考答案:
csharp
int[] ints = { 4, 5, 2, 1, 3 };
for (int i = 0; i < ints.Length - 1; i++) //比较的轮数
{
for (int j = 0; j < ints.Length - 1 - i; j++) //每轮比较的次数
{
/*
升序:ints[j] > ints[j + 1]
降序:ints[j] < ints[j + 1]
*/
if (ints[j] > ints[j + 1])
{
int temp = ints[j];
ints[j] = ints[j + 1];
ints[j + 1] = temp;
}
}
}
//结果为 1 2 3 4 5
Q:以数组 {5, 2, 4, 3, 1} 为例,描述第一轮冒泡的比较过程。
考察点: 冒泡排序过程推演
参考答案:
- 第一轮共比较 4 次(长度 5 - 1 - 0):
- 第一次比较,脚标 0 和脚标 1:5 和 2 比较,5 大于 2,交换位置 → {2, 5, 4, 3, 1}
- 第二次比较,脚标 1 和脚标 2:5 和 4 比较,5 大于 4,交换位置 → {2, 4, 5, 3, 1}
- 第三次比较,脚标 2 和脚标 3:5 和 3 比较,5 大于 3,交换位置 → {2, 4, 3, 5, 1}
- 第四次比较,脚标 3 和脚标 4:5 和 1 比较,5 大于 1,交换位置 → {2, 4, 3, 1, 5}
- 本轮结束,最大值 5 被"冒泡"到了数组最后。
Q:冒泡排序的外层循环和内层循环分别控制什么?
考察点: 冒泡排序双重循环结构
参考答案:
- 外层循环(i)控制比较的轮数:数组长度 - 1 轮。
- 内层循环(j)控制每轮比较的次数:第 i 轮比较 长度 - 1 - i 次。
- 内层每次比较相邻元素
ints[j]与ints[j + 1],不满足顺序则借助临时变量交换,比较一轮后最大(小)值出现在最右侧。
Q:升序排序和降序排序在冒泡排序代码中有什么区别?
考察点: 升序与降序
参考答案:
- 只需改变交换条件即可:
- 升序:
if (ints[j] > ints[j + 1])交换(前一个大于后一个,把大的往后放)。 - 降序:
if (ints[j] < ints[j + 1])交换(前一个小于后一个,把小的往后放)。
- 升序:
Q:双色球项目中是如何对 6 个红球进行排序的?为什么循环条件是 i < 5?
考察点: 冒泡排序在项目中的应用
参考答案:
- 6 个红球排序:冒泡排序比较的轮数为 长度 - 1 = 5 轮,每轮比较 5 - i 次,所以外层
i < 5,内层j < 5 - i。
csharp
//冒泡排序思想:相邻的两个元素依次进行比较和交换,比较一轮后最大|最小的元素出现在最右侧。
for (int i = 0; i < 5; i++) //比较的轮数
{
for (int j = 0; j < 5 - i; j++) //完成真正的比较和交换
{
//升序
if (lottery[count, j] > lottery[count, j + 1])
{
int temp = lottery[count, j];
lottery[count, j] = lottery[count, j + 1];
lottery[count, j + 1] = temp;
}
}
}
- 也可以直接调用
Array.Sort(kj);将数组中所有元素排序。
Q:Random 类的常用方法有哪些?使用时需要注意什么?
考察点: Random 随机数
参考答案:
Next():每次产生一个不同的随机正整数。Next(int maxValue):产生一个比 maxValue 小的整数。Next(int minValue, int maxValue):产生一个 minValue~maxValue 的正整数,但不包含 maxValue。NextDouble():产生一个 0.0~1.0 的浮点数。- 注意:区间是"左闭右开",即不包含最大值。
csharp
Random rnd = new Random();
int a = rnd.Next(1, 13); // 1~12
int b = rnd.Next(1, 7); // 1~6
int c = rnd.Next(52); // 0~51
Q:双色球项目的开奖号码是如何生成的?如何保证红球不重复?
考察点: Random 与去重逻辑
参考答案:
- 开奖过程由一台机器完成:里面装有 33 个红球,从 33 个红球里面随机出 6 个不重复的中奖红球号码;最后从 16 个蓝球里面随机产生一个蓝球中奖号码。
- 生成红球:
random.Next(1, 34)(1~33),每生成一个,与已经生成的开奖红球号码逐一比较,重复则重新生成,直到凑齐 6 个不重复的红球。 - 生成蓝球:
kj[6] = random.Next(1, 17);(1~16)。 - 红球生成完毕后,用冒泡排序(或
Array.Sort(kj))对开奖红球升序排序,再展示开奖号码。 - 注意:用 isNotKj 标识控制只能开奖一次,已开奖后再选择"开奖"会提示"已经开奖了"。
Q:双色球中奖判断的核心逻辑是什么?
考察点: 数组比较与分支判断
参考答案:
- 遍历每一组彩票(count 组),用双重循环统计该组 6 个购买红球与 6 个开奖红球中相同的个数 zjRedNumCount;再判断蓝球是否相同得到 zjBlueNumCount。
- 判断等级(原文设定的奖金):6红+1蓝一等奖(固定500万)、6红二等奖(5万)、5红+1蓝三等奖(3000)、(5红且0蓝)或(4红且1蓝)四等奖(200)、(4红且0蓝)或(3红且1蓝)五等奖(10)、(2红且1蓝)或(1红且1蓝)或(0红且1蓝)六等奖(5),其余未中奖;最终奖金为等级奖金乘以购买倍数
lottery[i, 7]。 - 核心是嵌套循环逐一比较统计命中个数,再按规则多分支判断。
方法
Q:什么是方法?为什么要使用方法?
考察点: 方法的概念与作用
参考答案:
- 方法就是一大段代码,然后给这段代码起了一个名称,这个名称叫做方法名(签名)。
- 每一个方法都封装了对应的功能,当需要使用一个功能时,就可以调用方法,也可以实现代码的复用性。
- 方法就是将对一堆代码进行重用的一种机制;方法就是一段代码,这段代码可能有输入的值(参数),可能会返回值。
- 在后续的学习或开发时,只要有一个新的功能通常就会定义一个方法。
Q:方法的语法格式是什么?各部分分别是什么含义?
考察点: 方法的语法
参考答案:
- 语法格式:
[访问权限修饰符] [static] 返回值类型 方法名([形参列表]) { 方法体 } []中内容可以没有。- 访问权限修饰符:是否有权限调用该方法。
- static:静态的。
- 返回值类型:方法执行完成后返回的结果类型;如果不需要返回结果使用 void 关键字。
- 方法名:自定义名称,大驼峰命名规范。
- 参数:方法运行时需要的数据,可以有参数,也可以没有参数。
Q:方法的定义位置在哪里?方法有哪几种形式?
考察点: 方法的定义位置与形式
参考答案:
- 方法的定义位置为类中。
- 四种形式:
- 没有参数,没有返回值:
csharp
public static void Test()
{
//...
}
2. 有参数,没有返回值:
csharp
public static void Test(int i)
{
//...
}
3. 没有参数,有返回值:
csharp
public static int Test()
{
//...
return 100;
}
4. 有参数,有返回值:
csharp
public static int Test(int i)
{
//...
return 200;
}
Q:什么是局部变量?它的作用域是什么?
考察点: 局部变量与作用域
参考答案:
- 定义在方法中的变量为局部变量。
- 局部变量的作用域:从定义变量位置开始生效,直到变量所在的代码块结束。
- 形参也属于局部变量。
Q:如何调用方法?static 方法如何调用?Main 方法有什么特殊之处?
考察点: 方法的调用
参考答案:
- 调用的为 static 修饰的静态方法。
- 调用没有返回值的方法:
类名.方法名([实参]); - 调用有返回值的方法:
数据类型 变量名 = 类名.方法名([实参]);,注意调用有返回值的方法时,可以选择接收或不接收返回的结果。 - 对于由 static 修饰的方法,使用
类名.方法名()调用;如果调用本类中由 static 修饰的方法,可以省略类名。 - Main 方法是每个 C# 应用程序的入口点,并在启动程序时被自动调用;Console.WriteLine()、Console.Write()、Console.ReadLine()、Console.ReadKey()、Convert.ToInt32(string) 等都是使用过的方法。
Q:值类型作为方法参数传递时有什么特点?
考察点: 值类型参数
参考答案:
- 值类型变量存储的为实际的值,作为参数传递时,传递的依然为实际的值。
- 在方法内修改形参,不会影响外部实参:
csharp
static void Main(string[] args)
{
int i = 10;
Test(i);
Console.WriteLine(i); //10
}
public static void Test(int i)
{
i = 20;
}
Q:引用类型作为方法参数传递时有什么特点?
考察点: 引用类型参数
参考答案:
- 引用类型变量存储的为地址,作为参数传递时,传递的依然为地址。
- 方法内通过形参修改对象内容,外部实参会看到变化:
csharp
static void Main(string[] args)
{
int[] ints = new int[3];
Test(ints);
foreach (var item in ints)
{
Console.WriteLine(item); // 1 0 0
}
}
public static void Test(int[] ints)
{
ints[0] = 1;
}
Q:string 作为方法参数时有什么特殊性?
考察点: 特殊引用类型 string 作为参数
参考答案:
- string 存储的值不允许被改变(不可变性)。
- 方法内
str = "bcd"只是让形参指向新的字符串,外部变量仍指向原来的字符串:
csharp
static void Main(string[] args)
{
string str = "abc";
Test(str);
Console.WriteLine(str); //abc
}
public static void Test(string str)
{
str = "bcd";
}
Q:ref 关键字的作用是什么?写出示例。
考察点: ref 参数
参考答案:
- 在方法的参数中使用 ref,将值类型的值传递变为引用传递。
- 调用时实参前也要加 ref:
csharp
static void Main(string[] args)
{
int i = 10;
Test(ref i);
Console.WriteLine(i); //200
}
public static void Test(ref int i)
{
i = 200;
}
Q:out 关键字的作用是什么?写出示例。
考察点: out 参数
参考答案:
- out 一般用在函数需要有多个返回值的场景,out 底层则是内部为外部变量赋值。
- 有多少种返回值,就可以声明多少个 out 修饰的返回值类型;接收返回值的变量必须由 out 修饰。
csharp
static void Main(string[] args)
{
int[] ints = { 1, 2, 3 };
int max;
int min;
int sum;
//接收返回值的变量必须由out修饰
Test(ints, out max, out min, out sum);
Console.WriteLine(max); //3
Console.WriteLine(min); //1
Console.WriteLine(sum); //6
}
public static void Test(int[] ints, out int max, out int min, out int sum)
{
max = int.MinValue;
min = int.MaxValue;
sum = 0;
foreach (var i in ints)
{
if (i > max)
{
max = i;
}
if (i < min)
{
min = i;
}
sum += i;
}
}
Q:ref 和 out 有什么区别?
考察点: ref 与 out 的对比
参考答案:
- 相同点:都能让方法内部修改外部变量的值(都是按引用方式传递)。
- 不同点:
- ref:在方法的参数中使用 ref,将值类型的值传递变为引用传递,侧重"把值传进去并允许方法修改";调用时实参前也要加 ref(C# 规则要求 ref 实参必须先赋值)。
- out:一般用在函数需要有多个返回值的场景;out 底层是内部为外部变量赋值,接收返回值的变量必须由 out 修饰,侧重"让方法向外输出多个值"。
- 使用场景:只有一个返回值用 return;需要多个返回值时用 out;需要修改外部变量的值本身时用 ref。
Q:params 关键字的作用是什么?使用时有哪些注意事项?
考察点: params 参数
参考答案:
- params 主要是在声明方法时,参数个数不确定时使用。
- 使用时需注意以下几点:
- 参数数组必须是一维数组。
- 不允许将 params 修饰符与 ref 修饰符组合起来使用。
- 与参数数组对应的实参可以是同一类型的数组名,也可以是任意多个与该数组的元素属于同一类型的变量。
- 形参列表中有多个参数,params 必须在最后一个。
csharp
static void Main(string[] args)
{
Test(1, 2, 3);
}
public static void Test(params int[] ints)
{
//...
}
Q:int.TryParse 方法有什么特点?它和 Convert.ToInt32 有什么不同?
考察点: TryParse 与 out 的综合应用
参考答案:
- int.TryParse() 方法:将字符串类型转换为 int 类型。
- 方法源码:
public static bool TryParse(string s, out int result) - 方法特点:
- 转换成功,返回 true,将成功转换的值由 result 返回。
- 转换失败,返回 false,result 赋值为 0 返回。
csharp
int result;
bool v = int.TryParse("abc", out result);
//bool v = int.TryParse("abc", out int result);
Console.WriteLine(v); //False
Console.WriteLine(result); //0
- 与 Convert.ToInt32 相比,TryParse 转换失败不会抛异常,而是返回 false,适合需要安全校验用户输入的场景。
Q:定义一个方法,参数为两个整数,返回两个整数中的最大值;再定义方法返回整数数组的最大值、和与平均数。
考察点: 方法的综合练习
参考答案:
csharp
//参数为两个整数,返回最大值
public static int Max(int i1, int i2)
{
return i1 > i2 ? i1 : i2;
}
//参数为整数数组,返回数组中的最大值
public static int ArrayMax(int[] values)
{
int max = values[0];
for (int i = 1; i < values.Length; i++)
{
if (values[i] > max)
{
max = values[i];
}
}
return max;
}
//参数为整数数组,返回数组中所有元素的和
public static int Sum(int[] values)
{
int sum = 0;
for (int i = 0; i < values.Length; i++)
{
sum += values[i];
}
return sum;
}
//参数为整数数组,返回数组中所有元素的平均数
public static int Avg(int[] values)
{
return Sum(values) / values.Length;
}
飞行棋项目实战
Q:飞行棋游戏的基本玩法是什么?
考察点: 飞行棋项目的整体玩法
参考答案:
- 初始位置为起点,两个玩家分别掷骰子,骰子的点数为玩家移动的格数,到达对应的格,完成对应的操作。
- 玩家A与玩家B轮流掷骰子移动,谁先到达终点(索引99)谁获胜。
Q:飞行棋项目整体分为哪些模块/步骤?
考察点: 飞行棋项目的整体设计思路
参考答案:
- 绘制游戏标题:控制台输出7行彩色"飞行棋"标题。
- 绘制地图标题:输出图例说明(幸运轮盘:◎ 地雷:● 暂停:▲ 时空隧道:〓)。
- 绘制地图:绘制共计100个□的地图框架。
- 绘制关卡:将关卡标识存储在 map 数组中,遍历地图按标识输出对应符号。
- 输入玩家:录入玩家A、B的姓名(校验非空、不重复),清屏后展示玩家信息。
- 初始化玩家位置:用 playerPositions 数组记录两个玩家的位置(默认值0)。
- 完成玩家的移动:掷骰子得到点数、移动、限制在0~99之间。
- 游戏规则:踩到另一玩家、方块、幸运轮盘、地雷、暂停、时空隧道分别执行对应操作。
- 胜利判断:玩家位置达到99(>=99)时输出胜利信息并结束游戏。
Q:绘制游戏标题是如何实现的?控制台如何设置颜色?
考察点: 控制台颜色与循环 + switch
参考答案:
- 控制台显示颜色:
- 前景色(字体颜色):
Console.ForegroundColor = 枚举类型颜色; - 背景颜色:
Console.BackgroundColor = 枚举类型颜色;
- 前景色(字体颜色):
- 标题共7行,用循环
for (int i = 1; i <= 7; i++),配合 switch(i) 给每一行设置不同颜色(Red、Blue、Yellow、White、Green、DarkRed、Cyan)。 - 第4行输出
***********************C#_基础 飞行棋***********************(46个*),其余行输出60个*。
Q:地图的框架是如何绘制的?为什么是100个格子?
考察点: 绘制地图
参考答案:
- 地图共计100个□,由5段组成:
- 第一横行:输出30个□。
- 第一竖列:输出5个□(一个□和两个空字符" "占用空间一样,所以每行先输出29个空格再输出□)。
- 第二横行:输出30个□(注意顺序:从脚标64到35倒序绘制)。
- 第二竖列:输出5个□。
- 第三横行:输出30个□。
- 30 + 5 + 30 + 5 + 30 = 100,与 maps 数组长度100对应。
Q:关卡标识是如何存储和绘制的?各标识代表什么?
考察点: 绘制关卡
参考答案:
- 将所有关卡的标识存储在
int[] maps = new int[100];数组中,按关卡脚标存放关卡标识,方便后续的操作。 - 标识含义:0:□(普通方块);1:幸运轮盘◎;2:地雷●;3:暂停▲;4:时空隧道〓。
- 绘制时遍历 map 数组的每一个脚标,DrawMapContent(i) 中用 switch(mapsi) 输出对应的符号并设置颜色(0白、1青、2红、3蓝、4黄)。
- 为了方便操作,将数组创建在类中方法外(static 字段),所有方法都可以使用。
Q:四类关卡的坐标分别是什么?InitMap 是如何初始化地图的?
考察点: 初始化地图关卡
参考答案:
- 各类关卡坐标:
- 幸运轮盘:{ 6, 23, 40, 55, 69, 83 }
- 地雷:{ 5, 13, 17, 33, 38, 50, 64, 80, 94 }
- 暂停:{ 9, 27, 60, 93 }
- 时空隧道:{ 20, 25, 45, 63, 72, 88, 90 }
- InitMap:遍历 maps 数组的每一个脚标,用 foreach 依次检查该脚标是否在各类关卡坐标数组中,是则给
maps[i]赋对应的标识(幸运轮盘1、地雷2、暂停3、时空隧道4)。 - 注意第二行的顺序:绘制第二横行时是从 64 到 35 倒序(
for (int i = 64; i >= 35; i--)),保证地图蛇形连接。
Q:输入玩家姓名时有哪些校验规则?
考察点: 输入玩家与校验
参考答案:
- 玩家A的姓名不能为空(为空则提示并重新输入)。
- 玩家B的姓名不能为空,且不能和玩家A的姓名相同。
- 录入成功后,用
Console.Clear()清屏,然后展示玩家信息、地图标题、地图等相关信息。 - 玩家姓名存放在
string[] playerNames = new string[2];(string 类型数组默认值为 null)。
Q:玩家位置是如何存储和展示的?
考察点: 初始化玩家位置与地图绘制优先级
参考答案:
- 用
int[] playerPositions = new int[2];存储玩家位置(int 类型数组默认值为 0,即起点)。 - DrawMapContent 中优先判断玩家位置:
- 玩家A的坐标和玩家B的坐标相同时:展示为 ★(洋红色)。
- 玩家A的坐标和玩家B的坐标不同时:分别展示为 #A(深青色)、*B(深绿色)。
- 都不是玩家位置时才输出关卡符号。
Q:掷骰子和玩家移动是如何实现的?
考察点: 完成玩家的移动
参考答案:
csharp
Random random = new Random();
int num = random.Next(1, 7); //骰子点数1~6
Console.WriteLine($"\n玩家{playerNames[playerNumber]} 按任意键开始掷骰子");
Console.ReadKey(true); //不显示在控制台按下的键
Console.WriteLine($"\n玩家{playerNames[playerNumber]} 掷出了{num}");
//存储玩家移动的位置
playerPositions[playerNumber] += num;
//玩家最多能移动到索引99
if (playerPositions[playerNumber] > 99)
{
playerPositions[playerNumber] = 99;
}
- 移动后重新展示玩家姓名、地图标题、地图(ShowPlayerNames、DrawMapTitle、DrawMap)。
Q:飞行棋的游戏规则有哪些?分别对应什么操作?
考察点: 游戏规则与玩法逻辑
参考答案:
- 玩家踩到了另一个玩家:被踩到的玩家退6格(
playerPositions[1 - playerNumber] -= 6;)。 - 玩家踩到了方块:什么都不做。
- 玩家踩到幸运轮盘:交换玩家位置(借助临时变量交换两个玩家的位置)。
- 玩家踩到了地雷:退6格(
playerPositions[playerNumber] -= 6;)。 - 玩家踩到了暂停:暂停一回合(
playerIsParsed[playerNumber] = true;)。 - 玩家踩到了时空隧道:进10格(
playerPositions[playerNumber] += 10;)。
Q:暂停一回合是如何实现的?
考察点: 暂停规则
参考答案:
- 用
bool[] playerIsParsed = new bool[2];记录玩家是否被暂停(bool 类型数组默认值为 false)。 - 踩到暂停时设置
playerIsParsed[playerNumber] = true;。 - 主游戏循环中:如果
playerIsParsed[i]为 true,则将其置为 false(本回合跳过掷骰子);否则调用PlayGame(i)正常行动。
Q:玩家位置越界是如何处理的?
考察点: CheckPositions 检查位置
参考答案:
- 用 CheckPositions 方法检查两个玩家的位置:
- 位置小于 0 时设为 0;
- 位置大于 99 时设为 99。
- 保证玩家位置始终在 0~99 之间(后退6格可能小于0,前进10格或骰子点数可能超过99)。
Q:游戏如何判断胜利?胜利时做什么?
考察点: 胜利判断
参考答案:
- 每轮玩家行动后判断:
if (playerPositions[playerNumber] >= 99)时输出"玩家X 战胜了 玩家Y",调用 Win() 并 break 结束游戏循环。 - Win() 方法用字符画输出"胜利"图案,最后用
Console.ResetColor()恢复控制台默认颜色。
Q:飞行棋项目用到了哪些已学知识点?
考察点: 综合设计能力
参考答案:
- 方法:把绘制标题、地图标题、地图、关卡、输入玩家、展示玩家、移动、规则、检查位置、胜利等功能封装成方法,实现代码复用,Main 中按顺序调用。
- 数组:maps 存储关卡标识、playerNames 存储玩家姓名、playerPositions 存储玩家位置、playerIsParsed 存储暂停状态,并利用了数组元素的默认值(int 默认0、string 默认 null、bool 默认 false)。
- 循环与分支:for/foreach 遍历数组、while(true) 游戏主循环、switch 判断操作编号和关卡标识。
- 随机数:
Random.Next(1, 7)模拟掷骰子。 - 枚举:ConsoleColor 枚举设置控制台前景/背景颜色。
- 值类型与引用类型:static 数组字段定义在类中方法外,供所有方法共享访问。