由于无法返回局部变量,从C或C++函数返回字符串的最佳方法是什么?

18
作为对这个问题的跟进:
从我所看到的,这应该按预期工作:
void greet(){
  char c[] = "Hello";
  greetWith(c);
  return;
}

但这会导致未定义的行为:

char *greet(){ 
  char c[] = "Hello";
  return c;
}

如果我是正确的,那么如何修复第二个问候函数?在嵌入式环境中?在桌面上?


我可以制作一艘帆船,一只翼龙... :-o 抱歉,当我读到这个问题时,我有一个《飞屋环游记》的瞬间。 - tvanfosson
实际上,行为已经被定义了,但数据将是未定义的。下一个函数调用将抹掉由greet()返回的数组。 - Jason Coco
8个回答

33

你说得完全正确。第二个例子中的 c 数组是在堆栈上分配的,因此内存将立即被重用。特别是,如果你有像下面这样的代码:

 printf("%s\n",greet());

如果您使用printf调用并重复使用了数组的一些空间,那么您将获得奇怪的结果。

解决方法是在其他地方分配内存。例如:

char c[] = "Hello";

char * greet() {
    return c;
}

可以工作。另一个选择是在范围内静态分配它:

char * greet() {
    static char c[] = "Hello";
    return c;
}

因为静态内存是在数据空间中单独分配的,与堆栈分开。
第三种选择是通过malloc在堆上分配内存:
char * greet() {
   char * c = (char *) malloc(strlen("Hello")+1);  /* +1 for the null */
   strcpy(c, "Hello");
   return c;
}

但现在你必须确保内存得到释放,否则就会出现内存泄漏。

更新

有些事情比我预期的更加令人困惑,其中一个就是“内存泄漏”的确切含义。当你动态分配内存,但丢失了地址以至于无法释放时,就会发生泄漏。这些示例中没有一个必然会出现泄漏,但只有第三个示例有可能会出现泄漏,因为它是唯一一个动态分配内存的示例。所以,假设使用第三个实现,你可以编写以下代码:

{
    /* stuff happens */
    printf("%s\n", greet());
}

这里存在一个内存泄漏问题;指向动态分配的内存的指针被返回,printf函数使用了它,然后它就丢失了;你不能再释放它。另一方面,

{
    char * cp ;
    /* stuff happens */
    cp = greet();
    printf("%s\n", cp);
    free(cp);
}

不会泄漏,因为指针保存在自动变量cp中,足够长以调用free()。现在,即使cp在执行结束括号后立即消失,由于已经调用了free,内存被回收并且没有泄漏。


第三个例子是否有泄漏,还是它们都有? - Ólafur Waage
它们中没有一个必须泄漏;只有第三个可能会泄漏。当对动态分配的内存的引用丢失时,就会发生泄漏,因此在释放内存之前如果返回的指针丢失,则第三个选项将会泄漏。 - Charlie Martin
我只想补充一点,虽然第二个例子没有泄漏,但我认为它确实分配了在应用程序生命周期内保持分配的内存,这可能导致类似泄漏的症状。 - Dave Costa
1
胡说八道。它只分配静态内存一次。它的行为与第一个示例完全相同,只是名称仅在大括号内范围内。 - Charlie Martin
这个答案以及Greg和j_random_hacker的回答列出了所有选项。不过我希望你能列出每种方法的优缺点;例如,静态内存通常不在缓存中,因此不适合高性能代码,或者你必须记得释放malloc分配的字符串。 - congusbongus
问题在于利弊取决于您所尝试做的事情。使用静态内存本质上不是线程安全的;使用malloc内存意味着您存在泄漏风险。如果您希望分配大量块并长时间运行,可以编写其他更复杂的方案,如创建具有垃圾回收功能的分配器。如果没有了解上下文,那么就没有“正确”的答案。 - Charlie Martin

12

如果你正在使用C++,那么你可能希望考虑使用std::string从你的第二个函数返回字符串:

std::string greet() {
    char c[] = "Hello";
    return std::string(c); // note the use of the constructor call is redundant here
}

或者在单线程环境下,您可以这样做:

char *greet() {
    static char c[] = "Hello";
    return c;
}

这里的static会在全局内存区域分配空间,并且永远不会消失。但是这个static方法也充满了危险。


如果你也将它定义为const并在全局范围内定义,那么就会更加安全。 - ChrisW
我本以为最好假设它是 C,因为它是嵌入式的。 - Jesse Pepper
1
Jesse: “c ++” 标签有点暗示它是一个选项。 - Greg Hewgill
@ChrisW:我在考虑greet()执行一些非平凡操作并返回某种计算结果的情况。它必须将其存储在某个非常量位置,并且您还必须处理foo(greet(), greet())这种情况。 - Greg Hewgill

9

这要取决于嵌入式环境是否具有堆内存。如果有,你应该按照以下方式使用 malloc:

char* greet()
{
  char* ret = malloc(6 * sizeof(char)); // technically " * sizeof(char)" isn't needed since it is 1 by definition
  strcpy(ret,"hello");
  return ret;
}

请注意,您后续需要调用free()来进行清理。如果您没有动态分配的权限,则需要将其设置为全局变量或堆栈上的变量,但是要考虑到调用堆栈的更高层次。

啊,比我的回答更好,也不那么刻薄。 - dmckee --- ex-moderator kitten
我需要在malloc之后对ret进行空指针检查吗? - nate
@nate:是的,你肯定应该这样做。如果分配失败,将返回Null。 - Jesse Pepper
@jesse:你在使用malloc的语法中使用了calloc... malloc只接受一个参数,即要分配的块的大小(以字节为单位)。 - Jason Coco
在C语言中,可以使用calloc(6, sizeof(char))或malloc(6 * sizeof(char))来分配内存。在C++中,如果没有堆内存可用,可以重载operator new函数以在预定义的内存中工作(可能是在栈底下保留的空间之类的地方)。 - user21037

7
如果您需要在C++中执行此操作,则使用Greg Hewgill建议的std::string绝对是最简单和可维护的策略。
如果您正在使用C语言,则可以考虑按照Jesse Pepper的建议返回一个使用malloc()进行动态分配的空间指针;但是另一种避免动态分配的方法是让greet()接受一个char *参数并将其输出写入那里。
void greet(char *buf, int size) {
    char c[] = "Hello";

    if (strlen(c) + 1 > size) {
        printf("Buffer size too small!");
        exit(1);
    }

    strcpy(buf, c);
}

size参数是为了安全起见,以帮助防止缓冲区溢出。如果您确切知道字符串的长度,则不需要该参数。


1
本地的 char c[] 是不必要的;通常在使用这种方法时,数据会直接复制到 buf 中。 - congusbongus
@CongXu:你说得对。在这个例子中,最好写成char *c = "Hello";,这样就不会在栈上创建字符串字面量的不必要副本了。 - j_random_hacker

5

您可以使用以下任一选项:

char const* getIt() {
    return "hello";
}

char * getIt() {
    static char thing[] = "hello";
    return thing;
}

char * getIt() {
    char str[] = "hello";
    char * thing = new char[sizeof str];
    std::strcpy(thing, str);
    return thing;
}

shared_array<char> getIt() {
    char str[] = "hello";
    shared_array<char> thing(new char[sizeof str]);
    std::strcpy(thing.get(), str);
    return thing;
}

第一种方法要求您不要写入返回的字符串,但它是您可以获得的最简单的方法。最后一种方法使用 shared_array,在引用内存丢失时会自动清理内存(最后一个 shared_array 超出其范围)。如果每次调用函数都需要一个新字符串,则必须使用最后两种方法。

我喜欢最后一种方法! - Shankar

4

按照其他回答所建议的在函数中返回malloc'd内存只会导致内存泄漏。调用者需要知道你使用了malloc并调用free来释放内存。如果他们错误地调用delete,则结果是未定义的(尽管可能没有问题)。

更好的方法是强制调用者为您提供内存,然后将字符串复制到内存中。这样调用者就知道他/她需要清理内存。


1
据我所读,最安全的选项是使调用者负责分配内存以容纳字符串。您还必须检查缓冲区是否足够大以容纳您的字符串:
char *greet(char *buf, int size) {
     char *str = "Hello!"
     if (strlen(str) + 1 > size) { // Remember the terminal null!
          return NULL;
     } 
     strcpy(buf, str);
     return buf;
}

void do_greet() {
    char buf[SIZE];
    if (greet(buf, SIZE) == NULL) {
        printf("Stupid C");
     } 
     else {} // Greeted!
}

大量的工作为了一个简单的任务... 但这就是 C:-) 哎呀!我想我被 random_hacker 打败了...

我听说你不应该将字符串字面值分配给char*,而是应该使用char[],对吗? - Nate Parsons
1
它们意思不同。如果您将文字分配给 char *,则会获得指向静态分配的只读文字的指针。如果您分配给 char [],则会获得自己的堆栈分配本地副本,可用于任何需要。 - Eclipse

0

在堆中分配字符数组?

能否使用malloc取决于您所说的“嵌入式环境”的具体含义。


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