What do people find difficult about C pointers?
从这里发布的问题的数量来看,很明显人们在获得指针和指针算术的时候有一些相当基础的问题。
我很好奇为什么。他们从来没有真正给我带来大问题(尽管我在新石器时代就开始了解他们)。为了更好地回答这些问题,我想知道人们觉得困难的地方。
所以,如果你正在与指针作斗争,或者你最近只是突然"明白了",那么指针的哪些方面导致了你的问题?
当我第一次开始使用它们时,我遇到的最大问题是语法。
1 2 3 | int* ip; int * ip; int *ip; |
都是一样的。
但是:
1 2 | int* ip1, ip2; //second one isn't a pointer! int *ip1, *ip2; |
为什么?因为声明的"指针"部分属于变量,而不是类型。
然后用一个非常相似的符号来取消对事物的引用:
1 2 3 | *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' |
除非你真的需要一个指针…然后你用一个和号!
1 | int *ip = &x; |
万岁的一致性!
然后,显然只是为了证明自己有多聪明,很多库开发人员使用指向指向指针的指针,如果他们期望这些东西的数组,那么为什么不也传递一个指针呢?
1 | void foo(****ipppArr); |
要调用此函数,我需要指向指向Ints指针的指针数组的地址:
1 | foo(&(***ipppArr)); |
在六个月内,当我必须维护这段代码时,我将花费更多的时间来理解这一切的含义,而不是从头开始重写。(是的,可能是语法错误——我已经很久没有用C语言做过任何事情了。我有点怀念它了,但后来我成了一个按摩师)
我怀疑人们的回答有点太深奥了。不需要了解调度、实际的CPU操作或程序集级内存管理。
当我在教学时,我发现学生理解中的以下漏洞是最常见的问题来源:
我的大多数学生都能理解一块内存的简化绘图,通常是当前范围内堆栈的局部变量部分。一般来说,向不同地点提供明确的虚拟地址会有所帮助。
总之,我想我是说,如果你想理解指针,你必须理解变量,以及它们在现代体系结构中的实际情况。
正确理解指针需要了解底层机器的体系结构。
如今,许多程序员不知道他们的机器是如何工作的,就像大多数懂得开车的人对发动机一无所知一样。
在处理指针问题时,困惑的人通常处于两个阵营中的一个。我以前(是吗?)两者都有。
这就是那些直接向上不知道如何将指针表示法转换为数组表示法(或者甚至不知道它们是相关的)的人群。以下是访问数组元素的四种方法:
nbsp;
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 | 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++程序员感到困惑,更不用说没有经验的新手了。
这是一篇很好的文章,它解释了不同之处,我将引用并从中窃取一些代码:)
作为一个小例子,如果你遇到这样的情况,很难确切地看到作者想要做什么:
1 2 3 4 5 6 7 8 9 10 11 | //function prototype void func(int*& rpInt); // I mean, seriously, int*& ?? int main() { int nvar=2; int* pvar=&nvar; func(pvar); .... return 0; } |
或者,在较小程度上,类似这样的事情:
1 2 3 4 5 6 7 8 9 10 11 | //function prototype void func(int** ppInt); int main() { int nvar=2; int* pvar=&nvar; func(&pvar); .... return 0; } |
所以在一天结束的时候,我们能用这些胡言乱语真正解决什么问题呢?没有什么。
Now we have seen the syntax of
ptr-to-ptr and ref-to-ptr. Are there
any advantages of one over the other?
I am afraid, no. The usage of one of
both, for some programmers are just
personal preferences. Some who use
ref-to-ptr say the syntax is"cleaner"
while some who use ptr-to-ptr, say
ptr-to-ptr syntax makes it clearer to
those reading what you are doing.
这种复杂性和与引用的看似(大胆的看似)互换性(这通常是指针的另一个警告,也是新指针的错误)使得理解指针变得困难。同样重要的是,为了完成,理解引用的指针在C和C++中是非法的,因为混淆的原因将你带入EDCOX1,3,EDCOX1,4的语义。
正如前面的回答所指出的,很多时候,你只会让这些热门的程序员认为他们使用
这个答案比我希望的要长…
我个人认为,参考资料的质量和从事教学工作的人都是我的错;C语言中的大多数概念(尤其是指针)都是教得不好的。我一直威胁要写自己的C语言书(题为《世界上最不需要的东西是关于C语言的另一本书》),但我没有时间和耐心去写。所以我在这里闲逛,向人们随机引用标准中的内容。
还有一个事实是,在最初设计C时,假设您对机器体系结构的理解非常详细,这仅仅是因为在日常工作中无法避免(内存非常紧张,处理器速度非常慢,您必须了解您所写的内容如何影响性能)。
有一篇很好的文章支持这样一种观点,即指针在乔尔·斯波斯基的网站上很难——javaschools的危险。
[免责声明-我不是一个Java仇恨者本身]。
型
如果你没有建立在"底层"的知识基础上,那么大多数事情都很难理解。当我教CS的时候,当我开始给学生们编程一个非常简单的"机器"时,它变得容易多了。这是一个模拟的十进制计算机,有十进制操作码,它的内存由十进制寄存器和十进制地址组成。例如,他们会在非常短的程序中添加一系列数字以得到总数。然后他们会一步一步观察发生了什么。他们可以按住"回车"键,看着它"快跑"。
我敢肯定,几乎每个人都这么想,为什么这么基础有用呢?我们忘记了不知道如何编程是什么感觉。玩弄这样一台玩具计算机,就会产生一些你无法编程的概念,例如计算是一个循序渐进的过程,使用少量的基本原语来构建程序,以及将内存变量的概念作为存储数字的地方,在那里变量的地址或名称与t不同。它包含的数字。你进入程序的时间和它"运行"的时间是有区别的。我把学习比作程序跨越一系列"减速带",例如非常简单的程序,然后循环和子程序,然后数组,然后顺序I/O,然后指针和数据结构。所有这些都很容易通过参考计算机真正在底层做什么来学习。
最后,当谈到C时,指针是令人困惑的,尽管K&R在解释它们方面做得很好。我在C语言中学习它们的方式是知道如何阅读它们——从右到左。就像我在脑子里看到
直到我阅读了K&R中的描述,我才得到指针。直到那一点,指针才有意义。我读了很多东西,人们说"不要学指针,它们很混乱,会伤到你的头,给你动脉瘤",所以我很长时间回避它,创造了这个不必要的困难概念的空气。
否则,主要是我所想的,为什么你会想要一个变量,你必须通过箍来得到它的值,如果你想给它分配东西,你必须做一些奇怪的事情来获得价值去进入它们。我想,变量的整点都是用来存储一个值的,所以为什么有人想让它变得复杂,我无法理解。"所以,对于指针,您必须使用
它之所以复杂是因为我不明白指针是某个东西的地址。如果你解释它是一个地址,它是一个包含其他地址的地址,并且你可以操纵这个地址来做一些有用的事情,我认为它可能会消除混淆。
一个类需要使用指针来访问/修改PC上的端口,使用指针算法来处理不同的内存位置,并查看更复杂的C代码来修改它们的参数,这使我不再认为指针是毫无意义的。
型
下面是一个指针/数组示例,让我暂停了一下。假设您有两个数组:
1 2 | uint8_t source[16] = { /* some initialization values here */ }; uint8_t destination[16]; |
您的目标是使用memcpy()从源目标复制uint8_t内容。猜猜以下哪一项可以实现这一目标:
1 2 3 4 5 6 7 8 9 | 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]都是相同的值。"&"目的地"与其他两个类型不同,但它仍然是相同的值。"源"的排列也是如此。
作为旁白,我个人更喜欢第一个版本。
我首先要说C和C++是我学过的第一种编程语言。我从C开始,然后在学校做了很多C++,然后回到C来流利地读它。好的。
当我学习C时,第一件让我困惑的事情是:好的。
1 2 3 |
这种混淆主要是因为在指针被正确地引入我之前,已经引入了使用变量作为out参数的引用。我记得我跳过了用C语言为假人编写的前几个示例,因为它们太简单了,以至于无法使用我编写的第一个程序(很可能是因为这个)。好的。
让人困惑的是,
在我熟悉了这一点之后,我接下来记得我对动态分配感到困惑。我在某个时刻意识到,如果没有某种类型的动态分配,拥有指向数据的指针并不是非常有用,所以我写了如下内容:好的。
1 2 3 4 5 | char * x = NULL; if (y) { char z[100]; x = z; } |
尝试动态分配一些空间。它不起作用。我不确定它会起作用,但我不知道它还能起什么作用。好的。
后来我了解了
过了一段时间,我又学了递归(我以前自己学过,但现在在课堂上),我问它是如何在引擎盖下工作的——独立变量存储在哪里。我的教授说了"一言为定",很多事情我都明白了。我以前听过这个词,以前也实现过软件栈。我早就听到别人提到"那堆东西",但我忘了。好的。
大约在这个时候,我还意识到在C中使用多维数组会变得非常混乱。我知道它们是如何工作的,但它们很容易被卷入其中,所以我决定尽可能地使用它们。我认为这里的问题主要是句法上的(尤其是传递函数或从函数返回函数)。好的。
自从我在接下来的一两年里为学校写C++的时候,我对使用数据结构的指针有了很多的经验。在这里,我遇到了一系列新的麻烦——混淆了指针。我会有多个级别的指针(比如
在某个时刻,我学到了程序堆是如何工作的(有点,但足够好,它不再让我在夜间工作)。我记得读到过,如果在某个系统上的
不久之后,我上了一个汇编课,这门课并没有像大多数程序员想象的那样教会我很多关于指针的知识。不过,它确实让我思考了更多关于我的代码可能被翻译成什么程序集的问题。我一直试图写有效的代码,但现在我有了一个更好的方法。好的。
我还上了几节课,不得不写一些口齿不清的东西。在编写Lisp时,我不像在C语言中那样关心效率。我几乎不知道这段代码在编译后会被翻译成什么,但我确实知道,使用许多本地命名符号(变量)会使事情变得更容易。在某些时候,我写了一些AVL树旋转代码在LISP的一小部分,因为指针问题,我在C++中写了非常困难的时间。我意识到我对我认为是多余的局部变量的厌恶阻碍了我写C++和其他几个程序的能力。好的。
我还上过编译课。在这节课上,我学习了高级材料,学习了静态单赋值(SSA)和死变量,这并不重要,只是它教会了我,任何一个优秀的编译器都可以很好地处理不再使用的变量。我已经知道,更多的变量(包括指针)有正确的类型和好的名字将有助于我保持头脑中的事情,但现在我也知道,为了效率的原因而避免它们比我那些不太注重微观优化的教授告诉我的更愚蠢。好的。
所以对我来说,了解一个程序的内存布局有很大帮助。想想我的代码在象征意义上和硬件上意味着什么,可以帮助我摆脱困境。使用具有正确类型的本地指针有很大帮助。我经常编写如下代码:好的。
1 2 3 4 | int foo(struct frog * f, int x, int y) { struct leg * g = f->left_leg; struct toe * t = g->big_toe; process(t); |
所以如果我搞砸了一个指针类型,很明显编译器的错误是什么问题。如果我做到了:好的。
1 2 | int foo(struct frog * f, int x, int y) { process(f->left_leg->big_toe); |
如果指针类型出错,那么编译器的错误就更难理解了。我会试图诉诸试验和错误的变化,在我的挫折,可能使事情更糟。好的。好啊。
回顾过去,有四件事真的帮助我最终理解了指针。在此之前,我可以使用它们,但我没有完全理解它们。也就是说,我知道如果我遵循表单,我会得到我想要的结果,但是我没有完全理解表单的"为什么"。我知道这不是你所要求的,但我认为这是一个有用的推论。
编写一个将指针指向整数并修改该整数的例程。这给了我必要的形式来建立指针如何工作的任何心理模型。
一维动态内存分配。计算出一维内存分配使我了解指针的概念。
二维动态内存分配。计算出二维内存分配加强了这一概念,但也教会了我指针本身需要存储,必须考虑到这一点。
堆栈变量、全局变量和堆内存之间的差异。弄清楚这些差异,我就知道了指针指向的记忆类型。
这些项目中的每一项都需要想象在较低层次上发生了什么——建立一个心理模型,以满足我所能想到的每一个案例。这花了时间和精力,但很值得。我相信,要理解指针,你必须根据它们的工作方式和实现方式来构建这种心理模型。
现在回到你原来的问题。根据前面的列表,我最初很难掌握几个项目。
我让我的"指针时刻"在C语言中处理一些电话程序。我必须用一个只理解经典C语言的协议分析器编写一个AXE10交换仿真器。一切都依赖于知道指针。我试图在没有它们的情况下编写代码(嘿,我是"预指针"让我松懈了一些),但完全失败了。
对我来说,理解它们的关键是&;(地址)接线员。一旦我理解了
令我羞愧的是,我被迫进入VB和Java,所以我的指针知识不像以前那么敏锐,但我很高兴我是"邮政指针"。不过,不要让我使用需要我理解**P的库。
以下是一个非答案:使用CDDEL(或C++ DECL)来计算出来:
1 2 | 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.开始的,我是从Java开始的。直到大学里有几节课我被要求了解C,指针的整个概念都是非常陌生的,所以我自学了C的基本知识,以及如何使用指针的基本意义。即使如此,每次我发现自己在读C代码时,我都必须查找指针语法。
所以在我非常有限的经历中(1年的真实世界+4年的大学生活),指针让我感到困惑,因为我从来没有在课堂以外的任何地方使用过指针。我可以同情现在用Java而不是C或C++启动CS的学生。正如你所说,你在"新石器时代"学会了指点,从那以后可能一直在使用它。对于我们这些较新的人来说,分配内存和进行指针算术的概念是非常陌生的,因为所有这些语言都将其抽象化了。
附笔。在读了斯波斯基的文章之后,他对"javaschools"的描述和我在康奈尔大学(05-09年)的经历完全不同。我采用了结构和函数编程(SML)、操作系统(C)、算法(笔和纸)以及在Java中没有被教过的其他各种各样的类。然而,所有的介绍类和选修课都是在Java中完成的,因为当你试图做一些比用指针实现哈希表更高层次的事情时,不重新发明轮子是有价值的。
我用C++编程了2年,然后转换成Java(5年),再也没有回头看。然而,当我最近不得不使用一些本地的东西时,我惊奇地发现我没有忘记任何关于指针的东西,我甚至发现它们很容易使用。这与我7年前第一次尝试理解这个概念时的经历形成了鲜明的对比。所以,我想理解和喜欢是编程成熟度的问题吗?:)
或
指针就像骑自行车,一旦你知道如何与他们合作,就不会忘记它。
总之,很难理解或不理解,整个指针的概念是非常有教育意义的,我相信每个程序员都应该理解它,不管他是否在有指针的语言上编程。
型
它们为代码添加额外的维度,而不对语法进行显著的更改。想想这个:
1 2 | int a; a = 5 |
只有一件事要改变:
1 2 | int *a; a = &some_int; |
。
关于
1 | a = &some_other_int; |
…而
1 | *a = 6; |
。
在只有局部副作用的
由于间接性,指针很难指向。
指针是处理对象句柄和对象本身之间差异的一种方法。(好吧,不一定是反对,但你知道我的意思,也知道我的思想在哪里)
在某种程度上,你可能需要处理这两者之间的区别。在现代高级语言中,这就成为按价值复制和参照复制之间的区别。不管怎样,对于程序员来说,这是一个很难理解的概念。
然而,正如已经指出的,用C语言处理这个问题的语法是丑陋的、不一致的和混乱的。最后,如果你真的试图理解它,一个指针将是有意义的。但是当你开始处理指向指针的指针,等等令人恶心的事情时,我和其他人都会感到困惑。
关于指针还有一件重要的事情要记住,它们是危险的。C是一种编程大师的语言。它假设你知道你在做什么,从而给你权力真正把事情搞砸。虽然某些类型的程序仍然需要用C语言编写,但大多数程序都不需要,如果您有一种语言可以更好地抽象对象与其句柄之间的区别,那么我建议您使用它。
事实上,在许多现代C++应用中,通常需要封装和抽象任何需要的指针算法。我们不希望开发人员到处做指针算术。我们需要一个集中的、经过良好测试的API,它可以在最低级别执行指针算术。对此代码进行更改必须非常小心并进行广泛的测试。
我认为C指针很难理解的一个原因是它们合并了一些并不真正等价的概念;然而,由于它们都是使用指针实现的,人们很难解开这些概念。
在C语言中,指针用于表示其他事物:
- 定义递归数据结构
在C中,您将定义一个整数链表,如下所示:
1 2 3 4 | struct node { int value; struct node* next; } |
指针之所以存在,是因为这是在C中定义递归数据结构的唯一方法,而这个概念实际上与内存地址等低级细节无关。在haskell中考虑以下等价物,它不需要使用指针:
1 | data List = List Int List | Null |
非常简单-列表要么是空的,要么是由值和列表的其余部分组成的。
- 迭代字符串和数组
下面是如何将函数
1 2 | char *c; for (c ="hello, world!"; *c != '\0'; c++) { foo(c); } |
尽管也使用了一个指针作为迭代器,但这个例子与前一个例子几乎没有什么共同之处。创建可以递增的迭代器与定义递归数据结构是不同的概念。这两个概念都与内存地址的概念无关。
- 实现多态性
以下是在glib中找到的实际函数签名:
1 2 3 4 5 | typedef struct g_list GList; void g_list_foreach (GList *list, void (*func)(void *data, void *user_data), void* user_data); |
哇!这是一个相当多的
1 | map::(a->b)->[a]->[b] |
这就简单得多了:
总而言之:
我认为,如果人们首先将递归数据结构、迭代器、多态性等作为单独的概念来学习,然后学习如何使用指针在C中实现这些概念,而不是将所有这些概念混合到一个"指针"主题中,C指针就不会那么容易混淆了。
我一直(主要是自学)遇到的问题是"何时"使用指针。我可以用构造指针的语法来概括,但我需要知道在什么情况下应该使用指针。
我是唯一一个有这种心态的人吗?;-)
指针(以及一些低级工作的其他方面)要求用户去掉魔法。
大多数高级程序员都喜欢魔法。
我认为它需要一个坚实的基础,可能来自机器层面,介绍一些机器代码、程序集,以及如何在RAM中表示项目和数据结构。这需要一点时间,一些家庭作业或解决问题的练习,以及一些思考。
但如果一个人一开始懂高级语言(这没什么错——木匠用斧头)。一个需要分裂原子的人使用其他东西。我们需要的是木匠,我们有研究原子的人),而这个懂高级语言的人被给予了2分钟的指针介绍,然后很难期望他理解指针算术、指针指针、指向可变大小字符串的指针数组和字符数组等。低级的坚实的基础有很大帮助。
从前…我们有8位微处理器,每个人都用汇编语言编写。大多数处理器都包含一些用于跳转表和内核的间接寻址类型。当更高级别的语言出现时,我们添加了一层薄薄的抽象层,称之为指针。多年来,我们越来越远离硬件。这不一定是坏事。它们之所以被称为高级语言是有原因的。我越能集中精力做我想做的事情,而不是把事情的细节做得越好。
型
似乎很多学生对间接的概念都有问题,特别是当他们第一次遇到间接的概念时。我记得从我还是个学生的时候起,在我课程的+100名学生中,只有少数人真正理解指针。
间接性的概念并不是我们在现实生活中经常用到的,因此它最初是一个很难理解的概念。
型
我最近刚刚有了指针点击的时刻,我很惊讶我发现它令人困惑。更重要的是,每个人都这么谈论它,我以为一些黑暗魔法正在发生。
我就是这样得到的。假设所有定义的变量在编译时(在堆栈上)都有内存空间。如果您想要一个能够处理大型数据文件(如音频或图像)的程序,那么您就不需要为这些潜在结构提供固定数量的内存。因此,您要等到运行时分配一定数量的内存来保存这些数据(在堆上)。
一旦你的数据在内存中,你就不想每次你想在内存总线上运行一个操作的时候都在复制数据。假设您要对图像数据应用过滤器。您有一个指针,它从您分配给图像的数据的前面开始,一个函数在该数据上运行,将其就地更改。如果您不知道我们在做什么,那么您可能会在运行整个操作时复制数据。
至少我现在是这样看的!
在这里作为C++新手说话:
指针系统花了一段时间让我消化,不一定是因为这个概念,而是因为相对于Java的C++语法。我觉得有些事情令人困惑:
(1)变量声明:
1 | A a(1); |
VS
1 | A a = A(1); |
VS
1 | A* a = new A(1); |
显然
1 | A a(); |
是函数声明而不是变量声明。在其他语言中,基本上只有一种方法来声明变量。
(2)与符号有几种不同的用法。如果是
1 | int* i = &a; |
则&A是内存地址。
Otoh,如果是
1 | void f(int &a) {} |
然后,&a是一个按引用传递的参数。
虽然这看起来微不足道,但它可能会让新用户感到困惑——我来自Java和Java,这是一种使用更为统一的语言的操作员。
(3)数组指针关系
有一件事让人有点沮丧,那就是一个指针
1 | int* i |
可以是指向int的指针
1 | int *i = &n; // |
或
可以是int的数组
1 | int* i = new int[5]; |
然后,为了使事情更混乱,指针和数组在所有情况下都不能互换,指针不能作为数组参数传递。
这总结了我对C/C++的一些基本挫折,以及它的指针,IMO,很大程度上是因为C/C++具有所有这些语言特有的怪癖。
型
即使在我毕业后和第一份工作之后,我个人也不明白这一点。我唯一知道的是,对于链表、二叉树和将数组传递到函数中都需要它。这是我第一份工作时的情况。只有当我开始接受采访时,我才明白指针的概念是深刻的,具有巨大的用途和潜力。然后我开始阅读K&R并编写自己的测试程序。我的整个目标是工作驱动的。在这个时候,我发现,如果用一种好的方式来教,指点确实不坏也不难。不幸的是,当我在毕业时学C时,外教没有意识到指针,甚至作业中使用的指针也更少。在研究生阶段,指针的使用实际上只是为了创建二叉树和链表。这种认为你不需要正确理解指针就可以和它们一起工作的想法,扼杀了学习它们的想法。
主要问题是人们不明白为什么他们需要指针。因为它们不清楚堆栈和堆。最好从16位汇编程序开始,使用小内存模式的x86。它帮助许多人了解堆栈、堆和"地址"。现代程序员有时无法告诉你需要多少字节来处理32位空间。他们怎么知道指针?
第二个时刻是符号:您声明指针为*,您得到地址为&;这对于一些人来说是不容易理解的。
最后我看到的是存储问题:它们理解堆和堆栈,但不能进入"静态"的概念。
型
指针..哈哈……在我的头脑中,指针的全部内容是,它给出一个内存地址,其中包含任何引用的实际值。所以没有什么魔力……如果你学习了一些汇编,你就不会有那么多的麻烦来学习指针是如何工作的。来吧,伙计们……即使在Java中,一切都是参考。