关于C++:理解指针的障碍是什么,如何克服这些障碍?

What are the barriers to understanding pointers and what can be done to overcome them?

为什么指针对于许多新的,甚至是大学的C级或C++级的学生来说是一个混乱的主导因素?是否有任何工具或思想过程可以帮助您理解指针在变量、函数和级别之外的工作方式?

有什么好的实践可以让人达到"啊哈,我明白了"的水平,而不会让他们陷入整体概念的困境?基本上,类似演练的场景。


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

我发现最好的类比是把指针看作一张纸,上面有一个房屋地址,内存块作为实际房屋。因此,可以很容易地解释各种操作。好的。

我在下面添加了一些Delphi代码,并在适当的地方添加了一些注释。我选择Delphi是因为我的另一种主要编程语言C不以相同的方式显示内存泄漏之类的东西。好的。

如果您只想学习指针的高级概念,那么应该忽略下面解释中标记为"内存布局"的部分。它们的目的是给出操作后内存的外观示例,但它们在本质上更低级。然而,为了准确地解释缓冲区溢出是如何工作的,我添加这些图表是很重要的。好的。

免责声明:对于所有意图和目的,本解释和示例记忆布局大大简化了。你会有更多的开销和更多的细节需要知道您是否需要在底层处理内存。然而,对于为了解释记忆和指针,它足够精确。好的。

假设下面使用的thouse类如下所示:好的。

1
2
3
4
5
6
7
type
    THouse = class
    private
        FName : array[0..9] of Char;
    public
        constructor Create(name: PChar);
    end;

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

在内存中,会有一些与房屋分配相关的开销,我将如下所示:好的。

1
2
3
4
5
6
---[ttttNNNNNNNNNN]---
     ^   ^
     |   |
     |   +- the FName array
     |
     +- overhead

"tttt"区域是开销,对于不同类型的运行时和语言,通常会有更多的开销,比如8或12字节。存储在该区域中的任何值都必须不被内存分配器或核心系统例程以外的任何东西更改,否则会有导致程序崩溃的风险。好的。

分配内存好的。

找个企业家来建造你的房子,把地址给你。与现实世界相比,内存分配无法告知分配位置,而是会找到一个有足够空间的合适位置,并将地址报告给分配的内存。好的。

换言之,企业家会选择这个地点。好的。

1
THouse.Create('My house');

内存布局:好的。

1
2
---[ttttNNNNNNNNNN]---
    1234My house

用地址保存变量好的。

把地址写在一张纸上。这篇论文将作为你对你家的参考。如果没有这张纸,你会迷路,找不到房子,除非你已经在里面了。好的。

1
2
3
4
5
var
    h: THouse;
begin
    h := THouse.Create('My house');
    ...

内存布局:好的。

1
2
3
4
    h
    v
---[ttttNNNNNNNNNN]---
    1234My house

复制指针值好的。

把地址写在一张新纸上就行了。你现在有两张纸可以把你送到同一个房子,而不是两个分开的房子。如果你试图从一张纸上找到地址,重新整理那栋房子里的家具,就会发现另一栋房子也被同样的方式修改过,除非你能明确地发现它实际上只是一栋房子。好的。

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

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

释放内存好的。

拆掉房子。如果你愿意的话,你可以稍后再使用这张纸换一个新的地址,或者清除它,把不再存在的地址忘了。好的。

1
2
3
4
5
6
7
var
    h: THouse;
begin
    h := THouse.Create('My house');
    ...
    h.Free;
    h := nil;

在这里,我首先建造了这座房子,并得到了它的地址。然后我对房子做点什么(用它,用…代码,留给读者作为练习),然后我释放它。最后,我从变量中清除地址。好的。

内存布局:好的。

1
2
3
4
5
6
7
8
9
    h                        <--+
    v                           +- before free
---[ttttNNNNNNNNNN]---          |
    1234My house             <--+

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

悬空指针好的。

你告诉你的企业家摧毁房子,但你忘了把地址从纸上抹去。后来当你看那张纸的时候,你忘记了房子已经不在了,于是去拜访它,结果失败了(另见下面关于无效参考的部分)。好的。

1
2
3
4
5
6
7
8
var
    h: THouse;
begin
    h := THouse.Create('My house');
    ...
    h.Free;
    ... // forgot to clear h here
    h.OpenFrontDoor; // will most likely fail

打电话给.Free后使用h可能会奏效,但那只是纯粹的运气。最有可能的情况是,在一个客户的地方,在一个关键的操作过程中,它会失败。好的。

1
2
3
4
5
6
7
8
9
    h                        <--+
    v                           +- before free
---[ttttNNNNNNNNNN]---          |
    1234My house             <--+

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

如您所见,h仍然指向内存中数据的剩余部分,但是因为它可能不完整,像以前那样使用它可能会失败。好的。

内存泄漏好的。

你丢了那张纸,找不到房子。不过,这所房子仍然矗立在某个地方,当你后来想要建造一座新房子时,你不能再利用那个地方。好的。

1
2
3
4
5
6
7
8
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变量的内容,但是旧的仍然存在…某处。在这段代码之后,就没有办法到达那所房子了,它将被留在原地。换言之,分配的内存将保持分配状态,直到应用程序关闭,此时操作系统将关闭它。好的。

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

1
2
3
4
    h
    v
---[ttttNNNNNNNNNN]---
    1234My house

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

1
2
3
4
                       h
                       v
---[ttttNNNNNNNNNN]---[ttttNNNNNNNNNN]
    1234My house       5678My house

获得这个方法的一个更常见的方法是忘记释放一些东西,而不是像上面那样重写它。在Delphi术语中,这将通过以下方法发生:好的。

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

在这个方法执行之后,变量中没有地址存在于房子中,但是房子仍然存在。好的。

内存布局:好的。

1
2
3
4
5
6
7
8
9
    h                        <--+
    v                           +- before losing pointer
---[ttttNNNNNNNNNN]---          |
    1234My house             <--+

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

如您所见,旧数据在内存中是完整的,不会由内存分配器重用。分配器跟踪内存区域已被使用,除非您释放它。好的。

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

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

也许你会找到一个房子,但它不是你最初的地址,因此任何使用它的尝试,似乎它属于你可能会失败可怕。好的。

有时,你甚至会发现一个相邻的地址有一个相当大的房子,它占据了三个地址(主街1-3),而你的地址到了房子的中间。任何试图将这部分3个地址的大房子视为一个小房子的尝试都可能失败。好的。

1
2
3
4
5
6
7
8
9
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仍然有旧的、过时的地址。进入不再站立的房子可能会或可能不会工作。好的。

这是上面悬空指针的变体。查看它的内存布局。好的。

缓冲区泛滥好的。

你往房子里搬的东西太多了,可能会溅到邻居家或院子里。当邻居家的主人稍后回家时,他会找到他认为属于自己的各种东西。好的。

这就是我选择固定大小数组的原因。要设置阶段,假设由于某种原因,我们分配的第二套房子将放在记忆中的第一个。换言之,第二个房子的地址比第一个。而且,它们被分配到彼此的旁边。好的。

因此,此代码:好的。

1
2
3
4
5
6
7
8
var
    h1, h2: THouse;
begin
    h1 := THouse.Create('My house');
    h2 := THouse.Create('My other house somewhere');
                         ^-----------------------^
                          longer than 10 characters
                         0123456789 <-- 10 characters

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

1
2
3
4
                        h1
                        v
-----------------------[ttttNNNNNNNNNN]
                        5678My house

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

1
2
3
4
5
6
7
    h2                  h1
    v                   v
---[ttttNNNNNNNNNN]----[ttttNNNNNNNNNN]
    1234My other house somewhereouse
                        ^---+--^
                            |
                            +- overwritten

最常导致崩溃的部分是覆盖重要的部分您存储的数据中确实不应该随机更改的数据。例如h1房子的部分名称被更改可能不是问题,在程序崩溃方面,但覆盖了当您尝试使用破碎的对象时,对象很可能崩溃,以及覆盖存储到的链接对象中的其他对象。好的。

链表好的。

当你沿着一张纸上的地址走的时候,你就到了一个房子,在那个房子里还有另一张纸,上面有一个新的地址,链中的下一个房子,等等。好的。

1
2
3
4
5
6
var
    h1, h2: THouse;
begin
    h1 := THouse.Create('Home');
    h2 := THouse.Create('Cabin');
    h1.NextHouse := h2;

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
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;

内存布局(下一个添加为对象中的链接,用注释下图中的四个llll):好的。

1
2
3
4
5
6
    h1                      h2
    v                       v
---[ttttNNNNNNNNNNLLLL]----[ttttNNNNNNNNNNLLLL]
    1234Home       +        5678Cabin      +
                   |        ^              |
                   +--------+              * (no link)

基本上,什么是内存地址?好的。

内存地址基本上就是一个数字。如果你想到记忆作为一个大的字节数组,第一个字节的地址是0,下一个字节的地址是0。地址1等向上。这是简化的,但已经足够好了。好的。

所以这个内存布局:好的。

1
2
3
4
    h1                 h2
    v                  v
---[ttttNNNNNNNNNN]---[ttttNNNNNNNNNN]
    1234My house       5678My house

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

  • H1=4
  • H2=23

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

1
2
3
4
5
6
    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;

比尔现在指向萨姆。

我想你明白了。我想我们花了大约一个小时做这个,直到我们复习了指针分配的基本知识。


我发现一个有助于解释指针的类比是超链接。大多数人都可以理解,网页上的链接"指向"Internet上的另一个网页,如果您可以复制和粘贴该超链接,那么它们都将指向同一原始网页。如果您去编辑那个原始页面,然后按照这些链接(指针)中的任何一个进行操作,您将得到新的更新页面。


很多人似乎对指针感到困惑的原因是,它们大多没有或几乎没有计算机体系结构的背景。由于许多人似乎不知道计算机(机器)是如何实际实现的——在C/C++中工作似乎是陌生的。

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

任何需要稍微超过简单加法的东西都会涉及指针,它们肯定会得到它。


Why are pointers such a leading factor of confusion for many new, and even old, college level students in the C/C++ language?

值-变量-占位符的概念映射到我们在学校教的东西-代数。没有一个现有的并行,你可以不理解内存是如何在计算机内物理布局的,并且没有人想到这种事情,直到他们处理低级别的事情-在C/C++/Byter通信级别。

Are there any tools or thought processes that helped you understand how pointers work at the variable, function, and beyond level?

地址框。我记得当我学习将BASIC编程到微型计算机时,有一些漂亮的书里面有游戏,有时你必须把值输入特定的地址。他们有一组盒子的图片,用0,1,2递增标记。有人解释说,只有一个小东西(一个字节)可以放在这些盒子里,而且有很多这样的东西——有些电脑有多达65535个!他们在一起,都有一个地址。

What are some good practice things that can be done to bring somebody to the level of,"Ah-hah, I got it," without getting them bogged down in the overall concept? Basically, drill like scenarios.

练习?构造一个结构:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
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中除外:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
// 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
"
, *my_pointer);
my_pointer++;
printf("After: my_pointer = %c
"
, *my_pointer);
my_pointer = &mystruct.a;
printf("Then: my_pointer = %c
"
, *my_pointer);
my_pointer = my_pointer + 3;
printf("End: my_pointer = %c
"
, *my_pointer);

输出:

1
2
3
4
Start: my_pointer = s
After: my_pointer = t
Then: my_pointer = r
End: my_pointer = u

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


起初,我很难理解这些要点,原因是很多解释中都包含了很多关于引用传递的废话。所有这些都混淆了这个问题。使用指针参数时,仍按值传递;但该值恰好是地址,而不是int。

其他人已经链接到此教程,但我可以突出显示我开始理解指针的时刻:

关于C中指针和数组的教程:第3章-指针和字符串

1
int puts(const char *s);

For the moment, ignore the const. The parameter passed to puts() is a pointer, that is the value of a pointer (since all parameters in C are passed by value), and the value of a pointer is the address to which it points, or, simply, an address. Thus when we write puts(strA); as we have seen, we are passing the address of strA[0].

当我读到这些话的时候,云分开了,一束阳光将我包裹在了心底。

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


我发现TedJensen的"C语言中指针和数组的教程"是学习指针的极好资源。它分为10节课,首先解释指针是什么(以及它们的用途),最后是函数指针。http://home.netcom.com/~tjensen/ptr/cpoint.htm网站

接下来,Beej的网络编程指南介绍了UnixSocketsAPI,从中您可以开始做一些真正有趣的事情。网址:http://beej.us/guide/bgnet/


指针的复杂性超出了我们可以轻易教的范围。让学生互相指向对方,使用带有住址的纸片,都是很好的学习工具。他们在介绍基本概念方面做得很好。事实上,学习基本概念对于成功地使用指针是至关重要的。然而,在生产代码中,进入比这些简单的演示可以封装的更复杂的场景是很常见的。

我参与过一些系统,其中我们的结构指向其他结构指向其他结构。其中一些结构还包含嵌入式结构(而不是指向其他结构的指针)。这就是指针变得非常混乱的地方。如果您有多个间接级别,并且您开始以这样的代码结束:

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

它会很快变得混乱(想象更多的行,可能更多的级别)。抛出指针数组和节点到节点指针(树、链接列表),情况会更糟。我已经看到一些真正优秀的开发人员一旦开始使用这样的系统就会迷失方向,甚至那些非常了解基础知识的开发人员也会迷失方向。

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

我认为大学能帮助学生学习指针的最好方法是使用好的演示,结合需要指针使用的项目。一个困难的项目对理解指针的作用将超过一千个演示。演示可以让你有一个浅显的理解,但要深刻地把握指针,你必须真正地使用它们。


我想我会在这个列表中添加一个类比,当我作为一名计算机科学导师解释指针(回到今天)时,发现这个类比非常有用;首先,让我们:

设置舞台:

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

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

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

添加数据

停车场永远不会空置很久…如果它做到了,这将是毫无意义的,没有人会建立任何。所以让我们假设,随着一天的推移,停车场上有3辆车,一辆蓝色的车,一辆红色的车和一辆绿色的车:

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

这些车都是同一类型的(车),所以有一种方法可以这样认为:我们的车是某种数据(比如说int,但它们有不同的值(blueredgreen;可能是一种颜色enum

输入指针

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

你的手指(指针)不是我问题的答案。看着你的手指什么也没告诉我,但是如果我看到你的手指指向的地方(取消指针的引用),我就能找到我要找的车(数据)。

重新分配指针

现在我可以让你找一辆红色的车来代替,你可以把手指转向一辆新车。现在,您的指针(和以前一样)显示了相同类型(汽车)的新数据(可以找到红色汽车的停车位)。

指针没有物理变化,它仍然是你的手指,只是它显示的数据改变了。(停车场地址)

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

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

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

悬空的指针

现在让我们假设你感觉非常像一座雕像,你想握着你的手无限期地指向那辆红色汽车。如果那辆红色的车开走了呢?

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

你的指针仍然指向红色汽车的位置,但它已经不在了。假设有辆新车停在那里…橙色的汽车现在,如果我再问你一次,"红色汽车在哪里",你仍然指向那里,但现在你错了。那不是红色的车,那是橙色的。

指针算术

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

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

好吧,我现在有一个新问题……我想知道下一个停车位的车的颜色。你可以看到你指向了第二个点,所以你只要加上1,你就指向了下一个点。(finger+1号),既然我想知道那里有什么数据,你必须检查那一点(不仅仅是手指),这样你就可以根据指针(*(finger+1)号)来查看那里有一辆绿色汽车(数据在那个位置)。


我不认为指针作为一个概念是特别棘手的-大多数学生的心理模型映射到这样的东西和一些快速的方框草图可以帮助。

困难,至少是我过去经历过和看到其他人处理过的问题,是C/C++中指针的管理可以毫无意义地卷绕。


一个带有一组良好图表的教程示例可以极大地帮助理解指针。

乔尔·斯波斯基在他的《游击队采访指南》一文中提出了一些理解要点:

For some reason most people seem to be born without the part of the brain that understands pointers. This is an aptitude thing, not a skill thing – it requires a complex form of doubly-indirected thinking that some people just can't do.


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

几乎每个人都被教导关于指针的谎言:它们只不过是内存地址,或者它们允许您指向任意位置。

当然,他们很难理解,危险和半魔法。

这些都不是真的。指针实际上是相当简单的概念,只要你坚持C++语言必须对它们说什么,而不赋予它们"通常"在实践中工作的属性,但是它们不能被语言所保证,那么它们就不是指针的实际概念的一部分。

几个月前,我在这篇博文中试着对此做了解释——希望它能对某人有所帮助。

(注意,在任何人对我产生学问之前,是的,C++标准确实指出指针代表内存地址。但它并没有说"指针是内存地址,而不是内存地址,可以与内存地址互换使用或考虑"。区别很重要)


指针的问题不是概念。它涉及到执行和语言。当教师认为指针的概念是困难的,而不是术语,或者C和C++的复杂的概念。所以大量的努力都集中在解释这个概念上(就像这个问题的公认答案),而这几乎是浪费在像我这样的人身上,因为我已经理解了所有这些。只是解释了问题的错误部分。

为了让你知道我来自哪里,我是一个完全理解指针的人,我可以在汇编语言中很好地使用它们。因为在汇编语言中,它们不被称为指针。它们被称为地址。在C语言中,当涉及到编程和使用指针时,我犯了很多错误,并且非常困惑。我还没有把这件事弄清楚。让我举个例子。

当API说:

1
2
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"是从什么开始的,以及它打算做什么。该语言不支持实践中遇到的嵌套级别。就像我必须将一个"指针"交给一个创建新缓冲区的函数,它修改指针以指向缓冲区的新位置。它真的想要指针,还是指向指针的指针,所以它知道到哪里去修改指针的内容。大多数时候我只能猜测"指针"是什么意思,而大多数时候我都是错的,不管我有多少猜测经验。

"指针"太重了。指针是指向值的地址吗?或者它是一个将地址保存到值的变量。当一个函数需要一个指针时,它想要指针变量保存的地址,还是想要指针变量的地址?我搞糊涂了。


我认为,使指针变得难以学习的是,直到指针,你才习惯于"在这个内存位置是一组表示int,double,a character,随便什么"的想法。

当你第一次看到一个指针时,你并不能真正得到内存位置上的内容。"你什么意思?它有地址吗?"

我不同意"你要么得到他们,要么不得到"的观点。

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


之所以如此难以理解,不是因为这是一个困难的概念,而是因为语法不一致。

1
   int *mypointer;

您首先了解到变量创建的最左侧部分定义了变量的类型。指针声明在C和C++中不起作用。相反,他们说变量指向左边的类型。在这种情况下:*mypointer指向一个int。

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

1
  int* mypointer;

甚至不要让我开始使用函数指针…


当我只知道C++时,我可以用指针工作。在某些情况下,我知道该怎么做,在试验/错误中我知道不该怎么做。但让我完全理解的是汇编语言。如果使用编写的汇编语言程序进行一些严格的指令级调试,您应该能够理解很多东西。


我喜欢家庭住址的类比,但我一直认为这个地址就是邮箱本身。这样您就可以可视化取消引用指针(打开邮箱)的概念。

例如,在链接列表之后:1)从你的论文开始写地址2)转到纸上的地址3)打开邮箱,找到一张新纸,上面有下一个地址。

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

请注意,步骤3是取消引用的地方,当地址无效时,您将崩溃或出错。假设你可以走到一个无效地址的邮箱前,想象一下里面有一个黑洞或者什么东西能把世界翻出来。)


我认为人们对它有困难的主要原因是,它通常不是以一种有趣和吸引人的方式教授的。我想看到一个讲师从人群中选出10名志愿者,给他们每人一个1米长的尺子,让他们以某种形式站在周围,用尺子互相指向对方。然后通过移动人们来显示指针算术(以及他们指向他们的标尺的位置)。这将是一种简单但有效的(尤其是令人难忘的)展示概念的方式,而不会陷入机制中。

一旦你进入C和C++,对某些人来说似乎越来越难了。我不确定这是否是因为他们最终把他们没有正确掌握的理论付诸实践,或者是因为指针操作在这些语言中固有的困难。我记不清自己的转变,但我知道帕斯卡的指针,然后移到了C,完全迷路了。


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

所以我说假设RAM是一个数组(而您只有10个字节的RAM):

1
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++语法似乎不一致,比它需要的要复杂得多。

具有讽刺意味的是,实际上帮助我理解指针的事情是在C++标准模板库中遇到迭代器的概念。这很讽刺,因为我只能假设迭代器是作为指针的泛化而设计的。

有时你只是在学会忽视树木之前看不到森林。


我不认为指针本身是混淆的。大多数人都能理解这个概念。现在你能想到多少个指针,或者你能接受多少个间接的级别。把人推到边缘不需要太多。程序中的错误可以意外地更改它们,这也会使它们在代码出错时很难调试。


混淆来自于"指针"概念中混合在一起的多个抽象层。程序员不会被Java/Python中的普通引用弄糊涂,但是指针是不同的,因为它们暴露了底层内存体系结构的特性。

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


邮政信箱号码。

这是一条信息,可以让你访问其他东西。

(如果你对邮局的信箱号码做算术运算,你可能会有问题,因为信放错了。如果有人移动到另一个状态——没有转发地址——那么你有一个悬空的指针。另一方面——如果邮局转发邮件,那么您有一个指向指针的指针。)


通过迭代器,一个不错的方法来掌握它。但是继续看,你会看到亚历山德里斯科开始抱怨他们。

许多ex-c++开发人员(他们在转储语言之前从未理解迭代器是一个现代指针)跳到c并仍然相信他们有合适的迭代器。

嗯,问题是,所有的迭代器在运行时平台(Java/CLR)试图实现的方面都是完全不同的:新的、简单的、每一个IS-A DEV使用。这本书不错,但他们在紫色书里说过一次,甚至在C之前和C之前说过:

Indirection。

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

ienumerable+linq+massive framework=300MB运行时惩罚,通过大量引用类型的实例间接拖动应用程序。

"指针很便宜。"


上面的一些答案声称"指针不是很硬",但没有直接指向"指针很硬"的地方。来自。几年前,我辅导一年级的学生(只有一年,因为我很清楚地吸了它),我很清楚指针的概念并不难。难的是理解为什么和什么时候你想要一个指针。

我不认为你能从解释更广泛的软件工程问题中脱离这个问题——为什么以及何时使用指针。为什么每个变量都不应该是一个全局变量,为什么要将类似的代码分解成函数(也就是说,得到这个结果,使用指针专门化它们对调用站点的行为)。


每个C/C++初学者都有同样的问题,这个问题不是因为"指针很难学",而是因为"谁和它如何解释"。有些学习者口头收集一些视觉上的,最好的解释方法是使用"训练"的例子(适合口头和视觉上的例子)。

其中"机车"是一个指针,它不能容纳任何东西,"货车"是"机车"试图拉动(或指向)的东西。之后,您可以将"旅行车"本身分类,它能容纳动物、植物或人(或它们的混合体)。


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

1
int* my_int_pointer;

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

指针的问题是它们指向内存中的一个位置,因此很容易跟踪到不应该位于的某个位置。作为证据,从缓冲区溢出(通过指针越过分配的边界)查看C/C++应用程序中的许多安全漏洞。


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

http://arjay.bc.ca/modula-2/text/ch15/ch15.8.html 15.8.5比我更连贯地谈论它。-)