学习内容:
1.函数——strlen();
2.关键字——const;
3.字符串;
4..如何创建、存储字符串;
5.如何使用strlen()函数获取字符串的长度;
6.用C预处理器指令#define和ANSIC的const修饰符创建符号常量。
与程序交互和使用字符串可以编写个性化的程序,本次学习C语言的两个输入/输出函数:scanf()和printf()。学会使用这两个函数,不仅能与用户交互,还可根据个人喜好和任务要求格式化输出。此外,还要学习一个很重要的工具——C预处理指令,并学习如何定义、使用符号常量。
1.从一个简单的C程序示例开始
程序talkback.c与用户进行简单的交互,为了使程序的形式灵活多样,代码中使用了新的注释风格。
#include <stdio.h>
#include <string.h> //提供strlen()函数的原型
#define DENSITY 62.4 //人体密度(单位:磅/立方英尺)
int main()
{
float weight, volume;
int size, letters;
char name[40]; //name是一个可容纳40个字符的数组
printf("Hi! What's your first name?\n");
scanf("%s", name);
printf("%s, what's your weight in pounds?\n",name);
scanf("%f", &weight);
size = sizeof(name);
letters = strlen(name);
volume = weight / DENSITY;
printf("Well, %s, your volume is %2.2f cubic feet.\n", name, volume);
printf("Also, your first name has %d letters,\n", letters);
printf("and we have %d bytes to store it.\n",size);
return 0;
}
该程序包含以下新特性:
用数组存储字符串。再该程序中,用户输入的名被存储在数组中,该数组占用内存中40个连续的字节,每个字节存储一个字符值。
使用%s转换说明来处理字符串的输入和输出。注意,在scanf()中,name没有&前缀,而weight有。
用C预处理器把字符常量DENSITY定义为62.4.
用C函数strlen()获取字符串的长度。
对于BASIC的输入/输出而言,C的输入/输出看上去有些复杂。不过。复杂换来的是程序的高效和方便控制输入/输出。而且,一旦熟悉用法后,会发现它很简单。
2.字符串简介
字符串是一个或多个字符的序列,如下所示:
"Zing went the strings of my heart!"
双引号不是字符串的一部分。双引号仅告知编译器它括起来的是字符串,正如单引号用于标识单个字符一样。
2.1 char类型数组和null字符
C语言中没有专门用于存储字符串的变量类型,字符串都被存储在char类型的数组中。数组由连续的存储单元组成,字符串中的字符被存储在相邻的存储单元中,每个单元存储一个字符,如下图:
上图中数组末尾位置的字符\0。这时空字符,C语言用它标记字符串的结束。空字符不是数字0,它是非打印字符,其ASCII码值是0.C中的字符串一定以空字符结束,这意味着数组的容量必须至少比待存储字符串中的字符数多1.因此,talkback.c程序中有40个存储单元的字符串,只能存储39个字符,剩下一个字节留给空字符。
那么,什么是数组呢?可以把数组看作是一行连续的多个存储单元。更正式的说法是,数组是同类型数据元素的有序序列。上面程序中通过以下声明创建了一个包含40个存储单元的数组,每个单元存储一个char类型的值:
char name[40];
name后面的方括号表明这是一个数组,方括号中的40表明该数组中的元素数量。char表明每个元素的类型,如图:
字符串看上去比较复杂!必须先创建一个数组,把字符串中的字符逐个放入数组,还要记得在末尾加上一个\0。
2.2 使用字符串
praise1.c
#include <stdio.h>
#define PRAISE "You are an extraordinary being."
int main()
{
char name[40];
printf("What's your name? ");
scanf("%s", name);
printf("Hello, %s. %s\n", name, PRAISE);
return 0;
}
%s告诉printf()打印一个字符串。%s出现了两次, 因为程序要打印两个字符串: 一个储存在name数组中; 一个由PRAISE来表示。
你不用亲自把空字符放入字符串末尾, scanf()在读取输入时就已完成这项工作。也不用在字符串常量PRAISE末尾添加空字符。稍后会解释 #define指令, 现在先理解PRAISE后面用双引号括起来的文本是一个字符串。编译器会在末尾加上空字符。
注意 (这很重要), scanf()只读取了Angela Plains中的Angela, 它在遇到第1个空白 (空格、制表符或换行符) 时就不再读取输入。因此,scanf()在读到Angela和Plains之间的空格时就停止了。一般而言, 根据%s转换说明, scanf()只会读取字符串中的一个单词, 而不是一整句。C语言还有其他的输入函数 (如, fgets()),用于读取一般字符串。
字符串和字符
字符串常量"x"和字符常量'x'不同。
区别之一在于'x'是基本类型(char),而"x"是派生类型(char数组);
区别至二是"x"实际上由两个字符组成:'x'和空字符\0,见下图:
2.3 strlen()函数
sizeof运算符以字节为单位给出对象的大小。strlen()函数给出字符串中的字符长度。因为1字节存储一个字符,也许会认为这两种方法应用于字符串得到的结果相同,但事实并非如此。
//如果编译器不识别%zd,尝试换成%u或%lu
#include <stdio.h>
#include <string.h>
#define PRAISE "You are an extraordinary being."
int main()
{
char name[40];
printf("What's your name? ");
scanf("%s", name);
printf("Hello, %s. %s\n", name, PRAISE);
printf("Your name of %zd letters occupies %zd memory cells.\n", strlen(name), sizeof name);
printf("The phrase of praise has %zd letters ",strlen(PRAISE));
printf("and occupies %zd memory cells.\n", sizeof PRAISE);
return 0;
}
string.h头文件包含多个与字符串相关的函数原型,包括strlen()。一般而言,C把函数库中相关的函数归为一类,并为每类函数提供一个头文件。例如,printf()和scanf()都隶属标准输入和输出函数,使用stdio.h头文件。string.h头文件中包含了strlen()函数和其他一些与字符串相关的函数(如拷贝字符串的函数和字符串查找函数)。
从输出结果可以看出,sizeof运算符报告,name数组有40个存储单元。但是,只有前6个单元用来存储xiaogu,所以strlen()函数得出的结果是6.name数组的第7个单元存储空字符,strlen()并未将其计入。如下图:
对于PRAISE,用strlen()得出的也是字符串中的字符数(包括空格和标点符号)。然而,sizeof运算符给出的数更大,因为它把字符串末尾不可见的空字符也计算在内。该程序并未明确告诉计算机字符串预留多少个空间,所以它必须计算双引号内的字节数。
这里sizeof没有使用圆括号,何时使用圆括号取决于运算对象是类型还是特定量。运算对象是类型时,圆括号必不可少,但是对于特定量,圆括号可有可无。也就是说,对于类型,应写成sizeof(char)或sizeof(float);对于特定量,可写成sizeof name 或sizeof 3.28。不过还是建议所有情况下都使用圆括号,这样就不会搞错后者忘记了。
3.常量和C预处理器
有时, 在程序中要使用常量。例如, 可以这样计算圆的周长:
circumference = 3.14159 * diameter;
这里, 常量3.14159代表著名的常量pi()。在该例中, 输入实际值便可使用这个常量。然而, 这种情况使用符号常量 (symbolic constant) 会更 好。也就是说, 使用下面的语句, 计算机稍后会用实际值完成替换:
circumference = pi * diameter;
为什么使用符号常量更好? 首先, 常量名比数字表达的信息更多。请比较以下两条语句:
owed = 0.015 * housevalue;
owed = taxrate * housevalue;
如果阅读一个很长的程序, 第 2 条语句所表达的含义更清楚。
另外, 假设程序中的多处使用一个常量, 有时需要改变它的值。毕竟, 税率通常是浮动的。如果程序使用符号常量, 则只需更改符号常量的定义, 不用在程序中查找使用常量的地方, 然后逐一修改。
那么, 如何创建符号常量? 方法之一是声明一个变量, 然后将该变量设 置为所需的常量。可以这样写:
float taxrate;
taxrate = 0.015;
这样做提供了一个符号名,但是taxrate是一个变量,程序可能会无意间改变它的值。C语言还提供了一个更好的方案——C预处理器。预处理器可用来定义常量。只需在程序顶部添加下面一行:
#define TAXRATE 0.015
编译程序时,程序中所有的TAXRATE都会被替换成0.015.这一过程被称为编译时替换。在运行程序时,程序中所有的替换均已完成。通常,这样定义的常量也称为明示常量。
请注意格式, 首先是#define, 接着是符号常量名(TAXRATE), 然后是符号常量的值 (0.015) (注意, 其中并没有=符号)。所以, 其通用格式 如下:
#define NAME value
实际应用时, 用选定的符号常量名和合适的值来替换NAME和value。注意, 末尾不用加分号, 因为这是一种由预处理器处理的替换机制。为什么 TAXRATE 要用大写? 用大写表示符号常量是 C 语言一贯的传统。这样, 在程序中看到全大写的名称就立刻明白这是一个符号常量, 而非变量。大写常量只是为了提高程序的可读性, 即使全用小写来表示符号常量, 程序也能照常运行。
另外, 还有一个不常用的命名约定, 即在名称前带_c或_k前缀来表示常量(如, c_level或k_line)。
符号常量的命名规则与变量相同。可以使用大小写字母、数字和下划线字符, 首字符不能为数字。程序pizza.c演示了一个简单的示例。
#include <stdio.h>
#define PI 3.14159
int main()
{
float area, circum, radius;
printf("What is the radius of your pizza?\n");
scanf("%f", &radius);
area = PI * radius * radius;
circum = 2.0 * PI * radius;
printf("Your basic pizza parameters are as follows:\n");
printf("circumference = %1.2f, area = %1.2f\n", circum, area);
return 0;
}
printf()语句中的%1.2f表明,结果被四舍五入为两位小数输出。
#define指令还可定义字符和字符串常量。前者使用单引号,后者使用双引号。如下所示:
#define BEEP '\a'
#define TEE 'T'
#define ESC '\033'
#define OOPS "Now you have done it!"
记住,符号常量名后面的内容被用来替换符号常量。不要犯这样的常见错误:
/* 错误的格式 */
#define TOES = 20
如果这样做,替换TOES的是= 20,而不是20。这种情况下,下面的语句:
digits = fingers + TOES;
将被转换成错误的语句:
digits = fingers + = 20;
3.1 const限定符
C90标准新增了const关键字,用于限定一个变量为只读。其声明如下:
const int MONTHS = 12; // MONTHS在程序中不可更改,值为12
这使得MONTHS成为一个只读值。也就是说,可以在计算中使用MONTHS,可以打印MONTHS,但是不能更改MONTHS的值。const用起来比#define更灵活。
3.2 明示常量
C头文件limits.h和float.h分别提供了与整数类型和浮点类型大小限制相关的详细信息。每个头文件都定义了一系列供实现使用的明示常量。例如,limits.h头文件包含以下类似的代码:
#define INT_MAX +32767
#define INT_MIN -32768
这些明示常量代表int类型可表示的最大值和最小值。如果系统使用32位的int,该头文件会为这些明示常量提供不同的值。如果在程序中包含limits.h头文件,就可编写下面的代码:
printf("Maxium int value on this system = %d\n", INT_MAX);
如果系统使用4字节的int,limits.h头文件会提供符合4字节int的INT_MAX和INTMIN。
如下列出了一些limits.h中能找到的明示常量。
类似地,float.h头文件中也定义一些明示常量,如FLT_DIG和DBL_DIG,分别表示float类型和double类型的有效数字位数。下图中列出了float.h中的一些明示常量(可以使用文本编辑器打开并查看系统使用的float.h头文件)。表中所列都与float类型相关。把明示常量名中的FLT分别替换成DBL和LDBL,即可分别表示double和long double类型对应的明示常量(表中假设系统使用2的幂来表示浮点数)。
程序defines.c程序
//使用limits.h和float头文件中定义的明示常量
#include <stdio.h>
#include <limits.h>
#include <float.h>
int main(void)
{
printf("Some number limits for this system:\n");
printf("Biggest int: %d\n", INT_MAX);
printf("Smallest long long: %lld\n", LLONG_MIN);
printf("One byte = %d bits on this system.\n",CHAR_BIT);
printf("Largest double: %e\n", DBL_MAX);
printf("Smallest normal float: %e\n", FLT_MIN);
printf("float precision = %d digits\n", FLT_DIG);
printf("float epsilon = %e\n", FLT_EPSILON);
return 0;
}
4 printf()和scanf()
printf()函数和scanf()函数能让用户可以与程序交流,它们是输出/输入函数,或简称为I/O函数。它们不仅是C语言中的I/O函数,而且是最多才多艺的函数。虽然printf()函数是输出函数,scanf()是输入函数,但是它们的工作原理几乎相同。两个函数都使用格式字符串和参数列表。
4.1 printf()函数
请求printf()函数打印数据的指令要与待打印数据的类型相匹配。例如,打印整数时使用%d,打印字符时使用%c。这些符号被称为转换说明(conversion specification),它们指定了如何把数据转换成可显示的形式。我们先列出ANSI C标准为printf()提供的转换说明,然后再示范如何使用一些较常见的转换说明。下表列出了一些转换说明和各自对应的输出类型。
4.2 使用printf()函数
看一下下面的程序printout.c:
#include <stdio.h>
#define PI 3.141593
int main(void)
{
int number = 7;
float pies = 12.75;
int cost = 7800;
printf("The %d contestants ate %f berry pies.\n", number, pies);
printf("The value of pi is %f.\n", PI);
printf("Farewell! thou art too dear for my possessing,\n");
printf("%c%d\n", '$', 2 * cost);
return 0;
}
输出如下:
这是printf()函数的格式:
printf(格式字符串, 待打印项1, 带打印项2,...);
待打印项1、待打印项2等都是要打印的项。它们可以是变量、常量,甚至是在打印之前先要计算的表达式。格式字符串应包含每个待打印项对应的转换说明。例如,下面的语句:
printf("The %d contestants ate %f berry pies.\n", number,pies);
格式字符串是双引号括起来的内容。上面语句的格式字符串包含了两个待打印项number和pies对应的两个转换说明。如图演示了printf()语句的另一个例子:
程序printout.c中的一行:
printf("The value of pi is %f.\n", PI);
该语句中,待打印项列表只有一个项——符号常量PI。
如图,格式字符串包含两种形式不同的信息:
实际要打印的字符;
转换说明。
格式字符串中的转换说明一定要与后面的每个项相匹配,若忘记这个基本要求会导致严重的后果。千万别写成下面这样:
printf("The score was Squids %d, Slugs %d.\n", score1);
这里,第2个%d没有对应任何项。系统不同,导致的结果也不同。不过,出现这种问题最好的状况是得到无意义的值。
如果只打印短语或句子,就不需要使用任何转换说明。如果只打印数据,也不用加入说明文字。程序printout.c中的最后两个printf()语句都没问题:
printf("Farewell! thou art too dear for my possessing,\n");
printf("%c%d\n", '$', 2 * cost);
注意第2条语句,待打印列表的第1个项是一个字符常量,不是变量;第2个项是一个乘法表达式。这说明printf()使用的是值,无论是变量、常量还是表达式的值。
由于 printf()函数使用%符号来标识转换说明,因此打印%符号就成了个问题。如果单独使用一个%符号,编译器会认为漏掉了一个转换字符。解决方法很简单,使用两个%符号就行了:
pc = 2*6;
printf("Only %d%% of Sally's gribbles were edible.\n", pc);
下面是输出结果:
Only 12% of Sally's gribbles were edible.
4.3 printf()的转换说明修饰符
在%和转换字符之间插入修饰符可修饰基本的转换说明。表4.3.1和表4.3.2列出可作为修饰符的合法字符。如果要插入多个字符,其书写顺序应该与表4.3.1中列出的顺序相同。不是所有的组合都可行。表中有些字符是C99新增的,如果编译器不支持C99,则可能不支持表中的所有项。
表4.3.1
注意 类型可移植性
sizeof 运算符以字节为单位返回类型或值的大小。这应该是某种形式的整数,但是标准只规定了该值是无符号整数。在不同的实现中,它可以是unsigned int、unsigned long甚至是unsigned long long。因此,如果要用printf()函数显示sizeof表达式,根据不同系统,可能使用%u、%lu或%llu。这意味着要查找你当前系统的用法,如果把程序移植到不同的系统还要进行修改。
鉴于此, C提供了可移植性更好的类型。首先,stddef.h头文件(在包含stdio.h头文件时已包含其中)把size_t定义成系统使用sizeof返回的类型,这被称为底层类型(underlying type)。其次,printf()使用z修饰符表示打印相应的类型。同样,C还定义了ptrdiff_t类型和t修饰符来表示系统使用的两个地址差值的底层有符号整数类型。
注意 float参数的转换
对于浮点类型,有用于double和long double类型的转换说明,却没有float类型的。这是因为在K&R C中,表达式或参数中的float类型值会被自动转换成double类型。一般而言,ANSI C不会把float自动转换成double。然而,为保护大量假设float类型的参数被自动转换成double的现有程序,
printf()函数中所有float类型的参数(对未使用显式原型的所有C函数都有效)仍自动转换成double类型。因此,无论是K&R C还是ANSI C,都没有显示float类型值专用的转换说明。
表4.3.2
4.4 scanf()函数
C库包含了多个输入函数,scanf()是最通用的一个,因为它可以读取不同格式的数据。当然,从键盘输入的都是文本,因为键盘只能生成文本字符:字母、数字和标点符号。如果要输入整数2014,就要键入字符2、0、1、4.如果要将其存储为数值而不是字符串,程序就必须把字符依次转换成数值,这就是scanf()要做的。scanf()把输入的字符串转换成整数、浮点数、字符或字符串,而printf()正好与其相反,把整数、浮点数、字符和字符串转换成显示在屏幕上的文本。
scanf()和printf()类似,也使用格式化字符串和参数列表。scanf()中的格式字符串表明字符输入流的目标数据类型。两个函数主要的区别在参数列表中。printf()函数使用变量、常量和表达式,而scanf()函数使用指向变量的指针。注意,要记住下面两条简单的规则:
如果用scanf()读取基本变量类型的值,在变量名前加上一个&;
如果用scanf()把字符串读入字符数组中,不要使用&。
看下程序 input.c
#include <stdio.h>
int main(void)
{
int age;
float assets;
char pet[30];
printf("Enter your age, assets, and favorite pet.\n");
scanf("%d %f", &age, &assets); //这里要使用&
scanf("%s", pet); //字符数组不使用&
printf("%d $%.2f %s\n", age, assets, pet);
return 0;
}
下面是该程序与用户交互的示例:
Enter your age, assets, and favorite pet.
38
92360.88 llama
38 $92360.88 llama
scanf()函数使用空白(换行符、制表符和空格)把输入分成多个字段。在依次把转换说明和字段匹配时跳过空白。注意,上面示例的输入项(粗体部分是用户的输入)分成了两行。只要在每个输入项之间输入至少一个换行符、空格或制表符即可,可以在一行或多行输入:
Enter your age, assets, and favorite pet.
42
2121.45
guppy
42 $2121.45 guppy
唯一例外的是%c转换说明。根据%c,scanf()会读取每个字符,包括空白。我们稍后详述这部分。
scanf()函数所用的转换说明与printf()函数几乎相同。主要的区别是,对于float类型和double类型,printf()都使用%f、%e、%E、%g和%G转换说明。而scanf()只把它们用于float类型,对于double类型时要使用l修饰符。表4.4.1列出了C99标准中常用的转换说明。
表4.4.1
可以在上表中的转换说明中(百分号和转换字符之间)使用修饰符。如果要使用多个修饰符,必须按下表所列的顺序书写。
如你所见,使用转换说明比较复杂,而且这些表中还省略了一些特性。省略的主要特性是,从高度格式化源中读取选定数据,如穿孔卡或其他数据记录。
1.从scanf()角度看输入
接下来,我们更详细地研究scanf()怎样读取输入。假设scanf()根据一个%d转换说明读取一个整数。scanf()函数每次读取一个字符,跳过所有的空白字符,直至遇到第1个非空白字符才开始读取。因为要读取整数,所以scanf()希望发现一个数字字符或者一个符号(+或-)。如果找到一个数字或符号,它便保存该字符,并读取下一个字符。如果下一个字符是数字,它便保存该数字并读取下一个字符。scanf()不断地读取和保存字符,直至遇到非数字字符。如果遇到一个非数字字符,它便认为读到了整数的末尾。然后,scanf()把非数字字符放回输入。这意味着程序在下一次读取输入时,首先读到的是上一次读取丢弃的非数字字符。最后,scanf()计算已读取数字(可能还有符号)相应的数值,并将计算后的值放入指定的变量中。
如果使用字段宽度,scanf()会在字段结尾或第1个空白字符处停止读取(满足两个条件之一便停止)。
如果第1个非空白字符是A而不是数字,会发生什么情况?scanf()将停在那里,并把A放回输入中,不会把值赋给指定变量。程序在下一次读取输入时,首先读到的字符是A。如果程序只使用%d转换说明, scanf()就一直无法越过A读下一个字符。另外,如果使用带多个转换说明的scanf(),C规定在第1个出错处停止读取输入。
用其他数值匹配的转换说明读取输入和用%d 的情况相同。区别在于scanf()会把更多字符识别成数字的一部分。例如,%x转换说明要求scanf()识别十六进制数a~f和A~F。浮点转换说明要求scanf()识别小数点、e记数法(指数记数法)和新增的p记数法(十六进制指数记数法)。
如果使用%s 转换说明,scanf()会读取除空白以外的所有字符。scanf()跳过空白开始读取第 1 个非空白字符,并保存非空白字符直到再次遇到空白。这意味着 scanf()根据%s 转换说明读取一个单词,即不包含空白字符的字符串。如果使用字段宽度,scanf()在字段末尾或第1个空白字符处停止读取。无法利用字段宽度让只有一个%s的scanf()读取多个单词。最后要注意一点:当scanf()把字符串放进指定数组中时,它会在字符序列的末尾加上'\0',让数组中的内容成为一个C字符串。
实际上,在C语言中scanf()并不是最常用的输入函数。这里重点介绍它是因为它能读取不同类型的数据。C 语言还有其他的输入函数,如 getchar()和 fgets()。这两个函数更适合处理一些特殊情况,如读取单个字符或包含空格的字符串。目前,无论程序中需要读取整数、小数、字符还是字符串,都可以使用scanf()函数。
2.格式字符串中的普通字符
scanf()函数允许把普通字符放在格式字符串中。除空格字符外的普通字符必须与输入字符串严格匹配。例如,假设在两个转换说明中添加一个逗号:
scanf("%d,%d", &n, &m);
scanf()函数将其解释成:用户将输入一个数字、一个逗号,然后再输入一个数字。也就是说,用户必须像下面这样进行输入两个整数:
88,121
由于格式字符串中,%d后面紧跟逗号,所以必须在输入88后再输入一个逗号。但是,由于scanf()会跳过整数前面的空白,所以下面两种输入方式都可以:
88, 121
和
88,
121
格式字符串中的空白意味着跳过下一个输入项前面的所有空白。例如,对于下面的语句:
scanf("%d ,%d", &n, &m);
以下的输入格式都没问题:
88,121
88 ,121
88 , 121
请注意,“所有空白”的概念包括没有空格的特殊情况。
除了%c,其他转换说明都会自动跳过待输入值前面所有的空白。因此,scanf("%d%d", &n, &m)与scanf("%d %d", &n, &m)的行为相同。对于%c,在格式字符串中添加一个空格字符会有所不同。例如,如果把%c放在格式字符串中的空格前面,scanf()便会跳过空格,从第1个非空白字符开始读取。也就是说,scanf("%c", &ch)从输入中的第1个字符开始读取,而scanf(" %c", &ch)则从第1个非空白字符开始读取。
3.scanf()的返回值
scanf()函数返回成功读取的项数。如果没有读取任何项,且需要读取一个数字而用户却输入一个非数值字符串,scanf()便返回0。当scanf()检测到“文件结尾”时,会返回EOF(EOF是stdio.h中定义的特殊值,通常用#define指令把EOF定义为-1)。之后再讨论文件结尾的相关内容以及如何利用scanf()的返回值。在读者学会if语句和while语句后,便可使用scanf()的返回值来检测和处理不匹配的输入。