为什么指针对于许多 C 或 C++ 大学新学生甚至老学生来说是一个令人困惑的主要因素?是否有任何工具或思维过程可以帮助您了解指针如何在变量、函数以及其他级别上工作?

有哪些好的实践可以让人们达到“啊哈,我明白了”的水平,而不会让他们陷入整体概念的泥潭?基本上,演练类似的场景。

有帮助吗?

解决方案

对于许多人来说,指针这个概念一开始可能会令人困惑,特别是在复制指针值并仍然引用同一内存块时。

我发现最好的类比是将指针视为一张带有房屋地址的纸,并将其引用的内存块视为实际的房屋。因此,各种操作都可以很容易地解释。

我在下面添加了一些 Delphi 代码,并在适当的地方添加了一些注释。我选择 Delphi 是因为我的另一种主要编程语言 C# 不会以同样的方式表现出内存泄漏等问题。

如果您只想学习指针的高级概念,那么您应该忽略下面解释中标记为“内存布局”的部分。它们旨在给出操作后内存的样子的示例,但它们本质上更底层。然而,为了准确地解释缓冲区溢出的实际工作原理,我添加这些图表非常重要。

免责声明:出于所有意图和目的,此解释和示例内存布局已大大简化。如果您需要低水平处理内存,您需要知道更多的开销和更多细节。但是,为了解释记忆和指针的目的,它足够准确。


我们假设下面使用的 THouse 类如下所示:

type
    THouse = class
    private
        FName : array[0..9] of Char;
    public
        constructor Create(name: PChar);
    end;

当您初始化 house 对象时,为构造函数指定的名称将被复制到私有字段 FName 中。将其定义为固定大小的数组是有原因的。

在内存中,会有一些与房屋分配相关的开销,我将在下面说明这一点,如下所示:

---[ttttNNNNNNNNNN]---
     ^   ^
     |   |
     |   +- the FName array
     |
     +- overhead

“tttt”区域是开销,对于各种类型的运行时和语言,通常会有更多的区域,例如 8 或 12 字节。重要的是,存储在该区域中的任何值都不会被内存分配器或核心系统例程以外的任何内容更改,否则您将面临程序崩溃的风险。


分配内存

找一位企业家来建造你的房子,并给你房子的地址。与现实世界相反,内存分配不能被告知在哪里分配,而是会找到一个有足够空间的合适位置,并将地址报告给分配的内存。

换句话说,企业家将选择地点。

THouse.Create('My house');

内存布局:

---[ttttNNNNNNNNNN]---
    1234My house

保留一个带有地址的变量

将您的新房地址写在一张纸上。这篇论文将作为您对您的房子的参考。没有这张纸,您就会迷路,无法找到房子,除非您已经在里面了。

var
    h: THouse;
begin
    h := THouse.Create('My house');
    ...

内存布局:

    h
    v
---[ttttNNNNNNNNNN]---
    1234My house

复制指针值

只需将地址写在一张新纸上即可。现在你有两张纸可以让你到达同一所房子,而不是两栋独立的房子。任何试图按照一张纸上的地址并重新布置那所房子的家具的尝试都会让人觉得 另一栋房子 已以相同的方式进行修改,除非您可以明确检测到它实际上只是一栋房子。

笔记 这通常是我向人们解释时最困难的概念,两个指针并不意味着两个对象或内存块。

var
    h1, h2: THouse;
begin
    h1 := THouse.Create('My house');
    h2 := h1; // copies the address, not the house
    ...
    h1
    v
---[ttttNNNNNNNNNN]---
    1234My house
    ^
    h2

释放内存

拆掉房子。如果您愿意,您可以稍后将这张纸重新用于新地址,或者清除它以忘记不再存在的房子的地址。

var
    h: THouse;
begin
    h := THouse.Create('My house');
    ...
    h.Free;
    h := nil;

在这里我先把房子建好,并拿到地址。然后我对房子做了一些事情(使用它,...代码,留给读者作为练习),然后我释放它。最后我清除变量中的地址。

内存布局:

    h                        <--+
    v                           +- before free
---[ttttNNNNNNNNNN]---          |
    1234My house             <--+

    h (now points nowhere)   <--+
                                +- after free
----------------------          | (note, memory might still
    xx34My house             <--+  contain some data)

悬空指针

你告诉你的企业家摧毁房子,但你忘记从你的纸上删除地址。当你稍后查看这张纸时,你忘记了房子已经不在那儿了,并去参观它,但结果失败了(另请参阅下面关于无效参考的部分)。

var
    h: THouse;
begin
    h := THouse.Create('My house');
    ...
    h.Free;
    ... // forgot to clear h here
    h.OpenFrontDoor; // will most likely fail

使用 h 致电后 .Free 可能 工作,但这纯粹是运气。它很可能会在客户的关键操作过程中失败。

    h                        <--+
    v                           +- before free
---[ttttNNNNNNNNNN]---          |
    1234My house             <--+

    h                        <--+
    v                           +- after free
----------------------          |
    xx34My house             <--+

如您所见,H仍然指向内存中数据的残余物,但是由于可能未完成,因此使用它可能会失败。


内存泄漏

你丢了那张纸,也找不到房子了。但房子仍然矗立在某个地方,当你以后想要建造一座新房子时,你不能重复使用那个地方。

var
    h: THouse;
begin
    h := THouse.Create('My house');
    h := THouse.Create('My house'); // uh-oh, what happened to our first house?
    ...
    h.Free;
    h := nil;

这里我们重写了内容 h 变量是新房子的地址,但旧房子仍然矗立着......某处。执行此代码后,将无法到达该房屋,并且它将保持原样。换句话说,分配的内存将保持分配状态,直到应用程序关闭,此时操作系统将销毁它。

第一次分配后的内存布局:

    h
    v
---[ttttNNNNNNNNNN]---
    1234My house

第二次分配后的内存布局:

                       h
                       v
---[ttttNNNNNNNNNN]---[ttttNNNNNNNNNN]
    1234My house       5678My house

获取此方法的更常见方法是忘记释放某些内容,而不是像上面那样覆盖它。用 Delphi 术语来说,这将通过以下方法发生:

procedure OpenTheFrontDoorOfANewHouse;
var
    h: THouse;
begin
    h := THouse.Create('My house');
    h.OpenFrontDoor;
    // uh-oh, no .Free here, where does the address go?
end;

执行此方法后,我们的变量中不再存在房子的地址,但房子仍然在那里。

内存布局:

    h                        <--+
    v                           +- before losing pointer
---[ttttNNNNNNNNNN]---          |
    1234My house             <--+

    h (now points nowhere)   <--+
                                +- after losing pointer
---[ttttNNNNNNNNNN]---          |
    1234My house             <--+

如您所见,旧数据在内存中保持完整,并且不会被内存分配器重复使用。分配器跟踪使用了哪些内存区域,除非您释放,否则不会重复使用它们。


释放内存但保留(现在无效)引用

拆掉房子,擦掉其中一张纸,但你还有另一张纸,上面写着旧地址,当你去地址时,你不会找到房子,但你可能会发现类似废墟的东西之一。

也许您甚至会找到一栋房子,但这不是您最初获得地址的房子,因此任何试图将它当作属于您使用的尝试都可能会失败。

有时你甚至可能会发现邻近地​​址上有一栋相当大的房子,占据了三个地址(主街1-3),而你的地址就在房子的中间。任何将三地址大房子的那部分视为单个小房子的尝试也可能会失败。

var
    h1, h2: THouse;
begin
    h1 := THouse.Create('My house');
    h2 := h1; // copies the address, not the house
    ...
    h1.Free;
    h1 := nil;
    h2.OpenFrontDoor; // uh-oh, what happened to our house?

这里的房子被拆掉了,参考 h1, ,同时 h1 也被清除了, h2 仍然有旧的、过时的地址。进入不再矗立的房子可能有效,也可能无效。

这是上面悬挂指针的变体。查看其内存布局。


缓冲区溢出

你搬进房子里的东西超出了你所能容纳的范围,从而溢出到邻居的房子或院子里。当邻居房子的主人稍后回家时,他会发现各种他认为是自己的东西。

这就是我选择固定大小数组的原因。要设置舞台,假设我们分配的第二宫将出于某种原因将其放在记忆中的第一个房屋之前。换句话说,第二宫的地址将比第一个地址低。而且,它们被分配得紧挨着。

因此,这段代码:

var
    h1, h2: THouse;
begin
    h1 := THouse.Create('My house');
    h2 := THouse.Create('My other house somewhere');
                         ^-----------------------^
                          longer than 10 characters
                         0123456789 <-- 10 characters

第一次分配后的内存布局:

                        h1
                        v
-----------------------[ttttNNNNNNNNNN]
                        5678My house

第二次分配后的内存布局:

    h2                  h1
    v                   v
---[ttttNNNNNNNNNN]----[ttttNNNNNNNNNN]
    1234My other house somewhereouse
                        ^---+--^
                            |
                            +- overwritten

通常会导致崩溃的部分是,当您覆盖存储的数据的重要部分时,实际上不应该随机更改。例如,就崩溃程序而更改了H1-House名称的一部分可能不是一个问题,但是当您尝试使用损坏的对象时,覆盖对象的开销很可能会崩溃,覆盖链接存储在对象中的其他对象上。


链接列表

当你沿着一张纸上的地址前进时,你会到达一所房子,在那所房子里还有另一张纸,上面有新地址,是链中下一个房子的地址,依此类推。

var
    h1, h2: THouse;
begin
    h1 := THouse.Create('Home');
    h2 := THouse.Create('Cabin');
    h1.NextHouse := h2;

在这里,我们创建了从我们的家到我们的小屋的链接。我们可以沿着链条追踪,直到房子没有为止 NextHouse 参考,这意味着它是最后一个。要访问我们所有的房子,我们可以使用以下代码:

var
    h1, h2: THouse;
    h: THouse;
begin
    h1 := THouse.Create('Home');
    h2 := THouse.Create('Cabin');
    h1.NextHouse := h2;
    ...
    h := h1;
    while h <> nil do
    begin
        h.LockAllDoors;
        h.CloseAllWindows;
        h := h.NextHouse;
    end;

内存布局(添加了Nexthouse作为对象中的链接,在下图中指出了四个LLLL):

    h1                      h2
    v                       v
---[ttttNNNNNNNNNNLLLL]----[ttttNNNNNNNNNNLLLL]
    1234Home       +        5678Cabin      +
                   |        ^              |
                   +--------+              * (no link)

从根本上来说,什么是内存地址?

从根本上来说,内存地址只是一个数字。如果您将内存视为一系列字节,那么第一个字节具有地址0,下一个字节是地址1等。这是简化的,但已经足够好了。

所以这个内存布局:

    h1                 h2
    v                  v
---[ttttNNNNNNNNNN]---[ttttNNNNNNNNNN]
    1234My house       5678My house

可能有这两个地址(最左边的 - 是地址 0):

  • h1 = 4
  • h2 = 23

这意味着我们上面的链接列表实际上可能如下所示:

    h1 (=4)                 h2 (=28)
    v                       v
---[ttttNNNNNNNNNNLLLL]----[ttttNNNNNNNNNNLLLL]
    1234Home      0028      5678Cabin     0000
                   |        ^              |
                   +--------+              * (no link)

通常将“无处指向”的地址存储为零地址。


从根本上来说,什么是指针?

指针只是一个保存内存地址的变量。您通常可以要求编程语言为您提供数字,但是大多数编程语言和运行时间都试图隐藏下面有一个数字的事实,仅仅是因为数字本身并没有真正对您持有任何意义。最好将指针视为黑匣子,即。只要它起作用,您就不会真正知道或关心它实际实施。

其他提示

在我的第一堂计算机科学课上,我们做了以下练习。诚然,这是一个大约有200名学生的报告厅……

教授在黑板上写道: int john;

约翰站起来

教授写道: int *sally = &john;

莎莉站起来,指着约翰

教授: int *bill = sally;

比尔站起来,指着约翰

教授: int sam;

萨姆站起来

教授: bill = &sam;

比尔现在指着山姆。

我想你应该已经明白了。我想我们花了大约一个小时来做​​这件事,直到我们了解了指针分配的基础知识。

我发现超链接有助于解释指针。大多数人都可以理解网页上的链接“指向”互联网上的另一个页面,如果您可以复制并粘贴该超链接,那么它们都将指向同一个原始网页。如果您去编辑原始页面,然后按照这些链接(指针)中的任何一个,您将获得新的更新页面。

指针似乎让很多人感到困惑的原因是他们大多没有或很少有计算机体系结构背景。由于许多人似乎不知道计算机(机器)是如何实际实现的 - 使用 C/C++ 工作似乎很陌生。

练习是要求他们实现一个简单的基于字节码的虚拟机(在他们选择的任何语言中,python 都非常适合),并具有专注于指针操作(加载、存储、直接/间接寻址)的指令集。然后要求他们为该指令集编写简单的程序。

任何需要比简单加法稍微多一点的东西都将涉及指针,并且他们肯定会得到它。

为什么指针对于许多学习 C/C++ 语言的大学新学生甚至老学生来说是一个令人困惑的主要因素?

值占位符的概念 - 变量 - 映射到我们在学校教的东西 - 代数。如果不了解内存在计算机中的物理布局,就无法进行现有的并行处理,并且在处理低级事物(C/C++/字节通信级别)之前,没有人会考虑这种事情。

是否有任何工具或思维过程可以帮助您了解指针如何在变量、函数以及其他级别上工作?

地址框。我记得当我学习将 BASIC 编程到微型计算机中时,有一些漂亮的书籍,里面有游戏,有时你必须将值插入特定的地址。他们有一张一堆盒子的图片,上面依次标有 0、1、2...据解释,这些盒子只能容纳一个小东西(一个字节),而且盒子的数量很多——有些计算机有多达 65535 个!他们挨着,都有地址。

有哪些好的实践可以让人们达到“啊哈,我明白了”的水平,而不会让他们陷入整体概念的泥潭?基本上,演练类似的场景。

为了钻头?制作一个结构体:

struct {
char a;
char b;
char c;
char d;
} mystruct;
mystruct.a = 'r';
mystruct.b = 's';
mystruct.c = 't';
mystruct.d = 'u';

char* my_pointer;
my_pointer = &mystruct.b;
cout << 'Start: my_pointer = ' << *my_pointer << endl;
my_pointer++;
cout << 'After: my_pointer = ' << *my_pointer << endl;
my_pointer = &mystruct.a;
cout << 'Then: my_pointer = ' << *my_pointer << endl;
my_pointer = my_pointer + 3;
cout << 'End: my_pointer = ' << *my_pointer << endl;

与上面的示例相同,但在 C 中除外:

// Same example as above, except in C:
struct {
    char a;
    char b;
    char c;
    char d;
} mystruct;

mystruct.a = 'r';
mystruct.b = 's';
mystruct.c = 't';
mystruct.d = 'u';

char* my_pointer;
my_pointer = &mystruct.b;

printf("Start: my_pointer = %c\n", *my_pointer);
my_pointer++;
printf("After: my_pointer = %c\n", *my_pointer);
my_pointer = &mystruct.a;
printf("Then: my_pointer = %c\n", *my_pointer);
my_pointer = my_pointer + 3;
printf("End: my_pointer = %c\n", *my_pointer);

输出:

Start: my_pointer = s
After: my_pointer = t
Then: my_pointer = r
End: my_pointer = u

也许这可以通过示例解释一些基础知识?

一开始我很难理解指针的原因是,许多解释都包含了很多关于通过引用传递的废话。所有这些都会使问题变得混乱。当您使用指针参数时,您 仍然 按值传递;但该值恰好是一个地址,而不是一个 int。

其他人已经链接到本教程,但我可以强调我开始理解指针的那一刻:

C 语言指针和数组教程:第 3 章 - 指针和字符串

int puts(const char *s);

暂时忽略 const. 参数传递给 puts() 是一个指针, 这是指针的值(因为 C 中的所有参数都是按值传递的),而指针的值是它指向的地址,或者简单地说,是一个地址。 因此当我们写 puts(strA); 正如我们所看到的,我们传递的是 strA[0] 的地址。

当我读到这些文字的那一刻,乌云散去,一束阳光笼罩着我,带着指针般的理解。

即使您是 VB .NET 或 C# 开发人员(就像我一样)并且从不使用不安全代码,仍然值得了解指针的工作原理,否则您将无法理解对象引用的工作原理。然后您就会有一个常见但错误的概念:将对象引用传递给方法会复制该对象。

我发现 Ted Jensen 的“C 语言指针和数组教程”是学习指针的绝佳资源。它分为 10 节课,首先解释什么是指针(以及它们的用途),最后讲解函数指针。 http://home.netcom.com/~tjensen/ptr/cpoint.htm

接下来,Beej 的网络编程指南教授 Unix 套接字 API,从中您可以开始做真正有趣的事情。 http://beej.us/guide/bgnet/

指针的复杂性超出了我们可以轻松教授的范围。让学生互相指出并使用带有家庭地址的纸片都是很好的学习工具。他们在介绍基本概念方面做得很好。事实上,学习基本概念是 必不可少的 成功使用指针。然而,在生产代码中,通常会遇到比这些简单演示所能封装的更复杂的场景。

我参与过一些系统,其中的结构指向其他结构,而其他结构又指向其他结构。其中一些结构还包含嵌入结构(而不​​是指向其他结构的指针)。这就是指针真正令人困惑的地方。如果您有多个间接级别,并且最终会得到如下代码:

widget->wazzle.fizzle = fazzle.foozle->wazzle;

它很快就会变得混乱(想象一下更多的线条,可能还有更多的关卡)。再加上指针数组和节点到节点指针(树、链表),情况会变得更糟。我见过一些非常优秀的开发人员在开始开发此类系统后就迷失了方向,即使是非常了解基础知识的开发人员也是如此。

指针的复杂结构也不一定表明编码很差(尽管可以)。组合是良好的面向对象编程的重要组成部分,在具有原始指针的语言中,它将不可避免地导致多层间接。此外,系统通常需要使用结构在风格或技术上不匹配的第三方库。在这种情况下,复杂性自然会出现(当然,我们应该尽可能地与之抗争)。

我认为大学帮助学生学习指针的最好方法就是使用良好的演示,并结合需要使用指针的项目。一个困难的项目比一千个演示更有助于理解指针。演示可以让你粗浅地理解,但要深入掌握指针,你必须真正使用它们。

我想我应该在这个列表中添加一个类比,当我作为一名计算机科学导师解释指针时,我发现它非常有帮助;首先,让我们:


设置舞台:

考虑一个有 3 个停车位的停车场,这些停车位已编号:

-------------------
|     |     |     |
|  1  |  2  |  3  |
|     |     |     |

在某种程度上,这就像内存位置,它们是连续且连续的。有点像数组。现在其中没有汽车,所以它就像一个空数组(parking_lot[3] = {0}).


添加数据

停车场永远不会长时间空着……如果这样做的话,那就毫无意义,没有人会建造任何东西。假设随着时间的推移,停车场停满了 3 辆汽车,一辆蓝色汽车、一辆红色汽车和一辆绿色汽车:

   1     2     3
-------------------
| o=o | o=o | o=o |
| |B| | |R| | |G| |
| o-o | o-o | o-o |

这些汽车都是同一类型(汽车),因此一种思考方式是,我们的汽车是某种数据(比如 int)但它们有不同的值(blue, red, green;那可能是一种颜色 enum)


输入指针

现在,如果我带你进入这个停车场,并要求你给我找一辆蓝色的汽车,你伸出一根手指,用它指向地点 1 的一辆蓝色汽车。这就像获取一个指针并将其分配给一个内存地址(int *finger = parking_lot)

你的手指(指针)不能回答我的问题。看着 你的手指什么也没告诉我,但如果我看看你的手指在哪里 指向 (取消引用指针),我可以找到我正在寻找的汽车(数据)。


重新分配指针

现在我可以要求你找一辆红色汽车,你可以将手指转向一辆新车。现在,您的指针(与之前相同的指针)正在向我显示相同类型(汽车)的新数据(可以找到红色汽车的停车位)。

指针物理上没有改变,它仍然是 你的 手指,只是它显示给我的数据发生了变化。(“停车位”地址)


双指针(或指向指针的指针)

这也适用于多个指针。我可以问指针在哪里,指针指向红色汽车,你可以用另一只手用一根手指指向食指。(这就像 int **finger_two = &finger)

现在,如果我想知道蓝色汽车在哪里,我可以沿着第一根手指的方向到第二根手指,到汽车(数据)。


悬空指针

现在,假设您感觉自己非常像一尊雕像,并且想要无限期地用手指向红色汽车。如果那辆红色汽车开走了怎么办?

   1     2     3
-------------------
| o=o |     | o=o |
| |B| |     | |G| |
| o-o |     | o-o |

你的指针仍然指向红色汽车的位置 曾是 但不再是了。假设一辆新车停在那里......一辆橙色的汽车。现在,如果我再问你,“红色汽车在哪里”,你仍然指着那里,但现在你错了。那不是红色的车,是橙色的。


指针运算

好的,所以你仍然指着第二个停车位(现在被橙色车占据)

   1     2     3
-------------------
| o=o | o=o | o=o |
| |B| | |O| | |G| |
| o-o | o-o | o-o |

好吧,我现在有一个新问题......我想知道车子的颜色 下一个 停车位。您可以看到您指向的是位置 2,因此只需添加 1,您就指向了下一个位置。(finger+1),现在因为我想知道那里有什么数据,所以你必须检查那个点(不仅仅是手指),这样你就可以遵循指针(*(finger+1))查看那里有一辆绿色汽车(该位置的数据)

我不认为指针作为一个概念特别棘手——大多数学生的心智模型都会映射到这样的东西,一些快速的方框草图可以提供帮助。

困难(至少我过去经历过并看到其他人处理过)是 C/C++ 中指针的管理可能会不必要地复杂化。

带有一组良好图表的教程示例对理解指针有很大帮助.

Joel Spolsky 在他的著作中提出了一些关于理解指针的好观点。 面试游击指南 文章:

由于某种原因,大多数人似乎生来就没有理解指针的大脑部分。这是一个能力问题,而不是一个技能问题——它需要一种复杂形式的双重间接思维,而有些人就是做不到。

我认为理解指针的主要障碍是糟糕的老师。

几乎每个人都被教导关于指针的谎言:他们是 无非就是内存地址, ,或者他们允许你指向 任意位置.

当然,它们很难理解、危险且半神奇。

这些都不是真的。指针实际上是相当简单的概念, 只要你坚持 C++ 语言对它们的规定 并且不要向它们赋予“通常”在实践中有效但不受语言保证的属性,因此它们不是指针实际概念的一部分。

几个月前我试图在其中写下对此的解释 这篇博文 ——希望它能对某人有所帮助。

(请注意,在有人对我迂腐之前,是的,C++ 标准确实规定指针 代表 内存地址。但它并没有说“指针是内存地址,只不过是内存地址,并且可以与内存地址互换使用或考虑”。区别很重要)

指针的问题不在于概念。这是所涉及的执行和语言。当教师认为困难的是指针的概念,而不是行话或 C 和 C++ 对该概念造成的复杂混乱时,就会产生额外的混乱。因此,大量的精力都被浪费在解释这个概念上(就像这个问题的公认答案一样),而对于像我这样的人来说,这几乎是浪费了,因为我已经理解了所有这些。它只是解释了问题的错误部分。

为了让您了解我的出发点,我是一个非常了解指针的人,并且我可以在汇编语言中熟练地使用它们。因为在汇编语言中它们不被称为指针。它们被称为地址。当谈到用 C 语言编程和使用指针时,我犯了很多错误并且感到非常困惑。我还没有解决这个问题。让我举一个例子。

当 api 说:

int doIt(char *buffer )
//*buffer is a pointer to the buffer

它想要什么?

它可能想要:

代表缓冲区地址的数字

(为此,我是否要说 doIt(mybuffer), , 或者 doIt(*myBuffer)?)

表示缓冲区地址的数字

(就是它 doIt(&mybuffer) 或者 doIt(mybuffer) 或者 doIt(*mybuffer)?)

一个数字,表示缓冲区地址的地址

(也许那是 doIt(&mybuffer). 。或者是 doIt(&&mybuffer) ?甚至 doIt(&&&mybuffer))

等等,所涉及的语言并没有说得那么清楚,因为它涉及“指针”和“引用”这两个词,对我来说,它们的意义和清晰度不如“x持有y的地址”和“该函数需要 y" 的地址。答案还取决于“mybuffer”到底是什么,以及 doIt 打算用它做什么。该语言不支持实践中遇到的嵌套级别。就像当我必须将“指针”传递给创建新缓冲区的函数时,它会修改指针以指向缓冲区的新位置。它真的想要指针,还是指向指针的指针,这样它就知道去哪里修改指针的内容。大多数时候,我只需要猜测“指针”的含义,大多数时候我都是错的,无论我在猜测方面有多少经验。

“指针”实在是太重载了。指针是指向值的地址吗?或者它是一个保存值地址的变量。当函数需要指针时,它是想要指针变量所保存的地址,还是想要指针变量的地址?我很困惑。

我认为指针学习起来比较困难的原因是,在使用指针之前,您必须熟悉“在这个内存位置是一组代表 int、double、字符等的位”的想法。

当您第一次看到指针时,您并没有真正了解该内存位置的内容。“你什么意思,它持有一个 地址?"

我不同意“你要么得到它们,要么不得到它们”的观点。

当您开始找到它们的真正用途(例如不将大型结构传递到函数中)时,它们会变得更容易理解。

它如此难以理解的原因并不是因为它是一个困难的概念,而是因为 语法不一致.

   int *mypointer;

您首先了解到变量创建的最左边部分定义了变量的类型。指针声明在 C 和 C++ 中不是这样工作的。相反,他们说变量指向左侧的类型。在这种情况下: *我的指针 正在指着 在一个整数上。

我没有完全掌握指针,直到我尝试在 C# 中使用它们(不安全),它们的工作方式完全相同,但具有逻辑和一致的语法。指针本身就是一种类型。这里 我的指针 指向 int 的指针。

  int* mypointer;

甚至不让我开始了解函数指针......

当我只知道 C++ 时,我可以使用指针。通过反复试验,我知道在某些情况下该做什么、不该做什么。但让我完全理解的是汇编语言。如果您使用编写的汇编语言程序进行一些认真的指令级调试,您应该能够理解很多事情。

我喜欢家庭地址的比喻,但我一直认为地址是邮箱本身。通过这种方式,您可以直观地了解取消引用指针(打开邮箱)的概念。

例如下面的链接列表:1)从地址开始纸上开始2)转到纸上的地址3)打开邮箱以查找带有下一个地址的新纸

在线性链表中,最后一个邮箱中没有任何内容(链表末尾)。在循环链表中,最后一个邮箱包含第一个邮箱的地址。

请注意,第 3 步是取消引用发生的地方,当地址无效时,您将崩溃或出错。假设您可以走到一个无效地址的邮箱,想象一下那里有一个黑洞或什么东西,可以把世界翻个底朝天:)

我认为人们在这方面遇到麻烦的主要原因是因为它通常没有以有趣且引人入胜的方式进行教授。我希望看到讲师从人群中找 10 名志愿者,给他们每人一把 1 米的尺子,让他们以某种姿势站在一起,并用尺子互相指向对方。然后通过移动人们(以及他们指向标尺的位置)来显示指针算术。这将是一种简单但有效(最重要的是令人难忘)的展示概念的方式,而不会过于陷入机制之中。

一旦接触到 C 和 C++,对于某些人来说似乎会变得更困难。我不确定这是否是因为他们最终将他们没有正确掌握的理论付诸实践,或者是因为这些语言中的指针操作本质上更困难。我不太记得自己的转变,但我 知道 Pascal 中的指针,然后转移到 C 并完全迷失了。

我不认为指针本身令人困惑。大多数人都能理解这个概念。现在您可以考虑多少个指针,或者您可以接受多少个间接级别。不需要太多就能把人们推向边缘。事实上,它们可能会被程序中的错误意外更改,这也使得当代码出现问题时很难调试它们。

我认为这实际上可能是一个语法问题。指针的 C/C++ 语法似乎不一致并且比需要的更复杂。

讽刺的是,真正帮助我理解指针的是在 C++ 中遇到迭代器的概念 标准模板库. 。这很讽刺,因为我只能假设迭代器被视为指针的泛化。

有时,只有学会忽视树木,才能看到森林。

混乱来自于“指针”概念中混合在一起的多个抽象层。程序员不会对 Java/Python 中的普通引用感到困惑,但指针的不同之处在于它们公开了底层内存架构的特征。

干净地分离抽象层是一个很好的原则,而指针则不能做到这一点。

我喜欢用数组和索引来解释它——人们可能不熟悉指针,但他们通常知道索引是什么。

所以我说想象一下 RAM 是一个数组(并且你只有 10 字节的 RAM):

unsigned char RAM[10] = { 10, 14, 4, 3, 2, 1, 20, 19, 50, 9 };

那么指向变量的指针实际上只是 RAM 中该变量(的第一个字节)的索引。

所以如果你有一个指针/索引 unsigned char index = 2, ,那么该值显然是第三个元素,即数字 4。指向指针的指针是您获取该数字并将其用作索引本身的地方,例如 RAM[RAM[index]].

我会在一张纸上画一个数组,然后用它来显示诸如指向同一内存的许多指针、指针算术、指针到指针等内容。

邮政信箱号码。

这是一条允许您访问其他内容的信息。

(如果你对邮政信箱号码进行算术,你可能会遇到问题,因为信件进入了错误的信箱。如果有人搬到另一个州——没有转发地址——那么你就会有一个悬空指针。另一方面,如果邮局转发邮件,那么您就有一个指向指针的指针。)

通过迭代器来掌握它并不是一个坏方法。但继续看下去你会看到亚历山德雷斯库开始抱怨他们。

许多前 C++ 开发人员(在转储该语言之前从未理解迭代器是一种现代指针)跳到 C# 并仍然相信他们拥有不错的迭代器。

嗯,问题是迭代器的全部内容与运行时平台(Java/CLR)试图实现的目标完全不一致:新的、简单的、人人都是开发者的用法。这可能很好,但他们在紫皮书里说过一次,甚至在 C 之前和之前说过:

间接。

这是一个非常强大的概念,但如果你一直这样做的话,就永远不会如此。另一个例子是,迭代器很有用,因为它们有助于抽象算法。编译时是算法的地方,非常简单。你知道代码 + 数据,或者其他语言 C#:

IEnumerable + LINQ + Massive Framework = 300MB 运行时惩罚 通过大量引用类型实例拖动应用程序的糟糕间接间接。

“Le Pointer 很便宜。”

上面的一些答案断言“指针并不是很难”,但还没有直接解决“指针很难的地方!”来自。几年前,我辅导了一年级的计算机科学学生(只辅导了一年,因为我显然很糟糕),我很清楚, 主意 指针并不难。难的是理解 为什么以及何时需要指针.

我认为您不能将这个问题(为什么以及何时使用指针)与解释更广泛的软件工程问题分开。为什么每个变量都应该 不是 是一个全局变量,以及为什么应该将类似的代码分解成函数(即,得到这个,使用 指针 将他们的行为专门化到他们的调用站点)。

我不明白指针有什么令人困惑的。它们指向内存中的一个位置,即存储内存地址。在 C/C++ 中,您可以指定指针指向的类型。例如:

int* my_int_pointer;

表示 my_int_pointer 包含指向包含 int 的位置的地址。

指针的问题在于它们指向内存中的某个位置,因此很容易进入某个不应该位于的位置。作为证据,请查看 C/C++ 应用程序中因缓冲区溢出(将指针递增超过分配的边界)而存在的众多安全漏洞。

只是为了让事情变得更加混乱,有时您必须使用句柄而不是指针。句柄是指向指针的指针,以便后端可以移动内存中的内容以对堆进行碎片整理。如果指针在例行程序中发生变化,结果将是不可预测的,因此您首先必须锁定手柄以确保没有任何变化。

http://arjay.bc.ca/Modula-2/Text/Ch15/Ch15.8.html#15.8.5 谈论它比我更连贯一些。:-)

每个 C/C++ 初学者都会遇到同样的问题,出现这个问题不是因为“指针很难学”,而是因为“谁以及如何解释它”。有些学习者通过口头方式或视觉方式收集它,解释它的最好方法是使用 “火车”示例 (适合口头和视觉示例)。

在哪里 “机车” 是一个指针,其中 不能 握住任何东西并且 “车皮” 是“机车”试图拉(或指向)的东西。之后,您可以对“马车”本身进行分类,它可以容纳动物、植物还是人(或它们的混合)。

许可以下: CC-BY-SA归因
不隶属于 StackOverflow
scroll top