C言語の詳細な分析 ポインターと構造ポインター、ポインター関数、関数ポインターを理解する方法

1. ポインター変数

  • まず、ポインターが変数であることを理解する必要があります。次のコードを使用して確認できます。
#include "stdio.h"

int main(int argc, char **argv) {
    
    
    unsigned int a = 10;
    unsigned int *p = NULL;
    p = &a;
    printf("&a = %d\n",a);
    printf("&a = %d\n",&a);
    *p = 20;
    printf("a = %d\n",a);
    return 0;
}
  • 操作の結果は次のとおりです。
a = 10
&a = 6422216
a = 20
  • a の値が変更されていることがわかります。したがって、ポインターは本質的に変数のアドレスを配置するための特別な変数であり、その本質は依然として変数であることが明確に理解できます。ポインターは変数なので、変数型が存在する必要があります。
  • C言語では、すべての変数に整数型、浮動小数点型、文字型、ポインタ型、構造体、共用体、列挙型などの変数型があり、すべて変数型です。変数タイプの出現は、メモリ管理の必然的な結果です. 私たちは皆、すべての変数がコンピュータのメモリに格納されていることを知っています. それらはコンピュータのメモリに配置されるため、必然的に一定量のスペースを占有します.変数占有? スペースは? または、変数を配置するためにどのくらいのメモリ空間を割り当てる必要がありますか?
  • この問題を特定するために生まれたのが、32ビットコンパイラの場合、int型が4バイトつまり32ビット、long型が8バイトつまり64ビットを占めるタイプです。コンピュータでは、実行するプログラムはメモリに保存され、プログラム内のすべての変数は実際にはメモリに対する操作です。コンピュータのメモリ構造は比較的単純なので、ここではメモリの物理構造については詳しく説明せず、メモリ モデルについてのみ説明します。コンピュータのメモリは、人々が住む家に例えることができます。各部屋はコンピュータのメモリ アドレスに対応し、メモリ内のデータは家の中の人々に相当します。

ここに画像の説明を挿入

  • ポインタも変数なので, そのポインタもメモリに格納する必要があります. 32ビットコンパイラの場合, そのアドレス空間は 2 32 = 4GBです. すべてのメモリを操作できるようにするために (実際には不可能です.通常のユーザーがすべてのメモリを操作するには、ポインター変数ストレージも 32 桁、つまり 4 バイトを使用する必要があるため、ポインター &p のアドレスが存在するため、ポインターと変数の関係は次のように表すことができます。形:

ここに画像の説明を挿入

  • &p はポインタ p を格納するために使用されるポインタのアドレスであり、ポインタ p は変数 a のアドレスを格納するために使用される &a であり、C 言語には *p があることがわかります。これは「dequote」です。これは、このアドレスに格納されているコンテンツを取り出すようにコンパイラに指示することを意味します。&(*p) と *(&p) は何を意味し、どのように理解すればよいのでしょうか?
  • ポインター型の問題についてですが、32 ビット コンパイラの場合、どのポインターも 4 バイトしか占有しないため、なぜポインター型を導入する必要があるのでしょうか。同じ型の変数を制約するだけですか? 実際、ポインター操作について言及する必要があります。まず、次の 2 つの操作について考えてみましょう: p+1 と ((unsignedint)p)+1、どうやって理解するのでしょうか?
  • これら 2 つの操作の意味は異なります. まず、次の図に示すように、最初の p+1 操作について説明します。

ここに画像の説明を挿入

  • ポインタの型が異なれば, p+1 が指すアドレスも異なります. この増分はポインタ型が占有するメモリサイズに依存します. ((unsigned int)p)+1 の場合, が指すアドレスはp アドレスの値は直接数値に変換されてから +1 されるため、ポインター p の型に関係なく、結果はポインターが指すアドレスの次のアドレスになります。
  • 上記の分析から、ポインターの存在により、プログラマーは非常に簡単にメモリを操作できるようになり、ポインターが非常に危険であると考える人もいることがわかります.この考え方は C# や Java 言語にも反映されていますが、実際にはポインターを使用して.効率を大幅に向上させることができます。
  • ポインタを介してメモリを操作するためにもう少し深く行くと、メモリ 6422216 にデータ 125 を入力する必要があり、次の操作を実行できます。
unsigned int *p = (unsigned int*)(6422216);
*p = 125;
  • もちろん、上記のコードはポインターを使用しています. 実際、C言語では、逆参照操作を直接使用して、より便利にメモリに値を割り当てることができます.

二、解引用

  • いわゆる dequote 操作は、実際にはアドレスに対する操作です. たとえば、変数 a に値を代入したい場合、一般的な操作は a = 125 です. dequote 操作を使用してそれを完了します. 操作は次のとおりです.次のように:
*(&a) = 125;
  • dequote 演算子が * であることがわかります. この演算子はポインタに対して 2 つの異なる意味を持ちます. 宣言するときはポインタを宣言し, p ポインタを使用するときは dequote 操作です. dequote 操作の右側は a アドレスです.したがって、dequote 操作はアドレス メモリ内のデータを意味するため、メモリ 6422216 内のデータ 125 を埋めるには、次の操作を使用できます。
*(unsigned int*)(6422216) = 125;
  • 上記の操作は 6422216 の値をアドレスに変換します. これは, 値がアドレスであることをコンパイラに伝えるためです. 上記のすべてのメモリアドレスは無造作に指定できないことに注意してください. それらはコンピュータによって割り当てられたメモリでなければなりません.コンピューターはポインターが範囲外であると判断し、オペレーティング システムによって強制終了されるということは、プログラムが早期に終了することを意味します。

三、構造ポインタ

  • 構造体ポインタは通常の変数ポインタと同じです. 構造体ポインタは 4 バイトしか占有しませんが (32 ビット コンパイラ), 構造体ポインタは構造体型の任意のメンバに簡単にアクセスできます. これはポインタのメンバ演算子です - > .
  • 以下に示すように、p は構造体ポインターであり、p は構造体の最初のアドレスを指し、p->a を使用して構造体のメンバー a にアクセスできます。もちろん、p->a と *§ は同じです。

ここに画像の説明を挿入

4. 必須の型変換

  • 上記のテスト コードから、コンパイラはデータ型が一致しないことを意味する多くの警告を報告することがわかります.プログラムの正しい動作には影響しませんが、多くの警告は常に人々を不快にさせます. そのため、コードに問題がないことをコンパイラに伝えるために、キャストを使用してメモリの一部を必要なデータ型に変換できます。
  • 次のような配列 a があり、構造体型 stu にキャストされます。
#include <stdio.h>

typedef struct STUDENT {
    
    
    int name;
    int gender;
}stu;

int a[100] = {
    
    10,20,30,40,50};

int main(int argc, char **argv) {
    
    
    stu *student;
    student = (stu*)a;
    printf("student->name = %d\n", student->name);
    printf("student->gender = %d\n", student->gender);
    return 0;
}
  • 操作の結果は次のとおりです。
student->name = 10
student->gender = 20
  • a[100] が stu 構造体型にキャストされていることがわかります.もちろん、キャストを使用しないことも可能ですが、コンパイラはアラームを報告します. 以下に示すように、配列 a[100] の最初の 12 バイトは、配列を説明する struct stu 型に強制的に変換されます。同じことが、本質的にメモリ空間の一部である他のデータ型にも当てはまります。

ここに画像の説明を挿入

五、ボイドポインタ

  • void 型は空と考えるのは簡単ですが、ポインターの場合は void ではなく、不確定です。多くの場合、ポインタは宣言時にそれがどの型であるかを認識していない可能性があります。または、ポインタが指すデータ型が複数ある場合、またはポインタを介してメモリ空間を操作したい場合、この時点で宣言できます。 void 型のポインタ。
  • 次に問題が発生します。void 型のため、特定のデータ型を引用解除するときに、コンパイラは int p などの型が占有するスペースに従って対応するデータを逆参照し、その後、p は p としてコンパイラによって逆参照されます。ポインタのアドレスの空間サイズは 4 バイトです。しかし、null ポインター型の場合、コンパイラーは逆参照されるメモリーのサイズをどのように知るのでしょうか?
  • まず、次のコードを見てください。
#include <stdio.h>

int main(int argc, char **argv) {
    
    
    int a = 10;
    void *p;
    p = &a;
    printf("p = %d\n",*p);
    return 0;
}
  • 上記のコードをコンパイルした後、コンパイラがエラーを報告し、正常にコンパイルできないことがわかります。
error:invalid use of void expression
  • これは、コンパイラがデクォート時に *p のサイズを決定できないことを示しているため、コンパイラに p の型または *p のサイズを伝える必要があります。実際には、次のように必須の型変換を使用するだけで、非常に簡単です。
*(int*)p
  • したがって、上記のコードは次のように最適化できます。
#include <stdio.h>

int main(int argc, char **argv) {
    
    
    int a = 10;
    void *p;
    p = &a;
    printf("p = %d\n", *(int*)p);
    return 0;
}
  • 操作の結果は次のとおりです。
p = 10
  • void ポインタには空間サイズ属性がないため、void ポインタには ++ 操作がありません。
  • 概要: void ポインターは、指定された型のない単なるポインターです。つまり、ポインターはアドレス データ属性のみを持ち、デクォート時に空間サイズ属性を持ちません。

六、関数ポインタ

①関数ポインタの使用説明

  • 関数ポインタは Linux カーネルでよく使われ、OS の設計にも使用されます. 関数ポインタもポインタであるため、関数ポインタも 4 バイトを占有します (32 ビット コンパイラ)。
  • 簡単な例で説明します。
#include <stdio.h>

int  add(int a,int b) {
    
    
    return a+b;
}

int main(int argc, char **argv) {
    
    
    int (*p)(int,int);
    p = add;
    printf("add(10,20) = %d\n",(*p)(10,20));
    return 0;
}
  • 操作の結果は次のとおりです。
add (10, 20) = 30
  • ご覧のとおり、関数ポインターの宣言は次のとおりです。
返回类型(*函数名)(参数列表)
  • 関数ポインタの逆参照操作は通常のポインタの逆参照操作とは少し異なります. 通常のポインタの場合, 逆参照は型に従ってデータを取得するだけでよいのですが, 関数ポインタは関数を呼び出すためのものであり, その逆参照はデータにすることはできません.実際、関数ポインタの逆参照は本質的に関数を実行するプロセスですが、関数を実行するために使用される呼び出し命令は前の関数ではなく、関数ポインタの値、つまりのアドレスです関数。実際、関数を実行するプロセスは、本質的に呼び出し命令を使用して関数のアドレスを呼び出すため、関数ポインターは本質的に関数実行プロセスの最初のアドレスを保存します。
  • 関数ポインタは次のように呼び出されます。
函数指针调用(*(实参列表)
  • 関数ポインターが本質的に call 命令に渡される関数のアドレスであることを確認するために、2 つのコードを以下に示します。
#include <stdio.h>

void add (void) {
    
    
	printf("hello add\n");
}

int main (int arg, char **argv) {
    
    
	void (*p (void);
	p = add;
	(*р) О;
	return 0;
}
#include <stdio.h>

void add (void) {
    
    
	printf("hello add\n");
}

int main (int arg, char **argv) {
    
    
	add();
	return 0;
}
  • 2 つのコードをコンパイルした後のアセンブリ命令は次のとおりです。
0×4015d5 	push    ebp
0×4015d6	mov     ebp, esp
0×4015d8	and     esp, 0xfffffff0
0×4015db	sub     esp, 0x10
0×4015de	call    0x401690 <__main>
0×4015e3	mov     exa, DWORD PTR [esp+0xc]
0×4015eb	call    exa
0×4015ef	mov     exa 0x0
0×4015f1 	leave
0×4015f6    left
0×4015f7	ret
0×4015d5 	push    ebp
0×4015d6	mov     ebp, esp
0×4015d8	and     esp, 0xfffffff0
0×4015db	call    0x401680 <__main>
0×4015e0	call    0x4016c0 <add>
0×4015e5	mov     exa 0x0
0×4015ea	leave
0×4015eb    left
  • 関数ポインタを使用して関数を呼び出す場合、そのアセンブリ命令は次のようになります。
0x4015e3    mov    DWORD PTR [esp+0xc],0x4015c0
0x4015eb    mov    eax,DWORD PTR [esp+0xc]
0x4015ef    call   eax
  • mov 命令の 1 行目は、即値 0x4015c0 をレジスタ esp+0xc のアドレス メモリに割り当て、レジスタ esp+0xc のアドレスの値をレジスタ eax (アキュムレータ) に割り当て、call 命令を呼び出します。 、この時点で pc ポインターは add 関数を指し、0x4015c0 は関数 add の最初のアドレスにすぎないため、関数呼び出しが完了します。ところで面白い現象を見つけたのですが、上記の過程で関数ポインタの値がパラメータと同じようにスタックフレームに置かれるので、パラメータ受け渡しの過程らしいので、関数ポインタが最終的にパラメーターとして渡される フォームは呼び出された関数に渡され、渡された値はたまたま関数の最初のアドレスになります。
  • 関数ポインターは、通常のポインターのようにメモリを操作できないため、関数ポインターは関数参照宣言と見なすことができます。

②関数ポインタの応用

  • これは、Linux 主導のオブジェクト指向プログラミングのアイデアで最もよく使用され、関数ポインターを使用してカプセル化を実現します。次のように:
#include <stdio.h>

typedef struct TFT_DISPLAY {
    
    
    int   pix_width;
    int   pix_height;
    int   color_width;
    void (*init)(void);
    void (*fill_screen)(int color);
    void (*tft_test)(void);
}tft_display; 

static void init(void) {
    
    
    printf("the display is initialed\n");
}

static void fill_screen(int color) {
    
    
    printf("the display screen set 0x%x\n",color);

}

tft_display mydisplay = {
    
    
    .pix_width = 320,
    .pix_height = 240,
    .color_width = 24,
    .init = init,
    .fill_screen = fill_screen,
};

int main(int argc, char **argv) {
    
    

    mydisplay.init();
    mydisplay.fill_screen(0xfff);
    return 0;
}
  • 上記のサンプル コードは tft_display をオブジェクトにカプセル化します. 構造体の最後のメンバーは初期化されていません. Linux でよく使われます. 最も一般的なのは file_operations 構造体です. 一般的に言えば, この中で初期化する必要があるのは一般的な関数だけです.構造体. すべての初期化は必要ありません. 使用される構造体の初期化方法も Linux で最も一般的に使用される方法です. この方法の利点は, 構造体の順序に従って 1 対 1 である必要がないことです.

③コールバック機能

  • A が B に関数を渡して完了させると、A と B が同期して動作するような状況が時々あります.このとき、関数関数は完了していません.このとき、A は B に渡す API を定義することができます. , そして A API さえ気にすれば, 特定の実装を気にする必要はありません. 特定の実装は B によって完了することができます. この場合, コールバック関数 (Callback Function) が使用されます. A には FFT アルゴリズムが必要です. このとき、A は FFT アルゴリズムを B に渡して完了させます. このプロセスを実現しましょう:
#include <stdio.h>

int InputData[100] = {
    
    0};
int OutputData[100] = {
    
    0};

void FFT_Function(int *inputData, int *outputData, int num) {
    
    
    while(num--) {
    
    

    }
}

void TaskA_CallBack(void (*fft)(int*,int*,int)) {
    
    
    (*fft)(InputData, OutputData,100);
}

int main(int argc, char **argv) {
    
    
    TaskA_CallBack(FFT_Function);
    return 0;
}
  • TaskA_CallBack は、仮パラメーターが関数ポインターであるコールバック関数であるのに対し、FFT_Function は呼び出される関数であり、コールバック関数で宣言された関数ポインターの型は、呼び出される関数の型とまったく同じでなければならないことがわかります。

おすすめ

転載: blog.csdn.net/Forever_wj/article/details/128847659