c语言数组指针用法举例,c语言指针数组经典题目详解
c语言系列:5。指针和数组文章目录C语言系列:5。指针和数组1。指针和地址2。指针和函数参数3。指针和数组4。地址算术运算5。字符指针和功能6。指针数组和指向指针7的指针。多维数组8。指针数组9的初始化。指针和多维数组10。命令行参数11。指向函数12的指针。复杂声明。
指针是保存变量地址的变量。在C语言中,指针被广泛使用。原因之一是指针通常是表达计算的唯一方式。另一个原因是,与其他方法相比,指针通常可以生成更高效、更紧凑的代码。指针和数组的关系非常密切。在本章中,我们将讨论它们之间的关系,并探讨如何利用这种关系。
和指针goto语句一样,会让程序难以理解。如果用户不小心,指针很容易指向错误的地方。然而,如果你小心地使用指针,你可以用它们来编写简单明了的程序。我们将在本章中尽力解释这一点。
ANSI最重要的变化之一是它明确地设置了操作指针的规则。事实上,这些规则已经被许多优秀的程序员和编译器所采用。此外,ANSI C使用类型void *(指向void的指针)而不是char *作为通用指针的类型。
1.指针和地址。首先,我们通过一个简单的示意图来说明存储器是如何组织的。通常,机器有一系列连续编号或寻址的存储单元,有些存储单元可以单独或成组操作。通常,机器的一个字节可以存储char类型的数据,两个相邻的字节存储单元可以存储short类型的数据,四个相邻的字节存储单元可以存储long类型的数据。指针是一组可以保存地址的存储单元(通常是两个或四个字节)。因此,如果C的类型是char,P是指向C的指针,它们之间的关系可以如图5-1所示:
一元运算符可以用来取一个对象的地址,因此,下面的语句:
p=c;将C的地址赋给变量P,我们称P为指向C的指针,地址运算符只能应用于内存中的对象,即变量和数组元素。它不能应用于register类型的表达式、常数或变量。
一元运算符*是间接寻址或间接引用运算符。当它作用于指针时,它将访问指针所指向的对象。这里我们假设x和y是整数,而ip是指向int类型的指针。以下代码段解释了如何在程序中声明指针,以及如何使用运算符和*:
int x=1,y=2,z[10];
int * ip/* ip是指向int的指针*/
IP=x;/* ip现在指向x */
y=* ip/* y现在是1 */
* IP=0;/* x现在是0 */
IP=z[0];/* ip现在指向z[0] */我们在上一章已经看到了变量x,y,z的声明。我们来看指针ip的语句,如下:
int * ip这种说法是为了便于记忆。这个声明语句表明表达式*ip的结果是int类型的。这个声明变量的语法类似于声明它的表达式的语法。出于同样的原因,函数的声明也可以这样进行。
例如,声明:
double *dp,atof(char *);说明在表达式中*dp和atof(s)的值是double,atof的参数是指向char的指针。
我们要注意,指针只能指向特定类型的对象,即每个指针必须指向特定的数据类型。(一个例外是,指向void类型的指针可以保存指向任何类型的指针,但它不能间接引用自身。我们将在5.11节详细讨论这个问题)。
如果指针ip指向一个整数变量,那么*ip可以用在任何X可以出现的上下文中,所以语句
* ip=* ip 10将*ip的值增加10。
一元运算符*和比算术运算符具有更高的优先级,因此赋值语句
Y=*ip 1会取出*ip指向的对象的值加1,然后把结果赋给Y,下面的赋值语句:
*ip=1将ip指向的对象的值加1,这相当于
*ip或
(*ip)语句的执行结果。语句(*ip)中的括号是必需的。否则,这个表达式将在ip上加1,而不是在ip指向的对象上加1。这是因为一元运算符如*和遵循从右到左的组合顺序。
最后,因为指针也是变量,所以可以在程序中直接使用,不需要间接引用。例如,如果iq是另一个指向整数的指针,那么语句
Iq=ip中的值会被复制到iq中,这样指针IQ也会指向IP所指向的对象。
2.指针和函数参数,因为C语言通过传值的方式将参数值传递给被调用的函数。因此,被调用的函数不能直接修改主调函数中变量的值。例如,排序函数可能使用名为swap的函数来交换两个无序的元素。然而,如果交换函数被定义为如下形式:
void swap(int x,int y) /*错误*/
{
内部温度;
temp=x;
x=y;
y=温度;
}下面的语句无法达到这个目的。
互换(a,b);这是因为,因为参数是按值传递的,所以上面提到的swap函数不会影响调用它的例程中参数A和B的值。这个函数只交换A和b的副本的值。
那么,怎样才能达到目的呢?我们可以让调用程序把要交换的变量的指针传递给被调用函数,即:
互换(a,b);因为一元运算符是用来取变量的地址的,所以A是指向变量A的指针,swap函数的所有参数都声明为指针,它们所指向的操作数都是通过这些指针间接访问的。
void swap(int *px,int *py) /* interchange *px和*py */
{
内部温度;
temp=* px
* px=* py
* py=temp
}我们通过图5-2来说明。
指针使被调用函数能够访问和修改调用函数中对象的值。我们来看这样一个例子:函数getint接受自由形式的输入,进行变换,将输入的字符流分解成整数,每次调用都得到一个整数。Getint需要返回转换后的整数,当到达输入结束时,需要返回文件结束标记。这些值必须以不同的方式返回。EOF(文件结束标记)可以用任意值表示,但也可以用输入整数表示。
这个函数可以这样设计,将表示是否已经到达文件末尾的状态作为getint函数的返回值,同时使用指针参数存储转换后的整数并返回给主调用函数。在函数scanf的实现中采用了这种方法。详见第7.4节。
下面的loop语句调用getint函数为整数数组赋值:
int n,array[SIZE],getint(int *);
for(n=0;n SIZE getint( array[n])!=EOFn)每次调用getint,输入流中的下一个整数都会被赋给数组元素array[n]。同时,n的值将增加1。请注意,数组[n]的地址必须传递给函数getint,否则函数getint将无法将转换后的整数返回给调用者。
这个版本的getint函数在到达文件末尾时返回EOF,在下一个输入不是数字时返回0,在输入包含有意义的数字时返回正值。
#包含ctype.h
int getch(void);
void un getch(int);
/* getint:从*pn的输入中获取下一个整数*/
int getint(int *pn)
{
int c,sign
while(is space(c=getch())/*跳过空白*/
;
如果(!isdigit(c) c!=EOF c!= c!=-) {
un getch(c);/*它不是一个数字*/
返回0;
}
sign=(c==-)?-1 : 1;
if (c== c==-)
c=getch();
for(* pn=0;isdigit(c),c=getch())
* pn=10 * * pn(c- 0 );
*pn *=符号;
如果(c!=EOF)
un getch(c);
返回c;
}在getint函数中,*pn总是作为普通的整数变量使用。还使用了getch和ungetch两个函数(参见第4.3节)。有了这两个函数,一个必须由getint函数读入的额外字符可以被写回到输入中。
3.指针和数组在C语言中,指针和数组的关系非常密切。因此,在下一部分,我们将同时讨论指针和数组。任何可以用数组下标完成的操作都可以用指针来实现。一般来说,用指针编写的程序比用数组下标编写的程序快,但另一方面,用指针实现的程序理解起来稍微困难一些。
声明
int a[10];定义了长度为10的数组A。换句话说,它定义了一组10个对象,这些对象存储在相邻的内存区域中。这些名称是a[0],a[1],…,a[9](见图5-3)。
A[i]表示数组的第I个元素。如果pa的声明是
int * pa那么它就是一个指向整数对象的指针。然后,赋值语句
pa=a[0];可以将指针pa指向数组A的第0个元素,即pa的值就是数组元素a[0]的地址(见图5-4)。
这样,赋值语句
x=* pa将数组元素a[0]的内容复制到变量x中。
如果pa指向数组中的一个特定元素,那么根据指针操作的定义,pa 1将指向下一个元素,pa i将指向pa所指向的数组元素之后的第I个元素,pa-i将指向pa所指向的数组元素之前的第I个元素。因此,如果指针pa指向a[0],那么*(pa 1)指的是数组元素a[1]的内容,pa i是数组元素a[i]的地址,*(pa i)指的是数组元素a[i]的内容(见图5-5)。
无论数组A中元素的类型或数组长度是什么,上述结论都成立。“指针加1”是指pa 1指向pa所指对象的下一个对象。相应地,pa i指向pa所指向的对象之后的第I个对象。
下标指针操作和下标指针操作之间有着密切的对应关系。根据定义,数组类型的变量或表达式的值是数组的第0个元素的地址。执行赋值语句
pa=a[0];之后,pa和a的值相同。因为数组名代表数组第一个元素的地址,所以赋值语句pa=a[0]也可以写成以下形式:
pa=a;对数组元素a[i]的引用也可以写成*(a i)的形式。第一次接触这种写法的人可能会觉得很陌生。C语言在计算数组元素a[i]的值时,实际上是先将其转换成*(a i)的形式,再进行求值,所以在程序中两种形式是等价的。如果将地址运算符应用于这两个等价表达式,可以得出结论,a[i]和a i具有相同的含义。a是a后面第I个元素的地址,相应的,如果pa是指针,也可以在表达式中在它后面加一个下标。Pa[i]和*(pa i)是等价的。简而言之,一个用数组和下标实现的表达式,可以等价地用指针和偏移量实现。
但是,我们必须记住,数组名和指针是有区别的。指针是一个变量。因此,在C语言中,语句pa=a和pa都是合法的。但是数组名不是变量,所以像a=pa和a这样的语句是非法的。
当一个数组名被传递给一个函数时,实际上传递的是数组第一个元素的地址。在被调用的函数中,这个参数是局部变量,所以数组名参数必须是指针,也就是存储地址值的变量。我们可以用这个特性写另一个版本的strlen函数,用来计算一个字符串的长度。
/* strlen:返回字符串s的长度*/
整数字符串(char *s)
{
int n;
for(n=0;*s!=\0 ,s)
n;
返回n;
}因为s是指针,所以对它进行自增操作是合法的。执行s操作不会影响strlen函数调用者中的字符串,它只对strlen函数中指针的私有副本执行自增操作。因此,函数调用如下:
strlen(‘你好,世界’);/*字符串常量*/
strlen(数组);/* char数组[100];*/
strlen(ptr);/* char * ptr;*/都可以正确执行。
在函数定义中,形参
char s[];和
char * s;是等价的。我们通常更习惯使用后一种形式,因为它比前者更直观地说明参数是指针。如果将数组名传递给函数,函数可以根据情况决定将其作为数组还是指针对待,然后按照相应的方式对参数进行操作。为了直观恰当地描述函数,你甚至可以在函数中同时使用数组和指针。
还可以将指向子数组起始位置的指针传递给函数,从而将数组的一部分传递给函数。例如,如果是一个数组,那么下面两个函数调用
F( a[2])和
F(a 2)会将从a[2]开始的子数组的地址传递给函数f,在函数f中,参数的声明形式可以是
F(int arr[]) {.}或者
F(int *arr) {.}对于函数f来说,它并不关心被引用的元素是否只是一个更大数组的一部分。
如果确定对应的元素存在,也可以通过下标访问数组第一个元素之前的元素。p[-1]和p[-2]等表达式在语法上都是合法的,它们分别指代p[0]之前的两个元素。当然,引用数组边界之外的对象是非法的。
4.地址算术运算。如果p是指向数组中某个元素的指针,那么p会对p进行自递增运算,指向下一个元素,而p=i会对p进行递增运算,使其指向指针p当前指向的元素之后的第I个元素,这种运算是指针或地址算术运算的最简单形式。
C语言中地址的算术运算具有一致性和规律性,将指针、数组和地址的算术运算集成在一起是这种语言的一大优势。为了说明这一点,让我们看一个不完美的存储分配程序。它由两个功能组成。第一个函数alloc(n)返回一个指向n个连续字符存储位置的指针,alloc函数的调用者可以用它来存储字符序列。第二个函数afree释放已分配的存储空间供以后重用。这两个函数之所以“不完美”,是因为调用afree函数的顺序必须与调用alloc函数的顺序相反。换句话说,alloc和afree以堆栈的方式(即后进先出列表)管理存储空间。标准库中提供了具有类似功能的函数malloc和free。它们没有上述限制。我们将在8.7节解释如何实现这些功能。
最简单的方法是让alloc函数在一个大字符数组allocbuf中分配空间。应该
数组是alloc和afree函数的私有数组。因为由函数alloc和afree处理的对象引用
而不是数组下标,其他函数不需要知道数组的名字,所以可以在包含alloc和afree的源文件中将数组声明为静态类型,使其对外界不可见。实际上,数组甚至可以没有名字。可以通过调用malloc函数或者向操作系统申请一个指向未知内存块的指针来获得。
allocbuf中的空间使用情况也是我们需要了解的信息。我们使用指针allocp来指向
allocbuf的下一个空闲单元。当调用alloc请求n个字符的空间时,alloc会检查
allocbuf数组中还有足够的空间吗?如果有足够的空闲空间,alloc返回allocp的当前值(即空闲块的起始位置),然后在allocp上加n,使其指向下一个空闲区。如果没有足够的可用空间,Alloc返回0。如果p在allocbuf的边界内,afree只需将allocp的值设置为p(见图5-6)。
#define ALLOCSIZE 10000/*可用空间的大小*/
静态char allocbuf[ALLOCSIZE];/*用于分配的存储*/
静态char * allocp=allocbuf/*下一个空闲位置*/
char *alloc(int n) /*返回指向n个字符的指针*/
{
if(allocbuf ALLOCSIZE-allocp=n){/* it fits */
allocp=n;
返回allocp-n;/*老p */
} else /*没有足够的空间*/
返回0;
}
void afree(char * p)/* p指向的可用存储*/
{
if(p=allocbuf p allocbuf ALLOCSIZE)
allocp=p;
}
一般来说,像其他类型的变量一样,指针也可以初始化。通常,对指针有意义的初始化值只能是0或表示地址的表达式。对于后者,表达式所表示的地址必须是具有之前已经定义的适当数据类型的地址。例如,声明
静态char * allocp=allocbufAllocp定义为字符型指针,初始化为allocbuf的起始地址,也就是程序执行时的下一个空闲位置。上述语句也可以写成以下形式:
static char * allocp=alloc buf[0];这是因为数组名实际上是数组第0个元素的地址。
下面是if测试语句:
if(allocbuf allocsize-allocp=n){/* it fits */检查是否有足够的空闲空间来满足n个字符的存储空间请求。如果有足够的空闲空间,分配存储空间后allocp的新值最多比allocbuf的尾地址大1。如果存储空间的申请可以满足,alloc将返回一个指针,指向所需大小的字符块头的地址(注意函数本身的声明)。如果不能满足应用程序,alloc必须返回某种形式的信号,以表明没有足够的空闲空间可供分配。c语言保证0永远不是有效的数据地址,所以返回值0可以用来表示发生了异常事件。在本例中,返回值0表示没有足够的可用空间可供分配。
不能将指针转换成整数,但0是唯一的例外:可以将常数0赋给指针,指针可以与常数0进行比较。在程序中,经常用符号常量NULL代替常量0,这样更容易解释清楚常量0是指针的特殊值。符号常量NULL是在标准头文件stddef.h中定义的我们经常在后面部分使用NULL。
类似
if(allocbuf allocsize-allocp=n){/* it fits */and
IF(p=allocbuf p allocbuf allocsize)的条件测试语句表明指针算术运算具有以下重要特征。首先,有些情况下,指针是可以比较的。例如,如果指针P和Q指向同一个数组的成员,那么类似==,=,=的关系比较运算。如果P指向的数组元素的位置在Q指向的数组元素的位置之前,那么关系表达式
p的值为真。任何指针与0进行相等或不相等的比较都是有意义的。但是,指向不同数组元素的指针之间的算术或比较操作是未定义的。(这里有个特例:数组最后一个元素的下一个元素的地址可以用在指针的算术运算中。)
其次。从前面可以看出,指针可以对整数进行加减运算。例如,结构
p表示指针p当前指向的对象之后的第n个对象的地址。无论指针P指向什么类型的对象,上述结论都成立。在计算p ^ n时,n会根据p所指向的对象的长度进行缩放,而p所指向的对象的长度则取决于p的声明,例如,如果int类型占用了4个字节的存储空间,那么在int类型的计算中,对应的n会被计算为4的倍数。
指针减法也是有意义的:如果P和Q指向同一个数组中的元线,而P是Q,那么q-p 1就是位于P和Q所指向的元线之间的元素个数,由此,我们可以写出函数strlen的另一个版本,如下:
/* strlen:返回字符串s的长度*/
整数字符串(char *s)
{
char * p=s;
while (*p!=\0)
p;
返回p-s;
}在上述程序段的声明中,指针P被初始化为指向S,即指向字符串的第一个字符。The whi1e loop语句将依次检查字符串中的每个字符,直到遇到标识字符数组结尾的字符“\0”。由于p是指向一个字符的指针,所以每次执行p时,p都会指向下一个字符的地址,p-s表示已经检查的字符数,也就是字符串的长度。(字符串中的字符数可能超过int类型所表示的最大范围。头文件stddef.h中定义的ptrdiff_t类型足以表示两个指针之间的有符号差。不过这里我们用size_t作为函数strlen的返回值类型,可以匹配标准库中的函数版本。Size_t是由运算符sizeof返回的无符号整数。)
指针的算术运算是一致的:如果处理的数据类型是比字符类型占用更多存储空间的浮点类型,P是指向浮点类型的指针,那么P执行后,P会指向下一个浮点数的地址。因此,只有将alloc和afree函数中的char类型全部替换为float类型,才能得到一个适合浮点类型而不是字符类型的内存分配函数。的所有指针操作都会自动考虑它所指向的对象的长度。
的有效指针操作包括相同类型的指针之间的赋值操作;指针和整数之间的加法或减法;指向同一数组中元素的两个指针之间的减法或比较;将指针赋给0或将指针与0进行比较。所有其他形式的指针操作都是非法的,比如两个指针之间的加、乘、除、移位或屏蔽;指针浮点型或双精度型加法;一种操作,直接将指向一种类型对象的指针赋给指向另一种类型对象的指针,而不进行强制类型转换(除非两个指针中有一个是void *类型)。
5.字符指针和函数字符串常量是字符数组,例如:
我是字符串在字符串的内部表示中,字符数组以空字符 \0 结尾,所以程序可以通过检查空字符找到字符数组的结尾。因此,字符串占用的存储单元数比双引号中的字符数大1。
也许字符串最常见的用途是作为函数参数,例如:
princf(hello,world \ n );当程序中出现这样的字符串时,实际上是通过字符指针来访问的。在上面的语句中,printf接受一个指向字符数组第一个字符的指针。也就是说,字符串常量可以通过指向其第一个元素的指针来访问。
除了作为函数参数,字符串常量还有其他用途。假设指针pmessage声明如下:
char * pmessage所以,声明
pmessage=现在是时候了;将指向字符数组的指针赋给pmessage。这个过程不复制字符串,只涉及指针的操作。c语言不提供将整个字符串作为一个整体对待的运算符。
以下两个定义有很大的区别:
char amessage[]=nw是时间;/*定义一个数组*/
char *pmessage=现在是时候了;/* Define a pointer */在上面的语句中,amessage是一个一维数组,刚好可以容纳初始化字符串和空字符 \0 。数组中的单个字符可以修改,但是一条消息总是指向同一个存储位置。另一方面,pmessage是一个指针,它的初始值指向一个字符串常量。之后,可以修改它指向其他地址,但如果你试图修改字符串的内容,结果是未定义的(见图5-7)。
为了进一步讨论指针和数组的其他方面,下面以标准库中两个有用的函数为例,研究它们不同的实现版本。第一个函数strcpy(s,T)将指针T指向的字符串复制到指针s指向的位置,如果用语句s=t来实现这个函数,本质上只是复制了指针,而没有复制字符。为了复制字符,这里使用了循环语句。strcpy函数的第一个版本是通过数组方法实现的,如下所示:
/* strcpy:将t复制到s;数组下标版本*/
void strcpy(char *s,char *t)
{
int I;
I=0;
while ((s[i]=t[i])!=\0)
我;
}作为比较,下面的strcpy函数是用指针方法实现的:
/* strcpy:将t复制到s;指针版本*/
void strcpy(char *s,char *t)
{
int I;
I=0;
while ((*s=*t)!=\0) {
s;
t;
}
}因为参数是按值传递的,所以参数S和T可以在strcpy函数中以任何方式使用。这里,S和T是方便初始化的指针。每次执行循环时,它们沿着相应的数组前进一个字符,直到T中的终止符 \0 被复制到s。
实际上,strcpy函数不是这样写的。有经验的程序员更喜欢用下面的形式来写:
/* strcpy:将t复制到s;指针版本2 */
void strcpy(char *s,char *t)
{
while ((*s=*t)!=\0)
;
}在这个版本中,S和T的自增运算放在循环的测试部分。表达式*t的值是执行自动递增操作之前t指向的字符。后缀运算符意味着t的值在字符被读取后被改变。同样,在S执行自动递增操作之前,字符存储在指针S指向的旧位置。这个字符值也用于与空字符 \0 进行比较,以控制循环的执行。最后的结果是将T指向的字符依次复制到S指向的位置,直到遇到终止符 \0 (终止符也被复制)。为了进一步细化程序,我们注意到表达式和 \0 的比较是多余的,因为只需要判断表达式的值是否为0。因此,该函数可以进一步写成以下形式:
/* strcpy:将t复制到s;指针版本3 */
void strcpy(char *s,char *t)
{
while (*s=*t)
;
}这个函数乍一看不太好理解,但是这种表示方法非常有益。我们应该掌握这种方法,这种方法在C语言程序中经常用到。
标准中提供的函数strcpy(string . h)将目标字符串作为函数值返回。
我们研究的第二个函数是字符串比较函数strcmp(s,t)。该函数比较字符串s和t,
并根据s按字典顺序小于、等于或大于t的结果分别返回负整数、0或正整数。返回值是S和T从前到后逐字符比较时遇到的第一个不相等字符的字符差。
/* strcmp:如果s t则返回0,如果s==t则返回0,如果s t则返回0 */
int strcmp(char *s,char *t)
{
int I;
for(I=0;s[I]==t[I];我)
if (s[i]==\0 )
返回0;
return s[I]-t[I];
}下面的strcmp函数是在指针模式下实现的:
/* strcmp:如果s t则返回0,如果s==t则返回0,如果s t则返回0 */
int strcmp(char *s,char *t)
{
for(;* s==* t;s,t)
if (*s==\0 )
返回0;
return * s-* t;
}由于and既可以用作前缀运算符,也可以用作后缀运算符,因此运算符*也可以在其他方面与运算符and结合使用,但这些用法很少见。例如,下面的表达式
*-p在读取指针p所指的字符之前,对p进行自减运算,其实下面两个表达式:
* p=val/*将val压入堆栈*/
val=*-p;/*将栈顶元素弹出到val */是push和push的标准用法。有关更多详细信息,请参见第4.3节。
头文件string.h包含本节提到的函数的声明,以及标准库中其他字符串处理函数的声明。
6.指针数组和指向指针的指针因为指针本身就是变量,所以也可以像其他变量一样存储在数组中。这可以通过编写UNIX程序sort的简化版本来说明。该程序按字母顺序对一组文本行进行排序。
在第3章中,我们描述了一个shell排序函数,用于对整数数组中的元素进行排序,并在第4章中用快速排序对其进行了改进。这些排序算法在这里仍然有效,但现在它们处理的是不同长度的文本行。与整数不同,它们不能在单个操作中执行比较或移动操作。我们需要一种能够高效方便地处理变长文本行的数据表示方法。
我们引入指针数组来处理这个问题。如果要排序的文本行首尾相连地存储在一个长字符数组中,那么每个文本行都可以通过指向其第一个字符的指针来访问。这些指针本身可以存储在一个数组中。这样,指向这两个文本行的指针就可以传递给函数strcmp来比较这两个文本行。当两个逆序的文本行交换时,实际交换的是指针数组中两个文本行对应的指针,而不是两个文本行本身(见图5-8)。
这种实现方法消除了复杂的存储管理和移动文本行导致的巨大开销这两个问题。
分类过程包括以下三个步骤:
读取所有输入行,对文本行进行排序,并按顺序打印文本行。通常情况下,最好将程序分成几个与问题自然划分一致的函数,通过main函数控制其他函数的执行。关于文本行排序的步骤,我们将在后面解释。现在主要考虑数据结构和输入输出函数。
输入函数必须收集并保存每个文本行中的字符,并构建一个指向这些文本行的指针数组。同时,它必须计算输入行数,因为在排序和打印时需要这些信息。因为input函数只能处理有限数量的输入行,所以当输入行的数量超过有限的最大数量时,该函数将返回一个数值,如-1,以指示非法的行数。
输出函数只需要在指针数组中按顺序打印这些文本行。
#包含stdio.h
#包含字符串. h
#定义要排序的最大行数5000/*最大行数*/
char * line ptr[MAXLINES];/*指向文本行的指针*/
int readlines(char *lineptr[],int nlines);
void writelines(char *lineptr[],int nlines);
void qsort(char *lineptr[],int left,int right);
/*排序输入行*/
主()
{
国际线;/*读取的输入行数*/
if ((nlines=readlines(lineptr,MAXLINES))=0) {
qsort(lineptr,0,nlines-1);
writelines(lineptr,nlines);
返回0;
}否则{
printf(错误:输入太大,无法排序\ n );
返回1;
}
}
#定义MAXLEN 1000/*任何输入行的最大长度*/
int getline(char *,int);
char * alloc(int);
/* readlines:读取输入行*/
int readlines(char *lineptr[],int maxlines)
{
int len,nlines
char *p,line[MAXLEN];
nlines=0;
while ((len=getline(line,MAXLEN)) 0)
if(nlines=maxlines p=alloc(len)==NULL)
return-1;
否则{
line[len-1]= \ 0 ;/*删除换行符*/
strcpy(p,line);
lineptr[nlines]=p;
}
返回在线;
}
/* writelines:写入输出行*/
void writelines(char *lineptr[],int nlines)
{
int I;
for(I=0;在线;我)
printf(%s\n ,line ptr[I]);
}关于getline函数的详细信息,请参见1.9节。
在这个例子中,指针数组1ineptr的声明是一个重要的新概念:
char * line ptr[MAXLINES];这意味着1ineptr是一个包含MAXLINES元素的一维数组,其中数组的每个元素都是一个指向字符类型对象的指针。即lineptr[i]是一个字符指针,*lineptr[i]是这个指针指向的第I个文本行的第一个字符。
1因为1ineptr本身是一个数组名,所以它可以像上一个例子一样作为指针使用。这样,writelines函数可以重写为:
/* writelines:写入输出行*/
void writelines(char *lineptr[],int nlines)
{
while (nlines - 0)
printf(%s\n ,* line ptr);
}(注意这里的数组变量lineptr可以改变值)
循环执行开始时,*lineptr指向第一行,每次执行自动递增操作时,lineptr指向下。
行,同时减去nlines。
定义了输入输出函数的实现方法后,就可以开始考虑文本行的排序了。
这里,我们需要对第四章的快速排序函数做一些小的改动:首先,我们需要修改这个函数的声明部分;其次,我们需要调用strcmp函数来完成文本行的比较。但是排序算法在这里仍然有效,不需要做任何改变。
/* qsort:v排序[左].v[右]成递增顺序*/
void qsort(char *v[],int left,int right)
{
int i,last
void swap(char *v[],int i,int j);
if (left=right) /*如果数组包含*/
返回;/*少于两个元素*/
swap(v,left,(左右)/2);
last=左;
for (i=左1;i=对;我)
if (strcmp(v[i],v[left]) 0)
swap(v,last,I);
swap(v,左,最后);
qsort(v,左,last-1);
qsort(v,最后1,右);
}同样,swap函数只需要一些小的改动:
/* swap:互换v[i]和v[j] */
void swap(char *v[],int i,int j)
{
char * temp
temp=v[I];
v[I]=v[j];
v[j]=temp;
}因为V(别名lineptr)的所有元素都是字符指针,而temp也必须是字符指针,所以temp和V的任何元素都可以相互复制。
7.多维数组C语言提供了类似于矩阵的多维数组,但实际上并没有指针数组应用的那么广泛。本节将介绍多维数组的特征。
我们来考虑一下日期换算的问题,将某月的日期表达式换算成某年某日的表达式,反之亦然。例如,3月1日是非闰年的第60天,是闰年的第61天。这里,我们为日期转换定义了以下两个函数:函数day_of_year将一个月的日期表示转换为一年中某一天的表示,函数month_day执行相反的转换。因为后一个函数返回两个值,所以在函数month_day中,月和日这两个参数是指针的形式。例如,下面的语句:
月_日(1988,60,m,d);将m的值设置为2,d的值设置为29(2月29日)。
这些函数都使用一个记录每个月天数的表(如“九月有30天”等。).对于闰年和非闰年,每个月的天数是不同的,因此在计算过程中,将这些天存储在二维数组的两行中比判断二月有多少天更容易。执行日期转换的数组和函数如下:
静态char daytab[2][13]={
{0, 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31},
{0, 31, 29, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31}
};
/* day_of_year:从月日开始设置一年中的某一天*/
int day_of_year(int年,int月,int日)
{
int i,leap
闰年=年份%4==0 year0!=0 year @ 0==0
for(I=1;I月;我)
day=daytab[leap][I];
返回日;
}
/* month_day:从一年中的某一天开始设置月、日*/
void month_day(int year,int yearday,int *pmonth,int *pday)
{
int i,leap
闰年=年份%4==0 year0!=0 year @ 0==0
for(I=1;year day day tab[leap][I];我)
year day-=daytab[leap][I];
* p month=I;
* pday=yearday
}上一章我们说过,逻辑表达式的算术运算值只能是0(假时)或1(真时)。因此,在本例中,逻辑表达式leap可以用作数组daytab的下标。
数组daytab必须在函数day_of_year和month_day之外声明,这样这两个
所有函数都可以使用这个数组。这里之所以将daytab的元素声明为char类型,是为了说明在char类型的变量中存储更小的非字符整数是合法的。
到目前为止,daytab是我们遇到的第一个二维数组。在C语言中,二维数组实际上是一个特殊的一维数组,它的每个元素也是一维数组。因此,数组下标应该写成
TAB[I][J]/*[ROW][COL]*/而不是写成
Daytab[i,j] /*错*/除了表示方式的不同,C语言中二维数组的使用和其他语言是一样的。元素按行存储,所以当按存储顺序访问数组时,最右边的数组(即列)的索引变化最快。
数组可以用花括号括起来的初始值表初始化,二维数组的每一行都由对应的子列表初始化。在本例中,我们将数组daytab的第一列元素设置为0,这样月份的值就是1 ~ 12,而不是0 ~ 11。因为这里存储空间不是主要问题,所以这种方法比在程序中调整数组的下标更直观。
如果二维数组作为参数传递给函数,数组中的列数必须在函数的参数声明中指明。数组的行数没有太大关系,因为前面说过,调用函数时会传递一个指针,它指向一个由行向量组成的一维数组,其中每个行向量都是一个有13个整数元素的一维数组。在这个例子中,传递给函数的是一个指向许多对象的指针,其中每个对象都是一个由13个整数元素组成的一维数组。因此,如果数组daytab作为参数传递给函数f,那么f的声明应该按以下形式编写:
F(int daytab[2][13]) {.}也可以写成
F(int daytab[][13]) {.}因为数组中的行数无关紧要,所以声明也可以写成
F(int (*daytab)[13]) {.}这个声明形式表示参数是一个指针,指向一个有13个整数元素的一维数组。因为方括号[]优先于*,所以在上面的语句中必须使用括号。如果去掉括号,声明就变成了
Int *daytab[13]这相当于声明了一个有13个元素的数组,其中每个元素都是一个指向integer对象的指针。一般来说,除了数组的第一维(下标)之外,其他维度都必须显式指定。
我们将在5.12节进一步讨论更复杂的语句。
8.指针数组的初始化考虑这样一个问题:写一个函数month_name(n),返回一个指针,指向第n个月的名称的字符串。这是内部静态类型数组的理想应用。month_name函数包含一个私有字符串数组,当调用它时,它返回一个指向正确元素的指针。本节将解释如何初始化名称数组。
指针的初始化语法类似于前面提到的其他类型的对象:
/* month_name:返回第n个月的名称*/
char *month_name(int n)
{
静态char *name[]={
非法月份,
一月,二月,三月,
四月,五月,六月,
七月,八月,九月,
十月,十一月,十二月
};
}其中name的声明与排序示例中的lineptr相同,都是一维数组。数组的元素是字符指针。数组名的初始化是通过一个字符串列表来实现的,列表中的每个字符串都被赋给数组中相应位置的元素。第I个字符串的所有字符都存储在内存的某个地方,指向它的指针存储在name[i]中。由于上面的语句中没有指出name的长度,编译器将计算初始值的个数,并将这个精确的数填入数组的长度。
9.指针和多维数组对于C语言初学者来说,很容易混淆2D数组和指针数组的区别,比如上面例子中的name。如果有以下两个定义:
int a[10][20];
int * b[10];那么,从语法的角度来看,a[3][4]和b[3][4]都是对一个int对象的合法引用。但是A就是A
真二维数组,分配200个int型存储空间,用常规的矩阵下标计算公式20row col(其中row代表行,col代表列)计算元素a[row][col]的位置。但是,对于B来说,这个定义只分配了10个指针,并且它们没有被初始化。它们的初始化必须以显式的方式完成,比如静态初始化或代码初始化。假设B的每个元素指向一个有20个元素的数组,编译器会为它分配200个int-length存储空间和10个指针存储空间。指针数组的一个重要优点是数组的每行长度可以不同,即B的每个元素不一定要指向20个元素的向量,有的元素可以指向2个元素的向量,有的元素可以指向50个元素的向量,有的元素可以不指向任何向量。
虽然在上面的讨论中,我们都是借助integer进行讨论的,但是到目前为止,指针数组最常使用的是存储不同长度的字符串,比如函数month_name中的情况。结合下面的陈述和图形描述,我们可以做一个比较。下面是指针数组的声明和图形描述(见图5-9):
char *name[]={ 非法曼特,一月,二月,三月 };
下面是一个二维数组的声明和图形描述(见图5-10):
char aname[][15]={ 非法月份, Jan , Feb , Mar };
10.命令行参数在支持C语言的环境中,当程序开始执行时,可以将命令行参数传递给程序。当调用main函数main时,它有两个参数。第一个参数(习惯上称为argc,用于参数计数)的值表示运行程序时命令行中参数的个数;第二个参数(称为argv,用于参数向量)是一个指向字符串数组的指针,其中每个字符串对应一个参数。我们通常用多级指针来处理这些。
字符串。
最简单的例子是程序echo,它将命令行参数回显在屏幕上的一行中,其中命令行中各参数之间用空格隔开。也就是说,命令
echo hello,将打印下列输出:
hello,按照C 语言的约定,argv[0]的值是启动该程序的程序名,因此argc 的值至少为1。
如果argc的值为1,则说明程序名后面没有命令行参数。在上面的例子中,argc的值为3,argv[0]、argv[1]和argv[2]的值分别为“echo”、“hello,”,以及“world”。第一个可选参数为argv[1],而最后一个可选参数为argv[argc-1]。另外,ANSI 标准要求argv[argc]的值必须为一空指针(参见图5-11)。
程序 echo的第一个版本将argv看成是一个字符指针数组:
#include stdio.h
/* echo command-line arguments; 1st version */
main(int argc, char *argv[])
{
int i;
for (i = 1; i argc; i++)
printf("%s%s", argv[i], (i argc-1) ? " " : "");
printf("\n");
return 0;
}因为argv是一个指向指针数组的指针,所以,可以通过指针而非数组下标的方式处理命令行参数。echo程序的第二个版本是在对argv进行自增运算、对argc进行自减运算的基础上实现的,其中argv是一个指向char类型的指针的指针:
#include stdio.h
/* echo command-line arguments; 2nd version */
main(int argc, char *argv[])
{
while (--argc 0)
printf("%s%s", *++argv, (argc 1) ? " " : "");
printf("\n");
return 0;
}因为argv是一个指向参数字符串数组起始位置的指针,所以,自增运算(++argv)将使得
它在最开始指向argv[1]而非argv[0]。每执行一次自增运算,就使得argv指向下一个参数,*argv就是指向那个参数的指针。与此同时,argc执行自减运算,当它变成0 时,就完成了所有参数的打印。
也可以将printf语句写成下列形式:
printf((argc 1) ? "%s " : "%s”, *++argv);这就说明,printf的格式化参数也可以是表达式。
我们来看第二个例子。在该例子中,我们将增强4.1节中模式查找程序的功能。在4.1 节中,我们将查找模式内置到程序中了,这种解决方法显然不能令人满意。下面我们来效仿UNIX程序grep的实现方法改写模式查找程序,通过命令行的第一个参数指定待匹配的模式。
#include stdio.h
#include string.h
#define MAXLINE 1000
int getline(char *line, int max);
/* find: print lines that match pattern from 1st arg */
main(int argc, char *argv[])
{
char line[MAXLINE];
int found = 0;
if (argc != 2)
printf("Usage: find pattern\n");
else
while (getline(line, MAXLINE) 0)
if (strstr(line, argv[1]) != NULL) {
printf("%s", line);
found++;
}
return found;
}标准库函数strstr(s, t)返回一个指针,该指针指向字符串t在字符串s中第一次出现的
位置;如果字符串t 没有在字符串s 中出现,函数返回NULL(空指针)。该函数声明在头文件 string.h 中。
为了更进一步地解释指针结构,我们来改进模式查找程序。假定允许程序带两个可选参数。其中一个参数表示“打印除匹配模式之外的所有行”,另一个参数表示“每个打印的文本行前面加上相应的行号”。
UNIX 系统中的C语言程序有一个公共的约定:以负号开头的参数表示一个可选标志或参数。假定用-x(代表“除……之外”)表示打印所有与模式不匹配的文本行,用-n(代表“行号”)表示打印行号,那么下列命令:
find -x -n 模式将打印所有与模式不匹配的行,并在每个打印行的前面加上行号。
可选参数应该允许以任意次序出现,同时,程序的其余部分应该与命令行中参数的数目无关。此外,如果可选参数能够组合使用,将会给使用者带来更大的方便,比如:
find -nx 模式改写后的模式查找程序如下所示:
#include stdio.h
#include string.h
#define MAXLINE 1000
int getline(char *line, int max);
/* find: print lines that match pattern from 1st arg */
main(int argc, char *argv[])
{
char line[MAXLINE];
long lineno = 0;
int c, except = 0, number = 0, found = 0;
while (--argc 0 (*++argv)[0] == -)
while (c = *++argv[0])
switch (c) {
case x:
except = 1;
break;
case n:
number = 1;
break;
default:
printf("find: illegal option %c\n", c);
argc = 0;
found = -1;
break;
}
if (argc != 1)
printf("Usage: find -x -n pattern\n");
else
while (getline(line, MAXLINE) 0) {
lineno++;
if ((strstr(line, *argv) != NULL) != except) {
if (number)
printf("%ld:", lineno);
printf("%s", line);
found++;
}
}
return found;
}在处理每个可选参数之前,argc执行自减运算,argv执行自增运算。循环语句结束时,如果没有错误,则argc的值表示还没有处理的参数数目,而argv则指向这些未处理参数中的第一个参数。因此,这时argc 的值应为1,而*argv 应该指向模式。注意,*++argv 是一个指向参数字符串的指引,因此(*++argv)[0]是它的第一个字符(另一种有效形式是**++argv)。因为[]与操作数的结合优先级比*和++高,所以在上述表达式中必须使用圆括号,否则编译器将会把该表达式当*++(argv[0])。实际上,我们在内层循环中就使用了表达式*++argv[0],其目的是遍历一个特定的参数串。在内层循环中,表达式*++argv[0]对指针argv[0]进行了自增运算。
很少有人使用比这更复杂的指针表达式。如果遇到这种情况,可以将它们分为两步或三步来理解,这样会更直观一些。
11. 指向函数的指针在 C 语言中,函数本身不是变量,但可以定义指向函数的指针。这种类型的指针可以被赋值、存放在数组中、传递给函数以及作为函数的返回值等等。为了说明指向函数的指针的用法,我们接下来将修改本章前面的排序函数,在给定可选参数-n 的情况下,该函数将按数值大小而非字典顺序对输入行进行排序。
排序程序通常包括3 部分:判断任何两个对象之间次序的比较操作、颠倒对象次序的交换操作、一个用于比较和交换对象直到所有对象都按正确次序排列的排序算法。由于排序算法与比较、交换操作无关,因此,通过在排序算法中调用不同的比较和交换函数,便可以实现按照不同的标准排序。这就是我们的新版本排序函数所采用的方法。
我们在前面讲过,函数strcmp 按字典顺序比较两个输入行。在这里,我们还需要一个以数值为基础来比较两个输入行,并返回与strcmp同样的比较结果的函数numcmp。这些函数在main 之前声明,并且,指向恰当函数的指针将被传递给qsort 函数。在这里,参数的出错处理并不是问题的重点,我们将主要考虑指向函数的指针问题。
#include stdio.h
#include string.h
#define MAXLINES 5000/* max #lines to be sorted */
char *lineptr[MAXLINES]; /* pointers to text lines */
int readlines(char *lineptr[], int nlines);
void writelines(char *lineptr[], int nlines);
void qsort(void *lineptr[], int left, int right,
int (*comp)(void *, void *));
int numcmp(char *, char *);
/* sort input lines */
main(int argc, char *argv[])
{
int nlines; /* number of input lines read */
int numeric = 0; /* 1 if numeric sort */
if (argc 1 strcmp(argv[1], "-n") == 0)
numeric = 1;
if ((nlines = readlines(lineptr, MAXLINES)) = 0) {
qsort((void**) lineptr, 0, nlines-1,
(int (*)(void*,void*))(numeric ? numcmp : strcmp));
writelines(lineptr, nlines);
return 0;
} else {
printf("input too big to sort\n");
return 1;
}
}在调用函数qsort的语句中,strcmp和numcmp 是函数的地址。因为它们是函数,所以前面不需要加上取地址运算符 ,同样的原因,数组名前面也不需要 运算符。
改写后的qsort 函数能够处理任何数据类型,而不仅仅限于字符串。从函数qsort 的
原型可以看出,它的参数表包括一个指针数组、两个整数和一个有两个指针参数的函数。其中,指针数组参数的类型为通用指针类型void *。由于任何类型的指针都可以转换为void *类型,并且在将它转换回原来的类型时不会丢失信息,所以,调用qsort 函数时可以将参数强制转换为void *类型。比较函数的参数也要执行这种类型的转换。这种转换通常不会影响到数据的实际表示,但要确保编译器不会报错。
/* qsort: sort v[left]...v[right] into increasing order */
void qsort(void *v[], int left, int right,
int (*comp)(void *, void *))
{
int i, last;
void swap(void *v[], int, int);
if (left = right) /* do nothing if array contains */
return; /* fewer than two elements */
swap(v, left, (left + right)/2);
last = left;
for (i = left+1; i = right; i++)
if ((*comp)(v[i], v[left]) 0)
swap(v, ++last, i);
swap(v, left, last);
qsort(v, left, last-1, comp);
qsort(v, last+1, right, comp);
}我们仔细研究一下其中的声明。qsort函数的第四个参数声明如下:
int (*comp)(void *, void *)它表明comp是一个指向函数的指针,该函数具有两个void *类型的参数,其返回值类型为int。
在下列语句中:
if ((*comp)(v[i], v[left]) 0)comp的使用和其声明是一致的,comp是一个指向函数的指针,*comp代表一个函数。下列语句是对该函数进行调用:
(*comp)(v[i], v[left])其中的圆括号是必须的,这样才能够保证其中的各个部分正确结合。如果没有括号,例如写成下面的形式:
int *comp(void *, void *) /* WRONG */则表明comp 是一个函数,该函数返回一个指向int 类型的指针,这同我们的本意显然有很大的差别。
我们在前面讲过函数strcmp,占用于比较两个字符串。这里介绍的函数numcmp也是比较两个字符串,但它通过调用atof计算字符串对应的数值,然后在此基础上进行比较:
#include stdlib.h
/* numcmp: compare s1 and s2 numerically */
int numcmp(char *s1, char *s2)
{
double v1, v2;
v1 = atof(s1);
v2 = atof(s2);
if (v1 v2)
return -1;
else if (v1 v2)
return 1;
else
return 0;
}交换两个指引的swap函数和本章前面所述的swap函数相同,但它的参数声明为void *类型。
void swap(void *v[], int i, int j;)
{
void *temp;
temp = v[i];
v[i] = v[j];
v[j] = temp;
}还可以将其它一些选项增加到排序程序中,有些可以作为较难的练习。
12. 复杂声明C语言常常因为声明的语法问题而受到人们的批评,特别是涉及到函数指针的语法。C语言的语法力图使声明和使用相一致。对于简单的情况,C语言的做法是很有效的,但是,如果情况比较复杂,则容易让人混淆,原因在于,C语言的声明不能从左至右阅读,而且使用了太多的圆括号。我们来看下面所示的两个声明:
int *f(); /* f: function returning pointer to int */以及
int (*pf)(); /* pf: pointer to function returning int */它们之间的含义差别说明:*是一个前缀运算符,其优先级低于(),所以,声明中必须使用圆括号以保正确的结合顺序。
尽管实际中很少用到过于复杂的声明,但是,懂得如何理解甚至如何使用这些复杂的声明是很重要的。如何创建复杂的声明呢?一种比较好的方法是,使用typedef通过简单的步骤合成,这种方法我们将在6.7节中讨论。这里介绍另一种方法。接下来讲述的两个程序就使用这种方法:一个程序用于将正确的C 语言声明转换为文字描述,另一个程序完成相反的转换。文字描述是从左至右阅读的。
第一个程序dcl复杂一些。它将C语言的声明转换为文字描述,比如:
char **argv
argv: pointer to char
int (*daytab)[13]
daytab: pointer to array[13] of int
int *daytab[13]
daytab: array[13] of pointer to int
void *comp()
comp: function returning pointer to void
void (*comp)()
comp: pointer to function returning void
char (*(*x())[])()
x: function returning pointer to array[] of
pointer to function returning char
char (*(*x[3])())[5]
x: array[3] of pointer to function returning
pointer to array[5] of char程序dcl 是基于声明符的语法编写的。附录A 以及8.5 节将对声明符的语法进行详细的描述。下面是其简化的语法形式:
dcl: optional *s direct-dcl
direct-dcl name
(dcl)
direct-dcl()
direct-dcl[optional size]简而言之,声明符dcl就是前面可能带有多个*的direct-dcl。direct-dcl可以是name、由一对圆括号括起来的dcl、后面跟有一对圆括号的direct-dcl、后面跟有用方括号括起来的表示可选长度的direct-dcl。
该语法可用来对C语言的声明进行分析。例如,考虑下面的声明符:
(*pfa[])()按照该语法分析,pfa将被识别为一个name,从而被认为是一个direct-dcl。于是,pfa[]也是一个direct-dcl。接着,*pfa[]被识别为一个dcl,因此,判定(*pfa[])是一个direct-dcl。再接着,(*pfa[])()被识别为一个direct-dcl,因此也是一个dcl。可以用图5-12 所示的语法分析树来说明分析的过程(其中direct-dcl缩写为dir-dcl)。
程序 dcl的核心是两个函数:dcl与dirdcl,它们根据声明符的语法对声明进行分析。因为语法是递归定义的,所以在识别一个声明的组成部分时,这两个函数是相互递归调用的。我们称该程序是一个递归下降语法分析程序。
/* dcl: parse a declarator */
void dcl(void)
{
int ns;
for (ns = 0; gettoken() == *; ) /* count *s */
ns++;
dirdcl();
while (ns-- 0)
strcat(out, " pointer to");
}
/* dirdcl: parse a direct declarator */
void dirdcl(void)
{
int type;
if (tokentype == () { /* ( dcl ) */
dcl();
if (tokentype != ))
printf("error: missing )\n");
} else if (tokentype == NAME) /* variable name */
strcpy(name, token);
else
printf("error: expected name or (dcl)\n");
while ((type=gettoken()) == PARENS type == BRACKETS)
if (type == PARENS)
strcat(out, " function returning");
else {
strcat(out, " array");
strcat(out, token);
strcat(out, " of");
}
}该程序的目的旨在说明问题,并不想做得尽善尽美,所以对dcl 有很多限制,它只能处理类似于char或int这样的简单数据类型,而无法处理函数中的参数类型或类似于const这样的限定符。它不能处理带有不必要空格的情况。由于没有完备的出错处。
郑重声明:本文由网友发布,不代表盛行IT的观点,版权归原作者所有,仅为传播更多信息之目的,如有侵权请联系,我们将第一时间修改或删除,多谢。