C++常见语义错误类型与避坑指南

在语法错误与语义错误中,我们介绍了语法错误:当代码不符合 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. 其他常见语义错误

上文列举了新手最常犯的一批语义错误,但仍有很多其他陷阱。读者如有补充,欢迎在评论区留言。

关注公众号,回复"cpp-tutorial"

可领取价值199元的C++学习资料

公众号二维码

扫描上方二维码或搜索"cpp-tutorial"