提问人:Thomas Hedden 提问时间:1/9/2017 最后编辑:Right legThomas Hedden 更新时间:1/9/2017 访问量:1062
逐个字符访问外语字符串
Accessing foreign-language string character by character
问:
我知道这个问题可能非常初级。如果这是显而易见的,请原谅我。 请考虑以下程序:
#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"..."
答:
我认为 、 、 和 函数 from 是部分相关的。例如,您可以使用 找出字符串中每个字符由多少个字节组成。mblen()
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
我从中得到的输出是:
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
Э т о с т р о к о в а я к о н с т а н т а .
再加一点努力,代码就可以将 UTF-8 数据的字节对更紧密地打印在一起。D0 和 D1 前导字节对于西里尔文代码块 U+0400 的 UTF-8 编码是正确的。BMP(基本多语言平面)中的 U+04FF。
只是为了您的娱乐价值:BSD 拒绝处理输出,因为这些问号代表无效代码:.sed
sed: RE error: illegal byte sequence
我们正确地建议您编写自己的 UTF-8 解析器,这实际上很容易做到。下面是一个示例实现:
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
评论
这是使用该函数的简单解决方案。C99 要求 and(和朋友)都理解 and 字符代码的大小限定符,导致它们在多字节(即 UTF-8)表示和宽字符串/字符(即 ,这是一个足够大的整数类型,可以包含代码点)之间进行转换。这意味着您可以使用它一次将一个字符串分开一个(多字节)字符,而不必担心序列是否只是七位字符(英语)。从本质上讲,它只是向格式字符串添加一个限定符。sscanf
printf
scanf
l
%s
%c
wchar_t
l
这确实使用 ,在某些平台(Windows、咳嗽、咳嗽)上可能限制为 16 位。我怀疑如果你在 Windows 上使用星体平面字符,你最终会得到代理字符,这可能会让你感到悲伤,但代码在 Linux 和 Mac 上都能正常工作,至少在不太古老的版本中是这样。wchar_t
请注意程序开头的调用。这对于任何宽字符函数的工作都是必需的;它将执行区域设置设置为默认系统区域设置,该区域设置通常是多字节字符为 UTF-8 的区域设置。(但是,下面的代码并不在乎。它只要求函数的输入采用当前区域设置指定的多字节表示形式。setlocale
它可能不是解决这个问题的最快解决方案,但它的优点是编写起来要简单得多,至少在我看来是这样。
以下内容基于原始代码,但为了简单起见,我将输出重构为单个函数。我还将数字输出更改为十六进制(因为它更容易使用代码图表进行验证)。
#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
评论
print3()
loc
wchar_t
评论
char