【问题标题】:When does a while loop stop when it reads a string in C?当读取 C 中的字符串时,while 循环何时停止?
【发布时间】:2021-05-20 20:58:14
【问题描述】:

我正在尝试自己实现strcpy 函数。原始的strcpystring.h 库的一部分。

char *strcpy(char *dest, const char *src)
{
    assert(dest != NULL && src != NULL);
    char *temp = dest;
    while (*src)    
    {
        *dest = *src;
        src++;
        dest++;
    }
    return temp;
}
            
void strcpyTest()
{
   char source[20] = "aaaaaa";
   char dest1[20] = "bbbbbbbbb";
   char desta[10]="abcd";
   puts(dest1); // bbbbbbbbb
   strcpy(dest1, source);
   puts(dest1); // aaaaaa
   strcpy(desta, source);
   puts(desta); // aaaaaa
   strcpy(desta, dest1);
   puts(desta); // aaaaaa
   strcpy(dest1, desta);
   puts(dest1); // aaaaaa
   strcpy(source, desta); 
   puts(source); // aaaaaa
}

如您所见,即使是第一次调用destsrc 更长的函数也会给出正确的结果,尽管按照逻辑,它应该给出 aaaaaabb 而不是aaaaaa

   char source[20] = "aaaaaa";
   char dest1[20] = "bbbbbbbbb";
   strcpy(dest1, source);
   puts(dest1);
   /** aaaaaa **/

为什么我的函数有效?我猜我必须在while (*src)` 退出后手动在*dest* 末尾添加/0 字符。

我的意思是这个while (*src) 的全部意义在于当它到达*src* 的末尾时退出,这是/0 字符串中的最后一个字符。 因此,我想我必须自己将这个字符添加到*dest*,但代码会以某种方式工作并复制字符串,而无需手动添加/0

  1. 所以我的问题是它为什么以及如何仍然有效?

  2. 当我创建一个新数组时,假设 int *arrchar *arr,共 10 个,即 char arr[10]int arr[10],并且我只初始化前 2 个索引,其余部分中的值会发生什么情况索引?它们会填充零或垃圾值还是什么?

也许我的代码有效,因为它填充了零,这就是 while 循环停止的原因?

【问题讨论】:

  • 初始化数组时,任何未显式设置的元素都将被设置为与静态变量相同的方式,也就是说未显式初始化的元素将被隐式初始化为0。
  • 它完全是偶然的。您的目标数组恰好有 \0 在恰好给您“正确”结果的地方。
  • @NoobCoder 我希望它能打印出aaaaaabb。我说的对吗?
  • 抱歉,无法重现。仔细检查它。 ideone.com/xHqWWz - 这是按照我的预期打印的(虽然算错了bs)
  • C 标准明确禁止用您自己的同名函数替换库函数。所以编译器可以使用它自己的strcpy,即使你提供了一个不同的。尝试更改函数的名称,或者至少在函数的开头加上 printf 以确保它实际上被调用了。

标签: c while-loop c-strings strcpy function-definition


【解决方案1】:

对于初学者,您应该选择其他名称而不是 strcpy。

让我们逐步考虑您的函数的所有调用。

变量source 声明如下

char source[20] = "aaaa";

这个声明等价于下面的声明

char source[20] = 
{ 
    'a', 'a', 'a', 'a', '\0', '\0', '\0', '\0', '\0', '\0', 
    '\0', '\0', '\0', '\0', '\0', '\0', '\0', '\0', '\0', '\0'
};

因为根据 C 标准,未显式初始化的数组元素被隐式初始化为零。

变量desta 声明如下

char desta[10]="abcd";

这个声明等价于下面的声明

char desta[10]= { 'a', 'b', 'c', 'd', '\0', '\0', '\0', '\0', '\0', '\0' };

所以第一次调用

strcpy(desta, source);

只需将四个字符 "abcd" 替换为四个字符 "aaaa"。结果数组desta 将包含一个字符串,因为下界的零被覆盖了。

这次通话后

strcpy(desta, dest1);

数组desta 将包含字符串"bbbbbbbbb",因为此调用不会覆盖数组desta 的最后一个零字符。

这个电话

strcpy(dest1, desta);

实际上并没有被改变数组dest1

在本次通话中

strcpy(source, desta);

因为数组source 的所有零字符都没有被覆盖,所以数组将包含一个字符串。

如果你第一次打电话,你可能会得到一个不可预知的结果

strcpy(desta, dest1);

然后

strcpy(desta, source);

因为您的函数不会将终止零附加到目标数组。

这是一个演示程序。

#include <stdio.h>
#include <assert.h>

char * my_strcpy(char *dest, const char *src)
{
    assert(dest != NULL && src != NULL);
    char *temp = dest;
    while (*src)    
    {
        *dest = *src;
        src++;
        dest++;
    }
    return temp;
}

int main(void) 
{
    char source[20] = "aaaaaa";
    char dest1[20] = "bbbbbbbbb";
    char desta[10]="abcd";
    
    my_strcpy(desta, dest1);
    my_strcpy(desta, source);
    
    puts( desta );

    return 0;
}

程序输出是

aaaaaabbb

desta 包含字符串"aaaaaabbb" 而不是字符串aaaaaa

更新后的函数如下所示

char * strcpy(char *dest, const char *src)
{
    assert(dest != NULL && src != NULL);
    char *temp = dest;

    while ( ( *dest++ = *src++ ) );    

    return temp;
}

【讨论】:

  • 谢谢弗拉德。这里的其他人写道,不能保证将 '\0' 初始化为我没有初始化的索引的值。你怎么看待这件事?这是真的吗? int arrayschar 以外的其他类型的数组呢?
  • @NoobCoder 这由 C 标准保证。没有显式初始化器的数组元素(假设数组已初始化)被隐式初始化为零。
  • 我刚刚编译了您在答案末尾添加的建议。首先strcpy(desta, desti);,然后strcpy(desta, source);,一切正常。第一个strcpy(desta, desti); 之后的输出是:desti:bbbbbbbbb , desta:bbbbbbbbbstrcpy(desta, source); 之后的输出是:source:aaaaaa , desta:aaaaaa。所以一切都被很好地复制了。
  • @NoobCoder 查看我的更新答案。看来您正在调用标准字符串函数 strcpy 而不是函数 strcpy。
  • 你说得对,哈哈,我刚刚修复了它,正如你告诉我的那样,它没有按照应有的方式复制,因为我必须在副本末尾手动添加\0。最后一个问题 - 为什么while ( ( *dest++ = *src++ ) ); 有效?最后不需要添加\0,它是如何工作的?我的意思是,它将 *src 的值放在 *dest 中,然后提升它们。 \0 在哪里?
【解决方案2】:

更正此函数不会在目标字符串的末尾添加\0。您需要将最终的 \0 分配添加到 dest。

为什么它似乎按原样工作? 它“有效”是因为您对 dest 的初始化恰好在字符串的正确位置放置了一个 \0 字符。这些案例确实是“不幸的”,因为它们隐藏了各种问题。另一种可能发生这种情况的情况是,如果您正在运行调试版本,其中内存自动设置为 0,因此最终设置的错误被隐藏。

So my question is why and how it still works?

如果您初始化数组上的前 2 个值,则其余的值将被视为垃圾,再次与您所说的完全一样。例外情况是数组是“全局”或“静态”的。在这些情况下,编译器会为您将它们设置为 0。

【讨论】:

  • 跟debug无关,跟数组初始化的规则有关。
  • 啊——错过了
  • 要清楚一点,如果你初始化一个自动存储生命周期数组的 any 元素,那么所有未显式初始化的元素都将为 0,而不是垃圾。如果您根本不初始化数组,那只会是垃圾。
  • @ChristianGibbons 当您说“如果您根本不初始化数组”时,您究竟是什么意思?就像我没有在其中初始化一个索引一样?
  • @NoobCoder 是的。如果你声明一个没有初始化器的数组,例如 int x[20]; 并且它具有自动存储持续时间,那么元素的值将是垃圾。
【解决方案3】:

您的数组用零填充,相当于空终止符'\0'。当你初始化一个数组时,任何未显式设置的元素都将被设置为与静态变量相同的方式,也就是说,未显式初始化的元素将被隐式初始化为 0。所以在这种情况下,你的字符串就会发生在完成复制后有一个空终止符,因为在初始化数组时,初始化程序未明确设置的所有值都设置为0

如果您将一个 4 个字符的字符串复制到一个包含 8 个字符的字符串的缓冲区中,您将只会看到目标字符串中的前 4 个字符发生了变化,而在您遇到空终止符之前还有 4 个字符仍然存在.

来自 C11 标准工作草案 6.7.9 p21

如果大括号括起来的列表中的初始值设定项少于元素或成员的数量 用于初始化已知数组的字符串文字中的聚合或更少字符 大小超过数组中的元素,聚合的其余部分应为 隐式初始化与具有静态存储持续时间的对象相同

所以根据上面的段落,因为你初始化了数组的一些元素,你没有显式初始化的元素将被视为静态存储持续时间对象。 所以对于状态存储时长的规则,我们有 6.7.9 p10:

如果具有自动存储持续时间的对象未显式初始化,则其值为 不定。 如果具有静态或线程存储持续时间的对象未初始化 显式地,那么

——如果是指针类型,则初始化为空指针;

——如果是算术类型,则初始化为(正或无符号)零;

——如果是聚合,每个成员都根据这些规则(递归地)初始化, 并且任何填充都被初始化为零位;

——如果它是一个联合,第一个命名的成员根据这些被初始化(递归) 规则,并且任何填充都被初始化为零位;

上面的段落告诉我们聚合的每个成员(在这种情况下是一个数组)都将根据元素的规则进行初始化,在这种情况下,它们的类型为char,被认为是算术类型,规则状态将被初始化为0。

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 2015-01-12
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2012-03-17
    • 1970-01-01
    相关资源
    最近更新 更多