当在 C 中读取字符串时,while 循环何时停止?

问题描述

我正在尝试自己实现 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 **/

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

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

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

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

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

解决方法

对于初学者,您应该选择另一个名称而不是 strcpy。

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

变量source声明如下

char source[20] = "aaaa";

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

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

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

变量 desta 声明如下

char desta[10]="abcd";

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

char desta[10]= { 'a','b','c','d','\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;
}
,

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

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

So my question is why and how it still works?

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

,

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

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

来自 C11 标准工作草案 6.7.9 p21

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

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

如果没有显式初始化具有自动存储期的对象,则其值为 不定。 如果一个具有静态或线程存储持续时间的对象没有被初始化 明确,然后

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

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

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

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

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