人们觉得 C 指针有那些比较难的点?

从这里提出的问题的数量来看,很明显,人们在理解指针和指针算法时有一些非常基本的问题。

我很好奇为什么。它们从来没有真正给我带来过什么大问题(尽管我是在新石器时代第一次听说它们的)。为了更好地回答这些问题,我想知道人们觉得什么是困难的。

所以,如果你正在与指针斗争,或者你最近突然“明白了”,指针的哪些方面导致了你的问题?

44386 次浏览

在 Joel Spolsky 的站点 JavaSchools 的危险上有一篇很棒的文章支持指针是硬指针的观点。

[免责声明-我不是一个讨厌 Java 的 本质上。]

正确理解指针需要了解底层机器的体系结构。

今天,许多程序员不知道他们的机器是如何工作的,就像大多数会开车的人对引擎一无所知一样。

我认为它需要一个坚实的基础,可能从机器层面,介绍了一些机器代码,汇编,以及如何表示项目和数据结构的 RAM。它需要一点时间,一些家庭作业或解决问题的练习,和一些思考。

但是如果一个人一开始就知道高级语言(这没什么错——木匠用斧头。一个需要分裂原子的人使用别的东西。我们需要木匠,我们需要研究原子的人) ,这个懂高级语言的人被给予2分钟的指针介绍,然后很难指望他理解指针算术,指针指向指针,指向可变大小字符串的指针数组,以及字符数组,等等。一个低水平的坚实的基础可以帮助很多。

至少对我来说,使用指针的主要困难在于,我没有从 C 语言开始,而是从 Java 开始。整个指针的概念真的很陌生,直到在大学的一些课程中,我被期望知道 C。所以我自学了 C 语言的基础知识以及如何使用指针。即便如此,每次我发现自己在读 C 代码时,我都必须查找指针语法。

因此,在我非常有限的经验(一年的现实世界 + 4在大学) ,指针迷惑我,因为我从来没有真正使用它以外的任何其他课堂设置。我同情那些现在用 JAVA 代替 C 或 C + + 开始 CS 的学生。正如你所说,你在“新石器时代”学到了指针,而且可能从那时起就一直在使用它。对于我们这些新人来说,分配内存和做指针运算的概念是非常陌生的,因为所有这些语言都把它抽象化了。

附言。 在读完斯波尔斯基的文章后,他对“ JavaSchools”的描述与我在康奈尔大学(05-09)所经历的完全不同。我选修了结构和函数式编程(sml)、操作系统(C)、算法(笔和纸)以及一大堆没有用 Java 教授的课程。然而所有的介绍类和选修课都是在 java 中完成的,因为当你尝试做一些比实现带指针的 hashtable 更高级的事情时,不使用重造轮子是有价值的。

我怀疑人们的回答有点太深奥了。实际上并不需要了解调度、实际的 CPU 操作或汇编级内存管理。

当我在教学的时候,我发现以下学生理解上的漏洞是问题最常见的来源:

  1. 堆与堆栈存储。令人吃惊的是,有那么多人不理解这一点,甚至在一般意义上也是如此。
  2. 堆栈帧。只是关于本地变量的专用堆栈部分的一般概念,以及它是一个“堆栈”的原因... ... 诸如隐藏返回位置、异常处理程序细节和以前的寄存器等细节可以安全地保留,直到有人尝试构建编译器。
  3. “内存就是内存就是内存”的施法只是改变了运算符的版本或者编译器为特定内存块提供了多少空间。当人们谈论“什么(原始)变量 X 真的是什么”时,你知道你正在处理这个问题。

我的大多数学生能够理解内存块的简化绘图,通常是当前作用域中堆栈的局部变量部分。一般给不同地点明确的虚构地址有所帮助。

总而言之,我的意思是,如果你想理解指针,你必须理解变量,以及它们在现代架构中的实际意义。

我个人责怪参考资料的质量和教学人员; C 语言中的大多数概念(但是 尤其是指针)只是简单的 被教导。我一直威胁要自己写一本 C 语言的书(书名为 世界最不需要的就是另一本关于 C 语言的书) ,但我没有时间和耐心这样做。所以我在这里闲逛,随意引用《标准报》上的话。

还有一个事实是,当 C 最初设计的时候,它是 假设,你对机器架构的理解达到了一个相当详细的水平,只是因为在你的日常工作中没有办法避免它(内存是如此紧张,处理器是如此缓慢,你必须了解你写的东西如何影响性能)。

我没有得到指导,直到我读了 K & R 的描述。直到那时,指针才有意义。我读了一大堆的文章,其中有人说: “不要学习指导,它们会让人困惑,会伤害你的头,让你得动脉瘤。”所以我很长一段时间都回避这个问题,并创造了这种不必要的困难概念的氛围。

否则,我想的主要是,为什么你会想要一个变量,你必须通过循环才能得到它的值,如果你想给它赋值,你必须做一些奇怪的事情,才能得到它的值。我想,变量的全部意义在于存储一个值,所以我不明白为什么有人要把它复杂化。我想是 ”因此,对于指针,必须使用 *运算符才能得到它的值? ? ?这是哪门子的变量?”。毫无意义,没有双关语。

它之所以复杂是因为我不明白指针是指向某个东西的 地址。如果你解释说它是一个地址,它是包含一个地址到其他东西的东西,并且你可以操纵这个地址来做有用的事情,我认为这可能会消除混淆。

一个类需要使用指针来访问/修改 PC 上的端口,使用指针算法来寻址不同的内存位置,以及查看更复杂的 C 代码来修改他们的参数,这让我不再认为指针是无意义的。

回顾过去,有四件事真正帮助我最终理解了指针。在此之前,我可以使用它们,但我不完全理解它们。也就是说,我知道如果我按照形式,我会得到我想要的结果,但我不完全理解的“为什么”的形式。我知道这并不完全是你所要求的,但我认为这是一个有用的推论。

  1. 编写一个例程,该例程接受一个指向整数的指针并修改该整数。这给了我必要的形式,在此基础上构建指针如何工作的任何心智模型。

  2. 一维动态内存分配。计算出一维内存分配使我明白了指针的概念。

  3. 二维动态内存分配。计算出二维内存分配强化了这个概念,但也教会了我指针本身需要存储空间,必须考虑到这一点。

  4. 堆栈变量、全局变量和堆内存之间的差异。弄清楚这些差异教会了我指针指向/引用的内存类型。

这些项目中的每一项都需要想象在较低层次上发生了什么——建立一个心理模型,以满足我能想到的每一个情况。这需要时间和努力,但是很值得。我确信,要理解指针,你必须建立关于它们如何工作以及如何实现它们的心智模型。

现在回到你最初的问题。根据前面的列表,有几个项目我原来很难掌握。

  1. 如何以及为什么要使用指针。
  2. 它们与数组有什么不同,又有什么相似之处。
  3. 了解指针信息的存储位置。
  4. 理解指针指向的内容和位置。

我经常遇到的问题(主要是自学的)是“何时”使用指针。我可以理解构造指针的语法,但是我需要知道在什么情况下应该使用指针。

只有我有这种心态吗? ; -)

当我第一次开始使用它们时,最大的问题是语法。

int* ip;
int * ip;
int *ip;

都是一样的。

但是:

int* ip1, ip2;  //second one isn't a pointer!
int *ip1, *ip2;

为什么? 因为声明的“指针”部分属于变量,而不是类型。

然后去引用这个东西使用了一个非常相似的符号:

*ip = 4;  //sets the value of the thing pointed to by ip to '4'
x = ip;   //hey, that's not '4'!
x = *ip;  //ahh... there's that '4'

除非你真的需要得到一个指针... ... 然后你使用一个 & 符号!

int *ip = &x;

坚持万岁!

然后,显然只是为了证明他们有多聪明,很多库开发人员使用指针到指针到指针,如果他们期望这些东西的数组,那么为什么不只是传递一个指针到那个。

void foo(****ipppArr);

要调用这个函数,我需要指向指向整型指针的指针数组的地址:

foo(&(***ipppArr));

在六个月内,当我必须维护这段代码时,我将花更多的时间来弄清楚这一切意味着什么,而不是从头开始重写。 (是的,可能是语法错误——我已经有一段时间没有用 C 语言做任何事情了,我有点想念它,但是我有点受虐狂的感觉)

指针是困难的,因为间接。

指针(以及低级工作的一些其他方面)要求用户消除这种魔力。

大多数高级程序员都喜欢魔术。

首先,我应该说 C 和 C + + 是我学习的第一种编程语言。我从 C 语言开始学习,然后在学校学习 C + + ,很多次,然后回到 C 语言学习,流利地掌握它。

学习 C 语言时,第一件让我对指针感到困惑的事情是:

char ch;
char str[100];
scanf("%c %s", &ch, str);

这种混淆主要是因为在正确引入指针之前,我已经引入了在 OUT 参数中使用对变量的引用。我记得我跳过了在 C 为了 傻瓜中写最初的几个例子,因为它们太简单了,以至于我写的第一个程序永远也写不出来(很可能就是因为这个原因)。

令人困惑的是 &ch实际上是什么意思,以及为什么 str不需要它。

在我熟悉了这一点之后,我接下来记得对动态分配感到困惑。我在某个时候意识到,如果没有某种类型的动态分配,拥有指向数据的指针并不是非常有用,所以我写了这样的东西:

char * x = NULL;
if (y) {
char z[100];
x = z;
}

试图动态分配一些空间。没用。我不确定这能不能成功,但我不知道还有什么其他办法。

后来我学习了 mallocnew,但它们对我来说真的像是神奇的记忆发生器。我不知道它们是如何工作的。

过了一段时间,我又开始学习递归(我以前自己学过,但现在在上课) ,我问递归是如何工作的——单独的变量存储在哪里。我的教授说“在堆栈上”,很多事情对我来说变得清晰起来。我以前听说过这个术语,以前也实现过软件栈。我很久以前就听别人提到过“堆栈”,但是我已经忘记了。

大约在这个时候,我也意识到在 C 语言中使用多维数组会变得非常混乱。我知道它们是如何工作的,但是它们太容易被卷入其中了,所以我决定尽可能多地使用它们。我认为这里的问题主要是语法问题(特别是从函数中传递或返回它们)。

因为接下来一两年我都在为学校写 C + + ,所以我有了很多使用数据结构指针的经验。在这里,我遇到了一系列新的麻烦——混淆指针。我会有多个层次的指针(如 node ***ptr;)绊倒我。我会取消引用一个指针错误的次数,并最终求助于计算出有多少 *我需要通过试验和错误。

在某个时候,我了解了程序的堆是如何工作的(算是吧,但是足够好了,它不再让我夜不能寐)。我记得曾经读到过,如果在某个系统上的 malloc返回指针之前查看几个字节,就可以看到实际分配了多少数据。我意识到 malloc中的代码可能要求操作系统提供更多的内存,而这些内存不是我的可执行文件的一部分。对 malloc的工作原理有一个像样的工作想法是非常有用的。

不久之后,我参加了一个汇编课程,它没有像大多数程序员可能认为的那样教会我很多关于指针的知识。这确实让我思考了更多关于我的代码可能被翻译成什么样的程序集的问题。我一直试图编写高效的代码,但现在我有了一个更好的主意。

我还参加了一些课程,我必须写一些 口齿不清。在编写 lisp 的时候,我并不像在 C 语言中那样关心效率问题,我对这段代码在编译后可能被翻译成什么几乎一无所知,但是我知道看起来使用大量的本地命名符号(变量)可以使事情变得容易得多。在某个时候,我用一点 lisp 编写了一些 AVL 树旋转代码,由于指针问题,我在用 C + + 编写这些代码时遇到了很大的困难。我意识到我对过多的本地变量的厌恶阻碍了我用 C + + 编写本地变量和其他几个程序的能力。

我还上过编译器课。在这门课上,我翻到了高级材料,学习了 静电干扰 单身 任务(SSA)和死变量,这并不重要,除了它告诉我,任何一个像样的编译器都能很好地处理不再使用的变量。我已经知道更多的变量(包括指针)有正确的类型和好的名字可以帮助我在头脑中保持清醒,但是现在我也知道为了效率而避免它们比我那些不太注重微观优化的教授告诉我的更加愚蠢。

所以对我来说,了解一点程序的内存布局对我很有帮助。思考我的代码在象征意义上和在硬件上的含义,可以帮助我解决这个问题。使用具有正确类型的本地指针很有帮助。我经常写这样的代码:

int foo(struct frog * f, int x, int y) {
struct leg * g = f->left_leg;
struct toe * t = g->big_toe;
process(t);

因此,如果我搞错了一个指针类型,编译器的错误就会非常清楚问题是什么。如果我这么做了:

int foo(struct frog * f, int x, int y) {
process(f->left_leg->big_toe);

如果有任何指针类型错误,编译器的错误就会更加难以发现。我会在沮丧的时候尝试改变错误,可能会让事情变得更糟。

在处理指针时,容易混淆的人通常分为两个阵营,我在这两个阵营都有过经历。

array[]的观众

这群人完全不知道如何将指针符号转换为数组符号(或者甚至不知道它们是相关的)。以下是访问数组元素的四种方法:

  1. 数组表示法(索引) 数组名
  2. 数组表示法(索引) 指针名称
  3. 指针符号(*) 指针名称
  4. 指针符号(*) 数组名

int vals[5] = {10, 20, 30, 40, 50};
int *ptr;
ptr = vals;


array       element            pointer
notation    number     vals    notation


vals[0]     0          10      *(ptr + 0)
ptr[0]                         *(vals + 0)


vals[1]     1          20      *(ptr + 1)
ptr[1]                         *(vals + 1)


vals[2]     2          30      *(ptr + 2)
ptr[2]                         *(vals + 2)


vals[3]     3          40      *(ptr + 3)
ptr[3]                         *(vals + 3)


vals[4]     4          50      *(ptr + 4)
ptr[4]                         *(vals + 4)

这里的想法是通过指针 看起来访问数组非常简单和直接,但是可以通过这种方式完成大量非常复杂和聪明的事情。其中一些会让有经验的 C/C + + 程序员感到困惑,更不用说没有经验的新手了。

reference to a pointerpointer to a pointer的观众

这篇 是一篇很好的文章,它解释了两者之间的区别,我将从中引用并窃取一些代码:)

举个小例子,如果你遇到这样的事情,你很难看清楚作者到底想做什么:

//function prototype
void func(int*& rpInt); // I mean, seriously, int*& ??


int main()
{
int nvar=2;
int* pvar=&nvar;
func(pvar);
....
return 0;
}

或者,在一定程度上,像这样的东西:

//function prototype
void func(int** ppInt);


int main()
{
int nvar=2;
int* pvar=&nvar;
func(&pvar);
....
return 0;
}

所以说到底,我们用这些胡言乱语能解决什么问题? 什么都解决不了。

现在我们已经看到了 Ptr-to-ptr 和 ref-to-ptr 有什么好处吗? 恐怕不行。一个词的用法 对于一些程序员来说,两者都是公正的 个人偏好 Ref-to-ptr 表示语法是“ clean” 而一些使用 ptr-to-ptr 的人说 Ptr-to-ptr 语法使得 那些正在阅读你正在做的事情的人。

这种复杂性以及 看起来(看起来很大胆)与引用的可互换性(这通常是指针的另一个警告和新手的错误)使得理解指针变得困难。为了完成起见,理解在 C 和 C + + 中指向引用的指针是非法的也很重要,因为这些混乱的原因使您进入 lvalue-rvalue语义。

正如前面的答案所说,很多时候你会遇到这样的程序员,他们认为使用 ******awesome_var->lol_im_so_clever()很聪明,我们中的大多数人可能有时会犯下这样的错误,但这不是好的代码,当然也不可维护。

这个答案比我想象的要长。

从前... 我们有8位微处理器,每个人都用汇编语言写作。大多数处理器包括用于跳转表和内核的某种类型的间接寻址。当高级语言出现时,我们添加了一个抽象的薄层,称之为指针。这些年来,我们越来越远离硬件。这未必是件坏事。它们被称为高级语言是有原因的。我越能把注意力集中在我想做的事情上,而不是关注如何去做的细节,这样做就越好。

我有我的“指针时刻”工作在 C 语言的一些电话程序,我必须写一个 AXE10交换模拟器使用一个协议分析器,只能理解经典的 C 一切都取决于知道指针。我试着不用它们来编写代码(嘿,我是“预指针”,放我一马) ,但是彻底失败了。

对我来说,理解它们的关键是 & (地址)运算符。一旦我理解了 &i的意思是“ i 的地址”,那么理解 *i的意思是“由 i 指向的地址的内容”就晚了一点。每当我写或读我的代码时,我总是重复“ &”和“ *”的意思,最终我开始凭直觉使用它们。

令我感到羞愧的是,我被迫使用 VB,然后是 Java,所以我的指针知识不像以前那么敏锐了,但我很高兴我是“后指针”。但是,不要要求我使用需要理解 * * p 的库。

指针是处理对象句柄和对象本身之间差异的一种方法。(好吧,不一定是反对,但你知道我的意思,也知道我的想法)

在某些时候,您可能不得不处理这两者之间的差异。在现代的高级语言中,这成为按价值复制和按引用复制之间的区别。不管怎样,这是一个程序员经常难以理解的概念。

然而,正如已经指出的,用 C 处理这个问题的语法是丑陋的、不一致的和令人困惑的。最终,如果您真的尝试去理解它,那么一个指针将是有意义的。但是,当你开始处理指针到指针等令人作呕的东西时,我和其他人都会感到非常困惑。

关于指针,另一件需要记住的重要事情是,它们是 很危险。C,是一门大师级程序员的语言。它假设你知道自己在做什么,从而赋予你把事情搞砸的力量。虽然有些类型的程序仍然需要用 C 语言编写,但大多数程序都不需要,如果您有一种语言能够更好地抽象对象及其句柄之间的差异,那么我建议您使用它。

事实上,在许多现代 C + + 应用程序中,通常任何所需的指针算法都是封装和抽象的。我们不希望开发人员到处做指针算法。我们需要一个集中的、经过良好测试的 API,它可以在最底层进行指针算法。对此代码进行更改必须非常小心,并进行大量测试。

以下是一个没有答案的问题: 使用 cdecl (或 c + + decl)来解决这个问题:

eisbaw@leno:~$ cdecl explain 'int (*(*foo)(const void *))[3]'
declare foo as pointer to function (pointer to const void) returning pointer to array 3 of int

我用 c + + 编程了大概2年,然后转换成 Java (5年) ,再也没有回头。然而,当我最近不得不使用一些原生的东西时,我惊奇地发现我并没有忘记关于指针的任何东西,我甚至发现它们很容易使用。这与我7年前第一次尝试理解这个概念时的经历形成了鲜明的对比。所以,我猜理解和喜欢是一个编程成熟度的问题?:)

或者

指针就像骑自行车一样,一旦你知道如何使用它们,就永远不会忘记。

总而言之,不管是否难以理解,整个指针思想是非常有教育意义的,我相信它应该被每个程序员所理解,无论他是否使用指针编程语言。

下面是一个指针/数组的例子,假设你有两个数组:

uint8_t source[16] = { /* some initialization values here */ };
uint8_t destination[16];

您的目标是使用 memcpy ()从源目的地复制 uint8 _ t 内容。猜猜下列哪一项能够实现这一目标:

memcpy(destination, source, sizeof(source));
memcpy(&destination, source, sizeof(source));
memcpy(&destination[0], source, sizeof(source));
memcpy(destination, &source, sizeof(source));
memcpy(&destination, &source, sizeof(source));
memcpy(&destination[0], &source, sizeof(source));
memcpy(destination, &source[0], sizeof(source));
memcpy(&destination, &source[0], sizeof(source));
memcpy(&destination[0], &source[0], sizeof(source));

答案(剧透警告!)就是他们所有人。“目的地”、“和目的地”和“和目的地[0]”都是相同的值与其他两个 类型不同,但仍然是相同的值。“源”的排列也是如此。

顺便说一句,我个人比较喜欢第一个版本。

如果你没有扎根于“深层”的知识,那么大多数事情都很难理解。当我教计算机科学的时候,当我开始给我的学生编写一个非常简单的“机器”,一个模拟的十进制计算机和十进制操作码,它的内存由十进制寄存器和十进制地址组成时,事情变得容易多了。他们会加入一些非常简短的程序,例如,加上一系列的数字来得到总数。然后他们会一步一步地观察发生了什么。他们可以按住“ Enter”键,看着它“快速”运行。

我相信几乎每个人都在思考为什么这么基本的东西是有用的。我们忘记了不知道如何编程是什么感觉。玩这样一台玩具电脑,就会产生一些你无法编程的概念,比如计算是一个循序渐进的过程,使用少量的基本原语来构建程序,内存变量的概念是存储数字的地方,变量的地址或名称与它所包含的数字是不同的。输入程序的时间和程序“运行”的时间是有区别的。我把学习编程比作跨越一系列“障碍”,比如非常简单的程序,然后循环和子例程,然后数组,然后顺序 I/O,然后指针和数据结构。所有这些都更容易通过参考计算机在下面的实际操作来学习。

最后,当进入 C 语言时,指针是令人困惑的,尽管 K & R 做了很好的解释。我用 C 语言学习它们的方法是知道如何读它们——从右到左。就像当我在脑海中看到 int *p时,我会说“ p指向 int”。C 是从汇编语言发明出来的,这就是我喜欢它的地方——它接近于“地面”。如果没有这种基础,指针就像其他任何东西一样更难理解。

他们给代码增加了额外的维度,而没有对语法进行重大改变:

int a;
a = 5

只有一件事需要改变: a。你可以编写 a = 6,结果对大多数人来说是显而易见的。但现在想想:

int *a;
a = &some_int;

关于 a有两点在不同的时候是相关的: a的实际值、指针和指针后面的值。你可以改变 a:

a = &some_other_int;

... some_int仍然在某个值相同的地方。但是你也可以改变它指向的东西:

*a = 6;

a = 6*a = 6之间有一个概念上的差距,a = 6只有局部的副作用,而 *a = 6可能会影响其他地方的许多其他东西。我的观点是 没有,间接的概念本质上是棘手的,但是因为你可以用 a都有的直接的、局部的事情,或者用 *a做间接的事情... ... 这可能会使人们感到困惑。

就我个人而言,甚至在我毕业后和第一份工作之后,我都不理解这个指针。我唯一知道的是,你需要它的链表,二进制树和传递数组到函数。我第一份工作的时候就是这种情况。只有当我开始接受采访时,我才明白指针的概念是深刻的,有着巨大的用途和潜力。然后我开始阅读 K & R 并编写自己的测试程序。我的全部目标都是为了工作。
在这个时候,我发现如果教授的方法很好,那么指针真的不错,也不难。不幸的是,当我在毕业典礼上学 C 的时候,我的老师并没有意识到指针的重要性,甚至作业中指针的使用也很少。在研究生级别中,指针的使用实际上仅限于创建二叉树和链表。这种认为不需要正确理解指针就可以使用它们的想法扼杀了学习它们的想法。

指示。.哈。.我脑海中的关于指针的全部内容就是它给出了一个内存地址,在这个地址中,不管它的引用是什么,它的实际值。.所以没有魔法。.如果您学习了一些程序集,那么学习指针如何工作就不会有那么多麻烦。.拜托,伙计们... 即使在爪哇,一切都是参考。.

似乎很多学生对间接概念都有疑问,特别是当他们第一次遇到间接概念的时候。我记得当我还是一个学生的时候,在我的课程的 + 100学生中,只有少数人真正懂得指点。

间接概念并不是我们在现实生活中经常使用的概念,因此一开始很难理解。

我最近刚刚经历了点击鼠标的时刻,我惊讶地发现我一直感到困惑。更多的是因为每个人都在谈论这件事,以至于我以为这里面有什么黑魔法。

我得到它的方式是这样的。假设所有定义的变量在编译时(在堆栈上)都被给予了内存空间。如果您想要一个能够处理大型数据文件(如音频或图像)的程序,则不需要为这些可能的结构设置固定数量的内存。因此,要等到运行时才分配一定数量的内存来保存这些数据(在堆上)。

一旦在内存中存储了数据,您就不会希望每次在内存总线上运行操作时都要复制整个内存总线上的数据。假设您想对图像数据应用一个过滤器。您有一个指针,它从分配给图像的数据的前面开始,然后一个函数在该数据上运行,并在适当的位置对其进行更改。如果您不知道我们正在做什么,您可能最终会在运行操作时复制数据。

至少我现在是这么想的!

我认为 C 指针之所以困难是因为它们混合了几个并不真正等价的概念; 然而,因为它们都是使用指针实现的,人们很难理清这些概念。

在 C 语言中,指针被用来,包括其他的东西:

  • 定义递归数据结构

在 C 语言中,你可以像下面这样定义一个整数链表:

struct node {
int value;
struct node* next;
}

指针之所以存在,是因为这是在 C 语言中定义递归数据结构的唯一方法,而这个概念实际上与内存地址这样的底层细节没有任何关系。考虑下面的哈斯克尔版本,它不需要使用指针:

data List = List Int List | Null

非常简单-列表要么为空,要么由值和列表的其余部分组成。

  • 遍历字符串和数组

下面是如何将函数 foo应用于 C 语言中字符串的每个字符:

char *c;
for (c = "hello, world!"; *c != '\0'; c++) { foo(c); }

尽管也使用指针作为迭代器,但是这个例子与前一个例子没有什么共同之处。创建可以递增的迭代器与定义递归数据结构是不同的概念。这两个概念都没有特别与内存地址的概念联系起来。

  • 实现多态性

下面是在 油嘴滑舌中找到的一个实际的函数签名:

typedef struct g_list GList;


void  g_list_foreach    (GList *list,
void (*func)(void *data, void *user_data),
void* user_data);

哇!这是相当满口的 void*的。这只是为了声明一个函数,该函数遍历一个可以包含任何类型内容的列表,并向每个成员应用一个函数。比较一下哈斯克尔的 map:

map::(a->b)->[a]->[b]

这就简单多了: map是一个函数,它接受一个将 a转换为 b的函数,并将其应用于一个 a的列表以产生一个 b的列表。就像在 C 函数 g_list_foreach中一样,map不需要在它自己的定义中知道它将被应用到的类型。

总而言之:

我认为,如果人们首先把递归数据结构、迭代器、多态性等作为单独的概念来学习,然后再学习 如何使用指针在 C 语言中实现这些想法,而不是把所有这些概念混合到一个单独的“指针”主题中,那么 C 指针就不会那么令人困惑了。

作为一个 C + + 的新手,我想说:

我花了一段时间才理解指针系统,不一定是因为这个概念,而是因为相对于 Java 的 C + + 语法。让我感到困惑的是:

(1)变量声明:

A a(1);

对。

A a = A(1);

对。

A* a = new A(1);

显然

A a();

是函数声明而不是变量声明。在其他语言中,基本上只有一种声明变量的方法。

(2)与号有几种不同的用法

int* i = &a;

那么 & a 就是内存地址。

OTOH,如果是的话

void f(int &a) {}

那么 & a 是一个通过引用传递的参数。

尽管这可能看起来微不足道,但是对于新用户来说可能会很困惑——我来自 Java,而 Java 是一种使用运算符更加统一的语言

(数组-指针关系

有一件事让人有点沮丧,那就是一个指针

int* i

可以是指向 int 的指针

int *i = &n; //

或者

可以是整型数组的数组

int* i = new int[5];

为了让事情更复杂,指针和数组在任何情况下都不能互换,指针也不能作为数组参数传递。

这总结了我对 C/C + + 及其指针的一些基本不满,而事实上,C/C + + 具有所有这些特定于语言的怪癖。

人们不明白的主要问题是为什么他们需要指针。 因为它们不清楚堆栈和堆。 最好从16位汇编程序开始,用于小型内存模式的 x86。它帮助许多人了解堆栈、堆和“地址”的概念。和 byte:)现代程序员有时不能告诉你需要多少字节才能处理32位空间。他们怎样才能得到指针的概念?

第二个瞬间是符号: 你声明指针为 * ,你得到地址为 & ,这对于某些人来说是不容易理解的。

我最后看到的是存储问题: 他们了解堆和堆栈,但不能理解“静态”的概念。