C 语言字符类型简介

目录汇总:C 语言零基础入门教程

目前还没有讨论的唯一基本类型是 char 类型,即字符类型(也称字符型)。char 类型的值可以根据计算机的不同而不同,因为不同的机器可能会有不同的字符集。

字符集

当今最常用的字符集是美国信息交换标准码(ASCII)字符集,它用 7 位代码表示 128 个字符。在 ASCII 码中,数字 0~9 用 0110000~0111001 码来表示,大写字母 A~Z 用 1000001~1011010 码来表示。ASCII 码常被扩展用于表示 256 个字符,相应的字符集 Latin-1 包含西欧语言和许多非洲语言中的字符。

char 类型的变量可以用任意单字符赋值:

1
2
3
4
5
6
char ch;

ch = 'a';     /* lower-case a */
ch = 'A';     /* upper-case A */
ch = '0';     /* zero         */
ch = ' ';     /* space        */

注意,字符常量需要用单引号括起来,而不是双引号。

一、字符操作

在 C 语言中字符的操作非常简单,因为存在这样一个事实:C 语言把字符当作小整数进行处理。毕竟所有字符都是以二进制的形式进行编码的,而且无须花费太多的想象力就可以将这些二进制代码看成整数。例如,在 ASCII 码中,字符的取值范围是 0000000~1111111,可以看成 0~127 的整数。字符 'a' 的值为 97,'A' 的值为 65,'0' 的值为 48,而 ' ' 的值为 32。在 C 语言中,字符和整数之间的关联是非常强的,字符常量事实上是 int 类型而不是 char 类型(这是一个非常有趣的现象,但对我们并无影响)。

当计算中出现字符时,C 语言只是使用它对应的整数值。思考下面这个例子,假设采用 ASCII 字符集:

1
2
3
4
5
6
7
char ch;
int i;

i = 'a';        /* i is now 97    */
ch = 65;        /* ch is now  'A' */
ch = ch + 1;    /* ch is now  'B' */
ch++;           /* ch is now  'C' */

可以像比较数那样对字符进行比较。下面的 if 语句测试 ch 中是否含有小写字母,如果有,那么它会把 ch 转换为相应的大写字母。

1
2
if ('a' <= ch && ch <= 'z')
  ch = ch - 'a' + 'A';

'a'<= ch 这样的比较使用的是字符所对应的整数值,这些数值因使用的字符集而有所不同,所以程序使用 <<=>>= 来进行字符比较可能不易移植。

字符拥有和数相同的属性,这一事实会带来一些好处。例如,可以让 for 语句中的控制变量遍历所有的大写字母:

1
for (ch = 'A'; ch <= 'Z'; ch++)...

另外,以数的方式处理字符可能会导致编译器无法检查出来的多种编程错误,还可能会导致我们编写出 'a' * 'b' / 'c' 这类无意义的表达式。此外,这样做也可能会妨碍程序的可移植性,因为程序可能基于一些对字符集的假设。(例如,上述 for 循环假设字母 A~Z 的代码都是连续的。)

二、有符号字符和无符号字符

既然 C 语言允许把字符作为整数来使用,那么 char 类型应该像整数类型一样存在符号型和无符号型两种。通常有符号字符的取值范围是 -128~127,无符号字符的取值范围是 0~255。

C 语言标准没有说明普通 char 类型数据是有符号型还是无符号型,有些编译器把它们当作有符号型来处理,有些编译器则将它们当作无符号型来处理。(甚至还有一些编译器,允许程序员通过编译器选项来选择把 char 类型当成有符号型还是无符号型。)

大多数时候,人们并不真的关心 char 类型是有符号型还是无符号型。但是,我们偶尔确实需要注意,特别是当使用字符型变量存储一个小值整数的时候。基于上述原因, 标准 C 允许使用单词 signedunsigned 来修饰 char 类型:

1
2
signed char sch;
unsigned char uch;

可移植性技巧 不要假设 char 类型默认为 signedunsigned。如果有区别,用 signed charunsigned char 代替 char

由于字符和整数之间的密切关系,C89 采用术语整值类型(integral type)来统称整数类型和字符类型。枚举类型也属于整值类型。

C99 不使用术语“整值类型”,而是扩展了整数类型的含义,使其包含字符类型和枚举类型。C99 中的 _Bool 型(C 语言 if 语句简介)是无符号整数类型。

三、算术类型

整数类型和浮点类型统称为算术类型。下面对 C89 中的算术类型进行了总结分类。

  • 整值类型:

    • 字符数型(char);
    • 有符号整型(signed charshort intintlong int);
    • 无符号整型(unsigned charunsigned short intunsigned intunsigned long int);
    • 枚举类型。
  • 浮点类型(floatdoublelong double)。

C99 的算术类型具有更复杂的层次。

  • 整数类型:

    • 字符类型(char);
    • 有符号整型,包括标准的(signed charshort intintlong intlong long int)和扩展的;
    • 无符号整型,包括标准的(unsigned charunsigned short intunsigned intunsigned long intunsigned long long int_Bool)和扩展的;
    • 枚举类型。
  • 浮点类型:

    • 实数浮点类型(floatdoublelong double);
    • 复数类型(float_Complexdouble_Complexlong double_Complex)。

四、转义序列

正如在前面示例中见到的那样,字符常量通常是用单引号括起来的单个字符。然而,一些特殊符号(比如换行符)是无法采用上述方式书写的,因为它们不可见(非打印字符),或者无法从键盘输入。因此,为了使程序可以处理字符集中的每一个字符,C 语言提供了一种特殊的表示法——转义序列(escape sequence)。

转义序列共有两种:字符转义序列(character escape)和数字转义序列(numeric escape)。在 C 语言使用 printf 函数格式化输出 已经见过了一部分字符转义序列,表 5 给出了完整的字符转义序列集合。

表 5  字符转义序列

名称 转义序列
警报(响铃)符 \a
回退符 \b
换页符 \f
换行符 \n
回车符 \r
水平制表符 \t
垂直制表符 \v
反斜杠 \\
问号 \?
单引号 \'
双引号 \"

转义序列 \a\b\f\r\t\v 表示常用的 ASCII 控制字符, 转义序列 \n 表示 ASCII 码的回行符,转义序列 \\ 允许字符常量或字符串包含字符 \,转义序列 \' 允许字符常量包含字符 ',而转义序列 \" 则允许字符串包含字符 ", 转义序列 \? 很少使用。

字符转义序列使用起来很容易,但是它们有一个问题:转义序列列表没有包含所有无法打印的 ASCII 字符,只包含了最常用的字符。字符转义序列也无法用于表示基本的 128 个 ASCII 字符以外的字符。数字转义序列可以表示任何字符,所以它可以解决上述问题。

为了把特殊字符书写成数字转义序列,首先需要查找字符的八进制或十六进制值。例如,ASCII 码中的 ESC 字符(十进制值为 27)对应的八进制值为 33,对应的十六进制值为 1B。上述八进制或十六进制码可以用来书写转义序列。

  • 八进制转义序列由字符 \ 和跟随其后的一个最多含有三位数字的八进制数组成。(此数必须表示为无符号字符,所以最大值通常是八进制的 377。)例如,可以将转义字符写成 \33\033。跟八进制常量不同,转义序列中的八进制数不一定要用 0 开头。
  • 十六进制转义序列由 \x 和跟随其后的一个十六进制数组成。虽然标准 C 对十六进制数的位数没有限制,但其必须表示成无符号字符(因此,如果字符长度是 8 位,那么十六进制数的值不能超过 FF)。若采用这种表示法,可以把转义字符写成 \x1b\x1B 的形式。字符 x 必须小写,但是十六进制的数字(例如 b)不限大小写。

作为字符常量使用时,转义序列必须用一对单引号括起来。例如,表示转义字符的常量可以写成 '\33'(或 '\x1b')的形式。转义序列可能有点隐晦,所以采用 #define 的方式给它们命名通常是个不错的主意:

1
#define ESC '\33'    /* ASCII escape character */

正如 C 语言使用 printf 函数格式化输出 中看到的那样,转义序列也可以嵌在字符串中使用。

转义序列不是唯一一种用于表示字符的特殊表示法。三联序列提供了一种表示字符 #[\]^{|}~ 的方法,这些字符在一些语言的键盘上是打不出来的。C99 增加了通用字符名。通用字符名跟转义序列相似,不同之处在于通用字符名可以用在标识符中。

五、字符处理函数

本文前面已经讲过如何使用 if 语句把小写字母转换成大写字母:

1
2
if ('a' <= ch && ch <= 'z')
  ch = ch  'a' + 'A';

但这不是最好的方法。一种更快捷且更易于移植的转换方法是调用 C 语言的 toupper 库函数:

1
ch = toupper(ch);     /* converts ch to upper case */

toupper 函数在被调用时检测参数(本例中为 ch)是否为小写字母。如果是,它会把参数转换成相应的大写字母;否则,toupper 函数会返回参数的值。上面的例子采用赋值运算符把 toupper 函数返回的值存储在变量 ch 中。当然也可以同样简单地进行其他的处理,比如存储到另一个变量中,或用 if 语句进行测试:

1
if (toupper(ch) == 'A') ...

调用 toupper 函数的程序需要在顶部放置下面这条 #include 指令:

1
#include <ctype.h>

toupper 函数不是在 C 函数库中唯一实用的字符处理函数。

六、用 scanfprintf 读/写字符

转换说明 %c 允许 scanf 函数和 printf 函数对单个字符进行读/写操作:

1
2
3
4
char ch;

scanf("%c", &ch);    /* reads a single character */
printf("%c", ch);    /* writes a single character */

在读入字符前,scanf 函数不会跳过空白字符。如果下一个未读字符是空格,那么在前面的例子中,scanf 函数返回后变量 ch 将包含一个空格。为了强制 scanf 函数在读入字符前跳过空白字符,需要在格式串中的转换说明 %c 前面加上一个空格:

1
scanf(" %c", &ch);   /* skips white space, then reads ch */

回顾 C 语言使用 scanf 函数格式化输入 的内容,scanf 格式串中的空白意味着“跳过零个或多个空白字符”。

因为通常情况下 scanf 函数不会跳过空白,所以它很容易检查到输入行的结尾:检查刚读入的字符是否为换行符。例如,下面的循环将读入并且忽略掉当前输入行中剩下的所有字符:

1
2
3
do {
  scanf("%c", &ch);
} while  (ch != '\n');

下次调用 scanf 函数时,将读入下一输入行中的第一个字符。

七、用 getcharputchar 读/写字符

C 语言还提供了另外一些读/写单个字符的方法。特别是, 可以使用 getchar 函数和 putchar 函数来取代 scanf 函数和 printf 函数。putchar 函数用于写单个字符:

1
putchar(ch);

每次调用 getchar 函数时,它会读入一个字符并将其返回。为了保存这个字符,必须使用赋值操作将其存储到变量中:

1
ch = getchar();  /* reads a character and stores it in ch */

事实上,getchar 函数返回的是一个 int 类型的值而不是 char 类型的值。因此,如果一个变量用于存储 getchar 函数读取的字符,其类型设置为 int 而不是 char 也没啥好奇怪的。和 scanf 函数一样,getchar 函数也不会在读取时跳过空白字符。

执行程序时,使用 getchar 函数和 putchar 函数(胜于 scanf 函数和 printf 函数)可以节约时间。getchar 函数和 putchar 函数执行速度快有两个原因。第一个原因是,这两个函数比 scanf 函数和 printf 函数简单得多,因为 scanf 函数和 printf 函数是设计用来按不同的格式读/写多种不同类型数据的。第二个原因是,为了额外的速度提升,通常 getchar 函数和 putchar 函数是作为宏来实现的。

getchar 函数还有一个优于 scanf 函数的地方:因为返回的是读入的字符,所以 getchar 函数可以应用在多种不同的 C 语言惯用法中,包括搜索字符或跳过所有出现的同一字符的循环。思考下面这个 scanf 函数循环,前面我们曾用它来跳过输入行的剩余部分:

1
2
3
do {
  scanf("%c", &ch);
} while  (ch != '\n');

getchar 函数重写上述循环,得到下面的代码:

1
2
3
do {
  ch = getchar();
} while  (ch != '\n');

getchar 函数调用移到控制表达式中,可以精简循环:

1
2
while ((ch = getchar())  != '\n')
  ;

这个循环读入一个字符,把它存储在变量 ch 中,然后测试变量 ch 是否不是换行符。如果测试结果为真,那么执行循环体(循环体实际为空),接着再次测试循环条件,从而读入新的字符。实际上我们并不需要变量 ch,可以把 getchar 函数的返回值与换行符进行比较:

1
2
3
4
[惯用法]

while (getchar() != '\n')  /* skips rest of line */
  ;

这个循环是非常著名的 C 语言惯用法,虽然这种用法的含义是十分隐晦的,但是值得学习。

getchar 函数对搜索字符的循环和跳过字符的循环都很有用。思考下面这个利用 getchar 函数跳过不定数量空格字符的语句:

1
2
3
4
[惯用法]

while ((ch = getchar()) == ' ')  /* skips blanks */
  ;

当循环终止时,变量 ch 将包含 getchar 函数遇到的第一个非空白字符。

注意

如果在同一个程序中混合使用 getchar 函数和 scanf 函数,请一定要注意。scanf 函数倾向于遗留下它“扫视”过但未读取的字符(包括换行符)。思考一下,如果试图先读入数再读入字符的话,下面的程序片段会发生什么:

1
2
3
4
printf("Enter an integer: ");
scanf("%d", &i);
printf("Enter a command: ");
command = getchar();

在读入 i 的同时,scanf 函数调用将留下没有消耗掉的任意字符,包括(但不限于)换行符。getchar 函数随后将取回第一个剩余字符,但这不是我们所希望的结果。

程序 确定消息的长度

为了说明字符的读取方式,下面编写一个程序来计算消息的长度。在用户输入消息后,程序显示长度:

Enter a message: Brevity is the soul of wit.
Your message was 27 character(s) long.

消息的长度包括空格和标点符号,但是不包含消息结尾的换行符。

程序需要采用循环结构来实现读入字符和计数器自增操作,循环在遇到换行符时立刻终止。我们既可以采用 scanf 函数也可以采用 getchar 函数读取字符,但大多数 C 程序员更愿意采用 getchar 函数。采用简明的 while 循环书写的程序如下:

length.c

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
/* Determines the length of a message */

#include <stdio.h>

int main(void)
{
 char ch;
 int len = 0;

 printf("Enter a message: ");
 ch = getchar();
 while (ch != '\n') {
   len++;
   ch = getchar();
 }
 printf("Your message was %d character(s) long.\n", len);

 return 0;
}

回顾有关 while 循环和 getchar 函数惯用法的讨论,我们发现程序可以缩短成如下形式:

length2.c

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
/* Determines the length of a message */

#include <stdio.h>

int main(void)
{
 int len = 0;

 printf("Enter a message: ");
 while (getchar() != '\n')
   len++;
 printf("Your message was %d character(s) long.\n", len);

 return 0;
}

请参阅

(完)

comments powered by Disqus