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

我最近很高兴地向一位 C 编程初学者解释了一些指针,并碰巧遇到了以下困难。如果您已经知道如何使用指针,那么这看起来可能根本不是一个问题,但是请尝试以清晰的头脑看下面的示例:

int foo = 1;
int *bar = &foo;
printf("%p\n", (void *)&foo);
printf("%i\n", *bar);

对于绝对的初学者来说,输出可能是令人惊讶的。在第2行中,他/她刚刚声明 * bar 为 & foo,但在第4行中,* bar 实际上是 foo 而不是 & foo!

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

然而,这种“解释”对初学者毫无帮助。它通过指出一个微妙的差异引入了一个新的概念。这不可能是正确的教学方法。

Kernighan 和 Ritchie 是怎么解释的?

一元操作符 * 是间接操作符或解引用操作符; 当应用到指针时,它访问指针指向的对象。[...]

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

int *ip应该读作“ *ip将返回一个 int”?但是为什么声明后的赋值不遵循这种模式呢?如果初学者想要初始化变量该怎么办?int *ip = 1(读取: *ip将返回一个 intint1)不会像预期的那样工作。概念模型看起来不太一致。我错过了什么吗?


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

10414 次浏览

第2条语句 int *bar = &foo;可以在内存中以图形方式查看,

   bar           foo
+-----+      +-----+
|0x100| ---> |  1  |
+-----+      +-----+
0x200        0x100

现在 bar是一个类型为 int的指针,包含 foo的地址 &。使用一元操作符 *,我们通过使用指针 bar来检索包含在‘ foo’中的值。

编辑 : 我对初学者的方法是解释变量的 memory address,即

每个变量都有一个由操作系统提供的与之相关联的地址。在 int a;中,&a是变量 a的地址。

继续解释 C中变量的基本类型,

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

int a = 10; float b = 10.8; char ch = 'c'; `a, b, c` are variables.

例如,上面提到的变量

 int a = 10; // a contains value 10
int b;
b = &a;      // ERROR

可以分配 b = a但不分配 b = &a,因为变量 b可以保存值但不能保存地址,因此我们需要 指示

如果一个变量包含一个地址,它被称为指针变量。在声明中使用 *通知它是一个指针。

• Pointer can hold address but not value
• Pointer contains the address of an existing variable.
• Pointer points to an existing variable

表情 *bar的类型是 int; 因此,变量(和表达) bar的类型是 int *。由于变量具有指针类型,因此它的初始值设定项也必须具有指针类型。

指针变量初始化和赋值之间存在不一致性; 这只是一些必须通过艰苦的方式学习的东西。

int *bar = &foo;

什么是 bar

Ans: 它是一个指针变量(类型为 int)。指针应该指向某个有效的内存位置,然后应该使用一元操作符 *解引用(* bar) ,以读取存储在该位置的值。

什么是 &foo

Foo 是一个类型为 int.which 的变量,它存储在一些有效的内存位置中,这个位置是从操作符 &那里得到的,所以现在我们得到的是一些有效的内存位置 &foo

所以两者放在一起,也就是说,指针所需要的是一个有效的内存位置,这是由 &foo获得的,所以初始化是好的。

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

在开始学习 C 语言时,这个问题有些令人困惑。

下面是一些可以帮助你开始的基本原则:

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

    • char: 大小为1字节的整数值。

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

    • long: 一个大小为4字节的整数值。

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

    • float: 大小为4字节的非整数值。

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

    注意,每个类型的大小通常由编译器和 不符合标准。定义

    整数类型 shortlonglong long后面通常跟着 int

    但是,这不是必须的,而且您可以在没有 int的情况下使用它们。

    或者,您可以只声明 int,但是不同的编译器可能会对其进行不同的解释。

    总结一下:

    • shortshort int相同,但不一定与 int相同。

    • longlong int相同,但不一定与 int相同。

    • long longlong long int相同,但不一定与 int相同。

    • 在给定的编译器上,intshort intlong 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)。此类型仅用作指向内存地址的一般指针,而不指定驻留在该地址中的数据类型。

在这里你必须使用,理解和解释编译器逻辑,而不是人类的逻辑(我知道,是一个人,但在这里你必须模仿计算机...)。

当你写作的时候

int *bar = &foo;

编译器将作为

{ int * } bar = &foo;

即: 这里有一个新变量,它的名称是 bar,它的类型是指向 int 的指针,它的初始值是 &foo

你必须补充: 上面的 =表示一个初始化,而不是一个做作,而在下面的表达式 *bar = 2;一个做作

编辑每条评论:

注意: 在多重声明的情况下,*只与下列变量有关:

int *bar = &foo, b = 2;

Bar 是一个指向 int 的指针,由 foo 的地址初始化,b 是一个初始化为2的 int,在

int *bar=&foo, **p = &bar;

Bar 中的 still 指针指向 int,而 p 是指向初始化为地址或 bar 的 int 的指针。

医生:

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

答: 不要。解释给初学者的指针,然后告诉他们如何用 C 语法表示他们的指针概念。


我最近很高兴地向一位 C 编程初学者解释了一些指针,并碰巧遇到了以下困难。

在我看来,C 语法并不可怕,但也并不完美: 如果您已经理解了指针,那么它既不是一个很大的障碍,也不会对学习它们有任何帮助。

因此: 从解释指针开始,并确保他们真正理解它们:

  • 用方框和箭头图解释它们。可以不使用十六进制地址,如果它们不相关,只显示指向另一个框或指向某个空符号的箭头。

  • 用伪代码解释: 只写 食肆地址储存在酒吧的价值

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

我怀疑 K & r 文本没有提供概念模型的原因是因为 他们已经明白了指示,而且可能认为当时其他所有有能力的程序员也都这么做了。这个助记符只是提醒我们从易于理解的概念到语法的映射。

简写的原因:

int *bar = &foo;

在你的例子中可能会混淆的是,它很容易被误解为等价于:

int *bar;
*bar = &foo;    // error: use of uninitialized pointer bar!

事实上表示:

int *bar;
bar = &foo;

这样写出来,将变量声明和赋值分开,就不存在这种混淆的可能性,K & R 语录中描述的使用 & harr; 声明并行性非常完美:

  • 第一行声明了一个变量 bar,这样 *bar就是 int

  • 第二行将 foo的地址分配给 bar,使得 *bar(int)成为 foo(也是 int)的别名。

在向初学者介绍 C 指针语法时,最初坚持这种将指针声明与赋值分离的方式可能会有所帮助,只有在 C 中指针使用的基本概念已经被充分内化之后,才会引入组合速记语法(对其可能造成混淆的可能性提出适当的警告)。

我宁愿把它看作是第一个 *应用于 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

为了让你的学生理解 *符号在不同上下文中的含义,他们必须首先理解上下文的确是不同的。一旦他们理解了上下文的不同(例如,作业的左边和一般表达式之间的区别) ,理解这些区别就不是一个认知上的飞跃了。

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

当他们理解上下文是不同的时候,您可以继续解释当 *符号在变量标识符前面的变量声明中时,它的意思是“声明这个变量作为一个指针”。然后你可以解释,当在一个表达式(作为一元运算符)中使用时,*符号是“解引用运算符”,它的意思是“地址处的值”,而不是它早期的意思。

为了真正说服你的学生,解释 C 语言的创造者可以使用任何符号来表示解引用运算符(也就是说,他们可以使用 @来代替) ,但是不管出于什么原因,他们做出了使用 *的设计决定。

总而言之,我们没有办法解释上下文是不同的。如果学生不理解上下文是不同的,他们不能理解为什么 *符号可以表示不同的东西。

如果问题出在语法上,那么显示带有 template/using 的等价代码可能会有所帮助。

template<typename T>
using ptr = T*;

这可以用作

ptr<int> bar = &foo;

然后,将普通/C 语法与仅使用 C + + 的方法进行比较,这对于解释常量指针也很有用。

声明不足

了解声明和初始化之间的区别是很好的。我们将变量声明为类型并用值初始化它们。如果我们同时做这两件事,我们通常称之为定义。

1. int a; a = 42;

int a;
a = 42;

然后我们给它一个值 42来初始化它。

2. int a = 42;

我们将 声明int命名为 ,并给它赋值42。它是用 42初始化的。

3. a = 43;

当我们使用变量时,我们说我们 手术他们。a = 43是一个赋值操作。我们将数字43赋给变量 a。

通过说

int *bar;

我们声明 酒吧是一个指向 int 的指针

int *bar = &foo;

我们声明 酒吧并用 的地址初始化它。

在初始化 酒吧之后,我们可以使用相同的操作符星号来访问和操作 的值。如果没有操作符,我们就访问和操作指针指向的地址。

除此之外,我让照片说话。

什么

对正在发生的事情进行一个简化的归纳(如果你想暂停等等,这里是一个 播放器版本)

ASCIIMATION

K & R 风格偏爱 int *p和 Stroustrup 风格偏爱 int* p是有原因的; 两种风格在每种语言中都是有效的(意思相同) ,但正如 Stroustrup 所说:

在“ int * p;”和“ int * p;”之间的选择不是关于对与错,而是关于风格和强调。强调表达; 声明常常被认为只不过是一种必要的罪恶。另一方面,C + + 非常强调类型。

现在,既然你们要在这里教 C 语言,那就意味着你们应该更多地强调表达式而不是类型,但是有些人可以更容易地理解一个强调比另一个更快,这是关于他们而不是语言。

因此,一些人会发现从 int*int是不同的东西这个想法开始比较容易,然后从这个想法开始。

如果有人确实很快理解了使用 int* barbar作为一个不是整型而是指向 int的指针来看待它的方法,那么他们将很快看到 *bar做点什么bar,其余的也会跟着看到。一旦你这样做了,你可以稍后解释为什么 C 程序员倾向于选择 int *bar

还是算了吧。如果有一种方法可以让每个人第一次理解这个概念,那么你一开始就不会遇到任何问题,而向一个人解释这个概念的最佳方法,未必就是向另一个人解释这个概念的最佳方法。

看看这里的答案和注释,似乎大家都认为有问题的语法可能会让初学者感到困惑。他们中的大多数人都提出了类似的建议:

  • 在显示任何代码之前,使用图表、草图或动画来说明指针是如何工作的。
  • 当提出语法时,解释星号符号的两种不同作用。许多教程都缺少或回避了这一部分。混淆随之而来(“当您将一个初始化的指针声明分解为一个声明和一个稍后的赋值时,您必须记住删除 *”-常见问题解答) 我希望能找到一个替代方法,但我想这是唯一的办法。

您可以用 int* bar代替 int *bar来突出显示差异。这意味着你不会遵循 K & R“声明模仿使用”的方法,但是 Stroustrup C + + 方法:

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

缺点:

  • 您必须警告您的学生关于多指针声明问题(int* foo, bar vs int *foo, *bar)。
  • 你得让他们做好 伤害的世界的准备。许多程序员希望看到变量名称旁边的星号,他们会花很长时间来证明他们的风格。许多样式指南明确地强制执行这种表示法(Linux 内核编码样式、 NASA C 样式指南等)。

编辑: 有人建议使用 不同的方法,它采用 K & R“模仿”的方式,但是没有“速记”语法(参见 给你)。一旦你的 省略在同一行中进行声明和赋值,一切将看起来更加连贯。

但是,学生迟早要把指针当作函数参数来处理。以及作为返回类型的指针。和函数指针。您必须解释 int *func();int (*func)();之间的区别。我想事情迟早会搞砸的。也许越快越好。

也许多走一步会让事情变得容易一些:

#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;
}

让他们告诉你他们希望每行的输出是什么,然后让他们运行程序,看看会出现什么。解释他们的问题(这里的裸体版本肯定会提示一些问题——但你可以稍后再考虑风格、严格性和可移植性)。然后,在他们的大脑因为过度思考而变得糊涂或者变成午饭后的僵尸之前,写一个接受值的函数,和一个接受指针的函数。

以我的经验来看,这是为了克服“为什么这个印刷是那样的?”那么立即展示了为什么这在函数参数中很有用(作为一些基本的 K & R 材料(如字符串解析/数组处理)的前奏) ,这不仅使课程有意义,而且坚持。

下一步是让他们向 解释 i[0]&i的关系。如果他们可以做到这一点,他们就不会忘记,你可以开始讨论结构,即使是提前一点点,这样就可以理解了。

上面关于方框和箭头的建议也是不错的,但是它也可能最终偏离到关于内存如何工作的全面讨论——这是一个必须在某个时刻发生的讨论,但是可能会转移人们的注意力: 如何解释 C 语言中的指针符号。

你应该指出一个初学者,* 在声明和表达式中有不同的意思。如你所知,* In 表达式是一元运算符,而 * In 声明不是运算符,只是一种结合类型的语法,让编译器知道它是指针类型。 最好是说一个初学者,”* 有不同的含义。为了理解 * 的含义,您应该找到使用 * 的位置

我觉得恶魔在太空里。

我会写(不仅为初学者,也为我自己) : Int * bar = & foo; 而不是 Int * bar = & foo;

这应该可以说明句法和语义之间的关系

混淆的根源在于 *符号在 C 语言中可以有不同的含义,这取决于它在哪里被使用。为了解释指向初学者的指针,*符号在不同上下文中的含义应该加以解释。

在宣言里

int *bar = &foo;

*的符号是 不是间接操作符。相反,它有助于指定 bar的类型,通知编译器 bar指向 int的指针。另一方面,当它出现在语句中时,*符号(用作 一元运算符一元运算符时)执行间接操作。因此,声明

*bar = &foo;

将是错误的,因为它将 foo的地址分配给 bar指向的对象,而不是 bar本身。

“也许把它写成 int * bar 会更明显地表明,星号实际上是类型的一部分,而不是标识符的一部分。” 是的。 我说,它有点像 Type,但只对一个指针名称有效。

当然,这会让你遇到不同的问题,比如 int * a,b

已经注意到 * 有多个角色。

还有一个简单的想法可以帮助初学者理解:

认为“ =”也有多个角色。

当赋值与声明在同一行上使用时,可以将其视为构造函数调用,而不是任意赋值。

当你看到:

int *bar = &foo;

认为它几乎等同于:

int *bar(&foo);

括号优先于星号,所以“ & foo”更容易直观地归于“ bar”而不是“ * bar”。

前几天我看到这个问题,正好在看 Go 博客上的 Go 类型声明的说明。它首先给出了一个 C 类型声明的说明,这似乎是一个有用的资源添加到这个线程,即使我认为有更完整的答案已经给出。

C 对声明语法采用了一种不同寻常的聪明方法。我们不用特殊的语法来描述类型,而是编写一个涉及被声明项的表达式,并说明该表达式将具有什么类型。因此

int x;

声明 x 为 int: 表达式‘ x’将具有 int 类型。一般来说,要想知道如何写入新变量的类型,可以写一个表达式,其中包含计算结果为基本类型的变量,然后将基本类型放在左边,表达式放在右边。

因此,声明

int *p;
int a[3];

声明 p 是一个指向 int 的指针,因为“ * p”的类型是 int,而 a 是一个 int 数组,因为 a [3](忽略特定的索引值,它被双关表示为数组的大小)的类型是 int。

(它继续描述如何将这种理解扩展到函数指针等)

这是我以前没有考虑过的一种方法,但是它似乎是一种非常简单的解释语法重载的方法。

我会解释整型数是对象,就像浮点数一样。指针是一种对象类型,其值表示内存中的地址(因此指针默认为 NULL)。

第一次声明指针时,使用的是类型指针名语法。它被读作“一个称为 name 的整数指针,可以指向任何整数对象的地址”。我们只在声明过程中使用这种语法,类似于我们将 int 声明为“ int num1”,但是我们只在需要使用该变量时使用“ num1”,而不是“ int num1”。

Int x = 5;//值为5的整数对象

Int * ptr;//一个默认值为 NULL 的整数

要使指针指向一个对象的地址,我们使用’&’符号,可以读作“地址的”。

Ptr = & x;//now 值是‘ x’的地址

由于指针只是对象的地址,为了得到该地址的实际值,我们必须使用’*’符号,当在指针前使用时,该符号意味着“指向的地址的值”。

Cout < < * ptr;//输出地址处的值

您可以简要地解释一下,‘ 是一个“运算符”,它用不同类型的对象返回不同的结果’运算符不再意味着“乘以”。

它有助于绘制一个图表,显示变量如何具有名称和值,指针如何具有地址(名称)和值,并显示指针的值将是 int 的地址。

基本上,指针不是一个数组指示符。 初学者很容易认为指针看起来像数组。 大多数字符串示例使用

“ char * pstr” 看起来很像

“ char str [80]”

但是,重要的是,在编译器的较低级别中,指针仅被视为整数。

让我们来看一些例子:

#include <stdio.h>
#include <stdlib.h>


int main(int argc, char **argv, char **env)
{
char str[] = "This is Pointer examples!"; // if we assume str[] is located in 0x80001000 address


char *pstr0 = str;   // or this will be using with
// or
char *pstr1 = &str[0];


unsigned int straddr = (unsigned int)pstr0;


printf("Pointer examples: pstr0 = %08x\n", pstr0);
printf("Pointer examples: &str[0] = %08x\n", &str[0]);
printf("Pointer examples: str = %08x\n", str);
printf("Pointer examples: straddr = %08x\n", straddr);
printf("Pointer examples: str[0] = %c\n", str[0]);


return 0;
}

结果如下: 0x2a6b7ed0是 str []的地址

~/work/test_c_code$ ./testptr
Pointer examples: pstr0 = 2a6b7ed0
Pointer examples: &str[0] = 2a6b7ed0
Pointer examples: str = 2a6b7ed0
Pointer examples: straddr = 2a6b7ed0
Pointer examples: str[0] = T

所以,基本上,请记住,指针是某种类型的整数。表示地址。

指针只是一个用来存储地址的变量。

计算机中的内存是由按顺序排列的字节(字节由8位组成)组成的。每个字节都有一个与之相关联的数字,就像数组中的索引或下标一样,这个数字称为字节的地址。字节的地址从0开始,小于内存大小。例如,在一个64 MB 的 RAM 中,有64 * 2 ^ 20 = 67108864个字节。因此,这些字节的地址将从0开始到67108863。

enter image description here

让我们看看声明变量时会发生什么。

整型标记;

正如我们所知道的,int 占用4个字节的数据(假设我们使用的是32位编译器) ,所以编译器从内存中连续保留4个字节来存储整数值。所分配的4个字节的第一个字节的地址称为变量标记的地址。假设连续4个字节的地址是5004、5005、5006和5007,那么变量标记的地址就是5004。 enter image description here

声明指针变量

如前所述,指针是存储内存地址的变量。就像其他变量一样,在使用指针变量之前,首先需要声明一个指针变量。下面是声明指针变量的方法。

语法: data_type *pointer_name;

Data _ type 是指针的类型(也称为指针的基类型)。 指针 _ name 是变量的名称,它可以是任何有效的 C 标识符。

让我们举几个例子:

int *ip;


float *fp;

Ip 意味着 ip 是一个能够指向 int 类型变量的指针变量。换句话说,指针变量 ip 只能存储 int 类型变量的地址。类似地,指针变量 fp 只能存储 float 类型变量的地址。变量的类型(也称为基类型) ip 是指向 int 的指针,fp 的类型是指向 float 的指针。类型为 int 的指针变量可以用符号表示为 (int *).类似地,指向 float 的指针类型的指针变量可以表示为(float *)

在声明一个指针变量之后,下一步是为它分配一些有效的内存地址。不应该在没有为指针变量分配有效内存地址的情况下使用指针变量,因为在声明之后它就包含了垃圾值,并且可能指向内存中的任何地方。使用未分配的指针可能会产生不可预测的结果。它甚至可能导致程序崩溃。

int *ip, i = 10;
float *fp, f = 12.2;


ip = &i;
fp = &f;

资料来源: 古鲁是迄今为止我所发现的最简单而详细的解释。