C# 算术运算符:从加减乘除到自增自减
前置认知:程序的「计算器」
想象你正在使用一台多功能计算器。加减乘除是基本功能,但当你按下 1 ÷ 3 时,普通计算器显示 0.333...,而整数计算器直接显示 0——这就是「整数除法」与「浮点除法」的区别。
C# 的算术运算符就是这台计算器的按钮。但比实体计算器更强大的是:它处理的不只是数字,还有不同「精度等级」的数据类型,以及「先算哪一步」的优先级规则。理解这些规则,能避免「1 除以 3 等于 0」这类让人摸不着头脑的 Bug。
算术运算符全景图
C# 算术运算符
│
├─ 基础二元运算符(两个操作数)
│ ├─ + 加法 / 字符串拼接
│ ├─ - 减法
│ ├─ * 乘法
│ ├─ / 除法(整数除法 vs 浮点除法)
│ └─ % 取余(模运算)
│
├─ 一元运算符(单个操作数)
│ ├─ + 正号(通常省略)
│ └─ - 负号(取相反数)
│
├─ 复合赋值运算符
│ ├─ += 加后赋值
│ ├─ -= 减后赋值
│ ├─ *= 乘后赋值
│ ├─ /= 除后赋值
│ └─ %= 取余后赋值
│
└─ 自增自减运算符
├─ ++ 自增(前置/后置)
└─ -- 自减(前置/后置)术语小课堂:
- 操作数:参与运算的数据,如
a + b中的a和b - 优先级:先算哪一步,如
1 + 2 * 3先乘后加(像数学中的「先乘除后加减」) - 求值顺序:同优先级时从左到右,如
10 - 5 - 2先算10 - 5
一、基础算术运算符
1.1 加法 +:数字求和与字符串拼接
// 基础用法:数值加法
int a = 10, b = 20;
int sum = a + b; // 30
Console.WriteLine(sum);
// 边界场景:字符串拼接
string firstName = "张";
string lastName = "三";
string fullName = firstName + lastName; // "张三"
Console.WriteLine($"全名:{fullName}");
// 混合类型:字符串 + 数字 = 字符串拼接
string message = "年龄:" + 25; // "年龄:25"
Console.WriteLine(message);
// 注意顺序:先算数字,再拼接
Console.WriteLine("结果:" + 1 + 2); // "结果:12"(先拼接 "结果:1",再拼接 "2")
Console.WriteLine("结果:" + (1 + 2)); // "结果:3"(括号改变优先级)
Console.WriteLine(1 + 2 + "是结果"); // "3是结果"(先算 1+2=3,再拼接)避坑提醒:当 + 的一边是字符串时,另一边会被自动转为字符串。1 + 2 + "abc" 得到 "3abc",而 "abc" + 1 + 2 得到 "abc12"。
1.2 减法 -:数值相减
// 基础用法
int x = 15, y = 7;
int diff = x - y; // 8
// 边界场景:负数结果
int small = 5, big = 10;
int negative = small - big; // -5
Console.WriteLine(negative);
// 浮点数减法
double d1 = 10.5, d2 = 3.2;
double dResult = d1 - d2; // 7.3(注意:浮点数可能有精度误差)1.3 乘法 *:数值相乘
// 基础用法
int length = 5, width = 3;
int area = length * width; // 15
// 边界场景:溢出
int max = int.MaxValue; // 2147483647
int overflow = max * 2; // 默认不检查,结果错误!
Console.WriteLine(overflow); // 输出 -2(溢出后的错误值)
// 使用 checked 检查溢出
checked
{
// int willThrow = max * 2; // 抛出 OverflowException
}1.4 除法 /:整数除法 vs 浮点除法
这是最容易踩坑的运算符!核心规则:两边都是整数时,结果也是整数(小数部分直接丢弃)。
// 基础用法:浮点除法
double a = 10, b = 3;
double result1 = a / b; // 3.333...(正确)
// 陷阱:整数除法
int x = 10, y = 3;
int result2 = x / y; // 3(不是 3.333!小数部分被截断)
Console.WriteLine(result2); // 输出 3
// 混合运算:一边有小数,结果就是小数
int numerator = 10;
int denominator = 3;
double result3 = numerator / denominator; // 3(整数除法,再转为 double)
double result4 = (double)numerator / denominator; // 3.333...(先转 double,再除)
double result5 = numerator / (double)denominator; // 同上
// 更隐蔽的陷阱
double wrong = 10 / 3; // 3.0!整数除法后再转 double
double right = 10.0 / 3; // 3.333...(至少一边是小数字面量)避坑提醒:计算平均分、折扣、比例时,务必确保至少一边是浮点类型,否则结果永远是整数。
1.5 取余 %:模运算的正负逻辑
取余运算(模运算)返回除法后的余数,常用于「是否整除」「循环索引」「奇偶判断」。
// 基础用法:判断奇偶
int number = 7;
bool isEven = number % 2 == 0; // false(7 是奇数)
bool isOdd = number % 2 != 0; // true
// 基础用法:循环索引
string[] colors = { "红", "绿", "蓝" };
for (int i = 0; i < 10; i++)
{
string color = colors[i % colors.Length]; // 0,1,2,0,1,2... 循环
Console.WriteLine(color);
}
// 核心细节:余数的正负性与被除数一致
Console.WriteLine(10 % 3); // 1(10 是正数,余数 1)
Console.WriteLine(-10 % 3); // -1(-10 是负数,余数 -1)
Console.WriteLine(10 % -3); // 1(10 是正数,余数 1)
Console.WriteLine(-10 % -3); // -1(-10 是负数,余数 -1)
// 记忆口诀:余数符号跟「被除数」走(左边那个数)避坑提醒:C# 的 % 是「取余」而非「数学取模」。数学取模结果总是非负的,但 C# 取余可能为负。若需严格数学取模,使用 Math.FloorMod(.NET 7+)或自行调整。
二、一元算术运算符
2.1 正号 +:通常省略
int a = +5; // 等价于 5,正号通常省略
int b = +(-3); // -3,正号不改变负数的符号
// 实际用途:强调数值的正数属性(少见)
int positive = +10; // 明确表示这是正数2.2 负号 -:取相反数
int x = 5;
int negX = -x; // -5
double price = 19.99;
double negativePrice = -price; // -19.99
// 边界场景:对最小值取负会溢出
int min = int.MinValue; // -2147483648
// int willOverflow = -min; // 溢出!正数没有对应的 2147483648三、复合赋值算术运算符
复合赋值是「运算 + 赋值」的简写,让代码更简洁。
3.1 基础用法
int score = 100;
score += 10; // score = score + 10; 结果 110
score -= 20; // score = score - 20; 结果 90
score *= 2; // score = score * 2; 结果 180
score /= 3; // score = score / 3; 结果 60(整数除法)
score %= 7; // score = score % 7; 结果 4
Console.WriteLine(score); // 43.2 隐式类型转换规则
复合赋值运算符会自动处理类型转换,这是与展开写法的关键差异。
byte b = 100;
// 展开写法:编译错误!byte + byte 结果是 int,不能自动转回 byte
// b = b + 50;
// 复合赋值:编译通过!编译器自动插入显式转换
b += 50; // 等价于 b = (byte)(b + 50);
Console.WriteLine(b); // 150
// 但溢出仍会发生
byte max = 255;
max += 1; // 溢出!结果为 0(255 + 1 = 256,byte 最大 255)避坑提醒:复合赋值 += 会自动插入类型转换,可能掩盖溢出错误。关键计算建议用展开写法并配合 checked。
四、自增自减运算符
4.1 前置与后置的核心差异
这是面试常考、新手必错的知识点!核心规则:前置先变后用,后置先用后变。
int a = 5;
int b = 5;
// 前置 ++:先加 1,再参与运算
int result1 = ++a; // a 先变为 6,然后赋值给 result1
// a = 6, result1 = 6
// 后置 ++:先参与运算,再加 1
int result2 = b++; // b 的当前值 5 赋值给 result2,然后 b 变为 6
// b = 6, result2 = 5
Console.WriteLine($"前置:a={a}, result1={result1}"); // a=6, result1=6
Console.WriteLine($"后置:b={b}, result2={result2}"); // b=6, result2=5生活化类比:
- 前置
++:先涨工资,再干活(先变后用) - 后置
++:先干活,再涨工资(先用后变)
4.2 复杂表达式中的执行顺序
int i = 5;
// 分析:i++ + ++i
// 步骤 1:i++ → 取 i 的当前值 5,然后 i 变为 6
// 步骤 2:++i → i 先变为 7,然后取 7
// 结果:5 + 7 = 12
int result = i++ + ++i;
Console.WriteLine(result); // 12
Console.WriteLine(i); // 7
// 更复杂的例子
int x = 1;
int y = x++ + x++ + x++;
// 步骤:1(x变2)+ 2(x变3)+ 3(x变4)= 6
Console.WriteLine(y); // 6
Console.WriteLine(x); // 4避坑提醒:同一表达式中多次使用 ++/-- 会降低代码可读性,且不同编译器的求值顺序可能不同(C# 保证从左到右,但其他语言未必)。实际开发中应拆分简单语句。
4.3 自减 --:同理
int c = 10;
int d = 10;
int r1 = --c; // c 先变为 9,r1 = 9
int r2 = d--; // d 的 10 赋给 r2,然后 d 变为 9
Console.WriteLine($"c={c}, r1={r1}"); // c=9, r1=9
Console.WriteLine($"d={d}, r2={r2}"); // d=9, r2=10五、混合类型运算:不同数值类型的碰撞
当 int、long、float、double、decimal 混合运算时,C# 会自动向「更高精度」的类型转换。
5.1 类型提升规则
精度从低到高:int → long → float → double → decimalint i = 10;
long l = 20;
float f = 1.5f;
double d = 2.5;
decimal m = 3.5m;
// int + long → long
long r1 = i + l;
// int + float → float
float r2 = i + f;
// int + double → double
double r3 = i + d;
// float + double → double(float 精度低于 double)
double r4 = f + d;
// double + decimal → 编译错误!double 和 decimal 不自动转换
// decimal r5 = d + m; // 错误!
decimal r5 = (decimal)d + m; // 正确:显式转换
// decimal 与任何类型混合 → decimal
decimal r6 = i + m; // int 自动转 decimal避坑提醒:double 和 decimal 不能隐式转换。金融计算中若混用,必须显式转换,否则编译报错。
5.2 精度丢失的隐蔽场景
// float 精度只有 6-7 位有效数字
float bigFloat = 123456789f;
Console.WriteLine(bigFloat); // 123456800(精度丢失!)
// double 精度 15-16 位
double bigDouble = 123456789.123456789;
Console.WriteLine(bigDouble); // 123456789.123457(四舍五入)
// decimal 精度 28-29 位,金融专用
decimal bigDecimal = 123456789.123456789m;
Console.WriteLine(bigDecimal); // 123456789.123456789(完整保留)六、溢出检查:checked 与 unchecked
6.1 默认行为与显式控制
int max = int.MaxValue; // 2147483647
// 默认(unchecked):溢出静默发生,结果错误但不报错
int silent = max + 1;
Console.WriteLine(silent); // -2147483648(绕回最小值)
// checked:溢出时抛出异常,暴露问题
try
{
int loud = checked(max + 1); // 抛出 OverflowException
}
catch (OverflowException ex)
{
Console.WriteLine($"溢出:{ex.Message}");
}
// 项目级设置:在 .csproj 中启用全局检查
// <PropertyGroup>
// <CheckForOverflowUnderflow>true</CheckForOverflowUnderflow>
// </PropertyGroup>6.2 实际应用场景
// 金融计算:必须严格检查溢出
decimal CalculateInterest(decimal principal, decimal rate, int years)
{
checked
{
// 若溢出立即抛异常,而非返回错误结果
return principal * (decimal)Math.Pow(1 + (double)rate, years);
}
}
// 高性能场景:确认安全后关闭检查提升性能
int FastSum(int[] numbers)
{
unchecked
{
int sum = 0;
foreach (int n in numbers)
sum += n; // 若确认不会溢出,用 unchecked 提升性能
return sum;
}
}七、null 值参与算术运算
int? nullableA = 10;
int? nullableB = null;
// 可空类型与值类型运算:结果自动转为可空类型
int? result1 = nullableA + 5; // 15
int? result2 = nullableA + nullableB; // null(任何数与 null 运算都得 null)
// 空合并运算符提供默认值
int safe = (nullableA ?? 0) + (nullableB ?? 0); // 10 + 0 = 10
// 除法中的 null
int? dividend = 10;
int? divisor = null;
int? quotient = dividend / divisor; // null(不会抛异常,因为可空类型)
// 但值类型除零会抛异常
int zero = 0;
// int crash = 10 / zero; // DivideByZeroException!避坑提醒:可空类型的运算结果总是可空类型。使用 ?? 提供默认值,避免 null 传播导致后续计算全部失效。
八、常见坑点总结
| 坑点 | 错误示例 | 正确做法 | 原因 |
|---|---|---|---|
| 整数除法截断 | double d = 10 / 3; → 3.0 | double d = 10.0 / 3; | 先整数除法,再转 double |
| 取余正负混淆 | x % 2 == 1 判断奇数 | x % 2 != 0 | 负数取余得 -1 |
| 前置后置混淆 | int i = 5; Console.Write(i++ + ++i); | 拆分为两行 | 可读性差,易出错 |
| 复合赋值溢出 | byte b = 255; b += 1; → 0 | 用 checked 或展开写法 | 隐式转换掩盖溢出 |
| 浮点精度丢失 | float f = 123456789f; | 用 double 或 decimal | float 仅 7 位有效数字 |
| double/decimal 混算 | decimal m = 1.0 + 1.0m; | (decimal)1.0 + 1.0m | 无隐式转换 |
| null 传播 | int? a = null; int b = a + 1; | (a ?? 0) + 1 | null + 任何数 = null |
九、实战场景应用
场景 1:电商折扣计算
// 计算商品折后价(保留两位小数)
decimal originalPrice = 199.99m;
int discountPercent = 15; // 85 折
// 错误:整数除法导致折扣计算错误
// decimal wrong = originalPrice * (discountPercent / 100); // discountPercent/100 = 0!
// 正确:显式转为 decimal 再除
decimal correct = originalPrice * (discountPercent / 100m); // 或 / 100.0m
Console.WriteLine($"折后价:{correct:F2}"); // 169.99
// 更专业的写法:避免中间精度损失
decimal finalPrice = originalPrice * discountPercent / 100m;
Console.WriteLine($"最终价:{finalPrice:F2}");场景 2:分页计算总页数
int totalItems = 47; // 总数据量
int pageSize = 10; // 每页条数
// 计算总页数:向上取整
int totalPages = totalItems / pageSize; // 4(错误!还差 7 条)
int correctPages = (totalItems + pageSize - 1) / pageSize; // 5(正确)
// 或使用 Math.Ceiling
int ceilPages = (int)Math.Ceiling(totalItems / (double)pageSize); // 5
Console.WriteLine($"总页数:{correctPages}");场景 3:循环队列索引
string[] weekDays = { "周一", "周二", "周三", "周四", "周五", "周六", "周日" };
int todayIndex = 3; // 周四
// 计算 n 天后的星期
int n = 10;
int futureIndex = (todayIndex + n) % weekDays.Length;
Console.WriteLine($"{n}天后是:{weekDays[futureIndex]}"); // 周日
// 计算 n 天前的星期(处理负数)
int m = 5;
int pastIndex = (todayIndex - m + weekDays.Length) % weekDays.Length; // 加长度防负数
Console.WriteLine($"{m}天前是:{weekDays[pastIndex]}"); // 周六十、练习题
题目 1:整数除法陷阱
以下代码输出什么?为什么?
int a = 7, b = 2;
double result = a / b;
Console.WriteLine(result);查看答案提示
答案:3(不是 3.5)
解析:a / b 是整数除法,结果为 3(int 类型)。赋值给 double 时发生隐式转换,3 变为 3.0,但小数部分已在整数除法中被截断。修正:double result = (double)a / b;
题目 2:前置后置混合运算
以下代码输出什么?
int x = 5;
int y = x++ + ++x * 2;
Console.WriteLine($"x={x}, y={y}");查看答案提示
答案:x=7, y=19
解析:
x++:取 5,x 变为 6++x:x 先变为 7,取 77 * 2 = 145 + 14 = 19
题目 3:复合赋值与类型转换
以下代码能否编译?若可以,输出什么?
byte b = 200;
b += 100;
Console.WriteLine(b);查看答案提示
答案:编译通过,输出 44
解析:b += 100 等价于 b = (byte)(b + 100)。200 + 100 = 300,byte 最大 255,溢出后 300 - 256 = 44。若需检测溢出,使用 checked { b += 100; }。
知识点思维导图
C# 算术运算符
│
├─ 基础二元运算符
│ ├─ + 加法/字符串拼接(注意拼接顺序)
│ ├─ - 减法
│ ├─ * 乘法(注意溢出)
│ ├─ / 除法
│ │ ├─ 整数/整数 = 整数(截断)
│ │ └─ 含小数 = 浮点除法
│ └─ % 取余
│ ├─ 余数符号与被除数一致
│ └─ 判断奇偶用 != 0
│
├─ 一元运算符
│ ├─ + 正号(省略)
│ └─ - 负号(取相反数)
│
├─ 复合赋值
│ ├─ += -= *= /= %=
│ └─ 隐式类型转换(byte += int 合法)
│
├─ 自增自减
│ ├─ 前置 ++/--:先变后用
│ ├─ 后置 ++/--:先用后变
│ └─ 避免在同一表达式中多次使用
│
├─ 混合类型运算
│ ├─ 自动向高精度提升:int→long→float→double→decimal
│ └─ double 与 decimal 需显式转换
│
├─ 溢出检查
│ ├─ checked:溢出抛异常
│ └─ unchecked:默认,溢出静默
│
└─ null 值运算
├─ 可空类型参与运算 → 结果可空
├─ null + 任何 = null
└─ 用 ?? 提供默认值