- 一、前言
- 二、C/C++字符串
- 1.字符
- 字符的表示
- 字符的输出
- 字符与整数
- 2.字符和字符串编码
- 源文件使用什么编码
- 窄字符串使用什么编码
- 总结
- 3.ASCII编码了解
- 对控制字符的解释
- 4.C语言转义字符
- 5.在C语言中使用中文字符
- 中文字符的存储
- 1) 足够长的数据类型
- 2) 选择包含中文的字符集
- 宽字符的输出
- 宽字符串
- 6.C语言字符数组和字符串
- 字符串结束标志(划重点)
- 字符串长度
- C语言字符串的输入和输出
- 字符串的输出
- 字符串的输入
- 其实 scanf() 也可以读取带空格的字符串
- 7.C语言数组是静态的,不能插入或删除元素
- 8.字符数组和字符串的初始化和赋值
- 1.C语言字符串指针(指向字符串的指针)
- 到底使用字符数组还是字符串常量
- 2.初始化
- 逐个字符初始化
- 字符串常量来初始化字符数组
- 使用字符指针
- 3.赋值
- 当为已经完成定义的字符数组赋值时,不能采用类似于初始化的方式为字符数组赋值了。如下语句是错误的:
- 逐个字符赋值
- 拷贝赋值
- 三、C++ string类(C++字符串)完全攻略
- 1. 构造函数
- 2. 对 string 对象赋值
- 3. 求字符串的长度
- 4. string对象中字符串的连接
- 5. string对象的比较
- 6. 求 string 对象的子串
- 7. 交换两个string对象的内容
- 8. 查找子串和字符
- 9. 替换子串
- 10. 删除子串
- 11. 插入字符串
- 12. 将 string 对象作为流处理
- 13. 用 STL 算法操作 string 对象
- 四、字符数组、字符指针和string对象之间的转换
- 字符数组转化成string类型
- 将string类型转换为字符数组
- 实验 : 字符串、数组、指针三者转换
- 五、字符操作函数,内存操作函数详解
最近在学习Redis和看点Redis源码,而Redis是用C语言写的,其中会用到很多字符串相关的知识。而且就C语言来说,关于字符串的内容其实一直都没咋了解(不太用得到),现在需要要用到了,因此写一篇关于字符串的博客来了解一下。
之前简单写过一些关于字符串的博客:
C语言字符 串注意事项
字符串,str函数,mem函数,文件操作大全(全网最全)
C语言和C++语言的字符串会有点区别,C语言支持的字符串类型C++全部都支持,C++在C的基础上新增了string对象。
因此在学习上,我们可以先学C语言支持的字符串,再学一下C++新增的string对象即可。
二、C/C++字符串 1.字符字符串,它是多个字符的集合。在了解字符串之前,我们可以先了解一下字符。
字符的表示我们可以用 char 类型来专门表示一个字符,例如:
//正确的写法 char a = '1'; char b = '$'; char c = 'X'; char d = ' '; // 空格也是一个字符 //错误的写法 char x = '中'; //char 类型不能包含 ASCII 编码之外的字符 char y = 'A'; //A 是一个全角字符 char z = "t"; //字符类型应该由单引号包围
char 称为字符类型,只能用单引号' '来包围,不能用双引号" "包围。而字符串只能用双引号" "包围,不能用单引号' '包围。
字符的输出输出 char 类型的字符有两种方法,分别是:
- 使用专门的字符输出函数 putchar;
- 使用通用的格式化输出函数 printf,char 对应的格式控制符是%c。
请看下面的演示:
#includeint main() { char a = '1'; char b = '$'; char c = 'X'; char d = ' '; //使用 putchar 输出 putchar(a); putchar(d); putchar(b); putchar(d); putchar(c); putchar('n'); //使用 printf 输出 printf("%c %c %cn", a, b, c); return 0; }
运行结果:
1 $ X
1 $ X
putchar 函数每次只能输出一个字符,输出多个字符需要调用多次。
字符与整数我们知道,计算机在存储字符时并不是真的要存储字符实体,而是存储该字符在字符集中的编号(也可以叫编码值)。对于 char 类型来说,它实际上存储的就是字符的 ASCII 码。
无论在哪个字符集中,字符编号都是一个整数;从这个角度考虑,字符类型和整数类型本质上没有什么区别。
我们可以给字符类型赋值一个整数,或者以整数的形式输出字符类型。反过来,也可以给整数类型赋值一个字符,或者以字符的形式输出整数类型。
请看下面的例子:
#includeint main() { char a = 'E'; char b = 70; int c = 71; int d = 'H'; printf("a: %c, %dn", a, a); printf("b: %c, %dn", b, b); printf("c: %c, %dn", c, c); printf("d: %c, %dn", d, d); return 0; }
输出结果:
a: E, 69
b: F, 70
c: G, 71
d: H, 72
在 ASCII 码表中,字符 ‘E’、‘F’、‘G’、‘H’ 对应的编号分别是 69、70、71、72。
a、b、c、d 实际上存储的都是整数:
- 当给 a、d 赋值一个字符时,字符会先转换成 ASCII 码再存储;
- 当给 b、c 赋值一个整数时,不需要任何转换,直接存储就可以;
- 当以 %c 输出 a、b、c、d 时,会根据 ASCII 码表将整数转换成对应的字符;
- 当以 %d 输出 a、b、c、d 时,不需要任何转换,直接输出就可以。
可以说,是 ASCII 码表将英文字符和整数关联了起来。
2.字符和字符串编码关于字符和字符串的编码,不可混为一谈,必须要区分开来谈论。
C语言是 70 年代的产物,那个时候只有 ASCII,各个国家的字符编码都还未成熟,所以C语言不可能从底层支持 GB2312、GBK、Big5、Shift-JIS 等国家编码,也不可能支持 Unicode 字符集。
稍微有点C语言基本功的读者可能认为C语言使用 ASCII 编码,字符在存储时会转换成对应的 ASCII 码值,这也是错误的,你被大学老师和教材误导了!在C语言中,只有 char 类型的窄字符才使用 ASCII 编码,char 类型的窄字符串、wchar_t 类型的宽字符和宽字符串都不使用 ASCII 编码!
wchar_t 类型的宽字符和宽字符串使用 UTF-16 或者 UTF-32 编码,这个在上节已经讲到了,现在只剩下 char 类型的窄字符串(下面称为窄字符串)没有讲了,这就是本节的重点。
对于窄字符串,C语言并没有规定使用哪一种特定的编码,只要选用的编码能够适应当前的环境即可,所以,窄字符串的编码与操作系统和编译器有关。
但是,可以肯定的说,在现代计算机中,窄字符串已经不再使用 ASCII 编码了,因为 ASCII 编码只能显示字母、数字等英文字符,对汉语、日语、韩语等其它地区的字符无能为力。
讨论窄字符串的编码要从以下两个方面下手。
源文件使用什么编码源文件用来保存我们编写的代码,它最终会被存储到本地硬盘,或者远程服务器,这个时候就要尽量压缩文件体积,以节省硬盘空间或者网络流量,而代码中大部分的字符都是 ASCII 编码中的字符,用一个字节足以容纳,所以 UTF-8 编码是一个不错的选择。
UTF-8 兼容 ASCII,代码中的大部分字符可以用一个字节保存;另外 UTF-8 基于 Unicode,支持全世界的字符,我们编写的代码可以给全球的程序员使用,真正做到技术无国界。
常见的 IDE 或者编辑器,例如 Xcode、Sublime Text、Gedit、Vim 等,在创建源文件时一般也默认使用 UTF-8 编码。但是 Visual Studio 是个奇葩,它默认使用本地编码来创建源文件。
所谓本地编码,就是像 GBK、Big5、Shift-JIS 等这样的国家编码(地区编码);针对不同国家发行的操作系统,默认的本地编码一般不同。简体中文本的 Windows 默认的本地编码是 GBK。
对于编译器来说,它往往支持多种编码格式的源文件。微软编译器、GCC、LLVM/Clang(内嵌于 Xcode 中)都支持 UTF-8 和本地编码的源文件,不过微软编译器还支持 UTF-16 编码的源文件。如果考虑到源文件的通用性,就只能使用 UTF-8 和本地编码了。
窄字符串使用什么编码前面讲到,用 puts 或者 printf 可以输出窄字符串,代码如下:
#includeint main() { puts("C语言中文网"); printf("http://c.biancheng.net"); return 0; }
"C语言中文网"和"http://c.biancheng.net"就是需要被处理的窄字符串,程序运行后,它们会被载入到内存中。你看,这里面还包含了中文,肯定不能使用 ASCII 编码了。
-
微软编译器使用本地编码来保存这些字符。不同地区的 Windows 版本默认的本地编码不一样,所以,同样的窄字符串在不同的 Windows 版本下使用的编码也不一样。对于简体中文版的 Windows,使用的是 GBK 编码。
-
GCC、LLVM/Clang 编译器使用和源文件相同的编码来保存这些字符:如果源文件使用的是 UTF-8 编码,那么这些字符也使用 UTF-8 编码;如果源文件使用的是 GBK 编码,那么这些字符也使用 GBK 编码。
你看,对于代码中需要被处理的窄字符串,不同的编译器差别还是挺大的。不过可以肯定的是,这些字符始终都使用窄字符(多字节字符)编码。
正是由于这些字符使用 UTF-8、GBK 等编码,而不是使用 ASCII 编码,所以它们才能包含中文。
那么,为什么很多初学者会误认为C语言使用 ASCII 编码呢?
不管是在课堂跟着老师学习,还是通过互联网自学,初学者都是从处理英文开始的,对于英文来说,使用 GBK、UTF-8、ASCII 都是一样的,GBK、UTF-8 都兼容 ASCII,初学者根本察觉不出用了哪种编码。
另外,很多大学老师和书籍作者也经常会念叨,字符在存储时会被转换成对应的 ASCII 码,在读取时又会从 ASCII 码转换成对应的字符实体,大家需要熟悉 ASCII 编码,它是C语言处理字符的基础,这从很大程度上给初学者造成一种错误印象:C语言和 ASCII 编码是绑定的,C语言使用 ASCII 编码。
总结对于 char 类型的窄字符,始终使用 ASCII 编码。
对于 wchar_t 类型的宽字符和宽字符串,使用 UTF-16 或者 UTF-32 编码,它们都是基于 Unicode 字符集的。
对于 char 类型的窄字符串,微软编译器使用本地编码,GCC、LLVM/Clang 使用和源文件编码相同的编码。
另外,处理窄字符和处理宽字符使用的函数也不一样:
头文件中的 putchar、puts、printf 函数只能用来处理窄字符; 头文件中的 putwchar、wprintf 函数只能用来处理宽字符。
你看,仅仅是字符的处理,C语言就能玩出这么多花样,让人捉摸不透,不容易学习。这是因为,C语言是一种较为底层和古老的语言,既有历史遗留问题,又有贴近计算机底层的特性。不过,一旦搞明白这些繁杂的底层问题,你的编程内功将精进一个层次,这也许就是学习C语言的乐趣。
3.ASCII编码了解ASCII(American Standard Code for Information Interchange,美国信息互换标准代码)是一套基于拉丁字母的字符编码,共收录了 128 个字符,用一个字节就可以存储,它等同于国际标准 ISO/IEC 646。
ASCII 规范于 1967 年第一次发布,最后一次更新是在 1986 年,它包含了 33 个控制字符(具有某些特殊功能但是无法显示的字符)和 95 个可显示字符。
| 二进制 | 十进制 | 十六进制 | 字符/缩写 | 解释 |
|---|---|---|---|---|
| 00000000 | 0 | 00 | NUL (NULL) | 空字符 |
| 00000001 | 1 | 01 | SOH (Start Of Headling) | 标题开始 |
| 00000010 | 2 | 02 | STX (Start Of Text) | 正文开始 |
| 00000011 | 3 | 03 | ETX (End Of Text) | 正文结束 |
| 00000100 | 4 | 04 | EOT (End Of Transmission) | 传输结束 |
| 00000101 | 5 | 05 | ENQ (Enquiry) | 请求 |
| 00000110 | 6 | 06 | ACK (Acknowledge) | 回应/响应/收到通知 |
| 00000111 | 7 | 07 | BEL (Bell) | 响铃 |
| 00001000 | 8 | 08 | BS (Backspace) | 退格 |
| 00001001 | 9 | 09 | HT (Horizontal Tab) | 水平制表符 |
| 00001010 | 10 | 0A | LF/NL(Line Feed/New Line) | 换行键 |
| 00001011 | 11 | 0B | VT (Vertical Tab) | 垂直制表符 |
| 00001100 | 12 | 0C | FF/NP (Form Feed/New Page) | 换页键 |
| 00001101 | 13 | 0D | CR (Carriage Return) | 回车键 |
| 00001110 | 14 | 0E | SO (Shift Out) | 不用切换 |
| 00001111 | 15 | 0F | SI (Shift In) | 启用切换 |
| 00010000 | 16 | 10 | DLE (Data link Escape) | 数据链路转义 |
| 00010001 | 17 | 11 | DC1/XON (Device Control 1/Transmission On) | 设备控制1/传输开始 |
| 00010010 | 18 | 12 | DC2 (Device Control 2) | 设备控制2 |
| 00010011 | 19 | 13 | DC3/XOFF (Device Control 3/Transmission Off) | 设备控制3/传输中断 |
| 00010100 | 20 | 14 | DC4 (Device Control 4) | 设备控制4 |
| 00010101 | 21 | 15 | NAK (Negative Acknowledge) | 无响应/非正常响应/拒绝接收 |
| 00010110 | 22 | 16 | SYN (Synchronous Idle) | 同步空闲 |
| 00010111 | 23 | 17 | ETB (End of Transmission Block) | 传输块结束/块传输终止 |
| 00011000 | 24 | 18 | CAN (Cancel) | 取消 |
| 00011001 | 25 | 19 | EM (End of Medium) | 已到介质末端/介质存储已满/介质中断 |
| 00011010 | 26 | 1A | SUB (Substitute) | 替补/替换 |
| 00011011 | 27 | 1B | ESC (Escape) | 逃离/取消 |
| 00011100 | 28 | 1C | FS (File Separator) | 文件分割符 |
| 00011101 | 29 | 1D | GS (Group Separator) | 组分隔符/分组符 |
| 00011110 | 30 | 1E | RS (Record Separator) | 记录分离符 |
| 00011111 | 31 | 1F | US (Unit Separator) | 单元分隔符 |
| 00100000 | 32 | 20 | (Space) | 空格 |
| 00100001 | 33 | 21 | ! | |
| 00100010 | 34 | 22 | " | |
| 00100011 | 35 | 23 | # | |
| 00100100 | 36 | 24 | $ | |
| 00100101 | 37 | 25 | % | |
| 00100110 | 38 | 26 | & | |
| 00100111 | 39 | 27 | ’ | |
| 00101000 | 40 | 28 | ( | |
| 00101001 | 41 | 29 | ) | |
| 00101010 | 42 | 2A | * | |
| 00101011 | 43 | 2B | + | |
| 00101100 | 44 | 2C | , | |
| 00101101 | 45 | 2D | - | |
| 00101110 | 46 | 2E | . | |
| 00101111 | 47 | 2F | / | |
| 00110000 | 48 | 30 | 0 | |
| 00110001 | 49 | 31 | 1 | |
| 00110010 | 50 | 32 | 2 | |
| 00110011 | 51 | 33 | 3 | |
| 00110100 | 52 | 34 | 4 | |
| 00110101 | 53 | 35 | 5 | |
| 00110110 | 54 | 36 | 6 | |
| 00110111 | 55 | 37 | 7 | |
| 00111000 | 56 | 38 | 8 | |
| 00111001 | 57 | 39 | 9 | |
| 00111010 | 58 | 3A | : | |
| 00111011 | 59 | 3B | ; | |
| 00111100 | 60 | 3C | < | |
| 00111101 | 61 | 3D | = | |
| 00111110 | 62 | 3E | > | |
| 00111111 | 63 | 3F | ? | |
| 01000000 | 64 | 40 | @ | |
| 01000001 | 65 | 41 | A | |
| 01000010 | 66 | 42 | B | |
| 01000011 | 67 | 43 | C | |
| 01000100 | 68 | 44 | D | |
| 01000101 | 69 | 45 | E | |
| 01000110 | 70 | 46 | F | |
| 01000111 | 71 | 47 | G | |
| 01001000 | 72 | 48 | H | |
| 01001001 | 73 | 49 | I | |
| 01001010 | 74 | 4A | J | |
| 01001011 | 75 | 4B | K | |
| 01001100 | 76 | 4C | L | |
| 01001101 | 77 | 4D | M | |
| 01001110 | 78 | 4E | N | |
| 01001111 | 79 | 4F | O | |
| 01010000 | 80 | 50 | P | |
| 01010001 | 81 | 51 | Q | |
| 01010010 | 82 | 52 | R | |
| 01010011 | 83 | 53 | S | |
| 01010100 | 84 | 54 | T | |
| 01010101 | 85 | 55 | U | |
| 01010110 | 86 | 56 | V | |
| 01010111 | 87 | 57 | W | |
| 01011000 | 88 | 58 | X | |
| 01011001 | 89 | 59 | Y | |
| 01011010 | 90 | 5A | Z | |
| 01011011 | 91 | 5B | [ | |
| 01011100 | 92 | 5C | ||
| 01011101 | 93 | 5D | ] | |
| 01011110 | 94 | 5E | ^ | |
| 01011111 | 95 | 5F | _ | |
| 01100000 | 96 | 60 | ` | |
| 01100001 | 97 | 61 | a | |
| 01100010 | 98 | 62 | b | |
| 01100011 | 99 | 63 | c | |
| 01100100 | 100 | 64 | d | |
| 01100101 | 101 | 65 | e | |
| 01100110 | 102 | 66 | f | |
| 01100111 | 103 | 67 | g | |
| 01101000 | 104 | 68 | h | |
| 01101001 | 105 | 69 | i | |
| 01101010 | 106 | 6A | j | |
| 01101011 | 107 | 6B | k | |
| 01101100 | 108 | 6C | l | |
| 01101101 | 109 | 6D | m | |
| 01101110 | 110 | 6E | n | |
| 01101111 | 111 | 6F | o | |
| 01110000 | 112 | 70 | p | |
| 01110001 | 113 | 71 | q | |
| 01110010 | 114 | 72 | r | |
| 01110011 | 115 | 73 | s | |
| 01110100 | 116 | 74 | t | |
| 01110101 | 117 | 75 | u | |
| 01110110 | 118 | 76 | v | |
| 01110111 | 119 | 77 | w | |
| 01111000 | 120 | 78 | x | |
| 01111001 | 121 | 79 | y | |
| 01111010 | 122 | 7A | z | |
| 01111011 | 123 | 7B | { | |
| 01111100 | 124 | 7C | | | |
| 01111101 | 125 | 7D | } | |
| 01111110 | 126 | 7E | ~ | |
| 01111111 | 127 | 7F | DEL (Delete) | 删除 |
ASCII 编码中第 0~31 个字符(开头的 32 个字符)以及第 127 个字符(最后一个字符)都是不可见的(无法显示),但是它们都具有一些特殊功能,所以称为控制字符( Control Character)或者功能码(Function Code)。
这 33 个控制字符大都与通信、数据存储以及老式设备有关,有些在现代电脑中的含义已经改变了。
有些控制符需要一定的计算机功底才能理解,初学者可以跳过,选择容易的理解即可。
下面列出了部分控制字符的具体功能:
NUL (0)
NULL,空字符。空字符起初本意可以看作为 NOP(中文意为空操作,就是啥都不做的意思),此位置可以忽略一个字符。
之所以有这个空字符,主要是用于计算机早期的记录信息的纸带,此处留个 NUL 字符,意思是先占这个位置,以待后用,比如你哪天想起来了,在这个位置在放一个别的啥字符之类的。
后来呢,NUL 被用于C语言中,表示字符串的结束,当一个字符串中间出现 NUL 时,就意味着这个是一个字符串的结尾了。这样就方便按照自己需求去定义字符串,多长都行,当然只要你内存放得下,然后最后加一个 ,即空字符,意思是当前字符串到此结束。
SOH (1)
Start Of Heading,标题开始。如果信息沟通交流主要以命令和消息的形式的话,SOH 就可以用于标记每个消息的开始。
1963年,最开始 ASCII 标准中,把此字符定义为 Start of Message,后来又改为现在的 Start Of Heading。
现在,这个 SOH 常见于主从(master-slave)模式的 RS232 的通信中,一个主设备,以 SOH 开头,和从设备进行通信。这样方便从设备在数据传输出现错误的时候,在下一次通信之前,去实现重新同步(resynchronize)。如果没有一个清晰的类似于 SOH 这样的标记,去标记每个命令的起始或开头的话,那么重新同步,就很难实现了。
-
STX (2) 和 ETX (3)
STX 表示 Start Of Text,意思是“文本开始”;ETX 表示 End Of Text,意思是“文本结束”。
通过某种通讯协议去传输的一个数据(包),称为一帧的话,常会包含一个帧头,包含了寻址信息,即你是要发给谁,要发送到目的地是哪里,其后跟着真正要发送的数据内容。
而 STX,就用于标记这个数据内容的开始。接下来是要传输的数据,最后是 ETX,表明数据的结束。
而中间具体传输的数据内容,ASCII 并没有去定义,它和你所用的传输协议有关。
帧头 数据或文本内容 SOH(表明帧头开始) …(帧头信息,比如包含了目的地址,表明你发送给谁等等) STX(表明数据开始) …(真正要传输的数据) ETX(表明数据结束
BEL (7)
BELl,响铃。在 ASCII 编码中,BEL 是个比较有意思的东西。BEL 用一个可以听得见的声音来吸引人们的注意,既可以用于计算机,也可以用于周边设备(比如打印机)。
注意,BEL 不是声卡或者喇叭发出的声音,而是蜂鸣器发出的声音,主要用于报警,比如硬件出现故障时就会听到这个声音,有的计算机操作系统正常启动也会听到这个声音。蜂鸣器没有直接安装到主板上,而是需要连接到主板上的一种外设,现代很多计算机都不安装蜂鸣器了,即使输出 BEL 也听不到声音,这个时候 BEL 就没有任何作用了。
BS (8)
BackSpace,退格键。退格键的功能,随着时间变化,意义也变得不同了。
退格键起初的意思是,在打印机和电传打字机上,往回移动一格光标,以起到强调该字符的作用。比如你想要打印一个 a,然后加上退格键后,就成了 aBS^。在机械类打字机上,此方法能够起到实际的强调字符的作用,但是对于后来的 CTR 下时期来说,就无法起到对应效果了。
而现代所用的退格键,不仅仅表示光标往回移动了一格,同时也删除了移动后该位置的字符。
HT (9)
Horizontal Tab,水平制表符,相当于 Table/Tab 键。
水平制表符的作用是用于布局,它控制输出设备前进到下一个表格去处理。而制表符 Table/Tab 的宽度也是灵活不固定的,只不过在多数设备上制表符 Tab 都预定义为 4 个空格的宽度。
水平制表符 HT 不仅能减少数据输入者的工作量,对于格式化好的文字来说,还能够减少存储空间,因为一个Tab键,就代替了 4 个空格。
LF (10)
Line Feed,直译为“给打印机等喂一行”,也就是“换行”的意思。LF 是 ASCII 编码中常被误用的字符之一。
LF 的最原始的含义是,移动打印机的头到下一行。而另外一个 ASCII 字符,CR(Carriage Return)才是将打印机的头移到最左边,即一行的开始(行首)。很多串口协议和 MS-DOS 及 Windows 操作系统,也都是这么实现的。
而C语言和 Unix 操作系统将 LF 的含义重新定义为“新行”,即 LF 和 CR 的组合效果,也就是回车且换行的意思。
从程序的角度出发,C语言和 Unix 对 LF 的定义显得更加自然,而 MS-DOS 的实现更接近于 LF 的本意。
现在人们常将 LF 用做“新行(newline)”的功能,大多数文本编辑软件也都可以处理单个 LF 或者 CR/LF 的组合了。
VT (11)
Vertical Tab,垂直制表符。它类似于水平制表符 Tab,目的是为了减少布局中的工作,同时也减少了格式化字符时所需要存储字符的空间。VT 控制符用于跳到下一个标记行。
说实话,还真没看到有些地方需要用 VT,因为一般在换行的时候都是用 LF 代替 VT 了。
FF (12)
Form Feed,换页。设计换页键,是用来控制打印机行为的。当打印机收到此键码的时候,打印机移动到下一页。
不同的设备的终端对此控制符所表现的行为各不同,有些会清除屏幕,有些只是显示
^L
字符,有些只是新换一行而已。例如,Unix/Linux 下的 Bash Shell 和 Tcsh 就把 FF 看做是一个清空屏幕的命令。
CR (13)
Carriage return,回车,表示机器的滑动部分(或者底座)返回。
CR 回车的原意是让打印头回到左边界,并没有移动到下一行的意思。随着时间的流逝,后来人们把 CR 的意思弄成了 Enter 键,用于示意输入完毕。
在数据以屏幕显示的情况下,人们按下 Enter 的同时,也希望把光标移动到下一行,因此C语言和 Unix 重新定义了 CR 的含义,将其表示为移动到下一行。当输入 CR 时,系统也常常隐式地将其转换为LF。
SO (14) 和 SI (15)
SO,Shift Out,不用切换;SI,Shift In,启用切换。
早在 1960s 年代,设计 ASCII 编码的美国人就已经想到了,ASCII 编码不仅仅能用于英文,也要能用于外文字符集,这很重要,定义 Shift In 和 Shift Out 正是考虑到了这点。
最开始,其意为在西里尔语和拉丁语之间切换。西里尔语 ASCII(也即 KOI-7 编码)将 Shift 作为一个普通字符,而拉丁语 ASCII(也就是我们通常所说的 ASCII)用 Shift 去改变打印机的字体,它们完全是两种含义。
在拉丁语 ASCII 中,SO 用于产生双倍宽度的字符(类似于全角),而用 SI 打印压缩的字体(类似于半角)。
DLE (16)
Data link Escape,数据链路转义。
有时候我们需要在通信过程中发送一些控制字符,但是总有一些情况下,这些控制字符被看成了普通的数据流,而没有起到对应的控制效果,ASCII 编码引入 DLE 来解决这类问题。
如果数据流中检测到了 DLE,数据接收端会对数据流中接下来的字符另作处理。但是具体如何处理,ASCII 规范中并没有定义,只是弄了个 DLE 去打断正常的数据流,告诉接下来的数据要特殊对待。
DC1 (17)
Device Control 1,或者 XON – Transmission on。
这个 ASCII 控制符尽管原先定义为 DC1, 但是现在常表示为 XON,用于串行通信中的软件流控制。其主要作用为,在通信被控制符 XOFF 中断之后,重新开始信息传输。
用过串行终端的人应该还记得,当有时候数据出错了,按 Ctrl+Q(等价于XON)有时候可以起到重新传输的效果。这是因为,此 Ctrl+Q 键盘序列实际上就是产生 XON 控制符,它可以将那些由于终端或者主机方面,由于偶尔出现的错误的 XOFF 控制符而中断的通信解锁,使其正常通信。
DC3 (19)
Device Control 3,或者 XOFF(Transmission off,传输中断)。
EM (25)
End of Medium,已到介质末端,介质存储已满。
EM 用于,当数据存储到达串行存储介质末尾的时候,就像磁带或磁头滚动到介质末尾一样。其用于表述数据的逻辑终点,即不必非要是物理上的达到数据载体的末尾。
FS(28)
File Separator,文件分隔符。FS 是个很有意思的控制字符,它可以让我们看到 1960s 年代的计算机是如何组织的。
我们现在习惯于随机访问一些存储介质,比如 RAM、磁盘等,但是在设计 ASCII 编码的那个年代,大部分数据还是顺序的、串行的,而不是随机访问的。此处所说的串行,不仅仅指的是串行通信,还指的是顺序存储介质,比如穿孔卡片、纸带、磁带等。
在串行通信的时代,设计这么一个用于表示文件分隔的控制字符,用于分割两个单独的文件,是一件很明智的事情。
GS(29)
Group Separator,分组符。
ASCII 定义控制字符的原因之一就是考虑到了数据存储。
大部分情况下,数据库的建立都和表有关,表包含了多条记录。同一个表中的所有记录属于同一类型,不同的表中的记录属于不同的类型。
而分组符 GS 就是用来分隔串行数据存储系统中的不同的组。值得注意的是,当时还没有使用 Excel 表格,ASCII 时代的人把它叫做组。
RS(30)
Record Separator,记录分隔符,用于分隔一个组或表中的多条记录。
US(31)
Unit Separator,单元分隔符。
在 ASCII 定义中,数据库中所存储的最小的数据项叫做单元(Unit)。而现在我们称其字段(Field)。单元分隔符 US 用于分割串行数据存储环境下的不同单元。
现在的数据库实现都要求大部分类型都拥有固定的长度,尽管有时候可能用不到,但是对于每一个字段,却都要分配足够大的空间,用于存放最大可能的数据。
这种做法的弊端就是占用了大量的存储空间,而 US 控制符允许字段具有可变的长度。在 1960s 年代,数据存储空间很有限,用 US 将不同单元分隔开,能节省很多空间。
DEL (127)
Delete,删除。
有人也许会问,为何 ASCII 编码中其它控制字符的值都很小(即 0~31),而 DEL 的值却很大呢(为 127)?
这是由于这个特殊的字符是为纸带而定义的。在那个年代,绝大多数的纸带都是用7个孔洞去编码数据的。而 127 这个值所对应的二进制值为111 1111(所有 7 个比特位都是1),将 DEL 用在现存的纸带上时,所有的洞就都被穿孔了,就把已经存在的数据都擦除掉了,就起到了删除的作用。
也有人将 ASCII 编码分成两部分:
- 前 128 个字符称为基本 ASCII,包含常见字符;
- 后 128 个字符称为扩展 ASCII,包含一些特殊字符。
字符集(Character Set)为每个字符分配了唯一的编号,我们不妨将它称为编码值。在C语言中,一个字符除了可以用它的实体(也就是真正的字符)表示,还可以用编码值表示。这种使用编码值来间接地表示字符的方式称为转义字符(Escape Character)。
转义字符以或者x开头,以开头表示后跟八进制形式的编码值,以x开头表示后跟十六进制形式的编码值。对于转义字符来说,只能使用八进制或者十六进制。
字符 1、2、3、a、b、c 对应的 ASCII 码的八进制形式分别是 61、62、63、141、142、143,十六进制形式分别是 31、32、33、61、62、63。下面的例子演示了转义字符的用法:
char a = '61'; //字符1 char b = '141'; //字符a char c = 'x31'; //字符1 char d = 'x61'; //字符a char *str1 = "x31x32x33x61x62x63"; //字符串"123abc" char *str2 = "616263141142143"; //字符串"123abc" char *str3 = "The string is: 616263x61x62x63" //混用八进制和十六进制形式
转义字符既可以用于单个字符,也可以用于字符串,并且一个字符串中可以同时使用八进制形式和十六进制形式。
一个完整的例子:
#includeint main(){ puts("x68164164x70://c.biancheng.x6e145x74"); return 0; }
运行结果:
http://c.biancheng.net
转义字符的初衷是用于 ASCII 编码,所以它的取值范围有限:
- 八进制形式的转义字符最多后跟三个数字,也即ddd,最大取值是177;
- 十六进制形式的转义字符最多后跟两个数字,也即xdd,最大取值是x7f。
超出范围的转义字符的行为是未定义的,有的编译器会将编码值直接输出,有的编译器会报错。
对于 ASCII 编码,0~31(十进制)范围内的字符为控制字符,它们都是看不见的,不能在显示器上显示,甚至无法从键盘输入,只能用转义字符的形式来表示。不过,直接使用 ASCII 码记忆不方便,也不容易理解,所以,针对常用的控制字符,C语言又定义了简写方式,完整的列表如下:
| 转义字符 | 意义 | ASCII码值(十进制) |
|---|---|---|
| a | 响铃(BEL) | 007 |
| b | 退格(BS) ,将当前位置移到前一列 | 008 |
| f | 换页(FF),将当前位置移到下页开头 | 012 |
| n | 换行(LF) ,将当前位置移到下一行开头 | 010 |
| r | 回车(CR) ,将当前位置移到本行开头 | 013 |
| t | 水平制表(HT) | 009 |
| v | 垂直制表(VT) | 011 |
| ’ | 单引号 | 039 |
| " | 双引号 | 034 |
| 反斜杠 | 092 |
n和t是最常用的两个转义字符:
- n用来换行,让文本从下一行的开头输出,前面的章节中已经多次使用;
- t用来占位,一般相当于四个空格,或者 tab 键的功能。
单引号、双引号、反斜杠是特殊的字符,不能直接表示:
- 单引号是字符类型的开头和结尾,要使用'表示,也即''';
- 双引号是字符串的开头和结尾,要使用"表示,也即"abc"123";
- 反斜杠是转义字符的开头,要使用\表示,也即'\',或者"abc\123"。
转义字符示例:
#includeint main(){ puts("CtC++tJavan"C" first appeared!"); return 0; }
运行结果:
C C++ Java
“C” first appeared!
大部分C语言教材对中文字符的处理讳莫如深,甚至只字不提,导致很多初学者认为C语言只能处理英文,而不支持中文。其实C语言是一门全球化的编程语言,它支持世界上任何一个国家的语言文化,包括中文、日语、韩语等。
中文字符的存储正确地存储中文字符需要解决两个问题。
1) 足够长的数据类型char 只能处理 ASCII 编码中的英文字符,是因为 char 类型太短,只有一个字节,容纳不下我大中华几万个汉字,要想处理中文字符,必须得使用更长的数据类型。
一个字符在存储之前会转换成它在字符集中的编号,而这样的编号是一个整数,所以我们可以用整数类型来存储一个字符,比如 unsigned short、unsigned int、unsigned long 等。
2) 选择包含中文的字符集C语言规定,对于汉语、日语、韩语等 ASCII 编码之外的单个字符,也就是专门的字符类型,要使用宽字符的编码方式。常见的宽字符编码有 UTF-16 和 UTF-32,它们都是基于 Unicode 字符集的,能够支持全球的语言文化。
在真正实现时,微软编译器(内嵌于 Visual Studio 或者 Visual C++ 中)采用 UTF-16 编码,使用 2 个字节存储一个字符,用 unsigned short 类型就可以容纳。GCC、LLVM/Clang(内嵌于 Xcode 中)采用 UTF-32 编码,使用 4 个字节存储字符,用 unsigned int 类型就可以容纳。
对于编号较小的字符,UTF-16 采用两个字节存储;对于编号较大的字符,UTF-16 使用四个字节存储。但是,全球常用的字符也就几万个,使用两个字节存储足以,只有极其罕见,或者非常古老的字符才会用到四个字节。
微软编译器使用两个字节来存储 UTF-16 编码的字符,虽然不能囊括所有的 Unicode 字符,但是也足以容纳全球的常见字符了,基本满足了软件开发的需求。使用两个字节存储的另外一个好处是可以节省内存,而使用四个字节会浪费 50% 以上的内存。
你看,不同的编译器可以使用不同的整数类型。如果我们的代码使用 unsigned int 来存储宽字符,那么在微软编译器下就是一种浪费;如果我们的代码使用 unsigned short 来存储宽字符,那么在 GCC、LLVM/Clang 下就不够。
为了解决这个问题,C语言推出了一种新的类型,叫做 wchar_t。w 是 wide 的首字母,t 是 type 的首字符,wchar_t 的意思就是宽字符类型。wchar_t 的长度由编译器决定:
- 在微软编译器下,它的长度是 2,等价于 unsigned short;
- 在GCC、LLVM/Clang 下,它的长度是 4,等价于 unsigned int。
wchar_t 其实是用 typedef 关键字定义的一个别名,我们会在《C语言typedef的用法详解》一节中深入讲解,大家暂时只需要记住,wchar_t 在不同的编译器下长度不一样。
wchar_t 类型位于
上节我们讲到,单独的字符由单引号' '包围,例如'B'、'@'、'9'等;但是,这样的字符只能使用 ASCII 编码,要想使用宽字符的编码方式,就得加上L前缀,例如L'A'、L'9'、L'中'、L'国'、L'。'。
注意,加上L前缀后,所有的字符都将成为宽字符,占用 2 个字节或者 4 个字节的内存,包括 ASCII 中的英文字符。
下面的例子演示了如何存储宽字符(注意引入
wchar_t a = L'A'; //英文字符(基本拉丁字符) wchar_t b = L'9'; //英文数字(阿拉伯数字) wchar_t c = L'中'; //中文汉字 wchar_t d = L'国'; //中文汉字 wchar_t e = L'。'; //中文标点 wchar_t f = L'ヅ'; //日文片假名 wchar_t g = L'♥'; //特殊符号 wchar_t h = L'༄'; //藏文
在以后的编程中,我们将不加L前缀的字符称为窄字符,将加上L前缀的字符称为宽字符。窄字符使用 ASCII 编码,宽字符使用 UTF-16 或者 UTF-32 编码。
宽字符的输出putchar、printf 只能输出不加L前缀的窄字符,对加了L前缀的宽字符无能为力,我们必须使用
- putwchar 函数专门用来输出一个宽字符,它和 putchar 的用法类似;
- wprintf 是通用的、格式化的宽字符输出函数,它除了可以输出单个宽字符,还可以输出宽字符串(稍后讲解)。宽字符对应的格式控制符为%lc。
另外,在输出宽字符之前还要使用 setlocale 函数进行本地化设置,告诉程序如何才能正确地处理各个国家的语言文化。由于大家基础还不够,关于本地化设置的内容我们不再展开讲解,请大家先记住这种写法。
如果希望设置为中文简体环境,在 Windows 下请写作:
setlocale(LC_ALL, “zh-CN”);
在 Linux 和 Mac OS 下请写作:
setlocale(LC_ALL, “zh_CN”);
setlocale 函数位于
下面的代码完整地演示了宽字符的输出:
#include#include int main(){ wchar_t a = L'A'; //英文字符(基本拉丁字符) wchar_t b = L'9'; //英文数字(阿拉伯数字) wchar_t c = L'中'; //中文汉字 wchar_t d = L'国'; //中文汉字 wchar_t e = L'。'; //中文标点 wchar_t f = L'ヅ'; //日文片假名 wchar_t g = L'♥'; //特殊符号 wchar_t h = L'༄'; //藏文 //将本地环境设置为简体中文 setlocale(LC_ALL, "zh_CN"); //使用专门的 putwchar 输出宽字符 putwchar(a); putwchar(b); putwchar(c); putwchar(d); putwchar(e); putwchar(f); putwchar(g); putwchar(h); putwchar(L'n'); //只能使用宽字符 //使用通用的 wprintf 输出宽字符 wprintf( L"Wide chars: %lc %lc %lc %lc %lc %lc %lc %lcn", //必须使用宽字符串 a, b, c, d, e, f, g, h ); return 0; }
运行结果:
A9中国。ヅ♥༄
Wide chars: A 9 中 国 。 ヅ ♥ ༄
给字符串加上L前缀就变成了宽字符串,它包含的每个字符都是宽字符,一律采用 UTF-16 或者 UTF-32 编码。输出宽字符串可以使用
下面的代码演示了如何使用宽字符串:
#include#include int main(){ wchar_t web_url[] = L"http://c.biancheng.net"; wchar_t *web_name = L"C语言中文网"; //将本地环境设置为简体中文 setlocale(LC_ALL, "zh_CN"); //使用通用的 wprintf 输出宽字符 wprintf(L"web_url: %ls nweb_name: %lsn", web_url, web_name); return 0; }
运行结果:
web_url: http://c.biancheng.net
web_name: C语言中文网
其实,不加L前缀的窄字符串也可以处理中文,它和加上了L前缀的宽字符串有什么区别呢?我们将在下节《C语言到底使用什么编码?谁说C语言使用ASCII码,真是荒谬!》中详细讲解。
6.C语言字符数组和字符串用来存放字符的数组称为字符数组,例如:
char a[10]; //一维字符数组
char b[5][10]; //二维字符数组
char c[20]={'c', ' ', 'p', 'r', 'o', 'g', 'r', 'a','m'}; // 给部分数组元素赋值
char d[]={'c', ' ', 'p', 'r', 'o', 'g', 'r', 'a', 'm' }; //对全体元素赋值时可以省去长度
字符数组实际上是一系列字符的集合,也就是字符串(String)。在C语言中,没有专门的字符串变量,没有string类型,通常就用一个字符数组来存放一个字符串。
C语言规定,可以将字符串直接赋值给字符数组,例如:
char str[30] = {"c.biancheng.net"};
char str[30] = "c.biancheng.net"; //这种形式更加简洁,实际开发中常用
数组第 0 个元素为'c',第 1 个元素为'.',第 2 个元素为'b',后面的元素以此类推。
为了方便,你也可以不指定数组长度,从而写作:
char str[] = {"c.biancheng.net"};
char str[] = "c.biancheng.net"; //这种形式更加简洁,实际开发中常用
给字符数组赋值时,我们通常使用这种写法,将字符串一次性地赋值(可以指明数组长度,也可以不指明),而不是一个字符一个字符地赋值,那样做太麻烦了。
这里需要留意一个坑,字符数组只有在定义时才能将整个字符串一次性地赋值给它,一旦定义完了,就只能一个字符一个字符地赋值了。请看下面的例子:
char str[7]; str = "abc123"; //错误 //正确 str[0] = 'a'; str[1] = 'b'; str[2] = 'c'; str[3] = '1'; str[4] = '2'; str[5] = '3';字符串结束标志(划重点)
字符串是一系列连续的字符的组合,要想在内存中定位一个字符串,除了要知道它的开头,还要知道它的结尾。找到字符串的开头很容易,知道它的名字(字符数组名或者字符串名)就可以;然而,如何找到字符串的结尾呢?C语言的解决方案有点奇妙,或者说有点奇葩。
在C语言中,字符串总是以' '作为结尾,所以' '也被称为字符串结束标志,或者字符串结束符。
' '是 ASCII 码表中的第 0 个字符,英文称为 NUL,中文称为“空字符”。该字符既不能显示,也没有控制功能,输出该字符不会有任何效果,它在C语言中唯一的作用就是作为字符串结束标志。
C语言在处理字符串时,会从前往后逐个扫描字符,一旦遇到' '就认为到达了字符串的末尾,就结束处理。' '至关重要,没有' '就意味着永远也到达不了字符串的结尾。
由" "包围的字符串会自动在末尾添加' '。例如,"abc123"从表面看起来只包含了 6 个字符,其实不然,C语言会在最后隐式地添加一个' ',这个过程是在后台默默地进行的,所以我们感受不到。
下图演示了"C program"在内存中的存储情形:
需要注意的是,逐个字符地给数组赋值并不会自动添加' ',例如:
char str[] = {'a', 'b', 'c'};
数组 str 的长度为 3,而不是 4,因为最后没有' '。
当用字符数组存储字符串时,要特别注意' ',要为' '留个位置;这意味着,字符数组的长度至少要比字符串的长度大 1。请看下面的例子:
char str[7] = "abc123";
"abc123"看起来只包含了 6 个字符,我们却将 str 的长度定义为 7,就是为了能够容纳最后的' '。如果将 str 的长度定义为 6,它就无法容纳' '了。
当字符串长度大于数组长度时,有些较老或者不严格的编译器并不会报错,甚至连警告都没有,这就为以后的错误埋下了伏笔,读者自己要多多注意。
有些时候,程序的逻辑要求我们必须逐个字符地为数组赋值,这个时候就很容易遗忘字符串结束标志' '。下面的代码中,我们将 26 个大写英文字符存入字符数组,并以字符串的形式输出:
#includeint main(){ char str[30]; char c; int i; for(c=65,i=0; c<=90; c++,i++){ str[i] = c; } printf("%sn", str); return 0; }
在 VS2015 下的运行结果:
ABCDEFGHIJKLMNOPQRSTUVWXYZ口口口口i口口0 ?
口表示无法显示的特殊字符。
大写字母在 ASCII 码表中是连续排布的,编码值从 65 开始,到 90 结束,使用循环非常方便。
在《C语言变量的定义位置以及初始值》一节中我们讲到,在很多编译器下,局部变量的初始值是随机的,是垃圾值,而不是我们通常认为的“零”值。局部数组(在函数内部定义的数组,本例中的 str 数组就是在 main() 函数内部定义的)也有这个问题,很多编译器并不会把局部数组的内存都初始化为“零”值,而是放任不管,爱是什么就是什么,所以它们的值也是没有意义的,也是垃圾值。
在函数内部定义的变量、数组、结构体、共用体等都称为局部数据。在很多编译器下,局部数据的初始值都是随机的、无意义的,而不是我们通常认为的“零”值。这一点非常重要,大家一定要谨记,否则后面会遇到很多奇葩的错误。
本例中的 str 数组在定义完成以后并没有立即初始化,所以它所包含的元素的值都是随机的,只有很小的概率会是“零”值。循环结束以后,str 的前 26 个元素被赋值了,剩下的 4 个元素的值依然是随机的,不知道是什么。
printf() 输出字符串时,会从第 0 个元素开始往后检索,直到遇见' '才停止,然后把' '前面的字符全部输出,这就是 printf() 输出字符串的原理。本例中我们使用 printf() 输出 str,按理说到了第 26 个元素就能检索到' ',就到达了字符串的末尾,然而事实却不是这样,由于我们并未对最后 4 个元素赋值,所以第 26 个元素不是' ',第 27 个也不是,第 28 个也不是……可能到了第 50 个元素才遇到' ',printf() 把这 50 个字符全部输出出来,就是上面的样子,多出来的字符毫无意义,甚至不能显示。
数组总共才 30 个元素,到了第 50 个元素不早就超出数组范围了吗?是的,的确超出范围了!然而,数组后面依然有其它的数据,printf() 也会将这些数据作为字符串输出。
你看,不注意' '的后果有多严重,不但不能正确处理字符串,甚至还会毁坏其它数据。
要想避免这些问题也很容易,在字符串的最后手动添加' '即可。修改上面的代码,在循环结束后添加' ':
#includeint main(){ char str[30]; char c; int i; for(c=65,i=0; c<=90; c++,i++){ str[i] = c; } str[i] = 0; //此处为添加的代码,也可以写作 str[i] = ' '; printf("%sn", str); return 0; }
第 9 行为新添加的代码,它让字符串能够正常结束。根据 ASCII 码表,字符' '的编码值就是 0。
但是,这样的写法貌似有点业余,或者说不够简洁,更加专业的做法是将数组的所有元素都初始化为“零”值,这样才能够从根本上避免问题。再次修改上面的代码:
#includeint main(){ char str[30] = {0}; //将所有元素都初始化为 0,或者说 ' ' char c; int i; for(c=65,i=0; c<=90; c++,i++){ str[i] = c; } printf("%sn", str); return 0; }
还记得《什么是数组》一节中强调过的吗?如果只初始化部分数组元素,那么剩余的数组元素也会自动初始化为“零”值,所以我们只需要将 str 的第 0 个元素赋值为 0,剩下的元素就都是 0 了。
字符串长度所谓字符串长度,就是字符串包含了多少个字符(不包括最后的结束符' ')。例如"abc"的长度是 3,而不是 4。
在C语言中,我们使用string.h头文件中的 strlen() 函数来求字符串的长度,它的用法为:
length strlen(strname);
strname 是字符串的名字,或者字符数组的名字;length 是使用 strlen() 后得到的字符串长度,是一个整数。
下面是一个完整的例子,它输出《C语言入门教程》网址的长度:
#include#include //记得引入该头文件 int main(){ char str[] = "http://c.biancheng.net/c/"; long len = strlen(str); printf("The lenth of the string is %ld.n", len); return 0; }
运行结果:
The lenth of the string is 25.
在C语言中,有两个函数可以在控制台(显示器)上输出字符串,它们分别是:
- puts():输出字符串并自动换行,该函数只能输出字符串。
- printf():通过格式控制符%s输出字符串,不能自动换行。除了字符串,printf() 还能输出其他类型的数据。
这两个函数相信大家已经非常熟悉了,这里不妨再演示一下,请看下面的代码:
#includeint main(){ char str[] = "http://c.biancheng.net"; printf("%sn", str); //通过字符串名字输出 printf("%sn", "http://c.biancheng.net"); //直接输出 puts(str); //通过字符串名字输出 puts("http://c.biancheng.net"); //直接输出 return 0; }
运行结果:
http://c.biancheng.net
http://c.biancheng.net
http://c.biancheng.net
http://c.biancheng.net
注意,输出字符串时只需要给出名字,不能带后边的[ ],例如,下面的两种写法都是错误的:
printf("%sn", str[]);
puts(str[10]);
在C语言中,有两个函数可以让用户从键盘上输入字符串,它们分别是:
- scanf():通过格式控制符%s输入字符串。除了字符串,scanf() 还能输入其他类型的数据。
- gets():直接输入字符串,并且只能输入字符串。
但是,scanf() 和 gets() 是有区别的:
- scanf() 读取字符串时以空格为分隔,遇到空格就认为当前字符串结束了,所以无法读取含有空格的字符串。
- gets() 认为空格也是字符串的一部分,只有遇到回车键时才认为字符串输入结束,所以,不管输入了多少个空格,只要不按下回车键,对 gets() 来说就是一个完整的字符串。换句话说,gets() 用来读取一整行字符串。
请看下面的例子:
#includeint main(){ char str1[30] = {0}; char str2[30] = {0}; char str3[30] = {0}; //gets() 用法 printf("Input a string: "); gets(str1); //scanf() 用法 printf("Input a string: "); scanf("%s", str2); scanf("%s", str3); printf("nstr1: %sn", str1); printf("str2: %sn", str2); printf("str3: %sn", str3); return 0; }
运行结果:
Input a string: C C++ Java Python↙
Input a string: PHP Javascript↙
str1: C C++ Java Python
str2: PHP
str3: Javascript
第一次输入的字符串被 gets() 全部读取,并存入 str1 中。第二次输入的字符串,前半部分被第一个 scanf() 读取并存入 str2 中,后半部分被第二个 scanf() 读取并存入 str3 中。
注意,scanf() 在读取数据时需要的是数据的地址,这一点是恒定不变的,所以对于 int、char、float 等类型的变量都要在前边添加&以获取它们的地址。但是在本段代码中,我们只给出了字符串的名字,却没有在前边添加&,这是为什么呢?因为字符串名字或者数组名字在使用的过程中一般都会转换为地址,所以再添加&就是多此一举,甚至会导致错误了。
就目前学到的知识而言,int、char、float 等类型的变量用于 scanf() 时都要在前面添加&,而数组或者字符串用于 scanf() 时不用添加&,它们本身就会转换为地址。读者一定要谨记这一点。
至于数组名字(字符串名字)和地址的转换细节,以及数组名字什么时候会转换为地址,我们将在《数组到底在什么时候会转换为指针》一节中详细讲解,大家暂时“死记硬背”即可。
其实 scanf() 也可以读取带空格的字符串以上是 scanf() 和 gets() 的一般用法,很多教材也是这样讲解的,所以大部分初学者都认为 scanf() 不能读取包含空格的字符串,不能替代 gets()。其实不然,scanf() 的用法还可以更加复杂和灵活,它不但可以完全替代 gets() 读取一整行字符串,而且比 gets() 的功能更加强大。比如,以下功能都是 gets() 不具备的:
- scanf() 可以控制读取字符的数目;
- scanf() 可以只读取指定的字符;
- scanf() 可以不读取某些字符;
- scanf() 可以把读取到的字符丢弃。
这些我们已经在《scanf的高级用法,原来scanf还有这么多新技能》讲解过了,本节就不再赘述了。
7.C语言数组是静态的,不能插入或删除元素我们知道在C语言中,是不存在字符串这种数据类型的,字符串其实都是用字符数组来存储的。
因此理解数组的特性可以帮助我们更好的理解字符串。
在C语言中,数组一旦被定义后,占用的内存空间就是固定的,容量就是不可改变的,既不能在任何位置插入元素,也不能在任何位置删除元素,只能读取和修改元素,我们将这样的数组称为静态数组。
反过来说,如果数组在定义后可以改变容量,允许在任意位置插入或者删除元素,那么这样的数组称为动态数组。
PHP、Javascript 等解释型的脚本语言一般都支持动态数组,而 C、C++ 等编译型的语言一般不支持动态数组。
总之,C语言中的数组是静态的,一旦定义后长度就不能改变了,大家要注意这一点,不要尝试去插入或删除元素。
如果由于项目要求,必须要在数组中插入或者删除元素,该怎么办呢?没办法,只能再造一个新数组!
下面的代码演示了数组元素的插入和删除操作:
#include//自定义函数,用来输出数组元素 void display_array(int arr[], int len){ int i; for(i=0; i 6){ nums_new1[i-1] = nums[i]; } } display_array(nums_new1, 9); //在nums第6个元素后面插入一个整数 55 for(i=0; i<10; i++){ if(i < 7){ nums_new2[i] = nums[i]; }else if(i > 7){ nums_new2[i+1] = nums[i]; }else { //i等于7 nums_new2[i] = 55; nums_new2[i+1] = nums[i]; } } display_array(nums_new2, 11); return 0; }
运行结果:
1 2 3 4 5 6 8 9 10
1 2 3 4 5 6 7 55 8 9 10
display_array() 是一个自己定义的函数,用来输出数组的所有元素;使用 display_array() 时,需要将数组名和数组长度传递给它。读者暂时知道这些即可,之后我们将在《C语言函数》一章中详细讲解。
这段代码总体的思路是使用 for 循环遍历所有的数组元素,并逐个给新的数组元素赋值。这种方法比较简单粗暴,更加高效和简洁的做法是直接复制数组内存,不过由于大家的知识还不充足,所以暂时无法讲解,等大家学了指针以后可以尝试使用 memcpy() 函数来解决。
C语言数组为什么是静态的
不能插入和删除数组元素有时候会非常麻烦,比如一个数组保存了某个班级的学生学号,现在有一名学生退学了,就得把 TA 从数组中剔除,但是C语言并不支持这么做,这就给编程带来了不小的麻烦。
数组元素都是紧挨着排布的,中间没有空隙,不管是插入元素还是删除元素,都得移动该元素后面的内存:
- 在第 i 个元素后面插入一个新元素时,第 i 个元素后面的所有元素都要往后移动一个元素的位置,从而给新元素腾出位置来。如果该数组后面紧跟的是其它有用数据,那么为了防止覆盖有用数据,还不敢直接往后移动元素,必须得重新开辟一块内存,把所有的元素都复制过去。
- 删除第 i 个元素就比较简单了,不管三七二十一,把第 i 个元素后面的所有元素都向前移动即可。
插入和删除数组元素都要移动内存,甚至重新开辟一块内存,这是相当消耗资源的。如果一个程序中有大量的此类操作,那么程序的性能将堪忧,这有悖于「C语言非常高效」的初衷,所以C语言并不支持动态数组。
另外,很多时候我们需要把数组的地址保存到一个变量里面(等大家学到指针时就会见到这种情况),如果数组重新开辟了内存,而变量里面的地址不跟着改变的话,后续再使用该变量就会导致错误。让C语言本身去维护这些变量的值,以保持同步更新,这又是不可能做到的,所以这个矛盾无法从根本上解决。
总之,为了保证程序执行效率,为了防止操作错误,C语言只支持静态数组,不支持动态数组。
了解了数组这个特性,那我们可以联想到,在已经创建了字符串/字符数组之后,那么赋值就需要注意。
字符数组只有在定义时才能将整个字符串一次性地赋值给它,一旦定义完了,就只能一个字符一个字符地赋值了。请看下面的例子:
char str[30] = {"c.biancheng.net"};
char str[30] = "c.biancheng.net"; //这种形式更加简洁,实际开发中常用
char str[7];
str = "abc123"; //错误
//正确
str[0] = 'a'; str[1] = 'b'; str[2] = 'c';
str[3] = '1'; str[4] = '2'; str[5] = '3';
8.字符数组和字符串的初始化和赋值
我们知道在C语言中,字符串的存储是靠数组,我们又又知道,在C语言中,数组和指针之间有着非常紧密的练习。
因此,在C语言中,表示字符串有两种方式,数组和指针(实际还是数组,字符数组指针)。
字符数组我们上面已经介绍了,下面我们来了解一下字符串指针。
1.C语言字符串指针(指向字符串的指针)C语言中没有特定的字符串类型,我们通常是将字符串放在一个字符数组中,这在《C语言字符数组和字符串》中已经进行了详细讲解,这里不妨再来演示一下:
#include#include int main(){ char str[] = "http://c.biancheng.net"; int len = strlen(str), i; //直接输出字符串 printf("%sn", str); //每次输出一个字符 for(i=0; i 运行结果:
http://c.biancheng.net
http://c.biancheng.net字符数组归根结底还是一个数组,上节讲到的关于指针和数组的规则同样也适用于字符数组。更改上面的代码,使用指针的方式来输出字符串:
#include#include int main(){ char str[] = "http://c.biancheng.net"; char *pstr = str; int len = strlen(str), i; //使用*(pstr+i) for(i=0; i 运行结果:
http://c.biancheng.net
http://c.biancheng.net
http://c.biancheng.net除了字符数组,C语言还支持另外一种表示字符串的方法,就是直接使用一个指针指向字符串,例如:
char *str = "http://c.biancheng.net";或者:
char *str; str = "http://c.biancheng.net";字符串中的所有字符在内存中是连续排列的,str 指向的是字符串的第 0 个字符;我们通常将第 0 个字符的地址称为字符串的首地址。字符串中每个字符的类型都是char,所以 str 的类型也必须是char *。
下面的例子演示了如何输出这种字符串:
#include#include int main(){ char *str = "http://c.biancheng.net"; int len = strlen(str), i; //直接输出字符串 printf("%sn", str); //使用*(str+i) for(i=0; i 运行结果:
http://c.biancheng.net
http://c.biancheng.net
http://c.biancheng.net这一切看起来和字符数组是多么地相似,它们都可以使用%s输出整个字符串,都可以使用*或[ ]获取单个字符,这两种表示字符串的方式是不是就没有区别了呢?
有!它们最根本的区别是在内存中的存储区域不一样,字符数组存储在全局数据区或栈区,第二种形式的字符串存储在常量区。全局数据区和栈区的字符串(也包括其他数据)有读取和写入的权限,而常量区的字符串(也包括其他数据)只有读取权限,没有写入权限。
内存权限的不同导致的一个明显结果就是,字符数组在定义后可以读取和修改每个字符,而对于第二种形式的字符串,一旦被定义后就只能读取不能修改,任何对它的赋值都是错误的。
我们将第二种形式的字符串称为字符串常量,意思很明显,常量只能读取不能写入。请看下面的演示:
#includeint main(){ char *str = "Hello World!"; str = "I love C!"; //正确 str[3] = 'P'; //错误 return 0; } 这段代码能够正常编译和链接,但在运行时会出现段错误(Segment Fault)或者写入位置错误。
第4行代码是正确的,可以更改指针变量本身的指向;第5行代码是错误的,不能修改字符串中的字符。
到底使用字符数组还是字符串常量在编程过程中如果只涉及到对字符串的读取,那么字符数组和字符串常量都能够满足要求;如果有写入(修改)操作,那么只能使用字符数组,不能使用字符串常量。
获取用户输入的字符串就是一个典型的写入操作,只能使用字符数组,不能使用字符串常量,请看下面的代码:
#includeint main(){ char str[30]; gets(str); printf("%sn", str); return 0; } 运行结果:
C C++ Java Python Javascript
C C++ Java Python Javascript最后我们来总结一下,C语言有两种表示字符串的方法,一种是字符数组,另一种是字符串常量,它们在内存中的存储位置不同,使得字符数组可以读取和修改,而字符串常量只能读取不能修改。
2.初始化 逐个字符初始化当定义一个字符数组时,可以采用逐个字符初始化的方式:
char str[10]={ 'h','e','l','l','o'};当显示指定的字符不足字符数组的长度时,编译器将剩余字符置为空字符’ ’。
字符串常量来初始化字符数组在C语言中,将字符串作为字符数组来处理,因此可以使用字符串来初始化字符数组。
char str[]={"hello"};也可以省略花括号。
char str[10]="hello";不及字符数组长度时,剩余字符置为空字符’ ’。因此,我们不难得出,当为一个字符数组初始化为空字符数组的做法有如下几种:
char test1[256]=""; char test2[256]={""}; char test3[256]={0}; char test3[256]={' '};使用字符指针因为字符指针只是一个指针,指向一块连续的内存,自己本身没有申请内存空间,因此只能将其赋值指向一个字符串。
使用字符指针来访问一个字符串,通过字符指针指向存放字符串数组的首元素地址来进行访问.
char * a ="hello!" ;但在这可以:
char * a ; a="hello!" ; //正确注意:这里只是用字符指针指向一个字符串,它依然是用字符数组存储的,这里只是把字符串首地址赋值给a!
这里是一个字符串常量,所以只能读,不能写
char c =a[0]; //正确 a[0]='a'; //错误3.赋值因为字符指针指向一个字符串,指向的字符串为字符常量,不能修改,因此这边的复制将的是字符数组的赋值。
当为已经完成定义的字符数组赋值时,不能采用类似于初始化的方式为字符数组赋值了。如下语句是错误的:char str[10]; //已经完成定义(包括编译器默认的初始化) str={'a','d','s'}; //错误 str="abc"; //错误 str={0}; //错误错误的原因是字符数组名代表字符数组的收地址,不可修改,不能作为左值。左值的概念见博客:认识左值与常引用。
逐个字符赋值(1)for循环的方式。
char str[10]; for(int i=0;i


