C言語 -- ポインタのヌルポインタ (void *)


序文

この記事では、C 言語の頭痛の種であるポインターを紹介します。特別なポインター void * の紹介、ポインターのメモリ割り当ての紹介、ポインターを関数パラメーターとして使用する方法、およびvoid * ポインターで一部の操作を直接実行できない理由について説明します


1. void * ポインタとは

C 言語にはいくつかのポインター型があり、その中で最も特殊なものは void * ポインターです。A void * pointer is a pointer to any type. それが指す型はコンパイル中に決定されず、プログラム内で指されます。

void* ポインタは使いやすい

前述のように、void * ポインターは任意の型のポインターを指すことができるので、その使用方法を見てみましょう。

int main() 
{
    
     
  
  int i_t[2]={
    
    0,1};
  
  void *v_t=i_t;/*void *可以获得指向任何地址,但不知道指向地址的类型大小*/
  /*普通指针使用,获得void *指针指向的地址并强制转换为int *类型,使之有知道指向类型的大小*/
  int * p_t=(int *)v_t;
  printf("Get int * i_t[0] = %d i_t[1] = %d \n",*p_t,*(p_t+1));

  /*void指针使用,可以直接给void *指针赋值,让它指向的类型改为int*的地址*/  
  v_t=p_t;

  printf("Get void * i_t[0] = %d i_t[1] = %d \n",*(int *)v_t,* (int *)(v_t+4));
  /*printf("Get void * i_t[0] = %d i_t[1] = %d \n",*(int *)v_t,* (char *)(v_t+4));*/ 
  //这两条printf输出效果一样,

  /*第二条printf输出警告,对void * 进行 +操作的警告,在GNU标准,对void *取加操作是仅仅类似char *的取加操作*/
  system("pause");
  return 0; 
}

運用実績

Get int * i_t[0] = 0 i_t[1] = 1
Get void * i_t[0] = 0 i_t[1] = 1

注意: void * を使用する場合、void * のアドレスの + または - 操作を実行する必要があり、操作を実行するには強制的な型変換が必要です. すべてのコンパイラが GNU 標準をサポートしているわけではありません.

2、ポインタメモリサイズ

ポインタのサイズ

ポインタのサイズは、実際にはコンパイラとオペレーティング システムによって決定されますが、一般的なルールは次のとおりです。

  • 32 ビット オペレーティング システム、32 ビット コンパイラ、ポインタ サイズは 32 ビット
  • 64 ビット オペレーティング システム、32 ビット コンパイラ、ポインタ サイズは 32 ビット
  • 64 ビット オペレーティング システム、64 ビット コンパイラ、ポインタ サイズは 64 ビット

32 ビット ポインター サイズは 4G をアドレス指定でき、62 ビット ポインター サイズは 1800T アドレスをアドレス指定できます。

void * 型ポインターと他の型のポインターの違い

基本的な違い

違いは下の図のように、ポインタが置かれているメモリは同じですが、void * ポインタと他のポインタの違いは、ポインタが指す型の情報を持っていることです。この機能は、任意の型の変数を指すことができることを意味し、void * 型ポインターはこの変数のアドレスを指すだけです。
ここに画像の説明を挿入

バリューオペレーションとアドレスグロースオペレーション

ANSI 標準では、void * 型がキャストされていない場合、値とアドレスの拡張操作を実行できません。

なぜ?まず、int * 型のポインターが値取得操作を実行する方法を見てみましょう。

値操作

int * は取得値の範囲(現在の先頭アドレス+int型オフセットサイズ)です。double * は、現在の最初のアドレス + double 型のオフセット サイズです。ここでのオフセット サイズは、占有されているバイト数、つまり sizeof (int) または sizeof (double) です。

void * ポインターには、それが指す型の情報が含まれていません。つまり、必須の型変換を使用しないと、コンパイラーは、void * のオフセット アドレス範囲がポイントされた値のアドレス範囲であることがわかりません。つまり、void * は現在ターゲットを指している最初のアドレスしかなく、最初のアドレスから何バイト後が適切かがわからないため、コンパイラはそれが 4 バイトなのか 8 バイトなのかわかりません。この馬鹿に言うほど頭が良くない、強要して言う。

成長に対処する

アドレスの伸びと値は似ている、void ※++で何バイト動くか分からないので詳しくは割愛しますが、頭のいい人は心の中に答えを持っているはずです。

3. void * ポインターが関数パラメーターとして渡される

ここでは、void * ポインターをパラメーターとして使用して、プログラムをより汎用的にします。ここでは、 memsetmemcpy を2 つの C ライブラリに実装し、実装中に上記の知識ポイントを確認し、着信パラメーターが void * 型である理由と長さを指定する必要がある理由を理解します。

memset を実装する

関数プロトタイプ: 最初のアドレスを返します。入力では、初期化するアドレスと初期化する値と長さを指定する必要があります。

void * memset(void *s,int c,size_t n)

関数 function: 特定のブロック内のすべてのメモリを指定された値に設定する初期化関数。

以下のコードとテスト関数を見てください。

void * my_memset(void *s,int c,size_t n)
{
    
    
    char *c_s=(char *)s;
    while(n--) *c_s++=c;
    return s;
}

int main() 
{
    
     
  
  int i_t[2]={
    
    1,1};

  my_memset(i_t,0,sizeof(i_t));

  for(int i=0;i<(sizeof(i_t)/sizeof(int));i++)
  printf(" %d ",i_t[i]);
  system("pause");
  return 0; 
}

操作結果:

 0  0 

memcpy を実装する

関数プロトタイプ:

void *memcpy(void *dest, void *src, unsigned int count);

関数 function: src が指すメモリ領域から dest が指すメモリ領域に count バイトをコピーします。

コードとテスト機能:

void * my_memcpy(void *dest,void *src,unsigned int size)
{
    
    
    char *b_dest=(char *)dest,*b_src=(char *)src;
    unsigned int len;
    for(len=size;len>0;len--)
    {
    
    
        *b_dest++=*b_src++;
    }
    return dest;
}


int main() 
{
    
     
  
  int i_t[2]={
    
    1,1};
  int i_f[2]={
    
    0,0};

  my_memcpy(i_f,i_t,sizeof(i_t));

  for(int i=0;i<(sizeof(i_t)/sizeof(int));i++)
  printf(" %d ",i_f[i]);
  system("pause");
  return 0; 
}

コードのアイデアの簡単な分析:
コピーとクリアのアイデアは、着信アドレスに対してビットごとの操作を実行することです。

前述のように、void * には型が含まれていないため、任意の型として渡すことができるため、任意の型を操作できます。渡されるのは型のないアドレス情報だけです。C では、ポインタ自体には操作が必要な情報が含まれていないため、操作が必要な長さの情報を渡す必要があります。そうしないと、コンパイラーはポインターを渡すだけでは操作の範囲を認識できません。

要約する

void * ポインターを使用すると、関数の適用性を高めることができますが、void * ポインターの操作には十分注意する必要があります。そうしないと、ポインター操作が範囲外になり、その他の未知のプログラム エラーが発生しやすくなります。

おすすめ

転載: blog.csdn.net/weixin_46185705/article/details/123847127