指针_Pointer
**
没有任何装逼的意思,我强烈建议大家VS和notepad一起使用。
**
小伙子,请问做程序员压力大吗?
写在最前面的两个知识点:
1. 首地址:一段内存空间中第一个存储单元的地址。存储单元。
2. 指针变量的加减:+ - ++ --。以指针指向的类型空间为单位进行偏移。
一、内存四区
- 理解指针的工作原理以及使用指针操作数据,首先要理解计算机内存数据之间的工作原理。 C语言之所以效率高,就是因为C可以像汇编一样去操控内存。
所以,也可以说,学C语言,学的就是内存。
1.1 粗浅理解内存
假设计算机的内存为图1.
如果我们定义一个整型变量a。int a = 5;那么,在计算机的从内存中,就会开辟出四个字节4B的空间。图中每个空格表示一个字节,红色部分表示一个int型变量所占的内存空间为4B。
- 栈区:系统自动开辟,自动释放;不死很大。
- 堆区:程序员手动开辟,手动释放,在C++中是,new delete。
- 全局区:全局静态变量。
- 代码区:保存代码。
什么叫自动开辟呢!
我们定义一个for循环:
int nain()
{
for(int i = 0; i<0;i++)
//函数体
cout << i <<endl;
return 0;
}
这个时候,这个i占据的空间就是程序自动开辟出来的变量空间,在该for循环运行结束后,会自动释放。
该图片来自黑马老师视频
二、地址
- 把内存以字节为单位进行编号,这个编号就是地址。并且是唯一的,如图2所示,我们可以把内存看成是一个由一个个1个字节的空间组成的“长”。而每个字节所在的空间(方格)都有一个编号,这个编号就是地址。如下面所示,假设第一个地址是1000,以此类推,这些编号都是连续的、唯一的。
- 当然了,我们也可以这些内存理解成学校里的每间教室,而每个“方格”就是一个房间,每个房间都有一个门牌号,这个门牌号其实就是地址。比如,六号教学楼的六楼的6号房间,就是666。
地址是一种数据,
使用 &表示取址符,该符号返回一个变量的地址。
实例:
int main()
{
int a=6;//定义一个整形变量。
int * ptr_a = &a;//定义一个指向变量a的变量,该变量存储a的地址。
return 0;
}
- 注意了,这个很重要》!!首地址:首地址是这个数据的 第一个存储单元 的地址。{后面细讲}
三、首地址
一段内存空间中第一个存储单元的地址。
- 如图3所示,我们先定义一个整型变量a,inta;,该变量在内存空间中占据四个字节(4B),如图3中上半部分。则首地址就是1000,即一段内存空间中第一个存储单元的地址。
- 这时,我们在定义一个一维数组inta[5];。那个这个数组由5个整型变量组成,因此占据了20个字节。其中第一维就占据了4个字节(这边一定要理解)。
- 这个时候的首地址依然是1000,但是这个时候的1000就不是a[0]的第一个单元的地址了,而是a[0]的地址。
- 到这边的时候大家可能都还不明白,还是稀里糊涂的。OK,没关系。
如图4所示:
- 这边大家都可能还会有点不明白,但是后面肯定就会懂了。多看几遍。这一章节的主要任务是大家要记住什么是首地址。
四、指针变量
到这里,我们才正式进入主题。
- 用来存放地址的变量;
- 内存大小永远为4B。
地址是一些编号,一种数据。
变量有好多类型:
整型变量:int a;
字符型变量: char c;
小数:float f;
地址: 指针变量。
总的来讲,指针变量是用来存放内存地址的变量。
4.1 指针变量的定义
格式:数据类型 *变量名;
int main()
{
int a = 5;
int *p;//这是定义一个指针变量p,其中p中存的是地址。
//int则指明了这个指针中所保存的数据类型。
//*则指明了p是一个指针变量。
int *ptr_a = &a;
}
解释一下int *ptr_a = &a;//&表示取址符,获取变量a的地址。
这句话我们可以给写成下面两个语句的形式:
int *p;
p = &a;//理解成p指向a。
如图5所示,我们假设每个空格是4字节。
为什么我们说p指向a呢,因为p存储着变量a的地址。
4.2 指针变量的赋值
很简单,如下所示,完成赋值。
int *p;
p = &a;
我们的重点是如何访问每个变量
4.3 指针变量的引用
访问a这个变量。
- 方法一:直接使用下标法。
int main()
{
int a[5] = {1,2,3,4,5} ;
for(int I = 0;i<5;i++)
{
//打印出每个变量
cout <<a[i] << “,”;
}
cout << endl;
}
- 方法二:使用变量名访问。
int main()
{
int a = 5;
cout <<”a = ”<< a <<endl;
}
- 方法三:使用指针访问
用p指向的内存中的值。
用指针访问变量的值的方法:*指针变量(即:*p)
int main()
{
int a = 8;
int *p;
p = &a;
cout << “使用指针p=”<< *p<<endl;
}
这边需要注意:关于指针的*的两种情况:
- 当使用 ∗ * ∗去访问变量的值时,即 *p,此时的 ∗ * ∗表的是取值运算符,返回的是某一个地址中的值。参见上述公式第4行.
- 当在定义指针变量的时候,即int ∗ p _{}^{*}\textrm{p} ∗p,此时的 ∗ * ∗只是表示定义这个p为一个指针。此时的a和 ∗ p _{}^{*}\textrm{p} ∗p是一个概念,因为 ∗ p _{}^{*}\textrm{p} ∗p是指向a的。参见上述公式第6行。
int main()
{
//验证*号的用法
int a = 5 ;
int *p ;
p = &a;
int *p_a = &a;
cout << "取地址 p:" << p << endl;
cout << "取地址p_a:" << p_a << endl;
//如果想获取地址所存储的变量,则需要加上*号
cout << "取数值 p:" << *p << endl;
cout << "取数值p_a:" << *p_a << endl;
}
这边结合图5一下子就明白啦
- 野指针:
不能明确指向的指针变量,这种指针很危险的。
举个例子:小时候会有人说野孩子,什么是野孩子?就是找不到父母,没有父母的流浪孩子。这个野指针也是这样,野指针不知道自己具体指向哪个变量,以后你调用这个野指针的时候,会非常危险,因为你不知道他返回的是什么样的值。
来个代码说明一下:
先来个简单的,
int main()
{
int a;
cout<< a<<endl;
return 0;
}
这个时候,你打印这个变量,绝对编译器会报错,不信你可以试试。
同样,对于野指针也是这样:
int main()
{
int a = 5;
int *p;
cout<< a<<endl;
cout<< p<<endl;
return 0;
}
此时,p离面保存的地址是不确定的,p的指向是不明确的。
危险,危险,危险……
比如说,这个指针指向的是sudo rm –rf,那就惨啦。不懂的就去百度sudo rm –rf的意思啦。
- 肯定有解决方案:
在定义这个指针的时候,如果你暂时用不到它,就把他弄成一个空指针。
int *p = NULL;
下面开始讲空指针啦
- 空指针:
- 指针有如下几种类型:int* float* char* double ∗ ^{*} ∗等类型的指针,而定义空指针则使用:void ∗ ^{*} ∗。也就是说这样的一个指针不知道指向一个什么样类型的内存(这个比野指针还恶心好像)。
- 简单解释一下,假如我在内存中开辟了一个4B的内存空间,这4B的空间可以存放int ∗ ^{*} ∗型,long ∗ ^{*} ∗型,float ∗ ^{*} ∗型。在我还不知道这个内存到底存什么类型的变量的时候,我可以先用void*开辟一个空间。然后再需要使用的时候强制转换一下。强制转换后面讲好不好。
4.4 指针变量的运算
严格来讲,指针的运算只有下面四个:
+ - ++ –
有哪些没意义的地址运算操作:
- 两个地址相加,例如:北京路1号+南京路1号。没有任何意义
- 两个地址相乘或一个地址乘以或者除一个常数,例如:北京路1号*3,没有任何意义
什么是有意义的呢?
指针的偏移,即指针的运算就是地址的偏移。如图6所示。
注意啦!!下面是关键。
一). 指针变量的加减,以指针所指向的类型空间为单位进行偏移。我们上面图6的例子是默认该指针所指向的类型空间是1B,依次每次+1指针都偏移一个字节。
二). 因为我们可以定义不同类型的指针:
char *p; // 1B 指针+1一次,指针偏移1个字节
int *p;// 4B 指针+1一次,指针偏移4个字节
double *p;// 8B 指针+1一次,指针偏移8个字节
…
当然了,也不是说只能+1 还可以+2 +3 ………
-----------------------------------------------------
int main
{
int a;
int * p_a = &a;
double b;
double *p_b = &b;
cout << "int型变量站4个字节" << endl;
cout << " p_a=" << p_a<<endl;;
cout << "p_a+1=" << p_a+1<< endl;
cout << "p_a+2=" << p_a + 2 << endl;
cout << "double型变量站8个字节" << endl;
cout << " p_b=" << p_b << endl;
cout << "p_b+1=" << p_b+1 << endl;
cout << "p_b+2=" << p_b + 2 << endl;
return 0;
}
上述代码在我的电脑上的截图,每次运行,每个人的电脑结果是不一样的。但是偏移量是一样的。
- 上述是16进制计算的,大家看的时候要注意点。Int型每次偏移4个,+2的时候就偏移八个字节。
- double型占8个字节,每次偏移8个字节,+2的时候就偏移16个字节。大家算一下就知道了。
三). 强调开头的那两个知识点了。
1) 首地址:一段内存空间中,第一个存储单元的地址。存储单元。
2) 指针变量的加减,以指针所指向的类型空间为单位进行偏移。
五、一维数组与指针
当我们去定义一个数组,这个数组里面的元素是连续存放的。数组名是这个数组的《首地址》。
int main()
{
int x[5], i;
for (i = 0; i < sizeof(x); i++)
printf("00%x\n", ((char*)(&x)) + i);
cout << "数组名x地址:"<< x <<endl;
cout << " 寻址&x地址:" << &x << endl;
return 0;
}
看,截图是我的运行结果。数组名x的地址就是第一个变量的地址,即首地址。而寻址&x获取的值也是第一个变量的地址,即首地址。
对,大家都发现了。
- 这两个东西是一样的。但是,他们所指向的类型空间不一样(看到没有,这边是上面的第二句话)。下面我来证明一下他们来的类型空间不一样。这个关键哦,学会了就懂得如何操作指针的第一步了。
int main()
{
int b[5];
int *ptr_b = &b[5];
cout <<" b="<< b << endl;//b这个地址指向b[0]这个元素,因为是整型,因此占4个字节。
cout<<" &b=" << &b << endl;//&b指向的是整个数组,整个数组有5个int型的元素,因此有每当&b+1时,指针移动的是5*4 = 20 个字节。
cout << " ptr_b=" << ptr_b << endl;
//所以有结论,指针变量的加减,以指针指向的类型空间为单位进行偏移。
cout << " b+1=" << b+1 << endl;
cout << " &b+1=" << &b+1 << endl;
cout << "ptr_b+1=" << ptr_b+1 << endl;
return 0;
}
这个图是上面的一次运行结果。分析结果都在代码的注释里面了。
总结一下:
- a这个地址指向的是a[0]这个int元素,他的类型是:int*,4个字节 。
- &a这个地址指向整个数组元素,它的类型是:int(*)[5];
这边大家后面进阶部分会可以理解,或者百度其他坐着的连接。
5.2 访问数组元素
- 下标法:这个很简单啦,学过matlab和python的小伙伴应该很喜欢的。
int main()
{
int a[5] = { 1,2,3,4,5 };
for (int i = 0; i < sizeof(a) / sizeof(int); i++)
{
cout << a[i] << ", " ;
}
cout << endl;
return 0;
}
- 指针法:
下面就有意思啦。
int main()
{
int a[5] = { 1,2,3,4,5 };
int *p_a = a;//p_a指向a[0]
cout << *p_a << endl;
for (int i = 0; i < sizeof(a) / sizeof(int); i++)
{
cout << "a[" << i << "]=" << *p_a + i << endl;//或者*(p_a + i),都可以。
}
Return 0;
}
这边大家结合着这句话理解呀!
- 当使用 ∗ * ∗去访问变量的值时,即 ∗ p _{}^{*}\textrm{p} ∗p,此时得 ∗ * ∗表的是取值运算符,返回的是某一个地址中的值。
- 当在定义指针变量的时候,即int
∗
p
_{}^{*}\textrm{p}
∗p,此时的
∗
*
∗只是表示定义这个p为一个指针。此时的a和
∗
p
_{}^{*}\textrm{p}
∗p是一个概念,因为
∗
p
_{}^{*}\textrm{p}
∗p是指向a的。
int main()
{
int a[5] = { 1,2,3,4,5 };
int *p_a = a;//p_a指向a[0]
cout << *p_a << endl;
for (int i = 0; i < sizeof(a) / sizeof(int); i++)
{
cout << "a[" << i << "]=" << *p_a++ << endl;
}
return 0;
}
小小的分析一下:
- *(p+1):进行运算的时候,p是不会发生变化的
- *p++:进行运算的时候,p是一直在偏移的。
我们说了,a是数组的第一个元素的地址,它指向a[0]。我们也定义了一个 ∗ p _{}^{*}\textrm{p} ∗p = a;这个 ∗ p _{}^{*}\textrm{p} ∗p也指向a[0]。
六、二维及以上数组与指针操作
6.1 二维及以上数组的存储特性
- 首先,我要先把你们的思想掰直了。特别是那些先matlab和python的非计算机专业的同学,尤其是先学matlab的同学非计算机专业的同学。你们可要注意了:看图7
图7
虽然我们经常会说一个矩阵式m行n列,但是在计算机中不是这样存储的,而是就把他存储成一行。至于我们在matlab中看到的,那些都是经过内部代码处理的,最核心部分其实是还是指针,只是开发人员将它们封装起来了。
我们举个例子,首先,我们定义一个3行4列的矩阵。虽然我们平时说他是三行四列,但是在计算机中,其实就是一行的连续存储方式,如图8所示。像我们在数学书上看到的那种正方形的形式,是根本不存在的。
int main()
{
//二维数组部分
int a[3][4] = { 1,2,3,4,5,6,7,8,9,10,11,12 };
//虽然我们定义的这个a是一个3行4列的,但是,在我们的计算机存储中,他已然是连续存储的。
cout << "a[0]=" << a[0] << endl;
cout << "a[1]=" << a[1] << endl;
cout << "a[2]=" << a[2] << endl;
}
- 这边,其实a[0]表示第0行,其实a[1]表示第1行,其实a[2]表示第2行。因为每行有4个元素,每个元素占4B;上面的代码显示的指针偏移正好每个偏移16位,而代码的进制也正好是16进制的。
- 这也验证了我们上面的话:“a是数组的第一个元素的地址”。我们将上述代码扩充,显示每个地址和每个地址对应的元素。
int main()
{
//二维数组部分
int a[3][4] = { 1,2,3,4,5,6,7,8,9,10,11,12 };
int i;
//虽然我们定义的这个a是一个3行4列的,但是,在我们的计算机存储中,他已然是连续存储的。
cout << "a[0]=" << a[0] << endl;
cout << "a[1]=" << a[1] << endl;
cout << "a[2]=" << a[2] << endl;
for ( i = 0; i < sizeof(a); i++)
{
//printf("0%x\n", &a+ i);
cout<< &a + i << endl;
}
cout << "a[1][0]=" <<a[1][0] << endl;
}
- 但是如果想显示a[0] a[1] a[2]所对应的元素,还是需要调用指针的,这时候其实就能看出来,计算机存储是一个连续的了,而不是几行几列的那种。
- Note that:地址是一直在变的。为什么在变呢,因为计算机的运行内存你就那么点,16G,32G。。。。。看你插几个内存条了。这么点内存,肯定是用的时候开辟,不用的时候就是释放掉。
下面就小小的总结一下上面要表达的东西。 - 数组名:a。a是数组a[3][4]的首地址。首地址????大家应该知道了吧。a的类型是:int(
∗
^{*}
∗)[4];所以,a+1,加一次就偏移16个字节。a[0]是这个一维数组的数组名,a[0]指向a[0]
[0],这边为什么说指向a[0] [0],大家看上面的地址就知道了。所以a[0]
的类型是:int ∗ ^{*} ∗,每次加4个字节。而&a的类型是:int ∗ ^{*} ∗[3][4],所以它是指向整个数组的。所以&a+1;则跳过48B。
a指向数组的第一个存储单元,第一个存储单元就是这个数组的第一个一维数组。一个二维数组的存储单元就是一个个的一维数组。因此,我们可以引申出,一个三维数组a[2][3][4],那么,我们先把这个数组先拆成两个二维数组,然后,再把这两个二维数组分别拆成三个一维数组,再把这每个一维数组拆成四个元素。所以,这个数组我们可以将其看成是一个由两个3*4的矩阵组成的数据。那个,三维数组的第一个存储单元就是一个二维数组。以此类推,到高维数组。
int main()
{
//二维数组部分
int a[3][4] = { 1,2,3,4,5,6,7,8,9,10,11,12 };
int i;
//虽然我们定义的这个a是一个3行4列的,但是,在我们的计算机存储中,他依然是连续存储的。
cout << "a[0]=" << a[0] << endl;
cout << "a[1]=" << a[1] << endl;
cout << "a[2]=" << a[2] << endl;
for ( i = 0; i < sizeof(a); i++)
{
//printf("0%x\n", &a+ i);
cout<< &a + i << endl;
}
cout << "a[1][0]=" <<a[1][0] << endl;
cout << "a指向数组的第一个存储单元:" << a << endl;
}
好,下面来个震撼的,大家就可以一目了然了。我们定义一个八维数组
int a[2][3][4][5][6][7][8][9];
则:a是数组名。
- a指向7维数组
- a[0]指向6维数组
- a[0][0]指向5维数组
- a[0][0][0]指向4维数组。
- ……
- 理解了这些东西有什么用? 对了,就是访问数组。
6.2 二维及以上数组元素的访问
在5.2小节中,我们学习了一维数组的访问,分别有下表法和指针法。
6.2.1 下标法
下标法我们就不介绍了。直接给个代码吧
int main()
{
//下标法在二维数组中的应用
int a[3][4] = { 1,2,3,4,5,6,7,8,9,10,11,12 };
//简单调用
cout << "简单访问" << endl;
cout << "a[0][0] = " << a[0][0] << endl;
cout << "a[0][3] = " << a[0][3] << endl;
cout << "a[1][0] = " << a[1][0] << endl;
cout << "a[1][3] = " << a[1][3] << endl;
//for循环调用
cout << "for循环访问" << endl;
for (int i = 0; i < 3; i++)
{
for (int j = 0; j < 4; j++)
{
cout << "a[" << i << "][" << j << "] = " << a[i][j] << endl;
}
}
return 0;
}
- 下标法没什么好讲的。大家都懂的,特别是学过matlab的同学,什么切片什么的,matlab里面用的贼方便。
6.2.2 指针法
先看图。如图9所示。同样是针对那个数组:int a[2][3][4][5][6][7][8][9];
- 我要根据指针操作,找到m行的第n个元素。首先找到这一行的首地址,然后再对该指针进行加操作,实现指针的偏移。如图9所示,如果我们想要访问图中三角形所示的元素,我们首先要找到该维数组的首地址,然后+2。
- 即a[m]+m。
- 不多说,直接上代码
int main()
{
//用指针访问二维数组。
int a[3][4] = { 1,2,3,4,5,6,7,8,9,10,11,12 };
int *ptr_a;
//简单访问元素
cout <<"访问第二行第三列的元素7: "<< *a[1] + 2 << endl;
cout << "访问第三行第二列的元素10: " << *a[2] + 1 << endl;
//使用for循环访问
cout << "用指针访问" << endl;
for (int i = 0; i < 3; i++)
{
for (int j = 0; j < 4; j++)
{
cout << *a[i]+j <<" ";
}
cout<<endl;
}
return 0;
}
下一章可能会是引用把
有错误的请指正,微信:shuoshi6666.欢迎讨论。