【问题标题】:What is the safest way to pass strings around in C?在 C 中传递字符串的最安全方法是什么?
【发布时间】:2010-04-15 00:00:53
【问题描述】:

我有一个使用 Solaris 的 C 语言程序,它似乎具有非常古老的兼容性。很多例子,即使在 SO 上,也不起作用,还有我在 Mac OS X 上编写的很多代码。

那么当使用非常严格的 C 时,传递字符串最安全的方法是什么?

由于我认为简单,我目前在各处都使用 char 指针。所以我有返回 char* 的函数,我将 char* 传递给它们,等等。

我已经看到了奇怪的行为,比如当我输入一个函数时,我传递了一个 char*,它的值是正确的,然后在一个 printf() 或 malloc 之类的简单操作之后,该值神秘地消失或损坏/覆盖其他一些指针。

我确定是不正确的函数的一种方法可能是:

char *myfunction(char *somestr) {    
  char localstr[MAX_STRLENGTH] = strcpy(localstr, somestr);
  free(somestr);
  /* ... some work ... */
  char *returnstr = strdup(localstr);
  return returnstr;
}

这似乎……草率。任何人都可以在一个简单的要求上指出我正确的方向吗?

更新

我对正在发生的事情不知所措的一个函数示例。不确定这是否足以弄清楚,但这里是:'

char *get_fullpath(char *command, char *paths) {
  printf("paths inside function %s\n", paths); // Prints value of paths just fine

  char *fullpath = malloc(MAX_STRLENGTH*sizeof(char*));

  printf("paths after malloc %s\n", paths); // paths is all of a sudden just blank
}

【问题讨论】:

  • 我认为,更有可能的是,您正在做的事情会引发未定义的行为。在将其归咎于编译器或操作系统之前,我建议您与我们分享一些示例代码,以便我们可以告诉您碰巧在 OS X 上运行的原始代码是否真的有效。
  • 这看起来……至少是错误的。您分配给数组 (???) ...您想将 strcpy 放入其中?你有一个 returnstr 但返回 localstr(在堆栈上,哎呀!)等等。无论如何,欢迎来到 C 的有趣世界。对象的所有权(是的,C 也有它们)必须明确定义。例如,如果上面的代码被调用为 myfunction("Hello world!") 会发生什么——无论如何,定义合约。一种方法是让 CALLER 负责传入一个能够接收 n 个字符的有效对象(如果需要更多,调用将失败,等等)
  • 我对“真正严格的 C”的含义感到困惑。我同意迈克尔的观点,即您看到的“非常奇怪的行为”只是上面代码中未定义的行为。在 C 中没有特殊的方法来传递“字符串”,它与任何其他数组的工作方式相同。你到底有什么问题?
  • 您是否要让函数返回原始字符串的副本?还是它的修改版?此外,如果您尝试为 MAX_STRLENGTH 个字符分配空间,则应该只有 sizeof(char),而不是 sizeof(char*)
  • 当您调用 get_fullpath() 时,您不可能从之前调用 get_fullpath() 或类似构造函数中获得的指针传递参数 2,对吗?因为只要您回到较浅的堆栈深度,您可能会侥幸逃脱,只是当您再次深入调用树时才开始丢失缓冲区。

标签: c string unix pointers


【解决方案1】:

编写良好的 C 代码遵循以下约定:

  • 所有函数都返回 int 类型的状态码,其中返回值 0 表示成功,-1 表示失败。失败时,该函数应将 errno 设置为适当的值(例如 EINVAL)。
  • 函数“报告”的值应通过使用“输出参数”进行报告。换句话说,其中一个参数应该是指向目标对象的指针。
  • 指针的所有权应该属于调用者;因此,函数不应free 其任何参数,而应仅free 它本身使用malloc/calloc 分配的对象。
  • 字符串应作为const char* 对象或char* 对象传递,具体取决于是否要覆盖字符串。如果不修改字符串,则应使用const char*
  • 每当传递的数组不是以 NUL 结尾的字符串时,都应提供一个参数来指示数组中的元素数量或该数组的容量。
  • 当一个可修改的字符串/缓冲区(即char*)对象被传递给一个函数,并且该函数要覆盖、追加或以其他方式修改字符串时,需要一个指示字符串/缓冲区容量的参数提供(以便允许动态缓冲区大小并避免缓冲区溢出)。

我应该指出,在您的示例代码中,您返回的是 localstr 而不是 returnstr。因此,您在当前函数的堆栈帧中返回一个对象的地址。一旦函数返回,当前函数的堆栈帧将消失。之后立即调用另一个函数可能会更改该位置的数据,从而导致您观察到的损坏。返回局部变量的地址会导致“未定义的行为”并且是不正确的。

编辑
根据您更新的代码(get_fullpath),很明显问题不在于您的函数 get_fullpath,而在于调用它的函数。最有可能的是,paths 变量是由一个返回局部变量地址的函数提供的。因此,当您在 get_fullpath 中创建局部变量时,它使用的堆栈位置与路径先前占用的完全相同。由于“paths”是“fullpaths”的别名,它基本上被你分配的缓冲区地址覆盖,它是空白的。

编辑 2
我在my website 上创建了一个C Coding Conventions 页面,其中包含更详细的建议、解释和编写C 代码的示例,以防您感兴趣。此外,由于上次编辑了问题,因此返回 localstr 而不是 returnstr 的语句不再正确。

【讨论】:

  • 很好,我喜欢这个列表。谢谢您的帮助!我有很多要学习的纪律......
  • 这就是我迷路的地方,“完整路径”如何甚至触及已经分配并在“路径”中使用的内存?
  • @chucknelson - 他暗示问题在于不再分配内存。您的第一个 printf() 打印出您所期望的事实实际上是未定义的行为 - 您碰巧看到存储在内存中该位置的最后一个东西,然后它被新的堆栈变量“fullpath”重新使用。
  • 如果对 malloc 的调用正在更改“路径”指向的内存,这可能意味着“路径”指向的内存已被释放,或者您的堆内存已损坏以某种方式(例如,通过写入 malloc 块的末尾)。
  • 不错的风格指南,但需要注意的是,第一点只是一种可能的错误状态解决方案。还有其他的,每个都有其优点和缺点。虽然错误指示现在已经很好地定义了,但您已经省略了其中最重要的部分。 1)如果一个函数返回一个状态,那么你必须在函数调用后检查状态。没有例外,没有讨论,没有借口。 2) 错误处理缺失。所以一个函数返回了一个错误。然后?现在做什么?特别是因为 errno 是一个全局变量,可能会被下一个错误覆盖。
【解决方案2】:

您不能返回指向在函数内本地分配的数组的指针。一旦函数返回,该数组就会被破坏。

还有,当你放

char localstr[MAX_STRLENGTH] = strcpy(localstr, somestr);

发生的情况是 strcpy() 会将字节复制到 localstr[] 数组中,但随后您将进行不必要的赋值操作。你可能会得到两行预期的效果,因此..

char localstr[MAX_STRLENGTH];
strcpy(localstr, somestr);

此外,在这样的函数中嵌入 free() 调用也是一种不好的形式。理想情况下,free() 应该在 malloc() 发生的相同范围内可见。按照同样的逻辑,以这种方式在函数中分配内存有点可疑。

如果你想要一个函数来修改一个字符串,一个常见的约定是这样的

// use a prototype like this to use the same buffer for both input and output
int modifyMyString(char buffer[], int bufferSize) {
    // .. operate you find in buffer[],
    //    leaving the result in buffer[]
    //    and be sure not to exceed buffer length
    // depending how it went, return EXIT_FAILURE or maybe
    return EXIT_SUCCESS;

// or separate input and outputs
int workOnString(char inBuffer[], int inBufSize, char outBuffer[], int outBufSize) {
    // (notice, you could replace inBuffer with const char *)
    // leave result int outBuffer[], return pass fail status
    return EXIT_SUCCESS;

不嵌入 malloc() 或 free() 也有助于避免内存泄漏。

【讨论】:

  • 我制作了一个副本并将其指向 returnstr ......这不是避免本地范围问题吗?
  • @chucknelson:这可能是示例中的拼写错误,但返回的是指向本地数组的指针,而不是 returnstr 中指向的新分配块。
  • 如果可以保证数组一旦超出范围就会被破坏......事实上它可能会被破坏,这取决于你有多少其他功能调用和它在堆栈上的相对位置使这成为一种令人毛骨悚然的体验。
  • @Duncan - 是的,这是解决间歇性故障的好方法。此外,另一个堆栈破坏器是由于中断而保存的上下文。
  • 关于返回 EXIT_FAILURE 或 EXIT_SUCCESS 的快速问题...我是否应该有多个返回,如果在函数中的任何地方遇到一些错误条件,则使用 E​​XIT_FAILURE 跳出?我有一个以这种方式包含 4 个返回语句的函数,这似乎不是很简单......虽然它是可读的......?
【解决方案3】:

您的“更新”示例是否完整?我认为这不会编译:它需要一个返回值,但你永远不会返回任何东西。你永远不会做任何事情会完整路径,但也许这是故意的,也许你的意思只是说当你做 malloc 时,其他事情会中断。

没有看到来电者,就不可能确切地说出这里发生了什么。我的猜测是 path 是一个动态分配的块,在你调用这个函数之前它是 free 的。根据编译器的实现,一个被释放的块可能仍然包含有效数据,直到未来的 malloc 接管该空间。

更新:实际回答问题

字符串处理是 C 中一个众所周知的问题。如果创建一个固定大小的数组来保存字符串,则必须担心长字符串会溢出分配的空间。这意味着不断检查副本上的字符串大小,使用 strncpy 和 strncat 而不是普通的 strcpy 和 strcat 或类似技术。你可以跳过这个,直接说,“好吧,没有人的名字会超过 60 个字符”或类似的,但总会有危险,有人会这样做。即使在应该有已知大小的东西上,比如社会安全号码或 ISBN,也可能有人输入错误并按了两次键,或者恶意用户可能故意输入很长的东西。等等。当然这主要是数据输入或读取文件的问题。一旦你在某个已知大小的字段中有一个字符串,那么对于任何副本或其他操作,你就知道大小了。

另一种方法是使用动态分配的缓冲区,您可以根据需要将它们设置得尽可能大。当您第一次听到它时,这听起来像是一个很好的解决方案,但实际上这在 C 语言中是一个巨大的痛苦,因为分配缓冲区并在您不再需要它们时释放它们非常麻烦。这里的另一位海报说分配缓冲区的函数应该与释放它的函数相同。一个好的经验法则,我大体上同意,但是……如果一个子程序想要返回一个字符串怎么办?所以它分配缓冲区,返回它,然后......它如何释放它?它不能,因为重点是它想将它返回给调用者。调用者无法分配缓冲区,因为它不知道大小。此外,看似简单的事情:

if (strcmp(getMeSomeString(),stringIWantToCompareItTo)==0) etc

是不可能的。如果 getMeSomeString 函数分配了字符串,当然,它可以返回它以便我们进行比较,但是现在我们丢失了句柄并且我们永远无法释放它。你最终不得不编写像

这样的尴尬代码
char* someString=getMeSomeString();
int f=strcmp(someString,stringIWantToCompareItTo);
free(someString);
if (f==0)
etc

好吧,它有效,但可读性直线下降。

在实践中,我发现当可以合理地预期字符串的大小可以确定时,我会分配固定长度的缓冲区。如果输入大于缓冲区,我会根据上下文截断它或给出错误消息。我只在大小可能很大且不可预测时才使用动态分配的缓冲区。

【讨论】:

  • 是的,这只是一个例子,说明它什么时候会表现得很奇怪。我将采用使用输出参数的方法,并声明固定大小的字符数组以传递给函数,然后从中读取结果。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 2021-12-25
  • 2022-12-03
  • 1970-01-01
  • 1970-01-01
  • 2018-03-21
  • 1970-01-01
  • 2011-09-05
相关资源
最近更新 更多