1> 著者: 街角に #include があるそうです。
2> 知識ポイント: ポインタ関数と関数ポインタの説明、関数ポインタの応用: 関数ポインタ配列とコールバック関数。
3>開発環境: vs 2022
序文
C言語を学習すると、配列ポインタ、ポインタ配列、関数ポインタ配列、ポインタ関数、関数ポインタなど、頭を悩ませ、非常に面倒な知識が登場します。しかし、これらの知識ポイントの本質を理解すれば、これらの内容を学習することは難しくありません。
1. ポインタ機能
ポインタ関数は本質的には関数ですが、この関数はポインタを返します。これは実際には一般的な関数の定義とそれほど変わりません。
1.1 ポインタ関数の定義
- 関数定義をよく使用する方法は、(型名識別子 (仮パラメータ リスト) {関数本体})です。一般的な形式は (たとえば):
int ret(int x,int y) {}
- ポインタ関数の定義は実際には同じで、定義方法も(型名識別子(パラメータリスト) {関数本体})ですが、ポインタ型が使用されます。一般的な形式は次のとおりです (例):
int* ret(int x,int y) {} 或者 int *ret(int x,int y) {}
書き方は個人の習慣に依存します。違いはありません。
この観点から見ると、実際には比較的単純であり、通常の関数の定義と大きな違いはありません。つまり、余分な * があるだけです。
1.2 ポインタ関数の使用
ポインター関数は特定のシナリオでよく使用されるため、非常に重要な知識ポイントです。
- 例を挙げてみましょう。たとえば、今 malloc を使用したいとします (malloc についてよくわからない場合は、こちらを参照してください:動的メモリ割り当て) でメモリ内のスペースを開きます。後で realloc を使用して拡張したい場合は、ただし、関数の可読性を高めるために、それを実装するポインター関数を定義します。
int* Main_realloc(int* p, int n)
{
if (p != NULL)
{
int* tmp = (int*)realloc(p, n);
if (tmp != NULL)
{
p = tmp;
}
else
{
printf(扩容失败);
}
}
else
{
printf(扩容失败);
}
//************
//函数体
//对内存的一些操作
//***********
return p;
}
int main()
{
int* p = (int*)malloc(sizeof(int));
//*******************
//
// *函数体*
// 假设在执行完后发现空间不够
// 需要扩容并执行一些操作
//
//*******************
p = Main_realloc(p,100); //扩容函数
free(p);
p = NULL;
return 0;
}
一部のアルゴリズムでポインター関数を使用すると、空間の複雑さを大幅に軽減できますが、ポインター関数を使用する場合には特別な注意が必要な点が 1 つあります。
- ローカル変数のアドレスや関数の引数のアドレスは、関数使用直後に破棄されてしまいますので、返さないように注意してください。アドレス内のデータとともに消去されるわけではありませんが、使用権は失われます。メモリのこの部分が返されるので、偶然望む結果が得られるかもしれませんが、メモリのこの部分のデータを使用する権利は返還されているため、再割り当てされ、他のデータによって占有される可能性があります。 、そして私たちは望むものを得ることができません。アクセスできないコンテンツへのアクセスによるプログラムのクラッシュなど、重大な問題が発生する可能性が非常に高くなります。
- ただし、ローカル変数のアドレスを返す必要がある場合は、 static キーワードを使用して変更する必要があります。static を使用すると、ローカル変数のライフサイクルを延長できます。詳しくは前回の記事(静的キーワードの詳細)をご覧ください。
2. 関数ポインタ
ポインタ関数とは異なり、関数ポインタの本質はポインタであり、このポインタは関数を指します。関数の定義はコードセグメント内に存在し、各関数はコードセグメント内に独自のエントリアドレスを持ち、関数ポインタはエントリアドレスへのポインタです。
2.1 関数ポインタの定義と使用
関数ポインタの宣言:函数返回类型 (*标识符) (形参类型1, 形参类型2,···) 如int (*pf)(int, int)
コード例:
int test()
{
return 0;
}
int main()
{
int (*pTest)(int, int) = &test;
}
また、関数ポインタに関しても、関数名は配列名と同様に、実際には関数のエントリアドレスになります。ただし、関数ポインタには通常のポインタの加減算や整数の加減算などの一部の演算がありません。逆参照せずに関数を呼び出すために関数ポインタを使用することさえあります。
コード例:
#include <stdio.h>
void test1(int a){
printf("hello bit\n");
}
int main(){
//通过typedef关键字将void(*)(int)类型的函数指针重命名为P简化代码
typedef void(*P)(int a);
//用P的函数指针类型定义一个p指针变量;
P p = test1;
p(1);
(*p)(1);//解引用或者不解引用效果相同
P p1 = &test1;//是否对函数名取地址不影响,无论取不取,函数名代表的都是函数的入口地址
p1(1);
return 0;
}
2.2 関数ポインタの適用
2.2.1 関数ポインタの配列
配列は、同じ型を格納する記憶空間です。ポインタ配列についてはすでに学習しました。最初に、次のようにポインタ配列の定義を思い出してください。
int * arr[10];
n 個の関数のアドレスを配列に入れてみましょう。この配列は関数ポインター配列と呼ばれます。関数ポインター配列はどのように定義されるべきですか: (次のように)
int (*parr1[10])(); //正确
int *parr2[10](); //错误
int (*)() parr3[10]; //错误
関数ポインタの配列を定義するときは非常に間違いが起こりやすいため、を定義するときはまずpを[]と組み合わせて、定義するものが配列であり、配列の内容が であることを示す必要があります int (*)() 类型的函数指针
。
応用例: (電卓)
関数ポインターの配列がわからない場合、作成するコードは次のようになります。
#include <stdio.h>
int add(int a, int b)
{
return a + b;
}
int sub(int a, int b)
{
return a - b;
}
int mul(int a, int b)
{
return a*b;
}
int div(int a, int b)
{
return a / b;
}
int main()
{
int x, y;
int input = 1;
int ret = 0;
do
{
printf( "*************************\n" );
printf( " 1:add 2:sub \n" );
printf( " 3:mul 4:div \n" );
printf( "*************************\n" );
printf( "请选择:" );
scanf( "%d", &input);
switch (input)
{
case 1:
printf( "输入操作数:" );
scanf( "%d %d", &x, &y);
ret = add(x, y);
printf( "ret = %d\n", ret);
break;
case 2:
printf( "输入操作数:" );
scanf( "%d %d", &x, &y);
ret = sub(x, y);
printf( "ret = %d\n", ret);
break;
case 3:
printf( "输入操作数:" );
scanf( "%d %d", &x, &y);
ret = mul(x, y);
printf( "ret = %d\n", ret);
break;
case 4:
printf( "输入操作数:" );
scanf( "%d %d", &x, &y);
ret = div(x, y);
printf( "ret = %d\n", ret);
break;
case 0:
printf("退出程序\n");
breark;
default:
printf( "选择错误\n" );
break;
}
} while (input);
return 0;
}
その後、コードが長くて扱いにくく、読みにくく、非常に冗長に見えると投稿しました。しかし、今は違います。関数ポインターの配列を学習したので、次のコードを書く必要があります。
#include <stdio.h>
int add(int a, int b)
{
return a + b;
}
int sub(int a, int b)
{
return a - b;
}
int mul(int a, int b)
{
return a*b;
}
int div(int a, int b)
{
return a / b;
}
int main()
{
int x, y;
int input = 1;
int ret = 0;
int(*p[5])(int x, int y) = {
0, add, sub, mul, div }; //转移表
while (input)
{
printf( "*************************\n" );
printf( " 1:add 2:sub \n" );
printf( " 3:mul 4:div \n" );
printf( "*************************\n" );
printf( "请选择:" );
scanf( "%d", &input);
if ((input <= 4 && input >= 1))
{
printf( "输入操作数:" );
scanf( "%d %d", &x, &y);
ret = (*p[input])(x, y);
}
else
printf( "输入有误\n" );
printf( "ret = %d\n", ret);
}
return 0;
}
2.2.2 コールバック関数
コールバック関数は、関数ポインターを通じて呼び出される関数です。関数ポインター (アドレス) をパラメーターとして別の関数に渡す場合、このポインターがそれが指す関数を呼び出すために使用されるとき、これをコールバック関数と呼びます。コールバック関数は、関数の実装者によって直接呼び出されるのではなく、特定のイベントまたは条件が発生したときに別の当事者によって呼び出され、イベントまたは条件に応答するために使用されます。たとえば、ソート関数qsort (今後の特別記事で説明します) は、コールバック関数の典型的なアプリケーションです。
コールバック関数を使用してソート関数 (バブルソートの方法) を実装します。
#include <stdio.h>
int int_cmp(const void* p1, const void* p2)
{
return (*(int*)p1 - *(int*)p2);
}
void _swap(void* p1, void* p2, int size)
{
int i = 0;
for (i = 0; i < size; i++)
{
char tmp = *((char*)p1 + i);
*((char*)p1 + i) = *((char*)p2 + i);
*((char*)p2 + i) = tmp;
}
}
void bubble(void* base, int count, int size, int(*cmp)(void*, void*))
{
int i = 0;
int j = 0;
for (i = 0; i < count - 1; i++)
{
for (j = 0; j < count - i - 1; j++)
{
if (cmp((char*)base + j * size, (char*)base + (j + 1) * size) > 0)
{
_swap((char*)base + j * size, (char*)base + (j + 1) * size, size);
}
}
}
}
int main()
{
int arr[] = {
1, 3, 5, 7, 9, 2, 4, 6, 8, 0 };
//char *arr[] = {"aaaa","dddd","cccc","bbbb"};
int i = 0;
bubble(arr, sizeof(arr) / sizeof(arr[0]), sizeof(int), int_cmp);
for (i = 0; i < sizeof(arr) / sizeof(arr[0]); i++)
{
printf("%d ", arr[i]);
}
printf("\n");
return 0;
}
スリーリンクをお願いするのは簡単ではありませんが、皆様のご支援が私の最大のモチベーションです。