字符串是C语言中最有用、 最重要的数据类型之一。
C 库提供大量的函数用于读写字符串、拷贝字符串、 比较字符串、 合并字符串、 查找字符串等。
表示字符串和字符串I/O
字符串是以空字符(\0) 结尾的char类型数组。
和printf()函数一样, puts()函数也属于stdio.h系列的输入/输出函数。 但是, 与printf()不同的是, puts()函数只显示字符串, 而且自动在显示的字符串末尾加上换行符。
在程序中定义字符串
字符串字面量( 字符串常量)用双引号括起来的内容称为字符串字面量(string literal) , 也叫作字符串常量(string constant) 。 双引号中的字符和编译器自动加入末尾的\0字符, 都作为字符串储存在内存中。
从ANSI C标准起, 如果字符串字面量之间没有间隔, 或者用空白字符分隔, C会将其视为串联起来的字符串字面量。
如果要在字符串内部使用双引号, 必须在双引号前面加上一个反斜杠(\)
字符串常量属于静态存储类别(static storage class) , 这说明如果在函数中使用字符串常量, 该字符串只会被储存一次, 在整个程序的生命期内存在, 即使函数被调用多次。 用双引号括起来的内容被视为指向该字符串储存位置的指针。 这类似于把数组名作为指向该数组位置的指针。
rintf()根据%s 转换说明打印 We, 根据%p 转换说明打印一个地址。 因此, 如果"are"代表一个地址, printf()将打印该字符串首字符的地址(如果使用ANSI之前的实现, 可能要用%u或%lu代替%p)。
字符串数组和初始化
定义字符串数组时, 必须让编译器知道需要多少空间。 一种方法是用足够空间的数组储存字符串。
在指定数组大小时, 要确保数组的元素个数至少比字符串长度多1(为了容纳空字符) 。 所有未被使用的元素都被自动初始化为0(这里的0指的是char形式的空字符, 不是数字字符0)。
编译器确定初始化字符数组的大小,处理字符串的函数通常都不知道数组的大小, 这些函数通过查找字符串末尾的空字符确定字符串在何处结束。
声明数组时, 数组大小必须是可求值的整数。 在C99新增变长数组之前, 数组的大小必须是整型常量, 包括由整型常量组成的表达式。
字符数组名和其他数组名一样, 是该数组首元素的地址。带双引号的字符串本身决定了预留给字符串的存储空间。
数组和指针
数组形式(ar1[])在计算机的内存中分配为一个内含29个元素的数组(每个元素对应一个字符, 还加上一个末尾的空字符'\0') , 每个元素被初始化为字符串字面量对应的字符。 通常, 字符串都作为可执行文件的一部分储存在数据段中。 当把程序载入内存时, 也载入了程序中的字符串。 字符串储存在静态存储区(static memory) 中。 但是, 程序在开始运行时才会为该数组分配内存。
编译器便把数组名ar1识别为该数组首元素地址(&ar1[0]) 的别名。在数组形式中, ar1是地址常量。 不能更改ar1, 如果改变了ar1, 则意味着改变了数组的存储位置(即地址)。
指针形式(*pt1) 也使得编译器为字符串在静态存储区预留29个元素的空间。 另外, 一旦开始执行程序, 它会为指针变量pt1留出一个储存位置,并把字符串的地址储存在指针变量中。 该变量最初指向该字符串的首字符,但是它的值可以改变。 因此, 可以使用递增运算符。
字符串字面量被视为const数据。 由于pt1指向这个const数据, 所以应该把pt1声明为指向const数据的指针。 这意味着不能用pt1改变它所指向的数据, 但是仍然可以改变pt1的值(即, pt1指向的位置) 。
初始化数组把静态存储区的字符串拷贝到数组中, 而初始化指针只把字符串的地址拷贝给指针。
第一, pt和MSG的地址相同, 而ar的地址不同, 这与我们前面讨论的内容一致。 第二, 虽然字符串字面量"I'm special"在程序的两个 printf()函数中出现了两次, 但是编译器只使用了一个存储位置, 而且与MSG的地址相同。编译器可以把多次使用的相同字面量储存在一处或多处。 另一个编译器可能在不同的位置储存3个"I'm special"。第三, 静态数据使用的内存与ar使用的动态内存不同。 不仅值不同, 特定编译器甚至使用不同的位数表示两种内存。
数组和指针的区别
char heart[] = "I love Tillie!";
const char *head = "I love Millie!";
两者主要的区别是: 数组名heart是常量, 而指针名head是变量。
两者都可以使用数组表示法;两者都能进行指针加法操作;但是, 只有指针表示法可以进行递增操作赋值运算符的左侧必须是变量(或概括地说是可修改的左值)
另外, 还可以改变heart数组中元素的信息。数组的元素是变量(除非数组被声明为const) , 但是数组名不是变量。
如果不修改字符串, 不要用指针指向字符串字面量。
字符串数组
从某些方面来看, mytalents和yourtalents非常相似。 两者都代表5个字符串。 使用一个下标时都分别表示一个字符串, 如mytalents[0]和yourtalents[0]; 使用两个下标时都分别表示一个字符。
它们也有区别。 mytalents数组是一个内含5个指针的数组, 在系统中共占用40字节。 而yourtalents是一个内含5个数组的数组, 每个数组内含40个char类型的值, 共占用200字节。 所以, 虽然mytalents[0]和yourtalents[0]都分别表示一个字符串。
mytalents和yourtalents的类型并不相同。 mytalents中的指针指向初始化时所用的字符串字面量的位置, 这些字符串字面量被储存在静态内存中; 而 yourtalents 中的数组则储存着字符串字面量的副本, 所以每个字符串都被储存了两次。
mytalents想象成不规则的数组, 每行的长度不同。mytalents 数组的指针元素所指向的字符串不必储存在连续的内存中。
如果要用数组表示一系列待显示的字符串, 请使用指针数组, 因为它比二维字符数组的效率高,mytalents 中的指针指向的字符串字面量不能更改; 而yourtalentsde 中的内容可以更改。
指针和字符串
mesg和copy都以字符串形式输出(%s转换说明)。第二项打印两个指针的地址。最后一项, 显示两个指针的值。 所谓指针的值就是它储存的地址。
字符串输入
如果想把一个字符串读入程序, 首先必须预留储存该字符串的空间, 然后用输入函数获取该字符串。
分配空间
分配空间, 以储存稍后读入的字符串。要为字符串分配足够的空间。
char *name;
scanf("%s", name);
scanf()要把信息拷贝至参数指定的地址上, 而此时该参数是个未初始化的指针, name可能会指向任何地方。在声明时显式指明数组的大小。
为字符串分配内存后, 便可读入字符串。 C 库提供了许多读取字符串的函数: scanf()、 gets()和fgets()。、
不幸的gets()函数
在读取字符串时, scanf()和转换说明%s只能读取一个单词。 gets()函数简单易用, 它读取整行输入, 直至遇到换行符, 然后丢弃换行符, 储存其余字符, 并在这些字符的末尾添加一个空字符使其成为一个 C 字符串。 它经常和 puts()函数配对使用, 该函数用于显示字符串, 并在末尾添加换行符。
整行输入(除了换行符) 都被储存在 words 中, puts(words)和printf("%s\n, words")的效果相同。
gets()唯一的参数是 words, 它无法检查数组是否装得下输入行。
数组名会被转换成该数组首元素的地址, 因此, gets()函数只知道数组的开始处, 并不知道数组中有多少个元素。
如果输入的字符串过长, 会导致缓冲区溢出(buffer overflow) , 即多余的字符超出了指定的目标空间。
C11标准委员会采取了更强硬的态度, 直接从标准中废除了gets()函数。 既然标准已经发布, 那么编译器就必须根据标准来调整支持什么, 不支持什么。 然而在实际应用中, 编译器为了能兼容以前的代码, 大部分都继续支持gets()函数。
gtes()的代替品
C11标准新增的gets_s()函数也可代替gets()。 该函数与gets()函数更接近, 而且可以替换现有代码中的gets()。
fgets()函数( 和fputs())
fgets()函数通过第2个参数限制读入的字符数来解决溢出的问题。 该函数专门设计用于处理文件输入
fgets()函数的第2个参数指明了读入字符的最大数量。如果fgets()读到一个换行符, 会把它储存在字符串中。 fgets()函数的第3 个参数指明要读入的文件。
因为 fgets()函数把换行符放在字符串的末尾(假设输入行不溢出) , 通常要与 fputs()函数(和puts()类似) 配对使用, 除非该函数不在字符串末尾添加换行符。
第1行输入, apple pie, 比fgets()读入的整行输入短, 因此, apple pie\n\0被储存在数组中。 所以当puts()显示该字符串时又在末尾添加了换行符, 因此apple pie后面有一行空行。 因为fputs()不在字符串末尾添加换行符, 所以并未打印出空行。
第2行输入, strawberry shortcake, 超过了大小的限制, 所以fgets()只读入了13个字符, 并把strawberry sh\0 储存在数组中。
fputs()函数返回指向 char的指针。
系统使用缓冲的I/O。 这意味着用户在按下Return键之前, 输入都被储存在临时存储区(即, 缓冲区) 中。 按下Return键就在输入中增加了一个换行符, 并把整行输入发送给fgets()。 对于输出, fputs()把字符发送给另一个缓冲区, 当发送换行符时, 缓冲区中的内容被发送至屏幕上。
处理掉空字符
一个方法是在已储存的字符串中查找换行符, 并将其替换成空字符
一个可行的办法是, 如果目标数组装不下一整行输入, 就丢弃那些多出的字符。
空字符(或'\0') 是用于标记C字符串末尾的字符, 其对应字符编码是0。 由于其他字符的编码不可能是 0, 所以不可能是字符串的一部分。
空指针(或NULL) 有一个值, 该值不会与任何数据的有效地址对应。通常, 函数使用它返回一个有效地址表示某些特殊情况发生,
空字符是整数类型, 而空指针是指针类型。它们都可以用数值0来表示。 但是, 从概念上看, 两者是不同类型的0。另外, 空字符是一个字符, 占1字节; 而空指针是一个地址, 通常占4字节。
gets_s()函数
C11新增的gets_s()函数(可选) 和fgets()类似, 用一个参数限制读入的字符数。
gets_s()只从标准输入中读取数据, 所以不需要第3个参数。如果gets_s()读到换行符, 会丢弃它而不是储存它。
如果gets_s()读到最大字符数都没有读到换行符, 会执行以下几步。 首先把目标数组中的首字符设置为空字符, 读取并丢弃随后的输入直至读到换行符或文件结尾, 然后返回空指针。 接着, 调用依赖实现的“处理函数”(或你选择的其他函数) , 可能会中止或退出程序。
只要输入行未超过最大字符数, gets_s()和gets()几乎一样, 完全可以用gets_s()替换gets()。
当输入与预期不符时, gets_s()完全没有fgets()函数方便、 灵活。也许这也是gets_s()只作为C库的可选扩展的原因之一。
s_gets()函数
scanf()函数
scanf()和gets()或fgets()的区别在于它们如何确定字符串的末尾:scanf()更像是“获取单词”函数, 而不是“获取字符串”函数; 如果预留的存储区装得下输入行, gets()和fgets()会读取第1个换行符之前所有的字符。
scanf()函数有两种方法确定输入结束。 无论哪种方法, 都从第1个非空白字符作为字符串的开始。 如果使用%s转换说明, 以下一个空白字符(空行、空格、 制表符或换行符) 作为字符串的结束(字符串不包括空白字符)。
scanf()函数返回一个整数值, 该值等于scanf()成功读取的项数或EOF(读到文件结尾时返回EOF) 。
第1个输出示例, 两个名字的字符个数都未超过字段宽度。第2次调用scanf()时, 从上一次调用结束的地方继续读取数据。
根据输入数据的性质, 用fgets()读取从键盘输入的数据更合适。
scanf()和gets()类似, 也存在一些潜在的缺点。 如果输入行的内容过长,scanf()也会导致数据溢出。 不过, 在%s转换说明中使用字段宽度可防止溢出。