指针是一种保存变量地址的变量。在C语言中,指针的使用非常广泛,原因之一是,指针常常是表达某个计算的唯一途径,另一个原因是,同其他方法比较起来,使用指针通常可以生成更高效、更紧凑的代码。
5.1 指针与地址
通常的机器都有一系列连续编号或编址的存储单元,这些存储单元可以单个进行操纵,也可以以连续成组的方式操纵。通常情况下,机器的一个字节可以存放一个char类型的数据,两个相邻的字节存储单元可存储一个short(短整型)类型的数据,而4个相邻的字节存储单元可存储一个1ong(长整型)类型的数据。指针是能够存放一个地址的一组存储单元(通常是2或4个字节)。因此,如果c的类型是char,并且p是指向c的指针,则可用图5-1表示它们之间的关系。
一元运算符&可用于取一个对象的地址,因此,下列语句
p = &c;
将把c的地址赋值给变量p,我们称p为“指向”c的指针。地址算符&只能应用于内存中的对象,即变量与数组元素。它不能作用于表达式、常量或regisrer类型的变量。
一元运算符*是间接寻址或间接引用运算符。当它作用于指针时,将访问指针所指向的对象。我们在这里假定x与y是整数,而ip是指向int类型的指针。下面的代码段说明了如何在程序中声明指针以及如何使用运算符&和*:
一元运算符*和&的优先级比算术运算符的优先级高,因此,赋值语句
y=*ip=1
将把*ip指向的对象的值取出并加1,然后再将结果赋值给y,而赋值语句
*ip+=1
则将ip指向的对象的值加1,它等同于
++*ip 或 (*ip)++
语句的执行结果。语句(*iP)++中的圆括号是必需的,否则,该表达式将对ip进行加1运算而不是对ip指向的对象进行加1运算,这是因为,类似于*和、这样的一元运算符遵循从右至左的结合顺序。
最后说明一点,由于指针也是变量,所以在程序中可以直接使用,而不必通过间接引用的方法使用。例如,如果iq是另一个指向整型的指针,那么语句
iq=ip
将把ip中的值拷贝到iq中,这样,指针iq也将指向ip指向的对象。
5.2 指针与函数参数
由于C语言是以传值的方式将参数值传递给被调用函数的,因此,被调用函数不能直接修改主调函数中变量的值。例如,排序函数可能会使用一个名为swap的函数来交换两个次序颠倒的元素。但是,如果将swap函数定义为下列形式:
则下列语句
swap(a,b) ;
无法达到该目的。这是因为,由于参数传递采用传值方式,因此上述的swap函数不会影响到调用它的例程中的参数a和b的值。该函数仅仅交换了a和b的副本的值。
那么,如何实现我们的目标呢?可以使主调程序将指向所要交换的变量的指针传递给被调用函数:swap(&a, &b);
由于一元运算符&用来取变量的地址,这样&a就是一个指向变量a的指针。swap函数的所有参数都声明为指针,并且通过这些指针来间接访问它们指向的操作数。
指针参数使得被调用函数能够访问和修改主调函数中对象的值。
5.3 指针与数组
在C语言中,指针和数组之间的关系十分密切,因此,在接下来的部分中,我们将同时讨论指针与数组。通过数组下标所能完成的任何操作都可以通过指针来实现。一般来说,用指针编写的程序比用数组下标编写的程序执行速度快,但另一方面,用指针实现的程序理解起来稍微困难一些。
声明
int a【10】;
定义了一个长度为10的数组a。换句话说,它定义了一个由10个对象组成的集合,这10个对象存储在相邻的内存区域中,名字分别为a[0]、a[1]、…、a[9],a[i]表示该数组的第1个元素。如果pa的声明为
int *pa;
则说明它是一个指向整型对象的指针,那么,赋值语句
pa = &a[0];
则可以将指针pa指向数组a的第0个元素,也就是说,pa的值为数组元素a[0]的地址。如果pa指向数组中的某个特定元素,pa+i将指向pa所指向数组元素之后的第i个元素。
下标和指针运算之间具有密切的对应关系。执行赋值语句
pa = &a[0];
后,pa和a具有相同的值。。因为数组名所代表的就是该数组最开始的一个元素的地址,赋值语句pa=&a[0]也可以写成下列形式:
pa = a;
对数组元素a[i]的引用也可以写成*(a+i)这种形式。
但是,我们必须记住,数组名和指针之间有一个不同之处。指针是一个变量,因此,在C语言中,语句pa=a和pa++都是合法的。但数组名不是变量,因此,类似于a=pa和a++形式的语句是非法的。
5.4 地址算术运算
C语言中的地址算术运算方法是一致且有规律的,将指针、数组和地址的算术运算集成在一起是该语言的一大优点。我们来看一个不完善的存储分配程序。第一个函数alloc(n)返回一个指向n个连续字符存储单元的指针,alloc函数的调用者可利用该指针存储字符序列。第二个函数afree(p)释放已分配的存储空间,以便以后重用。之所以说这两个函数是不完善的,是因为对afree函数的调用次序必须与调用alloc函数的次序相反。换句话说,alloc与afree以栈的方式(即后进先出的列表)进行存储空间的管理。标准库中提供了具有类似功能的函数malloc和free,它们没有上述限制。
有效的指针运算包括相同类型指针之间的赋值运算;指针同整数之间的加法或减法运算,指向相同数组中元素的两个指针间的减法或比较运算;将指针赋值为0或指针与0之间的比较运算。其他所有形式的指针运算都是非法的,例如两个指针间的加法、乘法、除法、移位或屏蔽运算,指针同float或double类型之间的加法运算;不经强制类型转换而直接将指向一种类型对象的指针赋值给指向另一种类型对象的指针的运算(两个指针之一是void*类型的情况除外)。
5.5 字符指针与函数
字符串常量是一个字符数组,例如,"I am a string"在字符串的内部表示中,字符数组以空字符‘\0’结尾,所以,程序可以通过检查空字符找到字符数组的结尾。字符串常量占据的存储单元数也因此比双引号内的字符数大1。
字符串常量最常见的用法也许是作为函数参数,例如:
printf("hello, world\n");
除了作为函数参数外,字符串常量还有其他用法。假定指针pmessage的声明如下:
char *pmessage;
那么,语句
pmessage = "now is the time";
将把一个指向该字符数组的指针赋值给pmessage。该过程并没有进行字符串的复制,而只是涉及指针的操作。C语言没有提供将整个字符串作为一个整体进行处理的运算符。
下面两个定义之间有很大的差别:
上述声明中,amessàge是一个仅仅足以存放初始化字符串以及空字符的一维数组。数组中的单个字符可以进行修改,但amessage始终指向同一个存储位置。另一方面pmessage是一个指针,其初值指向一个字符串常量,之后它可以被修改以指向其他地址但如果试图修改字符串的内容,结果是没有定义的(参见图5-7) 。
5.6 指针数组以及指向指针的指针
由于指针本身也是变量,所以它们也可以像其他变量一样存储在数组中。
一级指针的值虽然是地址,但这个地址做为一个值亦需要空间来存放,是空间就具有地址 ,这就是存放地址这一值的空间所具有的地址,二级指针就是为了获取这个地址。
一级指针所关联的是其值(一个地址)名下空间里的数据,这个数据可以是任意类型并做任意用途,但二级指针所关联的数据只有一个类型一个用途,就是地址,指针就是两个用途提供目标的读取或改写, 那么二级指针就是为了提供对于内存地址的读取或改写。
5.7 多维数组
C语言提供了类似于矩阵的多维数组,但实际上它们并不像指针数组使用得那样广泛。
5.8 命令行参数
在C语言中,main
函数是程序的入口点,用于接收命令行参数。
命令行参数紧跟在可执行文件名之后,以空格分隔的字符串。main
函数可以接收两个参数:
argc
(argument count):表示传递给程序的命令行参数的个数,包括程序名称本身。
argv
(argument value):一个指针数组,数组中的每一个指针都指向一个字符串。其中argv
是程序的名称,argv:ml-citation{ref="1" data="citationList"}
、argv:ml-citation{ref="2" data="citationList"}
等依次是传递给程序的命令行参数。
C语言命令行参数的主要用途是允许用户在程序运行时提供外部数据,从而控制程序的行为或提供输入。这些参数通过主函数的参数传递,通常包括文件的路径或其他需要输入的数据。通过命令行参数,程序可以获得更高的灵活性和可扩展性,用户可以在不修改代码的情况下,通过命令行传递不同的参数来改变程序的行为。
在C语言中,命令行参数是通过main
函数的参数来接收的。main
函数通常有两个参数:argc
和argv
。argc
表示命令行参数的数量,而argv
是一个字符串数组,包含了每个命令行参数的具体值。程序的名字也会作为argv
传递,因此argc
的值至少为1。这些参数通过命令行传递给程序,因此被称为命令行参数。
5.9 指向函数的指针
在C语言中,函数本身不是变量,但可以定义指函数的指针。我们可以通过这个指针来调用函数。
以下是一些示例代码,展示如何使用指向函数的指针
定义一个函数,然后创建一个指向该函数的指针,并使用该指针调用函数: