【问题标题】:How to separate a string into multiple other strings of specific lengths in C?如何在C中将一个字符串分成多个其他特定长度的字符串?
【发布时间】:2018-10-11 05:44:13
【问题描述】:

在 main 中,我将一个字符串传递给一个不同的函数,该函数应该分隔字符串,然后处理每个子字符串。在这种情况下,我需要取一个 30 个字符的字符串并将其分成长度为 7、5、5、7 和 6 的子字符串,以便稍后进行操作。这是我开始尝试的:

void breakString(const char *lineStr) {
        char a[7] = " "; //I tried with them all initialized empty and without doing so.
        char b[5];       //Didn't seem to make a difference.
        char c[5];
        char d[7];
        char e[6];

        //sscanf(lineStr, "%7s", &a);     //tried sscanf at first, but didn't know how to 
        strncpy(a, lineStr, 7);           //scan the middle so i switched to strncpy
        strncpy(b, lineStr + 7, 5);
        //continue this pattern for c,d,e

        (rest of function here, where each substring is manipulated accordingly.)

我通过打印子字符串a 和b(以及strcmp() 将它们打印到正确的输出)测试了第一位,但它不能完全工作。我一直在胡言乱语。例如,如果传递的完整字符串是"abcdefghijklmnopqrstuvwxyz1234",那么a 应该是"abcdefg",b 应该是"hijkl",等等。但是,当我打印a 时,它会显示为"abcdefg^#@%^&",每个子字符串后面都有一些随机的字符。

我做错了什么?还是有更好的方法来实现这一点?

【问题讨论】:

  • strncpy 不会空终止,所以你看到的是溢出。解释见randomascii.wordpress.com/2013/04/03/stop-using-strncpy-already
  • C 中的字符串末尾有一个零字节。如果你有一个像a[7] 这样的数组,而你是strncpy(a, lineStr, 7),你得到的是一个字符数组。它不是字符串,因为它末尾没有零字节。所以你不能把它传递给像strcmp这样的字符串函数,你也不能用一个简单的%s格式说明符来打印它。您可以使用%.7s 打印它
  • strncpy 是邪恶的。始终使用strlcpy。如果它在您的系统上不可用,请自行查找源并使其可用。 strncpy 不是也从未用于复制字符串,这与它的名字所暗示的相反。

标签: c arrays string parsing strncpy


【解决方案1】:

我一直在胡言乱语......

这是因为strncpy() 不会在源长度大于传递的大小时在目标末尾隐式附加空字符。在 C 语言中,字符串是一个以 null 结尾的字符数组。

因此,在此之后:

strncpy(a, lineStr, 7);

如果源比传递的大小长,则需要在末尾添加空字符,如下所示:

a[7] = '\0';

缓冲区大小应为 +1 以容纳缓冲区末尾的空字符:

char a[8];
char b[6];      
char c[6];
char d[8];
char e[7];

您应该尽量避免使用strncpy(),因为您需要手动处理附加空字符。相反,请使用可以保证始终以空值终止目标的东西,例如snprintf()。你可以这样做:

char a[8];
snprintf(a, 8, "%s", lineStr);

您不需要附加终止空字符,它会在写入内容后自动附加。阅读更多关于snprintf()here的信息。


补充:

您尝试初始化空数组的方式不正确:

char a[7] = " "; 

这不是空数组,但这实际上将使用空格字符初始化数组(a[0]) 的第一个元素,其余元素将使用0 初始化。要初始化空数组,您可以这样做:

char a[8] = {0};

这将使用0 初始化数组的所有元素。

【讨论】:

    【解决方案2】:

    您的问题可以通过strncpy 解决,但您永远不应该使用此函数,因为它的精确语义被广泛误解并且极易出错。

    阅读https://randomascii.wordpress.com/2013/04/03/stop-using-strncpy-already/ 了解信息。

    此外,您应该使数组比您计划为空终止符存储到其中的字符数长一个字节。

    以下是针对您的情况的简单解决方案:

    #include <stdio.h>
    
    void breakString(const char *lineStr) {
        char a[7+1] = ""; /* destination strings must be initialized */
        char b[5+1] = ""; /* because the %c conversion specifier */
        char c[5+1] = ""; /* will set a null terminator. */
        char d[7+1] = "";
        char e[6+1] = "";
    
        if (strlen(lineStr) >= 7+5+5+7+6 &&
            sscanf(lineStr, "%7c%5c%5c%7c%6c", a, b, c, d, e) == 5) {
            /* string was long enough, fields correctly initialized */
            printf("a: %s\nb: %s\nc: %s\nd: %s\ne: %s\n", a, b, c, d, e);
        }
    }
    
    int main() {
        breakString("abcdefghijklmnopqrstuvwxyz0123456789");
        return 0;
    }
    

    输出:

    a: abcdefg
    b: hijkl
    c: mnopq
    d: rstuvwx
    e: yz0123
    

    虽然此解决方案简单明了,但我建议您采用不同的方法,使用实用函数。事实上,sscanf 解决方案使用了一组非常不寻常的转换说明符,这将使大多数程序员扬眉吐气并拒绝它。此外,它不适合将可变数量的字符提取到适当大小的子数组中。

    这是一种不同的方法:

    #include <stdio.h>
    
    size_t getchunk(char *dest, size_t n, const char *str) {
        size_t i;
        for (i = 0; i < n && *str; i++) {
            dest[i] = *str++;
        }
        dest[i] = '\0';
        return i;
    }
    
    void breakString(const char *lineStr) {
        char a[7+1];
        char b[5+1];
        char c[5+1];
        char d[7+1];
        char e[6+1];
        size_t pos = 0;
    
        pos += getchunk(a, 7, lineStr + pos);
        pos += getchunk(b, 5, lineStr + pos);
        pos += getchunk(c, 5, lineStr + pos);
        pos += getchunk(d, 7, lineStr + pos);
        pos += getchunk(e, 6, lineStr + pos);
    
        if (e[0] != '\0') {
            /* string was long enough, fields correctly initialized */
            printf("a: %s\nb: %s\nc: %s\nd: %s\ne: %s\n", a, b, c, d, e);
        }
    }
    
    int main() {
        breakString("abcdefghijklmnopqrstuvwxyz0123456789");
        return 0;
    }
    

    【讨论】:

      【解决方案3】:

      1) sscanf()

      有了sscanf(),你可以做到

      sscanf(lineStr, "%7c%5c%5c%7c%6c", a, b, c, d, e);
      a[7]=b[5]=c[5]=d[7]=e[6]='\0';
      

      %c 可用于读取超过 1 个字节。 %7c 最多可读取 7 个字节。但是\0不会被自动添加。

      感谢chqrlie 提供此方法。

      或者只是

      sscanf(lineStr, "%7s%5s%5s%7s%6s", a, b, c, d, e);
      

      如果lineStr 中没有空格。

      或许

      sscanf(lineStr, "%7[^\n]%5[^\n]%5[^\n]%7[^\n]%6[^\n]", a, b, c, d, e);
      

      如果lineStr 没有\n 字符。

      格式字符串中的数字表示要复制的子字符串的宽度。

      这样,您不需要\0 手动终止字符串。 sscanf() 会处理的。


      2) strncpy()

      如果您必须使用 `strncpy(),那么您就在正确的轨道上。你可以这样做

      void breakString(const char *lineStr) {
          char a[8];
          char b[6];      
          char c[6];
          char d[8];
          char e[7];
      
          strncpy(a, lineStr, 7);
          a[7]='\0';
          lineStr+=7;
      
          strncpy(b, lineStr, 5);
          b[5]='\0';
          lineStr+=5;
      
          strncpy(c, lineStr, 5);
          c[5]='\0';
          lineStr+=5;
      
          strncpy(d, lineStr, 7);
          d[7]='\0';
          lineStr+=7;
      
          strncpy(e, lineStr, 6);
          e[6]='\0';
          //lineStr+=6;
      }
      

      请注意,需要一个额外的字节来存储字符串的\0 字符。所以数组的大小会相应改变。

      【讨论】:

      • sscanf 格式不正确。字符串可能包含会导致意外行为的空格。你应该使用sscanf(lineStr, "%7c%5c%5c%7c%6c", a, b, c, d, e);
      • 另外,使用strncpy 修复代码的最安全方法是删除所有此类调用。
      • @chqrlie 为什么在sscanf() 中建议%c? a 和 b 必须有字符串,对吧?另外,我试了一下,得到了看起来像垃圾的意外输出。
      • sscanf(s, "%7c", a); 将存储从s 到b 的最多7 个字节,它必须指向一个足够大的char 数组。一个很少使用的功能,我从未见过它的实际应用,尽管我自己实现了sscanf() 几次不同的时间!问题是 sscanf() 不会存储空终止符,因此目标数组必须已经初始化。 1 的默认宽度更常用,指向单个 char 的指针适合此。
      • scanf 就是这样一部作品,有很多怪癖和缺点,而且被广泛滥用。
      猜你喜欢
      • 2011-09-01
      • 1970-01-01
      • 2019-03-14
      • 2013-05-26
      • 1970-01-01
      • 2020-01-15
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多