人们对C指针有什么困难?

时间:2010-10-26 16:31:57

标签: c pointers

从这里发布的问题数量可以看出,人们在掌握指针和指针算术时会遇到一些非常有趣的问题。

我很想知道原因。他们从来没有真正给我带来过重大问题(尽管我在新石器时代首次了解它们)。为了给这些问题写出更好的答案,我想知道人们觉得哪些困难。

所以,如果你正在努力使用指针,或者你最近突然“得到它”,指针的哪些方面会导致你出现问题?

29 个答案:

答案 0 :(得分:143)

当我第一次开始使用它们时,我遇到的最大问题是语法。

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中做过任何事情已经有一段时间了。我有点想念它,但后来我有点像一个狂欢节)

答案 1 :(得分:87)

我怀疑人们的答案有点太深了。实际上并不需要了解调度,实际CPU操作或汇编级内存管理。

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

  1. 堆与堆栈存储。令人惊讶的是,即使在一般意义上,也有多少人不理解这一点。
  2. 堆叠帧。只是局部变量的堆栈专用部分的一般概念,以及它是“堆栈”的原因...诸如存储返回位置,异常处理程序详细信息和以前的寄存器等详细信息可以安全地保留,直到有人试图构建一个编译器。
  3. “内存是内存是内存”转换只会更改运算符的版本或编译器为特定内存块提供多少空间。当人们谈论“什么(原始)变量X 真正是”时,你知道你正在处理这个问题。
  4. 我的大多数学生能够理解一块内存的简化图,通常是当前范围内堆栈的局部变量部分。通常会给各个地点提供明确的虚构地址。

    总的来说,我想说如果你想理解指针,就必须了解变量,以及它们在现代架构中的实际含义。

答案 2 :(得分:52)

正确理解指针需要了解底层机器的架构。

今天许多程序员都不知道他们的机器是如何工作的,就像大多数知道如何驾驶汽车的人对发动机一无所知。

答案 3 :(得分:41)

在处理指针时,困惑的人在两个阵营中的一个中广泛存在。我一直都是(我?)。

array[]人群

这是直接不知道如何从指针符号转换为数组符号的人群(或者甚至不知道它们甚至是相关的)。以下是访问数组元素的四种方法:

  1. 数组表示法(索引)用 数组名称
  2. 数组表示法(索引)用 指针名称
  3. 指针符号(*)用 指针名称
  4. 指针符号(*)用 数组名称
  5. 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人群

    This是一篇很棒的文章,解释了差异,我将引用并窃取一些代码:)

    作为一个小例子,如果您遇到这样的事情,很难确切地看到作者想要做什么:

    //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()很聪明,我们大多数人可能有时会写这些暴行,但事实并非如此好的代码,它肯定无法维护。

    这个答案结果比我希望的要长......

答案 4 :(得分:29)

我个人认为参考资料的质量和教学人员的质量; C中的大多数概念(但尤其是指针)只是简单地教授。我一直威胁要写自己的C书(标题为 The World Thing the World Thing the World Theds是另一本关于C编程语言的书),但我没有时间或耐心去做。所以我在这里闲逛,并在人们的标准中随机引用。

还有一个事实是,当C最初设计时,假设您将机器架构理解为非常详细的级别,因为在您的日常工作中无法避免它(内存非常紧张,处理器速度太慢,你必须了解你所写的内容如何影响性能)。

答案 5 :(得分:26)

有一篇很棒的文章支持Joel Spolsky网站上的指针很难 - The Perils of JavaSchools

[免责声明 - 我本身并不是Java-hater 。]

答案 6 :(得分:24)

如果你没有基于“下面”的知识,那么大多数事情都难以理解。当我教CS时,当我开始让学生编写一个非常简单的“机器”,一个带十进制操作码的模拟十进制计算机,其存储器由十进制寄存器和十进制地址组成时,它变得容易多了。他们会投入非常短的程序,例如,添加一系列数字来获得总数。然后他们会单步去看看发生了什么。他们可以按住“输入”键并观察它“快速”运行。

我敢肯定几乎每个人都想知道为什么这么基本是有用的。我们忘记了不知道如何编程的感觉。使用这样的玩具计算机就可以实现无法编程的概念,例如计算是一步一步的想法,使用少量基本原语来构建程序,以及记忆的概念变量作为存储数字的位置,其中变量的地址或名称与其包含的数字不同。您输入程序的时间与“运行”的时间之间存在差异。我把学习比作编程为跨越一系列“速度颠簸”,例如非常简单的程序,然后是循环和子程序,然后是数组,然后是顺序I / O,然后是指针和数据结构。通过参考计算机在底下的实际操作,所有这些都更容易学习。

最后,当进入C时,虽然K& R在解释它们方面做得非常好,但指针却令人困惑。我在C中学习它的方式是知道如何阅读它们 - 从右到左。就像我在脑海中看到int *p一样,我说“p指向int”。 C被发明为汇编语言的一步,这就是我喜欢它 - 它接近于“基础”。如果没有这种基础,就像其他任何东西一样,指针很难理解。

答案 7 :(得分:18)

在我阅读K& R中的描述之前,我没有得到指示。在那之前,指针没有意义。我读了很多东西,人们说“不要学习指针,它们会让人感到困惑,会伤到你的脑袋并给你动脉瘤”,所以我很长时间地避开它,并创造了这种难以理解的不必要的空气

否则,大多数我认为的是,为什么你想要一个变量,你必须通过箍来获得价值,如果你想给它分配东西,你必须做一些奇怪的事情才能获得进入他们的价值观。我想,变量的整个点是存储价值的东西,所以为什么有人想让它变得复杂在我之外。 “所以使用指针你必须使用*运算符来获取它的值???那是什么样的傻瓜变量?”,我想。没有意义,没有双关语。

它复杂的原因是因为我不明白指针是某个地址。如果你解释它是一个地址,它是包含其他东西的地址的东西,并且你可以操纵该地址做有用的事情,我认为它可以清除混乱。

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

答案 8 :(得分: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));

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

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

答案 9 :(得分:7)

答案 10 :(得分:6)

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

  1. 编写一个带有指向整数的指针并修改整数的例程。这为我提供了构建指针如何工作的心理模型的必要形式。

  2. 一维动态内存分配。弄清楚1-D内存分配让我理解了指针的概念。

  3. 二维动态内存分配。弄清楚2-D内存分配强化了这个概念,但也告诉我指针本身需要存储,必须加以考虑。

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

  5. 这些项目中的每一项都需要想象在较低层次上发生的事情 - 建立一个心理模型,满足我能想到的每一个案例。这需要时间和精力,但这非常值得。我相信,要理解指针,你必须建立他们的工作方式和实施方式的心理模型。

    现在回到你原来的问题。根据之前的清单,有几个项目我最初难以掌握。

    1. 如何以及为何使用指针。
    2. 它们有何不同,但与数组类似。
    3. 了解指针信息的存储位置。
    4. 了解指针指向的位置和位置。

答案 11 :(得分:6)

我的“指针时刻”正在处理C中的一些电话程序。我不得不使用只能理解经典C的协议分析器编写AXE10交换仿真器。一切都取决于知道指针。我试着在没有它们的情况下编写我的代码(嘿,我是“预指针”让我有些松懈)并完全失败了。

对我而言理解它们的关键是& (地址)运营商。一旦我理解&i意味着“我的地址”,那么理解*i意味着“我指向的地址的内容”稍后会出现。每当我编写或阅读我的代码时,我总是重复“&”意思是“*”的意思,最终我直观地使用它们。

令我惭愧的是,我被迫进入VB然后是Java,所以我的指针知识不像以前那样尖锐,但我很高兴我是“后指针”。不要让我使用需要我理解 * * p的库。

答案 12 :(得分:5)

指针的主要困难,至少对我来说,是我没有从C开始。我从Java开始。整个指针的概念确实是外来的,直到我在大学的几个课程中我都应该知道C.然后我自学了C的基础知识以及如何在他们非常基本的意义上使用指针。即便如此,每当我发现自己正在阅读C代码时,我都必须查找指针语法。

因此,在我非常有限的经历(1年真实世界+大学4年)中,指针让我感到困惑,因为除了课堂设置以外,我从来没有真正使用它。我现在可以同情学生用JAVA而不是C或C ++开始CS。正如你所说,你学习了“新石器时代”时代的指针,并且从那以后可能一直在使用它。对于我们这些新人来说,分配内存和进行指针运算的概念实际上是外来的,因为所有这些语言都已经抽象出来了。

P.S。 在阅读了Spolsky的文章之后,他对“JavaSchools”的描述与我在康奈尔大学('05 -'09)中所经历的完全不同。我采用了结构和函数式编程(sml),操作系统(C),算法(笔和纸),以及一系列未在java中讲授的其他类。然而,所有的介绍类和选修都是用java完成的,因为当你尝试做一些比使用指针实现散列表更高级别的东西时,重新发明轮子是有价值的。

答案 13 :(得分: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

答案 14 :(得分: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 ...可能会让人感到困惑。

答案 15 :(得分:4)

我用c ++编程了2年,然后转换为Java(5年),从未回头。然而,当我最近不得不使用一些原生的东西时,我发现(惊奇地)我没有忘记关于指针的任何东西,我甚至发现它们很容易使用。这与我7年前第一次尝试掌握这一概念时所经历的形成鲜明对比。所以,我想理解和喜欢是编程成熟的问题? :)

OR

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

总而言之,难以掌握,整个指针的想法非常有教育意义,我相信每个程序员都应该理解它,不管他是否使用指针语言进行编程。

答案 16 :(得分:3)

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

在某些时候,你可能不得不处理两者之间的差异。在现代的高级语言中,这成为了按值复制和逐个引用复制之间的区别。无论哪种方式,这都是程序员难以掌握的概念。

但是,正如已经指出的那样,在C中处理这个问题的语法是丑陋的,不一致的和令人困惑的。最终,如果你真的想要理解它,指针就会有意义。但是当你开始处理指针指针等等时,对我和其他人来说都会让人感到困惑。

要记住关于指针的另一个重要事项是它们危险。 C是主程序员的语言。它假设你知道你正在做什么,从而给你真正搞砸的能力。虽然某些类型的程序仍然需要用C语言编写,但大多数程序都没有,如果你有一种语言可以更好地抽象出对象与句柄之间的区别,那么我建议你使用它。

实际上,在许多现代C ++应用程序中,通常情况是任何所需的指针算法都被封装和抽象。我们不希望开发人员在所有地方进行指针运算。我们需要一个集中的,经过良好测试的API,它可以在最低级别执行指针运算。对此代码进行更改必须非常谨慎并进行广泛的测试。

答案 17 :(得分:3)

指针因间接而很难。

答案 18 :(得分: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*的一大口。而且只是声明一个函数迭代一个可以包含任何类型的东西的列表,将函数应用于每个成员。将其与Haskell中声明map的方式进行比较:

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

这更直截了当:map是一个函数,它接受一个将a转换为b的函数,并将其应用于a的列表得出b的列表。就像在C函数g_list_foreach中一样,map在其自己的定义中不需要知道它将应用的类型。

总结一下:

我认为,如果人们首先将递归数据结构,迭代器,多态等等作为单独的概念学习,那么C指针会更容易混淆,然后学习如何使用指针在C中实现这些想法< / em>,而不是将所有这些概念混合成一个单一的“指针”主题。

答案 19 :(得分:2)

似乎很多学生对间接概念存在问题,特别是当他们第一次遇到间接概念时。我记得当时我是一名学生,在课程的100多名学生中,只有少数人真正理解指针。

间接的概念不是我们在现实生活中经常使用的东西,因此最初需要掌握这个概念。

答案 20 :(得分:2)

我最近刚刚点了指针点击时刻,我很惊讶我发现它令人困惑。更多的是每个人都这么谈论它,我假设有一些黑魔法正在发生。

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

将数据存储在内存中后,每次要对内存总线执行操作时,都不希望将数据复制到内存总线上。假设您要对图像数据应用过滤器。您有一个指针,它从您分配给图像的数据的前面开始,一个函数在该数据上运行,并在适当的位置进行更改。如果您不知道我们正在做什么,那么当您在整个操作中运行时,您可能最终会制作重复的数据。

至少那是我现在看到的方式!

答案 21 :(得分:2)

曾几何时......我们有8位微处理器,每个人都在汇编中写道。大多数处理器包括用于跳转表和内核的某种类型的间接寻址。当更高级语言出现时,我们添加一个薄层抽象并将其称为指针。多年来,我们越来越远离硬件。这不一定是坏事。出于某种原因,它们被称为高级语言。我越能专注于我想做的事情,而不是更好地完成它的细节。

答案 22 :(得分:2)

指针(以及低级别工作的其他一些方面)要求用户带走魔法。

大多数高级程序员喜欢神奇。

答案 23 :(得分:2)

我一直有的问题(主要是自学)是“何时”使用指针。我可以围绕构造指针的语法,但我需要知道在什么情况下应该使用指针。

我是唯一有这种心态的人吗? ; - )

答案 24 :(得分:2)

我认为它需要一个坚实的基础,可能来自机器级别,介绍一些机器代码,装配以及如何在RAM中表示项目和数据结构。这需要一点时间,一些家庭作业或解决问题的练习,以及一些思考。

但如果一个人一开始就知道高级语言(这没什么不对 - 木匠使用斧头。需要分裂原子的人使用别的东西。我们需要木匠的人,我们有学习的人这个知道高级语言的人被给了2分钟的指针介绍,然后很难指望他理解指针算术,指针指针,可变大小字符串指针数组和字符数组数组低水平的坚实基础可以帮助很多。

答案 25 :(得分:1)

在这里作为C ++新手说:

指针系统需要一段时间才能消化,不一定是因为概念,而是因为相对于Java的C ++语法。我发现令人困惑的一些事情是:

(1)变量声明:

A a(1);

VS

A a = A(1);

VS

A* a = new A(1); 

显然

A a(); 

是函数声明而不是变量声明。在其他语言中,基本上只有一种方法来声明变量。

(2)&符号以几种不同的方式使用。如果是

int* i = &a;

然后&amp; a是一个内存地址。

OTOH,如果是

void f(int &a) {}

然后&amp; a是传递参考参数。

虽然这看起来似乎微不足道,但对于新用户来说可能会让人感到困惑 - 我来自Java和Java,这是一种更加统一使用运算符的语言

(3)数组指针关系

理解的一点点令人沮丧的是指针

int* i

可以是指向int

的指针
int *i = &n; // 

可以是int的数组

int* i = new int[5];

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

这总结了我对C / C ++及其指针的一些基本挫折,IMO因C / C ++具有所有这些语言特有的怪癖而大大加剧了这一点。

答案 26 :(得分:0)

即使在我毕业后和第一份工作之后,我个人也不理解指针。我唯一知道的是你需要它用于链表,二叉树以及将数组传递给函数。甚至在我第一份工作时也是如此。只有当我开始接受采访时,我才明白指针概念很深,具有巨大的用途和潜力。然后我开始阅读K&amp; R并编写自己的测试程序。我的整个目标是以工作为导向 在这个时候,我发现指针确实不坏也不困难,如果它们以一种好的方式被教导的话。不幸的是,当我在毕业时学习C语言时,老师并不知道指针,甚至指派都使用较少的指针。在研究生阶段,指针的使用实际上只能创建二叉树和链表。认为你不需要正确理解与它们一起工作的指针,就会扼杀学习它们的想法。

答案 27 :(得分:0)

指针..哈哈......所有关于我头脑中的指针的是它给出了一个内存地址,其中任何参考的实际值都没有...所以没有魔法...如果你学习了一些装配,你就不会有学习指针是如何工作的很麻烦..来吧......即使在Java中,一切都是参考..

答案 28 :(得分:0)

主要问题是人们不明白为什么他们需要指针。 因为他们不清楚堆栈和堆。 从具有微小内存模式的x86的16位汇编程序开始是很好的。它帮助很多人了解堆栈,堆和“地址”。而字节:)现代程序员有时无法告诉你需要多少字节来处理32位空间。他们如何理解指针?

第二个时刻是符号:你将指针声明为*,你得到地址为&amp;这对某些人来说并不容易理解。

我看到的最后一件事是存储问题:他们理解堆和堆栈,但无法理解“静态”。