作为(非常痛苦的)C字符串的练习,我编写了一个字符串反转程序。我是C语言的新手,所以我想确保我没有使用不良的编码做法。它运行得很好(至少使用预期的输入),但是我不确定逻辑是否足够好。

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
char* strreverse(char* c) 
{
    char input[500], output[500] = {}, buffer[2] = {' ', 'q4312078q'};
    strcpy(input, c);
    int i;
    for(i = strlen(input); i >= 0; i--)
    {
        buffer[0] = input[i];
        strcat(output, buffer);
    }
    return (char*) output;
}

int main(void)
{
    char* in = malloc(256);
    printf("Enter a string: ");
    scanf("%s", in);
    char* out = strreverse(in);
    printf("%s", out);
    return 0; 
}


评论

您实际上有什么方法可以就地进行换向吗?这样您将获得一些不错的性能(交换一些字符)

您的规范(反向字符串)是一个好的开始,但是三个词太短了。说您的函数是就地反转还是分配新字符串,是否对字符串有限制,如果字符串为null怎么办,等等。然后向我们展示您的规范和验证它的测试用例。

此代码返回临时输出的地址[]。这是未定义的行为,并且是主要的代码缺陷。商品答案应提及这一点。

“曾经如此痛苦的C弦乐”。我听到了!

#1 楼

实现:到目前为止,该代码不切实际,因为限制为500个字符(包括零终止)。它执行不必要的复制。您需要依靠C字符串以null终止的事实来确定字符串的实际长度。

size_t length = 0;
while (*(str + length) != 0)
{
    ++length;
}


请注意,我正在使用size_t,因为它旨在描述内存中对象的大小。尽管手动操作很容易,但应始终尽可能使用strlen

size_t length = strlen(str);


应注意,该函数返回的长度不带空终止符。然后,我们将分配内存用于结果,因为我们正在创建新的字符串,而不是就位进行更改:

评论。如果分配失败,则进一步的操作肯定会调用未定义的行为,因此我们返回NULL。 length + 1用于空终止符。然后,我们立即将其终止为null:

char* result = malloc(length + 1);
if (result == NULL)
{
    return NULL;
}


现在我们需要捕获原始字符串中最后一个字符的位置,然后从那里开始复制。它恰好在空终止符(索引为length)之前,因此所需位置为length - 1。存在一个长度为0的边沿情况,因此我们应该首先检查那个

result[length] = 0;


,然后编写算法,以相反的顺序将第一个字符串的内容写入第二个字符串:

if (length == 0)
{
    return result;
}
size_t last = length - 1;


,然后简单地返回结果:

size_t it = 0;
while (it <= last)
{
    result[it] = str[last - it];
    ++it;
}


尽管实现具有良好的性能,但是一些进行微优化的机会。函数输入类型应为const char*,因为我们不修改原始字符串。此外,应注意,调用方负责释放malloc的内存。

化妆品:

函数的名称有点难以理解,因此建议您使用str_reverse。另外,c根本不是好名字,因此最好使用str

放在一起:

return result;


评论


\ $ \ begingroup \ $
评论不用于扩展讨论;此对话已移至聊天。
\ $ \ endgroup \ $
– Mathieu Guindon♦
16年7月11日在18:14

#2 楼

返回指向局部变量的指针

返回指向局部变量的指针output是不正确的。它可能会起作用,但这样做是错误的,因为在您从函数返回时,该局部变量的存储超出了范围。为正确起见,您应该分配一个缓冲区并返回它(首选),或者将output设置为static。被避免。将数组初始化为零的标准方法是output

不必要的变量

您的{}变量是不必要的,因为您可以在使用{0}的任何地方使用input

错误的算法

您的字符串反转函数只需要\ $ O(n)\ $时间就花费了\ $ O(n ^ 2)\ $时间。问题是您使用c一次附加一个字符,并且input每次都需要查找字符串的结尾。如果您发现自己在循环中使用strcat,那可能不是最好的方法。

其他


使用strcat读入固定大小的字符串是不安全,并可能导致缓冲区溢出。
在打印结果时,还应该打印换行符,否则shell提示符将与输出在同一行。
函数参数strcat可能被标记为scanf("%s",in);因为您不修改它。另外,c听起来像是字符而不是字符串。
由于您已经在使用C99样式变量声明,因此可以将const声明放入c循环中。

建议重写

char *reverseString(const char *str)
{
    size_t len = strlen(str);
    char  *ret = calloc(len+1, sizeof(char));

    if (ret == NULL)
        return NULL;

    for (size_t i = 0, j = len-1; i < len; i++, j--)
        ret[i] = str[j];
    return ret;
}


评论


\ $ \ begingroup \ $
虽然如果要更改类型,sizeof(char)可能很好,但是sizeof(char)定义为始终返回1。
\ $ \ endgroup \ $
–烟斗
16年7月11日在9:14

\ $ \ begingroup \ $
@pipe我知道,但是我喜欢在那里放一些sizeof的东西,否则对我来说“看起来不对”。
\ $ \ endgroup \ $
– JS1
16年7月11日在9:17

\ $ \ begingroup \ $
好的,我不会反对。只是认为值得一提,也许它可以帮助某人了解您为什么将其放在此处。 :)
\ $ \ endgroup \ $
–烟斗
16年7月11日在9:18

\ $ \ begingroup \ $
@ JS1当时我还不知道您可以循环通过一个char *。您可以在一个字符中编辑单个字符吗?
\ $ \ endgroup \ $
–空火花
16年7月11日在16:58

\ $ \ begingroup \ $
推荐some_type * ret = calloc(len + 1,sizeof * ret);为避免确保代码使用rightizeofof(char)。 sizeof * ret将永远是正确的。 (到目前为止最好的评论)。
\ $ \ endgroup \ $
–chux-恢复莫妮卡
16年7月11日在21:37



#3 楼


    char input[500], output[500] = {}, buffer[2] = {' ', '
str1 = strreverse(input1);
str2 = strreverse(input2);
'};



这不是在C中分配内存的好方法。这在C中的含义是仅保证在函数范围内唯一的内存。因此,如果您有

strreverse(in, reversed, maximum_length);


,那么str1str2通常都指向input2的反面,就像您在input1的第一个调用中所写的那样。

这可以通过两种不同的方式发生。有时,编译器会为该函数分配少量内存,而将其分配给整个程序。您可以通过将变量声明为static来强制这种情况发生,该变量表示在程序运行期间将使用相同的变量。

发生这种情况的第二种方法是可以在函数调用范围内隐式分配和释放内存。然后可以将内存分配给其他人。如果您连续两次调用相同的函数,则可能不太可能两次都分配相同的内存。但是在某些方面,如果您在两次之间做其他事情,情况会更糟。然后,第一次指向output的内存可用于其他变量,并被其他操作完全或部分覆盖。

它使用哪种方法取决于编译器(除非您使变量static始终以第一种方式执行;否则使函数递归,而在这种情况下它将无法执行)第一种方式)。关键点是,您不应该依赖此模式来产生正确甚至是可预测的结果。与垃圾回收语言不同,C在没有指向它的指针之前不会跟踪内存使用情况。它根据范围取消分配静态分配。

第一种方法是在.bss或数据段上进行静态分配。第二种方法是使用堆栈的自动分配(仍然是静态的)。如果您使用malloc或其他动态分配方法,则您正在使用堆分配。如果您想了解更多有关其工作原理的信息,可以从什么开始,堆栈和堆在哪里?您可以跟进静态内存分配和动态内存分配之间的区别。

处理此问题的更正常方法是将输出字符串传递给函数

    char* in = malloc(256);


然后该函数不必担心关于分配内存。另一种可能性是让函数使用malloc或类似的函数分配内存,但是随后您必须显式地由函数隐式分配free内存。通过强制调用者执行此操作,调用者可以弄清楚如何分配和释放内存。


    strcpy(input, c);



每次显式分配内存时,都应该显式分配内存。

该程序无关紧要,因为该程序将在您结束free(某些原因因此将其遗漏)后立即结束并释放所有内存。但是在实际程序中,这可能会导致内存泄漏。


    strncpy(input, c, 500);
    input[499] = '
    for(i = strlen(input); i >= 0; i--)
    {
        buffer[0] = input[i];
';



应改用free

    for (i = strlen(input) - 1; i >= 0; i--)
    {
        buffer[0] = input[i];


这将确保strncpy的长度不会超过c可以容纳的长度,并且input始终为空终止。

或者,您可以检查input的长度是否不再超过c的尺寸,如果可以,请执行一些操作。


q4312078q


请注意,在第一次迭代时,您将在字符串末尾复制空字节。 input函数可以处理此问题(将零长度的字符串复制到strcat中),但这纯属浪费。这应该是

q4312078q

评论


\ $ \ begingroup \ $
哦...我不知道c每次都会分配相同的内存。请问c如何决定将数组存储在哪里?
\ $ \ endgroup \ $
–空火花
16年7月11日在5:10



\ $ \ begingroup \ $
实际上str1 = strreverse(input1); str2 = strreverse(输入2);不一定都指向input2的反面。两者都指向调用strreverse时输出所具有的堆栈地址,因为这些调用具有相同的调用堆栈深度,因此它们将获得相同的指针。如果调用的深度不同,则应该返回不同的指针。但是,下次您调用函数时,指针的内容可能会被垃圾覆盖。如果输出是静态的,那么它将如您所描述的那样发生。
\ $ \ endgroup \ $
–艾米莉·L。
16年7月11日在7:29

#4 楼

建议不要在原处反转输入的字符串,而建议在原处反转输入的字符串:

char* strreverse(char* c)
{
    /* construct pointers to beginning and end of string */
    char* cur = c;
    char* end = c + (strlen(c) - 1);

    /* loop until pointers cross each other (midpoint of string) */
    while (cur < end)
    {
        /* swap characters at current and end locations */
        const char tmp = *cur;
        *cur = *end;
        *end = tmp;

        /* move pointers inward */
        cur++;
        end--;
    }

    return c;
}


(Edit)此方法的工作原理说明:

为简单起见,我们假设此函数的输入字符串为"Hello"

该函数首先在字符串中直接构造两个指针curendcur初始化为指向与字符串开头相同的存储位置,并且end前进到字符串的长度减1(有效指向字符串的最后一个字符)。从视觉上看,这在内存中看起来像以下内容:

cur
v
Hello
    ^
    end


由于end指针位于内存中的cur指针之后,因此字符存储在当前和结束存储位置然后被交换:

cur
v
oellH
    ^
    end


然后两个指针都向内移动; cur向前移动而end向后移动: br />
 cur
 v
oellH
   ^
   end


然后指针再次向内移动:内存比end指针大,因此循环结束,并且字符串被反转。

评论


\ $ \ begingroup \ $
请注意,返回值始终与参数相同,如果希望保留非反向字符串,则在调用前必须strcpy可能很有用。
\ $ \ endgroup \ $
–丰富的历史
16年7月11日在18:49

\ $ \ begingroup \ $
@richremer:实际上,这意味着调用函数已经具有该地址,因此无需返回它(例如,Jeryl Vaz的回答。而且,如果这变成一个空值,我们确实需要“ cur”和“ c”。(只需将第一行中的“ c”重命名为“ cur”,然后删除第一个变量声明。)我仍在投票支持该答案的当前版本,因为我最喜欢它。(但是, sizeof('\ 0')可能比数字1更好,因此1并非未记录的幻数。)
\ $ \ endgroup \ $
– TOOGAM
16年7月11日在20:38

\ $ \ begingroup \ $
char * end = c +(strlen(c)-1);是未定义的行为,应c ==“”。使用char * end = c + strlen(c);并结束-到循环的开始。
\ $ \ endgroup \ $
–chux-恢复莫妮卡
16年7月11日在21:42



\ $ \ begingroup \ $
@Falken您能解释为什么这样做吗?我是C语言的新手,所以我发现指针逻辑令人难以置信。
\ $ \ endgroup \ $
–空火花
16年7月11日在22:29

\ $ \ begingroup \ $
@chux在进一步研究之后,您是对的:由于确实没有为任意对象定义指针之间的范围比较运算符,因此代码确实会根据标准调用UB内存位置。如果将零长度的字符串(\ 0的单个字节)不幸地放在给定段内的0000h偏移处,我的建议确实会失败。感谢您让我想起25年前的代码。 ;)
\ $ \ endgroup \ $
–失败
16年7月19日在6:48

#5 楼

从main()中,很明显,您不需要单独存储反转的字符串。因此,您可以将字符串反转到位,从末端开始交换字符,直到到达中间为止。这也具有只使用一半迭代的好处。

还有更多的东西,还有超出范围的内存访问。您可能需要查看此处提出的解决方案:https://stackoverflow.com/questions/16870485/how-can-i-read-an-input-string-of-unknown-length

评论


\ $ \ begingroup \ $
每次循环迭代都要除以2。可能值得:A)使用内存来存储len / 2,或B)用“(i << 1) \ $ \ endgroup \ $
– TOOGAM
16年7月11日在20:20

\ $ \ begingroup \ $
@TOOGAM“位移位比加法快”对于汇编语言可能是正确的,但对于C语言,如果代码为i * 2或i << 1或其他情况为i / 2或i >> 1-不会更改可执行代码。这样,首选编码就是在这种情况下对人或i * 2最有意义的编码。如果编译器生成的代码不是最佳代码,则问题出在编译器上,而不是编码不够清晰的充分理由。
\ $ \ endgroup \ $
–chux-恢复莫妮卡
16年7月11日在21:55



\ $ \ begingroup \ $
@chux:正确。编译器可能会重新安排我们的代码。但是,为什么不让C代码实际上反映出编译器更有可能做什么呢?我对阅读的代码(C代码)的想法与计算机最终实际完成的工作更加相似,但又有所相似,对此感到有些自在。好吧,移位对于许多人来说可能并不熟悉,但是(i + i)或(2 * i)而不是(len / 2)可能同样可读,并且可能更接近编译器的输出且易于阅读。另外,如果将代码移植到未优化的编译器,则使用DIV可能会很昂贵。
\ $ \ endgroup \ $
– TOOGAM
16年7月12日在3:33

#6 楼

在其他答案中也有一些优点,只是我未曾提及过的几个问题:

char* strreverse(char* c) 


c是一个很好的名称,它是一个字符参数,少了
一个字符串。 s(或str)会更常见。当然,两者都相当简短,但在上下文明显的情况下仍会使用。 (甚至包括memset之类的一些标准函数)。

    char output[500] = {};
    return (char*) output;


这不仅每次都指向同一数组,但实际上是无效的。当函数返回并且指针在此之后无效时,output超出范围。声明它为static可以使其有效,但存在其他人提到的问题。

我同意Jeryl的观点,就是这样的函数可能会就地反转字符串,并使调用者担心生成一个如果他们需要,请复制。如果没有,则不需要其他缓冲区,如果需要,则可以执行strdup。手动结束output,只需从零开始运行另一个索引变量。即使您确切知道每次迭代的位置,在这里strcat也必须每次都找到output字符串的结尾。

    buffer[0] = input[i];
    strcat(output, buffer);


考虑如果有人输入长度超过256个字符的单词。
此外,从终端读取时,scanf与行缓冲交互不良。我建议fgets

#7 楼

对上述某些解决方案的批评

奥尔扎斯解决方案似乎过时了。它在做多余的检查,以确保不同的结构会更短。与falken的解决方案进行比较

Falken的解决方案本身并不是一个不错的答案,但似乎很可能导致混乱。我建议要么使它返回空,这将使它清楚地表明没有新的分配。那也仍然会使它变得微妙,因此我建议使用更清晰的名称strreverseInPlace命名。

是否进行适当的反转是否比制作新的弦更好?例如,strcpy不分配字符串。也许命名函数strcpyreverse会使其更清楚,与strcpy相同。它在2个位置计算字符的索引并检查长度。一个好的编译器可能会优化它们,但为什么要这么做(请参阅Falken的书)。如果编译器无法优化它,它的速度就会变慢。

JS1的解决方案使用calloc,这意味着它将访问目标中的每个字节两次。一次清除它,然后在从src复制时再次清除,因此它的速度将是原来的两倍。

这是另一种解决方案处理器具有处理后增量和前减量的特定指令。当然,即使不是以这种方式编写代码,优化的编译器也可能会找到一种使用这些指令的方式。

请注意,这可能会或可能不会比使用索引更快,这再次取决于处理器(以及编译器的优化),尽管使用直接指针可以说更像C。如果您想使用索引,那么它应该是

/* like strcpy but in reverse. 
   Assumes dst is big enough to hold result. */
char* strcpyreverse(const char* src, char* dst) {
    // copy from end
    const char* s = src + strlen(src);
    char* d = dst;

    while (s > src) {
      *d++ = *--s;
    }
    *d = '
/* like strcpy but in reverse. 
   Assumes dst is big enough to hold result. */
char* strcpyreverse(const char* src, char* dst) {
    // copy from end
    size_t s = strlen(src);
    size_t d = 0;

    while (s > 0) {
      dst[d++] = src[--s];
    }
    dst[d] = '
value = *somepointer;
somepointer += 1;
return value
'; return dst; }
'; return dst; }


在任何情况下,使用*somepointer++的代码在C语言中都是极为常见的模式。表示

/* like strdup but in reverse. */
char* strdupreverse(const char* src) {
    size_t len = strlen(src);
    char* dst = malloc(len + 1);
    if (dst != NULL) { 
        // copy from end
        const char* s = src + len;
        char* d = dst;

        while (s > src) {
          *d++ = *--s;
        }
        *d = 'q4312078q';
    }

    return dst;
}


我感觉它特别存在,因为最初编写C的处理器具有完全做到这一点的指令。

如果您真的希望它分配字符串,则有一个通用函数strdup可以分配和复制字符串,因此可以将其命名为strdupreversestrreversedup

q4312078q

请注意,我没有检查src是否为null,因为任何标准C字符串函数IIRC都没有。

评论


\ $ \ begingroup \ $
这是正确的方法。如果您的目标是学习C,那么您将想学习如何编写惯用的C。您会发现C字符串很痛苦,因为您试图将它们当作字符串对象而不是指针来使用。 C的低级方法既有优点也有缺点。如果您不利用指针之类的东西,那么使用该语言确实毫无意义。 (一个小批判:src和s应该是const char *,而不是char *。)
\ $ \ endgroup \ $
–雷
16年7月12日在10:37



\ $ \ begingroup \ $
@Ray引用* char_pointer和char_pointer有什么区别?
\ $ \ endgroup \ $
–空火花
16年7月12日在17:15

\ $ \ begingroup \ $
感谢const注释。自从我直接写C语言以来已经很久了,我忘了它是C语言还是C ++语言
\ $ \ endgroup \ $
– gman
16年7月13日在0:04

\ $ \ begingroup \ $
@NullSpark我的评论是关于const char * vs char *,而不是s vs. * s。那是你的意思吗?无论如何,这是两者之间的区别:假设您有char buf [] =“ foo”; char * s = buf; const char * t = buf;。然后s和t指向字符串的第一个字符的地址(并且相等,因为在这种情况下,它们都指向同一物理内存buf)。 * s和* t指代字符('F')本身,但是您不能通过* t修改字符串,因为它指向const char:* s ='F';是合法的,但* t ='F'不是。
\ $ \ endgroup \ $
–雷
16年7月13日在18:58

\ $ \ begingroup \ $
不错的strcpyreverse(const char * src,char * dst)。 (可以使用(const char *限制src,char *限制dst))
\ $ \ endgroup \ $
–chux-恢复莫妮卡
16年7月14日在5:04

#8 楼

只需增加几点,便可以添加到您已经收到的答案中。因此,您的str给出了未定义的行为。将其重命名为strreverse可以解决此问题(即,在这里纯粹是重要的名称)。

使用malloc

至少在我看来,reverse_string通常应该是保留用于以下情况:要么直到运行时才知道要分配的空间量;要么分配过多的空间,以至于如果您在堆栈上分配空间,可能会导致堆栈溢出堆。您在malloc中的char* in = malloc(256);不符合任何一项条件。您也可以只将main定义为char数组并完成操作。虽然。特别是,如果您无法修改输入字符串,则希望分配一个可以返回的缓冲区,在这种情况下,in很有意义: >使用malloc


strreversemalloc转换一起使用时,至关重要的是指定要读取的缓冲区的大小,例如:
char *reverse_string(char const *input) { 
    size_t size = strlen(input) + 1;

    char *temp = malloc(size);

    /* ... */


请注意,在这种情况下,您为scanf指定的是可接受的最大输入字符数。它总是在此之后附加一个NUL字符,因此您需要指定一个小于传递的缓冲区大小的值(如果不想使用整个缓冲区,则小于该值,但这很少见)。

就目前而言,没有指定长度,您对scanf的使用与被广泛要求的%s基本相同。我更喜欢尽可能使用无强制转换的代码,因此添加根本不需要的强制转换只会让我感到不安。在您的情况下,您已经从scanf的返回中击中了这个讨厌的表情:在大多数情况下(包括此数组),数组的第一个元素没有任何强制转换。您只需scanf就可以了(除了您要返回本地地址的事实,但其他人已经指出了问题所在,以上有关使用gets的部分也介绍了如何更正它)。 br />