全13回でお届けする【おさらいC言語】シリーズ。第2回目のテーマは、C言語のメイン関数がコマンドラインから受け取ることができるパラメータargc
とargv
の構造についてです。これらの中に何のデータがどのように格納されるのかを、イメージしやすいように解説していきます。また、ほかのプログラミング言語との違いについても紹介します。
コマンドライン引数(コマンドライン・パラメータ)とは
コマンドライン引数とは、ターミナル(コマンドライン)からコマンドやプログラムを起動する時に渡すことができる引数(パラメータ)のことをいいます。ターミナルから実行できるプログラムにはさまざまなものがありますが、その多くではコマンドライン引数としてオプションやファイル名などを与えられるようになっています。
前回(第1回)の記事で、ハローワールドのプログラムをターミナルからコンパイルする例を紹介しました。次のようにgccを実行すると、a.out
という名前の実行ファイルが作られます。この例では、main.c
の部分が引数です。
$ gcc main.c
引数が2つ以上あるときは、次のようにスペースで区切って指定します。
$ gcc -o main main.c
この例では-o
、main
、main.c
の3つが引数です。gccでは-o
は出力(output)先を指定するオプションで、-o main
という2つの引数の並びで「出力する実行ファイル名を(a.out
ではなく)main
にする」という指定になります。
C言語のコマンドライン引数の構造
コマンドライン引数は、自分でコンパイルしたプログラムにも与えることが可能です。ここからは、C言語で書いたプログラムを実行したときに、コマンドライン引数がどのような構造で受け取られるのかをみていきましょう。
C言語でコマンドライン引数を受け取るには
例えば、ハローワールドの実行ファイルa.out
を起動するときにabc
とdefg
の2つの引数を指定したい場合は、ターミナルで次のように入力して実行します。
$ ./a.out abc defg
Hello, world.
しかし、コマンドライン引数を与えても与えなくても、ハローワールドの動作は変わりません。なぜなら、ハローワールドの中で使われているメイン関数は、コマンドライン引数を無視しているからです。メイン関数には2通りのプロトタイプがありますが(詳しくは第1回の記事を参照)、コマンドライン引数を受け取るプログラムを書くには、次のようにパラメータ付きのメイン関数を使う必要があります。
int main(int argc, char *argv[]) {
// argcとargvの2つのパラメータでコマンドライン引数を受け取っている
}
これで、プログラムが受け取ったコマンドライン引数は、メイン関数のパラメータとしてargc
とargv
の2つの変数に格納されるようになります。読みづらい変数名ですが、前者は「引数の数(argument count)」、後者は「引数の配列(argument vector)」の略です。配列とは、同じ形式のデータが一列に並んだようなデータ構造のことです。データを配列として格納しておくと、先頭から何番目にあるのか(インデックス)さえわかれば、どのデータにでも素早くアクセスすることができます。そのため、プログラミングでは配列が頻繁に使われています。
argc
に入っている値
C言語よりも新しいプログラミング言語では、配列にはサイズ(配列内のデータの個数)も記憶されているのが一般的です。しかし、C言語の配列にはサイズを知る方法が備わっていません。そのため、メイン関数には配列argv
に格納されたコマンドライン引数の一覧とともに、そのサイズ情報としてargc
も引き渡されるようになっています。
サイズ情報はint argc
となっているので、整数(integer)型です。実際にどのような値が格納されるのかを確認するために、argc
の値を表示するプログラムを作ってみましょう。
#include <stdio.h>
int main(int argc, char *argv[]) {
printf("argc: %d\n", argc);
}
このprintf
では、1つ目のパラメータでテキストのフォーマットを指定しています。フォーマット中の%d
の部分には、2つ目のパラメータとして与えたargc
の値が入ります。d
は、10進数の符号付き整数(Decimal signed integer)の意味です。では、プログラムをコンパイルして、コマンドライン引数を与えて実行してみましょう。
$ gcc main.c
$ ./a.out abc
argc: 2
$ ./a.out abc defg
argc: 3
実行してみると、argc
にはコマンドライン引数の個数よりも1つ大きい値が入っていることがわかります。では、コマンドライン引数を指定しない場合はいくつになるか試してみましょう。
$ ./a.out
argc: 1
このように実行すると、コマンドライン引数がないときのargc
は1だということがわかります。
argc
には「コマンドライン引数の個数+1」が格納される。argv
に入っている値
次は、argv
の中身を確認してみましょう。次のように、argc
を表示するプログラムにfor
ループを追加します。
#include <stdio.h>
int main(int argc, char *argv[]) {
printf("argc: %d\n", argc);
for (int i=0; i<argc; i++) {
printf("argv[%d]: %s\n", i, argv[i]);
}
}
メイン関数のargv
はchar *argv[]
と少し複雑な形になっていますが、これは文字列の配列だということを表す書き方のひとつです。配列のインデックスは0からはじまるので、最初の文字列にはargv[0]
とすればアクセスできます。また、配列の個数がargc
に格納されることから、最後の文字列はargv[argc-1]
となります。これを利用すれば、for
ループでargv
に含まれるすべての文字列にアクセスすることが可能です。
上のプログラムでは、for
ループによって変数i
の値を0
からargc-1
まで変化させながらprintf
を実行しています。printf
のフォーマットに%s
という指定が出てきましたが、これは文字列(String)を当てはめるためのものです。i
は数値なので%d
、argv[i]
は文字列なので%s
で、うまく表示することができます。
では、コマンドライン引数を与えてプログラムを実行してみましょう。
$ gcc main.c
$ ./a.out abc defg
argc: 3
argv[0]: ./a.out
argv[1]: abc
argv[2]: defg
実行結果をよく観察すると、argc
の値が「コマンドライン引数の個数+1」になっている理由がわかります。argv
の最初の要素には、実行ファイル名が入っているのです。そのため、C言語でコマンドライン引数を使ったプログラムを書くときは、argv
の2つ目以降の値をチェックすればよいということになります。
argv
には実行ファイル名とコマンドライン引数が文字列で格納される。ほかのプログラミング言語との比較
ここまでC言語についてみてきましたが、ほかのプログラミング言語でもコマンドライン引数を受け取ることは可能です。例として、ここではJavaとSwiftについて簡単に触れておきます。
Javaの場合
下記は、コマンドライン引数をすべて表示するプログラムをJavaで書き直したものです。
public class Main {
// Javaのメイン関数には文字列の配列だけが渡される
public static void main(String[] args) {
// C言語のargcにあたる値を表示する
System.out.println(String.format("argc: %d", args.length));
// C言語のargvにあたる配列の要素をすべて表示する
for (int i=0; i<args.length; i++) {
System.out.println(String.format("argv[%d]: %s", i, args[i]));
}
}
}
C言語と異なり、Javaのメイン関数が受け取るパラメータは文字列の配列のみとなっているのがわかるでしょうか。これは、配列そのものがサイズ情報をもっているのでargc
が不要なためです。コンパイル方法などの詳細は省きますが、上記を実行するとC言語とは少し異なる動きをします。メイン関数が受け取るargs
にはコマンドライン引数のみが含まれており、実行ファイル名は入っていません。
Swiftの場合
次は、Swiftで書き直したプログラムです。
import Foundation
// Swiftにはメイン関数がないが、コマンドライン引数を文字列の配列として取り出せる
let args = ProcessInfo.processInfo.arguments
// C言語のargcにあたる値を表示する
print("argc: \(args.count)")
// C言語のargvにあたる配列の要素をすべて表示する
for i in 0..<args.count {
print("argv[\(i)]: \(args[i])")
}
Swiftにはメイン関数がありませんが、Foundation
と呼ばれる標準機能によってコマンドライン引数を取り出すことができます。こちらもコンパイル方法などは省きますが、上記を実行すると、C言語とよく似た動作をすることがわかるはずです。配列の最初に実行ファイル名が入る点も、C言語と同じです。
基本的な概念に大きな違いはない
ここまでのプログラムを比較してみると、配列の柔軟性やテキストのフォーマット方法、ループの文法などがプログラミング言語によって少しずつ異なることがわかるでしょう。また、後発の言語はC言語に比べて洗練されていますが、基本的な概念には大きな違いがありません。これは、C言語の知識があれば、ほかの言語も理解しやすいということを意味しています。
文字列の配列を正確にイメージしよう
ここからは、話をC言語に戻します。メイン関数のargv
は文字列の配列でした。では、C言語における文字列の配列とは、どのような構造をしたデータなのかを整理していきます。
文字列の配列とは
先程、配列とは「同じ形式のデータが一列に並んだような構造」をしていると説明しました。ですから、文字列の配列とは、文字列が一列に並んでいる構造だと想像できます。では、これを図に描いてみるとどのようになるでしょうか。次のような構造をイメージする人が多いかもしれません。
上記のようなイメージは概念図としては間違っていませんが、C言語のデータ構造としては正解とはいえません。C言語における文字列の配列とは、より正確には「文字列を指すポインタの配列」だからです。これを踏まえて文字列の配列を表現すると、次の図のようになります。
少し難しくなってきましたので、順を追って説明していきます。特に「ポインタ」はC言語の学習者がつまずきやすいといわれている箇所ですが、正しいイメージさえできればそれほど難しいものではありません。
文字列の構造
しっかりとイメージを固めるために、まずは文字列について理解しましょう。文字列とは、文字がずらっと並んで列になったデータのことです。そのため、「文字の配列」として表すことができます。C言語の言葉でより正確に表現するなら、「ヌル文字で終わるchar
型の配列」です。char
型というのは文字(character)を格納するための整数のことで、文字のひとつひとつに対応づけられた番号によって、半角の英数字や記号を1文字だけ表現できます。また、char
型は「ヌル(null)」という特別な値をとることもできます。
char
型の値が配列となって並び、その最後の要素がヌルになっているのが文字列です。図にすると、下記のようなイメージです。
ヌル文字は、C言語ではバックスラッシュを使って\0
と書くことになっています。ただし、日本語環境ではバックスラッシュの部分が半角の円記号(¥
)になる場合があります。「null」は「ナル」と書いたほうが実際の発音に近いのですが、慣例的に「ヌル」と書くことが多いようです。
ちなみに、null
には「何もない」という意味があります。第1回の記事で出てきたvoid
も同じような意味を持つ単語です。null
が「何もないことを示すデータ」であるのに対し、void
は「データすらない」と捉えると違いがわかるでしょう。
文字列の配列とポインタ
文字列の構造がわかったので、argv
のような「文字列の配列」についてあらためて考えてみましょう。文字列が「文字の配列」だということから、「文字列の配列」=「文字の配列の配列」と考えるとどうなるでしょうか。図にすると、下記のようなイメージです。
上記は「2次元配列」と呼ばれる構造です。この場合、すべての文字列に同じサイズの領域を用意する必要があります。しかし、文字列の長さはバラバラなので、どうしても無駄な領域ができてしまいます。このような無駄をなくすためには、ポインタが便利です。
ポインタには、データの「アドレス」を格納することができます。プログラムから直接アクセスできるデータは、int
もchar
も、それらの配列であってもすべてメモリ上に配置されます。また、ただ配置するだけでは見失ってしまうので、どこにあるのかがわかるように必ずアドレスが割り振られます。そのため、ポインタにアドレスを格納しておけば、ポインタがデータを指し示すようになり、どのようなデータにでもたどり着くことができます。
文字列のアドレスも、ポインタに格納することができます。そのため、「文字列の配列」を「文字列を指すポインタの配列」として表現することが可能です。図にすると、下記のようなイメージです。
ポインタを使うことで、無駄な領域がなくなったことがわかるでしょうか。これが、argv
の構造です。
なお、実際のargv
では最後に0
が追加され、要素数がargc
より1つ多くなっています。つまり、argv[argc]
は必ず0
になります。ポインタに格納される0
は、「どのデータも指し示していない」ということを意味する特別な値です。C言語では、0
の代わりにNULL
と書くこともできます。また、0
が格納されたポインタは「ヌル(NULL)ポインタ」と呼ばれます。char
型に格納できるヌル(\0
)と名前が似ていますが、別のものなので区別するようにしましょう。
この記事の上のほうでは、コマンドライン引数をすべて表示するプログラムを紹介しました。argv
の終端がヌルポインタであることを利用する方法でも、同じようなことが可能です。例えば、次のようにすればよいでしょう。
#include <stdio.h>
int main(int argc, char *argv[]) {
for (int i=0; argv[i]!=0; i++) { // ヌルポインタの手前までループする
printf("argv[%d]: %s\n", i, argv[i]);
}
}
まとめ
C言語のメイン関数がコマンドラインから受け取ることができるパラメータargc
とargv
には、コマンドライン引数のサイズとその一覧が入っています。ここまで読んで、これらの構造がなんとなくイメージできたのではないでしょうか。しかし、ポインタのイメージはまだ少し曖昧な状態かもしれません。より正確なイメージをもつには、データやポインタがメモリの中でどうなっているのかを想像できるようになることが大切です。これはC言語だけでなく、いろいろなプログラミング言語への理解を深めるためにも重要な部分だといえます。
argv
がメモリの中でどのようになっているのかについて、詳しく説明していきます。