C语言基础
一、简介
C 语言是一种通用的、面向过程的计算机程序设计的高级语言。
特点:结构化语言,高效率,可处理底层活动,可移植性,表达方式灵活能力强,语言简洁
二、变量与常量
(一)变量
(1) 标识符有它自己的构造规则,只能由字母、数字和下划线"_"组成
(2) 数字不能出现在第一个位置
(3) 关键字(保留字)不能作为标识符
关键字(保留字):auto,break,case,char,const,continue,default,do,double,else,enum,extern,float,for,goto,if,int,long,register,return,short,signed,
sizeof,static,struct,switch,typedef,union,unsigned,void,volatile,while,inline,restrict
基本类型:int , char , float , double , void
例:price=0;将0值赋给变量price,对于变量一定要初始化
(二)常量
-
整数常量
85 – 十进制,025 – 八进制,0x4a – 十六进制
10l – 长整数,30u – 无符号整数
-
浮点常量
3.14f – float型,2.5 – double型,1.23L – long double型
-
字符常量
常用 :\f – 换页符,\n – 换行符, \r – 回车 ,\b – 退格
-
定义
const修饰符 : const int a = 10;
#define预处理器 :#define WIDTH 3
三、运算符和表达式
(一)运算符
-
算术运算符
运算符 描述 举例 + 两个操作数相加 1 + 1 =2 - 两个操作数相减 1 - 1 = 0 * 两个操作数相乘 1 * 1= 1 / 两个操作数相除 5 / 2 = 2 % 两个操作数相除取余数 5 % 2 = 1 ++ 自增,一个操作数 1++ 得到2 – 自减,一个操作数 1-- 得到0 -
关系运算符
运算符 描述 举例 != 判断两个操作数是否相等,不相等为真 (1!=2)为真 == 判断两个操作数是否相等,相等为真 (1==1)为真 > 判断左操作数是否大于右操作数,是为真 (1>2)为假 < 判断左操作数是否小于右操作数,是为真 (1<2)为真 >= 判断左操作数是否大于等于右操作数,是为真 (2>=1)为真 <= 判断左操作数是否小于等于右操作数,是为真 (1<=2)为真 -
逻辑运算符
运算符 描述 举例 && 逻辑与,两个操作数为真,返回真 true && false为假 || 逻辑或,两个操作数至少一个为真,返回真 true || false为真 ! 逻辑非,如果条件为真则返回假 !true为假 -
位运算符
运算符 描述 举例 & 按位与,按二进制位进行“与”运算 0 & 1 = 0 | 按位或,按二进制位进行“或”运算 0 | 1 = 1 ^ 异或,按二进制位进行“异或”运算 0 ^ 1 = 1,0 ^ 0 = 0 ~ 取反,按二进制位进行“取反”运算,二进制0变1,1变0 ~ 1 = -2 << 二进制左移运算符,左移若干位,右边补0 7 << 2 为28 >> 二进制右移运算符,正数左补0,负数左补1 7 >> 2 为1 一般情况下,左移n位就是原数乘以2的n次方,右移n位就是原数除以2的n次方
-
赋值运算符
运算符 描述 举例 = 右操作数赋值给左操作数 A = B += 右操作数加上左操作数结果赋值给左操作数 A +=B <=> A = A + B -= 右操作数减去左操作数结果赋值给左操作数 A -=B <=> A = A - B *= 右操作数加乘以操作数结果赋值给左操作数 A *=B <=> A = A * B /= 右操作数除以左操作数结果赋值给左操作数 A /=B <=> A = A / B %= 右操作数除以左操作数取余结果赋值给左操作数 A %=B <=> A = A % B <<= 左移且赋值 A <<=1 <=> A = A << 1 >>= 右移且赋值 A >>=1 <=> A = A >> 1 &= 按位与且赋值 A &=1 <=> A = A & 1 |= 按位或且赋值 A |=1 <=> A = A | 1 ^= 按位异或且赋值 A ^=1 <=> A = A ^ 1 -
其他运算符
运算符 | 描述 | 举例 |
---|---|---|
& | 返回变量地址 | &a,返回a的地址 |
?: | x>y? y : x,判断条件x>y,为真值为y,否则值为x | 1>2 ? true : false 值为false |
* | 指向一个变量 | *p,p指针 |
[] | 下标运算符 | a[1] ,数组a中下标为1的数 |
sizeof | 返回变量字节大小 | int a; sizeof(a) ,a=4 |
(类型) | 强制转换类型 | a为字符串,(int) a,a强制转换为整型 |
-
运算符的优先级
(1) 单目运算符 > 双目运算符 > 三目运算符
(2) {[],(),.,->} > {-,(类型),++,–,*(指针),!,&(地址),~,sizeof} > 算术运算符 > {<< , >>} > 关系运算符 > {&,|,^} > { &&,||} > ? > 赋值运算符 > ,
(二)表达式
表达式包含运算符和操作数
.
例如:a + b, 5-- ,"Hello World"等等
四、数据类型
(一)基本类型
1. 整型类型
int - 整型 , short int - 短整型 ,long int -长整型,
char - 字符型,bool - 布尔型(C99标准后新增)
2. 浮点类型
double - 双精度浮点类型 ,float - 单精度浮点类型
(二)派生类型
* -- 指针类型 , [] -- 数组类型 ,
struct - 结构体类型 , union - 共用体类型,
函数类型
(三)其他
enum - 枚举类型 , void - 空类型
五、数据的输入与输出
(一) 输入输出概念
1.所谓的输入输出是以计算机主机为主体而言的,从输入设备(如键盘、光盘等)向计算机输入数据称为输入,从计算机向输出设备(如显示屏、打印机等)输出数据称为输出。
2.对于C语言而言,它本身并不提供输入与输出语句,均是由C标准函数库中的函数来实现的,例如printf函数和scanf函数。printf和scanf并不是语言的关键字,而只是库函数的名字。
3.C程序中需在程序文件开头用预处理命令#include <stdio.h> (#include “stdio.h”),目的是把相关头文件放在C程序中。如果C程序需要调用标准输入输出函数,就需要在程序的开头就需要使用该预处理命令。
(二) 用printf函数输出数据
1.一般格式
printf(格式控制,输出表列),例如:printf("%d\n",a);
(1) 格式控制即格式字符串,由格式声明和普通字符组成。
格式声明由"%“和格式字符组成,例如”%d","%f"等,作用是将输出数据转换为指定的格式后输出。
普通字符即在输出时原样输出的字符,例如printf函数中" "中的空格,逗号等。
(2) 输出表列是程序需要输出的数据,可以是变量,表达式或者常量。
2.格式字符
在输出时,不同类型的数据需要指定不同的输出格式声明,格式声明中最重要的就是格式字符。
(1) d格式字符
指定输出数据所占的列数(域宽),如"%3d",指定输出数据占3列。
若输出long类型,则在格式字符"d"前加字母"l",即"%ld"。
int a = 5;
printf("%3d",a);
(2) c格式字符
用于输出字符和域宽,若整数在0~127范围中,也可以用该格式按字符形式输出,系统会将该整数作为ASCII码转换成相应的字符。
int a = 100;
printf("%c",a);
(3) s格式字符
用于输出一个字符串。
printf("%s","Hello C");
(4) f格式字符
用来输出实数,以小数形式输出。
-
%f,不指定输出数据长度,系统根据数据大小决定列数,整数部分全部输出,小数点后6位
-
%m.nf,指定输出数据列数和小数位数,其中m表示输出数组所占列数m列,小数占n位
double a = 5;
printf("%8.7f",a/3);
(5) e格式字符
指定以指数形式输出实数,同f格式字符一样,也可指定数据列数和小数位数。
printf("%8.7e",123.456);
(6) 其他格式字符
“%i”:以十进制形式输出
“%o”:以八进制形式输出
“%x”:以十六进制输出
(三) 用scanf函数输入数据
1.一般形式
scanf(格式控制,地址表列),例如:scanf("%d",&a);
(1) 格式控制含义同上printf函数。
(2) 地址表列是由若干个地址组成的表列,可以是变量的地址、字符串的首地址。
2.格式声明
scanf中的格式声明与printf函数中的格式声明相似,以%开始,一个格式字符结束,中间可插入附件的字符。
格式字符的用法也和printf函数中的差不多,可以直接参考printf函数的格式字符用法。
注意:在输入数据时,需输入与格式控制中相应的字符,即在对应的位置输入同样的字符,否则会报错
int a,b;
scanf("a=%d,b=%d",&a,&b);
(四) 字符输入输出函数
1.putchar函数输入字符
putchar函数的一般形式putchar(c),例如:putchar(a);
2.getchar函数输入字符
getchar函数的一般形式getchar(),getchar函数没有参数,例如:a=getchar();
六、判断结构
判断结构的一般形式:
(一) if语句
1.一般形式
(1) if (表达式) 语句 1;(无else语句)
(2) if (表达式) 语句 1 ;else 语句 2;
(3) if (表达式) 语句 1;else if 语句 2;… … … ;else if 语句 n; else 语句 n+1;
以(3)为例,代码如下,
int a;
int number = 0;
scanf("%d\n",&a);
if(a>1000){
number = 1;
}
else if(a>500){
number = 2;
}
else if(a>250){
number = 4;
}
else{
number = 8;
}
printf("%d\n",number);
2. 内嵌if
一般形式,"{}"限定了内嵌语句的范围
if (表达式)
{
if( ) 语句 1 ;
else 语句 2;
}
else
{
if( ) 语句 1 ;
else 语句 2;
}
int a ;
scanf("%d",&a);
int number = 0;
if(a>=0){
if(a == 0){
number = 1;
}
else{
number = 2;
}
}
else{
number = 4;
}
printf("%d\n",number);
(二) switch语句
1.一般形式
switch语句是多分支判断语句,当一个问题分支比较多时,可使用该语句,避免if语句的多层嵌套,提高程序的可读性。
switch(表达式)
{
case 常量1 :语句1;
case 常量2 :语句2;
case 常量3 :语句3;
… …
case 常量n :语句n;
default : 语句 n+1
}
char grade;
scanf("%c",&grade);
switch(grade)
{
case 'A':printf("90~100");break;
case 'B':printf("80~89");break;
case 'C':printf("70~79");break;
case 'D':printf("60~69");break;
default :printf("error");
}
七、循环结构
循环结构的一般形式
(一) while语句实现循环
一般形式:while(表达式) 语句
当循环条件为真时,就执行循环体语句,直至为假
int i = 1,sum = 0;
while(i < 10)
{
sum = sum + i;
i++;
}
printf("%d",sum);
(二)do…while实现循环
一般形式:do 语句 while(表达式);
先执行一次指定的循环体语句,然后判别表达式,当表达式为真时,返回循环体重新执行语句,直至为假
int i = 1,sum = 0;
do
{
sum = sum + i;
i++;
} while(i < 10);
printf("%d",sum);
(三) for语句实现循环
一般形式:for(表达式1;表达式2;表达式3) 语句
表达式1:设置初始条件,只执行一次,可为零至多个变量设置初值
表达式2:循环条件表达式
表达式3:循环的调整,循环体语句每次执行后再进行
int sum = 0;
for(int i = 1;i < 10;i++)
{
sum += i;
}
printf("%d",sum);
return 0;
若for语句中三个表达式留空,将会无限循环。
(四) 循环执行状态的改变
1.break语句
break语句可以使流程从循环体内跳出循环体,提前结束循环
while(a >= 0)
{
if(a == 15)
{
break;
}
printf("%d",a)
}
2.continue语句
continue语句可提前结束本次循环,而接着执行下次循环
int n;
for(n = 1;n <= 100;n++)
{
if(n%3 == 0)
{
continue;
}
printf("%d",n)
}
3.区别
break语句直接结束整个循环过程,不再去判断执行前循环条件是否成立;而continue语句只结束本次循环,而不是终止整个循环的执行。
如果是双重循环,在内循环体内有个break语句,则将提前终止内循环。
八、函数
(一) 函数的定义
1.无参函数
类型名 函数名( ) /函数名( void )
{
函数体
}
void message()
{
printd("Hello C\n");
}
2.有参函数
类型名 函数名( 形式参数表列)
{
函数体
}
int max(int x,int y)
{
int z;
z = x > y ? x > y;
return z;
}
3.空函数
类型名 函数名()
{
}
void x()
{
}
(二) 函数的声明与调用
1.函数声明
函数声明会告诉编译器函数名称以及如何调用函数
用户在一个函数中调用另一个自己定义的函数时需要在该主函数中对被调用的函数作声明
用自己定义的函数首部‘(函数原型)来作为函数声明即可
int main()
{
int add(int x,int y);
... ...
return 0;
}
2.函数调用
(1) 形参与实参
实际参数(实参):在一个函数中调用一个函数时函数名后括号内的参数
形式参数(形参):定义函数时函数名后括号内的参数
在调用函数的过程中,系统会把实参的值传递给被调用函数的形参。
(2) 调用过程
在定义函数时指定形参,在未出现函数调用的情况下,形参不占用内存;
发送函数调用时,系统临时为被调用函数分配内存;
将实参的值传给形参后,函数开始执行,由于形参已有值,则利用形参进行有关运算;
然后通过return语句将函数值带回主函数;
调用结束以后,形参分配的内存被释放。
(3) 函数的返回值
函数的返回值通过return语句带回主函数,如果不需要从被调函数带回函数值则可不要return;
在定义函数时需要指定函数值的类型,并且指定类型应当与return语句中的表达式类型一致,即函数类型决定返回值得类型;
对于不带回值的函数应当定义为void类型
(4)调用类型
- 传值调用
把参数实际值传递给形参
- 引用调用
通过指针传递方式,形参是指向实参地址的指针,
(三) 函数的递归调用
在调用一个函数的过程又出现直接或间接调用函数本身,则称之为函数的递归调用
int f(int x)
{
int y,z;
z = f(y);
return (2*z);
}
(四) 局部变量与全局变量
从变量的作用域可分为局部变量和全局变量
1.局部变量
在函数内部或块内部定义的变量为局部变量,函数的形式参数也被作为是函数内的局部变量。
2.全局变量
在所有函数外定义的变量为全局变量,它的有效范围是从定义到源文件结束。
3.在内存中的区别
- 全局变量保存在内存的全局存储区中,占用静态的存储单元;
- 局部变量保存在栈中,只有在所在函数被调用时才动态地为变量分配存储单元。
(五) 变量的存储方式
从变量的生存期可分为静态存储方式和动态存储方式
用户区(内存中供用户使用的存储情况) |
---|
程序区 |
静态存储区 |
动态存储区 |
1.动态存储方式
动态存储方式指在程序运行期间根据需求进行动态的分配存储空间的方式
2.静态存储方式
静态存储方式指在程序运行期间由系统分配固定的存储空间的方式
3.数据存放
全局变量全部存储在静态存储区中,程序执行完就释放
动态存储区:
- 形参
- 函数中定义的无关键字static声明的变量(自动变量)
- 函数调用时的现场保护和返回地址等
以上在调用函数时分配存储空间,函数结束时释放
4.局部变量的存储类别
C语言中每个变量和函数都有两个属性:数据类型和数据的存储类别
(1) 自动变量(auto)
函数的局部变量若无static存储类别声明,都是动态的分配在存储空间的,这类就为自动变量
自动变量关键字auto可以省略,在函数定义中没有声明的默认为auto
(1) 静态局部变量(static)
函数中的局部变量的值在调用后继续保留原值,函数调用结束后不释放所分配的存储空间,下次调用时,该变量的值为上次函数调用结束时的值。
属于静态存储类别,在静态存储区分配单元
int main()
{
int f(int a);
int a = 1;
for(int i = 0;i < 3;i++)
{
printf("%d\n",f(a));
}
return 0;
}
int f(int a)
{
int b = 0; //自动变量
static c = 2; //静态局部变量
b++;
c++;
return (a+b+c);
}
调用次数 | 调用时初值b | 调用时初值c | 调用结束时b | 调用结束时c | 调用结束时a+b+c |
---|---|---|---|---|---|
1 | 0 | 2 | 1 | 3 | 5 |
2 | 0 | 3 | 1 | 4 | 6 |
3 | 0 | 4 | 1 | 5 | 7 |
(3) 寄存器变量(register)
当程序中用到变量的值时,由控制器发出指令将内存中该变量的值送到运算器中
寄存器只用于需要快速访问的变量,寄存器的存储速度远高于对内存的存储速度,可以提高执行效率
5.全局变量的存储类别
用extern声明外部变量
- 可扩展外部变量在程序文件中的作用域
- 将外部变量的作用域扩展到其他文件
- 将外部变量的作用域限制在本文件中(需在文件中对全局变量声明static,另一文件声明extern)(静态外部变量)
(六) 内部函数和外部函数
1.内部函数(静态函数)
若一个函数只能被本文件中的其他函数调用则为内部函数,定义函数时前面加上static
static 类型名 函数名(形参表列)
2.外部函数
在定义函数时,在函数首部的最左边加上关键字extern则为外部函数,可以被其他文件调用
extern 类型名 函数名(形参表列)
extern int hello(int x, int y)
此时hello函数可为其他文件调用,若定义是省略extern ,则默认为外部函数
九、数组
(一) 一维数组
1.定义
类型符 数组名[ 常量表达式]
C语言中不允许对数组大小作动态定义
2.引用
数组名 [下标],下标从0开始
3.初始化
-
在定义数组时对全部数组元素赋予初值
int a[10] = {0,1,2,3,4,5,6,7,8,9};
-
可只给部分数组元素赋值,剩余自动赋值为0
int a[10] = {0,1,2,3,4};
-
可使一个数组中元素全为0
int a[10] = {0,0,0,0,0,0,0,0,0,0};
-
若数组个数确定,可不指定数组长度,系统会根据括号内数据个数确定长度
int a[] = {1,2,3,4,5}
4.冒泡法
每次比较相邻的两个数,将小的排在前面
int a[10] = {2,3,25,14,78,54,34,42,16,30};
int i,j,t;
for(j = 0;j < 9;j++)
{
for(i = 0;i < 9 - j;i++)
{
if(a[i] > a[i+1])
{
t = a[i];
a[i] = a[i+1];
a[i+1] = t;
}
}
}
printf("结果为:\n");
for(i = 0;i < 10;i++)
{
printf("%d ",a[i]);
}
(二) 二维数组
1.定义
可把二维数组写成行和列的排列方式,按行存放
类型符 数组名[常量表达式] [常量表达式];
逻辑上可用矩阵形式表示二维数组;在内存中,各元素是连续存放的,不是二维的,是线性的
2.引用
数组名[下标] [下标]
3.初始化
- 分行给二维数组赋值
int a[2][3] = {{1,2,3},{4,5,6}};
- 所有元素写在一块,按数组元素在内存中的排列顺序对各元素赋值
int a[2][3] = {1,2,3,4,5,6};
- 可对部分元素赋值
int a[2][3] = {{1},{2,3}};
(三) 字符数组
1.定义和初始化
与数值型数组类似
//举例
char c[9];
char c[9] ={'I','','L','o','v','e','','C','\0'};
2.字符串
在C语言中,是将字符串作为字符数组来处理的
字符串结束标志:‘\0’,遇到’\0’时,表示字符串结束,它前面的组成一个字符串,也是有效字符
字符串在向内存存储时,系统自动在最后一个字符后加上’\0’作为结束标志
3.输入与输出
(1) 逐个字符输入输出,用格式符"%c"
(2) 将整个字符串一次输入输出,用格式符"%s",用"%s"输出字符串时,输出项应是字符数组名,而不是数组元素名
4.字符串处理函数
使用字符串处理函数时应在程序文件开头用#include <string.h>
(1) puts函数—输出字符串
一般形式:puts(字符数组)
(2) gets函数—输入字符串
一般形式:gets(字符数组)
(3) strcat函数—字符串连接
一般形式:strcat(字符数组1,字符数组2)
(4)strcpy和strncpy函数—字符串复制
一般形式:strcpy(字符数组1,字符串2)
- 将字符串2复制到字符数组1中
- 字符数组1必须写成数组名形式,字符串2可以是字符数组名或者字符串常量
- 不能用赋值语句直接将一个字符串常量或字符数组直接赋给一个字符数组
(5)strcmp函数—字符串比较
一般形式:strcmp(字符串1,字符串2)
比较规则:两个字符串自左向右逐个字符按ASCII码比较
比较结果由函数值带回
- 字符串1=字符串2,返回 0
- 字符串1>字符串2,返回正整数
- 字符串1<字符串2,返回负整数
(6)strlen函数—字符串长度
一般形式:strlen(字符数组)
(6)strlwr函数—字符串转换为小写
一般形式:strlwr(字符串)
(6)strupr函数—字符串转换为大写
一般形式:strupr(字符串)
(四) 数组作为函数参数
1.数组元素作函数实参
数组元素可作元素函数实参,但不能作为形参
数组是一个整体,在内存中占连续的一段存储空间,形参是在函数被调用时临时分配存储空间的,不可能为一个数组元素单独分配存储空间
2.一维数组名作函数参数
数组名作实参时,向形参传递的是数组首元素的地址
形参数组可以不指定大小,在定义数组时在数组名后面跟一个空的方括号
int add(int array[])
3.多维数组名作函数参数
可作函数的实参和形参,在被调用函数中对形参数组定义时可以省略一维的大小,必须指定二维(列数)的大小
十、指针
(一) 什么是指针
1.访问
直接访问:通过直接按变量名字进行的访问
间接访问:将变量a的地址存放在另一变量中,然后通过该变量来找到变量a的地址,从而访问a变量
(对变量的访问都是通过地址来进行的)
2.指针概念
指针是一个编程语言中的一个对象,利用地址找到所需的变量单元,即地址指向变量单元,通过指针可以找到以它为地址的内存单元
简单来说,指针就是地址
3.野指针
(1) 概念:指针指向的位置是不确定的
(2) 造成原因
- 指针未初始化
- 指针越界访问
- 指针指向的空间释放(即指针指向的空间被释放了,不属于自己)
野指针是我们需要去避免的
(二) 指针变量
1.概念
存放地址的变量就是指针变量,指针变量的值就是地址
一般形式:类型名 *指针变量名
int a = 1;//定义变量a并赋值
int *p = &a;//使用操作符&取出变量a的地址,存放在p变量中,p就是一个指针变量
2.指针大小
指定大小由CPU寻址位数决定,而不是字长
它的大小是固定的,与指向对象的类型无关
在32位编译器指针大小占4字节,在64位编译器指针大小占8字节
printf("%d\n",sizeof(int*));
printf("%d\n",sizeof(char*));
printf("%d\n",sizeof(double*));
printf("%d\n",sizeof(short*));
printf("%d\n",sizeof(float*));
3.指针变量类型的意义
int a = 0X11223344;
int *pa = &a;
char *pc = &a;
printf("%p\n",pa);
printf("%p",pc);
通过测试,我们发现,我们把变量a的地址存放在两个不同类型的指针变量中,发现都可以存放,那么地址的类型是否就没有意义了呢?
int main()
{
int a = 0x11223344;
int* pa = &a;
*pa = 0;
return 0;
}
进行调试时,pa存放了a的地址,地址中存放了一定的值,因为为int型,所以占4个字节,*pa通过地址将值改为0
若将pa的类型改为char * ,*pa通过地址只能将值中的“44”改为“00”,因为char *型决定了它只能访问1个字节
int a = 0x11223344;
int* pa = &a;
printf("%p\n", pa);
printf("%p\n", pa+1);
我们发现,指针变量的类型有如下意义
- 指针类型决定了指针向前或向后走一步有多大距离(即指针变量加一减一整型)
- 指针类型决定了对指针解引用有多大的权限(即能访问字节的大小)
4.指针变量作为函数参数
当函数的参数类型是指针类型的时候,它的作用就是将一个变量的地址传到另一个函数中
接下来一个例子来进行说明,对两个输入整数按大小输出
#include <stdio.h>
int main()
{
void compare(int *p1,int *p2);
int *pointer1,*pointer2;
int a,b;
printf("请输入两个整数:\n");
scanf("%d,%d",&a,&b);
pointer1 = &a;
pointer2 = &b;
if(a<b){
compare(pointer1,pointer2);
}
printf("max = %d,min = %d",a,b);
return 0;
}
void compare(int *p1,int *p2)
{
int temp;
temp = *p1;
*p1 = *p2;
*p2 = temp;
}
本题中,将两个指向整型变量的指针作为函数实参传递给compare函数的形参指针变量,在函数中通过指针实现两个变量值的交换
(三)指针引用数组与字符串
1.数组元素的指针
一个变量有一个地址,一个数组包含若干个元素, 每个元素在内存中占用存储单元,都要相应的地址。指针变量也可以指向数组元素,在C语言中,数组名代表数组中的首元素的地址。
p = &a[0] <=> p = a //两种等价
一个指针变量指向一个数组元素
2.数组元素的指针运算
(1) 指针加减整数
指针变量p已经指向一个数组中的一个元素,p+1指向下一个数组元素,p-1指向上一个数组元素
执行时是将p的值加上一个数组元素所占用的字节
*(p+i) <=> *(a+i) <=> a[i] //数组名a,指针p,三者等价
C编译系统会将a[i]转换为*(a+i)来计算,即先计算元素地址
(2) 自加或自减运算
若p原来指向a[0],指向++p后值改变了,在p原来的基础上加上指针类型的字节,p就指向数组下一个元素a[1]
(3)两个指针相减
当两个指针都指向同一数组中的元素时,结果为两个指针的值之差(地址之差)除以数组元素的长度,得到相差元素个数
两个地址不能相加,无实际意义
int *p1,*p2;
int a[9] = {1,2,3,4,5,6,7,8,9};
p1 = &a[2];
p2 = &a[4];
printf("%d",p2 - p1);
3.数组名作为函数参数
若有一个实参数组,想要在函数中改变数组中元素的值,实参与形参对应关系有下四种情况(地址的传递)
int a[10],*p = a;
- 形参与实参都是数组名 ,调用期间,形参数组和实参数组共用一段内存单元
int f(int x[],int n) //函数 f(a,10)//调用
- 实参数组名,形参指针变量,x为int*型指针变量,通过x值的改变,可指向数组a中任一元素
void f(int *x,int n) //函数 f(a,10)
- 形参与实参都是指针变量
void f(int *x,int n)//函数 f(p,10)
- 实参为指针变量,形参为数组名
void f(int x[],int n)//函数 f(p,10)
4.指针引用二维数组
(1) 多维数组有关指针
设一个二维数组
int a[3][4] = {{2,4,6,8},{10,12,14,16},{18,20,22,25}};
形式举例
形式 | 描述 | 值 |
---|---|---|
a | 二维数组名,指向一位数组0行首元素a[0] | 2000 |
a[0],*(a+0), *a | 0行0列元素地址 | 2000 |
a+1,&a[1] | 1行起始地址 | 2016 |
*(a +1),a[1] | 1行0列元素地址 | 2016 |
a[1]+2,*(a+1)+2,&a[1] [2] | 1行2列元素地址 | 2024 |
*(a[1]+2), *( *(a+1)+2),a[1] [2] | 1行2列元素的值 | 元素值14 |
看完这个表格,可能会存在一些疑问,以下的解释说明能更好地帮组你去理解这些概念
从二维数组的角度来看,a代表二维数组首元素的地址,但是此时的首元素并不是一个简单的整型类型数组元素,而是一个由4个数组元素组成的一维数组,即a代表的是首行的地址或者说是数组0行的地址
a[i]性质的区分:
- 若a是一个一维数组,则a[i]是一个有确定地址的存储单元,表示序列号为i的数组元素
- 若a是一个二维数组,则a[i]是一维数组名,它不作为一个存储单元也不代表一个存储单元的值,只是一个地址
在二维数组a[i] [j]中,不能简单地把&a[i]理解为a[i]元素的存储单元的地址,上面也提到过,a[i]不作为存储单元,所以根本不存在一个这样的实际的数据存储单元。它只作为一种地址的计算方法,能够得出第i行的起始地址,因此&a[i]和a[i]的值时相等的,只是它们的基类型不一样。
并且,&a[i]或a+i指向行,而a[i]或*(a+i)指向列。当j为0时,&a[i]和a[i] (a[i]+j)的值相等,或者说是纯地址相等,只是指向对象的基类型不同。
*(a+1)是a[i]的另一种表现形式,可以参照前面a[i]来进行理解。
(2) 指向多维数组元素的指针变量
指向数组元素的指针变量:
对于a[i] [j],求相对于开头位置a[0] [0]的相对位置:i * m + j
指向一维数组的指针变量:
例:int (*p)[4] ,表示( *p)有4个元素,每个元素都是整型,即p指向的是一个由4个整型元素组成的一维数组,p的类型为int ( * )[4]型
*( *(p + i) + j )为a[i] [j]的值
int a[2][3] = {1,2,3,4,5,6};
int (*p)[3],i,j;
p = a;
printf("please input\n");
scanf("%d,%d",&i,&j);
printf("a[%d,%d]=%d\n",i,j,*(*(p+i)+j));
(3)指向数组的指针作为函数参数
例:在一个班级里,有4个学生,每个学生学4门课,求总平均分(总分数/分数数量)及第n个学生的成绩
#include <stdio.h>
//在一个班级里,有4个学生,每个学生学4门课,
//求总平均分及第n个学生的成绩
int main()
{
void average(float* p, int n);
void search(float(*p)[4], int n);
float a[4][4] = { {62,64,66,68},{72,74,76,78},{82,84,86,88},{92,94,96,98} };
average(*a, 16);
search(a, 3);
return 0;
}
//求平均成绩函数
void average(float* p, int n)
{
float* p_end;
p_end = p + (n - 1);
float sum = 0, aver = 0;
for (; p <= p_end; p++)
{
sum = sum + (*p);
}
aver = sum / n;
printf("average=%5.2f\n", aver);
}
//求第n个学生成绩函数
void search(float(*p)[4], int n)
{
int i = 0;
printf("The No.%d of student's score:\n", n);
for (i = 0; i < 4; i++)
{
printf("%5.2f ", *(*(p + n) + i));
}
printf("\n");
}
5.通过指针引用字符串
(1)字符串的引用方式
- 用字符指针变量指向一个字符串常量,通过字符指针变量引用字符串常量
int main()
{
char* string = "I Love C!";
printf("%s\n", string);
return 0;
}
此时,string指向的是字符串的第一个字符,格式符%s在输出项中给出字符指针变量string,则系统会输出string中的第一个字符,然后自动使string+1,直到遇到结束标志符“\0”
- 用指针变量来处理将a数组中的字符逐个复制到b数组中的问题
#include <stdio.h>
int main()
{
char a[] = "Hello C!", b[20], * p1, * p2;
p1 = a;
p2 = b;//p1,p2分别指向a,b数组中的首元素
for (; *p1 != '\0'; p1++, p2++)
{
*p2 = *p1;//p1指向元素值赋给p2
}
*p2 = '\0';//复制完全后加结束字符
printf("string a : %s\n", a);
printf("string b : %s\n", b);
return 0;
}
(2) 使用字符指针变量和字符数组的比较
用字符数组和字符指针变量都能实现字符串的存储与运算,两者也是有一定的区别的
- 字符数组由若干个元素组成,每个元素只能存放一个字符,而字符指针变量存放的是字符串第一个字符的地址
- 赋值方式:可以对字符指针变量赋值,不能对数组名赋值
char a[14];
a[0] = 'I';//合法
a = "I Love C";//非法
- 初始化:数组可以在定义时对各元素赋初值,但不能用赋值语句对字符数组中全部元素整体赋值
//字符指针变量
char *a = "Hello";
//等价于
char *a;
a = "Hello";//把字符串第一个元素地址赋给a
//字符数组
char str[6] = "Hello";
//不等价于
char str[6];
str[] = "Hello";//企图把字符串赋给数组各个元素,错误
- 存储单元:编译时,为字符数组分配若干个存储单元,对于字符指针变量只分配一个存储单元
- 指针变量的值是可以改变的,而字符数组名代表着一个固定的值,即数组首元素的地址
- 字符数组中各个元素的值是可改变的(可再赋值),但字符指针变量指向字符串常量中的内容是不可变的(不可再赋值)
char a[] = "Hello";//初始化数组a
char *b = "Hello";//指针指向字符串常量的第一个字符
a[1] = 'o';//合法,'o'取代a[1]的原值
b[1] = 'o';//非法
- 引用数组元素:数组直接用下标法或者地址法引用,若定义了字符指针变量并指向了数组a的首元素,则也可以使用下标法或者地址法
char *a = "Hello";//a[2]按*(a+2)处理,结果为'l' //虽然未定义数组a,但字符串在内存中是以字符数组的形式存放的
- 用指针变量指向一个格式字符串,则可以用它代替printf函数中的格式字符串
char *format;
format = "a=%d,b=%f\n";
printf(format,a,b);
//相当于printf("a=%d,b=%f\n",a,b);
//用字符数组实现
//char format[] = "a=%d,b=%f";
//printf(format,a,b);
因此只要改变指针变量format所指向的字符串,就可以改变输入输出的格式,这种printf函数就就叫做可变格式输出函数
(四) 指向函数的指针
1.概念
若定义了一个函数,在编译时会把函数的源代码转换为可执行代码并分配一定内存,内存空间里有一个起始地址,即函数的入口地址,每次调用函数时均从该地址入口执行。
函数名就是函数的指针,也代表着函数的起始地址。
指向函数的指针:指针指向函数,用于存放某一函数的起始地址
2.定义与使用
一般形式:类型名 (*指针变量名)(函数参数表列)
- 在一个程序中,一个指针变量可以先后指向同类型的不同函数
- 对于指向函数的指针变量是不能进行算术运算的
举例:输入两个整数,让用户选择1或2,调用不同的函数,输出二者中的大数或小数
int main()
{
int max(int x, int y);
int min(int x, int u);
int (*p)(int, int);
int a, b, n,c;
printf("please input two figures:\n");
scanf("%d,%d",&a,&b);
printf("please choose 1 or 2:\n");
scanf("%d",&n);
printf("a=%d, b=%d\n", a, b);
if (n == 1)
{
p = max;
c =(*p)(a, b);
printf("max=%d\n", c);
}
if(n == 2)
{
p = min;
c = (*p)(a, b);
printf("min=%d\n", c);
}
return 0;
}
int max(int x, int y)
{
int z;
if (x > y)
z = x;
else
z = y;
return z;
}
int min(int x,int y)
{
int z;
if (x < y)
z = x;
else
z = y;
return z;
}
3.指向函数的指针做函数参数
把函数的入口地址作为参数传递到其他函数
我们在上例的基础上来进行说明,定义一个fun函数,在fun函数中调用函数
int fun(int x, int y,int (*p)(int,int))
{
int result;
result = (*p)(x,y);
printf("%d\n",result);
}
若在每次调用fun函数时,调用的函数不是固定的,用这种方式就比较方便。只需在每次调用fun函数时给出不同的函数名作为函数参数即可,fun函数不作修改
4.返回指针值的函数
函数一般形式:类型名* 函数名(函数表列);
指针返回类型为定义函数时函数的类型
#include <stdio.h>
int main()
{
float score[][3] = { {67,57,89},{90,78,66}};
float* search(float(*p)[3], int n);
float* p;
int i, k;
printf("input number\n");
scanf("%d\n",&k);
printf("The scores of No.%d are:\n", k);
p = search(score, k);
for (i = 0; i < 3; i++)
{
printf("%5.2f\t",*(p+i));
}
return 0;
}
float* search(float(*p)[3], int n)
{
float* pt;
pt = *(p + n);
return (pt);
}
(五) 指针数组与多重指针
1.指针数组
指针数组:一个数组它的元素均为指针类型数据
一般形式:类型名 *数组名[数组长度];
指针数组一般适用于指向若干个字符串,使字符串处理更加灵活
指针数组中的元素只能存放地址,不能存放实数
例:定义一个指针数组并初始化,利用选择法排序,不移动字符串,而是改变指针数组各元素的指向
#include <stdio.h>
int main()
{
void sort(char *lession[],int n);
char *lession[] = {"python","java","c++","assembly language"};
int n = 4;
int i;
sort(lession,n);
printf("排序结果:\n");
for(i = 0;i < 4;i++)
{
printf("%d\n",lession[i]);
}
return 0;
}
void sort(char *lession[],int n)
{
char *t;
int i,j,k;
for(i = 0;i < n - 1;i++)
{
k = i;//记录元素位置
for(j = i + 1;j < n;j++)//与后面的数进行比较
{
if(strcmp(lession[k],lession[j])>0)
{
k = j; //交换后的位置
}
}
if(k != i)
{
t = lession[i];
lession[i] = lession[k];
lession[k] = t;
}
}
}
将printf函数中的格式符换成’%d’,即可输出各字符串地址的排序
2.指向指针数据的指针变量
例:char **p;
*运算符的结合性是从右到左,因此**p相当于 * (*p),即表示指针变量p指向了一个char *型的数据
int a[4] = {1,2,3,4};
int *num[4] = {&a[0],&a[1],&a[2],&a[3]};
int **p,i;
p = num;
for(i = 0;i < 4;i++)
{
printf("%d\n",**p);
p++;
}
3.指针数组做main函数的形参
main函数默认是无参的,有参的一般表示形式为
int main(int argc,char *argv[])
argc是参数的个数,argv是参数向量,是一个char*的指针数组,接收系统命令行传来的字符串首字符的地址,形参类型均是固定的。
形参的值不可能在程序中得到,main函数时操作系统调用的,实参只能由操作系统给出。在操作系统命令状态下,实参时和执行文件的命令一起给出的。
命名行命令一般形式:命令名(可执行文件名,包括盘符、路径) 参数1 参数 2 …参数n(命令名与各参数之间用空格分隔)
在VS中可以直接操作:调试->(自己项目)调试属性->配置属性->调试->命令参数
(六) 动态分配
1.概念
全局变量是分配在内存的静态存储区,非静态的局部变量是分配在内存的动态存储区,这个区域称为栈。
建立内存动态分配区域来存放一些临时数据,随时需要随时开辟,不需要时则释放,这种自由存储区称为堆。
注意:因为堆中的数据没有在声明部分定义,所以不能通过变量名或者数组名去引用,只能通过指针来引用
2.建立内存的动态分配
(1) 用malloc函数开辟动态存储区
void *malloc(unsigned int size);
它的作用是在内存的动态存储区中分配一个长度为为size的空间,不指向任何类型数据,只提供纯地址,返回值是分配区域第一个字节的地址
若函数未能成功地执行(例如内存空间不足等),则返回空指针(NuLL)
(2) 用calloc函数开辟动态存储区
void *calloc(unsigned n,unsigned size);
它的作用是在内存的动态存储区中分配一个长度为为size的空间,这个空间足够大,能保存一个数组
可以为一维数组开辟动态存储空间,n为数组个数,每个元素长度为size,就是动态数组
返回指向分配区域的第一个字节的指针,分配不成功,则返回空指针(NuLL)
(3)用realloc函数重新分配动态存储区
void *realloc(void *p,unsigned int size);
如果已经用malloc或calloc函数获得了动态空间,则可以用该函数重新分配大小
将p所指向的动态空间大小改为size,p的值不变,分配不成功,则返回空指针(NuLL)
(4)用free函数释放动态存储区
void free(void *p);
释放p指向的动态空间,使该空间能重新被其他变量使用
p是最近一次调用malloc或calloc函数时得到的函数返回值,该函数无返回值
以上4个函数均声明在stdlib.h头文件中
3.void指针类型
在C99标准中允许使用void类型,它指向空类型或者不指向确定的类型,在将它的值赋给另一指针变量时由系统根据需要进行合适的类型转换,在程序中它是过渡性的,只有转换为有指向的地址,才能存储数据
那么什么情况下会用到这种类型呢?这种情况会在在调用动态存储分配函数时出现。用户在用函数开辟动态存储区时,显然希望获得此动态存储区的起始地址,以便利用该动态存储区。
int a = 5;
int *p1 = &a;
char *p2;
void *p3;
p3 = (void*)p1;//将p1的值转换为void*型,然后赋值给p3
p2 = (char*)p3;
printf("%d",*p1);
举例:用malloc函数来开辟一个动态自由区域,用来存6个学生的成绩,用int型的指针指向动态数组的各元素,并输出值
#include <stdio.h>
#include <stdlib.h>
int main()
{
void grade(int* p);
int* pt, i;
pt = (int*)malloc(6 * sizeof(int));
printf("请输入6个学生成绩:\n");
for (i = 0; i < 6; i++)
{
scanf("%d,", pt + i);
}
grade(pt);
return 0;
}
void grade(int* p)
{
int i;
printf("不合格:\n");
for (i = 0; i < 6; i++)
{
if (p[i] < 60)
{
printf("%d\n", p[i]);
}
}
}
在程序中没有定义数组,而是开辟了一段动态存储空间作为动态数组。在调用malloc函数时返回值是void*型的,要把它赋值给pt,应先进行类型转换,把该指正转换成int *型。按照地址法根据输入的6个成绩,计算出相应的存储单元的地址,分别赋值给动态数组的6个元素.指针变量pt,pt+1…pt+6依次指向各个整型数据,调用grade函数时把pt的值传给形参,形参p也就指向动态数组中的第一个元素,相当于形参和实参共用一段动态分配存储区。
十一、用户自己建立数据类型
(一) 结构体
1.建立结构体类型
结构体:允许用户自己建立由不同数据类型组成的组合型的数据结构,有时也称作“记录”,它与系统提供的标准类型具有相似的作用,都可以定义变量
声明结构体类型的一般格式:struct 结构体名(结构体标记) { 成员表列};
struct Student
{
//结构体成员
char name[30];
int id;
char sex;
int age;
};//注意最后有个分号
成员可以属于另一个结构体
struct Date
{
int year;
int month;
int day;
};
struct Teacher
{
char name[30];
int id;
char sex;
int age;
struct Date birthday;//属于struct Date类型
char class[30];
};
2.定义结构体变量
结构体类型虽然建立了,但是并没有定义变量,在定义好的结构体类型中并无具体数据,系统对它也不分配存储单元。为了能够在程序中继续使用结构体类型的数据,应当定义结构体类型的变量,并存放具体的数据。
(1) 先声明结构体类型,再定义该类型变量
这里我们用前面已经声明的结构体类型struct Student,该类型所占字节为结构体类型内所有数据类型相加
struct Student student;//定义struct Student类型的变量
这种方式是声明类型和定义变量相分离,在声明类型后可随时定义变量,比较灵活
(2)在声明结构体类型时同时定义变量
定义的一般形式:struct 结构体名 { 成员表列 } 变量名表列;
struct Student
{
//结构体成员
char name[30];
int id;
char sex;
int age;
} student1,student2;
这种方式比较直观,能直接看到结构体的结构,适用于小程序,大程序要求两者分离,便于维护
(3)不指定类型名而直接定义结构体类型变量
一般形式:struct { 成员表列 } 变量名表列;
因为是无名的结构体类型,所以无法以此结构体类型去定义变量
结构体变量与结构体类型区别:
1.在编译时,对类型不分配空间,只对变量分配空间
2.结构体类型的成员名可以与程序中的变量名相同,但二者不代表同一对象
3.结构体的变量中的成员可单独引用
3.结构体变量的初始化与引用
(1) 在定义结构体变量时可以对它的成员进行初始化
C99标准允许对某一成员进行初始化,其他未指定成员中数值型的初始化为0,字符型为’\0’,指针型为NULL
struct Student
{
//结构体成员
char name[30];
int id;
char sex;
int age;
}student = {"张三",1001,'男',22};
(2) 引用结构体变量中成员的值
引用方式:结构体变量名.成员名
student.age = 22;
不能通过输出变量名来输出其所有成员的值,只能对结构体变量中的各个成员分别输出
(3) 若成员本身又属于一个结构体类型,则要一级一级地找到最低的一级成员
只能对最低级的成员进行赋值、存储及运算
teacher.birthday.year = 2001;
(4)结构体变量的成员可根据其类型进行可行的运算
(5)同类的结构体变量可相互赋值
(6)可以引用结构体变量及其成员的地址
scanf("%d",&teacher.id);
printf("%o",&teacher);
(二) 结构体数组
结构体数组中的每个元素都是一个结构体类型,每个结构体类型都包含着各自的成员项
定义结构体数组的一般形式:
- struct 结构体名 {成员表列} 数组名[数组长度];
- 结构体类型 数组名[数组长度];(需先声明一个结构体类型)
数组初始化:在定义数组的后面加上 = {初值表列};
struct Student student[3] = {"chen",22,"qin",21,"huang",23};
举例:有n个学生,按成绩高低输出各学生的信息
#include <stdio.h>
struct Student//声明结构体类型
{
int id;
char name[10];
int age;
float score;
};
int main()
{
struct Student stu[3] = { {1,"chen",21,98},{2,"wang",20,60},{4,"qin",19,87.5}};//定义结构体数组并初始化
struct Student t;
int i, j, k;
printf("排序为:\n");
for (i = 0; i < 2; i++)
{
k = i;
for (j = i + 1; j < 3; j++)
{
if (stu[j].score > stu[k].score)//成绩比较
k = j;
}
t = stu[k];//交换位置
stu[k] = stu[i];
stu[i] = t;
}
for (i = 0; i < 3; i++)
{
printf("%d %4s %d %2.2f\n", stu[i].id, stu[i].name, stu[i].age, stu[i].score);
}
return 0;
}
(三) 结构体指针
结构体指针就是指向结构体变量的指针,一个结构体变量的起始地址就是这个结构体变量的指针。
1.指向结构体变量的指针
指针变量的基类型必须与结构体变量的类型相同
struct Student *pt;//pt指向struct Student类型的变量或数组元素
如果p指向一个结构体变量,以stu为例,以下3种表示方法是等价的
1.stu.成员名,例stu.id
2.( * p).成员名,例( * p).id)
3.p->成员名,,例p->id
2.指向结构体数组的指针
可以用指针变量指向结构体数组的元素
#include <stdio.h>
struct Student
{
int id;
char name[10];
int age;
int sex;
};
struct Student stu[3] = { {1001,"zhang",18,'M'},{1002,"wang",21,'M'},{1003,"chen",20,'F'}};
int main()
{
struct Student* p;
printf("id name age sex \n");
for (p = stu; p < stu + 3; p++)//p指向stu数组的首元素
{
printf("%d %s %4d %5c\n",p->id,p->name,p->age,p->sex);
}
return 0;
}
在程序中,p是指向结构体类型对象的指针变量,而不能指向stu数组元素中的某一成员,若要将元素某一成员的地址赋给指针变量p,需要将成员强制转换为p的类型
3.结构体变量和结构体变量指针作参数
(1)用结构体变量的成员作为实参
用法和普通变量作实参一样
(2)用结构体变量作为实参
依然采用“值传递”的方式,但是在调用函数期间形参也要占用内存单元,如果结构体规模大的话,这种传递方式在空间和时间的开销就会很大
此外,如果在执行被调用期间改变了形参(即结构体变量)的值,该值不能返回主函数,会造成使用的不便
(3)用指向结构体变量(或数组元素)的指针作为实参
将结构体(或数组元素)的地址传给形参
(四)共用体类型(联合体)
1.概念与定义
共用体类型:用同一段内存单元存放不同类型的变量的结构
定义共用体类型的一段形式:union 共用体名 { 成员表列 }变量表列;
union Data
{
//共用体成员
int i;
char a;
float f;
}a,b,c;
共用体的定义也可以将类型声明与变量定义分开,与“结构体”类似,只是含义不一样,在这就不列出来了,可以参考结构体的定义来理解
共用体变量所占的内存长度等于最长的成员长度
2.引用共用体变量的方式
定义共用体变量后才能进行引用,但不能引用共用体变量,而只能引用共用体变量中的成员。
因为共用体变量的存储区单元按不同的类型存储数据,有不同的长度,所以只写一个变量名,系统无法判断应该输出哪个成员的值
3.共用体数据类型特点
(1)同一内存段可存放几种不同类型的成员,但每一瞬间只能存放其中一个成员,而不时同时存放
(2)可以对共用体变量初始化,但初始化表中只能有一个常量
union Data
{
int i;
char a;
float f;
}a = {1,'a',0.5};//错误,不能初始化多个,它们占用一段存储单元
union Data a = {1}//正确,对第一个成员初始化
union Data a = {.char = 'b'};//正确,C99标准允许对某一个
(3)共用体变量中起作用的是最后一次被赋值的成员,赋值后,原有的值被取代
a.i = 2;
a.a = 'm';
a.f = 20;
在执行完以上赋值语句后,因为最后存放的是20,所以2和’m‘都被覆盖了,输出a.f的值是20。
(4) 共用体变量的地址和它每个成员的地址是同一地址
(5) 不能对共用体变量赋值,C99允许同类型的共用体变量相互赋值
(6) C99允许用共用体变量作为函数参数,之前版本只能使用指向共用体变量的指针作函数参数
(7)共用体类型和结构体类型能出现在彼此定义中
(五)枚举类型
如果一个变量只有几种可能的值,则可以定义为枚举类型
声明枚举类型的一般形式:enum [枚举名] {枚举元素列表};
//举例
enum Weekday {mon,tus,wen,thu,fri,sat,sun};//声明枚举类型
emm Weekday workday,weekend;//定义枚举变量
枚举变量的值只限于花括号中指定的值之一,也可以不声明有名字的枚举类型,直接定义
- 在编译时,对枚举类型的枚举元素按常量进行处理,所以也叫枚举常量,定义好后不能再在程序中对其赋值
- 每一个枚举元素代表一个整数,编译时按定义的顺序默认值为0,1,2…,
- 可以在定义枚举类型时人为指定枚举元素的数值
- 枚举类型也可以用来作判断比较
(六)用typedef声明新类型名
1.用一个新类型名代替原有的类型名
typedef int Integer;//指定Integer为类型名,作用与int相同
typedef float F;//指定F为类型名,作用与float相同
//类型名指定以后,下列两种情况等价
1.int i; float a;
2.Integer i; F a;
2.简单类型名代替复制类型
(1)命名新的类型名代表结构体类型
typedef struct
{
int year;
int month;
int day;
}Date;//定义了新类型名Date代表这个结构体类型
Date *p;//定义结构体指针
Date a;//定义结构体变量
(2)命名新的类型名代表数组类型
typedef int N[10];//声明N为数组名
N a;//定义a为整型数组名,有10个元素
(3)命名新的类型名代表指针类型
typedef int* Integer;//声明Integer为整型指针类型
Integer p,a[10];//定义p为指针变量,a为指针数组
(4)命名新的类型名代表指向函数的指针类型
typedef int (*Pointer)();//声明Pointer为指向函数的指针类型
Pointer p1,p2;//指针变量
习惯上,我们常常把用typedef声明的类型名的第一个字母用大写表示,以便与系统提供的标准类型标识符相区别
3.特点
- 用typedef只是对已存在的类型指定了一个新的类型名,并没有创造新的类型
- 用typedef声明数组,指针,共用体,结构体,枚举类型等会使编程更加方便
- 与#define的区别:#defiine 在预编译时处理,作简单字符串替换;typedef在编译阶段处理,替换原有变量类型名,用它去定义变量
- 当不同源文件用到typedef声明的同一类型数据,可将所有typedef名称声明单独放到一个头文件中,在需要用时用#include包含到文件中
- 有利于程序的通用和移植
十二 、对文件的输入输出
(一) 文件
1.类型
程序文件:文件内容时程序代码,包含源程序文件、目标文件、可执行文件等
数据文件:内容非程序,而是供程序运行时读写的数据
2.文件名
一个文件的唯一的文件标识,包括三部分:文件路径、文件名主干、文件后缀
为了方便起见,文件标识常被称为文件名(此时的文件名实际上应包括3以上3部分内容)。
文件名主干的命名规则遵循标识符的命名规则,后缀表示文件的性质
3.分类
根据数据的组织形式,数据文件可分为ASCII文件和二进制文件。
-
数据是以二进制的形式存储的,不输出到外存就是一个二进制文件,就可以认为是存储在内存的数据的映像,也就叫做映像文件。
-
若在外存上以ASCII代码形式存储,则需要在存储前进行转换,ASCII文件又称文本文件,每个字节存放一个字符的ASCII码
在磁盘上时,字符一律以ASCII码形式存储,数值型两种存储形式都可以
4.文件缓冲区
每个文件在内存中有一个缓冲区,在文件输入/输出数据时,它就作为输入/输出缓冲区
缓冲区可以节省存取时间,提高效率,缓冲区的大小由C编译系统决定
5.文件类型指针
每个被使用的文件都在内存中开辟一个相应的文件信息区,用于存放文件的有关信息,这些信息存放在一个结构体变量中。
该结构体的类型由系统声明,取名为FILE,声明结构体类型FILE的信息包含在头文件“stdio.h”中。
每一个FILE类型变量对应一个文件的信息区,一般不定义FILE类型的变量命名,而是设置一个指向文件的指针变量,通过该指针来引用FILE类型的变量
指向文件的指针变量是指向内存中的文件信息区的开头,而不是外部介质上的数据文件的开头
(二) 打开与关闭文件
1.fopen函数打开数据文件
调用函数一般形式: fopen(文件名,使用文件方式);
FILE *f;//定义指针变量
f = fopen(file,"r");//返回值赋给f
表示打开名为flie的文件,以只读的方式,返回值是指向file文件的指针(即文件信息区的起始地址),t通常将返回值赋给一个指向文件的常量
2.用fclose函数关闭数据文件
调用函数一般形式: fclose(文件指针);
fclose(file);
撤销文件信息区和文件缓存区,使文件指针变量不再指向该文件,若不关闭文件就结束了程序则会丢失数据。
函数成功指向返回值为0,否则返回文件结束标志EOF(即-1)
当向文件写数据时,会先将数据输出到缓冲区,等待缓冲区满了以后才正式输出到文件中。如果当缓存区未满时未关闭文件就结束程序,则会造成数据丢失
3.使用文件的方式
文件使用方式 | 含义 | 如果指定文件不存在 |
---|---|---|
r | 只读,为输入数据打开存在的一个文本文件 | 出错 |
w | 只写,为输出数据打开一个文本文件 | 建立新文件 |
a | 追加,向文本文件尾添加数据 | 出错 |
rb | 只读,为输入数据打开一个二进制文件 | 出错 |
wb | 只写,为输出数据打开一个二进制文件 | 建立新文件 |
ab | 追加,向二进制文件尾添加数据 | 出错 |
“r+” | 读写,打开一个文本文件 | 出错 |
“w+” | 读写,建立一个新的文本文件 | 建立新文件 |
“a+” | 读写,打开一个文本文件 | 出错 |
“rb+” | 读写,打开一个二进制文件 | 出错 |
“wb+” | 读写,建立一个新的二进制文件 | 建立新文件 |
“ab+” | 读写,打开一个二进制文件 | 出错 |
(三)顺序读写数据文件
1.向文件读写字符
函数名 | 调用形式 | 功能 | 返回值 |
---|---|---|---|
fgetc | fgetc(fp) | 从fp指向的文件读入一个字符 | 读成功返回字符,否则返回EOF |
fputc | fputc(ch,fp) | 把字符写到文件指针fp所指文件 | 输出成功返回字符,否则返回EOF |
2.向文件读写字符串
函数名 | 调用形式 | 功能 | 返回值 |
---|---|---|---|
fgets | fgets(str,n,fp) | 从fp指向的文件读入长度为(n-1)的字符串,存放到字符数组str | 成功返回地址str,否则返回NULL |
fputs | fputs(str,fp) | 把str所指向的字符串写到文件指针变量fp所指向的文件 | 成功返回0,否则返回非0值 |
(1)fgets函
- 函数原型:
char * fgets(char *str,int n,FILE *fp);
-
n为要求得到的字符个数,但实际上fp指向的文件中读入了(n-1)个字符,然后再加上’\0’,这样就共有n个
-
如在读完(n-1)个字符前遇到”\n“或文件结束符EOF,读入即结束,但需将遇到的”\n“也作为一个字符读入
(2)fputs函数
- 函数原型:
int fputs(char *str,FILE *fp);
- 第一个字符可以是字符串常量、字符串数组和字符串指针
- '\0’不输出
3.用格式化的方式读写文本文件
一般调用方式:
fprintf(文件指针,格式字符串,输出表列);
fscanf(文件指针,格式字符串,输入表列);
fprintf(fp,"%d,%5.2f",i,f);//将变量i,f按格式输出到fp指向的磁盘文件中
fscanf(fp,"%d,%f",&i,&f);//从磁盘文件上读入ASCII字符
输入时将文件中的ASCII码转换为二进制形式,输出时将二进制形式转换为字符,花费时间多,尽量不用
4.用二进制方式向文件读写一组数据
一般调用方式:
fread(buffer,size,count,fp);
fwrite(buffer,size,count,fp);
- buffer : 对于fread,是存放从文件读入数据的存储区的地址;对于fwrite,是把此地址开始的存储区中的数据向文件输出(均值起始地址)
- size : 要读写的字节数
- count : 要读写多少个数据项,每个数据项长度为size
- fp : FILE*型指针
(四) 随机读写数据文件
随机访问不是按数据在文件中的物理位置次序进行读写,而是可以对任意位置上的数据进行访问
1.文件位置标记及其定位
(1) 文件位置标记
系统为每个文件都设置了一个文件读写位置标记,来指示接下来要读写的下一个字符的位置
对字符文件进行顺序读写时,文件位置标记指向文件头,当进行读操作时,就读第一个字符,如何文件位置标记向后移一位
随机读取时可以人为的移动文件位置标记
(2)定位
可以强制使文件标记指向人们指定的位置
-
feof函数:feof(fp);用于判断是否读到了文件结尾,读到返回1,否则为0
-
用reward函数使文件位置标记指向文件开头(即使文件位置标记重新返回文件头),无返回值
//例:有一个磁盘文件存有一些信息,要求第一次将它内容显示在屏幕上,第二次复制到另一个文件上
#include <stdio.h>
int main()
{
FILE *fp1,*fp2;
fp1 = fopen("D:/file1.dat","r");//打开输入文件
fp2 = fopen("D:/file2.dat","w");//打开输出文件
char ch = getc(fp1);//读入第一个字符
while(! feof(fp1))//当未读取文件尾标志
{
putchar(ch);//屏幕输出一个字符
ch = getc(fp1);//再次读入
}
putchar(10);
rewind(fp1);//文件位置返回开头
ch = getc(fp1);
while(! feof(fp1))
{
fputc(ch,fp2);//向file2文件输出字符
ch = fgetc(fp1);
}
fclose(fp1);
fclose(fp2);
return 0;
}
- 用fseek函数改变文件位置标记
函数调用一般形式:fseek(文件类型指针,位移量,起始点);
C标准指定的名字
起始点 | 名字 | 用数字代表 |
---|---|---|
文件开始位置 | SEEK_SET | 0 |
文件当前位置 | SEEK_CUR | 1 |
文件末位位置 | SEEK_END | 2 |
位移量以起始点为基点来计算向前移动的字节数,应是long型数据
fseek函数一般用于二进制文件
- 用ftell函数测定文件位置标记当前位置
作用是得到流式文件中文件位置标记的当前位置,用相对于文件开头的位移量来表示,若调用失败,返回-1L
i = ftell(fp);//变量i存放当前位置
if(i == -1L)
{
printf("error\n");
}
2.随机读写
有了以上函数就可以实现随机读写了
例:在磁盘文件上存有10个学生的数据,要求输入几个数据输入计算机,并在屏幕上显示出来
#include <stdio.h>
#include <stdlib.h>
struct Student //学生结构体类型
{
char name[4];
int id;
int age;
}stu[4];
int main()
{
int i;
FILE *fp;
if((fp = fopen("D:/stu.dat","rb")) == NULL)
{
printf("error\n");
exit(0);
}
for(i = 0;i < 4;i++)
{
fseek(fp,i*sizeof(struct Student),0);//移动文件位置标记
fread(&stu[i],sizeof(struct Student),1,fp);//读一个数据块的结构体变量
printf("%s %d %d\n",stu[i].name,stu[i].id,stu[i].age);
}
fclose(fp);
return 0;
}
(五)文件读写的出错检测
1.ferror函数
一般调用形式:ferror(fp);
执行函数初始值为0,若返回值为假(即0),表示未出错;若返回为一个非零值,表示出错
对同一文件,每一次调用输入输出函数后都会产生一个新的ferror函数值
2.clearerr函数
一般调用形式:clearerr(fp);
clearerr函数的作用是使文件出错标志和文件结束标志为0
当调用输入输出函数后产出的ferror函数值非零时,应该立即调用clearerr函数,使函数值变为0,以便进行下一次检测