从这里发布的问题数量来看,很明显人们在理解指针和指针算术时有一些非常基本的问题。
我很想知道为什么会这样。虽然我早在新石器时代就学习了指针,但它们从未给我带来过大问题。为了更好地回答这些问题,我想知道人们觉得困难的是什么。
所以,如果你在处理指针方面遇到了困难,或者最近突然“明白了”,那么哪些方面会使你感到困难呢?
从这里发布的问题数量来看,很明显人们在理解指针和指针算术时有一些非常基本的问题。
我很想知道为什么会这样。虽然我早在新石器时代就学习了指针,但它们从未给我带来过大问题。为了更好地回答这些问题,我想知道人们觉得困难的是什么。
所以,如果你在处理指针方面遇到了困难,或者最近突然“明白了”,那么哪些方面会使你感到困难呢?
当我开始与他们一起工作时,我遇到的最大问题是语法。
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了。我有点想念它,但我有点自虐倾向)
我怀疑人们在他们的答案中过于深入。实际上,不需要了解调度、实际CPU操作或汇编级内存管理。
当我教学时,我发现以下学生理解方面的漏洞是问题最常见的根源:
我的大多数学生能够理解一个简化的内存块的绘画,通常是当前作用域下栈的局部变量部分。通常为各个位置提供明确的虚构地址会有所帮助。
总之,我想说的是,如果你想理解指针,你必须理解变量,以及在现代架构中它们实际上是什么。
正确理解指针需要了解底层计算机的架构。
如今很多程序员并不知道他们的机器是如何工作的,就像大多数会开车的人对发动机一无所知一样。
当处理指针时,那些感到困惑的人通常分为两类。我曾经(还是现在?)都属于这两类。
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++程序员困惑,更不用说缺乏经验的新手。
引用指针
和指向指针
人群这篇文章是一篇很棒的介绍它们之间差异的文章,我将引用并借用其中的一些代码 :)
举个小例子,如果你遇到下面这样的代码,很难理解作者想要做什么:
//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;
}
最后,我们用所有这些废话解决了什么问题呢?什么都没有。
现在我们已经看到了指向指针和引用的指针的语法。它们之间是否有任何优势?恐怕没有。对于一些程序员来说,使用其中之一只是个人喜好。一些使用引用指针的人说语法更加“简洁”,而一些使用指向指针的人则说,指向指针的语法使那些阅读你正在做什么的人更清楚。
指针的复杂性和似乎与引用的互换性(这常常是指针的另一个警告和新手错误),使理解指针变得困难。出于完整性的考虑,也很重要的一点是,指向引用的指针在C和C++中是非法的,原因涉及到-语义。
正如前面的答案所提到的,很多时候你会遇到那些自认为很聪明地使用******awesome_var->lol_im_so_clever()
的高级程序员,而我们中的大多数人可能有时也会写出这样的罪行,但这不是好代码,肯定也不能维护。
好吧,这个答案比我想象的要长...
void func(int*& rpInt);
这部分的含义或提供任何参考资料吗?我真的不明白。 - ajaysinghnegi个人认为,C语言中大多数概念(特别是指针)的教学质量很差,这主要归咎于参考材料和教师水平的问题。我一直在威胁要写自己的C语言书籍,名为《世界上最不需要另一本C语言编程书籍》,但是我没有时间和耐心去做。因此,我会在这里闲逛,并向人们提供标准库的随机引用。
同时,C语言最初的设计假定程序员对计算机体系结构有相当详细的了解,因为在日常工作中无法避免(由于存储器非常紧张,处理器速度非常缓慢,所以必须了解所编写的代码对性能的影响)。
这里有一篇非常好的文章支持指针在 Joel Spolsky 的网站上很难使用 - Java 学校的危险。
[免责声明 - 我并非 Java 反对者。]
如果你没有相关知识作为基础,大多数事情都很难理解。当我教授计算机科学时,当我让学生们开始编程一个非常简单的“机器”时,它变得更容易了。这个机器是一台模拟的十进制计算机,具有十进制操作码,其存储器由十进制寄存器和十进制地址组成。他们会输入非常短的程序,例如添加一系列数字以获得总和。然后他们会逐步执行程序并观察发生的情况。他们可以按住“回车”键观看其快速运行。
我相信几乎每个SO上的人都想知道为什么这么基础的东西有用。我们忘记了不懂编程时的感受。玩这样一个玩具计算机可以建立编程所必需的概念,例如计算是一个逐步的过程,使用少量基本原语来构建程序,并且将内存变量的概念视为存储数字的位置,其中变量的地址或名称与其包含的数字不同。输入程序的时间和运行程序的时间是有区别的。我认为学习编程就像穿越一系列“减速带”,例如非常简单的程序,然后是循环和子例程,然后是数组,然后是顺序I/O,然后是指针和数据结构。通过参考计算机在底层实际执行的内容,所有这些都更容易学习。
最后,在学习C语言时,指针很难理解,尽管K&R做了非常好的解释。我学习它们的方式是知道如何阅读它们——从右到左。就像当我看到int *p
时,在我的脑海中会说“p
指向一个int
”。C语言被发明出来是作为汇编语言的一步进化,这也是我喜欢它的原因——它接近于“基础”。指针就像其他任何东西一样,如果你没有相关知识作为基础,就很难理解。
在阅读K&R中的描述之前,我并不理解指针。在那之前,指针对我来说是没有意义的。我看了很多人写的东西,其中说“不要学指针,它们很令人困惑,会让你头痛并导致动脉瘤”,因此我一直回避学习,创造了这种不必要的难以理解的氛围。
否则,我大多数的想法是,你为什么要定义一种变量,必须跨过重重障碍才能得到它的值,如果你想把东西赋给它,你必须做奇怪的事情才能将值放入其中。我认为,变量的整个目的是用来存储一个值,所以我想,为什么有人想要将其复杂化呢?“所以使用指针,您必须使用*
运算符来获取其值???这是什么样的变量?” 我想,毫无意义,双关语未打算。
它被复杂化的原因是因为我不理解指针是指向某些东西的地址。如果您解释指针是一个地址,即它是包含指向其他东西的地址的东西,并且可以操纵该地址以进行有用的操作,我认为这可能会消除困惑。
一个需要使用指针访问/修改PC端口,使用指针算术来寻址不同的内存位置,并查看更复杂的C代码来修改其参数的类使我放弃了指针毫无意义的想法。
这里有一个指针/数组的例子让我感到困惑。假设你有两个数组:
uint8_t source[16] = { /* some initialization values here */ };
uint8_t destination[16];
你的目标是使用memcpy()从源destination复制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" 是与其他两个不同的类型,但它仍然是相同的值。对于"source"的排列也是如此。sizeof(source)
,因为如果source
是一个指针,那么它的大小将不会是你想要的。我有时候(但并不总是)写成sizeof(source[0]) * number_of_elements_of_source
,只是为了远离这个错误。 - Mike Dunlavey我应该先说一下C和C++是我学习的第一种编程语言。我开始学习C语言,然后在学校学了很多C++,最后又回到C语言中成为熟练者。
在学习C语言时,让我困惑的第一件事就是指针:
char ch;
char str[100];
scanf("%c %s", &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);