C语言程序设计

Yizumi Konata Lv3

C语言简介

C语言发展历史

image-20240605231934219

其中从BCPL语言B语言为无类型语言。C语言源于B语言,与1969至1973年间在UNIX上以B语言为基础设计实现。

最初的C语言依附于UNIX操作系统,在1978年后被移植到各种类型的计算机上,从此独立于所有操作系统。

C语言特征

  • 语言简洁紧凑:只有37个关键字,12种语句
  • 目标代码质量高:目标代码质量指其所占空间大小和执行速度快慢,C语言编译产生的目标代码的质量可以与汇编媲美
  • 语言表达能力强:类型丰富,操作符丰富,对硬件操纵能力强
  • 语句集简单:基本语句仅有七种
  • 流程控制结构化:支持三种基本结构,引入breakcontinue,保留goto语句
  • 弱类型:支持隐式类型转换
  • 中级语言:具备高级语言的表达能力和对计算机硬件良好的控制能力
  • 书写自由:书写代码没有强制的格式要求
  • 可移植性好:C语言源程序可以不改动或稍加改动便可以移植,重新编译链接后即可运行

简单的C语言程序实例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
#include<stdio.h>
void show(char str[]);
int main(void)
{
char name[20];
printf("Input your name please!\n");
gets(name);
show(name);
return 0;
}
void show(char str[])
{
printf("Hello %s",str);
}

代码第一行表示导入stdio.h库到程序中,导入后可以在程序中使用stdio.h中的函数,比如之后的printf以及gets

代码第二行是对show函数原型的声明,C语言规定所有函数必须先声明后使用,之后的gets和printf函数原型的声明在stdio.h中。void表示函数无返回值,一个函数执行完后会有返回值,返回值的类型在函数定义时给出,void表示无类型。函数返回值除void外,还可以是intfloatchar以及指针和其他更为复杂的类型。show是函数名称,自定义函数的函数名必须是合法的标识符。圆括号里的式子是函数的参数。一个函数可以有多个参数,对于这个函数来说只有一个参数char str[],一个字符数组的首地址。

代码第三行为主函数的定义,命名为main的函数称为主函数,一般主函数的返回值设为int类型,参数设为void(注意主函数是可以传特定参数的)。一个C语言程序可以有多个函数,但只能有一个主函数,主函数是程序执行的入口。

主函数内char name[20]是声明一个名称为name的字符数组,char为变量类型[20]表示长度为20个字符的数组,C语言规定在使用一个变量前必须先声明其类型和名称,之后顺次执行三个函数,printf函数打印指定内容至输出设备,puts函数从输入设备读取值至圆括号中的变量,show函数的作用在后面的定义中,return 0表示函数执行结束后向操作系统返回一个0。返回值类型只要和函数定义时一致就行,但对主函数一般习惯返回0.

代码最后一部分时show函数的定义。由于show函数的定义在main函数使用show之后,所以才需要开头的声明。若将定义移至main函数前边则可以省去声明语句。show函数的作用也是打印特定内容,其中的str是函数参数,类似数学中函数的x,可以被传入函数的具体值代替,本程序在执行是str会被name替代。

函数的执行结果是先在屏幕上出现

Input your name please!

输入你的名字(比如Mike)后屏幕出现

Hello Mike

计算机系统及内存编址

计算机系统的思维导图表示

计算机系统

其中需要注意的是内存,内存以字节为单位连续线性编址即按照0x0000,0x0001,0x0002......从低地址到高地址连续线性编号。

计算机中的常用储存单位:一个二进制位为1比特(1b),8b为一个字节(1B),1024B为1K,1024K为1M,1024M为1G,1024G为1T。

cpu的数据总线的宽度称为cpu的机器字长

数字与字符的编码表示

进位计数制

计算机中主要涉及到的进位制有二进制,八进制和十六进制,编写程序时无法表示二进制,通过在八进制数字前加0表示八进制,在十六进制数前加0x表示十六进制,不加前缀的数字为十进制

进位制数之间的转换

  • 十进制转k进制:除k取余法
  • k进制转十进制:按权展开
  • 二进制转八进制:从低位到高维三位分组,高位不足补零,将每一组转化为相应八进制数
  • 二进制转十六进制:四位分组,方法与上面类似

数的机器码表示

机器数:最高位为符号位,其余位为数值位,符号位为0时表示正值,为1时表示负值

真值:机器数的数值位称为机器数的真值

原码:最高位为符号位,其余各位为数值位,符号位为0表示正,为1表示负

反码:正数的反码等于其原码,负数的反码为其符号位不变,其余各位按位求反(1变0,0变1)

补码,正数的原码补码反码都相等,负数的补码等于反码加一

注意:原码补码反码都是对整数来说的,浮点数(即小数)的表示方法与整数不同

字符的编码表示

ASCII码:对于西文字符采用ASCII码,ASCII码采用单字符编码,最高位留作校检位,低七位用于编码,ASCII码字符集中包含128个字符的编码。只有第七位参与的编码称为基本ASCII码(最高位为0),高字节参与编码称为扩展ASCII码

汉字编码

  • 拼音码、五笔字型码:用于输入
  • 国标码:两字节编码
  • 区位码:将国标码中的字符按位置划分为94个区,每区94个字符的汉字编码方案,是国标码的变形码,换算关系为 国标码(十六进制)=区位码(十六进制)+0x2020
  • 机内码:计算机储存和处理汉字的编码,将国标码两个字节的最高位置为1形成,换算关系为机内码(十六进制)=国标码(十六进制)+0x8080

基本语法词汇与程序元素

字符及词法元素

字符集

C语言字符集是ASCII码的子集,包括二十六个英文字母的大小写,十个十进制数字,特殊字符如! " # % & ' ( ) * + , - . / : ; < > = ? [ ] \ ^ - { } | ~以及空白字符如空格,换行符,水平制表符,垂直制表符,换页符等。

有些国家本国字符集不包括所有的C语言字符集,于是标准C语言定义了一组三字符序列。比如有的国家的字符集不包括#字符,于是可以用??=代替#。注意对三字符序列的识别与编译器有关,有点编译器可能不识别三字符序列

词法元素

C语言编译器会按照特定规则将C程序中的字符序列分解为记号,记号分为五类:标识符,关键字,常量,运算符和标点符号。

编译器从左往右收集字符,总是尽量建立最长的记号,即使结果不一定构成有效的C语言程序,如x+++++y,编译器会将其拆分为x++ ++ + y,这是错误的。虽然将其拆解为x++ + ++y是正确的,但是编译器不会这样分解。

标识符、关键字及分隔符

标识符

标识符是用来给变量、常量、数据类型以及函数命名的符号。表示符只能由数字、字母以及下划线组成,且首字符必须是字母和下划线。

除基本命名规则外,还要求用户自定义的标识符不能与C语言关键字以及程序库中的函数和常量重名,此外受编译系统以及机器的限制,可能还对标识符的长度有要求。

一般情况下标识符尽量有一定含义,方便阅读程序。

关键字

关键字是被系统赋予特别含义并有专门用途的标识符,不能作为普通标识符给变量和函数等命名,但可以作为宏名(宏名在程序执行的预处理步骤会被替换,而预处理发生在识别关键字之前)C语言关键字

分隔符

分隔符统称为空白字符包括空格符,制表符,换页符,换行符以及注释符,仅起分隔单词的作用

基本数据类型

  • 字符类型:分为charunsigned char,都占一个字节,前者最高位为符号位,后者最高位和其他位一样是数值位,普通char对象是有符号还是无符号的取决于机器,但打印字符总是正的,大多数系统中两者是同一类型。
  • 整型类型:即整数类型,一般的int类型占2或4字节,short int占2字节,long int占4字节,无unsigned修饰为有符号数,有unsigned修饰为无符号数,相应表示范围有变化
  • 浮点类型:即小数类型,只有有单精度浮点型float,双精度浮点型double,长双精度浮点型long double三种(没有unsigned的浮点型)。在计算机中二进制浮点数V表示为V = (-1)S ✖ M ✖ 2E,M为大于等于一小于二的数,称为尾数,E称为阶码。由于M规定为1.xxxxxx,相当于默认整数位为1,编码表示时尾数区其实只保存小数点后的数字。float占四字节,其中最高位为符号位,最高位后八位为指数区,最后二十三位为尾数区。 double占八位,最高位为符号位,中间十一位为指数区,最后五十二位为尾数区。long double型长度由具体定义实现,大于等于double

注意

  1. 区别字符‘1’,整型‘1’,以及之后的字符串“1”
  2. 在数值较小时,比如范围为0到9或者是在ASCII码的范围之内,可以用字符型变量代替整型变量以节省空间,前者减去‘0’即可当作整型09使用(可读性相对较强),后者直接用ASCII码运算(可读性相对较弱)。
  3. C语言中,当储存数据过大或过小时存在溢出情况,字符型和整型只有上溢(数据过大溢出),浮点型还可能因为指数区精度不足下溢(数据过小而溢出)。浮点数下溢时不会再将尾数位小数点前的数字当作1而是当作0用以输出一个很小的接近0的数字;上溢时会出现特殊提示来提示程序员数据发生上溢
  4. 由于计算机计算浮点数有误差,浮点数不可直接进行是否相等的比较

常量与变量

文字常量

c语言的文字常量包括整型,浮点型,字符型和字符串型。这里的文字常量可以理解为数据

  • 整型常量:可以是十进制,八进制和十六进制整数书写,八进制加前缀0,十六进制加前缀0x,无前缀时表示十进制。整型常量也可以带后缀,后缀u表示ubsignedl表示long,可以在一个整数后面加上ululullll(大小写皆可)等表示整数的类型为相应后缀类型。无后缀时表示int。
  • 浮点型常量:两种表示方法,可以用带小数点的十进制数来表示(小数点前全为0或小数点后全为0时0可以不写)如23.14 .56 12. 等,也可以用科学计数法表示,如45e-3表示0.45,其中e(大小写皆可)±n表示10的±n次方。浮点型也可以加后缀指定其类型,无后缀为double型,加f为浮点型,加llong double
  • 字符常量:单引号括住单个字符即为一个字符常量。大部分字符常量可以直接用单引号括住字符,一部分字符需要使用转义序列如换行、水平制表,单双引号和反斜线,需要用‘  '的方式表示。一般的字符也可以用数字转义序列表示,即。ooo表示1-3个八进制数字,hh表示1-2个十六进制数字,x为前缀,此时可以用ASCII码的八进制和十六进制表示字符。
  • 字符串常量:双引号括住的0至多个字符,字符串中有的特殊字符也必须使用转义字符如双引号和单斜线。字符串本质是字符数组,由组成该字符串的字符以及空字符表示,如hello就是'h','e','l','l','o','0'组成的字符数组,用sizeof得到该字符串的大小为6(五个英文字符加一个空字符)

符号常量及符号常量定义

一个符号常量就是一个代表某一个定值的标识符。定义符号常量的方法有#define定义,const定义,枚举类型定义

  • #define定义常量的方式为#define 标识符 常量
  • const定义符号常量的方法为const类型名 标识符=常量
  • 枚举类型定义符号常量的方法为enum 枚举名 {标识符=常量表达式,标识符=常量表达式,……},每一个标识符是一个枚举常量,枚举常量的类型为int,常量表达式可以不给出,在未给出常量表达式的情况下第一个枚举常量的值为0,第二个为1,第三个为2,以此类推。若只给出部分枚举常量的值的情况下,未给出值的枚举常量的值为其前一个常量的值加1,比如enum test {a=1,b,c=5,d,e};则b为2,d为6,e为7

:三种方式定义常量的区别在于第一种分配内存空间,第二种和第三种都会占用一定的内存空间

变量定义

C语言中的变量使用前必须声明,这样编译器参可以为变量分配适当的储存单元

变量声明的格式为 类型名 变量名=初值;可以不进行初始化。

可以在一个语句里声明多个变量,如:

1
int a=2,b=3,c;

但是不建议这样写,不方便注释。初始化时每个变量必须显式初始化,下面的声明式不合法的

1
int a=b=3;       //错误,a没有显式初始化,应改为int a=3,b=3;

运算符与表达式

C语言的运算包括算术运算,关系运算,逻辑运算,自增自减运算,赋值运算,条件运算 ,逗号运算,sizeof运算,位运算

  • 算术运算:包括+ - * / %,都为双目运算符,其中对/,如果两个操作数都是整数则结果也会是整数,若有一个是浮点数则运算结果为浮点数。另外对两个异号整数相除或取模运算时结果的符号以及取整的方向C89与C99标准不同,注意。
  • 关系运算:包括< <= > >= == !=,都为双目运算符,关系表达式的计算结果总是int型,1为真,0为假。注意在写程序时要判断a<b<c时不能写if(a<b<c),写作a<b<c会先算a<b得到1或0,然后1或0再与c比较。应该是if(a<b&&b<c)
  • 逻辑运算:包括!(非)&&(与) ||(或)。是单目其他两个为双目。逻辑运算中整型0,浮点型0.0,空字符'\0'以及空指针为假,非0为真。注意或运算在出现非0项后就直接结束,不会执行后面的语句。
  • 自增自减运算:包括前后缀的++--。只能作用于变量,前缀立即生效,后缀在程序执行到序列点时生效。序列点为&&||?:以及的第一个操作数之后,还有表达式完整结束遇到;
  • 赋值运算:右结合,即从右读。除简单的赋值运算外还有复合赋值运算,即一个双目运算符op加上=构成的op=运算符op可以是算数运算符和位运算符。符合赋值表达式为 表达式1 op= 表达式2,等价于表达式1=表达式1 op 表达式2
  • 条件运算:三目运算符?:,一般形式为 表达式1?表达式2:表达式3,先计算表达式1,若其值为非0则计算表达式1,表达式1的值就是该运算的结果,否则计算表达式2,得到的结果为该运算的结果
  • 逗号运算:表达式的形式为 表达式1,表达式2,…… 表达式n,运算结果b为表达式n的值,即最后一个表达式的值
  • sizeof运算:有两种形式,第一种为sizeof(类型名), 得到某个数据类型所占用的储存字节数,第二种为 sizeof 表达式sizeof(表达式),若不带圆括号则需要空格,运算结果为表达式结果的类型所占用的字节数
  • 位运算符:包括~(按位求反)、&(按位与)、|(按位或)、^(按位异或) 、<<(按位左移)、>>(按位右移),其中按位异或的意思是全为1或全为0时为0,一个为1一个为0时为1。按位左移时高位丢弃,低位填入0,在不丢失有效位时左移n位的结果是;按位右移时低位被丢弃,若操作数为有符号类型高位一般填入符号位(当然也有机器填入0但是很少),若为无符号类型则填入0 。

类型转换

整数提升

任何表达式中的charunsigned charunsigned short都要先转化为intunsigned参与运算(所有值都可以转化为int时转化为int,否则转化为unsigned

算数转换

当对双目运算符的操作数求值时先进行整数提升,若此时操作数类型还不相同则进行算数转换,也称隐式类型转换,从值域较窄的值向值域较宽的值转化,示意图如下

image-20240605231950569

赋值转化

赋值运算中右操作数的值转化为左操作数的类型,例如

1
2
3
4
short s=5,a;
double d=2.9,b;
a=d;
b=s;

此时d的值被转化为short再赋给aa的值为2

s的值被转化为double再赋给bb的值为5.0。

强制类型转换

又称显示类型转换,形式为

1
(类型名) 操作数

例如

1
2
(int) a
(double)('a'-32)

第一个表达式的值的类型为double,但变量a的值以及类型不变。第二个表达式的值为65

枚举类型

枚举类型常量的定义在前面已经给出,枚举变量的声明有以下两种方式

1
2
enum color{RED,GREEN,BLUE};
enum color c1,c2
1
enum color{RED,GREEN,BLUE} c1,c2;

枚举变量的类型也为int,取值范围为其对应枚举类型所列出的值。例如对上面的枚举类型color。我们定义了两个color类型的变量c1,c2,只能取值0,1,2。所以给c1c2赋值时若不为0,1,2都是非法的。

枚举类型的意义是便于程序阅读,不要和后面的数组搞混。*

基本标准输入输出

基本标准输入输出函数的定义在头文件stdio.h中,在使用这些函数时需要导入该头文件。其实基于字符与字符串的基本输入输出函数有很多,本章介绍的是最常用的。

字符输入输出

字符输出函数为putchar,字符输入函数为getchar,函数原型的声明如下

1
2
int putchar(int c);
int getchar(void);

对函数putchar,其传入的参数为所需要输出的字符的ASCII码(当然也可以直接传入字符会做整数提升),函数将ASCII码转化为unsigned char送到标准输出设备中,若函数执行成功则返回输出字符的ASCII码,否则返回EOF

对函数getchar,其不需要传入参数,函数执行时会从输入流中读取一个字符并将字符转化为int类型后作为返回值返回。getchar函数的执行流程为:

  1. 检查输入流中是否有字符,有则读取第一个字符后将其转化为int返回,剩余字符留在输入流中。没有则进入等待状态。
  2. 在等待状态中可以通过键盘输入字符,在输入完毕后按下回车,所有之前输入的东西包括回车产生的换行符会被送入输入流
  3. 按下回车的同时getchar激活,回到第1步。

字符串输入与输出

字符串输出函数为puts,字符串输入函数为gets,函数原型的声明如下

1
2
int puts(const char *s);
char *gets(char *s);

puts函数其参数为要输出的字符串的内存首地址(C语言中没有字符串类型,如前面所介绍的字符串的本质是字符数组,数组的元素在内存中连续存储,由于一个字符串以空字符结尾,所以知道传入字符串首地址就相当于传入字符串,详细内容见数组一章),函数执行时将字符串输入到标准输出设备并在结尾添加换行符。函数正确执行时返回一个非负整数,错误执行时返回EOF

gets函数,传入参数为将要用来存放字符串的数组的首地址,执行过程中gets函数从输入流中读取一行字符(以换行符结尾,理解为按一个回车),将结尾的换行符换为空字符存入传入的数组中。为防止越界访问内存地址,s指向的内存缓冲区应该足够大以包含输入的字符串。gets函数相比于后面的格式化输入函数的好处是可以直接输入有空格的字符串,但是由于用户输入的一行字符串的大小未知,而存放输入字符串的数组的大小是已经定好的,可能出现越界访问内存的情况,而该函数没有相应处理办法,因而具有安全隐患,在C11标准中该函数已被删除。

格式化输入输出

格式化输出函数printf

printf的函数原型为

1
int printf(const char *format,...);

该函数为参数数目可变的参数,其中第一个参数format是一个字符串,其中的转换说明的个数和转换字符决定了省略号代表的参数个数,例如

1
printf("%d%8.3f"15-8.2)

格式字符串中有两个 转换说明,%d和 %8.3f,所以后面跟两个参数,%d代表int型变量,%f代表double型变量,所以后面跟的两个参数一个是整数一个是浮点数。

函数的返回值是输出到函数输出到标准输出设备中的字符个数。

转换说明的语法格式为%[域宽说明]转换字符,域宽说明部分用来表示输出的对齐方向、输出的数据域的宽度以及精度,转换字符格式如下

注意要在格式字符串中直接输出%应该要打两个%,在%后跟的不是转换字符时多数系统也会将其作为普通字符输出,但是保险起见还是打两个%。

对%g的输出,首先以十进制表示这个数的宽度和指数形式表示这个数所占的宽度谁小(宽度就是将数据打印到屏幕上所显示的长度),谁小输出谁

主要注意小数点的用法就好

格式化输入函数scanf

函数原型如下

1
int scanf(const char*format,...);

scanf函数的调用形式与printf类似,format中的转换说明的个数决定了后面参数的个数,后面的参数都是地址值,读入的数据按转换说明中的格式转化后储存在后面的地址中,函数正确执行时返回值为被转换的数据的个数,遇到文件尾或出错时会返回EOF

一般情况下函数的调用方式如下

1
2
3
4
int a;
double f;
char s[50]
scanf("%d%f%s",&a,&f,s);

&为取地址符,用来得到变量af的地址,s本身就是地址,在数组一章会提到。

一般情况下scanf的格式字符串中只有转换说明,若有除转换说明以外的除空格和制表符以外的其他字符,则在输入时必须输入一样的字符才能正确执行,比如

1
2
int a,b;
scanf("these are %d,%d",&a,&b);

此时必须在输入的两个整数之前加上these are ,整数之间加逗号才行。

scanf的转换字符与printf类似多一个n,%n的作用时统计输入了多少个字符并将个数储存到特定的储存单元。

scanf函数的转换说明一次只能读入一个输入域。从输入流当前位置开始知道第一个空白字符出现为止(如从键盘输入great days其实就是两个输入域了),或根据转换说明不能被转化的字符之前(如转换字符为%d,从键盘键入的值为3a,则只有3在输入域内),或直至指定域宽内的所有字符都是一个输入域。

所以scanf函数无法向一个空间读入带空格的字符,也不会对输入的字符自动进行整数提升,只会按照指定格式输入。

scanf在输入时没有像printf函数一样的域宽说明,但是有其特定的可选项

可选项意义
m(正整数)用于指定域宽,及读入字符的个数,从自然输入域中取前m个字符。当自然输入域不足m时则将自然输入域全部读入为止
h输入短整数
l输入长整数
L输入长双精度浮点数
*跳过输入域

*的用法如下

1
2
3
4
5
6
7
8
#include<stdio.h>
int main(void)
{
int n1,n2,n3,n4,n5;
scanf("%d %*d %d %*d %d %*d %d %*d %d",&n1,&n2,&n3,&n4,&n5);
printf("%d %d %d %d %d",n1,n2,n3,n4,n5);
return 0;
}

执行程序时输入1 2 3 4 5 6 7 8 9

输出1 3 5 7 9

流程控制

计算机程序是由有限条语句构成的序列,语句依次执行,对语句执行次序的控制称为流程控制。

任何复杂的算法都可以通过顺序、分支、循环三种结构实现,对只包含这三种结构的程序称为结构化程序。

C语言是很好的结构化程序设计语言,同时也支持部分转移语句。

C语言的语句分为以下六类

  1. 表达式语句
  2. 复合语句
  3. 选择语句
  4. 循环语句
  5. 标号语句
  6. 转移语句

表达式语句

一个表达式末尾加一个分号即为一个表达式语句,这个表达式可以是赋值,可以是定义变量,也可以是调用函数,甚至可以没有东西

注意:只有加了分号才可以称为语句,在判断题中要注意

复合语句

以一对花括号括起来的一组语句称为复合语句。表达式语句在语法上属于一个语句,复合语句中的每一个句子都要加分号但是在反花括号外面不需要。复合语句主要用于if语句和循环语句中。

一个复合语句称为一个块,在块内定义的变量可以和外面的变量重名并覆盖外界变量,但是块内定义的变量的作用域也仅限于块内

复合语句可以嵌套,但函数定义不能嵌套,不能在函数体中定义函数

选择语句

if语句

if语句有两种格式

  1. if(条件) 语句1
  2. if(条件) 语句1 else 语句2

if语句

表达式中的值不为0时执行if后面的语句

if语句可以嵌套使用如

1
2
3
4
5
6
7
8
9
10
11
12
if(a>b){
if(a>c)
max=a;
else
max=c;
}
else{
if(b>c)
max=b;
else
max=c;
}

嵌套的if-else语句中else与if的配套规则为:else与其前面最靠近的还未配对的if配对,即内层有优先配对

switch语句

switch语句的语法形式为

1
2
3
4
5
6
switch(表达式){
case 常量表达式1: 语句序列1;
case 常量表达式2: 语句序列2;
......
default:语句序列;
}

  1. switch后括号里的表达式的值必须为整数(整型,字符型,枚举型)
  2. case常量的类型与表达式一致,各个case常量的值不能相等.
  3. switch语句的执行过程是首先定位到表达式的值刚好等于case值的那一条语句,然后执行后面所有的语句。如果要只执行某一条语句应该在case语句序列后面加上break
  4. 当表达式的值与所有的case常量都不相等时执行default语句,default语句至多一个,也可以没有
  5. casedefault后面的语句序列可以没有语句也可以有多条语句,多条语句不用打花括号
  6. case执行相同语句时可以写的简略一些,比如下面的例子
1
2
3
4
5
6
7
switch(a){
case 0: printf("nice to meet you");break;
case 1: printf("hey");break;
case 3: printf("hello");break;
case 4: printf("hello");break;
case 5: printf("hello");break;
}

可以改为

1
2
3
4
5
6
7
switch(a){
case 0: printf("nice to meet you");break;
case 1: printf("hey");break;
case 3:
case 4:
case 5: printf("hello");break;
}

以及

1
2
3
4
5
switch(a){
case 0: printf("nice to meet you");break;
case 1: printf("hey");break;
case 3: case 4: case 5: printf("hello");break;
}

循环语句

所有循环语句的循环体都只能是一个语句,所以在需要多条语句时需要借助复合语句。

while语句

while语句的形式为

1
2
while(表达式)
语句;

括号里的值非零时执行下面的语句。若下面的语句有多条则必须使用复合语句

for语句

for语句的形式为

1
2
for(表达式1;表达式2;表达式3)
语句;
  1. 表达式1只在循环开始时进行一次,可用于变量初始化,可以使用逗号运算符。

  2. 表达式2是循环条件,每次循环开始时进行判断,若其值不为0执行循环体语句,为0则停止循环。

  3. 表达式3在每次循环结尾执行,可以用于进行改变循环变量

  4. 三个表达式可以部分或全部省略,但是两个分号不能少,其中表达式2省略时表示无限循环(当然你可以在循环体内部设置跳出循环的转移语句

for语句

do-while语句

do-while语句的形式为

1
2
3
do
语句;
while(表达式)

其与whlie语句差不多,但是会在一开始先执行一次循环体里的内容,类似下面的结构

1
2
3
语句
while(表达式)
语句;

但是写起来更简洁

标号语句

形式为

1
标号:语句

其中的语句必须是不含标号的C语言语句。上面的语句也称为标号的定义。标号语句主要用在goto语句中

转移语句

goto语句

goto语句的形式为

1
goto 标号;

上面的语句引用的标号必须定义过,而且goto语句只能作用在一个函数内,不可以跳出函数,但可以跳转到函数内的任意程序块中。执行goto语句时,程序会跳转到标号所在的语句执行,例如

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
#include<stdio.h>
int main(void)
{
double x,y;
char op;
intx:
printf("请输入算式");
scanf("%lf",&x);
while((op=getchar())!='\n'){
inty:
scanf("lf",&y);
switch(op){
case '+':x+=y; break;
case '-':x-=y; break;
case '*':x*=y; break;
case '/':
if(y) x/=y;
else{
printf("除数为0,请重新输入除数");
goto inty;
}
default:
printf("运算符非法,请重新输入算式");
}
}
printf("%lf\n",x);
return 0;
}

该程序为一个计算器,有两个标号分别为intxinty,在输入的算式为除法且除数为0时执行goto语句使程序跳转到inty处执行,当运算符不为加减乘除时执行goto语句时程序跳转到intx处执行。

goto语句不是程序必须成分,虽然很方便但是使用过多会降低程序的可读性,应该尽量避免使用。

break,continue和return语句

break语句的作用为

  1. 用于switch中,中途退出switch语句
  2. 用于循环语句,直接退出循环

continue语句用于循环语句,用于直接跳到循环体末尾

  1. 用在whiledo-while中会直接检验循环条件
  2. 用在for语句中会执行表达式3后再检验循环条件

return语句的功能时从被调用的函数返回调用处,简单理解为结束函数的执行。有两种形式

  1. 不带表达式,用于无返回值的函数,只表示结束不返回值。
  2. 带表达式,会将表达式的值带回调用处,只能用于有返回值的函数

一个函数会可以包含多个return函数,一般作为选择语句的子句出现,最终只会执行一个。

函数与程序结构

C语言的一般结构

在开发和维护大型程序时最好的办法时使用易于管理的结构化编程的方法,自顶而下,逐步求精

结构化程序设计方法是以模块化设计为中心,将待开发的软件和系统划分为若干独立的模块进行开发,最后将模块组装到一起。实现结构化程序设计的手段具体到C语言是编写自己的函数,把每一个模块设计成一个函数。

一个C程序由一个或多个函数组成,其中有且只有一个main函数,程序的执行总是从main函数开始,程序执行到一个后面跟有括号的函数名时就调用函数,将程序执行转移到函数。除main函数外的函数分为两类,一类是库函数,如printfscanf等,包含在相应头文件中,只需包含对应头文件即可使用;第二类是用户自定义函数,需要先给出定义或先声明函数原型再使用。

组成一个C源文件的程序的各个函数可以编 辑成多个C源文件,各C源文件收中要用到的一些变量与常量的声明以及预处理指令可以编辑成一个后缀为.h的头文件,在每个C文件中包含头文件,则每一个C文件都可以使用该头文件里声明的一些标识符。C程序

函数的定义与函数原型

函数的定义一般形式为

1
2
3
4
5
类型名 函数名(参数列表)
{
声明部分
语句部分
}

花括号前的部分称为函数头,花括号里的称为函数体

函数头中类型名为函数返回值的数据类型,可以是除数组以外的任何类型,当为void时表示函数无返回值。函数的返回值通过return语句获得。一般来说函数的返回值的类型应该与函数头中的定义一致。对基本类型,当返回值与函数头的定义不一致时会自动转化,对指针类型则必须手动强制转换为一致,对结构和联合则必须一致

函数名称必须为合法的标识符,而且最好有实际意义,便于阅读。

参数列表的参数也称形式参数,给出给出形参的过程也是一个变量定义的过程,但是不能像定义普通变量那样一个类型名后面跟好几个变量,每一个形参必须有自己的数据类型和名字

1
2
double pow(int x,int y)    //正确
double pow(int x,y) //错误

函数体中的变量属于局部变量,作用域仅限于函数体

函数在调用前需要声明函数原型,函数原型以分号结束,类似函数头,但是不必给出形参的名字,例如

1
2
void GuessNum(int x);
void GuessNum(int);

以上原型声明均合法。注意如果将函数原型放在任何函数之外则所有函数都可以使用,若放在某个函数体内则只有在该函数内可以使用。

函数的调用与参数传递

函数调用的一般形式为

1
函数名称(实参列表);

函数调用过程中传递的参数称为实参,若函数不需要接受参数则括号里不需要内容。程序从main函数开始执行,执行到其中的函数调用语句时系统将实参的值传给形参并将控制转移到该函数,执行函数体内的内容,最后将函数的返回值返回到调用处。

传入实参时应注意实参是有求值顺序的,有的从做往右,有的从右往左,所以尽量不要类似写下面给出的右副作用的语句

1
power(a,a++);

注意通过实参传值只是将实参的值传递给形参,不会改变实参的值,形参和实参是两个变量而且形参仅作用于函数体内。要通过函数改变实参的值只能通过传地址

作用域与可见性

在函数外部(文件开头或函数之间)定义的变量是全局变量,也称外部变量,其作用域是整个程序,程序块(花括号)内部定义的变量是局部变量,其作用域仅限于程序块内部。外部变量的作用域最广,但是当局部变量与外部变量重名时局部变量可见性更强。

在C程序的不同源文件之间或同一个源文件的不同函数之间共享变量时可以使用外部变量,所以外部变量也可以进行函数间的传值。若外部变量的定义在引用的后面则需进行声明,声明的格式为

1
extern 类型 变量名

引用性声明时不分配储存单元,只有定义时分配。好的编程习惯是将外部变量的定义放在程序开头。

局部变量没有引用性声明,只有定义性声明。过度依赖外部变量会降低函数的独立性,造成封装不好,所以建议使用形参传值,提高函数的通用性

存储类型

C/C++程序占用的内存分为一下几个部分

  1. 代码程序区:存放二进制代码
  2. 静态数据区:存放程序运行期间的用到的数据,空间在编译时分配,整个程序期间数据一直存在,程序结束后由系统释放
  3. 动态数据区(栈):存放程序运行期间的数据,,空间在程序运行期间由编译器分配,生命周期短于程序运行期
  4. 堆区:由程序员分配释放,若程序员不释放,程序结束时由系统回收

下面介绍几种存储类型的关键字

auto

只作用于变量,称为自动变量,是局部变量的默认储存方式,一般情况下省略,储存于动态数据区中

extern

既能作用于变量又能作用于函数。

函数外部定义的变量是外部变量,存储类型为extern,但定义时不使用关键字,储存于静态数据区,未初始化时默认值为0.由于外部变量的作用域时整个程序,所以可能在一个源文件中用到另一个源文件的外部变量,此时需要用到extern进行引用性声明,方式上面已经提到。注意引用过来的变量的作用域是有限的,比如在该源文件的一个函数内部进行外部变量引用性声明,另一个函数无法使用,但在源文件的函数外引用则所有函数都可用。

函数一般都是全局的,储存类型为extern,在函数定义和函数原型中可以使用关键字extern。同样在一个源文件引用另一个源文件的函数时需要extern声明

static

static可以用于定义静态局部变量,静态局部变量的生命周期与自动变量一样,但是其储存于静态数据区,生命周期是与整个函数的运行周期一致的,退出块时变量的值仍保留

还可以用于定义静态外部变量,静态外部变量与外部变量的区别是静态外部变量只能作用于定义它的文件里

还可以用于定义静态函数,静态函数的作用域仅限于定义它的文件

register

register只能用于定义局部变量,将局部变量储存在寄存器中以加快运算速度,其他方面于局部变量一致,但是用register定义变量只是一种向编译器提供的建议,在寄存器资源不足时编译器会忽略register

编译预处理

#开头的指令称为预处理指令,这些指令是在编译之前完成的

#include

#include是文件包含指令,形式有两种

1
2
#include<文件名>
#include"文件名"

作用是用文件的内容取代这一行。被包含的内容没有限制,C编译系统提供的头文件扩展名为.h,设计者可以自行决定自己要包含的文件的路径和后缀。

可以将文件名换成具体路径,此时双引号和尖括号没有区别。但如果只有文件名的话尖括号会到系统指定的标准目录下寻找,而双引号是先到当前目录下寻找,找不到再到标准目录下寻找。标准目录与系统相关。一般标准头文件用尖括号,用户自定义文件用双引号

#define

C语言中允许用一个标识符来表示一个字符串,该标识符称为“宏”,在编译预处理时宏会被无条件的替换为其代表的字符串参与之后的编译。宏的定义由#define完成

无参宏定义

标准形式为

1
#define 标识符 字符串

一般用于定义一些常量。

带参宏定义

一般形式为

1
#define 标识符(标识符,标识符,......) 字符串

其中第一个标识符为宏名,括号中的标识符为形参。宏展开时先用字符串替换宏,在用实参替代相应形参,例如

1
2
3
4
5
6
7
8
#include <stdio.h>
#define square(x) ((x)*(x))
int main()
{
int a=2;
printf("%d",square(a));
return 0;
}

在这个程序中square(a)会被无条件替换为a*a,然后将a的值代入算得为2

注意由于宏的展开是在编译之前无条件地直接替换,所以要注意带参宏的括号,比如对上面的宏的字符串,如果直接写成x*x

在计算square(2+3)时会直接替换为2+3*2+3,显然是错误的。

#undef

用于取消宏定义。比如我们包含了一个头文件,现在要定义一个变量假设叫做number,我们不知道这个number是否在头文件中已经定义过了,为了防止宏名的冲突可以先

1
#undef number

再来定义变量

条件编译

功能类似于if语句,但注意一切预处理发生在编译之前,所以常量表达式必须时整型而不能含有sizeof,枚举型和强制类型转换运算符。条件编译有三种形式。

1
2
3
4
5
6
#if 常量表达式
程序
#elif 常量表达式
程序
......
#endif

程序依次判断#if和#elif中常量表达式的值,非0时执行其后的程序

1
2
3
4
5
6
#ifdef 标识符
程序
#elif 常量表达式
程序
.......
#endif

首先判断#ifdef后的标识符是否已经被#define定义过,是则执行后面的程序,#elif与第一种用法相同

1
2
3
4
5
6
#ifndef 标识符
程序
#elif 常量表达式
程序
.......
#endif

先判断#ifndef后的表达式是否已经被定义过,未被定义过则执行后面的程序块

条件编译只会编译符合条件的程序段,所以生成的目标代码比if语句短

在条件编译中可以使用defined运算符,defined(标识符)defined标识符 可以判断标识符是否已经被#define定义过,配合逻辑运算以及#if可以用来判断多个宏是否存在

assert断言

assert宏包含在头文件assert.h中,形式为assert(condition),若condition为真(非零)则什么都不会发生,若为假则会输出错误信息并终止程序执行,可以用来判断是否出现非法数据。

频繁调用assert会影响程序性能,可以通过在#include<assert.h>前插入#define NDEBUG来禁用assert

数组

数组是同类型的数据在内存中连续存放的集合,下标小的元素在前,小的在后。数组的下标都是从0开始计数

一维数组

一维数组只有一个下标,用于表示一个线性数据队列,声明一维数组的形式为

1
存储类型 类型修饰 数据类型说明 数组名[常量表达式]={初值表}

存储类型即之前提到的autoexternstatic,register等,类型修饰为constvolatile(前者表示数组不可修改,后者表示可以被其他程序修改),数组名是一个标识符,属于地址常量,储存着数组首地址,常量表达式表示数组的大小。数组在声明时必须确定其大小。初值表是可选项,用于初始数组元素。当有初值表时[]里的常量表达式可以忽略,数组的大小即为初值表中元素的个数。在给定大小后初值表中的元素个数可以小于数组长度,此时只会对前几个元素进行初始化。

要访问数组里的元素要利用[]下标操作符,数组名[i]表示该数组下标为i的元素。由于编译器不会对下标越界做检查,所以要注意下标不要越界。

一位数组元素在内存中连续存放,比如对一个int型数组a={1,2,3},假设第一个元素首的地址为0x12ff74,由于一个整型变量占四字节,所以第二个元素的首地址为0x12ff78,第三个为0x12ff7c

注意不可以将两个数组直接做四则运算或者赋值,比如a={1,2,3},不可以再来个int b[3]=a,这是错误的(之前提到过a其实是一个地址值)

一维数组直接做函数参数的写法为 数据类型名 数组名[],例如

1
void bubble(int a[],int b[]);

传入一维数组作为函数参数其实是传地址

字符数组

字符数组也是一维数组,所以可以像一维数组那样声明,但是由于字符串也是字符数组,所以在初始化时可以直接=字符串,比如

1
char s[]="Hello";

注意字符串的长度为字符串的储存长度减一

一些常用的字符串处理函数

  • int strlen(char s[]); 求字符串长度
  • void strcpy(char t[],char s[]);将s的内容复制到t中
  • int strcmp(char s[],char t[]);比较字符串s与t,相等时返回0,前面的字符串大时返回正值,后面的字符串大时返回负值
  • char *strcat(char t[],char s[]); 将s字符串s连接到t的后面
  • int strstr(char cs[],char ct[]);判断字串函数,若后面的是前面的子串返回ct在cs中第一次出现的位置,否则返回-1
  • int trim (char s[]); 删除字符串首尾的空白字符,返回删除后的字符串长度

多维数组

多位数组相当于数组的元素还是数组,如二维数组就相当于数组的元素是一维数组,二维数组可以用来描述矩阵或行列式,n维数组可以表示n维线性空间中的n维向量

n维数组需要n个下标来表示,形式为

1
类型说明 数组名[常量表达式1][常量表达式2]...[常量表达式n]={初值表}

常量表达式1称为第一维,常量表达式2称为第二维,以此类推

对多维数组元素的间访形式为

1
数组名[下标1][下标2]......[下标n]

由于内存是线性的,所以多维数组的数据在内存中依然线性储存。如前面所说多维数组就是数组套数组,所以储存的顺序为先是最第一级的第一个一维数组的第一个元素,然后将第一个一位数组的元素存完后存第二个一位数组,第一个二维数组中的一维数组存完后存第二个二维数组,以此类推

对多维数组初始化可以按照上面的储存顺序直接当作一位数组进行初始赋值,也可以按照数组套数组的逻辑方式赋值,以下两种方式都是正确的

1
2
int a[2][3][2]={1,2,3,4,5,6,1,2,3,4,5,6};
int b[2][3][2]={{{1,2},{3,4},{5,6}},{{1,2},{3,4},{5,6}}};

初值全部给出时第一维大小,可以省略不写

二维字符数组

对二维字符数组其实相当于一个字符串数组,除了在初始化时可以直接采用字符串,在其他地方都一样。

1
2
char a[3][10]={"apple","banana","cucumber"};
char b[][10]={"Java","Python","Rust","Golang"};

指针

变量在计算机中以字节为单位储存,每个字节都有其编码,称为地址。大部分类型变量都占有一至多个字节,变量的首字节的地址称为变量的地址,储存地址的变量称为指针

指针的声明

指针的声明形式为

1
类型说明符 数据类型 *标识符1,*标识符2,.....

指针类型是一种派生类型,声明中数据类型部分是其基类型,决定了指针能够指向何种类型的变量。

指针需要明确基类型是因为指针的操作要求知道变量所占的字节数。指针的类型可以为voidvoid类型的指针可以储存任何类型的变量的地址,但由于所占字节数为止所以不可以间访。。如果要间访可以通过强制类型转化将void类型转化为其他类型。

声明的指针在未赋值时处于无所指的状态,称为悬挂指针,其值为一个随机值。

指针的使用

指针的使用主要涉及取地址运算符&和间访运算符*

&为单目运算符,用法为&操作数 操作数必须是一个左值,可以得到操作数的地址。例如有以下声明

1
2
3
4
int a;
char ch;
double d;
float f[3];

&a,&ch,&d都合法,&f不合法,f是地址但不占内存单元。&的操作数必须占有内存单元

*也为单目运算符,用法为*操作数 其中的操作数必须是一个基类型明确的指针,作用为得到该指针所指的地址所储存的值,得到的值为一个左值。

注意所有单目运算符都为第二优先级,结合性为右结合,只能通过右结合来判断运算关系先后。

在向函数中传递参数时,之前提到的通过形参传值其实是将实参的值传递给形参,实参和形参的地址值是不同的,所以无法改变实参的值。若要通过函数改变实参的值必须传入实参的地址例如下面的程序

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
#include<stdio.h>
void exchange(int a,int b);
int main()
{
int a=2;
int b=3;
exchange(a,b);
printf("%d,%d",a,b);
return 0;
}
void exchange(int a,int b)
{
int t;
t=a;
a=b;
b=t;
}

这个程序的想法是直接将a b的值传给exchange中的a b,在exchangea b的确交换了,但是main中的a b并没有被交换,两个函数的a b储存的地址并不同,正确的做法是交换地址中储存的值

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
#include<stdio.h>
void exchange(int* a,int* b);
int main()
{
int a=2;
int b=3;
exchange(&a,&b);
printf("%d,%d",a,b);
return 0;
}
void exchange(int* a,int* b)
{
int t;
t=*a;
*a=*b;
*b=t;
}

指针运算

指针的算数运算仅限于加、减和自增自减,且以sizeof(基类型)为最小变化单位。如定义一个以T为基类型的指针pp++代表指针后移一个sizeof(T)p--代表指针前移一个sizeof(T)。两个不同类型的指针之间不能做算术运算,同类型的指针之间的加法运算没有意义。对同一个数组中的元素的地址可以进行减法运算,地址值较大的指针减去地址值较小的指针可以得到得到两个指针所指元素的下标相减,如下面的代码

1
2
3
4
5
6
7
8
9
#include<stdio.h>
int main()
{
int a[]={1,2,3,4,5};
int *p1=a;
int *p2=&a[4];
printf("%d",p2-p1);
return 0;
}

最后的执行结果是4,其他情况下指针相减也没有意义

指针之间可以进行关系运算,但是只有同一数组中元素的指针进行比较才有意义,指针之间比较的作用和指针所指元素的下标之间相比较的意义一样。

多级指针

直接指向整型、浮点型、字符型的指针称为一级指针,指向指针的指针称为多级指针

1
2
3
4
int a=0;
int *p1=&a; //p1是一个一级指针,其基类型为int型
int **p2=&p1; //p2是一个二级指针,其基类型为一个整型一级指针
int ***p3=&p2; //p3是一个三级指针

多级指针与一级指针的区别仅在于基类型不同

用指针表示数组

用指针表示一维数组

在指针的运算中说过一个T类型指针加上或减去一个整数相当于指针后移或前移sizeof(T),其效果相当于有数组中有一个元素a[i],将i加上或减去一个整数。所以我们可以另指针p指向数组中的一个元素,通过改变指针的指向后间访来访问一维数组中的元素

1
2
3
4
5
6
7
8
9
int *p1;
int *p2;
int *p3;
int a[5]={1,2,3,4,5};
int b;
p1 = a; /*相当于p=&a[0]*/
p2 = p1+2; /*此时p2指向a[2],即p2=&a[2]*/
p3 = p2-1; /*此时p3指向a[1]*/
b=*p3; /*此时b等于a[1],即b=2*/

通过上面的例子不难发现,若p=a,则a[i]==*(p+i),在运算中我们可以直接ap可以混用

同时指针也可以指向一些常量

1
2
char *p1="string";
char *p2=(int[]){2,3};

此时指针内容不可改动

用指向数组基本元素的指针表示多维数组

多维数组在内存中依然线性存放,根据这个原理我们可以利用指向数组基本元素的指针来表示多维数组。

1
2
3
4
5
6
int a[2][3]={{1,2,3},{4,5,6}};
int i,j;
int *p=&a[0][0];
按照上述声明则a[i][j]=*(p+i*3+j)
对于任意高维数组a[m1][m2]...[mn],假设指针p=&a[0][0]...[0],
则a[n1][n2]...[nn]=*(p+n1*m2+n2*m3+...+nn*mn)

用指向数组的指针表示数组

由于数组也是一种数据类型,我们可以直接用一个以数组为基类型的指针指向数组。

1
2
int a[2][3][2];
int (*p)[3][2]=a;

以上是指向一个三位数组的指针,此时pa等价,p的移动以该三维数组中的二维数组的长度为基本单位。其他维数的指针声明类似。

这种指向数组的指针本质上是一个指针的指针,如上面这个三维数组,这个p储存的其实是第一个二维数组的首地址的地址

指针数组

数组元素为指针的数组称为指针数组,声明形式如下

1
2
3
类型说明 *指针名[数组长度]		例如:
int *p[3];
char *str[]={"Hello","World"};

其实二维数组就相当于一个一维指针数组。

指针函数

返回值为指针的函数声明形式如下

1
2
类型说明 *函数名(参数表) 如:
char *strcpy(char *t,char *s);

指向函数的指针

任何一个函数经编译之后都会形成对应的一系列机器指令,这些指令的机器码在内存中连续存放,而首次执行指令的地址被称为该函数的入口地址并用函数名进行标识,指向函数的指针就是指向函数入口地址的指针变量,简称函数指针。换言之,函数指针是以函数的入口地址为其值的指针变量。通过函数指针,可以调用函数。函数指针声明方式如下

1
2
3
类型 (*函数指针名)(形参表) 如:
char (*p)(char *,char *);
p=strcpy;

函数指针指向的函数的类型和形参表必须与函数指针完全一致,调用函数指针指向的函数时可以使用p(s,t),也可以使用(*p)(s,t)

带参数的main函数

main函数参数又称为命令行参数,指在操作系统环境下执行一个程序时所提供的参数,它提供了程序运行时,向程序提供参数的一种途径。需要使用命令行参数时,main函数在编写时需要带上参数

1
2
3
4
int main(int argc,char *argv[])
{
…………
}

其中argc为参数个数,argv[]为存放参数字符串的数组。命令行参数只能传递字符串数组。

假设编译完成的程序为test.exe,储存在D盘根目录下,则调用test.exe并向其传递命令行参数的方法为

1
D:\test.exe arg1 arg2

参数之间以空格分开。

结构与联合

结构与联合都属于C语言的构造类型。 对结构与联合而言,都需要先定义结构类型和联合类型,然后再根据已经定义的结构类型来定义对应的结构变量,以及用已经定义的联合类型来定义对应的联合变量。

结构

结构类型的声明

结构类型是一种将不同数据类型的成员组织起来所形成的一种新的构造类型。结构类型又称为结构体、聚合类型。C语言中的结构在其他程序设计语言中往往称为记录。

结构的声明格式如下:

1
2
3
struct 结构类型名{
成员声明表
};

声明结构类型是用户创建自定义数据类型的过程,不进行存储分配。其中成员声明表说明了成员的数据结构以及类型,和声明变量的格式一致。例如以学生的学习情况描述为例,科声明如下的结构类型:

1
2
3
4
5
6
7
struct stu_study{ 	/* stu_study是结构类型名 */
char num[12]; /* 学号成员,字符数组类型 */
char name[9]; /* 姓名成员,字符数组类型 */
char sex;/* 性别成员,字符类型 */
int English; /* 英语成员,整型 */
int Math,Physics,C;/* 数学、物理、C语言,整型 */
};

在声明结构时应注意以下几点

  • 同一结构内的成员不能同名
  • 结构不允许递归定义,即不能再结构中出现包含自身类型的实例,但是可以出现指向自身类型的指针
  • 结构可以嵌套定义,即一个结构体中可以包含其他结构变量
  • 同一结构的成员在内存中连续存放,成员存储分配按照结构声明体中不同声明从上向下,同一声明中从左向右的顺序进行,每个成员所占存储空间的大小由其类型确定。由于需要边界对齐,一般用sizeof运算符确定结构变量所占的存储空间的实际大小
  • 一般而言,成员所占的存储的大小必须在结构类型声明时确定,但根据ISO/IEO 9899标准,作为一个特例,结构中最后一个成员可以具有不完全的数组类型,即最后一个数据成员可以为动态数组

结构变量的定义

声明结构变量有两种方法,一种是结构类型和结构变量同时声明,另一种是先声明结构类型再声明结构变量。我们还可以使用typedef定义结构类型名方便声明变量。初始化时可以用花括号给定初值表序列

1
2
3
4
5
//结构类型和变量同时声明
struct point{
int x;
int y;
}p1,p2;//可以在struct前加上存储类型修饰
1
2
3
4
5
6
//先声明结构类型再声明结构变量
struct point{
int x,y;
};
struct point address;//声明了一个point类型变量address
//可以在struct前加上存储类型修饰
1
2
3
4
5
6
//使用typedef
typedef struct point{
int x,y;
}POINT;
POINT address;//声明了一个point类型变量address
//可以在struct前加上存储类型修饰
1
2
3
4
5
6
7
//初始化实例
struct point{
int x;
int y;
}p1={12},p2={34};

struct point p3={5,6};

结构类型的引用

结构变量可以进行的操作有赋值,取地址,间接访问。赋值时即将一个结构变量的所有成员的值赋给另一个同类型结构变量的对应成员,如果有指针类型成员则等号两边的结构变量的对应指针类型成员会指向同一块内存区域

对结构变量中的成员的引用有两种方法,一种是通过结构名加.运算符,一种时通过指向某个结构变量的指针加->运算符

1
2
3
4
5
6
7
//引用结构变量中的成员
struct point{
int x,y;
}p1={1,2};
int point_x=p1.x;
struct point *ptr1=&p1;
int point_y=ptr1->y;

嵌套结构类型变量的成员访问只需注意按层次访问即可

1
2
3
4
5
struct address{
char name[20];
struct point coordinates;
}p1={"home",{20,10}};
int home_x=p1.cooedinates.x;

结构类型变量可以有数组,也可以作为函数的返回值。

联合

联合类型成员共享储存,联合类型的声明如下:

1
2
3
union 联合名称{
联合成员声明
};

例如

1
2
3
4
5
union chl {
char c; /*字符成员*/
short h; /*短整型成员*/
long l; /*长整型成员*/
};

联合变量的声明与结构变量一样,C99以前在初始化时只能对第一个成员初始化,C99开始可以在初值表中用.访问指定成员并对其初始化,例如

1
2
union chl v={'a'};
union chl w={.l=0x12345678}

联合成员的引用与结构成员的引用完全相同

字段结构

相邻的若干个二进制位称为字段。字段结构是一种特殊的结构类型,其成员都是字段类型,成员的取值为无符号整数。 组成字段的二进制位的数目成为该字段的宽度。例如:

1
2
3
4
struct w16_bytes {    /* byte0为低字节, byte1为高字节 */
unsigned short byte0: 8;
unsigned short byte1: 8;
} ;

字段结构需要注意以下几个问题:

  • 字段的宽度n必须大于0且小于等于机器字的宽度,比如在32位系统中,n>0且n<=32。
  • 字段成员的数据类型为无符号类型,应足够容纳该字段相应宽度的数据。比如,字段宽度为9,则该字段类型不能为unsigned char,可以为unsigned short, unsigned int, unsigned long
  • 所有字段成员的数据类型应该相同,否则会产生空位
  • 字段结构的成员不能取地址

文件的输入与输出

文件概述

文件指内存以外的介质上以某种形式组织起来的数据集合或设备。文件的输入输出即对文件的读写

文件分为文本文件和二进制文件,文件的读写方式分为顺序读写和随机读写,方法有调用stdio.h中的库函数调用io.h中的接口函数。

C语言的标准I/O是基于流的输入输出,程序员只需要按照标准I/O提供的库函数对流进行I/O操作就可以完成数据的输入输出。

流的特点:有缓冲区,动态性,实时性。流的类型:文本流,二进制流(对应两种类型的文件)

文件从操作步骤分为三步

  1. 打开文件,建立文件指针或文件描述符与文件间的关系。
  2. 对文件进行读写操作
  3. 关闭文件,取消文件指针或文件描述符与文件间的联系

FILE指针和标准流式文件

FILEsidio.h中定义的一种结构类型,VC中的定义如下

1
2
3
4
5
6
7
8
9
10
11
struct _iobuf {
char *_ptr; /*当前读写位置指针,即缓冲区中偏移量*/
int _cnt; /*缓冲区空满*/
char *_base; /*指向缓冲区基地址指针*/
int _flag; /*文件状态标志*/
int _file; /*文件描述符*/
int _charbuf; /*非缓冲回送字符*/
int _bufsiz; /*缓冲区大小*/
char *_tmpfname; /*临时文件描述符*/
};
typedef struct _iobuf FILE;

FILE类型的结构变量在打开文件时由系统自动创建,其成员的值也只对系统进行赋值和更新。在VC6.0的头文件stdio.h中,有这样一个声明extern FILE _iob[];,引用了一个数组_iob,该数组元素的个数称为系统中流的数目,C语言标准规定流的数目,也就是打开文件的数目不得小于8。同时在VC中通过#define FOPEN MAX 20规定了一个程序中打开文件的最大数目为20。

VC6.0中通过如下声明定义了系统的标准流式文件

1
2
3
#define stdin (&_iob[0])
#define stdout (&_iob[1])
#define stderr (&_iob[2])

三个声明分别为指向数组_iob的下标为0,1,2的元素的指针,stdin代表标准输入设备,通常是键盘,stdout代表标准输出设备,通常是显示器,stderr代表标准出错设备,始终是显示器。(注意前面说过设备也是文件

流式文件的顺序输入与输出

流式文件的输入输出库函数缺省情况下是按顺序读写方式工作的。

打开文件

1
FILE *fopen(const char *filename, const char *mode);

fopen函数打开由形参filename指向的字符串所指定的文件,字符串中包含文件名、扩展名、还可以包含驱动器名和目录路径。如果文件打开成功,fopen函数返回一个文件指针(数组_iob某个元素的地址),否则返回NULL。形参mode指向字符串,该字符串设置文件的打开方式。

mode有以下几种情况:r表示以只读方式打开文本文件,w表示以只写方式打开文本文件并将原文本文件清零(若文件不存在则创建新的文件),a表示只在文本文件尾部进行写。在这三者的基础上加上b表示对二进制文件操作,如rb wb ab;加上+表示以即可读又可写方式打开,区别在于 r+将文件指针指向文件头,原内容不会被清零,w+将文件指针指向文件头后将原内容清空,a+将文件指针指向文件尾。+b可同时添加,顺序无所谓。

关闭文件

1
int fclose(FILE *);

参数为文件指针,fclose函数关闭文件指针所指文件。 它使缓冲区中尚未存盘的数据全部强制性的存盘,释放打开文件时系统分配的输入输出缓冲区,取消FILE 指针与文件之间的映射关系。 如果文件正常关闭,fclose函数返回0,否则返回EOF

文件重定向

使原本指向A文件的文件指针指向B文件

1
FILE *freopen(const char * filename, const char *mode, FILE *fp);

该函数会先关闭fp指向的文件然后以指定方式打开文件filename并使fp指向它

基于字符的文件读写

1
2
3
4
5
6
7
8
9
10
int fgetc(FILE * ); /*从文件中读一个字符*/
int fputc(int, FILE *); /*向文件中写一个字符*/
int fgetchar(void); /*从标准输入设备中读一个字符*/
int fputchar(int); /*向标准输出设备中写一个字符*/
int ungetc(int c, FILE * stream); /*回送字符到流stream*/

//读取字符到文件尾时会返回EOF
//常用的getchar()与putchar函数的定义如下
#define getchar() getc(stdin)
#define putchar(c) putc((c), stdout)

基于字符串的文件读写

1
2
3
4
char *gets(char *s);		//从标准输入流读取一行字符到s指向的内存空间,不读取换行符,读到文件尾或错误时返回NULL
int puts(const char *s); //向标准输出流输出s指向的字符串
char * fgets(char *s, int n, FILE *stream);//从stream中读取n个字符到s指向的内存空间中,不读取换行符,读到文件尾或错误时返回NULL
int fputs(const char *s, FILE *stream);//向stream输出s指向的字符串

文件的格式读写

1
2
3
4
5
6
int   printf(const char *format, ...);//向标准输出流打印格式化字符串,返回成功打印字符的个数
int scanf(const char *format, ...);//从标准输入流格式化读入数据,遇到遇到空格或回车就停止,不读取空格回车,返回参数列表中被成功赋值的参数个数
int fprintf(FILE *stream, const char *format, ...);//向文件stream写入格式化字符串,返回成功写入字符的个数,失败返回负数
int fscanf(FILE *stream, const char *format, ...);//从文件stream中格式化读入数据,遇到遇到空格或回车就停止,不读取空格回车,返回参数列表中被成功赋值的参数个数,遇到文件尾返回EOF
int sprintf(char *buffer, const char *format, ...);//向buffer指向的内存写入格式化字符串,返回成功写入字符的个数,失败返回负数
int sscanf(const char *buffer, const char *format, ...);//从buffer指向的内存中格式化读入数据,遇到遇到空格或回车就停止,不读取空格回车,返回参数列表中被成功赋值的参数个数。

判断文件尾还有一个常用函数

1
int feof(FILE *stream);

若文件结束返回非0值,若文件未结束返回0

流式文件的随机输入输出

定位文件读写位置使用

1
int fseek(FILE *stream, long offset, int whence);

定位值的计算是从基准点whence开始,加上以字节为单位的偏移量offset所得,即文件读写位置=基准点+偏移量。基准点有三种取值,在stdio.h中有定义

1
2
3
#define SEEK_SET   0  //表示以文件起始位置为基准点 
#define SEEK_CUR 1 //表示以文件当前位置为基准点
#define SEEK_END 2 //表示以文件尾部位置为基准点

文件定位相关函数还有:

1
2
3
4
long  ftell(FILE *stream);//获取文件读写位置
int fgetpos(FILE *stream, fpos_t *pos);//将stream的位置保存到pos中,其中pos指向fpos_t类型的变量
int fsetpos(FILE *stream, const fpos_t *pos);//将pos指向的位置赋给stream
void rewind(FILE *stream);//将读写位置重置为起始位置
  • Title: C语言程序设计
  • Author: Yizumi Konata
  • Created at : 2020-01-21 10:56:32
  • Updated at : 2024-06-06 23:04:54
  • Link: https://zz12138zz.github.io/2020/01/21/C语言与程序设计/
  • License: This work is licensed under CC BY-NC-SA 4.0.
Comments