指针深刻理解
看完鹏哥讲的c语言进阶视频后,又找来C语言深度剖析这本书仔细看了一遍,来进一步巩固和理解指针这个重点。
1:数组
如上图所示,当我们定义一个数组 a 时,编译器根据指定的元素个数和元素的类型分配确定大小(元素类型大小*元素个数)的一块内存,并把这块内存的名字命名为 a。名字 a 一旦与这块内存匹配就不能被改变。a[0],a[1]等为 a 的元素,但并非元素的名字。
这里需要注意的是:
1 sizeof(数组名)
,计算整个数组的大小,sizeof
内部单独放一个数组名,数组名表示整个数组。
- &数组名,取出的是数组的地址。&数组名,数组名表示整个数组。除此1,2两种情况之外,所有的数组名都表示数组首元素的地址。
c
A)char *p = "abcdef";
B)char a[] = "123456";
例子A中若是想访问字符串中的e的话,有两种方法,
(1)*(p+4)
这是这种方法,因为p中存放着这块内存首地址,加上偏移量4,即可访问到e。
(2)p[4]:编译器总是把以下标的形式的操作解析为以指针的形式的操作。p[4]这个操作会被解析成:先取出 p 里存储的地址值,然后加上中括号中 4 个元素的偏
移量,计算出新的地址,然后从新的地址中取出值。也就是说以下标的形式访问在本质上与以指针的形式访问没有区别,只是写法上不同罢了。
B的话和A同理。
下面继续来看一段代码来深刻理解&a和a的区别是啥:
c
int main()
{
int a[5]={1,2,3,4,5};
int *ptr=(int *)(&a+1);
printf("%d,%d",*(a+1),*(ptr-1));
return 0;
}
首先上面已经说过了除了sizeof(数组名)
和&数组名,其余的数组名都是表示数组首元素的地址。
对指针进行加 1 操作,得到的是下一个元素的地址,而不是原有地址值直接加 1。所以,一个类型为 T 的指针的移动,以 sizeof(T)
为移动单位。 因此,对上题来说,a 是一个一维数组,数组中有 5 个元素; ptr
是一个 int 型的指针。
&a + 1: 取数组 a 的首地址,该地址的值加上 sizeof(a)
的值,即 &a + 5sizeof(int)
,也就是下一个数组的首地址,当前指针已经越过了数组的界限。
ptr
这个指针变量存放的是下一个数组的首地址,ptr
也就是指向a[5],ptr-1
也就是指向a[4],
*(a+1): a,&a 的值是一样的,但意思不一样,a 是数组首元素的首地址,也就是 a[0]的首地址,&a 是数组的首地址,a+1 是数组下一元素的首地址,即 a[1]的首地址,&a+1 是下一个数组的首地址。所以输出 2
2:c指针数组与数组指针区别和理解
指针数组和数组指针从基本的汉字表面上去理解,会发现指针数组的主语是数组,数组指针的主语是指针,所以很明显的第一个区别就是指针数组是数组,用来存放指针。
下面接着看一段代码:
c
#include <stdio.h>
int main()
{
char a[5] = { 'A','B','C','D' };
char(*p3)[5] = &a;
char(*p4)[5] = a;
return 0;
}
因为char(*p3)[5] = &a ;char(*p4)[5] = a;
定义的都是数组指针,指向的数组大小为char[5]类型,虽然&a和a的值是一样的,但是其表示的意义是不一样的,这里一样的原因理解是,但由于&a 和 a 的值一样,而变量作为右值时编译器只是取变量的值,所以运行并没有什么问题。
对上述代码进行更改,将数组指针指向的数组大小发生更改。
c
int main()
{
char a[5]={'A','B','C','D'};
char (*p3)[3] = &a;
char (*p4)[3] = a;
return 0;
}
运行结果也好理解:p3+1 和 p4+1 的值就相当于跳过sizeof(char)*(数组指针所指向数组的大小,要是数组大小为3,这里就是3)
1:指针数组详解
其中指针数组的一个应用是可以用一维数组模拟二维数组。
c
#include <stdio.h>
int main()
{
int arr1[] = { 1,2,3,4,5,6,7,8,9 };
int arr2[] = { 7,8,9,6,5,4,1,2,3 };
int arr3[] = { 7,8,9,5,2,1,0,4,6 };
int* arr[] = { arr1,arr2,arr3 };//arr中每个元素都为一个指针,指针指向对应的数组;每个元素为对应数组的首地址
int i;
for (i = 0; i < 3; i++)
{
int j;
for (j = 0; j < 9; j++)
{
printf("%d", *(arr[i] + j));//实现对应元素的遍历;
//printf("%d", arr[i][j]);//实现对应元素的遍历
}
printf("\n");
}
return 0;
}
两种不同的方式来访问并打印数组元素:
-
使用指针运算和解引用操作符
*
:cprintf("%d", *(arr[i] + j));
这里,
arr[i]
获取指针数组arr
的第i
个元素,即指向arr1
、arr2
或arr3
的指针之一。arr[i] + j
计算出指向当前数组第j
个元素的指针,而*(arr[i] + j)
解引用该指针,获取该元素的值。 -
使用数组下标访问:
cprintf("%d", arr[i][j]);
这种方式更直观。
arr[i]
获取指向某个数组的指针,而arr[i][j]
直接访问该数组的第j
个元素。这是因为arr[i]
被视为指向第i
个数组的首元素的指针,而arr[i][j]
就是访问该指针所指向数组的第j
个元素。
第二种写法的原因如下:在C语言中,arr[i][j]
这种双重下标访问方式背后的原理与指针算术紧密相关。当你有一个指向指针的指针(或者数组的数组,或者指针数组,如本例所示),这种双重下标访问实际上是两步操作的简写:
arr[i]
访问第i
个元素,这里的元素是指向int
的指针(在你的例子中,它指向arr1
、arr2
或arr3
)。[j]
对该指针进行解引用,访问它所指向数组的第j
个元素。
1.1理解指针的指针和数组的数组
在C语言中,当你声明一个如 int* arr[]
的数组时,你创建了一个数组,其每个元素都能存储一个指向 int
类型的指针。所以,arr
是一个数组,但每个 arr
的元素(比如 arr[0]
、arr[1]
、arr[2]
等)是一个指针,指向一个 int
数组。
1.2双重下标访问的工作原理
当你使用 arr[i][j]
进行访问时,arr[i]
首先得到第 i
个数组的首地址(一个指针)。然后,[j]
操作符被应用到这个地址上,它实际上是通过指针算术来完成的:从这个地址开始,移动 j
个 int
的大小的距离,来访问特定的元素。这是因为在C语言中,数组名或指针被用作数组时,pointer[offset]
等价于 *(pointer + offset)
。
1.3为什么这样做是有效的
这种访问方式之所以有效,是因为C语言设计时就考虑到了这种使用场景。数组和指针在很多情况下是可以互换的。当你声明一个像 int *arr[]
这样的指针数组时,C语言允许你通过像访问二维数组那样的语法来访问它,即使它实际上是一个数组的数组或指针的数组。这种设计极大地简化了对于复杂数据结构的操作,同时保持了代码的可读性和易于理解。
简而言之,arr[i][j]
能够直接访问指针所指向数组的第 j
个元素,是因为C语言的设计允许通过指针算术和解引用操作符的组合来简化这种操作,使得指针数组的操作既直观又高效。
3:地址的强制转换
c
struct Test
{
int Num;
char *pcName;
short sDate;
char cha[2];
short sBa[4];
}*p;
假设 p 的值为 0x100000
。 如下表表达式的值分别为多少?
c
p + 0x1 = 0x___ ?
(unsigned long)p + 0x1 = 0x___?
(unsigned int*)p + 0x1 = 0x___?
在C语言中,当你对一个指针进行算术操作时,如 p + 1
,这个操作会根据指针指向的数据类型的大小来移动指针。这意味着,如果 p
是一个指向 struct Test
的指针,那么 p + 1
不是简单地将 p
的值增加 1
,而是增加 sizeof(struct Test)
的大小,这样 p + 1
就指向了内存中紧接着 p
指向的 struct Test
实例之后的下一个 struct Test
实例的起始位置。
给定的结构体 Test
包含以下成员:
- 一个
int
类型的Num
,通常是4个字节。 - 一个
char *
类型的pcName
,指针大小通常是4个字节(在32位架构上)或8个字节(在64位架构上)。 - 一个
short
类型的sDate
,通常是2个字节。 - 一个
char
类型的数组cha[2]
,总共2个字节。 - 一个
short
类型的数组sBa[4]
,总共8个字节。
根据这些信息,我们可以计算 struct Test
的大小。然而,实际的结构体大小还需要考虑到内存对齐(padding)。内存对齐是编译器自动进行的,以确保结构体中的每个成员都按照其自然对齐要求放置在内存中,这可能会导致结构体的实际大小比简单加起来的成员大小要大。,假定 struct Test
的总大小是20字节。这个大小已经考虑了可能的内存对齐。因此,当你做 p + 1
操作时,指针会按照结构体的实际大小(20字节)进行移动。
如果 p
的初始值是 0x100000
,那么 p + 1
的计算将是:
c
0x100000 + sizeof(struct Test) * 1
如果 sizeof(struct Test)
是20字节,那么:
c
p + 1 = 0x100000 + 20 = 0x100014
(unsigned long)p + 0x1 = 0x100001
(unsigned int*)p + 0x1 = 0x100004
unsigned long将p强制类型转为无符号长整型,数值不发生改变,但是所属类型已经发生了改变。
unsigned int*一个指向无符号整型的指针,所以为4个字节。
4:二维数组
实际上内存不是表状的,而是线性的。见过尺子吧?尺子和我们的内存非常相似。一般尺子上最小刻度为毫米,而内存的最小单位为 1 个 byte。平时我们说 32 毫米,是指以零开始偏移 32 毫米;平时我们说内存地址为 0x0000FF00
也是指从内存零地址开始偏移0x0000FF00
个 byte。既然内存是线性的,那二维数组在内存里面肯定也是线性存储的。实际上其内存布局如下图:
char a[3][4];
以数组下标的方式来访问其中的某个元素:a[i][j]
。编译器总是将二维数组看成是一个一维数组,而一维数组的每一个元素又都是一个数组。a[3]这个一维数组的三个元素分别为:a[0],a[1],a[2]。a[i][j]
的首地址为&a[i]+j*sizof(char)
写为指针的形式为:a+i*sizeof(char)*4+j*sizeof(char)
,可以换算成以指针的形式表示:*(*(a+i)+j)
。解释 *(a+i)
解引用得到第 i
行的首地址这一点,可能会有些混淆。对于二维数组 char a[3][4];
,a
本身可以被视为指向其第一个元素(即第一行 a[0]
)的指针。这里的每个元素(每一行)本身是一个 char[4]
类型的数组。因此,a
的类型是 char (*)[4]
,即指向一个含有 4 个字符的数组的指针。当我们说 *(a+i)
时,这里的操作实际上是在进行两个步骤:
- 指针算术运算 :
a+i
计算的是第i
行的首地址。由于a
是指向第一行的指针,a+i
实际上是利用指针算术来计算第i
行的起始地址。这里的i
乘以的是每行的大小(在本例中是 4 个char
),这是自动完成的,不需要显式乘以sizeof(char)
或行的大小。这一步并没有解引用,只是计算地址。 - 解引用 :
*(a+i)
的操作是对计算得到的地址进行解引用。但是,这里的"解引用"可能会造成一些理解上的混淆。在这个上下文中,我们不是在获取一个char
值,而是获取指向第i
行首元素的指针。这是因为a+i
已经是一个指向char[4]
的指针,解引用这个指针实际上给出的是char[4]
类型的数组的第一个元素的地址,这与直接使用a[i]
是等价的。 - 因此,当我们说"解引用得到第
i
行的首地址"时,我们实际上是在描述获取到这一行数组的起始点的过程。在 C 语言中,数组名(在这个例子中是a[i]
)被用作表达式时,会被转换(除了sizeof
和&
操作符的情况)为指向其第一个元素的指针。所以,*(a+i)
和a[i]
在这里是等价的,都表示第i
行的首地址。