9-3 C++中常见的语义错误
在第3.1节——语法和语义错误中,我们讨论了语法错误,即当你编写的代码不符合C++语言语法规则时发生的情况。编译器会通知你此类错误,因此它们很容易被发现,通常也容易修复。
我们还讨论了语义错误,即当你编写的代码未能实现预期功能时发生的情况。编译器通常无法检测语义错误(尽管某些智能编译器可能生成警告)。
语义错误可能引发与未定义行为相似的症状:导致程序输出错误结果、行为异常、数据损坏、程序崩溃——也可能完全不产生影响。
编写程序时,语义错误几乎不可避免。部分错误可能在使用程序时就能察觉:例如开发迷宫游戏时,角色竟能穿墙而过。程序测试(参见9.1节——代码测试入门)也能帮助发现语义错误。
但还有另一种方法能提供帮助——了解哪些类型的语义错误最常见,这样你就能在这些情况下多花些时间确保正确性。
本节课我们将探讨C++中最常见的语义错误类型(其中多数与流程控制相关)。
条件逻辑错误
语义错误中最常见的类型之一是条件逻辑错误。当程序员错误编写条件语句或循环条件的逻辑时,就会发生条件逻辑错误conditional logic error。以下是一个简单示例:
#include <iostream>
int main()
{
std::cout << "Enter an integer: ";
int x{};
std::cin >> x;
if (x >= 5) // oops, we used operator>= instead of operator>
std::cout << x << " is greater than 5\n";
return 0;
}
以下是展示条件逻辑错误的程序运行示例:

当用户输入5时,条件表达式x >= 5求值为真,因此关联的语句被执行。
下面是另一个使用for循环的示例:
#include <iostream>
int main()
{
std::cout << "Enter an integer: ";
int x{};
std::cin >> x;
// oops, we used operator> instead of operator<
for (int count{ 1 }; count > x; ++count)
{
std::cout << count << ' ';
}
std::cout << '\n';
return 0;
}
该程序本应打印1到用户输入的数字之间的所有数字。但实际运行结果如下:

它什么也没打印出来。这是因为进入 for 循环时,count > x 为假,因此循环根本没有迭代。
无限循环
在第8.8节——循环与while语句介绍中,我们探讨了无限循环,并展示了以下示例:
#include <iostream>
int main()
{
int count{ 1 };
while (count <= 10) // this condition will never be false
{
std::cout << count << ' '; // so this line will repeatedly execute
}
std::cout << '\n'; // this line will never execute
return 0; // this line will never execute
}
在此情况下,我们忘记递增计数器,因此循环条件永远不会为假,循环将持续输出:

……直到用户关闭程序。
这里还有一个老师们喜欢作为测验题的例子。以下代码有什么问题?
#include <iostream>
int main()
{
for (unsigned int count{ 5 }; count >= 0; --count)
{
if (count == 0)
std::cout << "blastoff! ";
else
std::cout << count << ' ';
}
std::cout << '\n';
return 0;
}
该程序本应输出“5 4 3 2 1 发射!”,它确实执行了输出,但并未就此停止。实际上,它输出了:

然后计数器就不断递减。程序永远不会终止,因为当计数器是无符号整数时,count >= 0 条件永远不会为假。
差一错误
差一错误off-by-one error是指循环执行次数过多或过少时发生的错误。以下是我们在第8.10节——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。
运算符优先级错误
根据第6.8节——逻辑运算符的内容,以下程序存在运算符优先级错误:
#include <iostream>
int main()
{
int x{ 5 };
int y{ 7 };
if (!x > y) // oops: operator precedence issue
std::cout << x << " is not greater than " << y << '\n';
else
std::cout << x << " is greater than " << y << '\n';
return 0;
}
由于逻辑非(logical NOT)运算符的优先级高于大于运算符(operator>),该条件表达式会被解释为 (!x) > y,这并非程序员的本意。

因此,该程序输出:(需要降低错误等级可编译运行为)

当同一表达式中同时使用逻辑或与逻辑与时(逻辑与优先于逻辑或),也可能出现此类情况。请使用显式括号来避免此类错误。
浮点类型的精度问题
以下浮点变量精度不足,无法存储完整数值:
#include <iostream>
int main()
{
float f{ 0.123456789f };
std::cout << f << '\n';
return 0;
}
由于这种精确度不足,该数值略微进行了四舍五入:

在第6.7节——关系运算符与浮点数比较中,我们讨论了由于微小舍入误差,使用运算符==和运算符!=处理浮点数可能引发的问题(以及相应的解决方法)。以下是一个示例:
#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 }; // should sum to 1.0
if (d == 1.0)
std::cout << "equal\n";
else
std::cout << "not equal\n";
return 0;
}
该程序输出:

对浮点数进行的算术运算越多,其累积的小量舍入误差就越大。
整数除法
在下面的示例中,我们本意是执行浮点除法,但由于两个操作数都是整数,最终执行的是整数除法:
#include <iostream>
int main()
{
int x{ 5 };
int y{ 3 };
std::cout << x << " divided by " << y << " is: " << x / y << '\n'; // integer division
return 0;
}
这将输出:

在第6.2节——算术运算符中,我们展示了如何使用static_cast将整数操作数之一转换为浮点数值,从而实现浮点除法运算。
意外的空语句
在第8.3节——常见if语句问题中,我们讨论了空(null)语句,即不执行任何操作的语句。
在下面的程序中,我们仅在获得用户许可时才希望引爆世界:
#include <iostream>
void blowUpWorld()
{
std::cout << "Kaboom!\n";
}
int main()
{
std::cout << "Should we blow up the world again? (y/n): ";
char c{};
std::cin >> c;
if (c == 'y'); // accidental null statement here
blowUpWorld(); // so this will always execute since it's not part of the if-statement
return 0;
}

然而,由于一个意外的空语句,对blowUpWorld()的函数调用始终会被执行,因此我们无论如何都会引爆它:

在需要复合语句时未使用复合语句
上述程序的另一种变体,总会让世界爆炸:
#include <iostream>
void blowUpWorld()
{
std::cout << "Kaboom!\n";
}
int main()
{
std::cout << "Should we blow up the world again? (y/n): ";
char c{};
std::cin >> c;
if (c == 'y')
std::cout << "Okay, here we go...\n";
blowUpWorld(); // Will always execute. Should be inside compound statement.
return 0;
}
该程序输出:


悬空else语句(详见第8.3节——常见if语句问题)也属于此类情况。
在条件语句中误用赋值运算符代替等号
由于赋值运算符(=)与等号运算符(==)形式相似,我们可能本意使用等号却误用了赋值运算符:
#include <iostream>
void blowUpWorld()
{
std::cout << "Kaboom!\n";
}
int main()
{
std::cout << "Should we blow up the world again? (y/n): ";
char c{};
std::cin >> c;
if (c = 'y') // uses assignment operator instead of equality operator
blowUpWorld();
return 0;
}
该程序输出:


赋值运算符返回其左操作数。c = ‘y’ 先执行,将 y 赋值给 c 并返回 c。随后 if (c) 被求值。由于 c 现为非零值,它被隐式转换为布尔值 true,从而触发 if 语句关联的执行。
由于条件语句中的赋值操作几乎从未被预期,现代编译器通常会对此发出警告。但若开发者未养成处理所有警告的习惯,此类警告极易被忽略。
调用函数时忘记使用函数调用运算符
#include <iostream>
int getValue()
{
return 5;
}
int main()
{
std::cout << getValue << '\n';
return 0;
}


虽然你可能预期该程序会输出5,但它很可能会输出1(在某些编译器上,它会输出十六进制的内存地址)。
我们使用的是不带函数调用运算符的getValue()(该运算符会调用函数并产生整数返回值),在多数情况下,这会导致值被转换为布尔值true。
在上例中,正是这个布尔值 true 被输出,因此显示结果为 1。
对于进阶读者
在不调用函数的情况下使用函数名,通常会得到一个指向该函数地址的函数指针。此类函数指针会隐式转换为布尔值。由于该指针地址永远不会为0,因此该布尔值始终为真。
我们在第20.1课——函数指针中详细讲解了函数指针。
还有呢?
以上列举了C++新手最常犯的语义错误类型,但类似问题还有很多。读者朋友们,若您还发现其他常见陷阱,欢迎在评论区留言补充。

浙公网安备 33010602011771号