按字符

时间:2017-01-09 03:27:27

标签: c string unicode-string

我知道这个问题可能非常基本。如果这是显而易见的事,请原谅我。 请考虑以下程序:

#include <stdio.h>

int main(void) {
   // this is a string in English
   char * str_1 = "This is a string.";
   // this is a string in Russian
   char * str_2 = "Это строковая константа.";
   // iterator
   int i;
   // print English string as a string
   printf("%s\n", str_1);
   // print English string byte by byte
   for(i = 0; str_1[i] != '\0'; i++) {
      printf(" %c  ",(char) str_1[i]);
   }
   printf("\n");
   // print numerical values of English string byte by byte
   for(i = 0; str_1[i] != '\0'; i++) {
      printf("%03d ",(int) str_1[i]);
   }
   printf("\n");
   // print Russian string as a string
   printf("%s\n", str_2);
   // print Russian string byte by byte
   for(i = 0; str_2[i] != '\0'; i++) {
      printf(" %c  ",(char) str_2[i]);
   }
   printf("\n");
   // print numerical values of Russian string byte by byte
   for(i = 0; str_2[i] != '\0'; i++) {
      printf("%03d ",(int) str_2[i]);
   }
   printf("\n");
   return(0);
}

输出:

This is a string.
 T   h   i   s       i   s       a       s   t   r   i   n   g   .
084 104 105 115 032 105 115 032 097 032 115 116 114 105 110 103 046
Это строковая константа.
 ▒   ▒   ▒   ▒   ▒   ▒       ▒   ▒   ▒   ▒   ▒   ▒   ▒   ▒   ▒   ▒   ▒   ▒   ▒   ▒   ▒   ▒   ▒   ▒       ▒   ▒   ▒   ▒   ▒   ▒   ▒   ▒   ▒   ▒   ▒   ▒   ▒   ▒   ▒   ▒   ▒   ▒   .
-48 -83 -47 -126 -48 -66 032 -47 -127 -47 -126 -47 -128 -48 -66 -48 -70 -48 -66 -48 -78 -48 -80 -47 -113 032 -48 -70 -48 -66 -48 -67 -47 -127 -47 -126 -48 -80 -48 -67 -47 -126 -48 -80 046

可以看出,英文(ASCII)字符串可以打印为字符串,也可以使用数组索引进行访问,逐字符打印(逐字节),但俄语字符串(我相信编码为UTF-8)可以作为字符串打印但不能逐个字符地访问。

据我所知,原因是在这种情况下俄语字符使用两个字节而不是一个字节进行编码。

我想知道的是,是否有任何简单的方法可以通过正确声明数据类型或通过标记字符串以某种方式使用标准C库函数逐字符打印Unicode字符串(在本例中为两个字节乘两个字节)或者通过设置区域设置或以其他方式。

我尝试用“u8”在俄语字符串前面,即char * str_2 = u8"...",但这不会改变行为。我想远离使用宽字符来假设使用什么语言,例如每个字符恰好两个字节。任何建议将不胜感激。

3 个答案:

答案 0 :(得分:3)

我认为来自mblen()的{​​{1}},mbtowc()wctomb()mbstowcs()wcstombs()函数具有部分相关性。例如,您可以通过<stdlib.h>找出字符串中每个字符组成的字节数。

另一个很少使用的标题和函数是mblen()<locale.h>

以下是对您的代码的修改:

setlocale()

#include <assert.h> #include <locale.h> #include <stdio.h> #include <stdlib.h> #include <string.h> static inline void ntbs_hex_dump(const char *pc_ntbs) { unsigned char *ntbs = (unsigned char *)pc_ntbs; for (int i = 0; ntbs[i] != '\0'; i++) printf(" %.2X ", ntbs[i]); putchar('\n'); } static inline void ntbs_chr_dump(const char *pc_ntbs) { unsigned char *ntbs = (unsigned char *)pc_ntbs; for (int i = 0; ntbs[i] != '\0'; i++) printf(" %c ", ntbs[i]); putchar('\n'); } int main(void) { char *loc = setlocale(LC_ALL, ""); printf("Locale: %s\n", loc); char *str_1 = "This is a string."; char *str_2 = "Это строковая константа."; printf("English:\n"); printf("%s\n", str_1); ntbs_chr_dump(str_1); ntbs_hex_dump(str_1); printf("Russian:\n"); printf("%s\n", str_2); ntbs_chr_dump(str_2); ntbs_hex_dump(str_2); char *mbp = str_2; while (*mbp != '\0') { enum { MBS_LEN = 10 }; int mbl = mblen(mbp, strlen(mbp)); char mbs[MBS_LEN]; assert(mbl < MBS_LEN - 1 && mbl > 0); // printf("mbl = %d\n", mbl); memmove(mbs, mbp, mbl); mbs[mbl] = '\0'; printf(" %s ", mbs); mbp += mbl; } putchar('\n'); return(0); } 很重要,至少在macOS Sierra 10.12.2(GCC 6.3.0)上,这是我开发和测试它的地方。没有它,setlocale()总是返回mblen(),代码中没有任何好处。

我得到的输出是:

1

通过更多的努力,代码可以更紧密地打印UTF-8数据的字节对。 D0和D1前导字节对于BMP(基本多语言平面)中西里尔字母代码块U + 0400 ... U + 04FF的UTF-8编码是正确的。

仅为了您的娱乐价值:BSD Locale: en_US.UTF-8 English: This is a string. T h i s i s a s t r i n g . 54 68 69 73 20 69 73 20 61 20 73 74 72 69 6E 67 2E Russian: Это строковая константа. ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? . D0 AD D1 82 D0 BE 20 D1 81 D1 82 D1 80 D0 BE D0 BA D0 BE D0 B2 D0 B0 D1 8F 20 D0 BA D0 BE D0 BD D1 81 D1 82 D0 B0 D0 BD D1 82 D0 B0 2E Э т о с т р о к о в а я к о н с т а н т а . 拒绝处理输出,因为这些问号代表无效代码:sed

答案 1 :(得分:2)

这是使用sscanf功能的简单解决方案。 C99要求printfscanf(和朋友)理解l%s字符代码的%c大小限定符,导致它们在多字节之间进行转换(即UTF-8)表示和宽字符串/字符(即wchar_t,这是一个足以包含代码点的整数类型)。这意味着您可以使用它一次将一个字符串分开一个(多字节)字符,而不必担心序列是否只是七位字符(英语)。 (如果这看起来很复杂,请查看代码。基本上,它只是在格式字符串中添加l限定符。)

这确实使用wchar_t,在某些平台上可能限制为16位(Windows,咳嗽,咳嗽)。我怀疑如果你在Windows上使用星界字符,你最终会有代理字符,这很可能会让你感到悲伤,但代码在Linux和Mac上运行良好,至少在不太古老的版本中

请注意在程序开头调用setlocale。任何宽字符函数的工作都是必要的;它将执行区域设置设置为默认系统区域设置,通常是多字节字符为UTF-8的区域设置。 (但是,下面的代码并不是非常关心。它只需要函数的输入是当前语言环境指定的多字节表示。)

它可能不是最快解决这个问题的方法,但它的优势在于编写起来相当简单,至少在我看来是这样。

以下内容基于原始代码,但为简单起见,我将输出重构为单个函数。我还将数字输出更改为十六进制(因为它更容易使用代码图表进行验证)。

#include <locale.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <wchar.h>

/* Print the string three ways */
void print3(const char* s);

void print3(const char* s) {
   wchar_t wch;
   int n;
   // print as a string
   printf("%s\n", s);
   // print char by char
   for (int i = 0; s[i] != '\0'; i += n) {
      sscanf(s+i, "%lc%n", &wch, &n);
      printf("   %lc   ", wch);
   }
   putchar('\n');
   // print numerical values char by char 
   for (int i = 0; s[i] != '\0'; i += n) {
      sscanf(s+i, "%lc%n", &wch, &n);
      printf(" %05lx ", (unsigned long)wch);
   }
   putchar('\n');
}

int main(void) {
   setlocale(LC_ALL, "");

   char *str_1 = "This is a string.";
   char *str_2 = "Это строковая константа.";
   char *str_3 = u8"\U0001d7d8\U0001d7d9\U0001f638 in the astral plane";

   print3(str_1);
   print3(str_2);
   print3(str_3);
   return 0;
}

以上尝试模仿OP中的代码。我实际上更喜欢使用指针而不是索引来编写循环,并检查sscanf的返回码作为终止条件:

/* Print the string three ways */
void print3(const char* s) {
   wchar_t wch;
   int n;
   // print as a string
   printf("%s\n", s);
   // print char by char
   for (const char* p = s;
        sscanf(p, "%lc%n", &wch, &n) > 0;
        p += n) {
           printf("   %lc   ", wch);
   }
   putchar('\n');
   for (const char* p = s;
        sscanf(p, "%lc%n", &wch, &n) > 0;
        p += n) {
           printf(" %5.4lx ", (unsigned long)wch);
   }
   putchar('\n');
}

更好的做法是确保sscanf没有返回错误,表明存在无效的多字节序列。

这是我系统上的输出:

This is a string.
   T      h      i      s             i      s             a             s      t      r      i      n      g      .   
  0054   0068   0069   0073   0020   0069   0073   0020   0061   0020   0073   0074   0072   0069   006e   0067   002e 
Это строковая константа.
   Э      т      о             с      т      р      о      к      о      в      а      я             к      о      н      с      т      а      н      т      а      .   
  042d   0442   043e   0020   0441   0442   0440   043e   043a   043e   0432   0430   044f   0020   043a   043e   043d   0441   0442   0430   043d   0442   0430   002e 
 in the astral plane
                            i      n             t      h      e             a      s      t      r      a      l             p      l      a      n      e   
 1d7d8  1d7d9  1f638   0020   0069   006e   0020   0074   0068   0065   0020   0061   0073   0074   0072   0061   006c   0020   0070   006c   0061   006e   0065

答案 2 :(得分:1)

您被正确建议编写自己的UTF-8解析器,实际上是quite easy to do。这是一个示例实现:

int utf8decode(unsigned char *utf8, unsigned *code) {
  while(*utf8) { /* Scan the whole string */
    if ((utf8[0] & 128) == 0) { /* Handle single-byte characters */
      *code = utf8[0];
      utf8++;
    } else { /* Looks like it's a 2-byte character; is it? */
      if ((utf8[0] >> 5) != 6 || (utf8[1] >> 6) != 2)
        return 1;
      /* Yes, it is; do bit magic */
      *code = ((utf8[0] & 31) << 6) + (utf8[1] & 63);
      utf8 += 2;
    }
    code++;
  }
  *code = 0;
  return 0; /* We got it! */
}

让我们做一些测试:

int main(void) {
  int i = 0;
  unsigned char *str = "Это строковая константа.";
  unsigned codes[1024]; /* Hope it's long enough */ 

  if (utf8decode(str, codes) == 1) /* Decode */
    return 1;
  while(codes[i]) /* Print the result */
    printf("%u ", codes[i++]); 

  puts(""); /* Final newline */
  return 0;
}
  

1069 1090 1086 32 1089 1090 1088 1086 1082 1086 1074 1072 1103 32 1082 1086 1085 1089 1090 1072 1085 1090 1072 46