C语言学习教程(七)

C语言学习教程(七):本系列教程第21-25章。

21-Input & Output

当我们说输入(Input)时,这意味着将一些数据输入程序。输入可以是以文件的形式或命令行的形式中进行。C 语言提供了一套内置函数来读取给定的输入并根据需要将其提供给程序。

当我们说输出(Output)时,意思是在屏幕、打印机或任何文件中显示一些数据。C 语言提供了一组内置函数来将数据输出到计算机屏幕上或将这些数据保存为文本或二进制文件。

(1)标准文件

C语言将所有设备都当作文件。因此,所有设备(比如显示器)被处理的方式与文件相同。以下三个文件会在程序执行时自动打开,以便访问键盘和屏幕。

Standard File File Pointer Device
标准输入(Standard input) stdin 键盘
标准输出(Standard output) stdout 屏幕
标准错误(Standard error) stderr 屏幕

文件指针(File Pointer)是访问文件的方式,本节将讲解如何从键盘上读取值以及如何把结果输出到屏幕上。

(2)getchar()和putchar()

int getchar(void)函数,从屏幕读取下一个可用字符并将其作为整数返回。需要注意,此函数一次只能读取一个字符。如果你想从屏幕上读取多个字符,可以在循环中使用此方法。

int putchar(int c)函数,把传递的字符输出到屏幕上,并返回相同的字符。这个函数在同一个时间内只能输出一个单一的字符。你可以在循环内使用这个方法,以便在屏幕上输出多个字符。

示例代码:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
#include <stdio.h>

int main(){
	int c;

	printf("Enter a value: ");
	c = getchar();

	printf("You entered: ");
	putchar(c);

	return 0;
}

运行结果:

1
2
3
$ ./test1
Enter a value: this is test.
You entered: t

(3)gets()和puts()

char *gets(char *str)函数,从stdin读取一行,并把它存储到到str所指向的缓冲区(Buffer)中,直到遇到一个换行符或EOF(End of File,文件结尾)。

int puts(const char *str)函数,把字符串str和一个尾随的换行符写入到stdout

示例代码:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
#include <stdio.h>

int main(){
	char str[100];

	printf("Enter a value: ");
	gets(str);

	printf("You entered: ");
	puts(str);

	return 0;
}

运行结果:

1
2
3
4
$ gcc -o test2 test2.c
$ ./test2
Enter a value: this is test.
You entered: this is test.

(4)scanf()和printf()

C 语言中的 I/O(输入/输出)通常使用scanf()printf()两个函数。scanf()函数用于从标准输入(键盘)读取并格式化数据, printf()函数发送格式化的数据输出到标准输出(屏幕)。

int scanf(const char *format, ...)函数,从标准输入流stdin读取输入,并按照提示的**格式(format)**来输入。

int printf(const char *format, ...)函数,把输出写入到标准输出流stdout,并根据提供的**格式(format)**来输出。

format(格式) 可以是一个简单的常量字符串,但是你可以分别指定 %s%d%c%f 等来输出或读取字符串、整数、字符或浮点数。还有许多其他可用的格式选项,可以根据需要使用。如需了解完整的细节,可以查看这些函数的参考手册。现在让我们通过下面这个简单的实例来加深理解:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
#include <stdio.h>

int main(){

	int i;
	char str[100];

	printf("Enter a value: ");
	scanf("%s %d", str, &i);

	printf("You entered: %s %d\n", str, i);

	return 0;
}

运行结果:

1
2
3
4
$ gcc -o test3 test3.c
$ ./test3
Enter a value: seven 7
You entered: seven 7

需要注意的是,scanf()函数期待输入的格式与你给出的%s%d相同,这意味着您必须提供有效的输入,比如 string integer,如果您提供的是string stringinteger integer,它会被认为是错误的输入。

特别注意:在读取字符串时,只要遇到一个空格,scanf()函数就会停止读取,所以,this is test对scanf()函数来说是三个字符串。

22-File I/O

上一章我们讲解了 C 语言处理的标准输入和输出设备。本章我们将介绍 C 程序员如何创建、打开、关闭文本文件或二进制文件来进行数据存储。

一个文件,无论它是文本文件还是二进制文件,都是代表了一个字节序列。

C语言不仅提供了对高级函数的访问,还提供了底层调用(操作系统级)来处理存储设备上的文件。本章将讲解文件管理的重要调用。

(1)打开文件

你可以使用fopen()函数来创建一个新文件或打开一个已存在的文件,这个函数调用会初始化一个FILE类型的对象,FILE类型的对象包含了所有用来控制流(Stream)的必要信息。下面是fopen()函数调用的原型:

1
FILE *fopen( const char * filename, const char * mode );

在这里,filename是字符串,用来标识文件名称,访问模式mode的值可以是下列值中的一个:

mode Description
r 打开一个现有的文本文件,允许读取文件。
w 打开一个文本文件,允许写入文件。如果文件不存在,则会创建一个新文件。如果文件存在,则文件会被截断为零长度,重新写入。在这里,你的程序会从文件的开头写入内容。
a 打开一个文本文件,以追加模式写入文件。如果文件不存在,则会创建一个新文件。在这里,你的程序会在已有的文件内容中追加内容。
r+ 打开一个文本文件,允许读写文件。
w+ 打开一个文本文件,允许读写文件。如果文件不存在,则会创建一个新文件。如果文件已存在,则文件会被截断为零长度,重新写入。
a+ 打开一个文本文件,允许读写文件。如果文件不存在,则会创建一个新文件。读取会从文件的开头开始,写入则只能是追加模式。

如果处理的是二进制文件,则需使用下面的访问模式来取代上面的访问模式:

1
2
3
4
"rb", "wb", "ab", 
"rb+", "r+b", 
"wb+", "w+b", 
"ab+", "a+b",

(2)关闭文件

为了关闭文件,需要使用fclose()函数。fclose()函数的原型如下:

1
int fclose( FILE *fp );

如果成功关闭文件,fclose()函数返回,如果关闭文件时发生错误,函数返回EOF(End of File,文件结尾)EOF是一个定义在头文件 stdio.h 中的常量。

fclose()函数实际上会清空缓冲区中的数据,关闭文件,并释放用于该文件的所有内存。

(3)写入文件

C 标准库提供了各种函数来按字符或者以固定长度字符串的形式来读写文件。

①下面是把一个字符写入到流(Stream)中的最简单的函数fputc()

1
int fputc( int c, FILE *fp );

函数fputc(),把参数c的字符值写入到fp所指向的输出流中。如果写入成功,它会返回写入的字符,如果发生错误,则会返回EOF

②下面是把一个以null结尾的字符串写入到流(Stream)中函数fputs()

1
int fputs( const char *s, FILE *fp );

函数fputs()把字符串s写入到fp所指向的输出流中。如果写入成功,它会返回一个非负值,如果发生错误,则会返回EOF

③你也可以使用函数fprintf()把一个字符串写入到文件中。

1
int fprintf(FILE *fp,const char *format, ...)

示例代码:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
#include <stdio.h>

int main(){
	FILE *fp = NULL;

	fp = fopen("./test.txt", "w+");
	
    fputc(97, fp);  //十进制ASCII值97对应小写字母a.
	fputc(10, fp);  //换行符LF(Line Feed)的ASCII值为十进制10.
	fputs("This is testing for fputs...\n", fp);
	fprintf(fp, "This is testing for fprintf...\n");

	fclose(fp);

	return 0;
}

运行结果:

1
2
3
4
5
6
7
$ gcc -o test1 test1.c
$ ./test1

$ cat test.txt
a
This is testing for fputs...
This is testing for fprintf...

(4)读取文件

①下面是从文件读取单个字符的最简单的函数fgetc()

1
 int fgetc( FILE * fp );

函数fgetc(),从fp所指向的输入文件中读取一个字符。返回值是读取的字符,如果发生错误则返回EOF

②下面的函数允许你从流中读取一个字符串

1
char *fgets( char *buffer, int n, FILE *fp );

函数fgets(),从fp所指向的输入流中读取n - 1个字符。它会把读取的字符串复制到缓冲区buffer中,并在最后追加一个null字符('\0')来终止字符串。如果这个函数在读取最后一个字符之前就遇到一个换行符\n或文件末尾符 EOF,则只会返回到该字符前读取到的字符,包括这个换行符或EOF。

③您也可以使用fscanf()函数来从文件中读取字符串,但是在遇到第一个空格和换行符时,它会停止读取。

1
 int fscanf(FILE *fp, const char *format, ...)

示例代码:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
#include <stdio.h>

int main(){
	char c;
	char buffer[255];
	FILE *fp = NULL;

	fp = fopen("./test.txt", "r");

	c = fgetc(fp);            //读取一个字符
	printf("1: %c\n", c);

	c = fgetc(fp);            //读取一个字符
	printf("2: %c", c);

	fgets(buffer, 255, fp);   //遇到换行符(LF)或文件末尾符(EOF)时,它会停止读取。
	printf("3: %s", buffer);

	fscanf(fp, "%s", buffer);  //遇到空格和换行符时,它会停止读取。
	printf("4: %s", buffer);

	return 0;
}

文件test.c内容:

1
2
3
4
$ cat test.txt
a
This is testing for fputs...
This is testing for fprintf...

运行结果:

1
2
3
4
5
$ ./test2
1: a
2:
3: This is testing for fputs...
4: This

可以看到,fscanf()函数只读取了字符串This,因为它在后边遇到了一个空格。

(5)二进制I/O函数

下面两个函数用于二进制输入和输出:

1
2
3
size_t fread(void *ptr, size_t size_of_elements, size_t number_of_elements, FILE *a_file);

size_t fwrite(const void *ptr, size_t size_of_elements, size_t number_of_elements, FILE *a_file);

这两个函数都是用于存储块的读写——通常是数组或结构体。

23-Preprocessors

C 预处理器(C Preprocessor)不是编译器的组成部分,但是它是编译过程中一个单独的步骤。简言之,C 预处理器只不过是一个文本替换工具而已,它们会指示编译器在实际编译之前完成所需的预处理。我们将把C 预处理器(C Preprocessor)简写为CPP

所有的预处理器命令都是以井号(#)开头的。它必须是第一个非空字符,为了增强可读性,预处理器指令应从第一列开始。下面列出了所有重要的预处理器指令:

指令 描述
#include 包含一个源代码文件
#define 定义宏
#undef 取消已定义的宏
#ifdef 如果宏已经定义,则返回真
#ifndef 如果宏没有定义,则返回真
#if 如果给定条件为真,则编译下面代码
#else #if的替代方案
#elif 如果前面的 #if 给定条件不为真,但当前条件为真,则编译下面代码
#endif 结束一个 #if……#else 条件编译块
#error 当遇到标准错误(stderr)时,输出错误消息
#pragma 使用标准化方法,向编译器发布特殊的命令到编译器中

(1)预处理器实例

分析下面的实例来理解不同的指令。

1
#define MAX_ARRAY_LENGTH 20

这个指令告诉 CPP 把所有的 MAX_ARRAY_LENGTH 定义为 20。使用#define定义常量来增强可读性。

1
2
#include <stdio.h>
#include "myheader.h"

第一条指令告诉 CPP 从系统库中获取stdio.h,并添加内容到当前的源文件中。

第二条指令告诉 CPP 从本地目录中获取myheader.h,并添加内容到当前的源文件中。

1
2
#undef  FILE_SIZE
#define FILE_SIZE 42

这个指令告诉 CPP 取消之前定义的 FILE_SIZE,并新定义它为 42。

1
2
3
#ifndef MESSAGE
   #define MESSAGE "You wish!"
#endif

这个指令告诉 CPP 只有当 MESSAGE 未定义时,才定义 MESSAGE。

1
2
3
#ifdef DEBUG
   /* Your debugging statements here */
#endif

这个指令告诉 CPP 如果定义了 DEBUG,则执行处理语句。在编译时,如果你向 gcc 编译器传递了-DDEBUG参数,这个指令就非常有用。它定义了 DEBUG,你可以在编译期间随时开启或关闭调试。

(2)预定义宏

ANSI C 定义了许多宏。在编程中你可以使用这些宏,但是不能直接修改这些预定义的宏。

描述
__DATE__ 当前日期,一个以 “MMM DD YYYY” 格式表示的字符常量。
__TIME__ 当前时间,一个以 “HH:MM:SS” 格式表示的字符常量。
__FILE__ 这会包含当前文件名,一个字符串常量。
__LINE__ 这会包含当前行号,一个十进制常量。
__STDC__ 当编译器以 ANSI 标准编译时,则定义为 1。

示例代码:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
#include <stdio.h>

int main(){

	printf("File : %s\n", __FILE__);
	printf("LINE : %d\n", __LINE__);
	printf("ANSI : %d\n", __STDC__);
	printf("Date : %s\n", __DATE__);
	printf("Time : %s\n", __TIME__);
	
	return 0;
}

运行结果:

1
2
3
4
5
6
7
$ gcc -o test1 test1.c
$ ./test1
File : test1.c
LINE : 6
ANSI : 1
Date : Oct 17 2022
Time : 17:07:51

(3)预处理器运算符

C 预处理器提供了下面的运算符来帮助你创建宏。

①宏延续运算符(\)

一个宏通常写在一个单行上。但是如果宏太长,一个单行容纳不下,则使用宏延续运算符(\)。例如:

1
2
#define  message_for(a, b)  \
    printf(#a " and " #b ": We love you!\n")

②字符串常量化运算符(#)

在宏定义中,当需要把一个宏的参数转换为字符串常量时,则使用字符串常量化运算符(#)。

此运算符只能在具有指定参数或参数列表的宏中使用。例如:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
#include <stdio.h>

#define  message_for(a, b)  \
    printf(#a " and " #b ": We love you!\n")
//#a:可以理解为一个占位符
//#b:可以理解为一个占位符
//" and ":表示一个字符串" and "
//": We love you!\n":表示一个字符串": We love you!\n"


int main(void) {

	message_for(Carole, Debra);

	return 0; 
}

运行结果:

1
2
3
$ gcc -o test2 test2.c
$ ./test2
 Carole and Debra : We love you!

③标记粘贴运算符(##)

宏定义内的标记粘贴运算符(##)会合并两个参数。它允许你在宏定义中两个独立的标记被合并为一个标记。例如:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
#include <stdio.h>

#define tokenpaster(n) printf("token" #n " = %d", token##n)


int main(){

	int token34 = 40;

	tokenpaster(34);

	return 0;
}

运行结果:

1
2
3
$ gcc -o test3 test3.c
$ ./test3
token34 = 40

这是怎么发生的,因为这个实例会从编译器产生下列的实际输出:

1
printf ("token34 = %d", token34);

这个实例演示了token##n会连接到token34中,在这里,我们使用了字符串常量化运算符(#)和标记粘贴运算符(##)。

defined()运算符

预处理器defined()运算符是用在常量表达式中的,用来确定一个标识符是否已经使用#define定义过。如果指定的标识符已定义,则值为真(非零)。如果指定的标识符未定义,则值为假(零)。

下面的实例演示了defined()运算符的用法:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
#include <stdio.h>

#if !defined(MESSAGE)
	#define MESSAGE "You Wish!"
#endif

int main(){

	printf("Here is the message: %s\n", MESSAGE);

	return 0;
}

运行结果:

1
2
3
$ gcc -o test4 test4.c
$ ./test4
Here is the message: You Wish!

(4)参数化的宏

CPP 一个强大的功能是可以使用参数化的宏来模拟函数。例如,下面的代码是计算一个数的平方的函数:

1
2
3
int square(int x){
	return x * x;
}

我们可以使用宏重写上面的代码,如下:

1
#define square(x) ((x) * (x))

在使用带有参数的宏之前,必须使用#define指令定义。参数列表是括在圆括号内,且必须紧跟在宏名称的后边。宏名称和左圆括号之间不允许有空格。例如:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
#include <stdio.h>

#define MAX(x,y) ((x)>(y)?(x):(y))

int main(){

	printf("Max between 20 and 10 is %d\n", MAX(10, 20));

	return 0;
}

运行结果:

1
2
3
$ gcc -o test5 test5.c
$ ./test5
Max between 20 and 10 is 20

24-Header Files

头文件是扩展名为.h的文件,包含了 C 函数声明和宏定义,并在多个源文件之间共享。

有两种类型的头文件:程序员编写的头文件编译器自带的头文件

在程序中要使用头文件,需要使用 C 预处理指令#include来引用它。前面我们已经看过引用stdio.h的头文件,它是编译器自带的头文件。

引用头文件相当于复制头文件中的内容,但是我们一般不会直接在源文件中复制粘贴头文件的内容,因为这么做很容易出错,特别在程序是由多个源文件组成的时候。

C 或 C++ 程序中的一个简单做法是,我们将所有常量、宏、系统范围的全局变量和函数原型保存在头文件中,并在需要的地方包含该头文件。

(1)引用头文件的语法

使用预处理指令#include可以引用用户和系统的头文件。它的形式有以下两种:

1
#include <file>

这种形式用于引用系统头文件。它在系统目录的标准列表中搜索名为file的文件。在编译源代码时,你可以通过-I选项将目录添加到此列表中。

1
#include "file"

这种形式用于引用用户头文件。它在包含当前文件的目录中搜索名为file的文件。如果在当前文件目录没有找到引用的头文件,则会到系统目录的标准列表中找该头文件。在编译源代码时,你可以通过-I选项将目录添加到此列表中。

gcc -I

-I: Add directory to the end of the list of include search paths

(2)引用头文件的操作

#include指令会指示 C 预处理器浏览指定的文件作为输入。预处理器的输出包含了:已经生成的输出被引用文件生成的输出以及#include指令之后的文本输出

例如,如果您有一个头文件header.h,内容如下:

1
char *test (void);

和一个使用了头文件的主程序program.c,内容如下:

1
2
3
4
5
6
int x;
#include "header.h"

int main(void){
    puts(test());
}

编译器会看到如下的代码信息:

1
2
3
4
5
6
int x;
char *test (void);

int main(void){
    puts(test());
}

(3)只引用一次头文件

如果一个头文件被引用两次,编译器会处理两次头文件的内容,这将产生错误。为了防止这种情况,标准的做法是把文件的整个内容放在条件编译语句中,如下:

1
2
3
4
5
6
#ifndef HEADER_FILE
#define HEADER_FILE

the entire header file file

#endif

这种结构就是通常所说的包装器#ifndef。当再次引用头文件时,条件为假,因为 HEADER_FILE 已定义。此时,预处理器会跳过文件的整个内容,编译器会忽略它。

(4)有条件的引用头文件

有时需要从多个不同的头文件中选择一个引用到程序中。例如,需要指定在不同的操作系统上使用的配置参数。你就可以通过一系列条件来实现这点,如下:

1
2
3
4
5
6
7
#if SYSTEM_1
   #include "system_1.h"
#elif SYSTEM_2
   #include "system_2.h"
#elif SYSTEM_3
   ...
#endif

但是如果头文件比较多的时候,这么做是很不妥当的,预处理器使用宏来定义头文件的名称。这就是所谓的有条件引用。它不是用头文件的名称作为#include的直接参数,你只需要使用宏名称代替即可:

1
2
3
#define SYSTEM_H "system_1.h"
...
#include SYSTEM_H

SYSTEM_H 会扩展,预处理器会查找 system_1.h,就像#include最初编写的那样。SYSTEM_H 可通过 -D 选项被您的 Makefile 定义。

25-Type Casting

强制类型转换(Type Casting)是把变量从一种数据类型转换为另一种数据类型。例如,如果你想存储一个 long 类型的值到一个简单的整型中,您需要把 long 类型强制转换为 int 类型。您可以使用强制类型转换运算符来把值显式地从一种类型转换为另一种类型,如下所示:

1
(type_name) expression

请看下面的实例,使用强制类型转换运算符把一个整数变量除以另一个整数变量,得到一个浮点数:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
#include <stdio.h>

int main(){

    int count = 5;
    int sum = 17;
    double mean;

    mean = (double) sum / count;

    printf("Value of mean : %f\n", mean);
    
    return 0;
}

运行结果:

1
2
3
$ gcc -o test1 test1.c
$ ./test1
Value of mean : 3.400000

这里要注意的是强制类型转换运算符的优先级大于除法,因此 sum 的值首先被转换为 double 型,然后除以 count,得到一个类型为 double 的值。

类型转换可以是隐式的,由编译器自动执行,也可以是显式的,通过使用强制类型转换运算符来指定。

在编程时,有需要类型转换的时候都要用上强制类型转换运算符,是一种良好的编程习惯。

(1)整数提升

整数提升(Integer Promotion)是指把小于 intunsigned int 的整数类型转换为 intunsigned int 的过程。请看下面的实例,在 int 中添加一个字符:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
#include <stdio.h>

int main(){

	int i = 17;
	char c = 'c'; //ascii 'c', value is 99
	int sum;

	sum = i + c;

	printf("Value of sum : %d\n", sum);

	return 0;
}

运行结果:

1
2
3
$ gcc -o test2 test2.c
$ ./test2
Value of sum : 116

在这里,sum 的值为 116,因为编译器进行了整数提升,在执行实际加法运算时,把 ‘c’ 的值转换为对应的 ascii 值。

(2)常用的算数转换

常用的算术转换是隐式地把值强制转换为相同的类型。编译器首先执行整数提升,如果操作数类型不同,则它们会被转换为下列层次中出现的最高层次的类型:

image-20221017212006245

常用的算术转换不适用于赋值运算符、逻辑运算符 && 和 ||。

让我们看看下面的实例来理解这个概念:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
#include <stdio.h>

int main(){

	int i = 17;
	char c = 'c'; //ascii 'c', value is 99
	float sum;

	sum = i + c; 

	printf("Value of sum : %f\n", sum);

	return 0;
}

运行结果:

1
2
3
$ gcc -o test3 test3.c
$ ./test3
Value of sum : 116.000000

在这里,c 首先被转换为整数,但是由于最后的值是 float 型的,所以会采用常用的算术转换,编译器会把 i 和 c 转换为浮点型,并把它们相加得到一个浮点数。

updatedupdated2022-10-172022-10-17