字符数组与字符串


字符串就是一串文本,在 C++ 谈到字符串的话,一个意义是指字符组成的数组,最后加上一个空(null)字符'\0',例如底下是个"hello"字符串:

char text[] = {'h', 'e', 'l', 'l', 'o', '\0'};

之后可以直接使用text来代表"hello"文本,例如:

cout << text << endl; // 显示 hello

也可以使用""来包含文本,例如:

char text[] = "hello";

"hello"是字符串字面常量,在这个例子中,虽然没有指定空字符'\0',但是会自动加上空字符,来看看底下这个程序就可以得知:

#include <iostream> 
using namespace std; 

int main() { 
    char text[] = "hello"; 

    for(auto ch : text) { 
            if(ch == '\0') {
            cout << "null"; 
        } 
        else { 
            cout << ch; 
        }
    } 
    cout << endl; 

    return 0; 
}

text基本上还是字符数组,空字符用来识别字符数组单纯就只是字符数组,或者是表示字符串。

执行结果:

hellonull

这种字符串,其实是延续自 C 风格的字符串,因此更多细节可参考我写的 C 语言文件中〈字符串(字符数组)〉,C 标准函数库中有一些函数可以协助此类字符串的处理,像是〈字符串长度、复制、连接〉、〈字符串比较、搜寻〉与〈字符串转换、字符测试〉,只不过在 C++ 中,标头文件必须引用cstring而不是string.h

在 C++ 中并不鼓励使用 C 风格的字符串,不过在这边倒是想借此探讨一些字符细节。例如,有辨法指定'林'char变量吗?这会引发编译警讯:

// warning: multi-character character constant
// warning: overflow in conversion from 'int' to 'char' changes value
char t = '林';

文本「林」不会是一个字节就可以存储的数据,因此引发警讯,你需要使用以下的方式:

char text[] = "林";

若使用strlen(text)的话,会得到什么数字呢?若单纯使用g++编译,不加上任何实参的话,答案是看你的源码编码是什么,如果使用 Big5 编写源码的话,答案会是 2,如果使用 UTF-8 编写源码的话,答案会是 3。

记得在〈数据类型〉中谈过吗?char用来存储字符数据,但没有规定什么是字符数据,若单纯使用g++编译,不加上任何实参的话,源码中字符串怎么存储,执行时期text就怎么存储,当源码是 Big5 时,因为"林"会用上两个字节,strlen(text)会是 2,当源码是 UTF-8 时,"林"会用上三个字节,因此strlen(text)会是是 3。

现代程序设计鼓励使用 UTF-8,如果使用 UTF-8 编写源码,单纯使用g++编译,不加上任何实参的话,若是在 Windows 的文本模式执行程序,就会出现乱码,因为 Windows 的文本模式默认采用 Big5(MS950),为了可以看到正确的文本,编译时可以加上-fexec-charset=BIG5,执行时期字符串使用 Big5 编码,这时strlen(text)又会是 2 了。

这就要问到一个问题了,字符是什么呢?C++ 中的char又是什么呢?C++ 是个历史悠久的语言,早期用char存储的文本仅需单一字节,例如 ASCII 的文本,使用char代表字符是没问题,因为 ASCII 既定义了字符集,也定义了字符编码,在表示 ASCII 的文本时,char确实就代表字符,然而后来为了支持更多的文本,char就不再是代表字符了。

char是用来存储字符数据,至于存什么没有规定,对于char text[] = "林"的情况,应该将text中每个索引位置当成是码元(code unit),而不是字符了,因为必须以多个字节来存储「林」,因此这类字符在 C++ 被称为多字节字符(multibyte character),技术上来说,是用数个char组成的一个字符,如何组成就要看采用哪种编码了。

如果采用 Big5 编码,那"林"是个 Big5 字符,如果采用 UTF-8 编码,那"林"是个 Unicode 字符,现代程序设计鼓励用 UTF-8,若要固定使用 UTF-8 编码字符串,C++ 17 可以 UTF-8 编写源码,并在""前置u8,指定字符串使用 UTF-8 编码:

char text[] = u8"林";
cout << strlen(text) << endl; // 显示 3

若不使用 UTF-8 编码的源码,可以使用码指定:

char text[] = u8"\u6797";
cout << strlen(text) << endl; // 显示 3

如果处理中文本符串时,想知道有几个中文本怎么办?这要知道wchar_t类型,对应的字符常量是L'林'这样的写法称为扩充字符字面常量(wide character literal),wchar_t其实是个整数类型,用来存储码,就现今来说,基本上是指 Unicode。

例如,若以 UTF-8 编写源码,底下的程序会显示 Unicode 码号码:

wchar_t ch = L'林'; // 也可以写 L'\u6797'
cout << ch << endl; // 显示码十进制 26519 或十六进制 6797(视平台而定)

对于字符串,也可以使用wchar_t定义数组进行处理。例如:

#include <iostream> 
#include <cstring> 

using namespace std; 

int main() { 
    wchar_t text[] = L"良葛格"; 
    cout << wcslen(text);  // 显示 3

    return 0; 
}

L"良葛格"这种写法,称为扩充字符字符串(wide-chararater string),C 风格的字符串处理函数,都有对应wchar_t的版本,只要将函数名称的 str 前置改为 wcs 前置就可以了,wcs 就是 wide-chararater string 的缩写。

wchar_t并没有规定大小,只要求必须容纳系统中可以使用的字符,C++ 11 制定了char16_tchar32_t,这会让人误以为它们用来存储编码,其实它们依旧是存储码。

char16_t可存储的码,必须能涵盖 UTF-16 编码可表现的全部字符,使用的字符常量或字符串常量前要加上u,例如:

char16_t ch = u'林'; 
char16_t text[] = u"良葛格";

char32_t可存储的码,必须能涵盖 UTF-32 编码可表现的全部字符,使用的字符常量或字符串常量前要加上U,例如:

char32_t ch = U'林'; 
char32_t text[] = U"良葛格";

C++ 20 制定了char8_t,必须够大到能容纳 UTF-8 编码可表现的全部字符,使用的字符常量或字符串常量前要加上u8

至于char之间与wchar_tchar16_tchar32_t间要怎么转换呢?这问题基本上涉及 Unicode 码要转换至哪个编码,若是 Unicode 码与 UTF-8 的转换,可以参考底下的实现(修改自C++ UTF-8 codepoint conversion):

#include <iostream>

using namespace std;
string toUTF8(int cp);
int toCodePoint(const string &u);

int main(int argc, char *argv[]) {
    // 在 UTF-8 终端下会显示「林」
    cout << toUTF8(L'林') << endl;  
    cout << toUTF8(u'林') << endl;
    cout << toUTF8(U'林') << endl;

    string utf8 = u8"林";
    // 显示 26519
    cout << toCodePoint(utf8) << endl;

    return 0;
}

string toUTF8(int cp) {
    char ch[5] = {0x00};
    if(cp <= 0x7F) { 
        ch[0] = cp; 
    }
    else if(cp <= 0x7FF) { 
        ch[0] = (cp >> 6) + 192; 
        ch[1] = (cp & 63) + 128; 
    }
    else if(0xd800 <= cp && cp <= 0xdfff) {} // 无效区块
    else if(cp <= 0xFFFF) { 
        ch[0] = (cp >> 12) + 224; 
        ch[1]= ((cp >> 6) & 63) + 128; 
        ch[2]= (cp & 63) + 128; 
    }
    else if(cp <= 0x10FFFF) { 
        ch[0] = (cp >> 18) + 240; 
        ch[1] = ((cp >> 12) & 63) + 128; 
        ch[2] = ((cp >> 6) & 63) + 128; 
        ch[3]= (cp & 63) + 128; 
    }
    return string(ch);
}

int toCodePoint(const string &u) {
    int l = u.length();
    if(l < 1) {
        return -1; 
    }

    unsigned char u0 = u[0]; 
    if(u0 >=0 && u0 <= 127) {
        return u0;
    }

    if(l < 2) {
        return -1;
    } 

    unsigned char u1 = u[1]; 
    if (u0 >= 192 && u0 <= 223) {
        return (u0 - 192) * 64 + (u1 - 128);
    }

    if(u[0] == 0xed && (u[1] & 0xa0) == 0xa0) {
        return -1; //code points, 0xd800 to 0xdfff
    }

    if(l < 3) {
        return -1; 
    }

    unsigned char u2 = u[2]; 
    if(u0 >= 224 && u0 <= 239) {
        return (u0 - 224) * 4096 + (u1 - 128) * 64 + (u2 - 128);
    }

    if (l < 4) {
        return -1;
    }

    unsigned char u3 = u[3]; 
    if(u0>=240 && u0<=247) {
        return (u0 - 240) * 262144 + (u1 - 128) * 4096 + (u2 - 128) * 64 + (u3 - 128);
    }

    return -1;
}

string是 C++ 建议使用的字符串类型,也有一些现有的程序库,可以提供编码转换,这之后会介绍。

若字符串中包含\"等字符,会需要转义,例如:

char text[] = "c:\\workspace\\exercise";

C++ 11 后可以使用原始字符串常量R"(...)"的写法,在括号中的文本无需转义,也可以直接编写",例如:

char text1[] = R"(c:\workspace\exercise)";
char text2[] = R"(This is a "test")";

也可以进行换行:

#include <iostream> 
using namespace std; 

int main() { 
    char text[] = R"(Your left brain has nothing right.
    Your right brain has nothing left.)";
    cout << text << endl;
    return 0; 
}

在原始字符串中编写的内容都会保留,因此显示结果会是:

Your left brain has nothing right.
    Your right brain has nothing left.

UL等前置字,都可以与原始字符串结合,例如UR"(...)"LR"..."等,不过要留意的是,若结合原始字符串,\u1234这类写法,就会如实呈现,不会作为码表示法的转义。


展开阅读全文