Bootstrap

【C语言高级指导】标准库和输入输出

标准库

标准库是一组预先定义好的函数和宏,它们为C语言提供了广泛的功能支持,包括但不限于输入输出操作、字符串处理、数学计算、内存管理、文件操作等。这些库函数是C语言编程的基础,被广泛地使用在各种C语言程序中。

标准库的使用

使用C语言标准库时,通常需要遵循以下步骤:

  1. 包含头文件:使用标准库的函数之前,需要在你的C程序中包含相应的头文件。例如,如果你想使用printf函数进行输出,你需要包含stdio.h头文件。

    #include <stdio.h>
    
  2. 编写函数原型:在调用任何函数之前,你需要知道它的原型,即函数的返回类型、名称和参数列表。标准库的函数原型通常在相应的头文件中定义。

  3. 调用函数:使用正确的参数调用函数。例如,使用printf函数打印字符串:

    printf("Hello, World!\n");
    
  4. 处理返回值:许多函数会返回一个值,例如strlen函数返回字符串的长度。你需要根据函数的返回值进行相应的处理。

    char str[] = "Hello";
    int length = strlen(str);
    printf("Length of the string is: %d\n", length);
    
  5. 内存管理:如果你使用malloccalloc等函数动态分配内存,记得在使用完毕后释放内存,以避免内存泄漏。

    int *array = malloc(10 * sizeof(int));
    if (array != NULL) {
        // 使用分配的内存
        free(array); // 释放内存
    }
    
  6. 错误处理:使用标准库函数时,需要检查它们是否执行成功。例如,fscanf函数会返回读取的项目数,如果返回值小于预期,可能表示输入有问题。

    int num;
    if (fscanf(stdin, "%d", &num) != 1) {
        printf("Error reading input.\n");
    }
    
  7. 使用宏:标准库中的一些功能是通过宏实现的,例如NULL宏表示空指针。

  8. 遵循标准:确保你的代码遵循C语言的标准,以确保在不同平台和编译器上的兼容性。

  9. 了解库限制:不同的编译器和平台可能对标准库的实现有细微差异,了解这些差异可以帮助你编写更健壮的代码。

  10. 使用合适的函数:选择最适合你需求的函数。例如,如果你需要复制字符串,使用strcpy而不是strcat,因为strcat会连接字符串,而不是复制。

下面是一个简单的示例,演示了如何使用标准库中的几个函数:

#include <stdio.h>
#include <stdlib.h>
#include <string.h>

int main() {
    char buffer[100];
    printf("Enter your name: ");
    if (fgets(buffer, sizeof(buffer), stdin) != NULL) {
        char *name = strtok(buffer, "\n"); // 移除换行符
        printf("Hello, %s!\n", name);
    }

    int *numbers = malloc(5 * sizeof(int));
    if (numbers != NULL) {
        for (int i = 0; i < 5; ++i) {
            numbers[i] = i * i;
        }
        printf("Squares: ");
        for (int i = 0; i < 5; ++i) {
            printf("%d ", numbers[i]);
        }
        printf("\n");
        free(numbers);
    }

    return 0;
}

在这个示例中,我们使用了stdio.h中的printffgetsstdlib.h中的mallocfree,以及string.h中的strtok

对标准库中所用名字的限制

在C语言标准库中,有一些关于名字使用的规则和限制,这些规则主要是为了避免名称冲突、确保代码的可移植性,并遵循良好的编程实践。以下是一些主要的限制和建议:

  1. 避免使用标准库的宏和函数名:不要定义宏或者函数使用与标准库中相同的名字,这可能会导致编译错误或者运行时行为不可预测。
  2. 避免使用保留关键字:C语言有一组保留关键字,如intfloatifelse等,这些关键字在C语言中有特定的含义,不能用作变量名、宏名或其他标识符。
  3. 使用下划线开头的名称:在C语言中,以下划线开头的名称(如_foo)通常保留给编译器或者操作系统内部使用。尽管这不是强制性的,但是出于对编译器和操作系统的尊重,最好避免使用这种命名方式。
  4. 遵循命名规范:对于变量和函数名,应该使用易于理解的命名方式,避免使用单字符或者模糊的名称。例如,使用totalLength而不是tll
  5. 避免使用大写字母开头的名称:大写字母开头的名称(如FOO)通常用于宏定义,因此最好避免用这种方式命名变量或函数。
  6. 避免使用非标准字符:在标识符中,只使用字母、数字和下划线。避免使用空格、特殊字符或国际字符,因为它们可能会导致代码在不同的编译器或平台上出现问题。
  7. 避免过长的名称:虽然C语言允许较长的名称,但是过长的名称可能会使代码难以阅读和维护。保持名称简洁而具有描述性。
  8. 避免使用内联汇编的名称:如果你的代码中使用了内联汇编,确保不要使用与内联汇编指令相同的名称。
  9. 遵循编码风格:不同的项目或团队可能有不同的编码风格,遵循这些风格可以提高代码的一致性和可读性。
  10. 考虑可移植性:在编写代码时,考虑到不同平台和编译器的兼容性,避免使用特定于平台的特性或名称。

遵守这些规则和建议,可以帮助你编写出更加健壮、可读和可移植的C语言代码。

使用宏隐藏的函数

在C语言中,宏(Macro)是一种预处理器指令,它允许你定义一个简短的名称来替代更长的代码片段。使用宏可以隐藏函数的实现细节,使得代码更加简洁和易于维护。然而,宏的使用也存在一些限制和潜在的问题,以下是一些使用宏隐藏函数时的注意事项:

  1. 函数式宏:宏可以用来定义函数式宏,即模拟函数的行为。例如:

    #define SQUARE(x) ((x) * (x))
    

    这里SQUARE宏接受一个参数x,并返回其平方值。

  2. 参数和返回类型:在使用宏定义函数时,需要确保参数的类型正确,并且宏的返回值类型与预期一致。

  3. 副作用:宏中的参数如果有副作用(例如,是递增或递减操作),可能会导致意外的行为。例如:

    #define INCREMENT_AND_SQUARE(x) ((x)++ * (x))
    

    这个宏首先递增x,然后计算其平方。但是,如果x是一个宏或者表达式,那么递增操作可能会导致问题。

  4. 调试问题:由于宏在预处理阶段就被展开,所以在调试时可能看不到宏的原始形式,这可能会使调试变得更加困难。

  5. 代码膨胀:宏在每个使用点都会被展开,这可能会导致代码膨胀,特别是在宏体较大或者在循环中多次使用时。

  6. 作用域问题:宏没有作用域的概念,它们在定义它们的文件中是全局可见的。这可能会导致意外的名称冲突。

  7. 条件编译:宏可以用来控制条件编译,但是过度使用条件编译可能会使代码难以理解和维护。

  8. 避免复杂的逻辑:过于复杂的宏可能会使代码难以阅读和理解。在这种情况下,使用内联函数可能更合适。

  9. 类型安全:宏不是类型安全的,它们在预处理阶段只是简单的文本替换,不会进行类型检查。

  10. 使用内联函数:对于复杂的函数,使用内联函数(使用inline关键字)可能是更好的选择,因为它们提供了类型检查和作用域控制。

下面是一个使用宏隐藏函数实现的简单例子:

#include <stdio.h>

#define MAX(a, b) ((a) > (b) ? (a) : (b))

int main() {
    int x = 5, y = 10;
    printf("The maximum is: %d\n", MAX(x, y));
    return 0;
}

在这个例子中,MAX宏接受两个参数并返回它们的较大值。使用宏的好处是代码简洁,但是也要注意上述提到的潜在问题。

C89标准库概述

C89标准,也被称为ANSI C或C90标准23,是C语言的第一个官方标准,由美国国家标准协会(ANSI)在1989年发布,并在1990年被国际标准化组织(ISO)和国际电工委员会(IEC)采纳,正式名称为ISO/IEC 98992。C89标准确立了C语言的基本语法、数据类型、运算符、控制结构、函数以及标准库等内容,其核心目标是确保C语言在不同平台和编译器上的可移植性3。

C89标准库是C89标准的一部分,它定义了一系列标准库函数,这些函数覆盖了广泛的功能,包括但不限于输入输出操作、字符串处理、数学计算、内存分配、字符分类等。这些库函数为C语言程序提供了基础功能支持,是C语言编程不可或缺的一部分4。

C89标准库包含了一系列头文件,例如:

  • ctype.h:提供字符分类和转换函数,如isalphaisdigit等4。
  • math.h:提供数学计算相关的函数,如sincoslog等4。
  • stdio.h:提供标准输入输出函数,如printfscanf等6。
  • stdlib.h:提供内存分配、进程控制等函数,如mallocfreeexit等4。
  • string.h:提供字符串操作函数,如strcpystrlen等4。
  • time.h:提供时间日期相关的函数4。

此外,1995年的Normative Addendum 1(NA1)批准了3个新的头文件加入C标准库,包括iso646.hwchar.hwctype.h4。随后的C99和C11标准又进一步增加了更多的头文件和功能,但C89标准库本身定义了C语言编程的基础4。

C99标准库更新

C99标准在1999年发布,对C语言进行了扩展和改进,特别是在标准库方面引入了多项更新和新增功能11121314。以下是C99标准库的一些主要更新:

  1. 新增头文件:C99增加了几个新的头文件,例如 <stdbool.h> 引入了布尔类型 _Bool 和宏 truefalse<complex.h> 提供了复数类型 _Complex 以及相关的数学函数;<fenv.h> 用于浮点环境控制;<inttypes.h> 提供了固定宽度的整数类型和格式化输出;<iso646.h> 定义了替代运算符;<wchar.h><wctype.h> 用于宽字符和宽字符分类。
  2. 变长数组(Variable Length Arrays, VLAs):C99允许在函数内部定义数组时使用变量作为数组的长度,这是C89所不支持的12。
  3. 复合字面量(Compound Literals):C99允许在声明时直接构造匿名结构体或数组对象,无需先声明再赋值14。
  4. 单行注释(//):C99引入了C++风格的单行注释,使得代码注释更为便捷12。
  5. 内联函数(Inline Functions):C99标准中对内联函数的支持更加明确,使用 inline 关键字声明的函数可以在编译时被展开,以减少函数调用的开销13。
  6. 新的数据类型:C99引入了 long long int 类型,提供了至少64位的整数类型,以及与之对应的 unsigned long long int 无符号类型12。
  7. 改进的类型安全性:C99通过引入 restrict 关键字,增强了指针的类型安全性,允许编译器优化访问受限的指针13。
  8. 标准库函数的增强:C99对现有标准库函数进行了增强和扩展,例如数学库函数增加了对长双精度类型 long double 的支持14。
  9. 更严格的类型转换规则:C99加强了类型转换的严格性,减少了隐式类型转换可能带来的问题14。

这些更新使得C99标准在功能上更加丰富和现代化,提高了编程的灵活性和效率。然而,需要注意的是,并非所有编译器都完全支持C99标准的所有特性,开发者在使用时应考虑目标编译器的实际支持情况13。

<stddef.h>:常用定义

<stddef.h> 是C语言标准库的一部分,它定义了一组与通用数据类型和宏相关的项目,这些定义对于实现C标准库的其他部分是必需的。以下是 <stddef.h> 中一些常用的定义:

  1. NULL: 定义了一个空指针常量,表示一个空(null)的指针值。

  2. size_t: 定义了无符号整数类型,通常用于表示大小和计数,如 strlen 函数的返回类型。

  3. ptrdiff_t: 定义了有符号整数类型,通常用于表示指针之间的差异。

  4. wchar_t: 定义了宽字符类型,用于表示宽字符常量和变量。

  5. offsetof: 一个宏,用于计算结构体成员的偏移量。

  6. NULL 的定义: 在 C99 之前,NULL 通常定义为 (void *)00,但在 C99 中,它被明确定义为宏。

  7. 最大和最小宏:定义了 INT_MININT_MAX 等宏,表示 int 类型可能的最小和最大值。

  8. 其他类型定义:可能还包括其他类型的最小和最大值定义,例如 CHAR_BITCHAR_MINCHAR_MAX 等,这些定义了 char 类型的特性。

  9. 通用类型别名:例如,intmax_tuintmax_t 分别是最大宽度的有符号和无符号整数类型。

  10. wchar_t 相关的宏:定义了与宽字符类型相关的宏,如 WCHAR_MAXWCHAR_MIN

  11. EXIT_SUCCESSEXIT_FAILURE:定义了程序退出时使用的宏,通常用于 exit 函数。

  12. 动态内存分配相关宏:如 malloc_size 等,可能在某些实现中用于获取 malloc 分配的内存块大小。

<stddef.h> 头文件通常在编译时由编译器自动包含,因为它被其他标准头文件所依赖。这些定义使得程序员能够编写与平台无关的代码,因为它们提供了一种标准的方式来处理大小和计数,以及确保数据类型在不同平台上的一致性。

<stdbool.h>:布尔类型和值

<stdbool.h> 是 C99 标准引入的一个头文件,它定义了 C 语言中的布尔类型 _Bool 以及两个宏 truefalse。在 C89 和之前的版本中,C 语言并没有原生的布尔类型,通常使用 int 类型和特定的值(如 0 表示假,非 0 值表示真)来模拟布尔逻辑。C99 通过 <stdbool.h> 头文件提供了一种更清晰和类型安全的方式来处理布尔值。

以下是 <stdbool.h> 头文件中定义的主要内容:

  1. _Bool: 定义了布尔类型,它是一个单一比特的整数类型,足以存储布尔值 truefalse

  2. bool: 在 C99 中,bool_Bool 的别名。但在 C++ 中,bool 是一个独立的类型。

  3. true: 定义了宏 true,其值为 1,表示布尔真值。

  4. false: 定义了宏 false,其值为 0,表示布尔假值。

  5. __bool_true_false_are_defined: 这个宏在 <stdbool.h> 被包含后定义,表明 stdbool.h 已经提供了 _Boolbooltruefalse 的定义。

使用 <stdbool.h> 可以提高代码的可读性和可维护性,特别是在需要进行布尔逻辑操作时。下面是一个使用 <stdbool.h> 的示例:

#include <stdio.h>
#include <stdbool.h>

bool is_positive(int number) {
    return number > 0;
}

int main() {
    int number = 5;
    if (is_positive(number)) {
        printf("The number is positive.\n");
    } else {
        printf("The number is not positive.\n");
    }
    return 0;
}

在这个示例中,is_positive 函数使用 bool 类型来表示函数的返回值,它根据传入的 number 是否大于 0 返回 truefalse。然后在 main 函数中根据返回值打印相应的信息。这种方式比使用 int 类型和特定的整数值来表示布尔逻辑更为清晰和直观。

输入与输出

在C语言中,流(Stream)是一个抽象概念,用于表示任何序列的输入输出操作。流可以是文件、键盘(标准输入)、屏幕(标准输出和标准错误)等。流通过文件指针(File Pointer)来访问,文件指针是一个指向文件控制块(FCB)的指针,FCB 包含了流的所有相关信息。

文件指针

文件指针通常在使用 fopen 函数打开文件时被创建,并返回一个指向新流的指针。如果 fopen 调用失败,它会返回 NULL。文件指针用于后续的文件操作,如读写、定位等。

标准流

C语言定义了三个标准流:

  • stdin(标准输入):通常与键盘输入关联。
  • stdout(标准输出):通常与屏幕输出关联。
  • stderr(标准错误):用于输出错误信息,也与屏幕关联,但通常不与其他输出混合。

重定向

重定向是将标准流的输入/输出从一个设备转移到另一个设备的过程。在大多数操作系统中,可以通过在命令行中使用重定向操作符来实现:

  • 使用 > 可以将 stdout 重定向到一个文件。
  • 使用 2> 可以将 stderr 重定向到一个文件。
  • 使用 < 可以将 stdin 重定向到一个文件。

例如,在Unix/Linux系统中:

./program > output.txt    # 将程序的标准输出重定向到 output.txt
./program 2> error.log   # 将程序的标准错误重定向到 error.log
./program < input.txt    # 将程序的标准输入重定向到 input.txt

文本文件与二进制文件

文件可以以文本模式或二进制模式打开:

  • 文本文件:以文本模式打开的文件,操作系统可能会对换行符等字符进行转换(例如,在Windows上将 \n 转换为 \r\n)。
  • 二进制文件:以二进制模式打开的文件,操作系统不会对数据进行任何转换,直接读写文件的二进制内容。

在C语言中,使用 fopen 函数打开文件时,可以通过模式字符串指定文件的打开模式:

  • "r":以文本模式读取。
  • "w":以文本模式写入,会截断文件。
  • "a":以文本模式追加。
  • "rb":以二进制模式读取。
  • "wb":以二进制模式写入,会截断文件。
  • "ab":以二进制模式追加。

文本模式和二进制模式的主要区别在于如何处理换行符和可能的字符编码转换。在处理文本数据时通常使用文本模式,在处理非文本数据(如图片、视频等)时使用二进制模式。

示例代码

以下是使用标准库函数操作文件的示例:

#include <stdio.h>

int main() {
    FILE *fp;

    // 以文本模式打开文件进行读取
    fp = fopen("example.txt", "r");
    if (fp == NULL) {
        perror("Error opening file");
        return -1;
    }

    // 执行文件操作...

    // 关闭文件
    fclose(fp);

    return 0;
}

在实际编程中,始终要检查 fopen 的返回值,并在使用完文件后调用 fclose 来关闭文件,以释放系统资源。

文件操作

文件操作是编程中的一项基本技能,尤其是在处理数据持久化和输入输出时。以下是对文件操作各个环节的解释:

  1. 打开文件:在C语言中,使用 fopen 函数打开文件,它需要两个参数:文件路径和模式。成功时返回一个文件指针,失败时返回 NULL

    FILE *fp = fopen("example.txt", "r"); // 以只读模式打开文本文件
    if (fp == NULL) {
        perror("Failed to open file");
        exit(EXIT_FAILURE);
    }
    
  2. 模式:打开文件时需要指定模式,常见的有:

    • "r":只读模式,文件必须存在。
    • `“w”``:只写模式(覆盖),如果文件存在会被截断,不存在则创建。
    • "a":追加模式,如果文件存在,则写入位置在文件末尾;不存在则创建。
    • "r+":读写模式,文件必须存在。
    • "w+":读写模式(覆盖),同 "w"
    • "a+":读写模式(追加),同 "a"
    • 二进制模式通常以 "b" 结尾,如 "rb""wb" 等,表示以二进制形式打开文件。
  3. 关闭文件:使用 fclose 函数关闭文件,释放与文件相关的资源。应该总是在文件操作完成后关闭文件。

  4. 为打开的流附加文件:在追加模式下打开文件时,写操作会在文件末尾添加内容,而读操作则从文件开头读取。

    // 假设已经以追加模式打开文件 append_fp
    fprintf(append_fp, "New data to append.\n");
    
  5. 从命令行获取文件名:可以通过 argv 参数从 main 函数接收命令行输入的文件名。例如,在Unix/Linux系统中,可以通过 ./program filename.txt 传递文件名。

    #include <stdio.h>
    
    int main(int argc, char *argv[]) {
        if (argc < 2) {
            fprintf(stderr, "Usage: %s filename\n", argv[0]);
            return 1;
        }
    
        FILE *file = fopen(argv[1], "r");
        if (!file) {
            perror("Error opening file");
            return 1;
        }
        
        // 处理文件...
        fclose(file);
        return 0;
    }
    
  6. 临时文件:使用 tmpfile 函数创建临时文件,该文件通常在程序结束时自动删除。也可以使用 mkstemp 函数创建临时文件,并获取一个文件描述符。

    FILE *temp_fp = tmpfile(); // 创建临时文件
    if (temp_fp == NULL) {
        perror("Failed to create temporary file");
        exit(EXIT_FAILURE);
    }
    // 写入临时文件
    fprintf(temp_fp, "This is a temporary file.\n");
    
  7. 文件缓冲:文件操作通常涉及缓冲,这意味着数据可能首先被写入内存中的缓冲区,然后才刷新到磁盘上。缓冲可以提高性能,但也可以通过 fflush 函数手动刷新缓冲区。

    setbuf(fp, NULL); // 禁用缓冲,直接写入文件系统
    fflush(fp); // 手动刷新缓冲区,确保数据写入
    
  8. 其他文件操作:包括文件定位(fseekrewind)、文件结束检测(feof)、错误检测(ferror)、获取文件大小(fsize)、改变文件缓冲大小(setvbuf)等。

示例代码

#include <stdio.h>
#include <stdlib.h>

int main(int argc, char *argv[]) {
    FILE *fp;
    char *filename;

    // 检查命令行参数
    if (argc != 2) {
        printf("Usage: %s filename\n", argv[0]);
        return EXIT_FAILURE;
    }
    filename = argv[1]; // 从命令行获取文件名

    // 打开文件
    fp = fopen(filename, "r");
    if (fp == NULL) {
        perror("Error opening file");
        return EXIT_FAILURE;
    }

    // 执行文件操作...

    // 关闭文件
    fclose(fp);

    return EXIT_SUCCESS;
}

这段代码演示了如何从命令行接收文件名,并尝试以只读模式打开它。如果文件打开失败,程序将打印错误消息并退出。

格式化的输入和输出

格式化输入输出是C语言中处理数据的重要方式,允许你控制数据如何显示(输出)或如何解析(输入)。以下是格式化输入输出的关键点和示例:

1. printf 函数

printf 用于格式化输出到标准输出(通常是屏幕)。它非常灵活,允许你指定输出格式。

示例:

#include <stdio.h>

int main() {
    int num = 10;
    float price = 19.99;
    printf("Number: %d, Price: %.2f\n", num, price);
    return 0;
}

2. printf 转换说明

转换说明定义了如何将数据转换为字符串。例如,%d 用于整数,%f 用于浮点数,%.2f 表示浮点数保留两位小数。

3. C99 对 printf 转换说明的修改

C99 引入了一些新的转换说明,如 %a%A 用于十六进制浮点数,%g%G 用于通用浮点数表示。

4. printf 转换说明示例

printf("Hex: %x, Octal: %o, Pointer: %p\n", num, num, &num);

5. scanf 函数

scanf 用于从标准输入(通常是键盘)读取格式化输入。

示例:

int main() {
    int age;
    printf("Enter your age: ");
    scanf("%d", &age);
    printf("You are %d years old.\n", age);
    return 0;
}

6. scanf 格式串

格式串定义了期望输入的格式,可以包含空白字符、非空白字符、各种数据类型转换说明。

7. scanf 转换说明

转换说明用于告诉 scanf 预期的输入类型,如 %d 用于整数,%f 用于浮点数。

8. C99 对 scanf 转换说明的改变

C99 为 scanf 增加了 %n,它可以返回读取的字符数。

9. scanf 示例

int a, b;
scanf("%d %d", &a, &b); // 读取两个整数

10. 检测文件末尾和错误条件

使用 feof 函数检测文件末尾,使用 ferror 函数检测错误条件。

示例:

FILE *fp = fopen("example.txt", "r");
if (fp == NULL) {
    perror("Error opening file");
    return -1;
}

int ch;
while ((ch = fgetc(fp)) != EOF) {
    putchar(ch);
}

if (feof(fp)) {
    printf("End of file reached.\n");
}

if (ferror(fp)) {
    perror("Error reading file");
}

fclose(fp);

这个示例展示了如何读取文件直到文件末尾,并检查读取过程中是否有错误发生。如果到达文件末尾,将打印相应的消息。如果发生错误,将打印错误消息。最后,使用 fclose 关闭文件。

字符的输入和输出

字符的输入/输出是文件操作中的基本形式之一,涉及到对单个字符或一小段数据的读写。以下是对字符输入输出函数的解释和示例:

输出函数

  • fputc:将一个字符写入到指定的流中。
  • putchar:将一个字符输出到标准输出(通常是屏幕)。

示例:

#include <stdio.h>

int main() {
    char ch = 'A';
    fputc(ch, stdout); // 输出字符到标准输出
    putchar(ch); // putchar是fputc的宏定义,同样输出字符到标准输出
    return 0;
}

输入函数

  • fgetc:从指定的流中读取一个字符。
  • getchar:从标准输入(通常是键盘)读取一个字符。

示例:

#include <stdio.h>

int main() {
    char ch;
    printf("Enter a character: ");
    ch = fgetc(stdin); // 从标准输入读取一个字符
    // getchar(); // getchar是fgetc的宏定义,同样从标准输入读取字符
    printf("You entered: %c\n", ch);
    return 0;
}

这些函数提供了一种简便的方式来处理字符级别的输入输出。与 printfscanf 相比,它们不进行任何格式化,因此速度更快,适用于需要高效字符读写的场合。

附加说明

  • putchar 实际上是 fputc 的一个宏定义,通常用于向 stdout 输出字符。
  • getcharfgetc 的一个宏定义,通常用于从 stdin 读取字符。
  • 这些函数返回读取或发送的字符,如果是 EOF(文件结束标志),则表示发生错误或文件结束。

使用字符函数读取一行文本示例

#include <stdio.h>

int main() {
    char c, line[1024];
    int i = 0;

    printf("Enter a line of text: ");

    while ((c = getchar()) != '\n' && c != EOF) { // 读取直到换行或文件结束
        line[i++] = c;
    }
    line[i] = '\0'; // 确保字符串以空字符结尾

    printf("You entered: %s\n", line);

    return 0;
}

这个示例展示了如何使用 getchar 函数来读取用户输入的一行文本,直到遇到换行符或文件结束符。读取的文本存储在字符数组 line 中。

行的输入和输出

行的输入和输出通常指的是读取或写入一串字符,直到遇到换行符 \n。这种操作通常使用标准库中的字符串处理函数来完成。

输出函数

puts
  • 功能:向标准输出写入一个字符串,并且在末尾自动添加换行符 \n
  • 原型:int puts(const char *str);

示例:

#include <stdio.h>

int main() {
    const char *message = "Hello, World!";
    puts(message); // 输出字符串并换行
    return 0;
}
fprintf
  • 功能:向指定的流写入格式化的字符串。
  • 原型:int fprintf(FILE *stream, const char *format, ...);

示例:

#include <stdio.h>

int main() {
    int num = 10;
    fprintf(stdout, "Number is %d\n", num); // 向标准输出写入格式化字符串
    return 0;
}

输入函数

fgets
  • 功能:从指定的流中读取一行字符串,直到遇到换行符、EOF 或最大长度限制。
  • 原型:char *fgets(char *str, int num, FILE *stream);

示例:

#include <stdio.h>

int main() {
    char line[1024];
    printf("Enter a line of text: ");
    if (fgets(line, sizeof(line), stdin) == NULL) {
        perror("Error reading line");
        return 1;
    }
    printf("You entered: %s", line);
    return 0;
}
gets
  • 注意gets 函数是不安全的,因为它不检查缓冲区溢出,已经被 fgets 函数取代。不要使用 gets 函数。
scanf
  • 功能:从标准输入读取并格式化输入数据。
  • 原型:int scanf(const char *format, ...);

示例:

#include <stdio.h>

int main() {
    char line[1024];
    printf("Enter a line of text: ");
    if (scanf("%1023s", line) != 1) { // 注意限制读取的字符数,防止溢出
        perror("Error reading line");
        return 1;
    }
    printf("You entered: %s", line);
    return 0;
}

在实际编程中,推荐使用 fgets 来读取一行文本,因为它提供了对读取字符数量的控制,可以防止缓冲区溢出,这是一种常见的安全漏洞。而 scanf 在读取一行文本时需要手动限制字符数,以确保安全。

块的输入和输出

块的输入/输出通常指的是对一定数量的数据进行读写操作,而不是单个字符或字符串。在C语言中,可以使用以下几种方式来实现块的输入/输出:

输出函数

fwrite
  • 功能:向文件写入数据块。
  • 原型:size_t fwrite(const void *ptr, size_t size, size_t nmemb, FILE *stream);

示例:

#include <stdio.h>

int main() {
    int data[] = {10, 20, 30, 40};
    size_t size = sizeof(data[0]);
    size_t nmemb = sizeof(data) / size;

    FILE *fp = fopen("data.bin", "wb");
    if (fp == NULL) {
        perror("Error opening file");
        return 1;
    }

    fwrite(data, size, nmemb, fp); // 写入整个数据数组

    fclose(fp);
    return 0;
}

输入函数

fread
  • 功能:从文件读取数据块。
  • 原型:size_t fread(void *ptr, size_t size, size_t nmemb, FILE *stream);

示例:

#include <stdio.h>

int main() {
    int data[4];
    size_t size = sizeof(data[0]);
    size_t nmemb = sizeof(data) / size;

    FILE *fp = fopen("data.bin", "rb");
    if (fp == NULL) {
        perror("Error opening file");
        return 1;
    }

    fread(data, size, nmemb, fp); // 从文件中读取数据到数组

    for (size_t i = 0; i < nmemb; ++i) {
        printf("%d ", data[i]);
    }
    printf("\n");

    fclose(fp);
    return 0;
}

说明

  • fwritefread 函数的参数 size 是单个元素的大小,nmemb 是元素的数量。
  • 这些函数返回实际读写的元素数量,如果少于请求的数量,可能是遇到了文件结束或发生错误。
  • 使用 fwritefread 时,需要确保数据块的大小和类型在读写时保持一致,以避免数据损坏。
  • 这些函数可以处理任何类型的数据块,包括自定义的数据结构。

块的输入/输出特别适用于二进制文件操作,因为它们不会对数据进行任何特殊处理,直接读写原始字节。这使得它们非常适合用于数据持久化、网络通信等场景。

文件定位

文件定位是指在文件中设置一个特定的读写位置,这个位置决定了下一次读写操作从文件的哪个点开始。C语言提供了几个函数来进行文件定位:

文件定位函数

fseek
  • 功能:移动文件位置指针到指定位置。
  • 原型:int fseek(FILE *stream, long offset, int whence);
    • stream:文件流指针。
    • offset:从whence指定的位置开始偏移的字节数。
    • whence:一个宏,指定偏移量offset的参考点,可以是SEEK_SET(文件开头)、SEEK_CUR(当前位置)、SEEK_END(文件末尾)。

示例:

#include <stdio.h>

int main() {
    FILE *fp = fopen("example.txt", "r");
    if (fp == NULL) {
        perror("Error opening file");
        return 1;
    }

    // 移动到文件开头的第100个字节
    fseek(fp, 100L, SEEK_SET);
    
    // 从当前位置向前移动50个字节
    fseek(fp, -50L, SEEK_CUR);
    
    // 从文件末尾向前移动20个字节
    fseek(fp, -20L, SEEK_END);

    fclose(fp);
    return 0;
}
ftell
  • 功能:获取当前文件位置指针的值。
  • 原型:long ftell(FILE *stream);
  • 返回值:当前文件位置指针的偏移量,以字节为单位,从文件开头开始计算。

示例:

#include <stdio.h>

int main() {
    FILE *fp = fopen("example.txt", "r");
    if (fp == NULL) {
        perror("Error opening file");
        return 1;
    }

    long position = ftell(fp); // 获取当前文件位置
    printf("Current position: %ld\n", position);

    fclose(fp);
    return 0;
}
rewind
  • 功能:重置文件位置指针到文件开头。
  • 原型:void rewind(FILE *stream);

示例:

#include <stdio.h>

int main() {
    FILE *fp = fopen("example.txt", "r");
    if (fp == NULL) {
        perror("Error opening file");
        return 1;
    }

    // 执行一些文件操作...

    rewind(fp); // 将文件位置指针重置到文件开头

    fclose(fp);
    return 0;
}
fgetposfsetpos
  • 功能:fgetpos 获取当前文件位置,fsetpos 设置文件位置。
  • 原型:int fgetpos(FILE *stream, fpos_t *pos);int fsetpos(FILE *stream, const fpos_t *pos);

示例:

#include <stdio.h>

int main() {
    FILE *fp = fopen("example.txt", "r");
    fpos_t pos;
    if (fp == NULL) {
        perror("Error opening file");
        return 1;
    }

    // 获取当前文件位置
    if (fgetpos(fp, &pos) != 0) {
        perror("Error getting file position");
        fclose(fp);
        return 1;
    }

    // 执行一些文件操作...

    // 恢复到之前获取的位置
    if (fsetpos(fp, &pos) != 0) {
        perror("Error setting file position");
    }

    fclose(fp);
    return 0;
}

文件定位功能对于需要在文件中随机访问或重新读取数据的应用程序非常有用。例如,它可以用来实现文件的重新读取、跳过某些数据或在日志文件中插入和追加信息。

字符串的输入和输出

字符串的输入和输出在C语言中是非常常见的操作,可以通过以下几种函数来实现。

输出函数

puts
  • 功能:向标准输出写入一个字符串,并在末尾添加换行符 \n
  • 原型:int puts(const char *str);

示例:

#include <stdio.h>

int main() {
    const char *greeting = "Hello, World!";
    puts(greeting); // 输出字符串并换行
    return 0;
}
printf
  • 功能:使用格式化的方法输出字符串和其他数据类型。
  • 原型:int printf(const char *format, ...);

示例:

#include <stdio.h>

int main() {
    const char *name = "Kimi";
    printf("Hello, %s!\n", name); // 格式化输出字符串
    return 0;
}
fprintf
  • 功能:向指定的流写入格式化字符串。
  • 原型:int fprintf(FILE *stream, const char *format, ...);

示例:

#include <stdio.h>

int main() {
    const char *message = "This is a test message.";
    fprintf(stdout, "%s\n", message); // 向标准输出写入格式化字符串
    return 0;
}

输入函数

fgets
  • 功能:从指定的流中读取一行字符串,直到遇到换行符、EOF 或达到最大长度。
  • 原型:char *fgets(char *str, int num, FILE *stream);

示例:

#include <stdio.h>

int main() {
    char line[1024];
    printf("Enter a line of text: ");
    if (fgets(line, sizeof(line), stdin) != NULL) {
        printf("You entered: %s", line);
    }
    return 0;
}
gets
  • 注意gets 函数是不安全的,容易引发缓冲区溢出,已经被 fgets 函数取代。不要使用 gets 函数。
scanf
  • 功能:从标准输入读取并格式化输入数据。
  • 原型:int scanf(const char *format, ...);

示例:

#include <stdio.h>

int main() {
    char name[50];
    printf("Enter your name: ");
    if (scanf("%49s", name) == 1) { // 限制读取字符数防止溢出
        printf("Hello, %s!\n", name);
    }
    return 0;
}

附加说明

  • 在使用 printffprintf 时,可以使用 %s 格式化说明符来输出字符串。
  • puts 函数只能输出字符串,并且在末尾自动添加换行符,适合输出简短的文本行。
  • fgets 是一种安全的方式来读取一行文本,因为它允许你指定最大读取字符数,从而避免缓冲区溢出。
  • 在使用 scanf 读取字符串时,应该指定最大字符数来避免安全风险。

字符串的输入输出操作是C语言中处理文本数据的基础,掌握这些函数的使用方法对于进行有效的文本处理至关重要。

;