指针澄清的指针


142

我在按照本教程讲解关于指针的工作方式。

让我引用相关段落:


    int i = 5, j = 6, k = 7;
    int *ip1 = &i, *ip2 = &j;

现在我们可以设置

    int **ipp = &ip1;

ipp指向ip1指向i*ippip1,和**ippi或。5。我们可以用熟悉的方格和箭头符号来说明这种情况,如下所示:

在此处输入图片说明

如果那我们说

    *ipp = ip2;

我们已将ippip1)指向的指针更改为包含的副本ip2,因此它(ip1)现在指向j

在此处输入图片说明


我的问题是:为什么在第二张图片中ipp仍指向ip1但不指向ip2


6
ipp在定义时添加类型,这样您的问题就完成了;-)
zmo 2014年

1
由于ipp指向ip1这样*ipp = ip2的一样ip1 = ip2
Grijesh肖汉

1
我们可以停止写远离尖形的星号吗?int **ipp远没有直观int** ipp,尤其是在**ipp类型声明之外意味着完全不同的事物时。
Darkhogg 2014年

4
@Darkhogg int **ipp对我来说似乎很直观。这意味着,我正在制作**ipp一个int。没错
ithisa 2014年

2
@ user54609含义,除了个人的直观性,可以这样考虑:该类型ippint**,所以只写int**,而不是神奇的imp解引用是一个int”的理解
Manu343726

Answers:


143

忘记有关指点类比的一秒钟。指针真正包含的是一个内存地址。的&是操作者“的地址” -即它在一个对象的存储器返回地址。该*运营商给你的对象的指针指,即给予包含地址的指针,在这一点内存地址返回的对象。因此,当您执行此操作时*ipp = ip2,您正在执行的操作是*ipp将对象保存在ipp其中的地址is中 ip1,然后将其分配给ip1其中存储的值,该值ip2是的地址j

只需
& ->地址
*->值


14
&和*从未如此轻松
2014年

7
我相信造成混淆的主要原因是*运算符的含糊不清,它在变量声明期间用于指示变量实际上是指向某种数据类型的指针。但是,另一方面,它也用于语句中以访问指针(解引用运算符)所指向的变量的内容。
卢卡斯A.

43

因为您更改了所指向的值,而ipp不是的值ipp。因此,ipp仍然指向ip1(的值ipp),ip1现在的值与ip2的值相同,因此它们都指向j

这个:

*ipp = ip2;

是相同的:

ip1 = ip2;

11
可能需要指出int *ip1 = &i和之间的区别*ipp = ip2;,即,如果int从第一条语句中删除,则分配看起来非常相似,但是*在两种情况下,它们所做的事情却截然不同。
克劳曼,2014年

22

像C标记中的大多数初学者问题一样,可以通过回到第一原理来回答此问题:

  • 指针是一种值。
  • 变量包含一个值。
  • &操作者打开一可变成指针。
  • *操作者接通一个指针到一个变量。

(从技术上讲,我应该说“左值”而不是“变量”,但我觉得将可变存储位置描述为“变量”更为明确。)

所以我们有变量:

int i = 5, j = 6;
int *ip1 = &i, *ip2 = &j;

变量ip1 包含一个指针。在&操作者接通i到一个指针和该指针值被分配给ip1。因此ip1 包含指向的指针i

变量ip2 包含一个指针。在&操作者接通j到一个指针和该指针被分配给ip2。因此ip2 包含指向的指针j

int **ipp = &ip1;

变量ipp包含一个指针。在&操作者接通变量ip1成一个指针和该指针值被分配给ipp。因此ipp包含指向的指针ip1

让我们总结到目前为止的故事:

  • i 包含5
  • j 包含6
  • ip1包含“指针i
  • ip2包含“指针j
  • ipp包含“指针ip1

现在我们说

*ipp = ip2;

*操作者打开一指针返回到一个变量。我们获取的值ipp,即“指向ip1它并将其变成变量。什么变量?ip1”当然!

因此,这只是另一种说法

ip1 = ip2;

因此,我们获取的值ip2。它是什么?“指向”的指针j。我们分配一个指针值ip1,所以ip1现在是“指针j

我们只更改了一件事:的值ip1

  • i 包含5
  • j 包含6
  • ip1包含“指针j
  • ip2包含“指针j
  • ipp包含“指针ip1

为什么ipp仍然指向ip1而不是ip2

分配给变量时,该变量会更改。计算作业;变量的更改不能超过赋值!您可以通过分配开始ijip1ip2ipp。然后*ipp,您将分配给,正如我们所见,它的含义与“分配给ip1” 相同。由于您没有ipp第二次分配,因此它没有改变!

如果要更改,ipp则必须实际分配给ipp

ipp = &ip2;

例如。


21

希望这段代码可以对您有所帮助。

#include <iostream>
#include <stdio.h>
using namespace std;

int main()
{
    int i = 5, j = 6, k = 7;
    int *ip1 = &i, *ip2 = &j;
    int** ipp = &ip1;
    printf("address of value i: %p\n", &i);
    printf("address of value j: %p\n", &j);
    printf("value ip1: %p\n", ip1);
    printf("value ip2: %p\n", ip2);
    printf("value ipp: %p\n", ipp);
    printf("address value of ipp: %p\n", *ipp);
    printf("value of address value of ipp: %d\n", **ipp);
    *ipp = ip2;
    printf("value ipp: %p\n", ipp);
    printf("address value of ipp: %p\n", *ipp);
    printf("value of address value of ipp: %d\n", **ipp);
}

它输出:

在此处输入图片说明


12

我个人的看法是,带有箭头的图片会指向这种方式,或者使指针更难以理解。它确实使它们看起来像是一些抽象的神秘实体。他们不是。

像计算机上的其他所有内容一样,指针就是数字。名称“指针”只是说“包含地址的变量”的一种奇特的方式。

因此,让我通过解释计算机的实际工作方式来搅动一切。

我们有一个int,它的名称i和值为5。它存储在内存中。像存储在内存中的所有内容一样,它需要一个地址,否则我们将无法找到它。可以说i结束于地址0x12345678,其伙伴j值为6 紧随其后。假设int是4个字节,指针是4个字节的32位CPU,则变量将如下存储在物理内存中:

Address     Data           Meaning
0x12345678  00 00 00 05    // The variable i
0x1234567C  00 00 00 06    // The variable j

现在我们要指向这些变量。我们创建一个指向int的指针int* ip1,然后创建一个int* ip2。像计算机中的所有内容一样,这些指针变量也分配在内存中的某个位置。假设它们结束于紧接在内存中的下一个相邻地址j。我们将指针设置为包含先前分配的变量ip1=&i;的地址:(“将i的地址复制到ip1”)和ip2=&j。两行之间发生的是:

Address     Data           Meaning
0x12345680  12 34 56 78    // The variable ip1(equal to address of i)
0x12345684  12 34 56 7C    // The variable ip2(equal to address of j)

因此,我们得到的只是包含数字的4个字节的内存块。视线中没有神秘或神奇的箭头。

实际上,仅通过查看内存转储就无法判断地址0x12345680是否包含intint*。不同之处在于我们的程序选择使用此地址存储的内容的方式。(我们程序的任务实际上只是告诉CPU如何处理这些数字。)

然后,我们使用添加另一个间接级别int** ipp = &ip1;。同样,我们只是获得了一块内存:

Address     Data           Meaning
0x12345688  12 34 56 80    // The variable ipp

该模式看起来确实很熟悉。4个字节的另一个块包含一个数字。

现在,如果我们有上述虚拟RAM的内存转储,则可以手动检查这些指针指向的位置。我们窥视ipp变量地址中存储的内容,并找到内容0x12345680。当然,这ip1是存储地址。我们可以转到该地址,检查其中的内容,然后找到的地址i,最后,我们可以转到该地址并找到数字5。

因此,如果我们获取ipp的内容*ipp,我们将获得指针变量的地址ip1。通过编写,*ipp=ip2我们将ip2复制到ip1,它等效于ip1=ip2。无论哪种情况,我们都会得到

Address     Data           Meaning
0x12345680  12 34 56 7C    // The variable ip1
0x12345684  12 34 56 7C    // The variable ip2

(这些示例是针对大端CPU给出的)


5
尽管我同意您的观点,但将指针视为抽象的神秘实体还是有价值的。指针的任何特定实现都只是数字,但是您草绘的实现策略不是实现的要求,它只是一种通用策略。指针的大小不必与int相同,指针不必是平面虚拟内存模型中的地址,依此类推;这些仅仅是实现细节。
埃里克·利珀特

@EricLippert我认为可以通过不使用实际的内存地址或数据块来使此示例更加抽象。如果它是一张表,说明诸如location, value, variable位置1,2,3,4,5和值位于何处A,1,B,C,3,则无需使用箭头,就可以轻松地解释指针的相应概念,而箭头本身就是令人困惑的。无论选择哪种实现,值都会存在于某个位置,这是在使用箭头建模时变得迷惑的难题。
MirroredFate 2014年

@EricLippert以我的经验,大多数可能在理解指针方面遇到问题的C程序员都是那些接受抽象的人工模型的人。抽象是没有帮助的,因为当今C语言的全部目的是接近于硬件。如果您正在学习C语言,但又不想在硬件附近编写代码,那是在浪费时间。如果您不想了解计算机的工作原理,而只是进行高级编程,则Java等是更好的选择。
伦丁2014年

@EricLippert是的,可能存在各种晦涩的指针实现,其中指针不一定与地址相对应。但是绘制箭头也不会帮助您理解它们的工作原理。在某些时候,您必须离开抽象的思维,进入硬件级别,否则就不应该使用C。有许多更合适的现代语言用于纯粹的抽象高级编程。
伦丁2014年

@Lundin:我也不是箭头图的忠实拥护者。箭头作为数据的概念是一个棘手的问题。我更喜欢抽象地思考它,但是没有箭头。&变量上的运算符会为您提供代表该变量的硬币。该*硬币上的运算符会退还给您变量。无需箭头!
埃里克·利珀特

8

注意分配:

ipp = &ip1;

结果ipp指向ip1

因此,ipp指向ip2,我们应该以类似的方式进行更改,

ipp = &ip2;

我们显然没有这样做。相反,我们正在更改由指向的地址处ipp
通过以下

*ipp = ip2;

我们只是替换存储在中的值ip1

ipp = &ip1,手段*ipp = ip1 = &i
现在,*ipp = ip2 = &j
因此,*ipp = ip2与基本上相同ip1 = ip2



5

我的问题是:为什么在第二张图片中,ipp仍然指向ip1而不是ip2?

您放置了精美的图片,我将尝试制作精美的ascii艺术:

就像@ Robert-S-Barnes在回答中说的那样:忘记了指针,以及指向什么的东西,但是要考虑内存。基本上,这int*意味着它包含一个变量的地址,并且int**包含一个包含变量的地址的变量的地址。然后,您可以使用指针的代数访问值或地址:&fooMeans address of foo*fooMeans value of the address contained in foo

因此,由于指针是关于内存的,因此使该“有形”成为现实的最好方法是显示指针代数对内存的作用。

因此,这是程序的内存(出于示例目的而进行了简化):

name:    i   j ip1 ip2 ipp
addr:    0   1   2   3   4
mem : [   |   |   |   |   ]

当您执行初始代码时:

int i = 5, j = 6;
int *ip1 = &i, *ip2 = &j;

这是您的记忆样子:

name:    i   j ip1 ip2
addr:    0   1   2   3
mem : [  5|  6|  0|  1]

那里,你可以看到ip1ip2得到的地址ijipp仍然不存在。不要忘记,地址只是使用特殊类型存储的整数。

然后声明并定义ipp如下内容:

int **ipp = &ip1;

所以这是你的记忆:

name:    i   j ip1 ip2 ipp
addr:    0   1   2   3   4
mem : [  5|  6|  0|  1|  2]

然后,您要更改存储在中的地址所指向的值,该地址是存储在ipp中的地址ip1

*ipp = ip2;

该程序的内存是

name:    i   j ip1 ip2 ipp
addr:    0   1   2   3   4
mem : [  5|  6|  1|  1|  2]

注意:作为int*一种特殊类型,我宁愿始终避免在同一行上声明多个指针,因为我认为int *x;int *x, *y;符号可能会引起误解。我更喜欢写int* x; int* y;

高温超导


你的榜样,初始值ip2应该是3没有4
2014年

1
哦,我只是更改了内存,使其与声明的顺序匹配。我想我是这样做的吗?
zmo 2014年

5

因为当你说

*ipp = ip2

您说的是“所指向的对象ipp”,它指向所指向的内存方向ip2

你不是说ipp要点ip2


4

如果将取消引用运算符添加*到指针,则会从指针重定向到指向的对象。

例子:

int i = 0;
int *p = &i; // <-- N.B. the pointer declaration also uses the `*`
             //     it's not the dereference operator in this context
*p;          // <-- this expression uses the pointed-to object, that is `i`
p;           // <-- this expression uses the pointer object itself, that is `p`

因此:

*ipp = ip2; // <-- you change the pointer `ipp` points to, not `ipp` itself
            //     therefore, `ipp` still points to `ip1` afterwards.



3

考虑这样表示的每个变量:

type  : (name, adress, value)

所以你的变量应该这样表示

int   : ( i ,  &i , 5 ); ( j ,  &j ,  6); ( k ,  &k , 5 )

int*  : (ip1, &ip1, &i); (ip1, &ip1, &j)

int** : (ipp, &ipp, &ip1)

由于的值ipp&ip1如此,所以:

*ipp = ip2;

将地址的值更改&ip1为的值ip2,这意味着ip1已更改:

(ip1, &ip1, &i) -> (ip1, &ip1, &j)

ipp仍然:

(ipp, &ipp, &ip1)

因此,ippstill 的值&ip1表示仍指向ip1


1

因为您正在更改的指针*ipp。它的意思是

  1. ipp (可变名称)----进入。
  2. 里面ipp是地址ip1
  3. 现在*ipp去(内部地址)ip1

现在我们在ip1*ipp(即ip1)= ip2。
ip2包含j.so的地址,所以ip1内容将被ip2的包含(即j的地址)代替,我们没有更改ipp内容。而已。


1

*ipp = ip2; 暗示:

分配ip2给指向的变量ipp。因此,这等效于:

ip1 = ip2;

如果您希望将的地址ip2存储在中ipp,只需执行以下操作:

ipp = &ip2;

现在ipp指向ip2


0

ipp可以保存指向指针类型对象的指针的值(即指向)。当你做

ipp = &ip2;  

然后ipp包含变量(指针)ip2地址,地址是指针&ip2类型为的指针()。现在ipp第二张图片中的箭头将指向ip2

维基说:
*操作者是一个引用操作上指针变量进行操作,并返回一个L值(变量)等效于在指针地址的值。这称为取消引用指针。

*运算符应用于ipp将其解引用int类型的指针的l值上。取消引用的l值*ipp指向int的类型指针,它可以保存int类型数据的地址。声明后

ipp = &ip1;

ipp持有的地址,ip1*ipp持有的地址(指向)i。您可以说*ipp是的别名ip1。这两个**ipp*ip1是别名i
通过做

 *ipp = ip2;  

*ippip2都指向相同的位置,但ipp仍然指向ip1

什么*ipp = ip2;实际上做的是,它复制的内容ip2(的地址j来)ip1(作为*ipp是一个别名ip1),实际上使两个指针ip1ip2指向同一个对象(j)。
因此,在第二个数字,箭头的ip1ip2指向j,同时ipp仍然指向ip1,因为没有修改完成改变的值ipp

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.