洛谷评测环境关于非传统题目中文字符(串)处理、操作的说明

· · 算法·理论

阅读提示:本文所提到的中文字符均为 UTF-8 编码格式。

导语:

洛谷是一个致力于为编程爱好者提供清爽、快捷的编程体验,集在线测评、学习于一体,拥有强大社区功能的网络平台。在这里,你会遇到形形色色的编程习题,其中不乏有一些非传统题目需要我们对中文字符(串)进行 IO 或其他操作。为此,本文共分为 6 个章节,将详细介绍洛谷 Linux 环境下对中文字符(串)的一些常规处理,方便广大爱好者提高编程能力。

① 中文字符(串)的存储格式及大小

一般地,单个英文字符或数字占用的内存大小为 1 byte。但在洛谷的程序环境中,中文字符使用的是 UTF-8 编码,每个汉字及其标点占用 3 byte 的内存空间。这意味着其与英文字符(串)在处理上有诸多不同,不能将二者混为一谈。

例如下列程序:

char str[] = "Abc12洛谷。";
cout << sizeof(str) << endl;
// 输出 15

样例解释:对于 str 字符串,其中 Abc12 为普通英文字符和数字,每个字符占用 1 byte 的内存空间;而后的 洛谷。 为中文子串,每个字符占用 3 byte 的内存空间,再加上 char 类型字符串末尾的 \0 转义字符,共计 5×1+3×3+1=15 byte 的内存。

对于 string 类型字符串,其空间大小一般不用 sizeof() 函数直接求出,因为该值在编译时确定,与字符串实际内容无关,故不在此赘述。

② 含有中文字符的字符串长度

求一个字符串的长度,我们经常用到 size()lengh()strlen() 函数。

类似于字符的存储大小(即占用内存的多少),单个字符所占字符串的长度在数值上与其完全相等,即英文和数字字符占用 1 个单位长度的字符串,中文字符在计算所占字符串长度时则以 3 计。

例如下列程序:

string str = "Abc12洛谷。";   // 或 char str[] = "Abc12洛谷。";
cout << str.size();   // 或 cout << strlen(str);
// 输出14(5×1+3×3=14)

注意:含有中文字符的字符串长度并不等于字符数!(当然,在纯英文字符串中这个等式是成立的)。

③ 含有中文字符的字符串的 IO 操作

一般情况下,将字符串视为一个整体输出时,含有中文的字符串与普通的字符串没有区别。

例如:

string str; // 或 char str[20];
cin >> str; // 输入 "Abc12洛谷。"
cout << str; // 输出 "Abc12洛谷。"

在上述示例中,若使用 getline()gets() 等函数同样能达到目的。只有在以单个字符为单元输入输出时,两者的区别才会体现。
造成这种情况的主要原因是中/英文字符在字节数和所占字符串长度上的区别。

例如下列错误案例❎:

char s;
s = getchar();  // 或 cin >> s;
// 输入 "三"
putchar(s);  // 或 printf("%c",s);
// 输出不可见!

案例解释:对于 char 类型的变量,不管以何种方式进行输入,都只能从缓冲区读入并存储 1 byte 大小的字符,而单个中文字符无论是从内存大小(字节数)还是所占字符串长度分析,都不是一个 char 类型的变量所能装下的,因此案例中的输入输出必然会造成数据的丢失。有的同学可能会问,那这种读入方式究竟读到了什么呢?其实也并不是什么也没读到,只是读入了中文字符“”的一部分,也就是其中的 1 个 byte,剩下 2 个 byte 的内容依然滞留在缓冲区,在后续的操作中可能会引发程序不可预测的错误。至于为什么输出不可见,具体原因将在下一子目分析。因此我们呼吁,非必要时应尽量选择用字符串盛装中文字符,以免导致程序错误。

:::warning[警示] 对于大多数初学者,他们在对中文字符进行处理时很容易将单个汉字直接用字符单引号引起,如 char c='中';。但这种方式其实是错误的,会导致数据的丢失。这是因为在 C/C++ 中,单引号用于表示 char 类型的字符常量,而 char 类型通常只占用 1 个字节(8 位),能表示的范围有限(一般是 -128127)。而 UTF-8 编码的中文字符一般占 3 byte。所以,用单引号括中文字符时,会因字节数不匹配导致编译或运行错误。若要表示单个中文字符,需使用对应编码的宽字符类型或直接使用字符串盛装。 :::

例如读入一行字符串:

string str;
getline(cin, str); // 输入 "一二三四五12345"
printf("%s", s.c_str()); // 输出 "一二三四五12345"

抑或换一种输入方式:

string str;
char s;
while ((s=getchar())!='\n') // 输入 "一二三四五。。。"
{
    str+=s;
}
cout << str; // 输出 "一二三四五。。。"

当然,若是你实在不嫌麻烦,也可以使用多个字符变量分批多次读入的方式存储中文字符(串),只要不改变输入时字符变量的顺序,将其依次输出同样能满足要求。但若是改变顺序输出,则中文字符的编码将会改变,同样会引发不可预测的错误。

例如:

char a, b, c; // 或 char s[3];
cin >> a >> b >> c; // 或使用 getchar() 函数,又或者 cin >> s[0] >> s[1] >> s[2];
// 输入 "三"
cout << a << b << c; // 或使用 putchar() 函数,又或者 cout << s[0] << s[1] <<s[2];
// 输出 "三"
char a, b, c; // 或 char s[3];
cin >> a >> b >> c; // 或 cin >> s[0] >> s[1] >> s[2];
// 输入 "三"
cout << c << b << a; // 或 cout << s[2] << s[1] <<s[0];

// 输出不可见,引发错误,在 IDE 上甚至无法运行!

④ 有关中文字符的基本知识(中文 ASCII 码)

就像初学字符变量一样,本节将介绍有关中文字符的一些基础知识。

类比于西文字符,中文字符其实也有自己的 ASCII 码。当然,这个 ASCII 码并非针对单个中文字符本身,而是指组成中文字符的字符单元。这句话有点绕,什么意思呢?上文已经提到,每个中文字符占 3 byte 的空间,将其存储于字符串中,也同样占用 3 个单位长度的字符串,即 3 个字符单元。我们将字符存储于字符串中的最小访问单元称之为字符单元(也就是 1 个 byte 的内容),例如程序中 str[1],str[2] 这样可以直接用数组下标访问到的数据就是字符单元。所以,单个中文字符存储到字符串中后就产生了 3 个字符单元,其每个字符单元的 ASCII 码值都小于 0,这是其区别于英文字符的主要依据,且中文字符单元单个输出时均为不可见字符,这也正是子目③中字符输出不可见的原因。但值得注意的是,按照一定顺序将这些不可见字符组合输出后,又可以显示出原本的中文字符,是不是很神奇?

例如:

string str;
cin >> str;
// 输入 "洛"
cout << str[0];
// 输出不可见
cout << (int)str[0] <<" "<< (int)str[1] <<" "<< (int)str[2];
// 输出 -26 -76 -101
char a, b, c;
a = -26;
b = -76;
c = -101;
printf("%c%c%c", a, b, c);
// 输出 "洛"

中文字符的 ASCII 码常用来判定其是否为中文字符。

温馨提示:在操作含有中文字符的字符串时,要特别注意不要单独对单个中文字符的单个字符单元进行修改,3 个字符单元是一个整体,它们共同决定了一个中文字符,切忌随意改变其顺序,切忌单独修改、操作,否则将引发不可预测的错误!

⑤ 其它关于中文字符(串)的补充介绍

  1. 字符串库中自带的的一些字符串处理函数,在中文字符串这里同样适用,如查找子串,删除子串等。使用时只需注意将中文字符的 3 个字符单元整体操作即可。
  2. string 类型的 2 个中文字符串同样可以用关系运算符“==”、“”、“”来比较字典序。char 类型的 strcmp() 函数同理。

例如:

string s1 = "Abc12洛谷。";
string s2 = "Abc12洛谷,";
cout << (s1 == s2) << endl;
// 输出 0

⑥ 非传统题目友情推荐链接

以下是一些含有中文字符操作的非传统题目的链接,方便广大爱好者练练手。

  1. U546490 自幂数的判断
  2. U550858 浮点数的运算【升级版】
  3. U553360 中文输出
  4. U535472 完全数(完美数)【升级版】

全篇终,\color{#0aa}\cancel{\ \ \ \textrm{\textit{\textbf{{\Large\color{#0af}感 }}}}^\textrm{\textit{\textbf{{\small\color{#e99100}谢}}}}_\textrm{\textit{\textbf{\large\color{#778891}大}}}\textrm{\textit{\textbf{\LARGE\color{#9c1}家}}}\circlearrowleft\ \ }阅读!