9.9 指针与结构体
9.9.1 类型问题
结构体类型是一种数据类型,结构体数据也是一种数据对象。因此也可以构造出对应的指针类型。这种指针的运算规则遵守指向数据类型指针的运算规则。仍以前面的结构体类型为例:
- struct shijian {
- int shi ;
- int fen ;
- int miao;
- } ;
这种结构体类型的名称是“struct shijian”,对应的指针的类型是“struct shijian *”。可以用这个类型名定义相应的指针变量,如:
同样,如果定义了这种类型的结构体变量:
也可以通过“&”运算求得指向这个结构体变量的指针“&cs”,它的类型也是“struct shijian *”,显然这是一个指针常量。如图9-28所示,如果希望指针变量“p_cs”指向结构体变量“cs”,可以通过赋值运算实现:
图9-28 指向结构体数据的指针
9.9.2 通过指针读写结构体的成员
通过指向结构体类型的指针,同样可以对结构体类型量的成员进行访问。由于“* p_cs”就是“cs”,因而可以通过下面的形式访问“cs”的成员:
注意这里“()”是必需的,因为“*”运算的优先级低于“.”运算的优先级。和“cs.shi”一样,这个表达式也可以作为左值。
此外,C语言还提供了另一种通过指针访问结构体成员的方法,即“->”运算,具体的方法是:
这和“(*p_cs).shi”是一样的。
下面代码是指向结构体的指针用法的演示。
程序代码9-29
- /*
- 题目:21点36分23秒后再过3小时28分47秒是几点?
- */
- #include <stdio.h>
- #include <stdlib.h>
- #define MSX 60 // 秒数的上限
- #define FSX 60 // 分数的上限
- #define SSX 24 // 时数的上限
- #define SHIJIAN struct shijian
- SHIJIAN {
- int shi ;
- int fen ;
- int miao;
- } ;
- void jg( SHIJIAN * , SHIJIAN );
- int main ( void )
- {
- SHIJIAN sj = { 21 , 36 , 23 } , zl = { 3 , 28 , 47 }; //时间和时间的增量
- printf ( "%d点%d分%d秒后再过" , sj.shi , sj.fen , sj.miao );
- printf ( "%d小时%d分%d秒后是" , zl.shi , zl.fen , zl.miao ) ;
- jg ( &sj , zl ) ;
- printf ( "%d点%d分%d秒\n" , sj.shi , sj.fen , sj.miao ) ;
- system("PAUSE");
- return 0;
- }
- /* jg()函数功能:
- 根据指向时间的指针和时间的增量
- 改变时间的值
- */
- void jg( SHIJIAN *p_sj , SHIJIAN zl )
- {
- p_sj -> shi += zl.shi ;
- p_sj -> fen += zl.fen ;
- p_sj -> miao += zl.miao ;
- p_sj -> fen += p_sj -> miao / MSX ;
- p_sj -> miao %= MSX ;
- p_sj -> shi += p_sj -> fen / FSX ;
- p_sj -> fen %= FSX ;
- p_sj -> shi %= SSX ;
- }
运行结果如下:
21点36分23秒后再过3小时28分47秒后是1点5分10秒
请按任意键继续. . .
9.10 指针与函数
9.10.1 函数名是指针
如同数组名是指针一样,在C语言中,函数名也是指针。当然这种指针也必然是一种“常量”,因为在内存中“移动”变量尚不可能,更不必说“移动”构成函数的一群机器指令了。
作为一种指针,首先要明白它的类型。描述函数名这样的指针非常容易,只要把函数声明中的函数名换成“(*)”就可以了。比如某个函数的函数声明为:
那么,“qiuhe”这个函数名的类型是:
这种类型写法对于我们来说除了具有形式上的意义,并没有告诉我们更多的关于这种类型的含义,除非我们知道这种类型本身占据多少内存空间以及这个指针指向什么。
没有理由说这种类型和前面数据指针所需要的内存空间相同,这是必须在具体环境中才能确定的事情。但在这里我们不妨假设这种指针需要4个字节的内存空间,无论实际情况是否如此,对后面的讨论都没有什么影响。
笼统地说,这种指针指向函数也没有任何意义,因为我们并不清楚也不可能清楚函数在内存中是什么样子。毕竟函数不同于数据。数据具有统一的类型和构造规则,相同类型的数据具有相同大小的连续存储空间。而函数在内存中的存储空间我们是不可能加以考察的,甚至我们都不清楚函数占据的内存空间是否连续,可以肯定的是各个函数占据的空间原则上是不相同的。
这恰恰是函数与数据这种连续且具有确定内存长度的对象(Object)最大的区别。这个区别,在后面我们可以看到,决定了指向函数的指针与指向数据的指针之间巨大的差异。
函数与数据的相同之处是它们都占据内存空间,而它们各自所占据的空间都是从各自的某个内存单元开始的,这是可以有指向函数指针的基础,毕竟指针的值是地址。函数名的值也是函数经过编译之后在内存中的映像的起始内存单元的编号或地址。
如图9-29所示的部分内容是不真实的,只是为了帮助理解,把函数“比拟”成了一种类似数组对象的东西。后面将会说明哪些是能被C语言证实的,而哪些是虚构的。
图9-29 函数名的意义
函数占据内存空间,这是确定无疑的。但不清楚占据的是否为连续空间,也不可能清楚这块空间的大小。但在图中画成了一块连续的内存空间来表示“int qiuhe()”函数在内存中的实体,这是虚构的,但是只要我们不从这种虚构中引申出错误的结论,而只是为了帮助理解指向函数的指针这种数据类型,应该是能够获得大家的理解和宽恕的。
函数占据的内存空间有个起点,这个起点处的内存单元有一个编号,也就是所谓的“入口地址”,这是确定的。图中“qiuhe”这个函数名的实线箭头表示的是这一点。
图中,虚线箭头表示“qiuhe”这个指针指向函数所占据的这块内存的整体,这是虚拟的想象,C语言并没有承认这是事实;方向向上的“}”用来表示“qiuhe”这个函数名也代表函数所占据的内存实体,这是作者虚构的,C语言没有这样说过。这样做的目的是把函数名比拟成数组名 ,期待我们能自然地接受函数名的某些性质。
9.10.2 指向函数指针的性质
前面搭建的那个半真半假的模型的本质如下。
- qiuhe这个函数名是指向qiuhe()这个函数的指针。
- qiuhe这个函数名也代表qiuhe()函数所占据的内存实体。
第一点没有人会否认,只不过C语言没有明确“指向函数”的具体含义。而我虚构了一个“指向函数”的具体含义,我确信这对于编程没有什么危险,因为编程不会用到这点,只可能用到后面推导出的和C语言一致的结论。第二点则完全是我虚构的,是为了更直接地导出下面的推理和正确的结论。
由于“qiuhe”是指向函数的指针,所以“*qiuhe”就是函数的实体;而函数的实体又可以用函数名“qiuhe”表示,所以结论是
同理,由于“qiuhe”代表函数的内存实体,所以“&qiuhe”就是指向这个函数的指针;而指向这个函数的指针又是“qiuhe”这个函数名本身,所以可以得到另一个结论
这样我们就用一个半真半假的模型,自然地推导出了C语言生硬且直接给出的函数名最重要的性质
9.10.3 指向函数指针的运算
定义与函数名类型相同的指针变量 如前所述,函数名是指针常量。也可以定义这种类型的变量。仍以“int qiuhe( int , int );”这个函数原型为例,定义与函数名“qiuhe”类型相同的指针变量的方法是:
当然也可以构造这种类型的数组:
这个定义有些复杂,这里不准备详细解读,后面将专门介绍复杂定义的解读问题。
赋值运算 由于“p”的类型与函数名“qiuhe”的类型一致,所以可以进行赋值运算:
这时称指针“p”指向了“qiuhe()”函数。
类似的,指向函数的指针也可以作为函数的实参把值传给相同类型的形参。
函数调用运算 函数名可以进行函数调用运算是不言而喻的,与其相同类型的指针变量也可以进行这种运算。由于函数名这种指针具有函数名 == *函数名 == & 函数名 这样的性质,所以很容易地可以得到结论—下面几种函数调用方式是完全等价的:
- qiuhe(2,3)
- (*qiuhe)(2,3)
- (&qiuhe)(2,3)
- p(2,3)
- (*p) (2,3)
其中“(*qiuhe)”、“(&qiuhe)”、“(*p)”的括号是必需的,因为“*”、“&”的优先级低于函数调用运算的优先级。
由于函数名这种指针具有函数名 == *函数名 == & 函数名 这样的性质,甚至可以得出更惊人的推论:
与
完全等价。
除了赋值、函数调用以及类型转换,其他的运算对于指向函数的指针没有意义,也是非法的。
指向函数的指针是解决某些复杂问题的一个非常巧妙的手法,它可以使代码更具有表现力、更简洁、更有美感。
9.10.4 例题
例题:编程,在键盘上输入:
这样的表达式,要求程序按照C语言的表达式的规则计算其值。
补充说明如下。
- 键盘输入格式为ddd…doddd…doddd…d,其中ddd…d表示连续的十进制字符序列,所得到的数值不超过int的表示范围,且表达式求值中和最后的结果也不超过“int”的表示范围。
- o表示“+”、“-”、“*”、“/”、“%”这5个运算符中的一个。
讨论:由所规定的输入格式,显然可以理解为"%d%c%d%c%d "并通过调用scanf()函数获取这些数据。之后需要考虑的是两个运算符的优先级问题。根据优先级关系的不同,可以借助switch语句完成运算,这种写法究竟有多烦琐可以自己试写一下。
下面的代码演示了指向函数的指针的用法,并且假定输入没有任何错误。
程序代码9-30
- /*
- 编程,在键盘上输入
- 1+2*3
- 这样的表达式,要求程序按照C语言的表达式的规则计算其值。
- */
- #include <stdio.h>
- #include <stdlib.h>
- #define SHI 1
- int yxjg( char , char ) ;
- int jia ( int , int ) ;
- int jian ( int , int ) ;
- int cheng ( int , int ) ;
- int chu ( int , int ) ;
- int qiuyu ( int , int ) ;
- int (*qiuys(char))(int,int) ;
- int main( void )
- {
- int czs1 , czs2 , czs3; //共三个操作数
- char ysf1 , ysf2; //两个运算符
- int zhi; //表达式的值
- int (*ys1)(int,int) , (*ys2) (int,int) ; //两个运算
- //输入
- printf("请输入一个三元算术表达式\n");
- scanf("%d%c%d%c%d",&czs1,&ysf1,&czs2,&ysf2,&czs3);
- //确定与运算符对应的函数
- ys1 = qiuys ( ysf1 ) ;
- ys2 = qiuys ( ysf2 ) ;
- if(yxjg(ysf2,ysf1)==SHI)
- zhi = ys1(czs1,ys2(czs2,czs3)) ;
- else
- zhi = ys2(ys1(czs1,czs2),czs3) ;
- printf("%d%c%d%c%d=%d\n",czs1,ysf1,czs2,ysf2,czs3,zhi);
- system("PAUSE");
- return 0;
- }
- //判断运算符ysf2是否比ysf1优先级高
- int yxjg( char ysf2, char ysf1)
- {
- if ( ysf2 == '*' || ysf2 == '/' || ysf2 == '%' )
- if ( ysf1 == '+' || ysf1 == '-' )
- return SHI ;
- return ! SHI;
- }
- int jia ( int s1 , int s2 )
- {
- return s1 + s2 ;
- }
- int jian ( int s1 , int s2 )
- {
- return s1 - s2 ;
- }
- int cheng ( int s1 , int s2 )
- {
- return s1 * s2 ;
- }
- int chu ( int s1 , int s2 )
- {
- if ( s2 == 0 )
- {
- printf("表达式有错误,按任意键退出\n");
- system("PAUSE");
- exit(1); //没什么好返回的,只能退出运行程序
- }
- return s1 / s2 ;
- }
- int qiuyu ( int s1 , int s2 )
- {
- if ( s2 == 0 )
- {
- printf("表达式有错误,按任意键退出\n");
- system("PAUSE");
- exit(1); //退出运行程序
- }
- return s1 % s2 ;
- }
- //求与运算符ysf对应的函数
- int (*qiuys(char ysf))(int,int)
- {
- if ( ysf == '+' )
- return jia ;
- if ( ysf == '-' )
- return jian ;
- if ( ysf == '*' )
- return cheng ;
- if ( ysf == '/' )
- return chu ;
- if ( ysf == '%' )
- return qiuyu ;
- }
程序运行结果如图9-30所示。
图9-30 指向函数的指针
代码中的函数调用exit(1)的作用是结束程序,并返回一个值“1”给操作系统,告之程序运行的最后状态。
9.11 指向虚无的指针
C语言中有一种数据类型是“void”类型,这种类型的特点就是没有任何值。
与这种类型相对应,C语言中还有一种“void *”类型的指针,这种指针不指向任何类型的内存对象,但具有一个值,这个值当然也是地址。只有对于这种类型的指针,说“指针就是地址”才是一种恰当的说法。对于其他类型说“指针就是地址”显然是掩盖了指针更为本质、更为重要的内涵—指针所指向的数据对象或函数的类型。
作为一种只有值而没有更多含义的“void *”类型的指针,其作用仅仅在于传递、保存这个值。“void *”类型的指针可以参加赋值运算(包括作为函数的参数)和类型转换运算,除此之外,“void *”类型的指针不可以进行其他任何运算,甚至一元“*”运算这种多数指针类型的基本运算也不可以。
但是“void*”类型指针的最大优点在于,无论什么类型的指针赋值给“void*”都不用类型转换,反之亦然。然而不少严谨的人士却并不领这个情,他们一如既往地、明白地写出这种转换,尽管他们知道这不是必须的。
在写函数定义时,可能并不清楚函数的调用者会提供什么样的指针,这时只能把对应的形参声明为“void *”类型;同样也有可能不清楚函数调用者需要什么样的指针,这时也只能把函数的返回值声明为“void*”类型。
9.12 参数不确定的函数
到此为止,至少有一类函数的实现方式和工作原理我们尚未提到,这就是最常用到的printf()函数和scanf()函数。
这两个函数的特点是,它们的定义(甚至编译)都是在被调用之前完成的,但是这两个函数的作者并不清楚调用这两个函数的人究竟要用几个什么样的实参。然而这两个函数竟然被写出来了,而且编译后确实能够很好地工作。
还可以提出这样类似的问题:在不清楚数量和类型的情况下,如何写一个求几个数(可能是整数也可能是小数)的平均值的函数。
为此,首先剖析一下实现printf()函数的技术手段,研究一下它的工作原理,然后再试写一个求若干个数的平均值的函数。
9.12.1 printf()的函数原型
由于经常使用printf()函数,在源代码中几乎总要写一行编译预处理命令。
这是因为在文件“stdio.h”中描述了“printf”这个标识符的含义,也就是函数原型。用记事本打开这个文件会发现这个函数原型是这个样子的:
- int printf (const char*, ...);
这里只关注这个函数原型所描述的形参的类型,我们发现第一个参数的类型是“const char*”,这很容易理解,而后面的参数的类型描述全然没有,只写了一个“…”。看来,这个“…”是解决任意个参数问题的一个要点。事实的确如此。
9.12.2 “…”是什么
从第一章中可以看到,“...”也是C语言的一个标点符号。其他的标点符号主要作为运算符或类型说明符,“{}”还可以作为很多情况下某种语言元素开始和结束的标记。但“...”这个标点符号只用于函数声明和定义(此外还用于宏),它的作用是让编译器对出现在这部分的实参与形参不做类型与个数的检查。
此外在函数声明和定义中使用“...”时有一个限制,只能指定后面的参数,且它的前面必须有确定类型的参数。比如
是合法的。但
- void f(…);
- void f(…,int);
都不合法。至于理由,后面将会看到。
9.12.3 实现原理
首先考察一个简单的函数调用过程。
程序代码9-31
- #include <stdio.h>
- #include <stdlib.h>
- int qh(int,int);
- int main(void)
- {
- int m=3,n=4;
- printf("%d\n" , qh(m,n) );
- system("Pause");
- return 0;
- }
- int qh(int i ,int j)
- {
- return i+j;
- }
在第6章中曾经提到,在进行函数调用运算时,计算机首先要求出各个实参的值,然后被调用函数的形参将把这些值作为自己的初始值。
这就是说,在程序代码9-31中,在进行qh(m,n)函数调用时,形参“i”、“j”用到的只是“m”、“n”的(右)值而不是“m”、“n”本身,这一点首先应该十分清醒。换句话说,函数调用时,“m”、“n”的值被复制到了其他地方,而这个地方恰恰就是形参占据的内存。如图9-31所示,显示了形参与实参之间的这种关系。
图9-31 函数调用之初
函数的形参一旦获得了初值就可以进行运算了。
特别要注意的是,在图9-
31中的两个形参,也就是“i”、“j”,是排在一起的,这是不确定参数实现的关键。
毫无疑问,在qh()函数中通过“&”运算可以求得指向“i”的指针“&i”,而一旦两个形参排列在一起的话,那么在数值上“&i+1”和指向“j”的指针“&j”是相等的,这个值就是“(void *) (&i+1)”。如果事先知道了第二个参数“j”的类型,那么就可以求出指向第二个参数“j”的指针。现在假定qh()函数的作者知道“j”的类型为“int”,那么他就完全可以根据第一个参数的信息和“j”的类型得到指向第二个参数的指针“(int *)(&i+1)”,而一旦他知道了这个指针,也就意味着他知道了第二个参数的一切。
因此qh()函数中的“return i+j;”语句也可以这样写:
- return i + * (int *)(&i+1);
这个return语句只用到了第二个参数“j”的类型“int”,而没有使用“j”这个参数。
结论就是,在形参相邻及知道第二个参数类型的前提下,从第一个实参也就是第一个形参的初值可以得到第二个实参也就是第二个形参的初值,这样第二个形参就完全没有必要了。代码也可以写成:
程序代码9-32
- #include <stdio.h>
- #include <stdlib.h>
- int qh(int,...);
- int main(void)
- {
- int m=3,n=4;
- printf("%d\n" , qh(m,n) );
- system("Pause");
- return 0;
- }
- int qh(int i ,...)
- {
- return i+*(int *)(&i+1);
- }
对于参数个数不确定的情形是类似的,比如编写一个求若干(>0)个“double”数据平均值的函数,可以通过函数的第一个实参传入“double” 数据的个数。代码可以写成:
程序代码9-33
- #include <stdio.h>
- #include <stdlib.h>
- double qpj(const int,...);
- int main(void)
- {
- printf("%lf\n" , qpj(1,1.2) ); //测试
- printf("%lf\n" , qpj(2,1.5,1.9) ); //测试
- system("Pause");
- return 0;
- }
- double qpj(const int n ,... )
- {
- double he =0.0 , * p_d = NULL ;
- int i ;
- p_d = (double *)( &n + 1 );//指向第一个double量
- for ( i = 0 ; i < n ; i++ , p_d ++ )
- he += *p_d ;
- return he/n;
- }
输出为:
1.200000
1.700000
请按任意键继续. . .
这就是不确定参数函数实现的基本原理,前提条件是形参在内存中的排列遵守一定的规则,且“…”所代表的各个参数的类型和个数都已知。一般情况下,“…”所代表的各个参数的类型和个数是通过前面确定参数传入的。例如:
- printf("%d,%c,%lf",123,65,34.0);
在“%d,%c,%f”中就包含有后面参数个数为3,类型分别为“int”,“int”,“double”的信息。
此外要说明的是,形参在内存中的次序规律在不同的环境下是不同的,所以求未定参数的方法也不同。本小节代码中的写法并不具有一般性,只是原理性的示意代码,换句话说没有可移植性。如果希望写出具备可移植性的代码,则需要采用下一小节中的方法。
9.12.4 标准形式
为了保证不确定参数函数代码的可移植性,C语言标准库提供了一套宏。尽管这套宏具有很好的可移植性,但使用起来非常笨拙且程式化,含义非常抽象难解,因此本书在每个步骤后都提供了一个不严格的非正式注解,以帮助读者理解。
这套宏的定义写在stdarg.h文件中,因此需要首先写编译预处理命令。
(1)#include
(2)va_list ap;/*这个“ap”用于遍历各个“…”中的参数。“va_list”是什么类型?是“…”类型。“…”是什么类型?不清楚。实际上这应该是个“void *”,但这是我猜的。*/
(3)va_start(ap,最后一个确定参数的类型)/*这是让“ap”获得初始值,也就是指向第一个可变参数。应该是“ap = (void)(&最后一个确定参数+1)”,这也是我猜的。*/
(4)va_arg(ap,可变参数的类型) /*这句的含义是求当前可变参数的值并把“ap”移至下一个可变参数。大体上应该是“*((可变参数的类型 *)ap)++”,然而“((可变参数的类型 *)ap)++”并不合法,所以这里很可能还需要其他编译手段,比如借助临时变量等。*/
(5)va_copy(dst,src) /*这是C99新增加的内容,可以复制一个“ap”的副本,在“src”被改变的情况下,一旦需要,还可以从前面重新读取参数。*/
(6)va_end(ap) /*这是在读完参数后对前面可能用到的临时变量等进行清理。*/
从前面几条可以看出,C语言已经把不确定参数的使用完全程式化地包装起来,并把实现细节完全留给了编译器。如果不是针对具体的环境,很难琢磨其中具体的技术实现细节。下面代码是前面小节中例题的标准化写法。
程序代码9-34
- #include <stdio.h>
- #include <stdlib.h>
- #include <stdarg.h>
- double qpj(const int,...);
- int main(void)
- {
- printf("%lf\n" , qpj(1,1.2) ); //测试
- printf("%lf\n" , qpj(2,1.5,1.9) ); //测试
- system("Pause");
- return 0;
- }
- double qpj(const int n ,... )
- {
- double he =0.0 ;
- int i ;
- va_list ap ; //这是老生常谈的写法
- va_start(ap,n); //这句需要记一下
- for ( i = 0 ; i < n ; i++ )
- he += va_arg(ap,double) ; //这句最主要
- va_end(ap);//这也是老生常谈的写法
- return he/n;
- }
输出为:
1.200000
1.700000
请按任意键继续. . .
小结概念与术语
- 指针(Pointer)是C语言中的一类数据类型的统称,这类数据类型专门用来存储和表示内存单元的编号—地址。
- 指针数据类型是一种需要借助其他数据类型才能构造出来的数据类型。
- 指针也泛指具有指针数据类型的数据对象。
- 指针数据类型特定的类型说明符是“*”。
- 指针总是和另外一种具体的数据类型联系在一起。
- 根据指针所关联的数据类型,可以把指针分为三类:数据指针、函数指针和空指针(void *)。这三类指针拥有的运算种类的集合不同。
- “数据对象”是指内存中一段以byte为单位的、特定长度的、连续的区域,这段内存区域中的内容具有数据类型的含义。
- 函数类型不属于数据对象。
- 数据指针的基本运算有“*”和“+1”,这是理解数据指针的基础,而这两个运算都是与指针的类型息息相关的。因此理解数据指针的根本在于理解指针的类型。
- “&”是求得指向运算对象的指针的运算,“&”的运算对象通常是变量,但更一般的是它的运算对象是一个左值表达式—表示一块内存的表达式。
- 称指针“指向某种类型数据”是指这个指针指向那种数据所占据的内存整体。
- 对于某个左值表达式“E”,“*&E”得到的依然是“E”。
- 如果“*p”有意义,那么“&*p”一定是“p”。
- 所有类型的指针都可以进行赋值运算,但一般应该用相同类型的指针赋值。
- “[]”运算是用“*”运算定义的 :e1[e2]≡(*((e1)+(e2)))
- 数组名作为一个值参与运算的时候是指向数组首个元素的指针,且是一个指针常量。
- 数组名参与“sizeof”、“&”等运算时,含义是数组所占据的内存空间。
- 没有内存含义的非左值表达式做“&”运算是错误的。
- “*”可以作为乘法运算符,可以作为指针类型说明符,也可以作为间接引用运算符。在具体的场合下才能确定。
- 指向具体数据类型的指针“p”可以与整数类型数据进行“+”运算,“p+i”的含义是得到指向“p”指向数据对象后面第“i”个这样类型数据对象的指针。但是“p”或“p+i”都应该指向某个有意义的数据对象或某个有意义的数据对象之后的首个“虚拟”的数据对象。
- 指针可以用来通过调用函数改变本地局部变量的值。
- 指针必须正确恰当地初始化之后才可以进行“*”或“[]”运算。
- 两个类型相同且指向同一数组内元素或数组后面一个同类型“数据”的指针可以做减法运算,结果为“int”,含义是元素下标之差。
- 两个类型相同且指向同一数组内元素或数组后面一个同类型“数据”的指针可以做关系运算,结果表示两个指针的前后相对位置关系。
- 两个相同类型的数据指针做“==”或“!=”这两个等式运算的含义十分明显,无非是它们所指向的数据是否为同一个。
- “type []”类型的形参等价于“type *”类型的形参。
- “高维数组名”作为左值表示一个数组,作为右值是一个指针。
- “*高维数组名”或“高维数组名[0]”作为左值表示一个数组,作为右值是一个指针。
- 变量长度数组是指尺寸用变量描述的数组,并不是长度可变的数组(C99)。
- 用变量作为类型说明符的类型叫变量修饰类型(C99)。
- 在函数原型或函数定义中出现的标识符必须首先说明(C99)。
- C99允许直接写出数组类型的字面量,这种字面量可以进行与数组名同样的运算。
- 在C99中,“(类型名){字面量列表}”是运算符,优先级同“[]”等运算符。
- 可以通过指向结构体的指针进行“->”运算访问结构体的成员。
- 函数名是指向函数类型的指针常量。
- 函数名==*函数名==&函数名
- 指向函数的指针可以进行“=”、“*”运算和“()”运算(函数调用运算)。
- “void *”是纯粹的地址,一般用来传递指针的值。
- C语言中的函数可分为三类,无参函数、确定参数函数及参数不确定的函数。
- “…”用来描述参数不确定函数的函数原型和函数定义中参数不确定的部分。
- “…”只能描述函数最后的几个参数,前面的参数必须是确定的。
- 为了使程序具有可移植性,一般应通过“stdarg.h”中定义的宏实现参数不确定函数。
常见错误
- 典型错误:“int *p=3;”,这里“*”是类型说明符不是运算符。这个错误写法的真正含义是“int *p;p=3;”。
- 类似“Suspicious pointer conversion in”的警告通常意味着错误,这个警告一般出现在指针类型与要求不一致的场合。
- 调用printf()函数输出指针的值应该用“%p”格式说明符,使用“%u”在一些场合会发生错误。
- 指向数组元素的指针经加减运算后得到的指针不指向数组元素或数组后面一个虚拟的元素。
- int *f(void) {int i; return &i}:这个函数返回的是一个指向局部auto类别变量的指针。然而函数调用之后,“i”已经不存在了。
风格
- 每定义一个指针变量,要么初始化为0(NULL),要么使它指向恰当的位置。
牛角尖 对于下面的程序片段
- n =1 ;
- do
- {
- int a[n][n];
- static int (*p)[n] = a ;
- /*其他*/
- }
- while ( n ++ < 10);
需要特别注意的是,每次进入循环体“a”的尺寸是变化的,但是由于“p”是static类别,“p”的值和类型却一直保持不变。这可能会带来很大的问题。
练习与自测
(1)写一个能化简分数的函数,并测试。
(2)一个旅行社要从n个旅客中选出一名旅客,为他提供免费的环球旅行服务。旅行社安排这些旅客围成一个圆圈,从帽子中取出一张纸条,用上面写的正整数m(编程对某个给定的n = 8与m = 3,给出被淘汰出列的旅客编号,以及最终的幸存者。
(3)围绕着山顶有10个洞,狐狸要吃兔子,兔子说:“可以,但必须找到我,我就藏身于这10个洞中,你从10号洞出发,先到1号洞找,第二次隔1个洞找,第三次隔2个洞找,以后如此类推,次数不限。”但狐狸从早到晚进进出出了1 000次,仍没有找到兔子。问兔子究竟藏在哪个洞里?
(4)某人将一缸鱼分五次出售,第一次卖出全部的一半加二分之一条,第二次卖出余下的三分之一加三分之一条,第三次卖出余下的四分之一加四分之一条,第四次卖出余下的五分之一加五分之一条,最后卖出余下的11条。问原来有多少鱼。
(5)如下图所示,分别找出和为最大及和为最小的4个相邻的数。
(6)编写函数将一个一维数组中的元素颠倒顺序。
(7)编写函数求一个二维矩阵的鞍点(在行上最大、在列上最小)。
(8)编写函数将数组中下标为偶数的元素与各自下一个元素对调位置,如果数组元素个数为奇数,则最后一个元素不动。
(9)写一个函数实现将一个方阵的行列互换。
(10)按递增顺序依次列出所有分母小于等于40的最简单真分数。