一、标准输入输出
键盘输入与屏幕输出是编写简单的顺序结构程序时最常用到的操作,在C中通过调用输入与输出函数实现。根据系统级I/O,C的stdio将会在程序开始时将文件描述符fd0与fd1分别指向键盘与屏幕,从而实现键盘的输入与屏幕的输出。
1.1 数据格式化输入与输出
C使用stdio.h调用C的标准I/O库。C的数据格式化输入与输出是指按指定格式和类型输出变量的值。典型的,使用
int printf(const char *format, ...);
进行标准化输出,参数分别为格式控制字符串与输出值参数表。其可以同时输出多个任意类型的数据。其格式字符包括:
-%d【decimal】,输出有符号整型int;
-%u【unsigned】,输出无符号整型unsigned int;
-%f【float】,输出浮点数float与double;
-%e【exponent】,输出标准指数形式的浮点数float与double;
-%c【character】,输出字符形式的字符char。
此外,还可以在上述格式字符前加修饰符,如:
-%ld,描述long格式int;
-%hd,描述short格式int;
-%.?f,描述浮点数的精度;
-%?f,描述位宽。
同时,当%%出现在printf的格式控制字符串中,表示输出字符%。
C的数据格式化输入使用
int scanf(const char *format, ...);
其参数为格式控制字符串与输入值地址表,通过取址符对变量取址。当存在多个输入数据时,一个数据输入结束的标志为:
-键入空格、回车或制表;
-达到输入限制的位宽;
-遇到非法字符。
scanf的返回值指明了正确读入的数据项。要注意的是,scanf通常会出现如下使用错误问题:
-scanf()不能指定输入数据的精度,如%7.2f;
-scanf()的参数为输入值地址表;
-scanf()的参数不允许出现换行符\n。
scanf()需要指明变量的格式,因此需要明确的使用%f、%lf来指明浮点的精度,以及%ld、%d、%hd指明整型的长度。
scanf()还提供*以忽略输入项,如
scanf("%2d%*2d%2d", &a, &b);
那么对于输入
123456
整型变量a会赋值为12,b会赋值为56,而34被忽略。
由于函数scanf()不进行参数类型匹配检查,当参数地址表中的变量类型与格式字符不符时,只是导致数据不能正确读入,但编译器并不提示任何出错信息。即使参数地址表中的变量类型与格式字符相符,也无法保证用户输入的数据都是合法的,一旦遇到非法字符输入,则scanf()认为输入数据结束,同样会导致数据不能正确读入。为了提高程序的健壮性,有必要对输入非法数据进行检查和处理。
按照正常的处理方法,通过返回值判断已成功读入的数据项数。当非法输入时,通过循环结构提示用户重新输入,并使用fflush()清除输入缓冲区的内容。
#include <stdio.h>
#define MAX 10
int main() {
int a, b;
int ret = 0;
int i = 0;
while (i < MAX) {
ret = scanf("%d,%d", &a, &b);
if (ret != 2) {
printf("Input data quantity or format error!\n");
fflush(stdin);
}
else {
printf("%d\n", a);
printf("%d\n", b);
break;
}
i++;
}
return 0;
}
1.2 单字符输入与输出
C规定字符常量是以单引号标识的一个字符,并引入特殊形式的字符常量转义字符,用于描述特定的控制字符。字符常量在内存中以ASCII码的二进制值存储。 在描述一个字符常量时,可以选择ASCII码、八进制或十六进制进行表达,如
char ch = 'B';
char ch = '\102';
char ch = '\x42';
上述定义是等价的,且由于其存储方式,字符型变量可以作算术运算。标准字符输入输出函数为
int getchar(void);
int putchar(int char);
那么一个简单的大小写英文字母转换的程序代码如下:
#include <stdio.h>
int main() {
char ch;
ch = getchar();
ch += 32;
putchar(ch);
putchar('\n');
}
在键入字符B与回车后,就可以在屏幕中输出字符b。要注意的是,即使键入多个字符并回车后,也仅输出首个键入字符的大小写转换,这是由于getchar()仅处理单个字符。
1.3 stdio.h
标准输入输出【standard input and output】头文件提供了输入输出功能的库,包括宏、函数等,下面介绍的是一些输入输出常用的功能。
size_t是一个库变量类型,是一个无符号整数类型,是sizeof关键字的结果,可以用于分配动态内存的内存空间等,例如
size_t sz = sizeof(int);
int* ptr = (int*)malloc(4 * sz);
NULL是一个宏,是一个空指针常量的值,可以用于判断指针是否为空,例如
int* ptr = (int*)malloc(4 * sizeof(int));
if (ptr == NULL) {
printf("Not enough memory!\n");
exit(1);
}
EOF是一个宏,是一个负整数,代表文件结束。
gets()函数从标准输入stdin读取一行,并把它存储在str所指向的字符串中。当读取到换行符或者达到文件结尾时结束。其函数声明为
char* gets(char* str);
其中:
-str是指向字符数组的指针,其存储了C字符串;
-返回值如果成功是str,否则是NULL。
要注意的是gets的参数是已经定义的、需要修改字符的字符串,一个例子是
char str[50];
gets(str);
与gets()相似的,puts()将一个字符串写入标准输入stdout,直到空操作符,且如果没有空操作符,则换行符会被追加到输出中。其函数声明为
int puts(const char* str)
其中:
-str是指向字符数组的指针,其存储了C字符串;
-返回值如果成功为非负的、包含空操作符的字符串长度,否则是EOF。
二、动态内存
2.1 指针与数组
一旦给出数组的定义,编译系统就会为其在内存中分配固定的存储单元。相应的,数组的首地址也就确定了。数组元素在内存中是连续存放的,C的数组名就直接的代表了连续存储空间的首地址,即指向数组中首个元素的指针常量。因此,数组元素既可以用下标也可以使用指针来访问。
指针的算术运算和关系运算通常是针对数组元素而言的。考虑定义了指向整形数据的指针变量p,使其为数组array的首地址,那么要注意如下操作的异同:
int N = 10;
int array[N];
int* p = array;
printf("%x\n", p); // p = 8a8e0c80 and print 8a8e0c80
printf("%x\n", p + 1); // p = 8a8e0c80 and print 8a8e0c84
printf("%x\n", p++); // p = 8a8e0c84 and print 8a8e0c80
指针的运算会以sizeof()个字节为基础。
对于一维数组而言,用数组作函数形参和用指针变量作函数形参本质上是一样的:
void OutputArray(int a[], int n) {
for (int i = 0; i < n; i++) {
printf("%4d", *(a + i)); // *(a + i) is equivalent to a[i]
}
}
2.2 字符串
字符串常量是由以一对双引号标识的字符序列,区别于字符常量,并且为了便于确定字符串的长度,C编译器会自动再字符串的末尾添加ASCII值为0的空操作符‘\0’,在字符串中可以不显式的写出。因此,字符串也可以看作由若干个有效字符构成并且以空操作符作为结束的字符序列。
C不提供字符串数据类型,其存取要用字符数组实现,且其初始化操作有如下等价操作:
char str0[6] = {'H', 'e', 'l', 'l', 'o', '\0'};
char str1[] = {'H', 'e', 'l', 'l', 'o', '\0'};
char str2[] = {"Hello"};
char str3[] = "Hello";
要注意的是,当字符数组末尾不能添加空操作符时,系统无法将字符数组当作字符串来处理。通常,一个字符串存放在一维字符数组中,多个字符串存放在二维字符数组中。当二维字符数组存放多个字符串时,数组的第一维代表存储的字符串的个数,可以省略,但第二维度代表字符串的最大长度,不能省略,例如如下等价操作:
char weekday0[7][10] = {"Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday"};
char weekday1[][10] = {"Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday"};
字符指针是指向字符型数据的指针变量,要注意的是,字符串常量本身就代表了存放它的常量存储区的首地址,是一个地址常量,例如如下等价操作:
char *ptr0 = "Hello";
char *ptr1;
ptr1 = "Hello";
其表示一个字符指针变量ptr,并用字符串常量"Hello"在常量存储区中的首地址为其初始化,即ptr指向"Hello",而不是将"Hello"赋值给ptr。一般的,"Hello"保存在只读的常量存储区中,因此只可以改变ptr的值,而不可以改变ptr指向的存储单元的值。
字符串的单个字符可以使用数组的访问方式,但要注意的是,字符串名str不可以使用str++操作,因为str是一个地址常量,其值是不能被改变的,而指向str的指针ptr是可以使用的。
字符串的输入与输出可以通过如下三种方式:
-按%c格式符,逐个字符的访问;
-按%s格式符,将字符串作为一个整体访问,但空格会作为结束标志;
-使用gets()函数,输入带空格的字符串。
2.3 动态内存
一个编译后的C获得并使用4块在逻辑上不同且用于不同目的的内存储区,分别是:
-只读存储区,存放程序的机器代码与字符串常量;
-静态存储区,存放程序的全局变量与静态变量;
-堆,调用动态内存分配函数分配自由存储单元;
-栈,保存函数调用的状态。
其中,动态内存分配函数可以申请从堆上分配内存,使用十分灵活,也易出现内存泄露问题。C的指针与动态内存分配函数联用,使定义动态数组成为可能。
考虑一个典型的,计算平均分数的例子,其实用malloc()申请动态内存,形成长度可变的动态数组:
int main() {
int *p = NULL;
int n;
double aver;
printf("How many students?");
scanf("%d", &n);
p = (int *)malloc(n * sizeof(int));
if (p == NULL) {
printf("No enough memory!\n");
exit(1);
}
printf("Input %d score:", n);
InputArray(p, n); // Input function
aver = Average(p, n); // Compute average function
printf("aver = %.1f\n", aver);
free(p);
return 0;
}
一定要注意使用free()释放申请的内存。