如何向初学者解释C指针(声明与一元运算符)?

arm*_*min 138 c pointers

我最近很高兴向C编程初学者解释指针并偶然发现了以下困难.如果你已经知道如何使用指针,它可能看起来似乎不是一个问题,但尝试以清醒的头脑看下面的例子:

int foo = 1;
int *bar = &foo;
printf("%p\n", (void *)&foo);
printf("%i\n", *bar);
Run Code Online (Sandbox Code Playgroud)

对于绝对的初学者来说,输出可能会令人惊讶.在第2行他/她刚刚宣布*bar为&foo,但在第4行结果显示*bar实际上是foo而不是&foo!

您可能会说,混淆源于*符号的模糊性:在第2行中,它用于声明指针.在第4行中,它用作一元运算符,它获取指针指向的值.两件不同的事,对吗?

然而,这种"解释"根本不能帮助初学者.它通过指出一个微妙的差异引入了一个新的概念.这不是教授它的正确方法.

那么,Kernighan和Ritchie是如何解释的呢?

一元运算符*是间接或解除引用运算符; 当应用于指针时,它访问指针指向的对象.[...]

指针ip的声明int *ip用作助记符; 它说表达式*ip是一个int.变量声明的语法模仿可能出现变量的表达式的语法.

int *ip应该读作" *ip会回来int"吗?但是为什么声明后的作业不遵循这种模式?如果初学者想要初始化变量怎么办?int *ip = 1(阅读:*ip将返回a intintis 1)将无法按预期工作.概念模型似乎并不一致.我在这里错过了什么吗?


编辑:它试图在这里总结答案.

Ilm*_*nen 79

简写之所以:

int *bar = &foo;
Run Code Online (Sandbox Code Playgroud)

在你的例子中可能令人困惑的是,它很容易被误读为等同于:

int *bar;
*bar = &foo;    // error: use of uninitialized pointer bar!
Run Code Online (Sandbox Code Playgroud)

当它实际意味着:

int *bar;
bar = &foo;
Run Code Online (Sandbox Code Playgroud)

像这样写出来,变量声明和赋值分开,没有这种混淆的可能性,并且你的K&R引用中描述的使用↔声明并行性完美地起作用:

  • 第一行声明一个变量bar,例如*bara int.

  • 第二行指定footo 的地址bar,使*bar(a int)为foo(也是)的别名int.

在向初学者介绍C指针语法时,最初坚持这种从指派中分离指针声明的方式可能会有所帮助,并且只有在指针使用的基本概念中,才会引入组合的简写语法(对其混淆的可能性有适当的警告). C已经充分内化.

  • 这只是大脑受损的C声明风格; 它并不特定于指针.考虑`int a [2] = {47,11};`,这不是(不存在的)元素a a [2]`eiher的初始化. (5认同)
  • 我会想要`typedef`.`typedef int*p_int;`表示`p_int`类型的变量具有`*p_int`为`int`的属性.然后我们有`p_int bar =&foo;`.鼓励任何人创建未初始化的数据,然后将其作为默认习惯分配给它似乎......就像一个坏主意. (4认同)
  • @MarcvanLeeuwen同意脑损伤.理想情况下,`*`应该是类型的一部分,而不是绑定到变量,然后你就可以编写`int*foo_ptr,bar_ptr`来声明两个指针.但它实际上声明了一个指针和一个整数. (4认同)

Pha*_*rap 43

为了让学生*在不同的语境中理解符号的含义,他们必须首先理解语境确实是不同的.一旦他们理解了上下文不同(即作业的左手边和一般表达之间的差异),理解差异是不是太过认知上的飞跃.

首先解释变量的声明不能包含运算符(通过显示在变量声明中放置-+符号只会导致错误来证明这一点).然后继续表明表达式(即在赋值的右侧)可以包含运算符.确保学生理解表达式和变量声明是两个完全不同的上下文.

当他们理解上下文不同时,您可以继续解释当*符号位于变量标识符前面的变量声明中时,它意味着"将此变量声明为指针".然后你可以解释当在表达式中使用时(作为一元运算符),*符号是'解引用运算符',它表示'地址的值'而不是它的早期含义.

为了真正说服你的学生,请解释C的创建者可以使用任何符号来表示解除引用操作符(即他们可以使用它们@),但无论出于何种原因,他们都要使用设计决策*.

总而言之,没有办法解释上下文是不同的.如果学生不理解上下文不同,他们就无法理解为什么*符号可能意味着不同的东西.


Mor*_*pfh 30

声明不足

很高兴知道声明和初始化之间的区别.我们将变量声明为类型并使用值初始化它们.如果我们同时做两个,我们经常称之为定义.

1. int a; a = 42;

int a;
a = 42;
Run Code Online (Sandbox Code Playgroud)

我们宣布一个int名为a.然后我们通过给它一个值来初始化它42.

2. int a = 42;

我们声明int命名为a,并给它赋值42.它用42.初始化.一个定义.

3. a = 43;

当我们使用变量时,我们说我们它们进行操作.a = 43是一项任务操作.我们将数字43分配给变量a.

通过说

int *bar;
Run Code Online (Sandbox Code Playgroud)

我们声明bar是一个指向int的指针.通过说

int *bar = &foo;
Run Code Online (Sandbox Code Playgroud)

我们声明bar并用foo的地址初始化它.

在我们初始化了bar之后,我们可以使用相同的运算符星号来访问和操作foo的值.没有操作符,我们访问并操作指针指向的地址.

除此之外,我让图片说出来.

什么

关于正在发生的事情的简化ASCIIMATION.(这里有玩家版本,如果你想暂停等)

          ASCIIMATION


Sun*_*lly 22

第二个陈述int *bar = &foo;可以在记忆中以图形方式查看,

   bar           foo
  +-----+      +-----+
  |0x100| ---> |  1  |
  +-----+      +-----+ 
   0x200        0x100
Run Code Online (Sandbox Code Playgroud)

现在bar是一个int包含地址&的类型的指针foo.使用一元运算符,*我们依赖于检索指针所指向的值bar.

编辑:我理解指向初学者的方法将开始解释memory address变量即

Memory Address:每个变量都有一个由OS提供的与之关联的地址.在int a;,&a变量的地址a.

继续解释Cas中的一些基本类型的变量,

Types of variables: 变量可以保存各种类型的值,但不能保存地址.

int a = 10; float b = 10.8; char ch = 'c'; `a, b, c` are variables. 
Run Code Online (Sandbox Code Playgroud)

Introducing pointers: 如上所述,例如

 int a = 10; // a contains value 10
 int b; 
 b = &a;      // ERROR
Run Code Online (Sandbox Code Playgroud)

它可能是分配b = a但不是b = &a,因为变量b可以保持值而不是地址,因此我们需要指针.

Pointer or Pointer variables :如果变量包含地址,则称为指针变量.*在声明中使用以通知它是一个指针.

• Pointer can hold address but not value
• Pointer contains the address of an existing variable.
• Pointer points to an existing variable
Run Code Online (Sandbox Code Playgroud)

  • 问题是将"int*ip"读作"ip是int类型的指针(*)",在读取类似`x =(int)*ip`的内容时会遇到麻烦. (3认同)
  • @abw这是完全不同的东西,因此括号.我不认为人们会很难理解声明和演员之间的区别. (2认同)
  • @abw:这就是为什么教`int*bar =&foo;`让_loads_更有意义.是的,我知道当你在一个声明中声明多个指针时会引起问题.不,我认为这根本不重要. (2认同)

arm*_*min 17

看看这里的答案和评论,似乎普遍认为有问题的语法可能会让初学者感到困惑.他们中的大多数人都提出了以下建议:

  • 在显示任何代码之前,请使用图表,草图或动画来说明指针的工作方式.
  • 在演示语法时,请解释星号符号的两个不同角色.很多教程都缺少或回避这一部分.混乱随之而来("当你将初始化的指针声明分解为声明和后来的赋值时,你必须记住删除*" - comp.lang.c FAQ)我希望找到另一种方法,但我想这是要走的路.

你可以写int* bar而不是int *bar强调差异.这意味着您不会遵循K&R"声明模仿使用"方法,而是遵循Stroustrup C++方法:

我们不声明*bar是整数.我们宣布bar是一个int*.如果我们想在同一行中初始化一个新创建的变量,很明显我们正在处理bar,而不是*bar.int* bar = &foo;

缺点:

  • 你必须警告你的学生多指针声明问题(int* foo, barvs int *foo, *bar).
  • 你必须为受伤世界做好准备.许多程序员希望看到变量名称旁边的星号,他们将花费很多时间来证明他们的风格.许多样式指南明确强制执行此表示法(Linux内核编码样式,NASA C样式指南等).

编辑:一个不同的方法已经被提出,是去K&R"模仿"的方式,但没有"简写"语法(见这里).一旦省略了在同一行中进行声明和赋值,一切都会变得更加连贯.

但是,学生迟早要将指针作为函数参数处理.和指针作为返回类型.和指向函数的指针.你将不得不解释之间的差异int *func();int (*func)();.我想迟早会有事情崩溃.也许早点比以后好.


Jon*_*nna 16

K&R风格有利于int *pStroustrup风格,这是有原因的int* p; 两种语言都有效(并且意思相同),但正如Stroustrup所说:

"int*p;"之间的选择 和"int*p;" 不是对与错,而是关于风格和重点.C强调表达; 声明通常被认为只是一种必要的邪恶.另一方面,C++非常重视类型.

现在,既然你想在这里教C,那就建议你应该更多地强调表达类型,但是有些人可以更容易地比另一个更快地强调一个强调,那就是关于它们而不是语言.

因此,有些人会发现更容易从int*一个int与a不同的东西开始并从那里开始.

如果有人迅速神交看它使用的方式int* barbar,因为这是不是一个int的事情,而是一个指针int,那么他们很快就会看到,*bar正在做一些事情bar,其余的将随之而来.完成后,您可以稍后解释为什么C编码员倾向于选择int *bar.

或不.如果有一种方式让每个人都能首先理解这个概念,那么首先你就不会遇到任何问题,而向一个人解释它的最佳方式并不一定是向另一个人解释它的最佳方式.

  • 我赞成Pascal的语法(通常扩展)优于C的原因之一是`Var A,B:^ Integer;`清楚地表明"指向整数的指针"类型适用于"A"和"B".使用`K&R`样式`int*a,*b`也是可行的; 但是,像`int*a,b;`这样的声明看起来好像`a`和`b`都被声明为`int*`,但实际上它将`a`声明为`int*`和`b`为`int`. (4认同)
  • 我猜你的意思是``int&r =*p``.我敢打赌,借款人仍在试图消化这本书. (3认同)

Use*_*ess 9

TL;博士:

问:如何向初学者解释C指针(声明与一元运算符)?

答:不要.解释初学者的指针,并向他们展示如何用C语法表示他们的指针概念.


我最近很高兴向C编程初学者解释指针并偶然发现了以下困难.

IMO的C语法并不可怕,但也不是很好:如果你已经理解了指针,那么它既不是一个很大的障碍,也不是学习它们的任何帮助.

因此:首先解释指针,并确保他们真正理解它们:

  • 用方框图解释它们.您可以在没有十六进制地址的情况下执行此操作,如果它们不相关,只需显示指向另一个框的箭头,或指向一些零符号.

  • 用伪代码解释:只写foo的地址存储在bar的值.

  • 然后,当你的新手理解指针是什么,为什么,以及如何使用它们; 然后显示映射到C语法.

我怀疑K&R文本没有提供概念模型的原因是他们已经理解了指针,并且可能假设当时其他所有有能力的程序员都这样做了.助记符只是提醒人们从易于理解的概念到语法的映射.


bar*_*nos 7

这个问题在开始学习C时有点令人困惑.

以下是可能有助于您入门的基本原则:

  1. C中只有几种基本类型:

    • char:一个大小为1个字节的整数值.

    • short:一个大小为2个字节的整数值.

    • long:整数值,大小为4个字节.

    • long long:一个大小为8个字节的整数值.

    • float:非整数值,大小为4个字节.

    • double:非整数值,大小为8个字节.

    请注意,每种类型的大小通常由编译器定义,而不是由标准定义.

    整数类型short,long并且long long通常接着int.

    但是,它不是必须的,你可以在没有它的情况下使用它们int.

    或者,您可以只声明int,但不同的编译器可能会对此有不同的解释.

    总结一下:

    • short与...相同short int但不一定相同int.

    • long与...相同long int但不一定相同int.

    • long long与...相同long long int但不一定相同int.

    • 在给定的编译器上,intshort int或者long intlong long int.

  2. 如果声明某种类型的变量,那么您也可以声明指向它的另一个变量.

    例如:

    int a;

    int* b = &a;

    所以从本质上讲,对于每种基本类型,我们也有相应的指针类型.

    例如:shortshort*.

    有两种方法可以"查看"变量b (这可能会让大多数初学者感到困惑):

    • 您可以将其b视为类型的变量int*.

    • 您可以将其*b视为类型的变量int.

    因此,有些人会宣布int* b,而其他人会宣布int *b.

    但事实是这两个声明是相同的(空格是没有意义的).

    您可以将其b用作指向整数值的指针,也可以*b用作实际的指向整数值.

    你可以得到(读)指出的值:int c = *b.

    你可以设置(写)指向的值:*b = 5.

  3. 指针可以指向任何内存地址,而不仅仅指向您之前声明的某个变量的地址.但是,在使用指针时必须小心,以获取或设置位于指向的内存地址的值.

    例如:

    int* a = (int*)0x8000000;

    这里,我们有变量a指向内存地址0x8000000.

    如果此内存地址未映射到程序的内存空间中,则使用任何读取或写入操作*a很可能会导致程序因内存访问冲突而崩溃.

    您可以安全地更改值a,但您应该非常小心地更改值*a.

  4. 类型void*是例外的,因为它没有可以使用的相应"值类型"(即,您不能声明void a).此类型仅用作指向内存地址的通用指针,而不指定驻留在该地址中的数据类型.


zxq*_*xq9 7

也许单步执行它可以更容易:

#include <stdio.h>

int main()
{
    int foo = 1;
    int *bar = &foo;
    printf("%i\n", foo);
    printf("%p\n", &foo);
    printf("%p\n", (void *)&foo);
    printf("%p\n", &bar);
    printf("%p\n", bar);
    printf("%i\n", *bar);
    return 0;
}
Run Code Online (Sandbox Code Playgroud)

让他们告诉你他们期望输出在每一行上,然后让他们运行程序,看看会出现什么.解释他们的问题(在那里的裸体版本肯定会提示一些 - 但你可以担心后来的风格,严格性和便携性).然后,在他们的思想转变为过度思考或者成为午餐后僵尸之前,写一个带有值的函数,以及带有指针的那个函数.

根据我的经验,它克服了"为什么这样打印?" 驼峰,然后通过动手玩弄(作为一些基本的K&R材料的前奏,如字符串解析/数组处理)立即显示为什么这在函数参数中有用,这使得课程不仅有意义而且坚持.

下一步是让他们向解释如何i[0]与之相关&i.如果他们能够做到这一点,他们就不会忘记它,你可以开始谈论结构,甚至提前一点,只是因为它沉入其中.

关于方框和箭头的上述建议也很好,但它也可以逐渐深入讨论关于记忆如何工作的全面讨论 - 这是一个必须在某个时刻发生的谈话,但可以分散注意力. :如何解释C中的指针表示法


Joh*_*ode 6

表达式 的类型*barint; 因此,变量(和表达式) 的类型barint *.由于变量具有指针类型,因此其初始值设定项也必须具有指针类型.

指针变量初始化和赋值之间存在不一致; 这只是必须以艰难的方式学习的东西.

  • 看看这里的答案,我感觉很多有经验的程序员甚至都看不到*问题*了.我想这是"学会与不一致生活"的副产品. (3认同)
  • @abw:初始化规则与赋值规则不同; 对于标量算术类型,差异可以忽略不计,但它们对指针和聚合类型很重要.这是你需要与其他一切解释的东西. (3认同)

Gop*_*opi 5

int *bar = &foo;
Run Code Online (Sandbox Code Playgroud)

Question 1:什么是bar

Ans:它是一个指针变量(要键入int).指针应指向某个有效的内存位置,稍后应使用一元运算符取消引用(*bar)*以读取存储在该位置的值.

Question 2:什么是&foo

Ans:foo是一个类型的变量int.which存储在一些有效的内存位置,我们从运营商那里得到它,&所以我们现在拥有的是一些有效的内存位置&foo.

所以两者放在一起,即指针所需的是一个有效的内存位置,并且得到了&foo所以初始化是好的.

现在指针bar指向有效的内存位置,存储在其中的值可以取消引用它,即*bar


gro*_*rel 5

我宁愿阅读它作为第一个*申请int超过bar.

int  foo = 1;           // foo is an integer (int) with the value 1
int* bar = &foo;        // bar is a pointer on an integer (int*). it points on foo. 
                        // bar value is foo address
                        // *bar value is foo value = 1

printf("%p\n", &foo);   // print the address of foo
printf("%p\n", bar);    // print the address of foo
printf("%i\n", foo);    // print foo value
printf("%i\n", *bar);   // print foo value
Run Code Online (Sandbox Code Playgroud)

  • 是的,但我不认为应该使用`int*a,b`.为了更好的可行性,更新等...每行应该只有一个变量声明,而且永远不会更多.即使编译器可以处理它,也可以向初学者解释. (4认同)
  • 然后你必须解释为什么`int*a,b`没有做他们认为它做的事情. (2认同)

Yon*_*won 5

你应该指出一个初学者*在声明和表达式中有不同的含义.如你所知,*在表达式中是一元运算符,并且*在声明中不是运算符,只是一种结合类型的语法,让编译器知道它是一个指针类型.说初学者更好,"*有不同的含义.为了理解*的含义,你应该找到*使用的地方"