从函数中返回一个C字符串

139

我试图从一个函数中返回一个C字符串,但是没有成功。下面是我的代码。

char myFunction()
{
    return "My String";
}

main 函数中,我是这样调用它的:

int main()
{
  printf("%s", myFunction());
}

我也尝试了一些其他的myFunction方法,但它们都没有起作用。例如:

char myFunction()
{
  char array[] = "my string";
  return array;
}

注意:我不能使用指针!

这个问题的背景:

有一个函数用于确定现在是哪个月份。比如,如果现在是1月,那么它就会返回"January"等等。

因此,当要打印出月份时,代码会这样写:printf("Month: %s",calculateMonth(month));。现在的问题是如何从 calculateMonth 函数中返回该字符串。


16
很遗憾,在这种情况下你需要使用指针。 - Nick Bedford
1
@Hayato 我相信我们都是成年人,知道它应该返回0,只是为了举例说明而已。 - itsaboutcode
4
仅在C99(和C ++)中默认隐含return 0,但不包括C90。 - hrnt
1
那么你将无法做到它,除了愚蠢的黑客技巧,这实际上只是破解指针操作。指针存在是有原因的...:| - GManNickG
1
请查看Steve Summit关于C语言中返回数组的长篇解释 - lifeisfoo
显示剩余9条评论
15个回答

267
你的函数签名需要是这样的:
const char * myFunction()
{
    return "my String";
}

背景:

对于C和C++来说,这是非常基础的知识,但还需要更多的讨论。

在C(以及C++)中,字符串只是一个以零字节结尾的字节数组 - 因此术语“零终止字符串”用于表示这种特定类型的字符串。还有其他类型的字符串,但在C(以及C++)中,这种类型是语言本身固有的。其他语言(如Java、Pascal等)使用不同的方法来理解"my string"

如果您曾经使用过Windows API(它是用C++编写的),您会经常看到函数参数,例如:“LPCSTR lpszName”。'sz'部分代表了这种“零终止字符串”的概念:一个以空(/零)终止符结尾的字节数组。

澄清:

为了方便起见,在这个“介绍”中,我将“字节”和“字符”交替使用,因为这样学习更容易。请注意,还有其他方法(宽字符和多字节字符系统(mbcs))用于处理国际字符。UTF-8就是一种mbcs的例子。为了简单起见,我没有详细解释这些内容。

内存:

这意味着像"my string"这样的字符串实际上使用了9+1(=10!)个字节。当您最终开始动态分配字符串时,这一点非常重要。

因此,如果没有这个“终止零”,你就没有字符串。你只有一个在内存中挂着的字符数组(也称为缓冲区)。

数据的持久性:

这种方式使用函数:

const char * myFunction()
{
    return "my String";
}

int main()
{
    const char* szSomeString = myFunction(); // Fraught with problems
    printf("%s", szSomeString);
}

...通常会导致随机的未处理异常/段错误等问题,特别是“在未来”。

简而言之,虽然我的答案是正确的——如果你以那种方式使用它,9次中有9次会导致程序崩溃,特别是如果你认为这是“好习惯”的话。总之:通常不是这样。

例如,想象一下将来的某个时候,字符串现在需要以某种方式进行操作。通常,编码人员会“走捷径”并(尝试)编写如下代码:

const char * myFunction(const char* name)
{
    char szBuffer[255];
    snprintf(szBuffer, sizeof(szBuffer), "Hi %s", name);
    return szBuffer;
}

也就是说,你的程序会崩溃,因为编译器(可能)已经释放了szBuffer使用的内存,这时在main()函数中调用printf()。 (你的编译器应该事先警告你这样的问题。)
有两种方法可以返回不容易出错的字符串:
1. 返回具有一定生命周期的缓冲区(静态或动态分配)。在C++中使用“辅助类”(例如std::string)来处理数据的寿命(这需要更改函数的返回值),或者 2. 将缓冲区传递给函数,并填充其中的信息。
请注意,在C语言中无法使用字符串而不使用指针。正如我所展示的,它们是同义词。即使在C++中使用模板类,背后仍然会使用缓冲区(也就是指针)。
因此,为了更好地回答(现在修改后的)问题。(肯定还有其他答案可以提供。)
更安全的答案:
例1,使用静态分配的字符串:
const char* calculateMonth(int month)
{
    static char* months[] = {"Jan", "Feb", "Mar" .... };
    static char badFood[] = "Unknown";
    if (month < 1 || month > 12)
        return badFood; // Choose whatever is appropriate for bad input. Crashing is never appropriate however.
    else
        return months[month-1];
}

int main()
{
    printf("%s", calculateMonth(2)); // Prints "Feb"
}

这里static的作用(许多程序员不喜欢这种'分配'类型)是将字符串放入程序的数据段中。也就是说,它是永久分配的。

如果你转向C++,你会使用类似的策略:

class Foo
{
    char _someData[12];
public:
    const char* someFunction() const
    { // The final 'const' is to let the compiler know that nothing is changed in the class when this function is called.
        return _someData;
    }
}

...但如果你只是为自己编写代码(而不是为了与他人共享的库),使用辅助类(例如std::string)可能更容易。

示例2,使用调用方定义的缓冲区:

这是传递字符串的更加“防傻”方法。返回的数据不受调用方的操纵。也就是说,示例1很容易被调用方滥用,并暴露应用程序故障。这种方式更安全(尽管使用了更多的代码):

void calculateMonth(int month, char* pszMonth, int buffersize)
{
    const char* months[] = {"Jan", "Feb", "Mar" .... }; // Allocated dynamically during the function call. (Can be inefficient with a bad compiler)
    if (!pszMonth || buffersize<1)
        return; // Bad input. Let junk deal with junk data.
    if (month<1 || month>12)
    {
        *pszMonth = '\0'; // Return an 'empty' string
        // OR: strncpy(pszMonth, "Bad Month", buffersize-1);
    }
    else
    {
        strncpy(pszMonth, months[month-1], buffersize-1);
    }
    pszMonth[buffersize-1] = '\0'; // Ensure a valid terminating zero! Many people forget this!
}

int main()
{
    char month[16]; // 16 bytes allocated here on the stack.
    calculateMonth(3, month, sizeof(month));
    printf("%s", month); // Prints "Mar"
}

有很多原因,第二种方法更好,特别是当你编写要被其他人使用的库时(你不需要锁定特定的分配/释放方案,第三方无法破坏你的代码,你不需要链接到特定的内存管理库),但像所有代码一样,取决于你最喜欢哪个。因此,大多数人选择示例1,直到他们被烧过很多次,才拒绝再以那种方式编写它 ;)

免责声明:

我已经退休几年了,我的C语言现在有点生疏。这个演示代码应该可以在C中正确编译(它对任何C++编译器也是可以的)。


2
实际上,该函数需要返回char *,因为C中的字符串字面量是char []类型。但是,它们不能以任何方式被修改,因此最好返回const char *(请参见https://www.securecoding.cert.org/confluence/x/mwAV)。如果字符串将用于遗留或外部库函数,并且该函数(不幸地)期望一个char *作为参数,即使它只会从中读取,可能需要返回char *。另一方面,C++具有const char []类型的字符串字面量(自C++11以来,您还可以拥有std::string字面量)。 - TManhente
18
@cmroanirgo 的 my 前缀告诉读者该函数是由用户创建的。在这种情况下使用该前缀我认为非常合理。 - quant
5
根据这里的链接:https://dev59.com/YWkw5IYBdhLWcg3wVpBi,你可以返回字符串字面值。 - Giorgi Moniava
9
“长期保存数据”部分标记为“存在问题”的代码实际上是完全有效的。在C/C++中,字符串文字具有静态寿命。请参见Giorgi上面提到的链接。 - ST0
1
@cmroanirgo 返回字符串字面量是良好的实践和风格。它不会“充满问题”,也不会在9次中崩溃:它永远不会崩溃。即使是80年代的编译器(至少我使用过的)也正确支持字符串字面量的无限生命周期。注意:我不确定您所说的编辑答案的意思:我仍然看到它说它容易崩溃。 - cesss
显示剩余7条评论

14

一个C字符串被定义为指向字符数组的指针。

如果你不能使用指针,按照定义,你就不能使用字符串。


2
你可以将一个数组传递给函数,然后对该数组进行操作:void foo(char array[], int length)。当然,在底层,array是一个指针,但它不是“显式”的指针,因此对于那些正在学习数组但尚未完全掌握指针的人来说,这可能更直观。 - jvriesem

13
请注意这个新函数:
const char* myFunction()
{
    static char array[] = "my string";
    return array;
}

我将 "数组" 定义为静态的。否则当函数结束时,变量(以及您返回的指针)就会超出范围。由于该内存是在堆栈上分配的,因此它损坏。这种实现的缺点是代码不可重入且不线程安全。

另一种选择是使用 malloc 在堆中分配字符串,然后在代码的正确位置释放。这段代码将是可重入和线程安全的。

正如评论中所指出的那样,这是非常糟糕的做法,因为攻击者可以将代码注入到您的应用程序中(他/她需要使用 GDB 打开代码,然后设置断点并修改返回变量的值以溢出,接着乐趣就开始了)。

更推荐让调用方处理内存分配。请参考以下新示例:

char* myFunction(char* output_str, size_t max_len)
{
   const char *str = "my string";
   size_t l = strlen(str);
   if (l+1 > max_len) {
      return NULL;
   }
   strcpy(output_str, str);
   return output_str;
}

请注意,唯一可修改的内容是用户的内容。另一个副作用是,从库的角度来看,此代码现在是线程安全的。调用此方法的程序员应验证所使用的内存部分是否是线程安全的。

2
这通常是一种不好的做法。 char* 可以被周围的代码所操作。也就是说,你可以做这样的事情:strcpy(myFunction(), "一个非常长的字符串"); 但你的程序将因为访问冲突而崩溃。 - cmroanirgo
2
在“用户所使用的那个”附近似乎缺少了一些东西。 - Peter Mortensen

8
您的问题出在函数的返回类型上 - 它必须是:
char *myFunction()

...然后你的原始公式就能够正常工作。

请注意,你无法在某个地方涉及到指针时使用C字符串。

另外:增加编译器警告。它应该已经警告你返回行将char *转换为char而没有明确的转换。


2
我认为签名应该使用const char*,因为字符串是文字常量,但如果我没有记错,编译器会接受这个。 - Luke

5

根据您新添加的问题背景,为什么不返回一个1到12的整数代表月份,并让main()函数使用switch语句或if-else梯形结构来决定要打印什么呢?这当然不是最好的方法 - char*会更好 - 但在这种类的情况下,我想这可能是最优雅的。


3

你的函数返回类型是单个字符 (char)。你应该返回一个指向字符数组第一个元素的指针。如果你不能使用指针,那么你就无法完成任务。 :(


3

您可以在调用者(即主函数)中创建该数组,并将数组传递给被调用者(即您的myFunction())。因此,myFunction 可以将字符串填入数组中。不过,您需要将myFunction()声明为:

char* myFunction(char * buf, int buf_len){
  strncpy(buf, "my string", buf_len);
  return buf;
}

main 函数中,应以以下方式调用 myFunction
char array[51];
memset(array, 0, 51); /* All bytes are set to '\0' */
printf("%s", myFunction(array, 50)); /* The buf_len argument  is 50, not 51. This is to make sure the string in buf is always null-terminated (array[50] is always '\0') */

然而,仍然会使用指针。

2
或者这个怎么样:
void print_month(int month)
{
    switch (month)
    {
        case 0:
            printf("January");
            break;
        case 1:
            printf("february");
            break;
        ...etc...
    }
}

在其他地方计算出月份后,使用该月份调用该函数。


1
+1 不是OP要求的,但这可能是作业期望你做的,因为他不能使用指针。 - Vitim.us
即使是printf也使用指针。指针就像刀子——生活和工作中必不可少,但你必须握住它的把手,用锋利的一面来切割,否则你会遭遇麻烦。函数定义中空格的不幸放置是新C程序员的脑部错误。char * func( char * s); char func( char s); char func * char s);它们都是相同的,但看起来都不同,为了加深混淆, *也是变量指针的反引用运算符。 - Chris Reid

1
如果你真的不能使用指针,可以像这样做:
char get_string_char(int index)
{
    static char array[] = "my string";
    return array[index];
}

int main()
{
    for (int i = 0; i < 9; ++i)
        printf("%c", get_string_char(i));
    printf("\n");
    return 0;
}

魔数9很糟糕,这不是好的编程示例。但你明白了重点。请注意,指针和数组是相同的东西(有点),所以这有点作弊。


通常情况下,如果您需要实现这样的解决方案来解决作业问题,那么您的初步假设可能是错误的。 - hrnt

1
一个 char 只能存储单个的一字节字符,它无法存储一串字符,也不是指针(显然你无法使用指针)。因此,如果不使用指针(char[] 的语法糖),你无法解决问题。

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