指针【一】(C/C++)
1. 什么是指针
指针为C/C++中的一种变量,与常见的 int
, double
等相似,只不过指针变量存储的是一段地址。因此,可以在将指针看做门牌号。C/C++中,可用以下方法定义指针
type *var_name;
其中, type
是指针的基类型,它必须是一个有效的 C/C++ 数据类型, var-name
是指针变量的名称。用来声明指针的星号 * 与乘法中使用的星号是相同的。
但是,在这个语句中,星号是用来指定一个变量是指针。
如下就是指针的有效声明
int *ip; //整型指针 double *dp; //双精度浮点型指针 char *cp; //字符型指针
实际上,所有实际数据类型,不管是整型、浮点型、字符型,还是其他的数据类型,对应指针的值的类型都是一样的,都是一个代表内存地址的长的十六进制数。
不同数据类型的指针之间唯一的不同是,指针所指向的变量或常量的数据类型不同。
比如:
int *ptr; //指针的类型是int*
char *ptr; //指针的类型是char*
int **ptr; //指针的类型是int**
int (*ptr)[3]; //指针的类型是int(*)[3]
int *(*ptr)[4]; //指针的类型是int*(*)[4]
你通过指针来访问指针所指向的内存区时,指针所指向的类型决定了编译器将把那片内存区里的内容当做什么来看待。我们在后面将会讨论。因此,用与数据类型不相符的指针操作数据会出错。**
敲错门肯定完蛋
华丽的分割线
2.指针的值(指针的指向)
由上可知,指针也是一种变量,因此它具有变量的通性——可赋值,也就是说,指针也是有值的。指针的值是指针本身存储的数值,这个值将被编译器当作一个地址,而不是一个普通的数值。指针所指向的内存区就是从指针的值所代表的那个内存地址开始,长度为sizeof(type)
的一片内存区域。通常情况下,如果通过 cout
输出指针的值,它大概长这个样子 0xef0002781b
。
以后,我们说一个指针的值是XX,就相当于说该指针指向了以XX 为首地址的一片内存区域;我们说一个指针指向了某块内存区域,就相当于说该指针的值是这块内存区域的首地址。
毕竟只有门牌号没有房子🏠还是挺可怜的
又是华丽的分割线
3.指针所占空间及多级指针
由于指针本身就是一种数据类型,因此操作系统也会开辟空间用来存放指针,就和普通变量一样。既然需要存放,指针肯定会占用内存,通常,一个指针变量占用4字节空间
另一方面,也正因如此,指针也有一个地址,或者说门牌号,而这个门牌号,又可以成为其它指针的值,因此,多级指针也就很明了了。
门牌号的门牌号,疯狂套娃🐶
###多级指针###
假设有一个 int 类型的变量 a,p1是指向 a 的指针变量,p2 又是指向 p1 的指针变量,它们的关系如下图所示:
将这种关系转换为C++代码:
int a =100;
int *p1 = &a;
int **p2 = &p1;
C/C++不限制指针的级数,每增加一级指针,在定义指针变量时就得增加一个星号*。p1 是一级指针,指向普通类型的数据,定义时有一个*;p2 是二级指针,指向一级指针 p1,定义时有两个*。
想要获取指针指向的数据时,一级指针加一个*,二级指针加两个*,三级指针加三个*,以此类推,有下例:
#include <iostream>
int main(){
int a =100;
int *p1 = &a;
int **p2 = &p1;
int ***p3 = &p2;
cout<<a<<*p1<<**p2<<***p3<<endl;
cout<<"&p2="<<&p2<<"p3="<<p3<<endl;
cout<<"&p1=" <<&p1<<"p2="<<p2<<"*p3="<<*p3<<endl;
cout<<"&a="<<&a<<"p1="<<p1<<"*p2="<<*p2<<"**p3="<<**p3<<endl;
return 0;
}
结果如下:
100, 100, 100, 100
&p2 = 0X28FF3C, p3 = 0X28FF3C&p1 = 0X28FF40, p2 = 0X28FF40, *p3 = 0X28FF40 &a = 0X28FF44, p1 = 0X28FF44, *p2 = 0X28FF44, **p3 = 0X28FF44
以三级指针 p3 为例来分析上面的代码。*p3等价于((p3))。p3 得到的是 p2 的值,也即 p1 的地址;(p3) 得到的是 p1 的值,也即 a 的地址;经过三次“取值”操作后,((*p3)) 得到的才是 a 的值。
假设 a、p1、p2、p3 的地址分别是 0X00A0、0X1000、0X2000、0X3000,它们之间的关系可以用下图来描述:
方框里面是变量本身的值,方框下面是变量的地址。
这不是分割线
4.指针的运算
通过对门牌号加减一个数,我们可以知道n个门之后的门牌号是多少,但是对门牌号做乘法就完全没有意义了除非你在练口算。指针也同理。下面的例子总结了指针的基本运算:
#include <iostream>
#using namespace std;
int main(){
int a = 10, *pa = &a, *paa = &a;
double b = 99.9, *pb = &b;
char c = '@', *pc = &c;
//最初的值
cout<<"&a="<<&a<<" &b="<<&b<< "&c="<<&c;
cout<<"pa="<<pa<<"pb="<<pb<<" pc="<<pc;
//加法运算
pa++; pb++; pc++;
cout<<"pa="<<pa<<"pb="<<pb<<" pc="<<pc;
//减法运算
pa -= 2; pb -= 2; pc -= 2;
cout<<"pa="<<pa<<"pb="<<pb<<" pc="<<pc;
//比较运算
if(pa == paa)
{
cout<<*ppa;
}
else{
cout<<*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 倍。这就说明上面的结论正确,也就是说所指内存为一段内存首地址。
##在这里,我们给以下解释:##
以 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 就表示指向下一个元素,减 1 就表示指向上一个元素,这样指针的加减运算就具有了现实的意义,也就是指针操作数组。这部分内容我们将在《指针2》详细探讨。如果有的话
注意!不要对非连续空间使用指针进行操作!
#include <iostream>
#using namespace std;
int main()
{
int a = 1, b = 2, c = 3;
int *p = &c;
int i;
for(i=0;i<8;i++)
{
cout<<*(p+i);
}
return 0;
}
C++17/20标准报错,C++99标准通过,结果如下:
3, -858993460, -858993460, 2, -858993460, -858993460, 1, -858993460,
可以发现,变量 a、b、c 并不挨着,它们中间还参杂了别的辅助数据。
指针变量除了可以参与加减运算,还可以参与比较运算。当对指针变量进行比较运算时,比较的是指针变量本身的值,也就是数据的地址。如果地址相等,那么两个指针就指向同一份数据,否则就指向不同的数据。上面的代码(第一个例子)在比较 pa 和 paa 的值时,pa 已经指向了 a 的上一份数据,所以它们不相等。而 a 的上一份数据又不知道是什么,所以会导致 cout
输出一个没有意义的数,这正好印证了上面的观点,不要对指向普通变量的指针进行加减运算。
未完待续~~~~~~
部分来自于:C语言中文网