在C语言中,传递字符串的最安全方式是什么?

5

我有一个使用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上运行的代码是否有效。 - Michael Aaron Safyan
2
这看起来... 至少不对。你将赋值给数组(???)... 你想要strcpy到其中的数组?你有一个returnstr,但返回的是localstr(在堆栈上,糟糕!),等等。无论如何,欢迎来到有趣的C世界。对象的所有权(是的,C也有它们)必须明确定义。例如,如果以上代码被调用为myfunction(“Hello world!”),会发生什么--无论如何,定义合同。一种方法是使调用者负责传递能够接受n个字符的有效对象(如果需要更多,则调用将失败等)。 - user166390
我对“真正严格的C”是什么意思感到困惑。 我同意Michael的看法,您所看到的“非常奇怪的行为”只是由于上面的代码造成了未定义的行为。 在C中没有特殊的方法来传递“字符串”,它与任何其他数组的工作方式相同。 您究竟遇到了什么问题? - Brian Roach
你是想让函数返回原始字符串的副本还是修改后的版本?此外,如果你想为MAX_STRLENGTH个字符分配空间,应该使用sizeof(char),而不是sizeof(char*)。 - JustJeff
1
当您调用get_fullpath()时,您是否偶然从先前调用get_fullpath()或类似构造函数获得的指针传递参数2?因为只要您在回到较浅的堆栈深度,您可能会成功地完成此操作,但是当您再次深入调用树时,您可能会开始丢失缓冲区。 - JustJeff
显示剩余2条评论
3个回答

12

良好编写的 C 代码应遵循以下惯例:

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

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

编辑
基于你更新后的代码(get_fullpath),显然问题不在于你的get_fullpath函数,而是调用它的函数。很可能, paths 变量由返回局部变量地址的函数提供。因此,当你在get_fullpath内创建一个局部变量时,它使用的是之前占据的同一堆栈位置。由于“paths”别名为“fullpaths”,它基本上会被malloc的缓冲区的地址所覆盖,而这个缓冲区是空白的。

编辑2
我已经在我的网站上创建了一个C编码约定页面,其中包含有关编写C代码的更详细建议、说明和示例,如果您有兴趣的话可以去看看。此外,由于问题已经修改过,localstr被返回而不是returnstr的语句现在已经不再正确。

1
很好,我喜欢这个列表。谢谢你的帮助!我还有很多要学习关于自律的事情... - chucknelson
1
这就是我迷失的地方,"fullpaths" 如何接触到已经分配并在 "paths" 中使用的内存? - chucknelson
1
@chucknelson - 他建议问题在于内存不再分配。你的第一个printf()打印出了你期望的内容,实际上是未定义的行为 - 在新的堆栈变量'fullpath'重新使用之前,你碰巧看到了该位置存储的最后一件事情。 - Brian Roach
@Secure,我同意优化的部分...如果不是为了将检查和实际操作分开,我是不会这样做的...否则,逻辑就会在错误检查中丢失。如果只是为了优化,那么我同意这样做没有意义。 - Michael Aaron Safyan
但是,当我编写纯C而不是C ++时,为什么保持标题兼容性很好呢?我是一名程序员,我打字越少,犯的错误就越少。你说我应该这样做,因为这是一个好主意。但这更费力,会导致视觉界面膨胀。这额外的工作必须有优势。仅仅说“这很好”听起来像货物崇拜。http://en.wikipedia.org/wiki/Cargo_cult_programming - Secure
显示剩余12条评论

4
您不能返回指向函数内部分配的数组的指针。一旦函数返回,该数组就会被破坏。
另外,当您放置
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
@chucknelson:例子中可能有一个打字错误,但返回的是指向本地数组的指针,而不是指向returnstr中指向的新分配的块。 - Michael Burr
1
如果只能保证一旦数组超出范围,它就会被破坏……事实上,根据您调用的其他函数数量以及其在堆栈上的相对位置等因素,可能会导致其被破坏,这是令人非常痛苦的经历。 - Duncan
1
@Duncan - 是的,这是一个很好的方法来解决间歇性故障。另外,另一个破坏堆栈的因素是由于中断而导致的上下文保存。 - JustJeff
快速问题:在返回EXIT_FAILURE或EXIT_SUCCESS时,我应该有多个返回吗?如果在函数中遇到某些错误条件,是否应该使用EXIT_FAILURE跳出?我有一个函数,按照这种方式有4个返回语句,这似乎不是很直观...但它可读性还可以吗? - chucknelson
@chucknelson,是的。一旦您遇到一个错误,如果不返回错误状态代码就无法恢复精度/可靠性,那么您应该立即返回。这样的做法更易读。 - Michael Aaron Safyan

0
你的“update”示例完整吗?我认为那不会编译:它要求返回值,但你从未返回过任何东西。你从未对fullpath做任何操作,但也许这是有意为之的,也许你的意图只是说当你进行malloc时,其他事情就会出错。
如果没有看到调用者,就不可能确切地说在这里发生了什么。我的猜测是paths是一个动态分配的块,在调用此函数之前被释放了。根据编译器实现,已释放的块仍然可能包含有效数据,直到将来的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

好吧,它能工作,但可读性大大降低了。

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


是的,那只是一个例子,展示了何时会出现异常情况。我采用使用输出参数的方法,并声明固定大小的字符数组来传递给函数,然后从它们中读取结果。 - chucknelson

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接