内容比较多。
大家可以复制做一个文档,然后用得时候直接搜索就好了,非常方便,不用网络也可以使用。
第二周的学习总结
1.对C语言的介绍与初步认识
C语言的产生与发展
FORTRAN(1957年)
ALGOL 60(1960年) 传统C或K&R C(1978)
CPL(1963年) ANSI C(1989,美国国家标准)
BCPL(1967年) 标准 C(ISO/IEC 9899:1990,俗称C90)
B(1969年-1970年) 最新标准 C(ISO/IEC 9899:1999俗称C99)
C(1971年-1973年)
1.2 C语言的语言特征
-
语言简洁紧凑
-
目标代码质量高
-
语言表达能力强
-
流程控制结构化
-
弱类型
-
“中级语言”特性
-
书写自由、使用灵活
-
可移植性好
1.3 计算机硬件系统
-
一台计算机硬件系统由CPU、内存、外存、其他I/O设备和总线组成。
-
CPU又称为中央处理器。 CPU又是由运算器、控制器、指令计数器、内部寄存器、标志寄存器等部件组成。
-
内存又称为内存储器,用以存放程序和数据。
-
总线是用以连接计算机各个部件的线路,它完成各个部件之间的信息传送。根据传送信号种类
-
的不同,总线又分成地址总线、数据总线和控制总线。
-
内存以字节为单位线性连续编址。即按照0x0000,0x0001,0x0002,…的方式;从低地址端开始向高地址端为每一个内存字节进行顺序连续编号。
-
1024个字节称为1K字节,1024K字节称为1M字节,1024M字节称为1G字节。1024G字节称为1T字节。
-
CPU数据总线的宽度(bit数)称为计算机的机器字长。对8位CPU,机器字长为1字节;对16位CPU,机器字长为2字节;对32位CPU,机器字长为4字节。
-
外存指计算机的外存储器。常用的硬盘、软盘、U盘、光盘、以及磁盘阵列都属于外存。外存存储的信息在断电之后仍然能够保存,这是外存的第一个特点。
-
其他I/O设备 :键盘 ,显示器 ,打印机 ,绘图仪
1.3.1 二进制
-
二进制数是由0和1组成的数字串。对于一个二进制数来讲,它具有两个下面基本特点:
-
(1) 只有0和1两个不同的数字符号。
-
(2) 逢2进位1。
1.3.2 八进制(O或o)
-
八进制数是由0到7组成的数字串。八进制数具有如下两个基本特点:
-
(1) 只有0,1,2,3,4,5,6,7八个不同的数字符号。
-
(2) 逢8进位1。
-
在C语言中,通过加前导零的方式来表示一个数是八进制数。如:0136表示的是八进制数(136)8。
1.3.3 十六进制(Ox或ox)
-
将4位二进制数组合成为十六进制数。与二进制、八进制数类似,十六进制数也有两个基本特点:
-
(1)只有0,1,2,3,4,5,6,7,8,9,A,B,C,D,E,F十六个不同的数字符号(A∼F也可以采用小写英文字母a∼f)。这十六个数字符号与二进制、八进制、十进制数之间的关系如表1.2所示。
-
(2)逢16进位1。
-
在C语言中,通过加前导0x或前导0X的方式来表示一个数是十六进制数。如:0x2d5b表示的是十六进制数(2d5b)16。0XABCD也是合法的十六进制数。
1.4.1 机器数与真值
-
机器数: 在计算机中,将一个数的最高位定义为符号位,其余各位为数值位。并且规定符号位之值为0表示正,符号位之值为1表示负。用这种方法表示的数称为机器数。
-
真值: 机器数的数值称为该机器数的真值。也就是正、负号后跟二进制数的绝对值就构成真值。
1.4.2 原码
定义: 对于一个二进制数X,如果规定用最高位为符号位,其余各位为该数的绝对值。并且规定符号位之值为0表示正,符号位之值为1表示负,则采用这种方式形成的二进制编码称为称为该二进制数X的原码。
值得注意的是,根据定义,真值0的原码表示不惟一,有+0和-0之分。
[+0]原=00000000 (8位)
[-0]原=10000000 (8位)
1.4.3 补码
定义:正数的补码等于正数的原码,负数的补码为其原码除符号位不动,其余各位变反再加1所得。
例1.13 分别求十进制数35和-1在8位机和16位机中的补码表示。
解 设x1=35,x2=-1;
在8位机中,∵x1>0,∴[x1] 补=[x1] 原 =00100011;
∵x2<0,∴[x2]原 =10000001,
[x2] 补 =11111110+1=11111111;
在例1.13中,如果令x1和x2都为零,则容易算出它们的补码都是零。也就是说,对于真值0,其补码是惟一的。即:
[+0]补=[-0]补=000…000
最右边式子中0的个数等于机器的字长数。
1.4.4 反码
定义:在计算机中,有时还会用到数的反码。对正数而言,其反码与原码、补码的表示相同;对负数而言,反码符号位的定义与原码、补码相同,但需要将对应原码的数值位按位变反。
例1.15 分别求十进制数6和-1在8位机中的反码表示。
解 [6]反=00000110;[-1]反=对10000001的数值位按位变反=11111110
2.数据类型、运算符和表达式
C程序是一个字符序列,字符序列先被分解为称之为记号(token)的词法元素,再根据语法规则检查这些记号组合是否合法。
C语言是一门大小写敏感的,抢类型的语言,随便写一个数它都是有类型固定的。
词法元素(记号)分析举例:
例2.1 sum=x+y
分解成sum、=、x、+和y 共5个记号。
例2.2 int a,b=10 ;
分解成int、a、,、b、=、10和 ; 共7个记号
例2.3 x+++++y
分解成x、++、++、+、y 共5个记号
(一个数值是不可以自增的!)
语法图是另一种表示文法的常见方式,对应于每个非终结符有一个子图,图中每条路径对应于该非终结符的一个生成式,路径上的是生成式右端的终结符和非终结符。终结符用弧形框表示,非终结符用矩形框表示。
通俗的说,椭圆形:定义的关键字,方括号:我们编写的
分隔符统称为空白字符(包括空格符、制表符、换行符、换页符及注释符),在语法上仅起分隔单词的作用。
当程序中两个相邻的单词之间如果不用分隔符就不能区分开时则必须加分隔符(通常用空格符)。
例如,int x,y;不能写成 intx,y;
能写成 int x , y ;
char的存储长度是一字节 。
多数系统中char与signed char同(-128~127). unsigned char (0~255).
字符数据以ASCII码存储在内存中 。
在不要求大整数的情况下,可用字符型代替整型 。
int型值存储在一个机器字中 .
int(负2的31次方~2的31次方-1),2B指2个字节长,B = bye(字节)
假设字长为2B,int取值范围为-32768~32767,unsigned取值范围为0 ~ 65535
指数区称为阶码;
阶码的+-分别用来表示正数与小数。
尾数所占的位数决定值的精度,指数所占的位数决定值的范围。
float占4字节,其中符号1b,指数8b,尾数23b,其精度大约为7位,范围约10-38~10+38。
double占8字节,其中符号1b,指数11b,尾数52b,其精度大约为15位,范围约为10-308~10+308。
很多编译器将long double处理为double,在某些系统中,它占用10或12B。很少被使用。
浮点数的表示可能只是近似的。其值与表示法之间的差称为“可表示误差”。
计算也可能造成可表示误差。不能使用==和!=运算符比较浮点数据。
可以用两个数值之差同一个预定的小正数epsilon比较的方法解决这个问题。
如: x==0 可以用 abs(x-0)<0.000001表示 下溢时,有些系统指数域全为0,尾数域非0,有些系统简单地用0表示。
上溢后,用称为“无穷大”的特殊位模式表示,即指数域全为1,尾数域0。有些系统中将输出+Infinity或-Infinity。
整型常量
有三种表示方法(通过前缀字符区分):
十进制:无前缀
八进制:前缀为0
十六进制:前缀为0x或0X时。
例如,31可写成037,也可写成0x1f或0X1F
整型常量可以带有后缀,用以指定其类型:
字母u或U表示unsigned
字母l或L表示long
字母ul或UL
表示unsigned long
字母ll或LL表示long long (C99)
字母ull或ULL表示unsigned long long (C99)
无后缀时,表示int
当常量值超出指定类型的范围时,其实际类型取决于数值大小、前缀等,确定类型的规则很复杂,在标准化前的C语言、C89和C99中各不相同
转义序列
有些以\开头的特殊字符称为转义序列
转义序列有两种形式,一种是“字符转义序列”,即反斜线后面跟一个图形符号,用于表示字符集中的非图形符号和一些特殊的图形字符。
\n 换行 \t 水平制表符
\\ 反斜杠 \‘ 单引号
\“ 双引号 \0 空字符
\? 问号
\t 打印八个字节,用于对齐和补齐。
100%打印时应该再加一个%为100%%
写成用一对双引号括住0至多个字符的形式。
"string\n" /* 包含7个字符的字符串 */
"" /* 包含0个字符的空字符串*/
字符串中的单引号可以用图形符号表示,但双引号和反斜线必须用转义序列表示。例如:
"3'40\"" /* 表示5个字符的字符串:3'40" */
“c:\tc” /* 表示4个字符的字符串 */
“c:\\tc” /* 表示5个字符的字符串 */
如何将一个较长的字符串写成多行?有两种方法:
(1)行连接:在前一行的末尾输入续行符(\) 再换行。
"Hello,\
how are you " /* 换行后应紧靠行首 */
(2)字符串连接:将字符串分段,分段后的每个字符串用双引号括起来。
"Hello, "
"how are you" /* 换行后不必紧靠行首*/
‘a’与 “a”有何区别 ?
‘a’ : 字符常量,占1 B内存空间
“a” : 字符串常量,占2B内存空间
“a”存储时,系统自动在后面补上\0( 空字符,ASC11值为0,作为字符串结束标志)
字符串的存储长度比字符串的实际长度大1
“a”2个字符
“a\n”3个字符
字符串长度比实际长度小于1
strlen查看字符串长度
用一个标识符表示一个常量.
C语言中有三种定义符号常量的方法:
(1) 用#define指令(预处理指令,宏命令)
(2) 用const声明语句
(3) 用枚举类型
const是关键字,称为类型限定符。格式为:
const 类型名 标识符=常量;
例如:
const double PI=3.14159;
const int DOWN=0x5000;/* 下光标键的扫描码 */
const int YES=1,NO=0;
const声明的标识符是一个只读变量,编译时系统会根据定义的类型为该标识符分配存储单元,并把对应的常量值放入其中,该值不能再被更改,此后,程序中每次出现该标识符都是对所代表存储单元的访问。
#define定义的标识符没有对应的存储单元,只是在编译之前由预处理程序进行简单的文本替换。
变量代表内存中具有特定属性的一个存储单元,它用来存放数据,这就是变量的值,在程序运行期间,这些值是可以改变的。
要求对所有用到的变量作定义,也就是“先定义,后使用” 。
形式: 类型名 变量表;
如: int total,average;
变量在声明时可以同时赋一个初值(称为变量的显示初始化),每个变量必须分别显示初始化。
如:int count=0,sum=0;
char alert=‘\a’, c ;
int count=0,sum=0;(不能 int count=sum=0;)
3.流程控制
语句,表达式语句,复合语句。
复合语句在程序设计中主要有以下两种用途:
1)用于语法上只允许出现单个语句而处理上需要执行多个语句的地方,例如作为if语句的子句及循环语句的循环体。
(2)用于改变嵌套if-else语句的配对规则。
此外,当需要说明临时使用的局部变量时,也可使用复合语句。
if语句有两种形式:
(1) if格式:if (表达式)语句1;
(2) if-else格式:if (表达式)语句1;else语句2;
嵌套的if 语句
当if子句或else子句中又包含if 语句时,则形成嵌套的if语句。例如,可以用下面的一个嵌套的if语句求a,b,c三个数中最大值:
if ( a > b )
if ( a > c ) max = a;
else max = c;
else
if ( b > c ) max = b;
else max = c;
嵌套if 语句中else的配对规则
对嵌套if语句中else与if的配对必须制定一个规则,否则会造成理解上的二义性。例如:
if ( n > 0 )
if ( a > b ) z = a;
else z = b;
编译程序约定:else与其前面最靠近的还未配对的if配对,即内层优先配对原则。
switch语句的一般形式为:
switch(表达式){
case 常量表达式1:语句序列1;
case 常量表达式2:语句序列2;
…
case 常量表达式n:语句序列n;
default:语句序列n+1;
}
switch语句的使用要点
第一要注意列出的case应能包括选择表达式所有的取值情况,如果不能全部包括,则应使用default子句处理余下的情况。
第二应特别注意break在switch中的作用,如果希望执行完某一case下的语句之后便跳出switch语句,则必须使用break或return转移语句。break跳出switch语句之后继续执行switch语句后面的一个语句(如果有),return语句则立即结束函数并返回到调用处(如果是主函数,则结束程序)。
例如:下面是一个不含转移语句的switch语句,注意观察该语句执行时的输出。
i = 1;
switch ( i ){
case 0:printf("%d\t", i);
case 1:printf("%d\t", i++);
case 2:printf("%d\t", i++);
case 3:printf("%d", i++);
default:printf("\n");
}
printf("%d\n", i);
输出:1 23
4
while语句的一般形式为:
while (表达式) 语句
while语句流程图:
for语句的形式
for语句的一般语法形式表示为:
for语句流程图:
for(e1;e2;e3) s
等价于:
e1;
while(e2) {
s;
e3;
}
使用for语句时须注意表达式e1,e2,e3的用法:
(1)三个表达式可以全部或部分缺省,但无论缺省e1,e2或e3,它们之间的分号不能省。
(2)缺省e1和e3时的for语句形如for(;e2;)s,等价于一个形如while (e2) s的while语句。
(3)缺省e2时的for语句for(e1 ; ; e3) s和三个表达式都缺省的for语句for(; ;)s都是无限循环语句。被省略的e2缺省值恒为非0(e1和e3没有缺省值)。
for循环语句示例:
int i;
for(i = 1; i < 4; i++)
printf(”i=%d s=%d\n”, i, 2 * i);
几种等价的形式:
i = 1;
for(; i < 4; i++)
printf(”i=%d s=%d\n”, i, 2 * i);
或
for( i = 1; i < 4;){
printf(”i=%d s=%d\n”, i, 2 * i);
i++;
}
或
i = 1;
for(; i < 4;){
printf(”i=%d s=%d\n”, i, 2 * i);
i++;
}
或
i = 1;
for(; ;){
printf(”i=%d s=%d\n”, i, 2 * i);
i++;
if (!(i < 4)) break;
}
do-while语句的一般形式为:
do 语句 while (表达式);
do-while语句流程图:
可以用以下等价的while循环语句来代替。
语句
while (表达式)
{
语句
}
三种循环语句的区别及使用要点归纳如下(s是循环体;e,e1,e2,e3是表达式):
(1)while(e)s和for(e1;e2;e3)s先测试e或e2,后执行s,若第一次测试时e或e2结果为0,则s一次也不执行;do s while(e);先执行s,后测试e,所以s总是至少被执行一次。使用时应根据具体情况选用,一般说来,必定要执行的循环可以用三种循环语句中任何一种;可能不被执行的循环则不能用do-while。
(2)第一次测试循环条件(e或e2)之前,循环变量必须赋初值,初值只赋一次;在循环体(s)或e3(对于for语句)中必须有能够改变循环变量值的语句或表达式。写循环条件时,应注意避免无限循环、永不执行的循环或执行次数不正确的循环等情况。
(3)for语句控制部分的e1可以包含给循环变量赋初值以及其他与循环有关的运算,即在循环开始之前仅执行一次的运算;e2不要求一定是关系表达式或逻辑表达式,只要能正确控制循环体的执行(非0值执行循环体,0值结束循环),任何表达式都可以;e3是每次执行循环体后紧接着要执行的表达式,通常用于改变循环变量的值,如i++之类,e3也可以包括某些属于循环体部分的内容,也可将e3放到循环体最后。可见,for语句使用非常灵活,其控制部分的三个表达式可以容纳除循环变量赋初值、测试循环条件和修改循环变量值的运算以外的其他与循环有关的运算。
(4)任何循环语句当循环体含有一个以上语句时,必须写成复合语句(用{ }括起来);当循环体为空语句时不要掉了分号(;)。
goto语句又称为无条件转移语句,它的一般形式为:
goto 标号;
任何可执行C语句都可以加标号前缀成为标号语句。
标号语句的形式为:
标号:语句
goto语句中的标号是对标号的引用,标号语句中的标号是对标号的定义。
被goto语句引用的标号必须有且仅有一个对应的标号语句,对应的标号语句称为称为该goto语句的
目标语句;而允许标号语句没有对应的goto语句。
概而言之,有标号的引用必须有惟一的标号定义,而有标号的定义不必有标号的引用。
goto语句的目标语句允许出现的范围称为标号的作用域。
C语言中标号的作用域是goto语句所在的函数,即goto语句不能从一个函数转移到另一个函数中,但可以在一个函数内从嵌套结构的内层直接转到最外层。
使用标号语句时,要注意同一函数内的标号不能同名。
goto语句和标号语句在函数中出现的先后位置没有约束,即对标号的定义和对标号的引用没有先后次序的规定。
注意:goto语句不是必需的语言成分。因为用goto语句实现的任何控制转移,都可以通过循环语句、if语句和其他转移语句的适当配合以及用整型变量标记状态的方法实现。
goto语句的惟一好处是可以从嵌套结构的最内层(switch语句或循环语句)直接转到最外层(隔层转移),用起来较方便.
但如果随意地使用goto语句则会破坏程序的结构化特性,使程序的逻辑结构不清,因此应尽量少用或不用goto语句。
break语句的形式为:
break;
break是关键字。break语句有以下两种用途:
(1)用于switch语句中,从中途退出switch语句;
(2)用于循环语句中,从循环体内直接退出当前循环。
注意:对于嵌套的循环语句和switch语句,break语句的执行只能退出直接包含break的那一层结构。
continue语句的形式为:
continue;(continue是关键字)
continue语句只能出现在循环语句中,用于终止循环体的本次执行(并非退出循环语句);即在循环体的本次执行中,跳过从continue语句之后直到循环体结束的所有语句,控制转移到循环体的末尾。
对于while (e) s;和do s while(e);,在执行continue语句之后马上执行对循环控制表达式(e)的计算和测试;
对于for (e1;e2;e3) s则马上执行表达式e3,然后执行对表达式e2的计算和测试。
return语句有下面两种形式:
(1)不带表达式的return语句:
return;
(2)带表达式的return语句:
return 表达式;
return语句的功能是从被调用函数返回到调用函数。
不带表达式的return语句只能返回控制、不能返回值,因此只能用于从无返回值的函数中返回。
带表达式的return语句(表达式可以用()括起来)在返回控制的同时,将表达式的值返回到调用处,函数调用表达式的值就是这个返回值。
多重循环语句的使用要点
(1)对于多重循环,特别要注意给与循环有关的变量赋初值的位置。以程序4.31为例:只需赋一次初值的操作应放在最外层循环开始执行之前,例如,赋值表达式s=0位于外循环for语句控制部分的表达式1;给内循环的有关变量赋初值应放在外循环体内、内循环开始执行之前,例如,赋值表达式语句term=1;和j=1;位于内循环do-while语句的前面,是外循环for语句循环体的一部分。
(2)内、外循环变量不应同名,否则,将造成循环控制混乱,导致死循环或计算结果错误。
(3)应正确书写内、外循环的循环体:需要在内循环中执行的所有语句必须用{}括起来组成复合语句作为内循环体;属于外循环的语句应放在内循环体之外、外循环体之中。例如,程序4.31的赋值语句term=1;j=1;和sum+=term;都是组成外循环体的语句,其中term=1;和j=1;位于内循环do-while语句之前,sum+=term;语句位于do-while语句之后,它们均位于内循环体之外。
(4)不应在循环中执行的操作应放在进入最外层循环之前或最外层循环结束之后。例如,程序4.31中对输入提示(input n:)的输出及读入项数(n)是在程序运行过程中仅需执行一次的操作,且需在循环开始之前执行;最后输出整个计算结果(sum)只需执行一次,且应在循环结束之后执行。
4.函数与程序结构
结构化编程是一种解决问题的策略,它包括如下2条编程标准:
(1) 程序中的控制流应该尽可能简单。
(2) 应该自顶向下地设计程序结构。
自顶向下设计也称为逐步细化,即把一个问题按功能分解为若干子问题,如果子问题还较复杂,可将其继续分解,直到分解成为容易求解的子问题为止。分解而来的每个子问题被称为模块,C中提供的函数机制完成每个模块的编程任务,即用函数编写由分解而来的子问题的代码。
自顶向下的分解问题:
(1) 显示标题
(2) 显示表头
(3) 分列整齐地显示整数1到10的2至5次幂
每个子问题都能作为函数直接被编写成代码,
在main中调用这些函数,解决总的问题。
-
使程序编制方便,易于管理、修改和调试。
-
增强了程序的可读性、可维护性和可扩充性,方便于多人分工合作完成程序的编制。
-
函数可以公用,避免在程序中使用重复的代码。
-
提高软件的可重用性,软件的可重用性是转向面向对象程序设计的重要因素。
C程序的一般结构
C程序由一或多个函数组成,这些函数可以编辑成多个C源文件,每一个C源文件含有一个或多个函数定义。
各C源文件中要用到的一些外部变量说明、枚举类型声明、结构类型声明、函数原型和编译预处理指令等可编辑成一个.h头文件,然后在每个C文件中包含该头文件。
每个源文件可单独编译生成目标文件,组成一个C程序的所有源文件都被编译之后,由连接程序将各目标文件中的目标函数和系统标准函数库的函数装配成一个可执行C程序。
除main以外的其它函数分两类,一类是由系统提供的标准函数。另一类是需要由程序员自己编写的函数(“自定义函数”)。
函数的定义与函数的声明
程序中若要使用自定义函数实现所需的功能,需要做三件事:
① 按语法规则编写完成指定任务的函数,即定义函数;
② 有些情况下在调用函数之前要进行函数声明;
③ 在需要使用函数时调用函数
函数定义的一般形式为:
类型名 函数名(参数列表)
{
声明部分
语句部分
}
-
无返回值,类型名用void。
-
无参数,参数列表void(可缺省不写)
未指明函数返回值的类型,默认为 int
必须明确地列出每一个参数的类型。
函数定义中的参数称为形式参数(形参)
return语句可以是如下两种形式之一:
(1)return ; /* void函数 */
(2)return 表达式 ; /* 非void函数 */
void函数也可以不包含return语句。如果没有return语句,当执行到函数结束的右花括号时,控制返回到调用处,把这种情况称为离开结束。
表达式值的类型应该与函数定义的返回值类型一致,如果不相同,就把表达式值的类型自动转换为函数返回值的类型 。
函数返回的值,程序可以使用它,也可以不使用它
while(…) {
getchar(); /* 返回值不被使用 */
c=getchar(); /* 返回值被使用 */
…
}
一旦return被执行,控制立即返回到调用处,其后的代码不可能被执行
/* is_prime:如果n是素数,则返回1;否则,返回0 */
int is_prime(int n) /* 等价于 is_prime(int n) */
{
int k, limit;
if (n==2) return 1;
if (!(n % 2)) return 0;
limit=n/2 ;
for (k=3; k<=limit; k+=2)
if ( !(n % k) ) return 0;
return 1;
}
注意:函数定义起函数声明的作用 ,函数定义在前,调用函数在后。
例:
#include<stdio.h>
void prn_banner(void)
{ … }
void prn_headings(void)
{ … }
double power(int x,int n)
{ … }
void main(void)
{ … }
函数原型
函数定义出现在函数调用后,被调用函数在其它文件中定义,必须在函数调用之前给出函数原型。
函数原型的一般形式
类型名 函数名(参数类型表);
参数类型表通常是用逗号隔开的形参类型列表,而形参名可以省略。例如,
double power(int, int);
等价于
double power(int x, int n);
无参函数的函数原型参数表必须指定为void
函数原型的作用
函数原型告诉编译器函数返回值的数据类型,传递给函数的参数个数、参数类型和参数顺序。编译器用函数原型校验函数调用, 强制转换传递的参数类型,从而避免错误的函数调用导致的致命运行错误或微妙而难以检测的非致命逻辑错误。例如,
printf ("%.1f \n", sqrt(4) ) ;
(1)包含了math.h,输出 2.0 */
(2)没有包含math.h,函数调用sqrt(4)不会产生正确的值。
遗漏的函数原型
编译器首次遇到没有声明的函数调用时,将构造一个缺省声明并继续编译:
int f(); /*函数是int类型,但不给出参数表信息*/
(1) 函数类型不是int,给出一个错误提示:
Type mismatch in redeclaration of 'f‘
(2) 函数类型是int,就不给出错误提示。编译器对参数类型不作检查,把正确类型的参数传递给函数是程序员的责任。假设函数f只有一个类型为int的参数,函数调用f (2)会产生正确的值,而函数调用f(2.5)将产生错误的运行结果。
函数调用的形式
函数名(实参列表)
实参是一个表达式 ,对于无函数,调用形式:
函数名()
函数调用在程序中出现的形式:
(1) 作为表达式语句出现。
prn_headings();
putchar(c);
(2)作为表达式中的一个操作数出现。例如:
c=getchar()
sum += power(i,j);
(3)作为函数调用的一个实参出现,即嵌套调用。
printf("%10.0f", power(i , j) );
putchar ( getchar() );
实参的求值顺序:
实参的求值顺序由具体实现确定,有的按从左至右的顺序计算,有的按从右至左的顺序计算
a=1;
max (a,a++)
----从左至右:max(1,1)
----从右至左:max(2,1) (多数 )
为了保证程序清晰、可移植,应避免使用会引起副作用的实参表达式 .
实参的求值顺序C语言采用从右至左规则
例:
#include "stdio.h"
void main(void)
{
int x=1;
printf("%d\t%d\t%d\n",x,x++,x);
}
运行结果:
2 1 1
参数的传递方式是“值传递”,实参的值单向传递给相应的形参。
如果实参、形参都是x,被调用函数不能改变实参x的值。
例 说明值传递概念的程序
#include<stdio.h>
long fac(int);
void main()
{ int n=4;
printf("%d\n",n); /* 输出4 */
printf("%ld\n",fac(n)); /* 输出24 */
printf("%d\n",n); /* 输出4 */
}
long fac(int n) /* 计算n的阶乘 */
{ long f=1;
for( ;n>0;--n) f *=n ; /* main中的n值未改变 */
printf("%d\n",n); /* 输出0 */
return(f);
}
作用域:程序正文中可以使用该标识符的那部分程序段。根据作用域,变量有两类:
局部变量:在函数内部定义的变量,作用域是定义该变量的程序块,程序块是带有说明的复合语句(包括函数体)。不同函数可同名,同一函数内不同程序块可同名。形式参数是局部变量。
外部变量:在函数外部定义的变量,其作用域从其定义处开始一直到其所在文件的末尾,可由程序中的部分或所有函数共享。
局部变量是指大括号里面定义的变量
关键字extern的作用
int sp=0;/*sp是外部变量 ,main,push和pop均可访问 */
void main() {…} /* val不能用在main中 */
double val[MAXVAL]; /* val是外部数组 */
void push(double f ) {…}
double pop(void) {…}
-
利用extern对外部变量进行声明可以扩大其作用域,使得外部变量在定义之前可以使用,以及其它源文件中的函数也可以使用。例如
void main()
{ extern double val[MAXVAL];
……
}
外部变量的声明和定义的区别
外部变量的声明用于通报变量的类型(引用性声明)
外部变量的定义除此以外还引起存储分配(定义性声明)
int sp;
这是外部变量的定义,一方面说明了sp类型为int,另一方面系统还要为其分配2B的存储单元。而
extern int sp ;
这是外部变量的声明,仅仅说明了sp类型为int,不会为其分配存储单元。
外部变量只能定义一次,而外部变量的声明可以出现多次。外部变量的初始化只能出现在其定义中。
存储类型
变量和函数都有两个属性:数据类型和存储类型。
函数的存储类型决定函数的作用域,可使用的关键字有extern和static(5.9节)。
变量的存储类型决定变量的作用域、存储分配方式、生命周期和初始化方式,可使用的关键字有:auto、extern、static和register。
存储分配方式:在何时、何处给变量分配存储单元;
生命周期:变量在内存中的存在期;
初始化方式:在定义变量时如果未显示初始化,是否有缺省初值,如果有显示初始化,赋初值操作如何执行(执行一次还是执行多次)。
局部变量的缺省存储类型是auto,称自动变量
auto int a; /*等价于int a; 也等价于auto a; */
作用域:局部于定义它的块,从块内定义之后直到该块结束有效。
存储分配方式:动态分配方式,即在程序运行过程中分配和回收存储单元。
生命周期:短暂的,只存在于该块的执行期间。
初始化方式:定义时没有显示初始化,其初值是不确定的;有显示初始化,则每次进入块时都要执行一次赋初值操作。
例 块多重嵌套时自动变量的作用域
#include<stdio.h>
void main(void)
{
int a=2,b=4;
printf("a=%d,b=%d\n",a,b);
{
int a;
a=3;b=5;
printf("a=%d,b=%d\n",a,b);
}
printf("a=%d,b=%d\n",a,b);
}//b是同一个内存,a里面重新定义了一个,所以操作之后不会影响原来的a值
输出:
a=2,b=4
a=3,b=5
a=2,b=5
内外层有同名变量时,外层变量不可见.
存储类型extern
外部变量的存储类型是extern,但在定义时(定义性声明)不使用关键字extern。
外部变量的作用域:从定义之后直到该源文件结束的所有函数,通过用extern做声明,外部变量的作
用域可以扩大到整个程序的所有文件。
存储分配方式:静态分配方式,即程序运行之前,系统就为外部变量在静态区分配存储单元,整个程序
运行结束后所占用的存储单元才被收回。
生命周期:永久的,存在于整个程序的执行期间。
初始化方式:定义时没有显示初始化,初值0;有显示初始化,只执行一次赋初值操作。
例 说明外部变量的缺省初值和作用域
#include<stdio.h>
int n; /* 外部变量,等价于int n =0 */
int f1(void )
{ n++; return n; }
void f2(int x)
{ n=x; }
void main(void)
{ int i;
printf("global n is %d on entering main \n",n);
f2(10);
for (i=0;i<3;i++) printf("%6d",f1());
printf("\nglobal n is %d on exiting main \n",n);
}
global n is 0 on entering main
11 12 13
global n is 13 on exiting main
例 关键字extern的用法
文件file1.c的内容为:
#include<stdio.h>
int n; /* 定义外部变量 */
int f1(void); /* f1的函数原型 */
void f2(int); /* f2的函数原型 */
void main(void)
{
int i;
printf("global n is %d on entering main \n",n);
f2(10);
for (i=0;i<3;i++) printf("%6d",f1());
printf("\nglobal n is %d on exiting main \n",n);
}
文件file2.c的内容为
extern int n; /* 外部变量n的声明 */
int f1(void)
{
n++;
return n;
}
void f2(int x)
{
n=x;
}
存储类型static
关键字static有两个重要而截然不同的用法:
(1) 用于定义局部变量,称为静态局部变量。
(2) 用于定义外部变量,称为静态外部变量。
存储分配方式:静态分配方式
生命周期: 永久的,
缺省初值: 0,只执行一次
静态局部变量和自动变量有根本性的区别。
静态外部变量和外部变量区别:作用域不同。
静态局部变量
作用域: 和自动变量一样
静态局部变量的值有连续性. 输出:1!=1
#include<stdio.h> 2!=2
long fac(int); /* 函数原型 */ 3!=6
void main(void) 4!=24
{ int i;
for(i=1;i<5;i++)
printf("%d != %ld\n", i, fac(i));
}
long fac(int n)
{ static long f=1;/* 静态局部变量只初始化1次 */
f *=n; return f;
}
将f定义为自动变量,输出结果如何?
#include<stdio.h>
long fac(int); /* 函数原型 */
void main(void)
{ int i;
for(i=1;i<5;i++)
printf("%d != %ld\n", i, fac(i));
}
long fac(int n)
{ long f=1; /* 局部变量 */
f *=n; return f;
}
输出: 1!=1
2!=2
3!=3
4!=4
静态外部变量
静态外部变量和外部变量的唯一区别是作用域的限制。
静态外部变量只能作用于定义它的文件,其它文件中的函数不能使用,
外部变量用extern声明后可以作用于其它文件。
使用静态外部变量的好处在于:当多人分别编写一个程序的不同文件时,可以按照需要命名变量而不必考虑是否会与其它文件中的变量同名,保证文件的独立性。
printf和scanf就是典型的参数数目可变的函数。
int printf(const char *format,…);
函数调用时,实参可以有1至多个。
定义参数数目可变的函数,要用到可变参数头文件stdarg.h中定义的宏和类型。
递归函数与递归调用
#include<stdio.h>
void prn_int(int n)
{ if (n>0) {
printf ("%d ",n);
prn_int(n-1); /* 递归调用 */
}
}
void main(void)
{
prn_int(10);
}
递归调用:函数直接调用自己或通过另一函数间接调用自己的函数调用方式
递归函数 :函数定义中含有递归调用的函数
递归的两个要素:
(1)递归结束条件。也就是所描述问题的最简单情况,递归如果没有结束条件,递归过程将永不终止,即无穷递归(和无限循环类似)。
(2)递归定义。为了描述问题的某一状态,必须用到它的上一状态,而描述上一状态,又必须用到它的上一状态,……,这种用自已来定义自己的方法,称为递归定义。递归定义必须能使问题越来越简单,使问题向结束条件转化。
递归算法解决三类问题
(1)数据的定义形式按递归定义。例如,
n! =n*(n-1)! ; 0! =1。
f(n)=f(n-1)+f(n-2);
f(1)=1; f(2)=1。
(2)数据的结构形式是按递归定义的。如
树的遍历,图的搜索等。
(3)问题解法按递归算法实现。例如回溯法
以求n!为例
long fac(int n)
{
if (n==0||==1) return 1;
else return (n*fac(n-1));
}
简化算法;
不节省存储空间,运行效率也不高
静态函数和外部函数
对于多文件C程序,需正确使用变量和函数的存储类型。函数的存储类型决定了函数的作用域,分为静态函数和外部函数。
外部函数:存储类型是extern。既可被本文件中的函数调用,也可被其它文件中的函数调用。函数定义时一般省略extern,在其它需要调用它的文件中,一般用extern声明。例如:
extern double power(int,int);/* power的函数原型*/
double power(int x,int n) /* power是外部函数*/
{…}
静态函数:存储类型是static,作用域从定义之后直至该文件结束,通过写函数原型,可将作用域扩大到定义之前,但只限制在定义它的文件中。
double power(int,int ); /* 函数原型 */
void main(void)
{
… /* 函数power 可被调用,但在其它文件不能被调用 */
}
static double power(int x,int n); /* 静态函数 */
{ … }
和静态外部变量一样,静态函数也只作用于所在文件,不同文件中的静态函数可以同名,保证文件的独立性。
5.编译预处理
编译预处理:对源程序进行编译之前所作的工作,它由预处理程序负责完成。编译时,系统将自动引用预处理程序对源程序中的预处理指令进行处理。
程序员通过编译预处理命令规定编译器在编译前所作的工作。
预处理指令:以“#”号开始的指令。
文件包含#include
功能:用指定文件的内容取代该预处理指令行,有2种形式:
(1) #include <文件名>
在指定的标准目录下寻找被包含文件
(2) #include "文件名"
首先在用户当前目录中寻找被包含文件,
若找不到,再在指定的标准目录下寻找
如:#include “stdio.h”
#include “math.h”
宏定义#define
功能:用一个标识符来表示一个字符串.一般形式为:
#define 标识符 字符串
宏名:被定义的标识符。宏代换(宏展开):在编译预处理时,用字符串去取代宏名
预处理前
#define M (y*y+3*y )
void main(void)
{ int s,y;
printf("Input a number: ");
scanf("%d",&y);
s=3*M+4*M+y*M;
printf("s=%d\n",s);
}
预处理后
void main(void)
{ int s,y;
printf("Input a number: ");
scanf("%d",&y);
s=3* (y*y+3*y ) +4*(y*y+3*y )
+y* (y*y+3*y );
printf("s=%d\n",s);
}
宏调用:给出实参
宏展开:(1)用字符串替换宏,
(2)用实参去替换形参
例 定义计算x2的宏
#define SQ(x) ((x)*(x))
X:形式参数
宏调用:SQ(a+1) /*a+1为实参*/
宏展开: ((a+1) * (a+1))
实际上是用((x)*(x))代替SQ(x),用实参a+1代替形参X。
宏调用:SQ(SQ(a))
宏展开:( (((a)*(a))) * (((a)*(a))) )
为什么要这么多的括号?
考虑 :#define SQ(x) x*x
宏调用: SQ(a+b)
宏展开:a+b*a+b /*与(a+b)*(a+b)不同 */
再考虑 :#define SQ(x) (x)*(x)
宏调用:27/SQ(3)
宏展开:27/(3)*(3) /*值27, 与 27/32不同 */
定义带参数的宏时,为了保证计算次序的正确性,表达式中的每个参数用括号括起来,整个表达式也用括号括起来。
注意:宏名和与左括号之间不能有空格
#define SQ (x) ((x)*(x))
被认为是无参宏定义 。
宏调用:SQ(3)
宏展开:(x) ((x)*(x)) (3) /*显然错误的*/
带参的宏虽被认为不安全,但还是很有价值
#define SQ(x) ((x)*(x))
宏调用:SQ(++a)
宏展开: ((++a)*(++a)) /*a加2次
如是函数调用,将不会有问题 */
宏节省了函数调用的开销,程序运行速度更快,形式参数不分配内存单元,不必作类型说明。但是,宏展开后使源程序增长。
宏比较适合于经常使用的简短表达式,以及小的可重复的代码段;当任务比较复杂,需要多行代码才能实现时,或者要求程序越小越好时,就应该使用函数。
终止宏名的作用域,其形式为:
#undef 标识符
何时使用#undef指令?
防止宏名的冲突
#include "everything.h"
#undef SIZE /*everything.h中定义了SIZE,就取消它;
否则该指令不起作用*/
#define SIZE 100
保证调用的是一个实际函数而不是宏
#undef getchar
int getchar(void) {…}
条件编译:在预处理中进行条件控制,根据所求条件的值有选择地包含不同的程序部分,因而产生不同的目标代码。 这对于程序的移植和调试是很有用的 。
条件编译指令三种形式,控制流与if-else语句类似。
在头文件assert.h中,用来测试表达式的值是否符合要求,其形式如下:
assert(condition)
如果condition值非0,程序继续执行下一个语句。如果condition值0,就输出错误信息,并通过调用实用库中的函数abort终止程序的执行。
assert (x<=10);
如果x大于10,就会输出如下包含行号和文件名的错误信息并中断执行:
Assertion failed:x<= 0,file test.c,line 12
对于大多数编译器来说,在头文件assert.h的assert宏定义中,如果定义了符号常量NDEBUG,其后的assert将被忽略。因此,如果不再需要assert,那么可把代码行
#define NDEBUG
插入到程序中,而无需手工删除assert。
6.数组
基本概念
-
一维数组的声明、初始化和使用
-
数组的运算、作为函数参数的使用
-
字符串、多维数组
同时包括:
-
字符串操作函数,数字串与数之间的转换函数,二分查找,排序等程序的设计.
-
相关算法和程序设计方法要熟练掌握.
-
程序=算法+数据结构
-
PASCAL程序设计语言发明者Niklaus Wirth曾经说过
-
简单数据类型的变量
-
仅能描述一个单独的数据
-
对客观对象的描述能力十分有限
-
如何描述一群有联系的数据集合?(全班C语言考试成绩)
-
数组
(不同数据类型数据如何考虑? 如学生对象有学号,姓名性别,年龄,各科成绩等属性)
-
元素
-
组成数组的这些数据
-
任何类型(简单类型、构造类型)
-
数组特点
-
其所有元素数目固定
-
其所有元素类型相同
-
其所有元素顺序存放 (在内存中也是连续存放的)
-
数组作用
-
集中管理
-
元素顺序存放,但可随机定位
-
例如定义float score[30],可表述30位学生成绩
-
用数组具有什么好处?
-
问题
-
解决方法
-
设置30个float型变量来记录成绩
-
设置一个有30个float型元素的数组来记录成绩
-
问题分析
-
参与运算的平均成绩,其数据类型都相同(符合数组特点)
-
30位同学属于一个班,用数组可把30个成绩表示成一个整体
-
用数组的优点
-
便于循环处理
-
提高效率,便于书写、检查、修改(对海量数据效果更明显)
-
要解决三个问题
-
确定数组的数据类型
-
给数组定义一个名字,以便在程序中使用
-
指明数组的大小,即数组中元素的个数
-
声明形式
-
[存储类型说明符] [类型修饰符] 类型说明符 数组名[常量表达式]={初值表};
-
存储类型说明符:extern、static
-
类型修饰符:const、volatile
-
类型说明符: int 、 char 、 …
-
数组名:是一个标识符,是一个地址常量,用以表示数组中打头元素的地址
-
例: 具有基本数据类型的一维数组的声明
-
#define SIZE 10
-
int array[5];
-
double d[5],e[SIZE];
-
char name[SIZE*5];
-
错误例子
-
unsigned int size;
-
char str[size],buffer[2*size];
-
错误原因
-
数组的大小一经说明就不能改变
-
长度说明不是表达式,在编译之前就必须明确确定
例: 采用类型修饰符的一维数组的声明
-
static int y[10];
-
extern double s[2];
-
作了一个外部双精度型数组的引用性声明
-
应该在另外的源文件中通过double s[2];来定义s数组,这样第2个声明语句才有意义
-
C提供的各种操作符
-
数组
-
访问数组
-
不需设计专门的数组操作符
-
方法:数组名[下标表达式]
-
例int a[5], j=2;
-
5个元素依次是a[0],a[1],a[2],a[3],a[4]
-
正确用法: a[j+2] 、a[++j] 、a[j--]、a[5*j-7]
-
错误用法:a[j-3] 、a[2*j+1]
-
显式初始化值的个数与说明长度相同
-
int x[5]={0,1,2,3,4};
-
int y[5]={0,1,2,3,4,5}; 错误:初值个数大于数组长度
-
有初始化值时,长度说明可缺省
-
数组长度由初值个数确定
-
int y[]={1,2,3,4,5,6,7,8};
-
初始化值的个数可以小于说明长度,但只能缺省最后连续元素的初值
-
int z[10]={0,1,2,3,4}; /*前5个下标变量赋值*/
-
int u[9]={ , 1, , ,2}; 错误:缺省u[0], u[2]不是最后连续元素
-
存放方法
-
各个元素从数组名标明的起始地址开始在内存中连续存放
-
例如int a[5];
一位数组的运算
-
C提供的各种操作符
-
赋值运算、各种算术运算、++、--
-
针对基本数据类型的变量
-
可针对int、float、以及double类型数组中元素
-
合法操作
-
int x[3]={1,2,3},y[3]={4,5,6},z[3],k=1;
-
z[0] = x[0] + y[0];
-
z[1] = x[0] + y[3];
-
z[k] = ++x[0] + --y[k++];
-
z[1] = x[0] + y[x[1]];
-
不允许两个数组直接相加
-
z=x+y; 编译时给出提示“cannot add two pointers”
-
例
-
设5个同学修了高等数学﹑普通物理、程序设计语言并取得了成绩,现计算三门课总分、平均分,每门课的总分、平均分,每个同学的总分、平均分
-
#include "stdio.h"
-
#define SIZE 5
-
void main(void)
-
{
-
int math[SIZE]={91,67,88,78,81};
-
int physics[SIZE]={87,79,81,86,67};
-
int programming[SIZE]={86,81,85,92,87}; /*3个数组依次存放数学、物理、程序设计的成绩*/
一维数组作为函数参数
例:
-
#include "stdio.h"
-
void fun(int y[],int n); /* y是形式数组 */
-
void main(void)
-
{ int k,x[5]={1,2,3,4,5};
-
……
-
fun(x,3);
-
……
-
fun(&x[2],3);
-
……}
-
void fun(int y[],int n)
- {……}
-
子函数
-
需给出形式数组的声明和形式数组中元素的个数
-
调用函数
-
实参的值是数组x的起始地址
-
参数传递
字符数组的声明和使用
-
字符数组
-
元素的数据类型为char或wchar_t
-
声明格式
-
字符串
-
用一对双引号界定的一个字符序列
-
C语言没有规定字符串类型
-
用一个字符数组来存放字符序列,并且在末尾加一个空字符ˊ\0ˊ来构造字符串
-
字符串的长度
-
设计字符数组的最小长度
-
应该等于该字符串的存储长度
-
或字符数组的最小长度应该等于该字符串的长度加1
-
字符数组的使用
例:
-
#include "stdio.h"
-
void main(void)
-
{ char Capital[27],Lowercase[27];
-
int i, delt=ˊaˊ-ˊAˊ;
-
Capital[0]= ˊAˊ;
-
Lowercase[0]=Capital[0]+delt;
-
for(i=1;i<26;i++){
-
Capital[i]=Capital[i-1]+1;
-
Lowercase[i]=Lowercase[i-1]+1;
-
}
-
Capital[26]= ˊ\0ˊ; Lowercase[26]=Capital[26];
-
printf("%s\n",Capital);
-
printf("%s\n",Lowercase); }
程序运行结果如下:
ABCDEFGHIJKLMN OPQRSTUVWXYZ
abcdefghijklmnop qrstuvwxyz
-
通过初始化列表
-
char s1[8]={ˊWˊ,ˊuˊ,ˊhˊ,ˊaˊ,ˊnˊ,ˊ\0ˊ};
-
ˊ\0ˊ必须在初始化列表中显示给出
-
用字符串初值
-
char s2[28]=〞Computer Science〞;
-
末尾将自动加上一个ˊ\0ˊ
-
第三种方法
-
数组长度未明显给出
-
char s3[]=〞Computer Engineering〞;
-
字符数组的容量恰好等于字符串的存储长度
字符串处理函数
-
求字符串长度
-
字符串的拷贝
-
字符串的比较
-
字符串的连接
-
求字符串的子串
-
删除字符串首尾空白字符
-
从字符串中删除所有与给定字符相同的字符
-
将字符串反转等函数
例:求字符串长度的函数
-
int strlen(char s[])
- {
-
int j=0;
-
while(s[j]!= ′\0′)j++;
-
return j;
- }
-
void main(void)
- {
-
char str[]="there is a boat on the lake";
-
int length;
-
length = strlen(str);
-
printf("length of the string is %d\n",length);
- }
length of the string is 28
例:字符串拷贝的函数
-
void strcpy(char t[], char s[])
- {
-
int j=0;
-
while(t[j]=s[j++])
- ;
- }
-
void main(void)
- {
-
char str1[30], str2[]= "there is a boat on the lake.";
-
strcpy(str1,str2);
-
puts(str1);
- }
there is a boat on the lake.
7.指针
指针的概念
-
数据(变量、常量)(根据类型)占有一定数目的连续存储单元;
-
数据的连续存储单元首地址称为数据的地址。
-
变量的地址称为指针,存放地址数据的变量称为指针变量。
-
指针也是一种变量,也要占用一定的内存单元。指针的特殊之处在于它存放的是另一个变量所占存储单元的起始地址。
指针变量的类型
-
指针变量的类型由其基类型决定.
-
基类型是指针变量所指(即所指向)变量的数据类型。
-
因此,称指向某种类型变量的指针为该类型的指针。
如:指向整型变量的指针称为整型指针,指向字符型变量的指针称为字符指针等。
-
不管是何种类型的指针,所占的存储与机器字长相当.如:16位机,指针占2字节,…
-
思考: 为什么指针指针有类型?
如果整型变量x的地址是0xFEC0,x的值是0xABCD,整型指针ptr的地址是0xFEBC,整型指针ptr指向整型变量x的内存存储和指向情况可以由下图描述:
声明指针的一般形式:
T *标识符1,*标识符2,…, *标识符n;
其中,T是数据类型。*是指针说明符,用于将其说明的标识符说明为指针类型。标识符1,标识符2,…,标识符n是T类型的指针的名字.
例: char *pc1,*pc2;
int *pi;
float *pf;
指针变量也可以在声明的时候对其进行初始化,但必须用所指变量的地址进行初始化。初始化后的指针将指向以初值为地址的变量。
例9.2 声明指针并初始化。
int x=10,*p=&x;
错误的声明:
int *p=&x,x=10;
从右到左看,见到括号就调头:
-
int (*p1)[3];
-
double *p2[5];
-
char (*fp)(int,int);
-
int *pf(float a);
-
int (*fp_ary[2])(char *,int *);
指针的使用:
在声明指针变量的前提下,指针的使用首先要获取变量的地址作为指针值,然后要将该指针值赋给指针变量,建立指针变量与被指变量间的指向关系。接下来就是如何通过指针变量来间接访问和操作指针所指的变量。
1. 取地址运算符-单目&
单目&在C中表示取地址运算。该表达式的一般使用形式是:
&右操作数
根据表达式的操作语义可知,右操作数必须是一个左值表达式。如果右操作数的类型是T,则表达式&右操作数的类型是T *。
2. 指针的赋值
指针的声明只是创建了指针变量,获得了指针变量的存储,但并没有给出指针变量指向那个具体的变量,此时指针的值是不确定的随机值,指针处于“无所指”的状态。例如:用 int *p;语句来说明p是一个整型指针变量时,p的值是不确定的随机值。此时称p为悬挂指针。
例 设有下面说明:
int i,*pi;
char c,*pc;
float x[5],*pf;
则:
-
pi=&i; 取整型变量i的地址赋给整型指针变量pi.
-
pc=&c; 取出字符变量c的地址赋给字符指针变量pc.
-
pf=&x[0];和pf=x;的作用相同,均表示将x数组的起始地址赋给指针变量pf, 使pf指向x数组的第一个元素x[0]。
-
数组名x的数据类型是float *,并且标识x数组的起始地址,因此语句pf=x;合法有效。
3. 间访运算符单目*
-
声明指针的目的是希望通过指针实现对内存中变量的快速访问,必须使用单目*这个间访运算符才能实现利用指针对内存变量的访问。间访的含义是间接访问。单目*运算实际上是单目&运算的逆运算.
-
使用单目*间访变量的一般形式是:
*操作数
-
“*操作数”称为间访表达式,它的操作语义是引用以操作数之值为地址的变量。操作数也可以是表达式,但其值必须是地址值。
-
间访表达式的值是一个左值,代表指针所指的变量。而指针所指的变量的类型就是间访表达式运算结果的类型。例 间访运算符的基本操作举例。
设有声明和赋值:
char ch=’a’,*pc=&ch;
*pc=’b’;
并且假设字符变量ch的地址是0x1b2a,试问ch,pc,以及*pc的值是什么?
4. 无值型指针
类型为void *的指针称为无值型指针或void指针。C语言规定,任何类型的指针都可以将其值赋给void指针,但是不能对void指针执行间访操作,即对void指针施行“*”操作属于非法操作。
int x=1,*px=&x;
char ch=ˊaˊ,*pc=&ch,*pc1;
void *vp;
则:vp=px;
vp=pc;
pc1=(char *)vp;
*vp=ˊbˊ; /* 非法 */
指针允许进行算术运算、赋值运算和关系运算。通过指针运算可以快速定位到指定的内存单元,提高程序的执行效率。
指针的算术运算
1. 指针的移动
指针的移动指指针在原有位置的基础上,通过加一个整数实现指针的前移(地址增大的方向)或者通过减一个整数实现指针的后移。当然,如果加减的是一个负数,则移动的方向刚好和前面相反。
例 指针的算术运算应用举例
#include "stdio.h"
void main(void)
{
int iary[5]={0,1,2,3,4};
int *px=&iary[0];
printf("display the address and
the value of the elements:\n");
for(;px< &iary[0]+5;px++)
printf("px=%p *px=%d\n",px,*px);
px=&iary[0];
printf("after executing px=&iary[0]:\n");
printf("px=%p *px=%d\n”,px,*px);
px+=3;
printf("after px plus 3:\n");
printf("px=%p *px=%d\n",px,*px);
px-=2;
printf("after px minus 2:\n");
printf("px=%p *px=%d\n",px,*px);
printf("*(++px)=%d\n",*(++px));
printf("px=%p *px=%d\n",px,*px);
printf("*(px++)=%d\n",*(px++));
printf("px=%p *px=%d\n",px,*px);
printf("*(--px)=%d\n",*(--px));
printf("px=%p *px=%d\n",px,*px);
printf("*(px--)=%d\n",*(px--));
printf("px=%p *px=%d\n",px,*px);
}
指针的赋值运算和关系运算
1. 指针的赋值运算
指针的初始化和基本赋值操作前面已有叙述。下面主要进行一些归纳并给出应用的例子,其中利用指针的赋值运算以及类型强制来突破语言的一些制约因素的编程算法和思想可以在底层软件开发时借鉴,达到汇编级别开发的自由度。
指针的赋值运算规则:
(1) 同类型的指针才可以直接使用赋值操作符进行赋值操作。如:
int a[3]={1,2,3},*p=a,*q=p;
(2) 如果要实现不同基本类型指针之间的赋值运算,必须使用类型强制。利用指针的赋值运算以及类型强制可以实现一些特殊操作。
例: 一个长整型数占4个字节,其中每个字节又分成高4位和低4位。试从长整型数的低字节开始,依次取出每个字节的高4位和低4位并以ASCII码的形式进行显示。
#include "stdio.h“
void main(void)
{ long x=0x1234ABCD,k;
char *p=(char *)&x; /* 类型强制,x被视为4字节字符数组被p所指 */
char up_half,low_half; /* up_half存高4位,low_half存低4位 */
for(k=0;k<4;k++) {
low_half=(*p)&0x0f; /* 取低4位 */
if(low_half<10)
low_half|=ˊ0ˊ; /* 低4位二进制转换成ASCII码ˊ0ˊ-ˊ9ˊ */
else
low_half=(low_half-10)+ˊAˊ;/* 低4位二进制转换成ASCII码ˊAˊ-ˊFˊ*/
up_half=(*p>>4)&0x0f; /* 取高4位 */
if(up_half<10)
up_half|='0'; /* 高4位二进制转换成ASCII码'0'-'9' */
else
up_half=(up_half-10)+'A'; /* 高4位二进制转换成ASCII码'A'-'F' */
p++; /* 指向整型数x下一个字节*/
printf("%c %c\n",up_half,low_half);
}}
指针的关系运算
-
指针的关系运算指对指针施行诸如:<,<=,>,>=,==,以及!=这样一些比较操作。
-
指针属于一种派生类型,它属于何种类型的指针要受所指变量的类型,即指针的基类型的限制。
-
不同类型的变量在内存中分配的存储不同;对于不同的数据类型,定义于其上的操作和运算也不尽相同。因此,尽管所有指针的值都是二进制正整数,从关系运算的操作语义上看,允许不同类型的指针进行比较没有实际意义,并且只会给语言和程序带来副作用。
-
因此,C语言规定指针的关系运算只能限于同类型指针,不同类型指针之间的关系运算被视为非法操作。
指针作为函数的参数
形参指针对实参变量的影响
例: 被调用函数中对形参的修改无法影响调用函数中实参变量的值举例
#include "stdio.h"
void swap( int u,int v )
{ int t;
printf("in function swap, before swap,u=%d and v=%d\n",u,v);
t=u;u=v;v=t;
printf("in function swap, after swap,u=%d and v=%d\n",u,v);
}
(演示EX9_12)
void main(void)
{ int x,y;
printf("input two integers for x and y please!\n");
scanf("%d%d",&x,&y);
printf("in function main, before swap,x=%d and y=%d\n",x,y);
swap(x,y);
printf("in function main, after swap,x=%d and y=%d\n",x,y);
}
程序运行中的输入以及运行结果为:
input two integers for x and y please!
10 20(键盘输入)
in function main, before swap,x=10 and y=20
in function swap, before swap,u=10 and v=20
in function swap, after swap,u=20 and v=10
in function main, after swap,x=10 and y=20
指针作为函数形参的应用
对于数组,由于数组名标识的数组的起始地址,故可直接作为指针类型的实参;除此之外,都必须以&变量名的形式给出变量的地址作为实参,以保证在被调用函数中能够操作调用函数中的对应变量。
例: 将键盘输入的所有数字串逐个的转换为整数,并且依次存放于一个数组中。数字串前面可以加+、-号,各数字串之间至少用一个空格分开,用回车结束输入。
演示并解释p267 例9-14
#include "ctype.h"
#define NUMBER 100
int getint(int *pn);
int getint(int *pn)
{ int c,sign;
while(isspace(c=getchar())) ;
/* 跳过前导空格 */
if(!isdigit(c) && c!=EOF && c!= '+' && c!= '-') {
/* 若输入为非数字、非EOF、也不是+、-号,显示出错 */
printf("input error!\n");
return 0;}
sign = (c=='-') ?-1:1; /* 处理符号 */
if(c=='+' || c=='-')
c=getchar(); /* 跳过+、-号 */
for(*pn=0;isdigit(c);c=getchar())
*pn=10 * *pn+(c-'0');
/* 将数字串转换为对应整数 */
*pn *= sign; /* 处理整数的符号 */
return c;}
void main(void)
{
int a[NUMBER],i,j,k;
printf("input integers ends with Enter\n");
for(i=0;i< NUMBER && (k=getint(&a[i]))!= '\n';)
if(k) i++;
for(j=0;j<=i;j++)
printf("%8d",a[j]);
printf("\n");
}
数组的指针表示
-
数组元素既可以用下标表示,也可以用指针表示。
-
对于T类型的数组元素a[i],用下标表示时其地址为&a[i],用指针表示时其地址为a+i,
-
编译程序计算a[i]的地址都统一按照:
a + i*sizeof(T)
的形式进行。其中i*sizeof(T)表示数组元素a[i]相对该数组的起始地址向后偏移的字节数。
-
由于下标操作符[]是系统的预定义函数,下标操作实际涉及对系统预定义函数的调用。因此,一般说来,指针运算比数组下标运算的速度要快。
一维数组的指针表示
设有说明:int x[5];
各元素的访问方式:
-
x[0]、x[1]、x[2]、x[3]、x[4]
-
*(x+0)、*(x+1)、*(x+2)、*(x+3)、*(x+4)
各元素的指针:
-
&x[0]、&x[1]、&x[2]、&x[3]、&x[4]
-
x+0、x+1、x+2、x+3、x+4
ISO/IEC 9899中关于数组与指针关系的描述
因此: a[i] = (*((a)+(i))) = *(a+i)
对于:b[i][j],令b[i]=E1,E2=j
b[i][j] = *(b[i]+j) = *(*(a+i)+j)
所以:
C[i][j][k] =*(c[i][j]+k)
=*(*(c[i]+j)+k) =*(*(*(c+i)+j)+k)
思考:e[i][j][k][l][m]的间访表示?
8.C程序内存地址空间布局
从历史上讲,C程序一直由下面几部分组成:
1,文本段(正文段)
2,数据段
1)初始化数据段
2)非初始化数据段
3,栈(stack)
4,堆(heap)
正文段
CPU执行的机器指令部分。通常,正文段是可共享的,所以即使是经常执行的程序(如文本编辑程序、C编译程序、s h e l l等)在存储器中也只需有一个副本,另外,正文段常常是只读的,以防止程序由于意外事故而修改其自身的指令。
数据段 1
非初始化数据段
通 常将此段称为bss段,这一名称来源于早期汇编程序的一个操作符,意思是“block started by symbol(由符号开始的块)”,未初始化的全局变量和静态变量存放在这里。在程序开始执行之前,内核将此段初始化为0。函数外的说明:long sum[1000] ; 使此变量存放在非初始化数据段中。
a.未初始化的全局变量
b.未初始化的静态变量
数据段 2
初始化的数据
通常将此段称为数据段,它包含了程序中需赋初值的变量。初始化的全局变量和静态变量存放在这里。例如,C程序中任何函数之外的说明:int maxcount = 99; 使此变量以初值存放在初始化数据段中。
a.初始化的全局变量
b.初始化的静态变量
栈(stack)
由编译器自动分配释放管理。局部变量及每次函数调用时返回地址、以及调用者 的环境信息(例如某些机器寄存器)都存放在栈中。新被调用的函数在栈上为其自动和临时变量分配存储空间。通过以这种方式使用栈,C函数可以递归调用。递归 函数每次调用自身时,就使用一个新的栈帧,因此一个函数调用实例中的变量集不会影响另一个函数调用实例中的变量。
a.局部变量
b.函数调用时返回地址
c.调用者的环境信息(例如某些机器寄存器)
堆(heap)
需要由程序员分配释放管理,若程序员不释放,程序结束时才由OS回收。通常在堆中进行动态存储分配。
如程序中的malloc, calloc, realloc等函数都从这里面分配。堆是从下向上分配的。
高地址 ——0x7FFFFFFF———
命令行参数和环境变量
——————————
栈空间,向下增长
___________________
堆空间,向上增长
———————————
未初始化的数据
———————————
已初始化的数据
———————————
正文段
低地址—0x00000000————
下面给出一般的c程序存储布局的典型安排:
可以注意到未初始化的数据段的内容并不放在磁盘上的程序文件中,
因为,在程序开始运行前他们都被设置为0。
需要存放在程序文件中的只有正文段和初始化数据段。
9.结构与联合
-
结构与联合都属于C的构造类型。
-
对结构与联合而言,都需要先定义结构类型和联合类型,然后再根据已经定义的结构类型来定义对应的结构变量以及用已经定义的联合类型来定义对应的联合变量。
结构产生的背景:
-
程序设计语言的发展总是与数据类型的发展、丰富、以及完善联系在一起的。
-
变量可以认为是内存单元的有名表示。变量在处理大量同类型数据时就暴露出名字多,不利于用循环处理等一系列不足。
-
数组则是处理大量同类型数据的有力工具。通过数组名和下标,可以方便的描述大量同类型数据,并且可以充分利用循环来进行快速处理。
-
问题: 如何将类型不同而关系又非常密切的数据项组织在一起,统一加以处理?–结构产生的背景!
-
结构类型是一种能够将不同数据类型的成员组织起来所形成的一种新的构造类型。
-
结构类型的设计: 在程序设计中,要先确定需要被组织的数据类型,由这些数据类型说明的标识符称为结构类型的成员;通过定义结构类型将这些成员组织起来,形成新的数据类型。
-
结构类型的使用: 通过结构类型来声明对应的结构变量、结构指针、或结构数组。对结构变量、结构指针、或结构数组中成员的操作将最终完成所需要解决的计算任务。
注解:
-
有些教材中将结构类型称为结构体、聚合类型。
-
C中的结构在其它程序设计语言中往往称为记录(record)。
-
以记录为基础,可以进一步构造文件、基于记录的数据库、以及许多动态数据结构。
结构类型声明和结构变量的声明及初始化:
-
在声明结构类型时,需要规定该结构类型包括那些成员,要说明成员的数据类型和名字。
-
声明结构类型是创建用户自定义数据类型的过程,它并不创建对象,即不进行存储分配。
-
声明结构类型的一般形式是:
struct 结构类型名{
成员声明表
};
-
其中:struct是关键字,用以说明结构类型。结构类型名是该结构类型的名字,它应该是C的合法标识符。一对花括号界定的是成员声明表 。
成员声明表的一般形式
数据类型n 成员名n1,…,成员名nm;
-
数据类型1 和数据类型n可以相同,也可以不同。
-
每个声明允许用逗号为分隔符说明多个同类型的成员。
-
结构类型的声明应该以分号结束。
例:学生学习情况的结构类型声明
struct stu_study{ /* stu_study是结构类型名 */
char num[12]; /* 学号成员,字符数组类型 */
char name[9]; /* 姓名成员,字符数组类型 */
char sex;/* 性别成员,字符类型 */
int English;/* 英语成员,整型 */
int Math,Physics,C; / *数学、物理、C成员,整型 */
};
-
该声明定义了一个struct stu_study的结构类型.stu_study是结构类型名,用以区分其他类型的结构。
-
struct stu_study实际上是一种新的构造类型名,可用于说明struct stu_study类型的结构变量。其在语法方面的作用与int,char等一样,起类型说明符的作用。
-
num、name、sex、English、Math、Physics、C是成员,它们被组织到一个新的struct stu_study的结构类型之中。
C关于结构类型声明的规定
(1) 同一结构内的成员不能同名。但成员可以与结构外部的变量同名,也可以与其他结构的成员同名。
(2) 成员的数据类型可以是除本结构类型以外的其他任何类型。
(3) 结构类型可以嵌套定义,形成嵌套结构。
(4) 结构类型是包含一系列成员的构造类型,成员在内存中连续存放,成员存储分配按照结构声明体中不同声明从上向下,同一声明中从左向右的顺序进行,每个成员所占存储空间的大小由其类型确定。
(5) 一般而言,成员所占存储的大小必须在结构类型声明时确定。即C不支持动态结构类型。
注: 根据ISO/IEC 9899标准,作为一个特例,结构类型中最后一个成员可以具有不完全的数组类型,使得C可以在有限程度上支持动态结构类型。
含柔性数组成员(flexible array member)的声明
struct dy_stu_study {/* dy_stu_study是结构名 */
char num[12]; /* 学号成员,字符数组类型 */
char name[9]; /* 姓名成员,字符数组类型 */
char sex; /* 性别成员,字符类型 */
int score[];/* 各科成绩的柔性数组成员,动态整型数组 */
};
-
可以通过动态存储分配为score数组指定大小。如果score数组依次存放英语、数学、物理、C语言4门课程的成绩,在16位机中可以为其分配4×2=8个字节的存储;在32位机中则需要为其分配4×4=16个字节的存储。称这种结构类型为有限动态结构类型。
例: 声明一个只含有整数类型和指向自身实例指针的结构类型。
struct int_node {
intnumber;
struct int_node *next;
};
在该结构类型中:
-
number被称为数据域,
-
而next被称为指向自身结构实例的指针域。
-
结构类型的变量往往简称为结构变量,有时甚至简称为结构。
结构变量的声明有两种方式:
-
一是结构类型和结构变量分别声明。即:先声明结构类型,再声明结构变量。
-
二是结构类型和结构变量同时声明。即:在声明结构类型的同时声明结构变量。
1.结构类型和结构变量分别声明
声明结构变量的一般形式是:
存储类型 struct 结构类型名 结构变量列表;
其中,存储类型可以是extern、static、auto、register,分别表示外部结构变量、静态结构变量、自动结构变量(或局部结构变量)、以及寄存器结构变量。
结构变量列表由标识符序列组成,两个标识符之间必须用逗号分隔,每个标识符对应一个结构变量。结构变量的声明应该以分号结束 。
struct stu_study Zhang;
就声明了一个struct dy_stu_study类型的结构变量Zhang。
而结构变量声明语句:
struct address_book John,Mary,Jones;
则声明了三个struct address_book类型的结构变量John,Mary和Jones。
例:用存储类型修饰结构变量声明的例子。
(1) auto struct int_node a,b,c;
(2) register struct address_book addr;
(3) static struct int_node x,y;
(4) extern struct stu_study Wang;
2.结构类型和结构变量同时声明
其一般形式为:
存储类型 struct 结构类型名{
成员声明表
} 结构变量列表;
-
其中,结构变量列表由标识符序列组成,两个标识符之间要用逗号分隔,每个标识符对应一个结构变量。
-
并且可以通过存储类型修饰结构变量列表中的结构变量。
结构类型和结构变量同时声明的例子
int x;
int y;
} p1,p2;
-
它在声明结构类型struct point的同时,声明了struct point类型的静态结构变量p1和p2。P1, p2具有静态生存期.
-
struct point结构类型可以用来表示平面上的一个点,成员x、y分别表示平面上点的横坐标和纵坐标。结构变量p1和p2则分别表示平面上的两个点。
无名结构类型
-
一般称省略结构类型名的结构类型为无名结构类型。如:
struct {
int x;
float y;
} u,v;
-
它声明了结构变量u和v,结构变量u、v分别都有一个整型成员x,一个浮点型成员y。
-
由于省略了结构类型名,在后续程序中不能再声明类似类型的变量。
3. 用typedef定义结构类型名
-
-
用typedef可以为一个已知的结构类型进行命名。这种方法在程序设计中使用得非常普遍。例如:
typedef struct point {
int x;
int y;
} point;
-
它将struct point结构类型命名为point,
用point来表示struct point,结构类型的声明
-
point u,v; 它与 struct point u,v; 等价。
-
用typedef还可以为一个无名结构类型进行命名。如:
typedef struct {
int x;
int y;
} point;
-
则point就代表有两个整型成员x,y的结构类型。也可以用:
point u,v;
说明结构变量u和v。
结构变量的初始化
-
在声明结构变量的同时可以对其进行初始化。初始化所用的初值由初值表提供,初值表的形式与数组初始化时使用的初值表类似,是由一对花括号界定的初值序列。结构变量的初始化的一般形式是:
存储类型 struct 结构类型名{
成员声明表
} 结构变量1=初值表1,… ,结构变量n=初值表n;
或: 存储类型 struct 结构类型名 结构变量1=初值表1,… ,结构变量n=初值表n;
结构变量的初始化举例
char num[5];
char name[9];
char sex;
int English;
int Math,Physics,C;
} Wang={〞1234〞,〞Wang Wu〞,ˊmˊ,81,92,76,85 };
-
它将结构变量Wang的成员num、name、sex、English、Math、Physics、C依次初始化为〞1234〞、〞Wang Wu〞、ˊmˊ、81、92、76、85。
通过存储类型修饰符对结构变量的存储类型进行修饰例的例子
int x;
int y;
}p1={1,2},p2={10,20};
-
它在声明struct point类型的静态结构变量p1,p2的同时,分别将其成员x、y初始化为1,2和10,20。此时,成员x、y具有静态变量的特性。
结构类型的使用
-
结构类型的使用包括对结构变量的引用和对结构变量中成员的引用两个方面。
-
对结构变量的引用包括结构变量的赋值操作、取地址操作和间访操作,以及结构变量作为函数的参数及返回值。
-
对结构变量中成员的引用则需要通过成员选择运算符“.”来实现。
结构变量的引用
1.结构变量的赋值操作
-
当两个结构变量的类型相同时,它们之间可以直接相互值。
-
例如:
struct planet {
charcode[8];
charname[10];
short visible;
doublediameter;
}star1={"12345","Venus",1,9456.35};
struct planet star2;
-
则star2=star1;是完全合法的赋值。
-
赋值操作的语义是将赋值号右端结构变量中各个成员的值一一照赋给赋值号左端结构变量的各个成员。
-
此时结构变量star2的成员code、name、visible、diameter的值依次为"12345","Venus",1,9456.35,与star1的成员的值相同。
2.结构变量的取地址操作和间访操作
-
可以通过单目&对结构变量进行取地址操作,通过间访操作符*可以对结构指针所指的结构变量进行间访操作。
-
如: struct planet star2,*p=&star2;
它声明了一个struct planet类型的结构变量star2和一个struct planet类型的指针p,并且通过&star2取出结构变量star2的地址对指针p进行初始化,使得指针p指向了结构变量star2。指针p的数据类型是struct planet *。
-
通过间访操作符*对指针所指结构变量进行间访操作.如:
*p=star1;
它与star2=star1;等价。
通过成员选择运算符“.”访问成员
-
C提供了成员选择运算符“.”构成的成员选择表达式来支持对成员的访问和操作。成员选择表达式的一般形式是:
结构变量名. 成员名
-
其中,“.”是小数点或句号字符,称为成员选择运算符。
-
它是一个双目操作符,其左操作数是结构变量,右操作数是结构变量的成员。
-
“.”操作符具有最高优先级,由它构成的成员选择表达式在任何地方都是一个整体,表示对结构变量中成员的访问。
设有声明语句:struct stu_study Li;下面的代码片段对struct stu_study类型的结构变量Li的各个成员进行格式化输入或赋值操作。
struct stu_study Li;
...
scanf("%s",Li.num);
strcpy(Li.name,"Li Si");
Li.sex='m';
Li.English=Li.Math=80;
scanf("%d",&Li.Physics);
Li.C=86;
值得注意的是:
-
由于Li.num和Li.name都是字符数组名,其数据类型都是char *,因此必须用scanf函数、字符串拷贝函数strcpy或gets函数进行赋值操作。
-
Li.num="678";或Li.name="Li Si";都是错误的赋值方法。
-
但Li.num[0]=ˊ6ˊ; Li.num[1]=ˊ7ˊ ;
Li.num[2]=ˊ8ˊ; Li.num[3]= ˊ\0ˊ;
是合法的赋值操作。
下面的语句输出结构变量Li的各个成员之值:
printf("%s\t",Li.num);
printf("%s\t",Li.name);
putchar(Li.sex);
putchar(ˊ\tˊ);
printf("%d\t%d\t%d\t%d\n",Li.English,Li.Math,Li.Physics,Li.C);
其中,putchar(ˊ\tˊ);是用于格式控制。
嵌套结构的声明
-
C标准规定:结构变量的成员的数据类型还可以是除自身之外的其他结构类型。具有结构类型成员的结构类型称为嵌套结构类型,具有结构类型成员的结构变量称为嵌套结构变量。
-
嵌套结构的声明要遵循先声明嵌套结构类型,再声明嵌套结构类型变量的原则。这种原则称为先声明后引用原则。在声明嵌套结构类型时,应该先声明结构类型成员的结构类型,再声明嵌套结构类型。
例10-4 大学生基本信息嵌套结构类型的声明举例。
-
描述大学生基本信息需要学号、姓名、性别、出生日期、家庭住址、家庭收入、联系电话等属性。
-
可以先定义一个日期类型,再在大学生基本信息结构类型中引入日期类型的结构成员,用以描述出生日期.
-
然后再声明大学生基本信息结构类型的结构变量。
先声明日期类型
struct date{
char month[10];
int day;
int year;
};
再声明大学生基本信息嵌套结构类型struct stu_info
struct stu_info{
char num[12]; /* 学号 */
char name[9]; /* 姓名 */
char sex; /* 性别 */
struct date birthday; /* 出生日期 */
char address[60];/* 家庭住址 */
double income; /* 家庭收入 */
char phone_number[16]; /* 联系电话 */
char email[40]; /* 电子邮箱 */
};
-
其中,birthday是struct date类型的结构成员。此时,struct stu_info是一个嵌套的结构类型。
-
struct stu_info s; 声明了一个嵌套结构类型struct stu_info的结构变量s。
例:在嵌套结构类型声明的内部声明结构类型并声明结构类型成员,并且在声明嵌套结构类型时定义结构变量。
struct stu_info1{
charnum[12];
charname[9];
charsex;
struct date1 {
charmonth[10];
int day;
int year;
} birthday;
charaddress[60];
doubleincome;
charphone_number[16];
charemail[40];
} s1,s2;
-
此时,struct date1结构类型在struct stu_info1嵌套结构类型内部声明,同时还声明了结构成员birthday。
-
并且在声明嵌套结构类型struct stu_info1的同时,声明了嵌套结构类型struct stu_info1的变量s1和s2。
嵌套结构中结构成员的成员的访问
-
嵌套结构中由于包含结构成员,因此存在结构成员的成员。对嵌套结构中结构成员的成员的访问采用“.”操作符,构成结构成员的成员选择表达式。
-
“.”操作符的使用个数与嵌套结构的层数相等。在两层嵌套结构中,结构成员的成员选择表达式的一般形式是:
结构变量名.结构成员名.成员名
-
“.”操作符的结合性是从左至右,编译器对“结构变量名.结构成员名.成员名”理解为:
(结构变量名.结构成员名).成员名。
设有声明语句: struct stu_info zhang;
则下面是struct stu_info类型结构变量zhang的结构成员birthday的成员选择表达式:
zhang.birthday.month 或 (zhang.birthday).month
月份成员,类型char *。
zhang.birthday.day 或 (zhang.birthday).day
日成员,类型int。
zhang.birthday.year 或 (zhang.birthday).year
年份成员,类型int。
-
成员选择表达式是左值表达式,使用时要根据其数据类型进行相关操作。
-
例如, zhang.birthday.day=12为正确的赋值操作。而zhang.birthday.month="July"为非法赋值操作。
例: 根据平面上点的结构类型构造平面上线段的嵌套结构类型,说明对嵌套结构变量中结构成员的成员的引用,并求线段的长度。
#include "stdio.h"
#include "math.h"
struct point{ /* 平面上点的结构类型 */
intx,y;/* x,y是点的坐标 */
};
struct line{ /* 平面上线段的嵌套结构类型 */
char name[6];/* 线段名称 */
struct point start;/* 线段的起点 */
struct point end;/* 线段的终点 */
};
void main(void)
{
struct line a={"abc",{1,1},{10,10}};/* 声明嵌套结构变量时对其初始化 */
double dx2,dy2,length;
printf("line name is %s\n",a.name); /* 输出线段名称 */
/* 下面输出起点坐标和终点坐标 */
printf("starting point:(%d,%d)\n",a.start.x, a.start.y);
printf("end point:(%d,%d)\n",a.end.x, a.end.y);
/* 下面对线段名称和起点坐标进行格式化输入赋值 */
scanf("%s",a.name);
scanf("%d%d",&a.start.x, &a.start.y);
/* 下面对终点坐标进行赋值 */
a.end.x=100;a.end.y=100;
/* 下面计算线段长度并输出 */
dx2=(a.end.x-a.start.x)*(a.end.x-a.start.x);
dy2=(a.end.y-a.start.y)*(a.end.y-a.start.y);
length=sqrt(dx2+dy2);
printf("the tength is %lf\n",length);
}
类似的,由平面上的一个点和不包含该点的直线可以构造一个平面,可以设计出由struct point结构类型和struct line结构类型构造出的含有三层嵌套的struct plain结构类型:
struct point{
intx,y;
};
struct line{
struct point start,end;
};
struct plain{
struct point a;
struct line l;
};
-
声明三层嵌套的struct plain类型结构变量的声明语句如下:
struct plain p={{1,1},{{5,5},{20,15}}};
该语句定义了struct plain类型的结构变量p,并对其进行了初始化。
-
下面语句表达了如何对嵌套结构变量中具有基本类型成员的进行操作:
p.a.x=2;
p.l.start.x=5;
printf("p.l.start.x=%d\n",p.l.start.x);
scanf("%d", &p.l.start.y );
printf("p.l.start.y=%d\n", p.l.start.y);
-
可见,嵌套多少层,对基本类型成员的访问就需要多少个对应的“.”操作。同时,对基本类型成员进行操作时一定要符合其数据类型的要求。