C语言程序设计
文章目录
- C语言程序设计
- 第一章 基本数据类型
- 第二章 基本算数运算符
- 第三章 键盘输入和屏幕输出
- 第四章 选择控制结构
- 第五章 循环控制结构
- 第6章 函数与模块化程序设计
- 第7章 数组和算法基础
- 第8章 指针
- 第9章 字符串
- 第10章 指针和数组
- 第11章 结构体和数据结构基础
- 第12章 文件操作
- 第13章 简单的游戏设计
- 第14章 总结
第一章 基本数据类型
1.1常量与变量
C语言程序处理的数据有常量(Constant)和变量(Variable)两种形式。
1.1.1常量
-
顾名思义,常量就是在程序中不能改变其值的量。按照类型划分有以下几种:整型常量、实型常量、字符串字面量(string literal)和枚举常量。
-
C程序中的整形(Integer)常量通常习惯上用我们熟悉的十进制(Decimal)数来表示,但事实上他们都是以二进制形式存储在计算机内存中的。二进制数表示不直观方便,因此有时也将其表示为八进制(Octal)和十六进制(Hexadecimal),编译器会自动将其转换为二进制形式存储。
-
C程序中的实型常量有十进制小数和指数两种表现形式,实型常量有单精度、双精度和长双精度之分,但无有无符号和无符号之分。
1.1.2变量
-
变量不同于常量,其值在程序执行过程中是可以改变的。在C程序中,变量在使用前必须先定义。定义变量的一般形式为:
-
类型关键字 变量名;
-
关键字(Keyword)shiC语言预先规定的、具有特殊意义的单词。这里的类型关键字用于声明变量的类型。变量的类型觉得定了编译器为其分配内存单元的字节数、数据在内存单元中的存放形式、该类型变量合法 的取值范围以及该类型变量可参与的运算种类。
例:
int main(void)
{
int a; //用关键字int指定变量a的类型
float b; //用关键字float指定变量b的类型
char c; //用关键字char指定变量c的类型
a=1; //为int型变量a赋值整型常量1
b=1.5; //为float型变量b赋值实型常量1.5
c='A'; //为char型变量c赋值字符型常量'a'
return 0;
}
-
用标准C语言编写的程序都是以main()作为开头的,它指定了C程序执行的起点,在C程序中只能出现一次。一个C程序必须有且只能有一个用main作为名字的函数,这个函数称为主函数。
-
main后面圆括号内的void表示它没有函数参数。main前面的int表示函数执行后会返回操作系统一个整型值,在main函数的函数体中的最后一条语句使用return语句返回了这个值,通常返回0表示程序正常结束。C程序总是从主函数开始执行,与它在程序中的位置无关。
-
主函数中的语句(Statement)用花括号{和}括起来。一般情况下,C语句是以分号结尾的。
-
变量名是用户定义的标识符(Identifier),用于标识内存中一个具体的存储单元,在这个存储单元中存放的数据称为变量的值。
-
变量名的命名应遵守以下基本的命名规则(Niming Rules):
(1)标识符只能由英文字母、数字和下划线组成,建议使用见名知意的名字为变量名,可以使用英文单词大小写混排或中间加下划线的方式,而不要使用汉语拼音;
(2)标识符必须以字母或下划线开头;
(3)不允许使用C关键字为标识符命名;
(4)标识符可以包含任意多个字符,但一般会有最大长度限制,与编译器相关,不过大多数情况下不会达到此限制。
-
标识符是区分大小写(即对大小写敏感)的。在为变量赋值时,等号两边的空格不是必须的,增加空格只是为了增强程序的的可读性(Readability)。
-
C语言允许在定义变量的同时对变量初始化。
例:
int main(void)
{
int a=1; //定义整形变量a并对其初始化
float b=2.5; //定义实型变量b并对其初始化
char c='A'; //定义字符型变量c并对其初始化
return 0;
}
-
程序中以//开始到行末结束的内容是注释(Comment),本书程序的单行注释采用了C++风格的注释,如果需要跨行书写,则每一行都必须以//开始,也可采用C风格注释,即将跨行书写的内容放到/* 和*/之间
-
在一条语句中,可同时定义多个相同类型的变量,多个变量之间用逗号作分隔符(Separator),其书写的先后顺序无关紧要。
例:int a=0 ,b=0 ,c=0;
1.2简单的屏幕输出
- C的标准输入/输出函数printf()的作用是输出一个字符串,或者按指定格式和数据类型输出若干变量的值
例:
#include<stdio.h>
int main(void)
{
int a=1;
float b=2.5;
char c='A';
printf("a=%d\n",a); //按整形格式输出变量a的值
printf("b=%f\n",b); //按实型格式输出变量b的值
printf("c=%c\n",c); //按字符型格式输出变量c的值
printf("End of program\n");//输出一个字符串
return 0;
}
- 程序第一行以#开头而未以分号结尾的不是C语句,而是C的编译预处理命令(Preprocessor Directives)。
- 尖括号内的文件称为头文件,h为head之意,std为standard之意,i为input之意,o为output之意。编译预处理命令#include可使头文件在程序中生效。它的作用是:将写在尖括号内的输入/输出函数的头文件stdio.h包含到用户源文件中。
- 在C语言中,用一对双引号括起来的若干字符,称为字符串(String)。
1.3数据类型
- 在高级程序设计语言中引入数据类型(Date Type)的主要目的是便于在程序中对它们按不同方式和要求进行处理。由于不同类型的数据在内存中占用不同大小的存储单元,因此他们所能代表的数据的取值范围各不相同,此外,不同类型的数据的表达形式及其可以参与的运算种类也有所不同。
数据类型分类 | 关键字 | 变量声明实例 | ||
---|---|---|---|---|
基本类型 | 整型 | 基本整型 | int | int a; |
长整型 | long | long int a;或long a; | ||
长长整型 | long long | long long int a;或long long a; | ||
短整型 | short | short int a;或short a; | ||
无符号整型 | unsigned | unsigned int a; unsigned long b; unsigned short c; | ||
实型(浮点型) | 单精度实型 | float | float a; | |
双精度实型 | double | double a; | ||
长双精度实行 | long double | long double a; | ||
字符型 | char | char a; | ||
枚举类型 | enum | enum response{no,yes,none}; enum response answer; | ||
构造类型 | 数组 | - | int score[10]; char name[20]; | |
结构体 | struct | struct date{int year;int month;int day;};sttruct date d; | ||
共用体 | union | union{int single;char spouseName[20];struct date divorcedDay;}married; | ||
指针类型 | - | int *ptr; char *pStr; | ||
无类型 | void | void Sort(int array[],int n); void *malloc(unsigned int size); |
1.4计算变量或数据类型所占内存空间的大小
- 计算机的所有指令和数据都保存在计算机的储存部件——内存里,内存保存数据块,数据可被随机访问,但掉电即失。内存中的存储单元是一个线性地址表,是按字节(Byte)进行编址的,即每个字节的存储单元都对应着一个唯一的地址。在程序设计语言中,通常用字节数来衡量或数据类型所占内存空间的大小。
- sizeof是C语言的关键字,不是函数名。sizeof()是C语言提供的专门用于计算指定数据类型字节数的运算符。
例:
#include<stdio.h>
int main(void)
{
printf("char %d\n",sizeof(char));
printf("int %d\n",sizeof(char));
return 0;
}
1.5变量的赋值和赋值运算符
-
赋值运算符(Assignment Operator)用于给变量赋值。由赋值运算符及其两侧的操作数(Operand)组成的表达式称为赋值表达式(Assignment Expression)。
-
C语言没有提供专门的赋值语句。赋值操作是通过在赋值表达式后面加分号构成表达式语句来实现的。
-
虽然在书写形式上赋值运算符与数学中的等号相同,但两者的含义在本质上是不同的。赋值运算符的含义是将赋值运算符右侧的表达式的值(简称为右值)付给左侧的变量。
-
在计算含有不同类型运算符的表达式时,要考虑运算符的优先级(Precedence),根据优先级确定运算的顺序,即先执行优先级高的运算,然后再执行优先级低的运算。
-
结合性:当运算符的优先级相同时,根据运算符的结合性来确定运算顺序。运算符的结合性有两种:一种是左结合,即自左向右计算;另一种是右结合,即自右向左计算。
例:
x = 1;
- 执行时是从右向左把最右侧的表达式的值依次赋值给左侧的变量。像下面这种形式的赋值表达式称为多重赋值(Multiple Assignment)表达式,一般用于为多个变量赋予相同的值。
例:
a=b=c=0;
a=(b=(c=0)); //两表达式等价
第二章 基本算数运算符
2.1C运算符和表达式
2.11算数运算符和表达式
-
C语言中的算术运算符(Arithmetic Operators),由算术运算符及其操作数组成的表达式称为算术表达式。其中操作数(Operand)也称为运算对象,它既可以是常量、变量,也可以是函数。
-
算术运算符的优先级和结合性
运算符 | 含义 | 需要的操作数个数 | 运算实例 | 运算结果 | 优先级 | 结合性 |
---|---|---|---|---|---|---|
- | 取相反数(Opposite Number) | 1个(一元) | -1 -(-1) | -1 1 | 最高 | 从右向左 |
* / % | 乘法(Multiplication)除法(Division)求余(Modulus) | 2个(二元) | 12/5 12.0/5 11%5 | 2 2.4 1 | 较低 | 从左向右 |
+ - | 加法(Addition)减法( Subtraction) | 2个(二元) | 5+1 5-1 | 6 4 | 最低 | 从左向右 |
- 只需一个操作数的运算符称为一元运算符(或单目运算符),需要两个操作数的运算符称为二元运算符(或双目运算符),需要三个操作数的运算符称为三元运算 符(或三目运算符)。条件运算符是C语言提供的唯一一个三元运算符。
- 不同于数学中的算术运算,C语言中的算数运算的结果与参与运算的操作数类型相关。例如,1/2与1.0/2运算的结果值是不同的,前者是整数除法(Integer Division),后者是浮点数除法(Floating Division)。
- C语言中,求余运算限定参与运算的两个操作数必须为整形,不能对两个实型数据进行求余运算。将求余运算符的左操作数作为被除数,左操作数作为除数,而整除出后的余数(Remainder)即为求余运算的的结果,余数的符号与被除数的符号相同。
2.1.2复合的赋值运算符
- 复合的赋值运算符(Combined Assignment Operators)实现的简单的赋值(Shorthang Assignment)方法。
例:
n *= m+1; //等价于
n = n*(m+1);
- 相对于它的等价式而言,复合的赋值运算书写形式更简洁,而且执行效率也更高一些。
2.1.3增1和减1运算符
-
对变量进行加1或减1是一种很常见的操作,为此,C语言专门提供了执行这种功能的运算符,即增1运算符(Increment Operator)和减1运算符(Increment Operator)。
-
增1和减1运算符都是一元运算符,只需要一个操作数,且操作数须有“左值性质”,必须是变量,不能是常量表达式。增1运算符是对变量本身执行加1操作,因此也称为自增运算符。减1运算符是对自变量本身执行减1操作,因此也称自减运算符。
-
增1运算符既可以写在变量的前面(如++x),也可以写在变量的后面(如x++)。二者实现的功能有所差异,写在变量的前面即作为前缀(Prefix)运算符时,是在变量使用前先对其执行加1操作,而写在变量的后面即作为后缀(Postfix)运算符时,是先使用变量当前值,然后对其进行加1操作。
-
自加自减运算符优先级高于其它一元运算符
-
增1和减1运算的例子
语句 | 等价的语句 | 执行该语句后m的值 | 执行该语句后n的值 |
---|---|---|---|
m=n++; | m=n; n=n+1; | 3 | 4 |
m=n–; | m=n; n=n-1; | 3 | 2 |
m=++n; | n=n+1; m=n; | 4 | 4 |
m=–n; | n=n-1; m=n; | 5 | 2 |
2.2宏常量与宏替换
- 宏常量(Macro Constant)也称符号常量(Symbolic Names or Constants),是指用一个标识符号来表示的常量,这时该标识符号与此常量是等价的。宏常量是由宏定义编译预处理命令来定义的。宏定义的一般形式为:
#define 标识符 字符串
- 同printf()函数一样,scanf()函数也是C的标准输入/输出函数。scanf()函数用于从键盘输入一个数,%lf指定输入的数据类型为双精度实型,&是必要的,&称为取地址运算符,&r指定了用户输入数据存放的变量的地址。
- 圆的周长和面积计算公式中用到了圆周率π,而π值在程序中使用一个常数近似表示的,像这种在程序中直接使用的常数,称为幻数(Magic Number)。
例:使用宏常量定义π,编程从键盘输入圆的半径r,计算并输出圆的周长和面积。
#include<stdio.h>
#define PI 3.14159 //定义宏常量PI
int main(void)
{
double r;
printf("Input r:"); //提示用户输入半径的值
scanf("%lf",&r); //以双精度实型从键盘输入半径的值
printf("circumference = %f\n",2*PI*r); //编译时PI被替换为3.14159
printf("area = %f\n",PI*r*r); //编译时PI被替换为3.14159
return 0;
}
- 宏定义中的标识符被称为宏名(Macro Name)。为了与源程序中的变量名有所区别,习惯上用字母全部大写的单词来命名宏常量。将程序中出现的宏名替换成字符串的过程称为宏替换(Macro Substitution)。
- 注意,宏定义中的宏名与字符串之间可有多个空白符,但无需加等号,且字符串后面一般不以分号结尾,因为宏定义不是C语句,而是一种编译预处理命令。
- 宏替换时是不做任何语法检查的,因此,只有在对被宏展开后的源程序进行编译时才会发现语法错误。宏替换只是“傻瓜式”的字符换替换,极易产生意想不到到的错误,例如,若在字符串后加分号,则宏替换时会连同分号一起进行替换。
2.3const常量
-
在声明语句中,只要将const类型修饰符放在类型名前,即可将类型名后的标识符声明为具有该类型的const常量 。
-
由于编译器将其放在只读存储区,不允许在程序中改变其值,因此,const常量只能在定义时赋值。
-
与宏常量对比,const常量的优点是它有数据类型,编译器能对其进行类型检查,某些集成化调试工具可以对const常量进行调试。
例:使用const常量定义π,编程从键盘输入圆的半径r,计算并输出圆的周长和面积。
#include<stdio.h>
int main(void)
{
const double PI = 3,14159; //定义实型的const常量PI
double r;
printf("Input r:");
scanf("%lf",&r);
printf("circumference = %f\n",2*PI*r);
printf("area = %f\n",PI*r*r);
return 0;
}
2.4自动类型转换与强制类型转换运算符
1.表达式中的自动类型转换
-
C编译器在对操作数进行运算之前将所有操作数都转换成取值范围较大的操作数类型,称为类型提升(Type Promotion)。
-
表达式中的自动类型转换规则
由低到高 | 数据类型 | 提升 |
---|---|---|
低 | int | char,short |
unsigned int | ||
long | ||
unsigned long | ||
double | float | |
高 | long double |
- 提升表示必然的转换,即将所有的char和short都提升为int,这一步称为整数提升(Integral Promotion)。
- float型数据在运算时一律转换为双精度(double)型,以提高运算精度。
- 在C99中整数提升还可以产生向unsigned int的转换,完成这一步的转换后,其他类型转换将根据参与运算的操作数类型按由低到高的方向转化。
2.赋值中的自动类型转换
-
在一个赋值语句中,若赋值运算符左侧(目标测)变量的类型和右侧表达式的类型不一样,则赋值时将发生自动类型转换。类型转换的规则是:将右侧表达式的值转换成左侧变量的类型。
-
自动类型转换是一把双刃剑,它给取整等某些运算带来方便的同时,也会给程序埋下了错误的隐患,在某些情况下有可能会发生数据信息丢失、类型溢出等错误。
-
一般而言,将取值范围小的类型转换为取值范围大的类型是安全的,而反之则是不安全的,好的编译器会发出警告。
3.强制类型转换运算符
-
强制类型转换(Casting)运算符简称强转运算符或转型运算符,它的主要作用是将一个表达式值的类型强制转换为用户指定的类型,它是一个一元运算符,与其他的一元运算符具有相同的优先级。通过下面方式可以把表达式的值转为任意类型:
-
(类型) 表达式
-
自动类型转化是一种隐式的类型转换,而强转运算符是一种显示的类型转换。类型强转就是明确地表明程序打算执行哪种类型转换,有助于消除因隐式的自动类型转换而导致的程序隐患。
例:
#include<stdio.h>
int main(void)
{
int m = 5;
printf("m/2 = %d\n",m/2); //整数除法运算,其结果仍为整数
printf("(float)(m/2) = %f\n",(float)(m/2)); //将表达式(m/2)整数相除的结果强转为实数型
printf("(float)m/2 = %f\n",(float)m/2);//先用(float)m将m的值强转为实型数据,然后再将这个实型数据与2进行浮点数除法运算
printf("m = %d\n",m); //(float)m只是将m的值强转为实型,但它不能改变变量m的数据类型
return 0;
}
程序运行结果:
m/2 = 2
(float)(m/2) = 2.000000
(float)m/2 = 2.500000
m = 5
2.5常用的标准数学函数
- 使用数学函数时,只要在程序的开头加上如下的预编译处理命令即可。
#include<math.h>
第三章 键盘输入和屏幕输出
3.1单个字节的输入/输出
3.1.1字符常量
-
C语言中的字符常量是用单引号括起来的一个字符。
-
把字符放在一对单引号里的做法,适用于多数可打印字符,但不适用于某些控制字符(如回车符、换行符等)。C语言中用转义字符(Escape Character)即以反斜线(\)开头的字符序列来描述特定的控制字符。
-
常用的转义字符
字符 | 含义 | 字符 | 含义 |
---|---|---|---|
’ \n ’ | 换行(Newline) | ’ \a ’ | 响铃报警提示音(Alert or Bell) |
’ \r ’ | 回车(不换行)(Carriage Return) | ’ \ " ’ | 一个双引号(Double Quotation) |
’ \0 ’ | 空字符(Null) | ’ \ ’ ’ | 单引号(Single Quotation Mark) |
’ \t ’ | 水平制表(Horizontal Tabulation) | ’ \ \ ’ | 一个反斜线(Backslash) |
’ \v ’ | 垂直制表(Vertical Tabulation) | ’ ? ’ | 问号(Question Mark) |
’ \b ’ | 退格(Backspace) | ’ \ddd ’ | 1到3位八进制ASCII码值所代表的字符 |
’ \f ’ | 走纸换页(Form Feed) | ’ \xhh ’ | 1到2位十六进制ASCII码值所表示的字符 |
- 注意,每次按下Tab键,并不是从当前光标位置向后移动一个Tab宽度,而是移到下一个制表位,实际移动的宽度视当前光标位置距相邻的下一个制表位的距离而定。
- 当转义序列出现在字符串中时,是按单个字符计数的。
- 目前计算机上广泛使用的字符集是ASCII码(美国标准信息交换码)字符集
- 为了解决跨语言、跨平台文本转换和处理,国际标准化组织(ISO)制定了更强大的编码标准——Unicode字符集,为各种语言中的每个字符设定统一且唯一的数字编号,所有字符统一用2个字节保存,也称宽字节字符。
3.1.2字符的输入/输出
-
getchar()和putchar()是C标准函数库中专门用于字符输入/输出的函数。
-
函数putchar()的作用是把一个字符输出到屏幕的当前光标位置。
-
函数getchar()的作用是从键盘读取字符。当程序调用到getchar()时,程序就等待用户键入。
-
用户键入的字符首先放到输入缓存区中,直到用户按下回车键为止(回车符也会被放到输入缓冲区中)
-
当用户键入回车后,getchar()才开始从标准输入流中读取字符,并且每次调用只读取一个字符其返回值是用户输入的字符的ASCII码
-
若遇到文件结尾(End-Of-File),则会返回-1,且将用户输入的字符回显到屏幕上。
-
如果用户在按回车之前输入了多个字符,那么其他字符会继续留在缓冲区内,等待后续getchar()函数调用来读取,直到缓冲区中的字符(包括回车)全部读完后,才会等待用户按键。
例:从键盘输入一个大写英文字母,将其转换为小写字母后再显示到屏幕上
#include<stdio.h>
int main(void)
{
char ch;
printf("Press a key and then press Enter:");
ch = getchar(); //从键盘键入一个字符,按回车键结束输入,该字符被存入变量ch
ch = ch + 32; //将大写英文字母转换为小写英文字母
putchar(ch); //在屏幕上显示变量ch中的字符
putchar('\n'); //输出一个回车换行控制符
return 0;
}
- 程序第六行首先调用函数getchar()从键盘输入一个字符,然后将读入的字符即getchar()的返回值赋值给字符型变量ch。
- 注意,函数getchar()没有参数,函数的返回值就是从终端键盘读入的字符。
3.2数据的格式化屏幕输出
1.函数printf()的一般格式
printf(格式控制字符串);
printf(格式控制字符串,输出值参数表);
- 其中,格式控制字符串(Format String)是用双引号括起来的字符串,也称转换控制字符串,输出值参数表中可以有多个输出值,也可以没有(只输出一个字符串时)。
- 一般情况下,格式控制字符串包括两部分:格式转换说明(Format Specifier)和原样输出的普通字符。
- 格式转换说明由%开始,并以转换字符(Conversion Character)结束,用于指定各输出值参数的输出格式。
- 函数printf()的格式转换说明
格式转换说明 | 用法 |
---|---|
%d | 输出带符号的十进制整数,正数的符号省略 |
%u | 以无符号的十进制整数形式输出 |
%o | 以无符号的八进制整数形式输出,不输出前导符0 |
%x | 以无符号十六进制整数形式(小写)输出,不输出前导符0x |
%X | 以无符号十六进制整数形式(大写)输出,不输出前导符0x |
%c | 输出一个字符 |
%s | 输出字符串 |
%f | 以十进制小数形式输出实数(包括单、双精度),整数部分全部输出,隐含输出六位小数,输出的数字并非全部都是有效数字,单精度实数的有效位数一般为7位,双精度实数的有效位数一般为16位。%f适合输出像3.14这样小数位较少的实数。可以使实数输出的宽度较小 |
%e | 以指数形式(小写e表示指数部分)输出实数,要求小数点前必须有且仅有1位非零数字。%e适合于输出项1.0e+10这样的小数位较多的实数,可以使实数输出的宽度较小。在不同的编译环境下,使用%e输出数据所占用的列数略有差异 |
%E | 以指数形式(大写E表示指数部分)输出实数 |
%g | 自动选取f或e格式中输出宽度较小的一种使用,且不输出无意义的0 |
%% | 输出百分号% |
例:从键盘输入一个大写英文字母,将其转换为小写英文字母,将其转换为小写英文字母后,将转换后的小写英文字母及其十进制的ASCII码值显示到屏幕上
#include<stdio.h>
int main(void)
{
char ch;
printf("Press a key and then press Enter:");
ch = getchar();
ch = ch+32;
printf("%c,%d\n",ch,ch); //分别输出变量ch中的字符及其ASCII码值
return 0;
}
2.函数printf()中的格式修饰符
-
在函数printf()的格式说明中,还可以在%和格式符中间插入格式修饰符,用于对输出格式进行微调,如指定输出数据域宽(Field of Width)、显示精度(小数点后显示的小数位数)、左对齐等。
-
函数printf()的格式修饰符
格式修饰符 | 用法 |
---|---|
英文字母l | 修饰格式符d,o,x,u时,用于输出long型数据 |
英文字母L | 修饰格式符f,e,g时,用于输出long double型数据 |
英文字母h | 格式修饰符d,o,x时,用于输出short型数据 |
输出域宽m(m为整数) | 指定输出项输出时所占的数列。若m为正整数,当输出数据宽度小于m时,在域内右向靠齐,左边多余位补空格;当输出数据宽度大于m时,按实际宽度全部输出;若m有前导符0,则左边多余位补0.。若m为负整数,则输出数据在域内向左靠齐 |
显示精度.n(n为大于或等于0的整数) | 精度修饰符位于最小域宽修饰符之后,由一个圆点及其后的整数构成。对于浮点数,用于指定输出的浮点数的小数位数。对于字符串,用于指定从字符串左侧开始截取的子串字符个数 |
例:使用const常量定义π,编程从键盘输入圆的半径r,计算并输出圆的周长和面积。
#include<stdio.h>
int main(void)
{
const double pi = 3.14159;
double r,circum,area;
printf("Input r:");
scanf("%lf",&r);
circum = 2*pi*r;
area = pi*r*r;
printf("printf WITHOUT width or precision specification:\n");
printf("circumference = %f,area = %f\n",circum,area);
printf("printf WITH width and precision specification:\n");
printf("circumference = %7.2f,area = %7.2f\n",circum,area);
return 0;
}
- 注意,小数点也占1个字符位置
3.3数据的格式化键盘输入
1.函数scanf()的一般格式
scanf(格式控制字符串,参数地址表);
- 其中,格式控制字符串是双引号括起来的字符串,它包括格式转换说明符和分隔符两个部分
- 函数scanf()的格式转换说明符通常由%开始,并以一个格式字符结束,用于指定各参数的输入格式
- 函数scanf()的格式转换说明符
格式转换说明符 | 用法 |
---|---|
%d | 输入十进制整数 |
%o | 输入八进制整数 |
%x | 输入十六进制整数 |
%c | 输入一个字符,空白字符(包括空格、回车、制表符)也作为有效字符输入 |
%s | 输入字符串,遇到空白字符(包括空格、回车、制表符)时,系统认为读入结束(但在开始读之前遇到的空白字符会被系统跳过) |
%f或%e | 输入实数,以小数或指数形式输入均可 |
%% | 输入一个百分号% |
- 参数地址表是由若干变量的地址组成的列表,这些参数之间用逗号分隔。
- 函数scanf()要求必须指定用来接收数据的变量的地址,否则数据不能正确读入指定的内存单元
2.函数scanf()中的格式修饰符
-
与printf()类似,在函数scanf()的%和格式符中间也可以插入格式修饰符
-
函数scanf()的格式修饰符
格式修饰符 | 用法 |
---|---|
英文字母l | 加在格式符d、o、x、u之前,用于输入long型数据加在格式符f、e之前、用于double型数据 |
英文字母L | 加在格式f、e之前,用于输入long double型数据 |
英文字母h | 加在d、o、x之前,用于输入short型数据 |
域宽m(正整数) | 指定输入数据的宽度(列数),系统自动按此宽度截取所需数据 |
显示精度.n(0或正整数) | scanf()没有京都修饰符,即用scanf()输入实型数据时不能规定精度 |
忽略输入修饰符* | 表示对应的输入项在读入后不赋给相应的变量 |
- 在用函数scanf()输入数值型数据时,遇到以下几种情况都认为数据输入结束:
(1)遇到空格、回车符、制表符(Tab)
(2)达到输入域宽
(3)遇到非法字符输入
- 注意,如果函数scanf()的格式控制字符串中存在除格式说明符以外的其他字符,那么这些字符必须在输入数据时由用户从键盘原样输入
3.4特殊问题
3.4.1用%c输入字符时存在的问题
- 在用%c格式读入字符时,空格字符和转义字符(包括回车)都会被当作有效字符读入
3.4.2%c格式符存在问题的解决方法
- 方法一:用函数getchar()将数据输入时存入缓冲区中的回车符读入,以避免后面的字符型变量作为有效字符读入
#include<stdio.h>
int main(void)
{
int a;
char b;
float c;
printf("Please input an integer:");
scanf("%d",&a);
printf("integer:%d\n",a);
getchar(); //将存于缓冲区的回车符读入,避免在后面作为有效字符读入
printf("Please input a character:");
scanf("%c",&b);
printf("character:%c\n",b);
printf("Please input a float number:");
scanf("%f",&c);
printf("float:%f\n",c);
return 0;
}
- 方法二:在%c前面加一个空格,忽略前面数据输入时存入缓冲区中的回车符,避免被后面的字符型变量作为有效字符读入
#include<stdio.h>
int main(void)
{
int a;
char b;
float c;
printf("Please input an integer:");
scanf("%d",&a);
printf("integer:%d\n",a);
printf("Please input a character:");
scanf(" %c",&b); /*在%c前面加一个空格,将存于缓冲区中的回车符读入*/
printf("character:%c\n",b);
printf("Please input a float number:");
scanf("%f",&c);
printf("float:%f\n",c);
return 0;
}
}
第四章 选择控制结构
4.1算法的概念及其描述方法
4.1.1算法的概念
-
所谓算法(Algorithm),简单的说,就是为了解决一个具体问题而采取的确定、有限、有序、可执行的操作步骤
-
程序应由两部分组成:
(1)数据结构(Date Structure)是计算机存储、组织数据的方式,指相互之间存在一种或多种特定关系的数据元素集合。
(2)算法是对操作或行为(即操作步骤)的描述。算法代表着用系统的方法描述解决问题的策略。不同的算法可能用不同的时间、空间或效率来完成同样的任务
- 计算机进行问题求解的算法大致分为两类:
(1)数据算法,主要用于解决数值求解问题
(2)非数值算法,主要用于解决需要用逻辑推理才能解决的问题,如人工智能中的许多问题以及搜索、分类等问题都属于这类算法
- 衡量一个算法的正确性
(1)有穷性。算法包含的操作步骤应是有限的,每一步都应在合理的时间内完成,否则算法就失去了它的使用价值
(2)确定性。算法的每个步骤都应该是确定的,不要允许有歧义。
(3)有效性。算法中的每个步骤都应能有效执行,且能得到确定的结果。
(4)允许没有输入或者有多个输入。
(5)必须有一个或者多个输出。
4.1.2算法的描述方法
-
自然语言描述
用自然语言(Natural Language)描述算法时,可使用汉语、英语和数学符号等,通俗易懂,比较符合人们日常思维习惯,但描述文字显得冗长,在内容表达上容易引起理解上的歧义,不易直接转换为程序,所以一班适用于算法较为简单的情况。
-
流程图描述
流程图(Flow Chart)是描述程序的控制流程和指令执行的有向图,它是程序的一种比较直观的表示形式。用传统流程图描述的算法的优点是流程图可直接转化为程序,形象直观,各种操作一目了然,不会产生歧义,易于理解和发现算法设计中存在的错误;但缺点是所占篇幅较大,允许使用流程线,使用者可以使流程任意转向,降低程序的可读性和可维护性,使程序难于理解和修改。
-
NS结构化流程图描述
NS结构化流程图是由美国学者I.Nassi和B.Schneiderman于1973年提出的,NS图就是以这两位学者名字的首字母命名的。它的重要特点就是完全取代了流程线,这样迫使算法只能从上到下顺序执行,从而避免了算法流程的任意转向,保证了程序质量。与传统的流程图相比,NS图的另一个优点就是形象、直观,节省篇幅,尤其适合于结构化程序的设计。
-
伪码描述
伪码(Pseudocode)是介于自然语言和计算机语言之间的一种代码,它的最大优点是,与计算机语言比较接近,易于转换为计算机程序。书写无固定格式和规范,比较灵活。
4.2关系运算符与关系表达式
-
输入数据(Input)对数据进行计算和处理(Processing)输出运算结果(Output)只是一种常见的IPO形式程序结构,即顺序结构(Sequential Structure)在顺序结构中,只能自顶向下、按照代码书写的先后顺序来执行程序。赋值和数据的输入/输出是顺序结构中最典型性的操作,主要由表达式语句组成。表达式语句由表达式后接一个分号(;)构成。
-
选择结构(Seletion Structure),也称为分支控制结构。
-
简单的判断条件可用关系表达式来表示,复杂的条件可用逻辑表达式表示,关系运算实质上是比较运算。C语言中的关系运算符(Relation Operator)
-
C语言中的关系运算符及其优先级
运算符 | 对应的数学运算符 | 含义 | 优先级 |
---|---|---|---|
< | [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-8KnVSeO3-1650170280282)(https://bkimg.cdn.bcebos.com/formula/9c8dc6de7e31e08e2c3571b0dccda78c.svg)] | 小于 | 高 |
> | [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-KrE0tK3c-1650170280287)(https://bkimg.cdn.bcebos.com/formula/1683fa1a1d32204f7c26bdfeb6840378.svg)] | 大于 | 高 |
<= | [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-1UGAnl6b-1650170280291)(https://bkimg.cdn.bcebos.com/formula/5abe3656b502e1bfba948aaf2e7c5350.svg)] | 小于或等于 | 高 |
>= | [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-hxhRUOmR-1650170280294)(https://bkimg.cdn.bcebos.com/formula/b33249e39ddc764f15916bf5f5a87a5b.svg)] | 大于或等于 | 高 |
== | [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-hZYXmqNF-1650170280296)(https://bkimg.cdn.bcebos.com/formula/9033168bff43c45720c00e07c577b352.svg)] | 等于 | 低 |
!= | [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-jHIXXvwq-1650170280299)(https://bkimg.cdn.bcebos.com/formula/727d13d6c31a236a9c87349b4481edbd.svg)] | 不等于 | 低 |
- 不能与相应的数学运算符相混淆,否则将会产生语法错误,即编译错误(Compile Error)。
- 尤其注意的是,不要将==误写成=。前者是 相等关系运算符,而后者是赋值运算符。尽管关系表达式和赋值表达式都可用来表示一个判断条件,但二者的值可能是不同的,因而会影响到对条件真假的正确判断,进而导致运行时错误(Run-time Error),编译器通常不能识别这种误写。
- 用关系运算符将两个操作数连接起来组成的表达式,称为关系表达式(Relational Expression)。
- zaiC语言中,用非零值表示“真”,用零值表示“假”。
4.3用于单分支控制的条件语句
-
当条件P成立(为真)时,执行A操作,否则执行B操作;如果B操作为空(即什么也不做),则为单分支选择结构(Single Selection Structure);如果B操作不为空,则为双分支选择结构(Double Selection Structure),如果B操作中又包含另一个选择结构,则构成了一个多分支选择结构(Multiple Selection Structure)。
-
单分支选择结构:
if(条件表达式P)
语句A
例:使用单分支的条件语句编程,计算并输出两个整数的最大值
#include<stdio.h>
int main(void)
{
int a,b,max;
printf("Input a,b:");
scanf("%d,%d",&a,&b);
if(a>b)
max = a;
if(a<=b)
max = b;
printf("max = %d\n",max);
return 0;
}
4.4用于双分支控制结构的条件语句
- 双分支选择结构
if(表达式P)
{
语句1
}
else
{
语句2
}
例:使用双分支条件语句编程,计算并输出两个整数的最大值
#include<stdio.h>
int main(void)
{
int a,b,max;
printf("Input a,b:");
scanf("%d,%d",&a,&b);
if(a>b)
{
max = a;
}
else
{
max = b;
}
printf("max = %d\n",max);
return 0;
}
4.5条件运算符和条件表达式
-
条件运算符(Conditional Operator)是C语言中唯一的一个三元运算符(Ternary Operator),运算时需要三个操作数。
-
条件表达式
表达式1 ? 表达式2 : 表达式3
- 若表达式1的值非0,则该条件表达式的值是表达式2的值,否则是表达式3的值
例:使用条件运算符编程,计算并输出两个整数的最大值
#include<stdio.h>
int main(void)
{
int a,b,max;
printf("Input a,b:");
scanf("%d,%d",&a,&b);
max = a>b?a:b;
printf("max = %d\n",max);
return 0;
}
4.6用于多分支控制的条件语句
- 多分支选择结构
if(表达式1)
{
语句1
}
else if(表达式2)
{
语句2
}
else if(表达式n)
{
语句n
}
else
{
语句n+1
}
-
条件语句在语法上只允许每个条件分支中带一条语句,而实际中条件分支要处理的操作往往需要多条语句才能完成,这时就需要是用花括号将这些语句括起来。
-
一般来说,将一组逻辑相关的语句用一对花括号括起来构成的语句,称为复合语句(Compound Statement)。
-
函数exit()的作用是终止整个程序的执行,强制返回操作系统,并将int型参数code的值传给调用程序(一班为操作系统)。
exit(code);
- 浮点数并非真正意义上的实数,只是其在某种范围内的近似。因此也就只能用近似的方法将实数与0进行比较。
4.7用于多路选择的switch语句
- 当问题需要讨论的情况较多(一般大于三种)时,通常使用开关语句来简化程序的设计。
switch(表达式)
{
case 常量1:
可执行语句序列1
case 常量2:
可执行语句序列2
case 常量n:
可执行语句序列n
default:
可执行语句序列n+1
}
-
表达式只能是char型或int型。
-
常量与case中间至少有一个空格,常量的后面是冒号,常量的类型应与switch后括号内表达式的类型一致。
-
switch条件语句只有遇到break语句才会停下否则将继续执行。
-
default语句可处理非法运算符。
-
由于每个case后的常量只起到一个语句标号(Label)的作用,所以case后的常量必须互不相同,否则会在匹配中出现相互矛盾的问题。
4.8逻辑运算符和逻辑表达式
-
逻辑运算也称为布尔运算,用于表达复杂的逻辑关系
-
在数学上正确的表达式在C语言的逻辑上不一定总是正确的。
-
用逻辑运算符连接操作数组成的表达式称为逻辑运算符(Logic Operator)。
-
逻辑运算符
逻辑运算符 | 类型 | 含义 | 优先级 | 结合性 |
---|---|---|---|---|
! | 单目 | 逻辑非 | 最高 | 从右向左 |
&& | 双目 | 逻辑与 | 较高 | 从左向右 |
|| | 双目 | 逻辑或 | 较低 | 从左向右 |
- 逻辑运算的真假值表
A的取值 | B的取值 | !A(求反运算) | A&&B(逻辑与) | A||B(逻辑或) |
---|---|---|---|---|
非0 | 非0 | 0 | 1 | 1 |
非0 | 0 | 0 | 0 | 1 |
0 | 非0 | 1 | 0 | 1 |
0 | 0 | 1 | 0 | 0 |
- 逻辑与的运算特点是:仅当两个操作数都为真时,运算结果才为真;只要有一个为假,运算结果就为假。
- 逻辑或的运算特点是:两个操作数中只要有一个为真,运算结果就为真;仅当两个操作数都为假,运算结果才为假。
- 逻辑非的运算特点是:若操作数的值为真,则其逻辑非运算的结果为假;反之,则为真。
- 常用运算符的优先级与结合性
优先级运算 | 运算符种类 | 附加说明 | 结合方向 |
---|---|---|---|
1 | 一元运算符 | 逻辑非! 求相反数- ++ – sizeof类型强制转换等 | 右到左 |
2 | 算数运算符 | *、 /、 %高于 +、- | 左到右 |
3 | 关系运算符 | <、 <=、 > 、>= 高于==、 != | 左到右 |
4 | 逻辑运算符 | 除逻辑非之外,&&高于|| | 左到右 |
5 | 赋值运算符 | =、+=、-=、*=、/=、%= | 右到左 |
- 注意,运算符&&和||都具有“短路”特性
4.9扩充内容
4.9.1程序测试
- 程序测试是确保程序质量的一种有效手段
- 程序测试只能证明程序有错,而不能证明程序无错
- 程序测试的目的就是为了尽可能多地发现程序中的错误,成功的测试在于发现迄今为止尚未发现的错误
- 如果程序测试中没有发现任何错误,则可能是测试不充分,没有发现潜在的错误,而不能证明程序没有错误
- 程序测试能提高程序质量,但提高程序质量不能完全依赖于程序测试
- 由于进行程序测试需要运行程序,而运行程序需要数据,为测试设计的数据称为测试用例(Test Case)目的是为了利用测试用例找出潜在的各种错误和缺陷
白盒测试
- 按照程序的内部逻辑来设计测试用例,检验程序中的每条通路是否都能按照预定要求工作。这种测试方法称为白盒测试(White Box Testing),或玻璃盒测试(Glass Box Testing),也称为结构测试,这种测试主要用于测试的早期
黑盒测试
- 把系统看成一个黑盒子,不考虑程序内部的逻辑结构和处理过程,只根据需求规格说明书的要求,设计测试用例,检查程序的功能是否符合它的功能说明,这种测试方法称为黑盒测试(Black Box Testing),也称为功能测试。
- 选择有限数量的重要路径进行白盒测试,对重要功能需求进行黑盒测试。
4.9.2对输入非法字符的检查与处理
-
由于函数scanf()不进行参数类型匹配检查,因此当参数地址表中的变量类型与格式字符不符时,只是导致数据不能正确读入,但编译器并不提示任何出错信息。为了提高程序的强壮性,有必要对输入非法数据进行检查和处理,以使程序对用户输入具有一定的容错能力。
-
如果函数scanf()调用成功(能正常读入输入数据),则其返回值为已成功读入的数据项数
-
使用函数ffush()来清除输入缓冲区中的内容,可能会带来可移植性问题
例:输入两个整形数,计算并输出两个整数的最大值。如果用户输入了非法字符,那么程序提示”输入数据个数和格式错误“
#include<stdio.h>
int main(void)
{
int a,b,max,ret;
printf("Input a,b:");
ret = scanf("%d,%d",&a,&b);//记录scanf()函数返回值
if(ret != 2) //根据scanf()函数返回值,判断输入数据个数或者格式是否错误
{
printf("Input data quantity or format error!\n");
fflush(stadin);//清除输入缓冲区中的错误数据
}
else //此处可以是正确读入数据后应该执行的操作
{
max = a>b?a:b;
printf("max = %d\n",max);
}
return 0;
}
4.9.3位运算符
-
C语言既具有高级语言特点,又具有低级语言的特性,如支持位运算就是其具体体现。其操作对象不能是float、double、long、double等其他数据类型,只能是char和int型。
-
位运算符
运算符 | 含义 | 类型 | 优先级 | 结合性 |
---|---|---|---|---|
~ | 按位取反 | 单目 | 高 | 从右到左 |
<<,>> | 左移位、右移位 | 双目 | | | 从左到右 |
& | 按位与 | 双目 | | | 从左到右 |
^ | 按位异或 | 双目 | | | 从左向右 |
| | 按位或 | 双目 | 低 | 从左向右 |
- 位运算符的运算规则
a | b | a&b | a|b | a^b | ~a |
---|---|---|---|---|---|
0 | 0 | 0 | 0 | 0 | 1 |
0 | 1 | 0 | 1 | 1 | 1 |
1 | 0 | 0 | 1 | 1 | 0 |
1 | 1 | 1 | 1 | 0 | 0 |
(1)按位与
按位与可用于对字节中的某位清零,即两个操作数中的任意一位为0时,运算结果的对应位就会被置0.
(2)按位或
与按位与相反,按位或可用于对字节中的某位置1,即两个操作数中的任意一位为1时,运算结果的对应位就会被置为1.
(3)按位异或
如果两个操作数的某对应位不一样,则按位异或的对应位为1.
(4)按位取反
按位取反是对操作数的各位取反,即1变为0,0变为1.
(5)左移位
x<<n表示把x的每一位向左平移n位,右边空位补0.
(6)右移位
-
x>>n表示把x的每一位向右平移n位。当x为有符号数时,左边的空位补符号位上的值,这种位移称为算数位移;当x为无符号数时,左边空位补0,这种位移称为逻辑位移。
-
注意:无论左移位还是右移位,从一端移走的位不移入另一端,移出的位的信息都丢失了
-
在实现某些含有乘除的算法时,可以通过移位运算实现乘2或除2运算,这样非常有利与算法的硬件实现。
-
不要将逻辑运算与位运算相混淆
第五章 循环控制结构
5.1循环控制结构与循环语句
- 若需重复次数是已知的,则称为计数控制的循环(Counter Controlled Loop)
- 若重复处理的次数是已知的,则称为计数控制的循环(Condition Controlled Loop)
1.循环结构通常有两种类型:
(1)当型循环结构,表示当条件P成立(为真)时,反复执行A操作,直到条件不成立(为假)时结束循环
(2)直到型循环,表示先执行操作A一次,再判断条件P是否成立(为真),若条件成立(为真),则反复执行操作,直到条件P不成立(为假)时结束循环
2.C语言提供三种循环语句(Loop Statement)
- 被重复执行的语句序列称为循环体(Body of Loop)
(1)while语句
while语句属于当型循环。
while(循环控制表达式)
{
语句序列(循环体)
}
-
while语句中的循环控制表达式是在执行循环体之前测试的。其执行过程如下:
-
计算循环控制表达式的值
-
如果循环控制表达式的值为真,那么就执行循环体中的语句,并返回前一个步骤
-
如果循环控制表达式的值为假,就退出循环,执行循环体后面的语句。
为了使程序易于维护,建议即使循环体内只有一条语句,也将其用花括号括起来。
(2)do-while语句
- do-while语句属于直到型语句。
do
{
语句序列(循环体)
}while(循环控制表达式)(循环体)
-
与while语句不同的是,do-while语句中的循环控制表达式是在执行循环体之后的测试的
-
执行循环体中的语句
-
计算循环控制表达式的值
-
如果循环控制表达式的值为真,那么回到第一个步骤
-
若果循环控制表达式的值为假,就退出循环,执行循环体后面的语句
-
do-while语句最少被执行一次
(3)for语句
- for语句属于当型循环结构。
for(初始循环语句;循环控制表达式;增值表达式)
{
语句序列(循环体)
}
-
初始化表达式的作用是为循环控制变量初始化(Initialization) ,即赋初值,它决定了循环的起始条件;
-
循环控制表达式是循环控制条件(Loop Control Condition),准确地说是控制循环继续执行的条件,当这个表达式的值为真(非0)时继续重复执行循环,否则结束循环,执行循环体后面的语句,因此它也决定了循环何时才能结束;
-
增值表达式的作用是没执行一次循环后将循环控制变量增值(Increament),即定义没执行一次循环后循环控制变量如何变化。
-
注意,如何对循环变量进行增值,决定了循环的执行次数,如果在循环体内再次改变这个变量的值,将改变循环正常的执行次数
-
注意,for语句中三个表达式之间的分隔符是分号,有且仅有两个分号,既不能多,也不能少
-
注意,由于每次循环体执行完以后,都要执行一次增值表达式。因此,这里在最后推出for循环后,i的值实际为n+1
(4)for循环可用while语句来等价实现
初始化表达式;
while(循环控制表达式)
{
语句序列(循环体)
增值表达式;(循环体)
}
(5)逗号运算符(Comma Operator)
- 逗号运算符可把多个表达式连接在一起,构成逗号表达式,其作用是实现对各个表达式的顺序求值,因此逗号运算符也称为顺序求值运算符。
一般形式为:
表达式1,表达式2,...,表达式n
(6)空语句(Null Statement)
- 仅由一个分号构成的语句,称为空语句。空语句什么也不做,只表示语句的存在。当循环体中是空语句时,表示在循环体中什么也不做,常用于编写延时程序
for(i=1;i<50000000;i++)
{
;
}
或
for(i=1;i<50000000;i++)
{
}
或
for(i=1;i<50000000;i++);
- 除非特殊需要,一般不在for语句后加分号
for(i=1;i<=n;i++);//行末的分号将导致循环什么也不做,只起延时作用
{
sum = sum + i;
}
它相当于下面的语句序列
for(i=1;i<=n;i++)
{
;
}
sum = sum + i;
- 如果while后面被意外地加上分号,有可能产生死循环(Endless Loop)
例如:
i = 1;
while(i<=n);//行末的分号有可能导致死循环
{
sum = sum + i;
i++;
}
它相当于下面的语句序列
i = 1;
while(i<=n)// 如果输入的n的值大于则条件语句永真循环会一直执行
{
;
}
sum = sum + i;
i++;
- 当第一次测试循环条件就为假时,while和do-while语句不等价
5.2计数控制的循环
- 循环次数实现已知的循环称为计数循环控制,习惯上,用for语句编写计数控制循环更简洁方便。
例:从键盘输入n,然后计算并输出1~n之间所有数的阶乘值
#include<stdio.h>
int main(void)
{
int i,n;
long p = 1; //因阶乘取值范围为较大,故p定义为长整型,并赋初值1
printf("Please entern:");
scanf("%d",&n);
for(i = 1;i<=n;i++)
{
p = p * i; //做累乘运算
printf("%d != %ld\n",i,p); //以长整型格式输出1~n之间的所有数的阶乘值
}
return 0;
}
5.3嵌套循环
-
将一个循环语句放在另一个循环语句的循环体中构成的循环,称为嵌套循环(Nested Loop)。
-
while、do-while和for这三种循环均可以相互嵌套,即在while循环、do-while循环和for循环内,都可以完整地包含上述任一种循环结构
-
执行嵌套循环时,先由外层循环进入内层循环,并在内层循环终止之后接着执行外层循环,再由外层循环进入内循环中,当外层循环全部终止时,程序结束。
-
编写累加求和程序的关键在于寻找累加项(即通项)的构成规律。通常,当累加的项较为复杂或者前后项之间无关时,需要单独计算每个累加项。而当累加项的前项和后项之间有关时,则可以根据累加项的后项与前项之间的关系,通过前项来计算后项。
例:计算1!+2!+3!+...+n!
#include<stdio.h>
int main(void)
{
int i,j,n;
long p,sum = 0;
printf("Input n:");
scanf("%d",&a);
for(i = 1;j<=n;i++)
{
for(j = 1;j<=i;j++)
{
p = p * j;
}
sum = sum + p;
}
printf("1!+2!+3!+...+n! != %ld\n",n,sum);
return 0;
}
5.4条件控制的循环
- 循环次数事先未知的循环通常是由一个条件控制的,称为条件控制的循环
#include<time.h>
#include<stdlib.h>
#include<stdio.h>
int main(void)
{
int magic ,guess ,counter = 0 ,ret;
char reply;
srand(time(NULL));
do{
counter = 0;
magic = rand() % 100+1;
do{
printf("Please guess a magic number:");
ret = scanf("%d",&guess);
while(ret != 1)
{
while(getchar() != '\n');
printf("Please guess a magic number:");
ret = scanf("%d",&guess);
}
counter++;
if(guess > magic)
{
printf("Wrong! Too big!\n");
}
else if(guess < magic)
{
prinntf("Wrong! Too small!\n");
}
else
{
printf("Wrong! Too small!\n");
}
}while(guess != magic && counter<10);
printf("counter = %d\n",counter);
printf("Do you want to continue(Y/N or y/n)?");
scanf(" %c",&reply);
}while(reply == 'Y' || reply = 'y');
return 0;
}
5.5流程的转移控制
- goto语句、break语句、continue语句和return语句是C语言中用于流程转移的跳转语句
5.5.1goto语句
- 它的作用是在不需要任何条件下直接使程序跳转到该语句标号(Label)所标识的语句去执行,其中语句标号代表goto语句转向的的目标位置,应使用合法的标识符表示语句标号,其命名规则与变量名相同。尽管goto语句是无条件转向语句,但通常情况下goto语句与if语句联合使用。
- 良好的编程风格建议少用和慎用goto语句,尤其是不要使用往回跳转的goto 语句,不要让goto制造出永远不会被执行的代码(即死代码)。
例:读入5个正整数并显示他们。当读入的数据为负数时,程序立即终止。
#include<stdio.h>
int main(void){
int i,n;
for(i=1;i<=5;i++)
{
printf("Please enter n:");
scanf("%d",&n);
if(n < 0) goto END;// END为语句标号
printf("n = %d \n",n);
}
END: printf("Programe is over!\n");
return 0;
}
5.5.2break语句
- break语句除用于退出switch结构外,还可用于while、do—while和for构成的循环语句的循环体中。当执行循环体遇到break语句时,循环将立即停止,从循环语句后的第一条语句开始继续执行。
例:读入5个正整数并显示他们。当读入的数据为负数时,程序立即终止。
#include<stdio.h>
int main(void){
int i,n;
for(i = 1;i <= 5;i++)
{
printf("Please enter n:");
scanf("%d",&n);
if(n<0)
{
break;
}
printf("n = %d \n",n);
}
printf("Program is over!\n");
return 0;
}
- 虽然break语句和goto语句都可用于终止整个循环的执行,但二者的本质区别在于:goto语句可以向任意方向跳转,可以控制流程跳转到程序中任意指定的语句位置,而break语句只限定流程跳转到循环语句之后的第一条语句去执行。
5.5.3continue语句
- continue语句与break语句都可用于对循环进行内部控制,但二者对流程的控制效果是不同的。当在循环体中遇到continue语句时,程序将跳过continue语句后面尚未执行的语句,开始下一次循环,即只结束本次循环的执行,并不终止整个循环。
#include<stdio.h>
int main(void){
int i,n;
for(i = 1;i <= 5;i++)
{
printf("Please enter n:");
scanf("%d",&n);
if(n<0)
{
continue;
}
printf("n = %d \n",n);
}
printf("Program is over!\n");
return 0;
}
- 注意,在嵌套循环的情况下,break语句和continue语句只对包含它们的最内层的循环语句起作用,不能用break语句跳出多重循环。goto语句是跳出多重循环的一条捷径。
第6章 函数与模块化程序设计
6.1分而治之与信息隐藏
- 为了降低开发大规模软件的复杂度,程序员把较大的任务分解成若干个较小、较简单的任务,并提炼出公用的任务方法,称为分而治之(Divide and Conquer)。
- 程序模块化设计(Modular Programming)就体现了这种思想。通过功能分解实现模块化程序设计,功能分解是一个自顶向下、逐步求精的过程,即一步一步地把大功能分解为小功能,从上到下,逐步求精,各个击破,直到完成最终的程序。模块化程序设计不仅使程序更容易理解,也更容易调试和维护。
- 设计得当的函数可以把函数内部的信息(包括数据和具体操作细节)对不需要这些信息的其他模块隐藏起来,即不能访问,让使用者不必关注函数内部是如何做的,只知道它能做什么以及如何使用它即可,从而使得整个程序的结构更加紧凑,逻辑也更加清晰。这就是所谓的信息隐藏(Information Hiding)的思想。在进行程序模块化设计时,我们应遵循信息隐藏的原则。
6.2函数的定义
6.2.1函数的分类
- 在C语言中,函数(Function)是构成程序的基本模块。程序的执行从main()的入口开始,到main()的出口结束,中间循环,往复、迭代地调用一个又一个函数。
(1)标准函数库
-
符合ANSI C标准的C语言的编译器,都必须提供这些库函数。当然,函数的行为也要符合ANSI C的定义 。使用ANSI C的库函数,必须在程序的开头把函数所在的头文件包含进来。例如,使用在math.h内定义的fabs()函数时,只要在程序开头将头文件<math.h>包含到程序中即可。
-
此外,还有第三方函数库可供用户使用,它们不在ANSI C标准范围内,是由其他厂商自行开发的C语言函数库,能扩充C语言在图形、数据库等方面的功能,用于完成ANSI C未提供的功能。
(2)自定义函数
- 如果库函数不能满足程序设计者的编程需要,那么就需要自行编写程序来完成自己所需的功能,这类函数称为自定义函数。
6.2.2函数的定义
- 函数名是函数的唯一标识,用于说明函数的功能。函数名标识符的命名规则与变量的命名规则相同。函数命名应以只管以直观且易于拼读为宜,做到“见名知意”,不建议使用汉语拼音,最好使用英文单词及其组合,这样便于记忆和阅读。为了便于区分,通常变量名用小写字母开头的单词组合而成。
- 标识符的命名和命名风格的选择主要依照个人的习惯,此外从常采用与操作系统或开发工具一直的命名风格。
- Windows风格。函数名使用“动词”或者“动词+名词”(动宾短语)的形式,变量名使用“名词”或者“形容词+名词”的形式。
- Linux/UNIX应用程序的标识符通常采用“小写加下划线”的方式。
- 函数体必须用一对花括号 包围,这里的花括号{}是函数体的定界符。在函数体内部定义的变量只能在函数体内访问,称为内部变量。函数头部参数表里的变量 ,称为形式参数(Parameter,简称形参),也是内部变量,即只能在函数体内访问。
- 形参表是函数的入口。如果说函数名相当于说明运算的规则的话,那么形参表里的形参就相当于运算的操作数,而函数的返回值就是运算的结果。
- 若函数没有函数返回值,则需要用void定义返回值的类型。若函数不需要入口参数,则需用void代替形参表中的内容,表示该函数不需要任何外部数据。
例:用函数编写计算整数n的阶乘n!
long Fact(int n){
int i;
long result = 1;
for(i = 2;i <= n;i++)
{
result *= i;
}
return result;
}
- 注意,在函数定义的前面写上一段注释来描述函数的功能及其形参,是一个非常好的编程习惯。
6.3向函数传递值和从函数返回值
6.3.1函数调用
- main函数调用函数Fact()时,必须提供一个称为实际参数(Argument,简称实参)的表达式给被调用的函数。一般将调用其它函数的函数简称为主调函数,被调用的函数简称为被调函数。主调函数把实参的值复制给形参的过程,称为参数传递。
例:编写main函数,调用函数Fact()来计算m!。其中,m的值由用户从键盘输入
#include<stdio.h>
int mian(void){
int m;
long ret;
printf("Input m:");
scanf("%d",&m);
ret = Fact(m);
printf("%d! = %ld\n",m,ret);
return 0;
}
- 有返回值的函数必须有return语句。return语句用来指明函数将返回给主调函数的值是什么。
- 注意,函数的返回值只能有一个,函数返回值的类型可以是除数组以外的任何类型。函数中的return语句可以有多个,但不表示函数可以有多个返回值。
6.3.2函数原型
#include<stdio.h>
//函数功能:用迭代法计算n!
long Fact(int n){
int i;
long result = 1;
for(i = 2;i <= n;i++)
{
result *= i;
}
return result;
}
int main(void){
int m;
long ret;
printf("Input m:");
scanf("%d",&m);
ret = Fact(m);
printf("%d! %ld\n",m,ret);
return 0;
}
//另一种写法
#include<stdio.h>
long Fact(int n); //函数原型
int main(void){
int m;
long ret;
printf("Input m:");
scanf("%d",&m);
ret = Fact(m);
printf("%d! %ld\n",m,ret);
return 0;
}
//函数功能:用迭代法计算n!
long Fact(int n){
int i;
long result = 1;
for(i = 2;i <= n;i++)
{
result *= i;
}
return result;
}
- 两种写法在功能上完全等价,形式上的区别是第一个程序里后函数Fact()的定义在main()之前,而第二个程序在函数Fact()的定义在main()之后,并且在程序中多了一条称为函数原型(Function Prototype)声明的语句——long Fact(int n);
6.3.3函数封装与防御性程序设计
-
用户并不知道函数内部定义了哪些变量,使用使用了什么算法等细节内容(即函数是“怎样做到的”)全被封装了起来,用户看不到,这就是函数封装(Encapsulation)。
-
为了让函数具有遇到不正确使用或非法数据输入时仍能保护自己避免出错的能力,即增强程序的健壮性(Robustness),使函数具有防弹(Bulletproof)功能,需要在函数的入口处增加对函数参数合法性的检查,这是一种常用的增强程序健壮性的方法。像这种在程序中增加一些代码,用于专门处理某些异常情况的技术,称为防御式编程(Defensive Programming)。
#include<stdio.h> long Fact(int n); int main(void){ int m; long ret; printf("Input m:"); scanf("%d",&m); ret = Fact(m); printf("%d! = %ld",m,ret); return 0; } //函数功能:用迭代法计算n! long Fact(int n){ int i; long result = 1; if(n < 0) //增加对函数入口参数合法性的检查 { printf("Input data error!\n"); } else { for(i = 2;i <= n;i++) { result *= i; } return result; } }
6.3.4函数设计的基本原则
- 函数的规模要小,尽量控制在50行代码以内,因为这样的函数比代码行数更长的函数更容易维护,出错的几率更小。
- 函数的功能要单一,不要让它身兼数职,不要设计具有多种用途的函数。
- 每个函数只有一个入口和一个出口。尽量不使用全局变量向函数传递信息。
- 在函数接口中清楚地定义函数的行为,包括入口参数、出口参数、返回状态、异常处理等,让调用者清楚函数所能进行的操作以及操作是否成功,应尽可能多地考虑一些可能出错的情况。定义好函数接口以后,轻易不要改动。
- 在函数的入口处,对参数的有效性进行检查。
- 在执行某些敏感性操作(如执行除法、开方、取对数、赋值、函数参数传递等)之前,应检查操作数及其类型的合法性,以避免发生除零、数据溢出、类型转换、类型不匹配等因思维不缜密而引起的错误。
- 不能认为调用一个函数总会成功,要考虑到如果调用失败,应该如何处理。
- 对于与屏幕显示无关的函数,通常通过返回值来报告错误,因此调用函数时要校验函数的返回值,以判断函数调用是否成功。对于与屏幕显示有关的函数,函数要负责相应的错误处理。错误处理代码一般放在函数末尾,对于某些错误,还要设计专门的错误处理函数。
- 由于并非所有的编译器都能捕获实参与形参类型不匹配的错误,所以程序设计人员在函数调用时应确保函数的实参类型与形参类型相匹配。在程序开头进行函数原型声明,并将函数参数的类型书写完整(没有参数时用void声明),有助于编译器进行类型匹配检查。
- 当函数需要返回值时,应确保函数中的所有控制分支都有返回值。函数没有返回值时应用void声明。
6.4函数的递归调用和递归函数
- 如果一个对象部分地由它自己组成或按它自己定义,则我们称它是递归(Recursive)的。
例:用递归方法计算整数n的阶乘n!
#include<stdio.h>
long Fact(int n);
int main(void){
int n;
long result;
printf("Input n:");
scanf("%d",&n);
result = Fact(n);
if(result == -1)
{
printf("n < 0,data error!\n");
}
else
{
printf("%d! = %ld\n",n,result);
}
return 0;
}
//函数功能:用递归法计算n!,当年n>=0时返回n!,否则返回-1
long Fact(int n)
{
if(n<0)
{
return -1;
}
else if(n == 0 || n == 1)
{
return 1;
}
else
{
return (n*Fact(n-1));
}
}
- 递归函数必须包含如下两个部分:
(1)由其自身定义的与原始问题 类似的更小规模的子问题,它使递归调用过程持续进行,称为一般情况(General case)。
(2)递归调用的最简形式,它是一个能够用来结束递归调用过程的条件,通常称为基线情况(Base case)。
- “在函数内直接或间接地调用自己”的函数调用,就称为递归调用(Recursive Call),这样的函数则称为递归函数(Recursive Function)。
例:斐波那契数列的计算
#include<stdio.h>
long Fib(int n);
int main(void){
int n,i,x;
printf("Input n:");
scanf("%d",&n);
for(i = 1;i <= n;i++)
{
x = Fib(i);
printf("Fib(%d) = %d\n",i,x);
}
return 0;
}
//函数功能:用递归法计算Fibonacci数列中的第n项的值
long Fib(int n){
if(n == 1)
{
return 1;
}
else if(n == 2)
{
return 1;
}
else
{
return (Fib(n-1)+Fib(n-2));
}
}
6.5变量的作用域和生存期
6.5.1变量的作用域
- 程序被花括号括起来的区域,叫做语句块(Block)。函数体是语句块,分支语句和循环体也是语句块。
- 变量的作用域 (Scope)规则是:每个变量仅在他定义的语句块(包含下级语句块)内有效,并且拥有自己的存储空间。
- 不在任何语句块内定义的变量,称为全局变量(Global Variable)。全局变量的作用域为整个程序,即全局变量在程序的所有位置均有效。
- 除整个程序以外的其他语句块内定义的变量,称为局部变量(Local Variable)。
- 全局变量从程序运行开始起就占据内存,仅在程序结束时才将其释放,所谓释放内存,其实就是将内存中的值恢复为随机值(即乱码)。由于全局变量的作用域是整个程序,在程序运行期间始终占据着内存,因此在程序运行期间的任何时候,在程序的任何地方,都可以访问(读或者写)全局变量的值。
#include<stdio.h>
long Fib(int n);
int count; //全局变量count用于累计递归函数被调用的次数,自动初始化为0
int main(void){
int n,i,x;
printf("Input n:");
scanf("%d",&n);
for(i = 1;i <= n;i++)
{
count = 0; //计算下一项Fibonacci数列时将计数器count清零
x = Fib(i);
printf("Fib(%d) = %d,count = %d\n",i,x,count);
}
return 0;
}
//函数功能:用递归算法计算Fiaonacci数列中的第n项的值
long Fib(int n){
count++; //累计递归函数被调用的次数 ,记录用于全局变量的count中
if(n == 0)
{
return 0;
}
else if(n == 1)
{
return 1; //基线情况
}
else
{
return (Fib(n-1)+Fib(n-2)); //一般情况
}
}
- 在并列的语句块之间只能通过一些特殊通道传递数据,如函数参数、返回值,以及全局变量。因全局变量破坏了函数的封装性,所以不建议采用。
6.5.2变量的生存期
- 变量的存储类型的一般生命方式如下:存储类型 数据类型 变量名表;
- 变量只能在其生存期内被访问,而变量的作用域也会影响变量的生存期。
1.自动变量
- 自动变量的标准定义格式为 : auto 类型名 变量名;
- 如果没有指定的存储类型,那么变量的存储类型就缺省为auto,它仅能被语句块内的语句访问,在推出语句块以后不能再访问。自动变量的“自动”体现在进入语句块时自动申请内存,退出语句时自动释放内存。因此,自动变量也称为动态局部变量。
- 自动变量在定义时不会自动初始化,所以除非程序员在程序中显示制定初值,否则自动变量的值是随机不确定的,即乱码。
- 自动变量在退出函数后,其分配的内存立即被释放,再次进入语句块,该变量被重新分配内存,所以不会保持上一次退出函数前所拥有的值。
2.静态变量
- 如果希望系统为其保留这个值,除非系统分配给它的内存在退出函数调用时不释放。这时候就需要静态变量。用staic关键字定义的变量称为静态变量。静态变量的定义格式为: staic 类型名 变量名;
例:利用静态变量计算n的阶乘
#include<stdio.h>
long Func(int n);
int main(void){
int i,n;
printf("Input n:");
scanf("%d",&n);
for(i = 1;i <= n;i++)
{
printf("%d! = %ld\n",i,Func(i));
}
return 0;
}
long Func(int n){
staic long p = 1;
p = p * n;
return p;
}
- 静态变量是与程序“共存亡”的,而自动变量是与程序块“共存亡”的。
- 静态变量的值之所以会保持到下一次函数调用,是因为静态变量是在静态存储区分配内存的,在静态存储区分配的内存在程序运行期间是不会被释放的,其生存期是不会被释放的,其生存期是整个程序运行期间。
- 静态局部变量与自动变量都是在函数内定义的,因此它们的作用域都是局部的,即仅在函数内可被访问。
3.外部变量
- 如果在所有函数之外定义的变量没有指定其存储类别,那么它就是一个外部变量。外部变量是全局变量,它的作用域是从它的定义点到本文件的末尾。但如果要在定义点之前或者在其他文件中使用它,那么就需要使用关键字extern对其进行声明(注意不是定义,编译器并不对其分配内存),其格式为:extern 类型名 变量名;
- 和静态变量一样,外部变量也是在静态存储区内分配内存的,其生存期是整个程序 的运行期。没有显示初始化的外部变量由编译程序自动初始化为0。
- 在函数内定义的静态变量,称为 静态局部变量,静态局部变量只能在定义它的函数内部被访问,而在所有函数外定义的静态变量,称为静态全局变量,静态全局变量可以再定义它的文件内的任何地方被访问,但不能像非静态的全局变量那样被程序的其他文件所访问。
4.寄存器变量
- 寄存器变量就是用寄存器存储的变量。其定义格式为:register 类型名 变量名;
- 寄存器(Register)是CPU内部的一种容量有限但速度快的存储器。由于CPU进行访问内存的操作是很耗时的,使得有时对内存的访问无法与指令的执行保持同步,因此将需要频繁访问的数据存放在CPU内部的寄存器里,即将使用频率较高的变量声明为register,可以避免CPU对存储器的频繁数据访问,使程序更小,执行速度更快 。
- 现代编译器能自动优化程序,自动把普通变量优化为寄存器变量,并且可以忽略用户的regist指定,所以一般无需特别声明变量为register。
6.6模块化程序设计
6.6.1模块分解的基本原则
- 模块化程序设计(Modular programming)思想最早出现在汇编语言中,在结构程序设计的概念提出以后,逐步完善并形成了模块化程序设计方法。
- 按照模块化程序设计的思想,无论多么复杂的任务,都可以划分为若干个子任务。若子任务较为复杂还可以继续细分为一些容易解决的子任务为止。
- 无论结构化方法还好是面向对象方法,模块化的基本指导思想都是“信息隐藏”,即把不需要调用者知道的信息都封装在模块内部,使模块的实现细节对外不可见。按照这一指导思想,模块分解的基本原则是:高聚合、低耦合,保证每个模块的的相互独立性。
- 高聚合指的是模块内部的联系越紧密越好,内聚性越强越好,简单地说就是模块的功能要相对独立和单一,让模块各司其职,每个模块只专心负责一件事情。
- 低耦合指的是模块之间的联系越松散越好,模块之间仅仅交换那些为完成系统功能必须交换的信息,这意味着模块对外的接口越简单越好,因为接口越简单越好,因为接口越简单,模块与外界打交道的变量和交换的数据就越少,这样就会降低模块之间相互影响的程度。
- 模块化程序设计的好处是,可以先将模块各个击破,最后再将它们集成在一起完成总任务,这样可以进行单个模块的设计、开发、调试、测试和维护工作。
- 模块化程序设计是程序设计中最重要的四象之一。C语言通过模块和函数两种手段来支持这种思想。
6.6.2自顶向下,逐步求精
- 抽象处理复杂问题的重要工具,逐步求精(Stepwise Refinement)就一种具体的抽象技术,它是1971年由Wirth提出的用于结构化程序设计的一种最基本的方法。
- 自底向上(Down-top)方法是先编写出基本程序段,然后再扩大、补充和升级。
- 自顶向下(Top-down)的程序设计方法是相对与自底向上方法而言的,它是自底向上方法的逆方法。自顶向下方法是先写出结构简答、清晰的主程序来表达整个问题;再次问题中包含复杂子问题用子程序来实现;若子问题中还包含复杂的子问题,再用另一个子程序(即函数)来解决,直到每个细节都可用高级语言表达为止。
- 逐步求精技术可以理解为是一种由不断的自底向上修正所补充的自顶向下的程序设计方法。
6.6.3断言
- assert()的功能是在程序的调试阶段验证程序中某个表达式的真与假,assert()后面括号内的表达式为真时,继续执行下一条语句;为假时,它会停止程序的继续执行。
- 断言仅能用于调试程序,不能作为程序的功能。
- 使用断言的几种情况:
(1)检查程序中的各种假设的正确性,例如一个计算结果是否在合理的范围内。
(2)证实或测试某种不可能发生的状况确实不会发生,例如一些理论上永远不会执行到的分支(如switch的default:后)确实不会被执行。
6.6.4条件编译
- 如果定义了头文件a.h、b.h和源文件demo.c,在b.h中包含了a.h,在demo.cpp中同时包含了a.h和b.h,这样就会出现a.h被重复包含的问题,多重包含经常出现在需要使用很多头文件的大型程序中。使用条件编译可以解决头文件多重包含的问题。
- 例如,头文件GuessNummber.h写成下面这样
#ifndef_GuessNumber_H_
#define_GuessNumber_H_
...//(头文件内容)
#endif
- 这里的#ifndef和#endif均被称为条件编译的预处理命令,编译预处理命令主要包括三种:文件包含、宏定义和条件编译 。
- 条件编译由#if,#ifdef,#ifnedf,#elif和#endif组合而成,使用某些代码仅在特定的条件成立时才会被编译进可执行文件。
- 以头文件GuessNumber.h为例,当头文件第一次被包含时,执行条件编译指令下面的宏定义,宏常量_ GuessNumber_H_已经被定义,于是不再执行条件编译指令#ifndef和#endif之间的内容。
- #ifndef和#define后面的宏常量_ HEADERNAME_H_按照被包含的头文件的文件名取名,以避免由于其他头文件使用相同的宏常量而引起冲突。
第7章 数组和算法基础
7.1一维数组的定义和初始化
- 数组(Array)是一组具有相同类型的变量的集合,它是一种顺序存储、随机访问的顺序表结构。例如,可以将10个成绩值存储在内存的一个连续区域内,使用一个统一的名字 来标识这组相同类型的数据,这个名字称为数组名。
- 构成数组的每个数据项称为数组元素(Element)。C程序通过数组的下标(Subscript)实现对数组元素的访问。
- 例:int score[5];在该声明语句中,int代表该数组的基类型(Base Type),即数组中元素的类型。下标的个数表明数组的维数(Dimension),score后方括号内的数字代表数组元素的个数。
- 注意,C语言中数组下标 都是从0开始的。
例:计算五个学生的平均分的程序如下:
#include<stdio.h>
int main(void){
int score[5];
int totalScore = 0;
int i;
printf("Input the scores of five students:\n");
for(i = 0;i<5;i++)
{
scanf("%d",&score[i]);
totalScore = totalScore + score[i];
}
printf("The average score is %f\n",totalScore/5.0);
return 0;
}
- 注意C89规定再定义数组时不能使用变量定义数组的大小,但C99允许用变量定义数组的大小。
例:使用变量定义
scanf("%d",&n);
int score[n];
- 定义但未进行初始化的数组元素的值仍然是随机数。对一维数组进行初始化时,可将元素初值放在=后面用一对花括号括起来的初始化列表中。
即:
int score[5] = {90,80,70,100,95};
- 初始化列表中提供的初值个数不能多于数组元素的个数。若省略对数组长度的声明。
例如:
int score[] = {90,80,70,100,95};
//系统会自动按照初始化列表中提供的初值个数对数组进行初始化并确定数组的大小,所以只给部分数组元素赋初值时,对数组的长度声明不能省略
- 当数组在所有函数外定义,或用static定义为静态存储类型时,即使不给数组元素赋初值,那么元素也会自动初始化为0,这是在编译阶段完成的。
例:数组下标越界访问的程序示例
#include<stdio.h>
int main(void)
{
int a=1,c=2,b[5]={0},i;
printf("%p,%p,%p\n",b,&c,&a);
for(i = 0;i <= 8;i++)
{
b[i] = i;
printf("%d ",b[i]);
}
printf("\nc=%d,a=%d,i=%d\n",c,a,i);
return 0;
}
7.2二维数组的定义和初始化
- 定义一个二维数组,只要增加一维下标即可,二维数组的一般定义格式为:
类型 数组名[第一维长度][第二维长度];
-
一维数组在内存中占用的字节数为:数组长度 * sizeof(基类型),二维数组占用的字节数为:第一维长度 * 第二维长度*sizeof(基类型)。
-
注意,在不同编译系统中,intx型所占的字节数是不同的。因此,用sizeof运算符来计算一个类型或者变量在内存中所占的字节,并且有利于提高程序的可移植性(Portability)。
-
n维数组用n个下标来确定各元素在数组中的顺序。
-
由于C语言中不带下标的数组名具有特殊含义,它代表数组的首地址,因此不能整体引用一个数组,每次只能引用指定下标的数组元素。
-
对于二维数组,既可以按元素初始化,也可以按行初始化。
例:
short matrix[3][4] = {1,2,3,4,5,6,7,8,9,10,11,12};//按元素初始化
short matrix[3][4] = {{1,2,3,4},{5,6,7,8},{9,10,11,12}};//按行初始化
- 当初始化列表给出数组全部元素的初值时,第一维的长度声明可以省略。
例:
short matrix[][4] = {1,2,3,4,5,6,7,8,9,10,11,12};
- 按行初始化时,即使初始化列表中提供的初值个数可以少于数组元素的个数,第一维的长度声明也可以省略,此时系统自动给后面的元素初始化为0
例:
short matrix[][4] = {{1,2,3},{4,5},{6}};
short matrix[3][4] = {{1,2,3,0},{4,5,0,0},{6,0,0,0}};//两者等价
- 注意,数组第二维的长度声明永远都不能省略。
- C语言中的二维数组元素在C编译程序为其分配的连续存储空间中是按行存放的,即存完第一行后存第二行,依此类推。存放时系统必须知道每一行有多少个元素才能正确计算出该元素相对于二维数组第一个元素的偏移量。
7.3向函数传递一维数组
- 数组元素和基本型变量一样,既可出现在任何合法的C表达式中,也可用作函数参数。
例:从键盘输入某班学生某门课的成绩(已知每班人数最多不超过40人,具体人数由键盘输入),试编程计算其平均分
#include<stdio.h>
#define N 40
int Average(int score[],int n);//Averange()函数原型
void ReadScore(int score[],int n);//ReadScore()函数原型
int main(void)
{
int score[N],aver,n;
printf("Input n:");
scanf("%d",&n);
ReadScore(score,n);
aver = Average(score,n);
prinf("Average score is %d\n",aver);
return 0;
}
//函数功能:计算n个学生成绩的平均分
int Average(int score[],int n)//Average()函数定义
{
int i,sum = 0;
for(i = 0;i<n;i++)
{
sum += score[i];
}
return sum/n;
}
//函数功能:输入n个学生的某门课成绩
void ReadScore(int score[],int n)//ReadScore()函数定义
{
int i;
printf("Input score:");
for(i=0;i<n;i++)
{
scanf("%d",&score[i]);
}
}
-
若要把一个数组传递给一个函数,那么只要使用不带方括号的数组名作为函数实参调用函数即可。注意仅仅是数组名,不带方括号和下标。
-
由于数组名代表数组第一个元素的地址,因此用数组名做函数实参实际上是将数组的首地址传给被调函数。
-
注意:数组作函数形参时,数组的长度可以不出现在数组名后面的方括号内,通常用另一个整形形参来指定数组的长度。
-
如果数组名后面的方括号内出现的是正数,编译器并不生成具有相应个数的元素的数组,也不进行下标越界检查,编译器只检查它是否大于零,然后将其忽略掉。如果数组后面的方括号内出现的是负数,则将产生编译错误。
-
如果以负值作为输入结束的标记值(Sentinel Value),这种循环控制也称为标记控制的循环。
7.4排序和查找
- 排序(Sorting)是吧一系列无序的数据按照特定的顺序(如升序或降序)重新排列为有序序列的过程。
例:从键盘输入某班学生某门功课的成绩(每班人数最多不超过40人),当输入为负值时,表示输入结束,试编程将分数按从高到低顺序进行排序输出。
#include<stdio.h>//交换排序法
#define N 40
int ReadScore(int score[]);
void DataSort(int score[],int n);
void PrintScore(int score[],int n);
int main(void)
{
int score[N],n;
n = ReadScore(score);//调用函数ReadScore()输入成绩,返回学生人数
printf("Total students are %d\n",n);
DataSort(score,n); //调用函数DataSort()进行成绩排序
printf("Sorted scores:");
PrintScore(score,n); //调用函数PrintScore()输出成绩排序结果
return 0;
}
//函数功能:输入学生某门课的成绩,当输入负值时,结束输入,返回学生人数
int ReadScore(int score[])
{
int i = -1;
do{
i++;
printf("Input score:");
scanf("%d",&score[i]);
}while(score[i] >= 0);
return i;
}
//函数功能:按交换法将数组score的元素值按从高到底排序
void DataSort(int score[],int n)
{
int i,j,temp;
for(i = 0;i<n-1;i++)
{
for(j=i+1;j<n;j++)
{
if(score[j]>score[i])
{
temp = score[j];
score[j] = score[i];
score[i] = temp;
}
}
}
}
//函数功能:打印学生成绩
void PrintScore(int score[],int n)
{
int i;
for(i = 0;i<n;i++)
{
printf("%4d",score[i]);
}
printf("\n");
}
- 在余下的数中找出最大值或最小值再进行交换位置,可以简化算法,这种排序方法叫做选择法排序(Selection Sort)。
例:在上一例的基础上加入学号,要求输入学生成绩的同时输入学生的学号并将学生的学号与分数排序结果一同输出。
#include<stdio.h>
#define N 40
int ReadScore(int score[],long num[]);
void DataSort(int score[],long sum[],int n);
void PrintScore(int score[],long sum[],int n);
int main(void)
{
int score[N],n;
long sum[N];
n = ReadScore(score,num);//调用函数ReadScore()输入成绩,返回学生人数
printf("Total students are %d\n",n);
DataSort(score,num,n); //调用函数DataSort()进行成绩排序
printf("Sorted scores:");
PrintScore(score,num,n); //调用函数PrintScore()输出成绩排序结果
return 0;
}
//函数功能:输入学生的学号及某门课的成绩,当输入负值时,结束输入,返回学生人数
int ReadScore(int score[],long sum[])
{
int i = -1;
do{
i++;
printf("Input student's ID and score:");
scanf("%ld%d",&num[i],&score[i]);
}while(num[i]>0 && score[i] >= 0);
return i; //返回学生总数
}
//函数功能:用选择法按数组score的元素值按降序对score和num排序
void DataSort(int score[],long sum[],int n)
{
int i,j,k,temp1;
long temp2;
for(i = 0;i<n-1;i++)
{
k=i;
for(j=i+1;j<n;j++)
{
if(score[j]>score[k]) //按数组score的元素值从高到低排序
{
k=j; //记录最大数下标位置
}
}
if(k != i)
{
//交换成绩
temp1 = score[k];
score[k] = score[i];
score[i] = temp1;
//交换学号
temp2 = num[k];
num[k] = num[i];
num[i] = temp2;
}
}
}
//函数功能:打印学生学号和成绩
void PrintScore(int score[],long num[],int n)
{
int i;
for(i = 0;i<n;i++)
{
printf("%10l%4d",num[i],score[i]);
}
}
- 使用数据库时,用户可能需要频繁通过输入键字值来查找相应的记录。在数组中搜索一个特定的=元素的处理过程,称为查找(Searching)。线性查找(Sequential Search)算法简单直观,但效率较低。折半查找算法稍微复杂一些,但效率很高。两种查找算法都可用迭代法实现。
例:从键盘输入某班学生某门课的学号合成绩(假设每班人数最多不超过40人),当输入为负值时,表示输入结束。(线性查找法)
#include<stdio.h>
#define N 40
int ReadScore(int score[],long sum[]);
int LinSearch(long sum[],long x,int n);
int main(void)
{
int score[N],n,pos;
long sum[N],x;
n = ReadScore(score,sum);
printf("Total students are %d\n",n);
printf("Input the searching ID:");
scanf("%ld",&x);
pos = LinSearch(num,x,n);
if(pos != -1)
{
printf("score = %d\n",score[pos]);
}
else
{
printf("Not found!\n");
}
return 0;
}
//函数功能:输入学生的学号及其某门课成绩,当输入负值时,结束输入返回学生人数
int ReadScore(int score[],long sum[])
{
int i = -1;
do{
i++;
printf("Input student's ID and score:");
scanf("%ld%d",&num[i],&score[i]);
}while(num[i]>0 && score[i] >= 0);
return i;
}
//按线性查找值为x的数组元素,若找到则返回x在数组中的下标位置,否则返回-1
int LinSearh(long sum[],long x,int n)
{
int i;
for(i = 0;i<n;i++)
{
if(num[i] == x)
{
return i;
}
}
return -1;
}
- 当待查找信息有序排列时,折半查找法比顺序查找法的平均查找速度要快得多。折半查找也称为对分搜索。
- 折半查找法的基本思想为:首先选取位于数组中间的元素,将其与查找键进行比较。如果他们的值相等,则查找键被找到,返回数组中间元素的下标。否则,将查找的区间缩小为原来区间的一半,即在一半的数组元素中查找。
//按折半查找法查找值为x的数组元素,若找到则返回x在数组中的下标位置,否则返回-1
int BinSearch(long num[],long x,int n)
{
int low = 0,hight = n-1,mid;//区间左端点low置为0,右端点high置为n-1
while(low <= hight) //若左端点小于等于右端点,则继续查找
{
mid = (hight + low)/2; //取数据区间的中点
if(x > num[mid])
{
low = mid + 1; //若x>num[mid],则修改区间的左端点
}
else if(x < num[mid])
{
hight = mid - 1; //若x<num[mid],则修改区间的右端点
}
else
{
return mid; //若找到,则返回下标值mid
}
}
return -1; //循环结束仍未找到,则返回值-1
}
- 还可以使用标志变量法的方法来编写折半查找函数BInSearch()
int BinSearch(long num[],long x,int n)
{
int low = 0,hight = n-1,mid;//区间左端点low置为0,右端点high置为n-1
int pos = -1;//若循环结束仍未找到,则返回pos的初始值-1
int find = 0;//置找到标志变量flag为假
while(!find && low <= hight) //若左端点小于等于右端点,则继续查找
{
mid = (hight + low)/2; //取数据区间的中点
if(x > num[mid])
{
low = mid + 1; //若x>num[mid],则修改区间的左端点
}
else if(x < num[mid])
{
hight = mid - 1; //若x<num[mid],则修改区间的右端点
}
else
{
pos = mid;//若找到,则置下标值mid
find = 1;//置找到标志变量flag为真
}
}
return pos; //循环结束仍未找到,则返回值-1
}
- 在被调函数中改变形参数组元素值时,实参数组元素值也会随之改变。这种改变并不是形参反向传给实参造成的,而是形参和实参因具有同一地址,共享同一段内存单元造成的。
7.5向函数传递二维数组
- 对二维表格进行数据处理,必须使用二维数组。
例:某班期末考试科目为数学(MT)、英语(EN)和物理(PH),有最多不超过40人参加考试,要求计算学生总分和平均分,以及每门课程的总分和平均分
#include<stdio.h>
#define STUD_N 40
#define COURSE_N 3
void ReadScore(int score[][COURSE_N],long num[],int n);
void AverforStud(int score[][COURSE_N],int sum[],float aver[],int n);
void AverforCourse(int score[][COURSE_N],int sum[],float aver[],int n);
void Print(int score[][COURSE_N],long num[],int sumS[],float averS[],int sumC[],float averC[],int n);
int main(void)
{
int score[STUD_N][COURSE_N],sumS[STUD_N],sumC[COURSE_N],n;
long num[STUD_N];
float averS[STUD_N],averC[COURSE_N];
printf("input the total number of the students(n<=40):");
scanf("%d",&n);
ReadScore(score,num,n);//读入n个学生的学号和成绩
AverforStud(score,sumS,averS,n);//计算每个学生的总分平均分
AverforCourse(score,sumC,averC,n);//计算每门课程的总分平均分
Print(score,num,sumS,averS,sumC,averC,n);//输出学生成绩
return 0;
}
//函数功能:输入n个学生的学号及其三门课的成绩
void ReadScore(int score[][COURSE_N],long num[],int n )
{
int i,j;
printf("Input student's ID and score as : MT EN PH:\n");
for(i = 0;i<n;i++) //对所有学生进行循环
{
scanf("%ld",&num[i]); //以长整形格式输入每个学生的学号
for(j = 0;j<COURSE_N;j++) //对所有课程进行循环
{
scanf("%d",&score[i][j]); //输入每个学生的各门课成绩
}
}
}
//函数功能:计算每个学生的总分和平均分
void AverforStud(int score[][COURSE_N],int sum[],float aver[],int n)
{
int i,j;
for(i=0;i<n;i++)
{
sum[i] = 0;
for(j=0;j<COURSE_N;j++) //对所有课程进行循环
{
sum[i] = sum[i]+score[i][j]; //计算第i个学生的总分
}
aver[i] = (float)sum[i]/COURSE_N; //计算第i个学生的平均分
}
}
//函数功能:计算每门功课的总分和平均分
void AverforCourse(int score[][COURSE_N],int sum[],float aver[],int n)
{
int i,j;
for(j = 0;j < COURSE_N;j++)
{
sum[j] = 0;
for(i = 0;i<n;i++) //对所有学生进行循环
{
sum[j] = sum[j] + score[i][j]; //计算第i门课程的总分
}
aver[j] = (float)sum[j]/n;//计算第j门课程的平均分
}
}
//函数功能:打印每个学生的学号、各门课成绩、总分和平均分,以及每门课的总分和平均分
void Print(int score[][COURSE_N],long num[],int sumS[],float averS[],int sumC[],float averC[],int n)
{
int i,j;
printf("Student's ID\t MT\t EN\t PH\t SUM\t AVER\n");
for(i=0;i<n;i++)
{
printf("%12ld\t",num[i]); //以长整形格式打印学生的学号
for(j=0;j<COURSE_N;j++)
{
printf("%4d\t",score[i][j]); //打印学生的每门课成绩
}
printf("%4d\t% 5.1f\n",sumS[i],averS[i]); //打印学生总分和平均分
}
printf("SumofCourse\t");
for(j=0;j<COURSE_N;j++)
{
printf("%4d\t",sumC[j]); //打印每门课的总分
}
printf("\nAverofCourse\t");
for(j=0;j<COURSE_N;j++)
{
printf("%4.1f\t",averC[j]); //打印每门课的平均分
}
printf("\n");
}
- 当形参被声明为二维数组时,可以省略数组第一维的长度声明,但不能省略数组第二维的长度声明。
第8章 指针
8.1变量的内存地址
- 取地址运算符(Address Operator),即&。
例:使用取地址运算符&取出变量的地址,然后将其显示在屏幕上
#include<stdio.h>
int main(void)
{
int a = 0,b = 1;
char c = 'A';
printf("a is %d,&a is %p\n",a,&a);
printf("b is %d,&b is %p\n",b,&b);
printf("c is %d,&c is %p\n",c,&c);
return 0;
}
- %p格式符,表示输出变量a、b、c的地址值。
- 注意,这里的地址值是用一个十六进制(以16为基)的无符号整数表示的,其字长一般与主机的字长相同。
- 变量在内存中所占存储空间的首地址,称为该变量的地址(Address),而变量在存储空间中存放的数据,称为变量的值(Value)。
- 如果在声明变量时没有给变量赋初值,那么它们的内容就是随机的、不确定的。
- 变量的名字(Name)可看成是对程序中数据存储空间的一种抽象。
8.2指针变量的定义和初始化
- 存放变量的地址需要一种特殊类型的变量,这种特殊的数据类型就是指针(Pointer)。
- 具有指针类型的变量,称为指针变量,它是专门用于存储变量的地址值的变量。
- 定义形式为: 类型变量关键字 *指针变量名;
- 其中,类型关键字代表指针变量要指向的变量的数据类型,即指针变量的基类型(Base Type)。
例:使用指针变量在屏幕上显示变量的地址值
#include<stdio.h>
int main(void)
{
int a = 0,b = 1;
char c = 'A';
int *pa,*pb;//定义了可以指向整型数据的指针变量pa和pb
char *pc;//定义了可以指向字符型数据的指针变量pc
printf("a is %d,&a is %p,pa is %p\n",a,&a,pa);
printf("b is %d,&b is %p,pb is %p\n",b,&b,pb);
printf("c is %d,&c is %p,pc is %p\n",c,&c,pc);
return 0;
}
- 为避免忘记指针初始化给系统带来的潜在危险,习惯上在定义指针变量的同时将其初始化为NULL(在stdio.h中定义为零值的宏)。
#include<stdio.h>//修改后的程序
int main(void)
{
int a = 0,b = 1;
char c = 'A';
int *pa = NULL,*pb = NULL;//定义了可以指向整型数据的指针变量pa和pb并用NULL对其进行初始化
char *pc = NULL;//定义了可以指向字符型数据的指针变量pc并用NULL对其进行初始化
printf("a is %d,&a is %p,pa is %p\n",a,&a,pa);
printf("b is %d,&b is %p,pb is %p\n",b,&b,pb);
printf("c is %d,&c is %p,pc is %p\n",c,&c,pc);
return 0;
}
#include<stdio.h>//将地址赋给指着变量的程序
int main(void)
{
int a = 0,b = 1;
char c = 'A';
int *pa,*pb;
char *pc;
pa = &a;//初始化指针变量pa,使其指向a
pb = &b;//初始化指针变量pb,使其指向b
pc = &c;//初始化指针变量pc,使其指向c
printf("a is %d,&a is %p,pa is %p\n",a,&a,pa);
printf("b is %d,&b is %p,pb is %p\n",b,&b,pb);
printf("c is %d,&c is %p,pc is %p\n",c,&c,pc);
return 0;
}
- 指向某变量的指针变量,通常简称为某变量的指针,虽然指针变量中存放的是变量的地址值,二者在数值上相等,但在概念上变量的指针并不等同于变量的地址。
- 变量的地址是一个常量,不能对其进行赋值。而变量的指针则是一个变量,其值是可以改变的。
- 指针变量只能指向同一基类型的变量,否则将引起warning。
8.3间接寻址运算符
- 直接按变量名或者变量的地址存取变量的内容的访问方式,称为直接寻址(Direct Address)。
- 通过指针变量间接存取它所指向的变量的访问方式称为间接寻址(Indirect Address)。
- 在C语言中,获取变量的地址需要使用取地址运算符&。
- 指针运算符(Pointer Operator),也称间接寻址运算符(Indirect Operator)或解引用运算符(Dereference Operator),即*。间接寻址运算符 *用来访问指针变量指向的变量的值。
- 运算时要求指针已被正确初始化或者已指向内存中某个确定的存储单元。
例:使用指针变量,通过间接寻址输出变量的值
#include<stdio.h>
int main(void)
{
int a = 0,b = 1;
char c = 'A';
int *pa = &a,*pb = &b;
char *pc = &c;
*pa = 9;//修改指针变量pa所指向的变量的值
printf("a is %d,&a is %p,pa is %p,*pa is %d\n",a,&a,pa,*pa);
printf("b is %d,&b is %p,pb is %p,*pb is %d\n",b,&b,pb,*pb);
printf("c is %d,&c is %p,pc is %p,*pc is %c\n",c,&c,pc,*pc);
return 0;
}
- *作为间接引用运算符,用于读取并显示指针变量中存储的内存地址所对应的变量的值,即指针变量所指向的变量的值,这两种用法之间并无关系。应用指针所指向的变量的值,也称为指针的解引用(Pointer Dereference)。
- 未初始化的指针(Uninitialized Pointer)引起非法访问内存造成的必然后果。
- 使用指针必须恪守三条准则:
(1)永远清楚每个指针指向了哪里,指针必须指向一块有意义的内存;
(2)永远清楚每个指针指向的对象的内容是什么;
(3)永远不要使用未初始化的指针变量;
8.4按值调用与模拟按引用调用
- 按值调用(Call by Value)的方法,即程序将函数调用语句中的实参的一份副本传给函数的形参。
例:演示程序按值调用的例子
#include<stdio.h>
void Fun(int par);
int main(void)
{
int arg = 1;
printf("arg = %d\n",arg);
Fun(arg); //传递实参的副本给函数
printf("arg = %d\n",arg);
return 0;
}
void Fun(int par)
{
printf("par = %d\n",par);
par = 2; //改变形参的值
}
- 指针变量的一个重要应用就是用作函数参数,指针作函数参数时,虽然实际上也是传值给被调函数(C语言中的所有函数调用都是按值调用),但是传给被调函数的这个值不是变量的值,而是变量的地址,通过向被调函数传递某个变量的地址值可以在被调函数中改变主调函数中这个变量的值,相当于模拟了C++语言中的按引用调用,因此成为模拟按引用调用(Simulating Call by Reference)。
例:演示程序模拟按引用调用的例子
#include<stdio.h>
void Fun(int *par);
int main(void)
{
int arg = 1;
printf("arg = %d\n",arg);
Fun(&arg); //传递变量arg的地址值给函数
printf("arg = %d\n",arg);
return 0;
}
void Fun(int *par)
{
printf("par = %d\n",*par);
*par = 2; //改变形参指向的变量的值
}
例:从键盘任意输入两个整数,编程实现将其交换后再重新输出。
#include<stdio.h>
void Swap(int *x,int *y);
int main(void)
{
int a,b;
printf("Please enter a,b:");
scanf("%d,%d",&a,&b);
printf("Before swap:a = %d,b = %d\n",a,b);
Swap(&a,&b);//模拟按引用调用函数Swap()
printf("After swap:a = %d,b = %d\n",a,b);
return 0;
}
void Swap(int *x,int *y)
{
int temp;
temp = *x;
*x = *y;
*y = temp;
}
8.5用指针变量作函数参数的程序实例
例:从键盘输入某班学生某门课的成绩(每班人数最多不超过40人,具体人数由键盘输入)。
#include<stdio.h>
#define N 40
void FindMax(int score[],long num[],int n,int *pMaxScore,long *pMaxNum);
int main(void)
{
int score[N],maxScore;
int n,i;
long num[N],maxNum;
printf("How many students?");
scanf("%d",&n); //从键盘输入学生的人数n
printf("Input student's ID and score:\n");
for(i = 0;i<n;i++)
{
scanf("%ld%d",&num[i],&score[i]);
}
FindMax(score,num,n,&maxScore,&maxNum);//模拟按引用调用函数
printf("maxScore = %d,maxNum = %ld\n",maxScore,maxNum);
return 0;
}
void FindMax(int score[],long num[],int n,int *pMaxScore,long *pMaxNum)
{
int i;
*pMaxScore = score[0];//假设score[0]为当前最高分
*pMaxNum = num[0];//记录score[0]的学号num[0]
for(i = 1;i<n;i++)//对所有score[i]进行比较
{
if(score[i]>*pMaxScore)//如果score[i]高于当前最高i分
{
*pMaxScore = score[i];//用score[i]修改当前最高分
*pMaxNum = num[i];//记录当前最高分学生的学号num[i]
}
}
}
- 由于指针形参所指向的变量的值在函数调用结束后才能被确定,因此这两个指针形参称为函数的出口参数,函数FindMax()的前三个形参在函数调用前必须确定其值,因此称为函数的入口参数。
8.6函数指针及其应用
- 函数指针(Function Pointers)就是指向函数的指针(Pointer to a Function),指向函数的指针变量中存储的是一个函数在内存中的入口地址。
- 指向存储这个函数的第一条指令的地址,称为函数的入口地址。
例:用函数指针编程实现一个通用的排序函数,既能实现对学生成绩的升序排序,又能实现对学生成绩的降序排序。
#include<stdio.h>
#define N 40
int ReadScore(int score[]);
void PrintScore(int score[],int n);
void SelectionSort(int a[],int n,int (*compare)(int a,int b));
int Ascending(int a,int b);
int Descending(int a,int b);
void Swap(int *x,int *y);
int main(void)
{
int score[N],n;
int order;
n = ReadScore(score);
printf("Total students are %d\n",n);
printf("Enter 1 to sort in ascending order,\n");
printf("Enter 2 to sort in descending order:");
scanf("%d",&order);
printf("Data items in original order\n");
PrintScore(score,n);
if(order == 1)
{
SelectionSort(score,n,Ascending);//函数指针指向Ascending()
printf("Data items in ascending order\n");
}
else
{
SelectionSort(score,n,Descending);//函数指针指向Descending()
printf("Data items in descending order\n");
}
PrintScore(score ,n);
return 0;
}
//函数功能:输入学生某门课的成绩,当输入负值时,结束输入,返回学生人数
int ReadScore(int score[])
{
int i = -1;
do{
i++;
printf("Input score:");
scanf("%d",&score[i]);
}while(score[i]>=0);
return i;
}
//函数功能:输出学生成绩
void PrintScore(int score[],int n)
{
int i;
for(I = 0;i<n;i++)
{
printf("%4d",score[i]);
}
printf("\n");
}
//函数功能:调用函数指针compare指向的函数实现对数组a的交换法排序
void SelectionSort(int a[],int n,int (*compare)(int a,int b))
{
int i,j,k;
for(i = 0;i<n-1;i++)
{
k = i;
for(j =i+1;j<n;j++)
{
if((*compare)(a[j],a[k]))
{
k = j;
}
}
if(k != i)
{
Swap(&a[k],&a[i]);
}
}
}
//使数据按升序排序
int Ascending(int a,int b)
{
return a<b;//这样比较决定了按升序排序,如果a<b,则交换
}
//使数据按降序排序
int Descending(int a,int b)
{
return a>b;//这样比较决定了按降序排序,如果a>b,则交换
}
//函数功能:两整数值互换
void Swap(int *x,int *y)
{
int temp;
temp = *x;
*x = *y;
*y = temp;
}
第9章 字符串
9.1字符串字面量
- 字符串字面量(sting literal),有时候也称为字符串常量,它是由一对双引号括起来的一个字符序列。
- 无论双引号内是否包含字符,包含多少个字符,都代表一个字符串字面量。
- 为便于确定字符串的长度,C编译器会自动在字符串的末尾添加一个ASCII码值为0的空操作符’\0’作为字符串结束的标志,在字符串中可以不显示地写出。因此,字符串(String)实际就是由若干有效字符构成且以字符’\0’作为结束的一个字符序列。
9.2字符串的存储
- C语言没有提供字符串数据类型,因此字符串的存取要用字符型数组来实现。
- 字符串结束标志’\0’也占一个字节的内存,但它不计入字符串的实际长度,只计入数组的长度。
- 字符数组的初始化可表示为:
char str[6] = {'H','e','l','l','o','\0'};
- 还可以写成:
char str[] = {'H','e','l','l','o','\0'};
或
char str[] = "Hello";
- 通常,将一个字符串存放在一维字符数组中,将多个字符串存放在二维数字符数组中。当用二维字符数组存放多个字符串时,数组第一维的长度代表要存储的字符串的个数,可以省略,但是第二维的长度不能省略,应按最长的字符串长度设定数组第二维的长度。
char weekday[7][10] = {"Sunday","Monday","Tuesday","Wendnesday","Thursday","Friday","Saturday"};
- 若字符串太长,无法写在一行中,则可将其拆分成几个小的片段写在不同的行中。
char longString[] = "This is the first half of the string"
"and this is the second half.";
9.3字符指针
- 字符指针(Character Pointers)是指向字符型数据的指针变量。每个字符串在内存中都占用一段连续的存储空间,并有唯一确定的首地址。因此,只要将字符串的首地址赋值给字符指针,即可让字符指针指向一个字符串。
char *ptr = "Hello";
等价于
char *ptr;
ptr = "Hello";//都表示为定义一个字符指针变量ptr,并用字符串字面量"Hello"在常量存储区中的首地址为其初始化,即让ptr指向字符串字面量"Hello"
*ptr = 'W';//这种写法不合法
- 不能修改ptr指向的常量存储区中的字符,因为它是只读的。
char str[10] = "Hello";
char *ptr = str;//这种方式是合法的,由于数组名代表数组的首地址,因此将str赋值给ptr,就是让ptr指向数组str中存储的字符串
等价于
char *ptr;
ptr = str;//等价于ptr = &str[0];
- 因数组名是一个地址常量,所以str的值是不可以修改的,但ptr的值(即ptr的指向)可以被修改,ptr所指向的字符串也可以被修改。
*ptr = 'W';//等价于ptr[0] = 'W';相当于str[0] = 'W';
- 正确使用字符指针,必须明确字符串被保存到了哪里以及字符指针指向了哪里。
9.4字符串的访问和输入/输出
9.4.1如何访问字符串中的单个字符
- 若字符指针ptr指向了字符数组str的首地址,则即可通过*(ptr+i)来引用字符串中的第i+1个字符,(ptr+i)相当于(str+i),即str[i],也可以通过ptr++操作,即移动指针ptr,使ptr指向字符串中的某个字符。
- 注意,对于数组名str,不能使用str++操作使其指向字符串中的某个字符,因为数组名是一个地址常量,其值是不能被改变的。
9.4.2字符串的输入/输出
- 按c格式符,一个字符一个字符地单独输出/输入
for(i = 0;j<10;i++)
{
scanf("%c",&str[i]);//输出字符数组
}
for(i = 0;i<10;i++)
{
printf("%c",str[i]);//输出字符数组
}
//多数情况下字符数组的长度并不固定,因此很少使用上面这种方式,更常用的方式是借助字符串结束标志'\0',识别字符串的结束。
for(i = 0;str[i] != '\0';i++)
{
printf("%c",str[i]);//输出字符串
}
- 按s格式符,将字符串作为一个整体输入/输出
scanf("%s",str);//表示读入一个字符串,直到遇到空白符(空格、回车符或制表符)为止
printf("%s",str);//表示输出一个字符串,直到遇字符串结束标志为止
例:下面程序从键盘输入一个人名,并把它显示在屏幕上
#include<stdio.h>
#define N 12
int main(void)
{
char name[N];
printf("Enter your name:");
scanf("%s",name);
printf("Hello %s!\n",name);
scanf("%s",name);//读取输入缓冲区 中余下的上次未被读走的字符
printf("Hello %s!\n",name);
return 0;
}
-
用%d输入数字或%s输入字符串时,忽略空格、回车或制表符等空白字符(被作为数据的分隔符),读到这些字符时,系统认为数据读入结束,因此用函数scanf()按s格式符不能输入带空格的字符串。
-
使用字符串处理函数gets(),可以输入带空格的字符串,因为空格和制表符都是字符串的一部分。
-
此外,函数gets()与scanf()对回车符的处理也不同。gets()以回车符作为字符串的终止符,同时将回车符从缓冲区读走,但不作为字符串的一部分。而scanf()不读走回车符,回车符仍留在输入缓冲区中。
例:使用函数gets(),从键盘输入一个带有空格的人名,然后把它显示在屏幕上
#include<stdio.h>
#define N 12
int main(void)
{
char name[N];
gets(name);
printf("Hello %s!\n",name);
return 0;
}
- 函数puts()用于从括号内的参数给出的地址开始,依次输出存储单元中的字符,当遇到第一个’\0’时输出结束,并且自动输出一个换行符。
- 函数gets()不能限制输入字符串的长度,很容易引起缓冲区溢出。
- 使用scanf()和gets()输入字符串时,要确保输入字符串的长度不超过数组的大小,否则建议使用能限制输入字符长度的函数。
#include<stdio.h>
#define N 12
int main(void)
{
char name[N];
printf("Enter your name:");
fgets(name,sizeof(name),stdin);//限制输入字符串长度不超过数组大小
printf("Hello %s!\n",name);
return 0;
}
9.5字符串处理函数
- 字符串处理函数库提供了很多有用的函数可以用于字符串处理操作以及确定字符串的长度。
例:请编程实现按奥运会参赛国国名在字典中的顺序对其入场次序进行排序,假设参赛国不超过150个
#include<stdio.h>
#include<string.h>
#define MAX_LEN 10
#define N 150
void SortString(char str[][MAX_LEN],int n);
int main(void)
{
int i,n;
char name[N][MAX_LEN];//定义二维字符数组
printf("How many countries?");
scanf("%d",&n);
getchar();//读走输入缓冲区中的回车符
printf("Input their names:\n");
for(i = 0;i<n;i++)
{
gets(name[i]);//输入n个字符串
}
SortString(name,n);//字符串按字典顺序排序
printf("Sorted results:\n");
for(i = 0;i<n;i++)
{
puts(name[i]);//输出排序后的n个字符串
}
return 0;
}
//函数功能:交换法实现字符串按字典顺序排序
void SortString(char str[][MAX_LEN],int n)
{
int i,j;
char temp[MAX_LEN];
for(i = 0;i<n-1;i++)
{
for(j = i+1;j<n;j++)
{
if(strcmp(str[j],str[i])<0)
{
strcpy(temp,str[i]);
strcpy(str[i],str[j]);
strcpy(str[j],temp);
}
}
}
}
-
对单个字符进行赋值操作可以使用赋值运算符,但是赋值运算符不能用于字符串的赋值操作,字符串赋值只能使用函数strcpy()。
-
比较单个字符可以使用关系运算符,但比较字符串不能直接使用关系运算符。
-
不能使用if(str[j]<str[i])而应使用函数strcmp()来比较字符串的大小。
-
字符串比较大小时,实际上是根据两字符对比时出现的第一对不相等的字符的大小来决定它们所在字符串的大小的。
-
所有的字母在计算机中都被表示称数字编码(Numeric Codes)的。当计算机比较字符串时,实际比较的是字符串中字符的数字编码。
9.6向函数传递字符串
- 因为字符数组和字符指针都可以存取C字符串,因此,向函数传递字符串时,既可使用字符数组作函数参数,也可使用字符指针作函数参数。
例:编程是实现strcpy()函数的功能
#include<stdio.h>
#define N 80
void MyStrcpy(char dstStr[],char srcStr[]);
int main(void)
{
char a[N],b[N];
printf("Input a string:");
gets(a);//输入字符串
MyStrcpy(b,a);//将字符数组a中的字符串复制到b中
printf("The copy is:");
puts(b);//输出复制后的字符串
return 0;
}
//函数功能:用字符数组作为函数参数,实现字符串复制
void MyStrcpy(char dstStr[],char srcStr[])
{
int i = 0;//数组下标初始化为0
while(srcStr[i] != '\0')//若当前取出的字符不是字符串结束标志
{
dstStr[i] = srcStr[i];//复制字符
i++;//移动下标
}
dstStr[i] = '\0';//在字符串dstStr的末尾添加字符串结束标志
}
- 注意,与使用其他类型数组不同的是,通常不适用长度即计数控制的循环来判断数组元素是否遍历结束,而使用条件控制的循环,利用字符串结束标志’\0’判断字符串中的字符是否遍历结束。
函数MyStrcpy()的源代码如下:
//函数功能:用字符指针作为函数参数,实现字符串复制
void MyStrcpy(char *dstStr,char *srcStr)
{
while(*srcStr != '\0')//若当前srcStr所指字符不是字符串结束标志
{
*dstStr = *srcStr;//复制字符
srcStr++;//使srcStr指向下一个字符
dstStr++;//使dstStr指向下一个存储单元
}
*dstStr = '\0';//在字符串dstStr的末尾添加字符串结束标志
}
例:编程实现strlen()函数的功能
#include<stdio.h>
unsigned int MyStrlen(const char str[]);
int main(void)
{
char a[80];
printf("Input a string:");
gets(a);
printf("The length of the string is: %u\n",MyStrlen(a));
return 0;
}
//方法一
//函数功能:用字符型数组作函数参数,计算字符串的长度
unsigned int MyStrlen(const char str[])
{
int i;
unsigned int len = 0;//计数器置0
for(i = 0;str[i] != '\0';i++)
{
len++;//利用循环统计不包括'\0'在内的字符个数
}
return len;//返回实际字符个数
}
//方法二
unsigned int MyStrlen(const char *pStr);
//函数功能:用字符指针作函数参数,计算字符串的长度
unsigned int MyStrlen(const char *pStr)
{
unsigned int len = 0;
for(;*pStr != '\0';pStr++)
{
len++;
}
return len;
}
- 为防止实参在被调函数中被意外修改,可以在相应的形参前面加上类型限定符const。
- 如果在函数体内试图修改形参的值,那么将会产生编译错误。
9.7从函数返回字符串指针
- 函数之间的握手(信息交换)是通过函数参数和返回值来实现的。
char *f();//声明的是一个返回字符指针的函数f()
char (*f)();//定义的是一个函数指针f,该函数指针指向的函数没有形参,返回值是字符型
例:不使用字符串处理函数strcat(),编程实现strcat()的功能
#include<stdio.h>
#define N 80
char *MyStrcat(char *dstStr,char *srcStr);
int main(void)
{
char first[2*N];
char second[N];
printf("Input the first string:");
gets(first);
printf("Input the second string:");
gets(second);
printf("The result is:%s\n",MyStrcat(first,second));
return 0;
}
//函数功能:将字符串srcStr链接到字符串dstStr的后面
char *MyStrcat(char *dstStr,char *srcStr)
{
char *pStr = dstStr;//保存字符串dstStr首地址
//将指针移到字符串dstStr的末尾
while(*dstStr != '\0')
{
dstStr++;
}
//将字符串srcStr复制到字符串dstStr的后面
for(;*srcStr != '\0';dstStr++,srcStr++)
{
*dstStr = *srcStr;
}
*dstStr = '\0';//在连接后的字符串的末尾添加字符串结束标志
return pStr;//返回链接后的字符串dstStr的首地址
}
9.8扩充内容
9.8.1const类型限定符
(1)const放在类型关键字的前面
int a,b;
const int *p = &a;
//按照从右到左的顺序,可将这条变量声明语句读作:“p是一个指针变量,可指向一个整形常量(Integer Constant)”。它表明*p是一个常量,而p不是。由于*p是只读的,是不可以在程序中被修改的,所以一旦将*p作为左值在程序中对其进行赋值,将被视为非法操作。
//注意:虽然这里*p的值是不可被修改的,但p指向的变量a的值仍然是可以修改的,即对a执行赋值操作是合法的。因指针变量p的值是可以被修改的,所以这里如果执行赋值操作p = &b也是合法的,经过这个赋值之后,指针变量p就不再指向变量a而指向变量b了。
(2)const放在类型关键字的后面和*变量名的前面
int const *p = &a;
//按照从右到左的顺序,可将这条变量声明语句读作:“p是一个指针变量,可指向一个常量整数(Constant Integer)”。它表明*p是一个常量,而p不是。由于*p是只读的,所以不能使用指针变量p修改这个“为常量的整型数”,它和第一种情况是等价的。
(3)const放在类型关键字*的后面,变量名的前面
int* const p = &a;
//按从右到左的顺序,可将这条变量声明语句读作:“p是一个指针常量,可指向一个整型(Integer)数据”。它表明p是一个常量,而*p不是。由于p是一个常量指针,是只读的,其值是不可以被修改的,所以在程序中不能修改指针p,让它指向其他变量,但是他所指向的变量的值是可以修改的。例如,此时执行*p = 20这样的赋值操作是合法的,而执行p = &b这样的赋值操作就是非法的。
(4)一个const放在类型关键字之前,另一个const放在类型关键字*之后和变量名之前
const int* const p = &a;
//按从右到左的顺序,可将这条变量声明语句读作:“p是一个指针常量,可指向一个整型常量(Integer Constant)”。它表明p和*p都是一个常量,都是只读的。这时,无论执行*p=20还是执行p=&b这样的赋值操作,都将被视为非法操作。
9.8.2字符处理函数
- 字符处理函数库中包含了用于对字符数据进行测试和操作的标准库函数,使用这些函数时,必须在程序开头包含头文件<ctype.h>
例:输入一行字符,统计其中的英文字符、数字字符、空格和其他字符的个数
#include<stdio.h>
#define N 80
int main(void)
{
char str[N];
int i,letter = 0,digit = 0,space = 0,others = 0;
printf("Input a string:");
gets(str);
for(i = 0;str[i] != '\0';i++)
{
if(str[i]>='a'&&str[i]<='z' || str[i]>='A'&&str[i]<='Z')
{
letter++;//统计英文字符
}
else if(str[i]>='0'&&str[i]<='9')
{
digit++;//统计数字字符
}
else if(str[i] == ' ')
{
space++;//统计空格
}
else
{
others++;//统计其他字符
}
}
printf("English character:%d\n",letter);
printf("digit character:%d\n",digit);
printf("space:%d\n",space);
printf("other character:%d\n",others);
return 0;
}
//使用字符处理函数
#include<stdio.h>
#include<ctype.h>
#define N 80
int main(void)
{
char str[N];
int i,letter = 0,digit = 0,space = 0,others = 0;
printf("Input a string:");
gets(str);
for(i = 0;str[i] != '\0';i++)
{
if(isalpha(str[i]))
{
letter++;//统计英文字符
}
else if(isdigit(str[i]))
{
digit++;//统计数字字符
}
else if(isspace(str[i]))
{
space++;//统计空格(包括制表符)
}
else
{
others++;//统计其他字符
}
}
printf("English character:%d\n",letter);
printf("digit character:%d\n",digit);
printf("space:%d\n",space);
printf("other character:%d\n",others);
return 0;
}
例:从键盘输入一个人的英文名和姓,然后将其名(forename)和姓(surname)的第一个字母都变成大些字母。
#include<stdio.h>
#include<ctype.h>
#define N 80
int main(void)
{
char name[N];
int i;
printf("Input a name:");
gets(name);
i = 0;
while(!isalpha(name[i]))//跳过所有空格,直到遇字母为止
{
i++;
}
name[i] = toupper(name[i]);//将名的首字母变为大写
while(!isspace(name[i]))//跳过所有字母,直到遇空格为止
{
i++;
}
while(!isalpha(name[i]))//跳过所有空格,直到遇字母为止
{
i++;
}
name[i] = toupper(name[i]);//将姓的首字母变为大写
printf("Formatted Name:%s\n",name);
return 0;
}
9.8.3数值字符串向数值的转换
- C语言提供的字符串转换函数可将数字字符串转换为整型或浮点型的数值,使用这些函数时,必须在程序开头包含头文件<stdlib.h>
例:字符串转换函数的用法
#include<stdio.h>
#include<stdlib.h>
int main(void)
{
char str[] = {" 123.5"};
int intNum;
long longNum;
double doubleNum;
intNum = atoi(str);
longNum = atol(str);
doubleNum = atof(str);
printf("intNum = %d\n",intNum);//字符串转换成为整型数
printf("longNum = %ld\n",longNum);//字符串转换为长整型数
printf("doubleNum = %f\n",doubleNum);//字符串转换为双精度实型数
return 0;
}
第10章 指针和数组
10.1指针和一维数组间的关系
1.数组名的特殊意义及其在访问数组元素中的作用
- 一旦给出数组的定义,编译系统就会为其内存中分配固定的存储单元。相应地,数组的首地址也就确定了。数组元素在内存中更是连续存放的,C语言中的数组名有特殊的含义,它代表存放数组元素的连续存储空间的首地址,即指向数组中第一个元素的指针常量。
例:数组元素的引用方法
#include<stdio.h>
int main(void)
{
int a[5],i;
printf("Input five numbers:");
for(i = 0;i<5;i++)
{
scanf("%d",&a[i]);//用下标法引用数组元素
}
for(i = 0;i<5;i++)
{
printf("%4d",a[i]);//用下标法引用数组元素
}
printf("\n");
return 0;
}
//另一种写法
#include<stdio.h>
int main(void)
{
int a[5],i;
printf("Input five numbers:");
for(i = 0;i<5;i++)
{
scanf("%d",a+i);//这里a+i等价于&a[i]
}
for(i = 0;i<5;i++)
{
printf("%4d",*(a+i));//这里*(a+i)等价于a[i]
}
printf("\n");
return 0;
}
2.指针运算的特殊性及其在访问数组元素中的作用
- 指针的算数运算和关系运算常常是针对数组元素而言的。因数组在内存是连续存放的,所以指向同一数组中不同元素的两个指针的关系运算常用于比较他们所指元素在数组中的前后位置。指针的算数运算(如增1和减1)则常用于移动指针的指向,使其指向数组中的其他元素。
- 注意,p+1与p++本质上是两个不同的操作,因为没有对p进行赋值操作,所以p+1并不改变当前指针的指向,p仍然指向原来指向的元素,而p++相当于执行p = p+sizeof(p的指针类型),因此p执行了赋值操作而改变了指针的p的指向,此外该操作并不是将指针p向前移动一个字节,而是将指针变量p向前移动一个元素位置,即指向了下一个元素。
- p++并非将指针变量p的值简单地加1,而是加上1*sizeof(基类型)个字节。
#include<stdio.h>
int main(void)
{
int a[5],*p;
printf("Input five numbers:");
for(p = a;p<a+5;p++)
{
scanf("%d",p);//用指针法引用数组元素
}
for(p = a;p<a+5;p++)
{
printf("%4d",*p);//用指针法引用数组元素
}
printf("\n");
return 0;
}
//采用指针的下标表示法
#include<stdio.h>
int main(void)
{
int a[5],*p;
p = a;//p = a 等价于p = &a[0]
printf("Input five numbers:");
for(p = a;p<a+5;p++)
{
scanf("%d",&p[i]);//&p[i]等价于p+i
}
p = a;//在再次循环开始前,确保指针p指向数组首地址
for(p = a;p<a+5;p++)
{
printf("%4d",p[i]);//p[i]等价于*(p+i)
}
printf("\n");
return 0;
}
3.数组和指针作为函数参数进行模拟按引用调用中的相似性
- 用数组名和用指向一维数组的指针变量作函数实参,向被调函数传递的都是数组的起始地址,都是模拟按引用调用。
例:下面程序用于演示数组和指针变量作为函数参数
//方法一:被调函数的形参声明为数组类型,用下标法访问数组元素
void InputArray(int a[],int n);
void OutputArray(int a[],int n);
void InputArray(int a[],int n)//形参声明为数组,输入数组元素值
{
int i;
for(i = 0;i<n;i++)
{
scanf("%d",&a[i]);//用下标法访问数组元素
}
}
void OutputArray(int a[],int n)//形参声明为数组,输出数组元素值
{
int i;
for(i = 0;i<n;i++)
{
printf("%4d",a[i]);//用下标法访问数组元素
}
printf("\n");
}
//方法二:被调函数的形参声明为指针变量,用指针法访问数组元素
void InputArray(int *pa,int n);
void OutputArray(int *pa,int n);
void InputArray(int *pa,int n)//形参声明为指针变量,输出数组元素值
{
int i;
for(i = 0;i<n;i++;pa++)
{
scanf("%d",pa);//用指针法访问数组元素
}
}
void OutputArray(int *pa,int n)//形参声明为指针变量,输出数组元素值
{
int i;
for(i = 0;i<n;i++,pa++)
{
printf("%4d",*pa);//用指针法访问数组元素
}
printf("\n");
}
//方法三:被调函数的形参声明为数组类型,用指针法访问数组元素
void InputArray(int a[],int n);
void OutputArray(int a[],int n);
void InputArray(int a[],int n)//形参声明为数组,输入数组元素值
{
int i;
for(i = 0;i<n;i++)
{
scanf("%d",a+i);//这里a+i等价于&a[i]
}
}
void OutputArray(int a[],int n)//形参声明为数组,输出数组元素值
{
int i;
for(i = 0;i<n;i++)
{
printf("%4d",*(a+i));//这里*(a+i)等价于a[i]
}
printf("\n");
}
//方法四:被调函数的形参声明为指针变量,用下标法访问数组元素
void InputArray(int *pa,int n);
void OutputArray(int *pa,int n);
void InputArray(int *pa,int n)//形参声明为指针变量,输入数组元素值
{
int i;
for(i = 0;i<n;i++)
{
scanf("%d",&pa[i]);//形参声明为指针变量时也可以按下标方式访问数组
}
}
void OutputArray(int *pa,int n)//形参声明为指针变量,输出数组元素值
{
int i;
for(i = 0;i<n;i++)
{
printf("%4d",pa[i]);//形参声明为指针变量时也可以按下标方式访问数组
}
printf("\n");
}
//在主函数中,都可以用数组名作函数实参
#include<stdio.h>
int main(void)
{
int a[5];
printf("Input five numbers:");
InputArray(a,5);//用数组名作为函数实参
OutputArray(a,5);//用数组名作为函数实参
return 0;
}
//主函数也可以用指针变量作为函数实参
#include<stdio.h>
int main(void)
{
int a[5];
int *p = a;
printf("Input five numbers:");
InputArray(p,5);//用指向数组a的指针变量作为函数实参
OutputArray(p,5);//用指向数组a的指针变量作为函数实参
return 0;
}
10.2指针和二维数组间的关系
1.二维数组的行地址和列地址
- a[i] [j]与下面几种形式是等价的
a[i][j]等价于*(a[i]+j)等价于*(*(a+i)+j)等价于(*(a+i))[j]
- 如果将二位数的数组名a看成一个行地址(第0列的地址), 则a+i代表二维数组a的第i行的地址,a[i]可以看成列地址。行地址a每次加1,表示下一行,而列地址a[i]每次加1表示下一列
2.通过二维数组的行指针和列指针来引用二维数组元素
-
通过对二维数组的行地址和列地址的分析可知,二位数组中有两种指针。一种是行指针,使用二维数组的行地址进行初始化;另一种是列指针,使用二维数组的列地址进行初始化。
-
定义二维数组可使用此方法定义行指针:int (*p)[4];
-
在解释变量声明语句中变量的类型时,虽然说明符[]的优先级高于*,但由于圆括号的优先级更高,所以先解释 *,再解释[],所以,p的类型被表示为p——> *——>[4]——>int说明定义了一个可指向含有四个元素的一维整形数组的指针变量。关键字int代表行指针所指一维数组的类型。[]中的4表示行指针所指一维数组的长度,它是不可以省略的。实际上,这个指针变量p可作为一个指向二维数组的行指针,它所指向的二维数组的每一行有4个元素。
-
注意,在变量声明语句中必须显式地指定变量所指向的一维数组的长度(对应于二维数组的列数)。对指向二维数组的行指针p进行初始化的方法为:p = a;或p = &a[0];
-
通过指针p引用二维数组a的元素a [i] [j]的方法可用以下4种等价的形式:
p[i][j]等价于*(p[i]+j)等价于*((*p+i)+j)等价于(*(p+i))[j]
- 由于列指针所指向的数据类型为二维数组的元素类型,因此列指针和指向同类型简单变量的指针的定义是一样的。
- 定义二维数组a,定义列指针如下:int *p;
- 可用以下三种等价的方式对其进行初始化
p = a[0];等价于p = *a;等价于p = &a[0][0];
- 定义了指针p后,为了能通过p引用二维数组a的元素a[i] [j],可将数组a看成一个由(m行n列)个元素组成的一维数组。由于p代表数组的第0行第0列的地址,而从数组的第0行第0列寻址到数组的第i行第j列,中间需跳过i * n+j个元素,因此,p+i * n+j代表数组的第i行第j列的地址,即&a[i] [j],(p+i * n+j)或p[i * n+j]都表示a[i] [j].
- 注意此时不能用p[i] [j]来表示数组元素,这是因为此时并未将这个数组看成二维数组,而是将二维数组等同于一维数组看待的,也就是将其看成了一个具有m*n个元素的一维数组。正因如此,再定义二维数组指针的列指针时,无需指定它所指向的二维数组的列数。
例:编写程序 ,输入一个3行4列的二维数组,然后输出这个二维数组的元素值
//先用二维数组作函数形参编写程序如下
#include<stdio.h>
#define N 4
void InputArray(int p[][N],int m,int n);
void OutputArray(int p[][N],int m,int n);
int main(void)
{
int a[3][4];
printf("Input 3*4 numbers:\n");
InputArray(a,3,4);//向函数传递二维数组的第0行的地址
OutputArray(a,3,4);//向函数传递二维数组的第0行的地址
return 0;
}
//形参声明为列数已知的二维数组,输入数组元素值
void InputArray(int p[][N],int m,int n)
{
int i,j;
for(i = 0;i<m;i++)
{
for(i = 0;i<m;i++)
{
scanf("%d",&p[i][j]);
}
}
}
//形参声明为列数已知的二维数组,输出数组元素值
void OutputArray(int p[][N],int m,int n)
{
int i,j;
for(i = 0;i<m;i++)
{
for(j = 0;j<n;j++)
{
printf("%4d",p[i][j]);
}
}
}
//方法二
void InputArray(int (*p)[N],int m,int n);
void OutputArray(int (*p)[N],int m,int n);
//形参声明为指向列数已知的二维数组的行指针,输入数组元素值
void InputArray(int (*p)[N],int m,int n)
{
int i,j;
for(i = 0;i<m;i++)
{
for(j = 0;j<n;j++)
{
scanf("%d",*(p+i)+j);
}
}
}
//形参声明为指向列数已知的二维数组的行指针,输出数组元素值
void InputArray(int (*p)[N],int m,int n)
{
int i,j;
for(i = 0;i<m;i++)
{
for(j = 0;j<n;j++)
{
printf("%4d",*(*(p+i)+j));
}
printf("\n");
}
}
- 为了程序能适应二维数组数列数的变化,应使用二维数组的列指针作函数形参,在主函数中向其传递二维数组的第0行第0列元素的首地址
#include<stdio.h>
void InputArray(int *p,int m,int n);
void OutputArray(int *p ,int m,int n);
int main(void)
{
int a[3][4];
printf("Input 3*4 numbers:\n");
InputArray(*a,3,4);//向函数传递二维数组的第0行第0列的地址
OutputArray(*a,3,4);//向函数传递二维数组的第0行第0列的地址
return 0
}
//形参声明为指向二维数组的列指针,输入数组元素值
void InputArray(int *p,int m,int n)
{
int i,j;
for(i = 0;i<m;i++)
{
for(j = 0;j<n;j++)
{
scanf("%d",&p[i*n+j]);
}
}
}
//形参声明为指向二维数组的列指针,输出数组元素值
void OutputArray(int *p,int m,int n)
{
int i,j;
for(i = 0;i<m;i++)
{
for(j = 0;j<n;j++)
{
printf("%4d",p[i*n+j]);
}
printf("\n");
}
}
10.3指针数组及其应用
10.3.1指针数组用于表示多个字符串
-
由若干基类型相同的指针所构成的数组,称为指针数组(Point Array)。有定义可知,指针数组的每个元素都是一个指针,且这些指针指向相同数据类型的变量。
-
指针数组的最主要的用途之一就是对多个字符串进行处理操作。虽然有时字符指针数组和二维数组能解决同样的问题,但涉及多字符串处理操作时,使用字符指针数组比二维字符数组更有效,例如可以加快字符串的排列顺序。
例:用指针数组编程实现按奥运会参赛国国名在字典中的顺序对其入场次序进行排序参赛国不超过150人
#include<stdio.h>
#include<string.h>
#define MAX_LEN 10//字符串最大长度
#define N 150//字符串个数
void SortString(char *ptr[],int n);
int main(void)
{
int i,n;
char name[N][MAX_LEN];
char *pStr[N];
printf("How many countines?");
scanf("%d",&n);
getchar();
printf("Input their names:\n");
for(i = 0;i<n;i++)
{
pStr[i] = name[i];
gets(pStr[i]);
}
SortString(pStr,n);
printf("Sorted results:\n");
for(i = 0;i<n;i++)
{
puts(pStr[i]);//输出排序后的n个字符串
}
return 0;
}
//函数功能:用指针数组作函数参数书,采用交换法实现字符串按字典顺序排序
void SortString(char *ptr[],int n)
{
int i,j;
char *temp = NULL;//因交换法的是字符字符串的地址值,故temp定义为指针变量
for(i = 0;i<n-1;i++)
{
for(j = i+1;j<n;j++)
{
if(strcmp(ptr[j],ptr[i])<0)//交换指向字符串的指针
{
temp = prt[i];
ptr[i] = ptr[j];
ptr[j] = temp;
}
}
}
}
- 注意,因指针数组的元素是一个指针,所以与指针变量一样,在使用指针数组之前必须对数组元素进行初始化。指针变量为初始化时,其值是不确定的,即它指向的存储单元是不确定的,此时对其进行写操作是很危险的。
- 通过移动字符串在实际物理存储空间中的存放位置而实现的排序,称为物理排序;而用指针数组存储每个字符串的首地址时,字符串排序时无需改变字符串在内存中的存储位置,只要改变指针数组中各元素的指针即可。这种通过移动字符串的索引地址实现的排序,称为索引排序。显然,移动指针的指向比移动字符串要快得多,所以,相对于物理排序而言,使用索引排序的效率更高。
10.3.2指针数组用于表示命令行参数
- 在DOS操作系统下,将文件file1.c的内容复制到文件file2.c中,用如下命令:
copy file1.c file2.c
- 这种运行程序的方式称为命令行,copy、file1.c和file2.c称为命令行参数(Command Line Arguments)。命令行参数file1.c和file2.c分别代表复制的源文件和目标文件的文件名,他们之间用一个或多个空格分隔。
例:命令行参数与函数main()各形参之间的关系。
#include<stdio.h>
int main(int argc,char *argv[])
{
int i;
printf("The number of command line arguements is:%d\n",argc);
printf("The program name is:%s\n",argv[0]);
if(argc>1)
{
printf("The other arguments are following:\n");
for(i = 1;i<argc;i++)
{
printf("%s\n",argv[i]);
}
}
return 0;
}
- 命令行参数很有用,尤其在Linux操作系统下或批处理命令中使用较为广泛。
10.4动态数组
10.4.1C程序的内存映像
- 一个编译后的C程序获得并使用四块在逻辑上不同且用于不同目的的内存储区。从内存的低端开始,第一块内存为只读存储区,存放程序的机器代码和字符串字面量等只读数据,相邻的一块内存是静态存储区,用于存放程序中的全局变量和静态变量等,其他两块内存分别称为堆(Heap)和栈(Stack),为动态存储区。其中,栈用于保存函数调用时的返回地址、函数的形参、局部变量及CPU的当前状态等程序的运行信息。堆是一个自由存储区,程序可利用C的动态内存分配函数来使用它。
- C语言程序中变量的内存分配方式有以下3种:
(1)从静态存储区分配
程序的全局变量和静态变量都在静态存储区上分配,且在程序编译时就已经分配好了,在程序运行期间始终占据这些内存,仅在程序终止前,才被操作系统回收。
(2)在栈上分配
在执行函数调用时,系统在栈上为函数内的局部变量及形参分配内存,函数执行结束时,自动释放这些内存。栈内存分配运算内置于处理器的指令集之中,效率很高,但是容量有限。如果往栈中压入的数据超出预先给栈分配的容量,那么就会出现栈溢出,从而使程序运行失败。
(3)在程序运行期间,用动态内存分配函数来申请的内存都是从堆上分配的。动态内存的生存期由程序员自己来决定,使用非常灵活,但也最易出现内存泄露等问题。为防止内存泄漏的发生,程序员必须及时调用free()释放已不再使用的内存。
10.4.2动态内存分配函数
- 在C语言中,指针之所以重要,原因有以下4点:
(1)指针为函数提供修改变量值的手段;
(2)指着为C的动态内存分配系统提供支持;
(3)指针为动态数据结构(如链表、队列、二叉树等)提供支持;
(4)指针可以改善某些子程序的效率;
- 指针的另一个重要应用是把指针与动态内存分配函数联用,它使得实现动态数据(Dynamically Allocated Array)成为可能。动态内存分配(Dynamic Memory Allocation)是指在程序运行时为变量分配内存的一种方法。全局变量是编译时分配的,非静态的局部变量使用栈空间,因此两者在程序运行时既不能添加,也不能减少。而在实际应用中,有时在程序运行中需要数量可变的内存空间,即在运行时才能确定要用多少个字节的内存来存放数据。
- C的动态内存分配函数从堆上分配内存。使用这些函数时只要在程序开头将头文件<stdlib.h>包含到程序中即可。
1.函数malloc()
- 函数malloc()用于分配若干字节的内存空间,返回一个指向该内存首地址的指针。若系统不能够提供足够的内存单元,函数将返回空指针NULL。函数malloc()的原型为:
void *malloc(unsigned int size);
//其中,size表示向系统申请空间的大小,函数调用成功将返回一个指向void类型的指针
- void *指针是ANSI C新标准中增加的一种指针类型,具有一般性,通常称为通用指针(Generic Pointer)或者无类型的指针(Typeless Pointer),常用来说明未知的指针,即声明了一个指针变量,但未指定它可以指向哪一种基类型的数据。因此,若要将函数调用的返回值赋予某个指针,则应先根据该指针的基类型,用强转的方法将返回值的指针值强转为所需的类型,然后再进行赋值操作。
例如:
int *pi = NULL;
pi = (int *)malloc(2);
//其中,malloc(2)表示申请一个大小为2个字节的内存,将malloc(2)返回值的void *类型强转为int * 类型后再赋值给int型指针变量pi,即用int型指针变量pi指向这段存储空间的首地址。
- 若不能确定某种类型所占内存的字节数,则需使用sizeof()计算本系统中该类型所占内存的字节数,然后再用malloc()向系统申请相应字节的存储空间。
例如:
pi = (int *)malloc(sizeof(int));
//这种方法有利于提高程序的可移植性
2.函数calloc()
- 函数calloc()用于给若干同一类型的数据项分配连续的存储空间并赋值为0,其函数原型为:
void *calloc(unsigned int num,unsigned int size);
- 它相当于一个声明了一个一维数组。其中第一个num表示向系统申请的内存空间的首地址,否则返回空指针NULL。若要将函数的返回地址赋值给某个指针变量,则应先根据该指针的基类型,将其强转为与指针基类型相同的数据类型,然后进行赋值操作。
例如:
float *pf = NULL;
pf = (float *)calloc(10,sizeof(float));
//表示向系统申请10个连续的float型存储单元,并用指针pf指向该指针连续内存的首地址,系统申请的总的内存字节数为10*四则哦发(float)
- 相当于使用下面的语句:
pf = (float *)malloc(10*sizeof(float));
//但从安全角度考虑,使用calloc()更明智,因为与malloc()不同的是calloc()能自动将分配的内存初始化为0。
3.函数free()
- 函数free()的功能是释放向系统动态申请的由指针p指向的存储空间,其原型为:
void free(void *p);
//该函数无返回值。
- 唯一的形参p只能是由malloc()和calloc()申请内存时返回的地址。该函数执行后,将以前分配的由指针p指向的内存返还给系统,以便由系统重新支配。
4.函数realloc()
- 函数realloc()用于改变原来分配的存储空间的大小,其原型为:
void *realloc(void *p,unsigned int size);
//该函数的功能是将指针p所指向的存储空间的大小改为size个字节,函数返回值是新分配的存储空间的首地址,与原来分配的首地址不一定相同。
- 由于动态内存分配的存储单元是无名的,只能通过指针变量来引用它,所以一旦改变了指针的指向,原来分配的内存及数据也就随之丢失了。因此不要轻易改变指针变量的值。
10.4.3长度可变的一维动态数组
例:编程输入某班学生的某门课成绩,计算并输出其平均分。学生人数由键盘输入。
#include<stdio.h>
#include<stdlib.h>
void InputArray(int *p,int n);
double Average(int *p,int n);
int main(void)
{
int *p = NULL,n;
double aver;
printf("How many students?");
scanf("%d",&n);
p = (int *)malloc(n * sizeof(int));
if(p = NULL)//确保指针使用前是非空指针,当p为空指针时结束程序运行
{
printf("No enough memory!\n");
exit(1);
}
printf("Input %d score:",n);
InputArray(p,n);
aver = Average(p,n);
printf("%.1f\n",aver);//输出平均分
free(p);//释放向系统系统申请的内存
return 0;
}
//形参声明为指针变量,输入数组元素值
void InputArray(int *p,int n)
{
int i;
for(i = 0;i<n;i++)
{
scanf("%d",&p[i]);
}
}
//形参声明为指针变量,计算数组元素的平均值
double Average(int *p,int n)
{
int i,sum = 0;
for(i = 0;i<n;i++)
{
sum = sum + p[i];
}
return (double)sum/n;
}
- 注意不要忘记用free()释放不再使用的动态申请的内存
10.4.4长度可变的二维动态数组
例:编程输入m个班学生(每班n个学生)的某门课成绩,计算并输出平均分。班级数和每班学生数由键盘输入
#include<stdio.h>
#include<stdlib.h>
void InputArray(int *p,int m,int n);
double Average(int *p,int m,int n);
int main(void)
{
int *p = NULL,m,n;
double aver;
printf("How many classes?");
scanf("%d",&m);//输入班级数
printf("How many students in a class?");
scanf("%d",&n);//输入每班学生人数
p = (int *)calloc(m*n,sizeof(int));//向系统申请内存
if(p == NULL)//确保指针使用前是非空指针,当p为空指针时结束程序运行
{
printf("No enough memory!\n");
exit(1);
}
InputArray(p,m,n);//输入每班学生人数
aver = Average(p,m,n);//计算平均分
printf("aver = %.1f\n",aver);//输出平均分
free(p);//释放向系统申请的内存
return 0;
}
//形参声明为指向二维数组的列指针,输入数组元素值
void InputArray(int *p,int m,int n)
{
int i,j;
for(i = 0;i<m;i++)//m个班
{
printf("Please enter scores of class %d :\n",i+1);
for(j = 0;j<n;j++)//每班n个学生
{
scanf("%d",&p[i*n+j]);
}
}
}
//形参声明为指针变量,计算数组元素的平均值
double Average(int *p,int m,int n)
{
int i,j,sum = 0;
for(i = 0;i<m;i++)//m个班
{
for(j = 0;j<n;j++)//每班n个学生
{
sum = sum + p[i*n+j];
}
}
return (double)sum/(m*n);
}
10.5扩充内容
10.5.1常见的内存错误及其对策
- 常见的内存异常错误有两类,一是非法访问错误,即代码访问了不该访问的内存地址,二是因持续的内存泄漏导致系统内存不足。编译器往往不易发现这类错误,在程序运行时才能捕捉到,且因征兆时隐时现,增加了排错的难度.
-
内存分配未成功就使用
造成这类错误的原因是他们没有意识到内存分配会不会成功。避免这类错误的方法就是,在使用之前检查一下指向他的指针是否为空指针NULL即可。
-
内存分配成功了,但是尚未初始化就使用
此类错误的起因主要有两个,一是没有建立“指针必须先初始化后才能使用”的观念,二是误以为内存的默认初值全为零。尽管有时内存的默认初值是零(例如静态数组),但是为了避免使用未被初始化的内存导致的引用初值错误,解决这个问题的最简单方法就是不要嫌麻烦。也就是说,无论数组是以何种方式创建的,都不要忘记给他赋初值,即使是赋零值也不要省略。对用malloc()和calloc()动态分配的内存,最好使用函数memset()进行清零操作。对于指针变量,即使后面有对其进行赋初值的语句,也最好是在定义时就将其初始化为NULL。
-
内存分配成功了,也初始化了,但是发生了越界使用
在使用数组时常发生这类错误,特别是在循环语句中遍历数组元素时,循环次数很容易使下标“多1“或者”少1“,从而导致数组操作越界。
-
忘记了释放内存,造成了内存泄漏
向系统申请的动态内存是不会自动被释放的,因此,一定不要忘记释放不再使用的内存,否则会造成内存泄露(Memory Leak)。对于包含这类错误的函数,只要它被调用一次,就会丢失一块内存。有未释放的垃圾并不足以导致系统因内存不足而崩溃,在不同情况下,内存垃圾给系统带来的影响是不同的。内存泄露的严重程度取决于每次遗留内存垃圾的多少以及代码被调用的次数。调用次数越多,丢失内存就越多。因此这类错误比较隐蔽,刚开始时,系统内存也许是充足的,看不出错误的征兆,当系统运行一段时间后,随着丢失内存数量的增多,程序就会因为出现“内存耗尽”而突然死掉。
- 降低内存泄露错误发生概率的一般方法如下:
(1)仅在需要时才使用malloc(),并尽量减少malloc()调用的次数,能用自动变量解决的问题,就不要用malloc()来解决。
(2)配套使用malloc()和free(),并尽量让malloc()和与之配套的free()集中在一个函数内,尽量把malloc()放在函数的入口处,free()放在函数的出口处。
(3)如果malloc()和free()无法集中在一个函数中,那么就要分别单独编写申请内存和释放内存的函数,然后使其配对使用。
(4)重复利用malloc()申请到内存,有助于减少内存泄漏露发生的概率。
例:分析下面程序存在的问题 void Init(void) { char *pszMyName = NULL, *pszHerName = NULL,*pszHisName; pszMyName = (char *)malloc(256); if(pszMyName == NULL) { return; } pszHerName = (char *)malloc(256); if(pszHerName == NULL) { return; } pszHisName = (char *)malloc(256); if(pszHisName == NULL) { return; } ... //正常处理的代码 free(pszMyName); free(pszHerName); free(pszHisName); return; } //虽然程序中的malloc()和free()是配套使用的,但前面的malloc()调用成功后面的调用不成功时,直接退出函数将导致前面已分配的内存未被释放。修改程序如下: void Init(void) { char *pszMyName = NULL,*pszHerName = NULL,*pszHisName = NULL; pszMyName = (char *)malloc(256); if(pszMyName == NULL) { return; } pszHerName = (char *)malloc(256); if(pszHerName == NULL) { free(pszHerName); return; } pszHisName = (char *)malloc(256); if(pszHisName == NULL) { free(pszHisName) return; } ... //正常处理的代码 free(pszMyName); free(pszHerName); free(pszHisName); return; } //这个程序的问题是:有大量重复的语句,且如果再增加其他malloc函数调用语句,相应的free函数调用语句也要增加很多。修改程序如下: void Init(void) { char *pszMyName = NULL,*pszHerName = NULL,*pszHisName = NULL; pszMyName = (char *)malloc(256); if(pszMyName == NULL) { goto Exit; } pszHerName = (char *)malloc(256); if(pszHerName == NULL) { goto Exit; } pszHisName = (char *)malloc(256); if(pszHisName == NULL) { goto Exit; } ... //正常处理的代码 Exit: { if(pszMyName != NULL) { free(pszMyName); } if(pszHerName != NULL) { free(pszHerName); } if(pszHisName != NULL) { free(pszHisName); } } return; } //这个程序中使用了goto语句,使得“重用率很高,但很难写成单一函数”的代码的流程变得更加清晰,且代码集中,所有错误最后都指向Exit标号后的语句来实现。
-
释放内存后仍然能继续使用
非法内存操作的一个共同特征就是代码访问了不该访问的内存地址。例如,使用了未分配成功的内存、引用未初始化的内存、越界访问内存,以及释放了内存却继续使用它。其中,释放了内存但却仍然能够继续使用它,将导致产生“野指针”。
例:分析下面的程序能否实现“输入一个不带空格的字符串并显示到屏幕上”的功能。 #include<stdio.h> char *GetStr(void); int main(void) { char *ptr = NULL; ptr = GetStr(); puts(ptr);//试图使用野指针 return 0; } char *GetStr(void) { char s[80]; scanf("%s",s);//定义动态存储类型的数组 return s;//试图返回动态局部变量的地址 } //在Code::Blocks下编译此程序,将显示如下警告: //function returns address of local variable //这句警告的含义是“返回局部变量的地址”。虽然不影响程序的运行,但运行结果是乱码。这是因为程序在return s;这句代码试图从函数返回指向局部变量的地址,导致了野指针的错误。动态局部变量都是在栈上创建内存的,在函数调用结束后就被自动释放了,释放后的内存中的数据将变成随机数,因此此时输出其中的数据必然为乱码。
- 当指针指向的栈内存被释放以后,只想它的指针并未消亡。内存被释放后,指针的值(即栈内存的首地址)其实并没有改变,它仍然指向这块内存,只不过内存中存储的数据变成了随机值(乱码)而已。释放内存的结果只改变了内存中存储的数据,使该内存存储的内容变成了垃圾。指向垃圾内存的指针,就被称为野指针。
- 内存被释放后,指向它的指针不会自动变成空指针,野指针不是空指针,空指针很容易检查,但野指针却很危险,因为我们无法预知野指针的值究竟是多杀,所以用if语句对防止使用野指针并不奏效。
//程序修改如下: #include<stdio.h> void GetStr(char *); int main(void) { char s[80]; char *ptr = s;//指针初始化,使其指向数组s的首地址 GetStr(ptr); puts(ptr);//将用户输入的字符串正确地显示输出 return 0; } void GetStr(char *s) { scanf("%s",s); } //程序修改称如下,将会因为使用空指针而异常中止 #include<stdio.h> void GetStr(char *); int main(void) { char *ptr = NULL;//指针变量初始化为空指针 GetStr(ptr); puts(ptr);//试图使用空指针 return 0; } void GetStr(char *s)//指针形参接受实参传过来的是空指针 { scanf("%s",s);//试图使用空指针 } //若修改成下面的形式,将会因为形参s不能反回函数中动态分布的内存首地址给实参,这样实参普通人仍为空指针 #include<stdio.h> #include<stdlib.h> void GetStr(char *); int main(void) { char *ptr = NULL; GetStr(ptr); puts(ptr);//试图使用空指针 return 0; } void GetStr(char *s) { s = (char *)malloc(80);//申请动态分配的内存 scanf("%s",s); }
- 虽然函数形参s不能返回函数中在堆上动态分布的内存首地址给实参,但利用return语句可返回动态分配的内存首地址给主调函数,不会造成使用野指针的问题,这是因为动态分配的内存不会在函数调用结束后被自动释放,必须使用free()才能释放程序可修改为:
#include<stdio.h> #include<stdlib.h> char *GetStr(char *s); int main(void) { char *ptr = NULL; ptr = GetStr(ptr);//使ptr指向动态分配的内存首地址 puts(ptr); free(ptr); return 0; } char *GetStr(char *s) { s = (char *)malloc(80);//申请动态分配的内存 scanf("%s",s); return s;//返回动态分配的内存的首地址 } //或 #include<stdio.h> #include<stdlib.h> char *GetStr(); int main(void) { char *ptr = NULL; ptr = GetStr(); puts(ptr); free(ptr); return 0; } char *GetStr() { char *s = NULL; s = (char *)malloc(80);//申请动态分配内存 scanf("%s",s); return s;//返回动态分配的内存的首地址 }
- 综上所述,野指针的形成主要有以下几种情况:
(1)指针操作超越了变量的作用范围,如使用return语句返回动态局部变量的地址;
(2)指针变量未被初始化,指针混乱往往使得结果变得难以预料和莫名其妙;
(3)指针变量所指向的动态内存被free后未置为NULL,让人误以为它仍是合法的
- 针对上述问题的解决对策是:
(1)不要把局部变量的地址(即指向“栈内存”的指针)作为函数的返回值返回,因为局部变量的分配的内存在退出函数时将被自动释放。
(2)再定义指针变量的同时对其进行初始化,要么设置为NULL,要么使其指向合法内存。
(3)尽量把malloc()集中在函数的入口处,free()集中在函数的出口处,避免内存被释放后继续使用。如果free()不能放在函数的出口处,则在调用free()后,应立即将指向这段内存的指针设置为NULL,这样在使用指针之前检查其是否NULL才会有效。
- 除内存泄露外,内存分配为成功就使用、内存尚未初始化就使用、越界使用内存,以及使用已经被释放了的内存这几种情况都属于非法内存访问错误。
10.5.2缓冲区溢出攻击
- 网络黑客常常针对系统和程序自身存在的内存漏洞,编写相应的攻击程序。其中最常见的就是对缓冲区溢出漏洞的攻击,几乎占到了网络攻击次数的一半以上。而在诸多缓冲区溢出中又以堆栈溢出的问题最有代表性。
- 世界上第一个缓冲区溢出攻击——Internet蠕虫,曾造成全球多台网络服务器瘫痪。缓冲区溢出漏洞被攻击的现象已经越来越普遍,各种操作系统上出现的此类漏洞数不胜数。
- 对缓冲区溢出攻击的后果包括程序运行失败、系统崩溃和重新启动等。更为严重的是,可利用缓冲区溢出执行非授权指令,甚至取得系统特权,进而进行各种非法操作。于是如何防止和检测利用缓冲区溢出漏洞进行的攻击,称为防御网络入侵和入侵检测的重点之一。
- 缓冲区溢出通常是因gets()、scanf()、strcpy()等函数未对数组越界加以监视和限制,导致有用的堆栈数据被覆盖引起的。
例:分析下面程序的漏洞
#include<stdio.h》
#include<string.h>
#define N 10
int mian(void)
{
char str[N];
gets(str);
puts(str);
return 0;
}
//这个函数存在着“缓冲区溢出”的隐患。原因就出在函数gets()上面,他不限制用户输入字符串的长度,当用户输入的字符长度超过N时就会发生缓冲区溢出。为防止缓冲区溢出,可修改为:
//fgets(str,N*sizeof(char),stdin);
- strcpy()等字符串处理函数也存在类似的问题
#include<stdio.h>
#include<string.h>
#define N 1024
int main(int argc,char *argv[])
{
char buffer[N];
if(argc>1)
{
strcpy(buffer,argv[1]);
}
return 0;
}
/*假设攻击者调用这段程序时传入的字符串argv[1]的禅古大于1024字节,由于复制操作与参数压入堆栈的方向是相反的,因此执行程序strcpy(buffer,argv[1]);语句后,会将多于1024字节的内容复制到堆栈从栈顶开始向栈底延伸的地方,相应地,超出1024个字节的内容就回一次覆盖堆栈中保存的寄存器、函数调用的返回地址。如果攻击者精心设计传入的参数,在1024个字节以内的地方写上一段攻击代码,然后在恰巧能覆盖堆栈中函数返回地址的位置上写上了一个经周密计算得到的地址,该地址精确地指向前面的攻击代码,那么当函数执行完毕时,系统就返回到攻击代码中,进而夺取系统的控制权,完成攻击任务。*/
- 执行函数调用时,操作系统一般要完成如下几个工作:
(1)将函数参数argc和argv压入堆栈
(2)在堆栈中,保存函数调用的返回地址(即函数调用结束后要执行的语句的地址)
(3)在堆栈中,保存一些其他内容(如有用的系统寄存器)
(4)在堆栈中,为函数的局部变量分配存储空间
(5)执行函数代码
- 黑客常用的缓冲区溢出攻击都是从缓冲区溢出开始的,一方面是利用了操作系统中函数调用和局部变量存储的基本原理,另一方面是利用了应用程序中的内存操作漏洞,使用特定的参数造成应用程序内存异常,并改变操作系统的指令执行序列,让系统执行攻击者预先设定的代码,进而完成权限获取、非法入侵等攻击任务。
- 函数strcpy()不限制复制字符的长度,给黑客以可乘之机。而使用strncpy(),strncat()等“n族”字符处理函数,痛股票增加一个参数来限制字符串处理的最大长度,可防止发生缓冲区溢出。
第11章 结构体和数据结构基础
11.1从基本数据类型到抽象数据类型
- 当表达复杂数据对象时,仅使用几种基本数据类型显然是不够的。某些语言试图规定较多的基本数据类型(如数组、树、栈等)来解决这个问题。但实践表明,这不是一个好的方法,因为任何一种程序设计语言都无法将实际应用中涉及的所有复杂数据对象都作为其基本数据类型。所以,根本的解决方法就是允许用户自定义数据类型(User-Define Data Type)。于是在后来发展的语言(如C语言)中,出现了构造数据类型(也称为复合数据类型)。它允许用户根据实际需要利用已有的基本数据类型来构造自己所需要的数据类型,它们由基本数据类型派生而来,用于表示链表、树、堆栈等复杂的数据对象。
- 所谓抽象数据类型(Abstract Data Type,ADT)是指这样一种数据类型,它不再是单纯是一组值的集合,还包括作用在值集上的操作的集合,即在构造数据类型的基础上增加了对数据的操作,且类型表示的细节及操作的实现细节对外是不可见的
- 抽象数据类型可达到更好的信息隐蔽效果,因为它使程序不依赖与数据节后的具体实现的具体实现方法,只要提供相同的操纵,换用其它方法实现时,程序无需修改,这个特征对于系统的维护很有利。C++中的类(Class)是抽象数据类型的一种具体实现,也是面向对象(Object-Oriented)程序设计语言中的一个重要概念。
11.2结构体的定义
11.2.1为什么要定义结构体类型
- 由于数组是具有相同数据类型数据的集合,所以只能按照列的方向定义相应类型的数组来表示表格中的数据(假设表格中最多有30个数据),例如学生的学号、姓名、性别、出生年月、各科成绩。
- 这体现了以下问题:
(1)分配内存不集中,局部数据的相关性不强,寻址效率不高。每个学生的信息零散地分散在内存中,要查询一个学生的全部信息,需要东翻西找,十分不便,因而效率不高。
(2)对数组赋初值时容易发生错位。一个数据的错位将导致后面所有数据都发生错误。
(3)结构显得比较零散,不易管理。
- C语言允许用户根据具体问题利用已有的基本数据类型来构造自己所需的数据类型,数组、结构体和共用体都属于和构造数据类型,但各有其特点。
- 数组是由相同类型的数据构成的一种数据结构,适用于对具有相同属性的数据进行批处理;而结构体(Structure)是将不同类型的数据成员组织到统一的名字之下,适合于对关系紧密、逻辑相关、具有相同或者不同属性的数据进行处理,尤其在书库管理中得到了广泛应用;
- 共用体(Union)虽然也能表示逻辑相关的不同类型的数据集合,但数据成员是情形互斥的,每一时刻只有一个数据成员起作用。
11.2.2结构体变量的定义
- 定义结构体ude第一步是声明一个结构体模版(Structure Template),其格式如下:
struct 结构体名
{
数据类型 成员1的名字;
数据类型 成员2的名字;
......
成员类型 成员n的名字;
};
- 结构体模板是由关键字struct及其后的结构体名组成的。分号(;)是结构体声明的结束标志,不能省略。结构体的名字,称为结构体标签(Structure Tag),作为用户自定义的结构体类型的标志,用于与其他结构体类型相区别。结构体中的各信息是在结构体标签后面的花括号{和}内声明的。构成结构体的变量,称为结构体成员(Structure Member)。每个结构体成员都有一个名字和相应的数据类型。结构体成员的命名必须遵从变量的命名规则。
- 声明结构体模板的主要目的是利用已有的数据类型定义一个新的数据类型。
例:利用结构体声明一个struct student的结构体类型:
struct student
{
long studentID;//学号
char studentName[10];//姓名
char studentSex;//性别
int yearOfBirth;//出生年
int scoreMath;//数学课的成绩
int scoreEnglish;//英语课的成绩
int scoreComputer;//计算机原理课的成绩
int scoreProgramming;//程序设计课的成绩
}
//也可以声明为一下的形式,将更加简洁:
struct student
{
long studentID;//学号
char studentName[10];//姓名
char studentSex;//性别
int yearOFBirth;//出生年
int score[4];//4门课程的成绩
}
- 注意,结构体模板只是声明了一种数据类型,定义了数据的组织形式,并未声明结构体类型的变量,因而编译器不为其分配内存,正如编译器不为int型分配内存一样。
- 定义结构体的第二步是利用已经定义好的结构体数据类型来定义结构体变量。C语言允许按照如下两种方式来定义结构体变量。
(1)先声明结构体模板,再定义结构体变量。
例如:定义一个具有struct student类型的结构体变量stu1
struct student stu1;
(2)在声明结构体模板的同时定义结构体变量。
例如:下面语句在声明结构体类型的同时定义了struct student类型的结构体变量stu1
struct student
{
long studentID;
char studentName[10];
char studentSex;
int yearOFBirth;
int score[4];
}stu1;
当结构体模板和结构体变量放在一起定义时,结构体标记是可选的,即也可以不出现结构体名。
例如:
struct
{
long studentID;
char studentName[10];
char studentSex;
int yearOFBirth;
int score[4];
}stu1;
但该方法因未定义结构体标签,不能再在程序的其他处定义结构体变量,因而并不常用。
11.2.3用typedef定义数据类型
- 关键字typedef用于为系统固有的或程序员自定义的数据类型定义一个别名。数据类型的别名通常使用大写字母,目的是为了与已有的数据类型相区分。
例如:
typedef int INTEGER;
//为int定义了一个新名字INTEGER,也就是说INTEGER和int是同义词
- 也可以为结构体定义一个别名
例如:
typedef struct student STUDENT;
//等价于
typedef struct student
{
long studentID;
char studentName[10];
char studentSex;
int yearOFBirth;
int score[4];
}STUDENT;
- 二者都是为struct student结构体类型定义了一个新的名字STUDENT,即STUDENT与struct student是同义词。
//下面两条语句是等价的
STUDENT stu1,stu2;//更简洁的形式
struct student stu1,stu2;
- 注意,typedef只是为了一种已存在的类型定义一个新的名字而已,并未定义一种新的数据类型。
11.2.4结构体变量的初始化
- 结构体变量的成员可以通过将成员的初值置于花括号之内来进行初始化。
例如:
STUDENT stu1 = {100310121,"王刚",'M',1991,{72,83,90,82}};
//等价于
struct student stu1 = {100310121,"王刚",'M',1991,{72,83,90,82}};
//结构体变量stu1的第一个成员被初始化为100310121,第二个成员是一个字符型数组,被初始化为字符串"王刚",依次类推。
- 如果说STUDENT类型代表学生成绩管理表结构,那么STUDENT类型的变量stu1和stu2就分别代表了成绩管理表中的学生的记录信息,相当于STUDENT类型的实例化。
11.2.5嵌套的结构体
- 嵌套的结构体(Nested Structure)就是在一个结构体内包含了另一个结构体作为其成员。
例如:先声明一个日期结构体模板如下:
typedef struct date
{
int year;
int month;
int day;
}DATA;
//根据这个DATE构体模板来声明STUDENT结构体模板
typedef Struct student
{
long studentID;
char studentName[10];
char studentSex;
DATE birthday;//出生日期
int score[4];
}STUDENT;
- 这里,在结构体的定义中出现了“嵌套”,因为STUDENT结构体内包含了另一个DATE结构体类型的变量birthday作为其成员,因此他是一个嵌套的结构体。
//定义STUDENT类型的结构体变量stu1,并为其进行初始化如下:
STUDENT stu1 = {100310121,"王刚",'M',{1991,5,19},{72,83,90,82}};
//如果日期结构体模板设计成如下形式:
typedef struct date
{
int year;
char month[10];
int day;
}DATE;
//那么此时若要定义STUDENT类型的结构体变量stu1,则对其进行初始化的方法将变为:
STUDENT stu1 = {100310121,"王刚",'M',{1991,"May",19},{72,83,90,82}};
11.2.6结构体变量的引用
- 访问结构体变量的成员必须使用成员选择运算符(也称远点运算符)。其访问格式如下:
结构体成员变量名.成员名
//例如,可用下面的语句为结构体变量stu1和studentID成员进行赋值
stu1.studentID = 100310121;
//对于结构体成员,可以像其他普通变量一样进行赋值等运算。
- 当出现结构体嵌套时,必须以级联方式访问结构体成员,即通过成员选择运算符逐级找到最底层的成员时再引用。
例如:
stu1.birthday.year = 1991;
stu1.birthday.month = 5;
stu1.birthday.day = 19;
例:下面程序用于演示结构体变量的赋值和引用方法
#include<stdio.h>
typedef struct date
{
int year;
int month;
int day;
}DATE;
typedef struct student
{
long studentID;
char studentName[10];
char studentSex;
DATE birthday;
int score[4];
}STUDENT;
int main(void)
{
STUDENT stu1 = {100310121,"王刚",'M',{1991,5,19},{72,83,90,82}};
STUDENT stu2;
stu2 = stu1;//同类型的结构体变量之间的赋值操作
printf("stu2:%10ld%8s%3c%6d/ %02d/ %02d%4d%4d%4d%4d\n",
stu2.studentID,stu2.studentName,stu2.studentSex,stu2.birthday.year,
stu.birthday.month,stu2.birthday.day,
stu2.score[0],stu2.score[1],stu2.score[2],stu2.score[3]);
return 0;
}
//%02d,2d前面的前导符0表示输出数据时若左边有多余位则补0,于是输出为”1991/05/19“
-
C语言允许对具有相同结构体类型的变量进行整体赋值。在对两个同类型的结构体变量赋值时,实际上是按结构体的成员顺序逐一对相应成员进行赋值的额,赋值后的结果就是两个结构体变量的成员具有相同的内容。
-
对数组型结构体成员进行赋值时一定要使用strcpy(),因为,结构体成员studentName是一个字符型数组,studentName是该数组的名字,代表字符型数组的首地址,是一个常量,不能作为赋值表达式的左值。
-
并非所有的结构体成员都是可以使用赋值运算符来赋值,对字符数组类型的结构体成员进行赋值时,必须使用字符串处理函数strcpy()。
-
结构体类型的声明既可以放在所有函数体的外部,也可以放在函数体内部。在函数体外声明的结构体类型可以为所有函数使用,称为全局声明;在函数体内部声明的结构体类型只能在本函数体内使用,离开该函数,声明失效,称为局部生命。
例:从键盘输入结构体变量stu1的内容,那么程序的主函数可修改如下:
int main(void)
{
STUDENT stu1,stu2;
int i;
ptintf("Input a record:\n");
scanf("%ld",&stu1.studentID);
scanf("%s",stu1.studentName);//输入学生姓名,无需加&
scanf(" %c",&stu1.studentSex);//%c前有一个空格
scanf("%d",&stu1.birthday.year);
scanf("%d",&stu1.birthday.month);
scanf("%d",&stu1.birthday.day);
for(i = 0;i<4;i++)
{
scanf("%d",&stu1.score[i]);
}
stu2 = stu1;//同类型的结构体变量之间的赋值操作
printf("&stu2 = %p\n",&stu2);//打印结构体变量stu2的地址
printf("%10ld%8s%3c%6d/ %02d/ %02d%4d%4d%4d%4d\n",
stu2.studentID,stu2.studentName,stu2.studentSex,stu2.birthday.year,
stu.birthday.month,stu2.birthday.day,
stu2.score[0],stu2.score[1],stu2.score[2],stu2.score[3]);
return 0;
}
- 结构体变量的地址是结构体变量所占内存空间的首地址,而结构体成员的地址值与结构体成员在结构体中所处的位置及该成员所占内存的字节数相关。
11.2.7结构体所占内存的字节
例:下面程序用于演示结构体所占内存字节数的计算方法
#include<stdio,h>
typedef struct sample
{
char m1;
int m2;
char m3;
}SAMPLE;//定义结构体类型SAMPLE
int main(void)
{
SAMPLE s = {'a','2','b'};//定义结构体变量s并对其进行初始化
printf("bytes = %d\n",sizeof(s));//打印结构体变量s所占内存字节数
return 0;
}
//C99允许按名设置成员的初始值。例如,上面程序的第10行语句还可以写成下面的形式:
SAMPLE s = {.m1 = 'a',.m2 = 2,.m3 = 'b'};
- 若将上面程序中的第10行语句注释掉,并将第11行语句修改为:
printf("bytes = %d\n",sizeof(SAMPLE));//打印结构体类型所占内存字节数
//或修改为如下语句
printf("bytes = %d\n",sizeof(struct sample));
- 此程序输出的结果是12,难道不是6吗?,对于多数计算机系统而言,为了提高内存寻址的效率,很多处理器体系结构为特定的数据类型引入了特殊的内存对齐(Memory-Alignement)需求。不同的系统和编译器,内存对齐的方式有所不同,为了尽可能满足处理器的对其要求,可能会在较小的成员后加入补位,从而导致结构体实际所占内存的字节数会比我们想象的多出一些字节。
- 编译器是如何处理底层体系结构的对其限制的呢?32位体系结构 中,short型数据要求从偶数地址开始存放,而int型数据则被对齐在4字节地址边界,这样就保证了一个int型数据总能通过一次内存操作被访问到,每次内存访问是在4字节对齐的地址读取或存入32位数据。而读取存储在没有对齐的地址处的32位整数,则需两次读取操作,从两次读取得到的64位数中提取相关的32位整数还需要额外的操作,这样就会导致系统性能下降。
- 总之,系统为结构体变量分配内存的大小,或者说结构体类型所占内存的字节数,并非是所有成员所占内存字节数的总和,它不仅与所定义的结构体类型有关,还与计算机系统本身有关。由于结构体变量的成员的内存对齐方式和数据类型所占内存的大小都是与机器相关的,因此结构体在内存中所占的字节数也是与机器相关的。
- 计算结构体所占内存字节数时,一定要使用sizeof运算符,千万不要想当然地直接用对各成员进行简单求和的方式来计算,否则会降低程序的可移植性。
11.3结构体数组的定义和初始化
11.3.1结构体数组的定义
- 声明STUDENT结构体类型,然后定义结构体数组如下:
STUDENT stu[30];
//它定义了一个有30个元素的结构体数组,每个元素的类型为STUDENT
//该数组所占用的内存字节数为30*sizeof(STUDENT)。
//此时访问第一个学生的学号用stu[0].studentID
11.3.2结构体数组初始化
- 定义结构体数组的同时对其进行初始化。
STUDENT stu[30] = {{100310121,"王刚",'M',{1991,5,19},{72,83,90,82}},
{100310122,"李小明",'M',{1992,8,20},{88,92,78,78}},
{100310123,"王丽红",'F',{1991,9,19},{98,72,89,66}},
{100310124,"陈莉莉",'F',{1991,5,19},{87,95,78,90}},
};
例:利用结构体数组计算每个学生的4门课程的平均分
#include<stdio.h>
typedef struct date
{
int year;
int month;
int day;
}DATE;
typedef struct student
{
long studentID;
char studentName[10];
char studentSex;
DATE birthday;
int score[4];
}STUDENT;
int main(void)
{
int i,j,sum[30];
STUDENT stu[30] = {{100310121,"王刚",'M',{1991,5,19},{72,83,90,82}},
{100310122,"李小明",'M',{1992,8,20},{88,92,78,78}},
{100310123,"王丽红",'F',{1991,9,19},{98,72,89,66}},
{100310124,"陈莉莉",'F',{1991,5,19},{87,95,78,90}},
};\
for( i = 0;i<4;i++)
{
sum[i] = 0;
for(j = 0;j<4;j++)
{
sum[i] = sum[i] + stu[i].score[j];
}
printf("%10ld%8s%3c%6d/ %02d/ %02d%4d%4d%4d%4d%6.1f\n",
stu[i].studentID,
stu[i].studentName,
stu[i].studentSex,
stu[i].birthday.year,
stu[i].birthday.month,
stu[i].birthday.day,
stu[i].score[0],
stu[i].score[1],
stu[i].score[2],
stu[i].score[3],
sum[i]/4.0);
}
return 0;
}
11.4结构体指针的定义和初始化
11.4.1指针结构体变量的指针
- 假设已经声明了STUDENT结构体类型,那么定义一个指向该结构体类型的指针变量的方法为:
STUDENT *pt;//定义指向STUDENT结构体的指针变量
- 这里只是定义了一个指向STUDENT结构体类型的指针变量pt,但是此时的pt并没有指向一个确定的存储单元,其值是一个随机值。为使pt指向一个确定的存储单元,需要对指针变量进行初始化。
pt = &stu1;//让结构体指针变量pt指向结构体变量stu1
- 在定义指针变量pt的同时对其进行初始化,设计器指向结构体变量stu1
STUDENT *pt = &stu1;//定义指向STUDENT结构体的指针变量并对其进行初始化
- 如何访问结构体指针变量所指向的结构体成员呢?一种是使用成员选择运算符,也称圆点运算符。另一种是使用指向运算符,也称箭头运算符其标准的访问形式如下:
指针结构体的指针变量名->成员名
- 例如,若要访问结构体指针变量pt指向的结构体的studentID成员,则需使用下面的语句:
pt->studentID = 100310121;
//等价于
(*pt).studentID = 100310121;//这种方式不常用
//因()的优先级比成员选择运算符的优先级高,所以先将(*pt)作为一个整体,取出pt指向的结构体的内容,再将其看成一个结构体变量,利用成员选择运算符访问它的成员。
//例如:
pt->birthday.year = 1991;
pt->birthday.month = 5;
pt->birthday.day = 19;
11.4.2指向结构体数组的指针
- 定义结构体指针变量pt并将其指向结构体数组stu的方法为:
STUDENT *pt = stu;
//等价于
STUDENT *pt = &stu[0];
//等价于
STUDENT *pt;
pt = stu;
- 由于pt指向了STUDENT结构体数组stu的第一个元素stu[0]的首地址,因此可以用指向运算符来引用pt指向的结构体成员。例如,pt->score[0]引用的是stu[0].score[0]的值,即表示第一个学生的数学成绩。而pt+1指向的是下一个结构体数组元素stu[1]的首地址,以此类推。
11.5向函数传递结构体
- 将结构体传递给函数的方式有三种:
-
用结构体的单个成员作为函数参数,向函数传递结构体的单个成员。
用单个结构体成员作为函数实参,与普通类型的变量作为函数实参没社么区别,都是传值调用,在函数内部对其进行操作,不会引起结构体成员值得变化。这种向函数传递结构体的一个成员的方式,很少使用。
-
用结构体变量作为函数参数,向函数传递结构体的完整结构。
用结构体变量作为函数实参,向函数传递的是结构体的完整结构,即将整个结构体成员的内容复制给被调函数。在函数内可用成员选择运算符引用其结构体成员。因着种方式也是传值调用,所以,在函数内对形参结构体成员的值修改,不会影响相应的实参结构体成员。
当实参与形参是同一结构体类型时,才可以使用这种方式传递。当函数被调用时,系统为结构体行参变量分配的存储空间的大小由所定义的结构体类型决定。这种传递方式更直观,但因其占用的内存空间较大,因而时空开销较大。
-
用结构体指针或结构体数组作函数参数,向函数传递结构体的地址。
用指针结构体的指针变量或结构体数组作函数实参的实质是向函数传递结构体的地址,因为是传地址调用,所以在函数内部对形参结构体成员值的修改,将影响到实参结构体成员的值。
由于仅复制结构体首地址一个值给被调函数,并不是将整个结构体的内容复制给被调函数,因而相对与第二种方式而言,这种传递方式效率更高。
例:下面程序用于演示结构体变量作函数参数实现传值调用
#include<stdio.h>
struct date
{
int year;
int month;
int day;
};
void Func(struct date p)//结构体变量作函数形参
{
p.year = 2000;
p.month = 5;
p.day = 22;
}
int main(void)
{
struct date d;
d.year = 1999;
d.month = 4;
d.day = 23;
printf("Before function call:%d/%02d/%02d\n",d.year,d.month,d.day);
Func(d);//结构体变量作函数参数,传值调用
printf("After function call:%d/%02d/%02d\n",d.year,d.month,d.day);
return 0;
}
- 向函数传递结构体变量时,实际传递给函数的是该结构体变量成员值的副本,这就意味着结构体变量的成员值是不能在被调函数中被修改的。和其他变量一样,仅当将结构体的地址传递给函数时,结构体变量的成员值才可以在被调函数中被修改。
例:改用结构体指针变量作函数参数,观察和分析程序的运行结果有何变化
#include<stdio.h>
struct date
{
int year;
int month;
int day;
};
void Func(struct date *pt)//结构体指针变量作函数形参
{
pt->year = 2000;
pt->month = 5;
p->day = 22;
}
int main(void)
{
struct date d;
d.year = 1999;
d.month = 4;
d.day = 23;
printf("Before function call:%d/%02d/%02d\n",d.year,d.month,d.day);
Func(&d);//结构体变量的地址作函数参数,传地址调用
printf("After function call:%d/%02d/%02d\n",d.year,d.month,d.day);
return 0;
}
例:结构体除了可作为函数形参的类型以外,还可以作为函数返回值的类型。下面程序用于演示从函数返回结构体变量的值。
#include<stdio.h>
struct date
{
int year;
int month;
int day;
};
struct Func(struct date p)//函数的返回值为结构体类型
{
pt->year = 2000;
pt->month = 5;
p->day = 22;
return p;//从函数返回结构体变量
}
int main(void)
{
struct date d;
d.year = 1999;
d.month = 4;
d.day = 23;
printf("Before function call:%d/%02d/%02d\n",d.year,d.month,d.day);
d = Func(d);//函数返回值为结构体变量的值
printf("After function call:%d/%02d/%02d\n",d.year,d.month,d.day);
return 0;
}
例:用结构体数组作函数参数编程并输出计算学生的平均分
#include<stdio.h>
#define N 30
typedef struct date
{
int year;
int month;
int day;
}DATA;
typedef struct student
{
long studentID;
char studentName[10];
char studentSex;
DATE birthday;
int score[4];
}STUDENT;
void InputScore(STUDENT stu[],int n,int m);
void AverScore(STUDENT stu[],float aver[],int n,int m);
void PrintScore(STUDENT stu[],float aver[],int n,int m);
int main(void)
{
float aver[N];
STUDENT stu[N];
int n;
printf("How many student?");
scanf("%d",&n);
InputScore(stu,n,4);
AverScore(stu,aver,n,4);
PrintScore(stu,aver,n,4);
return 0;
}
//输出n个学生的学号、姓名、性别、出生日期以及m门课程的成绩到结构体数组stu中
void InputScore(STUDENT stu[],int n,int m)
{
int i,j;
for(i = 0;i<n;i++)
{
printf("Input record %d:\n",i+1);
scanf("% ld",&stu[i].studentID);
scanf("%s",stu[i].studentName);
scanf(" %c",&stu[i].studentSex);//%c前有一个空格
scanf("%d",&stu[i].birthday.year);
scanf("%d",&stu[i].birthday.month);
scanf("%d",&stu[i].birthday.day);
for(j = 0;j<m;j++)
{
scanf("%d",&stu[i].score[j]);
}
}
}
//计算n个学生的m门课程的平均分,存入数组aver中
void AverScore(STUDENT stu[],float aver[],int n,int m)
{
int i,j,sum[N];
for(i = 0;i<n;i++)
{
sum[i] = 0;
for(j = 0;j<m;j++)
{
sum[i] = sum[i] + stu[i].score[j];
}
aver[i] = (float)sum[i]/m;
}
}
//输出n个学生的学号、姓名、性别、出生日期以及m门课程的成绩
void PrintScore(STUDENT stu[],float aver[],int n,int m)
{
int i,j;
printf("Result:\n");
float(i = 0;i<n;i++)
{
printf("%10ld%8s%3c%6d/%02d/%02d",
stu[i].studentID,
stu[i].studentName,
stu[i].studentSex,
stu[i].birthday.year,
stu[i].birthday.month,
stu[i].birthday.day);
for(j = 0;j<m;j++)
{
printf("%4d",stu[i].score[j]);
}
printf("%6.1f\n".aver[i]);
}
}
11.6共用体
- 共用体,也称联合(Union),是将不同类型的数据组织在一起共同占用同一段内存的一种构造数据类型。共用体与结构体的类型声明方法类似,只是关键字变为union。
例:下面程序用于演示共用体所占内存字节数的计算方法
#include<stdio.h>
union sample
{
short i;
char ch;
float f;
};//定义共用体类型union sample的模板
typedef union sample SAMPLE;//定义union sample的别名为SAMPLE
int main(void)
{
printf("bytes = %d\n",sizeof(SAMPLE));//打印共用体类型所占的内存字节数
return 0;
}
- 如果将本例程序的共用体union改为结构体struct,那么程序运行结果将是bytes = 8这是为什么呢?这是因为,虽然共用体与结构体都是不同类型的数据组织在一起,但与结构体不同的是,共用体是从同一起始地址开始存放成员的值,即共用体中不同类型的成员共用同一段内存单元,因此必须有足够大的内存空间来存储占据内存空间最多的那个成员,所以共用体类型所占内存空间的大小取决于其成员中占内存空间最多的那个成员变量。
- C语言规定,共用体采用与开始地址对齐的方式分配内存空间。共用体使用覆盖技术来实现内存的共用,即当成员f进行赋值操作时,成员i的内容将被改变,于是i就失去其自身意义,再对ch进行赋值操作时,f的内容又被改变,于是f又失去了其自身的意义。由于同一内存单元在每一瞬时只能存放其中一种类型的成员,也就是说同一时刻只有一个成员是有意义的,因此,在每一瞬时起作用的成员就是最后一次被赋值的成员。正因如此,不能为共同体的所有成员同时进行初始化,C89规定只能对共同体的第一个成员进行初始化,但C99没有这个限制,允许按名设置成员的初值。例如对于前面的例子,可以按下面这样对指定的成员进行初始化。
SAMPLE U = {.CH = 'A'};
//共用体不能进行比较操作,也不能作为函数参数
- 与结构体一样,可使用成员选择运算符或指向运算符来访问共用体的成员变量
SAMPLE num;
num.i = 20;
- 采用共用体存储程序中逻辑相关但情形互斥的变量,使其共享内存空间的好处是除了可以省内内存空间以外,还可以避免因操作失误引起逻辑的上的冲突。例如,在教职工数据库管理中涉及某个人的婚姻状况时,一般有三种可能:未婚、已婚、离婚。任何一个人在同一时间只能处于其中的一种状态。
例:定义工人个人信息结构体类型
struct//定义日期结构体类型
{
int year;//年
int month;//月
int day;//日
};
struct marriedState//定义已婚结构体类型
{
struct date marryDay;//结婚日期
char spouseName[20];//配偶姓名
int child;;//子女数量
};
struct divorceState//定义离婚结构体类型
{
struct date divorceDay;//离婚日期
int child;//子女数量
};
union maritalState//定义婚姻状况共用体类型
{
int single;//未婚
struct marriedState married;//已婚
struct divorceState divorce;//离婚
};
struct person//定义职工个人信息结构体类型
{
char name[20];//姓名
char sex;//性别
int age;//年龄
union maritalState marital;//婚姻状况
int marryFlag;//婚姻状况标记
};
- 共用体的主要应用是有效使用存储空间,才外还可以用于构造混合的数据结构。例如,假设需要的数组元素是int型和float型数据的混合,则可以这样定义共用体。
typedef union
{
int i;
float f;
}NUMBER;
NUMBER array[100];
//此时,每个NUMBER类型的数组array的数组元素都有两个成员,既可以存储int型数据,也可以存储float型数据
11.7枚举数据类型
- 枚举(Enumeration)即“一一举例”之意,当某些量仅由有限个数据组成时,通常用枚举类型来表示。枚举数据类型(Enumerated Data Type)描述的是一组整型值得集合,需要enum来定义。
enum response{no,yes,none};
//声明了名为response的枚举类型,它的可能取值为:no、yes或none。这种定义形式和定义结构体模板很相似。
enum response answer;
//定义了一个reponse枚举型变量answer
-
在枚举类型声明语句中,花括号{和}内的标识符都是整形常量,称为枚举型常量。除非特别指定,一般情况下第1个枚举常量的值为0,依次递增1。使用枚举类型的目的是提高程序的可读性。
-
在上面的枚举类型声明语句中,response被称为枚举标签(Enumeration Tag),当枚举类型和枚举变量放在一起定义时,枚举标签可省略不写。
enum {no,yes,none}answer;
//也可以定义枚举型数组
enum response answer[10];
//C语言还允许在枚举类型定义时明确地指定每一个枚举常量的值
enum response{no = -1,yes = 1,none = 0};
//若要给response添加其他可能取值,在其后的花括号内直接增加新的数值即可。
enum response{no = -1,yes = 1,none = 0,unsure = 2};
//还可以省略定义枚举常量的值
enum month{JAN = 1,FEB,MAR,APR,MAY,JUN,JUL,AUG,SEP,OCT,NOV,DEC};
//这里,第一个枚举常量值被明确地设置为1,以下的常量值依次递增1
- 注意虽然枚举标签后面花括号内的标识符代表枚举型变量的可能取值,但其值是整型常数,不是字符串,因此只能作为整型值而不能作为字符串来使用。
answer = yes;//正确,给answer赋值为枚举常量yes的值(即1)
printf("%d",answer);//正确,输出answer的值为1
printf("%s",answer);//错误,不能作为字符串来使用
11.8动态数据结构——单向链表
11.8.1问题的提出
- 数组实质是一种顺序存储、随机访问的线性表,他的优点是使用直观,便于快速、随机地存取线性表中的任一元素。但缺点是对其进行插入和删除操作时需要移动大量的数组元素,同时由于数组属于静态内存分配,定义数组时必须指定数组的长度,程序一旦运行,其长度就不能再改变,若想改变,只能修改程序,实际使用的数组元素个数不能超过数组元素最大长度的限制,否则就会发生下标越界错误,而低于所设定的最大长度时,又会造成系统资源的浪费,因而空间效率差。
- 方法就是使用动态数据结构。它是利用动态内存分配、使用结构体并配合指针来实现的一种数据结构。
struct temp
{
int data;
struct temp pt;
};
//将含有上述类型定义的程序在Code::Blocks下编译将出现如下错误:
//field'pt'has incomplete type
//这说明,结构体声明时不能包含本结构体类型成员。
//因本结构体类型尚未定义结束,它所占用的内存字节数尚未确定,因此系统无法为这样的结构体成员分配内存
//然而,在声明结构体类型时可以包含指向本结构体类型的指针成员。
//因为,系统为指针变量分配的内存字节数(即存放地址所需的内存字节数)是固定的,不依赖于他所指向的数据类型
struct temp
{
int data;
struct temp *pt;
};
//这是动态数据结构的编程基础
11.8.2链表的定义
- 结构体和指针配合使用可以表示许多复杂的动态数据结构,如链表(Linked Table)、堆栈(Stack)、队列(Queue)、数(Tree)、图(Graph)等。其中,链表又包括单向链表、双向链表和循环链表等。
- 链表实际是链式存储、顺序访问的线性表,与数组不同的是,它是用一组任意的存储单元来存储线性表中的数据,存储单元不一定是连续的,且链表的长度不是固定的。链表数据结构的这一特点使其可以非常方便地实现节点的插入和删除操作。
- 链表的每个元素称为一个节点(Node),每个节点都可存储在内存中的不同位置。为了表示每个元素与后继元素的逻辑关系,以便构成“一个节点链着一个节点”的链式存储结构,除了存储元素本身的信息之外,还要存储其直接后继的信息。
- 因此,每个节点都包含两部分:第一部分称为链表的数据域,用于存储元素本身的数据信息,即用户需要的数据,这里用data表示,它不局限于一个成员数据,也可是多个成员数据;第2部分是一个结构体指针,称为链表的指针域,用于存储其直接后继的节点信息,这里用next表示,next的值实际上就是下一个节点的地址,即next指向下一节点,当前节点为末节点时,next的值设为空指针(NULL),表示链表结束。
- 为表示链表结构,必须在结构体中定义一个指针类型的成员变量,用它来存储下一个节点的地址,并且该指针变量必须具有与结构体相同的数据类型。
//假设单向链表的每个节点只有一个int型数据,则其数据结构定义如下:
struct link
{
int data;//数据域:存储节点数据信息
struct link *next;//指针域:存储直接后继节点的地址
}
- 链表还必须有一个指向链表的其实节点的头指针变量head。含有一个指针域、由n个节点链接形成的链表,就称为线性链表或者单向链表。
- 存储结构决定了对链表数据的特殊访问方式,即链表只能顺序访问,不能进行随机访问。
- 判断链表是否已经到尾部节点?首先要找到链表的头指针,因为它是指向第1个节点的指针,只有找到第1个节点才能通过他的指针域找到第2个节点,然后由第2个节点的指针域再找到第3个节点,依此类推,当节点的指针域为NULL时,表示已经搜索到了链表的尾部节点。
- 对单向链表而言,头指针是极其重要的,头指针一旦丢失,链表中的数据也将全部丢失。这种存储方式的最大缺点是容易出现断链,一旦链表中某个节点的指针域数据丢失,那么也就意味着将无法找到下一个节点,该节点后面的所有节点数据都将丢失。
11.8.3单向链表的建立
- 我们可以采取链表中添加节点的方式来建立一个单向链表。为了向链表中添加一个新的节点,首先我们要为新节点动态申请内存,让指针变量p指向这个新建节点,然后将新建节点添加到链表中,此时需要考虑以下两种情况:
(1)若原链表为空表,则将新建节点置为头节点
(2)若原链表为空表,则将新建节点添加到尾表
//根据上述思想编写向链表添加节点数据的程序如下:
#include<stdio.h>
#include<stdlib.h>
struct link *AppendNode(struct link *head);
void DisplyNode(struct link *head);
void DeleteMemory(struct link *head);
struct link
{
int data;
struct link *next;
};
int main(void)
{
int i = 0;
char c;
struct link *head = NULL;//链表头指针
printf("Do you want to append a new node(Y/N)?");
scanf(" %c",&c);//%c前有空格
while(c == 'Y' || c =='y');
{
head = AppendNode(head);//向head为头指针的链表末尾添加节点
DisplyNode(head);//显示当前链表中的各节点信息
printf("Do you want to append a new node(Y/N)?");
scanf(" %c".&c);//%c前面有一个空格
i++;
}
printf("%d new nodes have been apended!\n",i);
DeleteMemory(head);//释放所有动态分配的内存
}
//函数功能:新建一个节点并添加到链尾末尾,返回添加节点后的链表的头指针
struct link *AppendNode(struct link *head)
{
struct link *p = NULL,*pr = head;
int data;
p = (struct link *)malloc(siczeof(struct link));//让p指向新建节点
if(p = NULL)//若新建节点申请内存失败,则退出程序
{
printf("No enough memory to allocatte!\n");
exit(0);
}
if(head == NULL)//若原链表为空表
{
head = p;//将新建节点置为头节点
}
else//若原链表为非空,则将新建节点添加到表尾
{
while(pr->next != NULL)//若未到表尾,则移动pr直到pr指向表尾
}
printf("Input node data:");
scanf("%d",&data);//输入节点数据
p->data = data;//将新建节点的数据域赋值为输入的节点数据值
p->next = NULL;//将新建节点置为表尾
return head;//返回添加节点后的链表的头指针
}
//函数的功能:显示链表中所有节点的节点号和该节点中的数据项内容
void DisplyNode(struct link *head)
{
struct link *p = head;
int j = 1;
while(p != NULL)//若不是表尾,则循环打印节点的值
{
printf("%5d%10d\n",j,p->data);//打印第j个节点的数据
p = p->next;//让p指向下一个节点
j++;
}
}
//函数功能:释放head指向的链表中所有节点占用的内存
void DeleteMemory(struct link *head)
{
struct link *p = head,*pr = NULL;
while(p != NULL)//若不是表尾,则释放节点中占用的内存
{
pr = p;//在pr中保存当前节点的指针
p = p->next;//让p指向下一个节点
free(pr);//释放pr指向的当前节点占用的内存
}
}
11.8.4单向链表的删除操作
- 链表的删除操作就是将一个待删除节点从链表中断开,不再与链表的其他节点有任何联系。为了在已有的链表中删除一个节点,需要考虑如下四种情况:
- 若原链表为空表,则无需删除节点,直接退出程序。
- 若找到的待删除节点p是头节点,则将head指向当前节点的下一个节点(head = p->next),即可删除当前节点。
- 若找到的待删除节点不是头节点,则将前一节的指针域指向当前节点的下一个节点(pr ->next = p->next),即可删除当前节点,当待删除节点是末节点时,由于p->next值为NULL,因此执行pr->next = p->next后,pr->next的值也变为了NULL,从而使pr所指向的节点由倒数第2个节点变成了末节点。
- 若已搜做到表尾(p->next ==NULL),仍未找到待删除节点,则显示“未找到”。
- 注意:节点被删除后,只表示将它从链表中断开而已,它仍占用着内存,必须释放其所占的内存,否则将出现内存泄漏。
//从链表中删除一个节点的操作如下:
//函数功能:从head指向的链表中删除一个节点,返回删除节点后的链表的头指针
struct link *DeleteNode(struct link *head,int nodeData)
{
struct link *p = head,*pr = head;
if(head == NULL)//若链表为空表,则退出程序
{
printf("Linked Table is empty!\n");
return(head);
}
while(noteData != p->data && p->next != NULL)
{//未找到且未到表尾
pr = p;//在pr中保存当前节点的指针
p = p->next;//p指向当前节点的下一节点
}
if(nodeData == p->data)//若当前节点的节点值为nodeData,找到待删除节点
{
if(p == head)//若待删节点为头节点
{
head = p->next;//让头指针待删除节点p的下一节点
}
else
{
pr->next = p->next;//让前一节点的指针域指向待删除节点的下一节点
}
free(p);//释放为已删除节点分配的内存
}
else
{
printf("This Node has not been found!\n");
}
return head;//返回删除节点后的链表头指针head的值
}
11.8.5单向链表的插入操作
- 单链表中插入一个新节点时,首先要新建一个节点,将其指针域赋值为空指针(p->next = NULL),然后在链表中寻找适当的位置执行节点插入操作,此时需要考虑一下四种情况:
- 若原链表为空表,则将新节点p作为头节点,让head指向新节点p(head = p)。
- 若原链表为非空,则按节点值的大小(假设节点值已按升序排序)确定插入新节点的位置。若在头节点前插入新节点,则将新节点的指针域指向原链表的头节点(p->next = head),且让head指向新节点(head = p)。
- 若在链表中间插入新节点,则将新节点的指针域指向下一节点(p->next = pr->next),且让前一节点的指针域指向新节点(pr->next = p)。
- 若在表尾插入新节点,则末节点指针域指向新节点(pr->next = p)。
例:向节点值已按升序排序的链表中插入一个新节点的程序代码:
//函数功能:在已按升序排序的链表中插入一个节点,返回插入节点后的链表头指针
struct link *InserNode(struct link *head,int nodeData)
{
struct link *pr = head,*p = head,*temp = NULL;
p = (struct link *)malloc(sizeof(struct link));//让p指向待插入指针
if(p = NULL)
{
printf("No enough memory!\n");
exit(0);
}
p->next = NULL;//为待插入节点的指针域赋值为空指针
p->data = nodeData;//为待插入节点数据域赋值为nodeData
if(head == NULL)//若原链表为空表
{
head = p;//待插入节点作为头节点
}
else
{
while(pr->data<nodeData && pr->next != NULL)
{
temp = pr;//在temp中保存当前节点的指针
pr = pr->next;//pr指向当前节点的下一节点
}
if(pr->data >= nodeData)
{
if(pr == head)//若在头节点前插入新节点
{
p->next = head;//将新节点的指针域指向原链表的头节点
head = p;//让head指向新节点
}
else//若在链表中间插入新节点
{
pr = temp;
p->next = pr->next;//将新节点的指针域指向新节点
}
}
else//若在表尾插入新节点
{
pr->next = p;//若末节点的指针域指向新节点
}
return head;//返回插入节点后的链表头指针head的值
}
}
11.9扩充内容
11.9.1栈和队列
- 数组是一种连续存储、随机访问的线性表,链表属于分散存储、连续访问的线性表。它们的每个数据都有相对位置,有至多一个直接前驱和至多一个直接后继。栈(Stack)和队列(Queue)也属于线性表,但它们都是运算受限的线性表,也称限定性线性表。栈限定数据只能在栈顶指向插入(入栈)和删除(出栈)操作。队列限定只能在对头执行删除操作(出队),在队尾执行插入操作(入队)。
-
栈
- 对栈进行运算的一端称为栈顶(Top),栈顶的第一个元素称为栈顶元素。向一个栈中插入新元素,即把该元素放到栈顶元素的上面,使其成为新的栈顶元素,称为压入堆栈(Push)。从一个栈中删除元素,使原栈顶元素下方的相邻元素成为新的栈顶元素,称为弹出堆栈(Pop),栈的这种运算方式使其具有后进先出(Last Input First Output,LIFO)的特性。
- 栈的一个典型应用是表达式求值。表达和式求值是程序设计语言编译中的一个最基本的问题。以二元算数运算符为例,算数表达式的一般形式为s1+op+s2,则op+s1+s2为前缀表示法(也称为波兰表达式),s1+op+s2为中缀表示法,s1+s2+op为后缀表示法(也称为逆波兰表达式)。例如,对于表达式a*b+(c-d/e) f,则其前缀表达式为+ * ab-c/def,中缀表达式为ab+(c-d/e) * f,后缀表达式为ab * cde/-f *+。
- 用栈计算逆波兰表达式的基本思路是:按顺序遍历整个表达式u,若遇到操作数(假设都是二元运算符),则入栈,若遇到操作符,则连续弹出两个操作数并执行相应的运算,然后将其运算结果入栈。重复以上过程,直到遍历完,栈内只剩下一个操作数时,那就是最终的运算结果,弹出打印即可。
例:用栈的顺序存储结构实现的逆波兰表达式求值程序如下: #include<stdio.h> #include<string.h> #include<ctype.h> #include<stdlib.h> #define INT 1 #define FIT 2 #define N 20 typedef struct node { int ival; }NodeType; typedef struct stack { NodeType data[N]; int top;//控制栈顶 }STACK;//栈的顺序存储 void Push(STACK *stack,NodeType data); NodeType Pop(STACK *stack); NodeType OpInt(int d1,int d2,int op); NodeType OpData(NodeType *d1,NodeType *d2,int op); int main(void) { char word[N]; NodeType d1,d2,d3; STACK stack; stack.top = 0;//初始化栈顶 //以空格为分隔符输入逆波兰表达式,以#结束 while(scanf("%s",word) == 1 && word[0] != '#') { if(isdight(word[0]))//若为数字,则转换为整型后压栈 { d1.ival = atoi(word);//将word转换为整型数据 Push(&stack,d1); } else//否则弹出两个操作数,执行相应运算后再将结果压栈 { d2 = Pop(&stack); d1 = Pop(&stack); d3 = OpData(&d1,&d2,word[0]);//执行运算 Push(&stack,d3);//运算结果压入堆栈 } } d1 = Pop(&stack);//弹出栈顶保存的最终计算结果 printf("%d\n",d1.ival); return 0; } //函数功能:将数据data压入堆栈 void Push(STACK *stack,NodeType data) { memcpy(&stack->data[stack->top],&data,sizeof(NodeType)); stack->top = stack->top + 1;//改变栈顶指针 } //函数功能:弹出栈顶数据并返回 NodeType Pop(STACK *stack) { stack->top = stack->top-1;//改变栈顶指针 return stack->data[stack->top]; } //函数功能:对整型的数据d1和d2执行运算op,并返回计算结果 NodeType OpInt(int d1,int d2,int op) { NodeType res; switch(op) { case '+': res.ival = d1 + d2; break; case '-': res.ival = d1 - d2; break; case '*': res.ival = d1 * d2; break; case '/': res.ival = d1 / d2; break; } return res; } //函数功能:对d1和d2执行运算op,并返回计算结果 NodeType OpData(NodeType *d1,NodeType *d2,int op) { NodeType res; res = OpInt(d1->ival,d2->ival,op); return res; }
例:用栈的链式存储结构实现的逆波兰表达式求值程序如下: #include<stdio.h> #include<string.h> #include<stype.h> #include<stdlib.h> #define INT 1 #define FLT 2 #define N 20 typedef struct node { int ival; }NodeType; typedef struct stack { NodeType data; struct stack *next;//指向栈顶 }STACK;//栈的链式存储 STACK *Push(STACK *top,NodeType data); STACK *Pop(STACK *top); NodeType OpInt(int d1,int d2,int op); NodeType OpData(NodeType *d1,NodeType *d2,int op); int main(void) { char word[N]; NodeType d1,d2,d3; STACK *top = NULL;//初始化栈顶 //以空格为分隔符输入逆波兰表达式,以#结束 while(scanf("%s",word) == 1 && word[0] != '#') { if(isdight(word[0]))//若为数字,则转换为整型后压栈 { d1.ival = atoi(word);//将word转换为整型数据 top = Push(top,d1); } else { d2 = top->data; top = Pop(top); d1 = top->data; top = Pop(top); d3 = OpData(&d1,&d2,word[0]);//执行运算 top = Push(top,d3);//运算结果压入堆栈 } d1 = top->data; printf("%d\n",d1.ival); top = Pop(top);//弹出栈顶保存的最终计算结果 return 0; } } //函数功能:将函数data压入堆栈 STACK *Push(STACK *top,NodeType data) { STACK *p; p = (STACK *)malloc(sizeof(STACK));//创建一个新节点,准备入栈 p->data = data; p->next = top;//新创建的节点指向原栈顶 top = p;//让栈顶指针指向新创建的节点 return top; } //函数功能:弹出栈顶数据并返回 STACK *Pop(STACK *top) { STACK *p; if(top == NULL) { return NULL; } else { p = top; top = top->next; free(p);//注意弹出栈顶数据后要释放其所占用的内存 } return top; } //函数功能:对整型的数据d1和d2执行运算op,并返回计算结果 NodeType OpInt(int d1,int d2,int op) { NodeType res; switch(op) { case '+': res.ival = d1 + d2; break; case '-': res.ival = d1 - d2; break; case '*': res.ival = d1 * d2; break; case '/': res.ival = d1 / d2; break; } return res; } //函数功能:对d1和d2执行运算op,并返回计算结果 NodeType OpData(NodeType *d1,NodeType *d2,int op) { NodeType res; res = OpInt(d1->ival,d2->ival,op); return res; }
-
队列
- 向队尾(rear)插入新元素,称为进队或入队,新元素进队后将成为新的队尾元素,从对首(front)删除元素,称为离队或出队。其后继元素将成为新的队首元素。
- 因队列的插入和删除操作分别在各自的一端进行,每个元素必然按照进入的次序出队,所以队列具有先进先出(First Input First Output,FIFO)的特性,这一点与栈刚好相反。
- 为了便于管理队列中的元素,通常设置两个指针front和rear。注意,rear并非指向队尾元素,而是指向队尾元素的后一个位置。这样做的目的是为了区分空的队列和只有一个元素的队列,当且仅当front与rear相等时,表示队列为空。当rear等于队列的最大容量QMAX时,表示队列已满。
例:队列的顺序存储结构可以定义为如下的结构体类型: //队列的顺序存储 typedef struct queue { int data[N]; int front;//控制对头 int rear;//控制队尾 }QUEUE; 例:队列的链式存储结构可以用如下的结构体类型来定义: //队列的链式存储 typedef struct queue { int data; struct queue *front;//指向对头 struct queue *rear;//指向队尾 }QUERE;
- 队列操作需要注意的问题是有时会出现“假满”的极端情形,即当front和rear都指向QMAX时,此时既不能插入,也不能删除,因为若要插入,会被告知队列已满,若要删除,会被告知队列为空。一种较好的解决办法是采用循环队列。因为循环队列的插入和删除操作是在一个模拟成环行的存储空间“兜圈子”,所以不会产生“假满”问题。
- 当有数据入队时,队尾指针rear变为(rear+1)%QMAX,当有数据出队时,队头指针front变为i(front +1)%QMAX。front == rear仍是队列为空的标志,队列已满的标志是(rear + 1)%QMAX == front,队列之所以保留了一个空的单元,主要目的是避免与队空标志相冲突,因此具有QMAX个元素的循环队列最多只能存放QMAX - 1个元素。
例:假设在大学生的周末舞会上,男、女学生各自排成一队。舞会开始时,依次从男队和女队的对头各出一人配成舞伴。如果两队初始人数不等,则较长的那一队中未配对者等待下一轮舞曲。要求男、女学生人数及其姓名以及舞会的轮数,由用户从键盘输入,屏幕输出每一轮舞伴的配对名单,如果在该轮中有未配对的,则要求能够从屏幕显示下一轮第一个出场的未配对者的姓名。 //用顺序存储的循环队列实现的程序如下: #include<stdio.h> #include<stdlib.h> #include<string.h> #define N 100 typedef struct queue { char elem[N][N]; int qSize;//队列长度 int front;//控制对头 int rear;//控制队尾 }QUEUE; void CreatQueue(QUEUE *Q); int QueueEmpty(const QUEUE *Q); void DeQueue(QUEUE *Q,char *str); void GetQueue(const QUEUE *Q,char *str); void DancePartners(QUEUE *man,QUEUE *women); void Match(QUEUE *shortQ,QUEUE *longQ); int main(void) { QUEUE man,women; printf("男队:\n"); CreatQueue(&man); printf("女队:\n"); CreatQueue(&women); DancePartners(&man,&women); return 0; } //函数功能:创建一个队列 void CreatQueue(QUEUE *Q) { int n,i; Q->front = Q->rear = 0; printf("请输入跳舞人数:"); scanf("%d",&n); Q->qSize = n + 1; printf("请输入跳舞者人名:"); for(i = 0;i<n;i++) { scanf("%s",Q->elem[i]); } Q->rear = n; } //函数功能:判断循环队列是否为空 int QueueEmpty(const QUEUE *Q) { if(Q->front == Q->rear)//循环队列为空 return 1; else return 0; } //函数功能:循环队列出队,即删除队首元素 void DeQueue(QUEUE *Q,char *str) { strcpy(str,Q->elem[Q->front]); } //函数功能:根据队列长短确定如何调用舞伴配对函数 void DancePartners(QUEUE *man,QUEUE *women) { if(man->qSize < women->qSize)//若男队短 { Match(man,women); } else { Match(women,man); } } //函数功能:舞伴配对 void Match(QUEUE *shortQ,QUEUE *longQ) { int n; char str1[N],str2[N]; printf("%d",&n); while(n--)//循环n轮次 { while(!QueueEmpty(shortQ))//短队列不为空 { if(QueueEmpty(longQ)) { longQ->front = (longQ->front+1)%longQ->qSize; } DeQueue(shortQ,str1); DeQueue(longQ,str2); printf("配对的舞者:%s%s\n",str1,str2); } shortQ->front = (shortQ->front + 1)%shortQ->qSize; if(QueueEmpty(longQ)) { longQ->front = (longQ->front + 1)%longQ->qSize; } GetQueue(longQ,str1); printf("第一个出场的未配对者的姓名:%s\n",str1); } }
11.9.2树和图
- 每个数据元素只有一个直接前驱和一个直接后继,属于线性数据结构。
- 如果结构中数据元素之间存在一对或多对多的关系,称为非线性数据结构。树(Tree)和图(Graph)都是典型的非线性数据结构,在计算机领域中有着广泛的应用。
- 树的数据元素之间有明显的层次关系,且每一层上的数据元素可能和下一层中多个元素(即其孩子节点)相关,但只能和上一层中一个元素(即其父节点)相关。而图的节点之间的关系可以是任意的,图中任意两个元素之间都可能相关。
-
树是n(n>=0)个元素的有限集。树中的每个元素称为节点(node)。不含有任何节点的树(即年= 0时)称为空树。在一棵非空树中,它有且仅有一个称作根(root)的节点,其余的节点可分为m颗(m>=0)互不相交的子树(即称作根的子书树),每棵子树(Sub Tree)同样又是一棵树。显然,树的定义是递归的。
一棵反映父子关系的家族树,若兄弟节点之间是按照大小有序排列的,则称为有序树。
二叉树(Binary Tree)是一类非常重要的树形结构。它可以递归定义如下:
- 二叉树是n(n>=0)个节点的有限集合。该集合或者为空集(空二叉树),或者由一个根节点和两棵互不相交的、分别称为根节点的左子树和右子树的二叉树组成。
- 若用链表结构来表现,则可以定义如下的结构体类型:
typedef struct BiTNode { int data; struct BinTNode *lchild;//左子树 struct BinTNode *rchild;//右子树 }BI_TREE;
- 除了数据成员data外,每个节点还包括两个指针域lchild和rchild,分别指向该节点的左子树和右子树。
- 如果一棵二叉树中的所有分支节点都存在左子树和右子树,且所有叶子节点都在同一层上,则称为满二叉树。
- 对一棵具有n个节点的二叉树按层序编号,如果编号为i(1<=i<=n)的节点与同样深度的满二叉树中编号为i的节点在二叉树中位置完全相同,则这棵二叉树称为完全二叉树。也就是说,除最后一层外,其他的每一层的节点数都是满的。如果最后一层不满,缺少的节点全部集中在右边,这样的二叉树就是完全二叉树。满二叉树一定是完全二叉树,但完全二叉树不一定是满二叉树。
- 如果每颗子树的头节点的值都比各自左子树上的所有节点的值要大,比各自右子树上的所有节点值要小,则称为搜索二叉树。
- 遍历是树的一个基本操作,遍历的目的是把节点按照一定的过则排成线性序列,以便按此顺序访问节点。先序遍历、中序遍历和后续遍历是3种最重要的遍历树的方式。这里的“先、中、后”是指根节点在节点访问中的次序。
- 此外,还有一种称为层序遍历的遍历方法,先从根节点s出发,依照层次结构,逐层访问其他节点,仅在访问完距离顶点s为k的所有节点后,才会继续访问距离为k+1的其他节点。
例:以满二叉树为例,输出其先序遍历、中序遍历和后序遍历的程序结果如下: #include<stdio.h> #include<stdlib.h> #define N 20 //二叉树结点信息 typedef struct BiNTode { int data; struct BiNTode *lchild;//左子树 struct BiNTode *rchild;//右子树 }BI_TREE; BI_TREE *CreatTree(int *a,int n); int PreOrderTraverse(BI_TREE *root,void (*visit)(int)); int MidOrderTraverse(BI_TREE *root,void(*visit)(int)); int PostOrderTraverse(BI_TREE *root,void(*visit)(int)); void PrintNode(int node); int main(void) { int a[N] = {1,2,3,4,5,6,7}; int n = 7; BI_TREE *root = NULL; root = CreatTree(a,n); PreOrderTraverse(root,PrintNode); printf("\n"); MidOrderTraverse(root,PrintNode); printf("\n"); PostOrderTraverse(root,PrintNode); printf("\n"); return 0; } //函数功能:二叉树创建 BI_TREE *CreatTree(int *a,int n) { int i; BI_TREE *pNode[N] = {0}; for(i = 0;i<n;++i) { pNode[i] = (BI_TREE *)malloc(sizeof(BI_TREE)); if(pNode[i] == NULL) { printf("No enough memory to allocate!\n"); exit(1); } pNode[i]->lchild = NULL; pNode[i]->rchild = NULL; pNpde[i]->data = a[i]; } for(i = 0;i<n/2;++i) { pNode[i]->lchild = pNode[2 * (i+1)-1]; pNode[i]->rchild = pNode[2 * (i+1)+1-1]; } return pNode[0]; } //函数功能:二叉树先序遍历 int PreOrderTraverse(BI_TREE *root,void (*visit)(int)) { if(root == BULL) { return 1; } (*visit)(root->data); if(PreOrderTraverse(root->lchild,visit)) { if(PreOrderTraverse(root->rchild,visit)) { return 1; } } return 0; } //函数功能:二叉树中序遍历 int MidOrderTraverse(BI_TREE *root,void (*visit)(int)) { if(root == NULL) { return 1; } if(MidOrderTraverse(root->lchild,visit)) { (*visit)(root->data); if(MidOrderTraverse(root->rchild,visit)) { return 1; } } return 0; } //函数功能:二叉树后序遍历 int PostOrderTraverse(BI_TREE *root,void (*visit)(int)) { if(root == NULL) { return 1; } if(PostOrderTraverse(root->lchild,visit)) { if(PostOrderTraverse(root->rchild,visit)) { (*visit)(root->data); return 1; } } return 0; } //函数功能:打印节点信息 void PrintNode(int node) { printf("%3d",node); }
-
图
- 一个图G 是由顶点(vertex)的集合V和边(edge)的集合E组成的,可以表示为一个二元组:G=(V,E)。若图G中的每条边都是有方向的,则称G为有向图(Digraph)。在一个有向图中,由一个顶点出发的边的总数,称为出度,指向一个顶点的边的总数称为入读。若图G中的每条边都是没有方向的,则称G为无向图(Undigraph)。边上带有权值的图叫做带权图。在不同的实际问题中,权值可以代表距离、时间、价格等不同的属性。如果两个顶点之间有边连接,则称为两个定点相邻。相邻顶点的序列称为路径。起点和终点重合的路径叫做圈。没有圈的有向图,称为有向无环图(Directed Acyclic Graph,DAG)。没有圈的连接图称为树。
- 图的存储主要有两种方式:邻接矩阵和邻接表。邻接矩阵使用二维数组来表示图,数组元素A[i] [j]表示的是顶点i和顶点j之间的关系。在无向图中,只要顶点i和j之间有边相连,则A[i] [j]和A[j] [i]都设为1,否则设为0。
- 在有向图中,只要顶点i有一条指向j的边,则A[i] [j]设为1(注意A[i] [j]并不设为1),否则就设为0.在带权图中,A[i] [j]表示顶点i到顶点j的边的权值。因在边不存在的情况下,若将A[i] [j]设为0,则无法与权值为0的情况进行区分,因此选取适当的较大的常数inf,令A[i] [j] = inf。
- 使用邻接矩阵的好处是可以在常数时间内判断两点之间是否有边存在,但是当顶点数较大时的空间复杂度较高。
- 图的邻接表存储方法是对每个顶点建立一个边表(单向链表),在这个单向链表中存储与其各个相邻的顶点的信息。相对于邻接链表而言,邻接表存储方式的空间复杂度较低。
- 图的遍历就是从图的某个顶点出发,按照某种方式访问图中的所有顶点,且每个顶点仅被访问一次,这个过程称为图的遍历。为了保证图中的顶点在遍历过程中仅访问一次,要为每一个顶点设置一个访问标志。通常有两种方法:深度优先搜索(Depth-First Search,DFS)和广度优先搜索(Breadth-First Search,BFS)。这两种算法对有向图与无向图均适用。树中的先序遍历就是基于深度优先搜索的思想,而层序遍历则是基于广度优先搜索的思想。
- 深度优先搜索的基本步骤如下:
(1)从图中某个顶点v0出发,先访问v0.
(2)访问顶点v0的第一个邻接点,以这个邻接点v1作为一个新的顶点,访问v1所有的邻接点,直至以v1出发的所有顶点均被访问,然后回溯到v0的下一个未被访问过的邻接点,以这个邻接点作为新顶点,重复上述步骤,直到图中所有与v0相连的所有顶点均被访问。
(3)若图中仍有未被访问的顶点,则另选图中的一个未被访问的顶点作为起始点。重复上述过程,直到图中的所有顶点均被访问。
- 广度优先搜索类似于树的层序遍历,起搜索步骤如下:
(1)从图中某个顶点v0出发,先访问v0
(2)依次访问v0的各个未被访问的邻接点
(3)依次从上述邻接点出发,访问他们的各个未被访问的邻接点。需要注意的是:如果vi在vk之前被访问,则vi的邻接点应在vk的邻接点之前被访问。重述上述步骤,直到所有顶点均被访问。
(4)如果图中仍有未被访问的顶点,则随机选择一个作为起始点,重复上述过程,直到图中的所有顶点均被访问。
11.9.3数据的逻辑结构和存储结构
- 数据的逻辑结构就是对数据组织方式,主要有如下4种:
- 集合。集合是一批数据的聚集,集合中的每个数据都是各自独立和平等的,不分先后次序。
- 线性表。线性表是一批具有一对一关系的数据集,线性表中的每个数据都有其相对位置,有至多一个直接前驱和至多一个直接后继。栈和队列都属于线性表。
- 数。树是一批具有一对多关系的数据集,树中的每个数据也都有其相对位置,至多有一个直接前驱,但可以有任意多个(包括0个)直接后继。树中的每个数据向上(树根方向)只有一条路径,向下(树叶方向)可以有多条路径。如果每个数据至多有两个直接后继,并且左、右有序,则称为二叉树。
- 图。图是一批具有多对多关系的数据集,图中的每个数据都允许有任意多个直接前驱和任意多个直接后继,每对数据顶点之间可能有多条路径相连。
- 数据的存储结构是对数据的存储方式,主要有4种:
- 顺序存储。顺序存储要求对每个数据按序编号,并按编号的顺序依次存储到一块连续的存储空间中,对数据进行存取访问时,同样也是通过编号计算出其对应的存储位置。
- 链式存储。链式存储是通过在数据节点中设置地址指针来实现的,一个指针用来保存下一个数据的存储位置,即指向下一个数据。链式存储中的一个数据节点被访问后,利用其节点中保存的指针即可访问下一个数据节点。
- 索引存储。索引存储主要用于集合数,根据数据的某个属性,按照不同属性值划分为若干个子集,同一属性值属于同一个子集,每个子集被存储在一起。访问索引存储中的数据时,先按给定的属性值查找到对应的子集,然后再从该子集对应的存储空间中查到所需数据。可以通过多层次划分实现多级索引存储空间。
- 散列存储。散列存储也主要适应于集合数据,首先利用数据的关键字构造散列函数,然后把计算机得到的散列函数值视为存储该数据的存储空间的散列地址,最后把该数据存储到这个位置。对散列存储的数据进行访问时,先根据关键字和散列函数求出散列地址,然后从该地址中取出数据即可。
- 数据的逻辑结构是根据实现现实世界中数据之间固有的联系抽象出来的,或是根据人们组织数据的需求定义的,因此数据的逻辑结构是独立存在的,与存储结构无关。但数据的存储结构与其对应的逻辑结构息息相关,需要如实地反应其逻辑结构,不能因存储结构的改变而改变其原有的逻辑结构。对于集合结构的数据,可采用任何一种存储结构存储,对于线性表、树、图等有序数据结构,主要采用顺序或链式存储的方式。
- 定长数组、动态数组和动态数据结构各有其优缺点
定长数组 | 动态数组 | 动态数据结构 | |
---|---|---|---|
优点 | 连续存储,随机访问;使用直观方便;数据快速定位 | 连续存储,随机访问,空间不浪费,可像数组一样使用 | 节省空间,无浪费数据快速插入和删除 |
适用于存储长度固定的数据结合 | 适用于长度在程序运行时才能确切知道的集合 | 适用于长度在程序运行过程中动态变化的集合 | |
缺点 | 属于静态内存分配,程序一旦运行长度不能改变,若想改变,只能修改程序,按最大需求定义数组,浪费空间,还容易发生下标越界 | 程序员自己负责内存的分配和释放;频繁申请/释放,速度慢;运行时间稍长后,还会造成内存空间碎片化 | 分散存储顺序访问,不可随机访问;操作较为复杂 |
第12章 文件操作
12.1二进制文件和文本文件
- 与计算机内存存储数据不同的是,文件操作使用硬盘或U盘等永久性的外部存储设备来存储数据,这样保存的数据在程序结束时不会丢失。程序员不必关心这些复杂的存储设备是如何存取数据的,因为操作系统已经把这些复杂的存取方法抽象为了文件(File)。
- 文件是由文件名来识别的,因此只要指明文件名,就可读出或写入数据。只要文件不同名,就不会发生冲突。
- C语言文件有两种:文本文件(也称ASCII码文件)和二进制文件。其差别在于存数值型数据的方式不同。在二进制文件中,数值型数据是以二进制形式存储的;而在文本文件中,则是将数值型数据的每一位数字作为一个字符以其ASCII码的形式存储。
- 因此,文本文件中的每一为数字都单独占用一个字节的存储空间。而二进制文件则是把整个数字作为一个二进制数来存储的并非数值的每一位数字都占用单独的存储空间。
- 假设有如下变量定义语句:
short int n = 123;
- 在二进制文件中,变量n仅占2个字节的存储空间。而把变量n的值存储在文本文件中则需要3个字节的存储空间。
- 在文本文件中,变量会以字符型的格式存储,123会被拆分为’1’、‘2’、‘3’。
- 二进制文件和文本文件各有优缺点。文本文件可以很方便地被其他程序读取,包括文本编辑器、Office办公软件等,且其输出与字符一一对应,一个字节表示一个字符,便于对字符进行逐个处理,便于输出字符,但一般占用的存储空间较大,且需花费ASCII码与字符间的转换时间。以二进制文件输出数值,可节省外存空间和转换时间,但一个字节并不对应一个字符,不能直接输出其对应的字符型式。
- 无论一个C语言文件的内容是什么,它一律吧数据看成是由字节构成的序列,即字节流。对文件的存取也是以字节为单位的,输入/输出的数据流仅受程序控制而不受物理符号(如回车换行符)的控制。所以,C语言文件又称流式文件。
- 文件的写入和读出必须匹配,两者约定为同一种文件格式,并规定好文件的每个字节是什么类型和什么数据。很多文件都有公开的标准格式(如bmp、jpg和mp3等),并且通常还规定了相应的文件头的格式,想要正确读出文件中的数据,必须先了解头文件的格式和内容,只有正确读出文件头的内容才能正确读出文件头后面存储的内容。很多应用软件也都支持这些类型的文件的读和写。
- C语言有缓冲型和非缓冲型两种文件系统。缓冲型文件系统是指系统自动在内存中为每一个正在使用的文件开辟一个缓冲区,作为程序与文件之间数据交换的中间媒介。也就是在读写文件时,数据先送到缓冲区,再传给C语言程序或外存上。
- 缓冲文件系统利用文件指针标识文件。而非缓冲文件系统没有文件指针,它使用称为文件号的整数来标识文件。缓冲型文件系统中的文件操作,也称为高级文件操作,高级文件操作函数是ANSI C定义的可移植性的文件操作函数,具有跨平台和可移植的能力,可解决大多数文件操作问题。
12.2文件的打开和关闭
- 在使用文件前必须打开文件。函数fopen()用来打开文件,其函数原型如下:
FILE *fopen(const char *filename,const char *mode);
- fopen()的返回值是一个文件指针(File Pointer),FILE是在stdio.h中定义的结构体类型,封装了与文件有关的信息,如文件句柄、位置指针及缓冲区等。
- 缓冲文件系统为每个被使用的文件在内存中开辟一个缓冲区,用来存放文件的有关信息。fopen()有两个形参,第1个形参filename表示文件名,可包含路径和文件名两部分,第2个形参mode表示文件打开方式。
字符 | 含义 |
---|---|
“r” | 以只读方式,打开文本文件;以"r"方式打开的文件,只能读出,而不能向该文件写入数据。该文件必须是已经存在的,若文件不存在,则会出错 |
“w” | 以只写方式,创建并打开文本文件,已存在的文件将被覆盖;以"w"方式打开文件时,无论文件是否存在,都需要创建一个新的文本文件,只能写入数据 |
“a” | 以只写方式,打开文本文件,位置指针移到文件末尾,向文件末尾添加数据,原文件数据保留。若文件不存在,则会新建一个文件 |
“+” | 与上面的字符串组合,表示以读写方式打开文本文件。即可向文件中书写数据,也可以从文件中读出数据 |
“b” | 与上面的字符串组合,表示打开二进制文件 |
- 例如,若要以读写方式打开D盘根目录下的文本文件demo.txt,保留原文件所有内容,向其文件尾部添加数据,则用如下语句:
fp = fopen("D:\\demo.txt","a+");
//注意,下面这样书写有误
fp = fopen("D:\demo.txt","a+");//文件的路径表示有误
- 若要以读写方式打开D盘根目录下的二进制文件demo.bin,保留原文件所有内容,向其文件尾部添加数据,则用如下语句:
fp = fopen("D:\\demo.bin","ab+");
//其中,文件指针fp是指向FILE结构类型的指针变量,定义如下:
FILE *fp;
- 因为操作系统对于同时打开的文件数据是有限的,所以在文件使用结束后必须关闭文件,否则会出现意想不到的错误。在C语言中,函数fclose()用来关闭一个由fopen()打开的文件,其函数原型如下:
int fclose(FILE *fp);
- 函数fclose()返回一个整型数。当文件关闭成功时,返回0值,否则返回一个非0值。因此,可根据函数的返回值判断文件是否关闭成功。
- 不建议以读写方式代开文件,因为读写其实共用一个缓冲区,每次读写都会改变文件位置指针,很容易写乱,破坏原来文件的内容,并且需要调用文件定位函数才能在读写之间转换。
12.3按字符读写文件
- ANSI C提供了丰富的文件读写函数。包括按字符读写、按数据块读写、按格式读写等。
-
读写文件中的字符
- 函数fgetc()用于从一个以只读或读写方式打开的文件上读字符。其函数原型为:
int fgetc(FILE *fp);
- 其中,fp是函数fopen()返回的文件指针,该函数的功能是从fp所指的文件中读取一个字符,并将位置指针指向下一个字符。如读取成功,则返回该字符,若读到文件末尾,则返回EOF,(EOF是一个符号常量,在stdio.h中定义为-1)。
- 函数fputc()用于将一个字符写到一个文件上。fputc()的函数原型为:
int fputc(int c,FILE *fp);
- 其中,fp使函数fopen()返回的文件指针,c是要输出的字符(尽管C定义为int型,但只写入低字节)。该函数的功能是将字符c写到文件指针fp所指定的文件中。若写入错误,则返回EOF,否则返回字符c。
例:从键盘键入一串字符,然后将它们转存到磁盘文件上、 #include<stdio.h> #include<stdlib.h> int main(void) { FILE *fp; char ch; if((fp = fopen("demo.txt","w")) == NULL)//判断文件是否打开成功 { printf("Failure to open demo.txt!\n"); exit(0); } ch = getchar(); while(ch != '\n')//若键入回车换行符则结束键盘输入和文件写入 { fputc(ch,fp); ch = getchar(); } fclose(fp);//关闭由函数fopen()打开的fp指向的文件 return 0; }
- 为什么要判断文件打开是否成功呢?这是因为文件并不是每次都能被成功打开的。例如,文件损坏。
- 若文件打开失败,则函数fopen()返回空指针NULL。因此,可通过检查fopen()返回值是否为NULL来判断文件是否打开成功。若文件打开不成功,需显示错误提示信息(如驱动器是否正常工作、磁盘文件打开是否存在、路径表示是否正确、文件是否损坏等)。一般情况下,此时可调用exit(0)来种植程序的运行。
- 注意,使用getchar()输入字符时,是先将所有字符送入缓冲区,直到键入回车换行符后才从缓冲区中逐个读出并赋值给变量ch。
例:将0~127之间的ASCII字符写到文件中,然后从文件中读出并显示到屏幕上 #include<stdio.h> #include<stdlib.h> int main(void) { FILE *fp; char ch; int i; if((fp = fopen("demo.bin","wb")) == NULL)//以二进制写方式打开文件 { printf("Failure to open demo.bin!\n"); exit(0); } for(i = 0;i < 128;i++) { fputc(i,fp);//将ASCII码值在0~127之间的所有字符写入文件 } fclose(fp); if((fp = fopen("demo.bin","rb")) == NULL)//以二进制读方式打开文件 { printf("Failure to open demo.bin!\n"); exit(0); } while((ch = fgetc(fp)) != EOF)//从文件中读取字符直到文件末尾 { putchar(ch);//在显示器上显示从文件读出的所有字符 } fclose(fp); return 0; }
- 运行该程序显示的结果在不同的机器上可能显示的乱码有所不同,用记事本或写字板打开文件看到的也是乱码,而且与屏幕显示的结果可能并不完全一致。因为在Windows文本编辑器中打开文件时,显示的那些拐来拐去、方块箭头等符号i都不是可打印字符,使用不同的文本编辑软件会有不同的处理,而且还会配合字符集等进行转码,甚至换不同的字体时会有不同的显示结果。
- 除了判断返回值是否为EOF来判断是否读到了文件末尾,还可使用函数feof()来判断是否读到文件末尾.
//文中的代码 while((ch = fgetc(fp)) != EOF)//从文件中读取字符直到文件末尾 { putchar(ch);//在显示器上显示从文件读出的所有字符 } //可替换成下列代码 ch = fgetc(fp); while(!feof(fp)) { putchar(ch); ch = fgetc(fp); } //这里,函数feof()用于检查是否到达文件末尾,当文件位置指针指向我呢见结束符(End-of-file Indicator)时,返回非0值,否则返回0值。其函数原型为: int feof(FILE *fp);
例:修改程序,将ASCII码值在0~127之间的字符写到磁盘文件上,然后从文件中读出这些字符时,判断读出的字符是否为可打印字符,若是则直接将字符显示到屏幕上,否则将该字符的十进制ASCII码值显示到屏幕上。 #include<stdio.h> #include<stdlib.h> #include<ctype.h> int mian(void) { FILE *fp; char ch; int i; if((fp = fopen("demo.bin","wb")) == NULL)//以二进制写方式打开文件 { printf("Failure to open demo.bin!\n"); exit(0); } for(i = 0;i < 128;i++) { fputc(i,fp);//将ASCII码值在0~127之间的所有字符写入文件 } fclose(fp); if((fp = fopen("demo.bin","rb")) == NULL)//以二进制读方式打开文件 { printf("Failure to open demo.bin!\n"); exit(0); } while((ch = fgetc(fp)) != EOF)//从文件中读取字符直到文件末尾 { if(isprint(ch))//判断是否是可打印文件 { printf("%c\t",ch);//若是可打印字符,则显示该字符 } else { printf("%d\t",ch);//若非可打印字符,则显示该字符的ASCII码值 } } fclose(fp); return 0; }
- 还可使用feof()来判断是否读到文件末尾。
while(!feof(fp))//从文件中读取字符直到文件末尾 { ch = fgetc(fp); if(isprint(ch))//判断是否是可打印文件 { printf("%c\t",ch);//若是可打印字符,则显示该字符 } else { printf("%d\t",ch);//若非可打印字符,则显示该字符的ASCII码值 } }
- 采用feof()函数书写程序会出现,读到文件结束符时,才能判断出到达文件尾,而文件末尾的文件结束符又是一个值为-1、不可打印的非控制字符,因此按上面的程序的流程,就将其十进制ASCII码值(即-1)输出到了屏幕上。
- 解决此问题,只需将isprint()判断是否是可打印字符的语句修改为用函数iscntrl()判断是否是控制字符即可,即修改为
if(!iscntrl(ch))//判断是否是控制字符,若不是控制字符,则显示该字符 //另一种修改方法 /*将ch = fgetc(fp);语句复制到printf("%d\t",ch);之后作为while循环的最后一条语句,同时复制到最后一个while循环语句之前,然后删掉ch = fgetc(fp);语句。 */
- 这样修改是因为函数feof()总是在读完文件所有内容后再执行一次读文件操作(将文件结束符读走,但不显示)才能返回真(非0)值。
-
读走文件中的字符串
- 从文件中读取字符串可使用函数fgets(),其函数原型为:
char *fgets(char *s,int n,FILE *fp);
- 该函数从fp所指的文件中读取字符串并在字符串末尾添加’\0’,然后存入s,最多读n-1个字符。当读到回车换行符、叨叨文件尾或读满n-1个字符时,函数返回该字符串的首地址,即指针s的值;读取失败时返回空指针(NULL)。
- 因出错和到达文件尾时都返回NULL,因此应使用feof()或ferror()确定函数fgets()返回iNULL的实际原因是什么。
- 函数ferror()用来检测是否出现文件错误,如果出现错误,则函数返回一个非0值,否则返回0值。
- 将字符串写入文件中可使用函数fputs(),其函数原型为:
例:从键盘输入一串字符,然后把它们添加到文本文件demo.txt的末尾,使用fgets()实现。假设文本文件demo.txt中已有内容为:I am a student。 #include<stdio.h> #include<stdlib.h> #define N 80 int main(void) { FILE *fp; char str[N]; if((fp = fopen("demo.txt","a")) == NULL)//以添加方式打开文本 { printf("Failure to open demo.txt!\n"); exit(0); } gets(str);//从键盘读入一个字符串 fputs(str,fp);//将字符串str写入fp所指的文件 fclose(fp); if((fp = fopen("demo.txt","r")) == NULL)//以只读方式打开文本文件 { printf("Failure to open demo.txt!\n"); exit(0); } fgets(str,N,fp);//从fp所指的文件读出字符串,最多读N-1个字符 puts(str);//将字符串送到屏幕显示 fclose(fp); return 0; }
- 与gets()不同的是,fgets()从指定的流读字符串,读到换行符时将换行符也作为字符串的一部分读到字符串中来。同理,与puts不同的是,fputs()不会在写入文件的字符串末尾加上换行符。
12.4按格式读写文件
- C语言允许按指定格式读写文件。函数fscanf()用于指定格式从文件读数据。其函数原型为:
int fscanf(FILE *fp,const char *format,...);
- 其中,第1个参数为文件指针,第2个参数为格式控制参数,第3个参数为地址参数表列,后两个参数和返回值与函数scanf()相同。
- 函数fprintf()用于按指定格式向文件写数据。其函数原型为:
int fprintf(FILE *fp,const char *format,...);
- 其中,第1个参数为文件指针,第2个参数为格式控制参数,第三个参数为输出参数列表,后两个参数和返回值与函数printf()相同。
- 用函数fscanf()和fprintf()进行文件的格式化读写,读写方便,容易理解,但输入时要将ASCII字符转换成二进制数,输出时要将二进制数转换为ASCII字符,耗时较多。
例:编程计算每个学生的4门课程的平均分,将学生的各科成绩及平均分输出到文件score.txt中
#include<stdio.h>
#include<stdlib.h>
#define N 30
typedef struct data
{
int year;
int month;
int day;
}DATA;
typedef struct student
{
long studentID;
char studentName[10];
char studentSex;
DATA birthday;
int score[4];
float aver;
}STUDENT;
void InputScore(STUDENT stu[],int n,int m);
void AverScore(STUDENT stu[],int n,int m);
void WritetoFile(STUDENT stu[],int n,int m);
int main(void)
{
STUDENT stu[N];
int n;
printf("How many student?");
scanf("%d",&n);
InputScore(stu,n,4);
AverScore(stu,n,4);
WritetoFile(stu,n,4);
return 0;
}
//从键盘输入n个学生的学号、姓名、性别、出生日期以及m门课程的成绩到结构体数组stu中
void InputScore(STUDENT stu[],int n,int m)
{
int i,j;
for(i = 0;i < n;i++)
{
printf("Input record %d:\n",i+1);
scanf("%ld",&stu[i].studentID);
scanf("%s",stu[i].studentName);
scanf(" %c",&stu[i].studentSex);//%c前有一个空格
scanf("%d",&stu[i].birthday.year);
scanf("%d",&stu[i].birthday.month);
scanf("%d",&stu[i].birthday.day);
for(j = 0;j<m;j++)
{
scanf("%d",&stu[i].score[j]);
}
}
}
//计算n个学生的m门课程的平均分,存入数组stu的成员aver中
void AverScore(STUDENT stu[],int n,int m)
{
int i,j,sum;
for(i = 0;i<n;i++)
{
sum = 0;
for(j = 0;j<m;j++)
{
sum = sum +stu[i].score[j];
}
}
stu[i].aver = (float)sum/m;
}
//输出n个学生的学号、姓名、性别、出生年月以及m门课程的成绩到score.txt中
void WritetoFile(STUDENT stu[],int n,int m)
{
FILE *fp;
int i,j;
if((fp = fopen("score.txt","w")) == NULL)//以写方式打开文本文件
{
printf("Failure to open score.txt!\n");
exit(0);
}
fprintf(fp,"%d\t%d\n",n,m);//将学生人数和课程门数写入文件
for(i = 0;i<n;i++)
{
fprintf(fp,"10ld%8s%3c%6d/%02d/%02d",stu[i].studentID,stu[i].studentName,stu[i].studentSex,
stu[i].birthday.year,
stu[i].birthday.month,
stu[i].birthday.day);
for(j = 0;j<m;j++)
{
fprintf(fp,"%4d",stu[i].score[j]);
}
fprintf(fp,"%6.1f\n",stu[i].aver);
}
fclose(fp);
}
- 将平均分变量作为STUDENT结构体成员的好处是使函数的接口更简洁,程序更便于维护,因为无论是增加还是减少STUDENT结构体的多少个成员,都不必修改函数AverScore()和WriteFile()的接口参数和主函数的调用语句,只要修改相应函数内的个别语句即可。
例:在上例的基础上,编程从文件score.txt中读出每个学生的学号、性别、性别、出生年月、各科成绩及平均分,并输出到屏幕上
#include<stdio.h>
#include<stdlib.h>
#define N 30
typedef struct data
{
int year;
int month;
int day;
}DATA;
typedef struct student
{
long studentID;
char studentName[10];
char studentSex;
DATA birthday;
int score[4];
float aver;
}STUDENT;
void ReadfromFile(STUDENT stu[],int *n,int *m);
void PrintScore(STUDENT stu[],int n,int m);
int main(void)
{
STUDENT stu[N];
int n;
printf("How many student?");
scanf("%d",&n);
ReadfromFile(stu,&n,&m);
PrintScore(stu,n,m);
return 0;
}
//从文件中读取学生的信息到结构体数组stu中
void ReadfromFile(STUDENT stu[],int *n,int *m)
{
FILE *fp;
int i,j;
if((fp = fopen("score.txt","r")) == NULL)//以读方式打开文本文件
{
printf("Failure to open score.txt!\n");
exit(0);
}
fscanf(fp,"%d\t%d",n,m);//从文件中读出学生人数和课程门数
for(i = 0;i<*nli++)//学生人数保存在n指向的存储单元
{
fscanf(fp,"%10ld",&stu[i].studentID);
fscanf(fp,"%8s",&stu[i].studentName);
fscanf(fp," %c",&stu[i].studentSex);//%C前有空格
fscanf(fp,"%6d/%2d/%2d",&stu[i].birthday.year,&stu[i].birthday.month,&stu[i].birthday.day);
for(j = 0;j<*m;j++)
{
fscanf(fp,"%4d",&stu[i].score[j]);
}
fscanf(fp,"%f",&stu[i].aver);//
}
fclose(fp);
}
//输出n个学生的学号、姓名、性别、出生年月以及m门课程的成绩及平均分到屏幕上
void PrintScore(STUDENT stu[],int n,int m)
{
int i,j;
for(i = 0;i<n;i++)
{
fprintf(fp,"10ld%8s%3c%6d/%02d/%02d",stu[i].studentID,stu[i].studentName,stu[i].studentSex,
stu[i].birthday.year,
stu[i].birthday.month,
stu[i].birthday.day);
for(j = 0;j<m;j++)
{
printf("%4d",stu[i].score[j]);
}
printf("%6.1f\n",stu[i].aver);
}
12.5按数据块读写文件
- 函数fread()和write()用于一次读取一组数据,即按数据块读写文件。fread()的函数原型为:
unsigned int fread(void *buffer,unsigned int size,unsigned int count,FILE *fp);
- fread()功能是从fp所指的文件中读取数据块并存储到buffer指向的内存中。buffer是待读入数据块的起始地址。size是每个数据块的大小(待读入的每个数据块的字节数)。count是最多允许读取的数据块个数(每个数据块size个字节)。函数返回的是实际读到的数据块个数。
- fwrite()的函数原型为:
unsigned int fwrite(const void *buffer,unsigned int size,unsigned int count,FILE *fp);
- fwrite()的功能是将buffer指向的内存中的数据块写入fp所指的文件。buffer是待输入数据块的起始地址。size是每个数据块的大小(待输出的每个数据块的字节数)。count是最多允许写入的数据块个数(每个数据块size个字节)。函数返回的是实际写入的数据块个数。
- 块数据的读写使我们不再局限于一次只读取一个字符、一个单词或一行字符串,它允许用户指定想要读写的内存块大小,最小为1字节,最大为整个文件。
例:编程计算每个学生的4门课程的平均分,将学生的各科成绩及平均分输出到文件student.txt中,然后再从文件中读出数据并显示到屏幕上
#include<stdio.h>
#include<stdlib.h>
#define N 30
typedef struct data
{
int year;
int month;
int day;
}DATA;
typedef struct student
{
long studentID;
char studentName[10];
char studentSex;
FDATA birthday;
int score[4];
float aver;
}STUDENT;
void InputScore(STUDENT stu[],int n,int m);
void AverScore(STUDENT stu[],int n,int m);
void WritetoFile(STUDENT stu[],int n);
void ReadfromFile(STUDENT stu[]);
void PrintScore(STUDENT stu[],int n,int m);
int main(void)
{
STUDENT stu[N];
int n,m = 4;
printf("How many student?");
scanf("%d",&n);
InpuScore(stu,n,m);
AverScore(stu,n,m);
WritetoFile(stu,n);
n = ReadfromFile(stu);
PrintScore(stu,n,m);
return 0;
}
//从键盘输入n个学生的学号、姓名、性别、出生日期以及m门课程的成绩到结构体数组stu中
void InputScore(STUDENT stu[],int n,int m)
{
int i,j;
for(i = 0;i < n;i++)
{
printf("Input record %d:\n",i+1);
scanf("%ld",&stu[i].studentID);
scanf("%s",stu[i].studentName);
scanf(" %c",&stu[i].studentSex);//%c前有一个空格
scanf("%d",&stu[i].birthday.year);
scanf("%d",&stu[i].birthday.month);
scanf("%d",&stu[i].birthday.day);
for(j = 0;j<m;j++)
{
scanf("%d",&stu[i].score[j]);
}
}
}
//计算n个学生的m门课程的平均分,存入数组stu的成员aver中
void AverScore(STUDENT stu[],int n,int m)
{
int i,j,sum;
for(i = 0;i<n;i++)
{
sum = 0;
for(j = 0;j<m;j++)
{
sum = sum +stu[i].score[j];
}
}
stu[i].aver = (float)sum/m;
}
//输出n个学生的信息到文件student.txt中
void WritetoFile(STUDENT stu[],int n)
{
FILE *fp;
int i,j;
if((fp = fopen("student.txt"."w")) == NULL)//以写方式打开文本文件
{
printf("Failure to open student.txt!\n");
exit(0);
}
fwrite(stu,sizeof(STUDENT),n,fp);//按数据块写文件
fclose(fp);
}
//从文件中读取学生的信息到结构体数组stu中并返回学生数
int ReadfromFile(STUDENT stu[])
{
FILE *fp;
int i;
if((fp = fopen("student.txt","r")) == NULL)//以读方式打开文本文件
{
printf("Failure to open student.txt!\n");
exit(0);
}
for(i=0;!feof(fp);i++)
{
fread(&stu[i],sizeof(STUDENT),1,fp);//按数据块读文件
}
fclose(fp);
printf("Total students is %d.\n",i-1);
return i-1;//返回文件中的学生记录数
}
//输出n个学生的信息到屏幕上
void PrintScore(STUDENT stu[],int n,int m)
{
int i,j;
for(i = 0;i<n;i++)
{
fprintf(fp,"10ld%8s%3c%6d/%02d/%02d",stu[i].studentID,stu[i].studentName,stu[i].studentSex,
stu[i].birthday.year,
stu[i].birthday.month,
stu[i].birthday.day);
for(j = 0;j<m;j++)
{
printf("%4d",stu[i].score[j]);
}
printf("%6.1f\n",stu[i].aver);
}
12.6扩充内容
12.6.1文件的随机读写
- 前面的例程序执行的都是顺序文件处理(Sequential File Processing)。在顺序文件处理过程中,数据项是一个接着一个进行读取或者写入的。。不同于顺序读写的是,文件的随机访问(Random Access)允许在文件中随机定位,并在文件的任何位置直接读写数据。
- 为了实现文件的定位i,在每一个打开的文件中,都有一个文件位置指针(File Location Pointer),也称文件位置标记,用来指向当前读写文件的位置,他保存了文件中的位置信息。当对文件进行顺序读写时,每读完一个字节后,该位置指针自动移动到下一个字节的位置。当需要随机读写文件数据时,则需强制移动文件位置指针指向特定的位置。
- C语言提供了两个函数来定文件位置指针:
int fseek(FILE *fp,long offset,int fromwhere);
void rewind(FILE *fp);
- 其中,函数fseek()的功能是将fp的文件位置从fromwhere开始移动offset个字节,指示下一个要读取的数据的位置。
- offset是一个偏移量,它告诉文件位置指针要跳过多少个字节。offset为正时,向后移动,为负时,向前移动。ANSI C要求位移量offset是长整型数据(常量数据后要加L),这样当文件的长度大于64kb时不至于出问题。fromwhere用于确定偏移量计算的起始位置,它的可能取值有3种:SEEK_SET或0,代表文件开始处;SEEK_CUR或1,代表文件当前位置;SEED_END或2,代表文件结尾处。通过指定fromwhere和offset的值,可使位置指针移动到文件的任意位置,从而实现文件的随机读取。如果函数fseek()调用成功,则返回0值,否则返回非0值。
- 函数rewind()的功能是将文件位置指针指向文件首字节,即重置位置指针到文件首部。
- C语言还提供了一个用来读取当前文件位置指针的函数,其函数原型为:
long ftell(FILE *fp);
- 若函数调用成功,则返回文件的当前读写位置,否则返回-1L。函数ftell()用于相对文件起始位置的字节偏移量来表示返回的当前文件位置指针。
- C语言为了提高数据输入/输出的速度,在缓冲型文件系统中,给打开的每个文件建立一个缓冲区。文件内容先被批量地读入缓冲区。程序进行读操作时,实际上是从缓冲区中读数据。写入操作也是如此,首先将数据写入缓冲区,然后在适当的时候(例如关闭时)再批量写入磁盘。这样虽然可以提高I/O的性能,但也有一些副作用。
- 例如,在缓冲区内容还未写入磁盘时,计算机突然死机或掉电,数据就会丢失,永远也找不回来。再如,缓冲区中被写入无用的数据时,如果不清除,其后的文件读操作都首先要读取这些无用的数据。
- 为解决这些问题,C语言提供了如下函数:
int fflush(FILE *fp);
- fflush()的功能是无条件地把缓冲区中的所有数据写入物理设备。这样,程序员可自己决定在何时清除缓冲区中的数据,以确保输出缓冲区中的内容写入文件。
例:编写从文件student.txt中随机读取第k条记录的数据并显示到屏幕上,k由用户从键盘输入
#include<stdio.h>
#include<stdlib.h>
typedef struct data
{
int year;
int month;
int day;
}DATA;
typedef struct student
{
long studentID;
char studentName[10];
char studentSex;
FDATA birthday;
int score[4];
float aver;
}STUDENT;
void SearchinFile(char fileName[],long k);
int main(void)
{
long k;
printf("Input the searching record number:");
scanf("%ld",&k);
SearchinFile("student.txt",k);
return 0;
}
//从文件fileName中查找并显示第k条记录的数据
void SearchinFile(char fileName[],long k)
{
FILE *fp;
int i;
STUDENT stu;
if((fp = fopen(fileName,"r")) == NULL)//以读方式打开文本文件
{
printf("Failure to open %s!\n",fileName);
exit(0);
}
fseek(fk,(k-1) * sizeof(STUDENT),SEEK_SET);
fread(&stu,sizeof(STUDENT),1,fp);//按数据块读文件
printf("%10ld%8s%3c%6d/%02d/%02d",stu,studentID,
stu.studentName,stu.studentSex.stu.birthday.year,
stu.birthday.month,stu.birthday.month,stu.birthday.day);
for(j = 0;j<4;j++)
{
printf("%4d",stu.score[j]);
}
printf("%6.1f\n",stu.aver);
fclose(fp);
}
- 函数SearchinFile()的功能是从文件fileName中查找并显示第k条记录的数据,函数SearchinFile()的第1个形参fileName是字符数组,用于存放需要读取的文件名,函数SearchinFile()的第2个形参是长整型变量,表示要读取的记录号,为了在文件中直接读取第k条记录,程序中使用函数fseek()将文件位置指针从开头向后移动(k-1)*sizeof (STUDENT)个字节。
- 之所以这样计算偏移量,是因为fseek ()的第2个参数要给出文件位置指针需跳过的字节数,而每条记录的长度是sizeof (STUDENT)个字节。同理,因函数ftell()返回的文件位置使用字节偏移量表示的,所以必须通过除以sizeof(STUDENT)才能换算成当前的记录号。
12.6.2标准输入/输出重定向
- 前面的程序对数据的输入/输出是通过终端设备来完成的,看似与文件毫无瓜葛,但实际上,对于终端设备,系统自动会打开3个标准文件:标准输入、标准输出和标注错误输出。
- 相应的,系统定义了3个特别的文件指针常数:stdin、stdout、stdeer,分别指向标准输入、标准输出和标准错误文件,这三个文件都是以标准终端设备作为输入/输出对象。在默认情况下,标准输入设备是键盘,标准输出设备是屏幕。例如,函数printf()、putchar()等都是向标准输出设备输出数据,而函数scanf()、getchar()等都是从标准输入设备输入数据。
- fprintf()是printf()的文件操作版,二者的差别在于fprintf()多了一个FILE *类型的参数fp。如果为其提供的第1个参数是stdout,那么它就和printf()完全一样了。同理可将其推广到fputc()与putchar()等其它函数。
putchar(c);
fputc(c,stdout);
//两者等价
getchar();
fgetc(stdin);
//两者等价
puts(str);
fputs(str,stdout);
//两者等价
- 下面函数原型中,fgets()比gets()多了一个参数size
char *fgets(char *s,int size,FILE *stream);//安全性更高
char *gets(char *s);
//fgets()第2个参数size可以用来限制输入字符串的长度,使读入的字符数不能超过限定的缓冲区的大小,从而达到防止缓冲区溢出攻击的目的。
//同理可得
gets(buffer);
fgets(buffer,sizeof(buffer),stdin);//安全性更高
- 虽然系统隐含的标准I/0文件是指终端设备,但其实标准输入和标准输出是可以重定向的,操作系统可以重新定向它们到其他文件或具有文件属性的设备,只有标准错误输出不能进行一般的输出重定向。例如,可以临时将从终端(键盘)输入数据改成从文件读入数据,将向终端(显示器)输出数据改向文件写数据。
- 用"<“表示输入重定向,用”>"表示输出重定向。例如:假设exefile是可执行程序的文件名,执行该程序时,需要输入数据,现在如果要求从file.in中读取数据,而非键盘输入,那么在DOS命令提示符下,只需键入如下命令行即可:
C:\exefile.exe<file.in
- 于是,exefile的标准输入就被"<"重定向到了file.in。再如,若键入如下命令行:
C:\exefile.exe>file.out
- 于是,exefile的标准输出就被">"重定向到了文件file.out,此时程序exefile的所有输出内容都被输出到了文件file.out中,屏幕无任何现实。
- fopen()可用于指定模式将输入或输出重定向到另一个文件。其常见的使用方法为:
freopen("D:\\in.txt","r",stdin);//将输入流定位到in.txt
freopen("CON","r",stdin);//将输入流还原到键盘
freopen("D:\\out.txt","w",stdout);//将输出流定位到out.txt
freopen("CON","w",stdout);//将输入流还原到屏幕
第13章 简单的游戏设计
13.1动画的基本原理
- 设计动画和游戏常用的函数,如清屏、延时、获取用户键盘输入、获得标准输出设备句柄、定位光标位置等。
- 光栅扫描显示器是一种基于电视技术的显示器(CRT)。在光栅扫描显示器中,电子束按照固定的扫描顺序,从上到下,从左到右,依次扫过整个屏幕,只有整个屏幕被扫描完毕才能显示一幅完整的图形,称为一帧(Frame)。
- 以CRT为例,电子束”轰击“屏幕上的荧光粉,使其发光而产生图形。荧光粉的发光持续时间很有限,因此图形在屏幕上的存留时间很短。为了保持一个持续稳定的图形画面,就需要控制电子束反复的重绘屏幕图形,这个过程称为刷新。每秒重绘屏幕图形的次数,称为刷新频率。刷新频率至少应该在60帧/秒以上,才不会发生闪烁现象。
- 屏幕上的每个点,称为一个像素(Pixel),它是构成图形的基本元素。需要存储的图形信息有屏幕上的所有像素点的灰度值构成一个像素矩阵。这些信息被存储在刷新缓冲存储器(俗称显存)中。对于图形显示方式,用水平和垂直方向能显示的像素数的乘积表示屏幕的显示分辨率。
- 动画其实就是动态地产生一系列静止、独立而又存在一定内在联系的画面,然后将其按一定的播放速度显示出来,其中当前帧画面是对前一帧画面的局部修改。静止画面动起来主要利用了人眼的视觉暂留现象。视觉暂留现象就是指光对视网膜所产生的视觉在光停止作用后仍会保留一段时间,即在物体快速运动时,当人眼所看到的影响消失后,人眼仍能继续保留其影响0.1~0.4s之间的图像。
//设计动画一般化实现步骤如下:
while(1)//循环播放,即循环显示不断更新的图形
{
清屏
显示图形
延时
更新图形
}
- 延时的目的是为了降低屏幕图形闪烁现象,确保在输出图形后等待即让图形在屏幕上停留几毫秒的时间。为了实现延时操作,需要使用Sleep()函数,该函数的功能是将进程挂起一段时间,在Windows系统中使用这个函数需要包括windows.h。标准C中的这个函数的首字母是小写的,但在Code::Blocks和VS下是大写的。
- 为实现清屏操作,需要使用system()函数,该函数的功能是发出一个DOS命令。例如,system(“cls”)就是向DOS发送清屏指令,必须在文件中包含stdlib.h才能使用该函数。
13.2迷宫游戏
-
在游戏设计中,获取用户输入通常使用函数getch(),此函数是非缓冲输入函数,无需用户按回车键即可得到用户的输入,该函数该有个好处就是输入的字符不会显示在屏幕上。
-
通过键盘交互方式模拟电子老鼠走迷宫的参考程序:
//w、a、s、d控制方向
#include<stdio.h>
#include<stdlib.h>
#include<conio.h>
#include<windows.h>
#define N 50
#define M 50
void Show(char str[][M],int n);
void UpdateWithInput(char str[][M],int n);
int main(void)
{
char str[N][M] =
{
"************ ",
"*o * ",
"* ********** ",
"* * * ** ",
"* * * * * ** ",
"* * * * * * ",
"* * * * * * ",
"* * * * ** * ",
"* * * * ** * ",
"* * ** * ",
"********** * "
};//保存迷宫地图数据
int n =12;
Show(str,n);//显示迷宫
UpdateWithInput(str,n);//与用户输入有关的更新
return 0;
}
//函数功能:显示迷宫地图
void Show(char str[][M],int n)
{
int i;
for(i = 0;i<n;i++)//显示n行迷宫地图数据
{
puts(str[i]);
}
}
//函数功能:完成与用户输入有关的迷宫更新
void UpdateWithInput(char str[][M],int n)
{
int x = 1,y = 1;//初始位置
int exitX = 10,exitY = 10;//迷宫出口
char input;
while(x != exitX || y != exitY)//判断是否到达迷宫出口
{
if(kbhit())//检测是否有键盘输入,没有就继续循环
{
input = getch();//从键盘获取输入
if(input == 'a' && str[x][y-1] != '*')//左移
{
str[x][y] = ' ';
y--;
str[x][y] = 'o';
}
if(input == 'd' && str[x][y+1] != '*')//右移
{
str[x][y] = ' ';
y++;
str[x][y] = 'o';
}
if(input == 'w' && str[x-1][y] != '*')//上移
{
str[x][y] = ' ';
x--;
str[x][y] = 'o';
}
if(input == 's' && str[x+1][y] != '*')//下移
{
str[x][y] = ' ';
x++;
str[x][y] = 'o';
}
}
system("cls");//清屏
Show(str,n);//显示更新后的迷宫地图
Sleep(200);//延时200ms
}
printf("You Win! \n");
system("PAUSE");
}
- 电子老鼠自动寻路走出迷宫参考程序如下:
//结果为x1,y1,x2,y2:1,1,11,10
#include<stdio.h>
#include<stdlib.h>
#include<conio.h>
#include<windows.h>
#define N 50
#define M 50
int flag = 0;//全局变量flag用来标记是否路径全部走完
int a[N][N] = {{1,1,1,1,1,1,1,1,1,1,1,1},
{1,0,0,0,0,0,0,0,0,0,0,1},
{1,0,1,1,1,1,1,1,1,1,1,1},
{1,0,1,0,0,0,1,0,0,0,1,1},
{1,0,1,0,1,0,1,0,1,0,1,1},
{1,0,1,0,1,0,1,0,1,0,0,1},
{1,0,1,0,1,0,1,0,1,0,0,1},
{1,0,1,0,1,0,1,0,1,1,0,1},
{1,0,1,0,1,0,1,0,1,1,0,1},
{1,0,1,0,1,0,1,0,1,1,0,1},
{1,0,0,0,1,0,0,0,1,1,0,1},
{1,1,1,1,1,1,1,1,1,1,0,1}
};
void Show(int a[][M],int n,int m);
int Go(int x1,int y1,int x2,int y2);
int main(void)
{
int x1,y1,x2,y2;
int n = 12,m = 12;
Show(a,n,m);//显示初始迷宫
printf("Input x1,y1,x2,y2:");//输入迷宫的入口和出口
scanf("%d,%d,%d,%d",&x1,&y1,&x2,&y2);
if(Go(x1,y1,x2,y2) == 0)//若自动走迷宫
{
printf("没有路径!\n");
}
else
{
printf("You win!\n");
system("PAUSE");
}
return 0;
}
//函数功能:显示迷宫地图
void Show(int a[][M],int n,int m)
{
int i,j;
for(i = 0;i<n;++i)//显示n行m列迷宫地图数据
{
for(j = 0;j<m;++j)
{
if(a[i][j] == 0)
{
printf(" ");//显示路
}
else if(a[i][j] == 1)
{
printf("*");//显示障碍物
}
else if(a[i][j] == 2)
{
printf("o");//显示电子老鼠
}
}
printf("\n");
}
}
//函数功能:用递归函数实现自动走迷宫,从入口(x1,y1)走到出口(x2,y2)
int Go(int x1,int y1,int x2,int y2)
{
a[x1][y1] = 2;//将电子老鼠置于迷宫入口
system("cls");//清屏
Show(a,12,12);//显示更新后的迷宫地图
Sleep(200);//延时200ms
if(x1 == x2 && y1 == y2)//若到达迷宫出口(x2,y2)则将flag置为1
{
flag = 1;
}
if(flag != 1 && a[x1-1][y1] == 0)//判断向上是否有路
{
Go(x1-1,y1,x2,y2);
}
if(flag != 1 && a[x1+1][y1] == 0)//判断向下是否有路
{
Go(x1+1,y1,x2,y2);
}
if(flag != 1 && a[x1][y1+1] == 0)//判断向右是否有路
{
Go(x1,y1+1,x2,y2);
}
if(flag != 1 && a[x1][y1-1] == 0)//判断向左是否有路
{
Go(x1,y1-1,x2,y2);
}
if(flag != 1)
{
a[x1][y1] = 0;
}
return flag;
}
13.3Flappy bird游戏
- 玩家控制一只小鸟,跨越各种长度不同水管所组成的障碍物。
//玩家需要间歇性按动空格键来控制小鸟飞行
#include<stdio.h>
#include<stdlib.h>
#include<conio.h>
#include<time.h>
#include<windows.h>
#define DIS 22
#define BLAN 9//上下两部分柱子墙之间的缝隙
typedef struct bird
{
COORD pos;
int score;
}BIRD;
void CheckWall(COORD wall[]);//显示柱子墙体
void PrtBird(BIRD * bird);//显示小鸟
int CheckWin(COORD * wall,BIRD * bird);//检测小鸟是否碰墙或超出上下边界
void Begin(BIRD * bird);//显示上下边界和分数
BOOL SetConsoleColor(unsigned int wAttributes);//设置颜色
void Gotoxy(int x,int y);//定位光标
BOOL SetConsoleColor(unsigned int wAttributes);//设置颜色
void HideCursor();//隐藏光标,避免闪屏现象,提高游戏体验
int main(void)
{
BIRD bird = {{22,10},0};//小鸟的初始位置
COORD wall[3] = {{40,10},{60,6},{80,8}};//柱子的初始位置和高度
int i;
char ch;
while(CheckWin(wall,&bird))
{
Begin(&bird);//清屏并显示上下边界和分数
CheckWall(wall);//显示柱子墙
PrtBird(&bird);//显示小鸟
Sleep(200);
if(kbhit())//检测到有键盘输入
{
ch = getch();//输入的字符存入ch
if(ch == ' ')//输入的是空格
{
bird.pos.Y -= 1;//小鸟向上移动一格
}
else//未检测到键盘输入
{
bird.pos.Y += 1;//小鸟向下移动一格
}
for(i=0;i<3;++i)
{
wall[i].X--;//柱子墙向左移动一格
}
}
}
return 0;
}
//函数功能:显示柱子墙体
void CheckWall(COORD wall[])
{
int i;
HideCursor();
srand(time(NULL));
COORD temp = {wall[2].X+DIS,rand()%13+5};//随机产生一个新柱子
if(wall[0].X<10)//超出预设的左边界
{
wall[0] = wall[1];//最左侧的柱子墙消失,第2个柱子变成第1个
wall[1] = wall[2];//第3个柱子变成第2个
wall[2] = temp;//新产生的柱子变成第3个
}
for(i=0;i<3;i++)
{
//显示上半部分柱子墙
temp.X = wall[i].X + 1;//向右缩进一格显示图案
SetConsoleColor(0x0C);//设置黑色背景,亮红色前景
for(temp.Y=2;temp.Y<wall[i].Y;temp.Y++)//从第2行开始显示
{
Gotoxy(temp.X,temp.Y);
printf("$$$$$$");
}
temp.X--;//向左移动一格显示图案
Gotoxy(temp.X,temp.Y);
printf("$$$$$$");
//显示下半部分柱子墙
temp.Y += BLAN;
Gotoxy(temp.X,temp.Y);
printf("$$$$$$");
temp.X++;//向右缩进一格显示图案
temp.Y++;//在下一行显示下面的图案
for(;(temp.Y)<26;temp.Y++)//一直显示到第25行
{
Gotoxy(temp.X,temp.Y);
printf("$$$$$$");
}
}
}
//函数功能:显示小鸟
void PrtBird(BIRD * bird)
{
SetConsoleColor(0x0E);//设置黑色背景,亮黄色前景
Gotoxy(bird->pos.X,bird->pos.Y);
printf("o->");//显示小鸟
}
//函数功能:检测小鸟是否碰撞到墙体或者超出上下边界,是则返回0,否则分数加1并返回1
int CheckWin(COORD * wall,BIRD * bird)
{
if(bird->pos.X >= wall->X)//小鸟的横坐标进入柱子坐标范围
{
if(bird->pos.Y <= wall->Y || bird->pos.Y >= wall->Y + BLAN)
{
return 0;//小鸟的纵坐标碰到上下柱子,则返回0
}
}
if(bird->pos.Y < 1 || bird->pos.Y >26)
{
return 0;//小鸟的位置超出上下边界,则返回0
}
(bird->score)++;//分数加1
return 1;
}
//函数功能:显示上下边界和分数
void Begin(BIRD * bird)
{
system("cls");
Gotoxy(0,26);//第26行显示下边界
printf("=============================="
"============================"
"================================");
Gotoxy(0,1);//第一行显示上边界
printf("=============================="
"============================"
"================================");
SetConsoleColor(0x0E);//设置黑色背景,亮黄色前景
printf("\n %4d",bird->score); //第一行显示分数
}
//函数功能:定位光标
void Gotoxy(int x,int y)
{
COORD pos = {x,y};
HANDLE hOutput = GetStdHandle(STD_OUTPUT_HANDLE);//获得标准输出设备的句柄
SetConsoleCursorPosition(hOutput,pos);//定位光标位置
}
//函数功能:设置颜色
//一共有16种文字颜色,16种背景颜色,组合有256种。传入的参数值应当小于256
//字节的低4位控制前景色,高4位控制背景色,高亮+红+绿+蓝
BOOL SetConsoleColor(unsigned int wAttributes)
{
HANDLE hOutput = GetStdHandle(STD_OUTPUT_HANDLE);
if(hOutput == INVALID_HANDLE_VALUE)
{
return FALSE;
}
return SetConsoleTextAttribute(hOutput,wAttributes);
}
//函数功能:隐藏光标,避免屏闪现象,提高游戏体验
void HideCursor()
{
HANDLE handle = GetStdHandle(STD_OUTPUT_HANDLE);
CONSOLE_CURSOR_INFO CursorInfo;
GetConsoleCursorInfo(handle,&CursorInfo);//获取控制台的光标信息
CursorInfo.bVisible = 0;//隐藏控制台光标
SetConsoleCursorInfo(handle,&CursorInfo);//设置控制台光标状态
}
第14章 总结
- 写在后面,就我个人观点而言,学习编程类语言,从C语言开始是比较好的,俗话说好:“C生万物”。大部分的编程语言都由C语言衍生出来,同时先学好C语言也能让你拥有更好的编程基础。
- 前面的内容很适合初学者学习,也可用来查漏补缺。以上内容皆为本人自学所得,有需求请自取,如有问题还望大家指出,一同交流进步,方便的话还望看官点赞、评论、收藏、转发、打赏。