簡體   English   中英

如何向初學者解釋 C 指針(聲明與一元運算符)?

[英]How to explain C pointers (declaration vs. unary operators) to a beginner?

我最近有幸向 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 will return an int ”? 但是為什么聲明之后的賦值不遵循這種模式呢? 如果初學者想初始化變量怎么辦? int *ip = 1 (閱讀: *ip將返回一個int並且int1 )不會按預期工作。 概念模型似乎並不連貫。 我在這里錯過了什么嗎?


編輯:它試圖在這里總結答案

簡寫的原因:

int *bar = &foo;

在您的示例中可能會令人困惑的是,很容易將其誤讀為等效於:

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

當它實際上意味着:

int *bar;
bar = &foo;

像這樣寫,變量聲明和賦值分開,沒有這種混淆的可能性,並且在你的 K&R 引用中描述的 use ↔ 聲明並行非常有效:

  • 第一行聲明了一個變量bar ,這樣*bar是一個int

  • 第二行將foo的地址分配給bar ,使*bar (一個int )成為foo (也是一個int )的別名。

在向初學者介紹 C 指針語法時,最初堅持這種將指針聲明與賦值分開的風格可能會有所幫助,並且只有在指針使用的基本概念在C已被充分內化。

為了讓您的學生在不同的上下文中理解*符號的含義,他們必須首先了解上下文確實是不同的。 一旦他們理解上下文是不同的(即作業的左側和一般表達之間的差異),理解差異是什么並不是太多的認知飛躍。

首先解釋變量的聲明不能包含運算符(通過證明在變量聲明中放置-+符號只會導致錯誤來證明這一點)。 然后繼續證明表達式(即在賦值的右側)可以包含運算符。 確保學生理解表達式和變量聲明是兩個完全不同的上下文。

當他們理解上下文不同時,您可以繼續解釋當*符號位於變量標識符前面的變量聲明中時,它的意思是“將此變量聲明為指針”。 然后你可以解釋當在表達式中使用時(作為一元運算符) *符號是“解引用運算符”,它的意思是“地址處的值”而不是它的早期含義。

為了真正說服你的學生,解釋一下 C 的創建者可以使用任何符號來表示取消引用運算符(即他們可以使用@代替),但無論出於何種原因,他們做出了使用*的設計決定。

總而言之,沒有辦法解釋上下文不同。 如果學生不理解上下文不同,他們就無法理解為什么*符號可以表示不同的含義。

短聲明

很高興知道聲明和初始化之間的區別。 我們將變量聲明為類型並用值初始化它們。 如果我們同時進行這兩項工作,我們通常稱其為定義。

1. int a; a = 42; int a; a = 42;

int a;
a = 42;

我們聲明一個名為aint 然后我們通過給它一個值42初始化它。

2. int a = 42;

我們聲明一個名為a 的int並賦予它值 42。它被初始化為42 一個定義。

3. a = 43;

當我們使用變量時,我們說我們它們進行操作 a = 43是賦值操作。 我們將數字 43 分配給變量 a。

通過說

int *bar;

我們將bar聲明為一個指向 int 的指針。 通過說

int *bar = &foo;

我們聲明bar並用foo的地址初始化它。

初始化bar 后,我們可以使用相同的運算符星號來訪問和操作foo的值。 沒有操作符,我們訪問和操作指針指向的地址。

除此之外,我讓圖片說話。

什么

關於正在發生的事情的簡化 ASCIIMATION。 (如果你想暫停等,這里有一個播放器版本

ASCII化

第二條語句int *bar = &foo; 可以在內存中以圖形方式查看,

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

現在bar是一個包含地址&int類型的指針foo 使用一元運算符*我們尊重使用指針bar檢索包含在 'foo' 中的值。

編輯:我對初學者的方法是解釋變量的memory address ,即

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. 

Introducing pointers:如上面所說的變量,例如

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

可以分配b = a但不能分配b = a b = &a ,因為變量b可以保存值但不能保存地址,因此我們需要Pointers

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

看看這里的答案和評論,似乎有一個普遍的共識,即所討論的語法可能會讓初學者感到困惑。 他們中的大多數人提出了一些類似的建議:

  • 在顯示任何代碼之前,使用圖表、草圖或動畫來說明指針的工作原理。
  • 在呈現語法時,解釋星號符號的兩種不同作用 許多教程都缺少或回避了該部分。 混亂隨之而來(“當您將初始化的指針聲明分解為聲明和稍后的賦值時,您必須記住刪除 *” – comp.lang.c FAQ我希望找到一種替代方法,但我想這是要走的路。

你可以寫int* bar而不是int *bar來突出差異。 這意味着您不會遵循 K&R“聲明模擬使用”方法,而是使用Stroustrup C++ 方法

我們不將*bar聲明為整數。 我們將bar聲明為int* 如果我們想在同一行初始化一個新創建的變量,很明顯我們正在處理bar ,而不是*bar int* bar = &foo;

缺點:

  • 你必須警告你的學生關於多指針聲明問題( int* foo, bar vs int *foo, *bar )。
  • 你必須讓他們為受傷世界做好准備。 許多程序員希望看到變量名旁邊的星號,他們會不遺余力地證明自己的風格。 並且許多樣式指南明確地強制執行此表示法(Linux 內核編碼樣式、NASA C 樣式指南等)。

編輯:建議的另一種方法是采用 K&R“模仿”方式,但沒有“速記”語法(請參閱此處)。 一旦您省略在同一行中進行聲明和賦值,一切都會看起來更加連貫。

然而,學生遲早將不得不將指針作為函數參數來處理。 和指針作為返回類型。 和指向函數的指針。 您將不得不解釋int *func();之間的區別int *func(); int (*func)(); . 我想事情遲早會分崩離析。 也許越早越好。

K&R 風格偏愛int *p和 Stroustrup 風格偏愛int* p 兩者在每種語言中都是有效的(並且意思相同),但正如 Stroustrup 所說:

"int* p;" 之間的選擇和“int * p;” 不是關於對與錯,而是關於風格和重點。 C 強調表達式; 聲明通常被認為只不過是一種必要的罪惡。 另一方面,C++ 非常重視類型。

現在,既然你試圖在這里教 C,那意味着你應該更多地強調表達式,而不是類型,但有些人可以更容易地理解一個重點而不是另一個重點,這是關於它們而不是語言。

因此,有些人會發現很容易開始的想法是一個int*是不同的東西比int從那里走。

如果有人確實很快理解了使用int* barbar作為不是 int 而是指向int的指針的查看方式,那么他們很快就會看到*bar正在對bar做一些事情,並且其余的將隨之而來。 完成后,您可以稍后解釋為什么 C 編碼人員傾向於更喜歡int *bar

或不。 如果有一種方法讓每個人都首先理解這個概念,那么你一開始就不會有任何問題,向一個人解釋它的最佳方式不一定是向另一個人解釋它的最佳方式。

tl;博士:

問:如何向初學者解釋 C 指針(聲明與一元運算符)?

答:不要。 向初學者解釋指針,然后向他們展示如何用 C 語法表示他們的指針概念。


我最近有幸向 C 編程初學者解釋指針,並偶然發現了以下困難。

IMO 的 C 語法並不糟糕,但也不是很好:如果您已經理解指針,它既不是一個很大的障礙,也不是學習它們的任何幫助。

因此:從解釋指針開始,並確保他們真正理解它們:

  • 用方框和箭頭圖解釋它們。 您可以在沒有十六進制地址的情況下執行此操作,如果它們不相關,只需顯示指向另一個框或某個空符號的箭頭即可。

  • 用偽代碼解釋:只寫foo 的地址存儲在 bar 中的值

  • 然后,當您的新手了解什么是指針,為什么以及如何使用它們時; 然后顯示到 C 語法的映射。

我懷疑 K&R 文本沒有提供概念模型的原因是他們已經理解了指針,並且可能假設當時所有其他有能力的程序員也理解了。 助記符只是提醒人們從易於理解的概念到語法的映射。

這個問題在開始學習 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相同。

    • 在給定的編譯器上, int要么是short int要么是long int要么是long 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 ,但你應該非常小心改變的值*a

  4. 類型void*是個例外,因為它沒有可以使用的相應“值類型”(即,您不能聲明void a )。 此類型僅用作指向內存地址的通用指針,而不指定駐留在該地址中的數據類型。

也許多走一步會讓它更容易:

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

讓他們告訴您他們期望每一行的輸出是什么,然后讓他們運行程序並查看結果。 解釋他們的問題(那里的裸版肯定會提示一些——但你可以在以后擔心樣式、嚴格性和可移植性)。 然后,在他們的頭腦因過度思考而變得糊塗或成為午餐后僵屍之前,編寫一個接受值的函數,以及接受指針的同一個函數。

根據我的經驗,它克服了“為什么這種打印方式?” hump,然后通過動手玩弄(作為一些基本 K&R 材料的前奏,如字符串解析/數組處理)立即展示為什么這在函數參數中很有用,這使課程不僅有意義而且堅持。

下一步是讓他們向解釋i[0]&i 如果他們能做到這一點,他們就不會忘記它,你可以開始談論結構,甚至提前一點,這樣它就會深入人心。

上面關於盒子和箭頭的建議也很好,但它也可能最終離題,進入關於內存如何工作的全面討論——這是一個必須在某個時候發生的談話,但可能會分散手頭的注意力:如何解釋 C 中的指針符號。

表達式*bar的類型是int 因此,變量(和表達式) barint * 由於變量具有指針類型,因此其初始值設定項也必須具有指針類型。

指針變量初始化和賦值存在不一致; 這只是必須通過艱苦的方式學習的東西。

int *bar = &foo;

Question 1 :什么是bar

Ans :它是一個指針變量(輸入int )。 一個指針應該指向某個有效的內存位置,然后應該使用一元運算符*取消引用 (*bar) 以讀取存儲在該位置的值。

Question 2 :什么是&foo

Ans :foo是類型的變量int和。其中存儲在一些有效的存儲位置,該位置我們從運營商處獲得它&現在這樣,我們所擁有的是一些有效的內存位置&foo

所以兩者放在一起,即指針需要的是一個有效的內存位置,這是由&foo獲得的&foo所以初始化很好。

現在指針bar指向有效的內存位置,存儲在其中的值可以取消引用它,即*bar

我寧願將其閱讀為第一個*適用於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

您應該指出*在聲明和表達式中具有不同含義的初學者。 如你所知,表達式中的*是一元運算符,而聲明中的*不是運算符,只是一種與類型結合的語法,讓編譯器知道它是一個指針類型。 初學者最好說“*有不同的含義。要理解*的含義,您應該找到*的使用位置”

我認為魔鬼在太空中。

我會寫(不僅為初學者,也為我自己): int* bar = &foo; 而不是 int *bar = &foo;

這應該表明句法和語義之間的關系是什么

已經注意到 * 有多個角色。

還有一個簡單的想法可以幫助初學者掌握事物:

認為“=”也有多個角色。

當賦值與聲明在同一行使用時,將其視為構造函數調用,而不是任意賦值。

當你看到:

int *bar = &foo;

認為它幾乎相當於:

int *bar(&foo);

括號優先於星號,因此“&foo”更容易直觀地歸因於“bar”而不是“*bar”。

如果問題出在語法上,使用模板/使用顯示等效代碼可能會有所幫助。

template<typename T>
using ptr = T*;

這可以用作

ptr<int> bar = &foo;

之后,將普通/C 語法與這種僅限 C++ 的方法進行比較。 這對於解釋常量指針也很有用。

混淆的根源在於*符號在 C 中可能具有不同的含義,具體取決於使用它的事實。 為了向初學者解釋指針,應該解釋*符號在不同上下文中的含義。

在聲明中

int *bar = &foo;  

*符號不是間接運算符 相反,它有助於指定bar的類型,通知編譯器bar指向int指針 另一方面,當它出現在語句中時, *符號(當用作一元運算符時)執行間接操作。 因此,聲明

*bar = &foo;

將是錯誤的,因為它將foo的地址分配給bar指向的對象,而不是bar本身。

“也許把它寫成 int* bar 會更明顯地表明星號實際上是類型的一部分,而不是標識符的一部分。” 所以我願意。 我說,它有點像 Type,但只針對一個指針名稱。

“當然,這會讓你遇到像 int* a, b 這樣不直觀的東西的不同問題。”

前幾天看到這個問題,后來正好在看Go Blog上對Go的類型聲明的解釋。 它首先介紹了 C 類型聲明,這似乎是添加到此線程的有用資源,盡管我認為已經給出了更完整的答案。

C 對聲明語法采用了一種不同尋常且巧妙的方法。 不是用特殊的語法描述類型,而是編寫一個涉及被聲明項的表達式,並說明該表達式將具有什么類型。 因此

int x;

將 x 聲明為 int:表達式 'x' 將具有 int 類型。 通常,要弄清楚如何編寫新變量的類型,請編寫一個涉及該變量的表達式,該表達式的計算結果為基本類型,然后將基本類型放在左側,將表達式放在右側。

因此,聲明

int *p; int a[3];

聲明 p 是一個指向 int 的指針,因為 '*p' 的類型是 int,而 a 是一個 int 數組,因為 a[3](忽略特定的索引值,它被認為是數組的大小)具有類型內部

(它繼續描述如何將這種理解擴展到函數指針等)

這是一種我以前沒有考慮過的方法,但它似乎是一種非常直接的解釋語法重載的方法。

在這里你要使用、理解和解釋編譯器邏輯,而不是人的邏輯(我知道,是人,但在這里你必須模仿計算機......)。

當你寫

int *bar = &foo;

編譯器將其分組為

{ int * } bar = &foo;

即:這是一個新變量,它的名字是bar ,它的類型是指向 int 的指針,它的初始值是&foo

並且您必須添加:上面的=表示初始化而不是做作,而在以下表達式中*bar = 2; 做作

每條評論編輯:

注意:在多重聲明的情況下, *僅與以下變量相關:

int *bar = &foo, b = 2;

bar 是一個指向 int 的指針,由 foo 的地址初始化,b 是一個初始化為 2 的 int,而 in

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

bar in 仍然是指向 int 的指針,而 p 是指向初始化為地址或 bar 的 int 指針的指針。

基本上指針不是數組指示。 初學者很容易認為指針看起來像數組。 大多數字符串示例使用

"char *pstr" 它看起來很相似

“字符 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

所以,基本上,請記住指針是某種整數。 介紹地址。

我將解釋 int 是對象,浮點數等也是。指針是一種對象類型,其值表示內存中的地址(因此指針默認為 NULL )。

當你第一次聲明一個指針時,你使用 type-pointer-name 語法。 它被讀作“名為 name 的整數指針,可以指向任何整數對象的地址”。 我們只在聲明期間使用這種語法,類似於我們如何將 int 聲明為“int num1”,但我們只在想要使用該變量時使用“num1”,而不是“int num1”。

整數 x = 5; // 一個值為 5 的整數對象

int * ptr; // 默認值為 NULL 的整數

為了使指針指向對象的地址,我們使用“&”符號,該符號可以讀作“的地址”。

ptr = &x; // 現在值是 'x' 的地址

由於指針只是對象的地址,要獲得該地址的實際值,我們必須使用“*”符號,當在指針之前使用時,它表示“指向的地址處的值”。

std::cout << *ptr; // 打印出地址處的值

您可以簡要說明“ ”是一個“運算符”,它對不同類型的對象返回不同的結果。 當與指針一起使用時,' ' 運算符不再意味着“乘以”。

它有助於繪制一個圖表,顯示變量如何具有名稱和值以及指針如何具有地址(名稱)和值,並表明指針的值將是 int 的地址。

指針只是用於存儲地址的變量。

計算機中的內存由按順序排列的字節(一個字節由 8 位組成)組成。 每個字節都有一個與之關聯的數字,就像數組中的索引或下標一樣,稱為字節的地址。 字節地址從 0 開始,比內存大小小 1。 例如,假設在 64MB 的 RAM 中,有 64 * 2^20 = 67108864 bytes 。 因此,這些字節的地址將從 0 開始到 67108863 。

在此處輸入圖片說明

讓我們看看當你聲明一個變量時會發生什么。

整數標記;

我們知道一個 int 占用 4 個字節的數據(假設我們使用的是 32 位編譯器),因此編譯器從內存中保留 4 個連續字節來存儲整數值。 4 個分配字節的第一個字節的地址稱為變量標記的地址。 假設連續 4 個字節的地址是 5004 、 5005 、 5006 和 5007 ,那么變量標記的地址將是 5004 。 在此處輸入圖片說明

聲明指針變量

如前所述,指針是一個存儲內存地址的變量。 就像任何其他變量一樣,您需要先聲明一個指針變量,然后才能使用它。 以下是聲明指針變量的方法。

語法: data_type *pointer_name;

data_type 是指針的類型(也稱為指針的基類型)。 pointer_name 是變量的名稱,它可以是任何有效的 C 標識符。

讓我們舉一些例子:

int *ip;

float *fp;

int *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;

資料來源: thecguru是迄今為止我發現的最簡單但詳細的解釋。

暫無
暫無

聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.

 
粵ICP備18138465號  © 2020-2024 STACKOOM.COM