1. 语法和语义错误

当你编写的语句根据 C++ 语言的语法无效时,会发生语法错误。这包括缺少分号、括号或大括号不匹配等错误。例如,以下程序包含相当多的语法错误:

1
2
3
4
5
6
7
8
#include <iostream>

int main( // missing closing brace
{
int 1x; // variable name can't start with number
std::cout << "Hi there"; << x +++ << '\n'; // extraneous semicolon, operator+++ does not exist
return 0 // missing semicolon at end of statement
}

编译器能够检测语法错误并发出编译警告或错误,因此你可以轻松识别并修复问题。然后只需再次编译,直到消除所有错误。

语义错误是意义上的错误。当一个语句在语法上是有效的,但要么违反了语言的其他规则,要么没有按照程序员的意图做时,就会发生这样的错误。编译器可以捕获某种语义错误。常见的例子包括使用未声明的变量、类型不匹配(当我们在某处使用具有错误类型的对象时)等等。

以下程序包含多个编译时语义错误:

1
2
3
4
5
int main()
{
5 = x; // x not declared, cannot assign a value to 5
return "hello"; // "hello" cannot be converted to an int
}

其他语义错误仅在运行时表现出来。有时这些会导致你的程序崩溃,例如在被零除的情况下:

1
2
3
4
5
6
7
8
9
#include <iostream>

int main()
{
int a { 10 };
int b { 0 };
std::cout << a << " / " << b << " = " << a / b << '\n'; // division by 0 is undefined in mathematics
return 0;
}

2. 调试策略

  • 策略1:注释掉你的代码

  • 策略2:验证代码流程,在这种情况下,将语句放在函数顶部以打印函数名称会很有帮助。这样,当程序运行时,你可以看到哪些函数正在被调用。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    #include <iostream>

    int getValue()
    {
    std::cerr << "getValue() called\n";
    return 4;
    }

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

    return 0;
    }

    当出于调试目的打印信息时,请使用std::cerr而不是std::cout 。原因之一是std::cout可能会被缓冲,这意味着在您要求std::cout输出文本和实际输出文本之间可能需要一段时间。如果您使用std::cout进行输出,然后程序立即崩溃, std::cout可能尚未实际输出。这可能会误导您了解问题所在。另一方面, std::cerr是无缓冲的,这意味着您发送给它的任何内容都会立即输出。使用std::cerr还有助于明确输出的信息是针对错误情况而不是正常情况。

    添加临时调试语句时,不缩进它们会很有帮助。这使得以后更容易找到它们并将其删除。

    如果你使用 clang-format 来格式化你的代码,它将尝试自动缩进这些行。可以像这样禁止自动格式化:

    1
    2
    3
    // clang-format off
    std::cerr << "main() called\n";
    // clang-format on
  • 策略3:打印值,示例:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    int main()
    {
    int x{ getUserInput() };
    std::cerr << "main::x = " << x << '\n';
    int y{ getUserInput() };
    std::cerr << "main::y = " << y << '\n';

    int z{ add(x, 5) };
    std::cerr << "main::z = " << z << '\n';
    printResult(z);

    return 0;
    }

虽然向程序添加调试语句以进行诊断是一种常见的基本技术,也是一种功能性技术(尤其是当调试器由于某种原因不可用时),但它并不是那么好,原因有很多:

  1. 调试语句会使您的代码变得混乱。

  2. 调试语句使程序的输出变得混乱。

  3. 调试语句需要修改代码以添加和删除,这可能会引入新的错误。

  4. 调试语句在使用完毕后必须删除,这使得它们不可重用。

更轻松地在整个程序中禁用和启用调试的一种方法是使用预处理器指令使调试语句成为有条件的:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
#include <iostream>

#define ENABLE_DEBUG // comment out to disable debugging

int getUserInput()
{
#ifdef ENABLE_DEBUG
std::cerr << "getUserInput() called\n";
#endif
std::cout << "Enter a number: ";
int x{};
std::cin >> x;
return x;
}

int main()
{
#ifdef ENABLE_DEBUG
std::cerr << "main() called\n";
#endif
int x{ getUserInput() };
std::cout << "You entered: " << x << '\n';

return 0;
}

使用记录器:通过预处理器进行条件化调试的另一种方法是将调试信息发送到日志。日志是已发生事件的顺序记录,通常带有时间戳。生成日志的过程称为日志记录。通常,日志会写入磁盘上的文件(称为日志文件),以便稍后查看。

C++ 包含一个名为std::clog的输出流,旨在用于写入日志信息。但是,默认情况下, std::clog写入标准错误流(与std::cerr相同)。虽然您可以将其重定向到文件,但在这一领域,您通常最好使用众多现有的第三方日志记录工具之一。

使用plog记录器的示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
#include <plog/Log.h> // Step 1: include the logger headers
#include <plog/Initializers/RollingFileInitializer.h>
#include <iostream>

int getUserInput()
{
PLOGD << "getUserInput() called"; // PLOGD is defined by the plog library

std::cout << "Enter a number: ";
int x{};
std::cin >> x;
return x;
}

int main()
{
plog::init(plog::debug, "Logfile.txt"); // Step 2: initialize the logger

PLOGD << "main() called"; // Step 3: Output to the log as if you were writing to the console

int x{ getUserInput() };
std::cout << "You entered: " << x << '\n';

return 0;
}

使用 plog,可以通过将 init 语句更改为以下内容来暂时禁用日志记录:

1
plog::init(plog::none , "Logfile.txt"); // plog::none eliminates writing of most messages, essentially turning logging off

参考资料

Learn C++ – Skill up with our free tutorials