从零开始探索C语言(十二)----预处理器、输入输出及文件读写

文章目录

  • 1. 预处理器
    • 1.1 预处理器实例
    • 1.2 预定义宏
    • 1.3 预处理器运算符
    • 1.4 参数化的宏
  • 2. 输入和输出
    • 2.1 getchar() & putchar() 函数
    • 2.2 gets() & puts() 函数
  • 3. 文件读写
    • 3.1 打开文件
    • 3.2 关闭文件
    • 3.3 写入文件
    • 3.4 读取文件
    • 3.5 二进制 I/O 函数
  • 4. typedef 和 #define的用法与区别

1. 预处理器

C 预处理器是 C 编程语言中的一个重要组成部分,它在源代码编译之前执行一系列文本处理任务。
预处理器的任务包括宏替换文件包含条件编译等,它主要有以下几个作用:

  1. 宏替换:预处理器可以定义和展开宏,这是一种将标识符替换为具体的文本的方式。宏可以用来创建常量、函数或代码片段的别名,提高代码的可读性和维护性。

    示例:

    #define MAX(x, y) ((x) > (y) ? (x) : (y))
    int result = MAX(5, 8); // 替换为 int result = ((5) > (8) ? (5) : (8));
    
  2. 文件包含:预处理器可以使用#include指令将其他源代码文件的内容包含到当前文件中,这有助于模块化和组织代码。

    示例:

    #include  // 包含标准库头文件
    
  3. 条件编译:预处理器允许在编译时基于条件包括或排除特定部分的代码,这对于支持多个平台或构建配置非常有用。

    示例:

    #ifdef DEBUG
    // 仅在 DEBUG 定义时编译这部分代码
    #endif
    
  4. 注释删除:预处理器会删除注释,这有助于减小生成的可执行文件的大小。

  5. 符号替换:预处理器会执行一些符号替换操作,例如#操作符用于将宏参数字符串化,##操作符用于连接标识符等。

总之,C 预处理器在编译之前执行文本处理任务,以生成可编译的源代码。这使得 C 语言更加灵活,并可以根据不同的编译需求进行配置。然而,过度使用预处理器指令可能导致代码难以维护,因此需要谨慎使用。

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

我们将把 C 预处理器(C Preprocessor)简写为 CPP

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

下面列出了所有重要的预处理器指令:

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

1.1 预处理器实例

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

#define MAX_ARRAY_LENGTH 20

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

#include 
#include "myheader.h"

这些指令告诉 CPP 从系统库中获取 stdio.h,并添加文本到当前的源文件中。
下一行告诉 CPP 从本地目录中获取 myheader.h,并添加内容到当前的源文件中。

#undef  FILE_SIZE
#define FILE_SIZE 42

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

#ifndef MESSAGE
    #define MESSAGE "You wish!"
#endif

这个指令告诉 CPP 只有当 MESSAGE 未定义时,才定义 MESSAGE。
即如果 MESSAGE 这个宏没有被定义,那么将其定义为字符串 “You wish!”。如果 MESSAGE 已经在代码中定义了,那么这个代码块不会产生任何效果,因为条件不满足。这种条件编译的方式常用于确保宏在多次包含相同头文件时不会重复定义,从而避免编译错误。

#ifdef DEBUG
   /* Your debugging statements here */
#endif

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

1.2 预定义宏

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

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

让我们来尝试下面的实例:

#include 
 
main()
{
   printf("File :%s\n", __FILE__ );
   printf("Date :%s\n", __DATE__ );
   printf("Time :%s\n", __TIME__ );
   printf("Line :%d\n", __LINE__ );
   printf("ANSI :%d\n", __STDC__ );
 
}

当上面的代码(在文件 hong.c 中)被编译和执行时,它会产生下列结果:

File :.\hong.c
Date :Oct 13 2023 
Time :18:28:58    
Line :8
ANSI :1

1.3 预处理器运算符

C 预处理器提供了下列的运算符来帮助我们创建宏:

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

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

2. 字符串常量化运算符(#)
在宏定义中,当需要把一个宏的参数转换为字符串常量时,则使用字符串常量化运算符(#)。在宏中使用的该运算符有一个特定的参数或参数列表。
例如:

#include 
 
#define  message_for(a, b)  \
    printf(#a " and " #b ": We love you!\n")
 
int main(void)
{
   message_for(Carole, Debra);
   return 0;
}

当上面的代码被编译和执行时,它会产生下列结果:

Carole and Debra: We love you!

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

#include 
 
#define tokenpaster(n) printf ("token" #n " = %d", token##n)
 
int main(void)
{
   int token34 = 40;
   
   tokenpaster(34);
   return 0;
}

当上面的代码被编译和执行时,它会产生下列结果:

token34 = 40

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

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

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

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

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

#include 
 
#if !defined (MESSAGE)
   #define MESSAGE "You wish!"
#endif
 
int main(void)
{
   printf("Here is the message: %s\n", MESSAGE);  
   return 0;
}

当上面的代码被编译和执行时,它会产生下列结果:

Here is the message: You wish!

1.4 参数化的宏

CPP 一个强大的功能是可以使用参数化的宏来模拟函数。

例如,下面的代码是计算一个数的平方:

int square(int x) {
   return x * x;
}

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

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

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

例如:

#include 
 
#define MAX(x,y) ((x) > (y) ? (x) : (y))
 
int main(void)
{
   printf("Max between 20 and 10 is %d\n", MAX(10, 20));  
   return 0;
}

当上面的代码被编译和执行时,它会产生下列结果:

Max between 20 and 10 is 20

2. 输入和输出

scanf() 和 printf() 函数zheicx就不提了。

2.1 getchar() & putchar() 函数

getchar() 函数从屏幕读取下一个可用的字符,并把它返回为一个整数。

这个函数在同一个时间内只会读取一个单一的字符,所以我们可以在循环内使用这个方法,以便从屏幕上读取多个字符。

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

请看下面的实例:

#include 
 
int main( )
{
   int c;
 
   printf( "Enter a value :");
   c = getchar( );
 
   printf( "\nYou entered: ");
   putchar( c );
   printf( "\n");
   return 0;
}

当上面的代码被编译和执行时,它会等待您输入一些文本,当您输入一个文本并按下回车键时,程序会继续并只会读取一个单一的字符,显示如下:

Enter a value :12345

You entered: 1  

2.2 gets() & puts() 函数

gets() 函数从 stdin 读取一行到 s 所指向的缓冲区,直到一个终止符或 EOF。

puts() 函数把字符串 s 和一个尾随的换行符写入到 stdout。

实例

#include 
 
int main( )
{
   char str[100];
 
   printf( "Enter a value :");
   gets( str );
 
   printf( "\nYou entered: ");
   puts( str );
   return 0;
}

当上面的代码被编译和执行时,它会等待您输入一些文本,当您输入一个文本并按下回车键时,程序会继续并读取一整行直到该行结束,显示如下:

Enter a value :hello world

You entered: hello world

3. 文件读写

3.1 打开文件

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

下面是这个函数调用的原型:

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

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

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

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

"rb", "wb", "ab", "rb+", "r+b", "wb+", "w+b", "ab+", "a+b"

3.2 关闭文件

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

 int fclose( FILE *fp );

如果成功关闭文件,fclose( ) 函数返回零,如果关闭文件时发生错误,函数返回 EOF。

这个函数实际上会清空缓冲区中的数据,关闭文件,并释放用于该文件的所有内存。EOF 是一个定义在头文件 stdio.h 中的常量。

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

3.3 写入文件

下面是把字符写入到流中的最简单的函数:

int fputc( int c, FILE *fp );

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

可以使用下面的函数来把一个以 null 结尾的字符串写入到流中:

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

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

也可以使用 int fprintf(FILE *fp,const char *format, …) 函数把一个字符串写入到文件中。

尝试下面的实例:

#include 
 
int main()
{
   FILE *fp = NULL;
 
   fp = fopen("./test.txt", "w+");
   fprintf(fp, "This is testing for fprintf...\n");
   fputs("This is testing for fputs...\n", fp);
   fclose(fp);
}

当上面的代码被编译和执行时,它会在 程序所在当前目录中创建一个新的文件 test.txt,并使用两个不同的函数写入两行。

从零开始探索C语言(十二)----预处理器、输入输出及文件读写_第1张图片
接下来让我们来读取这个文件。

3.4 读取文件

下面是从文件读取单个字符的最简单的函数:

int fgetc( FILE * fp );

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

下面的函数允许我们从流中读取一个字符串:

char *fgets( char *buf, int n, FILE *fp );

函数 fgets() 从 fp 所指向的输入流中读取 n - 1 个字符。它会把读取的字符串复制到缓冲区 buf,并在最后追加一个 null 字符来终止字符串。

如果这个函数在读取最后一个字符之前就遇到一个换行符 ‘\n’ 或文件的末尾 EOF,则只会返回读取到的字符,包括换行符。

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

实例

#include 
 
int main()
{
   FILE *fp = NULL;
   char buff[255];
 
   fp = fopen("./test.txt", "r");
   fscanf(fp, "%s", buff);
   printf("1: %s\n", buff );
 
   fgets(buff, 255, (FILE*)fp);
   printf("2: %s\n", buff );
   
   fgets(buff, 255, (FILE*)fp);
   printf("3: %s\n", buff );
   fclose(fp);
 
}

当上面的代码被编译和执行时,它会读取上一部分创建的文件,产生下列结果:

1: This
2:  is testing for fprintf...  

3: This is testing for fputs...

首先,fscanf() 方法只读取了 This,因为它在后边遇到了一个空格。
其次,调用 fgets() 读取剩余的部分,直到行尾。
最后,调用 fgets() 完整地读取第二行。

3.5 二进制 I/O 函数

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

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);

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

4. typedef 和 #define的用法与区别

C 语言提供了 typedef 关键字,我们可以使用它来为类型取一个新的名字。

下面的实例为单字节数字定义了一个术语 BYTE:

typedef unsigned char BYTE;

在这个类型定义之后,标识符 BYTE 可作为类型 unsigned char 的缩写,例如:

BYTE  b1, b2;

按照惯例,定义时会大写字母,以便提醒用户类型名称是一个象征性的缩写,但您也可以使用小写字母,如下:

typedef unsigned char byte;

也可以使用 typedef 来为用户自定义的数据类型取一个新的名字。例如,可以对结构体使用 typedef 来定义一个新的数据类型名字,然后使用这个新的数据类型来直接定义结构变量,如下实例:

#include 
#include 
 
typedef struct Books
{
   char  title[50];
   char  author[50];
   char  subject[100];
   int   book_id;
} Book;
 
int main( )
{
   Book book;
 
   strcpy( book.title, "完美世界");
   strcpy( book.author, "辰东"); 
   strcpy( book.subject, "玄幻小说");
   book.book_id = 12345;
 
   printf( "书标题 : %s\n", book.title);
   printf( "书作者 : %s\n", book.author);
   printf( "书类目 : %s\n", book.subject);
   printf( "书 ID : %d\n", book.book_id);
 
   return 0;
}

当上面的代码被编译和执行时,它会产生下列结果:

书标题 : 完美世界 
书作者 : 辰东     
书类目 : 玄幻小说 
书 ID : 12345 

#define 是 C 指令,用于为各种数据类型定义别名,与 typedef 类似,但是它们有几点不同。

#define 和 typedef 的区别

1. typedef 仅限于为类型定义符号名称,#define 不仅可以为类型定义别名,也能为数值定义别名,比如您可以定义 1 为 ONE。
2. typedef 是由编译器执行解释的,#define 语句是由预编译器进行处理的。

下面是 #define 的最简单的用法:

实例

#include 
 
#define TRUE  1
#define FALSE 0
 
int main( )
{
   printf( "TRUE 的值: %d\n", TRUE);
   printf( "FALSE 的值: %d\n", FALSE);
 
   return 0;
}

当上面的代码被编译和执行时,它会产生下列结果:

TRUE 的值: 1
FALSE 的值: 0

你可能感兴趣的:(C语言,c语言)