【C】C程序设计(第4版)基础知识整理

来源:互联网 发布:广州网站建设优化 编辑:程序博客网 时间:2024/05/16 14:21

第1章 程序设计和C语言


例:调用max函数求最大值,共两个函数,不要以为在max函数中求出最大值z后就会自动地作为函数值返回调用处,必须用return语句指定将那个值作为函数值


C语言面向过程

c语言是面向结构也就是面向过程,java是纯面向对象,c++是即可面向过程编程也可面向对象编程;这里说的面向过程和面向对象是一种编程思想,具体说就是:

面向结构,把要实现的功能分成很多模块即函数,每个模块承担某一功能,每个模块可能会多次利用,这样只需调用函数,不用重新定义,节省了代码和时间(小程序)

面向对象,是我们把要实现的功能打包封装定义成一个类即一个对象,一个对象他既有多个属性也有多个行为,用类作为程序设计的单元,要实现这个类的功能还得把类实现,就是用类定义对象,就像面向结构里的函数定义后,你还得调用函数才能用函数的功能,类可以实现很多功能及对象的行为,也就是可以包含很多函数模块和变量,类与类直接可以通信也就是属性的传递就就是变量值的传递,这就比面向结构更加抽象,抽象级别更高(大程序)


程序总是从main函数开始执行

C程序对计算机的操作由C语句完成,为清晰起见,习惯上每行只写一个语句

C语言本身不提供输入输出语句

 

运行C程序的步骤与方法

1.上机输入和编辑源程序(.c文件

2.对源程序进行编译(.obj文件

3.进行连接处理(.exe文件

4.运行可执行程序,得到运行结果


C语言程序结构的特点

1)        一个程序由一个或多个源程序文件组成。

在一个源程序文件包括以下3个部分:①预处理指令;②全局变量;③函数定义;

2)        函数是C程序的主要组成部分,一个C程序必须包含一个main函数(且只能有一个main函数);

3)        一个函数包括两个部分:①函数首部;②函数体(声明部分+执行部分);

4)        程序总是从main函数开始执行的,不管main函数在程序中的位置如何;

5)        程序中对计算机的操作是由函数的C语句组成的;

6)        在每个数据声明和语句最后必须有分号

7)        C语言本身不提供输入输出语句,是由库函数scanf和printf完成的,C对输入输出实行“函数化”;

8)        程序应当包含注释,增加程序的可读性。


为使程序规范和可移植性,最好将main函数指定为int型,并在main函数最后加一个‘’return 0;”。是否加“return 0;”——如果一个独立的程序问题不大,但有些时候一个程序可能会被其他程序或脚本语言调用,这时候需要通过这个返回值来确定是否正确执行结束还是遇到异常,这时候这个返回值就很有用了。须包含一个main函数(只有一个)

输入变量时要加地址运算符!(把输入的变量保存到a、b地址里)


若函数没有参数,可以在括号中写void,也可以是空括号,如:

int main(void) int main()


第2章 算法——程序的灵魂

程序=数据结构+算法

1)        对数据的描述。在程序中要指定用到哪些数据以及这些数据的类型和数据的组织形式。这就是数据结构。

2)        对操作的描述。即要求计算机进行的步骤,也就是算法(algorithm)。

闰年能被4整除,但不能被100整除 or 能被400整除的年份。

素数除了1和该数本身之外,不能被任何整数整除的数。


三种基本结构

1)        顺序结构

2)        选择结构

3)        循环结构:①当型(while型)循环结构(当条件成立时~循环);②直到型(until型)结构(循环~直到条件成立)


第3章 最简单的C程序设计——顺序程序设计












printf格式字符   % 附加字符格式字符

d格式符

输出有符号的十进制整数,可指定域宽(所占的列数),如“%5d”

c格式符

输出一个字符,也可以指定域宽,整数以ASCII其码输出

s格式符

输出一个字符串

f格式符

输出实数(包括单、双精度、长双精度),以小数形式输出——①基本型,用%f,不指定输出数据的长度,由系统根据数据情况决定其所占的列数,一般实数的整数部分全部输出,小数部分输出6位;②指定数据宽度和小数位数,用%m.nf,如%7.2f决定了输出数据占7列,包括两位小数,最后一位四舍五入处理,%7.0f则不输出小数部分。用%f输出时要注意数据本身能提供的有效数字,如float型数据的存储单元只能保证6位有效数字,double型能15位有效数字;③输出的数据向左对齐,用%-m.nf,作用与%m.nf基本同,但当数据长度不足m时,数据向左靠,右端补空格

e/E格式符

%e以指数形式输出实数,若不指定输出数据所占的宽度和数字部分的小数位数,VC++会自动给出数字部分的小数位数6位,指数部分占5列(如e+002,e占1列,指数符号占1列,指数占3位),数值标准化指数形式输出(即小数点前必须有且仅有1位非零数字)

i格式符

作用同d格式符,按十进制整型数据输出,一般习惯用%d而少用%i

o格式符

按八进制整型数据输出,将内存单元中各位的值(0或1)按八进制形式输出,因此输出的数值不带符号,即将符号位也一起作为八进制数的一部分输出

x格式符

按十六进制数输出整数,%lx输出整数,也可以指定输出字段的宽度%12x

u格式符

输出无符号(unsigned)型数据,以十进制整数形式输出

g格式符

输出浮点数,系统自动选f或e格式中较短长度的输出,不输出无意义的0


scanf函数注意问题

1)        scanf函数中“格式控制”后面应是变量地址,而不是变量名,如scanf(“%f%f%f”,&a,&b,&c);

2)        若在“格式控制字符串”中除了格式声明外还有其他字符,则在输入数据时在对应位置上应输入与这些字符相同的字符,如scanf(“a=%d,b=%f,c=%f”,&a,&b,&c);在输入数据时要“a=1,b=3,c=2”;

3)        在用%c格式声明输入字符时,空格字符和“转义字符”中的字符都将作为有效字符输入,如scanf(“%c%c%c”,&c1,&c2.&c3);输入时应“abc”不要有空格间隔;

4)        输入数值数据时,如输入空格、回车、Tab键或遇非法字符(不属于数值的字符),认为该数据结束。输入数值时,在两个数值间需要插入空格(或其他分隔字符)以区分


C语言函数库的标准输入输出函数

printf():格式化输入

scanf():格式化输出

purchar():输出一个字符

getchar():输入一个字符,输入多个字符用多个getchar函数

puts():输出字符串

gets():输入字符串


第4章 选择结构程序设计

if结构的三种常用形式



条件运算符:? (问号是条件运算符)

条件表达式

表达式1 ? 表达式2 : 表达式3


选择结构的嵌套




注意结尾的分号!!!

switch语句实现多分支选择结构

switch(表达式)
{
case常量1:语句1
case常量2:语句2
……
case常量n:语句n

default:       语句n+1

}

◆可以无default——无匹配的case常量则不执行任何语句,跳转至switch的下面
case标号只是起标记作用,并不进行条件检查,若无break语句,则在执行完一个case语句后继续执行下去(一般在执行完一个case子句后,用break语句使流程跳出switch语句,最后一个case子句可不加break子句,因流程已到了switch结构的结尾)


第5章 循环结构程序设计


1.        while(表达式)语句


2.        do //先执行,再判断

      语句

while(表达式) ;    注意这里的分号!!!//当满足表达式~继续执行


3.        for(表达式1;表达式2;表达式3)     for(循环变量赋初值;循环条件;循环变量增值)         

语句                                语句

表达式的省略

表达式1可以省略,即不在for中设初值,但其后的分号不能省,如i=1; for(;i<=100;i++) sum=sum+i; 表达式2也可以省略,此时循环无终止地进行下去,即认为表达式3也可以省略,写在别处,如写在循环体中。甚至可以都省略,无终止循环,无实用价值。

表达式的内容

1)        表达式1可以是设置循环变量初值的赋值表达式,也可以是与循环变量无关的其他表达式,如for(sum=0;i<=100;i++) sum=sum+I;表达式3也可以是与循环控制无关的表达式,但不论怎样写for语句,都必须使循环能正常执行;

2)        表达式1和3可以是一个简单的表达式,也可以是逗号表达式,即包含一个以上的简单表达式,中间逗号间隔,如 for(sum=0,i=1;i<=100;i++) sum=sum+I; 或for(i=0,j=100;i<=j;i++,j--)k=i+j;。表达式2一般是关系表达式(i<=100)或逻辑表达式(a<b&&x<y),也可以是数值表达式或字符表达式,只要其值非零,就执行循环体,如 for(i=0;(c=getchar())!=’\n’;i+=c) 与 for;(c=getchar())!=’\n’;)printf(“%c”,c);;

3)        C99允许在for语句的表达式1中定义变量并赋初值,如for(inti=1;i<=100;i++) sum=sum+i;则无法在循环外使用i。


4. break语句提前终止循环,而执行循环下面的语句

break语句只能用于循环语句和switch语句中,而不能单独使用。

5.    continue语句提前结束本次循环,而执行下一次循环





第6章 利用数组处理批量数据

数组中的元素都属于同一个数据类型,每个元素由数组名和下标唯一确定,如s[15]

6.1 定义和引用一维数组

6.1.1定义一维数组

类型符数组名 [常量表达式]; 如int a[10];数组名a,有10个整型元素~在内存开辟存储空间。

①数组名的命名规则同变量名(只有字母/数字/下划线,且以数字开头);

②下标从0开始,即int a[10]包括[0]~[9];

③常量表达式中可以包括常量和符号常量,如“inta[3+5]”,不能包括变量(in a[n]×),C语言不允许对数组的大小做动态定义,即数组的大小不依赖于程序运行中的值——但如果是在被调用的函数(不包括主函数)中定义数组(如n的值从实参传来~可变长数组),其长度可以是变量或非变量。

6.1.2引用一维数组

只能引用数组元素而不能一次整体调用整个数组全部元素值:数组名[下标]

下标可以是整型常量或整型表达式,如t=a[2*3]


图 10 对10个数组元素依次赋值为0~9,并按逆序输出

6.1.3一维数组的初始化

即为了程序简洁,在定义数组的同时给其赋值。

(1)对全部元素赋初值:inta[10]={0,1,2,3,4,5,6,7,8,9};  {}内的数据成为“初始化列表”

(2)对数组一部分元素赋值,其他默认为0:int a[10]={1,2,3}即对前三个赋值
凡未被初始化列表指定初始化的数组元素,系统会自动把它们初始化为0(如果是字符型数组,则初始化为’\0’,如果是指针型数组,则初始化为NULL,即空指针)

(3)不指定个数,按赋值个数定:int a[]={1,2,3,4,5}即五个元素

6.1.4一维数组举例


图 11  Fibonacci数列(由 0 和 1 开始,之后的斐波那契系数就由之前的两数相加)


图 12  10个数冒泡排序

6.2 定义和引用二维数组(矩阵)

6.2.1定义二维数组

类型符数组名 [常量表达式] [常量表达式]; 如float a[3][4],b[5][10];即定义a为3×4(3行4列)的数组,b为5×10的数组,注意一对[]不能有俩下标floata[3,4]×,三维如float a[2][3][4]

①二维数组可以看作是特殊的一维数组:它的元素又是一个一维数组(见书149页)——C语言的这种处理方法在数组初始化和用指针表示时显得很方便。

②C语言中,二维数组元素排列的顺序是按列存放的,即在内存中先顺序存放第1行的元素,接着再存放第2行的元素(用矩阵形式表示二维数组时逻辑上的概念,实际在内存中,各元素是连续存放的,不是二维的,是线性的,这点务必明确)

6.2.2引用二维数组

数组名[下标][下标] 如a[2][3]即第二行第三列,而a[2,3]×  注意行列下标的范围

数组元素可以出现在表达式中,也可以被赋值,如b[1][2]=a[2][3]/2

6.2.3二维数组的初始化

可以用“初始化列表”对二维数组初始化

(1)分行,如:int a[3][4]={{1,2,3,4},{5,6,7,8},{9,10,11,12}};

(2)将所有数据写在一个{}里,按数组元素在内存中的排序对各元素赋初值,如int a[3][4]={1,2,3,4,5,6,7,8,9,10,11,12 };——此法不如(1)

(3)对部分元素赋值,如inta[3][4]={{1},{5},{9}};——只对各行第一列赋初值,其余全是0

(4)只对部分元素赋初值而省略第一维的长度,但应分行赋值,如int a[][4]={{0,0,3},{},{0,10}};

6.2.4二维数组举例


图 13  将一个二维数组行和列的元素互换,存到另一个二维数组中


图 14  有一个3×4的矩阵,要求编程求出元素的最大值,并输出行号和列号

6.3 字符数组

字符型数据是以字符的ASCII码存储在存储单元中的,一般占一个字节。在C99中,把字符类型归纳为整型类型中的一种。C语言中没有字符串类型,字符串是存放在字符型数组中的。

6.3.1定义字符数组

字符数组中的一个元素存放一个字符,如charc[10]; c[0]=’I’;也可用整型数组存放字符型数据,如int c[10]; c[0]=’a’;——合法,但浪费存储空间。

6.3.2 字符数组的初始化

 

 

若{}提供的初值个数>数组长度,则出现语法错误;若<,则只赋给前面的元素,其余元素自动定为空字符('\0’)  若提供的初值个数=数组长度,则可省略数组长度,如char c[]={‘I’,’ ‘,’p’};

6.3.3 引用字符数组中的元素


图 15  输出一个已知的字符串


图 16  输出一个菱形图

6.3.4 字符串和字符串结束标志

C语言中,将字符串当作字符数组处理,如char c[]={“I’m a girl”}或省略花括号即char c[]=“I’ma girl”——此时数组长度是10+1=11!(因为字符串常量最后有’\0’,而字符数组不一定这样)

字符串结束标志“\0”(ASCII码为0,这不是一个可显示的字符,而是一个“空操作符”,即什么也不做,用它作为字符串结束的标志不会产生附加字符,只是供辨别),把它前面的字符组成一个字符串。C系统在用字符数组存储字符串常量时会自动加一个“\0”作为结束符。在程序中往往依靠检测“\0”的位置来判定字符串是否结束,而不是根据数组长度来决定字符串长度。

6.3.5 字符数组的输入输出

逐个字符输出%c 或 整个字符串一次输出%s

①输出的字符中不包括结束符’\0’

②用“%s”格式符输出字符串时,printf函数中的输出项是字符数组名而不是数组元素名,错误示例printf(“%s”,c[0])×

③若数组元素>字符串的实际长度,也只输出到’\0’结束,即char c[10]={‘China’}; printf(‘%s’,c)——输出有效字符China而不是10个字符,这就是用字符串结束标志的好处

④可用scanf函数输入一个字符串,若输入多个字符串,应用空格分隔


图 17  scanf函数注意

scanf函数的输入项若是字符数组名,则不要再加地址符&,∵在C语言中数组名代表该数组的起始地址,错误示例scanf(“%s”,&str)——输出字符串printf(“%s”,c)实际上是这样执行的:按字符数组名c找到其数组的起始地址,然后逐个输出其中的字符,直至遇到’\0’为止。

6.3.6 处理字符串的函数

在使用字符串处理函数时,应 #include<string.h>把“string.h”文件包含到本文件中

字符串处理函数∈库函数,并非C语言本身的组成部分,不同的编译系统的用法有别

(1)puts函数——输出字符串的函数:put(字符串组) puts函数输出的字符串中可以包含转义字符,且输出时将字符串结束的标志’\0’转成’\n’即输出完字符串后换行。

(2)gets函数——输入字符串的函数:get(字符串组) 输入一个字符串,并得到一个函数值(字符数组的起始地址——一般用gets函数的目的是输入字符串,不太关心其函数值)

注意:用putsgets函数只能输出/输入一个字符串

(3)stract函数(stringcatenate)——字符串连接函数:stract(字符数组1,字符数组2)

字符串2接到字符串1后面,结果放到字符串1中(字符数组1必须足够大以容纳连接后的新字符串),函数调用后得到一个函数值——字符数组1的地址;只保留最后的’\0’。

(4)strcpy和strncpy函数(stringcopy)——字符串复制函数strcpy(字符串数组1,字符串2)

字符串1必须定义得足够大,以便容纳被复制的字符串2,字符数组1的长度不应小于字符串2的长度;②字符数组1必须写成数组名形式(如strl),字符串2可以是字符数组名也可以是一个字符串常量(如strcpy(strl,’China’);;③若复制前未对strl初始化或赋值,则strl各字节中的内容是无法预知的,复制时将strl中的字符串和其后的‘\0’一起复制到字符串数组1中,取代字符数组1中的前面6个字符,最后4个字符并不一定是’\0’,而是strl中原有的最后4个字节的内容;不能用赋值语句将一个字符串常量或字符数组直接给一个字符数组(错误示例:str=’China’×),只能用strcpy函数将一个字符串复制到另一个字符数组中去,用赋值语句只能将一个字符赋给一个字符型变量或字符数组函数;可以用strncpy函数将字符串中前面n个字符复制到字符数组中去,如strncpy(str1,str2,2);作用是将str2中最前面2个字符复制到str1中,取代str1中原有的最前面的两个字符(但复制的字符个数n不应多于str1原有的字符,不包括’\0’

(5)strcmp函数(stringcompare)——字符串比较函数:strcmp(字符串1,字符串2)

将两个字符串自左向右逐个字符相比(按ASCII码值大小比较),知道出现不同的字符或遇到’\0’为止(以第一对不相同的字符为准)在英文字典中,位置在后面的为“大”。

比较的结果由函数值返回:

(1)若字符串1=字符串2,则函数值为0;

(2)若字符串1>字符串2,则函数值为一个正整数;

(3)若字符串1<字符串2,则函数值为一个负整数;

错误示例:if(str2>str2)× 而应if(strcmp(str1,str2)>0)

(6)strlen函数(string length)——测字符串长度:strlen(字符数组)

函数值为字符串的实际长度(不包括’\0’)

(7)strlwr函数(string lowercase)——转换为小写:strlwr(字符串)

(8)strupr函数(string uppercase——转换为大写:strupr(字符串)


图 18  输入一行字符,统计有多少个单词,单词间用空格相隔(具体分析见书165页)


图 19  有三个字符串,找出其中最大者(具体分析见书167-168页)

第7章 用函数实现模块化程序设计

7.1为什么要用函数


图 20  输出结果

①定义这两个函数时指定函数类型为void,意为函数无类型,即无函数值,执行这两个函数后不会把任何值带回main函数;

②在程序中,定义函数的位置在main函数后,在这种情况下,应当在main函数之前或main函数中的开头部分,对以上两函数进行“声明——把有关函数的信息(函数名、函数类型、函数参数的个数与类型)通知编译系统,以便在编译系统对程序进行编译时,在进行到main函数调用print_star()print_message()时知道它们是函数而不是变量或其他对象。此外,还对调用函数的正确性进行检查(如类型、函数名、参数个数、参数类型等是否正确)。

(1)一个C程序由一个或多个程序模块组成,每一个程序模块作为一个源程序文件;

(2)一个源程序文件由一个或多个函数以及其他有关内容(如指令、数据声明与定义等)组成,一个源程序文件是一个编译单位;

(3)C程序的执行是从main函数开始的,若在main函数中调用其他函数,在调用后返回到main函数,在main函数中结束整个程序的运行;

(4)所有函数都是平行相互独立的,函数不能嵌套定义,函数间可以相互调用,但不能调用main函数,main函数是系统调用的;

(5)从用户的角度看,函数有两种:库函数or用户自定义的函数;

(6)从函数的形式看,函数分两类:无参函数or有参函数。

7.2怎样定义函数

必须“先定义,后使用”。库函数,用户不用自己定义,但需用#include指令把有关的头文件包含到本文件模块中,在有关的头文件中包括了对函数的声明,如数学函数sqrt、fabs(abs整数取绝对值,而fabs浮点数取绝对值)等,必须在文件模块开头写上“#include<math.h>”。

7.2.1 定义函数的方法

1.        定义无参函数

类型名函数名(void)——这个void可写可不写(void即空,表示函数没有参数)

{

函数体——函数体包括声明部分和语句部分

}

2.        定义有参函数

类型名函数名(形式参数表列)

{

函数体——函数体包括声明部分和语句部分

}

例子见书173页

3.        定义空函数

类型名函数名()

{ }

空函数没有任何实际作用——那为啥要定义空函数呢?在将来准备扩充功能的地方定义一个空函数(函数名取将来采用的实际函数名,如merge(),shell()等),分别表示合并、希尔法排序,但这些函数暂时还未编写好,先用空函数占一个位置,等以后扩充程序功能时用一个编好的函数代替它→使程序结构清楚,可读性好。

7.3调用函数

7.3.1 函数调用的形式

函数名(实参表列)——若调用无参函数,则“实参表列”可以没有,但()不能省略。

按函数调用在程序中出现的形式和位置来分,可以有以下三种函数调用方式:

1.        函数调用语句

如“printf_star();”这时不要求函数带回值,只要求函数完成一定的操作;

2.        函数表达式

函数调用出现在另一个表达式中,如“c=max(a,b);”,max(a,b)是一次函数调用,它是赋值表达式的一部分。这时要求函数带回一个确定的值以参加表达式的运算,如“c=2*max(a,b);”

3.        函数参数

如“m=max(a,max(b,c));”、“printf(“%d”,max(a,b));”其中max(b,c)是第一次调用,它的值作为max另一次调用的实参。

7.3.2 函数调用时的数据传递

1.        形式参数和实际参数

定义函数时函数名后面括号中的变量名称为“形式参数”或“虚拟参数”,在主调函数中调用一个函数时,函数名括号中的参数称为“实际参数(常量/变量/表达式)”。

2.        实参和形参间的数据传递


图 21  用函数求两数中的max

(1)实参可以是常量/变量/表达式,如“max(3,a+b)√”;

(2)实参与形参的类型应该相同或赋值兼容(int和float型转换、字符型与Int型通用)

7.3.3 函数调用的过程

(1)在定义函数中指定的形参,在未出现函数调用时,它们并不占内存中的存储单元,发生函数调用时,函数max的形参被临时分配内存单元;

(2)通过return语句将函数值带回到主调函数,注意返回值类型与函数类型一致;若函数不需要返回值,则不需要return语句,这时函数类型应定义为void类型;

(3)调用结束,形参单元被释放,而实参单元仍保留并维持原值,没有改变。

实参→形参:值传递单向传递,两者占有不同的存储单元

7.3.4 函数的返回值

(1)函数的返回值是通过函数中的return语句获得的(return语句将被调用函数中的一个确定值带回到主函数中去。若需要从被调用函数带回一个函数值,被调用函数必须包含return语句,若不需要从被调用函数带回函数值,可以不要return语句。return语句后的括号可以不要,return后的值可以是一个表达式“return(‘x>y?x:y’);”)

(2)函数值的类型(函数的返回值属于某一个确定的类型,应当在定义函数时指定函数值的类型,如“int max(float x,float y)”)

(3)在定义函数时指定的函数类型一般应该和return语句中的表达式类型一致(若不一致,则以函数类型为准,对数值型数据可以自动进行类型转换,即函数类型决定返回值类型


图 22  函数实现显示两者的最大值

(4)对于不带回值的函数,应当用定义函数“void类型”(空类型)这样,系统就保证不使函数带回任意值,即禁止在调用函数中使用被调用函数的返回值,此时在函数体中不得出现return语句。

7.4对被调用函数的声明和函数原型

(1)被调用函数必须是已经定义的函数(库函数或用户自己定义的函数)“先定义,再使用”;

(2)若使用库函数,需在本文件开头用#include指令将调用有关库函数时所需用到的信息“包含”到本文件中,如“#include<stdio.h>”stdio.h是头文件——包含了输入输出库函数的声明。使用数学库中的函数,应用“#include<math.h>”,h是头文件所用的后缀(head file);

(3)若使用用户自己定义的函数,而该函数的位置在调用它的函数(即主调函数)后面(同一个文件中),应在主调函数中对被调函数作声明(declaration)

(4)定义≠声明:函数的定义是指对函数功能的确立,包括指定函数名、函数值类型、形参及其类型以及函数体等,它是一个完整的、独立的函数单位;声明是把函数名、函数参数个个数和参数类型等信息通知编译系统,以便在遇到函数调用时,编译系统能正确识别函数并检查调用是否合法,它不包括函数体。

 

 

图 23  输入两个实数,用一个函数求出它们之和

(1)函数的首行(即函数首部)称为函数原型(functionprototype),函数声明可以简单地照写已定义函数的首行,再加一个分号,就成了函数的“声明”;

(2)函数声明中形参名可以省写或写啥也行,如“float add(float,float);”;

(3)外部声明:若在文件开头(函数的外部)已对要调用的函数进行了声明(即外部的声明),则在各函数中不必对其所调用的函数再作声明(∵编译系统已从外部声明中知道了函数的有关信息,因此不必在主调函数重复声明)。

7.5函数嵌套调用

函数不能嵌套定义,但能嵌套调用


图 24  输入四个整数,找max,用函数的嵌套调用来处理(max4的函数体可改为m=max2(max2(max2(a,b),c),d); max2的可改为return(a>=b?a:b);  甚至不用m,max4函数体改为int max2(int a,int b); return max2(max2(max2(a,b),c),d);)

7.6函数的递归调用

在调用一个函数的过程中又直接或间接地调用该函数本身


图 25 五个人,后一个比前一个大两岁,第一个10岁,递归求第五个人岁数


图 26  递归求n! (0!=1是一种规定)

注意int型数据分配4个字节,能表示的最大数是2147483647,当n>=13,超出范围。


图 27  Hanoi(汉诺)塔问题

7.7数组作为函数参数

7.7.1 数组元素作函数实参

数组元素只能作函数实参,不能作形参。因为形参是在函数调用时临时分配存储单元的,不可能为一个数组元素单独分配存储单元(数组是一个整体,在内存中占连续的一段存储单元),同样“值传递”、实参→形参“单向传递”。


图 28  输入十个数,要求输出max的元素和该数是第几个数

7.7.2数组名作函数参数

数组名作函数参数,包括实参和形参。注:用数组元素作实参时,向形参变量传递的是数组的值,而用数组名作函数实参时,向形参(数组名或指针变量)传递的是数组首元素的地址。


图 29  一个一维数组score内放10个学生的成绩,求平均成绩

(1)用数组名作函数参数,应在主调函数和被调函数分别定义数组

(2)实参数组和形参数组类型应一致,如不一致,结果将出错;

(3)定义average函数时,指定数组的大小实际上并无任何作用,因为C语言编译系统并不检查形参数组的大小,只是将实参数组的首元素的地址传给形参数组名。因此,形参数组名获得了实参数组的首元素的地址(数组名代表数组的首元素地址)……

(4)形参数组可以不指定大小,在定义数组时在数组名后面跟一个[],如“float average(floatarray[])//定义average函数”


图 30  有两个班级,分别有5和10名学生,调用average函数分别求两班的平均成绩

7.7.3 多维数组名作函数参数

可以用多维数组作为函数的实参和形参,在被调用函数中对形参数组定义时可以指定每一维的大小,也可以省略第一维的大小说明,如int array[3][10],但不能省略第2维以及其他高维的大小,因为二维数组是由若干个一维数组组成的,在内存中,数组是按行存放的,因此,在定义二维数组时,必须指定列数,由于形参数组与实参数组类型相同,所以它们是由具有相同长度的一维数组组成的,错误示例int array[3][];×


图 31  有一个3×4的矩阵,求所有元素的max

7.8局部变量和全局变量

7.8.1 局部变量

(1)主函数中定义的变量只在主函数中有效主函数也不能使用其他函数中定义的变量

(2)不同函数可以使用同名的变量,它们代表不同对象,在内存中占不同单元,互不干扰;

(3)形参也是局部变量,其他函数可以调用f1函数,但不能直接引用f1函数的形参;

(4)在一个函数内部,可以在复合语句中定义变量,这些变量只在本复合语句中有效,这种复合语句也叫“分程序”或“程序块”。

7.8.2 全局变量

在函数内定义的变量是局部变量,在函数之外定义的变量是外部变量(即全局变量,全程变量)——可以为本文件中其他函数所共用,有效范围:从定义变量的位置——本源文件结束。设置全局变量的作用是:增加了函数间数据联系的渠道。习惯上,将全局变量名的第一个字母大写。


图 32  有一个一维数组,内放10个学生成绩,写一个函数,当主函数调用此函数后,能求出平均分、最高分和最低分

但是,在不必要时最好不用全局变量,∵全局变量在全部执行过程中都占用存储单元、使函数通用性降低了(划分模块时应“内聚强”、“耦合弱”)、使程序清晰性降低了。


图 33  若外部变量与局部变量同名,分析结果

7.9 变量的存储方式和生存期

7.9.1 动态存储方式和静态存储方式

静态存储方式:指在程序运行期间由系统分配固定的存储空间的方式。关于静态存储区:全局变量全部存放在静态存储区中,在程序开始执行时给全局变量分配存储区,程序执行完毕就释放。在程序运行过程中占据固定的存储单元,而不是动态得进行分配和释放。

动态存储方式:在程序运行期间根据需要进行动态的分配存储空间的方式。关于动态存储区:在函数调用开始时分配存储空间,函数结束就释放这些空间(动态分配与释放)若在一个程序中两次调用同一函数,而在此函数中定义了局部变量,在两次调用时分配给这些局部变量的存储空间的地址可能是不相同的。关于静态存储区:存放的数据有①函数形参,在调用函数时给形参分配存储空间;②函数中定义的没有用关键字static声明的变量,即自动变量;③函数调用时的现场保护和返回地址等。

C语言中,每个变量和函数都有两个属性:数据类型和数据的存储类别。存储类别是指数据在内存中存储的方式,包括自动的(auto)、静态的(statics)、寄存器的(register)、外部的(extern),根据变量的存储类别,可以知道变量的作用域和生存期……

7.9.2 局部变量的存储类别

1.       自动变量(auto变量)

函数中的局部变量,若不专门声明为statics(静态)存储类别,都是动态地分配存储空间的,数据存储在动态存储区中。函数的形参和在函数中定义的局部变量(包括在复合语句中定义的局部变量)都属于此类。在调用该函数时,系统会给这些变量分配存储空间,在函数调用结束时就自动释放这些存储空间,因此这类局部变量称为自动变量。

实际上,关键字“auto”可以省略,不写“auto”则隐含指定为“自动存储类别”,它属于静态存储方式。程序中大多数变量都属于自动变量,在函数中定义的变量都没有声明auto,其实都隐含指定为自动变量。

2.       静态变量(static局部变量)

有时希望函数中的局部变量的值在函数调用结束后不消失而继续保留原值,即其占用的存储单元不释放,在下一次再调用该函数时,该变量已有值(就是上一次调用时的值)


图 34  考察静态变量的值

(1)静态局部变量属于静态存储类别,在静态存储区分配存储单元,在程序整个运行期间都不释放。而自动变量(即动态局部变量)属于动态存储类别,分配在动态存储区空间而不在静态存储区空间,函数调用结束后即释放;

(2)静态局部变量是在编译时赋初值的,即只赋初值一次,在程序运行时它已有值,以后每次调用函数时不再重新赋初值而只是保留上次函数调用结束时的值。而对自动变量赋初值,不是在编译时而是在函数调用时进行的,每调用一次函数重新给一次初值,相当于执行一次赋值语句;

(3)若在定义局部变量时不赋初值,则对静态局部变量来说,编译时自动赋初值0(对数值型变量)或空字符’\0’(对字符变量)。而对自动变量来说,它的值是一个不确定的值;

(4)虽然静态局部变量在函数调用结束后仍存在,但其他函数不能引用它。因为它是局部变量,只能被本函数引用而不能被其他函数引用。


图 35  输出1到5的阶乘值

用静态存储要多占内存(长期占用不释放,而不能像动态存储那样一个存储单元可以先后为多个变量使用,节约内存),而且降低了程序的可读性,当调用次数多时往往弄不清静态局部变量的当前值是什么——若非必要,不要用静态局部变量

3.       寄存器变量(register变量)

若一些变量使用频繁(如一个函数中执行10000次循环,每次循环都要引用该局部变量),允许将局部变量的值放在CPU寄存器中,需要时直接从寄存器取出参加运算,不必再到内存中去存取。由于对寄存器的存取速度远高于对内存的存取速度,这样可以提高执行效率,这种变量叫“寄存器变量”,用关键字“register”声明——但现在优化的编译系统能时被并自动将使用频繁的变量放到寄存器,因此用register声明变量的必要性不大,知道即可。

7.9.3 全局变量的存储类别

全局变量都是存放在静态存储区中的,存在于整个程序的运行过程。一般来说,外部变量是在函数的外部定义的全局变量,作用域是从变量的定义初开始到本程序文件的末尾。在此作用域内,全局变量可以被程序中各个函数所引用,但有时程序设计人员希望能扩展外部变量的作用域,有以下几种情况:

1.        在一个文件内扩展外部变量的作用域

若外部变量不在文件开头定义,其有效作用范围只限于定义处到文件结束。在定义点之前不能引用该外部变量。如果由于某种考虑,在定义点之前的函数需要引用外部变量,则应加“extern”作外部变量声明,表示把该外部变量的作用域扩展到此位置,有了此声明,就可以从“声明”处起,合法地使用该外部变量。


图 36  调用函数,求3个整数的最大者

2.        将外部变量的作用域扩展到其他文件

若C程序是由多个源程序组成的,多个文件都要用到同一个外部变量Num,不能分别在两个文件各定义一次外部变量——会引起“重复定义”的错误。正确的做法是:在任一文件中定义外部变量Num,在另一文件中用extern对Num作“外部变量声明”,可以从别处找到已定义的外部变量Num,并将在另一文件中定义的Num扩展到本文件。


图 37  给定b的值,输入a和m,求a*b和a的m次方值

  用这样的方法扩展全局变量的作用域应十分慎重,因为在执行一个文件中的操作时,可能会改变该全局变量的值,会影响到另一文件中全局变量的值,从而影响该文件中函数的执行结果。

实际上,在编译时遇到extern,先在本文件中找外部变量的定义,若找到,就在本文件中扩展作用域;若找不到,就在连接时从其他文件中找外部变量的定义—如果从其他文件中找到了,就将作用域扩展到本文件;如果找不到,则按出错处理。

3.        将外部变量的作用域限制在本文件中

加上static声明、只能用于本文件的外部变量——静态外部变量,使程序模块化,通用性提供方便。注意:不要误以为对外部变量加上static声明后才采取静态存储方式(存放在静态存储区中),而不加static的是采取动态存储(存放在动态存储区中)。声明局部变量的存储类型(指定变量存储区域:静/动态存储区)和声明全局变量的存储类型(都在静态存储区,是变量作用域扩展问题)的含义是不同的。

用static声明一个变量的作用是:①对局部变量用static声明,把它分配在静态存储区,该变量在整个程序执行期间不释放,其所分配的空间始终存在;②对全局变量用static声明,则该变量的作用域只限于本文件模块(即被声明的文件中)。

注意:用auto、register、static声明变量是在定义变量的基础上,加上这些关键字(如static int a;√),而不能单独使用(如static a;×编译时会被认为“重复定义”)

7.9.4存储类别小结

定义一个数据,需要指定其两种属性:数据类型存储类别;(详见书212-213)

(1)从作用域角度分:局部变量和全局变量

(2)从变量存在的时间(生存期)分:动态存储和静态存储

(3)从变量值存放的位置分:内存中静态存储区、内存中动态存储区、CPU中的寄存器

7.10 关于变量的声明和定义

函数由声明部分和执行语句组成,声明部分是对有关的标识符(如变量、函数、结构体、共用体等)的属性进行声明,是函数的原型;而定义是对函数功能的定义,独立的模块。

变量:在声明部分出现的变量有两种情况,一种是需要建立存储空间的“定义性声明(定义)”,如int a——既是声明也是定义;另一种是不需要建立存储空间的“引用性声明”,如extern a——是声明而不是定义。外部变量定义≠外部变量声明,外部变量的定义只能由一次,位于所有函数之外;而在同一文件中,可以有多次对同一外部变量的声明,作用是声明该变量已在其他地方定义,仅仅是为了扩展该变量的作用范围——“在函数中出现的对变量的声明(除了extern声明外)都是定义,在函数中对其他函数的声明不是函数的定义。”

7.11 内部函数和外部函数

函数本质上是全局的,因为定义一个函数的目的就是要被另外的函数调用。若不加声明,一个文件中的函数既可以被本文件中其他函数调用,也可以本其他文件中的函数调用。但是,也可以指定某些函数不能被其他文件调用。根据能否被其他源文件调用,将函数分为内部函数和外部函数。

7.11.1 内部函数

内部函数(即静态函数)只能被本文件中其他函数所调用:static类型名函数名(形参表)

如static int fun(inta,int b),通常把只能由本文件使用的函数和外部变量放在文件开头,前加static使之局部化,其他文件不能引用,提高程序的可靠性。

7.11.2 外部函数

extern 类型名函数名(形参表),如extern intfun(int a,int b)这样fun函数就可以为其他文件调用。C语言规定,若在定义函数时省略extern,则默认为外部函数(本书前面所用的函数都是外部函数)在需要调用此函数的其他文件中,需要对此函数做声明(即使在本文件中调用一个函数,也要用函数原型进行声明)。在对此函数作声明时,要加关键字extern,表示该函数“是在其他文件中定义的外部函数”。


图 38  书216页例子(未弄好)

第8章 善于利用指针

8.1 指针是什么

VC++为整型变量分配4个字节,单精度浮点型分配4个字节,字符型分配1个字节,内存区的每一个字节都有一个编号,这就是“地址”,它相当于旅馆中的房间号,指向就是通过地址来体现的,地址指向变量单元一个变量的地址形象化地称为该变量的“指针”,意思是通过它能找到以它为地址的内存单元。程序通过编译后已经将变量名转换为变量的地址,对变量值的存取都是通过地址进行的。

直接按变量名进行的访问——直接访问;将变量的地址存放在另一变量中,然后通过该变量找到变量i的地址,从而访问变量i——间接访问。

在C语言中,可以定义专门用来存放地址的变量,即指针变量i_pointer=&i;,指针变量就是地址变量,用来存放地址,指针变量的值是地址(即指针

8.2 指针变量

8.2.1 使用指针变量的例子


图 39  通过指针变量访问整型变量

8.2.2 怎样定义指针变量

类型名 *指针变量名;int *pointer_1, *pointer_2; 类型名是在定义指针变量时必须指定的“基类型”,用来指定此指针变量可以指向的变量的类型。指针变量是基本数据类型派生出来的类型,它不能离开基本类型而独立存在;可以在定义指针变量时,同时对它初始化,如int *pointer_1=&a, * pointer_2=&b; //定义指针变量pointer_1, pointer_2,并分别指向a、b。

1)        指针变量前的“*”表示该变量为指针型变量,指针变量名是pointer_1和pointer_2,而不是*pointer_1和*pointer_2。上图例子的中间框两行不能写成*pointer_1=&a;和*pointer_2=&b;,因为a的地址是赋给指针变量pointer_1而不是*pointer_1(即变量a);

2)        定义指针变量时必须指定基类型∵不同类型数据在内存中所占字节数和存放方式不同,一个指针变量只能指向同一个类型的变量,不能忽而指向一个整型变量,忽而指向一个实型变量;一个变量的指针包括两方面含义:①以存储单元编号表示的地址(如编号2000的字节);②它指向的存储单元的数据类型(如int、char、float等)

3)        指向整型数据的指针类型为“int *”,读作“指向int的指针”或“int指针”;

4)        指针变量只能存放地址(指针),不能将一个整数赋给指针变量,如*pointer_1=100;×

8.2.3 怎样引用指针变量

在引用指针变量时,可能有三种情况:

1)        给指针变量赋值:p=&a;//把a的地址赋给指针变量p,p指向a;

2)        引用指针变量指向的变量:若已执行“p=&a;”即执行变量p已指向了整型变量a,则printf(“%d”,*p);作用是以整数形式输出指针变量p所指向的变量(a)的值。若有*p=1;表示将1赋给p指向的变量,即a=1;


图 40    *pointer_1=1,*pointer_2=2;

3)        引用指针变量的值,如printf(“%o”,p); 以八进制数形式输出指针变量p的值,若p指向a就是输出了a的地址,即&a。

要熟练掌握两个有关的运算符:

1)        & 取地址运算符。&a是变量a的地址;

2)        * 指针运算符(间接访问运算符),*p代表指针变量p指向的对象。


图 41  输入a和b两个整数,按先大后小的顺序输出a和b(注意红字可改写为{p1=&b;p2=&a;})

8.2.4 指针变量作为函数参数

→将一个变量的地址传送到另一个函数中。


图 42  输入a和b两个整数,按先大后小的顺序输出a和b,不能企图通过改变指针形参值而使指针实参的值改变swap函数写得有错,下例正解!)


图 43  输入a和b两个整数,按先大后小的顺序输出a和b

注意:函数调用可以(而且只可以)得到一个返回值(即函数值),而使用指针变量作参数,可以得到多个变化了的值。若不用指针变量是很难做到这一点的。要善于利用指针法


图 44  输入3个整数a,b,c,要求从大到小输出,用函数实现

8.3 通过指针引用数组

8.3.1 数组元素的指针

数组名不代表整个数组,只代表数组首元素的地址。如“p=a”作用是把a数组的首元素地址赋给指针变量p,在定义指针时可以对它初始化,如“int *p=&a[0];”等效于“int *p;  p=&a[0];”,当然定义也可写成“int *p=a;”作用是将a数组首元素a[0]的地址赋给指针变量p(而不是*p)

8.3.2 引用数组元素时指针的运算

在指针指向元素时,可以对指针进行以下运算:

加一个整数(+或+=),如p+1;

减一个整数(-或-=),如p-1;

注:执行p+1时并不是将p的值(地址)简单加1,而是加上一个数组元素所占用的字节数→在定义指针变量时要指定基类型;

实际上,在编译时,对数组元素a[i]就是按*(a+i)处理的,即按数组首元素的地址加上相对位移量得到要找到的元素的地址,然后找到该单元的内容:若p初值为&a[0],则p+i和a+i就是数组元素a[i]的地址,即它们指向a数组序号为i的元素(a代表数组首元素的地址)。*(p+i)或*(a+i)是p+i或a+i所指向的数组元素,即a[i],如*(p+5)或*(a+5)就是a[5]

[] 实际上是变址运算符,即将a[i]按a+i计算地址,然后找出此地址单元中的值。

自加运算,如p++,++p;

自减运算,如p- -,- -p;

两个指针相减,如p1-p2(只有p1和p2指向同一数组中的元素时才有意义——结果是两地址之差÷数组元素的长度)这样就无需知道p1和p2的值然后去计算它们的相对位置,而是直接用p1-p2就可知道它们所指元素的相对距离。而这两个地址不能相加,如p1+p2无意义。

8.3.3 通过指针引用数组元素

引用数组元素有两种方法:

1)        下标法,如a[i]形式;

2)        指针法,如*(a+i)或*(p+i),其中a是数组名,p是指向数组元素的指针变量,其初值p=a。

 

,

图 45  下标法

 

图 46  法二:通过数组名计算数组元素地址,找出元素的值


图 47  用指针变量指向数组元素

前两种方法执行效率同,指针法较快。下标法较直观……书234


图 48  通过指针变量输出整型数组a的10个元素

8.3.4 用数组名作函数参数

前已介绍,实参数组名代表该数组首元素的地址,而形参是用来接收从实参传递过来的数组首元素地址的。因此,形参应该是一个指针变量。实际上,C编译都是将形参数组名作为指针变量来处理的。在函数调用进行虚实结合后,它的值就是实参数组首元素的地址。在函数执行期间,形参数组可以再被赋值。

需要说明的是,C语言调用函数时虚实结合的方法都是采用“值传递”方式,当用变量名作为函数参数时传递的是变量的值,当用数组名作为函数参数时,由于数组名代表的是数组首元素地址,因此传递的是值是地址,所以要求形参为指针变量(实参数组名代表一个固定的地址,或者说是指针常量,但形参数组名并不是一个固定地址,而是按指针变量处理)


图 49  例8.8


图 50  例8.8(改)


图 51  例8.8(改)


图 52  例8.9


图 53  例8.10

如果有一个实参数组,要想在函数中改变此数组中的元素值,实参与形参的对应关系有以下四种情况:(书241-242)

1)        形参和实参都用数组名……

2)        实参用数组名,形参用指针变量……

3)        实参和形参都用指针变量……

4)        实参为指针变量,形参为数组名……

8.3.5 通过指针引用多维数组

1.       多维数组元素的地址

从二维数组(数组的数组)的角度看,a代表二维数组首元素的地址,现在的首元素不是一个简单的整型元素,而是由4个整型元素(例子中)组成的一维数组,因此a代表的是首行(即序号为0的行)的首地址,a+1代表序号为1的行的地址。a[0]、a[1]、a[2]既然是一维数组名,而C语言又规定了数组名代表数组首元素的地址,因此a[0]代表一维数组a[0]中第0列元素的地址,即&a[0][0],也就是说,a[1]的值是&a[1][0],a[2]的值是&a[2][0]。

二维数组名(如a)是指向行的。因此“a+1”中的“1”代表一行中的全部元素所占字节数。一维数组名(如a[0]、a[1])是指向列元素的,a[0]+1中的“1”代表一个a元素所占的字节数。在指向行的指针前面加一个*,就转换为指向列的指针,如a和a+1都是指向行的指针,而*a和*(a+1)成为指向列的指针,分别指向数组a数组0行0列的元素和1行0列的元素;在指向列的指针前面加一个&,就转换为指向行的指针,如a[0]是指向0行0列的指针,而&a[0]与&*a等价(因a[0]与*(a+0等价)),指向二维数组的0行。(详见书245-247)


图 54  输出二维数组的有关数据(地址和值)

2.       指向多维数组元素的指针变量

1)        指向数组元素的指针变量


图 55  有一个3×4的二维数组,要求用指向元素的指针变量输出二维数组各元素的值

2)        指向由m个元素组成的一维数组的指针变量

int(*p)[4]; 中p的类型不是*int型,而是int(*)[4]型,p被定义为指向一维整型数组的指针变量,一维数组有4个元素,因此p的基类型是一维数组,其长度是16字节(详见书252)


图 56  输出二维数组任一行任一列元素的值


图 57  小例

3)        用指向数组的指针作函数参数

一维数组名可以作为函数参数,多维数组名也可以作函数参数。用指针变量作形参,以接受实参数组名传递来的地址,可以有两种方法:①用指向变量的指针变量;②用指向一维数组的指针变量。

注意:形参与实参若是指针类型,应当注意它们的类型必须一致,不应把int*型指针(即元素的地址)传给int(*)[4]型(指向一维数组)的指针变量,反之亦然。


图 58  有一个班,3个学生,各学4门课,计算总平均分以及第n个学生的成绩


图 59  在上题的基础上,查找有一门以上课程不及格的学生,输出它们全部课程的成绩

8.4 通过指针引用字符串(例子见书)

8.4.1 字符串的引用方式

在C语言中,字符串是存放在字符数组中的,想引用字符串,有以下两种方式:

1)        用字符数组存放一个字符串,可通过数组名和下标引用字符串中一个字符,也可以通过数组名和格式声明“%s”输出该字符串;

2)        用字符指针变量指向一个字符串常量,通过字符指针变量引用字符串常量。

8.4.2 字符指针作函数参数

若想把一个字符串从一个函数“传递”到另一函数,可以用地址传递的办法,即用字符数组名作参数,也可用字符指针变量作参数。在被调用的函数中可以改变字符串的内容,在主调函数中可以引用改变后的字符串。

8.4.3 使用字符指针变量和字符数组的比较

1)        组成:字符数组由若干元素组成,每个元素中放一个字符,而字符指针变量中存放的是地址(字符串第一个字符的地址),绝不是将字符串放到字符指针变量中;

2)        赋值方式:可以对字符指针变量赋值,但不能对数组名赋值;

3)        初始化:数组可在定义时对各元素赋值,但不能对其全部元素整体赋值;

4)        存储单元的内容:编译时为字符数组分配若干存储单元,以存放各元素的值,而对字符指针变量,只分配一个存储单元(VC++为指针变量分配四个字节);

5)        是否可变:指针变量的值可变,而数组名代表的是一个固定的值(数组首元素的地址),不能改变;

6)        字符数组中各元素的值是可以改变的(可对它们再赋值),但字符指针变量指向的字符串常量中的内容是不可被取代的(不能对它们再赋值);

7)        引用数组元素:对字符数组可用下标法(用数组名和下标)引用一个数组元素(如a[5]),也可用地址法(如*(a+5))引用数组元素a[5];但是,若指针变量未指向数组,则无法用p[5]或*(p+5)这样的形式引用数组中的元素;

8)        用指针变量指向一个格式字符串,可用它代替printf函数中的格式字符串;但使用字符数组时,只能采用在定义数组时初始化或逐个对元素赋值的方法,而不能用赋值语句对数组整体赋值。

因此,用指针变量指向字符串的方式更为方便。

8.5  指向函数的指针

8.5.1 什么是函数指针

若在程序中定义了一个函数,在编译时,编译系统为函数代码分配一段存储空间,这段存储空间的起始地址(又称入口地址)称为这个函数的指针。

可定义一个指向函数的指针变量用来存放某一函数的起始地址,int (*p)(int,int),p的类型为int(*)(int,int)。注意(*p)括号不能省,表明p先与*结合,是指针变量,再与后面函数结合,()表示的是函数,表明该指针变量是指向函数的。

与数组名代表数组首元素地址相似,函数名代表该函数的入口地址,p指向函数开头。

判定指针变量是指向函数的:①首先,变量名前有“*”表明是指针变量而非普通变量;②其次,变量名后有( 形参类型 ),表明是指向函数的指针变量,这对()就是函数的特征。

8.5.2 用函数指针变量调用函数

图 60  例8.22(1)通过函数名调用函数


图 61  例8.22(2)通过指针变量访问它所指向的函数

8.5.3 怎样定义和使用指向函数的指针变量

类型名*指针变量名)(函数参数表列)注意这里的类型名是指函数返回值的类型

1)        并非意味着它能指向任何函数,只能指向在定义时指定的类型的函数,可先后指向同类型的不同函数;

2)        若用指针调用函数,必须先使指针变量指向该函数(max函数的入口地址赋给指针变量)

3)        给指针变量赋值时,只需给出函数名而不用给出参数,如p=max;

4)        函数指针变量调用函数时,只需将(*p)代替函数名即可,在(*p)后的()内根据需要写上实参,如c=(*p)(a,b);

5)        对指向函数的指针变量不能进行算术运算,如p+n,p++,p- -都是无意义的;

6)        用函数名调用函数,只能调用所指定的一个函数,而通过指针变量调用函数比较灵活,可以根据不同情况调用不同函数。


图 62  例8.23 输入两个整数,然后让用户选择1或2——调用max或min函数

 

8.5.4 用指向函数的指针作函数参数

指向函数的指针变量的一个重要用途是把函数的地址(入口地址)作为参数传递到其他函数。


图 63  例8.24 有a b,输入1,2或3输入1—求较大数,输入2—求较小数,输入3—求和

8.6  返回指针值的函数

即返回值的类型是指针类型,即地址。

定义返回指针值的函数:类型名 *函数名(参数表列)

int*a (int x,int y) ——a是函数名,调用后能得到一个int*型(指向整型数据)的指针,即整型数据的地址,xy是函数a的形参,为整型。*a两侧无括号(),表明此函数是指针型函数(函数值是指针)。

图 64  有a个学生,每个学生b门课成绩,输入学号,输出全部成绩(用指针函数实现)


图 65  对上例中的学生,输出有不及格课程的学生及其学号

8.7  指针数组和多重指针

8.7.1 什么是指针数组

由指针型数据组成的数组,每个元素存放一个地址,相当于一个指针变量。

类型名 *数组名[数组长度] 如int (*p) [4]


图 66  将若干字符串按字母顺序(由小到大)输出


图 67  上例print函数的另一种形式

8.7.2 指向指针数据的指针

如char **p,*运算符的结合性是从右到左,故相当于*(*p),可看作char *和(*p),前面的char *表示指向的是char*型数据,后面(*p)表示p是指针变量。

图 68  例8.29 指针数组,其元素分别指向一个整型数组的元素

利用指针变量访问另一个变量就是“间接访问”。若在一个指针变量中存放一个目标变量的地址,这就是“单级间址”,指向指针数据的指针用的是“二级间址”——可延伸到更多级,即“多重指针”。

8.7.3 指针数组作main函数的形参

main函数一般形式:

nt main() 或 intmain(void)——main函数无参数,调用main函数不必给出实参。

某些情况下,main函数可以有参数:

int main(int argc,char * argv[])      argumentcount参数个数,argument vector参数向量

……

8.8  动态内存分配与指向它的指针变量

8.8.1 什么是内存的动态分配

需要时随意开辟,不需要时随时释放。

8.8.2 怎样建立内存的动态分配

通过系统提供的库函数:

malloc函数

void *malloc(unsignedint size);

在内存的动态存储区分配一个长为size的连续空间

calloc函数

void*calloc(unsigned n, unsigned size);

在内存的动态存储区分配n个长为size的连续空间

free函数

void free(void*p);

释放指针变量p所指向的动态空间,是这部分能重新被其他变量使用

realloc函数

void *realloc(void*p, unsigned int size);

已使用malloc或calloc函数获得了动态空间,想改变其大小,可用realloc函数重新分配

8.8.3 void指针类型

基类型为void的指针变量,它不指向任何类型的数据。≠指向任何类型的数据,而是指向空类型/不指向确定的类型。


图 69  例8.30 建立动态数组输入5学生成绩,另外用一个函数检查有无<60分的,输出不合格的成绩

8.9 指针小结

1)        指针就是地址,指针变量是用来存放地址的变量。类型是没有值的,变量才有值。指针变量的值是一个地址,地址本身就是一个值;

2)        地址即意味着指向,通过地址能找到具有该地址的对象,对于指针变量来说,把谁的地址存放在指针变量中,就说此指针变量指向谁。但并非任何类型数据的地址都可以存放在同一个指针变量中,只有与指针变量的基类型相同的数据的地址才能存放在相应的指针变量中;

void*指针不指向任何类型的数据,若需用此地址指向某类型的数据,应先对地址进行类型转换;

3)        一维数组名代表数组首元素的地址;

4)        指针变量的类型及含义表;

5)        指针运算;

6)        指针变量可以有空值,即该指针变量不指向任何变量。


第9章 用户自己建立数据类型

9.1 定义和使用结构体变量

9.1.1 自己建立结构体类型

数组:只能存放同一类型的数据;

结构体:可存放不同类型的数据,在某些高级语言中称为“记录”。

struct 结构体名

         {成员表列}

其中struct是声明结构体类型必须使用的关键字,不能省略。对各成员都要进行类型声明:类型名 成员名;成员(域)可以属于另一个结构体类型。

9.1.2 定义结构体类型变量

形式1):先声明结构体类型,再定义该类型的变量

structStudent  student1,student2

形式2):在声明类型的同时定义变量

structStudent

{……} student1,student2 ;

形式3):不指定类型名而直接定义结构体类型变量

为struct

{成员表列} 变量名表列 ;

注意:结构体类型≠结构体变量,只能对变量赋值、存取或运算而不能对类型赋值、存取或运算,编译时对类型不分配存储空间,只对变量分配存储空间;

② 结构体类型中的成员名可以与程序中的变量名相同,但二者不代表同一对象;

③ 结构体变量中的成员(即“域”)可以单独使用,它的地位相当于普通变量。

9.1.3 结构体变量的初始化和引用(例子见书P297…)

1)        定义结构体变量时,可对其成员初始化,{ } 括起来,依次赋给各成员。注意是对结构体变量初始化而不是对结构体类型;C99允许对某一成员初始化,而未被指定初始化的数值型成员初始化为0,字符型成员’\0’,指针型成员NULL;

2)        可以引用结构体变量中的值:结构体变量名.成员名

可对变量的成员赋值,如student1.num=100; 因“.”是成员运算符,优先级最高,故可将student1.num作为一个整体相当于一个变量、student1.age++是对student1.age自加而不是对age自加

只能对结构体变量中的各个成员分别输入/输出,而不能printf(“%s\n”,student1)

3)        只能对最低级的成员进行赋值、存取及运算,如student1.birthday.month

4)        结构体变量的成员可像普通成员一样进行各种运算;

5)        同类结构体变量可互相赋值,如student1=student2(若同类型)

6)        可引用结构体变量成员的地址,也可引用结构体变量的地址

注意:scanf(“%d%s%d”,&student1.num,student1.name,&student1.score)name是数组名,本身就代表地址(在前有char name[20]),不能画蛇添足地再加一个&。——如int a[10],a代表的是这个数组的首地址,即a[0]的地址。

9.2  使用结构体数组

9.2.1 定义结构体数组

定义结构体数组一般形式:

struct 结构体名

{ 成员表列 } 数组名[数组长度] ;

②先定义一个结构体类型(如structPerson)再用此类型定义结构体数组

结构体类型 数组名[数组长度] ; 如structPerson leader[3];

初始化结构体数组形式是在定义数组后加上:

={ 初值表列 } ;  如struct Personleader[3]={{“Li”,0},{”Zhang”,0},{”Sun”,0}};

清晰起见,可将每个人的信息用{}包起来

9.2.2 结构体数组的应用举例(见书P301)

9.3  结构体指针

即指向结构体变量的指针,一个结构体变量的起始地址就是这个结构体变量的指针。若把一个结构体变量的起始地址存放在一个指针变量中,那么,这个指针变量就指向该结构体变量。注意:指针变量的基类型必须与结构体变量的类型相同

9.3.1 指向结构体变量的指针

注意:若p指向一个结构体变量stu,以下3种用法等价:

①stu.成员名(如stu.num)

②(*p).成员名(如(*p).num)

③p->成员名(如p->num)

9.3.2 指向结构体数组的指针

p定义了一个指向structStudent类型对象的指针变量,它用来指向一个struct Student类型的对象,不应用来指向stu数组元素中的某一成员。

9.3.3 用结构体变量和结构体变量的指针作函数参数

将一个结构体变量的值传递给另一个函数:

1)        用结构体变量的成员作参数

2)        用结构体变量作参数

3)        用指向结构体变量(或数组元素)的指针作实参,将结构体变量(或数组元素)的地址传给实参。


图 1  例9.7 n个结构体变量_求平均分最高的学生学号

9.4  用指针处理链表(详见书)

9.4.1 什么是链表

有的班100人,有的班50人,若用一个数组先后存放不同班级的学生数据,则必须定义长度为100的数组,显然会浪费内存——链表没有这种缺点,它会根据需要开辟内存单元,动态地进行存储分配。

链表的“头指针”变量head、“表尾”。

链表中各元素在内存中的地址可以是不连续的,链表如铁链,一环扣一环无间断。

链表必须利用指针变量,即一个结点包含一个指针变量,用它来存放下一结点的地址,

注意:只定义struct Student类型不会实际分配存储空间,只有定义了变量才分派存储空间

9.4.2 建立简单的静态链表

所有结点都是在程序中定义的,不是临时开辟的,也不能用完后释放。

9.4.3 建立动态链表

在程序执行过程中从无到有地建立起一个链表,即一个一个地开辟结点和输入各结点数据,并建立起前后相链的关系

9.4.4 输出链表

9.5  共用体类型

9.5.1 什么是共用体类型

使几个不同的变量共享同一段内存的结构,称为“共用体”类型的结构。(用一段内存单元存放不同类型的变量)

union  共用体名

{ 成员表列

} ; 变量表列;

9.5.2 引用共用体变量的方式

不能引用共用体变量,只能引用共用体变量中的成员。

9.5.3 共用体类型数据的特点

1)        共用体变量中只能存放一个值

2)        可以初始化共用体变量,但初始化列表中只能有一个常量

3)        起作用的是最后一次被赋值的成员(应十分注意当前存放的在共用体变量中的究竟是哪个成员的值);

4)        共用体变量的地址和它的各成员的地址都是同一地址

5)        不能对共用体变量名赋值,也不能企图引用变量名来得到一个值;

6)        C99允许同类型的共用体变量互相赋值,C99允许用共用体变量作为函数参数

7)        共用体类型可以出现在结构体类型定义中,也可以定义共用体数组;反之,结构体也可以出现在共用体类型定义中,数组也可以作为共用体成员。

9.6  使用枚举类型

若一个变量只有几种可能的值,则可以定义为枚举(enumeration)类型,所谓“枚举”即把可能的值一一列举,变量的值只限于列举出来的值的范围内。

enum [枚举名] {枚举元素列表}  其中枚举名遵循标识符的命名规则

1)        C编译对枚举类型的枚举元素按常量处理,故称枚举常量。不能因它们是标识符(有名字)而把它们看作变量,不能对它们赋值;

2)        每个枚举元素都代表一个整数,C编译按定义时的顺序默认它们的值为0,1,2,3,4,5…也可人为地指定枚举元素的值,C99把枚举类型作为整型数据的一种;

3)        枚举元素可以用来作判断比较。

9.7  用typeof声明新类型名

按定义变量的方式,把变量名换上新类型名,并且在最前面加上“typeof”,就声明了新类型名代表原来的类型。

1.        简单地用一个新的类型名代替原有类型名

2.        命名一个简单的类型名代替复杂的类型表示方法

如代表结构体类型/数组类型/指针类型/指向函数的指针类型

1)        实际是为特定的类型指定了一个同义字(synonyms);

2)        用typeof只是对已经存在的类型指定一个新的类型名,而没有创造新类型;

3)        用typeof声明数组类型、指针类型、结构体类型、共用体类型、枚举类型等,使得编译十分方便;

4)        typeof与#define看似相近,实际上#define在预编译时处理,只能作简单的字符串替换;而typeof在编译阶段处理,不止简单的字符串替换。

5)        可把所有typeof名称单独放在一个头文件中,在需要用到它们的文件中用#include指令把它们包含到文件中。

6)        使用typeof有助于程序的通用与移植。

第10章  对文件的输入输出

10.1  C文件的有关基本知识

文件名:即文件标识,包括①文件路径、②文件名主干、③文件后缀。

文件的分类:数据文件分为ASCII文件和二进制文件。数据在内存中以二进制形式存储,若不加转换地输出到外存,就是二进制文件,可认为它就是存储在内存的数据的映像,称为映像文件;若要就在外存上以ASCII码形式存储,则需要在存储前进行转换,ASCII文件又称文本文件(textfile),每一字节放一个字符的ASCII码。

文件缓冲区:系统自动地在内存区为程序中的每一个正在使用的文件开辟一个文件缓冲区(内存-缓冲区-磁盘)

文件类型指针,即“文件指针”:每个被使用的文件都在内存中开辟一个相应的文件信息区,用来存放文件的有关信息(如文件的名字、文件状态及文件当前位置等),这些信息被保存在一个结构体变量中,该结构体类型由系统声明,取名为FILE。声明结构体类型的信息包含在头文件“stdio.h”中。

指向文件的指针变量fp:FILE *fp 可使fp指向某一文件的文件信息区(是一个结构体变量),通过该文件信息区中的信息就能访问该文件,即通过文件指针变量能够找到与它关联的文件。(若有n个文件,应设n个FILE型变量,以实现对n个文件的访问)指向文件的指针变量并不是指向外部介质上的数据文件的开头,而是指向内存中文件信息区的开头。

10.2  打开与关闭文件

需要打开、关闭文件!打开——为文件建立相应的信息区(用来存放有关文件的信息)和文件缓冲区(用来暂时存放输入输出的数据),关闭——撤销文件信息区和文件缓冲区,使文件指针变量不再指向该文件,从而无法进行读写。若不关闭文件将会丢失数据:书P337

fopen(文件名,使用文件方式)     如fopen(‘a1’,’’r)

fclose(文件指针) ;    如fclose(fp)

注意:书P335-336的表格!

10.3  顺序读写数据文件

10.3.1 向文件读写一个字符

fgetc(fp):从fp指向的文件读入一个字符

fputc(ch,fp):将字符ch写到文件指针变量fp所指向的文件中

10.3.2向文件读写一个字符串

fgets(str,n,fp):从fp指向的文件中读入一个长为(n-1)的字符串,存放到字符数组str中

fputs(str,fp):将str所指向的字符串写到文件指针变量fp所指向的文件中

其中(n-1)是因为最后有个’\0’字符

10.3.3 用格式化的方式读写文件

fprintf(文件指针,格式字符串,输出表列);

fscanf(文件指针,格式字符串,输入表列);

10.3.3 用二进制的方式读写一组数据

fread(buffer,size,count,fp);

fwrite(buffer,size,count,fp);

10.4  随机读写数据文件

10.4.1 文件位置标记及其定位

rewind函数:使文件位置标记指向文件开头

fseek函数:改变文件位置标记

ftell函数:测定文件位置标记的当前位置

10.4.2 随机读写

10.5  文件读写的出错检测

ferror函数

clearerr函数





















0 0
原创粉丝点击