文章目录
字符串和字符串函数
11.1表示字符串和字符串I/O
11.1.1在程序中定义字符串
字符串字面量(字符串常量)
用双引号括起来的内容称为字符串字面量,也叫作字符串常量。双引号中的字符和编译器自动加入末尾的
\0
字符,都作为字符串储存在内存中。
从ANSI C标准起,如果字符串字面量之间没有间隔,或者空白字符分隔,c会将其视为串联起来的字符串字面量。例如:
char greeting[50] = "Hello, and"" how are" " you"
" today!";
// 等价于
char greeting[50] = "Hello, and how are you today!";
字符串常量属于静态存储类别,这说明如果在函数中使用字符串常量,该字符串只会被储存一次,在整个程序的生命周期内存在,即使函数被调用多次。用双引号括起来的内容被视为指向该字符串储存位置的指针。这类似于把数组名作为指向该数组位置的指针。
字符串数组和初始化
在指定数组大小时,要确保数组的元素个数至少比字符串长度多1(为了容纳空字符)。所有未被使用的元素都被自动初始化为0(这里的0指的是
char
形式的空字符,不是数字字符0):
通常,让编译器确定数组的大小很方便:
const char m2[] = "If you can't think of anything, fake it.";
还可以使用指针表示法创建字符串:
const char *pt1 = "Something is pointing at me.";
// 等价于
const char ar1[] = "Something is pointing at me.";
数组和指针
数组形式在计算机的内存中分配为一个内含29个元素的数组(每个元素对应一个字符,还加上一个末尾的空字符
'\0'
),每个元素被初始化为字符串字面量对应的字符。通常,字符串都作为可执行文件的一部分储存在数据段中。当把程序载入内存时,也载入了程序中的字符串。字符串储存在静态存储区中。但是,程序在开始运行时才会为该数组分配内存。此时,才将字符串拷贝到数组中。注意,此时字符串有两个副本。一个是在静态内存中的字符串字面量,另一个是储存在数组中的字符串。
此后,编译器便把数组名识别为该数组首元素地址的别名。这里关键要理解,在数组形式中,例如const char ar1[]
,ar1
是地址常量。不能更改ar1
,如果改变了ar1
,则意味着改变了数组的内存位置(即地址)。因此,不允许进行++ar1
这样的操作。
指针形式也使得编译器为字符串在静态存储区预留相应的空间。另外,一旦开始执行程序,它会为指针变量留出一个储存位置,并把字符串的地址储存在指针变量中。该变量最初指向该字符串的首字符,但是它的值可以改变。因此,可以使用递增运算符。例如,++pt1
将指向第2个字符。
字符串字面量被视为const
数据。当指针指向这个const
数据时,应该把指针声明为指向const
数据的指针。这意味着不能用指针改变它所指向的数据,但是仍然可以改变指针的值(即,指针指向的位置)。如果把一个字符串字面量拷贝给一个数组,就可以随意改变数据,除非把数组声明为const
。
总之,初始化数组把静态存储区的字符串拷贝到数组中,而初始化指针只是把字符串的地址拷贝给指针。
数组和指针的区别
char heart[] = "I love Tillie!";
const char *head = "I love Millie!";
两者主要的区别是:数组名是常量,但是数组的元素变量(除非数组被声明为
const
),而指针名则是变量。
另一方面:
char *word = "frame";
word[1] = '1'; // 是否允许?
编译器可能允许这样做,但是对当前的c标准而言,这样的行为是未定义的。例如,这样的语句可能导致内存访问错误,原因在于编译器可以使用内存中的一个副本来表示所有完全相同的字符串字面量。
char *p1 = "Klingon";
p1[0] = 'F'; // ok?
printf("Klingon");
printf(": Beware the %ss!\n ", "Klingon");
也就是说,编译器可以用相同的地址替换每个
"Klingon"
实例。如果编译器使用这种单词副本表示法,并允许p1[0] = 'F'
,那将影响所有使用该字符串的代码。
因此,建议在把指针初始化为字符串字面量时使用const
限定符。然而,把非const
数组初始化为字符串字面量却不会导致类似的问题。因为数组获得的是原始字符串的副本。
字符串数组
const char *mytalents[LIM] = {
"Adding numbers swiftly",
"Multiplying accurately",
"Stashing data",
"Following instructions to the letter",
"Understanding the C language"
};
char yourtalents[LIM][SLEN] = {
"Walking in a straight line",
"Sleeping",
"Watching television",
"Mailing letters",
"Reading email"
};
两者非常相似,但是也有区别,例如,
mytalents
是一个内含5个指针的数组,在示例系统中共占用40字节,而yourtalents
是一个内含5个数组的数组,共占用200字节。
另一方面,mytalents
中的指针指向初始化时所用的字符串字面量的位置,这些字符串字面量被储存在静态内存中。而yourtalents
中的数组则储存着字符串字面量的副本,所以每个字符串都被储存了两次。此外,为字符串数组分配内存的使用率较低。yourtalents
中的每个元素的大小必须相同,而且必须是能储存最长字符串的大小。
因此,如果要用数组表示一系列待显示的字符串,请使用指针数组,因此它比二维字符数组的效率高。但是,缺点在于不能更改。
11.1.2指针和字符串
const char *mesg = "Don't be a fool!";
const char *copy;
// 为何不拷贝整个字符串?假设有50个元素,考虑一下哪种方法更效率:拷贝一个地址还是拷贝整个数组?
// 通常,程序要完成某项操作只需要知道地址就可以了。如果确实要拷贝整个数组,可以使用strcpy()或
// strncpy()函数。
copy = mesg;
11.2字符串输入
gets()
gets()
函数读取整行输入,直至遇到换行符,然后丢弃换行符,储存其余字符,并在这些字符的末尾添加一个空字符使其成为一个c字符串。它经常和puts()
函数配对使用,该函数用于显示字符串。并在末尾添加换行符。
gets()
的问题在于,其只有一个参数,只知道数组的开始处,并不知道数组中有多少个元素。因此无法检查数组是否装得下输入行。
此时,如果输入的字符串过长,会导致缓冲区溢出,即多余的字符超出了指定的目标空间。如果这些多余的字符只是占用了尚未使用的内存,就不会立即出现问题;如果它们擦写掉程序中的其他数据,会导致程序异常终止;或者还有其他情况。
fgets()/fputs()
fgets()
函数通过第2个参数限制读入的字符数来解决溢出的问题。fgets()
和gets()
的区别:
fgets()
函数的第2个参数指明了读入字符的最大数量。如果该参数的值是n
,那么fgets()
将读入n - 1
个字符,或者读到遇到的第一个换行符为止。- 如果
fgets()
读到一个换行符,会把它储存在字符串中,而gets()
会丢弃换行符。fgets()
函数的第3个参数指明要读入的文件。如果读入从键盘输入的数据,则以stdin
(标准输入)作为参数,该标识符定义在stdio.h
中。
scanf()
scanf()
更像是获取单词的函数。从第1个非空白字符作为字符串的开始,如果使用%s
转换说明,以下一个空白字符(空行、空格、制表符或换行符)作为字符串的结束。
11.3字符串输出
puts()
puts()
函数很容易使用,只需把字符串的地址作为参数传递给它即可,其在显示字符串时会自动在末尾添加一个换行符。需要注意的是,该函数在遇到空字符时就停止输出,所以必须确保有空字符。
fputs()
fputs()
函数是puts()
针对文件定制的版本。两者的区别:
fputs()
函数的第2个参数指明了要写入数据的文件。如果要打印在显示器上,可以用定义在stdio.h
中的stdout
(标准输出)作为该参数。- 与
puts()
不同,fputs()
不会在输出的末尾添加换行符。
11.5字符串函数
常用的字符串函数:
char *strcpy(char *restrict s1, const char *restrict s2);
:该函数把s2
指向的字符串(包括空字符)拷贝至s1
指向的位置,返回值是s1
。char *strncpy(char *restrict s1, const char *restrict s2, size_t n);
:该函数把s2
指向的字符串拷贝至s1
指向的位置,拷贝的字符数不超过n
,其返回值是s1
。该函数不会拷贝空字符后面的字符,如果源字符串的字符少于n
个,目标字符串就以拷贝的空字符结尾;如果源字符串有n
个或超过n
个字符,就不拷贝空字符。char *strcat(char *restrict s1, const char *restrict s2);
:该函数把s2
指向的字符串拷贝至s1
指向的字符串的末尾。s2
字符串的第1个字符将覆盖s1
字符串末尾的空字符。该函数返回s1
。char *strncat(char *restrict s1, const char *restrict s2, size_t n);
:该函数把s2
字符串中的n
个字符拷贝至s1
字符串末尾。s2
字符串的第1个字符将覆盖s1
字符串末尾的空字符。不会拷贝s2
字符串中空字符和其后的字符,并在拷贝字符的末尾添加一个空字符。该函数返回s1
。int strcmp(const char *s1, const char *s2);
:如果s1
字符串在机器排序序列中位于s2
字符串的后面,该函数返回一个正数;如果两个字符串相等,则返回0;否则返回一个负数。int strncmp(const char *s1, const char *s2, size_t n);
:该函数的作用和strcmp()
类似,不同的是,该函数在比较n
个字符后或遇到第1个空字符时停止比较。char *strchr(const char *s, int c);
:如果s
字符串中包含c
字符,该函数返回指向s
字符串首位置的指针(末尾的空字符也是字符串的一部分,所以在查找范围内);如果未找到,则返回空指针。char *strpbrk(const char *s1, const char *s2);
:如果s1
字符串中包含s2
字符串中的任意字符,该函数返回指向s1
字符串首位置的指针;否则返回空字符。char *strrchr(const char *s, int c);
:该函数返回s
字符串中c
字符的最后一次出现的位置(末尾的空字符也是字符串的一部分,所以在查找范围内)。如果未找到,则返回空指针。char *strstr(const char *s1, const char *s2);
:该函数返回指向s1
字符串中s2
字符串出现的首位置。如果在s1
中没有找到s2
,则返回空指针。size_t strlen(const char *s);
:该函数返回s
字符串中的字符数,不包括末尾的空字符。
11.8命令行参数
命令行参数是同一行的附加项:
fuss -r Ginger
一个c程序可以读取并使用这些附加项:
int main(int argc, char *argv[]) {
// ...
return 0;
}
C编译器允许
main()
没有参数或者有两个参数。main()
有两个参数时,第1个参数是命令行中的字符串数量。系统用空格表示一个字符串的结束和下一个字符串的开始。第2个参数接收以字符串常量形式存放的命令行参数(包括命令本身也作为一个参数)。
11.9把字符串转换为数字
atoi()
函数用于把字母数字转换成整数,该函数接受一个字符串作为参数,返回相应的整数值。如果字符串仅以整数开头,atoi()
也能处理。例如,atoi("42regular")
将返回42。如果字符串非法,则结果是未定义的。
从ANSI C开始,stdlib.h
头文件中包含atoi()
、atof()
和atol()
函数的原型。atof()
把字符串转换成doule
类型的值,atol()
把字符串转换成long
类型的值。
ANSI C还提供了一套更智能的函数:strtol()
把字符串转换成long
类型的值,strtoul()
把字符串转换成unsigned long
类型的值,strtod()
把字符串转换成double
类型的值。这些函数的智能之处在于识别和报告字符串中的首字符是否是数字。而且,strtol()
和strtoul()
还可以指定数字的进制。