目次
序文
ポインタは C 言語で重要な役割を果たします。C 言語の初心者として、私たちはすでに「ポインタ」の章でポインタの知識に触れており、ポインタの概念を知っています。
- ポインタとは、メモリ空間を一意に識別するアドレスを格納する変数です。
- ポインタのサイズは 4/8 バイト固定です (32 ビット プラットフォーム/64 ビット プラットフォーム)。
- ポインタにはタイプがあり、ポインタのタイプによって、ポインタの +- 整数ステップ サイズと、ポインタ逆参照操作中のアクセス許可が決まります。
- ポインタ演算。
ポインターの基本を学習したので、このブログではポインターの高度な使用法をいくつか掘り下げていきます。
1. 文字ポインタ
ポインタの種類の中に、文字ポインタ char* と呼ばれるポインタ型があることがわかります。
一般的な用途:
int main()
{
char ch = 'w';
char* pc = &ch;
return 0;
}
別の使用方法もあります。
int main()
{
const char* pstr = "abcdef";
printf("%s\n", pstr);
return 0;
}
上記の使い方について、文字列 abcdef が文字ポインタ pstr に置かれていると誤解している人が多くいますが、本質は、文字列abcdef の最初の文字のアドレスがpstr に置かれるということです。
以下でそれを証明してみてください。
- 文字列「abcdef」のアドレスが a のアドレスである場合、「abcdef」[3] は「" a address [3] "」と等価です。これは、最初の文字のアドレスが実際にポインタ pstr に格納されていることを確認します。 。
- 配列名は最初の要素のアドレスです。文字ポインタには最初の文字のアドレスが格納されているので、配列添字を使用して文字ポインタが指す内容にアクセスしてみます。出力もできることがわかりました。
- したがって、定数文字列を配列として完全に想像し、文字ポインターを使用してそれを受け取ることができ、その操作は配列と一貫性があります。
注:文字列は定数であり変更が許可されていないため、文字列を格納する文字ポインタを変更するにはconstを使用することをお勧めします。変更するとプログラムがクラッシュします。
「Sword Pointer Offer」の古典的な [インタビューの質問]:
次の最終的な出力は何でしょうか?
#include <stdio.h>
int main()
{
char str1[] = "nash.";
char str2[] = "nash.";
const char* str3 = "nash.";
const char* str4 = "nash.";
if (str1 == str2)
printf("str1 and str2 are same\n");
else
printf("str1 and str2 are not same\n");
if (str3 == str4)
printf("str3 and str4 are same\n");
else
printf("str3 and str4 are not same\n");
return 0;
}
【答え】
str1 と str2 は同じではありません
str3とstr4は同じです
【説明する】
- str1 と str2:同じ定数文字列を使用して異なる配列を初期化すると、異なるメモリ ブロックが開かれます。str1 と str2 は実際には nash を格納するためのスペースを作成するため、アドレスは一致しません。
- str3 と str4:ポインタで文字列を指す場合、nash は定数文字列であり変更されないため、コンパイラは複数のコピーを保存する必要はなく、コピーが 1 つだけ必要になります。ポインタは同じメモリを指すだけなので、アドレス値は一貫しています。
【延長】
int main()
{
char str1[] = "hello bit.";
char str2[] = "hello bit.";
const char* str3 = "hello bit.";
const char* str4 = "hello bit.";
if (&str3 == &str4)
printf("Yes");
else
printf("No");
return 0;
【答え】
str3とstr4のアドレス値は異なりますが、 str3とstr4が指す内容は同じであるため 、結果はNoとなります。
2. ポインタ配列
ここで、次のポインタ配列が何を意味するのかを確認します。
int* arr1[10]; //整形指针的数组
char *arr2[4]; //一级字符指针的数组
char **arr3[5];//二级字符指针的数组
それを比較してみましょう:
文字配列 - 文字を格納する配列
整数配列 - 整数を格納する配列
それで:
ポインター配列 - ポインターを格納する配列。つまり、配列に格納される要素はすべてポインター型です。
ポインター配列の用途は何ですか?という疑問を持つ人が非常に多いです。
多くの人は、abcd を定義し、そのアドレスを整数ポインターの配列に格納するのが直感的です。次のように:
错误的使用方式,【没有意义】
int main()
{
int a = 0;
int b = 1;
int c = 2;
int* arr[3] = { &a,&b,&c };
return 0;
}
しかし、このように使用する人はほとんどいません。そのような使用シナリオは存在しませんし、誰もこのように使用しません。このように書いても意味がありません。
正しい使用シナリオの 1 つ:
【可以使用指针数组模拟一个二维数组】
int main()
{
int arr1[] = { 1,2,3,4,5 };
int arr2[] = { 2,3,4,5,6 };
int arr3[] = { 3,4,5,6,7 };
int* arr[3] = { arr1,arr2,arr3 };
//遍历三个数组
int i = 0;
for ( i = 0; i < 3; i++)
{
int j = 0;
for ( j = 0; j < 5; j++)
{
printf("%d ", arr[i][j]);
}
printf("\n");
}
return 0;
}
ポインターの配列を使用して複数の配列を維持し、2 次元配列をシミュレートするため、操作は 2 次元配列と似ています。
複数の文字列を維持するためにポインター配列を使用することもできます。
もちろん、他にも多くのアプリケーション シナリオがありますが、スペースが限られているため、ここではすべてをリストすることはしません。
3. 配列ポインタ
3.1 配列ポインタの定義
配列ポインタはポインタですか? それとも配列でしょうか?
それを比較してみましょう:
整数ポインタ - 整数へのポインタ
文字ポインタ - 文字へのポインタ
それで:
配列ポインタ - 配列へのポインタ
次のコードのうち、配列ポインタはどれですか?
int *p1[10];
int (*p2)[10];
//p1, p2分别是什么?
【説明する】
p2は配列ポインタです。
p2 は最初に * と結合され、p2 がポインター変数であることを示し、次に 10 個の整数の配列を指します。したがって、 p は配列を指すポインターであり、配列ポインターと呼ばれます。
ここで注意してください: [ ] は * よりも優先度が高いため、p2 が最初に * と結合されるように() を追加する必要があります。
3.2 配列名(&A) VS 配列名
以前のブログで言及されました(リンク:クリックして移動):
配列名はアドレスです 一般的に言えば、配列名は配列の最初の要素のアドレスです。
ただし、次の 2 つの特殊なケースが存在します。
1. sizeof (配列名)。ここでの配列名は配列全体を表し、計算は配列全体のサイズ (バイト単位) です。
2. &Array name。ここでの配列名は配列全体を表し、配列全体のアドレスが取り出されます。
それ以外の場合、検出されるすべての配列名は、配列の最初の要素のアドレスになります。
上図から、第 1 グループと第 2 グループ +1 を使用すると、4 バイトだけがスキップされることがわかります。つまり、arr は最初の要素のアドレスを表します。
3 番目のグループでは、&arr と &arr+1 の間で 40 バイトがスキップされていることがわかります。これは配列全体のサイズであるため、&arr が配列全体を表すことが証明されます。
ポインタのタイプによって、ポインタ + 1 + のバイト数が決まります。
そこで大胆に推測してみましょう。arr の型は int* であり、&arr[0] の型も int* です。
この場合、&arr の型は int (*)[10] になります。つまり、配列ポインターには配列が格納されます。
配列ポインタとは何かを理解したので、理解しているかどうかをテストするための質問をしてみましょう。
【練習する】
以下のコードの PC の種類は何ですか?
int main()
{
char* arr[5];
pc = &arr; //给pc定义类型
return 0;
}
【答え】
char* (*pc)[5] = &arr;
【説明する】
まず、pc はポインタ、つまり (*pc) でなければなりません。
5 つの要素を持つ配列、つまり (*pc)[5] を指します。
配列の各要素の型は char*、つまり char* (*pc)[5] です。
では、配列ポインターの使用シナリオはどのようなものでしょうか?
3.3 配列ポインタの使用
間違った使用シナリオ:
このシナリオはまったく便利ではありませんが、配列を使用する場合はさらに面倒で、「ズボンを脱いでオナラをする」ような感じです。このような使い方は基本的にはありません。
配列を受け取るためにポインターを使用する必要がある場合は、配列全体のアドレスではなく、最初の要素のアドレスを受け取るためにポインターも使用する必要があります。これが正しいアクセス姿勢です。
実際、パラメータを 1 次元配列に渡すプロセスでは、仮パラメータは配列の形式またはポインタの形式で書き込むことができます。これは、本質的にこれら 2 つのメソッドが、パラメータの最初の要素のアドレスを転送するためです。配列、それらは互いに等しい。
これらは本質的にアドレスであるのに、なぜ配列形式で記述する必要があるのでしょうか?
配列形式で書けるのは完全に初心者向けで、実引数が配列で、仮引数も配列を受け取る配列を定義しているので、初心者にも分かりやすいです。したがって、このように書かれていても、本質はまだ指針です。
同様に、2 次元配列でパラメーターを渡すには 2 つの方法があります。
- 仮パラメータも 2 次元配列の形式になります。
- 仮パラメータは配列ポインタの形式です。
ポインター配列と配列ポインターについて学習した後、それらを一緒に確認し、次のコードが何を意味するかを見てみましょう。
int arr[5];
int *parr1[10];
int (*parr2)[10];
int (*parr3[10])[5];
【答え】
- arr は 5 つの整数データを格納できる配列です。
- parr1 は整数ポインタの配列を格納できる配列で、配列サイズは 10 です。
- parr2 は 10 個の要素の配列を指すポインタで、各要素の型は int* です。
- parr3 は配列です。配列には 10 個の要素があり、ポインタを指します。ポインタが指す内容は 5 つの要素の配列です。各要素の型は int で、配列ポインタを格納する配列です。
もちろん、parr3 が理解できなくても心配する必要はありません。この形式はめったに使用されず、知識を広げるための単なる手段です。
4. 配列パラメータとポインタパラメータ
4.1 1次元配列パラメータの転送
4.2 2次元配列パラメータの転送
2 次元配列でパラメーターを渡す場合、関数パラメーターの設計では最初の [] 番号のみを省略できます。2 次元配列の場合、行数を知る必要はありませんが、行内に要素が何個あるかは知っておく必要があるためです。これにより計算が容易になります。
4.3 第 1 レベルのポインタパラメータの受け渡し
第 1 レベルのポインターを使用してパラメーターを渡す場合、仮パラメーターは第 1 レベルのポインターとして記述することができます。
#include <stdio.h>
void print(int* p, int sz)
{
int i = 0;
for (i = 0; i < sz; i++)
{
printf("%d\n", *(p + i));
}
}
int main()
{
int arr[10] = { 1,2,3,4,5,6,7,8,9 };
int* p = arr;
int sz = sizeof(arr) / sizeof(arr[0]);
//一级指针p,传给函数
print(p, sz);
return 0;
}
【考える】
関数のパラメータ部分が第 1 レベルのポインタの場合、関数はどのようなパラメータを受け取ることができますか?
【答え】
4.4 第 2 レベルのポインタパラメータの受け渡し
#include <stdio.h>
void test(int** ptr)
{
printf("num = %d\n", **ptr);
}
int main()
{
int n = 10;
int* p = &n;
int** pp = &p;
test(pp);
test(&p);
return 0;
}
【考える】
関数のパラメータがセカンダリ ポインタの場合、どのようなパラメータを受け取ることができますか?
【答え】
5. 関数ポインタ
関数ポインタ - 関数へのポインタ - 関数のアドレスを格納します
次の例が示すように、&Add と Add は完全に同等です。
int (*pf2)(int,int) = &Add; を定義し、&Add を関数ポインターであるpf2 に割り当てることができます 。
- 参照追加関数は ret1 を返します
- このとき、pf2 を逆参照すると、関数呼び出し時に ret2 が返されます。
- pf2 で関数を直接呼び出すと ret3 が返されます。
- ret4 を返すために、pf2 の前に多くの逆参照を追加します。
最後に、結果を印刷すると、4 つの結果がすべて同じであることがわかりました。
したがって、関数ポインタがそれが指す関数を呼び出すとき、* を書かずにその関数を関数名として直接呼び出すことができ、ここでの * 記号は実際には単なる飾りです。その結果、逆参照に * 記号を大量に追加しても、結果は依然として正しい結果になります。
両端の興味深いコードを見てみましょう。
1、
(*(void (*)())0)();
- void (*)() ———— 関数ポインタ型
- ( void (*)() )0 ———— 0 を関数ポインタに強制的に型変換します。つまり、アドレス 0 が関数ポインタが格納されているアドレスとみなされます。
- (* ( void (*)() )0 )() ———— 関数ポインターを介して関数を呼び出しますが、関数パラメーターは空です。
つまり、上記のコードは実際にはアドレス 0 で関数を呼び出します。この関数にはパラメーターがなく、戻り値は void です。
このコードは「C の落とし穴と落とし穴」からのものです。
2、
void (*signal(int , void(*)(int)))(int);
- signal(int, void(*)(int)) ———— signal は、整数型 int と関数ポインタ型 void(*)(int) の 2 つのパラメータを持つ関数です。
- void (* signal(int, void(*)(int)) )(int); ———— シグナル関数の戻り値の型も関数ポインタ型 void (*)(int)
しかし、このコードは複雑すぎるようです。簡略化する方法はありますか?
私たちの習慣に従った形式でコードを記述する方法はありますか: 戻り値の型が最初、関数名が真ん中、関数パラメータが最後: void (*)(int) signal(int, void(*)(int)) 、このように直接書き込みはサポートされていませんが、typedefを通じて最適化できます。
typedef void(*pfun_t)(int); //对void(*)(int)重新起名为pfun_t
pfun_t signal(int, pfun_t);
この方法で記述されたコードがより明確になるように、 void(*)(int) の名前を pfun_t に変更します。
作者の文章が良いと思ったら高評価をして応援していただけると更新の最大のモチベーションになります!