C言語のポインタ【階層ごとにポインタを理解できる超詳しく解説】

目次

1. ポインタについて

2. ポインタ型

1. 整数ポインターのアクセス権の説明:

2. 文字ポインタのアクセス権の説明:

3. ポインタのタイプにより、1 ステップがどれだけ進むか戻るかが決まります。

3. ワイルド ポインターに関する知識

1. ワイルドポインタの原因

①ポインタが初期化されていない

② ポインタ範囲外アクセス

③ポインタが指したスペースが解放される

2. ワイルドポインタを回避する方法

① ポインタを初期化する必要がある

②ポインタが境界を越える問題に注意

③ポインタの指す空間は時間とともにNULLになります

④ローカル変数のアドレスを返さないようにする

⑤ 使用前にポインタの有効性を確認してください

4. 定数ポインタとポインタ定数

1. 定数ポインタ

2. ポインタ定数

五、ポインタの操作 

1. ポインタ + - 整数

2. ポインターポインター

3. ポインタの関係演算

6. 文字ポインタ

定数文字列 (関連知識)

7、ポインタ配列

配列名の意味

8. 配列ポインタ

9、配列パラメータ、ポインタパラメータ

1. 1次元配列パラメータの受け渡し

2. 2次元配列パラメータの受け渡し

3. 第 1 レベルのポインタパラメータの受け渡し

4. 二次ポインタパラメータの受け渡し

10. 関数ポインタ

関数ポインタの名前変更 

11. 関数ポインタの配列


​​​​​​​

1. ポインタについて

まず、メモリは小さなメモリ ユニットに分割され、各メモリ ユニットにはアドレスと呼ばれる番号が割り当てられます次の図に示すように、アドレスのことをポインタとも呼びます。


注: ポインタはアドレスを指しますが、私たちの話し言葉でのポインタは通常、ポインタ変数を指します。

ポインタ変数はアドレスを格納するために使用される変数です。

次のようにポインター変数の値を変更することで、元の値を変更できます。

a の元の値は 5 ですが、ポインタ変数 pa を介して逆参照すると、a の値も変更される可能性があります。


ポインタのサイズについて: サイズ32 ビット プラットフォームでは4 バイト64 ビット プラットフォームでは8 バイトであり、ポインタの種類が変わってもサイズは変わりません。以下は、VS2019 の 32 ビット プラットフォーム環境でのデモです。

 整数ポインタであっても文字ポインタであっても、32 ビット プラットフォームではメモリ サイズは 4 バイトであることがわかります。

注: 32 ビット プラットフォームは 32 ビットで構成されているため、32 ビット = 4 バイトとなり、32 ビット プラットフォーム上のポインタのサイズは 4 バイトになります。これは 64 ビットにも当てはまります。


2. ポインタ型

ポインターの型によって、ポインターが逆参照されるときのアクセス許可が決まります。次に例を示します。

整数ポインタの逆参照は4 バイトにアクセスします

Char ポインタ逆参照は1 バイトにアクセスします

これら 2 種類のポインターのアクセス権を具体的に証明してみましょう。

1. 整数ポインターのアクセス権の説明:

a は 16 進数の 0x11223344 です。監視ページで、a が *pa と同じで、両方とも値であることを確認します。&a は pa と同じで、両方ともアドレスです。

 この時点でのメモリ内の の保存を観察します。

a のアドレスを入力すると、メモリが 44 33 22 11 として保存されていることがわかります。これには、前述したビッグ エンディアン ストレージとスモール エンディアン ストレージが関係します。知らない人は、ブログ「C 言語でのデータ ストレージ」にアクセスしてください。学ぶ

私たちの VS はリトル エンディアン ストレージを使用しているため、44 33 22 11 となり、*pa=0 まで実行した後、メモリを観察できます。

aの値が00 00 00 00に変更され、4バイトの値が変更されました 

2. 文字ポインタのアクセス権の説明:

以下の図に示すように、前と同じ順序で:

コードの残りの部分は変更されず、ポインタの型のみが変更されます。

 モニタリングはメモリ部と同じ

コードの実行時 *pa=0

文字ポインター型の場合、逆参照では16 進数 2 桁、つまり8 ビット、つまり1 バイトしか変更できないことがわかりました。

上記の 2 つの例は、ポインターのアクセス権が異なることを証明しています。

3. ポインタのタイプにより、1 ステップがどれだけ進むか戻るかが決まります。

具体的な意味は、次のようにポインタ変数が 1 ずつ加算または減算され、アドレスが変化することです。

整数ポインタ変数 pa文字ポインタ変数 pb がそれぞれ定義されており アドレスをそれぞれ出力するために %p が使用されています。実行結果を観察すると、次のことがわかります。

pa と pb はどちらも a のアドレスを指しているため、pa と pb のアドレスは同じです。

ただし、pa+1 と pb+1 はまったく異なります。Pa +1 は pa に 4 を加算しますが、pb+1 は pb に 1 を加算するだけです。この例から、ポインタの型も決定されることがわかります。ポインタは、1 歩前進または後退することで移動できる距離を示します。


3. ワイルド ポインターに関する知識

ワイルド ポインタとは、ポインタが未知の位置を指していることです。

1. ワイルドポインタの原因

①ポインタが初期化されていない

この種の問題は非常に一般的です。詳細については、次のコードを参照してください。

このエラーは実行後にも表示されます

 コードint* p は初期化されていないため、変数 p に格納されているアドレスは現在のプログラムの空間を指しているのではなく、メモリ内のランダムな空間を指しているため、*p を使用してこのメ​​モリ空間にアクセスするのは間違っているに違いありません。 !

ここで p はワイルド ポインタです

② ポインタ範囲外アクセス

この問題は非常に一般的であると言えます。注意を払わないと、配列を走査するときにこのような問題が発生することがよくあります。

配列 arr を設定し、ポインター変数 p を配列の最初の要素のアドレスと等しくします。配列自体には5 つの要素がありますが、 for ループで6 回ループします。i は 0 1 2 3 4 5、そして、i 自体は 0 ~ 4 です。はい、ループ内に余分な 5 があり、不正なメモリ アクセスにつながるため、6 番目の数値が乱数として出力されます。これはワイルド ポインタの問題でもあります。 

③ポインタが指したスペースが解放される

この問題も頻繁に発生します。詳細については、次の例を参照してください。

テスト関数の戻り値は x のアドレスです。main 関数ではポインタ変数 p を使用して x のアドレスを受け取りますが、x 変数はテスト関数に入って作成され。このとき、*p の値を変更する、つまり x アドレスを使用すると、メモリにアクセスすることはできなくなり、ワイルド ポインタの問題も発生します。

2. ワイルドポインタを回避する方法

① ポインタを初期化する必要がある

たとえば、上記の例では、int* p を直接使用することはできず、ワイルド ポインタの問題を回避するために、int a = 10; int* p = &a; のように初期化する必要があります。

②ポインタが境界を越える問題に注意

配列を使用するときは、不用意な範囲外のアクセスを避けるために、配列内の要素の数とループの回数に注意を払う必要があります。

③ポインタの指す空間は時間とともにNULLになります

ポインター変数 p を使用しない場合は、 int* p = NULL; make it empty、次に p を使用したい場合は、if ステートメントを使用します。

if(p != NULL) ……

ワイルドポインタをうまく回避できる

④ローカル変数のアドレスを返さないようにする

上記のワイルド ポインタの原因の 3 番目の例と同様に、ローカル変数のアドレスを返さないようにします。

⑤ 使用前にポインタの有効性を確認してください

if(p != NULL)のように ...ポインタの有効性をチェックしています


4. 定数ポインタとポインタ定数

このトピックでは、定数ポインターとポインター定数が多数出てきます。それらはすべて、キーワードconstに関連しています。違いを区別できるかどうかを確認するために、次の 3 つの例を示します:

const int *pa = &a;

int const *pa = &a;

int *const pa = &a;

1. 定数ポインタ

定数ポインタの概念は次のとおりです。ポインタが指す空間の値は変更できません。また、ポインタが指す空間の値はポインタを逆参照しても変更できませんが、ポインタの指す位置は変更できます。

2. ポインタ定数

ポインタ定数の概念は次のとおりです。ポインタ自体は定数です。つまり、ポインタのポイントは変更できませんが、ポインタが指す空間の値は変更でき、ポインタが指す空間の値は変更できます。ポインタは逆参照することで変更できます


それをよりよく理解するにはどうすればよいでしょうか?

まず、定数ポインターの名前を見てみましょう。これは pointer で終わるため、ポインターの性質を持ち、変更可能です。また、定数ポインターであるため、指す値は定数です。それは変更できません

ポインタ定数の名前に注目してください。この定数は constant で終わり、定数のいくつかのプロパティを持っているはずです。したがって、当然のことながら、ポインタのポイントは変更できず、ポインタが指す値のみが変更できます。

では、コードのどこに定数ポインターと呼ばれるために const が追加され、ポインター定数と呼ばれるために const が追加されるのはどこでしょうか?

ちょっとしたコツを言うと、誰もがすぐに思い出すことができます。

const が * の左側にある場合、それは定数ポインタです

const が * の右側にある場合、それはポインタ定数です

なぜこのように覚えておきたいのかというと、考えてみると * にはポインタ内で逆参照する機能があり、 * の左側に const があり、これは const の変更 *、つまり値であると理解できます。ポインタが指す空間は変更できませんが、ポインタのポイントは確かに無制限です

次に、const は * の右側にあり、ポインタ変数として変更されます。つまり、ポインタ変数はそのポイントを変更できませんが、ポインタが指す空間の値は制限されません

上記の 3 つの例には明らかに答えがあります。const の変更が * であるかポインター変数であるかを確認するだけで済みます。

定数ポインタ: const int *pa = &a;、int const *pa = &a;

ポインタ定数: int *const pa = &a;


五、ポインタの操作 

1. ポインタ + - 整数

ポインターによる整数の加算と減算の状況を簡単に説明する例を示します。

 ポインタ変数 p は、配列の最初の要素のアドレスです。for ループを使用して、配列の各要素を出力します。* の優先順位は ++ よりも高いです。そのため、*p を実行するたびに、 array, p++ は array の次の要素を指します。したがって、5 回ループして配列内の 5 つの要素を出力します。

2. ポインターポインター

 ポインタ-ポインタ操作は、ポインタとポインタの間の要素の数を取得します。もちろん、2 つのポインタが同じ空間を指していることが前提です。以下の例を参照してください。

具体的な図を説明すると次のようになります。

上の図に示すように、これはメモリ内の配列内の 5 つの要素の格納を表しており、p1 は最初の要素のアドレスを指しp2 は5 番目の要素のアドレスを表します。コード内で p2-p1 を使用することは、意味します。 2 つのポインターの間にあるということです。間の要素の数は4 つの要素があることは明らかなので、出力結果は 4 になります。

3. ポインタの関係演算

ポインターのリレーショナル操作は、ポインターのサイズを比較して、次のような特定の機能を実現することです。

 丸で囲んだ部分はポインタの関係演算であり、以下の図で明確に説明できます。

 p は先頭の配列の最初の要素のアドレスです。次に、 p< &arr[5]の比較を通じて、それが満たされる場合、アドレスの下の要素が出力され、次に p++ で次の比較が行われます。 p<&arr[5] が満たされなくなるまで実行されます

ここに尋ねたい友達がいるかもしれません。上記では境界を越えることはできないと言っていませんでしたか?配列には要素が 5 つしかなく、添字の最大数は 4 です。arr[5] はどのように使用できますか?実際には、 , ここには境界がありません。上記が使用されています。境界外のアドレスはすでにメモリに不正にアクセスされており、このアドレスはここに書き込まれるだけです。ポインタ変数 p は関係演算に使用され、このアドレスは使用されません。中古ですのでワイルドポインタ等の問題はございませんのでご安心ください♪(^∇^*)

規則: 配列要素へのポインタは、(上記の例のように) 配列の最後の要素の後のメモリ位置へのポインタと比較できますが、最初の要素より前のメモリ位置へのポインタとの比較はできません。


6. 文字ポインタ

文字ポインタは、格納された文字のアドレスです。つまり、文字変数のアドレスが文字ポインタに入れられます。たとえば、次のようになります。

a のアドレスを格納するポインタ変数 pa を定義し、*pa の値を変更すると、それに応じて a も変更されます。 

定数文字列 (関連知識)

このようなコードがある場合: char* pa = "abcde"; "abcde" ここは定数文字列で、"abcde" は定数領域に格納され、pa は文字列のアドレスを指します。

定数文字列は変更できません。 *pa='w' のようなコードを記述しようとすると、プログラムがクラッシュするため、保護のために前に const を追加し、 const char * pa = " abcde " (これは定数ポインタのタイプです)

そして、このコードは「abcde」を pa に格納するのではなく、ポインタ変数 pa に a のアドレスを格納します。次の例は、この知識をより明確に説明できます。

  

 pa を逆参照し、出力すると、それが実際に文字 a であることがわかり、次に pa+1 です。これは文字 b を指しているはずです。逆参照すると、出力された文字は確かに文字 b の値です。文字列全体を出力したい場合は、% を使用します。 s を直接、pa を印刷できます

これは非常に古典的な例です: p1 と p2、p3 と p4 が等しいかどうかを考えることができます。

答えは、p1 と p2 は等しいですが、p3 と p4 は等しくありません。

 上記の内容から、「abcde」は定数領域に格納されている定数文字列であることがわかります。そのため、p1 と p2 は両方ともこの定数文字列を指し、両方とも a のアドレスを格納しているため、p1 と p2 は等しいことがわかります。

そして、p3 と p4 はそれぞれ2 つの配列の初期化であるため、p3 と p4 はそれぞれ 2 つの配列の最初の要素のアドレスを表すため、p3 と p4 は異なります


7、ポインタ配列

ポインター配列に関してはあまり聞かないかもしれませんが、整数配列や文字配列などの語彙となるとよく聞くかもしれません。ではポインター配列とは一体何なのでしょうか?

最初に整数配列と文字配列について話します

整数配列: int arr[5]={1,2,3,4,5} のように、整数配列は整数を格納する配列であることがリテラルからわかります。arr配列には 5 つの要素があり、配列内の各要素の型は int です。

文字配列も整数配列と同じです。

これに関して言えば、ポインタ配列の定義は非常に簡単です。これは、ポインタを格納する配列です。たとえば、 int* arr[3]={&a,&b,&c} です。配列には 3 つの要素があり、各要素配列内は Pointer で、各要素の型は int* です。このような配列はポインター配列と呼ばれます。

ポインター配列の実際の応用をよりよく理解するのに役立つ事例を次に示します。

まず 3 つの整数配列 arr1、arr2、arr3 を作成し、次に3 つの整数配列の最初の要素のアドレスを格納するポインター配列 arr を作成しました(配列名は配列の最初の要素のアドレスであるため)。 will 3 integer 型配列の最初の要素のアドレスはポインタ配列 arr に格納されるため、整数配列の要素を表現したい場合、これは 2 次元配列を作成するのと同等であり、これを明確に示すことができます。次の図で:

arr 配列の各要素は int* 型で、arr[i] は arr 配列の要素、つまり arr 配列内の 3 つの整数配列の最初の要素のアドレス、およびarr[i][ j] は 3 整数配列内の添字 j を持つ整数要素であるため、このポインター配列の適用は 2 次元配列と同等です。

別の表現方法は *(arr[i]+j) です。arr[i] は arr 配列の要素、つまり 3 つの整数配列の最初の要素のアドレスを意味し、+j はそのアドレスから逆方向を意味します。最初の要素 j 要素のアドレスを移動し、それらを逆参照して出力します。これも整数配列の要素です

arr1 配列の例を挙げてみましょう。誰でもより明確になるでしょう。

コード内の 2 つの印刷メソッドの説明はこれです。理由は皆さんも理解していると思います。

配列名の意味

配列と言えば、配列名の意味について話さなければなりません。

一般に、配列名は配列の最初の要素のアドレスです。

ただし、2 つの例外もあります。

1. sizeof (配列名)、この場合、配列名は配列の最初の要素のアドレスではありません、このとき、配列名は配列全体を表し、計算は配列全体のサイズになります

2. & 配列名、この場合、配列名は最初の要素のアドレスではなく、配列全体のアドレスでもあり、配列全体のアドレスが取り出されます。

理解しやすいように下に絵を描いてください

 図からわかるように、&arr[0]、arr、&arr の値は配列の最初の要素のアドレスですが、それらが表す意味は異なります。 1、整数はスキップされます。 type element は、次の整数要素のアドレスを指します。&arr が +1 の場合、配列をスキップし、要素 8 の次の位置のアドレスを指します。

 コードを通して、今導かれた結論をはっきりと観察できます。

 以上が配列名の意味についての知識の追加です♪(^∇^*)


8. 配列ポインタ

ポインタ配列について話したので、次は配列ポインタについて話しましょう ポインタ配列は配列なので、配列ポインタはポインタです。

以前に整数ポインタについて学習しましたが、 int* p=&a は整数へのポインタであり、整数変数のアドレスを格納します。文字ポインタは文字へのポインタで、文字変数のアドレスを格納します。配列ポインタは当然配列を指します

次に、例を挙げます。

int* p1[3];

int (*p2)[3];

上の 2 つの例のうち、どちらが配列ポインターで、どちらがポインターの配列ですか?

まず、前に学習した配列名である p1 と p2 を確認し、最初の int *p1[3] を確認します。[] は *p1 よりも優先順位が高いため、角括弧に遭遇すると、最初に次の値と一致ます角括弧 括弧は結合されているため、これは 3 つの要素を持つ配列であり、p1 の前の int* を観察すると、この配列の各要素の型がint*であることがわかります。つまり、これはポインターの配列です。

2 番目の int(*p2)[3]; を見ると、p2 は最初に * と結合されており、p2 はポインター変数であるため、これはサイズ 3 の配列へのポインターであり、各要素の型は int であるため、これは配列ポインタ

まず、整数ポインタの場合、int a = 5; int* pa = &a、整数変数 a が与えられた場合、a のアドレスを取得し、それを整数ポインタ pa に置きます(ポインタ変数 pa の型は int* です)。 int * pa ですpaの変数名を除いた残りがこのポ​​インタの型です

同様に、 int arr[3]={0}; int (*parr) [3] = &arr ;要素が 3 つある整数配列 arr が与えられた場合、配列のアドレスは配列内のポインター内に配置される必要があります。 parr の場合、問題は、配列ポインタ parr の型は何かということです。非常に簡単で、型に応じて取得できる int(*parr)[3]、つまり int(*)[3] からポインタ変数の名前を除いたものを取得できます。ポインタは配列を指しており、配列には 3 つの要素があり、各要素は int 型です。

以下に例を示します。

上記によれば、pa は変数 a のアドレスで、その型は int* であるため、アドレスを出力すると、pa+1 のアドレスは 00A8FB54 から 00A8FB58 に増加し、4 バイトのサイズが増加します。整数)、一方、parr これはストレージ配列 arr のアドレスであり、型は int (*) [3] であるため、当然のことながら、parr+1 のアドレスは、parr のアドレスと比較して 00A8FB34 から 00A8FB40 に変更されます。これにより、サイズが 12 バイト (3 つの整数 Size) 増加します。 このポインター変数が指す配列には 3 つの要素があり、各要素の型は int であるため、parr+1 は 3 つの整数変数のアドレスをスキップします。


9、配列パラメータ、ポインタパラメータ

コードでは、配列とポインタを関数に渡す必要があります。これには、配列パラメータとポインタ パラメータの概念が含まれます。

1. 1次元配列パラメータの受け渡し

arr1とarr2という1次元配列があり、test1とtest2にパラメータを渡しているのですが、testの仮パラメータはどのように書けばよいのでしょうか?

配列の名前を渡すことは、グループの最初の要素のアドレスを渡すことと同じであるため、仮パラメータは配列の形式またはポインタの形式で記述することができます。

Arr1 配列パラメータの受け渡し:

配列の形式: void test1(int arr1[]) または void test1(int arr1[5])

各要素の型は int であるため、ポインタは int* になります。

ポインタの形式: void test1(int* p1)

Arr2 配列パラメータの受け渡し:

配列の形式: void test2(int* arr2[]) または void test2(int* arr2[15])

各要素の型は int* なので、ポインタは int** になります。

ポインタの形式: void test2(int** p2)

2. 2次元配列パラメータの受け渡し

1 次元配列パラメータの受け渡しの基礎として、2 次元配列パラメータの受け渡しには 2 つの形式があります

2次元配列パラメータに注意してください。行は省略できますが、列は省略できません。列を省略すると、各列の具体的な番号が分からなくなり、メモリ配置に問題が発生するためです。 、メモリは1行目と2行目なので連続的に保存されます

配列形式: void テスト (int arr[][3]) または void テスト (int arr[2][3])

配列名は配列の最初の要素のアドレスであり、2 次元配列の最初の要素のアドレスは 1 次元配列の最初の行のアドレスであるため、 array は配列ポインタ: int(*)[3]、仮パラメータは Array pointer p: int(*p)[3] である必要があります。

ポインタの形式: void test(int(*p)[3])

3. 第 1 レベルのポインタパラメータの受け渡し

第 1 レベルのポインターはパラメーターを渡し、第 1 レベルのポインターを渡し、その後第 1 レベルのポインターを使用して受け取ります

test のパラメータが int* ptr の場合、関数は次の 3 つのパラメータを受け取ることができます。

①test(arr)②test(p)③int a = 0がある;aのアドレスをtest(&a)に渡すことができる

4. 二次ポインタパラメータの受け渡し

第 2 レベルのポインタがパラメータとして渡され、第 2 レベルのポインタが渡されて、第 2 レベルのポインタが受信に使用されます。

同様に、test のパラメータが int** ptr の場合、関数は次の 3 つのパラメータを受け取ることができます。

1 つと 2 つは理解しやすいです。3 つ目を見てください。arr 配列には 5 つの要素があり、各要素の型は int* です。arr を渡すことは、配列の最初の要素のアドレスを渡すことと同じであり、最初の要素の型が int* である場合、int* のアドレスは int** となり、これも要件を満たします。 


10. 関数ポインタ

関数ポインタは関数へのポインタです。関数ポインタの例は次のとおりです。

ここで p は関数ポインタです

まず、p と * が括弧内で結合されており、p がポインターであり、関数のパラメーターの型が括弧内で int と int であることを示しています。 

ここでも同様に、p は関数ポインタですが、ここでは &print ではなく print です。これら 2 つの方法の意味は同じであるためです。 

呼び出し時に上記の両方を関数に転送できます

関数ポインタの名前変更 

関数ポインタの型が int (*)(int,char) で、長すぎて複雑すぎる場合は、typedef を使用して名前を変更できますが、ここでの名前変更は以前の名前付け形式とは異なるため、名前は次のようにする必要があります。 * に配置 後者、つまり typedef int (*p)(int,char) のコードの意味は、関数ポインタ type int (*)(int,char) の名前を p に変更することです。


11. 関数ポインタの配列

関数ポインター配列は配列であり、関数ポインターは次のように配列に格納されます。

int(*ptr[3])(int, int) は関数ポインタ配列の使用です。ptr は関数ポインタ配列です。配列名 ptr と [3] を削除すると、配列内の各要素が int であることがわかります。 (*)(int, int ) 関数ポインターなので、関数ポインターの配列と呼ばれます。


ポインタについては以上です。興味があればさらに調べてください。O(∩_∩)O

おすすめ

転載: blog.csdn.net/m0_64411530/article/details/125584522