C语言学习笔记:指针
对于初学者而言,这是一个很麻烦的东西;对于大佬而言,这是无所不能的屠龙宝刀。作为C语言中最重要的概念之一,掌握它,是通往C语言高阶应用的一条必经之路。
简介
指针(pointer)是一个用来存储内存地址的变量/数据对象。缩句:指针是变量。即指针具备变量的通性。指针还有两个地址运算符:(解引用运算符)和&(引用运算符)。pointer给出指针pointer指向地址的值,&argument给出变量argument所在的地址。
指针可以这样赋值:ptr = &var;
即把var的地址赋给ptr。此时,ptr指向var。地址只能被存储在指针类型的变量中。
观察下面的程序:
1 | ptr = &var_a; |
int * pt;
*pt = 5;
1 |
|
请问这个例子合法吗?合法。因为数组名对应的值就是数组首元素的地址。这里a中存储的就是第一个数的地址,所以其用法和数组的用法是一致的。
指针的指针
1 | int a=12; |
那么这里的c
是什么?
指向指针的指针。
其中,*c
表示c
所指向的位置,也就是b
。也就是说,**c==*b,*b==a;
。
指针表达式
1 | char ch='a'; |
现在,我们有了两个变量。接下来,我们会以它为基础,讨论一些指针表达式。
先来个简单的:
1 | ch |
它可以当右值使用,此时表示ch
中的值。但是当它作为左值使用时,它表示的是ch
的地址。
1 | &ch |
它表示ch
的地址,这个值和cp
的值一样。它可以作为右值使用,但不能作为左值。因为它是一个数值,并没有指明一个计算机的内存地址。
1 | cp |
它的右值就是cp
的值,左值就是cp
所处的内存位置。
1 | &cp |
和第二个一样,可以作为右值,而不能作为左值。
1 | *cp |
作为右值时指ch
处存储的值,作为左值时表示ch
的内存位置。
1 | *cp+1 |
等价于(*cp)+1
。即把cp
的值再加一。既然是值,那么就只能作为右值使用。
1 | *(cp+1) |
作为右值时,表示在cp+[一个该存储单元长度]
处存储的值;作为左值使用时,表示cp
的下一个存储单元的地址。
1 | ++cp |
表示cp
的下一个位置的内存地址的值。因此不能作为左值使用,只能作为右值使用。但是注意,++
操作符的前缀形式表示将cp
增值后再拷贝一份,并作为返回值。
1 | cp++ |
表示cp
的下一个位置的内存地址的值。同样不能作为左值使用。但是注意,++
的后缀形式表示先拷贝一份cp
并作为返回值,然后再将cp
增值。
1 | *++cp |
作为右值时,它表示cp
的下一个内存地址的值;作为左值时,它表示cp
的下一个内存地址。这里注意下,++
的前缀形式和*
都是右结合的。这里因为++离得近所以先自增再间接访问。
1 | *cp++ |
作为右值时,它表示cp
的内存地址的值;作为左值时,它表示cp
的内存地址。注意,此处++
的优先级是高于*
的。但是因为前面说过的:
但是注意,
++
的后缀形式表示先拷贝一份cp
并作为返回值,然后再将cp
增值。
所以,cp
的值实际上已经自增了。之所以还表示ch
处的地址/地址的值,是因为**++
返回原值的拷贝再将cp
自增**。
1 | ++*cp |
看了上面的例子,你应该很清楚了:它表示将ch
处的值自增,并返回该值的拷贝。
1 | (*cp)++ |
表示将cp
处的值拷贝一份再返回,再自增cp
处的值。所以只能作为右值使用。
1 | ++*++cp |
表示将cp
的值(也就是ch
的地址)自增并返回一份拷贝(即ch
的下一个内存地址的指针),再对这份拷贝进行间接访问操作,再对此处(ch
的下一个内存地址处)存储的值自增并返回一份拷贝。同样,因为是值,所以只能作为右值。
1 | ++*cp++ |
此处注意,++
后缀形式的优先级较高,因此先返回cp
的值再将cp自增,*
得到++
返回的cp
的值(即ch
),并对其进行间接访问,再由前缀的++
将ch
处的值自增,并返回一份拷贝。
弄清了这些,对于指针的操作应该就熟悉了。
指针和数组
首先声明一个数组。
1 | int array[32]; |
array
表示指向首元素的指针。所以这两种形式等价:
1 | printf("%d",array[15]==*(array+(15))); //输出1 |
多元数组同样,只需要反复嵌套即可。
这里注意,对于数组的下标,由于C实现下标的方法,实际上有两种合法形式:
array[1]
和1[array]
都是合法的。
但是很显然,后一种的可读性极差,违反直觉。所以不应被使用。同样,函数声明也有一种旧式的K&R风格:
int func(a,b,c)int a;char b;float c;
。它的使用也应避免:参数传递之前,char
和short
类型会被提升成int
类型,float
会被提升为double
类型。这称作缺省参数提升。所以应尽量避免使用这种风格的声明。
指向数组的指针
先看这个语句:
1 | int matrix[3][10], *mp=matrix; |
这是错误的。因为matrix
是指向整型数组的指针。要声明这样的指针,需要加上下标:
1 | int (*p)[10] = matrix; |
它指向matrix
的第一个整型数组。
此处注意优先级:下标引用高于间接访问。但是因为加了括号,所以实际还是间接访问先执行。
如果需要一个指针逐个访问整型元素,则可以这样:
1 | int *pi = &matrix[0][0]; |
此时,pi++
会使它指向下一个整型元素。
指针数组
看这个声明:
1 | int* api[10]; |
它表示一个数组,它的每个元素都是指针:指向整型的指针。这个可以根据前面的优先级顺序推导出来。
指针和字符串常量
一个字符串常量的值是什么?是一个指针常量,一个指向它第一个字符的指针常量。为什么是常量呢?因为它的(偏移)地址是编译时编译器指定的。
下面来看几个似乎有点离谱的……表达式?
1 | "xyz"+1 |
看起来似乎没有意义?但结合前面所说,我们可以推知,这是一个指向它本身第二个字符的指针。
1 | *"xyz" |
对这个指向第一个字符x的指针,执行间接访问,结果是什么?就是它指向的字符'x'
。
1 | "xyz"[2] |
这表示字符'z'
。
但是这技巧有什么用呢?看看这个:
1 | void print_process_bar(int n) |
这个函数接收一个0-100间的值,输出相应数量除以10的*
。像不像一个进度条呢?
如果我们用for循环来实现,那么100%就需要循环100次。效率远不如这个函数。当然,还是可读性和可维护性更重要一些。
还有这个进制转换的方法:
1 | putchar("0123456789ABCDEF"[value%16]); |
它比传统的进制转换或许会更快一些,但是你应该写清楚注释,确保它的可读性。
指针和函数:函数指针
首先,在介绍更高级的指针类型之前,很有必要看看它们是如何声明的。
1 | int f; //一个整型变量 |
有一个叫做cdecl的程序,可以解释一个现存的C语言声明,不妨百度一下。
函数指针
作为一种技巧,它会降低代码的可读性,但是也会提升效率。最常用的两个用法就是转换表和作为参数传给另一个函数,即:回调函数。
回调函数
下面看一个程序。
1 |
|
这是一个类型无关的链表查找函数。它的第三个参数是一个指向比较函数的指针,所以在调用的时候,我们需要编写一个对应链表数据类型的比较函数:
1 | int cmp_int(void const *a, void const *b) |
注意这个函数。为了使上面的查找函数类型无关,所以它调用的函数的参数也必须是类型无关的。
也是因此,在编写比较函数时,我们需要对指针进行强制类型转换,然后再解引用,才能得到正确的值。
顺便注意一下我写的比较函数,用了一些方法简写了。
转移表
考虑一个计算器程序。对于一个功能很多的计算器,我们要对它的运算符编一个很长的switch语句。很繁琐,对吧?
假设操作符是从0开始的,则可以用转移表来替换掉这个大大的switch:
1 |
|
借用函数指针数组,我们就可以根据输入的运算符编号来调用函数指针数组中对应序号的函数。
一定要注意,函数原型必须声明在函数指针数组之前。
同样的,在这里也存在下标越界的问题。但是这里的越界更难诊断出来,程序可能会直接终止,但报错的位置可能是下标越界,也可能是很奇怪的位置,因为指针可能飞到一个数据段中去了,数据被当做指令执行,肯定会出错。
更离谱点,如果这个指针刚好飞到一个函数体中,那个函数可能会快乐地执行,并且修改谁也不知道的值。这时候要找出bug就难如登天了。
实例
这啥
我一个哥们问我的
1 | int *(*a[5])(int, char*); |
比较麻烦。。不过还能看出来,区分好结构就行了。
这是一个函数指针数组的指针,指针指向的每个函数返回一个int
类型的指针。
首先看大体结构。int* xxx(int,char)
应是一个函数的样子。然后再细看:
*a[5]
又是啥?我们先看下a[5]
。这是一个被初始化的,含有5个元素的数组。*
表示该数组每个元素都是指针。所以,这是一个函数指针数组。
字符串长度统计
1 |
|