C中的字符串处理实践

我正在以C(c99)开始一个主要用于文本的新项目。 由于外部项目的限制,这段代码必须非常简单和紧凑,由一个没有外部依赖的源代码文件或除libc和类似普遍存在的系统库之外的库组成。

根据这种理解,什么是最佳实践,陷阱,技巧或其他技术可以帮助使项目的字符串处理更加健壮和安全?

如果没有关于您的代码正在做什么的任何其他信息,我建议您设计所有的接口,如下所示:

 size_t foobar(char *dest, size_t buf_size, /* operands here */) 

snprintf这样的语义:

  • dest指向大小至少为buf_size的缓冲区。
  • 如果buf_size为零,则dest可接受null /无效指针,并且不会写入任何内容。
  • 如果buf_size非零,则dest 始终以空值终止。
  • 每个函数foobar返回完整的非截断输出的长度; 如果buf_size小于或等于返回值,则截断输出。

这样,当调用者可以容易地知道所需的目标缓冲区大小时,可以预先获得足够大的缓冲区。 如果调用者不能轻易知道,它可以使用buf_size的零参数或者“可能足够大”的缓冲区调用该函数一次,并且只有在空间不足时才重试。

您还可以创建类似于GNU asprintf函数的此类调用的包装版本,但如果您希望代码尽可能灵活,我将避免在实际的字符串函数中进行任何分配。 在调用者级别处理失败的可能性总是更容易,并且许多调用者可以通过使用在程序中更早获得的本地缓冲区或缓冲区来确保失败是不可能的,以便更大操作的成功或失败是primefaces的(这极大地简化了error handling)。

来自长期嵌入式开发人员的一些想法,其中大部分都阐述了您对简单性的要求,而不是C特定的:

  • 确定您需要哪些字符串处理function,并将该设置尽可能小以最小化故障点。

  • 按照R.的建议定义一个在所有字符串处理程序中保持一致的清晰界面。 严格的,小但详细的规则允许您使用模式匹配作为调试工具:您可能会怀疑任何看起来与其他代码不同的代码。

  • 正如Bart van Ingen Schenau所说,跟踪缓冲区长度与字符串长度无关。 如果您将始终使用文本,则可以安全地使用标准空字符来指示字符串结尾,但是您可以确保text + null适合缓冲区。

  • 确保所有字符串处理程序的一致行为,特别是缺少标准函数的情况:截断,空输入,空终止,填充

  • 如果您绝对需要违反任何规则,请为此创建单独的function并对其进行适当命名。 换句话说,给每个函数一个明确的行为。 因此,您可以将str_copy_and_pad()用于始终使用空值str_copy_and_pad()其目标的函数。

  • 尽可能使用安全的内置function( 例如每个Jonathan Leffler的memmove() )来完成繁重的工作。 但要测试它们以确保它们正在做你认为他们正在做的事情!

  • 尽快检查错误。 未检测到的缓冲区溢出可能导致“跳弹”错误,这些错误很难找到。

  • 每个function编写测试以确保其满足合同。 一定要覆盖边缘情况(关闭1,空/空字符串,源/目标重叠 )这听起来很明显,但请确保您了解如何创建和检测缓冲区欠载/溢出,然后编写测试明确生成并检查这些问题。 (我的质量保证人员可能会因为听到我的指示而感到厌倦“不要只是测试以确保它有效;测试以确保它不会破坏。”)

以下是一些对我有用的技巧:

  • 为内存管理例程创建包装器,在分配期间在缓冲区的任一端分配“fence bytes”,并在取消分配时检查它们。 您也可以在字符串处理程序中validation它们,可能是在设置了STR_DEBUG宏时。 警告 :您需要彻底测试您的诊断,以免它们产生额外的故障点。

  • 创建一个封装缓冲区及其长度的数据结构。 (如果你使用它们,它也可以包含fence字节。) 警告 :你现在有一个非标准的数据结构,你的整个代码库必须管理,这可能意味着重大的重写(以及附加的故障点)。

  • 让字符串处理程序validation其输入。 如果函数禁止空指针,请明确检查它们。 如果它需要一个有效的字符串(如strlen()应该)并且您知道缓冲区长度,请检查缓冲区是否包含空字符。 换句话说,validation您可能对代码或数据做出的任何假设。

  • 先写下你的测试。 这将有助于您理解每个函数的契约 – 正是它对调用者的期望,以及调用者对它的期望。 你会发现自己在考虑使用它的方式,它可能会破坏的方式,以及它必须处理的边缘情况。

非常感谢你提出这个问题! 我希望更多的开发人员能够考虑这些问题 – 特别是他们开始编码之前 。 祝您好运,并祝愿您拥有一款强大而成功的产品!

看看strlcpystrlcat ,请参阅original paper了解详情。

两分钱:

  1. 始终使用字符串函数的“n”版本:strncpy,strncmp,(或wcsncpy,wcsncmp等)
  2. 始终使用+1惯用语分配:例如char * str [MAX_STR_SIZE + 1],然后传递MAX_STR_SIZE作为字符串函数的“n”版本的大小,并以str [MAX_STR_SIZE] =’\ 0’结束; 确保所有字符串都正确完成。

最后一步很重要,因为如果达到最大大小,字符串函数的“n”版本在复制后将不会附加’\ 0’。

  • 只要可能,就在堆栈上使用数组并正确初始化它们。 您不必跟踪分配,大小和初始化。

     char myCopy[] = { "the interesting string" }; 
  • 对于中等大小的琴弦,C99具有VLA。 由于无法初始化它们,因此它们的可用性稍差。 但是你仍然拥有上述两个优点。

     char myBuffer[n]; myBuffer[0] = '\0'; 

一些重要的问题是:

  • 在C中,字符串长度和缓冲区大小之间根本没有关系。 字符串总是运行到(并包括)第一个'\0'字符。 作为程序员,您有责任确保在该字符串的保留缓冲区中找到该字符。
  • 始终明确跟踪缓冲区大小。 编译器会跟踪数组大小,但在您知道之前,这些信息将丢失给您。

当谈到时间与空间时,不要忘记从这里挑选标准位

在我早期的固件项目中,我使用查找表来计算O(1)操作效率中设置的位。