前面13_1章节中提到一个问题:指针指向的数据类型的大小.本章节围绕这个问题展开.

前置知识:

  1. 指针变量的声明,赋值,与解引用操作
  2. 指向不同类型的指针如何声明

指针指向类型的大小

前面提到,指针的类型即为其指向的类型的指针,声明语法如下:

1
[类型] *[指针变量名];

C语言中任何值都有其类型,例如int,float,char等等.

同理,每一个指针变量/指针值也都有固定的类型,例如int *,char *,float *等,在这些基本类型的后面加一个*,整体即为一个特定的指针类型.

一个指针变量只能指向与其指针类型一致的变量.例如int *的指针变量只能指向int类型的变量,而不能指向char类型的变量.原因就是,对一个指针进行寻址(解引用),必须要确定数据大小,从指针值开始,取出固定的几个字节.


以前面int i=2;为例:

image-20231121004333759

假设i在内存中的地址为0xFFFFFECC,如图.我们知道int变量需要4个字节才能够存下,而实际上计算机内存的每个字节都有自己的地址,那么我们讲的"i的地址",实际上是使用其最小字节的地址作为"代表",也就是0xFFFFFECC,当我们需要访问i的时候,从这个字节开始,将连续的4个字节一起拿出来,作为一个整体来处理,也就是02 00 00 00.

以上操作的前提是,我们已知了int占用4字节,即sizeof(int)==4.使用指针进行访问也是如此,光知道了i的起始地址还不够,我们需要知道从这个指针值起始需要取出多少字节的数据.指针类型就提供了这样的信息.例如我们有int *p;想要获取一个指针指向类型的大小,可以这样:

int size = sizeof(*p);

由于sizeof后的表达式不求值,所以尽管没有对p初始化,我们也是可以写sizeof(*p)的,想想,p为int*,那么对p解引用自然就是int了,sizeof int就算出来4,即使用指针p进行解引用,要将4个字节作为一个整体去处理.同理,char *p;对p解引用时只处理1个字节,因为sizeof(*p)即为sizeof(char),结果为1.

也就是说,我们可以确定一个指针所指向的类型占用的字节数,根据这个值在解引用时取出特定长度的一段内存数据.

指针运算

指针本质上就是一个有类型的内存地址,其实就是一个特定长度的无符号整数,所谓的指针类型是这个整数的特殊解释方式.

作为一个数值,指针也支持一些运算,但是由于其并不是一般的整数,他的运算较为特殊.

指针仅支持整数加减法,而且两个指针间只能做减法,因为其他的运算没有任何意义.另外,指针运算往往离不开数组.

指针加法

一个指针(地址)加1,即将地址加1,代表着这个指针指向下一个内存单元,即紧挨着原来那个字节再往后一个位置的字节.同理,一个指针加n,代表着这个指针指向原来的地址往后n个字节的地址.

例如有一个指向char类型的指针p,初始化为0x1:

image-20231211000827380

我们对p加上某个整数n,结果就是指向往后面数n个字节的那个内存地址.


现在问题来了,如果是int *p=(int*)0x1;呢?

image-20231211002229945

显然,p+1不再是指向紧挨着p之后的那个字节,而是向后偏移了整整4个字节,到达了0x5的位置.


这个问题其实很简单,我们在对指针进行解引用的时候,需要计算对应类型(即这个指针指向的类型)的大小,然后一次性取出特定长度的数据.对应地,我们将指针加上某个数,就是想要指向后面对应的数据,彼此之间应该是不能冲突的.

因此,我们需要做一个乘法,即:

(p+1)等价于(p)+sizeof(*p)*i 其中,前者是C语言表达式,而后者是普通的数学算式!

对于int *p,其指向的类型int占用4个字节(只考虑32位以上的机器),那么在计算p+1的时候,要先将1乘以4,然后再和p的值进行相加,得到的就是0x1+1*4,即0x5.同理,p+2得到的就是0x1+2*4,即0x9.

而对于char *p,其指向的类型char只占用1个字节而已,所以加1仅仅是向后偏移1而已.

注:好好体会偏移这个词,后面会经常遇到.


另一方面,虽然标题写的是指针加法,但是两个指针进行相加是没有任何意义的…

1
2
3
int a,b;
int *p1=&a,*p2=&b;
//int *p3=p1+p2; // 很容易理解,p3没有任何意义!因此实际上这行代码会报错

指针减法

一个指针减去一个常数和指针加一个常数的意义是一样的,只不过这回是向前(更低的地址)偏移了而已,这里就不举例了.

另一方面,与两个指针相加不同,两个指针相减是合法的,而且作用很大!

image-20231211005128076

这里,p2指向p1的下一个位置(相差1个int的长度),那么(p2-p1)的结果是这两个指针之间间隔的元素个数.

也许加一个数组进来会更加直观:

1
2
3
4
5
6
7
8
9
#include <stdio.h>

int main() {
int a[10];
int *p1 = &a[0], *p2 = &a[3];
printf("%p %p\n", p2, p1);
printf("%lld\n", p2 - p1); // 3
return 0;
}

输出结果是3,代表着二者之间相差3个int变量,从数组下标也能看出来.

也就是说,指针与指针的相减操作,表示两个指针指向的内存位置之间相隔多少个元素(而不是字节数),因此指针相减往往与数组结合使用.

另外,必须提出的一点是,这两个指针必须指向同一个数组的某两个元素,否则行为未定义.

指针自增

C语言中,++--运算符是可以用于指针变量的,和普通变量一样,对指针自增或自减都是让这个指针变量的值"加一"或"减一".

需要注意的区别是,其效果和指针加法,指针减法相同,都是向前向后偏移一个元素大小的长度,而不是1个字节的大小.

对指针变量自增/自减会修改指针变量的值,也就是修改这个指针的指向.这种写法有着十分重要的作用,例如达到只使用一个指针进行遍历的效果.

1
2
3
4
5
6
int a[10]={0};
int *p = &a[0];
// 以下3种写法等价
p++;
p = p + 1;
++p;

同样需要注意的是,前缀++后缀++的区别依然不变,如果不清楚的话请回看"运算符"这一部分.我们举一个例子:

1
2
3
4
5
6
7
8
9
10
11
#include <stdio.h>

int main() {
int a[10] = {0, 1, 2, 3, 4, 5, 6, 7, 8, 9};
int *p, *q; // 2个指针变量
p = q = &a[0]; // 都指向数组的第一个元素
int *i = p++; // 后缀++,i指向a[0],即p的旧值,p指向a[1]
int *j = ++q; // 前缀++,j指向a[1],即q的新值,q指向a[1]
printf("%d %d %d %d\n", *i, *j, *p, *q); // 0 1 1 1
return 0;
}

唯一的区别是对指针的自增遵循指针运算,而不是简单地将地址值+1.

指针自增实际上还有许多细节,易错点和应用技巧,他们和数组有着紧密的关联,后面细说.


关于指针运算,详见算术运算符

本部分讲解了指针类型与指针运算,读者应该意识到(虽然可能为时尚早),指针的行为与其类型息息相关.

接下来的指针话题会涉及到数组,而且比较复杂.

---WAHAHA



上一篇:C语言教程-13_1-初识指针

下一篇:C语言教程-13_3-初探指针和数组的关系