返回

C语言入门基础

C语言是一种高效的编程语言,广泛用于系统开发。其核心内容包括: 1. 基础语法:变量、控制结构和输入输出。 2. 数组与字符串:存储相同类型的数据和文本。 3. 指针:存储变量地址,常用于数组和函数操作。 4. 文件操作:读写外部文件。 5. 预处理器指令:控制编译行为。

第一部分:C语言基础概念详解

1. C语言的起源与特点

C语言诞生于20世纪70年代初,最初用于开发UNIX操作系统。它的设计目标是使得代码更高效且能够直接操作硬件,因此C语言也被称为“低级语言的高级化”。以下是C语言的几个主要特点:

  • 高效性:C语言编写的程序运行速度非常快,尤其适合底层开发,如操作系统、驱动程序等。
  • 可移植性:C语言代码具有良好的跨平台移植性,只需少量修改就可以在不同平台上编译运行。
  • 灵活性:C语言允许直接访问内存,可以进行精细的内存操作(如指针操作)。
  • 丰富的运算符:C语言提供了丰富的运算符集,能够进行各种类型的数据运算和逻辑运算。

2. 程序的基本结构

每个C语言程序都由以下几个主要部分组成:

  • 预处理指令:使用 #include来引入标准库,如 stdio.h,为输入输出提供函数。
  • 主函数 main():每个C程序的入口点是 main()函数,程序的执行从 main()开始。其返回类型通常是 int,表示程序的执行状态(0通常表示成功)。

一个简单的C语言程序如下:

1
2
3
4
5
6
#include <stdio.h>

int main() {
    printf("Hello, World!\n");
    return 0;
}

3. 变量和数据类型

C语言是一种静态类型语言,这意味着在使用变量之前,必须声明变量的类型。主要的数据类型有:

  • 整数类型 (int):用于存储整型数值,通常占用4字节。
    • 例:int a = 10;
  • 浮点类型 (float, double):用于存储小数,float通常占用4字节,double占用8字节。
    • 例:float pi = 3.14;
  • 字符类型 (char):用于存储单个字符,占用1字节。
    • 例:char letter = 'A';

C语言允许使用修饰符来改变基本类型的特性,如:

  • short:表示短整型,减少内存占用;
  • long:表示长整型,增加数值范围;
  • unsigned:表示无符号类型,仅存储非负数。

例如:

1
2
unsigned int positive_num = 100; // 无符号整数
long long large_num = 1234567890; // 长整型

4. 常量

常量是指在程序运行期间其值不可改变的量。C语言通过 const关键字来声明常量。例如:

1
const int days_in_week = 7; // 一周的天数不会改变

5. 基本输入输出

  • 输出函数 printf():用于向屏幕输出信息,常见的占位符有:
    • %d:输出整数;
    • %f:输出浮点数;
    • %c:输出字符;
    • %s:输出字符串。
1
printf("Integer: %d, Float: %.2f\n", 10, 3.1415);
  • 输入函数 scanf():用于从键盘读取输入,常见的格式符号与 printf()相似,使用 &符号来获取变量的地址。
1
2
int num;
scanf("%d", &num); // 读取整数

6. 运算符

C语言的运算符种类繁多,主要分为以下几类:

  • 算术运算符:用于基本的数学运算,包括:
    • +:加法;
    • -:减法;
    • *:乘法;
    • /:除法(整数除法时,结果为整数);
    • %:取模运算(只适用于整数)。

例如:

1
int result = 10 % 3; // result = 1
  • 关系运算符:用于比较两个值,返回布尔结果(1表示真,0表示假)。
    • ==:等于;
    • !=:不等于;
    • >:大于;
    • <:小于;
    • >=:大于等于;
    • <=:小于等于。

例如:

1
2
3
if (a == b) {
    printf("a equals b");
}
  • 逻辑运算符:用于处理逻辑关系。
    • &&:逻辑与,只有两个表达式都为真时,结果为真;
    • ||:逻辑或,任一表达式为真,结果为真;
    • !:逻辑非,将表达式的结果取反。

例如:

1
2
3
if (a > 0 && b > 0) {
    printf("Both a and b are positive numbers.");
}

7. 条件控制结构

C语言中的条件控制结构用于控制程序执行的流程,主要包括:

  • if语句:用于条件判断,当条件成立时执行特定代码块。
  • else语句:在 if条件不成立时执行。
  • else if语句:用于多条件判断。

例如:

1
2
3
4
5
6
7
8
int num = 5;
if (num > 0) {
    printf("Positive number");
} else if (num == 0) {
    printf("Zero");
} else {
    printf("Negative number");
}

8. 循环控制结构

C语言提供了三种常用的循环结构:

  • for循环:常用于已知循环次数的情况。
1
2
3
for (int i = 0; i < 5; i++) {
    printf("%d\n", i);
}
  • while循环:在条件成立时重复执行代码块,适用于未知循环次数的情况。
1
2
3
4
5
int i = 0;
while (i < 5) {
    printf("%d\n", i);
    i++;
}
  • do…while循环:先执行一次代码块,然后在条件成立时继续循环。
1
2
3
4
5
int i = 0;
do {
    printf("%d\n", i);
    i++;
} while (i < 5);

9. 函数与作用域

  • 函数是C语言中的重要组成部分,用于将代码逻辑封装成可重用的代码块。函数的声明和定义包括返回类型、函数名、参数列表和函数体。例如:
1
2
3
int add(int a, int b) {
    return a + b;
}

在调用函数时,参数会传递到函数内,函数执行完毕后将返回一个值。例如:

1
int result = add(5, 3); // result为8
  • 局部变量与全局变量:C语言中的变量分为局部变量和全局变量。局部变量是在函数或代码块内部声明的变量,只能在其定义的范围内使用;全局变量是在函数外部声明的,整个程序都可以访问。

第二部分:数组与字符串的详细介绍

在上一部分中,我们讲到了C语言的基本概念、控制结构和函数等内容。现在,我们将深入探讨C语言中的数组字符串,它们是编程中用于处理多个数据的一些关键工具。

1. 数组概念

数组是存储相同数据类型元素的连续内存块。它使得我们可以用同一个变量名来处理一组值,而不是为每个值都声明一个独立的变量。数组的元素通过下标(索引)进行访问,索引从 0开始。

一维数组

  • 一维数组可以看作是一个线性的数据集合。其语法为:
1
data_type array_name[array_size];

例如,声明一个存储5个整数的数组:

1
int numbers[5]; // 创建一个包含5个整数的数组

我们可以通过下标来给数组元素赋值和访问元素:

1
2
numbers[0] = 10; // 给第一个元素赋值10
int first_value = numbers[0]; // 获取第一个元素的值

也可以使用循环来遍历数组:

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

int main() {
    int numbers[5] = {1, 2, 3, 4, 5}; // 初始化数组
    for (int i = 0; i < 5; i++) {
        printf("%d ", numbers[i]); // 依次输出每个元素
    }
    return 0;
}

数组初始化

数组可以在声明时直接初始化,也可以在运行时赋值:

  • 静态初始化(在声明时指定初始值):
1
int numbers[5] = {1, 2, 3, 4, 5}; // 初始化5个值
  • 部分初始化时,未指定的元素会自动被初始化为 0
1
int numbers[5] = {1, 2}; // 数组的剩余元素被初始化为0

多维数组

C语言支持多维数组,常用的有二维数组。二维数组可以看作是一个矩阵,即行列形式的数据结构。声明语法如下:

1
data_type array_name[rows][cols];

例如:

1
int matrix[3][3]; // 3行3列的二维数组

二维数组的元素可以通过两个下标进行访问:

1
matrix[0][1] = 5; // 将第一行第二列的元素赋值为5

二维数组的遍历通常使用嵌套循环:

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

int main() {
    int matrix[2][3] = {{1, 2, 3}, {4, 5, 6}};
    for (int i = 0; i < 2; i++) {
        for (int j = 0; j < 3; j++) {
            printf("%d ", matrix[i][j]);
        }
        printf("\n");
    }
    return 0;
}

在上述例子中,matrix[2][3]是一个2行3列的数组,遍历时通过嵌套的 for循环访问每个元素。

2. 字符串

字符串在C语言中被视为字符数组。C语言中没有专门的字符串数据类型,因此字符串通常是一个以空字符 \0 结尾的字符数组。该空字符标志着字符串的结束,且不会被显示输出。

字符串的声明与初始化

字符串可以通过字符数组来声明,并且可以直接初始化为字符串常量:

1
char str[6] = "Hello"; // 自动在末尾添加'\0'

注意,这里的 str数组长度为6,因为 "Hello"有5个字符,加上末尾的空字符 \0总共6个字节。我们也可以手动初始化字符串:

1
char str[6] = {'H', 'e', 'l', 'l', 'o', '\0'}; // 手动添加结束符

字符串输入输出

可以使用 printf()输出字符串,也可以使用 scanf()读取字符串。特别地,scanf()读取字符串时,不需要写 &符号,因为字符串本质上是指针(稍后我们会详细讨论指针)。

  • 输出字符串:
1
printf("%s", str); // 输出字符串
  • 输入字符串:
1
scanf("%s", str); // 从用户输入获取字符串

不过,使用 scanf()时有一个重要问题:它只能读取空格之前的内容,遇到空格会停止输入。如果需要读取包含空格的字符串,可以使用 gets()函数(已废弃,不推荐)或 fgets()函数:

1
fgets(str, sizeof(str), stdin); // 读取带有空格的输入

常用字符串处理函数

C语言的 string.h库提供了一系列处理字符串的函数,如:

  • strlen():计算字符串长度。
  • strcpy():复制字符串。
  • strcat():拼接字符串。
  • strcmp():比较两个字符串。

示例:

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

int main() {
    char str1[20] = "Hello";
    char str2[20] = "World";
    strcat(str1, str2); // 将str2拼接到str1后
    printf("%s\n", str1); // 输出"HelloWorld"
    return 0;
}

3. 数组与字符串的常见操作

数组与指针

在C语言中,数组名实际上是一个指向数组第一个元素的指针。我们可以通过指针运算来访问数组元素:

1
2
3
int arr[5] = {1, 2, 3, 4, 5};
int *p = arr; // p指向数组的第一个元素
printf("%d\n", *(p + 1)); // 输出第二个元素的值,结果为2

字符串的常见问题

  • 数组越界:如果字符串数组的大小不足以容纳字符串和结束符 \0,可能会导致内存越界的问题。因此在声明字符串数组时要确保有足够的空间。
  • 空字符问题:字符串末尾必须有一个 \0,否则可能导致程序在输出或操作字符串时出现错误或崩溃。

4. 字符串与数组的区别

尽管字符串是以字符数组的形式存在,但处理方式上仍有细微的区别:

  • 字符数组可以包含任意字符,不必以 \0结尾;
  • 字符串必须以 \0结尾,printf等函数遇到 \0时才会停止输出。

第三部分:指针详解

在前面的部分,我们讨论了数组和字符串,并简要提到了指针的概念。现在我们将深入讨论指针,这是C语言中最强大、但也最难掌握的概念之一。指针在C语言中的作用非常重要,尤其是在处理内存、数组、函数参数和动态分配内存时。

1. 指针的基本概念

指针是存储另一个变量的地址的变量。也就是说,指针指向存储在内存中的一个特定位置,而这个位置存储了某个值。通过指针,可以间接地访问和修改存储在该位置的值。

指针声明

在C语言中,指针通过类型名和一个星号(*)声明。它的基本语法为:

1
data_type *pointer_name;
  • data_type:指针所指向的变量的类型,例如 int, float等。
  • pointer_name:指针变量的名称。

例如:

1
int *p;  // 声明一个指向整数的指针p

指针的地址和解引用

  • &运算符:取地址运算符,返回变量的内存地址。
  • *运算符:解引用运算符,返回指针指向的变量的值。

例如:

1
2
3
4
int a = 10;
int *p = &a;  // p存储变量a的地址
printf("Address of a: %p\n", p); // 输出变量a的地址
printf("Value of a: %d\n", *p);  // 通过指针访问变量a的值,结果为10

在这个例子中,p存储了 a的地址,而 *p则访问 a的值。

2. 指针与数组的关系

在C语言中,数组名实际上就是一个指向数组第一个元素的指针。因此,指针和数组之间有非常密切的关系。

例如:

1
2
3
4
5
int arr[3] = {10, 20, 30};
int *p = arr; // p指向数组的第一个元素

printf("%d\n", *p);    // 输出10,相当于arr[0]
printf("%d\n", *(p+1)); // 输出20,相当于arr[1]

这里,p指向 arr[0]p + 1指向 arr[1],通过解引用 *(p+1)可以访问数组的第二个元素。

总结指针与数组的关系

  • arr实际上是一个指向 arr[0]的指针;
  • arr[i]等价于 *(arr + i)
  • 指针可以通过指针运算遍历数组。

3. 指针与函数

在C语言中,指针与函数有着紧密的关系。通过指针,我们可以实现以下两种主要功能:

  • 函数参数传递:通过传递指针,可以使得函数内部对传递参数的修改在函数外部生效。
  • 指向函数的指针:通过指针来引用和调用函数。

通过指针传递参数

C语言中函数的参数传递是值传递,也就是说,函数接收到的是参数的副本。如果希望函数修改原始参数,可以通过指针来传递参数的地址。

例如:

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

void swap(int *a, int *b) {
    int temp = *a;
    *a = *b;
    *b = temp;
}

int main() {
    int x = 5, y = 10;
    swap(&x, &y);  // 传递x和y的地址
    printf("x = %d, y = %d\n", x, y);  // 输出:x = 10, y = 5
    return 0;
}

在这个例子中,swap函数通过指针修改了 xy的值,使得函数外的 xy被交换。

函数指针

指针不仅可以指向变量,还可以指向函数。一个函数指针可以存储某个函数的地址,并通过这个指针调用该函数。

函数指针的声明语法如下:

1
return_type (*pointer_name)(parameter_types);

例如,声明一个指向返回 int类型、接受两个 int参数的函数指针:

1
int (*func_ptr)(int, int);

我们可以将一个函数的地址赋给函数指针,并通过指针调用该函数:

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

int add(int a, int b) {
    return a + b;
}

int main() {
    int (*func_ptr)(int, int) = add;  // 将函数add的地址赋给指针
    int result = func_ptr(3, 4);  // 通过指针调用函数
    printf("Result: %d\n", result);  // 输出:Result: 7
    return 0;
}

4. 指针的分类

指针在C语言中有多个分类,每种指针类型在不同的场景下使用。以下是几种常见的指针类型:

空指针(NULL指针)

空指针是指向**“无效"地址**的指针。通常将一个指针赋值为 NULL来表明它不指向任何有效的内存地址。在实际编程中,空指针常用于指针初始化或检查指针是否有效。

1
int *p = NULL; // p不指向任何有效地址

使用空指针可以避免一些未初始化指针带来的错误。

悬空指针

悬空指针是指一个指针指向了被释放或不存在的内存地址。它是造成内存访问错误的常见原因。为了避免悬空指针,在释放动态分配的内存后,应该将指针设为 NULL

1
2
3
int *p = (int *)malloc(sizeof(int)); // 动态分配内存
free(p);  // 释放内存
p = NULL;  // 避免悬空指针

野指针

野指针指的是指向未知地址或非法内存地址的指针。野指针的行为是不可预料的,可能导致程序崩溃或产生未定义的行为。在使用指针时,要确保指针指向合法的内存区域。

5. 指针与动态内存管理

C语言提供了一套标准库函数来进行动态内存分配,它们包括:

  • malloc():分配指定大小的内存块,返回指向该内存块的指针。如果分配失败,返回 NULL
  • calloc():与 malloc()类似,但会初始化分配的内存为0。
  • free():释放之前用 malloc()calloc()分配的内存块。

例如:

 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>
#include <stdlib.h>

int main() {
    int *p = (int *)malloc(5 * sizeof(int)); // 分配5个int大小的内存
    if (p == NULL) {
        printf("Memory allocation failed\n");
        return 1;
    }

    // 初始化数组
    for (int i = 0; i < 5; i++) {
        p[i] = i * 10;
    }

    // 输出数组内容
    for (int i = 0; i < 5; i++) {
        printf("%d ", p[i]);
    }

    free(p);  // 释放内存
    return 0;
}

在这个例子中,malloc函数分配了足够存储5个整数的内存空间,并通过指针 p访问和操作这些内存。

6. 指针的常见错误

  • 未初始化的指针:指针在使用前必须初始化。如果未初始化就使用指针,可能会导致程序崩溃或产生不确定的行为。
  • 内存泄漏:使用 malloccalloc动态分配内存后,必须使用 free释放内存,否则会导致内存泄漏。
  • 解引用空指针:解引用空指针会导致程序崩溃。在解引用指针之前,必须确保指针指向有效的内存区域。

第四部分:结构体、联合体与指针的结合使用

在上一部分中,我们深入讨论了C语言中的指针。现在我们继续探讨结构体联合体,并进一步学习它们如何与指针结合使用。在C语言中,结构体和联合体是非常重要的数据类型,常用于定义复杂的数据结构,例如链表、树等。

1. 结构体(Struct)

结构体是一种用户自定义的数据类型,它允许将不同类型的变量组合成一个单一的实体。结构体特别适合用于描述复杂的对象或记录,例如描述一名学生的学号、姓名和成绩等信息。

结构体的声明

结构体的声明格式如下:

1
2
3
4
5
struct structure_name {
    data_type1 member1;
    data_type2 member2;
    ...
};

例如,定义一个表示学生信息的结构体:

1
2
3
4
5
struct Student {
    int id;        // 学号
    char name[50]; // 姓名
    float grade;   // 成绩
};

结构体变量的定义与访问

定义结构体变量时,可以使用结构体名称。例如:

1
struct Student s1; // 定义结构体变量s1

访问结构体的成员时使用点运算符(.):

1
2
3
s1.id = 1001;
strcpy(s1.name, "Alice"); // 将字符串" Alice"复制到name成员
s1.grade = 95.5;

通过这种方式,我们可以操作和访问结构体中的各个成员。

结构体初始化

结构体变量可以在声明时初始化:

1
struct Student s1 = {1001, "Alice", 95.5};

结构体数组

与基本类型数组类似,我们可以定义结构体数组来存储多个结构体变量。例如,定义一个存储多名学生信息的结构体数组:

1
2
3
4
5
struct Student students[3] = {
    {1001, "Alice", 95.5},
    {1002, "Bob", 89.0},
    {1003, "Charlie", 92.3}
};

2. 结构体与指针

指针可以与结构体结合使用,以更高效地操作结构体,尤其是在函数参数传递和动态内存分配时。

结构体指针的声明

结构体指针的声明与普通指针相同,只是类型为结构体类型。例如,声明一个指向 Student结构体的指针:

1
struct Student *p;

我们可以让指针 p指向某个结构体变量的地址:

1
p = &s1; // p指向结构体变量s1的地址

通过指针访问结构体成员

通过指针访问结构体成员时,不能直接使用点运算符,而是使用箭头运算符(->)。例如:

1
2
3
printf("ID: %d\n", p->id);       // 访问结构体成员id
printf("Name: %s\n", p->name);   // 访问结构体成员name
printf("Grade: %.2f\n", p->grade); // 访问结构体成员grade

这种方式避免了结构体的大量复制,特别是在传递结构体作为函数参数时,指针更加高效。

结构体指针与动态内存分配

结构体指针可以与 malloc()函数结合,动态分配结构体内存。例如:

1
2
3
4
5
6
struct Student *p = (struct Student *)malloc(sizeof(struct Student)); // 动态分配结构体内存
if (p != NULL) {
    p->id = 1004;
    strcpy(p->name, "David");
    p->grade = 88.5;
}

分配完成后,可以像普通结构体指针一样操作和访问结构体成员,使用完成后需要记得释放内存:

1
free(p); // 释放内存

3. 联合体(Union)

联合体与结构体类似,也是一种自定义的数据类型,但它允许多个成员共用同一块内存空间。联合体中的所有成员共享同一个内存地址,因此在某一时刻,联合体只能存储一个成员的值。

联合体的声明

联合体的声明格式与结构体类似:

1
2
3
4
5
union UnionName {
    data_type1 member1;
    data_type2 member2;
    ...
};

例如:

1
2
3
4
5
union Data {
    int i;
    float f;
    char str[20];
};

联合体的使用

定义联合体变量时,可以像结构体一样操作。因为所有成员共享同一块内存,修改一个成员的值会覆盖其他成员的值:

1
2
3
4
5
6
7
union Data d;
d.i = 10;
printf("d.i = %d\n", d.i); // 输出:d.i = 10

d.f = 3.14;
printf("d.f = %.2f\n", d.f); // 输出:d.f = 3.14
printf("d.i = %d\n", d.i);   // 输出的d.i值可能是未定义的,因为d.f覆盖了内存

联合体的大小

由于联合体的所有成员共享同一个内存,因此联合体的大小等于它所有成员中最大类型的大小。例如:

1
2
union Data d;
printf("Size of union: %ld\n", sizeof(d)); // 输出联合体的大小,取决于成员中最大的类型

4. 结构体与联合体的比较

特性 结构体 联合体
内存分配 每个成员都有独立的内存空间 所有成员共享同一块内存
使用场景 需要同时存储多个不同类型的数据 需要节省内存且一次只使用一个成员
成员访问 可以同时访问多个成员 只能访问最后一次赋值的成员
大小 总大小等于所有成员的大小之和 大小等于最大成员的大小

5. 嵌套结构体

在C语言中,结构体可以嵌套,即一个结构体的成员可以是另一个结构体。例如:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
struct Date {
    int day;
    int month;
    int year;
};

struct Student {
    int id;
    char name[50];
    struct Date birthdate; // 嵌套Date结构体
};

通过这种方式,可以创建更复杂的数据结构。例如:

1
2
3
4
5
6
struct Student s1;
s1.id = 1001;
strcpy(s1.name, "Alice");
s1.birthdate.day = 15;
s1.birthdate.month = 6;
s1.birthdate.year = 2000;

6. 指向结构体数组的指针

指针不仅可以指向单个结构体变量,还可以指向结构体数组。例如:

1
2
struct Student students[3];
struct Student *p = students; // p指向结构体数组的第一个元素

可以通过指针和下标一起操作结构体数组:

1
2
3
for (int i = 0; i < 3; i++) {
    p[i].id = i + 1001; // 通过指针访问数组元素
}

7. 位域(Bit Fields)

位域是一种特殊的结构体成员,用于控制某个成员占用的比特位数。位域用于节省内存空间,通常在需要精确控制内存分配的情况下使用,例如嵌入式系统中。

位域的声明

位域声明时,需要指定成员占用的位数:

1
2
3
4
struct {
    unsigned int a : 4; // a占用4位
    unsigned int b : 4; // b占用4位
} bitField;

在这个例子中,ab各占用4位,总共占用1个字节。

第五部分:文件操作与预处理器指令

在前面四部分的基础上,本部分将讨论文件操作预处理器指令,它们是C语言编程中非常实用且重要的功能。文件操作使得程序可以读写外部文件,而预处理器指令则帮助程序员更灵活地管理代码和进行条件编译。

1. 文件操作

文件操作是指通过程序与外部文件进行交互,比如读取文件内容或向文件中写入数据。C语言中,文件操作主要通过标准库函数来实现,这些函数定义在 stdio.h头文件中。

文件指针

在C语言中,文件通过文件指针进行操作。FILE类型是一个结构体,表示文件流,而 FILE *则是文件指针,用来引用和操作文件。

1
FILE *file_pointer;

打开和关闭文件

使用 fopen()函数来打开文件,并返回一个 FILE *指针。如果文件打开失败,fopen()会返回 NULL。其基本语法如下:

1
FILE *fopen(const char *filename, const char *mode);
  • filename:文件名。
  • mode:文件打开模式,有以下几种常见模式:
    • "r":以只读模式打开文件,文件必须存在。
    • "w":以写入模式打开文件,若文件不存在则创建,若文件存在则清空文件内容。
    • "a":以追加模式打开文件,文件必须存在。
    • "r+":以读写模式打开文件,文件必须存在。
    • "w+":以读写模式打开文件,若文件不存在则创建,若文件存在则清空文件内容。

例如:

1
2
3
4
FILE *fp = fopen("data.txt", "r"); // 打开文件data.txt用于读取
if (fp == NULL) {
    printf("Error: Could not open file.\n");
}

当操作完文件后,必须使用 fclose()函数关闭文件,以释放系统资源:

1
fclose(fp);

读写文件

C语言提供了多种函数用于文件的读写操作,包括单字符、字符串和格式化数据的读写。

字符读写

  • fgetc():从文件中读取一个字符。
  • fputc():将一个字符写入文件。
1
2
char ch = fgetc(fp); // 从文件中读取一个字符
fputc('A', fp);      // 向文件写入字符'A'

字符串读写

  • fgets():从文件中读取一行字符串,最多读取 n-1个字符(n是指定的大小),并在末尾添加 '\0'
  • fputs():向文件写入一个字符串。
1
2
3
char buffer[100];
fgets(buffer, 100, fp);  // 从文件中读取一行,存储在buffer中
fputs("Hello, World!", fp); // 向文件写入字符串

格式化读写

  • fprintf():将格式化的数据写入文件。
  • fscanf():从文件中读取格式化的数据。
1
2
fprintf(fp, "ID: %d, Name: %s\n", 1001, "Alice"); // 向文件写入格式化数据
fscanf(fp, "%d %s", &id, name); // 从文件中读取格式化数据

二进制文件操作

对于需要精确控制数据格式的场景,C语言提供了二进制文件的读写函数:

  • fread():从文件中读取二进制数据。
  • fwrite():向文件中写入二进制数据。
1
2
3
4
int arr[5] = {1, 2, 3, 4, 5};
fwrite(arr, sizeof(int), 5, fp);  // 写入5个整数到文件中

fread(arr, sizeof(int), 5, fp);   // 从文件中读取5个整数

文件定位

文件定位函数用于在文件中移动读写指针:

  • fseek():将文件指针移动到指定位置。
  • ftell():返回文件指针的当前偏移量。
  • rewind():将文件指针重置到文件开头。
1
2
3
fseek(fp, 0, SEEK_SET);  // 移动到文件开头
long position = ftell(fp);  // 获取当前指针位置
rewind(fp);  // 重置到文件开头

2. 预处理器指令

预处理器指令是由**#**号开头的指令,它们在编译器编译代码之前执行预处理工作。常见的预处理器指令包括文件包含、宏定义、条件编译等。

#include:文件包含

#include指令用于将外部文件的内容包含到当前文件中。常见的有两种包含形式:

  • 尖括号形式:用于包含标准库文件。
1
#include <stdio.h>
  • 双引号形式:用于包含用户自定义的头文件。
1
#include "myheader.h"

#define:宏定义

宏定义用于定义常量或简单的函数。在预处理阶段,所有宏的使用都会被替换为定义的内容。

  • 定义常量:
1
#define PI 3.14159

使用 PI时,编译器会将其替换为 3.14159

  • 宏函数:
1
#define SQUARE(x) ((x) * (x))

使用 SQUARE(5)时,编译器会将其替换为 ((5) * (5))

条件编译

条件编译指令用于控制代码的编译行为,允许编译器根据条件选择性地编译某些代码。

  • #if#elif#else#endif:根据条件选择性地编译代码。
1
2
3
4
5
6
7
#define DEBUG 1

#if DEBUG
    printf("Debug mode is on.\n");
#else
    printf("Release mode is on.\n");
#endif
  • #ifdef#ifndef:判断某个宏是否被定义。
1
2
3
4
5
6
7
#ifdef PI
    printf("PI is defined.\n");
#endif

#ifndef PI
    printf("PI is not defined.\n");
#endif

#undef:取消宏定义

使用 #undef可以取消某个宏的定义:

1
#undef PI

#pragma:特殊指令

#pragma指令是编译器特定的指令,用于设置编译器的特性。例如:

1
#pragma pack(1)  // 设置结构体成员的对齐方式为1字节

3. 宏与内联函数的区别

  • 宏在预处理阶段展开,不执行类型检查,可能会导致意外的错误。
  • 内联函数在编译时替换成函数代码,执行类型检查,通常比宏更安全。

例如:

1
2
#define MAX(a, b) ((a) > (b) ? (a) : (b)) // 宏
inline int max(int a, int b) { return (a > b) ? a : b; } // 内联函数
Licensed under CC BY-NC-SA 4.0
使用 Hugo 构建
主题 StackJimmy 设计