笔记 | C 语言复习框架 (应试篇)
序言
从标题中透漏的信息可知,本系列文章是围绕 “C语言” 展开学习的笔记总结,且目的很明确,笔记内容偏应试,适用于计算机等级考试、考研专业课(C语言)等的复习使用。文章推崇总结性、比对性的学习方法,对于模糊的知识模块需自行查阅参考书目,深化理解或可达到理想的效果。
针对C语言程序,推荐几本辅导复习的书目:
基础篇
- 《谭浩强:C语言程序设计》: 必不可少的经典教程,权威性的标准答案源。( 因讨论条件而异,如编译系统不同,部分题目的答案就具有了争议性 )
- 《明解C语言》: 入门基础教学。值得称赞的是,每个知识模块都符有实例,且实例的源码结构清晰,代码规范及注释到位,非常适合入门使用。
进阶篇
- 《征服C指针》: C语言的学习过程中,指针的运用是最大的难关。无论是在实际应用、应试中都是不可忽视的。对于作者前桥和弥,其一针见血的文风,在掌握一定基础之后,是深入了解C语言的一位不可多得 “良师益友” ( 书中有不少作者交谈式的独白,别有一番阅读风味 )。
总览
1 壹 程序设计和C语言
1.1 计算机程序与语言
程序:计算机能识别和执行的指令。
语言:人和计算机交流、人和计算机能识别的语言。
计算机语言发展阶段:
机器语言 | 符号语言 | 高级语言 ( 面向过程、面向对象 ) |
---|---|---|
0和1指针 | 英文、数字表示指令 | 人类自然语言和数字语言 |
1.2 C语言
-
特点
- 语言简洁、紧凑,使用方便、灵活
- 运算符丰富 ( 单目、双目、三目运算符 )
- 数据类型丰富 ( 整型、浮点型、字符型、数组类型、指针类型、结构体类型、共用体类型、枚举型 )
- 结构体控制语句
- 直接访问物理地址 ( 对硬件直接操作 )
- 可移植性好
-
结构
- 以程序由一个或着多个
源文件
组成。
源文件中包括:
预处理命令 ( #include、#define、#typedef等 )
全局声明 ( 全局变量、局部变量 )
函数定义 ( 参考函数原型 )- 函数是C程序的主要组成部分。
- 一函数包括
函数首部
和函数体
。
函数体包括:
声明部分和执行部分。- 程序总是从main函数开始执行的。
main函数有且仅有一个。
- C程序对计算机的操作有C语言完成。
- 数据声明和语句必须有分号 ( 作为结束 )。
- C本身不提供输入输出语句。
- 以程序由一个或着多个
1.3 程序设计的任务
-
问题分析
-
设计算法
-
编写程序
-
对源文件编辑、编译 ( *.obj ) 和连接 ( *.exe )
-
运行程序并分析结果
-
编写程序文档
[注] 对于编译,预编译和连接的概念及比对:
- 编译:检索语言错误;把源程序转为二进制形式的目标程序。
- 预编译:通过预处理得到的信息与程序其他部分一起,组成完整的、可以正式编译的源程序。
- 连接:与函数库相连接。
2 贰 程序之魂:算法
2.1 引入
-
对数据的描述:所用数据的类型和数据的
组织形式
。组织形式:数据结构 -- 特定关系的数据元素的集合
-
对操作的描述:计算机进行操作的步骤 -- 算法
-
从简理解:
数据结构 + 算法 = 程序
2.2 算法
-
概念:对特定问题求解的方法和描述。
-
特征
- 有穷性:
有穷时间
执行结束; - 确定性:算法
唯一执行路径
,既相同输入执行相同路径; - 可行性:
有限次
; - 零或一个以上的
输入
; - 一个或以上的
输出
;
- 有穷性:
-
要求
- 正确性;
- 可读性;
- 健壮性;
- 效率与低存储量需求 ( 时间复杂度和空间复杂度 )
-
时间复杂度
- 时间复杂度:也称渐进时间复杂度,即算法执行时间的增长率和 f(n) 的增长率相同。
- 渐进时间复杂度:
T(n) = Big O(f(n))
- f(n)为问题规模n的某个函数。
- 算法中的基本运算( 最深层循环内的语句 )的频度与T(n)同数量级。
-
空间复杂度
- 空间复杂度:算法所需存储空间的量度。
- 渐进空间复杂度:
S(n) = Big O(f(n))
- 原地工作:额外空间相对输入的数据量来说是常数。
2.3 三种基本结构和改进流程图
- 三种基本结构
- 顺序结构;
- 选择结构;
- 循环结构:当型循环结构 / 直到型循环结构;
- 改进的流程图:N-S流程图
2.4 结构化程序设计方法
- 自顶向下;
- 逐步细化;
- 模块化设计:
分而治之
;注意模块独立性
- 结构化编码;
3 叁 简单的C语言程序设计
3.1 数据的表现形式及运算
3.1.1 常量
- 概念:程序运行期间,其值不能改变。
- 类型
- 整型常量
- 字符常量 ( 与常变量作比对 [注释1] )
- 普通字符
- 转移字符:\n, \t, \012 (8进制), \x41 (16进制)
- 符号常量:
#define PI 3.14159
- 实型常量
- 10进制小数形式:3.14L
- 指数形式(科学计数法):
8.7e-25(正确);
8.7e-2.5(错误);
87e+25(正确);
3.1.2 变量
先定义,后使用
-
包含属性
- 数据类型 ( 整型、浮点型、字符型 )
- 存储类别 ( 自动变量,静态变量 )
-
概念:程序运行期间,其值可以改变。
-
类型
- 常变量:变量存在期间其值不能改变。
const int a = 10
- 自动变量与静态变量
- 全局变量与局部变量
从存储位置、生存周期、作用区域讨论差异性。[注释2]
- 常变量:变量存在期间其值不能改变。
-
标识符
一个对象的名称。除关键字外,字符、数字和下划线组成。且要求只能是字母或下划线开头。
[注释1] 符号常量与常变量的比较。
符号常量 | 常变量 |
---|---|
不占内存单元,预编译后符号不复存在 | 占存储单元 |
不能重新赋值 | 不能改变其值 |
[注释2] 局部变量与全局变量,自动变量与静态变量,内部函数与外部函数的比较。
局部变量 | 全局变量 | |
---|---|---|
存放于动态存储区 | 存放于静态存储区 | 位置 |
在定义函数内起作用 | 自定义位置开始,本文件起作用 | 作用域 |
函数调用完释放内存 | 程序结束时释放内存 | 生存期 |
- 静态的局部变量,存放于静态存储区,程序结束时释放内存。
- 静态的全局变量,不是因声明static,而误解全局变量才存放于静态存储区。
- 局部变量,声明存储类型指变量存储区以及产生的生存期问题。
- 全局变量,声明存储类型指变量作用域的扩展问题。
自动变量 | 静态变量 |
---|---|
1. 声明该变量的语句块被执行结束释放内存(栈) | 1. 程序结束时才释放内存 |
2. 每次函数调用时赋值 | 2. 保留上一步的赋值 |
3. 在编时赋予初值0或'\0' |
[注] 对比malloc()函数分配的内存,需调用free()函数释放内存。(堆)
内部函数 | 外部函数 (default) |
---|---|
本文件内使用(不限位置) | 可供其他文件使用(不限位置) |
定义:static 函数类型 函数名 | 定义:(extern) 函数类型 函数名 |
3.1.3 数据类型
- 基本类型
关键字 | 字节 | 取值范围 | |
---|---|---|---|
整型 | int | 2/4 | $-2^{15}$ ~ $-2^{15}-1$ / $-2^{31}$ ~ $2^{31}-1$ |
unsigned int | 2/4 | 0 ~ $-2^{16}-1$ / 0 ~ $-2^{32}-1$ | |
字符型 | char | 1 | $-2^7$ ~ $2^7-1$ |
unsigned char | 1 | 0 ~ $2^8-1$ | |
单浮点 | float (有效小数:6) | 4 | -- |
双浮点 | double (有效小数:15) | 8 | -- |
-
关于基本类型的特别说明:
- 字符是按其ASCII形式存储的。
- 单浮点定义:float a = 3.14f
- 双浮点定义:double a = 3.14
- 长浮点定义:long double a = 3.14L
-
派生类型
- 指针类型:指向函数的指针、多重指针
- 数组类型:指针数组
-
构造类型
- 结构体类型
- 共同体类型
- 枚举类型
-
类型转换:
- 低精度向高精度转换;
- 强制转换括号加类型;
int a = (int)3.14
- 多类型变量混合运算,取最高精度的类型;
4 肆 选择结构程序设计
4.1 关系运算符及其优先次序
-
各类运算符的优先级:
-
单目运算符 > 双目运算符 (算术、关系、逻辑) > 三目运算符
-
优先级由高到低排序:
初等运算符:(),[],->,.
单目运算符:!,++,--,~
算术运算符:*,/,%
,+,-
关系运算符:>,<,>=,<=
,!=,==
逻辑运算符:&&,||
条件运算符:a > b : a : b
赋值运算符:a += 1
逗号运算符:(a,b)
-
结合方式
自左向右:初等、单目、关系、逻辑、逗号运算符
自右向左:条件、赋值运算符
同一级的运算符,由结合方式决定优先级。
-
4.2 表达式
-
算术表达式:先乘除模,后加减,再由“自左向右”原则运算。
-
混合运算
- 优先级:遵循各运算符的优先次序。
- 结合性:算术运算符 (自左向右);赋值运算符 (自右向左)。
- 不同类型的混合运算:结果的类型为最高精度的数据类型。
4.3 运算符与表达式
-
关系运算符和关系表达式 ( a+b>c ) -> True or False?
0表示假,!0表示真。
-
逻辑运算符和逻辑表达式
- 逻辑运算:5 && 4 => 1;5 && 0 => 0;
- 按位逻辑:5 & 4 => 4;
[注] 关于逻辑运算与按位逻辑的比较
- 优先级:按位逻辑运算 > 逻辑运算
- max = a & b;min = a | b
-
条件运算符和条件表达式:a > b ? a : b
4.4 选择结构的嵌套
-
if语句只有两个分支可供选择,else总是与它上面最近的未配对的if()配对。
if(express1){ if(express2){ ... } else { ... } } else { if(express3){ ... } else { ... } }
-
switch语句实现多分支选择结构
switch(express1){ // 整型、字符型 case 常量/常量表达式:语句1;break; // break为拦截作用 case 常量/常量表达式:语句2;break; default: 语句3; }
5 伍 循环结构程序设计
5.1 while 语句实现
express1;
while(express2){
express3;
...
}
5.2 for 语句实现循环
for(express1; express2; express3){
...
}
5.3 do...while() 语句实现循环
express1;
do{
express3;
} while(express2);
5.4 break、continue与goto语句
- break:从循环体内跳出循环体。多层嵌套循环,跳出相邻一层循环。
- continue:提前结束本次循环。
- goto:跳出多层循环。
6 陆 数组
6.1 概念
- 一组有序数据的集合。
- 数组中每一元素同属一个数据类型。
- sname[0] <=> *(p+0) <=> 第一个数组元素。
6.2 定义
6.2.1 一维数组
-
定义
类型符 数组名[常量表达式] --> 正确
类型符 数组名[变量] --> 错误,不能为变量 -
初始化
int array[] = {1, 2, 3, 4, 5}; in array[5] = {0}; // 5个元素都为0。
-
引用
int *p = &array[0]; // 等同于 int *p = array; p++; // 指针运算 *(p+i); // 取第i位元素
6.2.2 二维数组
-
定义:类型符 数组名[常量表达式][常量表达式]
-
初始化
int array[2][2] = { {1, 2}, {3, 4} }; int array[2][2] = { 1, 2, 3, 4 }; int array[][2] = { {1, 2}, {3, 4} }; // 既只允许最外层元素个数定义时为空 int array[][2] = { {0}, {3, 4} }; // 正确 int array[][2] = { {}, {3, 4} }; // 错误 ```
-
引用
int num = array[1][1]; int *p = array; *(*(p+j)+j); // 等同于array[i][j];
6.2.3 字符数组
-
定义:char array[10]; <=> int array[10];
字符型数组是以整型形式存放的 (ASCII)。
-
初始化
- 字符串常量不可以数组形式取具体位置进行元素修改。
- ( array == "Hello" ) => True or False ?
False,array 与字符串常量比较的是内存地址。
char array[0] = 'A'; char array[] = {"Hello"}; // 字符数组的存储情况:| H | e | l | l | o | \0 | // sizeof() -- 6 // strlen() -- 5 char array[] = {'H', 'e', 'l', 'l', 'o'}; // sizeof() -- 5 // strlen() -- 5 int array[] = {"Hello"}; // sizeof() -- 4 // strlen() -- 1 int array[] = {'H', 'e', 'l', 'l', 'o'}; // sizeof() -- 20 // strlen() -- 4 char *array = "Hello"; // 字符串常量
-
引用
- 若字符数组中,存在'\0'两个或以上,系统则以第一次出现的位置提前终止字符输出。
- stdin 和 gets() 搭配,可获得换行符、空格等字符。 (需结束标记符来终止输入)
scanf("%c", &array[0]); printf("%c", array[0]); scanf("%s", array); printf("%s", array);
-
应用
- gets(字符数组) -- 输入一字符串到字符数组中
- puts(字符数组) -- 输出一字符串到终端
- strlen(字符数组) -- 测一字符串的实际长度
- strcat(char *src1, const char *src2);
数组src2后接于src1,src1中的'\0'被覆盖。且数组src1必须足够大,以容纳数组src2。
- strcpy(char *src1, const char *src2);
数组src1必须足够大,以容纳数组src2。
- strcmp(const char *src1, const char *src2);
实际为ASCII的比较,其返回值为 <0、==0,>0 的情况。
- strlwr(字符串) -- 将字符串中大写字母转为小写字母
- strupr(字符串) -- 将字符串中小写字母转为大写字母
- atoi(字符串) -- 字符串转int型
- atol(字符串) -- 字符串转long型
- atof(字符串) -- 字符串转double型
[注]
- 字符串处理函数需加入头文件:
#include <string.h>
. - 需掌握字符串函数自定义方法实现。
- 大部份字符串处理函数多数以标记量'\0'为临界点,若字符数组中含两个或或以上,需注意实际的结果。
- 引用atoi()、atol()、atof()函数需引用
#include <stdlib.h>
7 柒 函数
7.1 为什么要用函数
-
模块化程序设计:每一函数实现一特定的功能,函数的名称既反映功能。
-
更好地代码复用:使用库函数;使用自己编写的函数。
代码复用:减少重复编码程序段的工作量。
[说明]
对于所有完成相同功能的组件,应抽象出一个接口,它们都实现该接口。
具体在Java中,所有完成相同功能的组件都实现该接口
或从该抽象类中的继承
。
7.2 定义函数
建立存储空间的声明
- 定义函数:
函数返回类型
函数名
函数参数
函数体
(变量定义、声明,执行语句)- 函数返回类型:基本数据类型 / void型;
- 函数名:驼峰式命名法;
- 函数参数:实参、形参.
7.3 函数声明
不需要建立存储空间的声明
-
函数原型 (Prototype):函数返回类型、函数名、参数类型、参数个数、参数顺序
-
函数声明的方法
- 使用函数原型;
- 同一源文件,在调用该函数的前面定义 (可打包到自定义头文件中).
7.4 函数调用
- 嵌套调用、递归调用 (直接或间接调用该函数本身)
- 实参和形参
- 概念
实参:常量、变量或表达式、函数 (返回值)
形参:函数调用期间临时分配内存,值从实参中获得,调用结束后释放内存空间。 - 实质:值传递、地址传递
- 概念
8 捌 指针
8.1 指针是什么
-
指针变量:保存变量地址的变量。
-
指针类型
- 指针类型的变量:存放地址
- 指针类型的值:对应内存地址存放的值
在 swap(int *a, int *b); 的案例中可以形象说明两者的区别。
8.2 指针移动 (运算:加、减)
对指针加一、减一运算,即地址会增加或减少一单位长度。单位长度具体具体指当前指针所指向数据类型的所占空间大小。
8.3 指针类型
8.3.1 空指针
确保没有指向任何一个对象的指针。通常以宏定义NULL(0)表示空指针的常量值。
关于NULL、0和'\0',大部分情况都为零。特别地,
int *p = 0; // 正确,编译器将指针指向内存地址为0处。
int *p = 3; // 错误,赋值的数据类型不相符。
8.3.2 指针类型的派生
-
指向函数的指针
void (*func(int));
-
指向数组的指针(多重指针)
int (*p)[5];
8.3.3 数组类型的派生
-
指针数组
int *p[5]; // 存放5个指向int类型的指针。
-
用英语解读各种各样的C语言声明
C语言 | 英语表示 | 中文表示 |
---|---|---|
int huge; | huge is int | hoge是int型 |
int huge[10]; | huge is array[10] of int | hoge是int型的数组 |
int huge[2][4]; | huge is array[2] of array[4] of int | hoge是int型的数组的数组 |
int *huge[10]; | huge is array[10] of point to int | hoge是指向int型的指针的数组(存放指针变量) |
int (*huge)[10]; | hoge is pointer to array[10] of int | hoge是指向int型的数组的指针 |
int func(int a); | func is function(int a) returning int | func是返回int型的函数 |
int (*func)(int a); | func is pointer to function(int a) returning int | func是指向返回int型值的函数的指针 |
8.4 指针的应用
8.4.1 指针与数组
-
一维
p[i] // 等同于 *(p+i) i[p] // 等同于 *(i+p) &p[i] // 等同于 (p+i),即第i个元素的地址
-
二维
huge[i] // 等同于 *(huge+i),即第i行的首地址 *(huge+i)[j] // 等同于 *(*(p+j)+j),即 huge[i][j]
8.4.2 指针与字符串
8.4.2.1 字符指针变量
-
定义
char *array = "World"; array = "hello"; // 改变指向 char array[] = "Hello"; array = "World"; // 错误的做法 char *array = "Hello World"; array += 6; // 改变指向 (首地址改变)
8.4.2.2 字符数组
-
定义: int array[] = "Hello";
-
使用:printf("%c", array[0]);
字符指针变量的值是不能改变的,即已是字符串常量。
char *array = "Hello"; array[0] = 'W';
8.4.3 指针与函数
8.4.3.1 作为参数
即传递的是指向初始元素的指针。
-
数组名作函数参数
- int func( int array[] );
- int func( int *array );
-
多维数组作函数参数
- int func( int (*huge)[10] );
- int func( int huge[2][4] );
-
指向函数的指针作函数参数
- int func( int (*p)(int) );
-
指针数组作main函数形参
- int func( int argc, char *argv[] );
argv: 文件名 + 其他参数
-
字符指针作函数参数
8.4.3.2 作为返回值
返回指针值的函数,即返回的是地址。
[如] 返回的指针指向结构体变量、字符变量等。
9 玖 构造类型:用户自己建立数据结构
9.1 结构体类型
-
定义
struct Name { int num; char word[59]; } *p, name[5];
-
初始化:所有成员一起赋值。
-
使用
name[i].num; p->word[i]; (*p).num; struct Name *tmp; tmp = name; (tmp++)->num; // 先'++'操作,后'->'操作
-
大小:成员变量所占内存长度总和。
9.2 共用体类型
-
定义
union Name { int num; double digital; char word; } *p, name[5];
-
初始化:只允许给一个成员变量赋值。
union Name tmp = {10}; union Name tmp = {.word = 'Y'}; t.digital = 2.0; t.word = 'N'; // 最终的赋值
-
使用
name[i].num; p->word[i]; (*p).num;
-
大小:成员变量所占内存长度最大者。
[注] 关于结构体、共用体类型的内存长度问题,遵循4字节倍数的原则进行内存布局对齐。
[如]
sizeof(struct Name) = 64 (63)
sizeof(union Name) = 4 (4)
9.3 枚举类型
-
定义
enum Week { sun, mon, tue, wed, thu, fir, sat // 默认参数从0开始 } week;
-
初始化
enum Week { mon = 1, tue = 2, wed = 3, thu = 4, fir = 5, sat = 6,sun = 7 // 默认参数从0开始 } week;
-
使用:week.mon;
9.4 Typedef 声明新类型名
-
含义:引入变量别名,而不是另外地给变量分配空间。
-
使用
typedef int Integer; typedef long Integer; // 若编译器中,int为2字节,满足移值需求可以long型替换。 Interger num = 1;
-
与 #define 宏定义的区别
- #typedef:编译阶段处理
- #define:预编译阶段处理,实质是字符串替换
10 拾 文件处理
10.1 文件与流
-
stdin -- 标准输入流 -- 用于读取普通输入的流,在大多数环境中为键盘输入。scanf()与getchar()等函数会从这个流中读取字符。
-
stdout - 标准输入流 -- 用于写入普通输入的流,在大多数环境中为输出至显示器界面。printf()、puts()与putchar()等函数会向这个流写入字符。
-
stderr -- 标准错误流 -- 用于写出错误的流,在大多数环境中为输出至显示器界面。
10.2 文件分类
-
ASCII文件 (文本文件):每一字节存放一字符的ASCII代码。
-
二进制文件
- 优:节约存储空间
- 劣:精度有限
[如] 整数10000
ASCII形式存储空间为 5 字节
二进制形式存储空间为 4 字节
10.3 文件类型指针:FILE型
- 需引用
#include <stdio.h>
[注] 指向文件的指针变量并不是指向外部介质上的数据文件开头,而是指向内存中的文件信息区的开头。
10.4 打开文件
- 原型:FILE *fopen(const char *filename, const char *mode);
- 定义:FILE *fp = fopen("example.txt", "r");
文件类型 | 文本文件 | 二进制文件 |
---|---|---|
模式 | r w a | rb wb ab |
只读;只写(文件存在,则长度清零);追加 | 只读;只写(文件存在,则长度清零);追加 | |
r+ w+ a+ | rb+ wb+ ab+ | |
读和写(打开文件);读和写(建立文件;文件存在,则长度清零);读和写(打开文件) | 读和写(打开文件;文件存在,则长度清零);读和写(建立文件);读和写(打开文件) |
10.5 关闭文件
- 原型:int fclose(FILE *stream);
- 返回值 ( True:0;False:EOF(-1) )
- 数据存储的过程:数据 --> 缓存区 (充满) --> 文件
- 若不关闭文件,将会造成数据丢失。
- 若突然关闭文件,缓存区传输到文件的过程给中断,造成数据丢失。
10.6 顺序读写数据文件
-
格式化读取文件
int fscanf(FILE *stream, const char *format, ...); // 返回值: // Ture - 返回成功赋值的输入项数 // False - 返回文件结束标记EOF(-1) // 使用实例 fscanf(fp, "%s%lf%lf", name, &height, &weight);
-
格式化写入文件
int fprintf(FILE *stream, const char *format, ...); // 返回值: // Ture - 返回发送的字符数 // False - 返回文件结束标记EOF(-1) // 使用实例:获得当前运行时间,并存入文本中 time_t current = time(NULL); struct tm *timer = Localtime(¤t); // 将日历时间time_t型的值转换为分解时间tm结构体类型的值 // 其中,tm结构体为: struct tm { int tm_sec; // 秒(0 - 61) int tm_min; // 分 (0 - 59) int tm_hour; // 时 (0 - 24) int tm_mday; // 日 (1 - 31) int tm_mon; // 月 (0 - 11) int tm_year; // 从1900至今,经历了多少年 int tm_wday; // 星期 (0 - 6) int tm_yday; // 经历天数 (从1月1日计起) int tm_tm_isdst; // 夏时令 (夏季时间将提前1小时) }; fprintf(fp, "%d %d %d %d %d", timer->tm_year + 1900, timer->tm_mon + 1, timer->tm_day, timer->tm_hour, timer->tm_min, timer->tm_sec); fclose(fp);
-
读入/写入一个字符
int fgetc(FILE *stream); // 读入一个字符 int fputc(FILE *stream); // 写入一个字符 // 返回值: // Ture - 返回所读的字符数 // False - 返回文件结束标记EOF(-1)
-
用二进制方式向文件读写一组数据
size_t fwrite(const void *ptr, size_t size, size_t nmemb, FILE *stream); // 从ptr指向的数组中将最多nmemb个长度为size的元素写入stream指向的流中。 size_t fread(const void *ptr, size_t size, szie_t nmemb, FILE *stream); // 从stream流中读取nmemb个长度为size的元素写入到ptr数组。
10.7 随机读写数据文件
10.7.1 文件位置标记及其定位
-
文件位置标记:文件头、读写当前位置、文件尾
-
文件位置标记的定位:fseek(文件类型指针, 位移量, 起始点);
- 文件开始位置 -> SEEK_SET -> 0
- 文件当前位置 -> SEEK_CUR -> 1
- 文件末尾位置 -> SEEK_END -> 2
10.7.2 随机读写
结合fseek()与fread()函数实现。
[如] 读取第1,3,5,7,9个学生数据并输出。
for(i = 0; i<10; i+=2){
fseek(fp, i*sizeof(struct Student), 0);
fread(&student[i], sizeof(struct Student), 1, fp);
}