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;
}

以下是展示条件逻辑错误的程序运行示例:

image

当用户输入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到用户输入的数字之间的所有数字。但实际运行结果如下:

image

它什么也没打印出来。这是因为进入 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
}

在此情况下,我们忘记递增计数器,因此循环条件永远不会为假,循环将持续输出:

image

……直到用户关闭程序。

这里还有一个老师们喜欢作为测验题的例子。以下代码有什么问题?

#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 发射!”,它确实执行了输出,但并未就此停止。实际上,它输出了:

image

然后计数器就不断递减。程序永远不会终止,因为当计数器是无符号整数时,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,这并非程序员的本意。

image

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

image

当同一表达式中同时使用逻辑或与逻辑与时(逻辑与优先于逻辑或),也可能出现此类情况。请使用显式括号来避免此类错误。


浮点类型的精度问题

以下浮点变量精度不足,无法存储完整数值:

#include <iostream>

int main()
{
    float f{ 0.123456789f };
    std::cout << f << '\n';

    return 0;
}

由于这种精确度不足,该数值略微进行了四舍五入:

image

第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;
}

该程序输出:

image

对浮点数进行的算术运算越多,其累积的小量舍入误差就越大。


整数除法

在下面的示例中,我们本意是执行浮点除法,但由于两个操作数都是整数,最终执行的是整数除法:

#include <iostream>

int main()
{
    int x{ 5 };
    int y{ 3 };

    std::cout << x << " divided by " << y << " is: " << x / y << '\n'; // integer division

    return 0;
}

这将输出:

image

第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;
}

image

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

image


在需要复合语句时未使用复合语句

上述程序的另一种变体,总会让世界爆炸:

#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;
}

该程序输出:

image

image

悬空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;
}

该程序输出:

image
image

赋值运算符返回其左操作数。c = ‘y’ 先执行,将 y 赋值给 c 并返回 c。随后 if (c) 被求值。由于 c 现为非零值,它被隐式转换为布尔值 true,从而触发 if 语句关联的执行。

由于条件语句中的赋值操作几乎从未被预期,现代编译器通常会对此发出警告。但若开发者未养成处理所有警告的习惯,此类警告极易被忽略。


调用函数时忘记使用函数调用运算符

#include <iostream>

int getValue()
{
    return 5;
}

int main()
{
    std::cout << getValue << '\n';

    return 0;
}

image
image

虽然你可能预期该程序会输出5,但它很可能会输出1(在某些编译器上,它会输出十六进制的内存地址)。

我们使用的是不带函数调用运算符的getValue()(该运算符会调用函数并产生整数返回值),在多数情况下,这会导致值被转换为布尔值true。

在上例中,正是这个布尔值 true 被输出,因此显示结果为 1。

对于进阶读者
在不调用函数的情况下使用函数名,通常会得到一个指向该函数地址的函数指针。此类函数指针会隐式转换为布尔值。由于该指针地址永远不会为0,因此该布尔值始终为真。
我们在第20.1课——函数指针中详细讲解了函数指针。


还有呢?

以上列举了C++新手最常犯的语义错误类型,但类似问题还有很多。读者朋友们,若您还发现其他常见陷阱,欢迎在评论区留言补充。

posted @ 2026-03-01 11:28  游翔  阅读(2)  评论(0)    收藏  举报