C语言调试、测试、分析及C23标准新特性
1. 编译器诊断与静态分析
编译器通常会提供高质量的诊断信息,我们不应忽视这些警告。要努力理解警告产生的原因,并改写代码以消除错误,而不是简单地通过添加类型转换或随意修改代码来消除警告。
在处理完编译器警告后,可以使用静态分析器来识别更多潜在的缺陷。静态分析器会通过评估程序中的表达式、进行深入的控制流和数据流分析,以及推理可能的值范围和控制流路径,来诊断更复杂的缺陷。
市面上有多种免费和商业的静态分析工具,例如:
- Visual C++:可以使用 /analyze 标志调用其内置的静态分析器,还能指定要运行的规则集。更多信息可参考 Microsoft的网站 。
- Clang:其静态分析器可以作为独立工具运行,也可在Xcode中使用,详情见 https://clang-analyzer.llvm.org 。
- GCC:从版本10开始,通过 -fanalyzer 选项启用静态分析。
- 商业工具:如GitHub的CodeQL、TrustInSoft Analyzer、SonarSource的SonarQube、Synopsys的Coverity、LDRA Testbed、Perforce的Helix QAC等。
由于许多静态分析工具的功能并不重叠,因此使用多种工具可能会更有帮助。
2. 动态分析
动态分析是在系统或组件执行期间进行评估的过程,也称为运行时分析。常见的方法是对代码进行插桩,例如启用编译时标志,向可执行文件中注入额外的指令,然后运行插桩后的可执行文件。
以 dmalloc 库为例,它提供了具有运行时可配置调试功能的内存管理例程。可以使用命令行工具 dmalloc 来控制这些例程的行为,检测内存泄漏,发现并报告诸如越界写入和使用已释放指针等缺陷。
动态分析的优点是误报率较低,如果工具检测到问题,应及时修复。但它也有缺点,一是需要足够的代码覆盖率,如果有缺陷的代码路径在测试过程中未被执行,就无法发现缺陷;二是插桩可能会对程序的其他方面产生不良影响,如增加性能开销或增大二进制文件的大小。不过, FORTIFY_SOURCE 宏是个例外,它能以轻量级的方式检测缓冲区溢出,并且在生产环境中启用时对性能几乎没有影响。
3. AddressSanitizer
AddressSanitizer(ASan)是一种有效的动态分析工具,可免费用于多种编译器。相关的清理器还包括ThreadSanitizer、MemorySanitizer、Hardware - Assisted AddressSanitizer和UndefinedBehaviorSanitizer等。更多信息可参考 https://github.com/google/sanitizers 。
ASan 是用于C和C++程序的动态内存错误检测器,已集成到LLVM 3.1版、GCC 4.8版及更高版本的编译器中,从Visual Studio 2019开始也可使用。它能检测多种内存错误,包括:
- 使用已释放的内存(悬空指针解引用)
- 堆、栈和全局缓冲区溢出
- 函数返回后使用内存
- 作用域结束后使用内存
- 初始化顺序错误
- 内存泄漏
为了演示ASan的实用性,我们将 get_error 函数替换为 print_error 函数:
// error.c
errno_t print_error(errno_t errnum) {
rsize_t size = strerrorlen_s(errnum) + 1;
char* msg = malloc(size);
if (msg == nullptr) return ENOMEM;
errno_t status = strerror_s(msg, size, errnum);
if (status != 0) return EINVAL;
fputs(msg, stderr);
return EOK;
}
同时,将 get_error 函数的单元测试套件替换为 print_error 函数的单元测试套件:
// tests.cc
TEST(PrintTests, ZeroReturn) {
EXPECT_EQ(print_error(ENOMEM), 0);
EXPECT_EQ(print_error(ENOTSOCK), 0);
EXPECT_EQ(print_error(EPIPE), 0);
}
在Ubuntu Linux上构建并运行这些代码,测试结果显示测试通过。但这并不意味着代码没有缺陷,我们还需要对代码进行插桩。
插桩时,使用 -fsanitize=address 标志来编译和链接程序。以下是一些常用的编译器和链接器标志:
| 标志 | 用途 |
| ---- | ---- |
| -fsanitize=address | 启用AddressSanitizer(必须同时传递给编译器和链接器) |
| -g3 | 获取符号调试信息 |
| -fno-omit-frame-pointer | 保留帧指针,以便在错误消息中获得更详细的堆栈跟踪信息 |
| -fsanitize-blacklist=path | 传递黑名单文件 |
| -fno-common | 不将全局变量视为公共变量,允许ASan对其进行插桩 |
将这些标志添加到 CMakeLists.txt 文件中:
add_compile_options(-g3 -fno-omit-frame-pointer -fno-common -fsanitize=address)
add_link_options(-fsanitize=address)
根据使用的编译器版本,可能还需要定义以下环境变量:
ASAN_OPTIONS=symbolize=1
ASAN_SYMBOLIZER_PATH=/path/to/llvm_build/bin/llvm-symbolizer
重新构建并运行测试,ASan会检测到更多问题。例如,它可能会检测到 print_error 函数中的内存泄漏问题,因为 malloc 分配的内存没有被释放。修复方法是在函数返回前添加 free(msg) 调用。
4. C23标准新特性 - 语法与关键字
C23是C语言标准的第五版(ISO/IEC 9899:2024),它在保持C语言传统精神的同时,增加了新的特性和功能,以提高语言的安全性、可靠性和能力。
4.1 属性
C23引入了 [[attributes]] 语法,用于为各种源构造(如类型、对象、标识符或代码块)指定额外的信息。在C23之前,类似的功能是以实现定义(非可移植)的方式提供的。例如:
// 旧方式
__declspec(deprecated)
__attribute__((warn_unused_result))
int func(const char *str)__attribute__((nonnull(1)));
// C23方式
[[deprecated, nodiscard]]
int func(
const char *str [[gnu::nonnull]]
);
属性包括 deprecated 、 fallthrough 、 maybe_unused 、 nodiscard 、 unsequenced 和 reproducible 等,支持标准属性和特定于供应商的属性。可以使用 __has_c_attribute 条件包含运算符进行特性测试。
4.2 关键字
C23为一些关键字引入了更自然的拼写方式,如下表所示:
| C11关键字 | C23关键字 |
| ---- | ---- |
| _Bool | bool |
| _Static_assert | static_assert |
| _Thread_local | thread_local |
| _Alignof | alignof |
| _Alignas | alignas |
此外,C23还引入了 nullptr 常量,它比传统的 NULL 宏更具类型安全性。
5. C23标准新特性 - 其他特性
5.1 整数常量表达式
C23增加了 constexpr 变量,当你确实需要某个变量为常量时可以使用。例如:
// C17可能是可变长度数组
void func() {
static const int size = 12;
int array[size]; // might be a VLA
}
// C23不会是可变长度数组
void func() {
static constexpr int Size = 12;
int Array[Size]; // never a VLA
}
不过,C23目前只支持对象的 constexpr ,不支持 constexpr 函数,结构体成员也不能是 constexpr 。
5.2 枚举类型
C23允许程序员指定枚举类型的底层整数类型,例如:
enum E : unsigned short {
Valid = 0, // has type unsigned short
NotValid = 0x1FFFF // error, too big
};
// 可以使用固定类型进行前向声明
enum F : int;
// 声明大于int的枚举常量
enum G {
BiggerThanInt = 0xFFFF'FFFF'0000L,
};
5.3 类型推断
C23增强了 auto 类型说明符,用于单对象定义的类型推断。类似于C++,但 auto 不能出现在函数签名中。例如:
const auto l = 0L; // l is const long
auto huh = "test"; // huh is char *, not char[5] or const char *
void func();
auto f = func; // f is void (*)()
auto x = (struct S){ // x is struct S
1, 2, 3.0
};
#define swap(a, b) \
do { auto t = (a); (a) = (b); (b) = t; } \
while (0)
5.4 typeof运算符
C23增加了对 typeof 和 typeof_unqual 运算符的支持,类似于C++中的 decltype ,用于根据另一个类型或表达式的类型指定类型。 typeof 运算符保留限定符,而 typeof_unqual 去除限定符。
5.5 K&R C函数
K&R C允许在不使用原型的情况下声明函数,但这种方式在35年前就已被弃用,C23最终将其从标准中移除。现在所有函数都需要有原型,空参数列表现在表示“接受零个参数”,与C++相同。可以通过可变参数函数签名 int f(...); 来模拟“接受零个或多个参数”。
5.6 预处理器
C23为预处理器增加了新特性,如 #elifdef 指令补充了 #ifdef ,还有 #elifndef 形式; #warning 指令补充了 #error ,但不会停止翻译; __has_include 运算符用于测试头文件的存在, __has_c_attribute 运算符用于测试标准或供应商属性的存在。
此外, #embed 指令可以通过预处理器将外部数据直接嵌入到源代码中,例如:
unsigned char buffer[] = {
#embed "/dev/urandom" limit(32) // embeds 32 chars from /dev/urandom
};
struct FileObject {
unsigned int MagicNumber;
unsigned _BitInt(8) RGBA[4];
struct Point {
unsigned int x, y;
} UpperLeft, LowerRight;
} Obj = {
#if __has_embed(SomeFile.bin) == __STDC_EMBED_FOUND__
// embeds contents of file as struct
// initialization elements
#embed "SomeFile.bin"
#endif
};
5.7 整数类型和表示
从C23开始,二进制补码是唯一允许的整数表示方式,有符号整数溢出仍然是未定义行为。 int8_t 、 int16_t 、 int32_t 和 int64_t 类型现在在所有平台上都可移植使用。 [u]intmax_t 类型不再是最大的,只需要表示 long long 类型的值。
C23还引入了位精确的整数类型,允许指定位宽,这些整数不会进行整数提升。例如:
// C17
unsigned int add(
unsigned int L, unsigned int R)
{
unsigned int LC = L & 0xF;
unsigned int RC = R & 0xF;
unsigned int Res = LC + RC;
return Res & 0xF;
}
// C23
unsigned _BitInt(4) add(
unsigned _BitInt(4) L,
unsigned _BitInt(4) R)
{
return L + R;
}
C23还增加了二进制字面量,并支持使用数字分隔符提高可读性。此外,C23引入了检查整数运算,可检测加法、减法和乘法运算中的溢出和回绕问题,需要包含 <stdckdint.h> 头文件。
6. 总结与未来展望
通过使用静态和动态分析工具,我们可以更有效地调试、测试和分析代码,提高代码的质量和可靠性。
C23标准为C语言带来了许多新特性,这些特性不仅提高了语言的安全性和可读性,还增强了其表达能力。未来,C语言的下一个版本C2Y预计在2029年发布,可能会进一步改进自动类型推断、扩展 constexpr 支持,并可能采用C++的一些特性,同时还会继续关注安全和可靠性问题。
C语言调试、测试、分析及C23标准新特性(续)
7. 综合运用调试与测试技术
在实际开发中,我们可以将静态分析、动态分析等技术综合运用,以确保代码的高质量。以下是一个简单的流程图,展示了综合运用这些技术的基本流程:
graph LR
A[编写代码] --> B[编译器检查]
B --> C{是否有警告?}
C -- 是 --> D[理解警告原因并修复]
D --> B
C -- 否 --> E[静态分析]
E --> F{是否有缺陷?}
F -- 是 --> G[修复缺陷]
G --> E
F -- 否 --> H[动态分析(插桩)]
H --> I{是否发现问题?}
I -- 是 --> J[修复问题]
J --> H
I -- 否 --> K[代码通过测试]
具体操作步骤如下:
1. 编写代码 :按照需求编写C语言代码。
2. 编译器检查 :使用编译器编译代码,查看是否有警告信息。
3. 处理编译器警告 :如果有警告,理解警告产生的原因,并修改代码以消除警告。
4. 静态分析 :使用静态分析工具对代码进行分析,查找潜在的缺陷。
5. 修复静态分析发现的缺陷 :根据静态分析工具的报告,修复发现的缺陷。
6. 动态分析 :对代码进行插桩,使用动态分析工具(如AddressSanitizer)运行代码,检测运行时的问题。
7. 修复动态分析发现的问题 :根据动态分析工具的报告,修复发现的问题。
8. 代码通过测试 :经过上述步骤后,代码应该通过测试,质量得到保障。
8. 练习与实践建议
为了更好地掌握这些调试、测试和分析技术,我们可以通过以下练习来巩固所学知识:
1. 静态分析练习 :使用静态分析工具评估有缺陷的代码,查看是否能发现额外的问题。例如,对之前提到的 get_error 函数的代码进行静态分析,检查是否有潜在的缺陷。
2. 增加测试用例 :为 get_error 和 print_error 函数添加更多的测试用例,覆盖更多的错误处理路径。例如,可以测试不同的错误码,确保函数在各种情况下都能正常工作。
3. 处理AddressSanitizer结果 :对使用AddressSanitizer插桩后的测试结果进行评估,消除检测到的真正的错误。例如,修复 print_error 函数中的内存泄漏问题后,再次运行测试,确保问题得到解决。
4. 使用其他清理器 :使用 https://github.com/google/sanitizers 上提供的其他清理器对代码进行插桩,并处理发现的问题。例如,可以使用ThreadSanitizer检测多线程代码中的问题。
5. 应用到实际项目 :将这些调试、测试和分析技术应用到实际的C语言项目中,提高项目的质量和可靠性。
6. 优化代码 :使用编译器的文档,参考相关资料,对之前编写的素数分解程序进行基于性能分析的优化,例如使用Profile - Guided Optimization(PGO)技术。
9. C23标准新特性的应用场景
C23标准引入的新特性在不同的场景下都有其独特的应用价值。以下是一些具体的应用场景分析:
9.1 属性的应用
- 代码维护 :使用
[[deprecated]]属性可以标记即将废弃的函数或变量,提醒开发者在后续版本中进行替换,有助于代码的长期维护。 - 提高代码可读性 :
[[nodiscard]]属性可以确保函数的返回值被使用,避免开发者忽略重要的返回信息,提高代码的可读性和安全性。
9.2 关键字的应用
- 类型安全 :
nullptr常量的引入提高了指针操作的类型安全性,减少了因NULL宏类型不明确而导致的错误。 - 代码简洁性 :新的关键字拼写方式(如
bool、static_assert等)使代码更加简洁易读,符合现代编程的习惯。
9.3 整数常量表达式的应用
- 数组定义 :
constexpr变量可以确保数组的大小在编译时确定,避免了可变长度数组带来的潜在问题,提高了代码的可移植性。
9.4 枚举类型的应用
- 明确底层类型 :指定枚举类型的底层整数类型可以避免因底层类型不确定而导致的错误,提高代码的可靠性。
9.5 类型推断的应用
- 简化代码 :
auto类型说明符可以自动推断变量的类型,减少了代码的冗余,使代码更加简洁。
9.6 预处理器新特性的应用
- 资源嵌入 :
#embed指令可以将外部资源直接嵌入到源代码中,方便程序的分发和使用,特别是在嵌入式系统开发中具有重要的应用价值。
10. 总结
本文详细介绍了C语言的调试、测试和分析技术,以及C23标准的新特性。通过合理运用静态分析和动态分析工具,我们可以更高效地发现和解决代码中的问题,提高代码的质量和可靠性。C23标准的新特性则进一步提升了C语言的安全性、可读性和表达能力。
在实际开发中,我们应该养成良好的调试和测试习惯,充分利用这些技术和特性,不断提升自己的编程水平。同时,随着C语言的不断发展,我们也应该关注未来版本的新特性,以适应不断变化的编程需求。
11. 未来展望
C语言作为一门历史悠久且广泛应用的编程语言,其未来发展仍然值得期待。C2Y作为C语言的下一个版本,预计将在2029年发布,可能会带来更多令人期待的新特性。
从目前的趋势来看,C2Y可能会进一步加强类型安全和自动类型推断的支持,使代码编写更加高效和安全。同时,对 constexpr 的扩展支持可能会让更多的计算在编译时完成,提高程序的性能。此外,借鉴C++的一些特性(如lambda表达式)可能会增强C语言的表达能力,使其在现代编程环境中更具竞争力。
在安全和可靠性方面,C2Y可能会继续关注整数运算的安全性、内存管理的改进等问题,为开发者提供更强大的工具来编写安全可靠的代码。总之,C语言的未来充满了机遇和挑战,我们期待着C2Y为我们带来更多的惊喜。
超级会员免费看
38

被折叠的 条评论
为什么被折叠?



