10.ポインタ

10-6. ポインタで配列

前回、配列を関数へ引き渡す方法とそれを配列・ポインタで受け取る方法を説明しました。それではポインタで配列ができるのではないかということで今回はポインタで配列を作ってみます。

◇ポインタで配列

これは前回のプログラム。(プロトタイプ宣言等は省略しています)

 1:main()
 2:{
 3:    int i[]={1,2};
 4:    int ans;
 5:    ans=func(i);
 6:}
 7:
 8:int func(int *x)
 9:{
10:    return (*x * *(x+1));
11:}

これで、配列をfunc()に渡しポインタで受け取りました。

引数の i は配列 i の先頭アドレスを意味します。
それをポインタに渡したことでポインタxは配列をアクセスすることができました。その時の対応は次のとおりです。

x –> i(アドレス)
*x –> i[0]
*(x+1) –> i[1]

ということで最初からポインタで配列が作れないかやってみることにしました。

#include<stdio.h>

main()
{
    char *c ="Cプロダクション";
    printf("%s",c);
}

なぜ、文字列かというとint型ではコンパイルできませんでした(:_;)
文字列のみ宣言できるようです。

ここで注意してもらいたいのは初期化している代入式は

c = “Cプロダクション”;

であることです。宣言時の * はポインタ型であることを表すもので間接演算子の * ではないので注意してください。
ということでポインタの初期化は値でなくあくまでアドレスを渡していることになります。

前回のプログラムの8行目 int func(int *x) も同様で
x に配列の先頭アドレスを代入しているのです。

C言語のポインタではここらへんが勘違いしやすいところなのですがC++ではポインタの宣言が次のようになっているのでわかりやすいと思います。
int* i;
char* c = "Cプロダクション";

■最重要事項■
次にポインタを初期化する場合の注意。

#include<stdio.h>

main()
{
    char *c;
    c ="Cプロダクション";
    printf("%s",c);
}

DOS系では上のように記述しても動作しますが、理論的にも実運用てきにも危険極まりありません。

これで「ページ違反です」とか「スタックエラーです」というダイアログが出てプログラムの強制終了が出るくらいならまだマシでOSがフリーズしてもおかしくないほど危険です。(いまのところエラーは出ませんが・・・)

普通こういう場合はmalloc関数を使います。(これは後で・・・)
特に文字列が標準入力からだったりすると必須です。

因みに上のプログラムLinuxサーバでコンパイル・実行したところ一瞬で危険なプロセスと判断されたのか抹殺されてコアを吐いて死にました。

別にサーバーにはまったく影響ないのですが、自分でサーバーでの使用を禁止しておきながら・・・反省です。

みなさんは絶対サーバー(特にプロバイダの)では実行しないでくださいね。
もし暴走したりしたらまわりに迷惑がかかるだけでなく自分の接続権利が抹殺されるかもしれないので(笑)。

2011-08-09追記:
上記の理由についてご質問がありましたので。回答を追記いたします。

理論的に危険な理由:
C言語の変数は初期化が必要でその際に必要なメモリーを確保します。
char配列で文字列を初期化する場合は文字列の長さに合わせてメモリーの確保が行われます。

一方ポインタは参照先のアドレスと変数の型のみを定義し、メモリの確保はポインタ分だけで参照先の分はわからないので確保しません。
その後ポインタに対して初期化を行う場合、ポインタに割り当てるアドレスは変数として確保されているメモリの先頭アドレスになりますが、ここに未定義のアドレスやプログラム実行用(ワークメモリ)以外のアドレスを割り当てた場合、理論的には参照先が存在しないことになります。
Java的にはNullPointerExceptionってところでしょうか。C言語系ではコンパイル時にチェックされない可能性がありますので見落としやすいところです。

実運用的に危険な理由:
サンプルのプログラムの場合”Cプロダクション”は直接ソースに書かれている文字列で、実行時はメモリに展開したプログラム本体の中にあり、プログラムが使用するワークメモリ領域とは別の場所になります。
このようなアクセスを許すと、プログラムが自分自身を書き換えたり、他の実行中プログラムにアクセスして改変できてしまうため32bit以上の処理系の場合、メモリアクセスを監視して、アクセス違反があった場合にプログラムを強制停止しています。現在は16bit処理系が使われることが殆どなくなっていると思いますのでプログラムが強制終了される程度で済む場合が多いかと思います。

16bit処理系でサンプルプログラムを実行した場合”Cプロダクション”には最後にnull文字が入っているためprintf(“%s”,c)で出力してもnull文字でストップしますが、これが文字列ではなく文字で初期化(シングルクォートで挟んだ1文字)した場合、null文字が付与されない為、printfはnull文字が見つかるまでひたすらメモリーの内容を出力します。書き込みに成功した場合は実行中のプログラム自身が破壊される事になります。

以上でC言語で扱う変数などのオブジェクトは明示的にメモリ確保されたものなのでポインタでアクセスする際も正しくメモリ確保されたものに割り当てる必要があります。それでも配列や文字列の場合はループ上限の数え間違い等で確保された領域を飛び出し強制終了されることがありますので油断はできません。

2011-08-10追記:
昨日の追記について私の知識不足により不適切な表現がございました。
上記の「ワークメモリ領域」→.bssまたは.dataに該当
「直接ソースに書かれている文字列」→.rodataに該当
と解釈頂ければと思います。
メモリのアクセス違反の危険性についても認識違いや誇張した表現で不適切と判断いたしました。