一、输入和输出流
- C 语言的标准输入输出函数都是独立于设备的,不需要考虑如何在特定设备上传输数据;C 语言的库函数和操作系统会确保在特定设备上的操作完全正常。
- C 语言的每个输入源和输出目的地都称为流(stream);流和设备实体相互独立。
- 程序使用的设备通常都有一个或多个相关的流;
- 磁盘驱动器一般包含多个文件,流和数据源或目的地一一对应,而不是和设备一一对应。
- 输入输出流还可以进一步细分为:字符流(character stream)和二进制流(binary stream)。
- 字符流中传输的是一系列字符,可以根据格式规范库例程修改;
- 二进制流中传输的数据是以系列字节,不能以任何方式修改。
二、标准流
流有标识它的名称。C 语言有三个在
- stdin
- stdout
- stderr
stderr 流只是将来自 C 库的错误信息传送出去,也可以将自己的错误信息传送给 sterr。写入 stdout 和 stderr 这两个流的数据的目的地默认为命令行,这两个流的主要差别是:
- 输出到 stdout 的流在内存上缓存,所以写入到 stdout 的数据不会马上送到设备。
- 输出到 stderr 的流不进行缓存,所以写入 stderr 的数据会立即传送到设备上。
对于缓存的流,程序会在内存中传输缓存区域的数据,在物理设备上传输数据可以异步进行。这使得输入输出操作更为高效。
使用操作系统命令,stdin 和 stdout 都可以重定向到文件上,而不是默认的键盘和屏幕上。
三、键盘输入
stdin 上的键盘输入有两种形式:
- 格式化输入,主要由 scanf() 提供;
- 非格式化输入,通过 getchar() 等函数接收原始的字符串数据。
3.1、格式化键盘输入
scanf 从 stdin 流中读入字符,并根据格式控制字符串中的格式说明符,将它们转换成一个或多个值。
示例代码:
char string[100];
scanf("%s",string);
3.2、输入格式控制字符串
在 scanf() 函数中使用的格式控制字符串不完全类似于 printf() 中的格式控制字符串。在格式控制字符串中添加一个或多个空白字符串,如空格 ' '、制表符 '\t' 或换行符 '\n',scanf() 会忽略空白字符,直接读入输入中的下一个非空白字符。在格式控制字符串中只要出现一个空白字符,就会忽略无数个连续的空白字符。要注意的是,scanf() 默认忽略空白字符,但使用 %c、%[] 或 %n 说明符读取数据时除外(见下表)。
scanf() 会读入任何非空白字符(除了%以外),但不会存储这个连续出现的字符。以下是输入格式符的一般形式:
% * field_width 长度修饰符 conversion_character
含义如下:
格式部分 | 意 义 |
---|---|
% | 表示格式说明符的开始 |
* | 表示要忽略的输入值 |
field_width | 指定输入字段中的字符数 |
长度修饰符 | 指定数值和指针输入值的长度 |
conversion_character | 指定要把输入转换成什么类型 |
以下是一般格式的个部分说明:
- %:表示格式说明符的开头,不能省略。
- *:可选,表示忽略下一个输入值。
- 字段宽度:可选,它是一个整数,指定了 scanf() 读入的字符数。
- 长度修饰符:可选,
示例代码:
#include
#define SIZE 20
void try_input(char* prompt, char* format);
int main() {
try_input("Enter as input: -2.35 15 25 ready2go\n", "%f %d %d %[abcdefghijklmnopqrstuvwxyz] %*1d %s%n");
try_input("\nEnter the same input again: ", "%4f %4d %d %*d %[abcdefghijklmnopqrstuvwxyz] %*1d %[^o]%n");
try_input("Enter as input: -2.3A 15 25 ready2go\n", "%4f %4d %d %*d %[abcdefghijklmnopqrstuvwxyz] %*1d %[^o]%n");
return 0;
}
void try_input(char* prompt, char* format) {
int value_count = 0;
float fp1 = 0.0f;
int i = 0;
int j = 0;
char word1[SIZE] = " ";
char word2[SIZE] = " ";
int byte_count = 0;
printf(prompt);
value_count = scanf(format, &fp1, &i, &j, word1, word2, &byte_count);
fflush(stdin);
printf("The input format string for scanf() is:\n \"%s\"\n",format);
printf("Count of bytes read = %d\n",byte_count);
printf("Count of values read = %d\n",value_count);
printf("fp1 = %f i = %d j = %d\n",fp1, i, j);
printf("word1 = %s word2 = %s\n",word1, word2);
}
3.3、输入格式字符串中的字符
可以在输入格式字符串中包含一些不是格式转换说明符的字符。为此,必须指定输入中有这些字符,且 scanf() 函数应读取它们,但不存储它们。这些非格式转换字符必须和输入流的字符完全相同,只要有一个不同,scanf() 就会终止输入。
示例代码:
#include
int main() {
int i = 0;
int j = 0;
int value_count = 0;
float fp1 = 0.0f;
printf("Enter: fp1 = 3.14159 i = 7 8\n");
printf("\nInput:");
value_count = scanf("fp1 = %f i = %d %d", &fp1, &i, &j);
printf("\nOutput:");
printf("Count of values read = %d\n", value_count);
printf("fp1 = %f\ti = %d\tj = %d\n", fp1, i, j);
return 0;
}
运行结果:
Enter: fp1 = 3.14159 i = 7 8
输入:
Input:fp1 = 3.14 i = 4 5
输出:
Output:Count of values read = 3
fp1 = 3.140000 i = 4 j = 5
这些非格式转换字符必须和输入流的字符完全相同,才会得到正常的输出结果。
3.4、输入浮点数的各种变化
使用 scanf() 函数读取格式化的浮点数时,不仅可以选择格式说明符,还可以输入不同形式的数,示例代码:
#include
int main() {
float fp1 = 0.0f;
float fp2 = 0.0f;
float fp3 = 0.0f;
int value_count = 0;
printf("Enter: 3.14.314E1.0314e+02\n");
printf("\nInput:");
value_count = scanf("%f %f %f", &fp1, &fp2, &fp3);
printf("\nOutput:");
printf("Number of values read = %d\n", value_count);
printf("fp1 = %f fp2 = %f fp3 = %f\n", fp1, fp2, fp3);
return 0;
}
执行结果:
Enter: 3.14.314E1.0314e+02
Input:3.14.314E1.0314e+02
Output:Number of values read = 3
fp1 = 3.140000 fp2 = 3.140000 fp3 = 3.140000
3.5、读取十六进制和八进制值
可以使用格式说明符 %x 从输入流中读取十六进制值,使用 %o 读取八进制值,示例代码如下:
#include
int main() {
int i = 0;
int j = 0;
int k = 0;
int n = 0;
printf("Enter three integer values\n");
n = scanf("%d %x %o", &i, &j, &k);
printf("\nOutput:");
printf("%d values read.\n", n);
printf("i = %d j = %d k = %d\n", i, j, k);
return 0;
}
执行结果:
Enter three integer values: 12 12 12
Output:3 values read.
i = 12 j = 18 k = 10
3.6、使用 scanf() 读取字符
使用 %c 可以读取一个字符,并将它存储为 char 类型。对于字符串可以使用 %s 或是 %[]。此时要给存储的字符串追加上 '\0',作为最后一个字符。要注意的是,%[] 可以读入含有空格的字符串,
但是 %s 不可以。使用 %[] 说明符时,只需要在方括号内包含空格字符即可。
示例代码:
#include
#define MAX_TOWN 10
int main() {
char initial = ' ';
char name[80] = {' '};
char age[4] = {'0'};
printf("Enter your first initial: ");
scanf("%c", &initial);
printf("Enter your first name: ");
scanf("%s", name);
fflush(stdin);
if(initial != name[0]) printf("%s, you got your initial wrong.\n", name);
else printf("Hi, %s. Your initial is correct. Well done!\n", name);
printf("Enter your full name and your age separated by a comma:\n");
scanf("%[^,] , %[0123456789]", name, age);
printf("\nYour name is %s and your age are %s years old.\n", name, age);
return 0;
}
执行结果:
Enter your first initial: I
Enter your first name: Ivor
Hi, Ivor. Your initial is correct. Well done!
Enter your full name and your age separated by a comma:
Ivor Horton , 23
Your name is
Ivor Horton and your age are 23 years old.
scanf("%c", &initial) 中如果输入空格,那么程序就会将空白字符当成 initial 的值,根据控制字符串的定义方式,使用说明符 %c 输入的第一个字符就是要提取的字符。如果不接受空格
为 initial,可以修改为以下输入语句:
scanf(" %c", &initial);
控制字符串中的第一个字符是空格,因此 scanf() 会读入且忽略所有空格。
scanf("%[^,] , %[0123456789]", name, age) 中“,”后的空格也是必需的,原因与以上语句一样。
3.7、从键盘上输入字符串
char *gets_s(char *str, rsize_t n);
该函数会将之多 n-1 个连续字符读入指针 str 所指向的内存中,直到按下回车为止。它会用中止字符‘\0’取代按下的回车键时读入的换行符。其返回值与第一个变元相同。如果在输入的
过程中出错,str[0] 就设置为‘\0’字符。可以使用标准库函数 fgets() 替代该函数,示例代码如下:
#include
#define MAX_TOWN 10
int main() {
char initial[3] = {' '};
char name[80] = {' '};
printf("Enter your first initial: ");
fgets(initial, sizeof(initial), stdin);
printf("Enter your name: ");
fgets(name, sizeof(name), stdin);
if(initial[0] != name[0]) printf("%s, you got your initial wrong.\n", name);
else printf("Hi, %s, Your initial is correct. Well done!\n", name);
return 0;
}
执行结果:
Enter your first initial: I
Enter your name: Ivor Normal
Hi, Ivor Normal
Your initial is correct. Well done!
fgets() 函数读取的字符数比第二个变元指定的字符数少 1,再添加终止字符'\0'。fgets() 函数再输入字符串中存储一个换行符来对应按下的回车键,而 gets_s() 函数不是这样。
为了避免调用 printf() 输出姓名时输出一个换行符,需要覆盖这个换行符。
对于字符串输入,使用 gets_s() 和 fgets() 通常是首选,除非要控制字符串的内容,此时可以使用 %[]。
3.8、单个字符的键盘输入
stdio.h 中的 getc() 函数从流中读取一个字符,把字符代码返回为 int 类型。getc() 的变元是流标识的。读到流的末尾时,getc() 返回 EOF,这个符号在 stdio.h 中总是定义
为一个负整数。因为 char 类型可以由编译器的开发人员确定为带符号的或是不带符号的,所以 getc() 不返回 char 类型。如果它返回 char 类型的值,则 char 定义为无符号的类型
时,就不能返回 EOF。一般情况下,函数返回 int 类型的值,但是希望返回 char 类型时,几乎总是可以肯定它需要返回 EOF。
getchar() 函数可以从 stdin 中一次读取一个字符,等价于用 stdin 变元调用 getc()。getchar() 在 stdio.h 中定义,原型如下:
int getchar(void);
getchar() 不需要变元,它会把输入流中读入的字符代码返回为 int 类型。getchar() 和 putchar() 每次只处理一个字符,示例代码:
#include
int main(void) {
char ch;
while((ch = getchar()) != '#') {
putchar(ch);
}
return 0;
}
执行结果:
Hello MARVEL#I'm Iron man
Hello MARVEL
这里涉及了缓冲区,用户输入的字符被收集并存储在与一个被称为缓冲区的临时存储区,按下 ENTER 键后,程序才可使用用户输入的字符。缓冲区的意义是:首先,把若干字符作为一个块进行传输比逐个
发送这些字符节省时间;其次,如果用户打错了字符,可以直接通过键盘修正错误。
缓冲分为两类:
- 完全缓冲 I/O:缓冲区被填满时才刷新缓冲区,通常出现在文件输入流中;
- 行缓冲 I/O:出现换行符时刷新缓冲区,键盘输入通常是行缓冲输入。
ANSI C 和 后续的 C 标准都规定输入是缓冲的,不过最初 K&R 把这个权利交给了编译器的编写者。
stdio.h 中也声明了 ungetc() 函数,它允许把刚才读取的一个字符放回输入流。该函数需要两个参数:输入流
的字符和流的标识符(对于标准流就是 stdin)。ungetc() 返回一个 int 类型的值,对应放回输入流的字符,如果操作失败,返回一个特殊的字符 EOF。
原则上,可以把一连串字符串放回输入流,但只保证一个字符有效。
示例代码如下:
#include
#include
#include
#include
#define LENGTH 50
void eatspace(void);
bool getinteger(int* n);
char* getname(char* name, size_t length);
bool isnewline(void);
int main(void) {
int number;
char name[LENGTH] = {'\0'};
printf("Enter a sequence of integers and alphabetic names in asingle line:\n");
while(!isnewline()) {
if(getinteger(&number)) printf("Integer value: %8d\n", number);
else if(strlen(getname(name,LENGTH)) > 0) printf("Name: %s\n", name);
else {
printf("Invalid input.\n");
return 1;
}
}
return 0;
}
void eatspace(void) {
char ch = 0;
while(isspace(ch = (char)getchar()));
ungetc(ch, stdin);
}
bool getinteger(int* n) {
eatspace();
int value = 0;
int sign = 1;
char ch = 0;
if((ch = (char)getchar()) == '-') sign = -1;
else if(isdigit(ch)) value = ch - '0';
else if(ch != '+') {
ungetc(ch, stdin);
return false;
}
while(isdigit(ch = (char)getchar())) {
value = 10*value + (ch - '0');
}
ungetc(ch,stdin);
*n = value * sign;
return true;
}
char* getname(char* name, size_t length) {
eatspace();
size_t count = 0;
char ch = 0;
while(isalpha(ch = (char)getchar())) {
name[count++] = ch;
if(count == length -1) break;
}
name[count] = '\0';
if(count < length - 1) ungetc(ch, stdin);
return name;
}
bool isnewline(void) {
char ch = 0;
if((ch = (char)getchar()) == '\n') return true;
ungetc(ch, stdin);
return false;
}
执行结果:
Enter a sequence of integers and alphabetic names in asingle line:
Ivor 78 Horton 34 Jane 18
Name: Ivor
Integer value: 78
Name: Horton
Integer value: 34
Name: Jane
Integer value: 18
四、屏幕输出
将格式化数据传输到 stout 流的主要函数是 printf(),stdio.h 中还声明了可选的 printf_s() 函数,也就是 printf() 函数的安全版本。printf_s() 和 printf() 的
主要区别是 printf_s() 不允许在格式字符串中包含 %n,这是因为 该输出说明符会把数据写入内存,导致不安全。
4.1、使用 printf() 的格式化输出
函数原型如下:
int printf(char *format, ...);
printf() 函数的指针变元不能为 NULL,如果变元多余格式说明符,就会忽略多余的变元。
printf() 的格式转换说明符比 scanf() 复杂很多,输出格式说明符一般形式如下:
% flag field_width precision size_flag conversion_character
其内容和意义如下:
格式部分 | 标 记 | 意 义 | 是否可选 |
---|---|---|---|
% | 格式说明符的开头 | 否 | |
flag | -, +, space, # or 0 | 影响输出的标记 | 是 |
field_width | 输出字段中的最小字符数 | 是 | |
precision | 精度指定符 | 是 | |
size_flag | h, hh, l, ll, j, z, t, t or L | 修改转换类型的尺寸标记 | 是 |
conversion_character | d, i, o, u, x, X, p, n, e, E, f, F, g, G, a, A, c, or s | 要使用的输出转换类型 | 否 |
影响输出的标记:
字 符 | 用 途 |
---|---|
+ | 对于有符号的输出,这个字符确保输出值的前面总是有一个符号+或-。默认情况下,只有符号有 - 号 |
- | 指定输出值在输出字段中左对齐,右边用空格填充。输出的默认对齐方式为右对齐 |
0 | 指定在输出值的前面填充0,以填满字段宽度 |
# | 指定将0放在八进制的输出前面,将0X或0x放在十六进制的输出的前面,或者浮点数包含小数点。对于g或G浮点转换字符,忽略尾部的0 |
space | 指定在正数或0输出值前面放置一个空格,而不是+号 |
输出字段中的最小字符数:
如果输出需要更多的字符,它会自动增加。如果输出值需要的字符小于指定的最小字符数,多余位置会填充空白,除非字段宽度用前导 0 指定,例如 09,此时在左边会补入 0。
精度指定符:
它通常用于浮点数的输出,包含一个小数点后跟一个整数。说明符 .n 表示输出值精确到小数点后 n 位。如果输出的小数点位数多于 n,就四舍五入或舍弃掉。如果把它用于整数装换,它就指定要在
输出中显式的最少位数。
尺寸标记:
标 记 | 作 用 |
---|---|
h | 其后的整数转换说明符应用于 short 或 unsigned short 变元 |
hh | 其后的整数转换说明符应用于 signed char 或 unsigned char 变元 |
l | 其后的整数转换说明符应用于 long 或 unsigned long 变元 |
ll | 其后的整数转换说明符应用于 long long 或 unsigned long long 变元 |
j | 其后的整数转换说明符应用于 intmax 或 uintmax_t 变元。该标记会避免编译器发出警告,因为 size_t 取决于是实现码的整数类型 |
z | 其后的整数转换说明符应用于 size_t 变元 |
t | 其后的整数转换说明符应用于 ptrdiff_t 变元 |
L | 其后的浮点数转换说明符应用于 long double 变元 |
转换字符:
应用类型 | 转换字符 | 生成的输出 |
---|---|---|
整数 | ||
d 或 i | 带符号的十进制整数值 | |
o | 不带符号的八进制整数值 | |
u | 不带符号的十进制整数值 | |
x | 不带符号的十六进制整数(使用小写的十六进制数) | |
X | 不带符号的十六进制整数(使用大写的十六进制数) | |
浮点数 | ||
f 或 F | 带符号的小数值 | |
e | 带符号和指数的小数值 | |
E | 与 e 相同,使用 E | |
g | 与 e 或 f 相同,取决于值的大小和精度 | |
G | 与 g 相同,但用 E 表示指数 | |
A 或 a | 用十六进制表示双精度值,十六进制的尾数前面加上 0x 或 0X 前缀,指数前加上 p 或 P,例如:0xh.hhhp±d | |
指针 | ||
p | 把变元的输出为指针,变元的类型应该是 void* | |
字符 | ||
c | 单个字符或精度字符 | |
s | 在‘\0’之前的所有字符串或已输出的 precision 个字符 |
注意:%n 只能用于 printf()。对应的变元必须是 int* 类型。作用是将字符数输入 stdout。
4.2、转移序列
转移序列 | 说 明 |
---|---|
\b | 退格 |
\f | 换页 |
\n | 换行 |
\r | 回车(用于打印机),在屏幕输出中,就是移动到当前行的开头 |
\t | 水平制表符 |
4.3、整数输出
示例代码:
#include
int main() {
int i = 15, j = 345, k = 4567;
long long li = 56789LL, lj = 67891234567LL, lk = 23456789LL;
printf("i = %d j = %d k = %d i = %6.3d j = %6.3d k = %6.3d\n", i, j, k, i, j, k);
printf("i = %-d j = %+d k = %-d i = %-6.3d j = %-6.3d k = %-6.3d\n", i, j, k, i, j, k);
printf("li = %d lj = %d lk = %d\n", li, lj, lk);
printf("li = %lld lj = %lld lk = %lld\n", li, lj, lk);
return 0;
}
编译时会有如下警告:
warning: format ‘%d’ expects argument of type ‘int’, but argument 2 has type ‘long long int’ [-Wformat=]
printf("li = %d lj = %d lk = %d\n", li, lj, lk);
执行结果:
i = 15 j = 345 k = 4567 i = 015 j = 345 k = 4567
i = 15 j = +345 k = 4567 i = 015 j = 345 k = 4567
li = 56789 lj = -828242169 lk = 23456789
li = 56789 lj = 67891234567 lk = 23456789
由以上输出结果可以看出,标志“-”使输出左对齐。对于第二个 i 值的输出,插入了一个前导 0, 因为最小精度指定为 3.如果在格式说明符的最小字符宽度前放置一个 0,并忽略精度说明符,也可以
得到相同的结果。有精度说明符时,会忽略前导 0。
第三行输出在编译时会发出警告,给出值的字符宽度及精度不够大,会造成意想不到的结果。
4.4、输出浮点数
示例代码:
#include
int main() {
float fp1 = 345.678f, fp2 = 1.234E6f;
double fp3 = 234567898.0, fp4 = 11.22334455e-6;
printf("%f %+f %-10.4f %6.4f\n", fp1, fp2, fp1, fp2);
printf("%e %+E\n", fp1, fp2);
printf("%f %g %#+f %8.4f %10.4g\n", fp3, fp3, fp3, fp3, fp4);
return 0;
}
执行结果:
345.678009 +1234000.000000 345.6780 1234000.0000
3.456780e+02 +1.234000E+06
234567898.000000 2.34568e+08 +234567898.000000 234567898.0000 1.122e-05
fp1 的第二个输出值说明热如何限制小数点后的位数。为 fp2 的第二个输出指定的字符宽度太小,放不下小数位,因此会舍弃多余的部分。
4.5、字符输出
示例代码:
#include
#include
#include
int main() {
int count = 0;
printf("The printable characters are the following:\n");
for(int code = 0; code <= CHAR_MAX; ++code) {
char ch = (char)code;
if(isprint(ch)) {
if(count++ % 32 == 0) printf("\n");
printf(" %c", ch);
}
}
printf("\n");
return 0;
}
执行结果:
! " # $ % & ' ( ) * + , - . / 0 1 2 3 4 5 6 7 8 9 : ; < = > ?
@ A B C D E F G H I J K L M N O P Q R S T U V W X Y Z [ \ ] ^ _
` a b c d e f g h i j k l m n o p q r s t u v w x y z { | } ~
五、其他输出函数
在 stdio.h 头文件中声明的 puts() 函数和 gets_s() 函数互补。该函数原型如下:
int puts(const char *string);
puts() 函数接受字符串指针作为变元,将字符串后的一个换行符写入标准输出流 stdout。其字符串必须用字符‘\0’终止。puts() 函数的参数是 const,所以该函数不能修改
传送给她的字符串。puts() 函数用于输出单行信息,例如:
puts("Is there no end to input and output?");
使用 printf() 函数必须在字符串末尾添加‘\n’,才能达到该效果。
5.1 屏幕的非格式化输出
函数 putchar() 也是 stdio.h 中定义的函数,与 getchar() 函数互补。其原型如下:
int putchar(int c);
该函数将单个字符 c 输出到 stdout 上,并返回所显示的字符。它可以输出信息,一次先是一个字符,该方法可以控制是否输出某些字符。例如,输出一个字符串:
char string[] = "Beware the Jabberwock, \nmy son!";
puts(string);
或是:
int i = 0;
while() {
if(string[i] != '\n') putchar(string[i]);
++i;
}
5.2、数组的格式化
在 stdio.h 中声明的 sprintf_s() 或 snprintf_s() 函数,可以格式化数据写入 char 类型的数组中,它们是 sprintf() 标准函数的安全版本,因此它们禁止写到数组外部。它们的区别是,
sprintf_s() 将超出数组的范围看作一个运行错误,而 snprintf_s() 仅截断输出,使结果能放置在数组中。这里仅讨论 snprintf_s(),其函数原型如下:
int snprintf_s(char * restrict str, rsize_t n, const * restrict format, ...);
第一个变元是数组的地址,是数组的地址,使输出的目的地。第二个变元叔叔祖德长度,它的工作方式与 printf_s() 相同,只是将数据写入 str,而不是 stdout。示例代码:
char result[20];
int count = 4;
int nchars = snprintf_s(result, sizeof(result), "A dog has %d legs.", count);
5.3、数组的格式化输入
可选的 sscanf_s() 函数与 snprintf_s() 函数互补,因为 sscanf_s() 函数可以在格式字符串的控制下,从 char 类型的数组元素中读取数据。 它是 sscanf() 函数的安全版本,主要区别是
sscanf_s() 需要给每个 c、s 或是 [ 说明符指定地址和长度变元,而 sscanf() 只需要地址。sscanf_s() 函数的原型如下:
int sscanf_s(const char * restrict str, const char * restrict format, ...);
示例代码:
char *source = "Fred 94";
char name[10] ={0};
int age = 0;
int items = sscanf_s(source, " %s %d", name, sizeof(name), &age);
以上代码的执行结果是:name 包含字符串 "Fred",age 的值是 94。items 变量的值是 2,因为从source 中读取了两项。
参考
Beginning C
深入理解计算机操作系统