指针
指针的概念(地址和内存)
计算机中所有的数据都必须放在内存中,不同类型的数据占用的字节数不一样,例如 int 占用 4 个字节,char 占用 1 个字节。为了正确地访问这些数据,必须为每个字节都编上号码,就像门牌号、身份证号一样,每个字节的编号是唯一的,根据编号可以准确地找到某个字节。
下图是 4G 内存中每个字节的编号(以十六进制表示):
我们将内存中字节的编号称为地址(Address)或指针(Pointer)。
定义指针变量
int *a; //定义整型指针变量a
char *b; //定义char类型指针变量
int **c; //定义(int *)类型指针变量,所谓的二级指针
char ***d; //定义(char **)类型的指针变量,所谓的三级指针
//特殊的指针,void型指针,可以指向任何数据类型
void* ptr = a; //OK 指向int类型的数据
ptr = b; //OK 指向char类型的数据
指针变量的值中有一个非常特殊的值: NULL,它不指向系统中的任何变量或者函数。一般,我们使用它作为一个标志(返回指针的函数没有正确执行、到达链表末尾等等)
附其他类型定义:
int *p[3]; //首先从P 处开始,先与[]结合,因为其优先级比*高,所以P 是一个数组,然后再与*结合,说明数组里的元素是指针类型,然后再与int 结合,说明指针所指向的内容的类型是整型的,所以P 是一个由返回整型数据的指针所组成的数组
int (*p)[3]; //首先从P 处开始,先与 * 结合,说明P 是一个指针然后再与[]结合(与"()"这步可以忽略,只是为了改变优先级),说明指针所指向的内容是一个数组,然后再与int结合,说明数组里的元素是整型的.所以P 是一个指向由整型数据组成的数组的指针
int p(int); //从P 处起,先与()结合,说明P 是一个函数,然后进入()里分析,说明该函数有一个整型变量的参数,然后再与外面的int 结合,说明函数的返回值是一个整型数据
Int (*p)(int); //从P 处开始,先与指针结合,说明P 是一个指针,然后与()结合,说明指针指向的是一个函数,然后再与()里的int 结合,说明函数有一个int 型的参数,再与最外层的int 结合,说明函数的返回类型是整型,所以P 是一个指向有一个整型参数且返回类型为整型的函数的指针
& *运算符
这里&是取地址运算符,*是间接运算符。
&a 的运算结果是一个指针,指针的类型是a 的类型加个*,指针所指向的类型是a 的类型,指针所指向的地址嘛,那就是a 的地址。
*p 的运算结果就五花八门了。总之*p 的结果是p 所指向的东西,这个东西有这些特点:它的类型是p 指向的类型,它所占用的地址是p所指向的地址。
例六:
int a=12; int b; int *p; int **ptr;
p=&a; //&a 的结果是一个指针,类型是int*,指向的类型是int,指向的地址是a 的地址。
*p=24; //*p 的结果,在这里它的类型是int,它所占用的地址是p所指向的地址,显然,*p 就是变量a。
ptr=&p; //&p 的结果是个指针,该指针的类型是p 的类型加个*,在这里是int **。该指针所指向的类型是p 的类型,这里是int*。该指针所指向的地址就是指针p 自己的地址。
*ptr=&b; //*ptr 是个指针,&b 的结果也是个指针,且这两个指针的类型和所指向的类型是一样的,所以用&b来给*ptr 赋值就是毫无问题的了。
**ptr=34; //*ptr 的结果是ptr 所指向的东西,在这里是一个指针,对这个指针再做一次*运算,结果是一个int 类型的变量。
指针的运算
指针变量保存的是地址,而地址本质上是一个整数,所以指针变量可以进行部分运算,例如加法、减法、比较等,请看下面的代码:
#include <stdio.h>
int main(){
int a = 10, *pa = &a, *paa = &a;
double b = 99.9, *pb = &b;
char c = '@', *pc = &c;
//最初的值
printf("&a=%#X, &b=%#X, &c=%#X\n", &a, &b, &c);
printf("pa=%#X, pb=%#X, pc=%#X\n", pa, pb, pc);
//加法运算
pa++; pb++; pc++;
printf("pa=%#X, pb=%#X, pc=%#X\n", pa, pb, pc);
//减法运算
pa -= 2; pb -= 2; pc -= 2;
printf("pa=%#X, pb=%#X, pc=%#X\n", pa, pb, pc);
//比较运算
if(pa == paa)
{
printf("%d\n", *paa);
}
else
{
printf("%d\n", *pa);
}
return 0;
}
运行结果:
&a=0X28FF44, &b=0X28FF30, &c=0X28FF2B
pa=0X28FF44, pb=0X28FF30, pc=0X28FF2B
pa=0X28FF48, pb=0X28FF38, pc=0X28FF2C
pa=0X28FF40, pb=0X28FF28, pc=0X28FF2A
2686784
从运算结果可以看出:pa、pb、pc 每次加 1,它们的地址分别增加 4、8、1,正好是 int、double、char 类型的长度;减 2 时,地址分别减少 8、16、2,正好是 int、double、char 类型长度的 2 倍。
这很奇怪,指针变量加减运算的结果跟数据类型的长度有关,而不是简单地加 1 或减 1,这是为什么呢?
以 a 和 pa 为例,a 的类型为 int,占用 4 个字节,pa 是指向 a 的指针,如下图所示:
刚开始的时候,pa 指向 a 的开头,通过 *pa 读取数据时,从 pa 指向的位置向后移动 4 个字节,把这 4 个字节的内容作为要获取的数据,这 4 个字节也正好是变量 a 占用的内存。
如果pa++;使得地址加 1 的话,就会变成如下图所示的指向关系:
这个时候 pa 指向整数 a 的中间,*pa 使用的是红色虚线画出的 4 个字节,其中前 3 个是变量 a 的,后面 1 个是其它数据的,把它们“搅和”在一起显然没有实际的意义,取得的数据也会非常怪异。
如果pa++;使得地址加 4 的话,正好能够完全跳过整数 a,指向它后面的内存,如下图所示:
指针的应用
观察下面两种方法的输出结果有何不同
例1、 值传递
#include <stdio.h>
void swap(int a, int b) {
int c = a;
a = b;
b = c;
}
int main() {
int a = 2, b = 3;
swap(a, b);
printf("a=%d\tb=%d\n", a, b);
return 0;
}
例二、指针传递
#include <stdio.h>
void swap(int* pa, int* pb) {
int c = *pa;
*pa = *pb;
*pb = c;
}
int main() {
int a = 2, b = 3;
swap(&a, &b);
printf("a=%d\tb=%d\n", a, b);
return 0;
}
值传递只是拷贝了一份变量的值到新的内存空间,对新内存空间中的变量进行操作,而不会对原有变量造成任何影响。指针操作则直接对原有变量内存空间中的数据进行了修改,从而正确地完成了交换数据的功能。
指针与数组的关系
#include <stdio.h>
int main() {
int array1[10];
char array2[10];
printf("数组元素中,第0个元素的地址 %d\n", &array1[0]);
printf("整型数组的首地址是 %d\n数组中第一个元素的地址是 %d\n", array1, &array1[0]);
//使用指针加1的效果:
printf("int型指针加1: %d\n", array1 + 1);
printf("字符数组的首地址是 %d\n数组中第一个元素的地址是 %d\n", array2, &array2[0]);
//使用指针加1的效果:
printf("char型指针加1:%d\n", array2 + 1);
return 0;
}
C\C++使用指针的类型来确定一次指针变量 +1 需要在内存空间向后移动多少个字节,同时
指针的类型也使得程序能够正确地完成指针–>值的转换(这里的"值"指的是指针指向的内存
空间中储存的值)。
其次,上面的程序也证明了数组的首地址与数组名中储存的地址相同!
下图是一个字符数组在内存空间内的存储结构图,对于字符数组,其指针每加“1”在内存空间内就是将指向的地址向后移动了一个字节(8位)的长度。
下图是一个整型数组在内存空间内的储存结构图,对于整型数组,其指针每加“1”在内存空间内就是将指向的地址向后移动了四个字节(32位)的长度。
二维数组的指针
#include <stdio.h>
int main() {
int array[3][10];
printf("二维数组首地址 %d\n", array);
printf("第一行一维数组首地址 %d\n", *array);
printf("二维数组第二行地址 %d\n", array + 1);
printf("第二行一维数组首地址 %d\n", *(array + 1));
return 0;
}
是不是觉得二维数组的指针还是挺好理解的
那下面来看看提高部分吧
#include <stdio.h>
int main() {
int array[3][10] = {};
int **p = array;
printf("第1行第0列的值 %d\n", **(p1+1));
return 0;
}
好吧,这样居然就报错!我强制转换总行了吧!
#include <stdio.h>
int main() {
int array[3][10] = {};
int **p = (void**)array;
return 0;
}
还错,那怎样才正确嘛!
#include <stdio.h>
int main() {
int array[10][10] = {};
int(*p1)[10] = array;
printf("第1行第0列的值 %d\n", **(p1+1));
return 0;
}
二维数组指针总结
二维数组指针和二级指针的差异来自于内存中储存数组方式的不同
理想的情况下,二维数组储存的方式如下图所示
而在计算机的内存中,考虑到内存空间的总量有限,二维数组是以下图的形式储存的
也就是说,在C\C++中,二维数组是一个伪二维数组,它本质上就是一个一维数组