当記事は、全五回シリーズの五回目最終回。
今回はmain関数の引数に登場する
char *argv[] char **argv
と言ったポインタ型変数の意味を理解する。
では、本題に入ろう。
main関数引数の *argv[] あるいは **argv の意味を理解する
ここまでの説明で、C言語初心者の人でもポインタとはどういう物なのかある程度理解出来たと思う。
また、ポインタをどういう時に使うのかという例として、関数の引数で配列を渡す場合のやり方について理解出来たと思う。
もし、ここまでのワテの説明が良く分からないと言う人は、ご遠慮なくコメント欄で質問などお待ちしています。お気軽にお問い合わせください。
では、いよいよ、
第一回目に登場した、
#include <stdio.h> int main(int argc, char **argv) { printf("hello, world\n"); for (int i = 0; i < argc; i++) { printf("%s\n", argv[i]); } return 0; }
或いは、
#include <stdio.h> int main(int argc, char *argv[]) { for (int i = 0; i < argc; i++) { printf("->%s<-\n", argv[i]); } return 0; }
の **argv や *argv[] の意味を理解しよう。
文字列型の配列を定義する
#include <stdio.h> int main(int argc, char *argv[]) { int arrI [10] ={ 0 , 1 , 2 , 3 , 4 , 5 , 6 , 7 , 8 , 9 }; char arrC1[10] ={'h', 'e', 'l', 'l', 'o', 'w', 'o', 'r', 'l', 'd' }; char arrC2[10] = "helloworld"; for (int i = 0; i < 10; i++) { printf("->%d<-\n", arrI[i]); printf("->%c<-\n", arrC1[i]); printf("->%c<-\n", arrC2[i]); } getchar(); return 0; }
int arrI[10]は、今まで何度も例に出てきた整数型の配列だ。
char arrC1[10]は、今回初めて登場するchar型の配列だ。
C言語ではchar型というのは1バイト(=8ビット)の大きさとなる。
char型とは、つまり文字通りascii characterなどの文字を保管するなどの用途だが、8ビットつまり-128~127までの整数なら何でも保管出来る。
ちなみに
unsigned char型なら 0~255までの整数を保管できる。
さて、配列の初期化は、その配列を宣言する時に、同時に初期値を与えて、
char arrC1[10] = { 'h', 'e', 'l', 'l', 'o', 'w', 'o', 'r', 'l', 'd' };
とする事が可能だ。
JavaScript, PHPなどでは文字列を表す場合には、
'文字列' "文字列"
も全く同じ、あるいはほぼ同じような意味になるが、C言語の場合にはシングルクオーテーションとダブルクオーテーションの記号は厳密に使い分ける必要がある。
‘A’ はAという文字のアスキーコードを表すので10進数で言うと65となる。
’ABC’ と言う記述はエラーとなる。シングルクオーテーションで囲えるのはあくまで一文字だけだ。
あるいは、配列の初期化は、
char arrC2[10] = "helloworld";
のように記述する事も可能だ。
この場合には、配列初期化のデータが、
"文字列"
の形式となっているが、ダブルクオーテーションで囲った文字列は、文字列リテラルと呼ばれる。意味は、arrC1の初期化と全く同じで、10個の連なった文字列を一個ずつの文字に分解して、それぞれを配列に格納してくれる。
なので実用的にはこのarrC2の初期化のやり方のほうが使い易いと思う。
要するにC言語で文字列を扱いたい場合には、その文字列を保管するだけの要素数のchar型の配列を使うのだ。
なので、文字列を扱う時点で1次元の配列になる訳だから、複数の文字列を扱う場合だと、必然的に二次元の配列になる。
それ故に、このプログラムをコンパイルしてリンクして実行プログラムの名前が main.exe だとすると、コマンドプロンプトを表示しておいて、
C:\> main.exe 引数1 引数2 引数3 [ENTER]
のように実行すれば、argc=4となり、argvには四つの文字列、
"main.exe" "引数1" "引数2" "引数3"
が格納されるのだが、これらを保管している配列argvは2次元のchar型の配列なのだ。
注意:厳密に言うとchar型2元配列ではなくて、char*型1次元配列と言うのが正しいな。
で、そのchar*が指している先に1次元のchar配列がある。なので、あたかもchar型2次元配列のように複数の文字列を保管できる。そういう感じだ。
そのargvがどのように文字列を格納しているのかを見る前に、C言語で文字列を扱う場合の重要な決まり事を覚えておくと良い。
文字列の終わりは ‘\0’ で検出する
C言語には文字列を処理する幾つかの標準的な関数がある。
int strlen(char *p); // 文字列の長さを求める。 char* strcat(char *p1, char *p2); // 二つの文字列を連結する。
その他、数十種類くらいある。
この時に注意しなくてはならないのは、C言語の場合、文字列の末尾は ‘\0’ という文字で検出する事になっている。’\0’と言う文字は数字で言うと0だ。アスキーコード表の先頭にある。
null文字とも言われる。(大文字でNULLと書く場合もあるようだが、K&Rでは小文字のnullとなっている。nullとNULLが同じなのかどうか、ワテは良く知らない。たぶん同じで0だろう。)
例えば文字列の長さを求める関数strlenを実行した場合に、引数で与えたアドレス p から順番にメモリを増やして行きながらその番地のデータを読み取ってもし ‘\0’ と言う文字が出てきたら文字列の末尾に到達したと判定する。なので、もし文字列の末尾に ‘\0’ が付いていない場合には、誤った結果を返す事になる。
現実には、’\0′ が出て来るまでメモリをどんどん読み進めて行くとどこかで偶然に ‘\0’ が出て来るので、その場所までの長さがstrlen関数の結果として得られる。C言語のこういう、或る意味いい加減な設計が、Cで作ったシステムのセキュリティ問題の原因にもなるのだが、K&Rさん達がC言語を設計した時代にはそんなのを悪用する奴は想定していなかったんだろうと思う。
性善説と言う奴やな。
さて、本題に戻って、
例えば、上の例に出てきた、二つの文字列では、
char arrC1[10] ={'h', 'e', 'l', 'l', 'o', 'w', 'o', 'r', 'l', 'd' }; char arrC2[10] = "helloworld";
のように確保している。
これで文法的には間違いでは無いのだが、もしこれらの文字列の長さをstrlenで求めたい場合には、このままではおかしな値になる可能性が高い。末尾に ‘0’ が無いからだ。
もっともこの例の場合には、自分で10文字分の大きさの配列を定義しているのだから、strlen関数を使うまでもなく文字列の長さ(=配列の要素数)は10文字と分かっている。
なのでわざわざstrlen()関数を実行するまでもない。
しかしながら、文字列の末尾には ‘\0’ を付けておく習慣を付けておくほうが良いので、上記の例をもし改善するなら
char arrC1[11] ={'h', 'e', 'l', 'l', 'o', 'w', 'o', 'r', 'l', 'd', '\0' }; char arrC2[11] = "helloworld\0";
のように11個分の大きさを確保しておいて、末尾の ‘\0’ つまりヌル文字も追加しておくほうが良いだろう。
あるいは、
char arrC3[] = "helloworld";
このように配列の大きさを指定せずに10文字の文字列リテラル”helloworld”を使って初期化すると、自動的に11個分の配列が確保されて、”helloworld\0″が格納される。
では、
char arrC3[] = "helloworld\0";
のようにした場合はどうなるのか?
気になるので試したら、同じく11個分の配列が確保されて、”helloworld\0″が格納された。
てっきり、
"helloworld\0\0"
こんなふうに12個の配列を確保するのかと思ったがVisual StudioのCコンパイラではそんなふうには成らなかった。
まあ、そんな事はどうでも良いか。
複数の文字列を保管する配列
その説明をしてみたい。
文字列が一つあるとその時点で1次元のchar配列となる訳なので、下例のように三つの文字列を配列に保管したい場合には、以下のようにすれば可能だ。
#include <stdio.h> int main(int argc, char *argv[]) { char arr[3][10] = { "test0", "test1", "test2" }; for (int i = 0; i < 3; i++){ printf("->%s<-\n", arr[i]); printf("0x%08x\n", arr[i]); } getchar(); return 0; }
char arr[3][10] で長さ10文字のchar型1次元配列を3個確保すると言う意味になる。
この場合、保管する予定の文字列はどれも “testX” の形式なので末尾の ‘\0’ を含めても6文字だ。なので10文字も確保するのは無駄である。もしキッチリと長さ分だけ確保したいなら
char arr[3][6] = { "test0", "test1", "test2" };
とすれば良い。
でも、ここで疑問が湧くだろう。
それぞれの文字列の長さがバラバラなら、最も長い要素の長さを指定するのか?
もしそうなら、長短の文字列が入り混じっていると結局無駄が生じるんじゃないの?
その通りだ。
なので、配列名[n][m] のように2次元配列を宣言するなら、それはchar型ではなくてint型 とか double型などの数値型データを n x m 個の2次元で確保したい場合には適している。
とは言ってもchar型の2次元配列使ってはいけないと言うわけではない。長さが異なる複数の文字列を保管して使う場合には、
char arr[3][17] = { "hello", "hello world", "hello kitty-chan" };
とすると最も長い16文字の要素に合わせて配列を作成する事になるので、無駄が生じると言う意味だ。
この場合でも、このように作成した配列にプログラム内で別の文字列を代入して使っても良のでその場合には “hello” が入っている場所には最長で17文字までを入れる事が出来る。
末尾には ‘\0’ を入れておくほうが良いのでそうすると自由に使えるのは16文字 + ‘\0’ となるが、strlen, strcat, strcpyなど文字列末尾に ‘\0’ を想定している関数を使わなければ17文字分まるまる自分で好きな文字を書き込んでも良い。
17文字有れば、俳句が入れられるな。
ちなみに、このように charでもintでもdoubleでもどんな型でも arr[3][6] のように配列の各次元の大きさを指定して2次元(あるいはそれ以上の多次元)配列を宣言した場合には、これら n x m 個の要素はメモリ上の連続した領域に確保される。
実際の例を以下に示す。
0x003FFA4C 73 74 31 00 74 65 73 74 st1.test
0x003FFA54 32 00 cc cc cc cc cc cc 2.フフフフフフ
このように長さ6文字の配列が3本分メモリ上に確保されている事が分かる。
なお、この例では、 arr[3][5] としてもエラーにはならない。その場合には、文字列の末尾に null文字を追加する余裕がないので各文字列はヌル終端しない状態でメモリ上に配置される事になる。なので、その点を承知の上でプログラムを書くのであれば問題は無い。
フフフフフフ の部分は不気味な笑い声ではなくて、初期化されていないメモリ領域に数字のcc(=204)が入っていて、それは文字コードで言うと半角カタカナの ‘フ’ となるからだ。
arr[][]とarr[]の関係
上の例では、二次元のchar型配列 arr[3][10] を冒頭で宣言しておいて、
char arr[3][10] = { "test0", "test1", "test2" }; for (int i = 0; i < 3; i++){ printf("->%s<-\n", arr[i]); // ここと printf("0x%08x\n", arr[i]); // ここの arr[i] って何やねん? }
forループの中で、それが知らないうちに一次元配列
arr[i]
に変わっている!
この辺りも、初めて見る人にはC言語に馴染めない部分だと思う。
でもこれも意味を覚えておけば何ら難しくは無くて、 arr[3][10]のchar型二次元配列は、下図のようにメモリ上に30バイトの連続領域に確保されている。
[0][0] | [0][1] | [0][2] | [0][3] | [0][4] | [0][5] | [0][6] | [0][7] | [0][8] | [0][9] |
t | e | s | t | 0 | \0 | \0 | \0 | \0 | \0 |
[1][0] | [1][1] | [1][2] | [1][3] | [1][4] | [1][5] | [1][6] | [1][7] | [1][8] | [1][9] |
t | e | s | t | 1 | \0 | \0 | \0 | \0 | \0 |
[2][0] | [2][1] | [2][2] | [2][3] | [2][4] | [2][5] | [2][6] | [2][7] | [2][8] | [2][9] |
t | e | s | t | 2 | \0 | \0 | \0 | \0 | \0 |
この時、
arr[0] は arr[0][0]にある ‘t’ という文字が保管されているメモリのアドレス
arr[1] は arr[1][0]にある ‘t’ という文字が保管されているメモリのアドレス
arr[2] は arr[2][0]にある ‘t’ という文字が保管されているメモリのアドレス
となる(C言語ではそう決まっていると言うほうが正しい)。
だらか理屈ではなくてこれはそう決まっていると覚えるのが良い。
なので、
arr[0] と &arr[0][0] は同じ意味になる。
何故かと言うと arr[0][0] は配列の一番先頭のchar要素。つまり ‘t’ の一文字。
その要素に&付けてchar要素のアドレスを取り出しているのだが、それは上に書いた決まりによって arr[0] なのだ。
一般化すれば、
arr[i] と &arr[i][0] は同じ意味になる(i=0, 1, 2)。
となる。
ポインタと配列の関係を理解する
さらにもう一つ言うと、
arr[0] arr[1] arr[2]
と
arr arr + 1 arr + 2
は同じ意味になる。
というよりはC言語ではそう決まっている。
なので
例えば
arr+2 arr[2] &arr[2][0]
はどれも同じで、”test2\0″ の ‘t’ というchar文字が格納されているメモリのアドレスとなる。
なので、それらの頭に中身が欲しい * を付ければ
*(arr+2) *(arr[2]) *(&arr[2][0])
はどれも ‘t’ と言う文字が取り出せる。
う~ん、分りにくい。
だいたい、arr と arr+2 を比べると、メモリ上では20バイトの距離が離れているのだが、arr から20バイト進めるにもかかわらず +20 ではなくて +2 なのだ。
その理由も、C言語ではそういう設計に成っていると言う事かな?
1次元目の配列要素数が10個なので、それを考慮して10要素ずつ増やすと言う感じかな。
でもarrを使って文字列を順番に取り出す場合には、
printf("->%s<-\n", arr); // "test0" printf("->%s<-\n", arr+1); // "test1" printf("->%s<-\n", arr+2); // "test2"
で各文字が取り出せるので、そういう意味では便利である。
まあ、このあたりをすらっと一遍で理解出来る人は滅多にいないと思う。
お勧めの勉強方法としては、Visual Studioなどの高性能な開発環境を使うとメモリの中身を見る機能があるので、デバッガでブレークポイントを置いて、メモリの中身がどう変化するのかを地道に追っていると分って来るはずだ。
分ってしまうとああ、そう言う事かと成るのだが。
それにしても、カーニハンさんとリッチーさんは、よくまあこんな巧妙な設計を思いついたものだと感心する。
ここまでで、char型の2次元配列 arr[n][m] を扱う方法が理解できた(と言う事にする)。
この例ではchar型であるが、int型やdouble型でも殆ど同じだ。
唯一異なる点は、それらの変数のバイト数だ。
char型 1バイト int 型 4バイト double型 8バイト
となる。
ここまでの説明が分らなくても心配は無い。
なぜなら、それは以下の理由によるからだ。
重要な注意
配列に関して上で説明した各種の表記方法、
arr+2 arr[2] &arr[2][0]はどれも同じ。
また、
*(arr+2) *(arr[2]) *(&arr[2][0])はどれも同じである。
などは、最初のうちは分りにくいと思うので、慣れるまでは使わなくても全く問題ない。
もし整数型で二次元の配列を下のように宣言すれば、30個の要素が確保でき、
int arrI[3][10] = { { 0, 1, 2, 3, 4, 5, 6, 7, 8, 9 }, { 10, 11, 12, 13, 14, 15, 16, 17, 18, 19 }, { 20, 21, 22, 23, 24, 25, 26, 27, 28, 29 }, };
それらは
int sum = 0; for (int i = 0; i < 3; i++){ for (int j = 0; j < 10; j++){ int arr_ij = arr[i][j]; // int arr_ij = *(arr[i] + j); // これでも良い printf("0x%08x\n", &arr_ij); printf("arr_ij=%d\n", arr_ij); sum += arr_ij; } } printf("sum=%d\n", sum);
のように
int arr_ij = arr[i][j];
の形式でアクセス出来るからだ。
それを、わざわざ、
int arr_ij = *(arr[i] + j);
としても同じ結果になるが、まあ、こんな表記に慣れていない人はわざわざこんな書き方をしなくても良い。
と言う事で、あまりややこしく考える必要は無いのだ。
C言語には、多次元の配列要素にアクセスする方法が沢山あると言う程度だ。
ではchar型の場合に、
長さが異なる複数の文字列を効率よくメモリに確保する方法は?
その場合には、以下のように書く事が出来る。
#include <stdio.h> int main(int argc, char *argv[]) { char *arr[3] = { "hello", "hello world", "hello kitty-chan" }; for (int i = 0; i < 3; i++){ printf("->%s<-\n", arr[i]); printf("0x%08x\n", arr[i]); int len = strlen(arr[i]); printf("->%d<-\n", len); } getchar(); return 0; }
要点は、
char *arr[3] = { "hello", "hello world", "hello kitty-chan" };
この部分だ。
三つの要素を持つchar*型の配列ですよと言う意味で [3] がある。
で、各要素は今まで説明したようにそれらは文字列なので、それぞれ1次元のchar型配列となる。なので、
"hello" "hello world" "hello kitty-chan"
の3つの文字列はコンパイラさんが上手い具合に自動的にメモリを確保してメモリ上のどこかに保存してくれている。
その文字列の先頭のアドレスを保存する変数ですよというのが
char *arr[3]
の意味となる。
なので、
arr[0] には "hello" が保管されているアドレス arr[1] には "hello world" が保管されているアドレス arr[2] には "hello kitty-chan" が保管されているアドレス
が入る。
なので、
char arr[3] なら char型の文字を保管する大きさ3の配列 となるが、
char *arr[3] なら char型のポインタを保管する大きさ3の配列 と言う事だ。
で、後者の char *arr[3] で宣言した配列には、3つの文字列の先頭アドレスを入れる事により、それらの文字列を読み出して利用する事が出来るのだ。
ここで重要な点は、三つの文字列は、メモリ上に三つが連続して格納されているとは限らない。
しかしながら、その三つの文字列の格納場所(=アドレス)を保持するcharポインタ型配列
char *arr[3] の三つの要素は、メモリ上に連続して確保されている。
これは別に深い意味はなくて、配列を int arr[10] と宣言すれば、メモリ上に10個のint型の要素が連続領域に確保されるのと同じで、ポインタ型の配列でも同じくその要素数の数だけメモリが連続的に確保されるのだ。でも、そこに保管する三本の文字列がメモリ上で連続しているとは限らないというだけだ。
もしこの3本の文字列もメモリ上に連続させたいならば、上のほうで出て来た、
char arr[3][17] = { "hello", "hello world", "hello kitty-chan" };
という書き方をすれば良い。
これらの点は、このあとの説明を理解する上で重要な点なので良く理解しておいて頂きたい。
初期化に使う文字列の数3を指定しなくても良い記述方法がある。
それが以下のようになる。
#include <stdio.h> int main(int argc, char *argv[]) { char *arr[] = { "hello", "hello world", "hello kitty-chan" }; for (int i = 0; i < 3; i++){ printf("->%s<-\n", arr[i]); printf("0x%08x\n", arr[i]); int len = strlen(arr[i]); printf("->%d<-\n", len); } getchar(); return 0; }
先ほどの例と何が違うかと言うと、
char *arr[3] = { "hello", "hello world", "hello kitty-chan" };
が
char *arr[] = { "hello", "hello world", "hello kitty-chan" };
に変わっただけだ。
まあ、コンパイラさんにしてみれば今から自分がコンパイルしようとしてるソースコードを見れば、要素が
{ "hello", "hello world", "hello kitty-chan" }
で指定されている訳だから、ああ要素は3個だと分かるのだ。
なので、char *arr[] のようにカッコの中の数字が省略されていても3個分の配列を確保すれば良い事が分かるのである。
じゃあ、それなら、こんなのも行けるんじゃない?
char arr[][] = { "hello", "hello world", "hello kitty-chan" };
と思う人がいるかも知れないが、これはエラーとなる。
何でかな?ワテには分からない。
まあ、あまりヘンテコな事は考えないようにしよう。
で、
いよいよ *argv[] あるいは **argv とは何ぞや?
ここまで来れば、C言語で登場する、
int main(int argc, char *argv[]) { ... }
の引数 char *argv[] の意味はもう分かるだろう。
メモリ上に長さが異なる何本かの文字列が格納されていて、それぞれの文字列の先頭のアドレスを保持しているのが argv配列なのだ。
で、何本の文字列が格納されているのかと言う情報は、argvからは分からない。なぜなら[]となっていてカッコの中に数字が無いので、何本の文字列が保管されている配列なのかは分からないのだ。
なので、同時に int argc を与える事により、文字列の数が分かるのだ。
一方、各文字列の長さは、argvが保持している各文字列の末尾には ‘\0’ というヌル文字が追加されている事が保証されているので、それを頼りに読み出せば間違いなく目的の文字列が読み出せると言う訳だ。
その辺りの処理は、プログラムを実行した環境のオペレーティングシステムがやってくれている。具体的には Windows, Linux, OS X など。
最後にいよいよ鬼門の
**argv とは何ぞや?
と言う事になる。
ポインタが二つ。
結論から言うと、**argv と *argv[] は全く同じものだ。
なので、どちらの表記を使うかは好き好きだ。
以上で、【われこ式】C言語入門【第一章】を終わりたいのだが、念のために言うならば、**argv と *argv[] が同じというのはあくまで関数の引数としての argv に関してであり、一般のchar型配列を定義する場面では成り立たないので注意が必要だ。
例えば、
char *arr[] = { "hello", "hello world", "hello kitty-chan" };
は正しいが、では、
char **arr = { "hello", "hello world", "hello kitty-chan" };
も同じなのかと言うとエラーする。
この辺りの説明は、【われこ式】C言語入門【第二章】以降で説明したい。
と言うかワテも実は良く分っていない部分があるのでこの際、理解したい。
つまり、C言語をやっていると配列とポインタというのが頻繁に出てくる。
それらは *, &, [] などの記号を使うと、
int arr_ij = arr[i][j];
が
int arr_ij = *(arr[i] + j);
と同じとなるように、相互に変換?出来るような錯覚をするのだが、C言語では配列とポイイタは別物だ。
配列はあくまでメモリ上に連続的に確保されている領域であり、ポインタはメモリ上の特定のアドレスを指すだけなのだ。
そのポインタが指した先が配列の先頭要素であれば、そのアドレスを使って配列の各要素にアクセスする手法が沢山用意されている事は上のほうで説明したが、C言語でプログラムを書いていると、そういう理由で配列とポインタが同じような感覚になってしまうのだ。
C言語入門第一章まとめ
C言語における1次元配列、2次元配列の使い方を理解出来た。
int arr[10]; // 整数型の要素数10個の1次元配列 char arr[] = "hello"; // char型の要素数 6個の1次元配列 char arr[3][10] = { "test0", "test1", "test2" }; // 要素数3x10=30の2次元配列 char *arr[] = { "hello", "kitty", "chan" }; // 要素数3のchar*型1次元配列
配列を関数に渡す方法を理解出来た。
main関数の引数の理解
int main(int argc, char *argv[]) int main(int argc, char **argv)
などの引数の意味が理解出来た。
これでC言語のポインタを半分くらいは理解出来たと言っても良いと思う。
つづく
C言語を習得しようと言う人には、やはり、まずこの本をお勧めしたい。
コメント