プログラミング素人のはてなブログ

プログラミングも電気回路も専門外の技術屋の末端が勉強したことや作品をアウトプットするブログ。コードに間違いなど見つけられたら、気軽にコメントください。 C#、Python3、ラズパイなど。

ポインタと配列と構造体とメモリの切っても切れない関係(C言語)

配列や構造体にはポインタでアクセスできる

配列や構造体はPythonC#等でも使えて、ほぼ同じように使えます。
しかしながら、C言語ではこれに加えてポインタでアクセスする方法があります。

配列へのアクセス

まずは、配列の要素を出力してみます。これはPythonC#しか触ったことがない人でも理解できると思います。
ただし、ここで要素数sizeof()で求めています。
これは配列の確保したメモリサイズを配列の1つ目の要素のメモリサイズで割ることで要素数が求まる、というものです。
ここでは、10進数と16進数で出力するようにします。往々にしてC言語では16進数で出力するほうが便利なことがあるためです。

  printf("-array1\n");
  int arr1[] = {11, 22, 33, 44, 0x123456, 0x1234};
  printf("0x - 0d  \n");
  for (int i = 0; i < sizeof(arr1) / sizeof(int); i++)
  {
    printf("%08X - %08d \n", arr1[i], arr1[i]);
  }
/*
0x - 0d
0000000B - 00000011
00000016 - 00000022
00000021 - 00000033
0000002C - 00000044
00123456 - 01193046
00001234 - 00004660
*/

配列をByte列としてポインタでアクセス

次に、配列をポインタにキャストして4つまとまり毎に出力してみます。
ポインタにすることによって、Byte列として出力を見ることが出来ます。
これは、C言語が確保したメモリ上の並びを見ていることになります。
ここでは16進数で出力していますが、これをよく見ると、下位の桁から出力されていることが分かります。
これはこのC言語処理系では小さい桁(Byte)から小さいアドレスに格納(し読みだし)する処理系であるためです。
このような小さい桁(Byte)から小さいアドレスに格納(し読みだし)する処理系のことをリトルエンディアンといいます。
Windows PCなどがこれにあたり、逆に大きいByteから小さいアドレスに格納(し読みだし)する処理系をビッグエンディアンといいます。
異なる処理系(例えばwebサーバー等)と通信する場合は、この並びがどうなっているかを考慮して通信する必要があります。
Pythonなどはライブラリで自動的に処理されている気がします。

  printf("-array2\n");
  int arr2[] = {1, 2, 3, 4, 0x123456, 0x7890};
  unsigned char *ptr0 = (unsigned char *)arr2;

  for (int i = 0; i < sizeof(arr2); i++)
  {
    printf("%02x ", *(ptr0 + i));
    if (i % 4 == 3)
      printf("\n");
  }
/*
01 00 00 00
02 00 00 00
03 00 00 00
04 00 00 00
56 34 12 00
90 78 00 00
*/

構造体へのアクセス

次に構造体を考えます。
構造体とは、型の違う変数(同じでもよい)をひとまとめに出来るものです。
ここでも、構造体のByte列を見るためにポインタに格納します。
これを16進数、10進数、charで出力します。
このようにして、構造体のByte列を直接確認することが出来ます。
ここで、数値については10進数であるか16進数であるかと、先ほどのエンディアンが逆であることに気を付けて見てみると、構造体の要素が順番に並んでいますが、隙間があることが分かります。一般的には4byte毎に並べられています。
これはこのほうがアクセスするのに好都合(速度が速くなる)だからだそうです。
address[5]のように4byteの倍数でない場合は、余計に3byte確保したような見え方になります。
かといって8byte確保しているわけではないはずなので、address[5]に8文字格納するようなことは行わないほうがよいです。
ここでprintf(”%c", 変数)とすると、変数のasciiとしての文字を表示します。

#include <stdio.h>
#include <cstring> //strcpy,memcpyなど

  printf("\n-shiro\n");
  typedef struct
  {
    char name[8];
    int age;
    char address[5];
    int no;
  } man;

  man shiro;
  strcpy(shiro.name, "shiro");
  shiro.age = 54;
  strcpy(shiro.address, "tokyo");
  shiro.no = 0x12345678;

  // 構造体の全要素の文字コードを出力する
  unsigned char *ptr6 = (unsigned char *)&shiro;
  printf("0x - 0d  - char\n");
  for (int i = 0; i < sizeof(shiro); i++)
  {
    printf("%02X - %03d - %c\n", ptr6[i], ptr6[i], ptr6[i]);
  }
/*
  0x - 0d  - char
73 - 115 - s
68 - 104 - h
69 - 105 - i
72 - 114 - r
6F - 111 - o
00 - 000 -
00 - 000 -
00 - 000 -
36 - 054 - 6
00 - 000 -
00 - 000 -
00 - 000 -
74 - 116 - t
6F - 111 - o
6B - 107 - k
79 - 121 - y
6F - 111 - o
00 - 000 -
40 - 064 - @
00 - 000 -
78 - 120 - x
56 - 086 - V
34 - 052 - 4
12 - 018 - 
*/

構造体に含まれる配列も、通常の配列と同様に扱うことが出来ます。
C言語では「文字列型」というものが無く、文字charの配列として表現されます。
しかしながら、C#等のように要素インデックスでアクセスすることもできます。
char[]型の配列についてみると、以下のようになります。
配列の1つ目の要素は「配列」と同じものを示します。
ここでprintf(”%d", &変数)と&を付けるとその変数の格納されたアドレスを示します。
char型は1byteの変数なのでアドレスは1づつずれることになります。
int型であれば一般に4byteなので、4づつずれます。
また、本質的にC言語の文字は数値であり、printf(”%d", taro.name[0])printf(”%x", taro.name[0])とすれば文字コード(asciiコード)を示し、printf(”%c", taro.name[0])とすれば文字を表示します。

#include <stdio.h>
#include <cstring> //strcpy,memcpyなど

struct person
{
  char name[4];
  int age;
  int no;
};

int main(void)
{
  struct person taro;
  // char 型の配列へはstrcpyを使う
  strcpy(taro.name, "taro");

  printf("name= %s\n", taro.name); // taro
  printf("age= %d\n", taro.age);   // 10
  printf("-Char\n");
  printf("      0x - 0d   - char - Address\n");
  printf("name= %02x - %04d - %c    - %08d\n", *taro.name, *taro.name, *taro.name, &taro.name);             // t
  printf("name= %02x - %04d - %c    - %08d\n", taro.name[0], taro.name[0], taro.name[0], &taro.name[0]); // t
  printf("name= %02x - %04d - %c    - %08d\n", taro.name[1], taro.name[1], taro.name[1], &taro.name[1]); // a
  printf("name= %02x - %04d - %c    - %08d\n", taro.name[2], taro.name[2], taro.name[2], &taro.name[2]); // r
  printf("name= %02x - %04d - %c    - %08d\n", taro.name[3], taro.name[3], taro.name[3], &taro.name[3]); // o
}

/*
name= taro
age= 0
-Char
      0x - 0d   - char - Address
name= 74 - 0116 - t    - 06421980
name= 74 - 0116 - t    - 06421980
name= 61 - 0097 - a    - 06421981
name= 72 - 0114 - r    - 06421982
name= 6f - 0111 - o    - 06421983
*/

++演算子

ポインタも++演算子を使うことが出来ます。++演算子を使うと、以下のようにも文字列にアクセスもできます。
ここで、()はあってもなくても++でアドレスをインクリメントしてアクセスすることが出来ます。
最後の一回は何も格納されていない部分へアクセスしているので、表示されません。

  struct person jiro;
  strcpy(jiro.name, "jiro");
  char *ptr2 = jiro.name;
  printf("-jiro\n");
  printf("name= %c\n", *(ptr2)++);
  printf("name= %c\n", *ptr2++);
  printf("name= %c\n", *ptr2++);
  printf("name= %c\n", *(ptr2)++);
  printf("name= %c\n", *ptr2);
/*
name= j
name= i
name= r
name= o
name=
*/

まとめ

C言語のポインタで出来ることをまとめました。
github.com