从这里发布的问题数量可以清楚地看出,当人们围绕指针和指针算术时,人们会遇到一些非常有趣的问题.
我很想知道为什么.他们从来没有真正给我带来过重大问题(尽管我在新石器时代首次了解它们).为了更好地回答这些问题,我想知道人们发现什么困难.
所以,如果你正在努力使用指针,或者你最近突然"得到它",指针的哪些方面会导致你出现问题?
Jef*_*cht 144
当我第一次开始使用它们时,我遇到的最大问题是语法.
int* ip;
int * ip;
int *ip;
Run Code Online (Sandbox Code Playgroud)
都是一样的.
但:
int* ip1, ip2; //second one isn't a pointer!
int *ip1, *ip2;
Run Code Online (Sandbox Code Playgroud)
为什么?因为声明的"指针"部分属于变量,而不是类型.
然后取消引用该东西使用一个非常相似的表示法:
*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'
Run Code Online (Sandbox Code Playgroud)
除非你真的需要一个指针...然后你使用一个&符号!
int *ip = &x;
Run Code Online (Sandbox Code Playgroud)
万岁一致!
然后,显然只是成为混蛋并证明它们有多聪明,许多库开发人员使用指向指针的指针,如果他们期望这些东西的数组,那么为什么不直接传递一个指针呢? .
void foo(****ipppArr);
Run Code Online (Sandbox Code Playgroud)
要调用它,我需要指向int的指针指针数组的地址:
foo(&(***ipppArr));
Run Code Online (Sandbox Code Playgroud)
在六个月内,当我必须维护这段代码时,我会花更多的时间来弄清楚这一切意味着什么,而不是从头开始重写.(是的,可能这个语法错了 - 因为我在C中做过任何事情已经有一段时间了.我有点想念它,但后来我有点像一个狂欢节)
jke*_*ian 87
我怀疑人们的答案有点太深了.实际上并不需要了解调度,实际CPU操作或汇编级内存管理.
当我在教学时,我发现学生的理解中存在以下漏洞是最常见的问题来源:
我的大多数学生都能够理解一块内存的简化图,通常是当前范围内堆栈的局部变量部分.通常给各个位置提供明确的虚构地址有帮助.
总而言之,我想说如果你想理解指针,你必须理解变量,以及它们在现代架构中的实际含义.
Rob*_*vey 52
正确理解指针需要了解底层机器的架构.
今天许多程序员都不知道他们的机器是如何工作的,正如大多数知道如何驾驶汽车的人对发动机一无所知.
Dav*_*nco 42
在处理指针时,那些感到困惑的人在两个阵营中占有一席之地.我两个都在(我?).
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)
Run Code Online (Sandbox Code Playgroud)
这里的想法是通过指针访问数组看起来非常简单和直接,但是通过这种方式可以完成大量非常复杂和聪明的事情.其中一些可能会让经验丰富的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;
}
Run Code Online (Sandbox Code Playgroud)
或者,在较小程度上,这样的事情:
//function prototype
void func(int** ppInt);
int main()
{
int nvar=2;
int* pvar=&nvar;
func(&pvar);
....
return 0;
}
Run Code Online (Sandbox Code Playgroud)
所以在一天结束时,我们真正用这些胡言乱语解决了什么?没有.
现在我们已经看到了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()
,我们大多数人有时可能会犯这样的暴行,但这只是不好的代码,而且肯定无法维护.
那么这个答案结果比我希望的要长......
Joh*_*ode 29
我个人指责参考资料和教学人员的质量; 在C(但大多数概念尤其是三分球)都只是简单的教不好.我一直威胁要写自己的C书(题为"世界需要的最后一件事是C编程语言的另一本书"),但我没有时间或耐心去做.所以我在这里闲逛,并在人们的标准中随机引用.
还有的是当C最初的设计,这是事实假设你了解机器架构,以一个相当详细的水平,只是因为没有办法,以避免它在你的一天到一天的工作(记忆是如此紧张,处理器是如此慢你必须了解你写的是如何影响表现的).
Ste*_*end 26
有一篇很棒的文章支持Joel Spolsky网站上的指针很难--JavaSchools的危机.
[免责声明 - 我本身并不是Java仇恨者.]
Mik*_*vey 24
如果你没有基于"下面"的知识,那么大多数事情都难以理解.当我教CS时,当我开始让学生编写一个非常简单的"机器"时,它变得容易得多,这是一个带十进制操作码的模拟十进制计算机,其内存由十进制寄存器和十进制地址组成.他们会投入非常短的程序,例如,添加一系列数字来获得总数.然后他们会单步去看看发生了什么.他们可以按下"输入"键并观察它"快速"运行.
我敢肯定几乎所有人都想知道为什么这么基本有用.我们忘记了不知道如何编程的感觉.使用这样的玩具计算机就可以实现无法编程的概念,例如计算是一步一步的想法,使用少量基本原语来构建程序,以及记忆的概念变量作为存储数字的位置,其中变量的地址或名称与其包含的数字不同.您输入程序的时间与"运行"的时间之间存在差异.我把学习比作编程为跨越一系列"速度颠簸",例如非常简单的程序,然后是循环和子程序,然后是数组,然后是顺序I/O,然后是指针和数据结构.
最后,当进入C时,指针令人困惑,尽管K&R在解释它们方面做得非常好.我在C中学习它的方式是知道如何阅读它们 - 从右到左.就像我int *p
在脑海中看到的那样,我说" p
指向一个int
".C被发明为汇编语言的一步,这就是我喜欢它 - 它接近于"基础".如果你没有这种基础,就像其他任何东西一样,指针更难理解.
J. *_*fer 17
在我阅读K&R中的描述之前,我没有得到指示.在那之前,指针没有意义.我读了很多东西,人们说"不要学习指针,它们会让人感到困惑,会伤到你的脑袋并给你动脉瘤",所以我很长时间地避开它,并创造了这种难以理解的不必要的气氛.
否则,大多数我认为的是,为什么你想要一个变量,你必须通过箍获得价值,如果你想分配东西,你必须做一些奇怪的事情来获得价值去进入他们.我想,变量的整个点是存储价值的东西,所以为什么有人想让它变得复杂在我之外. "所以使用指针你必须使用*
运算符来获取它的值???这是什么样的傻瓜变量?" , 我想.毫无意义,没有双关语意.
它复杂的原因是因为我不明白指针是某个地址.如果你解释它是一个地址,它是包含一个地址的东西,并且你可以操纵该地址做有用的事情,我认为它可能清除混乱.
需要使用指针来访问/修改PC上的端口,使用指针算法来寻址不同的内存位置,以及查看修改其参数的更复杂的C代码的类,这使得我无法理解指针是毫无意义的.
And*_*ell 12
这是一个让我暂停的指针/数组示例.假设您有两个数组:
uint8_t source[16] = { /* some initialization values here */ };
uint8_t destination[16];
Run Code Online (Sandbox Code Playgroud)
您的目标是使用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));
Run Code Online (Sandbox Code Playgroud)
答案(Spoiler Alert!)就是其中之一."destination","&destination"和"&destination [0]"都是相同的值."&destination" 与另外两种类型不同,但它仍然是相同的值."源"的排列也是如此.
另外,我个人更喜欢第一个版本.
我应该首先说C和C++是我学到的第一种编程语言.我从C开始,然后在学校里做了很多C++,然后又回到C语言,使其流利.
在学习C时,第一件让我困惑的事情很简单:
char ch;
char str[100];
scanf("%c %s", &ch, str);
Run Code Online (Sandbox Code Playgroud)
这种混乱主要是因为在将指针正确地引入我之前引入了对OUT参数的变量的引用.我记得我跳过在C for Dummies中编写前几个例子,因为它们太简单了,只是为了永远不会得到我写的第一个程序(很可能是因为这个).
让人感到困惑的是&ch
实际意义以及为什么str
不需要它.
在我熟悉之后,我记得我对动态分配感到困惑.我在某种程度上意识到,如果没有动态分配某种类型,指向数据并不是非常有用,所以我写了类似的东西:
char * x = NULL;
if (y) {
char z[100];
x = z;
}
Run Code Online (Sandbox Code Playgroud)
尝试动态分配一些空间.它没用.我不确定它会起作用,但我不知道它有什么用.
后来我才知道约malloc
和new
,但他们真的好像神奇记忆发电机给我.我对他们的工作方式一无所知.
一段时间后,我再次被教授递归(我以前自己学过它,但现在在课堂上),我问它是如何在引擎盖下工作的 - 存储了单独的变量.我的教授说"在堆栈上",很多事情对我来说很清楚.我以前听过这个术语,之前已经实现了软件堆栈.我早就听过其他人提到"堆栈",但却忘记了它.
大约在这个时候,我也意识到在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);
Run Code Online (Sandbox Code Playgroud)
因此,如果我搞砸了指针类型,编译器错误就会清楚问题是什么.如果我做了:
int foo(struct frog * f, int x, int y) {
process(f->left_leg->big_toe);
Run Code Online (Sandbox Code Playgroud)
并且在那里得到任何指针类型错误,编译器错误将更难以弄清楚.我很想在我的挫折中诉诸试验和错误,并可能使事情变得更糟.
回顾过去,有四件事真的帮助我最终理解了指针.在此之前,我可以使用它们,但我并不完全理解它们.也就是说,我知道如果我遵循表格,我会得到我想要的结果,但我并不完全理解表格的"原因".我意识到这并不是你所要求的,但我认为这是一个有用的推论.
编写一个带有指向整数的指针并修改整数的例程.这为我提供了构建指针如何工作的任何心理模型的必要形式.
一维动态内存分配.弄清楚1-D内存分配让我理解了指针的概念.
二维动态内存分配.找出2-D内存分配强化了这个概念,但也告诉我指针本身需要存储,必须考虑在内.
堆栈变量,全局变量和堆内存之间的差异.找出这些差异教会了指针指向/引用的内存类型.
这些项目中的每一项都需要想象在较低层次上发生的事情 - 建立一个心理模型,满足我能想到的每一个案例.这需要时间和精力,但这非常值得.我相信,为了理解指针,你必须建立关于它们如何工作以及如何实现它们的心智模型.
现在回到你原来的问题.根据之前的清单,有几个项目我最初难以掌握.
我用"指针时刻"处理C中的一些电话程序.我不得不使用仅理解经典C的协议分析器编写AXE10交换仿真器.一切都取决于知道指针.我试着在没有它们的情况下编写我的代码(嘿,我是"预指针"让我有些松懈)并完全失败了.
对我来说,理解它们的关键是&(地址)运算符.一旦我明白这&i
意味着" 我的地址",那么理解*i
意味着"我所指的地址的内容"稍后会出现.每当我编写或阅读我的代码时,我总是重复"&"的含义以及"*"的含义,最终我直观地使用它们.
令我感到羞耻的是,我被迫进入VB,然后是Java,所以我的指针知识不像以前那么尖锐,但我很高兴我是"后指针".不要让我使用一个需要我理解**p 的库.
至少在我看来,指针的主要困难在于我没有从C开始.我从Java开始.整个指针的概念确实是外来的,直到我在大学里的几个课程,我被期望知道C.所以,然后我自学了C的基础知识以及如何在他们的基本意义上使用指针.即便如此,每当我发现自己正在阅读C代码时,我必须查找指针语法.
因此,在我非常有限的经历(1年真实世界+大学4年)中,指针让我感到困惑,因为除了课堂环境以外,我从来没有真正使用它.我现在可以同情学生用JAVA而不是C或C++开始CS.正如你所说,你学习了"新石器时代"时代的指针,并且从那以后可能一直在使用它.对于我们这些新人来说,分配内存和执行指针运算的概念实际上是外来的,因为所有这些语言都已经抽象出来了.
PS阅读完Spolsky文章后,他对"JavaSchools"的描述与我在康奈尔大学('05 -'09)所经历的完全不同.我采用了结构和函数式编程(sml),操作系统(C),算法(笔和纸),以及一系列未在java中讲授的其他类.然而,所有的介绍类和选修课都是用java完成的,因为当你试图做一些比使用指针实现散列表更高级别的东西时,不重新发明轮子是有价值的.
这是一个非答案:使用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
Run Code Online (Sandbox Code Playgroud)