理解指针
指针
C++英文名称CPlusPlus是对于C的一个扩展继承于C的精华,并对其进行扩充,而对于C语言来说如果你没有掌握指针,那么你很难学会和灵活运用这门编程语言。
接下来我从我学习和认知的角度出发分享下指针的认识
指针的定义
我们常见的指针是如下定义的
int *p1;
int p2;
从上述语法角度看指针和普通的变量有什么区别呢?区别在于多了一个*号,而正是这个星号赋予了指针不同的涵义。首先要声明的一点是,*是跟随变量而不是数据类型int也就是说
int *p1,p2
上述例子定义了一个int类型的指针变量p1和一个int类型的普通变量p2。所以从上述例子中我们可以看出指针是一类特殊的变量而不是特殊的变量类型。
那么接着我们知道p2是一个变量里面存储着int类型的值,那么p2里面存储的是什么东西呢?以及p2和p1存在什么关联呢?

在计算机中我们的变量是存储在内存空间中,可以想象一下有这样一张表格,如上图右侧所示。它代表了我们计算机内存,而一开始我们对计算机的内存分割成一个个个小的单元(通常以字节为单位),方便我们计算机识别和存储数据。那么int类型的数据是怎样存储的呢?int类型代表它占据4个单元长度如上图的int a,在这4个单元内存储着我们变量a的值。
除了知道变量a可以获得a的值,还有其他方法获得a的内容吗?答案就是指针,那么指针存储的是什么呢?
指针存储着我们的变量在上面说的表格中的开始位置(也可以说是变量a的地址),那么只知道开始位置还是不能获得a的内容还需要知道a的长度,也就是int类型所占的长度,这样我们就可以根据p获得的地址连续访问4个单元,这样我们就可以获得a的内容。
tips:从内存的角度来看,各种变量类型其本质上是告诉计算机我数据的长度是多少,因而在后续的过程中我们可以利用此实现各种数据的转换。
所以回到我们开始的例子
int *p1;
int p2;
p1 = &p2;
p2 = 3;
p1存储的是一个地址,p2是一个具体的变量。*在定义时表示这是一个指针变量,在调用时*代表从p1地址开始按照int类型长度(4 字节),在内存中连续读取4个字节的内容后按照int的格式获得数据。
因而我们也可以这么说*p1和p2从返回结果上看没有什么区别,但是一定要记住p1存储的是地址.
tips:上述代码中出现了一个&,这个字符表示取址,即获取当前变量的内存地址。记住指针存储的内容是地址,当想要获取对应地址的内容时,记得用*(指针变量名)
地址和变量的区别
先说一下什么是野指针,这是什么意思呢?
在内存中如果我们定义了一个指针变量,但是我们没有对其赋予一个值时,那么这个指针指向什么位置呢?处于这种薛定谔的状态的指针我们就可以说他是一个野指针,访问野指针是一件极其危险的事,因为我们也不知道这个指针指向的位置和操作的数据,因而我们应该极力避免这种情况。对于创建指针变量我们如果开始没有指定其内容的话一定要将其赋予成空指针
// 定义赋值
int *p = NULL; //C style
int *p = nullptr; //C++ style
tips:C++推荐采用C++11标准的nullptr来进行空指针的表示,NULL其本质为定义了一个叫NULL的宏这个宏代表0
举一个简单的例子,有一家图书馆。当定义一个变量的时候,相当于图书馆购买了一本编号为0x2333电子书,图书馆既拥有这本书也能对这本进行各种操作。
接着创建了一个指针,相当于你在图书馆办了一个电子借阅证,接着你尝试借阅这本0x2333电子书,这相当于给指针赋值,将你的电子借阅证指向了0x2333这本书的存储位置。也就是说你有这本书访问权和修改权限但是这本书并不是真正属于你的。
一天图书馆决定对它的0x2333电子书打上自己的水印(变量修改了内容)。此时,你通过借阅证访问0x2333图书是添加了水印的图书(书被修改了)。
一天你看书觉着某个地方写的有问题,在书中加了自己的题注(修改了内容)。此时,图书馆内的0x2333电子书内容也增加了你的题注。
一天你看完了这本书,于是你把上一本书还回去了,又借了一本新的书0x9999(指针指向新的内容),原来的图书依然属于图书馆,而此刻你又能更改新书的内容。
上述中0x2333就是地址,而书籍本身是变量具体的内容(内存中的数据)
综上,指针是一个根据地址访问内容,而不是拥有实际内容的特殊变量。同时访问的地址+地址存储的内容等于变量*address = p
指针的指针和函数参数传递
当我们操作函数的时候,有时候希望我们传入的数据可以得到更改,但是显示往往是,函数内部处理和外部无关,仅仅依赖于返回值,在下面的例子中x的值并没有得到改变仅仅是传入的值。
传入值
int func(int x)
{
x = x + 1;
return x;
}
int main()
{
int p = 10;
int u = func(p);
return 0;
}
tips:C++函数传递的过程中分为值传递和引用传递,其中引用传递实际上就是对指针传递的封装和简化
我认为从本质上讲函数内容的传递遵循如下过程
- 对于输入变量p,将其内容赋给临时变量x
- 处理x
- 函数结束,临时变量x销毁释放
传入地址|传入指针
所以从上述内容来看我们函数内外操作的都不是一个内容所以也就解释了为什么在函数内部操作了,但是函数外部没有收到影响。如果我们传递一个指针呢?
void func(int *x)
{
(*x) = (*x) + 1;
}
int main()
{
int p = 10;
int *v = &p;
func(&p);
printf("%d\n",p);
func(v);
printf("%d\n",p);
return 0;
}
还记的我们之前提到的公式吗?
\[*p = P\]
在这里我们看一下发生了什么过程
- 输入变量p的地址,将地址赋给x
- (*x)获取地址内容并操作
- 返回(*x)的内容
此时我们可以看到u和p的内容都发生了改变,这是为什么呢?我们看一下上述过程由于我们的函数参数变量是*x所以当我们呢将p赋给x实际上是赋给的p的地址,这也是我们为什要加上取址符&
接着我们通过*x获得对应地址的内容也就是变量p存储的内容,将其加1又赋值给(*x)
这样通过操纵指针我们就可以在函数内部修改函数外部的内容
指针的指针
上述内容在我们普通数据处理的过程中基本就够用了,为什么还需要指针的指针呢?
在部分情境下,我们需要申请、修改等对于内存的操作,而我们知道指针本质上是一个地址,当我们指向新的内存地址后原变量存储的内容也就不会随之发生变化了。这在一些树、堆、栈等需要动态申请、释放内存的数据结构中特别常见。
#include <stdio.h>
#include <cstdlib>
typedef int *intPoint;
void func1(int **x)
{
(*x) = (int*)malloc(sizeof(int));
(**x) = 1;
}
void func2(intPoint *x)
{
(*x) = (int*)malloc(sizeof(int));
(**x) = 2;
}
void func3(intPoint x)
{
x = (int*)malloc(sizeof(int));
(*x) = 3;
}
int main()
{
int *p1= NULL;
int *p2=NULL;
int *p3 =NULL;
func1(&p1);
func2(&p2);
func3(p3);
// printf("%d %d %d",*p1,*p2,*p3); //p3指向NULL编译器报错 期望为 1 2 NULL
}
func1
- 传入p1指针的地址,赋给临时变量x(也就是说x是指针的指针)
- 申请一段内存获得地址,对于指针的指针x取地址里的内容(*x)获得指针也就是p1,赋予p1新开辟的内存
- 采用(**x)获得这段内存的内容赋值1
- 返回主函数后输出p1地址的内容(*p1)为1
tips:(void*)malloc(内存长度) 动态申请内存并返回他的开始地址,借助前面的强制转化转化为其他类型并赋给指针(地址)。
func2
本质和func1没有区别,使用了typedef对指针进行了重命名,掩盖了一层指针
tips: typedef 是类型重命名,可以将类型名称如int重命名为我们需要的内容如Speed表示这是一个速度类型等,原有类型不受影响仅仅是新增一个别名。上述中我们采用typedef从int重定义为*intPoint新增了一个int指针类型intPoint,在这里我们仍然那可以采用之前的公式*p=p理解这个过程int = *intPoint 因而这里生成的是指针类型
func3
- 传入指针p3,赋给临时变量x(指针类型)
- 申请一段内存,并将其地址赋给x,
- 对于(*x)即x地址的内容进行赋值3
在Step2中由于新申请的内存赋给x导致此后x指向的内容和开始传入的p3指向的内容已经发生分离,也就是说两者指向了不同的地址指向了不同的内容。注意并不推荐此种写法,在此发生了内存的泄漏。
tips : 内存泄漏,动态申请内存有一个很重要的问题就是注意内存的管理。当申请一段内存后,用完后要及时释放,否则程序不断申请内存不释放相应资源会导致内存耗尽。这在指针的使用中尤为注意因为指针仅仅相当有有使用权而不具有所有权,当指针因为使用周期结束销毁或者指向了新的内容,而原有内容的内存没有释放掉这就发生了内存的泄漏。
指针的类型转化
未完待续

浙公网安备 33010602011771号