C Primer Plus(6) 中文版 第11章 字符串和字符串函数 11.1 表示字符串和字符串I/O
C库提供大量的函数用于读写字符串、拷贝字符串、比较字符串、合并字符串、查找字符串等。
11.1 表示字符串和字符串I/O
字符串是以空字符(\0)结尾的char类型数组。可以把数组和指针的知识应用于字符串。由于字符串十分常用,所以C提供了许多专门用于处理字符串的函数。
表示字符串的几种方式。
// strings1.c
#include <stdio.h>
#define MSG "I am a symbolic string constant."
#define MAXLENGTH 81
int main(void)
{
char words[MAXLENGTH] = "I am a string in an array.";
const char * pt1 = "Something is pointing at me.";
puts("Here are some strings:");
puts(MSG);
puts(words);
puts(pt1);
words[8] = 'p';
puts(words);
return 0;
}
/* 输出:
*/
puts()函数也属于stdio.h系列的输入/输出函数。puts()函数只显示字符串,而且自动在显示的字符串末尾加上换行符。
11.1.1 在程序中定义字符串
string1.c使用了多种方法(即字符串常量、char类型数组、指向char的指针)定义字符串。程序应该确保有足够的空间存储字符串。
1.字符串字面量(字符串常量)
用双引号括起来的内容称为字符串字面量(string literal),也叫作字符串常量(string constant)。双引号中的字符和编译器自动加入末尾的\0字符,都作为字符串存储在内存中。
从ANSI C标准起,如果字符串之间没有空格,或者用空白字符分隔,C会将其视为串联起来的字符串字面量。例如:
char greeting[50] = "Hello, and""how are" "you"
" today!";
与下面的代码等价:
char greeting[50] = "Hello, and how are you today!";
如果要在字符串内部使用双引号,必须在双引号前面加上一个反斜杠(\):
printf( "\"Run Spot, run!\" exclaimed Dick.\n" );
字符串常量属于静态存储类别(static storage class),这说明如果在函数中使用该字符串常量,该字符串只会被存储一次,在整个程序的生命期存在,即使函数被调用多次。用双引号括起来的内容被视为指向该字符串存储位置的指针。这类似于把数组名作为指向该数组位置的指针。
/* strptr.c -- strings as pointers */
#include <stdio.h>
int main(void)
{
printf("%s, %p, %c\n", "We", "are", *"space farers");
return 0;
}
/* 输出:
*/
*"space farers"表示该字符串所指向地址上存储的值,应该是字符串"space farers"的首字符。
2.字符串数组和初始化
定义字符串数组时,必须让编译器知道需要多少空间。一种方法是用足够空间的数组存储字符串。例如:
const char m1[40] = "Limit yourself to one line's worth.";
const表明不会更改这个字符串。
这种形式的初始化比标准的数组初始化形式简单得多:
const char m1[40] = {'L',..., '\0'}; ...表示"imit yourself to one line's worth."中所有的字符都用字符常量依次表示,并用逗号分隔。你不能像我这样写,应该用字符常量依次表示。
注意最后的空字符。没有这个空字符,这就不是一个字符串,而是一个字符数组。
在指定数组大小时,要确保数组的元素个数至少比字符串长度多1(为了容纳空字符)。所有未被使用的元素被自动初始化为0(这里的0指的是char形式的空字符,不是数字字符0,也就是字符'\0')。
通常,可以通过省略数组初始化声明中的大小,这些函数通过查找字符串末尾的空字符确定字符串在何处结束。
让编译器计算数组的大小只能用在初始化数组时。如果创建一个稍后在填充的数组,就必须在声明时指定大小。声明数组时,数组大小必须是可求值的整数。在C99新增变长数组之前,数组的大小必须是整型常量,包括由整型常量组成的表达式。
char curmbs[n]; //在C99标准之前无效,C99标准之后这种数组是变长数组
字符数组名和其他数组名一样,是该数组首元素的地址。因此,假设有下面的初始化:
char car[10] = "Tata";
那么,以下表达式都为真:
car == &car[0]、*car == 'T'、*(car + 1) == car[1] == 'a'
还可以使用指针表示法创建字符串。例如:
const char * pt1 = "Something is pointing at me.";
该声明与下面的声明几乎相同:
const char ar1[] = "Something is pointing at me.";
以上两个声明表明,pt1和ar1都是该字符串的地址。在这两种情况下,带双引号的字符串本身决定了预留给字符串的存储空间。尽管如此,这两种形式并不完全相同。
3.数组和指针
以上面的声明为例。数组形式(ar1[])在计算机的内存中分配为一个内含29个元素的数组(每个元素对应一个字符,还加上一个末尾的空字符'\0'),每个元素被初始化为字符串常量对应的字符。通常,字符串都作为可执行文件的一部分存储在数据段中。当程序载入内存时,也就载入了程序中的字符串。字符串存储在静态存储区(static memory)中。但是,程序在开始运行时才会为该数组分配内存。为此,才将字符串拷贝到数组中。注意,此时字符串有两个副本。一个是在静态内存中的字符串字面量,另一个是存储在ar1数组中的字符串。
此后,编译器便把数组名ar1识别为该数组首元素地址的别名。这里关键要理解,在数组形式中,ar1是地址常量。不能更改ar1,如果改变了ar1,则意味着改变了数组的存储位置(即地址)。可以进行类似ar1+1这样的操作,标识数组的下一个元素。但是不允许进行++ar1这样的操作。递增运算只能操作可修改的左值,不能是常量。
指针形式(*pt1)也使得编译器为字符串在静态存储区预留29个元素的空间。另外,一旦开始执行程序,它会为指针变量pt1留出一个存储位置,并把字符串的地址存储在指针变量中。该变量最初指向该字符串的首字符,但是它的值可以改变。因此,可以使用递增运算符。
字符串常量被视为const数据。由于pt1指向这个const数据,所以应该把pt1声明为指向const数据的指针。这意味着不能用pt1改变它所指向的数据,但是仍然可以改变pt1的值(即,pt1指向的位置)。如果把一个字符串字面量拷贝给一个数组,就可以随意改变数据,除非把数组声明为const。
总之,初始化数组把静态存储区的字符串拷贝到数组中,而初始化指针只把字符串的地址拷贝给指针。
// addresses.c -- addresses of strings
#define MSG "I'm special"
#include <stdio.h>
int main()
{
char ar[] = MSG;
const char *pt = MSG;
printf("address of \"I'm special\": %p \n", "I'm special");
printf(" address ar: %p\n", ar);
printf(" address pt: %p\n", pt);
printf(" address of MSG: %p\n", MSG);
printf("address of \"I'm special\": %p \n", "I'm special");
return 0;
}
/* 输出:
*//*这里的配套答案有问题,应该修改#define MSG "I'm special."为#define MSG "I'm special"*/
该程序的输出说明了什么?
第一,pt和MSG的地址相同,而ar的地址不同,这与之前的讨论的内容一致。
第二,"I'm special"在程序的两个printf()函数中出现了两次,但是编译器只使用了一个存储位置,而且与MSG的地址相同。编译器可以把多次使用的相同字面值存储在溢出或多处。另一个编译器可能在不同的位置存储3个"I'm special"。
第三,静态数据使用的内存与ar使用的动态内存不同。不仅值不同,特定编译器甚至使用不同的位数表示两种内存。
4.数组和指针的区别
假设有下面两个声明:
char heart[] = "I love Tillie!";
const char *head = "I love Millie!";
两者主要的区别是:数组名heart是常量,而指针名是变量。那么,实际使用有什么区别?
首先,两者都可以使用数组表示法:
for( i = 0; i < 6; i++ )
putchar( heart[i] );
putchar( '\n' );
for( i = 0; i < 6; i++ )
putchar( head[i] );
putchar( '\n' );
其次,两者都能进行指针加法操作:
for( i = 0; i < 6; i++ )
putchar( *(heart + i) );
putchar( '\n' );
for( i = 0; i < 6; i++ )
putchar( *(head + i) );
putchar( '\n' );
但是,只有指针表示法可以进行递增操作:
while( *(head) != '\0' ) /*在字符串末尾处停止*/
putchar( *(head++) ); /*打印字符,指针指向下一个位置*/
假设想让head和heart统一,可以这样做:
head = heart; /*head现在指向数组heart*/
这使得head指向指向heart数组的首元素。
但是,不能这样做:
heart = head; /*非法赋值,不能这样写,常量不能被赋值*/
顺带一提,head = heart;不会导致head指向的字符串消失,这样做只是改变了存储在head中的地址。除非已经保存了"I love Millie!"的地址,否则当head指向别处时,就无法访问该字符串。
另外,可以改变heart数组中元素的信息:
heart[7] = 'M';或者*(heart + 7) = 'M';
数组的元素是变量(除非数组声明为const),但是数组名不是变量。
未使用const限定符的指针初始化:
char *word = "frame";
是否能使用该指针修改这个字符串?
word[1] = 'l'; //是否允许?
编译器可能允许这样做,但是对当前的C标准而言,这样的行为是未定义的。例如,这样的语句可能导致内存访问错误。原因前面提到过,编译器可以使用内存中的一个副本来表示所有完全相同的字符串字面量。例如,下面的语句都引用字符串"Kingon"的一个内存位置:
char *p1 = "Klingon";
p1[0] = 'F'; //ok?
printf( "Klingon" );
printf( ": Beware the %ss!\n", "Klingon" );
也就是输哦,编译器可以用相同的地址替换每个"Klingon"实例。如果编译器使用这种单次副本表示法,并允许p1[0]修改'F',那将影响所有使用该字符串的代码。所以以上语句打印字符串字面量"Klingon"时实际上显示的是"Flingon":
建议在把指针初始化为字符串字面量时使用const限定符:
const char *p1 = "Klingon"; //推荐用法
然而,把非const数组初始化为字符串字面量却不会导致类似的问题。因为数组获得的是原始字符串的副本。
总之,如果打算修改字符串,就不要用指针指向字符串字面量。
5.字符串数组
指向字符串的指针数组和char类型数组的数组。
// arrchar.c -- array of pointers, array of strings
#include <stdio.h>
#define SLEN 40
#define LIM 5
int main(void)
{
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"
};
int i;
puts("Let's compare talents.");
printf ("%-36s %-25s\n", "My Talents", "Your Talents");
for (i = 0; i < LIM; i++)
printf("%-36s %-25s\n", mytalents[i], yourtalents[i]);
printf("\nsizeof mytalents: %zd, sizeof yourtalents: %zd\n",
sizeof(mytalents), sizeof(yourtalents));
return 0;
}
/* 输出:
*/
mytalents数组是一个内含5个指针的数组,在我们的系统中共占用40字节。而yourtalents是一个内含5个数组的数组,每个数组内含40个char类型的值,共占用200字节。所以,虽然mytalents[0]和yourtalents[0]都分别表示一个字符串,但mytalents和yourtalents的类型并不相同。mytalents中的指针指向初始化时所有的字符串字面值的位置,这些字符串字面值被存储在静态内存中;而yourtalents中的数组则存储着字符串字面量的副本,所以每个字符串都被存储了两次。此外,为字符串数组分配内存的使用率较低。yourtalents中的每个元素的大小必须相同,而且必须是能存储最长字符串的大小。
我们可以把yourtalents想象成矩形二维数组,每行的长度都是40字节;把mytalents想象成不规则的数组,每行的长度不同。实际上,mytalents数组的指针元素所指向的字符串不必存储在连续的内存中。
综上述所,如果要用数组表示一系列待显示的字符串,请使用指针数组,因为它比二维数组的效率高。指针数组也有缺点,其中的指针所指向的字符串字面量不能更改。所以,如果要改变字符串或为字符串输入项预留空间,不要使用指向字符串字面量的指针。
11.1.2 指针和字符串
实际上,字符串的绝大部分操作都是通过指针完成的。
/* p_and_s.c -- pointers and strings */
#include <stdio.h>
int main(void)
{
const char * mesg = "Don't be a fool!";
const char * copy;
copy = mesg;
printf("%s\n", copy);
printf("mesg = %s; &mesg = %p; value = %p\n",
mesg, &mesg, mesg);
printf("copy = %s; © = %p; value = %p\n",
copy, ©, copy);
return 0;
}
/* 输出:
*/
注意,如果编译器不识别%p,用%u或%lu代替%p。
注意最后一项,显示两个指针的值。所谓指针的值就是它存储的地址。mesg和copy的值都指向同一个位置。因此,程序并未拷贝字符串。语句copy=mesg;把mesg的值赋给copy,即让copy也指向mesg指向的字符串。
为什么要这样做?为何不拷贝整个字符串?考虑一下哪种方法更高效:拷贝一个地址还是拷贝整个数组?通常,程序要完成某项操作只需要知道地址就可以了。如果确实需要拷贝整个数组,可以使用strcpy()或strncpy()函数。
C Primer Plus(6) 中文版 第11章 字符串和字符串函数 11.1 表示字符串和字符串I/O相关推荐
- C Primer Plus(6) 中文版 第5章 运算符、表达式和语句 5.5 类型转换
5.5 类型转换 通常,在语句和表达式中应使用类型相同的变量和常量.如果,如果使用混合类型,C采用一套规则进行自动类型转换.虽然这很便利,但是有一定的危险性,尤其是在无意间混合使用类型的情况下(许多U ...
- C++语言篇 第六章 字符数组及函数(不能用在string字符串中)
字符数组 数组中的每个元素都是一个字符的数组称为"字符数组".有时,把一维字符数组又称为"字符串".定义字符数组的方法与定义其他类型数组的方法类似. 对于字符数 ...
- C Primer Plus(6) 中文版 第5章 运算符、表达式和语句 5.2 基本运算符
5.2 基本运算符 C用运算符(operator)表示算术运算. 基本运算的运算符:=.+.-.*./(C没有指数运算符.不过,C的标准数学库提供了一个pow()函数用于指数运算). 5.2.1 赋值 ...
- C Primer Plus(6) 中文版 第9章 函数 9.1 复习函数
C的设计思想是,把函数用作构件块. 9.1 复习函数 函数(function)是完成特定任务的独立程序代码单元.语法规则定义了函数的结构和使用方式.虽然C中的函数和其他语言中的函数. 子程序.过程作用 ...
- C Primer Plus(6) 中文版 第3章 数据和C 3.4 C语言基本数据类型
3.4 C语言基本数据类型 3.4.1 int类型 C语言提供了许多整数类型,因为C语言让程序员针对不同情况选择不同的类型.特别是,C语言中的整数类型可表示不同的取值范围和正负值. int类型是有符 ...
- C Primer Plus(6) 中文版 第12章 存储类别、链接和内存管理 12.3 掷骰子
12.3 掷骰子 最普遍的是使用两个6面骰子.在一些冒险游戏中,会使用5种骰子:4面.6面.8面.12面和20面.聪明的古希腊人证明了只有5种正多面体,它们的所有面都具有相同的形状和大小.各种不同类型 ...
- C Primer Plus(6) 中文版 第5章 运算符、表达式和语句 5.3 其他运算符
5.3 其他运算符 C语言有大约40个运算符. 5.3.1 sizeof运算符和size_t类型 sizeof运算符以字节为单位返回运算对象的大小(在C语言中,1字节定义为char类型占用的空间大小) ...
- C Primer Plus 第11章_字符串和字符串函数_代码和练习题
11.1 表示字符串和字符串I/O 字符串是以空字符(\0)结尾的char类型数据. strings1.c 演示在程序中表示字符串的几种方式 #include <stdio.h> #def ...
- 【控制】《鲁棒控制-线性矩阵不等式处理方法》-俞立老师-第11章-大系统的分散控制
第3章 回到目录 第5章 第11章-大系统的分散控制 11.1 时滞系统的分散稳定化控制 11.1 时滞系统的分散稳定化控制
- 《mysql必知必会》学习_第11章_20180801_欢
第11章:使用数据处理函数. P69 文本处理函数,upper()函数把文本变成大写字体. select vend_name,upper(vend_name) as vend_name_upcase ...
最新文章
- java公平索非公平锁_Java 并发编程中使用 ReentrantLock 替代 synchronized
- 『求助』请求服务器超时或失败问题
- [sh]shell案例
- 用Xlib库进行基本图形编程
- [JDK]找不到或无法加载主类 java
- spring源码之bean加载(bean解析下篇)
- Docker容器kali镜像导出/导入
- 计算机word的常用功能技巧,10个Word操作神技巧,看看你会多少?
- php蓝奏云网盘源码,蓝奏云网盘pc版易语言开源软件源码
- 如何从网页上下载内嵌的PDF文件
- android手机常用浏览器,Android手机 三大浏览器对比评测
- 娓娓道来图模型、图查询、图计算和图学习知识
- 赢在2022,面试官常问的软件测试面试题总结
- 2021最新Spring Security知识梳理
- panic or panick?
- 乐吾乐2D可视化为智慧电力赋能(二)
- 君正X1830芯片性能和处理器介绍
- BW4HANA cockpit 权限设置
- composer 安装问题
- Architectural Design