みなさん、こんにちは。今日は C 言語のポインタについて学習します。今日は主に初歩的なポインタを学習し、後で高度なポインタを学習します。
目次
1. ポインタとは
ポインタとは何ですか?
C では、ポインタは別の変数のメモリ アドレスを格納できる特別な変数です。ポインタ変数を使用すると、メモリに格納されているデータにアクセスしたり、データを変更したりできます。
変数のアドレスはポインタ変数に格納され、ポインタが指す変数の値には逆参照演算子 (*) を使用してアクセスできます。たとえば、整数変数とその変数へのポインタは、次のように宣言して使用できます。
この例では、 という名前の整変数を定義し a
、その値は です 10
。次に、 を 指すa
ポインタ変数を 定義しp
、アドレス演算子を使用して アドレスを &
取得し 、そのアドレスをポインタに格納します 。最後に、逆参照演算子を介して ポインタが指す変数の値を出力します 。つまり、 変数 の値 が出力されます。a
p
*
p
a
10
ポインタを理解するための 2 つの重要なポイント:
1. ポインタはメモリ内の最小単位の番号、つまりアドレスです。
2. 話し言葉におけるポインタは通常、メモリアドレスを格納するために使用される変数であるポインタ変数を指します。
次の図を使用してメモリを理解できます。
- 32 ビット マシンでは、アドレスは 32 個の 0 または 1 で構成されるバイナリ シーケンスであり、アドレスは 4 バイトに格納する必要があるため、ポインター変数のサイズは 4 バイトにする必要があります。
- 64 ビット マシン上で 64 のアドレス行がある場合、アドレスを格納するためのポインター変数のサイズは 8 バイトになります。
ポインターのサイズ:
int main()
{
printf("%d\n", sizeof(char*));
printf("%d\n", sizeof(short*));
printf("%d\n", sizeof(int*));
printf("%d\n", sizeof(long*));
printf("%d\n", sizeof(float*));
printf("%d\n", sizeof(double*));
return 0;
}
32 ビット マシンでは、ポインターのサイズは 4 バイトです
64 ビット マシンでは、ポインターのサイズは 8 バイトです
要約する
ポインタ変数は、メモリ ユニットを一意に識別するアドレスを格納するために使用されます。
ポインタのサイズは、32 ビット プラットフォームでは 4 バイト、64 ビット プラットフォームでは 8 バイトです。
2. ポインタとポインタの型
ここでは次のことについて議論します。
ポインタの種類
変数には整数、浮動小数点などのさまざまな型があることは誰もが知っています。ポインタには型がありますか?
正確に言うと、はい。
char*型のポインタは、char型変数のアドレスを格納するために使用されます。
short* 型ポインタは、short 型変数のアドレスを格納するために使用されます。
int* 型のポインタは、int 型の変数のアドレスを格納するために使用されます。
ポインタ型の意味は何ですか?
機能 1:
int* 型のポインター pa を使用して a のアドレスを受け取る場合、*pa が変更されると、a の 4 バイトの内容が変更されます。
char型のポインタpaを使用してaのアドレスを受け取る場合、*paを変更すると1バイトの内容のみが変更されます。
Int* ポインタ逆参照は 4 バイトにアクセスし、char* 型ポインタ逆参照は 1 バイトにアクセスします
概要: ポインターのタイプによって、ポインターを逆参照する際の権限 (操作できるバイト数) が決まります。
たとえば、char* ポインターの逆参照では 1 バイトしかアクセスできませんが、int* ポインターの逆参照では 4 バイトにアクセスできます。
残りは 2 つです:
次のコードを観察してください。
私たちは次のことを発見します:
int* 型の pa に 1 を加算し、アドレスを逆方向に 4 ビット増加します。
char*型のpbを1加算し、アドレスを後方に1ビットインクリメントします
概要: ポインターのタイプによって、ポインターが 1 歩前進または後退する量 (距離) が決まります。
2.1 ポインタ + - 整数
C言語ではポインタは整数の加減算を行うことができますが、この演算の意味はポインタの指すアドレスに整数を加算または減算することでポインタの位置の移動を実現することです。
以下にサンプルコードを示します。
この例では、整数配列 a と文字ポインター s を定義します。
整数ポインタ p の場合、それは配列 a の最初の要素を指し、ポインタの加算を使用して p をインクリメントして配列 a の 2 番目の要素を指すことができます。
文字ポインタ s の場合、文字列の最初の文字を指し、ポインタの加算によって s が 2 増加して、文字列の 3 番目の文字を指します。
配列にはポインタを使用してアクセスします。
int main()
{
int arr[10] = { 0 };
int* p = &arr[0];
int i = 0;
int sz = sizeof(arr) / sizeof(arr[0]);
for (i = 0; i < sz; i++)
{
*(p + i) = i;
}
for (i = 0; i < sz; i++)
{
printf("%d ", *(p + i));
}
}
このコードでは、ポインター変数 p を使用して配列要素に直接アクセスします。これは、p + i が指すメモリー位置が配列 arr[i] のアドレスであることを意味します。このアドレスに対して代入演算を実行して、配列値の変更を実現します。同時に、ポインター加算を使用すると、配列要素をより簡単に走査できます。
ポインタによる整数の加算および減算の操作は、配列の境界を越えて不正なメモリ位置にアクセスする場合があることに注意してください。したがって、ポインタによって整数を加算および減算する場合、ポインタが境界を越えないことを確認する必要があります。
2.2 ポインタの逆参照
このコードは、整変数 n の上位/下位ビットを操作し、メモリ内の変化を観察する方法を示しています。コードでは整数変数 n を定義し、16 進数値 0x11223344 を代入し、char 型を指すポインタ pc と int 型を指すポインタ pi を使用して n を演算しています。
まず、ポインタpcをnに指し、char型へのポインタに変換します。このようにして、char 型は 1 バイトしか占有しないため、PC は n 個の上位/下位ビットを 1 つずつ操作できます。次に、pc が指す最初のバイトを 0 に割り当てます。これにより、n の最下位バイトが 0 に設定され、他のバイトは変更されません。
次に、ポインタ pi を介して整数変数 n を操作します。pi は整数変数 n へのポインタであるため、n 全体を操作できます。pi が指す値に 0 を代入すると、n の値全体が 0 に設定されます。
整数変数 n の上位/下位ビットを操作するときは、マシンのエンディアンを考慮する必要があり、考慮しないとデータの読み取りと書き込みで問題が発生する可能性があることに注意してください。
要約:
ポインターの型によって、ポインターを逆参照する際の権限 (操作できるバイト数) が決まります。たとえば、char* ポインターの逆参照では 1 バイトしかアクセスできませんが、int* ポインターの逆参照では 4 バイトにアクセスできます。
3. ワイルドポインター
概念: ワイルド ポインターは、ポインターが指す場所が不明であることを意味します (ランダム、不正確、明確な制限がない)。
ワイルド ポインタとは、初期化されていないポインタまたは解放されたポインタを指し、メモリ内の不明なアドレスまたはアクセスできないアドレスを指します。ワイルド ポインターが出現すると、プログラムのクラッシュやメモリ リークなどの問題が発生する可能性があります。
3.1 ワイルドポインタの原因
1. ポインタに初期値が割り当てられていないか、不明な値が割り当てられているため、ポインタが不明なアドレスを指しています。
2. ポインタは解放されましたが、プログラムは依然としてポインタが指すメモリ空間にアクセスしようとします。
コード 1:
コード 2:
このプログラムには、ローカル変数へのポインタを返す、つまりローカル変数へのポインタを呼び出し元に返すという問題があります。具体的な問題は test()
関数にあり、関数で定義された変数は a
ローカル変数であり、関数の実行後、システムはこの変数のメモリ空間を取り戻し、関数で要求されたメモリ空間はすぐに解放されます。したがって、 a
変数が配置されているメモリ空間は関数の実行後に解放され、 &a
返されたポインタが指すメモリ空間は変数のメモリ空間ではなくなるため、このポインタは無効となり、「ダングリング ポインタ」と呼ばれます。 」。
3. ポインタが範囲外で、ポインタが指すメモリ空間に属さない場所を指しています。
コード 1:
コード 2:
このプログラムでは、ポインタ p が範囲外にあり、arr 配列のメモリが範囲外になるという問題があります。具体的な問題は、for ループ内のステートメント ループの数が、配列 *(p++) = i;
を使用すると、不正なアクセスが発生します。これは一般的なワイルド ポインタ エラーです。
変更方法としては、ループ範囲を確認し、10回ループするように修正することが考えられます。
3.2 ワイルド ポインタを回避する方法
1. ポインタの初期化
ポインター変数を定義するときは、初期化せずにポインターが使用されて予期しない動作が発生するのを防ぐために、ポインター変数を null ポインターとして初期化することをお勧めします。
2. 範囲外のポインタに注意してください
ポインタが範囲外にならないようにするには、次の点に注意する必要があります。
配列の長さを確認する: 配列を定義するときは、配列への範囲外アクセスを避けるために配列の長さが十分であることを確認してください。
配列の添字を確認してください: 配列の添字を使用するときは、配列の添字が境界を越えていないことを確認してください。たとえば、添字が 0 未満または配列の長さ以上の場合、配列は境界を超える可能性があります。境界。
ポインタのスコープ: ポインタ変数のライフサイクルは非常に長いため、ポインタが範囲外になる問題を避けるために、ポインタ変数が指すメモリ空間が解放されていないことを確認する必要があります。ポインタ変数が指すメモリ空間が解放されているためです。
ポインタの使用範囲: ポインタを使用する場合は、ポインタ変数が指すメモリ空間の有効範囲を確保し、ポインタが指すメモリ空間以外のメモリにアクセスしてポインタが範囲外になることは避けてください。
ポインタのタイプ: ポインタを使用してメモリ ブロックにアクセスする場合は、タイプの不一致によって引き起こされる範囲外のアクセスの問題を回避するために、ポインタのタイプがポイント先のメモリ ブロックのタイプと一致していることを確認してください。
3. ポインタは解放するスペースを指し、時間内に NULL を設定します
4. ローカル変数のアドレスを返さないようにする
5. ポインタを使用する前に、ポインタの有効性を確認してください。
ポインター変数を使用する前に、null ポインターによってスローされる例外やエラーを避けるために、ポインターが null かどうかを確認することをお勧めします。
4. ポインタ演算
4.1 ポインタ + - 整数
この部分は 2.1 と同じですが、新しい例が追加されています。
vp を配列の最初の要素のアドレス (&values[0]) に指定すると、for ループを使用して配列全体を走査できます。
for ループ内のステートメントはポインター加算を使用し、vp が毎回次の配列要素を指すようにします。配列要素の前に * を追加して、vp が指す配列要素の値を取得し、この値を 0 に代入します。このようにして、vp は配列内のすべての要素を指し、それらを 0 に設定します。ループ内にはループ変数が定義されておらず、条件チェックと反復はすべてループ ステートメント内にあることに注意してください。これは、for ループを記述する一般的な方法でもあります。
なお、ループ内の条件判定は vp < &values[N_VALUES]、ポインタ比較ではポインタが実際にメモリアドレスに対応し、アドレスの大きさを比較し昇順で判定します。したがって、vp が指すアドレスは、比較中に &values[N_VALUES] より小さい必要があります。そうでない場合は、範囲外になります。
4.2 ポインタ-ポインタ
次のコードの結果を分析します。
このコードでは、配列内の 2 つの要素間のアドレスの差が計算され、ポインタを減算することでそれらの間の要素の数が取得されます。配列要素はメモリ内に連続して格納されるため、それらのアドレスの差はそれらの間の要素の数になります。
ポインタ間演算の前提条件:
ポインタの減算は、ポインタが同じ配列の要素を指している場合にのみ実行できます。
4.3 ポインタの関係演算
コードの簡略化により、コードが次のように変更されます。
実際、このタスクはほとんどのコンパイラで正常に完了できますが、標準では動作が保証されていないため、この方法で記述することは避けるべきです。
標準規制:
配列要素へのポインタは、配列の最後の要素の後のメモリ位置へのポインタと比較できますが、最初の要素より前のメモリ位置へのポインタとは比較できません。
5. ポインタと配列
ポインターと配列の間のリンク:
配列では、配列名は実際には配列の最初の要素のアドレスであり、配列名 == アドレス == ポインタ
配列の最初の要素のアドレスがわかっている場合、配列は連続的に格納されるため、ポインターを介して配列を走査してアクセスできます。
6. セカンダリポインタ
ポインタ変数も変数であり、変数にはアドレスがありますが、ポインタ変数のアドレスはどこに格納されているのでしょうか?
これは 2 番目のポインタです。
2 次ポインターのアドレスは 3 次ポインターに保管されます。
7. ポインタの配列
ポインタの配列は、ポインタを含む配列です。つまり、配列の各要素は、特定の型へのポインタです。ポインタの配列は、複数のデータ構造を格納およびアクセスするためのポインタのセットを簡単に表現および操作できます。
int main()
{
char arr1[] = "abcdef";
char arr2[] = "hello world";
char arr3[] = "cuihua";
//指针数组
char* parr[] = { arr1,arr2,arr3 };
int i = 0;
for (i = 0; i < 3; i++)
{
printf("%s\n", parr[i]);
}
return 0;
}
以下は、int ポインターの配列の使用例です。
int main()
{
int arr1[] = { 1,2,3,4,5 };
int arr2[] = { 2,3,4,5,6 };
int arr3[] = { 3,4,5,6,7 };
//指针数组
int* parr[] = { arr1,arr2,arr3 };
int i = 0, j = 0;
for (i = 0; i < 3; i++)
{
for (j = 0; j < 5; j++)
{
//printf("%d ", parr[i][j]);
printf("%d ", *(parr[i] + j));
}
printf("\n");
}
return 0;
}
このコードは、ポインターの配列を含む簡単な例を示しています。arr1
この例では、 、arr2
、 、 の 3 つの整数配列を定義し arr3
、ポインターの配列を作成します parr
。parr
の各要素は、整数配列へのポインタ、つまり arr1
またはarr2
です arr3
。
この例では、次の方法 で、 番目のポインタ 要素 が指す整数配列の 番目の要素に *(parr[i] + j)
アクセス できます。i
j
parr[i][j]
使用するエフェクトと同じ手法 ですが、表現方法が少し異なります。ここでのポインターと配列添字の使用方法は、ポインター算術演算の形式です。つまり、最初に parr[i]
整数配列へのポインターを取得するために使用し、次に *(parr[i] + j)
ポインターが指す配列にアクセスするために使用します。