C语言——数组、函数、指针、结构体、文件
数组
一维数组
一维数组定义
为了处理方便,把具有相同类型的若干变量按有序的形式组织起来。这些按序排列的同类数据元素的集合称为数组。
- 一维数组定义的一般形式为:
类型说明符 数组名[常量表达式];
int a[10]; /*整型数组a,有10个元素*/
float b[10],c[20]; /*实型数组b,有10个元素,实型数组c,有20个元素*/
char ch[20]; /*字符数组ch,有20个元素*/
对于数组类型说明应注意以下几点:
①数组的类型实际上是指数组元素的取值类型。对于同一个数组,其所有元素的数据类型都是相同的。
②数组名的书写规则应符合标识符的命名规范。
③数组名不能与其它变量名相同。
④方括号中常量表达式表示数组元素的个数,其下标从0开始计算。
int a[5];//整型数组a,有5个元素,分别是a[0],a[1],a[2],a[3],a[4]
⑤不能在方括号中用变量来表示元素的个数,但是可以用符号常量或常量表达式。
#define FD 5
int main( )
{
int a[3+2],b[7+FD];
} //这是合法的
#include <stdio.h>
int main( )
{
int n=5;
int a[n];
} //C99以前这是错误的
⑥C编译系统为数组分配连续的存储空间,数组名代表数组在内存中存放的首地址。
例如:整型数组a[10],其存储情况如下图所示,每个存储单元占4字节。
一维数组初始化
- 数组可以在定义时初始化。初始化赋值的一般形式为:
类型说明符
数组名[常量表达式] = {值, 值, ……值};
int a[10]={ 0,1,2,3,4,5,6,7,8,9 };//相当于a[0]=0; a[1]=1;...a[9]=9;
C语言对数组的初始化赋值还有以下几点规定:
①可以只给部分元素赋初值。当{ }中值的个数少于元素个数时,只给前面部分元素赋值。
int a[10]={0,1,2,3,4};//表示只给a[0]~a[4]这5个元素赋值,而后5个元素自动赋0值
②只能给元素逐个赋值,不能给数组整体赋值。
//给十个元素全部赋1值,只能写为:
int a[10]={1,1,1,1,1,1,1,1,1,1};//而不能写为:int a[10]=1;
一维数组元素的引用
- 数组元素的一般表示形式为:
数组名[下标
]
其中,下标只能为整型常量或整型表达式。
例如:a[5]、a[i+j]、a[i++]都是合法的数组元素。
- 在C语言中只能逐个地使用下标变量,而不能一次引用整个数组。
//输出有10个元素的数组必须使用循环语句逐个输出各下标变量:
for(i=0; i<10; i++)
printf("%d",a[i]);
【例】用数组来处理求Fibonacci数列问题
可以发现,除了第1、2项,从第3项开始,每一项都是前面两项之和。将fibonacci数列的前N项依次放入数组之中,用循环语句从第3项开始求解。
【例】某位同学一学期有10门课,根据每门课程的得分情况,试查找出其最高分和最低分。
分析:
变量max表示最高分和min表示最低分,先假定最高分和最低分均为a[0],然后利用for循环随着i的变化依次访问a[1]~a[9],在此过程中,让max和min与每一个分数a[i]进行比较,最终得到所有分数的最高分和最低分。
【例】用冒泡法对10个整数进行由小到大排序。
分析:
冒泡排序法是对相邻的两个数进行比较。以5个数为例说明排序过程。设 int a[5]={10,7,4,5,8};
则排序过程如下:
二维数组
二维数组的定义
- 二维数组定义的一般形式是:
类型说明符
数组名[常量表达式1][常量表达式2];
例如:int a[3][3];
定义了一个三行三列的数组,所包含的数组元素及其存储情况如下所示:
二维数组的初始化
- 二维数组初始化也是在类型说明时给各下标变量赋以初值,以数组元素的存储顺序为依据。二维数组可按行分段赋值,也可按行连续赋值。
int a[3][3];
int a[3][3]={ {80,75,92},{61,65,71},{59,63,70} };//按行分段赋值
int a[3][3]={ 80,75,92,61,65,71,59,63,70};//按行连续赋值
//输出二维数组矩阵。
#include <stdio.h>
int main( )
{
int i, j;
int a[5][3]={{80,75,92},{61,65,71},{59,63,70},{85,87,90},{76,77,85}};
for(i=0;i<5;i++)
{ for(j=0;j<3;j++)
printf("%d ",a[i][j]);
printf("\n");
}
return 0;
}
- 二维数组同样也可以对部分元素赋初值,未赋初值的元素自动取0值。
例如: int a[3][3]={{1},{2},{3}};
赋值后各元素值:
- 若对二维数组中全部元素赋初值,则第一维的长度可以省略。
例如: int a[3][3]={1,2,3,4,5,6,7,8,9};
可写为: int a[ ][3]={1,2,3,4,5,6,7,8,9};
二维数组的元素的引用
- 二维数组元素的表示形式:
数组名[下标][下标]
下标应为整型常量或整型表达式
二维数组元素的访问涉及第1维和第2维两个下标,所以对二维数组的操作通常和二重循环结合。
【例】求二维数组(5行5列)中最大元素值及其行列号。
分析:
5行5列的矩阵共有25个数据,可以定义一个5行5列的二维数组来存放这些数据,并定义数组中的第一个元素为最大值,然后利用循环语句,让第一个元素与后面的元素进行比较,如果有更大者,便赋值给最大值,同时记录下最大值的行号和列号。
【例】一个学习小组有5个人,每个人有三门课的考试成绩。求全组分科的平均成绩和各科总平均成绩。
张 | 王 | 李 | 赵 | 周 | |
---|---|---|---|---|---|
Math | 80 | 61 | 59 | 85 | 76 |
C | 75 | 65 | 63 | 87 | 77 |
Foxpro | 92 | 71 | 70 | 90 | 85 |
分析:
可设一个二维数组a[5][3]存放五个人三门课的成绩。再设一个一维数组v[3]存放所求得各分科平均成绩,设变量average 为全组各科总平均成绩。通过循环来接收用户输入的成绩,放入数组a中,同时统计各科总成绩。每接收完一科成绩,计算出单科平均成绩,存入数组v中。最后通过计算数组v中的平均值得到全科的平均成绩。
字符数组和字符串
字符数组的定义及初始化
字符数组也是数组,只是数组元素的类型为字符型。所以字符数组的定义、初始化,字符数组元素的引用与一般的数组类似。
char c[10]; /*定义一个包含10个字符的数组*/
//字符数组也可以是二维或多维数组。
char c[5][10]; /*定义一个5行10列的字符数组*/
- 字符数组也允许在定义时作初始化赋值。
char c[10]={'c', ' ', 'p','r','o','g','r','a','m'};
- 当对全体元素赋初值时也可以省去长度说明。
char c[ ]={'c', ' ', 'p','r','o','g','r','a','m'};
【例】输出字符数组中各元素的值。
#include <stdio.h>
int main( )
{
int i,j;
char a[ ][5]={{'C','H','I','N','A',},{'B','a','s','i','c'}};
for(i=0;i<=1;i++)
{
for(j=0;j<=4;j++)
printf("%c",a[i][j]);
printf("\n");
}
return 0;
}
【例】我国自古以来是一个多民族国家。中华人民共和国成立后,经中央人民政府调查与统计正式确认的民族共有56个。汉族是我国的主体民族,约占全国人口总数的91.11%,其他还有55个民族,约占8.89%。汉族和55个少数民族共同组成伟大的中华民族。我国,是一个以汉族为主体、56个民族共同组成的统一的多民族国家。请编程输出我国56个民族名称。
分析:
定义一个字符数组用来存放民族名称,应用循环结构输出字符数组中各元素的值。
字符串及其处理函数
C语言允许用字符串的方式对数组作初始化赋值。
例如:
char c[ ]={'c',' ','p','r','o','g','r','a','m'};
可写为:
char c[ ]={"C program"};
或:
char c[ ]="C program";
在采用字符串方式后,可用printf函数和scanf函数一次性
输出输入一个字符数组中的字符串
#include <stdio.h>
int main( )
{
char c[ ]="CHINA\nhello";
printf("%s\n",c);
return 0;
}
当用scanf
函数输入字符串时,字符串中不能含有空格,否则将以空格作为字符串的结束符。
#include <stdio.h>
int main( )
{
char st[15];
printf("input string:\n");
scanf("%s",st);
printf("%s\n",st);
return 0;
}
用于输入输出的字符串函数,在使用前应包含头文件"stdio.h
",
使用其它字符串函数则应包含头文件"string.h
"
- 字符串输出函数 puts(字符数组名);
功能:
把字符数组中的字符串输出到显示器,直到遇到“字符串”结束标志。
- 字符串输入函数 gets(字符数组名);
功能:
从标准输入设备键盘上输入一个字符串。本函数得到一个函数值,即为该字符数组的首地址。
【小提示】
用puts和gets函数只能输出或输入一个字符串,不能写成puts(str1,str2) 或 gets(str1,str2)
- 字符串连接函数 strcat(字符数组名1,字符数组名2)
功能:
把字符数组2中的字符串连接到字符数组1 中字符串的后面,结果放在字符数组1中,并删去字符串1后的串标志“\0”
【例】根据用户输入的姓名x,输出“My name is x ”。
- 字符串拷贝函数 strcpy (字符数组名1,字符数组名2)
功能:
把字符数组2中的字符串拷贝到字符数组1中。串结束标志“\0”也一同拷贝。
在使用strcpy函数时应注意以下几点:
①字符数组1必须定义得足够大,以便容纳被复制的字符串
②字符数组1必须写成数组名形式,字符串2可以是字符数组名,也可以是一个字符串常量。
③如果在复制前未对字符数组赋值,则字符数组1各字节中的内容是无法预知的,复制时将字符数组2中的字符串和其后的'\0'一起复制到字符数组1中,取代字符数组1中的字符。
- 测字符串长度函数 strlen(字符数组名)
功能:
测字符串的实际长度(不含字符串结束标志‘\0’) 并作为函数返回值
- 字符串比较函数 strcmp(字符数组名1,字符数组名2)
功能:
按照ASCII码顺序比较两个数组中的字符串,并由函数返回值返回比较结果
函数返回的结果值为:
(1)字符串1=字符串2,返回值=0
(2)字符串1>字符串2,返回值>0
(3)字符串1<字符串2,返回值<0
【例】为了保证信息的安全,大多数系统都含有用户登录模块。只有输入正确的用户名和密码之后才能进行相应的操作。编写程序实现用户登录功能。
分析:要想成功登录,输入的密码和原密码要一致,也就是说,两个字符串要进行比较,这里就用到了我们的字符串比较函数strcmp();通过定义字符数组,可用于存放原密码及用户输入的密码;若输入密码不正确,可再进行尝试,这里考虑通过循环来实现多次尝试输入;若输入密码正确,则提示登录成功,循环结束。
函数
函数定义
- 库函数
格式化输入、输出函数 printf() scanf()
字符串连接函数 strcat(str1,str2)
求字符串长度函数 strlen(str)
- 用户自定义函数
函数名字
函数参数
函数返回值
函数功能
返回值类型 函数名([形式参数列表])
{
函数体
}
返回值类型:指定函数返回值的数据类型。如果函数没有返回值,则为void。
函数名:指定函数的名称,是用户自定义的标识符。
形式参数列表:参数是调用函数时传入的数据,函数定义时需要指定函数参数的名称和数据类型。
函数体:大括号“{}”括起来的部分,用于实现该函数的功能,是C语言语句
定义一个函数,实现两个整数的求和运算
void add(int x,int y)
{
int result;
result=x+y;
printf("%d",result) ;
}
void add()
{
int x,y,result;
scanf("%d%d",&x,&y);
result=x+y;
printf("%d",result) ;
}
函数分类
有返回值函数
无返回值函数
有参函数
无参函数
main函数
int main()
{
return 0;
}
函数时构成C程序的基本单位
一个源程序文件可以有多个函数
一个源程序文件只能有一个main函数,而且必须有一个main函数
C99标准中对main函数的返回值类型定义是int。
函数调用
函调用的形式
要想执行函数,需要在main()函数中调用它。C语言程序必须有一个主函数main(),而且只有一个主函数main(),程序的运行从主函数开始,主函数调用其它的函数。
- 无参函数的调用
- 有参函数的调用
1.无参函数的调用
【例】输出以下结果如图所示,通过函数调用实现。
在输出的结果中,第一行和第三行分别是一行“”号,使用模块化编程思想,用一个函数show_single实现输出一行“”号的功能,用一个函数show_txt实现输出第二行的信息,main函数中调用这两个函数。
2.有参函数的调用
与无参函数相比,有参函数在调用时,需要调用者传入参数值。
#include <stdio.h>
void add(int x,int y)
{
int result;
result=x+y;
printf("x+y=%d\n ",result);
}
main()
{
add(5,10);
}
函数的传递方式
【例】定义一个函数,实现两个整数的求和运算。
1、实参可以是常量、变量或表达式,但是必须有确定的值。
2、实参与形参必须:
个数相等
顺序对应
类型匹配
(1)函数add被调用前,形参x和y并不占用内存空间。
(2)函数add被main函数调用时,形参x和y被分配内存空间,并且被实参5和10分别赋值。函数调用过程中的参数传递如图所示。
(3)函数add执行期间,由于形参x和y已有值,所以可以执行x+y操作,并且将结果输出。
(4)函数add执行结束后,形参x和y的内存空间被释放。
#include <stdio.h>
void swap(int x,int y)
{
int temp;
printf("函数内部 交换之前:x=%d,y=%d\n",x,y);
temp=x;
x=y;
y=temp;
printf("函数内部 交换之后:x=%d,y=%d\n",x,y);
}
int main()
{
int num1,num2;
scanf("%d%d",&num1,&num2);
printf("交换之前:num1=%d,num2=%d\n",num1,num2);
swap(num1,num2);
printf("交换之后:num1=%d,num2=%d\n",num1,num2);
return 0;
}
函数的返回值
返回值类型 函数名([形式参数列表])
{
return (表达式);
}
void 函数名([形式参数列表])
{
}
//如果想要通过函数调用使主调函数得到一个值,这个值就是函数的返回值。函数的返回值通过函数体中的return语句获得。
#include <stdio.h>
int add(int x,int y)
{
int result;
result=x+y;
return result;
}
int main()
{
int sum;
sum=add(5,10);
printf("%d\n ",sum);
return 0;
}
函数被调用的位置可以有三种方式:
(1)函数语句
例如:fun();
(2)函数表达式
例如:sum=add(5,10)。
(3)函数参数
例如:printf("处理结果1::%d\n", add(5,10));
函数的参数类型
【例】使用函数实现下面功能。某位同学一学期有5门课程,根据每门课程的得分情况,找出其最低分和最高分。
int min(int array[5]) //找出最低分
int max(int array[5]) //找出最高分
地址传递
当函数的参数类型是数组时,实参向形参传递的是数组首元素的地址,这种传递方式被称为“地址传递”。
值传递:实参和形参占用不同的存储单元,只是存储的值相同。
地址传递:实参和形参占用相同的存储单元。
【例】某位同学第一学期有3门课程,第二学期有5门课程,根据每门课程的得分情况,找出每个学期中最高分和最低分。
int min(int array[],int len) //定义函数时不指定形参数组array的大小
{
int minnum=array[0];
for(int i=1;i<len;i++)
{
if(array[i]<minnum)
minnum=array[i];
}
return minnum;
}
函数的嵌套调用
函数的定义是独立的,在一个函数中不能嵌套定义另外一个函数。但是,函数可以嵌套调用,即在一个函数中调用另一个函数。
#include <stdio.h>
void fun2()
{
printf("调用fun2()函数\n");
}
void fun1()
{
printf("调用fun1()函数\n");
fun2();
}
int main()
{
fun1();
return 0;
}
函数的递归调用
void fun()
{
fun();
}
在调用fun函数时,又要调用fun函数,这就是fun函数的递归调用。但是,这里fun函数的调用是无终止的,程序中不应该出现。函数的递归调用必须要有终止条件,通常在函数定义时通过if语句控制。
【例】计算1~n之间整数之和。
#include <stdio.h>
int fun(int n)
{
if(n==1) //递归调用结束条件
return 1;
else
return n+fun(n-1); //fun函数的递归调用
}
int main()
{
int sum;
sum=fun(4);
printf("1~4之间的整数和是%d\n",sum);
return 0;
}
【例】输出斐波那契数列(Fibonacci Sequence)的前20个数。
斐波那契数列,又称黄金分割数列,因它是数学家莱昂纳多•斐波那契以兔子繁殖为例子而引入的,故又称为“兔子数列”。斐波那契数列可以这样描述,数列的第1个数是1,第2个数是1,以后每个数是其前面两个数的和,即1,1,2,3,5,8,13,21,34,55,…。
利用递归的方法分析斐波那契数列,可以得到递归公式:
函数的声明
1.函数声明的方法
函数定义时的首行被称为函数原型,函数声明时只需要在函数的原型后面加上“;”。例如:
int add(int x,int y);
在函数声明时,形参名也可以省略。例如:
int add(int ,int );
2.函数声明的位置
函数声明在主调函数的内部。
#include <stdio.h>
int main()
{ int add(int ,int );
int sum;
sum=add(5,10);
printf("%d\n ",sum);
return 0;
}
函数声明在主调函数的外部。
#include <stdio.h>
int add(int ,int );
int main()
{
………….
}
用户自定义函数的使用包括3个环节:
函数声明
函数定义
函数调用
变量的定义域和存储类型
变量的作用域
1.局部变量
在函数内定义的变量,它的作用域是本函数内。
2.全局变量
在函数外定义的变量,它的作用域是整个源程序文件内。
1.局部变量
#include <stdio.h>
void fun()
{
int x=6;
printf("fun函数:x=%d\n ",x);
}
int main()
{
int x=8;
fun();
printf("main函数:x=%d\n ",x);
return 0;
}
函数内定义的变量
函数的形参
复合语句内定义的变量
#include <stdio.h>
main()
{
int i=1, j=3;
{ int i=0;
i+=j*2;
printf("i=%d,j=%d\n" ,i,j );
}
printf("i=%d,j=%d\n" ,i,j );
}
2.全局变量
#include <stdio.h>
int x=10; //全局变量x
void fun()
{
printf("fun函数:x=%d\n ",x);
}
int main()
{
fun();
printf("main函数:x=%d\n ",x);
return 0;
}
【例】分析程序中全局变量与局部变量同名时各自的作用范围。
int a=3,b=5; //定义全局变量a,b
int max(int a,int b) //定义局部变量a,b,只在max函数中有效
{
int c;
c=a>b?a:b;
return c;
}
int main()
{
int a=10; //定义局部变量a,只在main函数中有效
printf("%d\n ",max(a,b));
return 0;
}
变量的存储类别
- 动态存储方式
在程序运行期间由系统动态的分配存储空间
- 静态存储方式
在程序运行期间由系统分配固定的存储空间
变量定义的完整形式是:
存储类别 数据类型 变量名;
变量的存储类别:
- 自动的(auto)
- 静态的(static)
- 寄存器的(register)
- 外部的(extern)
1.局部变量的存储类别
自动变量(auto)
在调用函数时,系统为这些变量分配存储空间,当函数调用结束时就自动释放这些空间。
静态变量(static)
在函数调用结束后并不释放存储空间,而在下一次再调用该函数时,该变量已有值。
寄存器变量(register)
它存储在处理器的寄存器中而不是内存中。寄存器变量可以提高程序的执行速度,因为直接访问寄存器比访问内存要快得多。
#include <stdio.h>
int fun(int n)
{
auto int x=0; //自动变量x
static int y=3; //静态变量y
x++;
y++;
return n+x+y;
}
int main()
{
int n=2,i;
for(i=0;i<3;i++)
printf("%d,",fun(2));
return 0;
}
2.全局变量的存储类别
静态存储类别
(1)将全局变量的作用域扩展到一个文件内
#include <stdio.h>
void fun();
int main()
{
extern int x; //把全局变量的作用域扩展到从此处开始
fun();
printf("main函数:x=%d\n ",x);
//函数main中不能引用全局变量x,程序编译会报错。
return 0;
}
int x=10;
void fun()
{
printf("fun函数:x=%d\n ",x);
}
(2)将全局变量的作用域扩展到其他文件
file1:
#include <stdio.h>
#include "file2.cpp"
int x=10; //全局变量x
int main()
{
printf("main函数:x=%d\n ",x);
fun();
return 0;
}
file2:
文件file2:
#include <stdio.h>
extern int x;
//将文件file1中的全局变量x的作用域扩展到文件file2中。
void fun()
{
printf("fun函数:%d\n",x);
}
(3)将全局变量的作用域限制到本文件
file1:
#include <stdio.h>
static int x=10;
int main()
{
printf("main函数:x=%d\n ",x);
fun();
return 0;
}
1.局部变量的存储类别
自动变量(auto)
静态变量(static)
寄存器变量(register)
2.全局变量的存储类别
静态存储类别
将全局变量的作用域扩展到一个文件内
将全局变量的作用域扩展到其他文件
将全局变量的作用域限制到本文件
指针
地址和指针
取地址运算符“&”
printf("%x",&i);
结果:62fe44
指针变量
指针变量:用来存放变量的地址(变量的指针)的变量。
定义形式为 数据类型名 * 指针变量名;
其中,星号“*”为定义指针变量的标志,称为指针运算符。
int i=3; /*定义整型变量i*/
int *pointer_i; /*定义指针变量pointer_i*/
pointer_i=&i; /*变量i的地址存放在指针变量pointer_i中*/
以上两句可以合并成:
int *pointer_i=&i;
星号“”也称为取值运算符, pointer_i表示变量i的值。
【例】 通过指针变量访问整型变量。
#include<stdio.h>
main()
{ int a,b,*p;
a=5;
p=&a;
b=*p+5;
printf("a=%d,*p=%d,b=%d\n",a,*p,b);
}
【例】取地址符号与取值符号是互为逆运算的。
#include<stdio.h>
main()
{ int a=5,*p;
p=&a;
printf("%d,%d,%d\n",&a,p,&(*p));
printf("%d,%d,%d\n",a,*p,*(&a));
}
两点说明:
- &和*为单目运算符,优先级别仅次于括号和成员运算符,具有右结合性。
- 运算符“&”的操作数允许是一般变量,运算符“*”的操作数必须为指针变量或地址型表达式。
【例】利用指针对两个数进行排序
#include <stdio.h>
main()
{ int *p1,*p2,*p,a,b;
printf("请输入两个整数\n");
scanf("%d%d",&a,&b);
p1=&a;
p2=&b;
if(a<b)
{p=p1;p1=p2;p2=p;}
printf("a=%d,b=%d\n",a,b);
printf("max=%d,min=%d\n",*p1,*p2);
}
指向数组的指针
指向一维数组的指针
#include <stdio.h>
main()
{
int a[5]={1,2,3,4,5};
printf("%d,%d\n",a,&a[0]);
printf("%d,%d\n",a[0],*a);
}
- 在C语言中,数组名代表数组的起始地址,这是一个地址常量,不允许赋值。
- 数组名是指向 数组第一个元素的指针。
1.指向一维数组元素的指针变量定义
int a[10];
int *p;
p=&a[0];
//或
int a[10];
int *p= &a[0];
//或
int a[10];
int *p=a;
如果有定义:
int a=3,array[5]={1,2,3,4,5},*p1,*p2;
//那么如下赋值运算都是正确的:
p1=&a; //将变量a的地址赋值给p1
p1=array; //将数组array的首地址赋值给p1
p1=&array[2]; //将数组元素array[2]的地址赋值给p1
p2=p1; //将指针变量p1的地址赋值给p2
数组指针的运算
如果p指向数组a的首地址,即a[0]的地址,那么表达式p+1、p+2…的含义是什么?
#include <stdio.h>
main()
{
int a[3];
int *p;
p=a;
printf("%x,%x,%x",p,p+1,p+2);
}
*p == a[0]
*(p+1) == a[1]
*(p+i) == a[i]
对于指针变量p,可以做以下运算:
p++,p--,p+i, p-i, p+=i, p-= i等。
#include <stdio.h>
main()
{ int a[]={1,3,6,7,9,12};
int x,*p=&a[2];
x=(*--p)++;
printf("x=%d\n",x);
printf("a[1]=%d\n",a[1]);
}
【例】显示指针变量的当前值
#include <stdio.h>
main()
{ int a[5],*p,i;
p=a;
for(i=0;i<5;i++)
scanf("%d",p++);
printf("\n");
for(i=0;i<5;i++,p++)
printf("%d\t",*p);
}
指针也可以进行关系运算:
①若p1和p2指向同一个数组,则
p1<p2表示p1指的元素在前;
p1>p2表示p1指的元素在后;
p1==p2表示p1与p2指向同一个元素。
②若p1和p2不指向同一个数组,比较毫无意义。
③若p1与p2指向同一数组,则p1 - p2的值是两个指针之间的元素个数。
④p1+p2无意义。
指向字符串的指针
在C语言中,字符串在内存中的存放方式与字符数组存储方式是一致的。
计算机给字符串自动分配一个首地址,并在字符串尾部添加字符串结束标志’\0’。
可以使用指向字符数组的指针来灵活方便地进行字符串的处理。
main()
{ char ch[10]="Hello";
char *p;
p="China";
printf("%s\n",p);
p++;
printf("%s\n",p);
printf("%c\n",*p);
p=ch;
printf("%s\n",p);
return 0;
}
【例】使用字符型指针复制字符串
#include<stdio.h>
main()
{ char str1[10],str2[10],*p,*q;
p=str1;
q=str2;
gets(str1);
while(*p)
{*q=*p; p++;q++;}
*q='\0';
puts(str1);
puts(str2);
}
指向二维数组的指针
二维数组名是数组的首地址,二维数组名的基类型不是数组元素类型,而是一维数组类型,因此,二维数组名a是一个行指针。
如果有 int a[3][4]; 则
1、a、a+1、a+2...a+i是行指针。
2、a[0]、a[1]、a[2]...a[i]是列指针、是一维数组名、是地址常量。
3、&a[i][j]= a[i]+j=*(a+i)+j 都可以表示数组元素a[i][j]的地址。
4、a[i][j]=*(a[i]+j)=*(*(a+i)+j)都可以表示数组元素a[i][j]。
【例】使用指针变量输出二维数组元素
#include<stdio.h>
main()
{ int a[2][3]={{1,2,3},{4,5,6}},*p;
for(p=a[0];p<a[0]+6;p++)
{ if((p-a[0])%3==0)printf("\n");
printf("%2d",*p);
}
}
用来存放二维数组行地址的指针变量称为指向二维数组的指针变量。
定义形式:数据类型名 (*指针变量名)[数组长度];
例如:
int (*p)[3];
【例】用行指针输出二维数组元素。
#include <stdio.h>
main()
{int a[3][4]={1,2,3,4,5,6,7,8,9,10,11,12};
int (*p)[4],i;
for(p=a;p<a+3;p++)
{ for(i=0;i<4;i++)
printf("%3d",*(*p+i));
printf("\n");}
}
【例】二维数组元素的不同表示方法
#include <stdio.h>
main()
{
int a[3][4]={{1,2,3,4},{5,6,7,8},{9,10,11,12}};
int (*p)[4],i=2,j=1;
p=a;
printf("%d,%d,%d",a[i][j],*(a[i]+j),*(*(p+i)+j));
}
如果定义了p是指向二维数组a的行指针,则可以用如下方式来表示二维数组元素a[i][j]:
*(*(p+i)+j) *(p[i]+j) *(*(p+i)+j)
*(*(a+i)+j) *(a[i]+j) *(*(a+i)+j)
指针数组
数组中的元素都是指针变量,这样的数组称为指针数组。
一维指针数组的定义形式为:
类型名 *数组名[常量表达式];
例如:
int *p[5];(不要写成 int (*p)[5];)
数组p是一个包含5个元素的一维数组。它的每个元素都是基类型为int的指针变量,所以称数组p为指针数组。
指针数组常用来处理字符串
例如:图书管理中的书名存储。
【例】将多个字符串按字母顺序(由小到大)输出。
例如:char *book[5];
用指针变量作为函数参数
指针变量作为形参时,对应的实参必须为地址类型的表达式的值
除了用return返回一个值之外,还可以通过指针参数返回多个数据。
【例】指针变量做形参,交换函数实参变量的值。
#include <stdio.h>
void swap(int *p,int *q)
{
int t;
t=*p;
*p=*q;
*q=t;
}
main()
{
int a=3,b=5;
printf("a=%d,b=%d\n",a,b);
swap(&a,&b);
printf("a=%d,b=%d\n",a,b);
}
【例】指针变量做形参,数组名做实参
#include<stdio.h>
void print(int *p,int n)
{
for(int i=0;i<n;i++)
printf("%3d",*p++);
}
int main()
{
int a[5]={1,2,3,4,5};
print(a,5);
return 0;
}
【例】数组名做形参,数组名做实参
#include<stdio.h>
void print(int b[],int n)
{
for(int i=0;i<n;i++)
printf("%3d",*b++);
}
int main()
{
int a[5]={1,2,3,4,5};
print(a,5);
return 0;
}
返回指针值的函数
函数可以返回指针型的数据。这种返回指针值的函数,
一般定义形式为:
类型名 *函数名 ( 参数列表 )
例如: int *a( int x,int y);
【例】从键盘输入一个字符串,再输入一个要查找的字符,编写match函数在字符串中查找该字符。若找到相同字符,则返回一个指向该字符所在位置的指针;如果没有找到,则返回一个空(NULL)指针。
#include <stdio.h>
int main() {
char *match(char,char *);
char s[50],c,*p;
printf("请输入一个字符串:\n");
gets(s);
printf("请输入一个字符:\n");
c=getchar();
p=match(c,s);
printf("查找结果:");
if(*p==‘\0’)
printf("字符串中无此字符\n");
else
printf("%c查找成功!\n",*p);
}
char *match(char c,char *s)
{
int i=0;
while(s[i]!='\0'&&c!=s[i])
i++;
return &s[i];
}
指向函数的指针 和 指向指针的指针
将函数名赋予一个指针变量,使指针变量指向函数所在的内存区域,这种指针就是指向函数的指针。
指向函数的指针变量的一般定义形式为
数据类型 (*指针变量)(参数列表)
int func(int x); /* 声明一个函数 */
int (*p) (int x); /* 声明一个指向函数的指针变量 */
p=func; /* 将func函数的首地址赋给指针变量p*/
//或者使用下面的方法将函数地址赋给函数指针:
p=&func;
【例】使用指向函数的指针变量调用一个函数,求出两个整数的和。
#include <stdio.h>
int sum()
{
int a,b;
printf("请输入两个整数:\n");
scanf("%d,%d",&a,&b);
return a+b;
}
int main()
{
int result;
int (*p)(); //p是指向无参函数的指针变量
p=sum; //变量p指向sum函数的首地址
result=(*p)(); //通过p指针调用sum函数
printf("两个数的和是%d\n",result);
}
关于该例题的几点说明:
(1)int (*p)() 不能省略括号
(2)p=sum;给指针变量赋值时,必须给出函数名,不必给出参数。
(3)p指向sum函数,但不能使用p++、p--、p+n等运算指向函数内某条指令。
(4)因为类型声明语句int (*p)(),所以p只能指向类型一致的函数,不能指向有参函数。
【例】使用指向函数的指针变量调用有参函数,求两个整数中较大的数。
#include <stdio.h>
int max(int a,int b)
{
return a>b?a:b;
}
int main()
{
int a,b,result;
int (*p)(int,int);
p=max; //给p赋值时不必给出函数参数
printf("请输入两个整数:\n");
scanf("%d,%d",&a,&b);
result=(*p)(a,b); //通过p指针调用max函数
printf("其中较大的数是%d\n",result);
}
如果一个指针指向的是另外一个指针,我们就称它为二级指针,或者指向指针的指针。
int a=100;
int *p1=&a;
int **p2=&p1;
动态内存分配
malloc函数
其函数原型为:
void *malloc(unsigned int size);
功能:分配一块长度为size字节的连续空间,并将该空间的首地址作为函数的返回值。如果函数没有成功执行,返回值为空指针(NUIL或0)。由于返回的指针的基类型为void,应该通过显式类型转换后才能存入其他基类型的指针变量中,否则会有警告提示。例子:
int *p;
p= (int *)malloc(sizeof(int));
free函数
其函数原型为:
void free(void *block);
功能是:释放以前分配给指针变量block的动态空间,但指针变量block不会自动变成空指针。例子:
int *ptr,i-0;
if((ptr=(int *)malloc(10*sizeof(int)))!=NULL){
for(i=0;i<10;i++)
ptr[i]=i*i;
free(ptr);
}
用memset函数将存储空间初始化
#include <stdio.h>
#include <malloc.h> //或者#include <stdlib.h>
#include <string.h>
int main({
int *ip;
ip=(int *)malloc(10*sizeof(int));
memset(ip,0,10*sizeof(int));
int i;
for(i=0;i<10;i++){
print("0x%-10x",ip[i]);
if(i==4)
printf("\n");
free(ip);
}
结构体和共用体
结构体类型
结构体类型的定义
学生:
- 学号 int sno;
- 姓名 char name[20];
- 班级 char classname[20];
- 课程成绩 int grade[3];
将多个不同类型的数据组成一个“整体”
struct 结构体类型名称
{
数据类型 成员名1;
数据类型 成员名2;
…
数据类型 成员名n;
};
struct student
{
int sno;//学号
char name[20];//姓名
char classname[20];//班级
int grade[3];//成绩
};
结构体变量的定义
先定义结构体类型,再定义结构体变量
struct student { int sno; char name[20]; char classname[20]; int grade[3]; }; struct student stu1;
在定义结构体类型的同时定义结构体变量
struct student { int sno; char name[20]; char classname[20]; int grade[3]; }stu1;
直接定义结构体变量
struct { int sno; char name[20]; char classname[20]; int grade[3]; }stu1;
结构体变量定义后,编译器就会为其分配内存。它所占用的实际字节数,就是其各个“成员”所占用字节数的总和。
本例中变量stu1中各成员所占用内存:
struct student { int sno; char name[20]; char classname[20]; int grade[3]; }stu1;
结构体变量的初始化
在定义结构体类型和结构体变量的同时,对结构体变量初始化。
struct student
{
int sno;
char name[20];
char classname[20];
int grade[3]
}stu1{201601, "李磊","软件16级1班",{90,85,80}};
结构体变量的引用
如何引用结构体变量中的一个成员,其语法结构是:
结构体变量名.成员名
stu1.sno=20160101;
stu1.name="李磊";
stu1.classname="软件16级1班";
stu1.grade[0]=90;
stu1.grade[1]=85;
stu1.grade[2]=80;
“.”是一个运算符,表示对结构体变量的成员进行访问
【例】要求通过键盘输入一个学生信息,并且输出这个学生的所有信息。
分析:
1、定义结构体类型student描述学生信息。其成员包括学号、姓名、班级、三门课程成绩。
2、通过键盘输入每个成员的数据。
【例】从键盘输入学生信息,计算该学生的成绩平均分,并输出该学生的信息。
分析:
1、定义结构体类型student描述学生信息。
2、学生的成绩是该结构体类型中的一个成员,使用数组类型存储数据,通过遍历数组中的数组元素,经过累加求和,就可以计算成绩的平均分。
结构体数组
struct student
{
int sno;
char name[20];
char classname[20];
double grade[3]
};
struct student stu[30];
【例】要求通过键盘输入两个学生信息,并且输出这两个学生的所有信息。
分析:
1、定义一个包含2个元素的数组,数组元素的类型是结构体类型。
2、使用循环结构遍历访问数组元素。
结构体指针
struct student
{
int sno;
char name[20];
char classname[20];
double grade[3]
};
struct student s ={201601, "李磊","软件16级1班",{90,85,80}};;
struct student *p;
p=&s;
*(结构体指针变量).成员名
(*p).sno=20160101;
(*p).name="李磊";
(*p).grade[0]=90;
结构体指针变量->成员名
p->sno=20160101;
p->name="李磊";
p->grade[0]=90;
【例】要求通过键盘输入一个学生信息,并且输出这个学生的所有信息。(用指针变量引用结构体变量的成员。)
结构体与函数
结构体与函数的关系主要分为三种:
结构体变量作为函数参数
结构体指针作为函数参数
函数的返回值是结构体类型。
值传递
#include <stdio.h>
struct student
{
int sno;
char name[20];
double grade[3];
};
void showStuInfo(struct student stu)
{
printf("学号:%d\n",stu.sno);
printf("姓名:%s\n",stu.name);
printf("课程1:%lf\n",stu.grade[0]);
printf("课程2:%lf\n",stu.grade[1]);
printf("课程3:%lf\n",stu.grade[2]);
}
int main()
{
struct student stu1={20160101,"李磊",{90,85,80}};
showStuInfo(stu1);
return 0;
}
地址传递
#include <stdio.h>
struct student
{
int sno;
char name[20];
double grade[3];
};
void showStuInfo(struct student *p)//结构体指针作函数参数
{
printf("学号:%d\n",(*p).sno);
printf("姓名:%s\n",(*p).name);
printf("课程1:%lf\n",(*p).grade[0]);
printf("课程2:%lf\n",(*p).grade[1]);
printf("课程3:%lf\n",(*p).grade[2]);
}
int main()
{
struct student stu1={20160101,"李磊",{90,85,80}};
showStuInfo(&stu1);
return 0;
}
函数的参数是结构体变量
值传递
函数的参数是结构体指针
地址传递
链表
链表的概念
头指针 :存储的是第一个结点的首地址。
结点:链表中的每一个元素。
数据域:存储用户需要的数据
指针域:存储下一个结点的地址
- 链表 在执行过程中根据需要动态申请空间,结点的个数可以根据需要增加或减少。 占用不连续的存储空间。
- 数组 定义时必须指定数组长度,所占用的内存空间大小固定。 占用连续的存储空间。
结点的定义
struct node
{
int data;
struct node *next;
};
链表的实现
初始化链表
struct node *h,*s;
h=NULL;//建立空链表
s=(struct node*)malloc(sizeof(struct node));//建立首结点
s->next=NULL;
h =s;
在尾结点后插入新结点
p=(struct node*)malloc(sizeof(struct node));
p->data=1;
p->next=NULL;
s->next=p;
s=p;
链表的操作
p=h->next;
//指针指向第一个结点
while(p!=NULL)
{
printf("%d\n",p->data);
p=p->next; //指针指向下一个结点
}
构造数据类型-共用体
共用体类型的定义
union 结构体类型名称
{
数据类型 成员名1;
数据类型 成员名2;
…
数据类型 成员名n;
};
union data
{
int x;
double y;
char z;
};
共用体变量的定义
#include <stdio.h>
union data
{
int x;
double y;
char z;
};
int main()
{
union data d1;
printf("%d\n",sizeof(d1));
return 0;
}
编译器为共用体变量分配空间是是按照其成员中字节数最大的数目分配
共用体变量的初始化和引用
在定义共用体变量的同时,只能对其中一个成员进行初始化操作,这与它的内存分配方式是对应的。
union data
{
int x;
double y;
char z;
};
union data d1={8};
#include <stdio.h>
union data
{
int x;
char z;
};
int main()
{
union data d1;
d1.x=8;
d1.z='a';
printf("d1.x=%d\n",d1.x);
printf("d1.z=%c\n",d1.z);
return 0;
}
枚举类型
枚举类型的定义
enum 枚举类型名称
{
枚举元素列表
};
enum Season{
Spring,Summer,Autumn,Winter
};
枚举变量的定义
枚举变量的定义和结构体变量的定义类似,可以采用3种方式。本小节中仅就“先定义枚举类型,再定义枚举变量”方式进行说明。
enum Season{Spring,Summer,Autumn,Winter};
enum Season s;
s=Summer;
使用typedef声明新类型
类型定义
C语言允许用户使用typedef语句定义新的数据类型名代替已有的数据类型名。类型定义的一般形式为:
typedef 类型名 新类型名;
typedef struct student
{
int sno;
char name[20];
char classname[20];
int grade[3]
}STU;
//使用typedef只是定义了一个新的类型名称,代替已有的类型名,并没有建立一个新的数据类型。
其中,STU就是新类型名。
文件
文件概述
文件分类
什么是文件?
所谓“文件”是指一组相关数据的有序集合。这个数据集有一个名称,叫做文件名。实际上在前面的各章中我们已经多次使用了文件,例如源程序文件、目标文件、可执行文件、库文件 (头文件)等。
文件通常是驻留在外部介质(如磁盘等)上的,在使用时才调入内存中来。
- 从用户的角度看,文件可分为普通文件和设备文件两种。
普通文件是指驻留在磁盘或其它外部介质上的一个有序数据集,可以是源文件、目标文件、可执行程序。
设备文件是指与主机相联的各种外部设备,如显示器、打印机、键盘等。
- 从文件编码的方式来看,文件可分为ASCII码文件和二进制码文件两种。
ASCII文件也称为文本文件,这种文件在磁盘中存放时每个字符对应一个字节,用于存放对应的ASCII码。可读。
二进制文件是按二进制的编码方式来存放文件的。不可读。
例如, 数5678的存储形式为:
00010110 00101110
- 从文件的逻辑结构看,文件可分为流式文件和记录文件。
流式文件:由一个个字符(字节)数据顺序组成,如视频流 记录文件:有具有一定结构的记录组成,如word文件,pdf文件
本章讨论流式文件的打开、关闭、读、写、定位等各种操作。
文件指针
在C语言中用一个指针变量指向一个文件,这个指针称为文件指针。通过文件指针就可对它所指的文件进行各种操作。
定义说明文件指针的一般形式为:
FILE *指针变量标识符;
其中FILE应为大写,它实际上是由系统定义的一个结构,该结构中含有文件名、文件状态和文件当前位置等信息。在编写源程序时不必关心FILE结构的细节。
例如:FILE *fp;
fp称为指向一个文件的指针。
文件的打开和关闭
所谓打开文件,实际上是建立文件的各种有关信息,并使文件指针指向该文件,以便进行其它操作。关闭文件则断开指针与文件之间的联系,也就禁止再对该文件进行操作。
文件的打开(fopen函数)
fopen用来打开一个文件,其调用形式为:
文件指针名=fopen(文件名,使用文件方式);
其中,
“文件指针名”必须是被说明为FILE 类型的指针变量;
“文件名”是被打开文件的文件名,是字符串常量或字符串数组;
“使用文件方式”是指文件的类型和操作要求。
例如:
- FILE *fp;
fp=("file_a","r");//当前目录打开文件file_a,只“读”
FILE *fphzk
fphzk=("c:\\hzk16","rb");//二进制只读
两个反斜线“\”中的第一个表示转义字符,第二个表示根目录。
文件使用方式 | 意 义 |
---|---|
“rt” | 只读打开一个文本文件,只允许读数据 |
“wt” | 只写打开或建立一个文本文件,只允许写数据 |
“at” | 追加打开一个文本文件,并在文件末尾写数据 |
“rb” | 只读打开一个二进制文件,只允许读数据 |
“wb” | 只写打开或建立一个二进制文件,只允许写数据 |
“ab” | 追加打开一个二进制文件,并在文件末尾写数据 |
“rt+” | 读写打开一个文本文件,允许读和写 |
“wt+” | 读写打开或建立一个文本文件,允许读写 |
“at+” | 读写打开一个文本文件,允许读,或在文件末追加数据 |
“rb+” | 读写打开一个二进制文件,允许读和写 |
“wb+” | 读写打开或建立一个二进制文件,允许读和写 |
“ab+” | 读写打开一个二进制文件,允许读,或在文件末追加数据 |
(1) 文件使用方式由r,w,a,t,b,+六个字符拼成,各字符的含义是:
- r(read): 读
- w(write): 写
- a(append): 追加
- t(text): 文本文件,可省略不写
- b(banary): 二进制文件
- +: 读和写
(2) 凡用“r”打开一个文件时,该文件必须已经存在,且只能从该文件读出。
(3) 用“w”打开的文件只能向该文件写入。若打开的文件不存在,则以指定的文件名建立该文件,若打开的文件已经存在,则将该文件删去,重建一个新文件。
(4) 若要向一个已存在的文件追加新的信息,只能用“a”方式打开文件。但此时该文件必须是存在的,否则将会出错。
打开文件常用以下方法
方式一
if((fp=fopen("file","r"))==NULL)
{
printf("不能打开该文件。\n");
exit(0);
}
方式二
if((fp=fopen("c:\\hzk16","rb")==NULL)
{
printf(" error on open c:\\hzk16 file!");
getch();
exit(1);
}
文件关闭函数 (fclose函数)
文件使用完毕,用关闭文件函数把文件关闭,以避免文件的数据丢失等错误。
fclose函数调用的一般形式是:
fclose(文件指针);
例如:
fclose(fp);
正常关闭时,fclose
返回值为0。返回非零表示有错误发生。
文件的读写数据
字符读写函数
读字符函数fgetc
fgetc函数的功能是从指定的文件中读一个字符,函数调用的形式为:
字符变量=fgetc(文件指针);
例如:
ch=fgetc(fp);
其意义是从打开的文件fp中读取一个字符并送入ch中。
fgetc函数的使用有以下几点说明
(1) 读取的文件必须是以读或读写方式打开的。
(2) 读取字符的结果可以不赋值,但读出的字符不能保存。
(3)在文件内部有一个位置指针。用来指向文件的当前读写字节。在文件打开时,该指针总是指向文件的第一个字节。使用fgetc函数后,该位置指针将向后移动一个字节。因此可连续多次使用fgetc函数,读取多个字符。
文件指针和文件内部的位置指针。文件指针是指向整个文件的,须在程序中定义说明,只要不重新赋值,文件指针的值是不变的。文件内部的位置指针用以指示文件内部的当前读写位置,每读写一次,该指针均向后移动,它不需在程序中定义说明,而是由系统自动设置的。
【例】读入文件d:\cproj\myfile.txt,在屏幕上输出
#include "stdio.h"
#include "stdlib.h"
int main(){
FILE * fp;
fp = fopen("d://cproj/myfile.txt", "r");
if(fp == NULL){
printf("文件打开异常");
exit(0);
}
while(!feof(fp)){
putchar(fgetc(fp));
}
return 0;
}
写字符函数fputc
fputc函数的功能是把一个字符写入指定的文件中,函数调用的形式为:
fputc(字符量,文件指针);
其中,待写入的字符量可以是字符常量或变量
例如:fputc('a',fp);其意义是把字符a写入fp所指向的文件中。
fputc函数的使用有以下几点说明
(1)被写入的文件可以用写、读写,追加方式打开。用写或读写方式打开一个已存在的文件时将清除原有的文件内容,写入字符从文件首开始。如需保留原有文件内容,必须以追加方式打开文件。被写入的文件若不存在,则创建该文件。
(2)每写入一个字符,文件内部位置指针向后移动一个字节。
(3)fputc函数有一个返回值,如写入成功则返回写入的字符,否则返回一个EOF。可用此来判断写入是否成功。
【例】从键盘输入一行字符,写入一个文件,再把该文件内容读出显示在屏幕上。
#include "stdio.h"
#include "stdlib.h"
int main(){
FILE* fp1 = fopen("test.txt", "w+");
char ch;
while((ch = getchar()) != '\n'){
putc(ch, fp1);
}
fclose(fp1);
FILE* fp2 = fopen("test.txt", "r");
while(!feof(fp2)){
putchar(fgetc(fp2));
}
printf("\n");
fclose(fp2);
return 0;
}
【例】把命令行参数中的前一个文件名标识的文件,复制到后一个文件名标识的文件中,如命令行中只有一个文件名则把该文件写到标准输出文件(显示器)中。
#include "stdio.h"
#include "stdlib.h"
int main(int argc, char * args[]){
FILE *fp1;
if(argc >= 2){
fp1 = fopen(args[1], "r");
}
if(argc == 2){
while(!feof(fp1)){
putchar(fgetc(fp1));
}
puts("");
}
if(argc == 3){
FILE *fp2 = fopen(args[2], "w");
while(!feof(fp1)){
fputc(fgetc(fp1), fp2);
}
fclose(fp2);
}
fclose(fp1);
return 0;
}
字符串读写函数
读字符串函数fgets
功能是从指定的文件中读一个字符串到字符数组中,函数调用的形式为:
fgets(字符数组名,n,文件指针);
其中的n是一个正整数。从文件中读出不超过 n-1个字符。最后加上一个串结束标志'\0'。
例如: fgets(str,n,fp);
从fp所指的文件中读出n-1个字符送入字符数组str中。
【例】从myfile.txt文件中读入一个含10个字符的字符串。
#include "stdio.h"
#include "stdlib.h"
int main(){
FILE * fp;
char str[10];
fp = fopen("d://cproj/myfile.txt", "r");
if(fp == NULL){
printf("文件打开异常");
exit(0);
}
fgets(str, 11, fp);
puts(str);
fclose(fp);
return 0;
}
对fgets函数有两点说明:
(1)在读出n-1个字符之前,如遇到了换行符或EOF,则读出结束。
(2)fgets函数也有返回值,其返回值是字符数组的首地址。
写字符串函数fputs
fputs函数的功能是向指定的文件写入一个字符串,其调用形式为:
fputs(字符串,文件指针);
其中字符串可以是字符串常量,也可以是字符数组名,或指针变量,例如:
fputs(“abcd“,fp);
其意义是把字符串“abcd”写入fp所指的文件之中。
【例10.5】在例13.2中建立的文件string中追加一个字符串。
#include "stdio.h"
#include "stdlib.h"
int main(){
char str[] = "World";
FILE * fp = fopen("string", "a+");
fputs(str, fp);
fclose(fp);
}
数据块读写函数
C语言还提供了用于整块数据的读写函数。可用来读写一组数据,如一个数组元素,一个结构变量的值等。
读数据块函数调用的一般形式为:
fread(buffer,size,count,fp);
写数据块函数调用的一般形式为:
fwrite(buffer,size,count,fp);
其中:
buffer是一个指针,在fread函数中,它表示存放输入数据的首地址。在fwrite函数中,它表示存放输出数据的首地址。
size表示数据块的字节数。
count表示要读写的数据块块数。
fp表示文件指针。
例如:
fread(fa,4,5,fp);
其意义是从fp所指的文件中,每次读4个字节(一个实数)送入实数组fa中,连续读5次,即读5个实数到fa中。
【例】从键盘输入两个学生数据,写入一个文件中,再读出这两个学生的数据显示在屏幕上。
#include "stdio.h"
#include "string.h"
struct stu
{
char name[10];
int num;
int age;
char addr[15];
}boya[2],boyb[2],*pp,*qq;
int main(){
strcpy(boya[0].name, "小明");
boya[0].num = 1;
boya[0].age = 20;
strcpy(boya[0].addr, "武汉");
strcpy(boya[1].name, "小张");
boya[1].num = 2;
boya[1].age = 21;
strcpy(boya[1].addr, "武汉");
// 将数据写入文件
FILE *fp = fopen("data.txt", "wb");
if (fp == NULL) {
printf("打开文件失败!\n");
return 1;
}
fwrite(boya, sizeof(struct stu), 2, fp);
fclose(fp);
// 从文件中读取数据并显示在屏幕上
fp = fopen("data.txt", "rb");
if (fp == NULL) {
printf("打开文件失败!\n");
return 1;
}
fread(boyb, sizeof(struct stu), 2, fp);
printf("学生信息如下:\n");
printf("%-10s %-5s %-5s %-15s\n", "姓名", "学号", "年龄", "地址");
printf("%-10s %-5d %-5d %-15s\n", boyb[0].name, boyb[0].num, boyb[0].age, boyb[0].addr);
printf("%-10s %-5d %-5d %-15s\n", boyb[1].name, boyb[1].num, boyb[1].age, boyb[1].addr);
fclose(fp);
return 0;
}
格式化读写函数
fscanf函数,fprintf函数与前面使用的scanf和printf 函数的功能相似,都是格式化读写函数。两者的区别在于fscanf函数和fprintf函数的读写对象不是键盘和显示器,而是磁盘文件。
这两个函数的调用格式为:
fscanf(文件指针,格式字符串,输入表列);
fprintf(文件指针,格式字符串,输出表列);
例如:
fscanf(fp,"%d%s",i,s);
fprintf(fp,"%d%c",j,ch);
【例】用fscanf和fprintf函数完成上面例子的问题。
#include "stdio.h"
#include "string.h"
struct stu
{
char name[10];
int num;
int age;
char addr[15];
}boya[2],boyb[2],*pp,*qq;
int main(){
strcpy(boya[0].name, "小明");
boya[0].num = 1;
boya[0].age = 20;
strcpy(boya[0].addr, "武汉");
strcpy(boya[1].name, "小张");
boya[1].num = 2;
boya[1].age = 21;
strcpy(boya[1].addr, "武汉");
// 将数据写入文件
FILE *fp = fopen("data.txt", "w");
if (fp == NULL) {
printf("打开文件失败!\n");
return 1;
}
for (int i = 0; i < 2; i++) {
fprintf(fp, "%s %d %d %s\n", boya[i].name, boya[i].num, boya[i].age, boya[i].addr);
}
fclose(fp);
// 从文件中读取数据并显示在屏幕上
fp = fopen("data.txt", "r");
if (fp == NULL) {
printf("打开文件失败!\n");
return 1;
}
printf("学生信息如下:\n");
printf("%-10s %-5s %-5s %-15s\n", "姓名", "学号", "年龄", "地址");
for (int i = 0; i < 2; i++) {
fscanf(fp, "%s %d %d %s", boyb[i].name, &boyb[i].num, &boyb[i].age, boyb[i].addr);
printf("%-10s %-5d %-5d %-15s\n", boyb[i].name, boyb[i].num, boyb[i].age, boyb[i].addr);
}
fclose(fp);
return 0;
}
文件的定位
前面介绍的对文件的读写方式都是顺序读写,即读写文件只能从头开始,顺序读写各个数据。但在实际问题中常要求只读写文件中某一指定的部分。为了解决这个问题可移动文件内部的位置指针到需要读写的位置,再进行读写,这种读写称为随机读写。实现随机读写的关键是要按要求移动位置指针,这称为文件的定位。文件定位移动文件内部位置指针的函数主要有两个,即 rewind 函数和fseek函数。
rewind(fp),它的功能是把文件内部的位置指针移到文件首。
fseek函数用来移动文件内部位置指针,其调用形式为:
fseek(文件指针,位移量,起始点);
其中:“文件指针”指向被移动的文件。“位移量”表示移动的字节数,要求位移量是long型数据,以便在文件长度大于64KB 时不会出错。当用常量表示位移量时,要求加后缀“L”。“起始点”表示从何处开始计算位移量,规定的起始点有三种:文件首,当前位置和文件尾。
起始点 表示符号 数字表示
───────────────────
文件首 SEEK—SET 0
当前位置 SEEK—CUR 1
文件末尾 SEEK—END 2
例如:fseek(fp,100L,0);其意义是把位置指针移到离文件首100个字节处。fseek函数一般用于二进制文件。
【小提示】在文本文件中由于要进行转换,故往往计算的位置会出现错误。文件的随机读写在移动位置指针之后,即可用前面介绍的任一种读写函数进行读写。由于一般是读写一个数据据块,因此常用fread和fwrite函数。
#include "stdio.h"
#include "string.h"
struct stu
{
char name[10];
int num;
int age;
char addr[15];
}boya[2],boyb[2],*pp,*qq;
int main(){
strcpy(boya[0].name, "小明");
boya[0].num = 1;
boya[0].age = 20;
strcpy(boya[0].addr, "武汉");
strcpy(boya[1].name, "小张");
boya[1].num = 2;
boya[1].age = 21;
strcpy(boya[1].addr, "武汉");
// 将数据写入文件
FILE *fp = fopen("data.txt", "wb");
fwrite(boya, sizeof(struct stu), 2, fp);
fclose(fp);
fp = fopen("data.txt", "rb");
// 从文件中读取第二个学生的数据
fseek(fp, sizeof(struct stu), SEEK_SET);
fread(boyb + 1, sizeof(struct stu), 1, fp);
printf("第二个学生的信息如下:\n");
printf("%-10s %-5s %-5s %-15s\n", "姓名", "学号", "年龄", "地址");
printf("%-10s %-5d %-5d %-15s\n", boyb[1].name, boyb[1].num, boyb[1].age, boyb[1].addr);
fclose(fp);
return 0;
}
文件的检错与处理函数
常用的文件的检错与处理函数为ferror函数与clearerr函数:
- ferror函数
ferror函数是读写文件出错检测函数,其调用格式为:
ferror(文件指针);
功能:检查文件在用各种输入和输出函数进行读写时是否出错,如ferror返回值为0表示未出错,否则表示有错。
对同一个文件每一次调用输入和输出函数,都会产生一个新的ferror函数值,因此应当在调用一个输入或输出函数后立即检查ferror函数的值,否则信息会丢失。在执行fopen函数时,ferror函数的初始值自动置为0。
- clearerr函数
clearerr函数的作用是使文件出错标志和文件结束标志置0,其调用格式为:
clearerr(文件指针);
功能:用于清除文件出错标志和文件结束标志,使它们为0值。
假设在调用一个输入或输出函数时出现错误,ferror函数值为一个非0值,应该立即调用clearerr(fp),使ferror(fp)的值变成0,以便进行下一次的检测。
【例】下面是一个使用ferror函数和clearerr函数的示例。
#include <stdio.h>
#include <stdlib.h>
int main()
{
FILE *fp;
int c;
fp = fopen("file.txt","r");
if(fp == NULL) {
perror("Error in opening file");
return(-1);
}
while(1) {
c = fgetc(fp);
if( feof(fp) ) {
break ;
}
printf("%c", c);
}
if(ferror(fp))
printf("I/O error reading file.");
else
printf("\nEnd of file reached successfully.");
clearerr(fp); //清除出错标志和文件结束标志
if(ferror(fp))
printf("\nError flag cleared.");
fclose(fp);
return(0);
}