【C语言简明教程提纲】(四):结构体与文件定义和操作

前言


1 结构体

1-1 结构体定义
  • C 语言提供了 结构体(struct),用来把多个不同类型的数据组合在一起。
  • 结构体使用 struct 关键字定义。
  • 基本语法:
c 复制代码
struct 结构体名
{
    成员变量;
    成员变量;
};
  • 例如定义一个学生结构体:
c 复制代码
#include <stdio.h>

struct Student
{
    int id;         //学号
    char name[20];  //姓名
    float score;    //成绩
};

1-2 结构体变量
  • 结构体定义完成后,就可以声明变量。声明变量有两种方法
  1. 先定义结构体,再定义变量
c 复制代码
struct Student
{
    int id;
    char name[20];
    float score;
};

struct Student stu1;
  1. 定义时直接创建变量
c 复制代码
struct Student
{
    int id;
    char name[20];
    float score;
} stu1, stu2;

1-3 结构体成员访问
  • 访问结构体成员使用 . 运算符
c 复制代码
#include <stdio.h>

struct Student
{
    int id;
    char name[20];
    float score;
};

int main()
{
    struct Student stu1;

    stu1.id = 1001;
    stu1.score = 95.5;

    printf("id = %d\n", stu1.id);
    printf("score = %.2f\n", stu1.score);

    return 0;
}

1-4 结构体初始化
  • 通常我们可以这样对结构体进行初始化:
c 复制代码
struct Student
{
    int id;
    char name[20];
    float score;
};

int main()
{
    struct Student stu = {1001, "Tom", 95.5};

    printf("%d %s %.1f\n", stu.id, stu.name, stu.score);
}

1-5 结构体指针
  • 结构体变量同样可以使用指针。
c 复制代码
struct Student *p;
  • 我们可以这样访问到结构体指针所指的结构体的成员:
c 复制代码
(*p).id
  • 同时C 语言提供了更方便的运算符->,使得我们可以这样访问结构体指针所指的的结构体成员:
c 复制代码
p->id
  • 例子:
c 复制代码
#include <stdio.h>

struct Student
{
    int id;
    float score;
};

int main()
{
    struct Student stu = {1001, 90};
    struct Student *p = &stu;

    printf("%d\n", p->id);
    printf("%.1f\n", p->score);

    return 0;
}

1-6 结构体数组
  • 结构体数组用于存储多个结构体变量。结构体数组也有多种定义方式:
  1. 先定义结构体,再定义数组(最常见)
c 复制代码
struct Student  
{  
int id;  
float score;  
};  
  
struct Student stu[3];
  • 其中每一个元素都是一个 Student 结构体,我们可以这样访问它:
c 复制代码
stu[0].id
stu[1].score
  1. 定义结构体时直接定义数组
c 复制代码
struct Student
{
    int id;
    float score;
} stu[3];
  1. 结构体数组也可以在定义时初始化:
c 复制代码
struct Student
{
    int id;
    float score;
};

struct Student stu[3] =
{
    {1001, 90},
    {1002, 85},
    {1003, 92}
};

1-7 结构体嵌套
  • 结构体中的成员也可以是另一个结构体 ,这种情况称为 结构体嵌套
  • 例如:学生信息中还包含地址信息。
c 复制代码
#include <stdio.h>

struct Address
{
    char city[20];
    char street[20];
};

struct Student
{
    int id;
    char name[20];
    struct Address addr;
};
  • 这里的结构关系可以理解为:
c 复制代码
Student
 ├─ id
 ├─ name
 └─ addr
      ├─ city
      └─ street
  • 访问时仍然使用 . 运算符:
c 复制代码
stu.addr.city
stu.addr.street

1-8 链表结构
  • 在很多数据结构中,结构体中经常会包含一个指向自身结构体的指针
  • 这种结构常用于实现 链表(Linked List)
  • 例如定义一个链表节点:
c 复制代码
struct Node
{
    int data;
    struct Node *next;
};
成员 含义
data 存储数据
next 指向下一个节点
  • 链表由多个节点连接而成,每个节点通过 next 指针连接到下一个节点。
c 复制代码
  node1       node2       node3
 ┌─────┐     ┌─────┐     ┌─────┐
 │data │     │data │     │data │
 │next │ ──► │next │ ──► │next │ ──► NULL
 └─────┘     └─────┘     └─────┘
  • 例子:
c 复制代码
#include <stdio.h>

struct Node
{
    int data;
    struct Node *next;
};

int main()
{
    struct Node n1, n2, n3;

    n1.data = 1;
    n2.data = 2;
    n3.data = 3;

    n1.next = &n2;
    n2.next = &n3;
    n3.next = NULL;

    printf("%d\n", n1.data);
    printf("%d\n", n1.next->data);
    printf("%d\n", n2.next->data);

    return 0;
}

1-9 typedef 简化结构体写法
  • 在前面的例子中,我们定义结构体变量时需要写:
c 复制代码
struct Student stu1;
  • 如果在程序中大量使用结构体,这种写法会显得比较繁琐。
  • 因此C 语言提供了 typedef 来给数据类型起别名,从而简化结构体的使用。
  • 基本用法:
c 复制代码
typedef struct
{
    int id;
    char name[20];
    float score;
} Student;
  • 这样以后定义变量时就可以直接写:(这点就类似C++class关键字了)
c 复制代码
Student stu1;
Student stu2;

1-10 结构体大小 sizeof
  • 在 C 语言中,我们可以使用 sizeof 运算符查看结构体所占用的内存大小。
c 复制代码
struct Student  
{  
	int id;  
	char name[20];  
	float score;  
};
  • 例如上面这个结构体计算 sizeof(struct Student)的结果为28
成员 类型 大小
id int 4
name char[20] 20
score float 4
  • 但在某些情况下,结构体大小 并不一定等于成员大小简单相加 ,这是因为编译器通常会进行 内存对齐(Memory Alignment)
c 复制代码
struct Test  
{  
	char a;  //1字节
	int b;   //4字节
};
  • 计算 sizeof(struct Student)的结果为8
  • 原因是编译器为了提高 CPU 访问效率,会按照一定规则对结构体成员进行 内存对齐 ,在成员之间插入一些 填充字节(padding)
  • 内存布局可能类似这样:
c 复制代码
地址:
a | 填充 | 填充 | 填充 | b b b b
1    1      1     1       4
  • 说人话就是1字节char类型的a变量为了要对齐4字节int类型的b变量要填充3字节
  • 在看一个例子:
c 复制代码
struct Example
{
    char a;
    char b;
    int c;
};
  • 答案是8
c 复制代码
地址
a   b   填充  填充   c c c c
1   1    1    1     4

2 文件定义和操作

2-0 EOF
  • 在 C 语言中,EOF 是一个宏,表示 文件结束标志(End Of File)。
  • 定义在 <stdio.h> 头文件中:
c 复制代码
#define EOF (-1)
  • 主要用途:
    1. 检查文件是否到达末尾
    2. 判断读取函数是否失败

2-1 文本文件与二进制文件
  • C语言中,文件主要分为两类:
类型 特点 打开方式
文本文件(Text File) 内容以可读字符形式存储,每行以换行符 \n 结束,可直接用文本编辑器打开 "r" "w" "a"
二进制文件(Binary File) 内容以二进制形式存储,不可直接用文本编辑器打开,需要用程序读取 "rb" "wb" "ab"

2-2 FILE* fp
  • 在 C 语言中,文件通过 FILE 结构体 进行操作
  • 文件指针类型定义为 FILE*,用于指向打开的文件
  • 典型声明:
c 复制代码
FILE *fp;  // 定义一个文件指针
  • 文件操作流程:
    1. 打开文件 fopen / fopen_s
    2. 读写文件 fgetc / fgets / fputc / fputs / fread / fwrite
    3. 定位文件 fseek / ftell
    4. 关闭文件 fclose

2-2 打开与关闭文件
  • 打开文件使用 fopen
c 复制代码
FILE *fopen(const char *filename, const char *mode);
  • 其中读取模式有一下几种:
模式 含义 文件必须存在 打开后文件内容 文件指针位置 写入行为
"r" 只读文本文件 不变 开头 只能读,不能写
"w" 只写文本文件 清空 开头 写入覆盖原文件
"a" 追加文本文件 不变 末尾 写入总追加到末尾
"r+" 可读可写文本文件 不变 开头 可读可写,写入覆盖当前位置
"w+" 可读可写文本文件 清空 开头 可读可写,写入覆盖
"a+" 可读可写文本文件 不变 末尾 写入总追加,读可用 fseek 调整位置
"rb" 二进制只读 不变 开头 只能读
"wb" 二进制只写 清空 开头 写入覆盖
"ab" 二进制追加 不变 末尾 写入总追加
"rb+" 二进制可读可写 不变 开头 可读可写,覆盖当前位置
"wb+" 二进制可读可写 清空 开头 可读可写,写入覆盖
"ab+" 二进制可读可写追加 不变 末尾 写入总追加,读可用 fseek 调整位置
c 复制代码
fclose(fp);
  • 示例:
c 复制代码
#include <stdio.h>  
  
int main()  
{  
    FILE *fp = fopen("test.txt", "w");  // 打开文件  
    if(fp == NULL)  
    {  
        printf("文件打开失败\n");  
        return 1;  
    }  
  
    fprintf(fp, "Hello World!\n");      // 写入文件  
    fclose(fp);                         // 关闭文件  
  
    return 0;  
}

2-3 文本文件读写
  • 写入文本文件:
c 复制代码
fputc(char c, FILE *fp);   // 写入一个字符
fputs(const char *str, FILE *fp);  // 写入一个字符串
fprintf(FILE *fp, const char *format, ...); // 类似 printf
  • 读取文本文件:
c 复制代码
fgetc(FILE *fp);   // 读取一个字符
fgets(char *str, int n, FILE *fp); // 读取一行
  • fgetc(fp) 的返回值为 字符的 ASCII 值EOF
  • fgets(a,10,fpn)返回值为 str 的地址NULL
  • 例子:
c 复制代码
#define _CRT_SECURE_NO_WARNINGS
#include <stdio.h>

int main()
{
	FILE* fp= fopen("test01.txt", "r");
	if (fp == NULL) {
		printf("文件打开失败!\n");
		return 1;
	}
	char buf[50];
	while (fgets(buf, sizeof(buf), fp) != NULL)
	{
		printf("%s", buf);
	}
	fclose(fp);
	return 0;
}

2-4 格式化输入输出函数
  • fscanf :从文件中按指定格式读取数据,类似于 scanf,但对象是文件。
    • fscanf 返回值为成功读取的项目数或 EOF
c 复制代码
fscanf(FILE *fp, const char *format, ...);
  • fprintf :向文件按指定格式写入数据,类似于 printf,但对象是文件。
    • 返回值:成功 → 写入的字符数;失败 → 负数
c 复制代码
fprintf(FILE *fp, const char *format, ...);
  • 示例:同时使用 fscanffprintf 进行文件的格式化读写
c 复制代码
#include <stdio.h>

int main() {
    FILE *fp = fopen("data.txt", "w");
    if (fp == NULL) {
        printf("文件打开失败!\n");
        return 1;
    }

    // 使用 fprintf 写入格式化数据
    fprintf(fp, "%d %.1f\n", 1001, 95.5);
    fprintf(fp, "%d %.1f\n", 1002, 88.0);
    fclose(fp);

    // 使用 fscanf 读取文件
    fp = fopen("data.txt", "r");
    if (fp == NULL) {
        printf("文件打开失败!\n");
        return 1;
    }

    int id;
    float score;
    while (fscanf(fp, "%d %f", &id, &score) == 2) {
        printf("id=%d, score=%.1f\n", id, score);
    }

    fclose(fp);
    return 0;
}

2-5 二进制文件的读写
  • 写入二进制文件:
    • 返回值是 实际成功读取的数据块数量
c 复制代码
size_t fwrite(const void *ptr, size_t size, size_t nmemb, FILE *fp);
  • 读取二进制文件:
    • 返回值是 实际成功读取的数据块数量
c 复制代码
size_t fread(void *ptr, size_t size, size_t nmemb, FILE *fp);
  • 其中:
    • ptr:数据缓冲区指针
    • size:每个数据块的大小
    • nmemb:数据块数量
    • fp:文件指针
    • 注意size * nmemb = 总共要读/写的字节数
  • 示例:写入和读取结构体到二进制文件
c 复制代码
#include <stdio.h>  
  
typedef struct  
{  
    int id;  
    float score;  
} Student;  
  
int main()  
{  
    Student stu[2] = {{1001, 90.5}, {1002, 88.0}};  
    FILE *fp = fopen("stu.dat", "wb");  
    fwrite(stu, sizeof(Student), 2, fp);  
    fclose(fp);  
  
    Student readStu[2];  
    fp = fopen("stu.dat", "rb");  
    fread(readStu, sizeof(Student), 2, fp);  
    fclose(fp);  
  
    for(int i = 0; i < 2; i++)  
    {  
        printf("id=%d score=%.1f\n", readStu[i].id, readStu[i].score);  
    }  
  
    return 0;  
}

2-6 文件定位函数
  • 文件读写时,可以使用 fseekftellrewind 来定位文件位置。
  1. fseek(FILE *fp, long offset, int whence)
    • 作用:设置文件指针的位置。
    • 参数
      • fp:文件指针。
      • offset:偏移量(以字节为单位)。
      • whence:起始位置,可取:
        • SEEK_SET:文件开头
        • SEEK_CUR:当前位置
        • SEEK_END:文件末尾
    • 返回值
      • 成功 → 0
      • 失败 → 非零值
  2. ftell(FILE *fp)
    • 作用:返回当前文件指针的位置(以字节为单位)。
    • 参数 :文件指针 fp
    • 返回值
      • 成功 → 文件当前位置(long 型)
      • 失败 → -1L
  3. rewind(FILE *fp)
    • 作用 :将文件指针移到文件开头,相当于 fseek(fp, 0, SEEK_SET)
    • 参数 :文件指针 fp
    • 返回值 :无(void
c 复制代码
#include <stdio.h>  
  
int main()  
{  
    FILE *fp = fopen("data.txt", "r");  
    if (fp == NULL) {  
        printf("文件打开失败!\n");  
        return 1;  
    }  
  
    // fseek: 移动文件指针到第7个字符  
    if (fseek(fp, 6, SEEK_SET) == 0) {  
        char c = fgetc(fp);  
        printf("第7个字符: %c\n", c);  
    } else {  
        printf("fseek 失败\n");  
    }  
  
    // ftell: 获取当前位置  
    long pos = ftell(fp);  
    if (pos != -1L) {  
        printf("当前位置: %ld\n", pos);  
    } else {  
        printf("ftell 失败\n");  
    }  
  
    // rewind: 回到文件开头  
    rewind(fp);  
    printf("文件指针已回到开头\n");  
  
    fclose(fp);  
    return 0;  
}

2-7 文件状态
  • C 语言提供了一些函数,用来检测文件的状态:
  1. feof(FILE *fp)
    • 作用:检查文件是否到达末尾(EOF)
    • 参数 :文件指针 fp
    • 返回值
      • 文件到达末尾 → 非零值(true)
      • 未到末尾 → 0(false)
  • 示例
c 复制代码
#include <stdio.h>  
  
int main() {  
	FILE *fp = fopen("data.txt", "r");  
	if (fp == NULL) {  
		printf("文件打开失败!\n");  
		return 1;  
	}  
  
	char c;  
	while ((c = fgetc(fp)) != EOF) {  
		putchar(c);  
	}  
  
	if (feof(fp)) {  
		printf("\n已到文件末尾\n");  
	}  
  
	fclose(fp);  
	return 0;  
}

  1. ferror(FILE *fp)
    • 作用:检查文件操作是否发生错误。
    • 参数 :文件指针 fp
    • 返回值
      • 有错误 → 非零值(true)
      • 无错误 → 0(false)
  • 示例
c 复制代码
#include <stdio.h>  
  
int main() {  
	FILE *fp = fopen("nofile.txt", "r");  
	if (fp == NULL) {  
		printf("文件打开失败!\n");  
		return 1;  
	}  
  
	int c = fgetc(fp);  
	if (ferror(fp)) {  
		printf("读取文件时发生错误\n");  
	}  
  
	fclose(fp);  
	return 0;  
}

  1. clearerr(FILE *fp)
    • 作用:清除文件的错误标志和 EOF 标志。
    • 参数 :文件指针 fp
    • 返回值 :无(void
  • 示例
c 复制代码
#include <stdio.h>  
      
int main() {  
	FILE *fp = fopen("data.txt", "r");  
	if (fp == NULL) return 1;  
  
	// 假设已经读取到文件末尾  
	while (fgetc(fp) != EOF);  
  
	if (feof(fp)) {  
		printf("到达文件末尾,准备清除标志\n");  
		clearerr(fp);  // 清除 EOF 标志  
	}  
  
	if (!feof(fp)) {  
		printf("EOF 标志已清除\n");  
	}  
  
	fclose(fp);  
	return 0;  
}

总结:

相关推荐
常利兵2 小时前
Jetpack Compose 1.8 新特性来袭,打造丝滑开发体验
android
牢七2 小时前
百家cms 审计 未完成
android·ide·android studio
hjxu20162 小时前
【 MySQL 速记5】插入
android·数据库·mysql
wsoz2 小时前
GCC编译
linux·c语言·嵌入式·gcc
七七肆十九4 小时前
PTA 习题9-1 时间换算
c语言·算法
難釋懷4 小时前
Redis搭建哨兵集群
数据库·redis·缓存
条tiao条4 小时前
从 “Top-K 问题” 入门二叉堆:C 语言从零实现与经典应用
c语言·算法·深度优先
盐水冰5 小时前
【Redis】学习(3)Redis的Java客户端
java·redis·学习
zb200641205 小时前
Redis的Spring配置
数据库·redis·spring