C/C++字符串基础,类型,使用方法大全(字符,字符串,字符数组,字符串函数,C++ string)

64 篇文章 99 订阅

文章目录

一、前言

最近在学习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

请看下面的演示:

#include <stdio.h>
int 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 %c\n", a, b, c);
    return 0;
}

运行结果:
1 $ X
1 $ X

putchar 函数每次只能输出一个字符,输出多个字符需要调用多次。

字符与整数

我们知道,计算机在存储字符时并不是真的要存储字符实体,而是存储该字符在字符集中的编号(也可以叫编码值)。对于 char 类型来说,它实际上存储的就是字符的 ASCII 码。

无论在哪个字符集中,字符编号都是一个整数;从这个角度考虑,字符类型和整数类型本质上没有什么区别。

我们可以给字符类型赋值一个整数,或者以整数的形式输出字符类型。反过来,也可以给整数类型赋值一个字符,或者以字符的形式输出整数类型。

请看下面的例子:

#include <stdio.h>
int main()
{
    char a = 'E';
    char b = 70;
    int c = 71;
    int d = 'H';
    printf("a: %c, %d\n", a, a);
    printf("b: %c, %d\n", b, b);
    printf("c: %c, %d\n", c, c);
    printf("d: %c, %d\n", 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 可以输出窄字符串,代码如下:

#include <stdio.h>
int main()
{
    puts("C语言中文网");
    printf("http://c.biancheng.net");
    return 0;
}

"C语言中文网""http://c.biancheng.net"就是需要被处理的窄字符串,程序运行后,它们会被载入到内存中。你看,这里面还包含了中文,肯定不能使用 ASCII 编码了。

  1. 微软编译器使用本地编码来保存这些字符。不同地区的 Windows 版本默认的本地编码不一样,所以,同样的窄字符串在不同的 Windows 版本下使用的编码也不一样。对于简体中文版的 Windows,使用的是 GBK 编码。

  2. 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 使用和源文件编码相同的编码。

另外,处理窄字符和处理宽字符使用的函数也不一样:

  • <stdio.h> 头文件中的 putchar、puts、printf 函数只能用来处理窄字符;
  • <wchar.h> 头文件中的 putwchar、wprintf 函数只能用来处理宽字符。

你看,仅仅是字符的处理,C语言就能玩出这么多花样,让人捉摸不透,不容易学习。这是因为,C语言是一种较为底层和古老的语言,既有历史遗留问题,又有贴近计算机底层的特性。不过,一旦搞明白这些繁杂的底层问题,你的编程内功将精进一个层次,这也许就是学习C语言的乐趣。

3.ASCII编码了解

ASCII(American Standard Code for Information Interchange,美国信息互换标准代码)是一套基于拉丁字母的字符编码,共收录了 128 个字符,用一个字节就可以存储,它等同于国际标准 ISO/IEC 646。

ASCII 规范于 1967 年第一次发布,最后一次更新是在 1986 年,它包含了 33 个控制字符(具有某些特殊功能但是无法显示的字符)和 95 个可显示字符。

二进制十进制十六进制字符/缩写解释
00000000000NUL (NULL)空字符
00000001101SOH (Start Of Headling)标题开始
00000010202STX (Start Of Text)正文开始
00000011303ETX (End Of Text)正文结束
00000100404EOT (End Of Transmission)传输结束
00000101505ENQ (Enquiry)请求
00000110606ACK (Acknowledge)回应/响应/收到通知
00000111707BEL (Bell)响铃
00001000808BS (Backspace)退格
00001001909HT (Horizontal Tab)水平制表符
00001010100ALF/NL(Line Feed/New Line)换行键
00001011110BVT (Vertical Tab)垂直制表符
00001100120CFF/NP (Form Feed/New Page)换页键
00001101130DCR (Carriage Return)回车键
00001110140ESO (Shift Out)不用切换
00001111150FSI (Shift In)启用切换
000100001610DLE (Data Link Escape)数据链路转义
000100011711DC1/XON (Device Control 1/Transmission On)设备控制1/传输开始
000100101812DC2 (Device Control 2)设备控制2
000100111913DC3/XOFF (Device Control 3/Transmission Off)设备控制3/传输中断
000101002014DC4 (Device Control 4)设备控制4
000101012115NAK (Negative Acknowledge)无响应/非正常响应/拒绝接收
000101102216SYN (Synchronous Idle)同步空闲
000101112317ETB (End of Transmission Block)传输块结束/块传输终止
000110002418CAN (Cancel)取消
000110012519EM (End of Medium)已到介质末端/介质存储已满/介质中断
00011010261ASUB (Substitute)替补/替换
00011011271BESC (Escape)逃离/取消
00011100281CFS (File Separator)文件分割符
00011101291DGS (Group Separator)组分隔符/分组符
00011110301ERS (Record Separator)记录分离符
00011111311FUS (Unit Separator)单元分隔符
001000003220(Space)空格
001000013321!
001000103422"
001000113523#
001001003624$
001001013725%
001001103826&
001001113927
001010004028(
001010014129)
00101010422A*
00101011432B+
00101100442C,
00101101452D-
00101110462E.
00101111472F/
0011000048300
0011000149311
0011001050322
0011001151333
0011010052344
0011010153355
0011011054366
0011011155377
0011100056388
0011100157399
00111010583A:
00111011593B;
00111100603C<
00111101613D=
00111110623E>
00111111633F?
010000006440@
010000016541A
010000106642B
010000116743C
010001006844D
010001016945E
010001107046F
010001117147G
010010007248H
010010017349I
01001010744AJ
01001011754BK
01001100764CL
01001101774DM
01001110784EN
01001111794FO
010100008050P
010100018151Q
010100108252R
010100118353S
010101008454T
010101018555U
010101108656V
010101118757W
010110008858X
010110018959Y
01011010905AZ
01011011915B[
01011100925C\
01011101935D]
01011110945E^
01011111955F_
011000009660`
011000019761a
011000109862b
011000119963c
0110010010064d
0110010110165e
0110011010266f
0110011110367g
0110100010468h
0110100110569i
011010101066Aj
011010111076Bk
011011001086Cl
011011011096Dm
011011101106En
011011111116Fo
0111000011270p
0111000111371q
0111001011472r
0111001111573s
0111010011674t
0111010111775u
0111011011876v
0111011111977w
0111100012078x
0111100112179y
011110101227Az
011110111237B{
011111001247C|
011111011257D}
011111101267E~
011111111277FDEL (Delete)删除
对控制字符的解释

ASCII 编码中第 0~31 个字符(开头的 32 个字符)以及第 127 个字符(最后一个字符)都是不可见的(无法显示),但是它们都具有一些特殊功能,所以称为控制字符( Control Character)或者功能码(Function Code)。

这 33 个控制字符大都与通信、数据存储以及老式设备有关,有些在现代电脑中的含义已经改变了。

有些控制符需要一定的计算机功底才能理解,初学者可以跳过,选择容易的理解即可。

下面列出了部分控制字符的具体功能:

NUL (0)

NULL,空字符。空字符起初本意可以看作为 NOP(中文意为空操作,就是啥都不做的意思),此位置可以忽略一个字符。

之所以有这个空字符,主要是用于计算机早期的记录信息的纸带,此处留个 NUL 字符,意思是先占这个位置,以待后用,比如你哪天想起来了,在这个位置在放一个别的啥字符之类的。

后来呢,NUL 被用于C语言中,表示字符串的结束,当一个字符串中间出现 NUL 时,就意味着这个是一个字符串的结尾了。这样就方便按照自己需求去定义字符串,多长都行,当然只要你内存放得下,然后最后加一个\0,即空字符,意思是当前字符串到此结束。

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,包含一些特殊字符。

4.C语言转义字符

字符集(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 = "\x31\x32\x33\x61\x62\x63";  //字符串"123abc"
char *str2 = "\61\62\63\141\142\143";  //字符串"123abc"
char *str3 = "The string is: \61\62\63\x61\x62\x63"  //混用八进制和十六进制形式

转义字符既可以用于单个字符,也可以用于字符串,并且一个字符串中可以同时使用八进制形式和十六进制形式。

一个完整的例子:

#include <stdio.h>
int main(){
    puts("\x68\164\164\x70://c.biancheng.\x6e\145\x74");
    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"

转义字符示例:

#include <stdio.h>
int main(){
    puts("C\tC++\tJava\n\"C\" first appeared!");
    return 0;
}

运行结果:
C C++ Java
“C” first appeared!

5.在C语言中使用中文字符

大部分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 类型位于 <wchar.h> 头文件中,它使得代码在具有良好移植性的同时,也节省了不少内存,以后我们就用它来存储宽字符。

上节我们讲到,单独的字符由单引号' '包围,例如'B''@''9'等;但是,这样的字符只能使用 ASCII 编码,要想使用宽字符的编码方式,就得加上L前缀,例如L'A'L'9'L'中'L'国'L'。'。

注意,加上L前缀后,所有的字符都将成为宽字符,占用 2 个字节或者 4 个字节的内存,包括 ASCII 中的英文字符。

下面的例子演示了如何存储宽字符(注意引入 <wchar.h> 头文件):

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前缀的宽字符无能为力,我们必须使用 <wchar.h> 头文件中的宽字符输出函数,它们分别是 putwchar 和 wprintf:

  • putwchar 函数专门用来输出一个宽字符,它和 putchar 的用法类似;
  • wprintf 是通用的、格式化的宽字符输出函数,它除了可以输出单个宽字符,还可以输出宽字符串(稍后讲解)。宽字符对应的格式控制符为%lc

另外,在输出宽字符之前还要使用 setlocale 函数进行本地化设置,告诉程序如何才能正确地处理各个国家的语言文化。由于大家基础还不够,关于本地化设置的内容我们不再展开讲解,请大家先记住这种写法。

如果希望设置为中文简体环境,在 Windows 下请写作:

setlocale(LC_ALL, “zh-CN”);

在 Linux 和 Mac OS 下请写作:

setlocale(LC_ALL, “zh_CN”);

setlocale 函数位于 <locale.h> 头文件中,我们必须引入它。

下面的代码完整地演示了宽字符的输出:

#include <wchar.h>
#include <locale.h>
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 %lc\n",  //必须使用宽字符串
        a, b, c, d, e, f, g, h
    );
   
   
    return 0;
}

运行结果:
A9中国。ヅ♥༄
Wide chars: A 9 中 国 。 ヅ ♥ ༄

宽字符串

给字符串加上L前缀就变成了宽字符串,它包含的每个字符都是宽字符,一律采用 UTF-16 或者 UTF-32 编码。输出宽字符串可以使用 <wchar.h> 头文件中的 wprintf 函数,对应的格式控制符是%ls

下面的代码演示了如何使用宽字符串:

#include <wchar.h>
#include <locale.h>
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: %ls\n", 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语言中,字符串总是以'\0'作为结尾,所以'\0'也被称为字符串结束标志,或者字符串结束符。

'\0'是 ASCII 码表中的第 0 个字符,英文称为 NUL,中文称为“空字符”。该字符既不能显示,也没有控制功能,输出该字符不会有任何效果,它在C语言中唯一的作用就是作为字符串结束标志。

C语言在处理字符串时,会从前往后逐个扫描字符,一旦遇到'\0'就认为到达了字符串的末尾,就结束处理。'\0'至关重要,没有'\0'就意味着永远也到达不了字符串的结尾。

" "包围的字符串会自动在末尾添加'\0'。例如,"abc123"从表面看起来只包含了 6 个字符,其实不然,C语言会在最后隐式地添加一个'\0',这个过程是在后台默默地进行的,所以我们感受不到。

下图演示了"C program"在内存中的存储情形:
img

需要注意的是,逐个字符地给数组赋值并不会自动添加'\0',例如:

char str[] = {'a', 'b', 'c'};

数组 str 的长度为 3,而不是 4,因为最后没有'\0'

当用字符数组存储字符串时,要特别注意'\0',要为'\0'留个位置;这意味着,字符数组的长度至少要比字符串的长度大 1。请看下面的例子:

char str[7] = "abc123";

"abc123"看起来只包含了 6 个字符,我们却将 str 的长度定义为 7,就是为了能够容纳最后的'\0'。如果将 str 的长度定义为 6,它就无法容纳'\0'了。

当字符串长度大于数组长度时,有些较老或者不严格的编译器并不会报错,甚至连警告都没有,这就为以后的错误埋下了伏笔,读者自己要多多注意。

有些时候,程序的逻辑要求我们必须逐个字符地为数组赋值,这个时候就很容易遗忘字符串结束标志'\0'。下面的代码中,我们将 26 个大写英文字符存入字符数组,并以字符串的形式输出:

#include <stdio.h>
int main(){
    char str[30];
    char c;
    int i;
    for(c=65,i=0; c<=90; c++,i++){
        str[i] = c;
    }
    printf("%s\n", str);
    return 0;
}

在 VS2015 下的运行结果:

ABCDEFGHIJKLMNOPQRSTUVWXYZ口口口口i口口0 ?

表示无法显示的特殊字符。

大写字母在 ASCII 码表中是连续排布的,编码值从 65 开始,到 90 结束,使用循环非常方便。

在《C语言变量的定义位置以及初始值》一节中我们讲到,在很多编译器下,局部变量的初始值是随机的,是垃圾值,而不是我们通常认为的“零”值。局部数组(在函数内部定义的数组,本例中的 str 数组就是在 main() 函数内部定义的)也有这个问题,很多编译器并不会把局部数组的内存都初始化为“零”值,而是放任不管,爱是什么就是什么,所以它们的值也是没有意义的,也是垃圾值。

在函数内部定义的变量、数组、结构体、共用体等都称为局部数据。在很多编译器下,局部数据的初始值都是随机的、无意义的,而不是我们通常认为的“零”值。这一点非常重要,大家一定要谨记,否则后面会遇到很多奇葩的错误。

本例中的 str 数组在定义完成以后并没有立即初始化,所以它所包含的元素的值都是随机的,只有很小的概率会是“零”值。循环结束以后,str 的前 26 个元素被赋值了,剩下的 4 个元素的值依然是随机的,不知道是什么。

printf() 输出字符串时,会从第 0 个元素开始往后检索,直到遇见'\0'才停止,然后把'\0'前面的字符全部输出,这就是 printf() 输出字符串的原理。本例中我们使用 printf() 输出 str,按理说到了第 26 个元素就能检索到'\0',就到达了字符串的末尾,然而事实却不是这样,由于我们并未对最后 4 个元素赋值,所以第 26 个元素不是'\0',第 27 个也不是,第 28 个也不是……可能到了第 50 个元素才遇到'\0',printf() 把这 50 个字符全部输出出来,就是上面的样子,多出来的字符毫无意义,甚至不能显示。

数组总共才 30 个元素,到了第 50 个元素不早就超出数组范围了吗?是的,的确超出范围了!然而,数组后面依然有其它的数据,printf() 也会将这些数据作为字符串输出。

你看,不注意'\0'的后果有多严重,不但不能正确处理字符串,甚至还会毁坏其它数据。

要想避免这些问题也很容易,在字符串的最后手动添加'\0'即可。修改上面的代码,在循环结束后添加'\0'

#include <stdio.h>
int 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] = '\0';
    printf("%s\n", str);
   
    return 0;
}

第 9 行为新添加的代码,它让字符串能够正常结束。根据 ASCII 码表,字符'\0'的编码值就是 0。

但是,这样的写法貌似有点业余,或者说不够简洁,更加专业的做法是将数组的所有元素都初始化为“零”值,这样才能够从根本上避免问题。再次修改上面的代码:

#include <stdio.h>
int main(){
    char str[30] = {0};  //将所有元素都初始化为 0,或者说 '\0'
    char c;
    int i;
    for(c=65,i=0; c<=90; c++,i++){
        str[i] = c;
    }
    printf("%s\n", str);
   
    return 0;
}

还记得《什么是数组》一节中强调过的吗?如果只初始化部分数组元素,那么剩余的数组元素也会自动初始化为“零”值,所以我们只需要将 str 的第 0 个元素赋值为 0,剩下的元素就都是 0 了。

字符串长度

所谓字符串长度,就是字符串包含了多少个字符(不包括最后的结束符'\0')。例如"abc"的长度是 3,而不是 4。

在C语言中,我们使用string.h头文件中的 strlen() 函数来求字符串的长度,它的用法为:

length strlen(strname);

strname 是字符串的名字,或者字符数组的名字;length 是使用 strlen() 后得到的字符串长度,是一个整数。

下面是一个完整的例子,它输出《C语言入门教程》网址的长度:

#include <stdio.h>
#include <string.h>  //记得引入该头文件
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语言字符串的输入和输出
字符串的输出

在C语言中,有两个函数可以在控制台(显示器)上输出字符串,它们分别是:

  • puts():输出字符串并自动换行,该函数只能输出字符串。
  • printf():通过格式控制符%s输出字符串,不能自动换行。除了字符串,printf() 还能输出其他类型的数据。

这两个函数相信大家已经非常熟悉了,这里不妨再演示一下,请看下面的代码:

#include <stdio.h>
int main(){
    char str[] = "http://c.biancheng.net";
    printf("%s\n", str);  //通过字符串名字输出
    printf("%s\n", "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("%s\n", str[]);
puts(str[10]);

字符串的输入

在C语言中,有两个函数可以让用户从键盘上输入字符串,它们分别是:

  • scanf():通过格式控制符%s输入字符串。除了字符串,scanf() 还能输入其他类型的数据。
  • gets():直接输入字符串,并且只能输入字符串。

但是,scanf() 和 gets() 是有区别的:

  • scanf() 读取字符串时以空格为分隔,遇到空格就认为当前字符串结束了,所以无法读取含有空格的字符串。
  • gets() 认为空格也是字符串的一部分,只有遇到回车键时才认为字符串输入结束,所以,不管输入了多少个空格,只要不按下回车键,对 gets() 来说就是一个完整的字符串。换句话说,gets() 用来读取一整行字符串。

请看下面的例子:

#include <stdio.h>
int 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: %s\n", str1);
    printf("str2: %s\n", str2);
    printf("str3: %s\n", 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 <stdio.h>
//自定义函数,用来输出数组元素
void display_array(int arr[], int len){
    int i;
    for(i=0; i<len; i++){
        printf("%d ", arr[i]);
    }
    printf("\n");
}
int main()
{
    int nums[10] = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10};
    int nums_new1[9];
    int nums_new2[11];
    int i;
   
    //删除nums第6个元素
    for(i=0; i<10; i++){
        //i等于6时直接跳过,不进行任何操作
        if(i < 6){
            nums_new1[i] = nums[i];
        }else if(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 <stdio.h>
#include <string.h>
int main(){
    char str[] = "http://c.biancheng.net";
    int len = strlen(str), i;
    //直接输出字符串
    printf("%s\n", str);
    //每次输出一个字符
    for(i=0; i<len; i++){
        printf("%c", str[i]);
    }
    printf("\n");
    return 0;
}

运行结果:
http://c.biancheng.net
http://c.biancheng.net

字符数组归根结底还是一个数组,上节讲到的关于指针和数组的规则同样也适用于字符数组。更改上面的代码,使用指针的方式来输出字符串:

#include <stdio.h>
#include <string.h>
int main(){
    char str[] = "http://c.biancheng.net";
    char *pstr = str;
    int len = strlen(str), i;
    //使用*(pstr+i)
    for(i=0; i<len; i++){
        printf("%c", *(pstr+i));
    }
    printf("\n");
    //使用pstr[i]
    for(i=0; i<len; i++){
        printf("%c", pstr[i]);
    }
    printf("\n");
    //使用*(str+i)
    for(i=0; i<len; i++){
        printf("%c", *(str+i));
    }
    printf("\n");
    return 0;
}

运行结果:
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 <stdio.h>
#include <string.h>
int main(){
    char *str = "http://c.biancheng.net";
    int len = strlen(str), i;
   
    //直接输出字符串
    printf("%s\n", str);
    //使用*(str+i)
    for(i=0; i<len; i++){
        printf("%c", *(str+i));
    }
    printf("\n");
    //使用str[i]
    for(i=0; i<len; i++){
        printf("%c", str[i]);
    }
    printf("\n");
    return 0;
}

运行结果:
http://c.biancheng.net
http://c.biancheng.net
http://c.biancheng.net

这一切看起来和字符数组是多么地相似,它们都可以使用%s输出整个字符串,都可以使用*[ ]获取单个字符,这两种表示字符串的方式是不是就没有区别了呢?

有!它们最根本的区别是在内存中的存储区域不一样,字符数组存储在全局数据区或栈区,第二种形式的字符串存储在常量区。全局数据区和栈区的字符串(也包括其他数据)有读取和写入的权限,而常量区的字符串(也包括其他数据)只有读取权限,没有写入权限。

内存权限的不同导致的一个明显结果就是,字符数组在定义后可以读取和修改每个字符,而对于第二种形式的字符串,一旦被定义后就只能读取不能修改,任何对它的赋值都是错误的。

我们将第二种形式的字符串称为字符串常量,意思很明显,常量只能读取不能写入。请看下面的演示:

#include <stdio.h>
int main(){
    char *str = "Hello World!";
    str = "I love C!";  //正确
    str[3] = 'P';  //错误
    return 0;
}

这段代码能够正常编译和链接,但在运行时会出现段错误(Segment Fault)或者写入位置错误。

第4行代码是正确的,可以更改指针变量本身的指向;第5行代码是错误的,不能修改字符串中的字符。

到底使用字符数组还是字符串常量

在编程过程中如果只涉及到对字符串的读取,那么字符数组和字符串常量都能够满足要求;如果有写入(修改)操作,那么只能使用字符数组,不能使用字符串常量。

获取用户输入的字符串就是一个典型的写入操作,只能使用字符数组,不能使用字符串常量,请看下面的代码:

#include <stdio.h>
int main(){
    char str[30];
    gets(str);
    printf("%s\n", str);
    return 0;
}

运行结果:
C C++ Java Python JavaScript
C C++ Java Python JavaScript

最后我们来总结一下,C语言有两种表示字符串的方法,一种是字符数组,另一种是字符串常量,它们在内存中的存储位置不同,使得字符数组可以读取和修改,而字符串常量只能读取不能修改。

2.初始化
逐个字符初始化

当定义一个字符数组时,可以采用逐个字符初始化的方式:

char str[10]={ 'h','e','l','l','o'};

当显示指定的字符不足字符数组的长度时,编译器将剩余字符置为空字符’\0’。

字符串常量来初始化字符数组

在C语言中,将字符串作为字符数组来处理,因此可以使用字符串来初始化字符数组。

char str[]={"hello"};

也可以省略花括号。

char str[10]="hello";

不及字符数组长度时,剩余字符置为空字符’\0’。因此,我们不难得出,当为一个字符数组初始化为空字符数组的做法有如下几种:

char test1[256]="";
char test2[256]={""};
char test3[256]={0};
char test3[256]={'\0'};
使用字符指针

因为字符指针只是一个指针,指向一块连续的内存,自己本身没有申请内存空间,因此只能将其赋值指向一个字符串。

使用字符指针来访问一个字符串,通过字符指针指向存放字符串数组的首元素地址来进行访问.

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<sizeof(str):++i)
    str[i]='\0';

(2)使用memset()赋值,较for循环高效率,建议使用。当然为字符数组置空应该在初始化时完成,不应该再多次一举。

char str[10];
memset(str,0,sizeof(str));
拷贝赋值

利用已有的字符串,通过memcpy,strcpy或者strncpy等函数实现拷贝赋值,参考代码如下:

char str[10];
char str2[]="hello";
memcpy(str,str2,sizeof(str2));
strcpy(str,str2);
strncpy(str,str2,strlen(str2)+1);

三、C++ string类(C++字符串)完全攻略

string 类是 STL 中 basic_string 模板实例化得到的模板类。其定义如下:

typedef basic_string string;

basic_string 此处可以不必深究。

string 类的成员函数有很多,同一个名字的函数也常会有五六个重载的版本。篇幅所限,不能将这些原型一一列出并加以解释。这里仅对常用成员函数按功能进行分类,并直接给出应用的例子,通过例子,读者可以基本掌握这些成员函数的用法。

要想更深入地了解 string 类,还要阅读 C++ 的参考手册或编译器自带的联机资料。对于前面介绍过的字符串处理的内容,这里不再重复说明。

1. 构造函数

string 类有多个构造函数,用法示例如下:

string s1();  // si = ""
string s2("Hello");  // s2 = "Hello"
string s3(4, 'K');  // s3 = "KKKK"
string s4("12345", 1, 3);  //s4 = "234",即 "12345" 的从下标 1 开始,长度为 3 的子串

为称呼方便,本教程后文将从字符串下标 n 开始、长度为 m 的字符串称为“子串(n, m)”。

string 类没有接收一个整型参数或一个字符型参数的构造函数。下面的两种写法是错误的:

string s1('K');
string s2(123);

2. 对 string 对象赋值

可以用 char* 类型的变量、常量,以及 char 类型的变量、常量对 string 对象进行赋值。例如:

string s1;
s1 = "Hello";  // s1 = "Hello"
s2 = 'K';  // s2 = "K”

string 类还有 assign 成员函数,可以用来对 string 对象赋值。assign 成员函数返回对象自身的引用。例如:

string s1("12345"), s2;
s3.assign(s1);  // s3 = s1
s2.assign(s1, 1, 2);  // s2 = "23",即 s1 的子串(1, 2)
s2.assign(4, 'K');  // s2 = "KKKK"
s2.assign("abcde", 2, 3);  // s2 = "cde",即 "abcde" 的子串(2, 3)

3. 求字符串的长度

length 成员函数返回字符串的长度。size 成员函数可以实现同样的功能。

4. string对象中字符串的连接

除了可以使用++=运算符对 string 对象执行字符串的连接操作外,string 类还有 append 成员函数,可以用来向字符串后面添加内容。append 成员函数返回对象自身的引用。例如:

string s1("123"), s2("abc");
s1.append(s2);  // s1 = "123abc"
s1.append(s2, 1, 2);  // s1 = "123abcbc"
s1.append(3, 'K');  // s1 = "123abcbcKKK"
s1.append("ABCDE", 2, 3);  // s1 = "123abcbcKKKCDE",添加 "ABCDE" 的子串(2, 3)

5. string对象的比较

除了可以用 <、<=、==、!=、>=、> 运算符比较 string 对象外,string 类还有 compare 成员函数,可用于比较字符串。compare 成员函数有以下返回值:

  • 小于 0 表示当前的字符串小;
  • 等于 0 表示两个字符串相等;
  • 大于 0 表示另一个字符串小。

例如:

string s1("hello"), s2("hello, world");
int n = s1.compare(s2);
n = s1.compare(1, 2, s2, 0, 3);  //比较s1的子串 (1,2) 和s2的子串 (0,3)
n = s1.compare(0, 2, s2);  // 比较s1的子串 (0,2) 和 s2
n = s1.compare("Hello");
n = s1.compare(1, 2, "Hello");  //比较 s1 的子串(1,2)和"Hello”
n = s1.compare(1, 2, "Hello", 1, 2);  //比较 s1 的子串(1,2)和 "Hello" 的子串(1,2)

6. 求 string 对象的子串

substr 成员函数可以用于求子串 (n, m),原型如下:

string substr(int n = 0, int m = string::npos) const;

调用时,如果省略 m 或 m 超过了字符串的长度,则求出来的子串就是从下标 n 开始一直到字符串结束的部分。例如:

string s1 = "this is ok";
string s2 = s1.substr(2, 4);  // s2 = "is i"
s2 = s1.substr(2);  // s2 = "is is ok"

7. 交换两个string对象的内容

swap 成员函数可以交换两个 string 对象的内容。例如:

string s1("West”), s2("East");
s1.swap(s2);  // s1 = "East",s2 = "West"

8. 查找子串和字符

string 类有一些查找子串和字符的成员函数,它们的返回值都是子串或字符在 string 对象字符串中的位置(即下标)。如果查不到,则返回 string::npos。string: :npos 是在 string 类中定义的一个静态常量。这些函数如下:

  • find:从前往后查找子串或字符出现的位置。
  • rfind:从后往前查找子串或字符出现的位置。
  • find_first_of:从前往后查找何处出现另一个字符串中包含的字符。例如:
  • s1.find_first_of(“abc”); //查找s1中第一次出现"abc"中任一字符的位置
  • find_last_of:从后往前查找何处出现另一个字符串中包含的字符。
  • find_first_not_of:从前往后查找何处出现另一个字符串中没有包含的字符。
  • find_last_not_of:从后往前查找何处出现另一个字符串中没有包含的字符。

下面是 string 类的查找成员函数的示例程序。

#include <iostream>
#include <string>
using namespace std;
int main()
{
    string s1("Source Code");
    int n;
    if ((n = s1.find('u')) != string::npos) //查找 u 出现的位置
        cout << "1) " << n << "," << s1.substr(n) << endl;
    //输出 l)2,urce Code
    if ((n = s1.find("Source", 3)) == string::npos)
        //从下标3开始查找"Source",找不到
        cout << "2) " << "Not Found" << endl;  //输出 2) Not Found
    if ((n = s1.find("Co")) != string::npos)
        //查找子串"Co"。能找到,返回"Co"的位置
        cout << "3) " << n << ", " << s1.substr(n) << endl;
    //输出 3) 7, Code
    if ((n = s1.find_first_of("ceo")) != string::npos)
        //查找第一次出现或 'c'、'e'或'o'的位置
        cout << "4) " << n << ", " << s1.substr(n) << endl;
    //输出 4) l, ource Code
    if ((n = s1.find_last_of('e')) != string::npos)
        //查找最后一个 'e' 的位置
        cout << "5) " << n << ", " << s1.substr(n) << endl;  //输出 5) 10, e
    if ((n = s1.find_first_not_of("eou", 1)) != string::npos)
        //从下标1开始查找第一次出现非 'e'、'o' 或 'u' 字符的位置
        cout << "6) " << n << ", " << s1.substr(n) << endl;
    //输出 6) 3, rce Code
    return 0;
}

9. 替换子串

replace 成员函数可以对 string 对象中的子串进行替换,返回值为对象自身的引用。例如:

string s1("Real Steel");
s1.replace(1, 3, "123456", 2, 4);  //用 "123456" 的子串(2,4) 替换 s1 的子串(1,3)
cout << s1 << endl;  //输出 R3456 Steel
string s2("Harry Potter");
s2.replace(2, 3, 5, '0');  //用 5 个 '0' 替换子串(2,3)
cout << s2 << endl;  //输出 HaOOOOO Potter
int n = s2.find("OOOOO");  //查找子串 "00000" 的位置,n=2
s2.replace(n, 5, "XXX");  //将子串(n,5)替换为"XXX"
cout << s2 < < endl;  //输出 HaXXX Potter

10. 删除子串

erase 成员函数可以删除 string 对象中的子串,返回值为对象自身的引用。例如:

string s1("Real Steel");
s1.erase(1, 3);  //删除子串(1, 3),此后 s1 = "R Steel"
s1.erase(5);  //删除下标5及其后面的所有字符,此后 s1 = "R Ste"

11. 插入字符串

insert 成员函数可以在 string 对象中插入另一个字符串,返回值为对象自身的引用。例如:

string s1("Limitless"), s2("00");
s1.insert(2, "123");  //在下标 2 处插入字符串"123",s1 = "Li123mitless"
s1.insert(3, s2);  //在下标 2 处插入 s2 , s1 = "Li10023mitless"
s1.insert(3, 5, 'X');  //在下标 3 处插入 5 个 'X',s1 = "Li1XXXXX0023mitless"

12. 将 string 对象作为流处理

使用流对象 istringstream 和 ostringstream,可以将 string 对象当作一个流进行输入输出。使用这两个类需要包含头文件 sstream。

示例程序如下:

#include <iostream>
#include <sstream>
#include <string>
using namespace std;
int main()
{
    string src("Avatar 123 5.2 Titanic K");
    istringstream istrStream(src); //建立src到istrStream的联系
    string s1, s2;
    int n;  double d;  char c;
    istrStream >> s1 >> n >> d >> s2 >> c; //把src的内容当做输入流进行读取
    ostringstream ostrStream;
    ostrStream << s1 << endl << s2 << endl << n << endl << d << endl << c <<endl;
    cout << ostrStream.str();
    return 0;
}

程序的输出结果是:
Avatar
Titanic
123
5.2
K

第 11 行,从输入流 istrStream 进行读取,过程和从 cin 读取一样,只不过输入的来源由键盘变成了 string 对象 src。因此,“Avatar” 被读取到 s1,123 被读取到 n,5.2 被读取到 d,“Titanic” 被读取到s2,‘K’ 被读取到 c。

第 12 行,将变量的值输出到流 ostrStream。输出结果不会出现在屏幕上,而是被保存在 ostrStream 对象管理的某处。用 ostringstream 类的 str 成员函数能将输出到 ostringstream 对象中的内容提取出来。

13. 用 STL 算法操作 string 对象

string 对象也可以看作一个顺序容器,它支持随机访问迭代器,也有 begin 和 end 等成员函数。STL 中的许多算法也适用于 string 对象。下面是用 STL 算法操作 string 对象的程序示例。

#include <iostream>
#include <algorithm>
#include <string>
using namespace std;
int main()
{
    string s("afgcbed");
    string::iterator p = find(s.begin(), s.end(), 'c');
    if (p!= s.end())
        cout << p - s.begin() << endl;  //输出 3
    sort(s.begin(), s.end());
    cout << s << endl;  //输出 abcdefg
    next_permutation(s.begin(), s.end());
    cout << s << endl; //输出 abcdegf
    return 0;
}

四、字符数组、字符指针和string对象之间的转换

在C/C++中,我们可以用字符数组和字符指针来表示字符串。

在C++中,我们可以同时使用字符数组、字符指针和string对象来表示字符串,因此有时我们需要在这几种字符串的表示方法中切换。

字符数组转化成string类型

char ch [] = "ABCDEFG";
string str(ch);//也可string str = ch;
或者
char ch [] = "ABCDEFG";
string str;
str = ch;//在原有基础上添加可以用str += ch;

可以直接转换

将string类型转换为字符数组

char buf[10];
string str("ABCDEFG");
length = str.copy(buf, 9);
buf[length] = '\0';
或者
char buf[10];
string str("ABCDEFG");
strcpy(buf, str.c_str());//strncpy(buf, str.c_str(), 10);

实验 : 字符串、数组、指针三者转换

#include <iostream>
#include <string>
using namespace std;

int main()
{ 
	string str1= "String";
	char ch[]={"charactar"};
	char *p = (char *)"Point";    //强制转换赋值

	str1 = ch;				    //字符串与数组 
	cout << str1 << endl ;
	str1= "String";
	//ch = str1;				//错误,不能直接赋值

	str1 = *p ;				    //字符串与指针
	cout << str1 << endl;	    //
	//p = str1;

	p = ch;						//数组与指针
	cout << *p <<endl;		    //只输出ch首地址的内的c字符,需要调用循环
	//ch = p;

	return 0;
}

输出:

character

p

c


更加详细的可以参考这篇博客:c++ 中 char 与 string 之间的相互转换问题

五、字符操作函数,内存操作函数详解

这个可以参考我之前的博客:字符串,str函数,mem函数,文件操作大全(全网最全)

  • 11
    点赞
  • 20
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

小熊coder

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值