指针深刻理解

指针深刻理解

看完鹏哥讲的c语言进阶视频后,又找来C语言深度剖析这本书仔细看了一遍,来进一步巩固和理解指针这个重点。

1:数组

如上图所示,当我们定义一个数组 a 时,编译器根据指定的元素个数和元素的类型分配确定大小(元素类型大小*元素个数)的一块内存,并把这块内存的名字命名为 a。名字 a 一旦与这块内存匹配就不能被改变。a[0],a[1]等为 a 的元素,但并非元素的名字。

这里需要注意的是:

1 sizeof(数组名),计算整个数组的大小,sizeof内部单独放一个数组名,数组名表示整个数组。

  1. &数组名,取出的是数组的地址。&数组名,数组名表示整个数组。除此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;
}

两种不同的方式来访问并打印数组元素:

  1. 使用指针运算和解引用操作符 *

    c 复制代码
    printf("%d", *(arr[i] + j));

    这里,arr[i] 获取指针数组 arr 的第 i 个元素,即指向 arr1arr2arr3 的指针之一。arr[i] + j 计算出指向当前数组第 j 个元素的指针,而 *(arr[i] + j) 解引用该指针,获取该元素的值。

  2. 使用数组下标访问:

    c 复制代码
    printf("%d", arr[i][j]);

    这种方式更直观。arr[i] 获取指向某个数组的指针,而 arr[i][j] 直接访问该数组的第 j 个元素。这是因为 arr[i] 被视为指向第 i 个数组的首元素的指针,而 arr[i][j] 就是访问该指针所指向数组的第 j 个元素。

第二种写法的原因如下:在C语言中,arr[i][j] 这种双重下标访问方式背后的原理与指针算术紧密相关。当你有一个指向指针的指针(或者数组的数组,或者指针数组,如本例所示),这种双重下标访问实际上是两步操作的简写:

  1. arr[i] 访问第 i 个元素,这里的元素是指向 int 的指针(在你的例子中,它指向 arr1arr2arr3)。
  2. [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] 操作符被应用到这个地址上,它实际上是通过指针算术来完成的:从这个地址开始,移动 jint 的大小的距离,来访问特定的元素。这是因为在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) 时,这里的操作实际上是在进行两个步骤:

  1. 指针算术运算a+i 计算的是第 i 行的首地址。由于 a 是指向第一行的指针,a+i 实际上是利用指针算术来计算第 i 行的起始地址。这里的 i 乘以的是每行的大小(在本例中是 4 个 char),这是自动完成的,不需要显式乘以 sizeof(char) 或行的大小。这一步并没有解引用,只是计算地址。
  2. 解引用*(a+i) 的操作是对计算得到的地址进行解引用。但是,这里的"解引用"可能会造成一些理解上的混淆。在这个上下文中,我们不是在获取一个 char 值,而是获取指向第 i 行首元素的指针。这是因为 a+i 已经是一个指向 char[4] 的指针,解引用这个指针实际上给出的是 char[4] 类型的数组的第一个元素的地址,这与直接使用 a[i] 是等价的。
  3. 因此,当我们说"解引用得到第 i 行的首地址"时,我们实际上是在描述获取到这一行数组的起始点的过程。在 C 语言中,数组名(在这个例子中是 a[i])被用作表达式时,会被转换(除了 sizeof& 操作符的情况)为指向其第一个元素的指针。所以,*(a+i)a[i] 在这里是等价的,都表示第 i 行的首地址。
相关推荐
xiaoshiguang33 小时前
LeetCode:222.完全二叉树节点的数量
算法·leetcode
爱吃西瓜的小菜鸡3 小时前
【C语言】判断回文
c语言·学习·算法
别NULL3 小时前
机试题——疯长的草
数据结构·c++·算法
TT哇3 小时前
*【每日一题 提高题】[蓝桥杯 2022 国 A] 选素数
java·算法·蓝桥杯
ZSYP-S5 小时前
Day 15:Spring 框架基础
java·开发语言·数据结构·后端·spring
yuanbenshidiaos5 小时前
C++----------函数的调用机制
java·c++·算法
唐叔在学习5 小时前
【唐叔学算法】第21天:超越比较-计数排序、桶排序与基数排序的Java实践及性能剖析
数据结构·算法·排序算法
ALISHENGYA5 小时前
全国青少年信息学奥林匹克竞赛(信奥赛)备考实战之分支结构(switch语句)
数据结构·算法
chengooooooo5 小时前
代码随想录训练营第二十七天| 贪心理论基础 455.分发饼干 376. 摆动序列 53. 最大子序和
算法·leetcode·职场和发展
jackiendsc5 小时前
Java的垃圾回收机制介绍、工作原理、算法及分析调优
java·开发语言·算法