人们对C指针有什么困难?[关闭]


172

从这里发布的问题数量来看,很明显,人们在使用指针和指针算术时会遇到一些根本性的问题。

我很好奇为什么。它们从未真正给我造成重大问题(尽管我是在新石器时代第一次了解它们的)。为了对这些问题写出更好的答案,我想知道人们发现困难之处。

因此,如果您在使用指针时遇到困难,或者您最近却突然“明白了”,那么指针的哪些方面导致了您的问题?


54
刻板的,夸张的,不必要的争议,用事实的真相来回答:他们被残废了-我是告诉我的-致残的-我先学会了其中一种“表达性”的高级语言。他们应该从对丹尼尔·布恩(Daniel Boone)想要的上帝这样的裸机编程开始!
dmckee ---前主持人小猫,2010年

3
...并且程序员会更好,因为尽管您尽了最大努力,它也会发展成为讨论。
dmckee ---前主持人小猫,2010年

这是论据和主观的,它产生N个结果,我的困难是您的容易。它可能适用于程序员,但由于该站点尚未超出Beta版,因此我们尚未将这些问题迁移到那里。
山姆·萨弗隆

2
@Sam Saffron:虽然我通常都同意这更多是一个程序员。SE类型的问题,但老实说,如果人们愿意将“我认为这很容易”和“我讨厌看到指针”标记为垃圾邮件也不错他们是。
jkerian

3
必须有人提出:“就像手指指向月球一样。不要专心于手指,否则您会错过所有天堂般的荣耀。”-布鲁斯·李
亩太短了2010年

Answers:


86

我怀疑人们的回答太过深入了。真正不需要了解调度,实际的CPU操作或程序集级内存管理。

在教学过程中,我发现学生理解中的以下漏洞是最常见的问题来源:

  1. 堆与堆栈存储。令人震惊的是,即使从一般意义上讲,有多少人不了解这一点。
  2. 堆叠框架。只是用于局部变量的堆栈专用部分的一般概念,以及它是“堆栈”的原因...可以安全地保留诸如存放返回位置,异常处理程序详细信息和先前寄存器之类的详细信息,直到有人尝试建立一个编译器。
  3. “内存就是内存就是内存”强制转换只更改操作符的版本或编译器为特定内存块提供的空间。您知道当人们谈论“什么(原始)变量X 真正是” 时,您正在处理此问题。

我的大多数学生都能理解内存的简化图,通常是当前作用域中堆栈的局部变量部分。通常,给各个位置明确的虚构地址很有帮助。

总而言之,我想说的是,如果您想了解指针,则必须了解变量及其在现代体系结构中的实际含义。


13
恕我直言,了解堆栈和堆与低级CPU详细信息一样没有必要。堆栈和堆恰好是实现细节。ISO C规范没有提到“堆栈”一词,K&R也没有。
sigjuice 2010年

4
@sigjuice:您的异议遗漏了问题和答案的重点。A)K&R C是过时的B)ISO C不是唯一的带有指针的语言,我的观点1和2是针对基于非C的语言开发的C)95%的体系结构(非语言)都使用堆/ stack系统,这是很常见的,相对于它解释了异常。D)问题的重点是“为什么人们不理解指针”,而不是“我如何解释ISO C”
jkerian 2010年

9
@John Marchetti:更是如此……考虑到问题是“人们对指针的问题的根源是什么”,我认为问指针相关问题的人们不会对“ You do n't”印象深刻。真的需要知道”作为答案。显然,他们不同意。:)
jkerian 2010年

3
@jkerian这可能已经过时了,但是解释指针的K&R的三到四页不需要这样做的细节。出于各种原因,对实现细节的了解是有用的,但是恕我直言,它不应该是理解语言的关键构造的先决条件。
sigjuice 2010年

3
“通常给各个地点明确的虚构地址有帮助。” -> +1
fredoverflow

146

当我第一次与他们合作时,我遇到的最大问题是语法。

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);

调用它,我需要一个指向int指针的指针数组的地址:

foo(&(***ipppArr));

在六个月内,当我必须维护此代码时,我将花费更多的时间来尝试找出所有这些含义,而不是从头开始进行重写。(是的,可能语法有误-自从我用C做任何事以来已经有一段时间了。我有点想念它,但是后来我有点受虐狂了)


21
您对第一个的评论>>>> ip = 4; //将ip的值设置为'4'<<是错误的。应该是>> //将ip
aaaa bbbb

8
在任何语言中,将太多类型叠加在一起都是一个坏主意。您可能会发现写“ foo(&(*** ipppArr));” 在C语言中很奇怪,但是写的东西像“ std :: map <std :: pair <int,int>,std :: pair <std :: vector <int>,std :: tuple <int,double,std :: list C ++中的<int >>>>”也非常复杂。这并不意味着C ++中的C或STL容器中的指针很复杂。这只是意味着您必须使用更好的类型定义,以使代码阅读者可以理解它。
帕特里克

20
我真心相信对语法的误解是投票最多的答案。那是关于指针最简单的部分。
杰森

4
即使阅读了这个答案,我也很想拿一张纸画画。在C语言中,我一直在画画。
迈克尔·复活节

19
@Jason除了大多数人认为困难之外,还有什么客观的困难衡量标准?
罗珀特·马登·阿博特

52

正确理解指针需要有关底层计算机体系结构的知识。

如今,许多程序员不知道他们的机器如何工作,就像大多数懂得如何驾驶汽车的人对引擎一无所知。


18
@dmckee:恩,我错了吗?多少Java程序员可以处理段错误?
罗伯特·哈维

5
分段错误与换挡有关吗?-Java程序员
Tom Anderson

6
@Robert:这是一个真正的补充。这是一个艰巨的话题,而且又不影响人们的感情。而且我的评论恐怕引发了我以为您设法避免的冲突。Mea cupla。
dmckee ---前主持人小猫,2010年

30
我不同意; 您无需了解底层架构即可获取指针(无论如何它们都是抽象的)。
杰森

11
@Jason:在C中,指针本质上是一个内存地址。如果不了解机器架构,就无法安全地与他们合作。参见en.wikipedia.org/wiki/Pointer_(computing)boredzo.org/pointers/#definition
罗伯特·哈维

42

在处理指针时,感到困惑的人广泛分布在两个阵营之一。我都去过(上午?)。

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的人说语法“更干净”,而一些使用ptr-to-ptr的人说ptr-to-ptr语法使阅读您正在做的事情的人更清楚。

这种复杂性以及与引用之间的表象(粗体表象)互换性(通常是指针的另一个警告和新手的错误)使理解指针变得困难。同样重要的是理解,完成的缘故,该指针引用是C和C ++非法为这样的困惑,带你进入的理由lvalue- rvalue语义。

如先前的回答所述,很多时候,您会遇到这些热衷于编程的程序员,他们认为他们通过使用是很聪明的,******awesome_var->lol_im_so_clever()而且我们大多数人有时都犯了这样的暴行罪名,但这只是不好的代码,而且肯定无法维护。

好吧,这个答案竟然比我希望的更长。


5
我认为您可能已经在这里为C问题提供了C ++答案……至少是第二部分。
2010年

指针的指针也适用于C::p
David Titarenco,2010年

1
?我仅在遍历数组时才看到指向指针的指针-您的第二个示例实际上不适用于大多数体面的C代码。同样,您会将C拖入C ++的混乱之中-C中的引用不存在。
new123456'1

在许多情况下,您都必须处理指向指针的指针。例如,当处理指向返回一个指针的函数的函数指针时。另一个示例:一个可以容纳可变数量其他结构的结构。还有更多...
David Titarenco

2
“引用的指针在C语言中是非法的”-更像是“缺少” :)
Kos 2012年

29

我个人指责参考资料的质量和从事教学的人员;C语言中的大多数概念(尤其是指针)都不好。我一直威胁要写自己的C书(《世界需要的最后一本书》是有关C编程语言的另一本书),但我没有时间或耐心这样做。因此,我在这里闲逛并向人们扔出标准的随机报价。

还有一个事实是,在最初设计C时,假设您只是相当详细地了解了机器体系结构,因为在日常工作中无法避免这种情况(内存太紧,处理器太慢了。您必须了解编写的内容如何影响性能)。


3
是。'int foo = 5; int * pfoo =&foo; 看看这有什么用?好的,继续前进...'在编写自己的双向链接列表库之前,我并没有真正使用过指针。
约翰·洛佩兹

2
+1。我曾经为CS100的学生提供辅导,所以许多问题只是通过以一种易于理解的方式遍历指针而得以解决。
benzado

1
+1为历史背景。在那之后很长时间开始,这对我来说从未发生过。
卢米

26

在Joel Spolsky的网站-JavaSchools的危险上,有一篇很棒的文章支持指针很难使用的观点

[免责声明-我本身不是Java仇恨者。]


2
@Jason-是的,但不会否定参数。
史蒂夫·汤森

4
Spolsky并不是说JavaSchools是人们发现指针困难的原因。他说,他们的结果是获得了计算机科学学位的文盲。
benzado

1
@benzado-公平的观点-如果我的简短帖子读为“一篇很好的文章,它支持指针很困难的观点,那将会得到改善”。文章暗示的是,“拥有'好学校'的CS学位”并不像过去的开发人员那样成功,它可以很好地预测成功,而“理解指针”(和递归)仍然如此。
史蒂夫·汤森

1
@史蒂夫·汤森(Steve Townsend):我认为您错过了斯波斯基先生的论点。
杰森

2
@Steve Townsend:Spolsky先生在争辩说Java学校正在培养一代不知道指针和递归的程序员,而不是因为Java学校的普遍性使得指针很难。正如您所说的“有一篇很棒的文章说明为什么这样做很难”并链接到所述文章一样,您似乎具有后一种解释。如果我错了,请原谅我。
杰森

24

如果您不了解“基础知识”,那么大多数事情将更难理解。当我教授CS时,让我的学生开始对一个非常简单的“机器”进行编程就变得容易得多,这是一台带有十进制操作码的模拟十进制计算机,其内存由十进制寄存器和十进制地址组成。他们将编写非常简短的程序,例如,添加一系列数字以获得总计。然后他们会一步一步观察发生的事情。他们可以按住“ enter”键,然后观察它“快速”运行。

我敢肯定,SO中的几乎每个人都想知道为什么变得如此基础很有用。我们忘记了不知道如何编程的感觉。玩这样的玩具计算机会放置一些您无法编程的概念,例如,计算是逐步的过程,使用少量基本原语来构建程序的概念以及内存的概念变量作为存储数字的位置,其中变量的地址或名称与其包含的数字不同。输入程序的时间与“运行”的时间之间存在区别。我喜欢学习编程,就像跨越一系列“减速带”一样,例如非常简单的程序,然后是循环和子例程,然后是数组,然后是顺序I / O,然后是指针和数据结构。

最后,当使用C时,尽管K&R在解释它们方面做得很好,但指针却令人困惑。我用C语言学习它们的方式是知道如何从右到左阅读它们。就像当我int *p在脑海中看到时一样,我说“ p指向int”。C的发明是从汇编语言发展而来的,这就是我所喜欢的-它接近那个“基础”。指针,像其他任何东西一样,如果没有这种基础,则更难理解。


1
了解这一点的一个好方法是对8位微控制器进行编程。他们很容易理解。拿Atmel AVR控制器;他们甚至得到了gcc的支持。
Xenu 2010年

@Xenu:我同意。对我来说是Intel 8008和8051 :-)
Mike Dunlavey 2010年

对我来说,那是一台在时间迷雾笼罩的麻省理工学院定制的8位计算机(“也许”)。
QuantumMechanic 2012年

迈克(Mike)–您应该给学生一个心脏:)
QuantumMechanic 2012年

1
@Quantum:CARDIAC,好人,没听说过。让我猜想,“也许”是当Sussman等人阅读Mead-Conway书并制造自己的LSI芯片时吗?那是我在那之后的时间。
Mike Dunlavey '04

17

在阅读K&R中的说明之前,我没有得到任何指示。直到那时,指针才有意义。我读了一堆书,人们说:“不要学指针,它们会令人困惑,会伤到你的头,给你动脉瘤”,所以我回避了很长时间,并创造了这种不必要的,难以理解的氛围。

否则,主要是我想的是,为什么在地球上您想要一个必须经过箍才能获得其值的变量,并且如果您想为其分配内容,就必须做一些奇怪的事情才能获得值进入他们。我认为,变量的全部意义在于可以存储值,所以为什么有人想要使其复杂化就超出了我的范围。 “所以使用指针必须使用*运算符来获取其值?那是什么样的愚蠢变量?” , 我想。没有意义,没有双关语。

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

一个类需要使用指针来访问/修改PC上的端口,使用指针算术来寻址不同的内存位置,并查看修改其参数的更复杂的C代码,这使我无法意识到指针毫无意义。


4
如果您的资源(RAM,ROM,CPU)有限(例如在嵌入式应用程序中),则指针将变得更加有意义。
尼克T

+1为尼克的评论-特别是对于传递结构。
new123456'1

12

这是一个让我停下来的指针/数组示例。假设您有两个数组:

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));

答案(“扰流板警报”!)全部都是。“ destination”,“&destination”和“&destination [0]”都是相同的值。“&destination”是与其他两个类型不同的类型,但其值仍然相同。“源”的排列也是如此。

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


我也更喜欢第一个版本(标点较少)。
sigjuice 2010年

++我也是,但是您确实需要注意sizeof(source),因为如果source是指针,sizeof它就不是您想要的。我有时(并非总是如此)sizeof(source[0]) * number_of_elements_of_source只是为了远离该错误而写。
Mike Dunlavey

destination,&destination,&destination [0]根本不相同-但是在memcpy中使用时,它们将通过不同的机制转换为相同的void *。但是,当用作sizeof的参数时,您将获得两个不同的结果,并且可能会有三个不同的结果。
gnasher729 2014年

我以为需要操作员的地址?
MarcusJ 2015年

7

首先,我要说C和C ++是我学到的第一门编程语言。我从C开始,然后在学校做了很多C ++,然后又回到C变得流利。

在学习C时,我对指针感到困惑的第一件事很简单:

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

这种混淆的根源主要是在将指针正确引入给我之前,已经引入了对OUT参数使用对变量的引用。我记得我跳过了用C for Dummies编写的前几个示例,因为它们太简单了,以至于无法使我编写的第一个程序正常工作(很可能是因为这样)。

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

熟悉之后,我接下来会记得对动态分配感到困惑。我意识到,在某种程度上,没有动态分配某种类型的数据指针并不是很有用,所以我写了类似以下内容:

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

尝试动态分配一些空间。没用 我不确定它是否会工作,但我不知道它还能如何工作。

后来我了解了mallocnew,但是对我来说,它们确实像是神奇的内存生成器。我对它们如何工作一无所知。

一段时间后,我又被教递归(以前我自己学过递归,但是现在在上课),我问它在幕后如何工作-单独的变量存储在哪里。我的教授说“在栈上”,我明白了很多事情。我以前听过这个词,并且以前已经实现过软件堆栈。我很早以前就听说过其他人提到“堆栈”,但是却忘记了。

大约在这个时候,我还意识到在C语言中使用多维数组会变得非常混乱。我知道它们是如何工作的,但是它们很容易纠结在一起,以至于我决定尽一切可能尝试使用它们。我认为这里的问题主要是语法上的(尤其是传递给函数或从函数返回)。

自从我在接下来的一两年里为学校编写C ++以来,我在使用数据结构指针方面有很多经验。在这里,我遇到了一系列的麻烦-混淆了指针。我会有多个级别的指针(例如node ***ptr;)触发我。我会错误地多次引用指针,并最终*通过反复试验找出我需要多少指针。

在某个时候,我了解了程序堆是如何工作的(某种程度,但足够好,它不再让我彻夜难眠)。我记得读过一篇文章,如果您malloc在某个系统上的指针返回之前查看几个字节,就可以看到实际分配了多少数据。我意识到其中的代码malloc可能会要求操作系统提供更多内存,而该内存不是我的可执行文件的一部分。对工作方式有一个体面的工作想法malloc非常有用。

此后不久,我参加了一个汇编课程,该课程并没有像大多数程序员想象的那样教给我太多关于指针的知识。它的确使我更加思考可以将我的代码转换为哪种程序集。我一直试图编写高效的代码,但是现在我有了一个更好的主意。

我还参加了几节课,不得不写一些Lisp。在编写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);

并且在那里存在任何指针类型错误,那么编译器错误将很难解决。我很容易在挫折中诉诸于试错法,并可能使情况变得更糟。


1
+1。彻底而有见地。我已经忘记了scanf,但是现在您提出来了,我记得也有同样的困惑。
乔·怀特2010年

6

回顾过去,有四件事确实帮助我终于了解了指针。在此之前,我可以使用它们,但是我还没有完全理解它们。就是说,我知道如果我遵循这些表格,将会得到想要的结果,但是我并没有完全理解表格的“原因”。我意识到这并不是您所要的,但我认为这是有用的推论。

  1. 编写一个使用指向整数的指针并修改该整数的例程。这给了我必要的形式,可以在此基础上建立关于指针如何工作的任何心理模型。

  2. 一维动态内存分配。弄清一维内存分配使我了解了指针的概念。

  3. 二维动态内存分配。弄清楚二维内存分配强化了这一概念,但同时也告诉我指针本身需要存储,必须将其考虑在内。

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

这些项目中的每一个都需要想象较低层次上发生的事情-建立一个满足我可能想到的所有情况的心理模型。这花费了时间和精力,但这是值得的。我坚信要理解指针,您必须建立关于它们如何工作以及如何实现的思维模型。

现在回到您的原始问题。根据先前的清单,我最初很难掌握一些项目。

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

嘿,您能指出我的文章/书籍/绘画/涂鸦/我能以您在答案中描述的类似方式学习的任何东西吗?我坚信,这是学习良好的方法,基本上是任何东西。深刻的理解和良好的心理模型
亚历山大·星巴克

1
@AlexStarbuck-我的意思不是听起来很轻松,但是科学的方法是一个很好的工具。画一张自己的图画,说明在特定情况下可能发生的情况。编写一些程序对其进行测试,然后分析得到的结果。它符合您的期望吗?如果没有,请找出不同之处?根据需要重复进行,逐渐增加复杂性以测试您的理解力和心理模型。
Sparky

6

我在C语言中的某些电话程序上使用了“指针时刻”。我必须使用仅能理解经典C语言的协议分析器来编写AXE10交换仿真器。一切都取决于指针。我尝试在没有它们的情况下编写代码(嘿,我是“预指点”,这使我有些懈怠)并完全失败了。

对我来说,理解它们的关键是&(地址)运算符。一旦我理解&i了“ i的地址”,然后理解了*i“ i所指向的地址的内容”。每当我编写或阅读我的代码时,我总是会重复“&”的含义和“ *”的含义,最终我会直观地使用它们。

令我感到羞耻的是,我被迫转入VB,然后再转入Java,因此我的指针知识不如以前那么深刻,但我很高兴自己是“后指针”。但是,不要要求我使用需要我理解* * p 的库。


如果&i是地址,*i是内容,那是i什么?
Thomas Ahle 2010年

2
我有点超载我的用法。对于任意变量i,&i表示“ i”的“地址”,i本身表示“ i的内容”,而* i表示“将i的内容作为地址处理,转到该地址,然后传回内容”。
加里·罗

5

至少对于我而言,使用指针的主要困难是,我不是从C开始的。我是从Java开始的。指针的整个概念真的很陌生,直到我在大学里学习了几门课程后,我才知道C。因此,我自学了C的基本知识以及如何在其最基本的意义上使用指针。即使这样,每次我发现自己在阅读C代码时,也必须查找指针语法。

因此,根据我非常有限的经验(1年真实世界+ 4年大学经验),指针使我感到困惑,因为除了教室环境外,我从来没有真正使用过它。我可以同情现在开始使用JAVA而不是C或C ++的CS的学生。如您所说,您学习了“新石器时代”的指针,并且从那以后可能就一直在使用它。对于我们新手来说,分配内存和执行指针算术的概念确实很陌生,因为所有这些语言都将其抽象化了。

PS在阅读了Spolsky的文章之后,他对“ JavaSchools”的描述与我在康奈尔大学('05 -'09)经历的情况完全不同。我学习了结构和函数式编程(sml),操作系统(C),算法(笔和纸),以及其他很多用Java讲不到的其他类。但是,所有介绍性类和选修课都在Java中完成,因为当您尝试执行比使用指针实现哈希表更高级别的操作时,不重新发明轮子很有用。


4
坦白说,鉴于您仍然无法使用指针,因此我不确定您在康奈尔大学的经历是否与Joel的文章相抵触。显然,您的大脑中有足够多的人陷入了Java思维定势中来提出自己的观点。
jkerian

5
?Java(或C#,Python或其他数十种语言)的引用只是没有算术运算符的指针。理解指针意味着理解为什么void foo(Clazz obj) { obj = new Clazz(); }void bar(Clazz obj) { obj.quux = new Quux(); }对参数进行突变时为何不进行操作……

1
我知道Java中有什么引用,但是我只是说,如果您要我用Java进行反射或用CI编写有意义的内容,就不能只是简单地将其淘汰。它需要大量的研究,就像每次都第一次学习它一样。
shoebox639

1
您如何通过C的操作系统类而又不熟练使用C?无意冒犯,只是我记得必须从头开始开发一个简单的操作系统。我一定已经使用指针一千次了……
Gravity

5

这是一个无法解答的问题:使用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

4

它们为代码增加了一个额外的维度,而语法没有重大变化。考虑一下:

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或间接的事情*a......这可能是什么人混淆。


4

我已经用c ++编程了2年,然后又转换为Java(5年),再也没有回头。但是,当我最近不得不使用一些本机的东西时,我惊奇地发现我没有忘记关于指针的任何知识,甚至发现它们易于使用。这与我7年前首次尝试掌握该概念时所经历的形成鲜明对比。因此,我想理解和喜欢是编程成熟度的问题吗?:)

要么

指针就像骑自行车一样,一旦您弄清楚如何使用它们,就不会忘记它。

总而言之,很难理解或不理解,整个指针的概念非常有教育意义,我相信每个程序员都应该理解它,无论他是否使用带有指针的语言进行编程。


3

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


“据说计算机科学中不存在不能通过更高级别的间接解决方案解决的问题”(尽管不知道是谁先说了这一点)
原型保罗,2010年

就像魔术一样,在这里,误导是使人感到困惑的东西(但那真是太了不起了)
Nick T


3

指针是一种处理对象句柄和对象本身之间差异的方法。(好的,不一定是对象,但是您知道我的意思,以及我的想法在哪里)

在某些时候,您可能必须处理两者之间的差异。在现代的高级语言中,这成为按值复制和按引用复制之间的区别。无论哪种方式,这都是程序员通常难以理解的概念。

但是,正如已经指出的那样,用C语言处理此问题的语法很丑陋,前后不一致且令人困惑。最终,如果您真正尝试理解它,那么指针将是有意义的。但是,当您开始处理指向指针的指针之类的东西时,诸如此类的恶作剧,无论是对我还是对其他人而言,这都确实使我感到困惑。

关于指针要记住的另一点重要是指针很危险。C是一种高级程序员的语言。它假设您知道自己在做什么,从而使您能够真正搞砸事情。尽管某些类型的程序仍需要用C编写,但大多数程序不需要,如果您使用的语言可以更好地抽象对象及其句柄之间的区别,那么建议您使用它。

实际上,在许多现代C ++应用程序中,通常都需要封装和抽象任何所需的指针算法。我们不希望开发人员到处都进行指针算术运算。我们需要一个经过良好测试的集中式API,该API在最低级别上执行指针算术。对此代码进行更改必须非常小心并进行大量测试。


3

我认为C指针很困难的一个原因是,它们混合了实际上并不等效的几个概念。但是,由于它们都是使用指针实现的,因此人们很难理解这些概念。

在C语言中,指针用于其他方面:

  • 定义递归数据结构

在C语言中,您将定义一个整数链接列表,如下所示:

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

指针之所以存在,是因为这是在C中定义递归数据结构的唯一方法,而该概念实际上与诸如存储器地址之类的低级细节无关。考虑一下Haskell中的以下等效项,它不需要使用指针:

data List = List Int List | Null

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

  • 遍历字符串和数组

这是将函数foo应用于C中字符串的每个字符的方法:

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

尽管还使用了指针作为迭代器,但此示例与上一个示例几乎没有共同之处。创建可以递增的迭代器与定义递归数据结构是不同的概念。两种概念都与内存地址的概念无关。

  • 实现多态

这是在glib中找到的实际函数签名:

typedef struct g_list GList;

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

哇!相当大void*的。只是声明一个在列表中迭代的函数,该列表可以包含任何种类的东西,并将函数应用于每个成员。将其与mapHaskell中的声明进行比较:

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

那要简单得多:这map是一个函数,该函数采用将an转换ab,并将其应用于的列表a以产生的列表的函数b。就像在C函数中一样g_list_foreachmap不需要在其自己的定义中了解将要应用的类型的任何信息。

总结一下:

我认为,如果人们首先了解递归数据结构,迭代器,多态性等作为单独的概念,然后学习如何使用指针在C中实现这些思想,而不是将所有这些都混为一谈,那么C指针将减少很多混乱。概念一起成为“指针”的单个主题。


c != NULL在您的“ Hello world”示例中滥用的 意思是…… *c != '\0'
Olaf Seibert 2015年

2

我认为,这可能需要扎实的基础,可能是从机器级别开始的,还要介绍一些机器代码,汇编以及如何在RAM中表示项目和数据结构。这需要一些时间,需要一些家庭作业或解决问题的实践,还需要一些思考。

但是,如果一个人起初知道高级语言(这没错,木匠用斧头。一个需要分裂原子的人则使用其他东西。我们需要木匠的人,而我们有研究原子的人)和这位熟悉高级语言的人将获得2分钟的指针介绍,然后很难指望他理解指针算术,指针指针,可变大小字符串指针数组和字符数组数组等。坚实的基础可以帮助很多。


2
不断增长的指针不需要了解机器代码或汇编。
杰森

要求,不行。但是,了解汇编的人可能会发现指针要容易得多,因为他们已经完成了大多数(如果不是全部)必要的心理联系。
cHao 2010年

2

我一直遇到的问题(主要是自学成才)是使用指针的“何时”。我可以将注意力集中在构造指针的语法上,但是我需要知道在哪种情况下应该使用指针。

我是唯一有这种想法的人吗?;-)


我明白了。我的回应有点关于这一点。
J. Polfer

2

从前...我们有8位微处理器,每个人都用汇编语言编写。大多数处理器都包含用于跳转表和内核的某种类型的间接寻址。当高级语言出现时,我们添加了一层薄薄的抽象并将其称为指针。多年来,我们与硬件的距离越来越远。这不一定是一件坏事。由于某种原因,它们被称为高级语言。我越专注于自己想做的事情,而不是专注于如何做的细节,那就越好。


2

似乎许多学生对间接概念有疑问,尤其是当他们第一次遇到间接概念时。我记得当我还是一个学生的时候,在我的课程的+100名学生中,只有少数人真正了解指针。

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


2

最近,我刚好有鼠标点击的瞬间,而令我感到困惑的是我感到惊讶。所有人都这么多地谈论它,以至于我以为某种黑暗的魔法正在发生。

我得到的方式就是这样。想象一下,所有定义的变量在编译时(在堆栈上)都被赋予了内存空间。如果您想要一个可以处理诸如音频或图像之类的大数据文件的程序,则不需要为这些潜在结构提供固定数量的内存。因此,您要等到运行时分配一定数量的内存来保存此数据(在堆上)。

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

至少我现在就是这样看的!


事后想想,您可以定义固定数量的内存来保存图像/音频/视频,例如在内存受限的设备上,但是随后您将不得不处理某种流进出内存解决方案。
克里斯·巴里

1

在这里以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是一种语言,使用的运算符更加统一

(3)数组指针关系

让人有点沮丧的一件事是指针

int* i

可以是一个指向int的指针

int *i = &n; // 

要么

可以是一个整数数组

int* i = new int[5];

然后,为了使事情变得更混乱,指针和数组在所有情况下都不能互换,并且指针不能作为数组参数传递。

这总结了我对C / C ++及其指针的一些基本挫败感,而IMO则由于C / C ++具有所有这些特定于语言的怪癖而变得更加复杂。


就变量声明而言,C ++ 2011进行了很多改进。
gnasher729

0

我个人甚至在毕业后和第一份工作后都不理解指针。我唯一知道的是,您需要它来用于链表,二进制树以及将数组传递给函数。即使是我的第一份工作,情况就是如此。只有当我开始接受采访时,我才知道指针概念是深刻的,具有巨大的用途和潜力。然后,我开始阅读K&R并编写自己的测试程序。我的整个目标是工作驱动的。
这时我发现,如果对指针进行了很好的学习,它们的确不是不好也不困难。不幸的是,当我在毕业时学习C语言时,外出的老师并不了解指针,甚至作业也很少使用指针。在研究生级别,指针的使用实际上只能创建二进制树和链接列表。这种以为您不需要对指针进行适当理解就可以使用它们,这扼杀了学习它们的想法。


0

指针..哈哈..我脑海中所有关于指针的地方就是它提供了一个内存地址,该内存地址是其引用的实际值。指针是如何工作的..伙计们...即使在Java中,所有东西都是参考。


0

人们不理解的主要问题是为什么他们需要指针。因为他们不清楚堆栈和堆。最好从具有微型内存模式的x86的16位汇编器开始。它帮助许多人了解堆栈,堆和“地址”的概念。字节:)现代程序员有时无法告诉您寻址32位空间所需的字节数。他们如何获得指针的想法?

第二点是表示法:将指针声明为*,将地址声明为&,这对于某些人来说并不容易理解。

我看到的最后一件事是存储问题:他们了解堆和堆栈,但不能陷入“静态”的想法。

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.