指针是一种保存变量地址的变量。在存储时,内存被分为一块一块的。每一块都有一个特有的编号。而这个编号可以暂时理解为指针,就像酒店的门牌号一样。
# 指针与地址
一元运算符 & 可用于取一个对象的地址
1 | p = & c ; |
将把 c 的地址赋值给变量 p,我们称 p 为 “指向” c 的指针。地址运算符 & 只能应用于内存中 的对象,即变量与数组元素。它不能作用于表达式、常量或 register 类型的变量。
一元运算符 * 是间接寻址或间接引用运算符。当它作用于指针时,将访问指针所指向的对象。下列程序介绍了 & 与 * 的使用方法
1 | int x = 1 , y = 2 , z[10] ; |
对函数的声明也可以采用这种方式。
例如,声明
1 | double *dp,atof(char *); |
表明,在表达式中,*dp 和 atof (s) 的值都是 double 类型,且 atof 的参数是一个指向 char 类型的指针。
指针只能指向某种特定类型的对象,也就是说,每个指针都必须指向某种特定的数据类型。(一个例外情况是指向 void 类型的指针可以存放指向任何类型的指针,但它不能间接引用其自身。)
如果指针 ip 指向整型变量,那么在 x 可以出现的任何上下文中都可以使用 * ip,因此, 语句
1 | *ip = *ip + 10; |
将把 ip 的值增加 10。 一元运算符和 & 的优先级比算术运算符的优先级高,因此,赋值语句
1 | y = *ip + 1 |
将把 * ip 指向的对象的值取出并加 1,然后再将结果赋值给 y,而下列赋值语句:
1 | *ip += 1 |
则将 ip 指向的对象的值加 1,它等同于
1 | ++*ip |
或
1 | (*ip)++ |
语句的执行结果。语句 (* ip) 中的圆括号是必需的,否则,该表达式将对 ip 进行加 1 运算,
而不是对 ip 指向的对象进行加 1 运算,这是因为,类似于 * 和这样的一元运算符遵循从右 至左的结合顺序。
最后说明一点,由于指针也是变量,所以在程序中可以直接使用,而不必通过间接引用
的方法使用。例如,如果 iq 是另一个指向整型的指针,那么语句
1 | iq = ip |
将把 ip 中的值拷贝到 iq 中,这样,指针 iq 也将指向 ip 指向的对象。
# 指针与指针参数
为了使被调用函数直接修改主调函数中的变量值,我们可以使主调程序将指向所要交换的变量的指针传递给被调用函数,即:
1 | swap ( & a , & b ) ; |
由于一元运算符 & 用来取变量的地址,这样 & a 就是一个指向变量 a 的指针。swap 函数的所有参数都声明为指针,并且通过这些指针来间接访问它们所指向的操作数。
1 | void swap ( int * px , int * py ) { |
指针参数使得被调用函数能够访问和修改主调函数中对象的值。
我们来看一个可以接受自由格式输入,并执行转换,并同时返回得到的整数和文件结束标志(EOF)的函数。
如果你觉得你已经了解了可以不用看下面的代码,直接进入下一节。
1 | int getch ( void ) { return ( bufp > 0 ) ? buf[--bufp] : getchar ( ) ; } |
在 getint 函数中,*pn 始终作为一个普通的整形变量使用。
# 指针与数组
一般来说,用指针编写的程序比用数组下标编写的程序执行速度快,但另一方面,用指针实现的程序理解 起来稍微困难一些。
声明
1 | int a[10]; |
定义了一个长度为 10 的数组 a。换句话说,它定义了一个由 10 个对象组成的集合,这 10 个
对象存储在相邻的内存区域中,名字分别为 a [0]、a [1]、…、a [9]
如果 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] 的内容
无论数组 a 中元素的类型或数组长度是什么,上面的结论都成立。“指针加 1” 就意味着,pa+1 指向 pa 所指向的对象的下一个对象。相应地,pa+i 指向 pa 所指向的对象之后的第 i 个对象。
对数组元素 a [i] 的引用也可以写成 *(a+i) 这种形式。对第一次接触这种写法的人来说, 可能会觉得很奇怪。在计算数组元素 a [i] 的值时,C 语言实际上先将其转换为 *(a+i) 的形
式,然后再进行求值,因此在程序中这两种形式是等价的。如果对这两种等价的表示形式分 别施加地址运算符 &,便可以得出这样的结论:&a [i] 和 a+i 的含义也是相同的。a+i 是 a
之后第 i 个元素的地址。相应地,如果 pa 是个指针,那么,在表达式中也可以在它的后面加 下标。pa [i] 与 *(pa+i) 是等价的。简而言之,一个通过数组和下标实现的表达式可等价地 通过指针和偏移量实现。
但是,我们必须记住,数组名和指针之间有一个不同之处,指针是一个变量,因此,在 C 语言中,语句 pa=a 和 pa 都是合法的。但数组名不是变量,因此,类似于 a=pa 和 a 形式的语句是非法的。
当把数组名传递给一个函数时,实际上传递的是该数组第一个元索的地址。在被调用函数中,该参数是一个局部变量,因此,数组名参数必须是一个指针,也就是一个存储地址值 的变量。
1 | /*return length of s */ |
执行 s++ 运算不会影响到 strlen 函数的调用者中的字符串,它仅对该指针在 strlen 函数中的私有副本进行自增运算。
以下的函数调用均正确:
1 | strlen ( "hello world" ) ; |
在函数定义中,形式参数
1 | char s[]; |
和
1 | char *s; |
是等价的。我们通常更习惯于使用后一种形式,因为它比前者更直观地表明了该参数是一个指针。如果将数组名传递给函数,函数可以根据情况判定是按照数组处理还是按照指针处理,随后根据相应的方式操作该参数。为了直观且恰当地描述函数,在函数中甚至可以同时使用数组和指针这两种表示方法。
也可以将指向子数组起始位置的指针传递给函数,这样,就将数组的一部分传递给了函
数。例如,如果 a 是一个数组,那么下面两个函数调用
1 | f(&a[2]) |
与
1 | f(a+2) |
都将把起始于 a [2] 的子数组的地址传递给函数 f。在函数 f 中,参数的声明形式可以为
1 | f(int arr[]) { ... } |
或
1 | f(int *arr) { ... } |
对于函数 f 来说,它并不关心所引用的是否只是一个更大数组的部分元素。 如果确信相应的元素存在,也可以通过下标访问数组第一个元素之前的元素。类似于
p [-1]、p [-2] 这样的表达式在语法上都是合法的,它们分别引用位于 p [0] 之前的两个元素。
当然,引用数组边界之外的对象是非法的。
# 地址算术运算
下面我们将给出函数 alloc 和 afree,他们分别对标 malloc.h 头文件中的 malloc 函数和 free 函数,只是功能相对于 malloc 和 free 更菜而已。这里就先直接贴代码,看不懂也没关系,继续往下看就行
1 | char * alloc ( int n ) { |
首先,在某些情况下对指针可以进 行比较运算。例如,如果指针 p 和 q 指向同一个数组的成员,那么它们之间就可以进行类似 于 ==、!=、<、>= 的关系比较运算。如果 p 指向的数组元素的位置在 q 指向的数组元素位置 之前,那么关系表达式
p < q
的值为真。任何指针与 0 进行相等或不等的比较运算都有意义。但是,指向不同数组的元素的指针之间的算术或比较运算没有定义。(这里有一个特例:指针的算术运算中可使用数组最 后一个元素的下一个元素的地址。)
其次。我们从前面可以看到,指针可以和整数进行相加或相减运算。例如,结构
p + n
表示指针 p 当前指向的对象之后第 n 个对象的地址。无论指针 p 指向的对象是何种类型,上述结论都成立。在计算 p+n 时,n 将根据 p 指向的对象的长度按比例缩放,而 p 指向的对象的长度则取决于 p 的声明。例如,如果 int 类型占 4 个字节的存储空间,那么在 int 类型的计算中,对应的 n 将按 4 的倍数来计算。指针的减法运算也是有意义的:如果 p 和 q 指向相同数组中的元索,且 p<q,那么 q-p+1 就是位于 p 和 q 指向的元索之间的元素的数目。我们由此可以编写出函数 strlen 的另一个
版本,如
1 | int strlen ( char * s ) { |
# 字符指针与函数
字符串常量是一个字符数组,例如:
1 | "I am a string" |
在字符串的内部表示中,字符数组以空字符 '\0' 结尾,所以,程序可以通过检查空字符找到 字符数组的结尾。字符串常量占据的存储单元数也因此比双引号内的字符数大 1。
字符串常量最常见的用法也许是作为函数参数,例如:
1 | princf("hello, world\n"}; |
当类似于这样的一个字符串出现在程序中时,实际上是通过字符指针访问该字符串的。在上
述语句中,printf 接受的是一个指向字符数组第一个字符的指针。也就是说,字符串常量可 通过一个指向其第一个元素的指针访问。
除了作为函数参数以外,字符串常量还有其它的用法。例如
1 | char *pmessage ; |
此操作把一个指向该字符数组的指针赋值给 pmessage。该过程并没有进行字符串的复制,而只 是涉及到指针的操作。C 语言没有提供将整个字符串作为一个整体进行处理的运算符。
下面两个定义之间有很大的差别:
1 | char amessage[] = "nw is the time"; /* 定义一个数组 */ |
上述声明中,amessage 是一个仅仅足以存放初始化字符串以及空字符 '\0' 的一维数组。数组中的单个字符可以进行修改,但 amessage 始终指向同一个存储位置。另一方面,pmessage 是一个指针,其初值指向一个字符串常量,之后它可以被修改以指向其它地址,但如果试图 修改字符串的内容,结果是没有定义的
掌握了上面的知识,我们现在可以理解下面的程序
1 | void strcpy ( char * s , char * t ) while ( * s ++ = * t ++ ) ; |
1 | int strcmp ( char * s , char * t ) { |
# 数组指针与指向指针的指针
下面是一个运用快速排序来实现字符串排序的程序,qsort 部分可自行使用快排实现,我自己想了想也可以用普通的 sort + 结构体实现,或许会更好点,这里就不展开了。
1 | #include <stdio.h> |
在该例子中,指针数组 lineptr 的声明是新出现的重要概念:
1 | char *lineptr[MAXLINES]; |
它表示 lineptr 是一个具有 MAXLINES 个元素的一维数组,其中数组的每个元素是一个指向字符类型对象的指针。也就是说,lineptr [i] 是一个字符指针,而 * lineptr [i] 是该指针指向的第 i 个文本行的首字符。 由于 lineptr 本身是一个数组名,因此,可按照前面例子中相同的方法将其作为指针使用,这样,writelines 函数可以改写为:
1 | /* writelines: write output lines */ |
(注意这里的数组变量 lineptr 可以改变值)
循环开始执行时,*lineptr 指向第一行,每执行一次自增运算都使得 lineptr 指向下 一行,同时对 nlines 进行自减运算。
# 多维数组
如果将二维数组作为参数传递给函数,那么在函数的参数声明中必须指明数组的列数。
数组的行数没有太大关系,因为前面已经讲过,函数调用时传递的是一个指针,它指向由行 向量构成的一维数组,其中每个行向量是具有 13 个整型元素的一维数组。在该例子中,传递 给函数的是一个指向很多对象的指针,其中每个对象是由 13 个整型元素构成的一维数组。因 此,如果将数组 daytab 作为参数传递给函数 f,那么 f 的声明应该写成下列形式:
1 | f(int daytab[2][13]) { ... } |
也可以写成
1 | f(int daytab[][13]) { ... } |
因为数组的行数无关紧要,所以,该声明还可以写成
1 | f(int (*daytab)[13]) { ... } |
这种声明形式表明参数是一个指针,它指向具有 13 个整型元素的一维数组。因为方括号 [] 的
优先级高于 * 的优先级,所以上述声明中必须使用圆括号。如果去掉括号,则声明变成
1 | int *daytab[13] |
这相当于声明了一个数组,该数组有 13 个元素,其中每个元素都是一个指向整型对象的指针。
一般来说,除数组的第一维(下标)可以不指定大小外,其余各维都必须明确指定大小。
# 指针数组的初始化
考虑这样一个问题:编写一个函数 month_name (n),它返回一个指向第 n 个月名字的 字符串的指针。这是内部 static 类型数组的一种理想的应用。month_name 函数中包含一 个私有的字符串数组,当它被调用时,返回一个指向正确元素的指针。本节将说明如何初始
化该名字数组。
指针数组的初始化语法和前面所讲的其它类型对象的初始化语法类似:
1 | /* month_name: return name of n-th month */ |
其中,name 的声明与排序例子中 lineptr 的声明相同,是一个一维数组,数组的元素为字 符指针。name 数组的初始化通过一个字符串列表实现,列表中的每个字符串赋值给数组相应 位置的元素。第 i 个字符串的所有字符存储在存储器中的某个位置,指向它的指针存储在 name [i] 中。由于上述声明中没有指明 name 的长度,因此,编译器编译时将对初值个数进 行统计,并将这一准确数字填入数组的长度。
# 指针与多维数组
对于 C 语言的初学者来说,很容易混淆二维数组与指针数组之间的区别,比如上面例子 中的 name。假如有下面两个定义:
1 | int a[10][20]; |
那么,从语法角度讲,a [3][4] 和 b [3][4] 都是对一个 int 对象的合法引用。但 a 是一个真正的二维数组,它分配了 200 个 int 类型长度的存储空间,并且通过常规的矩阵下标计算公式 20×row+col(其中,row 表示行,col 表示列)计算得到元素 a [row][col] 的位置。但是,对 b 来说,该定义仅仅分配了 10 个指针,并且没有对它们初始化,它们的初始化必须以显式的方式进行,比如静态初始化或通过代码初始化。假定 b 的每个元素 (其实只取决于最大的)都指向一个具有 20 个元 素的数组,那么编译器就要为它分配 200 个 int 类型长度的存储空间以及 10 个指针的存储空间。指针数组的一个重要优点在于,数组的每一行长度可以不同,也就是说,b 的每个元素不 必都指向一个具有 20 个元素的向量,某些元素可以指向具有 2 个元素的向量,某些元素可以指向具有 50 个元素的向量,而某些元素可以不指向任何向量。
# 命令行参数
编程实际场景中感觉是不常用的,我自己也没看懂,日后补
略
# 指向函数的指针
在系统学习完指向含糊的指针后,我发现这玩意就是个纸老虎,只是听起来挺高大上的,其实非常简单。
在实际运用中就差不多减少一点代码行数而已,感觉替代性挺强的。(也可能是因为我太菜了)
那就开始吧
在 C 语言中,函数本身不是变量,但可以定义指向函数的指针。这种类型的指针可以被赋值、存放在数组中、传递给函数以及作为函数的返回值等等。我们接下来将修改排序函数,在给定可选参数 - n 的情况下,该函数将按树枝大小而非字典顺序对输入行进行排序。
排序程序通常包括 3 部分:判断任何两个对象之间次序的比较操作、颠倒对象次序的交 换操作、一个用于比较和交换对象直到所有对象都按正确次序排列的排序算法。
由于排序算法与比较、交换操作无关,因此,通过在排序算法中调用不同的比较和交换函数,便可以实 现按照不同的标准排序。这就是我们的新版本排序函数所采用的方法。
函数 strcmp 按字典顺序比较两个输入行。在这里,我们还需要一个以数值为基础来比较两个输入行,并返回与 strcmp 同样的比较结果的函数 numcmp。这些函 数在 main 之前声明,并且,指向恰当函数的指针将被传递给 qsort 函数。在这里,参数的出错处理并不是问题的重点,我们将主要考虑指向函数的指针问题。
1 | #include <stdio.h> |
在调用函数 qsort 的语句中,strcmp 和 numcmp 是函数的地址,因为它们是函数,所以前面不需要加上取地址运算符 &,同样的原因,数组名前面也不需要 & 运算符。
改写后的 qsort 函数能够处理任何数据类型,而不仅仅限于字符串。从函数 qsort 的 原型可以看出,它的参数表包括一个指针数组、两个整数和一个有两个指针参数的函数。其中,指针数组参数的类型为通用指针类型 void 。由于任何类型的指针都可以转换为 void 类型,并且在将它转换回原来的类型时不会丢失信息,所以,调用 qsort 函数时可以将参数强制转换为 void * 类型。比较函数的参数也要执行这种类型的转换。这种转换通常不会影响到数据的实际表示,但要确保编译器不会报错。
1 | int ( * comp ) ( void * , void * ) |
它表明 comp 是一个指向函数的指针,该函数具有两个 void * 类型的参数,其返回值为 int。
值得注意的是圆括号是必须的,这样才能保证其中的各个部分正确结合,如果没有括号,如
1 | int * comp ( void * , void * ) |
则表明 comp 是一个函数,该函数返回一个指向 int 类型的指针
我们在前面讲过函数 strcmp,占用于比较两个字符串。这里介绍的函数 numcmp 也是比 较两个字符串,但它通过调用 atof 计算字符串对应的数值,然后在此基础上进行比较:
1 | #include <stdlib.h> |
交换两个指引的 swap 函数和本章前面所述的 swap 函数相同,但它的参数声明为 void *
类型。
1 | void swap(void * v[], int i, int j;) |
# 链表
关于 malloc:
# 函数原型:
1 | extern void \*malloc(unsigned int num\_bytes); |
# malloc 函数返回值
如果分配成功则返回指向被分配内存的指针,否则返回空指针 NULL。
# malloc 函数使用注意事项
malloc 函数的返回的是无类型指针,在使用时一定要强制转换为所需要的类型。
在使用 malloc 开辟空间时,使用完成一定要释放空间,如果不释放会造内存泄漏。
在使用 malloc 函数开辟的空间中,不要进行指针的移动,因为一旦移动之后可能出现申请的空间和释放空间大小的不匹配
1 | #include<malloc.h> |