字符数组与字符串


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

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

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

printf("%s\n", text);

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

char text[] = "hello";

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

#include <stdio.h>
#include <string.h>

int main(void) {
    char text[] = "hello";
    int length = sizeof(text) / sizeof(text[0]);

    for(int i = 0; i < length; i++) {
        if(text[i] == '\0') {
            puts("null");
        } else {
            printf("%c ", text[i]);
        }
    }
    printf("数组长度 %d\n", length);
    printf("字符串长度 %d", strlen(text));

    return 0;
}

执行结果:

h e l l o null
数组长度 6
字符串长度 5

字符串是字符数组,可以用数组访问方式取出每个字符,在指定"hello"时表面上虽然只有 5 个字符, 但是最后会加上一个空字符'\0',因此text就数组长度而言会是 6,不过就字符串长度而言会是 5,strlen可以获取字符串长度,定义在 string.h。

由使用者输入获取字符串值时,需注意不要超过字符串(字符数组)的长度;使用scanf从使用者输入获取字符串值,并存储至字符数组,只要这么作就可以了:

char buf[80];
printf("输入字符串:");
scanf("%s", buf);
printf("你输入的字符串为 %s\n", buf);

这个程序片段可以获取使用者的字符串输入,输入的字符串长度不得超过 80 个字符,80 个字符的上限包括空字符,因此实际上可以输入 79 个字符;如果输入的字符超出所定义的上限,会发生不可预期的结果,甚至成为安全弱点,如〈printf 与 scanf〉中最后谈到的,预防的方法之一是,限定scanf每次执行可以接受的最大字符数,或者是使用fgets

在使用scanf获取使用者输入的字符串时,格式指定字是使用%s,而变量前不用再加上&,因为实际上,字符串(字符数组)变量名称本身,即表示内存地址信息。

要指定新的字符串值给它时,不能像下面的方式指定:

char name[80]; 
name = "Justin";

而必须要一个字符一个字符的指定至数组中,并在最后加上空白字符,例如:

char name[80] = {'\0'}; 
name[0] = 'J'; 
name[1] = 'u'; 
name[2] = 's'; 
name[3] = 't'; 
name[4] = 'i';
name[5] = 'n';
name[6] = '\0'; 
puts(str);

这样的字符指定方式当然相当的不方便,所以 C 提供了字符串处理的相关函数,可以协助字符串处理,在之后的主题还会说明。

字符串的定义还有指针(Pointer)的定义方式,这个留待谈到指针时再来说明。

前面谈到的都是仅包含英文本母的字符串,那么包含中文的字符串呢?在谈到这个问题前,得先探讨一下,是否有辨法指定'林'char变量?这会引发编译警讯:

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

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

char text[] = "林";

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

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

现代程序设计鼓励使用 UTF-8,如果使用 UTF-8 编写源码,单纯使用gcc编译,不加上任何实参的话,若是在 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 编码字符串,C11 可以 UTF-8 编写源码,并在""前置u8,指定字符串使用 UTF-8 编码:

char text[] = u8"林";
printf("字符串长度 %d", strlen(text)); // 显示 3

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

char text[] = u8"\u6797";
printf("字符串长度 %d", strlen(text)); // 显示 3

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

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

wchar_t ch = L'林'; // 也可以写 L'\u6797'
printf("%d", ch);   // 显示 26519

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

#include <stdio.h>
#include <string.h>

int main(void) {
    wchar_t text[] = L"良葛格"; 
    printf("字符串长度 %d", wcslen(text)); // 显示 3

    return 0;
}

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

wchar_t并没有规定大小,只要求必须容纳系统中可以使用的字符,C11 在 uchar.h 中定义了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"良葛格";

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

#include <stdio.h>
#include <string.h>

void toUTF8(int cp, char* str);
int toCodePoint(char* str, int len);

int main(void) {
    char str1[5] = {0x00};

    toUTF8(L'林', str1);
    printf("%s\n", str1); // 在 UTF-8 终端下会显示「林」

    char str2[] = u8"林";                 // 「林」会使用三个字节
    printf("%d\n", toCodePoint(str2, 3)); // 显示 26519

    return 0;
}

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

int toCodePoint(char* u, int len) {
    if(len < 1) {
        return -1; 
    }

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

    if(len < 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(len < 3) {
        return -1; 
    }

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

    if (len < 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;
}




展开阅读全文