从这里发布的问题数量来看,很明显,人们在使用指针和指针算术时会遇到一些根本性的问题。
我很好奇为什么。它们从未真正给我造成重大问题(尽管我是在新石器时代第一次了解它们的)。为了对这些问题写出更好的答案,我想知道人们发现困难之处。
因此,如果您在使用指针时遇到困难,或者您最近却突然“明白了”,那么指针的哪些方面导致了您的问题?
从这里发布的问题数量来看,很明显,人们在使用指针和指针算术时会遇到一些根本性的问题。
我很好奇为什么。它们从未真正给我造成重大问题(尽管我是在新石器时代第一次了解它们的)。为了对这些问题写出更好的答案,我想知道人们发现困难之处。
因此,如果您在使用指针时遇到困难,或者您最近却突然“明白了”,那么指针的哪些方面导致了您的问题?
Answers:
我怀疑人们的回答太过深入了。真正不需要了解调度,实际的CPU操作或程序集级内存管理。
在教学过程中,我发现学生理解中的以下漏洞是最常见的问题来源:
我的大多数学生都能理解内存的简化图,通常是当前作用域中堆栈的局部变量部分。通常,给各个位置明确的虚构地址很有帮助。
总而言之,我想说的是,如果您想了解指针,则必须了解变量及其在现代体系结构中的实际含义。
当我第一次与他们合作时,我遇到的最大问题是语法。
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做任何事以来已经有一段时间了。我有点想念它,但是后来我有点受虐狂了)
正确理解指针需要有关底层计算机体系结构的知识。
如今,许多程序员不知道他们的机器如何工作,就像大多数懂得如何驾驶汽车的人对引擎一无所知。
在处理指针时,感到困惑的人广泛分布在两个阵营之一。我都去过(上午?)。
array[]
人群这是一群不知道如何从指针表示法转换为数组表示法的人群(或者甚至不知道它们甚至是相关的)。这是访问数组元素的四种方法:
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 pointer
和pointer 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()
而且我们大多数人有时都犯了这样的暴行罪名,但这只是不好的代码,而且肯定无法维护。
好吧,这个答案竟然比我希望的更长。
我个人指责参考资料的质量和从事教学的人员;C语言中的大多数概念(尤其是指针)都不好教。我一直威胁要写自己的C书(《世界需要的最后一本书》是有关C编程语言的另一本书),但我没有时间或耐心这样做。因此,我在这里闲逛并向人们扔出标准的随机报价。
还有一个事实是,在最初设计C时,假设您只是相当详细地了解了机器体系结构,因为在日常工作中无法避免这种情况(内存太紧,处理器太慢了。您必须了解编写的内容如何影响性能)。
在Joel Spolsky的网站-JavaSchools的危险上,有一篇很棒的文章支持指针很难使用的观点。
[免责声明-我本身不是Java仇恨者。]
如果您不了解“基础知识”,那么大多数事情将更难理解。当我教授CS时,让我的学生开始对一个非常简单的“机器”进行编程就变得容易得多,这是一台带有十进制操作码的模拟十进制计算机,其内存由十进制寄存器和十进制地址组成。他们将编写非常简短的程序,例如,添加一系列数字以获得总计。然后他们会一步一步观察发生的事情。他们可以按住“ enter”键,然后观察它“快速”运行。
我敢肯定,SO中的几乎每个人都想知道为什么变得如此基础很有用。我们忘记了不知道如何编程的感觉。玩这样的玩具计算机会放置一些您无法编程的概念,例如,计算是逐步的过程,使用少量基本原语来构建程序的概念以及内存的概念变量作为存储数字的位置,其中变量的地址或名称与其包含的数字不同。输入程序的时间与“运行”的时间之间存在区别。我喜欢学习编程,就像跨越一系列“减速带”一样,例如非常简单的程序,然后是循环和子例程,然后是数组,然后是顺序I / O,然后是指针和数据结构。
最后,当使用C时,尽管K&R在解释它们方面做得很好,但指针却令人困惑。我用C语言学习它们的方式是知道如何从右到左阅读它们。就像当我int *p
在脑海中看到时一样,我说“ p
指向int
”。C的发明是从汇编语言发展而来的,这就是我所喜欢的-它接近那个“基础”。指针,像其他任何东西一样,如果没有这种基础,则更难理解。
在阅读K&R中的说明之前,我没有得到任何指示。直到那时,指针才有意义。我读了一堆书,人们说:“不要学指针,它们会令人困惑,会伤到你的头,给你动脉瘤”,所以我回避了很长时间,并创造了这种不必要的,难以理解的氛围。
否则,主要是我想的是,为什么在地球上您想要一个必须经过箍才能获得其值的变量,并且如果您想为其分配内容,就必须做一些奇怪的事情才能获得值进入他们。我认为,变量的全部意义在于可以存储值,所以为什么有人想要使其复杂化就超出了我的范围。 “所以使用指针必须使用*
运算符来获取其值?那是什么样的愚蠢变量?” , 我想。没有意义,没有双关语。
之所以复杂,是因为我不知道指针是指向某个东西的地址。如果您解释说这是一个地址,它包含一个指向其他地址的地址,并且您可以操纵该地址来做有用的事情,那么我认为这可能会消除混乱。
一个类需要使用指针来访问/修改PC上的端口,使用指针算术来寻址不同的内存位置,并查看修改其参数的更复杂的C代码,这使我无法意识到指针毫无意义。
这是一个让我停下来的指针/数组示例。假设您有两个数组:
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”是与其他两个类型不同的类型,但其值仍然相同。“源”的排列也是如此。
顺便说一句,我个人更喜欢第一个版本。
sizeof(source)
,因为如果source
是指针,sizeof
它就不是您想要的。我有时(并非总是如此)sizeof(source[0]) * number_of_elements_of_source
只是为了远离该错误而写。
首先,我要说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;
}
尝试动态分配一些空间。没用 我不确定它是否会工作,但我不知道它还能如何工作。
后来我了解了malloc
和new
,但是对我来说,它们确实像是神奇的内存生成器。我对它们如何工作一无所知。
一段时间后,我又被教递归(以前我自己学过递归,但是现在在上课),我问它在幕后如何工作-单独的变量存储在哪里。我的教授说“在栈上”,我明白了很多事情。我以前听过这个词,并且以前已经实现过软件堆栈。我很早以前就听说过其他人提到“堆栈”,但是却忘记了。
大约在这个时候,我还意识到在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);
并且在那里存在任何指针类型错误,那么编译器错误将很难解决。我很容易在挫折中诉诸于试错法,并可能使情况变得更糟。
回顾过去,有四件事确实帮助我终于了解了指针。在此之前,我可以使用它们,但是我还没有完全理解它们。就是说,我知道如果我遵循这些表格,将会得到想要的结果,但是我并没有完全理解表格的“原因”。我意识到这并不是您所要的,但我认为这是有用的推论。
编写一个使用指向整数的指针并修改该整数的例程。这给了我必要的形式,可以在此基础上建立关于指针如何工作的任何心理模型。
一维动态内存分配。弄清一维内存分配使我了解了指针的概念。
二维动态内存分配。弄清楚二维内存分配强化了这一概念,但同时也告诉我指针本身需要存储,必须将其考虑在内。
堆栈变量,全局变量和堆内存之间的差异。弄清楚这些差异,使我知道了指针指向/引用的内存类型。
这些项目中的每一个都需要想象较低层次上发生的事情-建立一个满足我可能想到的所有情况的心理模型。这花费了时间和精力,但这是值得的。我坚信要理解指针,您必须建立关于它们如何工作以及如何实现的思维模型。
现在回到您的原始问题。根据先前的清单,我最初很难掌握一些项目。
我在C语言中的某些电话程序上使用了“指针时刻”。我必须使用仅能理解经典C语言的协议分析器来编写AXE10交换仿真器。一切都取决于指针。我尝试在没有它们的情况下编写代码(嘿,我是“预指点”,这使我有些懈怠)并完全失败了。
对我来说,理解它们的关键是&(地址)运算符。一旦我理解&i
了“ i的地址”,然后理解了*i
“ i所指向的地址的内容”。每当我编写或阅读我的代码时,我总是会重复“&”的含义和“ *”的含义,最终我会直观地使用它们。
令我感到羞耻的是,我被迫转入VB,然后再转入Java,因此我的指针知识不如以前那么深刻,但我很高兴自己是“后指针”。但是,不要要求我使用需要我理解* * p 的库。
&i
是地址,*i
是内容,那是i
什么?
至少对于我而言,使用指针的主要困难是,我不是从C开始的。我是从Java开始的。指针的整个概念真的很陌生,直到我在大学里学习了几门课程后,我才知道C。因此,我自学了C的基本知识以及如何在其最基本的意义上使用指针。即使这样,每次我发现自己在阅读C代码时,也必须查找指针语法。
因此,根据我非常有限的经验(1年真实世界+ 4年大学经验),指针使我感到困惑,因为除了教室环境外,我从来没有真正使用过它。我可以同情现在开始使用JAVA而不是C或C ++的CS的学生。如您所说,您学习了“新石器时代”的指针,并且从那以后可能就一直在使用它。对于我们新手来说,分配内存和执行指针算术的概念确实很陌生,因为所有这些语言都将其抽象化了。
PS在阅读了Spolsky的文章之后,他对“ JavaSchools”的描述与我在康奈尔大学('05 -'09)经历的情况完全不同。我学习了结构和函数式编程(sml),操作系统(C),算法(笔和纸),以及其他很多用Java讲不到的其他类。但是,所有介绍性类和选修课都在Java中完成,因为当您尝试执行比使用指针实现哈希表更高级别的操作时,不重新发明轮子很有用。
void foo(Clazz obj) { obj = new Clazz(); }
在void bar(Clazz obj) { obj.quux = new Quux(); }
对参数进行突变时为何不进行操作……
它们为代码增加了一个额外的维度,而语法没有重大变化。考虑一下:
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
......这可能是什么人混淆。
指针是困难的,因为是间接的。
指针(以及底层工作的其他一些方面)要求用户带走魔力。
大多数高级程序员都喜欢魔术。
指针是一种处理对象句柄和对象本身之间差异的方法。(好的,不一定是对象,但是您知道我的意思,以及我的想法在哪里)
在某些时候,您可能必须处理两者之间的差异。在现代的高级语言中,这成为按值复制和按引用复制之间的区别。无论哪种方式,这都是程序员通常难以理解的概念。
但是,正如已经指出的那样,用C语言处理此问题的语法很丑陋,前后不一致且令人困惑。最终,如果您真正尝试理解它,那么指针将是有意义的。但是,当您开始处理指向指针的指针之类的东西时,诸如此类的恶作剧,无论是对我还是对其他人而言,这都确实使我感到困惑。
关于指针要记住的另一点重要是指针很危险。C是一种高级程序员的语言。它假设您知道自己在做什么,从而使您能够真正搞砸事情。尽管某些类型的程序仍需要用C编写,但大多数程序不需要,如果您使用的语言可以更好地抽象对象及其句柄之间的区别,那么建议您使用它。
实际上,在许多现代C ++应用程序中,通常都需要封装和抽象任何所需的指针算法。我们不希望开发人员到处都进行指针算术运算。我们需要一个经过良好测试的集中式API,该API在最低级别上执行指针算术。对此代码进行更改必须非常小心并进行大量测试。
我认为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*
的。只是声明一个在列表中迭代的函数,该列表可以包含任何种类的东西,并将函数应用于每个成员。将其与map
Haskell中的声明进行比较:
map::(a->b)->[a]->[b]
那要简单得多:这map
是一个函数,该函数采用将an转换a
为b
,并将其应用于的列表a
以产生的列表的函数b
。就像在C函数中一样g_list_foreach
,map
不需要在其自己的定义中了解将要应用的类型的任何信息。
总结一下:
我认为,如果人们首先了解递归数据结构,迭代器,多态性等作为单独的概念,然后学习如何使用指针在C中实现这些思想,而不是将所有这些都混为一谈,那么C指针将减少很多混乱。概念一起成为“指针”的单个主题。
c != NULL
在您的“ Hello world”示例中滥用的 意思是…… *c != '\0'
。
我认为,这可能需要扎实的基础,可能是从机器级别开始的,还要介绍一些机器代码,汇编以及如何在RAM中表示项目和数据结构。这需要一些时间,需要一些家庭作业或解决问题的实践,还需要一些思考。
但是,如果一个人起初知道高级语言(这没错,木匠用斧头。一个需要分裂原子的人则使用其他东西。我们需要木匠的人,而我们有研究原子的人)和这位熟悉高级语言的人将获得2分钟的指针介绍,然后很难指望他理解指针算术,指针指针,可变大小字符串指针数组和字符数组数组等。坚实的基础可以帮助很多。
最近,我刚好有鼠标点击的瞬间,而令我感到困惑的是我感到惊讶。所有人都这么多地谈论它,以至于我以为某种黑暗的魔法正在发生。
我得到的方式就是这样。想象一下,所有定义的变量在编译时(在堆栈上)都被赋予了内存空间。如果您想要一个可以处理诸如音频或图像之类的大数据文件的程序,则不需要为这些潜在结构提供固定数量的内存。因此,您要等到运行时分配一定数量的内存来保存此数据(在堆上)。
一旦将数据存储在内存中,您就不想在每次要对其进行操作时都在内存总线周围复制该数据。假设您要对图像数据应用过滤器。您有一个指针,该指针从分配给图像的数据的开头开始,并且一个函数在该数据上运行,并在适当位置进行更改。如果您不知道我们在做什么,则可能会在操作过程中最终复制数据。
至少我现在就是这样看的!
在这里以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 ++具有所有这些特定于语言的怪癖而变得更加复杂。
我个人甚至在毕业后和第一份工作后都不理解指针。我唯一知道的是,您需要它来用于链表,二进制树以及将数组传递给函数。即使是我的第一份工作,情况就是如此。只有当我开始接受采访时,我才知道指针概念是深刻的,具有巨大的用途和潜力。然后,我开始阅读K&R并编写自己的测试程序。我的整个目标是工作驱动的。
这时我发现,如果对指针进行了很好的学习,它们的确不是不好也不困难。不幸的是,当我在毕业时学习C语言时,外出的老师并不了解指针,甚至作业也很少使用指针。在研究生级别,指针的使用实际上只能创建二进制树和链接列表。这种以为您不需要对指针进行适当理解就可以使用它们,这扼杀了学习它们的想法。