在语法错误与语义错误中,我们介绍了语法错误:当代码不符合 C++ 语法规则时产生。编译器会报出此类错误,因此极易发现,通常也易于修正。
我们还介绍了语义错误:代码语法正确,但行为与程序员预期不符。编译器通常不会报告语义错误(尽管某些智能编译器可能给出警告)。
语义错误可能导致与未定义行为类似的各种症状:程序输出错误结果、行为异常、数据损坏、程序崩溃——或者完全看不出影响。
编写程序时,出现语义错误几乎不可避免。有些错误通过实际运行即可察觉:例如,在迷宫游戏中,角色竟能穿墙而过。对程序进行测试(代码测试简介)也能帮助发现语义错误。
除此之外,还有一件极有帮助的事——了解最常见语义错误的类型,以便在相关场景投入更多精力确保正确性。
本课将集中讨论 C++ 中最常见的一批语义错误(多数与流程控制有关)。
1. 条件逻辑错误
最常见的一类语义错误是条件逻辑错误:程序员在条件语句或循环条件的逻辑上出现失误。示例如下:
#include <iostream>
int main()
{
std::cout << "请输入一个整数: ";
int x{};
std::cin >> x;
if (x >= 5) // 误用 >= 而非 >
std::cout << x << " 大于 5\n";
return 0;
}
运行后暴露条件逻辑错误:
请输入一个整数: 5
5 大于 5
用户输入 5 时,表达式 x >= 5
为真,于是执行了相关语句。
再看一个 for 循环示例:
#include <iostream>
int main()
{
std::cout << "请输入一个整数: ";
int x{};
std::cin >> x;
// 误用 > 而非 <
for (int count{ 1 }; count > x; ++count)
{
std::cout << count << ' ';
}
std::cout << '\n';
return 0;
}
程序本应打印 1 到用户输入值之间的所有整数,实际却无任何输出:
请输入一个整数: 5
原因:进入 for 循环时 count > x
为假,循环体从未执行。
2. 无限循环
在while 语句与循环简介中,我们介绍过无限循环,并举例如下:
#include <iostream>
int main()
{
int count{ 1 };
while (count <= 10) // 条件永不为假
{
std::cout << count << ' '; // 该行反复执行
}
std::cout << '\n'; // 永不会执行
return 0; // 永不会执行
}
此处忘记递增 count
,导致循环条件恒真,程序将持续输出:
1 1 1 1 1 1 1 1 1 1 1 1 1 …
直到用户强制终止。
另一经典考题:以下代码有何问题?
#include <iostream>
int main()
{
for (unsigned int count{ 5 }; count >= 0; --count)
{
if (count == 0)
std::cout << "点火! ";
else
std::cout << count << ' ';
}
std::cout << '\n';
return 0;
}
程序本应输出 5 4 3 2 1 点火!
,实际却继续打印:
5 4 3 2 1 点火! 4294967295 4294967294 4294967293 ...
并持续递减,永不退出。原因是:count
为无符号整型,count >= 0
永不为假。
3. 差一错误
差一错误指循环多执行或少执行一次。示例(见 for 语句):
#include <iostream>
int main()
{
for (int count{ 1 }; count < 5; ++count)
{
std::cout << count << ' ';
}
std::cout << '\n';
return 0;
}
程序员本想输出 1 2 3 4 5
,因误用 <
而非 <=
,实际只打印 1 2 3 4
。
4. 运算符优先级错误
6.8 课 —— 逻辑运算符中给出示例:
#include <iostream>
int main()
{
int x{ 5 };
int y{ 7 };
if (!x > y) // 运算符优先级错误
std::cout << x << " 不大于 " << y << '\n';
else
std::cout << x << " 大于 " << y << '\n';
return 0;
}
逻辑非 !
优先级高于 >
,实际计算 (!x) > y
,违背程序员本意。
程序输出:
5 大于 7
当逻辑或 ||
与逻辑与 &&
混用时亦会出现类似问题(&&
高于 ||
)。应显式使用括号避免歧义。
5. 浮点类型的精度问题
以下浮点变量精度不足,无法完整存储数据:
#include <iostream>
int main()
{
float f{ 0.123456789f };
std::cout << f << '\n';
return 0;
}
因精度不足,数值被四舍五入:
0.123457
关系运算符与浮点比较中指出,使用 ==
或 !=
比较浮点数易受舍入误差影响,并提供了解决方案。示例:
#include <iostream>
int main()
{
double d{ 0.1 + 0.1 + 0.1 + 0.1 + 0.1 + 0.1 + 0.1 + 0.1 + 0.1 + 0.1 }; // 应和为 1.0
if (d == 1.0)
std::cout << "相等\n";
else
std::cout << "不相等\n";
return 0;
}
程序输出:
不相等
对浮点数运算越多,累积误差越大。
6. 整数除法
以下代码本想进行浮点除法,却因两操作数均为整型,导致整数除法:
#include <iostream>
int main()
{
int x{ 5 };
int y{ 3 };
std::cout << x << " 除以 " << y << " 的结果是: " << x / y << '\n'; // 整数除法
return 0;
}
输出:
5 除以 3 的结果是: 1
算术运算符指出,可使用 static_cast
将某一整型操作数转换为浮点,从而实现浮点除法。
7. 意外的空语句
if 语句常见问题中介绍了空语句——什么都不做的语句。
以下程序仅在用户同意时才应“毁灭世界”:
#include <iostream>
void blowUpWorld()
{
std::cout << "轰隆!\n";
}
int main()
{
std::cout << "是否再次毁灭世界?(y/n): ";
char c{};
std::cin >> c;
if (c == 'y'); // 意外空语句
blowUpWorld(); // 总是执行,因其不属于 if 语句
return 0;
}
因意外空语句,无论用户输入何值,blowUpWorld()
均被执行:
是否再次毁灭世界?(y/n): n
轰隆!
8. 未使用复合语句却应使用
上述问题的另一种变体:
#include <iostream>
void blowUpWorld()
{
std::cout << "轰隆!\n";
}
int main()
{
std::cout << "是否再次毁灭世界?(y/n): ";
char c{};
std::cin >> c;
if (c == 'y')
std::cout << "好吧,开始...\n";
blowUpWorld(); // 总是执行,应置于复合语句内
return 0;
}
输出:
是否再次毁灭世界?(y/n): n
轰隆!
悬空 else(if 语句常见问题)亦属此类。
9. 误用赋值运算符代替相等运算符
因赋值运算符 =
与相等运算符 ==
相似,容易误用:
#include <iostream>
void blowUpWorld()
{
std::cout << "轰隆!\n";
}
int main()
{
std::cout << "是否再次毁灭世界?(y/n): ";
char c{};
std::cin >> c;
if (c = 'y') // 误用赋值运算符
blowUpWorld();
return 0;
}
输出:
是否再次毁灭世界?(y/n): n
轰隆!
赋值运算符返回左操作数。c = 'y'
先执行,将 'y'
赋给 c
并返回 c
;随后 if (c)
判断,因 c
非零,隐式转换为 true
,于是执行 if 分支。
由于条件中几乎不应出现赋值,现代编译器通常会对此发出警告;但若忽视警告,问题仍会遗漏。
10. 遗漏函数调用运算符
#include <iostream>
int getValue()
{
return 5;
}
int main()
{
std::cout << getValue << '\n';
return 0;
}
你可能期望输出 5,实际却可能输出 1(某些编译器输出十六进制地址)。
本应写 getValue()
(调用函数并返回 int),却写成了 getValue
而无调用运算符。多数情况下,这将导致函数指针转换为 true
,从而输出 1。
进阶阅读:单独使用函数名而不调用,将得到指向该函数的函数指针,该指针将隐式转换为布尔值 true。函数指针将在 20.1 课 —— 函数指针中详述。
11. 其他常见语义错误
上文列举了新手最常犯的一批语义错误,但仍有很多其他陷阱。读者如有补充,欢迎在评论区留言。