Hatena::ブログ(Diary)

A Day In The Life RSSフィード Twitter

2009-04-14

ポインタと配列

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つのポインタだけである。

ポインタについて考える際には、メモリによって解釈の仕方が決まっているわけではない、ということを覚えておくとよいだろう。特定のアドレスにあるビットが意味のあるデータに解釈されるのは、特定の型のポインタを使った場合だけである。CODE Complete(上)

そもそもポインタを普通の変数と同じように考えるから間違ったり勘違いしてしまうんじゃないかと。変数ポインタは全く違うものだと考えれば結構すっきりしました。ポインタはあくまでアドレスを格納しているだけでアドレスの指してる先はXX型のデータですよってことなんですね。

もう少し詳しく説明すると

配列ポインタコンパイラにとっては別物であり、実行時にも別物として扱われ、別のコードが生成される。コンパイラにとっては、配列はアドレスであり、ポインタとはアドレスのアドレスだ。エキスパートCプログラミング
になります。

説明だとわかりずらいのでポインタ配列のアドレスを渡す手順を図にしてみました(関数内で宣言された場合)。

配列とポインタのイメージ

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セグメント内に配置される。エキスパートCプログラミング
コードにするとこんな感じです。

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で検証しました。

参考

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

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

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

参考書籍

エキスパートCプログラミングエキスパートCプログラミング―知られざるCの深層 (Ascii books)
ピーター ヴァン・デ・リンデン
Code Complete第2版〈上〉―完全なプログラミングを目指してCode Complete第2版〈上〉―完全なプログラミングを目指して
スティーブ マコネル

kondoumhkondoumh 2009/04/16 19:20 文字列リテラルは書き換え不可能な特殊な領域で管理されていると思います(コンパイラー依存かな)。

glass-_-onionglass-_-onion 2009/04/17 11:46 >kondoumhさん
調べてみると「ANSI規格では、文字列リテラルの変更は未定義」とありました。Javaの感覚で文字列をいじらないように気を付けます。勉強になりました。ありがとうございます。

fusatsukatsujinfusatsukatsujin 2009/04/20 20:05 この炎上記事すごいですね。
ブクマコメントが、めちゃくちゃ笑えました。
職場で噴出しそうになっちゃいましたよ。

glass-_-onionglass-_-onion 2009/04/21 14:44 >fusatsukatsujinさん
あの記事ですが、自分のようなゆとりプログラマにはいい教訓になりました。サンプルコードの検証は大事ですね。

glass-_-onionglass-_-onion 2009/04/21 19:02 Joelさんのこの記事も参考になりますね。
http://local.joelonsoftware.com/mediawiki/index.php/Javaスクールの危険

fusatsukatsujinfusatsukatsujin 2009/04/23 00:39 こんなのもありました。

http://airbook.jp/AirSIN/631#

glass-_-onionglass-_-onion 2009/04/26 10:01 ひどいwww

スパム対策のためのダミーです。もし見えても何も入力しないでください
ゲスト


画像認証

トラックバック - http://d.hatena.ne.jp/glass-_-onion/20090414/1239716083