読者です 読者をやめる 読者になる 読者になる

A Day In The Life

とあるプログラマの備忘録

ポインタと配列

C言語のポインタと配列について理解があいまいなところがあったので整理します。
↓のコードがそもそもの混乱の原因です。

int main() {
  int a[] = {1,2,3};
  int *b = a;
  :
  return 0;
}

↓だと混乱しないのですが...。

int main() {
  int a[] = {1,2,3};
  int *b = &(a[0]);
  :
  return 0;
}

配列名だけだと配列の先頭アドレスを指すのでint *b = a;int *b = &(a[0]);って同じことやってるんですね。
はじめのコードだけ見るとポインタと配列って同じじゃないかと錯覚してしまいます。
このあたりの疑問はポインタはメモリのアドレスさしてるだけだってことと、メモリのどこ(スタックorヒープor静的領域)を使ってるのか意識することでだいぶ理解することができました。Cのメモリ管理のついては以下のページが参考になります。

基本

以下のようなコードがある場合、この2つの違いを表に示します。

int main() {
  int *a = (int *)malloc(sizeof(int) * 3);  …1
  int b[] = {1,2,3};                        …2
}

  1の場合(ポインタ) 2の場合(配列)
メモリ参照 間接メモリ参照 直接メモリ参照
利用領域 ヒープを使用 スタックを使用
メモリ管理 free()で明示的に領域を開放する 関数を抜ければ自動的に削除される

注意点としてはポインタだからヒープ使用というわけではありません。malloc()を使うとヒープに領域が作成されます。同じように配列も関数内で宣言されたときはスタックを使用しますがそれ以外(グローバル変数等)だとスタックは使用しません。

ポインタって何?

ポインタの説明で一番わかりやすかったのがCODE Complete(上)の395ページの説明です。

ポインタが整数を指しているとしたら、その本当の意味は、コンパイラがそのメモリアドレスの内容を整数として解釈する、ということである。もちろん整数ポインタ、文字列ポインタ、浮動小数点数ポインタのどれもが、同じメモリアドレスを指していてもかまわない。だが、そのメモリアドレスの内容を正しく解釈するのは、そのうちどれか1つのポインタだけである。
ポインタについて考える際には、メモリによって解釈の仕方が決まっているわけではない、ということを覚えておくとよいだろう。特定のアドレスにあるビットが意味のあるデータに解釈されるのは、特定の型のポインタを使った場合だけである。
そもそもポインタを普通の変数と同じように考えるから間違ったり勘違いしてしまうんじゃないかと。変数とポインタは全く違うものだと考えれば結構すっきりしました。ポインタはあくまでアドレスを格納しているだけでアドレスの指してる先はXX型のデータですよってことなんですね。
もう少し詳しく説明すると
配列とポインタはコンパイラにとっては別物であり、実行時にも別物として扱われ、別のコードが生成される。コンパイラにとっては、配列はアドレスであり、ポインタとはアドレスのアドレスだ。
になります。
説明だとわかりずらいのでポインタに配列のアドレスを渡す手順を図にしてみました(関数内で宣言された場合)。
配列とポインタのイメージ
int *b = &a[0];のところはint *b = a;と書いても同じです。配列の場合、配列名だけ指定すると配列の先頭アドレスを指します。これが普通の変数と配列の違うところです。そして混乱のもとでもあります。

スタックとヒープどういうときにどちらを使うのか

malloc()関数を使うとヒープにメモリが確保されるということがわかりましたが、char *hoge = "hogehoge";の場合どこにメモリを確保するのかわかりませんでした。

int main() {
  // スタックを使用
  int a = 5;
  // スタックを使用
  char b[] = "abcdefg";
  // スタックを使用
  int *c = &a;
  // ヒープを使用
  int *d = (int *)malloc(sizeof(int));
  // ヒープorスタック???(それ以外???)
  char *e = "hogehoge";
  :
  return 0;
}

今のところmalloc()関数(その他calloc,reallocも)を使わない限りスタックを使うと理解しています。でも文字列リテラルは特別なのかもしれません。
そこで以下のコードで出力結果を検証してみました。

char *get_str() {
  char *str = "abcdefg";
  return str;
}

int main() {
  char *str = get_str();
  printf("文字:%s\n", str);
  printf("文字:%s\n", str);
  hoge();
  printf("文字:%s\n", str);
}

出力結果

文字:abcdefg
文字:abcdefg
文字:abcdefg

きちんと文字が出力されたのでchar *hoge = "hogehoge";の場合ヒープにメモリ確保されると思われます(静的領域かもしれないという疑問が残りますが...)。

追記

id:kondoumhさんから書き換え不可能な特殊な領域と教えていただきました。ありがとうございました。
また処理系によっても動きが違うようです。安全に文字列返す場合はstatic変数使うのが良さげです(後述)。

char型と他の型との違い

char *hoge = "hoge";とできるのはchar型のときだけでほかの型だとエラーになります。char型で文字列リテラル使うときだけ特殊です。

int main() {
  // OK
  char *a = "hoge";
  // エラー
  long *b = 5;
  // これもエラー
  long *c = { 1, 2, 3 };
  :
  return 0; 
}

ではchar型以外のポインタを使うときはどうすればいいかというと領域を確保してから(アドレスを決めてから)値を代入してやればOKです。

int main() {
  // OK(スタックを使用)
  long a;
  long *b = &a; /* 変数aのアドレスを代入 */
  *b = 5;
  // OK(ヒープを使用)
  long *c = (long *)malloc(sizeof(long));
  *c = 10;
  // OK(ヒープを使用)
  int *d = (int *)malloc(sizeof(int) * 3);
  *d = 1;
  *(d + 1) = 2;
  *(d + 2) = 3;
  return 0;
}

変数bはスタック、変数c,dはヒープを使用します。

関数のリターンで気をつけること

以下のプログラムが想定通りに動作しないのはスタックを使用している変数を戻り値にしているからです(スタックは関数を抜けると削除対象になる)。

char *hoge() {
  // コンパイラで警告が出ます。
  char a[] = "hogehoge";
  return a;
}
char *hoge() {
  // 警告なしで動くけど、わけのわからん文字が返ってきます。
  char a[] = "hogehoge";
  return &(a[0]);
}

同じ理由で以下のコードも危険です。たまたまうまく動く場合もありますが、別の関数呼んだりしてスタック領域使ってやるとおかしくなります。

int *get_num() {
  int a = 10;
  int *b = &a;
  return b;
}

int main() {
  int *a = get_num();
  printf("数字:%d\n", *a);
  printf("数字:%d\n", *a);
  hoge();
  printf("数字:%d\n", *a);
}

環境によって違うと思いますが、自分の環境で実行してみると以下のような出力結果になりました。

数字:10
数字:11225804
数字:789113267

1回目だけ見るとちゃんと動いているように見えちゃうのでものすごく危険です。
上記の関数のように、関数内で定義したものへのポインタを返す場合はstatic変数を使うとよいみたいです。

関数内で定義したものへのポインタを返したければ、それをstaticと宣言しなくてはならない。これによって、変数はスタック上ではなくdataセグメント内に配置される。
コードにするとこんな感じです。

char *hoge() {
  // OK static宣言して静的領域を使う
  static char a[] = "hogehoge";
  return a;
}

配列とポインタが同じように扱われることもあります

配列が関数の引数として宣言された場合、配列はコンパイラによってポインタに変換されます。
なので以下の関数は同じ意味になります。

void hoge(int args[]) {
  :
}
void hoge(int *args) {
  :
}

ということは関数に配列を渡したときは配列のアドレスが渡るということになります。配列の値がコピーされて渡るわけではありません。

void hoge(int a[]) {
  a[0] = 1000;
  a[2] = 3000;
}

int main() {
  int a[] = { 1, 2, 3 };
  printf("数字:{%d,%d,%d}\n", a[0], a[1], a[2]);
  hoge(a);
  printf("数字:{%d,%d,%d}\n", a[0], a[1], a[2]);
}

出力結果は以下のようになります。値がコピーされてるわけじゃなくて、アドレスが渡されていることが確認できます。

数字:{1,2,3}
数字:{1000,2,3000}

まとめ

こうして自分の知識を整理してみるとまだまだ曖昧なところが多いことに気づきました。とはいえポインタが何かメモリがどのように使われているか、少しづつですが理解できるようになりました。ポインタが理解できてくるとC言語の勉強が楽しくなってきました。

今回使用した開発環境

この記事のサンプルコードはVisual C++ 2008で検証しました。

参考

昨年話題になった炎上記事です。記事の本文は参考にしないでください。記事のコメントを読むとすごく勉強になります。

この記事のブックマークコメントも参考になりました。

こちらの記事の説明がとてもわかりやすくてためになりました。