[C言語]ポインタがフリーダム - ポインタ・配列・文字列・構造体・共用体

『C言語のポインタは難しい』とはよく目にしたけど…難しい、頭痛い。でも自由な感じが楽しくもある。ポインタ虎の巻が理解できればよさそうではあるもののちょいと私の頭ではまだまだ時間が理解するために必要…現在の自分にとってC言語は重要ではないので細かいことは後回し orz

以下のソースはコンパイラやCPUによって結果が違うかもしれないけど、具体的にどう違うのか説明できる知識は無い。動作環境は学習用C言語開発環境 Ver 0.0.8.1@AMD Athlon

ポインタ

#include <stdio.h>

int main(void)
{
  char c1, c2;
  int n1, n2;
  double x1, x2;

  printf(
    "c1 %p \n c2 %p \n n1 %p \n n2 %p \n x1 %p \n x2 %p",
    &c1, &c2, &n1, &n2, &x1, &x2
  );

  return 0;
}

結果

c1 0018FF5F
c2 0018FF5E
n1 0018FF58
n2 0018FF54
x1 0018FF48
x2 0018FF40

アドレス演算子( & ) で変数のアドレスを取得できる。 アドレスの差を見ると、何バイトのデータが入っているか分かる(1バイト毎にアドレス管理されている)

#include <stdio.h>

int main(void)
{
  /*
   * 変数の宣言
   * それぞれの変数にアドレスと値(メモリ)が割り当てられる
   * ポインタ宣言子を使ってポインタを受け取る変数を宣言できる
   */
  int n, *pn, **ppn, ***pppn1, ***pppn2;

  /* pn に n のアドレス を代入 */
  pn = &n;

  /* ppn に pn のアドレス を代入 */
  ppn = &pn;

  /* pppn1 に ppn のアドレスを代入 */
  pppn1 = &ppn;

  /* pppn2 に pppn2 の値を代入 */
  pppn2 = pppn1;

  ***pppn1 = 777;
  printf("%d : %d\n", ***pppn2, n);
  printf("%p : %p\n", **pppn2, pn);
  printf("%p : %p\n", *pppn2, ppn);
  printf("%p : %p\n", pppn2, pppn1);
  printf("%p : %p\n", &pppn2, &pppn1);

  return 0;
}

結果

777 : 777
0018FF5C : 0018FF5C
0018FF58 : 0018FF58
0018FF54 : 0018FF54
0018FF4C : 0018FF50

ポインタを扱う変数を作るときはポインタ宣言子( * ) を使う。 で、ポインタ参照先にアクセスするときも * 記号を使うけどこれを間接参照演算子という。 間接参照演算子により参照先の変数の値を読み取ったり、 参照先の変数に値を入れたりすることができる。

#include <stdio.h>

int main(void)
{
  int n = 777;
  printf("%d", *&n);

  return 0;
}

*&n は… & で nのアドレス を取り出し、そのアドレスの参照先を * で取り出している。よって n と *&n は等価

#include <stdio.h>

int main(void)
{
  int n, *pn, *_pn;

  pn = _pn;
  _pn = &n;
  n = 666;

  printf("%d", *pn);

  return 0;
}

上のコードでは 666 が出力されない。pn は 初期化されていない _pn の値を受け取っているから。ポインタ変数は初期化するのを忘れないこと。

#include <stdio.h>

int main(void)
{
  int n = 0x01234567;
  int* pn = &n;
  char* pc = (char*) pn;
  short* ps = (short*) pn;
  double* px = (double*) pn;

  printf("%p | %p | %p | %p\n", pc, ps, pn, px);
  printf("%d | %d | %d | %f\n\n", *pc, *ps, *pn, *px);

  /*
   * 危険
   */
  px = (double*) n;
  printf("%p : %d", px, px);

  return 0;
}

結果

0018FF5C | 0018FF5C | 0018FF5C | 0018FF5C
103 | 17767 | 19088743 | 0.000000

01234567 : 19088743

キャストもできる。キャストしてもアドレスは変化しない。 値を読み取るときに読みにいくメモリのサイズが変わってくる。 ポインタ変数をインクリメントしたときに増える数値も型によって違う。

また、数値をポインタにキャストしたりもできるけど、上のソースのような使い方は危険なことになる。。

配列

#include <stdio.h>

int main(void)
{
  int n0[0] = {};
  int n1[1] = {};
  int n2[2];
  int n3[3] = {666,777,};
  printf("Size: %2d @ %p\n", sizeof n0, n0);
  printf("Size: %2d @ %p\n", sizeof n1, n1);
  printf("Size: %2d @ %p\n", sizeof n2, n2);
  printf("Size: %2d @ %p\n\n", sizeof n3, n3);

  printf("n3    Size: %2d @ %p / value: %d\n",
    sizeof n3, &n3, n3);
  printf("n3[0] Size: %2d @ %p / value: %d\n",
    sizeof n3[0], &n3[0], n3[0]);
  printf("n3[1] Size: %2d @ %p / value: %d\n",
    sizeof n3[1], &n3[1], n3[1]);
  printf("n3[2] Size: %2d @ %p / value: %d\n\n",
    sizeof n3[2], &n3[2], n3[2]);

  printf("n2[0] = %d", n2[0]);

  return 0;
}

結果

Size:  0 @ 0018FF60
Size:  4 @ 0018FF5C
Size:  8 @ 0018FF54
Size: 12 @ 0018FF48

n3    Size: 12 @ 0018FF48 / value: 1638216
n3[0] Size:  4 @ 0018FF48 / value: 666
n3[1] Size:  4 @ 0018FF4C / value: 777
n3[2] Size:  4 @ 0018FF50 / value: 0

n2[0] = 1973038045
  • 定義時の四角括弧を 配列宣言子 という
  • 配列変数の添え字(index) を入れる四角括弧を 配列添え字演算子 という
  • 波括弧で 配列の初期化 ができる。指定しなかった要素は 0 に初期化される。また要素数を指定していない場合は 波括弧の中の要素数を持つ配列がつくられる。
  • 初期化していない配列の中身は不定
  • 要素数ゼロの配列はメモリが割り当てられない。でもアドレスが割り当てられる。
  • 配列の宣言時に指定した要素数分のメモリが割り当てられる。添え字 0 の割り当ては一番値の小さいアドレスから始まる
  • 配列変数はメモリへの参照値を持っている。参照先を書き換えられないという点がポインタ変数とは異なる。値を代入しようとしてもコンパイルが通らない。

    #include <stdio.h>
    
    int main(void)
    {
    int two = 2;
    int n[3] = {123, 456, 789};
    printf("%d, %d, %d\n", n[0], n[1], n[two]);
    printf("%d, %d, %d\n", 0[n], 1[n], two[n]);
    printf("%d, %d, %d, %d", *(n + 0), *(1 + n), *(n + two), *n);
    
    return 0;
    }
    

結果

123, 456, 789
123, 456, 789
123, 456, 789, 123
#include <stdio.h>

int main(void)
{
  int n[9] = {1 ,2 ,3, 4, 5, 6, 7, 8, 9}, *pn = n, i;

  for(i = 0; i < 9; i++)
  {
    printf("%d,", *pn);
    pn++;
  }
  printf("\b");

  return 0;
}

結果

1,2,3,4,5,6,7,8,9
  • 配列(の参照値)[キー値] は 「配列 + 型のサイズ * キー値」byte のアドレスにある値を読みにいく
  • array[key] == *(array + key) == key[array] == *(key + array)

    #include <stdio.h>
    
    int main(void)
    {
    int n3[3] = {123, 456, 789};
    int n0[0];
    int n1[1];
    
    printf("%p\n%p\n%p\n", n3, n0, n1);
    printf("%d, %d", n0[2], n1[2]);
    
    return 0;
    }
    

結果

0018FF54
0018FF54
0018FF50
789, 456
  • 割り当てられたメモリの外でもアクセス可能

    #include <stdio.h>
    
    void f1(int*);
    int* f2(int[1000]);
    
    int main(void)
    {
    int n[] = {0, 0, 0};
    
    int *_n = f2( n );
    _n[2] = 777;
    
    printf("%d, %d, %d", n[0], n[1], n[2]);
    
    return 0;
    }
    
    void f1(int arg[]){
    arg[0] = 555;
    }
    
    int* f2(int* arg){
    f1(arg);
    arg[1] = 666;
    return arg;
    }
    

結果

555, 666, 777

ポインタ(配列)を関数に渡したり返してもらったり。

多次元配列

#include <stdio.h>

int main(void)
{
  int n[2][3], i, j;

  for(i = 0; i < 2; i++)
  {
    printf("&n[%d] = %p\n", i, &n[i]);

    for(j = 0; j < 3; j++)
    {
      printf("&n[%d][%d] = %p\n", i, j, &n[i][j]);
    }
  }

  return 0;
}

結果

&n[0] = 0018FF48
&n[0][0] = 0018FF48
&n[0][1] = 0018FF4C
&n[0][2] = 0018FF50
&n[1] = 0018FF54
&n[1][0] = 0018FF54
&n[1][1] = 0018FF58
&n[1][2] = 0018FF5C

多次元配列の並び順はこんな感じ。。

#include <stdio.h>

int main(void)
{
  int n[2][3], i, j;

  for(i = 0; i < 2; i++)
  {
    printf("&n[%d] = %p\n", i, n + i);

    for(j = 0; j < 3; j++)
    {
      printf("&n[%d][%d] = %p\n", i, j, *(n + i) + j );
    }
  }

  return 0;
}
  • これの結果は前のコードと同じ
  • array[k1] == *(array + k1)
  • array[k1][k2] == *(array[k1] + k2) == *( *(array + k1) + k2)
  • array[k1][k2][k3] == *( *( *(array + k1) + k2) + k3)

多次元配列を正しく理解するのはなかなか難しそう… ポインタ虎の巻~多次元配列の実現 を読んでもいまいち理解できない部分があった orz

関数へのポインタ

関数宣言時に指定した関数名は、関数へのポインタを持っている。

#include <stdio.h>

int f1(int n){ return n + 100; }
int f2(int n){}

int main(void)
{
  int (*pf)();
  pf = f1;

  printf(
    "%d",
    (*pf)(1)
  );

  // f1 = f2;
  // これは無理。f1 は 変数ではない

  return 0;
}

結果

101
#include <stdio.h>

int f1(int n){ return n + 30; }
int f2(int n){ return n + 200; }
int f3(int n){ return n + 1000; }

int callfuncs(int n, int (*f[])(), int length ){
  int i;
  for(i = 0; i < length; i++){
    n = (*f[i])(n);
  }
  return n;
}

int main(void)
{
  int (*f[3])() = {f1, f2, f3};
  int l = sizeof f / sizeof f[0];

  printf(
    "%d",
    callfuncs( 4, f, l )
  );

  return 0;
}

結果

1234

関数ポインタを配列にすることもできるし、関数に渡すこともできる。

文字列

文字列は…char型の配列で表現できる。文字列の先頭からヌル文字が出現するまでがひとかたまりの文字列になる。 ここでのヌル文字をEOS(End of String)と呼ぶことがある。

#include <stdio.h>

int main(void)
{
  char* c = "str";

  printf("%s のサイズは %d\n", c, sizeof c);
  printf("%c, %c, %c, %c", *c, c[0], c[1], c[2]);

  return 0;
}

結果

str のサイズは 4
s, s, t, r

"str" は3文字だけど、ヌル文字を含めると4文字になるため sizeof演算子 の評価が 4 になる。 ダブルクオートで文字列リテラルを作ることができる。 この文字列リテラルは、文字列の先頭のアドレスを返す。つまり "str" はアドレスの値である。

#include <stdio.h>

int main(void)
{
  printf("%c, %c, %c, %c", *"str", "str"[0], 1["str"], **&(2 + "str"));

  return 0;
}

結果

s, s, t, r

ここで "str" が4つ出てくるけど、これらのアドレスの値はすべて異なる。

文字配列

文字列を配列で扱うことができる。。 指定したアドレスからヌルバイトまでが文字列とみなされるということは…

#include <stdio.h>

int main(void)
{
  char c[10];
  c[0] = 's';
  c[1] = 't';
  c[2] = 'r';
  c[3] = '\0';
  c[4] = 'S';
  c[5] = 'T';
  c[6] = 'R';
  c[7] = '\0';

  printf("%s\n", c);
  printf("%s\n", &c[4]);

  printf("%d", c[8]);

  return 0;
}

結果

str
STR
64

※値を入れていない部分は不定なので注意

#include <stdio.h>

int main(void)
{
  char c[10] = "str\0STR";

  printf("%s\n", c);
  printf("%s\n", &c[4]);
  printf("%d", c[8]);

  return 0;
}

結果

str
STR
0

char型の配列の初期化に文字列を入れた場合、余った部分は ヌル文字 で埋められる

#include <stdio.h>

int main(void)
{
  int n[2][1];
  n[0][0] = 'S' | 't' << 8 | 'r' << 16 | 'i' << 24;
  n[1][0] = 'n' | 'g' << 8 | '!' << 16;

  printf("%s", (char*) n);

  return 0;
}

結果

String!

あんまり意味無いけど多次元配列でもできる。 int型でも強引に文字列を作れるけど、 int型の配列をそのまま文字列として printf で出力できないため char* にキャストしている

#include <stdio.h>

int main(void)
{
  char c1[4];
  c1 = "str";

  char c2[3] = "str";

  char *c3 = "str";

  return 0;
}

上のコードは一箇所コンパイルエラーになる。 またやってはならない間違いが一箇所ある。 これらが何かをしっかり把握しておくこと。

#include <stdio.h>

int main(void)
{
  char* arr[4] = {
  "AB\n",
  "cdef\n",
  "GHIJKL",
  "mnopqrstu"
};

  printf(arr[0]);
  printf(&2[arr[1]]);
  printf(*(arr + 2) + 8);

  return 0;
}

結果

AB
ef
nopqrstu

char*型の値を持つ配列は文字列を持つ配列になる。 たぶん文字列データはメモリ上に連続して並べられる。

構造体

異なるデータ型を1つにまとめて1つのデータ型として扱えるようにするもの。

#include <stdio.h>

struct Foo {
  int n;
  double x;
}; /* 最後にセミコロつけないとコンパイル通らない */

int main(void)
{
  struct Foo bar;
  bar.n = 0xFFFF;
  bar.x = 3.14;

  printf("%5d @ %p\n", bar.n, &bar.n);
  printf("%5.2f @ %p\n", bar.x, &bar.x);

  return 0;
}

結果

65535 @ 0018FF50
 3.14 @ 0018FF58

アドレスがなんだかおかしい気がしたけどどうやら構造体のメモリは配列とは違いやや複雑になっているらしい→C言語-ポインタとメモリと型(構造体)の関係 (2)

#include <stdio.h>

struct X {
  int n1, n2, n3;
  short s1, s2, s3, s4, s5;
  long long x1, x2;
} x ;

int main(void)
{
  x.n1 = 1;
  x.n2 = 2;
  x.n3 = 3;
  x.s1 = 0x1212;
  x.s2 = 0x3434;
  x.s3 = 0x2121;
  x.s4 = 0x4343;
  x.s5 = 0x5656;
  x.x1 = 0x1111222233334444;
  x.x2 = 0x5555666677778888;
  printf("%x %x %x\n %x %x %x\n %x %x %x %x",x,x,x,x,x,x,x,x,x,x,x,x,x,x,x);

  return 0;
}

結果

1 2 3
34341212 43432121 5656
33334444 11112222 77778888 55556666

これは構造体の定義時と同時に構造体変数を宣言している。 この場合はあとから構造体変数を追加宣言できない…って『猫でも分かるC言語プログラミング』に買いてあるけど、このIDEでは問題なくあとから追加できた。ようするにコンパイラ次第ってことなのかな?

ところで…構造体変数自体 を printf で出力するとなんだかおかしなことになった。 なんとなく法則性は見えるけどなんだか良く分からない… これ何が起こってるの?

#include <stdio.h>

struct X {
  int n1, n2, n3;
};

int main(void)
{
  int *p;
  struct X x = { 0x11112222, 0x33334444, 0x55556666 };

  printf("               x.n1 = %x\n", x.n1);
  printf("x = %x\n", x);
  printf("x = %x | x.n1 = %x\n\n", x, x.n1);

  p = (int*) &x;
  printf("%28x %x %x\n", p[0], p[1], p[2]);
  printf("%17x | %x %x %x\n", x, p[0], p[1], p[2]);
  printf("%x %x | %x %x %x", x, x, p[0], p[1], p[2]);

  return 0;
}

結果

               x.n1 = 11112222
x = 11112222
x = 11112222 | x.n1 = 33334444

                    11112222 33334444 55556666
         11112222 | 33334444 55556666 11112222
11112222 33334444 | 55556666 11112222 33334444

ここでは構造体変数宣言時に初期化している。 で、動作結果は…どういう動作をしているのかは なんとなく分かるんだけど訳がわからないよ… いろいろと知識不足なのでこのあたりで考えるのを止めた

#include <stdio.h>

struct X {
  int n1, n2, n3;
} x1 = {1, 2, 3}, x2 = x1;

int main(void)
{

  printf("%d %d %d \n", x1.n1, x1.n2, x1.n3);

  x1.n1 = 999;
  x1.n2 = 888;
  x1.n3 = 777;

  printf("%d %d %d", x2.n1, x2.n2, x2.n3);

  return 0;
}

結果

1 2 3
1 2 3

x2 = x1 により x1 の値が x2 にディープコピーされる。 構造体はポインタではなくデータを持つ。

#include <stdio.h>

struct data {
  int n1, n2;
};

int main(void)
{
  struct data arr[2] = { {1, 2}, {4, } };

  printf(
    "sizeof arr : %d\narr[0].n2 : %d\narr[1].n1",
    sizeof arr, arr[0].n2, arr[1].n1
  );

  return 0;
}

結果

sizeof arr : 16
arr[0].n2 : 2
arr[1].n1続行するには

構造体の配列とアクセス

#include <stdio.h>

struct data {
  int n1, n2;
} d;

int main(void)
{
  struct data *pd = &d;

  (*pd).n1 = 333;
  pd->n2 = 777;

  printf("%d %d", d.n1, d.n2);

  return 0;
}

構造体へのポインタと、ポインタから構造体へのアクセス。 (*pd) は構造体なので (*pd).n1 とすればアクセスできる。 (*pd).n1 は アロー演算子 をつかって pd->n1 ともできる。

typedef指定子

typedef指定子は 型に別の名前をつけることができる。

typedef 型 新しい型名;

#include <stdio.h>

typedef int IntX, IntY;

typedef struct data{
  IntX x;
  IntY y;
} Data, *pData;

int main(void)
{
  IntX x = 666;
  IntY y = 777;
  Data d = { x, y };
  pData pd = &d;

  printf("%d, %d", pd->x, pd->y);

  return 0;
}

結果

666 777
#include <stdio.h>

typedef struct data1 { int n; } Data1;
typedef struct data2 { int n; } Data2, Data3;

int main(void)
{
  Data1 d1 = {1};
  Data2 d2 = {2};
  Data3 d3;
  d3 = d2;
  d3 = d1;

  return 0;
}

11行目はコンパイルエラーにならないが 12行目はコンパイルエラーになる。

自己参照構造体

メンバに自分と同じ型の構造体へのポインタを持っている構造体

#include <stdio.h>

typedef struct data {
  int n;
  struct data *d;
} D, *pD;

int main(void)
{
  D d1 = {1, NULL};
  D d2 = {2, &d1};
  D d3 = {3, &d2};
  pD p;

  for(p = &d3; p; p = p->d){
    printf("%d ", p->n);
  }

  return 0;
}

結果

3, 2, 1

struct data *d を void *d に変えても動く。

共用体

構造体みたいに定義するものの、保持できるデータはたった一つだけなデータ型 共用体定義の文法は構造体と同じ。

#include <stdio.h>

typedef union test {
  int n;
  char c;
} U, *pU;

int main(void)
{
  U u;
  pU p = &u;
  printf("size u : %8d, size p : %8d \n", sizeof u, sizeof p);
  printf("&u     : %8p, &p     : %8p \n", &u, &p);

  u.n = 777;
  printf("u.n    : %8d, p->n   : %8d \n", u.n, p->n);

  p->c = 'a';
  printf("p->n   : %8d, p->c   : %8c", p->n, p->c);

  return 0;
}

結果

size u :        4, size p :        4
&u     : 0018FF5C, &p     : 0018FF58
u.n    :      777, p->n   :      777
p->n   :      865, p->c   :        a

参考にしたモノ

Share
関連記事